diff --git a/bin/assets/fonts/remixicon.ttf b/bin/assets/fonts/remixicon.ttf index 22ce6de49..1373ccd09 100644 Binary files a/bin/assets/fonts/remixicon.ttf and b/bin/assets/fonts/remixicon.ttf differ diff --git a/include/eepp/ui/tools/uicodeeditorsplitter.hpp b/include/eepp/ui/tools/uicodeeditorsplitter.hpp index 3fd586d6f..f123b7fbe 100644 --- a/include/eepp/ui/tools/uicodeeditorsplitter.hpp +++ b/include/eepp/ui/tools/uicodeeditorsplitter.hpp @@ -142,7 +142,7 @@ class EE_API UICodeEditorSplitter { UITabWidget* tabWidget ); void removeUnusedTab( UITabWidget* tabWidge, bool destroyOwnedNode = true, - bool immediateCloset = true ); + bool immediateClose = true ); UITabWidget* createEditorWithTabWidget( Node* parent, bool openCurEditor = true ); @@ -181,6 +181,8 @@ class EE_API UICodeEditorSplitter { void forEachTabWidgetStoppable( std::function run ) const; + void forEachTab( std::function run ) const; + void zoomIn(); void zoomOut(); @@ -355,11 +357,15 @@ class EE_API UICodeEditorSplitter { void setOnTabWidgetCreateCb( std::function cb ); bool getVisualSplitting() const; + void setVisualSplitting( bool visualSplitting ); Float getVisualSplitEdgePercent() const; + void setVisualSplitEdgePercent( Float visualSplitEdgePercent ); + UITabWidget* splitTabWidget( SplitDirection, UITabWidget* ); + protected: UISceneNode* mUISceneNode{ nullptr }; std::shared_ptr mThreadPool; @@ -397,8 +403,6 @@ class EE_API UICodeEditorSplitter { UITabWidget* createTabWidget( Node* parent ); - UITabWidget* splitTabWidget( SplitDirection, UITabWidget* ); - void updateTabWidgetVisualSplitting(); }; diff --git a/src/eepp/ui/iconmanager.cpp b/src/eepp/ui/iconmanager.cpp index 5f66b34f3..b55300700 100644 --- a/src/eepp/ui/iconmanager.cpp +++ b/src/eepp/ui/iconmanager.cpp @@ -96,6 +96,8 @@ UIIconTheme* IconManager::init( const std::string& iconThemeName, FontTrueType* { "stop", 0xF1A0 }, { "text-wrap", 0xF200 }, { "play-filled", 0xF00A }, + { "code-ai", 0xF56F }, + { "robot-2", 0xF3D9 }, }; for ( const auto& icon : icons ) diff --git a/src/eepp/ui/tools/uicodeeditorsplitter.cpp b/src/eepp/ui/tools/uicodeeditorsplitter.cpp index bcfb5ed29..6bdaa6c15 100644 --- a/src/eepp/ui/tools/uicodeeditorsplitter.cpp +++ b/src/eepp/ui/tools/uicodeeditorsplitter.cpp @@ -856,6 +856,12 @@ void UICodeEditorSplitter::forEachTabWidgetStoppable( return; } +void UICodeEditorSplitter::forEachTab( std::function run ) const { + for ( auto tabWidget : mTabWidgets ) + for ( size_t i = 0; i < tabWidget->getTabCount(); i++ ) + run( tabWidget->getTab( i ) ); +} + void UICodeEditorSplitter::forEachEditorStoppable( std::function run ) const { for ( auto tabWidget : mTabWidgets ) { @@ -1336,7 +1342,9 @@ void UICodeEditorSplitter::focusSomeEditor( Node* searchFrom ) { : nullptr ); UITabWidget* tabW = nullptr; - if ( editor && ( tabW = tabWidgetFromEditor( editor ) ) && !tabW->isClosing() && + if ( editor && editor->getParent() && editor->getParent()->getParent() && + editor->getParent()->getParent()->isType( UI_TYPE_TABWIDGET ) && + ( tabW = tabWidgetFromEditor( editor ) ) && !tabW->isClosing() && tabW->getTabCount() > 1 ) { if ( tabW && tabW->getTabSelected()->getOwnedWidget() != editor ) { tabW->setTabSelected( tabW->getTabSelected() ); diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index 3ff4bd463..aaa8e149e 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -9,6 +9,8 @@ #include #include +#include + using namespace EE::Network; using namespace eterm::UI; using json = nlohmann::json; @@ -397,7 +399,7 @@ struct ProjectPath { } }; -json saveNode( Node* node ) { +json AppConfig::saveNode( Node* node ) { json res; if ( node->isType( UI_TYPE_SPLITTER ) ) { UISplitter* splitter = node->asType(); @@ -435,6 +437,18 @@ json saveNode( Node* node ) { if ( term->isUsingCustomTitle() ) f["title"] = term->getTitle(); files.emplace_back( f ); + } else if ( node->isWidget() ) { + UIWidget* widget = ownedWidget->asType(); + if ( widget->getClasses().size() == 1 ) { + auto found = tabWidgetTypes.find( widget->getClasses()[0] ); + if ( found != tabWidgetTypes.end() ) { + auto f = found->second.onSave( widget ); + f["type"] = found->first; + if ( !f.contains( "title" ) || !f["title"].is_string() ) + f["title"] = tabWidget->getTab( i )->getText(); + files.emplace_back( f ); + } + } } } res["type"] = "tabwidget"; @@ -675,6 +689,19 @@ void AppConfig::loadDocuments( UICodeEditorSplitter* editorSplitter, json j, if ( curTabWidget->getTabCount() == totalToLoad ) curTabWidget->setTabSelected( eeclamp( currentPage, 0, curTabWidget->getTabCount() - 1 ) ); + } else { + auto found = tabWidgetTypes.find( file["type"] ); + if ( found != tabWidgetTypes.end() ) { + auto widget = found->second.onLoad( file ); + + editorSplitter->createWidgetInTabWidget( + curTabWidget, widget, file.contains( "title" ) ? file["title"] : "" ); + editorSplitter->removeUnusedTab( curTabWidget, true, false ); + + if ( curTabWidget->getTabCount() == totalToLoad ) + curTabWidget->setTabSelected( + eeclamp( currentPage, 0, curTabWidget->getTabCount() - 1 ) ); + } } } } else if ( j["type"] == "splitter" ) { diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index 2e5f7a264..eca4de4e0 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -9,8 +9,7 @@ #include #include -#include -using json = nlohmann::json; +#include using namespace EE; using namespace EE::Math; @@ -189,6 +188,11 @@ struct SessionSnapshotFile { std::string selection; }; +struct TabWidgetCbs { + std::function onSave; + std::function onLoad; +}; + class AppConfig { public: WindowStateConfig windowState; @@ -227,14 +231,29 @@ class AppConfig { const std::string& configPath, ProjectDocumentConfig& docConfig, ecode::App* app, bool sessionSnapshot, PluginManager* pluginManager ); + void addTabWidgetType( const std::string& type, TabWidgetCbs tabWidget ) { + Lock l( tabWidgetTypesMutex ); + tabWidgetTypes[type] = tabWidget; + } + + void removeTabWidgetType( const std::string& type ) { + Lock l( tabWidgetTypesMutex ); + tabWidgetTypes.erase( type ); + } + protected: Int64 editorsToLoad{ 0 }; - void loadDocuments( UICodeEditorSplitter* editorSplitter, json j, UITabWidget* curTabWidget, - ecode::App* app, + void loadDocuments( UICodeEditorSplitter* editorSplitter, nlohmann::json j, + UITabWidget* curTabWidget, ecode::App* app, const std::vector& sessionSnapshotFiles ); void editorLoadedCounter( ecode::App* app ); + + nlohmann::json saveNode( Node* node ); + + Mutex tabWidgetTypesMutex; + std::unordered_map tabWidgetTypes; }; } // namespace ecode diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 3ce52d878..91e7503a9 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -1959,6 +1959,8 @@ void App::closeEditors() { widget->asType()->getTab()->removeTab( true, true ); } ); + mSplitter->forEachTab( []( UITab* tab ) { tab->removeTab( true, true ); } ); + mCurrentProject = ""; mCurrentProjectName = ""; mDirTree = nullptr; diff --git a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp index be7fd1a76..4e8e5e9fe 100644 --- a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp +++ b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp @@ -2,6 +2,7 @@ #include "chatui.hpp" #include "protocol.hpp" +#include "../../appconfig.hpp" #include "../../widgetcommandexecuter.hpp" #include @@ -109,6 +110,10 @@ AIAssistantPlugin::AIAssistantPlugin( PluginManager* pluginManager, bool sync ) AIAssistantPlugin::~AIAssistantPlugin() { waitUntilLoaded(); mShuttingDown = true; + if ( mStatusButton ) + mStatusButton->close(); + + getPluginContext()->getConfig().removeTabWidgetType( "llm_chatui" ); } void AIAssistantPlugin::load( PluginManager* pluginManager ) { @@ -142,6 +147,18 @@ void AIAssistantPlugin::load( PluginManager* pluginManager ) { subscribeFileSystemListener(); mReady = !mProviders.empty(); + + TabWidgetCbs config; + config.onLoad = [this]( const nlohmann::json& j ) { + return ( eeNew( ChatUI, ( getUISceneNode(), mProviders ) ) )->getChatUI(); + }; + config.onSave = []( UIWidget* widget ) { + nlohmann::json j; + return j; + }; + + getPluginContext()->getConfig().addTabWidgetType( "llm_chatui", config ); + if ( mReady ) { fireReadyCbs(); setReady( clock.getElapsedTime() ); @@ -206,16 +223,23 @@ void AIAssistantPlugin::loadAIAssistantConfig( const std::string& path, bool upd for ( const auto& [key, value] : providers ) mProviders.insert_or_assign( key, value ); } + + if ( getUISceneNode() ) + initUI(); +} + +void AIAssistantPlugin::newAIAssistant() { + auto splitter = getPluginContext()->getSplitter(); + auto chatUI = eeNew( ChatUI, ( getUISceneNode(), mProviders ) ); + auto tabName( i18n( "ai_assistant", "AI Assistant" ) ); + UITabWidget* tabWidget = splitter->getTabWidgets()[splitter->getTabWidgets().size() - 1]; + if ( !splitter->hasSplit() ) + tabWidget = splitter->splitTabWidget( SplitDirection::Right, tabWidget ); + splitter->createWidgetInTabWidget( tabWidget, chatUI->getChatUI(), tabName ); } void AIAssistantPlugin::onRegisterDocument( TextDocument* doc ) { - doc->setCommand( "new-ai-assistant", [this] { - auto splitter = getPluginContext()->getSplitter(); - auto chatUI = eeNew( ChatUI, ( getPluginContext()->getUISceneNode(), mProviders ) ); - if ( !splitter->hasSplit() ) - splitter->split( SplitDirection::Right, splitter->getCurWidget(), false ); - splitter->createWidget( chatUI->getChatUI(), i18n( "ai_assistant", "AI Assistant" ) ); - } ); + doc->setCommand( "new-ai-assistant", [this] { newAIAssistant(); } ); } void AIAssistantPlugin::onRegisterEditor( UICodeEditor* editor ) { @@ -235,8 +259,8 @@ PluginRequestHandle AIAssistantPlugin::processMessage( const PluginMessage& msg kb.first ); } - // if ( !mInitialized ) - // updateUI(); + if ( !mUIInit ) + initUI(); break; } @@ -246,4 +270,27 @@ PluginRequestHandle AIAssistantPlugin::processMessage( const PluginMessage& msg return PluginRequestHandle::empty(); } +void AIAssistantPlugin::initUI() { + mUIInit = true; + + getPluginContext()->getMainLayout()->setCommand( "new-ai-assistant", + [this] { newAIAssistant(); } ); + + if ( !mStatusBar ) + getUISceneNode()->bind( "status_bar", mStatusBar ); + if ( !mStatusBar ) + return; + + if ( !mStatusButton ) { + mStatusButton = UIPushButton::New(); + mStatusButton->setLayoutSizePolicy( SizePolicy::WrapContent, SizePolicy::MatchParent ); + mStatusButton->setParent( mStatusBar ); + mStatusButton->setId( "ai_assistant_but" ); + mStatusButton->setClass( "status_but" ); + mStatusButton->setIcon( iconDrawable( "code-ai", 14 ) ); + mStatusButton->setTooltipText( i18n( "ai_assistant", "AI Assistant" ) ); + mStatusButton->on( Event::MouseClick, [this]( const Event* event ) { newAIAssistant(); } ); + } +} + } // namespace ecode diff --git a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp index 5f49a0f25..7f60b451c 100644 --- a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp +++ b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp @@ -27,6 +27,9 @@ class AIAssistantPlugin : public PluginBase { protected: LLMProviders mProviders; + bool mUIInit{ false }; + UIWidget* mStatusBar{ nullptr }; + UIPushButton* mStatusButton{ nullptr }; AIAssistantPlugin( PluginManager* pluginManager, bool sync ); @@ -41,6 +44,10 @@ class AIAssistantPlugin : public PluginBase { void onUnregisterEditor( UICodeEditor* editor ) override; void onRegisterDocument( TextDocument* doc ) override; + + void initUI(); + + void newAIAssistant(); }; } // namespace ecode diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index 5ce83399b..f5bc3fd73 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -118,7 +118,7 @@ PushButton.llm_button.primary { text-as-fallback: false; } - + @@ -146,8 +146,8 @@ static const char* DEFAULT_CHAT_GLOBE = R"xml( - @string(User, user) - @string(Assistant, assistant) + @string(user, User) + @string(assistant, Assistant) @string(system, System) @@ -162,6 +162,9 @@ ChatUI::ChatUI( UISceneNode* ui, LLMProviders providers ) { setProviders( std::move( providers ) ); mChatUI = ui->loadLayoutFromString( DEFAULT_LAYOUT ); + + mChatUI->on( Event::OnFocus, [this]( auto ) { mChatInput->setFocus(); } ); + mChatsList = mChatUI->findByClass( "llm_chats" ); mModelDDL = mChatUI->findByClass( "model_ui" ); @@ -173,6 +176,7 @@ ChatUI::ChatUI( UISceneNode* ui, LLMProviders providers ) { mChatScrollView->getVerticalScrollBar()->setValue( 1 ); mChatInput = mChatUI->findByClass( "llm_chat_input" ); + mChatInput->setData( reinterpret_cast( this ) ); mChatInput->getKeyBindings().addKeybindString( "mod+return", "prompt" ); mChatInput->getKeyBindings().addKeybindString( "mod+keypad enter", "prompt" ); @@ -362,7 +366,7 @@ void ChatUI::resizeToFit( UICodeEditor* editor ) { editor->setPixelsSize( editor->getPixelsSize().getWidth(), height ); } -nlohmann::json ChatUI::chatToJson( const std::string& /*provider*/ ) { +nlohmann::json ChatUI::chatToJson() { auto j = nlohmann::json::array(); auto chats = mChatUI->findAllByClass( "llm_conversation" ); for ( const auto& chat : chats ) { @@ -380,10 +384,9 @@ nlohmann::json ChatUI::chatToJson( const std::string& /*provider*/ ) { return j; } -nlohmann::json ChatUI::serialize( const std::string& /*provider*/ ) { - nlohmann::json j = { { "model", mCurModel.name }, - { "stream", true }, - { "messages", chatToJson( mCurModel.provider ) } }; +nlohmann::json ChatUI::serialize() { + nlohmann::json j = { + { "model", mCurModel.name }, { "stream", true }, { "messages", chatToJson() } }; if ( mCurModel.maxOutputTokens ) j["max_tokens"] = *mCurModel.maxOutputTokens; return j; @@ -446,8 +449,7 @@ void ChatUI::doRequest() { auto* editor = chat->findByClass( "data_ui" ); mRequest = std::make_unique( prepareApiUrl( apiKeyStr ), apiKeyStr, - serialize( mCurModel.provider ).dump(), - mCurModel.provider ); + serialize().dump(), mCurModel.provider ); mRequest->streamedResponseCb = [this, editor]( const std::string& chunk ) { auto conversation = chunk; editor->runOnMainThread( [this, conversation = std::move( conversation ), editor] { diff --git a/src/tools/ecode/plugins/aiassistant/chatui.hpp b/src/tools/ecode/plugins/aiassistant/chatui.hpp index 6c545aacc..5db927213 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.hpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.hpp @@ -42,7 +42,7 @@ class ChatUI { public: ChatUI( UISceneNode* ui, LLMProviders providers ); - nlohmann::json serialize( const std::string& /*provider*/ ); + nlohmann::json serialize(); void unserialize( const nlohmann::json& /*payload*/ ); @@ -66,7 +66,7 @@ class ChatUI { void showMsg( String msg ); - nlohmann::json chatToJson( const std::string& /*provider*/ ); + nlohmann::json chatToJson(); std::string prepareApiUrl( const std::string& apiKey ); diff --git a/src/tools/ecode/plugins/plugincontextprovider.hpp b/src/tools/ecode/plugins/plugincontextprovider.hpp index 3b5d4b69f..63e8aa82f 100644 --- a/src/tools/ecode/plugins/plugincontextprovider.hpp +++ b/src/tools/ecode/plugins/plugincontextprovider.hpp @@ -129,7 +129,7 @@ class PluginContextProvider { virtual std::string getDefaultFileDialogFolder() const = 0; - virtual const AppConfig& getConfig() const = 0; + virtual AppConfig& getConfig() = 0; }; } // namespace ecode diff --git a/src/tools/ecode/projectbuild.hpp b/src/tools/ecode/projectbuild.hpp index 6a1d3b7d5..9e4c21620 100644 --- a/src/tools/ecode/projectbuild.hpp +++ b/src/tools/ecode/projectbuild.hpp @@ -215,9 +215,9 @@ class ProjectBuild { ProjectBuildSteps replaceVars( const ProjectBuildSteps& steps ) const; - static json serialize( const ProjectBuild::Map& builds ); + static nlohmann::json serialize( const ProjectBuild::Map& builds ); - static ProjectBuild::Map deserialize( const json& j, const std::string& projectRoot ); + static ProjectBuild::Map deserialize( const nlohmann::json& j, const std::string& projectRoot ); protected: friend class ProjectBuildManager; diff --git a/src/tools/ecode/settingsactions.cpp b/src/tools/ecode/settingsactions.cpp index 4f4a1299d..7acf76a8d 100644 --- a/src/tools/ecode/settingsactions.cpp +++ b/src/tools/ecode/settingsactions.cpp @@ -43,9 +43,9 @@ void SettingsActions::checkForUpdatesResponse( Http::Response response, bool fro } ); }; - json j; + nlohmann::json j; try { - j = json::parse( response.getBody(), nullptr, true, true ); + j = nlohmann::json::parse( response.getBody(), nullptr, true, true ); if ( j.contains( "tag_name" ) ) { auto tagName( j["tag_name"].get() ); diff --git a/src/tools/ecode/universallocator.cpp b/src/tools/ecode/universallocator.cpp index e724d6395..5887c089b 100644 --- a/src/tools/ecode/universallocator.cpp +++ b/src/tools/ecode/universallocator.cpp @@ -978,7 +978,7 @@ std::shared_ptr UniversalLocator::emptyModel( const String& } bool UniversalLocator::findCapability( PluginCapability capability ) { - json capa; + nlohmann::json capa; capa["capability"] = capability; capa["uri"] = getCurDocURI(); PluginRequestHandle resp = mApp->getPluginManager()->sendRequest( @@ -996,7 +996,7 @@ String UniversalLocator::getDefQueryText( PluginCapability capability ) { } nlohmann::json UniversalLocator::pluginID( const PluginIDType& id ) { - json r; + nlohmann::json r; r["uri"] = getCurDocURI(); if ( id.isInteger() ) r["id"] = id.asInt(); @@ -1019,13 +1019,13 @@ void UniversalLocator::requestWorkspaceSymbol() { mLocateTable->setModel( mWorkspaceSymbolModel ); if ( mQueryWorkspaceLastId.isValid() ) { - json r( pluginID( mQueryWorkspaceLastId ) ); + nlohmann::json r( pluginID( mQueryWorkspaceLastId ) ); mApp->getPluginManager()->sendBroadcast( PluginMessageType::CancelRequest, PluginMessageFormat::JSON, &r ); } mApp->getThreadPool()->run( [this] { - json j; + nlohmann::json j; j["query"] = mWorkspaceSymbolQuery; auto hdl = mApp->getPluginManager()->sendRequest( PluginMessageType::WorkspaceSymbol, PluginMessageFormat::JSON, &j ); @@ -1070,7 +1070,7 @@ void UniversalLocator::requestDocumentSymbol() { emptyModel( getDefQueryText( PluginCapability::TextDocumentSymbol ) ); mLocateTable->setModel( mTextDocumentSymbolModel ); - json j; + nlohmann::json j; j["uri"] = mCurDocURI = getCurDocURI(); auto hdl = mApp->getPluginManager()->sendRequest( PluginMessageType::TextDocumentFlattenSymbol, PluginMessageFormat::JSON, &j );