diff --git a/ConfigurationManager.cpp b/ConfigurationManager.cpp index cde038a..77a8225 100644 --- a/ConfigurationManager.cpp +++ b/ConfigurationManager.cpp @@ -95,15 +95,24 @@ void ConfigurationManager::selectModel() auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel : m_generalSettings.caModel; - const auto modelList - = m_providersManager.getProviderByName(providerName)->getInstalledModels(providerUrl); + if (auto provider = m_providersManager.getProviderByName(providerName)) { + if (!provider->supportsModelListing()) { + m_generalSettings.showModelsNotSupportedDialog(targetSettings); + return; + } - QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() { - m_generalSettings.showSelectionDialog(modelList, - targetSettings, - Tr::tr("Select LLM Model"), - Tr::tr("Models:")); - }); + const auto modelList = provider->getInstalledModels(providerUrl); + + if (modelList.isEmpty()) { + m_generalSettings.showModelsNotFoundDialog(targetSettings); + return; + } + + QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() { + m_generalSettings.showSelectionDialog( + modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:")); + }); + } } void ConfigurationManager::selectTemplate() @@ -130,6 +139,10 @@ void ConfigurationManager::selectTemplate() void ConfigurationManager::selectUrl() { + auto *settingsButton = qobject_cast(sender()); + if (!settingsButton) + return; + QStringList urls; for (const auto &name : m_providersManager.providersNames()) { const auto url = m_providersManager.getProviderByName(name)->url(); @@ -137,19 +150,12 @@ void ConfigurationManager::selectUrl() urls.append(url); } - auto *settingsButton = qobject_cast(sender()); - if (!settingsButton) - return; - auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl : m_generalSettings.caUrl; QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() { - m_generalSettings.showSelectionDialog(urls, - targetSettings, - Tr::tr("Select URL"), - Tr::tr("URLs:")); + m_generalSettings.showUrlSelectionDialog(targetSettings, urls); }); } diff --git a/llmcore/Provider.hpp b/llmcore/Provider.hpp index 55f6da3..481725e 100644 --- a/llmcore/Provider.hpp +++ b/llmcore/Provider.hpp @@ -37,6 +37,7 @@ public: virtual QString url() const = 0; virtual QString completionEndpoint() const = 0; virtual QString chatEndpoint() const = 0; + virtual bool supportsModelListing() const = 0; virtual void prepareRequest(QJsonObject &request, RequestType type) = 0; virtual bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) = 0; diff --git a/providers/LMStudioProvider.cpp b/providers/LMStudioProvider.cpp index dde92c8..f04f6f8 100644 --- a/providers/LMStudioProvider.cpp +++ b/providers/LMStudioProvider.cpp @@ -53,6 +53,11 @@ QString LMStudioProvider::chatEndpoint() const return "/v1/chat/completions"; } +bool LMStudioProvider::supportsModelListing() const +{ + return true; +} + void LMStudioProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type) { auto prepareMessages = [](QJsonObject &req) -> QJsonArray { diff --git a/providers/LMStudioProvider.hpp b/providers/LMStudioProvider.hpp index a92a01f..bf29579 100644 --- a/providers/LMStudioProvider.hpp +++ b/providers/LMStudioProvider.hpp @@ -32,6 +32,7 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; + bool supportsModelListing() const override; void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override; bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; QList getInstalledModels(const QString &url) override; diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp index 65aa3ce..b852d61 100644 --- a/providers/OllamaProvider.cpp +++ b/providers/OllamaProvider.cpp @@ -53,6 +53,11 @@ QString OllamaProvider::chatEndpoint() const return "/api/chat"; } +bool OllamaProvider::supportsModelListing() const +{ + return true; +} + void OllamaProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type) { auto applySettings = [&request](const auto &settings) { diff --git a/providers/OllamaProvider.hpp b/providers/OllamaProvider.hpp index cd00e18..2fe20e0 100644 --- a/providers/OllamaProvider.hpp +++ b/providers/OllamaProvider.hpp @@ -32,6 +32,7 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; + bool supportsModelListing() const override; void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override; bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; QList getInstalledModels(const QString &url) override; diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp index 93e1a42..a8aaae6 100644 --- a/providers/OpenAICompatProvider.cpp +++ b/providers/OpenAICompatProvider.cpp @@ -50,6 +50,11 @@ QString OpenAICompatProvider::chatEndpoint() const return "/v1/chat/completions"; } +bool OpenAICompatProvider::supportsModelListing() const +{ + return false; +} + void OpenAICompatProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type) { auto prepareMessages = [](QJsonObject &req) -> QJsonArray { diff --git a/providers/OpenAICompatProvider.hpp b/providers/OpenAICompatProvider.hpp index ebe1c0a..0dfae36 100644 --- a/providers/OpenAICompatProvider.hpp +++ b/providers/OpenAICompatProvider.hpp @@ -32,6 +32,7 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; + bool supportsModelListing() const override; void prepareRequest(QJsonObject &request, LLMCore::RequestType type) override; bool handleResponse(QNetworkReply *reply, QString &accumulatedResponse) override; QList getInstalledModels(const QString &url) override; diff --git a/settings/CMakeLists.txt b/settings/CMakeLists.txt index cd4b281..d1be5a4 100644 --- a/settings/CMakeLists.txt +++ b/settings/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(QodeAssistSettings STATIC SettingsTr.hpp CodeCompletionSettings.hpp CodeCompletionSettings.cpp ChatAssistantSettings.hpp ChatAssistantSettings.cpp + SettingsDialog.hpp SettingsDialog.cpp ) target_link_libraries(QodeAssistSettings diff --git a/settings/GeneralSettings.cpp b/settings/GeneralSettings.cpp index efba2a3..f638597 100644 --- a/settings/GeneralSettings.cpp +++ b/settings/GeneralSettings.cpp @@ -19,15 +19,22 @@ #include "GeneralSettings.hpp" -#include -#include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include "Logger.hpp" #include "SettingsConstants.hpp" +#include "SettingsDialog.hpp" #include "SettingsTr.hpp" #include "SettingsUtils.hpp" @@ -109,16 +116,16 @@ GeneralSettings::GeneralSettings() auto ccGrid = Grid{}; ccGrid.addRow({ccProvider, ccSelectProvider}); + ccGrid.addRow({ccUrl, ccSetUrl}); ccGrid.addRow({ccModel, ccSelectModel}); ccGrid.addRow({ccTemplate, ccSelectTemplate}); - ccGrid.addRow({ccUrl, ccSetUrl}); ccGrid.addRow({ccStatus, ccTest}); auto caGrid = Grid{}; caGrid.addRow({caProvider, caSelectProvider}); + caGrid.addRow({caUrl, caSetUrl}); caGrid.addRow({caModel, caSelectModel}); caGrid.addRow({caTemplate, caSelectTemplate}); - caGrid.addRow({caUrl, caSetUrl}); caGrid.addRow({caStatus, caTest}); auto ccGroup = Group{title(TrConstants::CODE_COMPLETION), ccGrid}; @@ -161,6 +168,129 @@ void GeneralSettings::showSelectionDialog(const QStringList &data, } } +void GeneralSettings::showModelsNotFoundDialog(Utils::StringAspect &aspect) +{ + SettingsDialog dialog(TrConstants::CONNECTION_ERROR); + dialog.addLabel(TrConstants::NO_MODELS_FOUND); + dialog.addLabel(TrConstants::CHECK_CONNECTION); + dialog.addSpacing(); + + ButtonAspect *providerButton = nullptr; + ButtonAspect *urlButton = nullptr; + + if (&aspect == &ccModel) { + providerButton = &ccSelectProvider; + urlButton = &ccSetUrl; + } else if (&aspect == &caModel) { + providerButton = &caSelectProvider; + urlButton = &caSetUrl; + } + + if (providerButton && urlButton) { + auto selectProviderBtn = new QPushButton(TrConstants::SELECT_PROVIDER); + auto selectUrlBtn = new QPushButton(TrConstants::SELECT_URL); + auto enterManuallyBtn = new QPushButton(TrConstants::ENTER_MODEL_MANUALLY); + + connect(selectProviderBtn, &QPushButton::clicked, &dialog, [this, providerButton, &dialog]() { + dialog.close(); + emit providerButton->clicked(); + }); + + connect(selectUrlBtn, &QPushButton::clicked, &dialog, [this, urlButton, &dialog]() { + dialog.close(); + emit urlButton->clicked(); + }); + + connect(enterManuallyBtn, &QPushButton::clicked, &dialog, [this, &aspect, &dialog]() { + dialog.close(); + showModelsNotSupportedDialog(aspect); + }); + + dialog.buttonLayout()->addWidget(selectProviderBtn); + dialog.buttonLayout()->addWidget(selectUrlBtn); + dialog.buttonLayout()->addWidget(enterManuallyBtn); + } + + auto closeBtn = new QPushButton(TrConstants::CLOSE); + connect(closeBtn, &QPushButton::clicked, &dialog, &QDialog::close); + dialog.buttonLayout()->addWidget(closeBtn); + + dialog.exec(); +} + +void GeneralSettings::showModelsNotSupportedDialog(Utils::StringAspect &aspect) +{ + SettingsDialog dialog(TrConstants::MODEL_SELECTION); + dialog.addLabel(TrConstants::MODEL_LISTING_NOT_SUPPORTED_INFO); + dialog.addSpacing(); + + QString key = QString("CompleterHistory/") + .append( + (&aspect == &ccModel) ? Constants::CC_MODEL_HISTORY + : Constants::CA_MODEL_HISTORY); + QStringList historyList = qtcSettings()->value(Utils::Key(key.toLocal8Bit())).toStringList(); + + auto modelList = dialog.addComboBox(historyList, aspect.value()); + dialog.addSpacing(); + + auto okButton = new QPushButton(TrConstants::OK); + connect(okButton, &QPushButton::clicked, &dialog, [this, &aspect, modelList, &dialog]() { + QString value = modelList->currentText().trimmed(); + if (!value.isEmpty()) { + aspect.setValue(value); + writeSettings(); + dialog.accept(); + } + }); + + auto cancelButton = new QPushButton(TrConstants::CANCEL); + connect(cancelButton, &QPushButton::clicked, &dialog, &QDialog::reject); + + dialog.buttonLayout()->addWidget(cancelButton); + dialog.buttonLayout()->addWidget(okButton); + + modelList->setFocus(); + dialog.exec(); +} + +void GeneralSettings::showUrlSelectionDialog( + Utils::StringAspect &aspect, const QStringList &predefinedUrls) +{ + SettingsDialog dialog(TrConstants::URL_SELECTION); + dialog.addLabel(TrConstants::URL_SELECTION_INFO); + dialog.addSpacing(); + + QStringList allUrls = predefinedUrls; + QString key + = QString("CompleterHistory/") + .append((&aspect == &ccUrl) ? Constants::CC_URL_HISTORY : Constants::CA_URL_HISTORY); + QStringList historyList = qtcSettings()->value(Utils::Key(key.toLocal8Bit())).toStringList(); + allUrls.append(historyList); + allUrls.removeDuplicates(); + + auto urlList = dialog.addComboBox(allUrls, aspect.value()); + dialog.addSpacing(); + + auto okButton = new QPushButton(TrConstants::OK); + connect(okButton, &QPushButton::clicked, &dialog, [this, &aspect, urlList, &dialog]() { + QString value = urlList->currentText().trimmed(); + if (!value.isEmpty()) { + aspect.setValue(value); + writeSettings(); + dialog.accept(); + } + }); + + auto cancelButton = new QPushButton(TrConstants::CANCEL); + connect(cancelButton, &QPushButton::clicked, &dialog, &QDialog::reject); + + dialog.buttonLayout()->addWidget(cancelButton); + dialog.buttonLayout()->addWidget(okButton); + + urlList->setFocus(); + dialog.exec(); +} + void GeneralSettings::setupConnections() { connect(&enableLogging, &Utils::BoolAspect::volatileValueChanged, this, [this]() { diff --git a/settings/GeneralSettings.hpp b/settings/GeneralSettings.hpp index 9b0fc4f..a92dbe8 100644 --- a/settings/GeneralSettings.hpp +++ b/settings/GeneralSettings.hpp @@ -74,6 +74,12 @@ public: const QString &title = {}, const QString &text = {}); + void showModelsNotFoundDialog(Utils::StringAspect &aspect); + + void showModelsNotSupportedDialog(Utils::StringAspect &aspect); + + void showUrlSelectionDialog(Utils::StringAspect &aspect, const QStringList &predefinedUrls); + private: void setupConnections(); void resetPageToDefaults(); diff --git a/settings/SettingsDialog.cpp b/settings/SettingsDialog.cpp new file mode 100644 index 0000000..c1d7cea --- /dev/null +++ b/settings/SettingsDialog.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 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 "SettingsDialog.hpp" + +namespace QodeAssist::Settings { + +SettingsDialog::SettingsDialog(const QString &title, QWidget *parent) + : QDialog(parent) + , m_mainLayout(new QVBoxLayout(this)) + , m_buttonLayout(nullptr) +{ + setWindowTitle(title); + m_mainLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); +} + +QLabel *SettingsDialog::addLabel(const QString &text) +{ + auto label = new QLabel(text, this); + label->setWordWrap(true); + label->setMinimumWidth(300); + m_mainLayout->addWidget(label); + return label; +} + +QLineEdit *SettingsDialog::addInputField(const QString &labelText, const QString &value) +{ + auto inputLayout = new QGridLayout; + auto inputLabel = new QLabel(labelText, this); + auto inputField = new QLineEdit(value, this); + inputField->setMinimumWidth(200); + + inputLayout->addWidget(inputLabel, 0, 0); + inputLayout->addWidget(inputField, 0, 1); + inputLayout->setColumnStretch(1, 1); + m_mainLayout->addLayout(inputLayout); + + return inputField; +} + +void SettingsDialog::addSpacing(int space) +{ + m_mainLayout->addSpacing(space); +} + +QHBoxLayout *SettingsDialog::buttonLayout() +{ + if (!m_buttonLayout) { + m_buttonLayout = new QHBoxLayout; + m_buttonLayout->addStretch(); + m_mainLayout->addLayout(m_buttonLayout); + } + return m_buttonLayout; +} + +QComboBox *SettingsDialog::addComboBox( + const QStringList &items, const QString ¤tText, bool editable) +{ + auto comboBox = new QComboBox(this); + comboBox->addItems(items); + comboBox->setCurrentText(currentText); + comboBox->setMinimumWidth(300); + comboBox->setEditable(editable); + m_mainLayout->addWidget(comboBox); + return comboBox; +} + +QVBoxLayout *SettingsDialog::mainLayout() const +{ + return m_mainLayout; +} + +} // namespace QodeAssist::Settings diff --git a/settings/SettingsDialog.hpp b/settings/SettingsDialog.hpp new file mode 100644 index 0000000..014b28f --- /dev/null +++ b/settings/SettingsDialog.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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 +#include +#include +#include +#include +#include + +#include + +namespace QodeAssist::Settings { + +class SettingsDialog : public QDialog +{ +public: + explicit SettingsDialog(const QString &title, QWidget *parent = Core::ICore::dialogParent()); + + QLabel *addLabel(const QString &text); + QLineEdit *addInputField(const QString &labelText, const QString &value); + void addSpacing(int space = 12); + QHBoxLayout *buttonLayout(); + QVBoxLayout *mainLayout() const; + + QComboBox *addComboBox( + const QStringList &items, const QString ¤tText = QString(), bool editable = true); + +private: + QVBoxLayout *m_mainLayout; + QHBoxLayout *m_buttonLayout; +}; + +} // namespace QodeAssist::Settings diff --git a/settings/SettingsTr.hpp b/settings/SettingsTr.hpp index a47b279..1cf84ed 100644 --- a/settings/SettingsTr.hpp +++ b/settings/SettingsTr.hpp @@ -42,6 +42,44 @@ inline const char *RESET_SETTINGS = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Reset inline const char *CONFIRMATION = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Are you sure you want to reset all settings to default values?"); + +inline const char CONNECTION_ERROR[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Connection Error"); +inline const char NO_MODELS_FOUND[] + = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Unable to retrieve the list of models from the server."); +inline const char CHECK_CONNECTION[] = QT_TRANSLATE_NOOP( + "QtC::QodeAssist", + "Please verify the following:\n" + "- Server is running and accessible\n" + "- URL is correct\n" + "- Provider is properly configured\n\n" + "You can try selecting a different provider or changing the URL:"); +inline const char SELECT_PROVIDER[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Select Provider"); +inline const char SELECT_URL[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Select URL"); +inline const char CLOSE[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Close"); +inline const char MODEL_SELECTION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Model Selection"); +inline const char MODEL_LISTING_NOT_SUPPORTED_INFO[] = QT_TRANSLATE_NOOP( + "QtC::QodeAssist", + "Select from previously used models or enter a new model name.\n\n" + "If entering a new model name:\n" + "• For providers with automatic listing - ensure the model is installed\n" + "• For providers without listing support - check provider's documentation\n" + "• Make sure the model name matches exactly"); +inline const char MODEL_NAME[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Model name:"); +inline const char OK[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "OK"); +inline const char CANCEL[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Cancel"); +inline const char ENTER_MODEL_MANUALLY[] + = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter Model Manually"); +inline const char URL_SELECTION[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "URL Selection"); +inline const char URL_SELECTION_INFO[] = QT_TRANSLATE_NOOP( + "QtC::QodeAssist", + "Select from the list of default and previously used URLs, or enter a custom one.\n" + "Please ensure the selected URL is accessible and the service is running."); +inline const char PREDEFINED_URL[] + = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Use default provider URL or from history"); +inline const char CUSTOM_URL[] = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter custom URL"); +inline const char ENTER_MODEL_MANUALLY_BUTTON[] + = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enter Model Name Manually"); + } // namespace TrConstants struct Tr