diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index 6f9e02b67..ca24ef83d 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -21,7 +21,6 @@ This operation cannot be reverted! AI Conversations History What can I help with? - Rename Conversation Any https or http URL Any name... App Output diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index 17acaa0b5..1a6f1c86f 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -402,8 +402,8 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : setCmd( "ai-toggle-lock-chat", [this] { renameChat( mSummary, true ); } ); setCmd( "ai-rename-chat", [this] { - UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::INPUT, - i18n( "ai_rename_chat", "Rename Conversation" ) ); + UIMessageBox* msgBox = UIMessageBox::New( + UIMessageBox::INPUT, i18n( "rename_conversation", "Rename Conversation" ) ); msgBox->on( Event::OnConfirm, [this, msgBox]( const Event* ) { std::string newName( msgBox->getTextInput()->getText().toUtf8() ); if ( newName.empty() || mSummary == newName ) @@ -420,6 +420,8 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : msgBox->showWhenReady(); } ); + setCmd( "ai-regenerate-title", [this] { regenerateChatName(); } ); + setCmd( "new-ai-assistant", [this] { if ( getPlugin() == nullptr ) return; @@ -508,6 +510,13 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : menu->addSeparator(); + if ( chatExistsInDisk() ) { + menu->add( i18n( "regenerate_conversation_title", "Regenerate Conversation Title" ), + getUISceneNode()->findIconDrawable( "refresh", PixelDensity::dpToPxI( 12 ) ), + getKeyBindings().getCommandKeybindString( "ai-regenerate-title" ) ) + ->setId( "ai-regenerate-title" ); + } + menu->add( i18n( "refresh_model_ui", "Refresh Local Models" ), getUISceneNode()->findIconDrawable( "refresh", PixelDensity::dpToPxI( 12 ) ), getKeyBindings().getCommandKeybindString( "ai-refresh-local-models" ) ) @@ -1130,6 +1139,73 @@ static bool allNewLines( const std::string& s ) { return !s.empty() && std::all_of( s.begin(), s.end(), []( char c ) { return c == '\n'; } ); } +void LLMChatUI::generateChatName( bool isRenaming ) { + auto apiKey = AIAssistantPlugin::getApiKeyFromProvider( mCurModel.provider, getPlugin() ); + if ( !apiKey ) { + showMsg( getUISceneNode()->i18n( "configure_api_key", + "You must first configure your provider api key." ) ); + return; + } + std::string apiKeyStr{ *apiKey }; + std::string apiUrl( prepareApiUrl( apiKeyStr ) ); + auto model = mCurModel; + + static const std::string SummaryPrompt = + "Generate a concise 3-7 word title for this conversation, omitting punctuation. Go " + "straight to the title, without any preamble and prefix like `Here's a concise " + "suggestion:...` or `Title:`. Ignore this message for the summary generation."; + + auto jchat = serializeChat( getCheapestModelFromCurrentProvider(), true ); + + jchat["messages"].push_back( { { "role", LLMChat::roleToString( LLMChat::Role::User ) }, + { "content", SummaryPrompt } } ); + + auto chatstr = jchat.dump(); + + mSummaryRequest = + std::make_unique( apiUrl, apiKeyStr, chatstr, model.provider ); + + mSummaryRequest->doneCb = [this, isRenaming]( const LLMChatCompletionRequest& req, + Http::Response& response ) { + auto status = response.getStatus(); + String oldSummary = std::move( mSummary ); + + if ( status == Http::Response::Ok ) { + mSummary = String::trim( req.getResponse() ); + String::trimInPlace( mSummary, '\n' ); + String::trimInPlace( mSummary, ' ' ); + String::trimInPlace( mSummary, '"' ); + } else { + // TODO: Implement generating a summary based on the user prompt (take the + // first few words) + mSummary = i18n( "untitled_conversation", "Untitled Conversation" ); + } + + if ( isRenaming ) { + String newSummary = std::move( mSummary ); + mSummary = std::move( oldSummary ); + runOnMainThread( + [this, newSummary = std::move( newSummary )] { renameChat( newSummary ); } ); + } else + saveChat(); + + runOnMainThread( [this] { + updateTabTitle(); + mSummaryRequest.reset(); + } ); + }; + mSummaryRequest->requestAsync(); +} + +bool LLMChatUI::chatExistsInDisk() const { + return FileSystem::fileExists( getFilePath() ); +} + +void LLMChatUI::regenerateChatName() { + if ( chatExistsInDisk() ) + generateChatName( true ); +} + void LLMChatUI::doRequest() { if ( mRequest ) return; @@ -1193,86 +1269,50 @@ void LLMChatUI::doRequest() { removeLastChat(); }; - mRequest->doneCb = [this, editor, apiUrl = std::move( apiUrl ), - apiKeyStr = std::move( apiKeyStr ), model = std::move( model )]( - const LLMChatCompletionRequest&, Http::Response& response ) { - auto status = response.getStatus(); - auto statusDesc = response.getStatusDescription(); + mRequest->doneCb = + [this, editor, apiUrl = std::move( apiUrl ), apiKeyStr = std::move( apiKeyStr ), + model = std::move( model )]( const LLMChatCompletionRequest&, Http::Response& response ) { + auto status = response.getStatus(); + auto statusDesc = response.getStatusDescription(); - runOnMainThread( [this, editor, status, statusDesc, apiUrl = std::move( apiUrl ), - apiKeyStr = std::move( apiKeyStr ), model = std::move( model )] { - if ( status != Http::Response::Ok ) { - auto resp = nlohmann::json::parse( mRequest->getStream(), nullptr, false ); - if ( resp.contains( "error" ) && resp["error"].contains( "message" ) ) { - showMsg( resp["error"].value( "message", "" ) ); - } else if ( resp.contains( "error" ) && resp["error"].is_string() ) { - showMsg( URI::decode( resp.value( "error", "" ) ) ); - } else if ( resp.is_array() && !resp.empty() ) { - const auto& first = resp.at( 0 ); - if ( first.contains( "error" ) && first["error"].contains( "message" ) ) - showMsg( first["error"].value( "message", "" ) ); - else - showMsg( statusDesc ); - } else { - showMsg( statusDesc ); - } - - removeLastChat(); - } - mRequest.reset(); - toggleEnableChats( true ); - editor->setEnabled( true ); - - mChatStop->setVisible( false )->setEnabled( false ); - mChatRun->setVisible( true )->setEnabled( true ); - - if ( editor->hasFocus() ) - mChatInput->setFocus(); - - if ( !mChatIsPrivate && !mSummaryRequest && mSummary.empty() && - status == Http::Response::Ok ) { - static const std::string SummaryPrompt = - "Generate a concise 3-7 word title for this conversation, omitting " - "punctuation. Go " - "straight to the title, without any preamble and prefix like `Here's a concise " - "suggestion:...` or `Title:`. Ignore this message for the summary generation."; - - auto jchat = serializeChat( getCheapestModelFromCurrentProvider(), true ); - - jchat["messages"].push_back( - { { "role", LLMChat::roleToString( LLMChat::Role::User ) }, - { "content", SummaryPrompt } } ); - - auto chatstr = jchat.dump(); - - mSummaryRequest = std::make_unique( - apiUrl, apiKeyStr, chatstr, model.provider ); - - mSummaryRequest->doneCb = [this]( const LLMChatCompletionRequest& req, - Http::Response& response ) { - auto status = response.getStatus(); - if ( status == Http::Response::Ok ) { - mSummary = String::trim( req.getResponse() ); - String::trimInPlace( mSummary, '\n' ); - String::trimInPlace( mSummary, ' ' ); - String::trimInPlace( mSummary, '"' ); + runOnMainThread( [this, editor, status, statusDesc, apiUrl = std::move( apiUrl ), + apiKeyStr = std::move( apiKeyStr ), model = std::move( model )] { + if ( status != Http::Response::Ok ) { + auto resp = nlohmann::json::parse( mRequest->getStream(), nullptr, false ); + if ( resp.contains( "error" ) && resp["error"].contains( "message" ) ) { + showMsg( resp["error"].value( "message", "" ) ); + } else if ( resp.contains( "error" ) && resp["error"].is_string() ) { + showMsg( URI::decode( resp.value( "error", "" ) ) ); + } else if ( resp.is_array() && !resp.empty() ) { + const auto& first = resp.at( 0 ); + if ( first.contains( "error" ) && first["error"].contains( "message" ) ) + showMsg( first["error"].value( "message", "" ) ); + else + showMsg( statusDesc ); } else { - // TODO: Implement generating a summary based on the user prompt (take the - // first few words) - mSummary = i18n( "untitled_conversation", "Untitled Conversation" ); + showMsg( statusDesc ); } + + removeLastChat(); + } + mRequest.reset(); + toggleEnableChats( true ); + editor->setEnabled( true ); + + mChatStop->setVisible( false )->setEnabled( false ); + mChatRun->setVisible( true )->setEnabled( true ); + + if ( editor->hasFocus() ) + mChatInput->setFocus(); + + if ( !mChatIsPrivate && !mSummaryRequest && mSummary.empty() && + status == Http::Response::Ok ) { + generateChatName( false ); + } else { saveChat(); - runOnMainThread( [this] { - updateTabTitle(); - mSummaryRequest.reset(); - } ); - }; - mSummaryRequest->requestAsync(); - } else { - saveChat(); - } - } ); - }; + } + } ); + }; mRequest->requestAsync(); } @@ -1284,7 +1324,7 @@ void LLMChatUI::toggleEnableChat( UIWidget* chat, bool enabled ) { chat->findByClass( "erase_but" )->setEnabled( enabled ); chat->findByClass( "move_up" )->setEnabled( enabled ); chat->findByClass( "move_down" )->setEnabled( enabled ); - chat->findByClass( "copy_contents" )->setEnabled( enabled ); + // chat->findByClass( "copy_contents" )->setEnabled( enabled ); } void LLMChatUI::toggleEnableChats( bool enabled ) { diff --git a/src/tools/ecode/plugins/aiassistant/chatui.hpp b/src/tools/ecode/plugins/aiassistant/chatui.hpp index 39dda0b53..f8e5edec6 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.hpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.hpp @@ -89,6 +89,8 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { void setManager( PluginManager* manager ) { mManager = manager; } + bool chatExistsInDisk() const; + protected: UUID mUUID; std::string mSummary; @@ -195,6 +197,10 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { void insertFileToDocument( std::string path, std::shared_ptr cdoc ); void replaceFileLinksToContents( std::string& text ); + + void generateChatName( bool isRenaming ); + + void regenerateChatName(); }; } // namespace ecode