More AI Assistant WIP.

This commit is contained in:
Martín Lucas Golini
2025-03-12 01:27:20 -03:00
parent 4ef3dfa312
commit 2bccd1f836
15 changed files with 158 additions and 40 deletions

Binary file not shown.

View File

@@ -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<bool( UITabWidget* )> run ) const;
void forEachTab( std::function<void( UITab* )> run ) const;
void zoomIn();
void zoomOut();
@@ -355,11 +357,15 @@ class EE_API UICodeEditorSplitter {
void setOnTabWidgetCreateCb( std::function<void( UITabWidget* )> 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<ThreadPool> mThreadPool;
@@ -397,8 +403,6 @@ class EE_API UICodeEditorSplitter {
UITabWidget* createTabWidget( Node* parent );
UITabWidget* splitTabWidget( SplitDirection, UITabWidget* );
void updateTabWidgetVisualSplitting();
};

View File

@@ -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 )

View File

@@ -856,6 +856,12 @@ void UICodeEditorSplitter::forEachTabWidgetStoppable(
return;
}
void UICodeEditorSplitter::forEachTab( std::function<void( UITab* )> run ) const {
for ( auto tabWidget : mTabWidgets )
for ( size_t i = 0; i < tabWidget->getTabCount(); i++ )
run( tabWidget->getTab( i ) );
}
void UICodeEditorSplitter::forEachEditorStoppable(
std::function<bool( UICodeEditor* )> 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() );

View File

@@ -9,6 +9,8 @@
#include <eepp/system/sys.hpp>
#include <eterm/ui/uiterminal.hpp>
#include <nlohmann/json.hpp>
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<UISplitter>();
@@ -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<UIWidget>();
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<Int32>( 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<Int32>( currentPage, 0, curTabWidget->getTabCount() - 1 ) );
}
}
}
} else if ( j["type"] == "splitter" ) {

View File

@@ -9,8 +9,7 @@
#include <eepp/ui/uicodeeditor.hpp>
#include <eepp/window/window.hpp>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
#include <nlohmann/json_fwd.hpp>
using namespace EE;
using namespace EE::Math;
@@ -189,6 +188,11 @@ struct SessionSnapshotFile {
std::string selection;
};
struct TabWidgetCbs {
std::function<nlohmann::json( UIWidget* )> onSave;
std::function<UIWidget*( const nlohmann::json& )> 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<SessionSnapshotFile>& sessionSnapshotFiles );
void editorLoadedCounter( ecode::App* app );
nlohmann::json saveNode( Node* node );
Mutex tabWidgetTypesMutex;
std::unordered_map<std::string, TabWidgetCbs> tabWidgetTypes;
};
} // namespace ecode

View File

@@ -1959,6 +1959,8 @@ void App::closeEditors() {
widget->asType<UIBuildSettings>()->getTab()->removeTab( true, true );
} );
mSplitter->forEachTab( []( UITab* tab ) { tab->removeTab( true, true ); } );
mCurrentProject = "";
mCurrentProjectName = "";
mDirTree = nullptr;

View File

@@ -2,6 +2,7 @@
#include "chatui.hpp"
#include "protocol.hpp"
#include "../../appconfig.hpp"
#include "../../widgetcommandexecuter.hpp"
#include <eepp/system/filesystem.hpp>
@@ -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

View File

@@ -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

View File

@@ -118,7 +118,7 @@ PushButton.llm_button.primary {
text-as-fallback: false;
}
</style>
<vbox class="llm_chatui" lw="mp" lh="mp">
<vbox class="llm_chatui" lw="mp" lh="mp">
<hbox class="llm_topbar" lw="mp" lh="wc">
<TextView text="@string(llm_model, LLM Model:)" margin-right="4dp" />
<DropDownList class="model_ui" lw="350dp" selected-index="0"></DropDownList>
@@ -146,8 +146,8 @@ static const char* DEFAULT_CHAT_GLOBE = R"xml(
<vbox class="llm_conversation" lw="mp" lh="wc">
<hbox class="llm_conversation_opt">
<DropDownList class="role_ui" lw="150dp" selected-index="1">
<item>@string(User, user)</item>
<item>@string(Assistant, assistant)</item>
<item>@string(user, User)</item>
<item>@string(assistant, Assistant)</item>
<item>@string(system, System)</item>
</DropDownList>
<PushButton class="move_up" text="@string(move_up, Move Up)" icon="icon(arrow-up-s, 12dp)" tooltip="@string(move_up, Move Up)" />
@@ -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<UIDropDownList>( "model_ui" );
@@ -173,6 +176,7 @@ ChatUI::ChatUI( UISceneNode* ui, LLMProviders providers ) {
mChatScrollView->getVerticalScrollBar()->setValue( 1 );
mChatInput = mChatUI->findByClass<UICodeEditor>( "llm_chat_input" );
mChatInput->setData( reinterpret_cast<UintPtr>( 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<UICodeEditor>( "data_ui" );
mRequest = std::make_unique<LLMChatCompletionRequest>( 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] {

View File

@@ -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 );

View File

@@ -129,7 +129,7 @@ class PluginContextProvider {
virtual std::string getDefaultFileDialogFolder() const = 0;
virtual const AppConfig& getConfig() const = 0;
virtual AppConfig& getConfig() = 0;
};
} // namespace ecode

View File

@@ -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;

View File

@@ -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<std::string>() );

View File

@@ -978,7 +978,7 @@ std::shared_ptr<LSPSymbolInfoModel> 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 );