Compare commits

...

26 Commits

Author SHA1 Message Date
31145f191b chore: Upgrade plugin to 0.4.9
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-01-24 18:27:07 +01:00
9096adde6f fix: Remove installing plugin from update dialog 2025-01-24 18:23:11 +01:00
b8e578d2d7 chore: Update plugin to QtCreator 15.0.1
* fix: Additional check qtc version
* build: Upgrade plugin to QtC 15.0.1
* chore: Upgrade plugin version to 0.4.8
2025-01-24 13:29:44 +01:00
4e45774bce chore: Upgrade version to 0.4.7 2025-01-24 01:52:09 +01:00
928490d31f fix: small style changes 2025-01-24 01:50:56 +01:00
97163cf6c9 fix: Add calculate tokens after clean chat 2025-01-24 01:08:30 +01:00
f85c162692 fix: Improve scroll bar style 2025-01-24 00:59:26 +01:00
258053d826 feat: Add chat file name in top bar 2025-01-24 00:52:10 +01:00
bf63ae5714 refactor: Improve systemPrompt for code completion 2025-01-24 00:37:52 +01:00
ae76850e78 fix: Buttons order in urls dialog on general page 2025-01-24 00:29:15 +01:00
bf3c0b3aa0 feat: Add auto sync open files with model context 2025-01-24 00:22:44 +01:00
9add61c805 feat: Add possibility to link files to the current system prompt
- Add linking files to chat
- Rework tokens counting
2025-01-23 10:17:38 +01:00
add86d2e67 chore: Upgrade version to 0.4.6 2025-01-21 15:05:48 +01:00
a6c909d34d exp: Add ubuntu 22.04 experimental builds 2025-01-21 14:58:44 +01:00
2814dec3e5 fix: Improve file attachment handling
- Add files to existing list instead of replacing when using attach dialog
- Prevent duplicate files from being added to attachment list
2025-01-21 11:33:13 +01:00
1b86b60de8 Add system prompt configuration to readme 2025-01-20 10:00:36 +01:00
4b7f638731 Add setup OpenAI provider 2025-01-19 20:28:06 +01:00
de046f0529 chore: Bump version to 0.4.5 2025-01-19 17:44:11 +01:00
e975e143b1 fix: Handling Ollama messages 2025-01-19 17:32:12 +01:00
c97c0f62e8 fix: Handling full input message from OpenAI compatible providers 2025-01-19 01:16:33 +01:00
61fded34ea feat: add OpenAI provider settings 2025-01-19 00:50:23 +01:00
289a19ac1a feat: Add OpenAI provider and template 2025-01-17 01:22:12 +01:00
43ac662671 fix: Text width in chat item 2025-01-17 00:46:11 +01:00
1d64d2afc9 refactor: Move to using colors from QtC theme palette 2025-01-15 00:05:12 +01:00
9db61119aa feat: Add check plugin update and dialog for update 2025-01-13 20:11:27 +01:00
70481b3116 Remove discord link from README.md 2025-01-08 16:03:00 +01:00
52 changed files with 1719 additions and 359 deletions

View File

@ -13,8 +13,8 @@ on:
env:
PLUGIN_NAME: QodeAssist
QT_VERSION: 6.8.1
QT_CREATOR_VERSION: 15.0.0
QT_CREATOR_VERSION_INTERNAL: 15.0.0
QT_CREATOR_VERSION: 15.0.1
QT_CREATOR_VERSION_INTERNAL: 15.0.1
MACOS_DEPLOYMENT_TARGET: "11.0"
CMAKE_VERSION: "3.29.6"
NINJA_VERSION: "1.12.1"
@ -41,6 +41,12 @@ jobs:
platform: linux_x64,
cc: "gcc", cxx: "g++"
}
- {
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64(Ubuntu-22.04-experimental)",
os: ubuntu-22.04,
platform: linux_x64,
cc: "gcc", cxx: "g++"
}
- {
name: "macOS Latest Clang", artifact: "macOS-universal",
os: macos-latest,

View File

@ -53,13 +53,15 @@ add_qtc_plugin(QodeAssist
templates/ChatML.hpp
templates/Alpaca.hpp
templates/Llama2.hpp
providers/Providers.hpp
templates/Claude.hpp
templates/OpenAI.hpp
providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
QodeAssist.qrc
LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp
@ -68,4 +70,5 @@ add_qtc_plugin(QodeAssist
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
)

View File

@ -18,9 +18,12 @@ qt_add_qml_module(QodeAssistChatView
qml/parts/BottomBar.qml
qml/parts/AttachedFilesPlace.qml
RESOURCES
icons/attach-file.svg
icons/attach-file-light.svg
icons/attach-file-dark.svg
icons/close-dark.svg
icons/close-light.svg
icons/link-file-light.svg
icons/link-file-dark.svg
SOURCES
ChatWidget.hpp ChatWidget.cpp
ChatModel.hpp ChatModel.cpp

View File

@ -28,7 +28,6 @@ namespace QodeAssist::Chat {
ChatModel::ChatModel(QObject *parent)
: QAbstractListModel(parent)
, m_totalTokens(0)
{
auto &settings = Settings::chatAssistantSettings();
@ -90,26 +89,19 @@ void ChatModel::addMessage(
.arg(attachment.filename, attachment.content);
}
}
int tokenCount = estimateTokenCount(fullContent);
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
Message &lastMessage = m_messages.last();
int oldTokenCount = lastMessage.tokenCount;
lastMessage.content = content;
lastMessage.attachments = attachments;
lastMessage.tokenCount = tokenCount;
m_totalTokens += (tokenCount - oldTokenCount);
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{role, content, tokenCount, id};
Message newMessage{role, content, id};
newMessage.attachments = attachments;
m_messages.append(newMessage);
m_totalTokens += tokenCount;
endInsertRows();
}
emit totalTokensChanged();
}
QVector<ChatModel::Message> ChatModel::getChatHistory() const
@ -117,18 +109,11 @@ QVector<ChatModel::Message> ChatModel::getChatHistory() const
return m_messages;
}
int ChatModel::estimateTokenCount(const QString &text) const
{
return text.length() / 4;
}
void ChatModel::clear()
{
beginResetModel();
m_messages.clear();
m_totalTokens = 0;
endResetModel();
emit totalTokensChanged();
emit modelReseted();
}
@ -199,11 +184,6 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
return messages;
}
int ChatModel::totalTokens() const
{
return m_totalTokens;
}
int ChatModel::tokensThreshold() const
{
auto &settings = Settings::chatAssistantSettings();

View File

@ -33,7 +33,6 @@ namespace QodeAssist::Chat {
class ChatModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int totalTokens READ totalTokens NOTIFY totalTokensChanged FINAL)
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
QML_ELEMENT
@ -47,7 +46,6 @@ public:
{
ChatRole role;
QString content;
int tokenCount;
QString id;
QList<Context::ContentFile> attachments;
@ -70,22 +68,17 @@ public:
QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
int totalTokens() const;
int tokensThreshold() const;
QString currentModel() const;
QString lastMessageId() const;
signals:
void totalTokensChanged();
void tokensThresholdChanged();
void modelReseted();
private:
int estimateTokenCount(const QString &text) const;
QVector<Message> m_messages;
int m_totalTokens = 0;
};
} // namespace QodeAssist::Chat

View File

@ -29,12 +29,15 @@
#include <projectexplorer/projectmanager.h>
#include <utils/theme/theme.h>
#include <utils/utilsicons.h>
#include <coreplugin/editormanager/editormanager.h>
#include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProjectSettings.hpp"
#include "context/TokenUtils.hpp"
#include "context/ContextManager.hpp"
namespace QodeAssist::Chat {
@ -43,6 +46,13 @@ ChatRootView::ChatRootView(QQuickItem *parent)
, m_chatModel(new ChatModel(this))
, m_clientInterface(new ClientInterface(m_chatModel, this))
{
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect(&Settings::chatAssistantSettings().linkOpenFiles, &Utils::BaseAspect::changed,
this,
[this](){
setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles());
});
auto &settings = Settings::generalSettings();
connect(&settings.caModel,
@ -50,20 +60,33 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this,
&ChatRootView::currentTemplateChanged);
connect(&Settings::chatAssistantSettings().sharingCurrentFile,
&Utils::BaseAspect::changed,
this,
&ChatRootView::isSharingCurrentFileChanged);
connect(
m_clientInterface,
&ClientInterface::messageReceivedCompletely,
this,
&ChatRootView::autosave);
connect(m_chatModel, &ChatModel::modelReseted, [this]() { m_recentFilePath = QString(); });
connect(
m_clientInterface,
&ClientInterface::messageReceivedCompletely,
this,
&ChatRootView::updateInputTokensCount);
generateColors();
connect(m_chatModel, &ChatModel::modelReseted, [this]() { setRecentFilePath(QString{}); });
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
connect(&Settings::chatAssistantSettings().useSystemPrompt, &Utils::BaseAspect::changed,
this, &ChatRootView::updateInputTokensCount);
connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
this, &ChatRootView::updateInputTokensCount);
auto editors = Core::EditorManager::instance();
connect(editors, &Core::EditorManager::editorOpened, this, &ChatRootView::onEditorOpened);
connect(editors, &Core::EditorManager::editorAboutToClose, this, &ChatRootView::onEditorAboutToClose);
connect(editors, &Core::EditorManager::editorsClosed, this, &ChatRootView::onEditorsClosed);
updateInputTokensCount();
}
ChatModel *ChatRootView::chatModel() const
@ -71,14 +94,9 @@ ChatModel *ChatRootView::chatModel() const
return m_chatModel;
}
QColor ChatRootView::backgroundColor() const
void ChatRootView::sendMessage(const QString &message)
{
return Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
}
void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile)
{
if (m_chatModel->totalTokens() > m_chatModel->tokensThreshold()) {
if (m_inputTokensCount > m_chatModel->tokensThreshold()) {
QMessageBox::StandardButton reply = QMessageBox::question(
Core::ICore::dialogParent(),
tr("Token Limit Exceeded"),
@ -89,12 +107,12 @@ void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile)
if (reply == QMessageBox::Yes) {
autosave();
m_chatModel->clear();
m_recentFilePath = QString{};
setRecentFilePath(QString{});
return;
}
}
m_clientInterface->sendMessage(message, m_attachmentFiles, sharingCurrentFile);
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
clearAttachmentFiles();
}
@ -116,49 +134,14 @@ void ChatRootView::clearAttachmentFiles()
}
}
void ChatRootView::generateColors()
void ChatRootView::clearLinkedFiles()
{
QColor baseColor = backgroundColor();
bool isDarkTheme = baseColor.lightness() < 128;
if (isDarkTheme) {
m_primaryColor = generateColor(baseColor, 0.1, 1.2, 1.4);
m_secondaryColor = generateColor(baseColor, -0.1, 1.1, 1.2);
m_codeColor = generateColor(baseColor, 0.05, 0.8, 1.1);
} else {
m_primaryColor = generateColor(baseColor, 0.05, 1.05, 1.1);
m_secondaryColor = generateColor(baseColor, -0.05, 1.1, 1.2);
m_codeColor = generateColor(baseColor, 0.02, 0.95, 1.05);
if (!m_linkedFiles.isEmpty()) {
m_linkedFiles.clear();
emit linkedFilesChanged();
}
}
QColor ChatRootView::generateColor(const QColor &baseColor,
float hueShift,
float saturationMod,
float lightnessMod)
{
float h, s, l, a;
baseColor.getHslF(&h, &s, &l, &a);
bool isDarkTheme = l < 0.5;
h = fmod(h + hueShift + 1.0, 1.0);
s = qBound(0.0f, s * saturationMod, 1.0f);
if (isDarkTheme) {
l = qBound(0.0f, l * lightnessMod, 1.0f);
} else {
l = qBound(0.0f, l / lightnessMod, 1.0f);
}
h = qBound(0.0f, h, 1.0f);
s = qBound(0.0f, s, 1.0f);
l = qBound(0.0f, l, 1.0f);
a = qBound(0.0f, a, 1.0f);
return QColor::fromHslF(h, s, l, a);
}
QString ChatRootView::getChatsHistoryDir() const
{
QString path;
@ -185,26 +168,6 @@ QString ChatRootView::currentTemplate() const
return settings.caModel();
}
QColor ChatRootView::primaryColor() const
{
return m_primaryColor;
}
QColor ChatRootView::secondaryColor() const
{
return m_secondaryColor;
}
QColor ChatRootView::codeColor() const
{
return m_codeColor;
}
bool ChatRootView::isSharingCurrentFile() const
{
return Settings::chatAssistantSettings().sharingCurrentFile();
}
void ChatRootView::saveHistory(const QString &filePath)
{
auto result = ChatSerializer::saveToFile(m_chatModel, filePath);
@ -219,8 +182,9 @@ void ChatRootView::loadHistory(const QString &filePath)
if (!result.success) {
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
} else {
m_recentFilePath = filePath;
setRecentFilePath(filePath);
}
updateInputTokensCount();
}
void ChatRootView::showSaveDialog()
@ -301,7 +265,7 @@ void ChatRootView::autosave()
QString filePath = getAutosaveFilePath();
if (!filePath.isEmpty()) {
ChatSerializer::saveToFile(m_chatModel, filePath);
m_recentFilePath = filePath;
setRecentFilePath(filePath);
}
}
@ -319,6 +283,16 @@ QString ChatRootView::getAutosaveFilePath() const
return QDir(dir).filePath(getSuggestedFileName() + ".json");
}
QStringList ChatRootView::attachmentFiles() const
{
return m_attachmentFiles;
}
QStringList ChatRootView::linkedFiles() const
{
return m_linkedFiles;
}
void ChatRootView::showAttachFilesDialog()
{
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
@ -329,12 +303,161 @@ void ChatRootView::showAttachFilesDialog()
}
if (dialog.exec() == QDialog::Accepted) {
QStringList filePaths = dialog.selectedFiles();
if (!filePaths.isEmpty()) {
m_attachmentFiles = filePaths;
emit attachmentFilesChanged();
QStringList newFilePaths = dialog.selectedFiles();
if (!newFilePaths.isEmpty()) {
bool filesAdded = false;
for (const QString &filePath : newFilePaths) {
if (!m_attachmentFiles.contains(filePath)) {
m_attachmentFiles.append(filePath);
filesAdded = true;
}
}
if (filesAdded) {
emit attachmentFilesChanged();
}
}
}
}
void ChatRootView::removeFileFromAttachList(int index)
{
if (index >= 0 && index < m_attachmentFiles.size()) {
m_attachmentFiles.removeAt(index);
emit attachmentFilesChanged();
}
}
void ChatRootView::showLinkFilesDialog()
{
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toString());
}
if (dialog.exec() == QDialog::Accepted) {
QStringList newFilePaths = dialog.selectedFiles();
if (!newFilePaths.isEmpty()) {
bool filesAdded = false;
for (const QString &filePath : newFilePaths) {
if (!m_linkedFiles.contains(filePath)) {
m_linkedFiles.append(filePath);
filesAdded = true;
}
}
if (filesAdded) {
emit linkedFilesChanged();
}
}
}
}
void ChatRootView::removeFileFromLinkList(int index)
{
if (index >= 0 && index < m_linkedFiles.size()) {
m_linkedFiles.removeAt(index);
emit linkedFilesChanged();
}
}
void ChatRootView::calculateMessageTokensCount(const QString &message)
{
m_messageTokensCount = Context::TokenUtils::estimateTokens(message);
updateInputTokensCount();
}
void ChatRootView::setIsSyncOpenFiles(bool state)
{
if (m_isSyncOpenFiles != state) {
m_isSyncOpenFiles = state;
emit isSyncOpenFilesChanged();
}
}
void ChatRootView::updateInputTokensCount()
{
int inputTokens = m_messageTokensCount;
auto& settings = Settings::chatAssistantSettings();
if (settings.useSystemPrompt()) {
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
}
if (!m_attachmentFiles.isEmpty()) {
auto attachFiles = Context::ContextManager::instance().getContentFiles(m_attachmentFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
}
if (!m_linkedFiles.isEmpty()) {
auto linkFiles = Context::ContextManager::instance().getContentFiles(m_linkedFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
}
const auto& history = m_chatModel->getChatHistory();
for (const auto& message : history) {
inputTokens += Context::TokenUtils::estimateTokens(message.content);
inputTokens += 4; // + role
}
m_inputTokensCount = inputTokens;
emit inputTokensCountChanged();
}
int ChatRootView::inputTokensCount() const
{
return m_inputTokensCount;
}
bool ChatRootView::isSyncOpenFiles() const
{
return m_isSyncOpenFiles;
}
void ChatRootView::onEditorOpened(Core::IEditor *editor)
{
if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toString();
if (!m_linkedFiles.contains(filePath)) {
m_linkedFiles.append(filePath);
emit linkedFilesChanged();
}
}
}
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
{
if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toString();
m_linkedFiles.removeOne(filePath);
emit linkedFilesChanged();
}
}
void ChatRootView::onEditorsClosed(QList<Core::IEditor *> editors)
{
if (isSyncOpenFiles()) {
for (Core::IEditor *editor : editors) {
if (auto document = editor->document()) {
QString filePath = document->filePath().toString();
m_linkedFiles.removeOne(filePath);
}
}
emit linkedFilesChanged();
}
}
QString ChatRootView::chatFileName() const
{
return QFileInfo(m_recentFilePath).baseName();
}
void ChatRootView::setRecentFilePath(const QString &filePath)
{
if (m_recentFilePath != filePath) {
m_recentFilePath = filePath;
emit chatFileNameChanged();
}
}
} // namespace QodeAssist::Chat

View File

@ -23,6 +23,7 @@
#include "ChatModel.hpp"
#include "ClientInterface.hpp"
#include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Chat {
@ -31,13 +32,11 @@ class ChatRootView : public QQuickItem
Q_OBJECT
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
Q_PROPERTY(QColor backgroundColor READ backgroundColor CONSTANT FINAL)
Q_PROPERTY(QColor primaryColor READ primaryColor CONSTANT FINAL)
Q_PROPERTY(QColor secondaryColor READ secondaryColor CONSTANT FINAL)
Q_PROPERTY(QColor codeColor READ codeColor CONSTANT FINAL)
Q_PROPERTY(bool isSharingCurrentFile READ isSharingCurrentFile NOTIFY
isSharingCurrentFileChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles MEMBER m_attachmentFiles NOTIFY attachmentFilesChanged)
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL)
QML_ELEMENT
@ -47,14 +46,6 @@ public:
ChatModel *chatModel() const;
QString currentTemplate() const;
QColor backgroundColor() const;
QColor primaryColor() const;
QColor secondaryColor() const;
QColor codeColor() const;
bool isSharingCurrentFile() const;
void saveHistory(const QString &filePath);
void loadHistory(const QString &filePath);
@ -64,38 +55,57 @@ public:
void autosave();
QString getAutosaveFilePath() const;
QStringList attachmentFiles() const;
QStringList linkedFiles() const;
Q_INVOKABLE void showAttachFilesDialog();
Q_INVOKABLE void removeFileFromAttachList(int index);
Q_INVOKABLE void showLinkFilesDialog();
Q_INVOKABLE void removeFileFromLinkList(int index);
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const;
bool isSyncOpenFiles() const;
void onEditorOpened(Core::IEditor *editor);
void onEditorAboutToClose(Core::IEditor *editor);
void onEditorsClosed(QList<Core::IEditor *> editors);
QString chatFileName() const;
void setRecentFilePath(const QString &filePath);
public slots:
void sendMessage(const QString &message, bool sharingCurrentFile = false);
void sendMessage(const QString &message);
void copyToClipboard(const QString &text);
void cancelRequest();
void clearAttachmentFiles();
void clearLinkedFiles();
signals:
void chatModelChanged();
void currentTemplateChanged();
void isSharingCurrentFileChanged();
void attachmentFilesChanged();
void linkedFilesChanged();
void inputTokensCountChanged();
void isSyncOpenFilesChanged();
void chatFileNameChanged();
private:
void generateColors();
QColor generateColor(const QColor &baseColor,
float hueShift,
float saturationMod,
float lightnessMod);
QString getChatsHistoryDir() const;
QString getSuggestedFileName() const;
ChatModel *m_chatModel;
ClientInterface *m_clientInterface;
QString m_currentTemplate;
QColor m_primaryColor;
QColor m_secondaryColor;
QColor m_codeColor;
QString m_recentFilePath;
QStringList m_attachmentFiles;
QStringList m_linkedFiles;
int m_messageTokensCount{0};
int m_inputTokensCount{0};
bool m_isSyncOpenFiles;
};
} // namespace QodeAssist::Chat

View File

@ -82,7 +82,6 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["tokenCount"] = message.tokenCount;
messageObj["id"] = message.id;
return messageObj;
}
@ -92,7 +91,6 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString();
message.tokenCount = json["tokenCount"].toInt();
message.id = json["id"].toString();
return message;
}
@ -107,7 +105,6 @@ QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
QJsonObject root;
root["version"] = VERSION;
root["messages"] = messagesArray;
root["totalTokens"] = model->totalTokens();
return root;
}

View File

@ -66,7 +66,7 @@ ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
ClientInterface::~ClientInterface() = default;
void ClientInterface::sendMessage(
const QString &message, const QList<QString> &attachments, bool includeCurrentFile)
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
{
cancelRequest();
@ -100,11 +100,8 @@ void ClientInterface::sendMessage(
if (chatAssistantSettings.useSystemPrompt())
systemPrompt = chatAssistantSettings.systemPrompt();
if (includeCurrentFile) {
QString fileContext = getCurrentFileContext();
if (!fileContext.isEmpty()) {
systemPrompt = systemPrompt.append(fileContext);
}
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
QJsonObject providerRequest;
@ -198,4 +195,21 @@ QString ClientInterface::getCurrentFileContext() const
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
}
QString ClientInterface::getSystemPromptWithLinkedFiles(const QString &basePrompt, const QList<QString> &linkedFiles) const
{
QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n")
.arg(file.filename, file.content);
}
}
return updatedPrompt;
}
} // namespace QodeAssist::Chat

View File

@ -39,7 +39,7 @@ public:
void sendMessage(
const QString &message,
const QList<QString> &attachments = {},
bool includeCurrentFile = false);
const QList<QString> &linkedFiles = {});
void clearMessages();
void cancelRequest();
@ -50,6 +50,9 @@ signals:
private:
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles(
const QString &basePrompt,
const QList<QString> &linkedFiles) const;
LLMCore::RequestHandler *m_requestHandler;
ChatModel *m_chatModel;

View File

@ -1,6 +1,7 @@
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_37_14)">
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_37_14">

Before

Width:  |  Height:  |  Size: 555 B

After

Width:  |  Height:  |  Size: 869 B

View File

@ -0,0 +1,11 @@
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51_20)">
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_51_20">
<rect width="24" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 869 B

View File

@ -0,0 +1,12 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_49_24)">
<path d="M10 12L10 32L10 12Z" fill="black"/>
<path d="M10 12L10 32" stroke="black" stroke-width="3"/>
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="black" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_49_24">
<rect width="20" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 513 B

View File

@ -0,0 +1,12 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51_24)">
<path d="M10 12L10 32Z" fill="white"/>
<path d="M10 12L10 32" stroke="white" stroke-width="3"/>
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="white" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_51_24">
<rect width="20" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@ -27,9 +27,6 @@ Rectangle {
property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.model
property color fontColor
property color codeBgColor
property color selectionColor
height: msgColumn.implicitHeight + 10
radius: 8
@ -49,7 +46,7 @@ Rectangle {
// why does `required property MessagePart modelData` not work?
required property var modelData
width: parent.width
Layout.preferredWidth: root.width
sourceComponent: {
// If `required property MessagePart modelData` is used
// and conversion to MessagePart fails, you're left
@ -100,14 +97,16 @@ Rectangle {
height: attachText.implicitHeight + 8
width: attachText.implicitWidth + 16
radius: 4
color: root.codeBgColor
color: palette.button
border.width: 1
border.color: palette.mid
Text {
id: attachText
anchors.centerIn: parent
text: modelData
color: root.fontColor
color: palette.text
}
}
}
@ -120,8 +119,6 @@ Rectangle {
verticalAlignment: Text.AlignVCenter
leftPadding: 10
text: itemData.text
color: root.fontColor
selectionColor: root.selectionColor
}
@ -136,8 +133,5 @@ Rectangle {
code: itemData.text
language: itemData.language
color: root.codeBgColor
selectionColor: root.selectionColor
}
}

View File

@ -58,6 +58,7 @@ ChatRootView {
ColumnLayout {
anchors.fill: parent
spacing: 0
TopBar {
id: topBar
@ -69,7 +70,10 @@ ChatRootView {
loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat()
tokensBadge {
text: qsTr("tokens:%1/%2").arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
text: qsTr("tokens:%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
}
recentPath {
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
}
}
@ -91,12 +95,8 @@ ChatRootView {
width: ListView.view.width - scroll.width
msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments
color: model.roleType === ChatModel.User ? root.primaryColor : root.secondaryColor
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
codeBgColor: root.codeColor
selectionColor: root.primaryColor.hslLightness > 0.5 ? Qt.darker(root.primaryColor, 1.5)
: Qt.lighter(root.primaryColor, 1.5)
color: model.roleType === ChatModel.User ? palette.alternateBase
: palette.base
}
header: Item {
@ -104,7 +104,7 @@ ChatRootView {
height: 30
}
ScrollBar.vertical: ScrollBar {
ScrollBar.vertical: QQC.ScrollBar {
id: scroll
}
@ -130,15 +130,28 @@ ChatRootView {
id: messageInput
placeholderText: qsTr("Type your message here...")
placeholderTextColor: "#888"
color: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
placeholderTextColor: palette.mid
color: palette.text
background: Rectangle {
radius: 2
color: root.primaryColor
border.color: root.primaryColor.hslLightness > 0.5 ? Qt.lighter(root.primaryColor, 1.5)
: Qt.darker(root.primaryColor, 1.5)
color: palette.base
border.color: messageInput.activeFocus ? palette.highlight : palette.button
border.width: 1
Behavior on border.color {
ColorAnimation { duration: 150 }
}
Rectangle {
anchors.fill: parent
color: palette.highlight
opacity: messageInput.hovered ? 0.1 : 0
radius: parent.radius
}
}
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
Keys.onPressed: function(event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
root.sendChatMessage()
@ -153,6 +166,21 @@ ChatRootView {
Layout.fillWidth: true
attachedFilesModel: root.attachmentFiles
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
: "qrc:/qt/qml/ChatView/icons/attach-file-light.svg"
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4))
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
}
AttachedFilesPlace {
id: linkedFilesPlace
Layout.fillWidth: true
attachedFilesModel: root.linkedFiles
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
}
BottomBar {
@ -163,13 +191,19 @@ ChatRootView {
sendButton.onClicked: root.sendChatMessage()
stopButton.onClicked: root.cancelRequest()
sharingCurrentFile.checked: root.isSharingCurrentFile
syncOpenFiles {
checked: root.isSyncOpenFiles
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
}
attachFiles.onClicked: root.showAttachFilesDialog()
linkFiles.onClicked: root.showLinkFilesDialog()
}
}
function clearChat() {
root.chatModel.clear()
root.clearAttachmentFiles()
root.updateInputTokensCount()
}
function scrollToBottom() {
@ -177,7 +211,7 @@ ChatRootView {
}
function sendChatMessage() {
root.sendMessage(messageInput.text, bottomBar.sharingCurrentFile.checked)
root.sendMessage(messageInput.text)
messageInput.text = ""
scrollToBottom()
}

View File

@ -26,7 +26,6 @@ Rectangle {
property string code: ""
property string language: ""
property color selectionColor
readonly property string monospaceFont: {
switch (Qt.platform.os) {
@ -41,6 +40,7 @@ Rectangle {
}
}
color: palette.alternateBase
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
: Qt.lighter(root.color, 1.3)
border.width: 2
@ -62,10 +62,10 @@ Rectangle {
readOnly: true
selectByMouse: true
font.family: root.monospaceFont
font.pointSize: 12
font.pointSize: Qt.application.font.pointSize
color: parent.color.hslLightness > 0.5 ? "black" : "white"
wrapMode: Text.WordWrap
selectionColor: root.selectionColor
selectionColor: palette.highlight
}
TextEdit {
@ -80,7 +80,7 @@ Rectangle {
font.pointSize: 8
}
Button {
QoAButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 5

View File

@ -26,4 +26,6 @@ TextEdit {
selectByMouse: true
wrapMode: Text.WordWrap
textFormat: Text.StyledText
selectionColor: palette.highlight
color: palette.text
}

View File

@ -23,13 +23,19 @@ import QtQuick.Layouts
import ChatView
Flow {
id: attachFilesPlace
id: root
property alias attachedFilesModel: attachRepeater.model
property color accentColor: palette.mid
property string iconPath
signal removeFileFromListByIndex(index: int)
spacing: 5
leftPadding: 5
rightPadding: 5
topPadding: attachRepeater.model.length > 0 ? 2 : 0
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
Repeater {
id: attachRepeater
@ -39,15 +45,32 @@ Flow {
required property string modelData
height: 30
width: fileNameText.width + closeButton.width + 20
width: contentRow.width + 10
radius: 4
color: palette.button
border.width: 1
border.color: mouse.hovered ? palette.highlight : root.accentColor
HoverHandler {
id: mouse
}
Row {
id: contentRow
spacing: 5
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
Image {
id: icon
anchors.verticalCenter: parent.verticalCenter
source: root.iconPath
sourceSize.width: 8
sourceSize.height: 15
}
Text {
id: fileNameText
@ -65,14 +88,10 @@ Flow {
id: closeButton
anchors.verticalCenter: parent.verticalCenter
width: closeIcon.width
height: closeButton.width
width: closeIcon.width + 5
height: closeButton.width + 5
onClicked: {
const newList = [...root.attachmentFiles];
newList.splice(index, 1);
root.attachmentFiles = newList;
}
onClicked: root.removeFileFromListByIndex(index)
Image {
id: closeIcon

View File

@ -27,8 +27,9 @@ Rectangle {
property alias sendButton: sendButtonId
property alias stopButton: stopButtonId
property alias sharingCurrentFile: sharingCurrentFileId
property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId
property alias linkFiles: linkFilesId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
@ -59,23 +60,37 @@ Rectangle {
text: qsTr("Stop")
}
CheckBox {
id: sharingCurrentFileId
text: qsTr("Share current file with models")
}
QoAButton {
id: attachFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/attach-file.svg"
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
height: 15
width: 8
}
text: qsTr("Attach files")
}
QoAButton {
id: linkFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
height: 15
width: 8
}
text: qsTr("Link files")
}
CheckBox {
id: syncOpenFilesId
text: qsTr("Sync open files")
ToolTip.visible: syncOpenFilesId.hovered
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
}
Item {
Layout.fillWidth: true
}

View File

@ -28,6 +28,7 @@ Rectangle {
property alias loadButton: loadButtonId
property alias clearButton: clearButtonId
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
@ -62,6 +63,14 @@ Rectangle {
text: qsTr("Clear")
}
Text {
id: recentPathId
Layout.fillWidth: true
elide: Text.ElideMiddle
color: palette.text
}
Item {
Layout.fillWidth: true
}

View File

@ -1,7 +1,7 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.4.4",
"Version" : "0.4.9",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",

149
README.md
View File

@ -1,5 +1,4 @@
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
[![Discord](https://dcbadge.limes.pink/api/server/https://discord.gg/DGgMtTteAD?style=flat?theme=discord)](https://discord.gg/DGgMtTteAD)
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71)
![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
@ -16,18 +15,18 @@
## Table of Contents
1. [Overview](#overview)
2. [Installation for using Ollama](#installation-for-using-Ollama)
3. [Installation for using Claude](#installation-for-using-Claude)
3. [Configure Plugin](#configure-plugin)
4. [Supported LLM Providers](#supported-llm-providers)
5. [Recommended Models](#recommended-models)
- [Ollama](#ollama)
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
7. [Development Progress](#development-progress)
8. [Hotkeys](#hotkeys)
9. [Troubleshooting](#troubleshooting)
10. [Support the Development](#support-the-development-of-qodeassist)
11. [How to Build](#how-to-build)
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
4. [Configure for OpenAI](#configure-for-openai)
5. [Configure for using Ollama](#configure-for-using-ollama)
6. [System Prompt Configuration](#system-prompt-configuration)
7. [Template-Model Compatibility](#template-model-compatibility)
8. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
9. [Development Progress](#development-progress)
10. [Hotkeys](#hotkeys)
11. [Troubleshooting](#troubleshooting)
12. [Support the Development](#support-the-development-of-qodeassist)
13. [How to Build](#how-to-build)
## Overview
@ -36,7 +35,8 @@
- Side and Bottom panels
- Support for multiple LLM providers:
- Ollama
- Claude
- OpenAI
- Anthropic Claude
- LM Studio
- OpenAI-compatible providers(eg. https://openrouter.ai)
- Extensive library of model-specific templates
@ -63,11 +63,46 @@
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
</details>
## Installation for using Ollama
## Install plugin to QtCreator
1. Install Latest Qt Creator
2. Download the QodeAssist plugin for your Qt Creator
3. Launch Qt Creator and install the plugin:
- Go to:
- MacOS: Qt Creator -> About Plugins...
- Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
1. Install Latest QtCreator
2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
3. Install a language models in Ollama via terminal. For example, you can run:
## Configure for Anthropic Claude
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Claude api key
3. Return to General tab and configure:
- Set "Claude" as the provider for code completion or/and chat assistant
- Set the Claude URL (https://api.anthropic.com)
- Select your preferred model (e.g., claude-3-5-sonnet-20241022)
- Choose the Claude template for code completion or/and chat
<details>
<summary>Example of Claude settings: (click to expand)</summary>
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
</details>
## Configure for OpenAI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure OpenAI api key
3. Return to General tab and configure:
- Set "OpenAI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.openai.com)
- Select your preferred model (e.g., gpt-4o)
- Choose the OpenAI template for code completion or/and chat
<details>
<summary>Example of OpenAI settings: (click to expand)</summary>
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
</details>
## Configure for using Ollama
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
2. Install a language models in Ollama via terminal. For example, you can run:
For standard computers (minimum 8GB RAM):
```
@ -81,31 +116,6 @@ For high-end systems (32GB+ RAM):
```
ollama run qwen2.5-coder:32b
```
4. Download the QodeAssist plugin for your QtCreator.
5. Launch Qt Creator and install the plugin:
- Go to MacOS: Qt Creator -> About Plugins...
Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
## Installation for using Claude
1. Install Latest QtCreator
2. Download the QodeAssist plugin for your QtCreator.
3. Launch Qt Creator and install the plugin:
- Go to MacOS: Qt Creator -> About Plugins...
Windows\Linux: Help -> About Plugins...
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
4. Select Claude provider
5. Select Claude api
6. Fill in api key for Claude
5. Select Claude templates for code completion and chat
6. Enjoy!
## Configure Plugin
QodeAssist comes with default settings that should work immediately after installing a language model. The plugin is pre-configured to use Ollama with standard templates, so you may only need to verify the settings.
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
2. Navigate to the "Qode Assist" tab
@ -113,51 +123,20 @@ QodeAssist comes with default settings that should work immediately after instal
- Ollama is selected as your LLM provider
- The URL is set to http://localhost:11434
- Your installed model appears in the model selection
- The prompt template is Ollama Auto FIM
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct
4. Click Apply if you made any changes
You're all set! QodeAssist is now ready to use in Qt Creator.
<details>
<summary>Example of Ollama settings: (click to expand)</summary>
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
</details>
## Supported LLM Providers
QodeAssist currently supports the following LLM (Large Language Model) providers:
- [Ollama](https://ollama.com)
- [LM Studio](https://lmstudio.ai)
- [OpenRouter](https://openrouter.ai)
- OpenAI compatible providers
## System Prompt Configuration
## Recommended Models:
QodeAssist has been thoroughly tested and optimized for use with the following language models:
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings.
- Qwen2.5-coder
- CodeLlama
- StarCoder2
- DeepSeek-Coder-V2
### Model Types
FIM models (codellama:7b-code, starcoder2:7b, etc.) - Optimized for code completion and suggestions
Instruct models (codellama:7b-instruct, starcoder2:instruct, etc.) - Better for chat assistance, explanations, and code review
For best results, use FIM models with code completion and Instruct models with chat features.
### Ollama:
### For autocomplete(FIM)
```
ollama run codellama:7b-code
ollama run starcoder2:7b
ollama run qwen2.5-coder:7b-base
ollama run deepseek-coder-v2:16b-lite-base-q3_K_M
```
### For chat and instruct
```
ollama run codellama:7b-instruct
ollama run starcoder2:instruct
ollama run qwen2.5-coder:7b-instruct
ollama run deepseek-coder-v2
```
### Template-Model Compatibility
## Template-Model Compatibility
| Template | Compatible Models | Purpose |
|----------|------------------|----------|
@ -173,12 +152,6 @@ ollama run deepseek-coder-v2
| Llama3 | `llama3 model family` | Chat assistance |
| Ollama Auto Chat | `Any Ollama chat model` | Chat assistance |
> Note:
> - FIM (Fill-in-Middle) templates are optimized for code completion
> - Chat templates are designed for interactive dialogue
> - The Ollama Auto templates automatically adapt to most Ollama models
> - Custom Template allows you to define your own prompt format
## QtCreator Version Compatibility
- QtCreator 15.0.0 - 0.4.x

72
UpdateStatusWidget.cpp Normal file
View File

@ -0,0 +1,72 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "UpdateStatusWidget.hpp"
namespace QodeAssist {
UpdateStatusWidget::UpdateStatusWidget(QWidget *parent)
: QFrame(parent)
{
setFrameStyle(QFrame::NoFrame);
auto layout = new QHBoxLayout(this);
layout->setContentsMargins(4, 0, 4, 0);
layout->setSpacing(4);
m_actionButton = new QToolButton(this);
m_actionButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_versionLabel = new QLabel(this);
m_versionLabel->setVisible(false);
m_updateButton = new QPushButton(tr("Update"), this);
m_updateButton->setVisible(false);
m_updateButton->setStyleSheet("QPushButton { padding: 2px 8px; }");
layout->addWidget(m_actionButton);
layout->addWidget(m_versionLabel);
layout->addWidget(m_updateButton);
}
void UpdateStatusWidget::setDefaultAction(QAction *action)
{
m_actionButton->setDefaultAction(action);
}
void UpdateStatusWidget::showUpdateAvailable(const QString &version)
{
m_versionLabel->setText(tr("new version: v%1").arg(version));
m_versionLabel->setVisible(true);
m_updateButton->setVisible(true);
m_updateButton->setToolTip(tr("Update QodeAssist to version %1").arg(version));
}
void UpdateStatusWidget::hideUpdateInfo()
{
m_versionLabel->setVisible(false);
m_updateButton->setVisible(false);
}
QPushButton *UpdateStatusWidget::updateButton() const
{
return m_updateButton;
}
} // namespace QodeAssist

47
UpdateStatusWidget.hpp Normal file
View File

@ -0,0 +1,47 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QFrame>
#include <QLabel>
#include <QLayout>
#include <QPushButton>
#include <QToolButton>
namespace QodeAssist {
class UpdateStatusWidget : public QFrame
{
Q_OBJECT
public:
explicit UpdateStatusWidget(QWidget *parent = nullptr);
void setDefaultAction(QAction *action);
void showUpdateAvailable(const QString &version);
void hideUpdateInfo();
QPushButton *updateButton() const;
private:
QToolButton *m_actionButton;
QLabel *m_versionLabel;
QPushButton *m_updateButton;
};
} // namespace QodeAssist

View File

@ -3,6 +3,7 @@ add_library(Context STATIC
ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp
ContentFile.hpp
TokenUtils.hpp TokenUtils.cpp
)
target_link_libraries(Context

54
context/TokenUtils.cpp Normal file
View File

@ -0,0 +1,54 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "TokenUtils.hpp"
namespace QodeAssist::Context {
int TokenUtils::estimateTokens(const QString& text)
{
if (text.isEmpty()) {
return 0;
}
// TODO: need to improve
return text.length() / 4;
}
int TokenUtils::estimateFileTokens(const Context::ContentFile& file)
{
int total = 0;
total += estimateTokens(file.filename);
total += estimateTokens(file.content);
total += 5;
return total;
}
int TokenUtils::estimateFilesTokens(const QList<Context::ContentFile>& files)
{
int total = 0;
for (const auto& file : files) {
total += estimateFileTokens(file);
}
return total;
}
}

36
context/TokenUtils.hpp Normal file
View File

@ -0,0 +1,36 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
#include "ContentFile.hpp"
#include <QList>
namespace QodeAssist::Context {
class TokenUtils
{
public:
static int estimateTokens(const QString& text);
static int estimateFileTokens(const Context::ContentFile& file);
static int estimateFilesTokens(const QList<Context::ContentFile>& files);
};
}

View File

@ -108,15 +108,22 @@ bool LMStudioProvider::handleResponse(QNetworkReply *reply, QString &accumulated
return false;
}
QByteArrayList chunks = data.split('\n');
for (const QByteArray &chunk : chunks) {
if (chunk.trimmed().isEmpty() || chunk == "data: [DONE]") {
bool isDone = false;
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
QByteArray jsonData = chunk;
if (chunk.startsWith("data: ")) {
jsonData = chunk.mid(6);
if (line == "data: [DONE]") {
isDone = true;
continue;
}
QByteArray jsonData = line;
if (line.startsWith("data: ")) {
jsonData = line.mid(6);
}
QJsonParseError error;
@ -128,15 +135,21 @@ bool LMStudioProvider::handleResponse(QNetworkReply *reply, QString &accumulated
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
if (message.hasError()) {
LOG_MESSAGE("Error in LMStudioProvider response: " + message.error);
LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue;
}
accumulatedResponse += message.getContent();
return message.isDone();
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
}
}
return false;
return isDone;
}
QList<QString> LMStudioProvider::getInstalledModels(const QString &url)

View File

@ -95,18 +95,36 @@ bool OllamaProvider::handleResponse(QNetworkReply *reply, QString &accumulatedRe
return false;
}
const QString endpoint = reply->url().path();
auto messageType = endpoint == completionEndpoint() ? LLMCore::OllamaMessage::Type::Generate
: LLMCore::OllamaMessage::Type::Chat;
QByteArrayList lines = data.split('\n');
bool isDone = false;
auto message = LLMCore::OllamaMessage::fromJson(data, messageType);
if (message.hasError()) {
LOG_MESSAGE("Error in Ollama response: " + message.error);
return false;
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
const QString endpoint = reply->url().path();
auto messageType = endpoint == completionEndpoint()
? LLMCore::OllamaMessage::Type::Generate
: LLMCore::OllamaMessage::Type::Chat;
auto message = LLMCore::OllamaMessage::fromJson(line, messageType);
if (message.hasError()) {
LOG_MESSAGE("Error in Ollama response: " + message.error);
continue;
}
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.done) {
isDone = true;
}
}
accumulatedResponse += message.getContent();
return message.done;
return isDone;
}
QList<QString> OllamaProvider::getInstalledModels(const QString &url)

View File

@ -109,15 +109,22 @@ bool OpenAICompatProvider::handleResponse(QNetworkReply *reply, QString &accumul
return false;
}
QByteArrayList chunks = data.split('\n');
for (const QByteArray &chunk : chunks) {
if (chunk.trimmed().isEmpty() || chunk == "data: [DONE]") {
bool isDone = false;
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
QByteArray jsonData = chunk;
if (chunk.startsWith("data: ")) {
jsonData = chunk.mid(6);
if (line == "data: [DONE]") {
isDone = true;
continue;
}
QByteArray jsonData = line;
if (line.startsWith("data: ")) {
jsonData = line.mid(6);
}
QJsonParseError error;
@ -133,11 +140,17 @@ bool OpenAICompatProvider::handleResponse(QNetworkReply *reply, QString &accumul
continue;
}
accumulatedResponse += message.getContent();
return message.isDone();
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
}
}
return false;
return isDone;
}
QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)

View File

@ -0,0 +1,229 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "OpenAIProvider.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include "llmcore/OpenAIMessage.hpp"
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp"
namespace QodeAssist::Providers {
OpenAIProvider::OpenAIProvider() {}
QString OpenAIProvider::name() const
{
return "OpenAI";
}
QString OpenAIProvider::url() const
{
return "https://api.openai.com";
}
QString OpenAIProvider::completionEndpoint() const
{
return "/v1/chat/completions";
}
QString OpenAIProvider::chatEndpoint() const
{
return "/v1/chat/completions";
}
bool OpenAIProvider::supportsModelListing() const
{
return true;
}
void OpenAIProvider::prepareRequest(QJsonObject &request, LLMCore::RequestType type)
{
auto prepareMessages = [](QJsonObject &req) -> QJsonArray {
QJsonArray messages;
if (req.contains("system")) {
messages.append(
QJsonObject{{"role", "system"}, {"content", req.take("system").toString()}});
}
if (req.contains("prompt")) {
messages.append(
QJsonObject{{"role", "user"}, {"content", req.take("prompt").toString()}});
}
return messages;
};
auto applyModelParams = [&request](const auto &settings) {
request["max_tokens"] = settings.maxTokens();
request["temperature"] = settings.temperature();
if (settings.useTopP())
request["top_p"] = settings.topP();
if (settings.useTopK())
request["top_k"] = settings.topK();
if (settings.useFrequencyPenalty())
request["frequency_penalty"] = settings.frequencyPenalty();
if (settings.usePresencePenalty())
request["presence_penalty"] = settings.presencePenalty();
};
QJsonArray messages = prepareMessages(request);
if (!messages.isEmpty()) {
request["messages"] = std::move(messages);
}
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else {
applyModelParams(Settings::chatAssistantSettings());
}
}
bool OpenAIProvider::handleResponse(QNetworkReply *reply, QString &accumulatedResponse)
{
QByteArray data = reply->readAll();
if (data.isEmpty()) {
return false;
}
bool isDone = false;
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) {
continue;
}
if (line == "data: [DONE]") {
isDone = true;
continue;
}
QByteArray jsonData = line;
if (line.startsWith("data: ")) {
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue;
}
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
}
}
return isDone;
}
QList<QString> OpenAIProvider::getInstalledModels(const QString &url)
{
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1/v1/models").arg(url));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
}
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("data")) {
QJsonArray modelArray = jsonObject["data"].toArray();
for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject();
if (modelObject.contains("id")) {
QString modelId = modelObject["id"].toString();
if (modelId.startsWith("gpt")) {
models.append(modelId);
}
}
}
}
} else {
LOG_MESSAGE(QString("Error fetching ChatGPT models: %1").arg(reply->errorString()));
}
reply->deleteLater();
return models;
}
QList<QString> OpenAIProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
{
const auto templateReq = QJsonObject{
{"model", {}},
{"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}},
{"temperature", {}},
{"max_tokens", {}},
{"top_p", {}},
{"top_k", {}},
{"frequency_penalty", {}},
{"presence_penalty", {}},
{"stop", QJsonArray{}},
{"stream", {}}};
return LLMCore::ValidationUtils::validateRequestFields(request, templateReq);
}
QString OpenAIProvider::apiKey() const
{
return Settings::providerSettings().openAiApiKey();
}
void OpenAIProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
}
}
} // namespace QodeAssist::Providers

View File

@ -0,0 +1,44 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "llmcore/Provider.hpp"
namespace QodeAssist::Providers {
class OpenAIProvider : public LLMCore::Provider
{
public:
OpenAIProvider();
QString name() const override;
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<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
};
} // namespace QodeAssist::Providers

View File

@ -93,16 +93,22 @@ bool OpenRouterProvider::handleResponse(QNetworkReply *reply, QString &accumulat
return false;
}
QByteArrayList chunks = data.split('\n');
for (const QByteArray &chunk : chunks) {
if (chunk.trimmed().isEmpty() || chunk.contains("OPENROUTER PROCESSING")
|| chunk == "data: [DONE]") {
bool isDone = false;
QByteArrayList lines = data.split('\n');
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty() || line.contains("OPENROUTER PROCESSING")) {
continue;
}
QByteArray jsonData = chunk;
if (chunk.startsWith("data: ")) {
jsonData = chunk.mid(6);
if (line == "data: [DONE]") {
isDone = true;
continue;
}
QByteArray jsonData = line;
if (line.startsWith("data: ")) {
jsonData = line.mid(6);
}
QJsonParseError error;
@ -114,15 +120,21 @@ bool OpenRouterProvider::handleResponse(QNetworkReply *reply, QString &accumulat
auto message = LLMCore::OpenAIMessage::fromJson(doc.object());
if (message.hasError()) {
LOG_MESSAGE("Error in OpenRouter response: " + message.error);
LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue;
}
accumulatedResponse += message.getContent();
return message.isDone();
QString content = message.getContent();
if (!content.isEmpty()) {
accumulatedResponse += content;
}
if (message.isDone()) {
isDone = true;
}
}
return false;
return isDone;
}
QString OpenRouterProvider::apiKey() const

View File

@ -24,6 +24,7 @@
#include "providers/LMStudioProvider.hpp"
#include "providers/OllamaProvider.hpp"
#include "providers/OpenAICompatProvider.hpp"
#include "providers/OpenAIProvider.hpp"
#include "providers/OpenRouterAIProvider.hpp"
namespace QodeAssist::Providers {
@ -36,6 +37,7 @@ inline void registerProviders()
providerManager.registerProvider<OpenAICompatProvider>();
providerManager.registerProvider<OpenRouterProvider>();
providerManager.registerProvider<ClaudeProvider>();
providerManager.registerProvider<OpenAIProvider>();
}
} // namespace QodeAssist::Providers

View File

@ -19,6 +19,8 @@
#include "QodeAssistConstants.hpp"
#include "QodeAssisttr.h"
#include "settings/PluginUpdater.hpp"
#include "settings/UpdateDialog.hpp"
#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h>
@ -32,19 +34,21 @@
#include <extensionsystem/iplugin.h>
#include <languageclient/languageclientmanager.h>
#include <texteditor/texteditor.h>
#include <utils/icon.h>
#include <QAction>
#include <QMainWindow>
#include <QMenu>
#include <QMessageBox>
#include <texteditor/texteditor.h>
#include <utils/icon.h>
#include "ConfigurationManager.hpp"
#include "QodeAssistClient.hpp"
#include "chat/ChatOutputPane.h"
#include "chat/NavigationPanel.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettingsPanel.hpp"
#include "UpdateStatusWidget.hpp"
#include "providers/Providers.hpp"
#include "templates/Templates.hpp"
@ -61,8 +65,8 @@ class QodeAssistPlugin final : public ExtensionSystem::IPlugin
public:
QodeAssistPlugin()
{
}
: m_updater(new PluginUpdater(this))
{}
~QodeAssistPlugin() final
{
@ -96,33 +100,36 @@ public:
}
});
auto toggleButton = new QToolButton;
toggleButton->setDefaultAction(requestAction.contextAction());
StatusBarManager::addStatusBarWidget(toggleButton, StatusBarManager::RightCorner);
m_statusWidget = new UpdateStatusWidget;
m_statusWidget->setDefaultAction(requestAction.contextAction());
StatusBarManager::addStatusBarWidget(m_statusWidget, StatusBarManager::RightCorner);
connect(m_statusWidget->updateButton(), &QPushButton::clicked, this, [this]() {
UpdateDialog::checkForUpdatesAndShow(Core::ICore::mainWindow());
});
m_chatOutputPane = new Chat::ChatOutputPane(this);
m_navigationPanel = new Chat::NavigationPanel();
Settings::setupProjectPanel();
ConfigurationManager::instance().init();
if (Settings::generalSettings().enableCheckUpdate()) {
QTimer::singleShot(3000, this, &QodeAssistPlugin::checkForUpdates);
}
}
void extensionsInitialized() final
{
}
void extensionsInitialized() final {}
void restartClient()
{
LanguageClient::LanguageClientManager::shutdownClient(m_qodeAssistClient);
m_qodeAssistClient = new QodeAssistClient();
}
bool delayedInitialize() final
{
restartClient();
return true;
}
@ -130,17 +137,38 @@ public:
{
if (!m_qodeAssistClient)
return SynchronousShutdown;
connect(m_qodeAssistClient,
&QObject::destroyed,
this,
&IPlugin::asynchronousShutdownFinished);
connect(m_qodeAssistClient, &QObject::destroyed, this, &IPlugin::asynchronousShutdownFinished);
return AsynchronousShutdown;
}
private:
void checkForUpdates()
{
connect(
m_updater,
&PluginUpdater::updateCheckFinished,
this,
&QodeAssistPlugin::handleUpdateCheckResult,
Qt::UniqueConnection);
m_updater->checkForUpdates();
}
void handleUpdateCheckResult(const PluginUpdater::UpdateInfo &info)
{
if (!info.isUpdateAvailable
|| QVersionNumber::fromString(info.currentIdeVersion)
> QVersionNumber::fromString(info.targetIdeVersion))
return;
if (m_statusWidget)
m_statusWidget->showUpdateAvailable(info.version);
}
QPointer<QodeAssistClient> m_qodeAssistClient;
QPointer<Chat::ChatOutputPane> m_chatOutputPane;
QPointer<Chat::NavigationPanel> m_navigationPanel;
QPointer<PluginUpdater> m_updater;
UpdateStatusWidget *m_statusWidget{nullptr};
};
} // namespace QodeAssist::Internal

View File

@ -11,6 +11,8 @@ add_library(QodeAssistSettings STATIC
ProjectSettings.hpp ProjectSettings.cpp
ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp
ProviderSettings.hpp ProviderSettings.cpp
PluginUpdater.hpp PluginUpdater.cpp
UpdateDialog.hpp UpdateDialog.cpp
)
target_link_libraries(QodeAssistSettings

View File

@ -44,15 +44,15 @@ ChatAssistantSettings::ChatAssistantSettings()
// Chat Settings
chatTokensThreshold.setSettingsKey(Constants::CA_TOKENS_THRESHOLD);
chatTokensThreshold.setLabelText(Tr::tr("Chat History Token Limit:"));
chatTokensThreshold.setLabelText(Tr::tr("Chat history token limit:"));
chatTokensThreshold.setToolTip(Tr::tr("Maximum number of tokens in chat history. When "
"exceeded, oldest messages will be removed."));
chatTokensThreshold.setRange(1, 900000);
chatTokensThreshold.setDefaultValue(8000);
sharingCurrentFile.setSettingsKey(Constants::CA_SHARING_CURRENT_FILE);
sharingCurrentFile.setLabelText(Tr::tr("Share Current File With Assistant by Default"));
sharingCurrentFile.setDefaultValue(true);
linkOpenFiles.setSettingsKey(Constants::CA_LINK_OPEN_FILES);
linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default"));
linkOpenFiles.setDefaultValue(false);
stream.setSettingsKey(Constants::CA_STREAM);
stream.setDefaultValue(true);
@ -171,7 +171,7 @@ ChatAssistantSettings::ChatAssistantSettings()
Space{8},
Group{
title(Tr::tr("Chat Settings")),
Column{Row{chatTokensThreshold, Stretch{1}}, sharingCurrentFile, stream, autosave}},
Column{Row{chatTokensThreshold, Stretch{1}}, linkOpenFiles, stream, autosave}},
Space{8},
Group{
title(Tr::tr("General Parameters")),
@ -227,6 +227,7 @@ void ChatAssistantSettings::resetSettingsToDefaults()
resetAspect(systemPrompt);
resetAspect(ollamaLivetime);
resetAspect(contextWindow);
resetAspect(linkOpenFiles);
}
}

View File

@ -34,7 +34,7 @@ public:
// Chat settings
Utils::IntegerAspect chatTokensThreshold{this};
Utils::BoolAspect sharingCurrentFile{this};
Utils::BoolAspect linkOpenFiles{this};
Utils::BoolAspect stream{this};
Utils::BoolAspect autosave{this};

View File

@ -153,16 +153,16 @@ CodeCompletionSettings::CodeCompletionSettings()
systemPrompt.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
systemPrompt.setDefaultValue(
"You are an expert in C++, Qt, and QML programming. Your task is to provide code "
"suggestions that seamlessly integrate with existing code. You will receive a code context "
"with specified insertion points. Your goal is to complete only one logic expression "
"within these points."
"suggestions that seamlessly integrate with existing code. Do not repeat code from position "
"before or after <cursor>. You will receive a code context with specified insertion points. "
"Your goal is to complete only one code block."
"Here is the code context with insertion points:<code_context>Before: {{variable}}After: "
"{{variable}}</code_context> Instructions: 1. Carefully analyze the provided code context. "
"2. Consider the existing code and the specified insertion points.3. Generate a code "
"suggestion that completes one logic expression between the 'Before' and 'After' points. "
"4. Ensure your suggestion does not repeat any existing code. 5. Format your suggestion as "
"a code block using triple backticks. 6. Do not include any comments or descriptions with "
"your code suggestion. Remember to include only the new code to be inserted.");
"your code suggestion.");
useUserMessageTemplateForCC.setSettingsKey(Constants::CC_USE_USER_TEMPLATE);
useUserMessageTemplateForCC.setDefaultValue(true);
@ -310,6 +310,7 @@ void CodeCompletionSettings::resetSettingsToDefaults()
resetAspect(autoCompletion);
resetAspect(multiLineCompletion);
resetAspect(stream);
resetAspect(smartProcessInstuctText);
resetAspect(temperature);
resetAspect(maxTokens);
resetAspect(useTopP);

View File

@ -37,6 +37,7 @@
#include "SettingsDialog.hpp"
#include "SettingsTr.hpp"
#include "SettingsUtils.hpp"
#include "UpdateDialog.hpp"
namespace QodeAssist::Settings {
@ -60,7 +61,12 @@ GeneralSettings::GeneralSettings()
enableLogging.setLabelText(TrConstants::ENABLE_LOG);
enableLogging.setDefaultValue(false);
enableCheckUpdate.setSettingsKey(Constants::ENABLE_CHECK_UPDATE);
enableCheckUpdate.setLabelText(TrConstants::ENABLE_CHECK_UPDATE_ON_START);
enableCheckUpdate.setDefaultValue(true);
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE;
initStringAspect(ccProvider, Constants::CC_PROVIDER, TrConstants::PROVIDER, "Ollama");
ccProvider.setReadOnly(true);
@ -129,13 +135,15 @@ GeneralSettings::GeneralSettings()
auto ccGroup = Group{title(TrConstants::CODE_COMPLETION), ccGrid};
auto caGroup = Group{title(TrConstants::CHAT_ASSISTANT), caGrid};
auto rootLayout = Column{Row{enableQodeAssist, Stretch{1}, resetToDefaults},
Row{enableLogging, Stretch{1}},
Space{8},
ccGroup,
Space{8},
caGroup,
Stretch{1}};
auto rootLayout = Column{
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}},
Row{enableLogging, Stretch{1}},
Row{enableCheckUpdate, Stretch{1}},
Space{8},
ccGroup,
Space{8},
caGroup,
Stretch{1}};
return rootLayout;
});
@ -295,6 +303,9 @@ void GeneralSettings::setupConnections()
Logger::instance().setLoggingEnabled(enableLogging.volatileValue());
});
connect(&resetToDefaults, &ButtonAspect::clicked, this, &GeneralSettings::resetPageToDefaults);
connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() {
QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent());
});
}
void GeneralSettings::resetPageToDefaults()
@ -316,6 +327,7 @@ void GeneralSettings::resetPageToDefaults()
resetAspect(caModel);
resetAspect(caTemplate);
resetAspect(caUrl);
resetAspect(enableCheckUpdate);
writeSettings();
}
}

View File

@ -35,6 +35,8 @@ public:
Utils::BoolAspect enableQodeAssist{this};
Utils::BoolAspect enableLogging{this};
Utils::BoolAspect enableCheckUpdate{this};
ButtonAspect checkUpdate{this};
ButtonAspect resetToDefaults{this};
// code completion setttings

181
settings/PluginUpdater.cpp Normal file
View File

@ -0,0 +1,181 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "PluginUpdater.hpp"
#include <coreplugin/coreconstants.h>
#include <coreplugin/coreplugin.h>
#include <extensionsystem/pluginmanager.h>
#include <extensionsystem/pluginspec.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QStandardPaths>
namespace QodeAssist {
PluginUpdater::PluginUpdater(QObject *parent)
: QObject(parent)
, m_networkManager(new QNetworkAccessManager(this))
{}
void PluginUpdater::checkForUpdates()
{
if (m_isCheckingUpdate)
return;
m_isCheckingUpdate = true;
QNetworkRequest request((QUrl(getUpdateUrl())));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
handleUpdateResponse(reply);
m_isCheckingUpdate = false;
reply->deleteLater();
});
}
void PluginUpdater::handleUpdateResponse(QNetworkReply *reply)
{
UpdateInfo info;
if (reply->error() != QNetworkReply::NoError) {
emit downloadError(reply->errorString());
return;
}
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
QJsonObject obj = doc.object();
info.version = obj["tag_name"].toString();
if (info.version.startsWith('v'))
info.version.remove(0, 1);
QString qtcVersionStr = Core::ICore::versionString().split(' ').last();
QVersionNumber qtcVersion = QVersionNumber::fromString(qtcVersionStr);
info.currentIdeVersion = qtcVersionStr;
auto assets = obj["assets"].toArray();
for (const auto &asset : assets) {
QString name = asset.toObject()["name"].toString();
if (name.startsWith("QodeAssist-")) {
QString assetVersionStr = name.section('-', 1, 1);
QVersionNumber assetVersion = QVersionNumber::fromString(assetVersionStr);
info.targetIdeVersion = assetVersionStr;
if (assetVersion != qtcVersion) {
continue;
}
#if defined(Q_OS_WIN)
if (name.contains("Windows"))
#elif defined(Q_OS_MACOS)
if (name.contains("macOS"))
#else
if (name.contains("Linux") && !name.contains("experimental"))
#endif
{
info.downloadUrl = asset.toObject()["browser_download_url"].toString();
info.fileName = name;
break;
}
}
}
if (info.downloadUrl.isEmpty()) {
info.incompatibleIdeVersion = true;
emit updateCheckFinished(info);
return;
}
info.changeLog = obj["body"].toString();
info.isUpdateAvailable = QVersionNumber::fromString(info.version)
> QVersionNumber::fromString(currentVersion());
m_lastUpdateInfo = info;
emit updateCheckFinished(info);
}
void PluginUpdater::downloadUpdate(const QString &url)
{
QNetworkRequest request(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::downloadProgress, this, &PluginUpdater::handleDownloadProgress);
connect(reply, &QNetworkReply::finished, this, &PluginUpdater::handleDownloadFinished);
}
QString PluginUpdater::currentVersion() const
{
const auto pluginSpecs = ExtensionSystem::PluginManager::plugins();
for (const ExtensionSystem::PluginSpec *spec : pluginSpecs) {
if (spec->name() == QLatin1String("QodeAssist"))
return spec->version();
}
return QString();
}
bool PluginUpdater::isUpdateAvailable() const
{
return m_lastUpdateInfo.isUpdateAvailable;
}
QString PluginUpdater::getUpdateUrl() const
{
return "https://api.github.com/repos/Palm1r/qodeassist/releases/latest";
}
void PluginUpdater::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
emit downloadProgress(bytesReceived, bytesTotal);
}
void PluginUpdater::handleDownloadFinished()
{
auto reply = qobject_cast<QNetworkReply *>(sender());
QTC_ASSERT(reply, return);
if (reply->error() != QNetworkReply::NoError) {
emit downloadError(reply->errorString());
reply->deleteLater();
return;
}
QString downloadPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
QString filePath = downloadPath + "/" + m_lastUpdateInfo.fileName;
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
emit downloadError(tr("Could not save the update file"));
reply->deleteLater();
return;
}
file.write(reply->readAll());
file.close();
emit downloadFinished(filePath);
reply->deleteLater();
}
} // namespace QodeAssist

View File

@ -0,0 +1,72 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <coreplugin/icore.h>
#include <coreplugin/plugininstallwizard.h>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <QVersionNumber>
namespace QodeAssist {
class PluginUpdater : public QObject
{
Q_OBJECT
public:
struct UpdateInfo
{
QString version;
QString downloadUrl;
QString changeLog;
QString fileName;
bool isUpdateAvailable;
bool incompatibleIdeVersion{false};
QString targetIdeVersion;
QString currentIdeVersion;
};
explicit PluginUpdater(QObject *parent = nullptr);
~PluginUpdater() = default;
void checkForUpdates();
void downloadUpdate(const QString &url);
QString currentVersion() const;
bool isUpdateAvailable() const;
signals:
void updateCheckFinished(const UpdateInfo &info);
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void downloadFinished(const QString &filePath);
void downloadError(const QString &error);
private:
void handleUpdateResponse(QNetworkReply *reply);
void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void handleDownloadFinished();
QString getUpdateUrl() const;
QNetworkAccessManager *m_networkManager;
UpdateInfo m_lastUpdateInfo;
bool m_isCheckingUpdate{false};
};
} // namespace QodeAssist

View File

@ -69,6 +69,14 @@ ProviderSettings::ProviderSettings()
claudeApiKey.setDefaultValue("");
claudeApiKey.setAutoApply(true);
openAiApiKey.setSettingsKey(Constants::OPEN_AI_API_KEY);
openAiApiKey.setLabelText(Tr::tr("OpenAI API Key:"));
openAiApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
openAiApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
openAiApiKey.setHistoryCompleter(Constants::OPEN_AI_API_KEY_HISTORY);
openAiApiKey.setDefaultValue("");
openAiApiKey.setAutoApply(true);
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
readSettings();
@ -83,6 +91,8 @@ ProviderSettings::ProviderSettings()
Space{8},
Group{title(Tr::tr("OpenRouter Settings")), Column{openRouterApiKey}},
Space{8},
Group{title(Tr::tr("OpenAI Settings")), Column{openAiApiKey}},
Space{8},
Group{title(Tr::tr("OpenAI Compatible Settings")), Column{openAiCompatApiKey}},
Space{8},
Group{title(Tr::tr("Claude Settings")), Column{claudeApiKey}},
@ -101,6 +111,7 @@ void ProviderSettings::setupConnections()
openAiCompatApiKey.writeSettings();
});
connect(&claudeApiKey, &ButtonAspect::changed, this, [this]() { claudeApiKey.writeSettings(); });
connect(&openAiApiKey, &ButtonAspect::changed, this, [this]() { openAiApiKey.writeSettings(); });
}
void ProviderSettings::resetSettingsToDefaults()
@ -116,6 +127,7 @@ void ProviderSettings::resetSettingsToDefaults()
resetAspect(openRouterApiKey);
resetAspect(openAiCompatApiKey);
resetAspect(claudeApiKey);
resetAspect(openAiApiKey);
}
}

View File

@ -36,6 +36,7 @@ public:
Utils::StringAspect openRouterApiKey{this};
Utils::StringAspect openAiCompatApiKey{this};
Utils::StringAspect claudeApiKey{this};
Utils::StringAspect openAiApiKey{this};
private:
void setupConnections();

View File

@ -50,6 +50,8 @@ const char CA_URL_HISTORY[] = "QodeAssist.caUrlHistory";
const char ENABLE_QODE_ASSIST[] = "QodeAssist.enableQodeAssist";
const char CC_AUTO_COMPLETION[] = "QodeAssist.ccAutoCompletion";
const char ENABLE_LOGGING[] = "QodeAssist.enableLogging";
const char ENABLE_CHECK_UPDATE[] = "QodeAssist.enableCheckUpdate";
const char PROVIDER_PATHS[] = "QodeAssist.providerPaths";
const char СС_START_SUGGESTION_TIMER[] = "QodeAssist.startSuggestionTimer";
const char СС_AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCharThreshold";
@ -60,7 +62,7 @@ const char CC_STREAM[] = "QodeAssist.ccStream";
const char CC_SMART_PROCESS_INSTRUCT_TEXT[] = "QodeAssist.ccSmartProcessInstructText";
const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate";
const char CA_TOKENS_THRESHOLD[] = "QodeAssist.caTokensThreshold";
const char CA_SHARING_CURRENT_FILE[] = "QodeAssist.caSharingCurrentFile";
const char CA_LINK_OPEN_FILES[] = "QodeAssist.caLinkOpenFiles";
const char CA_STREAM[] = "QodeAssist.caStream";
const char CA_AUTOSAVE[] = "QodeAssist.caAutosave";
@ -87,6 +89,8 @@ const char OPEN_AI_COMPAT_API_KEY[] = "QodeAssist.openAiCompatApiKey";
const char OPEN_AI_COMPAT_API_KEY_HISTORY[] = "QodeAssist.openAiCompatApiKeyHistory";
const char CLAUDE_API_KEY[] = "QodeAssist.claudeApiKey";
const char CLAUDE_API_KEY_HISTORY[] = "QodeAssist.claudeApiKeyHistory";
const char OPEN_AI_API_KEY[] = "QodeAssist.openAiApiKey";
const char OPEN_AI_API_KEY_HISTORY[] = "QodeAssist.openAiApiKeyHistory";
// context settings
const char CC_READ_FULL_FILE[] = "QodeAssist.ccReadFullFile";

View File

@ -28,6 +28,7 @@ inline const char *ENABLE_QODE_ASSIST = QT_TRANSLATE_NOOP("QtC::QodeAssist", "En
inline const char *GENERAL = QT_TRANSLATE_NOOP("QtC::QodeAssist", "General");
inline const char *RESET_TO_DEFAULTS = QT_TRANSLATE_NOOP("QtC::QodeAssist",
"Reset Page to Defaults");
inline const char *CHECK_UPDATE = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Check Update");
inline const char *SELECT = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Select...");
inline const char *PROVIDER = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Provider:");
inline const char *MODEL = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Model:");
@ -36,6 +37,9 @@ inline const char *URL = QT_TRANSLATE_NOOP("QtC::QodeAssist", "URL:");
inline const char *STATUS = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Status:");
inline const char *TEST = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Test");
inline const char *ENABLE_LOG = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Enable Logging");
inline const char *ENABLE_CHECK_UPDATE_ON_START
= QT_TRANSLATE_NOOP("QtC::QodeAssist", "Check for updates when Qt Creator starts");
inline const char *CODE_COMPLETION = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Code Completion");
inline const char *CHAT_ASSISTANT = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Chat Assistant");
inline const char *RESET_SETTINGS = QT_TRANSLATE_NOOP("QtC::QodeAssist", "Reset Settings");

180
settings/UpdateDialog.cpp Normal file
View File

@ -0,0 +1,180 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "UpdateDialog.hpp"
namespace QodeAssist {
UpdateDialog::UpdateDialog(QWidget *parent)
: QDialog(parent)
, m_updater(new PluginUpdater(this))
{
setWindowTitle(tr("QodeAssist Update"));
setMinimumWidth(400);
setMinimumHeight(500);
m_layout = new QVBoxLayout(this);
m_layout->setSpacing(12);
auto *supportLabel = new QLabel(
tr("QodeAssist is an open-source project that helps\n"
"developers write better code. If you find it useful, please"),
this);
supportLabel->setAlignment(Qt::AlignCenter);
m_layout->addWidget(supportLabel);
auto *supportLink = new QLabel(
tr("<a href='https://ko-fi.com/qodeassist' style='color: #0066cc;'>Support on Ko-fi "
"☕</a>"),
this);
supportLink->setOpenExternalLinks(true);
supportLink->setTextFormat(Qt::RichText);
supportLink->setAlignment(Qt::AlignCenter);
m_layout->addWidget(supportLink);
m_layout->addSpacing(20);
m_titleLabel = new QLabel(tr("A new version of QodeAssist is available!"), this);
m_titleLabel->setStyleSheet("font-weight: bold; font-size: 14px;");
m_titleLabel->setAlignment(Qt::AlignCenter);
m_layout->addWidget(m_titleLabel);
m_versionLabel = new QLabel(
tr("Version %1 is now available - you have %2").arg(m_updater->currentVersion()), this);
m_versionLabel->setAlignment(Qt::AlignCenter);
m_layout->addWidget(m_versionLabel);
m_releaseLink = new QLabel(this);
m_releaseLink->setOpenExternalLinks(true);
m_releaseLink->setTextFormat(Qt::RichText);
m_releaseLink->setAlignment(Qt::AlignCenter);
m_layout->addWidget(m_releaseLink);
if (!m_changelogLabel) {
m_changelogLabel = new QLabel(tr("Release Notes:"), this);
m_layout->addWidget(m_changelogLabel);
m_changelogText = new QTextEdit(this);
m_changelogText->setReadOnly(true);
m_changelogText->setMinimumHeight(100);
m_layout->addWidget(m_changelogText);
}
m_progress = new QProgressBar(this);
m_progress->setVisible(false);
m_layout->addWidget(m_progress);
auto *buttonLayout = new QHBoxLayout;
m_downloadButton = new QPushButton(tr("Download"), this);
m_downloadButton->setEnabled(false);
buttonLayout->addWidget(m_downloadButton);
m_closeButton = new QPushButton(tr("Close"), this);
buttonLayout->addWidget(m_closeButton);
m_layout->addLayout(buttonLayout);
connect(m_updater, &PluginUpdater::updateCheckFinished, this, &UpdateDialog::handleUpdateInfo);
connect(m_updater, &PluginUpdater::downloadProgress, this, &UpdateDialog::updateProgress);
connect(m_updater, &PluginUpdater::downloadFinished, this, &UpdateDialog::handleDownloadFinished);
connect(m_updater, &PluginUpdater::downloadError, this, &UpdateDialog::handleDownloadError);
connect(m_downloadButton, &QPushButton::clicked, this, &UpdateDialog::startDownload);
connect(m_closeButton, &QPushButton::clicked, this, &QDialog::reject);
m_updater->checkForUpdates();
}
void UpdateDialog::checkForUpdatesAndShow(QWidget *parent)
{
auto *dialog = new UpdateDialog(parent);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
}
void UpdateDialog::handleUpdateInfo(const PluginUpdater::UpdateInfo &info)
{
m_releaseLink->setText(
tr("<a href='https://github.com/Palm1r/QodeAssist/releases'>You can also download "
"from GitHub Releases</a>"));
if (info.incompatibleIdeVersion) {
m_titleLabel->setText(tr("Incompatible Qt Creator Version"));
m_versionLabel->setText(tr("This update requires Qt Creator %1, current is %2.\n"
"Please upgrade Qt Creator to use this version of QodeAssist.")
.arg(info.targetIdeVersion, info.currentIdeVersion));
m_downloadButton->setEnabled(false);
return;
}
if (!info.isUpdateAvailable) {
m_titleLabel->setText(tr("QodeAssist is up to date"));
m_downloadButton->setEnabled(false);
return;
}
m_titleLabel->setText(tr("A new version of QodeAssist is available!"));
m_versionLabel->setText(tr("Version %1 is now available - you have %2")
.arg(info.version, m_updater->currentVersion()));
if (!info.changeLog.isEmpty()) {
if (!m_changelogLabel) {
m_changelogLabel = new QLabel(tr("Release Notes:"), this);
m_layout->insertWidget(2, m_changelogLabel);
m_changelogText = new QTextEdit(this);
m_changelogText->setReadOnly(true);
m_changelogText->setMaximumHeight(200);
m_layout->insertWidget(3, m_changelogText);
}
m_changelogText->setText(info.changeLog);
}
m_downloadButton->setEnabled(true);
m_updateInfo = info;
}
void UpdateDialog::startDownload()
{
m_downloadButton->setEnabled(false);
m_progress->setVisible(true);
m_updater->downloadUpdate(m_updateInfo.downloadUrl);
}
void UpdateDialog::updateProgress(qint64 received, qint64 total)
{
m_progress->setMaximum(total);
m_progress->setValue(received);
}
void UpdateDialog::handleDownloadFinished(const QString &path)
{
m_progress->setVisible(false);
QMessageBox::information(this, tr("Update Successful"), tr("Update has been downloaded."));
accept();
}
void UpdateDialog::handleDownloadError(const QString &error)
{
m_progress->setVisible(false);
m_downloadButton->setEnabled(true);
QMessageBox::critical(this, tr("Update Error"), tr("Failed to update: %1").arg(error));
}
} // namespace QodeAssist

64
settings/UpdateDialog.hpp Normal file
View File

@ -0,0 +1,64 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDialog>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QTextEdit>
#include <QVBoxLayout>
#include "PluginUpdater.hpp"
namespace QodeAssist {
class UpdateDialog : public QDialog
{
Q_OBJECT
public:
explicit UpdateDialog(QWidget *parent = nullptr);
static void checkForUpdatesAndShow(QWidget *parent = nullptr);
private slots:
void handleUpdateInfo(const PluginUpdater::UpdateInfo &info);
void startDownload();
void updateProgress(qint64 received, qint64 total);
void handleDownloadFinished(const QString &path);
void handleDownloadError(const QString &error);
private:
PluginUpdater *m_updater;
QVBoxLayout *m_layout;
QLabel *m_titleLabel;
QLabel *m_versionLabel;
QLabel *m_releaseLink;
QLabel *m_changelogLabel{nullptr};
QTextEdit *m_changelogText{nullptr};
QProgressBar *m_progress;
QPushButton *m_downloadButton;
QPushButton *m_closeButton;
PluginUpdater::UpdateInfo m_updateInfo;
};
} // namespace QodeAssist

39
templates/OpenAI.hpp Normal file
View File

@ -0,0 +1,39 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonArray>
#include "llmcore/PromptTemplate.hpp"
namespace QodeAssist::Templates {
class OpenAI : public LLMCore::PromptTemplate
{
public:
LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
QString name() const override { return "OpenAI"; }
QString promptTemplate() const override { return {}; }
QStringList stopWords() const override { return QStringList(); }
void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override {}
QString description() const override { return "OpenAI"; }
};
} // namespace QodeAssist::Templates

View File

@ -30,6 +30,7 @@
#include "templates/Llama2.hpp"
#include "templates/Llama3.hpp"
#include "templates/Ollama.hpp"
#include "templates/OpenAI.hpp"
#include "templates/Qwen.hpp"
#include "templates/StarCoder2Fim.hpp"
@ -51,6 +52,7 @@ inline void registerTemplates()
templateManager.registerTemplate<Alpaca>();
templateManager.registerTemplate<Llama2>();
templateManager.registerTemplate<Claude>();
templateManager.registerTemplate<OpenAI>();
}
} // namespace QodeAssist::Templates