Compare commits

..

12 Commits

11 changed files with 525 additions and 178 deletions

View File

@ -51,7 +51,7 @@ jobs:
}
- {
qt_version: "6.10.1",
qt_creator_version: "18.0.1"
qt_creator_version: "18.0.2"
}
steps:

View File

@ -38,14 +38,6 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {false, "Failed to create directory structure"};
}
QString contentFolder = getChatContentFolder(filePath);
QDir dir;
if (!dir.exists(contentFolder)) {
if (!dir.mkpath(contentFolder)) {
LOG_MESSAGE(QString("Warning: Failed to create content folder: %1").arg(contentFolder));
}
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
@ -88,21 +80,22 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
return {true, QString()};
}
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message, const QString &chatFilePath)
QJsonObject ChatSerializer::serializeMessage(
const ChatModel::Message &message, const QString &chatFilePath)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
if (message.isRedacted) {
messageObj["isRedacted"] = true;
}
if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature;
}
if (!message.attachments.isEmpty()) {
QJsonArray attachmentsArray;
for (const auto &attachment : message.attachments) {
@ -113,7 +106,7 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
}
messageObj["attachments"] = attachmentsArray;
}
if (!message.images.isEmpty()) {
QJsonArray imagesArray;
for (const auto &image : message.images) {
@ -125,11 +118,12 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
}
messageObj["images"] = imagesArray;
}
return messageObj;
}
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, const QString &chatFilePath)
ChatModel::Message ChatSerializer::deserializeMessage(
const QJsonObject &json, const QString &chatFilePath)
{
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
@ -137,7 +131,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString();
if (json.contains("attachments")) {
QJsonArray attachmentsArray = json["attachments"].toArray();
for (const auto &attachmentValue : attachmentsArray) {
@ -148,7 +142,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
message.attachments.append(attachment);
}
}
if (json.contains("images")) {
QJsonArray imagesArray = json["images"].toArray();
for (const auto &imageValue : imagesArray) {
@ -160,7 +154,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
message.images.append(image);
}
}
return message;
}
@ -178,7 +172,8 @@ QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString
return root;
}
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
bool ChatSerializer::deserializeChat(
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
{
QJsonArray messagesArray = json["messages"].toArray();
QVector<ChatModel::Message> messages;
@ -189,17 +184,24 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json,
}
model->clear();
model->setLoadingFromHistory(true);
for (const auto &message : messages) {
model->addMessage(message.content, message.role, message.id, message.attachments, message.images, message.isRedacted, message.signature);
model->addMessage(
message.content,
message.role,
message.id,
message.attachments,
message.images,
message.isRedacted,
message.signature);
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
.arg(message.images.size())
.arg(message.isRedacted)
.arg(message.signature.length()));
}
model->setLoadingFromHistory(false);
return true;
@ -217,12 +219,14 @@ bool ChatSerializer::validateVersion(const QString &version)
if (version == VERSION) {
return true;
}
if (version == "0.1") {
LOG_MESSAGE("Loading chat from old format 0.1 - images folder structure has changed from _images to _content");
LOG_MESSAGE(
"Loading chat from old format 0.1 - images folder structure has changed from _images "
"to _content");
return true;
}
return false;
}
@ -234,10 +238,11 @@ QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
return QDir(dirPath).filePath(baseName + "_content");
}
bool ChatSerializer::saveContentToStorage(const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath)
bool ChatSerializer::saveContentToStorage(
const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath)
{
QString contentFolder = getChatContentFolder(chatFilePath);
QDir dir;
@ -247,34 +252,34 @@ bool ChatSerializer::saveContentToStorage(const QString &chatFilePath,
return false;
}
}
QFileInfo originalFileInfo(fileName);
QString extension = originalFileInfo.suffix();
QString baseName = originalFileInfo.completeBaseName();
QString uniqueName = QString("%1_%2.%3")
.arg(baseName)
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
.arg(extension);
.arg(baseName)
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
.arg(extension);
QString fullPath = QDir(contentFolder).filePath(uniqueName);
QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8());
QFile file(fullPath);
if (!file.open(QIODevice::WriteOnly)) {
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
return false;
}
if (file.write(contentData) == -1) {
LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString()));
return false;
}
file.close();
storedPath = uniqueName;
LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath));
return true;
}
@ -282,16 +287,16 @@ QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, cons
{
QString contentFolder = getChatContentFolder(chatFilePath);
QString fullPath = QDir(contentFolder).filePath(storedPath);
QFile file(fullPath);
if (!file.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath));
return QString();
}
QByteArray contentData = file.readAll();
file.close();
return contentData.toBase64();
}

View File

@ -1,7 +1,7 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.9.7",
"Version" : "0.9.9",
"CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",

View File

@ -125,16 +125,34 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
## Configuration
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide:
### Quick Setup (Recommended for Beginners)
### Supported Providers
The Quick Setup feature provides one-click configuration for popular cloud AI models. Get started in 3 easy steps:
<details>
<summary>Quick setup: (click to expand)</summary>
<img width="600" alt="Quick Setup" src="https://github.com/user-attachments/assets/20df9155-9095-420c-8387-908bd931bcfa">
</details>
1. **Open QodeAssist Settings**
2. **Select a Preset** - Choose from the Quick Setup dropdown:
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
- **OpenAI** (gpt-5.2-codex)
- **Mistral AI** (Codestral 2501)
- **Google AI** (Gemini 2.5 Flash)
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
### Manual Provider Configuration
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider (includes Responses API support)
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
- **[Anthropic Claude](docs/claude-configuration.md)** - Cloud provider (manual setup)
- **[OpenAI](docs/openai-configuration.md)** - Cloud provider (includes Responses API support)
- **[Mistral AI](docs/mistral-configuration.md)** - Cloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Cloud provider
- **LM Studio** - Local LLM provider
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
@ -360,7 +378,7 @@ See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](do
| Qt Creator Version | QodeAssist Version |
|-------------------|-------------------|
| 17.0.0+ | 0.6.0 - 0.x.x |
| 16.0.2 | 0.5.13 - 0.x.x |
| 16.0.2 | 0.5.13 - 0.9.6 |
| 16.0.1 | 0.5.7 - 0.5.13 |
| 16.0.0 | 0.5.2 - 0.5.6 |
| 15.0.1 | 0.4.8 - 0.5.1 |

View File

@ -236,7 +236,7 @@ public:
closeChatViewAction.setText(Tr::tr("Close QodeAssist Chat"));
closeChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
closeChatViewAction.addOnTriggered(this, [this] {
if (m_chatView->isVisible()) {
if (m_chatView && m_chatView->isActive() && m_chatView->isVisible()) {
m_chatView->close();
}
});
@ -250,8 +250,6 @@ public:
editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE);
editorContextMenu->addAction(showChatViewAction.command(),
Core::Constants::G_DEFAULT_THREE);
editorContextMenu->addAction(closeChatViewAction.command(),
Core::Constants::G_DEFAULT_THREE);
}
Chat::ChatFileManager::cleanupGlobalIntermediateStorage();

View File

@ -43,6 +43,106 @@ ConfigurationManager &ConfigurationManager::instance()
return instance;
}
QVector<AIConfiguration> ConfigurationManager::getPredefinedConfigurations(
ConfigurationType type)
{
QVector<AIConfiguration> presets;
AIConfiguration claudeOpus;
claudeOpus.id = "preset_claude_opus";
claudeOpus.name = "Claude Opus 4.5";
claudeOpus.provider = "Claude";
claudeOpus.model = "claude-opus-4-5-20251101";
claudeOpus.url = "https://api.anthropic.com";
claudeOpus.endpointMode = "Auto";
claudeOpus.customEndpoint = "";
claudeOpus.templateName = "Claude";
claudeOpus.type = type;
claudeOpus.isPredefined = true;
AIConfiguration claudeSonnet;
claudeSonnet.id = "preset_claude_sonnet";
claudeSonnet.name = "Claude Sonnet 4.5";
claudeSonnet.provider = "Claude";
claudeSonnet.model = "claude-sonnet-4-5-20250929";
claudeSonnet.url = "https://api.anthropic.com";
claudeSonnet.endpointMode = "Auto";
claudeSonnet.customEndpoint = "";
claudeSonnet.templateName = "Claude";
claudeSonnet.type = type;
claudeSonnet.isPredefined = true;
AIConfiguration claudeHaiku;
claudeHaiku.id = "preset_claude_haiku";
claudeHaiku.name = "Claude Haiku 4.5";
claudeHaiku.provider = "Claude";
claudeHaiku.model = "claude-haiku-4-5-20251001";
claudeHaiku.url = "https://api.anthropic.com";
claudeHaiku.endpointMode = "Auto";
claudeHaiku.customEndpoint = "";
claudeHaiku.templateName = "Claude";
claudeHaiku.type = type;
claudeHaiku.isPredefined = true;
AIConfiguration codestral;
codestral.id = "preset_codestral";
codestral.name = "Codestral";
codestral.provider = "Codestral";
codestral.model = "codestral-2501";
codestral.url = "https://codestral.mistral.ai";
codestral.endpointMode = "Auto";
codestral.customEndpoint = "";
codestral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
codestral.type = type;
codestral.isPredefined = true;
AIConfiguration mistral;
mistral.id = "preset_mistral";
mistral.name = "Mistral";
mistral.provider = "Mistral AI";
mistral.model = type == ConfigurationType::CodeCompletion ? "mistral-medium-latest" : "mistral-large-latest";
mistral.url = "https://api.mistral.ai";
mistral.endpointMode = "Auto";
mistral.customEndpoint = "";
mistral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
mistral.type = type;
mistral.isPredefined = true;
AIConfiguration geminiFlash;
geminiFlash.id = "preset_gemini_flash";
geminiFlash.name = "Gemini 2.5 Flash";
geminiFlash.provider = "Google AI";
geminiFlash.model = "gemini-2.5-flash";
geminiFlash.url = "https://generativelanguage.googleapis.com/v1beta";
geminiFlash.endpointMode = "Auto";
geminiFlash.customEndpoint = "";
geminiFlash.templateName = "Google AI";
geminiFlash.type = type;
geminiFlash.isPredefined = true;
AIConfiguration gpt52codex;
gpt52codex.id = "preset_gpt52codex";
gpt52codex.name = "gpt-5.2-codex";
gpt52codex.provider = "OpenAI Responses";
gpt52codex.model = "gpt-5.2-codex";
gpt52codex.url = "https://api.openai.com";
gpt52codex.endpointMode = "Auto";
gpt52codex.customEndpoint = "";
gpt52codex.templateName = "OpenAI Responses";
gpt52codex.type = type;
gpt52codex.isPredefined = true;
presets.append(claudeSonnet);
presets.append(claudeHaiku);
presets.append(claudeOpus);
presets.append(gpt52codex);
presets.append(codestral);
presets.append(mistral);
presets.append(geminiFlash);
return presets;
}
QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const
{
switch (type) {
@ -94,6 +194,9 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
configs->clear();
QVector<AIConfiguration> predefinedConfigs = getPredefinedConfigurations(type);
configs->append(predefinedConfigs);
if (!ensureDirectoryExists(type)) {
LOG_MESSAGE("Failed to create configuration directory");
return false;
@ -131,6 +234,7 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
config.customEndpoint = obj["customEndpoint"].toString();
config.type = type;
config.formatVersion = obj.value("formatVersion").toInt(1);
config.isPredefined = false;
if (config.id.isEmpty() || config.name.isEmpty()) {
LOG_MESSAGE(QString("Invalid configuration data in file: %1").arg(fileInfo.fileName()));
@ -185,6 +289,12 @@ bool ConfigurationManager::saveConfiguration(const AIConfiguration &config)
bool ConfigurationManager::deleteConfiguration(const QString &id, ConfigurationType type)
{
AIConfiguration config = getConfigurationById(id, type);
if (config.isPredefined) {
LOG_MESSAGE(QString("Cannot delete predefined configuration: %1").arg(id));
return false;
}
QDir dir(getConfigurationDirectory(type));
QStringList filters;
filters << QString("*_%1.json").arg(id);

View File

@ -41,6 +41,7 @@ struct AIConfiguration
QString customEndpoint;
ConfigurationType type;
int formatVersion = CONFIGURATION_FORMAT_VERSION;
bool isPredefined = false;
};
class ConfigurationManager : public QObject
@ -58,6 +59,8 @@ public:
AIConfiguration getConfigurationById(const QString &id, ConfigurationType type) const;
QString getConfigurationDirectory(ConfigurationType type) const;
static QVector<AIConfiguration> getPredefinedConfigurations(ConfigurationType type);
signals:
void configurationsChanged(ConfigurationType type);

View File

@ -23,6 +23,7 @@
#include <coreplugin/icore.h>
#include <utils/layoutbuilder.h>
#include <utils/utilsicons.h>
#include <QComboBox>
#include <QDesktopServices>
#include <QDir>
#include <QInputDialog>
@ -88,6 +89,27 @@ GeneralSettings::GeneralSettings()
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE;
ccPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
ccPresetConfig.setLabelText(Tr::tr("Quick Setup"));
loadPresetConfigurations(ccPresetConfig, ConfigurationType::CodeCompletion);
ccConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
ccConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
caPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
caPresetConfig.setLabelText(Tr::tr("Quick Setup"));
loadPresetConfigurations(caPresetConfig, ConfigurationType::Chat);
caConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
caConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
qrPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
qrPresetConfig.setLabelText(Tr::tr("Quick Setup"));
loadPresetConfigurations(qrPresetConfig, ConfigurationType::QuickRefactor);
qrConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
qrConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
initStringAspect(ccProvider, Constants::CC_PROVIDER, TrConstants::PROVIDER, "Ollama");
ccProvider.setReadOnly(true);
@ -127,6 +149,7 @@ GeneralSettings::GeneralSettings()
ccSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
ccLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
ccLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
ccOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
ccOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
ccOpenConfigFolder.m_isCompact = true;
@ -218,6 +241,7 @@ GeneralSettings::GeneralSettings()
caSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
caLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
caLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
caOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
caOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
caOpenConfigFolder.m_isCompact = true;
@ -262,6 +286,7 @@ GeneralSettings::GeneralSettings()
qrSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
qrLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
qrLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
qrOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
qrOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
qrOpenConfigFolder.m_isCompact = true;
@ -325,17 +350,24 @@ GeneralSettings::GeneralSettings()
title(TrConstants::CODE_COMPLETION),
Column{
Row{ccSaveConfig, ccLoadConfig, ccOpenConfigFolder, Stretch{1}},
Row{ccPresetConfig, ccConfigureApiKey, Stretch{1}},
ccGrid,
Row{specifyPreset1, preset1Language, Stretch{1}},
ccPreset1Grid}};
auto caGroup = Group{
title(TrConstants::CHAT_ASSISTANT),
Column{Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}}, caGrid}};
Column{
Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}},
Row{caPresetConfig, caConfigureApiKey, Stretch{1}},
caGrid}};
auto qrGroup = Group{
title(TrConstants::QUICK_REFACTOR),
Column{Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}}, qrGrid}};
Column{
Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}},
Row{qrPresetConfig, qrConfigureApiKey, Stretch{1}},
qrGrid}};
auto rootLayout = Column{
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}},
@ -570,6 +602,33 @@ void GeneralSettings::setupConnections()
connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() {
QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent());
});
connect(&ccPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
applyPresetConfiguration(ccPresetConfig.volatileValue(), ConfigurationType::CodeCompletion);
ccPresetConfig.setValue(0);
});
connect(&ccConfigureApiKey, &ButtonAspect::clicked, this, []() {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&caPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
applyPresetConfiguration(caPresetConfig.volatileValue(), ConfigurationType::Chat);
caPresetConfig.setValue(0);
});
connect(&caConfigureApiKey, &ButtonAspect::clicked, this, []() {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&qrPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
applyPresetConfiguration(qrPresetConfig.volatileValue(), ConfigurationType::QuickRefactor);
qrPresetConfig.setValue(0);
});
connect(&qrConfigureApiKey, &ButtonAspect::clicked, this, []() {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
updatePreset1Visiblity(specifyPreset1.volatileValue());
@ -776,11 +835,33 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
SettingsDialog dialog(TrConstants::LOAD_CONFIGURATION);
dialog.addLabel(TrConstants::SELECT_CONFIGURATION);
int predefinedCount = 0;
for (const AIConfiguration &config : configs) {
if (config.isPredefined) {
predefinedCount++;
}
}
if (predefinedCount > 0) {
auto *hintLabel = dialog.addLabel(
Tr::tr("[Preset] configurations are predefined cloud models ready to use."));
QFont hintFont = hintLabel->font();
hintFont.setItalic(true);
hintFont.setPointSize(hintFont.pointSize() - 1);
hintLabel->setFont(hintFont);
hintLabel->setStyleSheet("color: gray;");
}
dialog.addSpacing();
QStringList configNames;
for (const AIConfiguration &config : configs) {
configNames.append(config.name);
QString displayName = config.name;
if (config.isPredefined) {
displayName = QString("[Preset] %1").arg(config.name);
}
configNames.append(displayName);
}
auto configList = dialog.addComboBox(configNames, QString());
@ -790,9 +871,31 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
auto *okButton = new QPushButton(TrConstants::OK);
auto *cancelButton = new QPushButton(TrConstants::CANCEL);
auto updateDeleteButtonState = [&]() {
int currentIndex = configList->currentIndex();
if (currentIndex >= 0 && currentIndex < configs.size()) {
deleteButton->setEnabled(!configs[currentIndex].isPredefined);
}
};
connect(configList,
QOverload<int>::of(&QComboBox::currentIndexChanged),
updateDeleteButtonState);
updateDeleteButtonState();
connect(deleteButton, &QPushButton::clicked, &dialog, [&]() {
int currentIndex = configList->currentIndex();
if (currentIndex >= 0 && currentIndex < configs.size()) {
const AIConfiguration &configToDelete = configs[currentIndex];
if (configToDelete.isPredefined) {
QMessageBox::information(
&dialog,
TrConstants::DELETE_CONFIGURATION,
Tr::tr("Predefined configurations cannot be deleted."));
return;
}
QMessageBox::StandardButton reply = QMessageBox::question(
&dialog,
TrConstants::DELETE_CONFIGURATION,
@ -800,7 +903,6 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
const AIConfiguration &configToDelete = configs[currentIndex];
if (manager.deleteConfiguration(configToDelete.id, type)) {
dialog.accept();
onLoadConfiguration(prefix);
@ -860,6 +962,73 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
dialog.exec();
}
void GeneralSettings::loadPresetConfigurations(Utils::SelectionAspect &aspect,
ConfigurationType type)
{
QVector<AIConfiguration> presets = ConfigurationManager::getPredefinedConfigurations(type);
if (type == ConfigurationType::CodeCompletion) {
m_ccPresets = presets;
} else if (type == ConfigurationType::Chat) {
m_caPresets = presets;
} else if (type == ConfigurationType::QuickRefactor) {
m_qrPresets = presets;
}
aspect.addOption(Tr::tr("-- Select Preset --"));
for (const AIConfiguration &config : presets) {
aspect.addOption(config.name);
}
aspect.setDefaultValue(0);
}
void GeneralSettings::applyPresetConfiguration(int index, ConfigurationType type)
{
if (index <= 0) {
return;
}
QVector<AIConfiguration> *presets = nullptr;
if (type == ConfigurationType::CodeCompletion) {
presets = &m_ccPresets;
} else if (type == ConfigurationType::Chat) {
presets = &m_caPresets;
} else if (type == ConfigurationType::QuickRefactor) {
presets = &m_qrPresets;
}
if (!presets || index - 1 >= presets->size()) {
return;
}
const AIConfiguration &config = presets->at(index - 1);
if (type == ConfigurationType::CodeCompletion) {
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 (type == ConfigurationType::Chat) {
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 (type == ConfigurationType::QuickRefactor) {
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();
}
class GeneralSettingsPage : public Core::IOptionsPage
{
public:

View File

@ -23,6 +23,7 @@
#include <QPointer>
#include "ButtonAspect.hpp"
#include "ConfigurationManager.hpp"
namespace Utils {
class DetailsWidget;
@ -46,6 +47,9 @@ public:
ButtonAspect resetToDefaults{this};
// code completion setttings
Utils::SelectionAspect ccPresetConfig{this};
ButtonAspect ccConfigureApiKey{this};
Utils::StringAspect ccProvider{this};
ButtonAspect ccSelectProvider{this};
@ -91,6 +95,9 @@ public:
ButtonAspect ccPreset1SelectTemplate{this};
// chat assistant settings
Utils::SelectionAspect caPresetConfig{this};
ButtonAspect caConfigureApiKey{this};
Utils::StringAspect caProvider{this};
ButtonAspect caSelectProvider{this};
@ -116,6 +123,9 @@ public:
ButtonAspect caOpenConfigFolder{this};
// quick refactor settings
Utils::SelectionAspect qrPresetConfig{this};
ButtonAspect qrConfigureApiKey{this};
Utils::StringAspect qrProvider{this};
ButtonAspect qrSelectProvider{this};
@ -162,10 +172,17 @@ public:
void onSaveConfiguration(const QString &prefix);
void onLoadConfiguration(const QString &prefix);
void loadPresetConfigurations(Utils::SelectionAspect &aspect, ConfigurationType type);
void applyPresetConfiguration(int index, ConfigurationType type);
private:
void setupConnections();
void resetPageToDefaults();
QVector<AIConfiguration> m_ccPresets;
QVector<AIConfiguration> m_caPresets;
QVector<AIConfiguration> m_qrPresets;
};
GeneralSettings &generalSettings();

View File

@ -34,6 +34,7 @@
#include <QDialogButtonBox>
#include <QDir>
#include <QFontMetrics>
#include <QFrame>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
@ -72,15 +73,15 @@ static QIcon createThemedIcon(const QString &svgPath, const QColor &color)
painter.end();
QImage image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32);
uchar *bits = image.bits();
const int bytesPerPixel = 4;
const int totalBytes = image.width() * image.height() * bytesPerPixel;
const int newR = color.red();
const int newG = color.green();
const int newB = color.blue();
for (int i = 0; i < totalBytes; i += bytesPerPixel) {
int alpha = bits[i + 3];
if (alpha > 0) {
@ -100,11 +101,17 @@ QuickRefactorDialog::QuickRefactorDialog(QWidget *parent, const QString &lastIns
setWindowTitle(Tr::tr("Quick Refactor"));
setupUi();
if (!m_lastInstructions.isEmpty()) {
m_instructionEdit->setPlainText(m_lastInstructions);
m_instructionEdit->selectAll();
}
QTimer::singleShot(0, this, &QuickRefactorDialog::updateDialogSize);
m_textEdit->installEventFilter(this);
m_instructionEdit->installEventFilter(this);
m_commandsComboBox->installEventFilter(this);
updateDialogSize();
m_commandsComboBox->setFocus();
m_instructionEdit->setFocus();
}
void QuickRefactorDialog::setupUi()
@ -173,56 +180,65 @@ void QuickRefactorDialog::setupUi()
mainLayout->addLayout(actionsLayout);
QHBoxLayout *instructionsLayout = new QHBoxLayout();
instructionsLayout->setSpacing(4);
QLabel *instructionLabel = new QLabel(Tr::tr("Your Current Instruction:"), this);
mainLayout->addWidget(instructionLabel);
QLabel *instructionsLabel = new QLabel(Tr::tr("Custom Instructions:"), this);
instructionsLayout->addWidget(instructionsLabel);
m_instructionEdit = new QPlainTextEdit(this);
m_instructionEdit->setMinimumHeight(80);
m_instructionEdit->setPlaceholderText(Tr::tr("Type or edit your instruction..."));
mainLayout->addWidget(m_instructionEdit);
QHBoxLayout *savedInstructionsLayout = new QHBoxLayout();
savedInstructionsLayout->setSpacing(4);
QLabel *savedLabel = new QLabel(Tr::tr("Or Load saved:"), this);
savedInstructionsLayout->addWidget(savedLabel);
m_commandsComboBox = new QComboBox(this);
m_commandsComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_commandsComboBox->setEditable(true);
m_commandsComboBox->setInsertPolicy(QComboBox::NoInsert);
m_commandsComboBox->lineEdit()->setPlaceholderText("Search or select instruction...");
m_commandsComboBox->lineEdit()->setPlaceholderText(Tr::tr("Search saved instructions..."));
QCompleter *completer = new QCompleter(this);
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains);
m_commandsComboBox->setCompleter(completer);
instructionsLayout->addWidget(m_commandsComboBox);
savedInstructionsLayout->addWidget(m_commandsComboBox);
m_addCommandButton = new QToolButton(this);
m_addCommandButton->setText("+");
m_addCommandButton->setToolTip(Tr::tr("Add Custom Instruction"));
instructionsLayout->addWidget(m_addCommandButton);
m_addCommandButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_addCommandButton);
m_editCommandButton = new QToolButton(this);
m_editCommandButton->setText("");
m_editCommandButton->setToolTip(Tr::tr("Edit Custom Instruction"));
instructionsLayout->addWidget(m_editCommandButton);
m_editCommandButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_editCommandButton);
m_deleteCommandButton = new QToolButton(this);
m_deleteCommandButton->setText("");
m_deleteCommandButton->setToolTip(Tr::tr("Delete Custom Instruction"));
instructionsLayout->addWidget(m_deleteCommandButton);
m_deleteCommandButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_deleteCommandButton);
m_openFolderButton = new QToolButton(this);
m_openFolderButton->setText("📁");
m_openFolderButton->setToolTip(Tr::tr("Open Instructions Folder"));
instructionsLayout->addWidget(m_openFolderButton);
m_openFolderButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_openFolderButton);
mainLayout->addLayout(instructionsLayout);
mainLayout->addLayout(savedInstructionsLayout);
m_instructionsLabel = new QLabel(Tr::tr("Additional instructions (optional):"), this);
mainLayout->addWidget(m_instructionsLabel);
m_textEdit = new QPlainTextEdit(this);
m_textEdit->setMinimumHeight(100);
m_textEdit->setPlaceholderText(Tr::tr("Add extra details or modifications to the selected instruction..."));
connect(m_textEdit, &QPlainTextEdit::textChanged, this, &QuickRefactorDialog::updateDialogSize);
connect(
m_instructionEdit,
&QPlainTextEdit::textChanged,
this,
&QuickRefactorDialog::updateDialogSize);
connect(
m_commandsComboBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
@ -242,8 +258,6 @@ void QuickRefactorDialog::setupUi()
this,
&QuickRefactorDialog::onOpenInstructionsFolder);
mainLayout->addWidget(m_textEdit);
loadCustomCommands();
loadAvailableConfigurations();
@ -255,12 +269,23 @@ void QuickRefactorDialog::setupUi()
QDialogButtonBox *buttonBox
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QuickRefactorDialog::validateAndAccept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttonBox);
QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
QPushButton *cancelButton = buttonBox->button(QDialogButtonBox::Cancel);
setTabOrder(m_commandsComboBox, m_textEdit);
setTabOrder(m_textEdit, buttonBox);
if (okButton) {
okButton->installEventFilter(this);
}
if (cancelButton) {
cancelButton->installEventFilter(this);
}
setTabOrder(m_instructionEdit, m_commandsComboBox);
setTabOrder(m_commandsComboBox, okButton);
setTabOrder(okButton, cancelButton);
}
void QuickRefactorDialog::createActionButtons()
@ -295,27 +320,12 @@ void QuickRefactorDialog::createActionButtons()
QString QuickRefactorDialog::instructions() const
{
QString result;
CustomInstruction instruction = findCurrentInstruction();
if (!instruction.id.isEmpty()) {
result = instruction.body;
}
QString additionalText = m_textEdit->toPlainText().trimmed();
if (!additionalText.isEmpty()) {
if (!result.isEmpty()) {
result += "\n\n";
}
result += additionalText;
}
return result;
return m_instructionEdit->toPlainText().trimmed();
}
void QuickRefactorDialog::setInstructions(const QString &instructions)
{
m_textEdit->setPlainText(instructions);
m_instructionEdit->setPlainText(instructions);
}
QuickRefactorDialog::Action QuickRefactorDialog::selectedAction() const
@ -323,17 +333,33 @@ QuickRefactorDialog::Action QuickRefactorDialog::selectedAction() const
return m_selectedAction;
}
void QuickRefactorDialog::keyPressEvent(QKeyEvent *event)
{
QDialog::keyPressEvent(event);
}
bool QuickRefactorDialog::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_textEdit && event->type() == QEvent::KeyPress) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
if (keyEvent->modifiers() & Qt::ShiftModifier) {
return false;
}
accept();
return true;
if (watched == m_instructionEdit) {
if (keyEvent->key() == Qt::Key_Tab) {
m_commandsComboBox->setFocus();
return true;
}
}
if (watched == m_commandsComboBox || watched == m_commandsComboBox->lineEdit()) {
if (keyEvent->key() == Qt::Key_Tab) {
QPushButton *okButton = findChild<QPushButton *>();
if (okButton && okButton->text() == "OK") {
okButton->setFocus();
} else {
focusNextChild();
}
return true;
}
}
}
return QDialog::eventFilter(watched, event);
@ -343,8 +369,7 @@ void QuickRefactorDialog::useLastInstructions()
{
if (!m_lastInstructions.isEmpty()) {
m_commandsComboBox->setCurrentIndex(0);
m_commandsComboBox->clearEditText(); // Clear search text
m_textEdit->setPlainText(m_lastInstructions);
m_instructionEdit->setPlainText(m_lastInstructions);
m_selectedAction = Action::RepeatLast;
}
accept();
@ -353,10 +378,10 @@ void QuickRefactorDialog::useLastInstructions()
void QuickRefactorDialog::useImproveCodeTemplate()
{
m_commandsComboBox->setCurrentIndex(0);
m_commandsComboBox->clearEditText(); // Clear search text
m_textEdit->setPlainText(Tr::tr(
"Improve the selected code by enhancing readability, efficiency, and maintainability. "
"Follow best practices for C++/Qt and fix any potential issues."));
m_instructionEdit->setPlainText(
Tr::tr(
"Improve the selected code by enhancing readability, efficiency, and maintainability. "
"Follow best practices for C++/Qt and fix any potential issues."));
m_selectedAction = Action::ImproveCode;
accept();
}
@ -364,36 +389,29 @@ void QuickRefactorDialog::useImproveCodeTemplate()
void QuickRefactorDialog::useAlternativeSolutionTemplate()
{
m_commandsComboBox->setCurrentIndex(0);
m_commandsComboBox->clearEditText(); // Clear search text
m_textEdit->setPlainText(
Tr::tr("Suggest an alternative implementation approach for the selected code. "
"Provide a different solution that might be cleaner, more efficient, "
"or uses different Qt/C++ patterns or idioms."));
m_instructionEdit->setPlainText(
Tr::tr(
"Suggest an alternative implementation approach for the selected code. "
"Provide a different solution that might be cleaner, more efficient, "
"or uses different Qt/C++ patterns or idioms."));
m_selectedAction = Action::AlternativeSolution;
accept();
}
void QuickRefactorDialog::updateDialogSize()
{
QString text = m_textEdit->toPlainText();
QString text = m_instructionEdit->toPlainText();
QFontMetrics fm(m_textEdit->font());
QFontMetrics fm(m_instructionEdit->font());
QStringList lines = text.split('\n');
int lineCount = lines.size();
int lineCount = qMax(lines.size(), 3);
if (lineCount <= 1) {
int singleLineHeight = fm.height() + 10;
m_textEdit->setMinimumHeight(singleLineHeight);
m_textEdit->setMaximumHeight(singleLineHeight);
} else {
m_textEdit->setMaximumHeight(QWIDGETSIZE_MAX);
m_instructionEdit->setMaximumHeight(QWIDGETSIZE_MAX);
int lineHeight = fm.height() + 2;
int textEditHeight = qMin(qMax(lineCount, 2) * lineHeight, 20 * lineHeight);
m_textEdit->setMinimumHeight(textEditHeight);
}
int lineHeight = fm.height() + 2;
int textEditHeight = qMin(qMax(lineCount, 3) * lineHeight, 15 * lineHeight);
m_instructionEdit->setMinimumHeight(textEditHeight);
int maxWidth = 500;
for (const QString &line : lines) {
@ -405,14 +423,7 @@ void QuickRefactorDialog::updateDialogSize()
QRect screenGeometry = screen->availableGeometry();
int newWidth = qMin(maxWidth + 40, screenGeometry.width() * 3 / 4);
int newHeight;
if (lineCount <= 1) {
newHeight = 150;
} else {
newHeight = m_textEdit->minimumHeight() + 150;
}
newHeight = qMin(newHeight, screenGeometry.height() * 3 / 4);
int newHeight = qMin(m_instructionEdit->minimumHeight() + 200, screenGeometry.height() * 3 / 4);
resize(newWidth, newHeight);
}
@ -420,22 +431,16 @@ void QuickRefactorDialog::updateDialogSize()
void QuickRefactorDialog::loadCustomCommands()
{
m_commandsComboBox->clear();
m_commandsComboBox->addItem("", QString()); // Empty item for no selection
m_commandsComboBox->addItem("", QString());
auto &manager = CustomInstructionsManager::instance();
const QVector<CustomInstruction> &instructions = manager.instructions();
QStringList instructionNames;
int defaultInstructionIndex = -1;
for (int i = 0; i < instructions.size(); ++i) {
const CustomInstruction &instruction = instructions[i];
for (const CustomInstruction &instruction : instructions) {
m_commandsComboBox->addItem(instruction.name, instruction.id);
instructionNames.append(instruction.name);
if (instruction.isDefault) {
defaultInstructionIndex = i + 1;
}
}
if (m_commandsComboBox->completer()) {
@ -443,10 +448,6 @@ void QuickRefactorDialog::loadCustomCommands()
m_commandsComboBox->completer()->setModel(model);
}
if (defaultInstructionIndex > 0) {
m_commandsComboBox->setCurrentIndex(defaultInstructionIndex);
}
bool hasInstructions = !instructions.isEmpty();
m_editCommandButton->setEnabled(hasInstructions);
m_deleteCommandButton->setEnabled(hasInstructions);
@ -461,13 +462,13 @@ CustomInstruction QuickRefactorDialog::findCurrentInstruction() const
auto &manager = CustomInstructionsManager::instance();
const QVector<CustomInstruction> &instructions = manager.instructions();
for (const CustomInstruction &instruction : instructions) {
if (instruction.name == currentText) {
return instruction;
}
}
int currentIndex = m_commandsComboBox->currentIndex();
if (currentIndex > 0) {
QString instructionId = m_commandsComboBox->itemData(currentIndex).toString();
@ -475,13 +476,20 @@ CustomInstruction QuickRefactorDialog::findCurrentInstruction() const
return manager.getInstructionById(instructionId);
}
}
return CustomInstruction();
}
void QuickRefactorDialog::onCommandSelected(int index)
{
Q_UNUSED(index);
if (index <= 0) {
return;
}
CustomInstruction instruction = findCurrentInstruction();
if (!instruction.id.isEmpty()) {
m_instructionEdit->setPlainText(instruction.body);
}
}
void QuickRefactorDialog::onAddCustomCommand()
@ -493,10 +501,7 @@ void QuickRefactorDialog::onAddCustomCommand()
if (manager.saveInstruction(instruction)) {
loadCustomCommands();
m_commandsComboBox->setCurrentText(instruction.name);
m_textEdit->clear();
} else {
QMessageBox::warning(
this,
@ -509,10 +514,12 @@ void QuickRefactorDialog::onAddCustomCommand()
void QuickRefactorDialog::onEditCustomCommand()
{
CustomInstruction instruction = findCurrentInstruction();
if (instruction.id.isEmpty()) {
QMessageBox::information(
this, Tr::tr("No Instruction Selected"), Tr::tr("Please select an instruction to edit."));
this,
Tr::tr("No Instruction Selected"),
Tr::tr("Please select an instruction to edit."));
return;
}
@ -524,7 +531,6 @@ void QuickRefactorDialog::onEditCustomCommand()
if (manager.saveInstruction(updatedInstruction)) {
loadCustomCommands();
m_commandsComboBox->setCurrentText(updatedInstruction.name);
m_textEdit->clear();
} else {
QMessageBox::warning(
this,
@ -537,10 +543,12 @@ void QuickRefactorDialog::onEditCustomCommand()
void QuickRefactorDialog::onDeleteCustomCommand()
{
CustomInstruction instruction = findCurrentInstruction();
if (instruction.id.isEmpty()) {
QMessageBox::information(
this, Tr::tr("No Instruction Selected"), Tr::tr("Please select an instruction to delete."));
this,
Tr::tr("No Instruction Selected"),
Tr::tr("Please select an instruction to delete."));
return;
}
@ -569,12 +577,12 @@ void QuickRefactorDialog::onOpenInstructionsFolder()
{
QString path = QString("%1/qodeassist/quick_refactor/instructions")
.arg(Core::ICore::userResourcePath().toFSPathString());
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
@ -594,8 +602,8 @@ void QuickRefactorDialog::loadAvailableConfigurations()
auto &manager = Settings::ConfigurationManager::instance();
manager.loadConfigurations(Settings::ConfigurationType::QuickRefactor);
QVector<Settings::AIConfiguration> configs
= manager.configurations(Settings::ConfigurationType::QuickRefactor);
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::QuickRefactor);
m_configComboBox->clear();
m_configComboBox->addItem(Tr::tr("Current"), QString());
@ -640,4 +648,20 @@ void QuickRefactorDialog::onConfigurationChanged(int index)
}
}
void QuickRefactorDialog::validateAndAccept()
{
QString instruction = m_instructionEdit->toPlainText().trimmed();
if (instruction.isEmpty()) {
QMessageBox::warning(
this,
Tr::tr("No Instruction"),
Tr::tr("Please type an instruction or select a saved one."));
m_instructionEdit->setFocus();
return;
}
accept();
}
} // namespace QodeAssist

View File

@ -27,6 +27,8 @@ class QPlainTextEdit;
class QToolButton;
class QLabel;
class QComboBox;
class QLineEdit;
class QFrame;
namespace QodeAssist {
@ -49,6 +51,7 @@ public:
QString selectedConfiguration() const;
bool eventFilter(QObject *watched, QEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private slots:
void useLastInstructions();
@ -64,13 +67,14 @@ private slots:
void loadCustomCommands();
void loadAvailableConfigurations();
void onConfigurationChanged(int index);
void validateAndAccept();
private:
void setupUi();
void createActionButtons();
CustomInstruction findCurrentInstruction() const;
QPlainTextEdit *m_textEdit;
QPlainTextEdit *m_instructionEdit;
QToolButton *m_repeatButton;
QToolButton *m_improveButton;
QToolButton *m_alternativeButton;
@ -83,7 +87,6 @@ private:
QToolButton *m_thinkingButton;
QComboBox *m_commandsComboBox;
QComboBox *m_configComboBox;
QLabel *m_instructionsLabel;
Action m_selectedAction = Action::Custom;
QString m_lastInstructions;