diff --git a/ChatView/ChatRootView.cpp b/ChatView/ChatRootView.cpp index 67f1a17..147a213 100644 --- a/ChatView/ChatRootView.cpp +++ b/ChatView/ChatRootView.cpp @@ -35,6 +35,7 @@ #include "ChatAssistantSettings.hpp" #include "ChatSerializer.hpp" +#include "ConfigurationManager.hpp" #include "GeneralSettings.hpp" #include "ToolsSettings.hpp" #include "Logger.hpp" @@ -44,7 +45,6 @@ #include "context/TokenUtils.hpp" #include "llmcore/RulesLoader.hpp" #include "ProvidersManager.hpp" -#include "GeneralSettings.hpp" namespace QodeAssist::Chat { @@ -66,6 +66,20 @@ ChatRootView::ChatRootView(QQuickItem *parent) connect( &settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged); + + connect(&settings.caProvider, &Utils::BaseAspect::changed, this, [this]() { + auto &settings = Settings::generalSettings(); + m_currentConfiguration = QString("%1 - %2").arg(settings.caProvider.value(), + settings.caModel.value()); + emit currentConfigurationChanged(); + }); + + connect(&settings.caModel, &Utils::BaseAspect::changed, this, [this]() { + auto &settings = Settings::generalSettings(); + m_currentConfiguration = QString("%1 - %2").arg(settings.caProvider.value(), + settings.caModel.value()); + emit currentConfigurationChanged(); + }); connect( m_clientInterface, @@ -190,6 +204,7 @@ ChatRootView::ChatRootView(QQuickItem *parent) updateInputTokensCount(); refreshRules(); + loadAvailableConfigurations(); connect( ProjectExplorer::ProjectManager::instance(), @@ -1246,4 +1261,69 @@ bool ChatRootView::isImageFile(const QString &filePath) const return imageExtensions.contains(fileInfo.suffix().toLower()); } +void ChatRootView::loadAvailableConfigurations() +{ + auto &manager = Settings::ConfigurationManager::instance(); + manager.loadConfigurations(Settings::ConfigurationType::Chat); + + QVector configs = manager.configurations( + Settings::ConfigurationType::Chat); + + m_availableConfigurations.clear(); + m_availableConfigurations.append(QObject::tr("Current Settings")); + + for (const Settings::AIConfiguration &config : configs) { + m_availableConfigurations.append(config.name); + } + + auto &settings = Settings::generalSettings(); + QString currentProvider = settings.caProvider.value(); + QString currentModel = settings.caModel.value(); + m_currentConfiguration = QString("%1 - %2").arg(currentProvider, currentModel); + + emit availableConfigurationsChanged(); + emit currentConfigurationChanged(); +} + +void ChatRootView::applyConfiguration(const QString &configName) +{ + if (configName == QObject::tr("Current Settings")) { + return; + } + + auto &manager = Settings::ConfigurationManager::instance(); + QVector configs = manager.configurations( + Settings::ConfigurationType::Chat); + + for (const Settings::AIConfiguration &config : configs) { + if (config.name == configName) { + auto &settings = Settings::generalSettings(); + + settings.caProvider.setValue(config.provider); + settings.caModel.setValue(config.model); + settings.caTemplate.setValue(config.templateName); + settings.caUrl.setValue(config.url); + settings.caEndpointMode.setValue(settings.caEndpointMode.indexForDisplay(config.endpointMode)); + settings.caCustomEndpoint.setValue(config.customEndpoint); + + settings.writeSettings(); + + m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model); + emit currentConfigurationChanged(); + + break; + } + } +} + +QStringList ChatRootView::availableConfigurations() const +{ + return m_availableConfigurations; +} + +QString ChatRootView::currentConfiguration() const +{ + return m_currentConfiguration; +} + } // namespace QodeAssist::Chat diff --git a/ChatView/ChatRootView.hpp b/ChatView/ChatRootView.hpp index fca6079..00a775f 100644 --- a/ChatView/ChatRootView.hpp +++ b/ChatView/ChatRootView.hpp @@ -58,6 +58,8 @@ class ChatRootView : public QQuickItem Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) + Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL) + Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL) QML_ELEMENT @@ -138,6 +140,11 @@ public: Q_INVOKABLE void applyAllFileEditsForCurrentMessage(); Q_INVOKABLE void undoAllFileEditsForCurrentMessage(); Q_INVOKABLE void updateCurrentMessageEditsStats(); + + Q_INVOKABLE void loadAvailableConfigurations(); + Q_INVOKABLE void applyConfiguration(const QString &configName); + QStringList availableConfigurations() const; + QString currentConfiguration() const; int currentMessageTotalEdits() const; int currentMessageAppliedEdits() const; @@ -182,6 +189,8 @@ signals: void currentMessageEditsStatsChanged(); void isThinkingSupportChanged(); + void availableConfigurationsChanged(); + void currentConfigurationChanged(); private: void updateFileEditStatus(const QString &editId, const QString &status); @@ -213,6 +222,9 @@ private: int m_currentMessagePendingEdits{0}; int m_currentMessageRejectedEdits{0}; QString m_lastInfoMessage; + + QStringList m_availableConfigurations; + QString m_currentConfiguration; }; } // namespace QodeAssist::Chat diff --git a/ChatView/qml/RootItem.qml b/ChatView/qml/RootItem.qml index 777f6aa..b28d947 100644 --- a/ChatView/qml/RootItem.qml +++ b/ChatView/qml/RootItem.qml @@ -111,6 +111,19 @@ ChatRootView { root.isThinkingMode = thinkingMode.checked } } + configSelector { + model: root.availableConfigurations + displayText: root.currentConfiguration + onActivated: function(index) { + if (index > 0) { + root.applyConfiguration(root.availableConfigurations[index]) + } + } + + popup.onAboutToShow: { + root.loadAvailableConfigurations() + } + } } ListView { diff --git a/ChatView/qml/controls/TopBar.qml b/ChatView/qml/controls/TopBar.qml index 81abbad..71d8151 100644 --- a/ChatView/qml/controls/TopBar.qml +++ b/ChatView/qml/controls/TopBar.qml @@ -37,6 +37,7 @@ Rectangle { property alias agentModeSwitch: agentModeSwitchId property alias thinkingMode: thinkingModeId property alias activeRulesCount: activeRulesCountId.text + property alias configSelector: configSelectorId color: palette.window.hslLightness > 0.5 ? Qt.darker(palette.window, 1.1) : @@ -238,6 +239,17 @@ Rectangle { ToolTip.delay: 250 ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold") } + + QoAComboBox { + id: configSelectorId + + model: [] + currentIndex: 0 + + ToolTip.visible: hovered + ToolTip.delay: 250 + ToolTip.text: qsTr("Switch AI configuration") + } } } } diff --git a/ConfigurationManager.cpp b/ConfigurationManager.cpp index 23ab07d..c1eb655 100644 --- a/ConfigurationManager.cpp +++ b/ConfigurationManager.cpp @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2024-2025 Petr Mironychev +/* + * Copyright (C) 2025 Petr Mironychev * * This file is part of QodeAssist. * diff --git a/ConfigurationManager.hpp b/ConfigurationManager.hpp index 287b192..b5be510 100644 --- a/ConfigurationManager.hpp +++ b/ConfigurationManager.hpp @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2024-2025 Petr Mironychev +/* + * Copyright (C) 2025 Petr Mironychev * * This file is part of QodeAssist. * diff --git a/UIControls/CMakeLists.txt b/UIControls/CMakeLists.txt index 96a028d..83c773f 100644 --- a/UIControls/CMakeLists.txt +++ b/UIControls/CMakeLists.txt @@ -11,7 +11,12 @@ qt_add_qml_module(QodeAssistUIControls qml/Badge.qml qml/QoAButton.qml qml/QoATextSlider.qml + qml/QoAComboBox.qml qml/FadeListItemAnimation.qml + + RESOURCES + icons/dropdown-arrow-light.svg + icons/dropdown-arrow-dark.svg ) target_link_libraries(QodeAssistUIControls diff --git a/UIControls/icons/dropdown-arrow-dark.svg b/UIControls/icons/dropdown-arrow-dark.svg new file mode 100644 index 0000000..b761734 --- /dev/null +++ b/UIControls/icons/dropdown-arrow-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/UIControls/icons/dropdown-arrow-light.svg b/UIControls/icons/dropdown-arrow-light.svg new file mode 100644 index 0000000..6928799 --- /dev/null +++ b/UIControls/icons/dropdown-arrow-light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/UIControls/qml/QoAComboBox.qml b/UIControls/qml/QoAComboBox.qml new file mode 100644 index 0000000..48964ba --- /dev/null +++ b/UIControls/qml/QoAComboBox.qml @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic as Basic + +Basic.ComboBox { + id: control + + implicitWidth: Math.min(contentItem.implicitWidth + 8, 300) + implicitHeight: 30 + + indicator: Image { + id: dropdownIcon + x: control.width - width - 10 + y: control.topPadding + (control.availableHeight - height) / 2 + width: 12 + height: 8 + source: palette.window.hslLightness > 0.5 + ? "qrc:/qt/qml/UIControls/icons/dropdown-arrow-light.svg" + : "qrc:/qt/qml/UIControls/icons/dropdown-arrow-dark.svg" + sourceSize: Qt.size(width, height) + + rotation: control.popup.visible ? 180 : 0 + + Behavior on rotation { + NumberAnimation { duration: 150; easing.type: Easing.OutQuad } + } + } + + background: Rectangle { + id: bg + + implicitWidth: control.implicitWidth + implicitHeight: 30 + color: !control.enabled || !control.down ? palette.button : palette.dark + border.color: !control.enabled || (!control.hovered && !control.visualFocus) + ? palette.mid + : palette.highlight + border.width: 1 + radius: 5 + + Behavior on color { + ColorAnimation { duration: 150 } + } + + Behavior on border.color { + ColorAnimation { duration: 150 } + } + + Rectangle { + anchors.fill: bg + radius: bg.radius + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.alpha(palette.highlight, 0.4) } + GradientStop { position: 1.0; color: Qt.alpha(palette.highlight, 0.2) } + } + opacity: control.hovered ? 0.3 : 0.01 + + Behavior on opacity { + NumberAnimation { duration: 250 } + } + } + } + + contentItem: Text { + leftPadding: 10 + rightPadding: 30 + text: control.displayText + font.pixelSize: 12 + color: palette.text + verticalAlignment: Text.AlignVCenter + elide: Text.ElideNone + } + + popup: Popup { + y: control.height + 2 + width: control.width + implicitHeight: Math.min(contentItem.implicitHeight, 300) + padding: 4 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: control.popup.visible ? control.delegateModel : null + currentIndex: control.highlightedIndex + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + } + + background: Rectangle { + color: palette.base + border.color: Qt.lighter(palette.mid, 1.1) + border.width: 1 + radius: 5 + } + + enter: Transition { + NumberAnimation { property: "opacity"; from: 0.0; to: 1.0; duration: 150 } + } + + exit: Transition { + NumberAnimation { property: "opacity"; from: 1.0; to: 0.0; duration: 100 } + } + } + + delegate: ItemDelegate { + width: control.width - 8 + height: 32 + + contentItem: Text { + text: modelData + color: palette.text + font.pixelSize: 12 + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + leftPadding: 10 + } + + highlighted: control.highlightedIndex === index + + background: Rectangle { + radius: 4 + color: highlighted + ? Qt.alpha(palette.highlight, 0.2) + : parent.hovered + ? (palette.window.hslLightness > 0.5 + ? Qt.darker(palette.base, 1.05) + : Qt.lighter(palette.base, 1.15)) + : "transparent" + + Behavior on color { + ColorAnimation { duration: 100 } + } + } + } +} + diff --git a/settings/CMakeLists.txt b/settings/CMakeLists.txt index c65e064..7a53058 100644 --- a/settings/CMakeLists.txt +++ b/settings/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(QodeAssistSettings STATIC GeneralSettings.hpp GeneralSettings.cpp CustomPromptSettings.hpp CustomPromptSettings.cpp + ConfigurationManager.hpp ConfigurationManager.cpp SettingsUtils.hpp SettingsConstants.hpp ButtonAspect.hpp diff --git a/settings/ConfigurationManager.cpp b/settings/ConfigurationManager.cpp new file mode 100644 index 0000000..32fb69c --- /dev/null +++ b/settings/ConfigurationManager.cpp @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2024-2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "ConfigurationManager.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Logger.hpp" + +namespace QodeAssist::Settings { + +ConfigurationManager::ConfigurationManager(QObject *parent) + : QObject(parent) +{} + +ConfigurationManager &ConfigurationManager::instance() +{ + static ConfigurationManager instance; + return instance; +} + +QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const +{ + switch (type) { + case ConfigurationType::CodeCompletion: + return "code_completion"; + case ConfigurationType::Chat: + return "chat"; + case ConfigurationType::QuickRefactor: + return "quick_refactor"; + } + return "unknown"; +} + +QString ConfigurationManager::getConfigurationDirectory(ConfigurationType type) const +{ + QString path = QString("%1/qodeassist/configurations/%2") + .arg(Core::ICore::userResourcePath().toFSPathString(), + configurationTypeToString(type)); + return path; +} + +bool ConfigurationManager::ensureDirectoryExists(ConfigurationType type) const +{ + QDir dir(getConfigurationDirectory(type)); + if (!dir.exists()) { + return dir.mkpath("."); + } + return true; +} + +bool ConfigurationManager::loadConfigurations(ConfigurationType type) +{ + QVector *configs = nullptr; + switch (type) { + case ConfigurationType::CodeCompletion: + configs = &m_ccConfigurations; + break; + case ConfigurationType::Chat: + configs = &m_caConfigurations; + break; + case ConfigurationType::QuickRefactor: + configs = &m_qrConfigurations; + break; + } + + if (!configs) { + return false; + } + + configs->clear(); + + if (!ensureDirectoryExists(type)) { + LOG_MESSAGE("Failed to create configuration directory"); + return false; + } + + QDir dir(getConfigurationDirectory(type)); + QStringList filters; + filters << "*.json"; + QFileInfoList files = dir.entryInfoList(filters, QDir::Files); + + for (const QFileInfo &fileInfo : files) { + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QIODevice::ReadOnly)) { + LOG_MESSAGE(QString("Failed to open configuration file: %1").arg(fileInfo.fileName())); + continue; + } + + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + file.close(); + + if (!doc.isObject()) { + LOG_MESSAGE(QString("Invalid configuration file: %1").arg(fileInfo.fileName())); + continue; + } + + QJsonObject obj = doc.object(); + AIConfiguration config; + config.id = obj["id"].toString(); + config.name = obj["name"].toString(); + config.provider = obj["provider"].toString(); + config.model = obj["model"].toString(); + config.templateName = obj["template"].toString(); + config.url = obj["url"].toString(); + config.endpointMode = obj["endpointMode"].toString(); + config.customEndpoint = obj["customEndpoint"].toString(); + config.type = type; + config.formatVersion = obj.value("formatVersion").toInt(1); + + if (config.id.isEmpty() || config.name.isEmpty()) { + LOG_MESSAGE(QString("Invalid configuration data in file: %1").arg(fileInfo.fileName())); + continue; + } + + configs->append(config); + } + + emit configurationsChanged(type); + return true; +} + +bool ConfigurationManager::saveConfiguration(const AIConfiguration &config) +{ + if (!ensureDirectoryExists(config.type)) { + LOG_MESSAGE("Failed to create configuration directory"); + return false; + } + + QJsonObject obj; + obj["formatVersion"] = config.formatVersion; + obj["id"] = config.id; + obj["name"] = config.name; + obj["provider"] = config.provider; + obj["model"] = config.model; + obj["template"] = config.templateName; + obj["url"] = config.url; + obj["endpointMode"] = config.endpointMode; + obj["customEndpoint"] = config.customEndpoint; + + QString sanitizedName = config.name; + sanitizedName.replace(" ", "_"); + sanitizedName.replace(QRegularExpression("[^a-zA-Z0-9_-]"), ""); + + QString fileName = QString("%1/%2_%3.json") + .arg(getConfigurationDirectory(config.type), sanitizedName, config.id); + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + LOG_MESSAGE(QString("Failed to create configuration file: %1").arg(fileName)); + return false; + } + + QJsonDocument doc(obj); + file.write(doc.toJson(QJsonDocument::Indented)); + file.close(); + + loadConfigurations(config.type); + return true; +} + +bool ConfigurationManager::deleteConfiguration(const QString &id, ConfigurationType type) +{ + QDir dir(getConfigurationDirectory(type)); + QStringList filters; + filters << QString("*_%1.json").arg(id); + QFileInfoList files = dir.entryInfoList(filters, QDir::Files); + + if (files.isEmpty()) { + LOG_MESSAGE(QString("Configuration file not found for id: %1").arg(id)); + return false; + } + + for (const QFileInfo &fileInfo : files) { + QFile file(fileInfo.absoluteFilePath()); + if (!file.remove()) { + LOG_MESSAGE(QString("Failed to delete configuration file: %1") + .arg(fileInfo.absoluteFilePath())); + return false; + } + } + + loadConfigurations(type); + return true; +} + +QVector ConfigurationManager::configurations(ConfigurationType type) const +{ + switch (type) { + case ConfigurationType::CodeCompletion: + return m_ccConfigurations; + case ConfigurationType::Chat: + return m_caConfigurations; + case ConfigurationType::QuickRefactor: + return m_qrConfigurations; + } + return {}; +} + +AIConfiguration ConfigurationManager::getConfigurationById(const QString &id, + ConfigurationType type) const +{ + const QVector &configs = configurations(type); + for (const AIConfiguration &config : configs) { + if (config.id == id) { + return config; + } + } + return AIConfiguration(); +} + +} // namespace QodeAssist::Settings + diff --git a/settings/ConfigurationManager.hpp b/settings/ConfigurationManager.hpp new file mode 100644 index 0000000..6ba0d64 --- /dev/null +++ b/settings/ConfigurationManager.hpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024-2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include +#include +#include + +namespace QodeAssist::Settings { + +enum class ConfigurationType { CodeCompletion, Chat, QuickRefactor }; + +inline constexpr int CONFIGURATION_FORMAT_VERSION = 1; + +struct AIConfiguration +{ + QString id; + QString name; + QString provider; + QString model; + QString templateName; + QString url; + QString endpointMode; + QString customEndpoint; + ConfigurationType type; + int formatVersion = CONFIGURATION_FORMAT_VERSION; +}; + +class ConfigurationManager : public QObject +{ + Q_OBJECT + +public: + static ConfigurationManager &instance(); + + bool loadConfigurations(ConfigurationType type); + bool saveConfiguration(const AIConfiguration &config); + bool deleteConfiguration(const QString &id, ConfigurationType type); + + QVector configurations(ConfigurationType type) const; + AIConfiguration getConfigurationById(const QString &id, ConfigurationType type) const; + + QString getConfigurationDirectory(ConfigurationType type) const; + +signals: + void configurationsChanged(ConfigurationType type); + +private: + explicit ConfigurationManager(QObject *parent = nullptr); + ~ConfigurationManager() override = default; + + bool ensureDirectoryExists(ConfigurationType type) const; + QString configurationTypeToString(ConfigurationType type) const; + + QVector m_ccConfigurations; + QVector m_caConfigurations; + QVector m_qrConfigurations; +}; + +} // namespace QodeAssist::Settings + diff --git a/settings/GeneralSettings.cpp b/settings/GeneralSettings.cpp index 2bf7583..4507c2b 100644 --- a/settings/GeneralSettings.cpp +++ b/settings/GeneralSettings.cpp @@ -23,11 +23,16 @@ #include #include #include +#include +#include #include #include #include +#include #include #include +#include +#include #include #include #include @@ -35,6 +40,7 @@ #include #include "../Version.hpp" +#include "ConfigurationManager.hpp" #include "Logger.hpp" #include "SettingsConstants.hpp" #include "SettingsDialog.hpp" @@ -119,6 +125,12 @@ GeneralSettings::GeneralSettings() ccTemplateDescription.setReadOnly(true); ccTemplateDescription.setDefaultValue(""); + ccSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG; + ccLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG; + ccOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER; + ccOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon(); + ccOpenConfigFolder.m_isCompact = true; + // preset1 specifyPreset1.setSettingsKey(Constants::CC_SPECIFY_PRESET1); specifyPreset1.setLabelText(TrConstants::ADD_NEW_PRESET); @@ -204,6 +216,12 @@ GeneralSettings::GeneralSettings() caTemplateDescription.setReadOnly(true); caTemplateDescription.setDefaultValue(""); + caSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG; + caLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG; + caOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER; + caOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon(); + caOpenConfigFolder.m_isCompact = true; + // quick refactor settings initStringAspect(qrProvider, Constants::QR_PROVIDER, TrConstants::PROVIDER, "Ollama"); qrProvider.setReadOnly(true); @@ -242,6 +260,12 @@ GeneralSettings::GeneralSettings() qrTemplateDescription.setReadOnly(true); qrTemplateDescription.setDefaultValue(""); + qrSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG; + qrLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG; + qrOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER; + qrOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon(); + qrOpenConfigFolder.m_isCompact = true; + ccShowTemplateInfo.m_icon = Utils::Icons::INFO.icon(); ccShowTemplateInfo.m_tooltip = Tr::tr("Show template information"); ccShowTemplateInfo.m_isCompact = true; @@ -300,15 +324,18 @@ GeneralSettings::GeneralSettings() auto ccGroup = Group{ title(TrConstants::CODE_COMPLETION), Column{ + Row{ccSaveConfig, ccLoadConfig, ccOpenConfigFolder, Stretch{1}}, ccGrid, Row{specifyPreset1, preset1Language, Stretch{1}}, ccPreset1Grid}}; auto caGroup = Group{ - title(TrConstants::CHAT_ASSISTANT), Column{caGrid}}; + title(TrConstants::CHAT_ASSISTANT), + Column{Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}}, caGrid}}; auto qrGroup = Group{ - title(TrConstants::QUICK_REFACTOR), Column{qrGrid}}; + title(TrConstants::QUICK_REFACTOR), + Column{Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}}, qrGrid}}; auto rootLayout = Column{ Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}}, @@ -417,7 +444,7 @@ void GeneralSettings::showModelsNotSupportedDialog(Utils::StringAspect &aspect) QString key = QString("CompleterHistory/") .append( - (&aspect == &ccModel) ? Constants::CC_MODEL_HISTORY + (&aspect == &ccModel) ? Constants::CC_MODEL_HISTORY : (&aspect == &caModel) ? Constants::CA_MODEL_HISTORY : Constants::QR_MODEL_HISTORY); #if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0) @@ -494,7 +521,8 @@ void GeneralSettings::showUrlSelectionDialog( dialog.exec(); } -void GeneralSettings::showTemplateInfoDialog(const Utils::StringAspect &descriptionAspect, const QString &templateName) +void GeneralSettings::showTemplateInfoDialog( + const Utils::StringAspect &descriptionAspect, const QString &templateName) { SettingsDialog dialog(Tr::tr("Template Information")); dialog.addLabel(QString("%1: %2").arg(Tr::tr("Template"), templateName)); @@ -575,6 +603,48 @@ void GeneralSettings::setupConnections() connect(&qrShowTemplateInfo, &ButtonAspect::clicked, this, [this]() { showTemplateInfoDialog(qrTemplateDescription, qrTemplate.value()); }); + + connect(&ccSaveConfig, &ButtonAspect::clicked, this, [this]() { onSaveConfiguration("cc"); }); + connect(&ccLoadConfig, &ButtonAspect::clicked, this, [this]() { onLoadConfiguration("cc"); }); + + connect(&caSaveConfig, &ButtonAspect::clicked, this, [this]() { onSaveConfiguration("ca"); }); + connect(&caLoadConfig, &ButtonAspect::clicked, this, [this]() { onLoadConfiguration("ca"); }); + + connect(&qrSaveConfig, &ButtonAspect::clicked, this, [this]() { onSaveConfiguration("qr"); }); + connect(&qrLoadConfig, &ButtonAspect::clicked, this, [this]() { onLoadConfiguration("qr"); }); + + connect(&ccOpenConfigFolder, &ButtonAspect::clicked, this, [this]() { + auto &manager = ConfigurationManager::instance(); + QString path = manager.getConfigurationDirectory(ConfigurationType::CodeCompletion); + QDir dir(path); + if (!dir.exists()) { + dir.mkpath("."); + } + QUrl url = QUrl::fromLocalFile(dir.absolutePath()); + QDesktopServices::openUrl(url); + }); + + connect(&caOpenConfigFolder, &ButtonAspect::clicked, this, [this]() { + auto &manager = ConfigurationManager::instance(); + QString path = manager.getConfigurationDirectory(ConfigurationType::Chat); + QDir dir(path); + if (!dir.exists()) { + dir.mkpath("."); + } + QUrl url = QUrl::fromLocalFile(dir.absolutePath()); + QDesktopServices::openUrl(url); + }); + + connect(&qrOpenConfigFolder, &ButtonAspect::clicked, this, [this]() { + auto &manager = ConfigurationManager::instance(); + QString path = manager.getConfigurationDirectory(ConfigurationType::QuickRefactor); + QDir dir(path); + if (!dir.exists()) { + dir.mkpath("."); + } + QUrl url = QUrl::fromLocalFile(dir.absolutePath()); + QDesktopServices::openUrl(url); + }); } void GeneralSettings::resetPageToDefaults() @@ -620,6 +690,176 @@ void GeneralSettings::resetPageToDefaults() } } +void GeneralSettings::onSaveConfiguration(const QString &prefix) +{ + bool ok; + QString configName = QInputDialog::getText( + Core::ICore::dialogParent(), + TrConstants::SAVE_CONFIGURATION, + TrConstants::CONFIGURATION_NAME, + QLineEdit::Normal, + QString(), + &ok); + + if (!ok || configName.trimmed().isEmpty()) { + return; + } + + AIConfiguration config; + config.id = QUuid::createUuid().toString(QUuid::WithoutBraces); + config.name = configName.trimmed(); + + if (prefix == "cc") { + config.provider = ccProvider.value(); + config.model = ccModel.value(); + config.templateName = ccTemplate.value(); + config.url = ccUrl.value(); + config.endpointMode = ccEndpointMode.stringValue(); + config.customEndpoint = ccCustomEndpoint.value(); + config.type = ConfigurationType::CodeCompletion; + } else if (prefix == "ca") { + config.provider = caProvider.value(); + config.model = caModel.value(); + config.templateName = caTemplate.value(); + config.url = caUrl.value(); + config.endpointMode = caEndpointMode.stringValue(); + config.customEndpoint = caCustomEndpoint.value(); + config.type = ConfigurationType::Chat; + } else if (prefix == "qr") { + config.provider = qrProvider.value(); + config.model = qrModel.value(); + config.templateName = qrTemplate.value(); + config.url = qrUrl.value(); + config.endpointMode = qrEndpointMode.stringValue(); + config.customEndpoint = qrCustomEndpoint.value(); + config.type = ConfigurationType::QuickRefactor; + } + + auto &manager = ConfigurationManager::instance(); + if (manager.saveConfiguration(config)) { + QMessageBox::information( + Core::ICore::dialogParent(), + TrConstants::SAVE_CONFIGURATION, + TrConstants::CONFIGURATION_SAVED); + } else { + QMessageBox::warning( + Core::ICore::dialogParent(), + TrConstants::SAVE_CONFIGURATION, + Tr::tr("Failed to save configuration. Check logs for details.")); + } +} + +void GeneralSettings::onLoadConfiguration(const QString &prefix) +{ + ConfigurationType type; + if (prefix == "cc") { + type = ConfigurationType::CodeCompletion; + } else if (prefix == "ca") { + type = ConfigurationType::Chat; + } else if (prefix == "qr") { + type = ConfigurationType::QuickRefactor; + } else { + return; + } + + auto &manager = ConfigurationManager::instance(); + manager.loadConfigurations(type); + + QVector configs = manager.configurations(type); + if (configs.isEmpty()) { + QMessageBox::information( + Core::ICore::dialogParent(), + TrConstants::LOAD_CONFIGURATION, + TrConstants::NO_CONFIGURATIONS_FOUND); + return; + } + + SettingsDialog dialog(TrConstants::LOAD_CONFIGURATION); + dialog.addLabel(TrConstants::SELECT_CONFIGURATION); + dialog.addSpacing(); + + QStringList configNames; + for (const AIConfiguration &config : configs) { + configNames.append(config.name); + } + + auto configList = dialog.addComboBox(configNames, QString()); + dialog.addSpacing(); + + auto *deleteButton = new QPushButton(TrConstants::DELETE_CONFIGURATION); + auto *okButton = new QPushButton(TrConstants::OK); + auto *cancelButton = new QPushButton(TrConstants::CANCEL); + + connect(deleteButton, &QPushButton::clicked, &dialog, [&]() { + int currentIndex = configList->currentIndex(); + if (currentIndex >= 0 && currentIndex < configs.size()) { + QMessageBox::StandardButton reply = QMessageBox::question( + &dialog, + TrConstants::DELETE_CONFIGURATION, + TrConstants::CONFIRM_DELETE_CONFIG, + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + const AIConfiguration &configToDelete = configs[currentIndex]; + if (manager.deleteConfiguration(configToDelete.id, type)) { + dialog.accept(); + onLoadConfiguration(prefix); + } else { + QMessageBox::warning( + &dialog, + TrConstants::DELETE_CONFIGURATION, + Tr::tr("Failed to delete configuration.")); + } + } + } + }); + + connect(okButton, &QPushButton::clicked, &dialog, [&]() { + int currentIndex = configList->currentIndex(); + if (currentIndex >= 0 && currentIndex < configs.size()) { + const AIConfiguration &config = configs[currentIndex]; + + if (prefix == "cc") { + ccProvider.setValue(config.provider); + ccModel.setValue(config.model); + ccTemplate.setValue(config.templateName); + ccUrl.setValue(config.url); + ccEndpointMode.setValue(ccEndpointMode.indexForDisplay(config.endpointMode)); + ccCustomEndpoint.setValue(config.customEndpoint); + } else if (prefix == "ca") { + caProvider.setValue(config.provider); + caModel.setValue(config.model); + caTemplate.setValue(config.templateName); + caUrl.setValue(config.url); + caEndpointMode.setValue(caEndpointMode.indexForDisplay(config.endpointMode)); + caCustomEndpoint.setValue(config.customEndpoint); + } else if (prefix == "qr") { + qrProvider.setValue(config.provider); + qrModel.setValue(config.model); + qrTemplate.setValue(config.templateName); + qrUrl.setValue(config.url); + qrEndpointMode.setValue(qrEndpointMode.indexForDisplay(config.endpointMode)); + qrCustomEndpoint.setValue(config.customEndpoint); + } + + writeSettings(); + QMessageBox::information( + Core::ICore::dialogParent(), + TrConstants::LOAD_CONFIGURATION, + TrConstants::CONFIGURATION_LOADED); + dialog.accept(); + } + }); + + connect(cancelButton, &QPushButton::clicked, &dialog, &QDialog::reject); + + dialog.buttonLayout()->addWidget(deleteButton); + addDialogButtons(dialog.buttonLayout(), okButton, cancelButton); + + configList->setFocus(); + dialog.exec(); +} + class GeneralSettingsPage : public Core::IOptionsPage { public: diff --git a/settings/GeneralSettings.hpp b/settings/GeneralSettings.hpp index c96f3c0..07601ec 100644 --- a/settings/GeneralSettings.hpp +++ b/settings/GeneralSettings.hpp @@ -66,6 +66,10 @@ public: Utils::StringAspect ccTemplateDescription{this}; + ButtonAspect ccSaveConfig{this}; + ButtonAspect ccLoadConfig{this}; + ButtonAspect ccOpenConfigFolder{this}; + // TODO create dynamic presets system // preset1 for code completion settings Utils::BoolAspect specifyPreset1{this}; @@ -107,6 +111,10 @@ public: Utils::StringAspect caTemplateDescription{this}; + ButtonAspect caSaveConfig{this}; + ButtonAspect caLoadConfig{this}; + ButtonAspect caOpenConfigFolder{this}; + // quick refactor settings Utils::StringAspect qrProvider{this}; ButtonAspect qrSelectProvider{this}; @@ -128,6 +136,10 @@ public: Utils::StringAspect qrTemplateDescription{this}; + ButtonAspect qrSaveConfig{this}; + ButtonAspect qrLoadConfig{this}; + ButtonAspect qrOpenConfigFolder{this}; + ButtonAspect ccShowTemplateInfo{this}; ButtonAspect caShowTemplateInfo{this}; ButtonAspect qrShowTemplateInfo{this}; @@ -148,6 +160,9 @@ public: void updatePreset1Visiblity(bool state); + void onSaveConfiguration(const QString &prefix); + void onLoadConfiguration(const QString &prefix); + private: void setupConnections(); void resetPageToDefaults(); diff --git a/settings/SettingsTr.hpp b/settings/SettingsTr.hpp index fa4cd78..4b367c7 100644 --- a/settings/SettingsTr.hpp +++ b/settings/SettingsTr.hpp @@ -99,6 +99,19 @@ inline const char AUTO_COMPLETION_SETTINGS[] inline const char ADD_NEW_PRESET[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Add new preset for language"); +inline const char SAVE_CONFIG[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Save Config..."); +inline const char LOAD_CONFIG[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Load Config..."); +inline const char OPEN_CONFIG_FOLDER[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Open Folder"); +inline const char SAVE_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Save Configuration"); +inline const char LOAD_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Load Configuration"); +inline const char CONFIGURATION_NAME[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configuration name:"); +inline const char SELECT_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Select Configuration"); +inline const char NO_CONFIGURATIONS_FOUND[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "No saved configurations found."); +inline const char CONFIGURATION_SAVED[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configuration saved successfully."); +inline const char CONFIGURATION_LOADED[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Configuration loaded successfully."); +inline const char DELETE_CONFIGURATION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Delete"); +inline const char CONFIRM_DELETE_CONFIG[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Are you sure you want to delete this configuration?"); + } // namespace TrConstants struct Tr