diff --git a/ChatView/CMakeLists.txt b/ChatView/CMakeLists.txt index 18e5ab9..06f1323 100644 --- a/ChatView/CMakeLists.txt +++ b/ChatView/CMakeLists.txt @@ -1,8 +1,8 @@ qt_add_library(QodeAssistChatView STATIC) qt_policy(SET QTP0001 NEW) +qt_policy(SET QTP0004 NEW) -# URI name should match the subdirectory name to suppress the warning qt_add_qml_module(QodeAssistChatView URI ChatView VERSION 1.0 @@ -13,6 +13,14 @@ qt_add_qml_module(QodeAssistChatView qml/Badge.qml qml/dialog/CodeBlock.qml qml/dialog/TextBlock.qml + qml/controls/QoAButton.qml + qml/parts/TopBar.qml + qml/parts/BottomBar.qml + qml/parts/AttachedFilesPlace.qml + RESOURCES + icons/attach-file.svg + icons/close-dark.svg + icons/close-light.svg SOURCES ChatWidget.hpp ChatWidget.cpp ChatModel.hpp ChatModel.cpp diff --git a/ChatView/ChatModel.cpp b/ChatView/ChatModel.cpp index 102095e..fcaa080 100644 --- a/ChatView/ChatModel.cpp +++ b/ChatView/ChatModel.cpp @@ -55,6 +55,13 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const case Roles::Content: { return message.content; } + case Roles::Attachments: { + QStringList filenames; + for (const auto &attachment : message.attachments) { + filenames << attachment.filename; + } + return filenames; + } default: return QVariant(); } @@ -65,28 +72,43 @@ QHash ChatModel::roleNames() const QHash roles; roles[Roles::RoleType] = "roleType"; roles[Roles::Content] = "content"; + roles[Roles::Attachments] = "attachments"; return roles; } -void ChatModel::addMessage(const QString &content, ChatRole role, const QString &id) +void ChatModel::addMessage( + const QString &content, + ChatRole role, + const QString &id, + const QList &attachments) { - int tokenCount = estimateTokenCount(content); + QString fullContent = content; + if (!attachments.isEmpty()) { + fullContent += "\n\nAttached files list:"; + for (const auto &attachment : attachments) { + fullContent += QString("\nname: %1\nfile content:\n%2") + .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()); - m_messages.append({role, content, tokenCount, id}); + Message newMessage{role, content, tokenCount, id}; + newMessage.attachments = attachments; + m_messages.append(newMessage); m_totalTokens += tokenCount; endInsertRows(); } - trim(); emit totalTokensChanged(); } @@ -95,20 +117,6 @@ QVector ChatModel::getChatHistory() const return m_messages; } -void ChatModel::trim() -{ - while (m_totalTokens > tokensThreshold()) { - if (!m_messages.isEmpty()) { - m_totalTokens -= m_messages.first().tokenCount; - beginRemoveRows(QModelIndex(), 0, 0); - m_messages.removeFirst(); - endRemoveRows(); - } else { - break; - } - } -} - int ChatModel::estimateTokenCount(const QString &text) const { return text.length() / 4; @@ -156,7 +164,6 @@ QList ChatModel::processMessageContent(const QString &content) cons QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const { QJsonArray messages; - messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}}); for (const auto &message : m_messages) { @@ -171,7 +178,22 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con default: continue; } - messages.append(QJsonObject{{"role", role}, {"content", message.content}}); + + QString content + = message.attachments.isEmpty() + ? message.content + : message.content + "\n\nAttached files list:" + + std::accumulate( + message.attachments.begin(), + message.attachments.end(), + QString(), + [](QString acc, const Context::ContentFile &attachment) { + return acc + + QString("\nname: %1\nfile content:\n%2") + .arg(attachment.filename, attachment.content); + }); + + messages.append(QJsonObject{{"role", role}, {"content", content}}); } return messages; diff --git a/ChatView/ChatModel.hpp b/ChatView/ChatModel.hpp index 2bc0369..3811046 100644 --- a/ChatView/ChatModel.hpp +++ b/ChatView/ChatModel.hpp @@ -26,6 +26,8 @@ #include #include +#include "context/ContentFile.hpp" + namespace QodeAssist::Chat { class ChatModel : public QAbstractListModel @@ -36,17 +38,19 @@ class ChatModel : public QAbstractListModel QML_ELEMENT public: - enum Roles { RoleType = Qt::UserRole, Content }; - enum ChatRole { System, User, Assistant }; Q_ENUM(ChatRole) + enum Roles { RoleType = Qt::UserRole, Content, Attachments }; + struct Message { ChatRole role; QString content; int tokenCount; QString id; + + QList attachments; }; explicit ChatModel(QObject *parent = nullptr); @@ -55,7 +59,11 @@ class ChatModel : public QAbstractListModel QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; - Q_INVOKABLE void addMessage(const QString &content, ChatRole role, const QString &id); + Q_INVOKABLE void addMessage( + const QString &content, + ChatRole role, + const QString &id, + const QList &attachments = {}); Q_INVOKABLE void clear(); Q_INVOKABLE QList processMessageContent(const QString &content) const; @@ -74,7 +82,6 @@ class ChatModel : public QAbstractListModel void modelReseted(); private: - void trim(); int estimateTokenCount(const QString &text) const; QVector m_messages; diff --git a/ChatView/ChatRootView.cpp b/ChatView/ChatRootView.cpp index 93fd2eb..72b1b94 100644 --- a/ChatView/ChatRootView.cpp +++ b/ChatView/ChatRootView.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -75,9 +76,26 @@ QColor ChatRootView::backgroundColor() const return Utils::creatorColor(Utils::Theme::BackgroundColorNormal); } -void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) const +void ChatRootView::sendMessage(const QString &message, bool sharingCurrentFile) { - m_clientInterface->sendMessage(message, sharingCurrentFile); + if (m_chatModel->totalTokens() > m_chatModel->tokensThreshold()) { + QMessageBox::StandardButton reply = QMessageBox::question( + Core::ICore::dialogParent(), + tr("Token Limit Exceeded"), + tr("The chat history has exceeded the token limit.\n" + "Would you like to create new chat?"), + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + autosave(); + m_chatModel->clear(); + m_recentFilePath = QString{}; + return; + } + } + + m_clientInterface->sendMessage(message, m_attachmentFiles, sharingCurrentFile); + clearAttachmentFiles(); } void ChatRootView::copyToClipboard(const QString &text) @@ -90,6 +108,14 @@ void ChatRootView::cancelRequest() m_clientInterface->cancelRequest(); } +void ChatRootView::clearAttachmentFiles() +{ + if (!m_attachmentFiles.isEmpty()) { + m_attachmentFiles.clear(); + emit attachmentFilesChanged(); + } +} + void ChatRootView::generateColors() { QColor baseColor = backgroundColor(); @@ -293,4 +319,22 @@ QString ChatRootView::getAutosaveFilePath() const return QDir(dir).filePath(getSuggestedFileName() + ".json"); } +void ChatRootView::showAttachFilesDialog() +{ + 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 filePaths = dialog.selectedFiles(); + if (!filePaths.isEmpty()) { + m_attachmentFiles = filePaths; + emit attachmentFilesChanged(); + } + } +} + } // namespace QodeAssist::Chat diff --git a/ChatView/ChatRootView.hpp b/ChatView/ChatRootView.hpp index 81681c8..b116a5d 100644 --- a/ChatView/ChatRootView.hpp +++ b/ChatView/ChatRootView.hpp @@ -37,6 +37,8 @@ class ChatRootView : public QQuickItem 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) + QML_ELEMENT public: @@ -62,16 +64,19 @@ class ChatRootView : public QQuickItem void autosave(); QString getAutosaveFilePath() const; + Q_INVOKABLE void showAttachFilesDialog(); + public slots: - void sendMessage(const QString &message, bool sharingCurrentFile = false) const; + void sendMessage(const QString &message, bool sharingCurrentFile = false); void copyToClipboard(const QString &text); void cancelRequest(); + void clearAttachmentFiles(); signals: void chatModelChanged(); void currentTemplateChanged(); - void isSharingCurrentFileChanged(); + void attachmentFilesChanged(); private: void generateColors(); @@ -90,6 +95,7 @@ public slots: QColor m_secondaryColor; QColor m_codeColor; QString m_recentFilePath; + QStringList m_attachmentFiles; }; } // namespace QodeAssist::Chat diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index 5e87b70..d46bc89 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -33,6 +33,7 @@ #include #include "ChatAssistantSettings.hpp" +#include "ContextManager.hpp" #include "GeneralSettings.hpp" #include "Logger.hpp" #include "PromptTemplateManager.hpp" @@ -64,11 +65,13 @@ ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent) ClientInterface::~ClientInterface() = default; -void ClientInterface::sendMessage(const QString &message, bool includeCurrentFile) +void ClientInterface::sendMessage( + const QString &message, const QList &attachments, bool includeCurrentFile) { cancelRequest(); - m_chatModel->addMessage(message, ChatModel::ChatRole::User, ""); + auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments); + m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles); auto &chatAssistantSettings = Settings::chatAssistantSettings(); diff --git a/ChatView/ClientInterface.hpp b/ChatView/ClientInterface.hpp index e47215a..4883a1b 100644 --- a/ChatView/ClientInterface.hpp +++ b/ChatView/ClientInterface.hpp @@ -36,7 +36,10 @@ class ClientInterface : public QObject explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr); ~ClientInterface(); - void sendMessage(const QString &message, bool includeCurrentFile = false); + void sendMessage( + const QString &message, + const QList &attachments = {}, + bool includeCurrentFile = false); void clearMessages(); void cancelRequest(); diff --git a/ChatView/icons/attach-file.svg b/ChatView/icons/attach-file.svg new file mode 100644 index 0000000..2272b2a --- /dev/null +++ b/ChatView/icons/attach-file.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ChatView/icons/close-dark.svg b/ChatView/icons/close-dark.svg new file mode 100644 index 0000000..662e796 --- /dev/null +++ b/ChatView/icons/close-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ChatView/icons/close-light.svg b/ChatView/icons/close-light.svg new file mode 100644 index 0000000..f1c2016 --- /dev/null +++ b/ChatView/icons/close-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ChatView/qml/Badge.qml b/ChatView/qml/Badge.qml index 354e580..ef09f27 100644 --- a/ChatView/qml/Badge.qml +++ b/ChatView/qml/Badge.qml @@ -23,18 +23,18 @@ Rectangle { id: root property alias text: badgeText.text - property alias fontColor: badgeText.color implicitWidth: badgeText.implicitWidth + root.radius implicitHeight: badgeText.implicitHeight + 6 - color: "lightgreen" + color: palette.button radius: root.height / 2 + border.color: palette.mid border.width: 1 - border.color: "gray" Text { id: badgeText anchors.centerIn: parent + color: palette.buttonText } } diff --git a/ChatView/qml/ChatItem.qml b/ChatView/qml/ChatItem.qml index 34b4a17..47ce2cc 100644 --- a/ChatView/qml/ChatItem.qml +++ b/ChatView/qml/ChatItem.qml @@ -17,28 +17,28 @@ * along with QodeAssist. If not, see . */ -pragma ComponentBehavior: Bound - import QtQuick import ChatView +import QtQuick.Layouts import "./dialog" Rectangle { id: root property alias msgModel: msgCreator.model + property alias messageAttachments: attachmentsModel.model property color fontColor property color codeBgColor property color selectionColor - height: msgColumn.height + height: msgColumn.implicitHeight + 10 radius: 8 - Column { + ColumnLayout { id: msgColumn - anchors.verticalCenter: parent.verticalCenter width: parent.width + anchors.verticalCenter: parent.verticalCenter spacing: 5 Repeater { @@ -80,6 +80,38 @@ Rectangle { } } } + + Flow { + id: attachmentsFlow + + Layout.fillWidth: true + visible: attachmentsModel.model && attachmentsModel.model.length > 0 + leftPadding: 10 + rightPadding: 10 + spacing: 5 + + Repeater { + id: attachmentsModel + + delegate: Rectangle { + required property int index + required property var modelData + + height: attachText.implicitHeight + 8 + width: attachText.implicitWidth + 16 + radius: 4 + color: root.codeBgColor + + Text { + id: attachText + + anchors.centerIn: parent + text: modelData + color: root.fontColor + } + } + } + } } component TextComponent : TextBlock { diff --git a/ChatView/qml/RootItem.qml b/ChatView/qml/RootItem.qml index 4c0e6e0..eadc9ab 100644 --- a/ChatView/qml/RootItem.qml +++ b/ChatView/qml/RootItem.qml @@ -17,56 +17,59 @@ * along with QodeAssist. If not, see . */ -pragma ComponentBehavior: Bound - import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic as QQC import QtQuick.Layouts import ChatView +import "./controls" +import "./parts" ChatRootView { id: root + property SystemPalette sysPalette: SystemPalette { + colorGroup: SystemPalette.Active + } + + palette { + window: sysPalette.window + windowText: sysPalette.windowText + base: sysPalette.base + alternateBase: sysPalette.alternateBase + text: sysPalette.text + button: sysPalette.button + buttonText: sysPalette.buttonText + highlight: sysPalette.highlight + highlightedText: sysPalette.highlightedText + light: sysPalette.light + mid: sysPalette.mid + dark: sysPalette.dark + shadow: sysPalette.shadow + brightText: sysPalette.brightText + } + Rectangle { id: bg + anchors.fill: parent - color: root.backgroundColor + color: palette.window } ColumnLayout { anchors.fill: parent - RowLayout { + TopBar { id: topBar - Layout.leftMargin: 5 - Layout.rightMargin: 5 - spacing: 10 - - Button { - text: qsTr("Save") - onClicked: root.showSaveDialog() - } - - Button { - text: qsTr("Load") - onClicked: root.showLoadDialog() - } - - Button { - text: qsTr("Clear") - onClicked: root.clearChat() - } + Layout.preferredWidth: parent.width + Layout.preferredHeight: 40 - Item { - Layout.fillWidth: true - } - - Badge { + saveButton.onClicked: root.showSaveDialog() + loadButton.onClicked: root.showLoadDialog() + clearButton.onClicked: root.clearChat() + tokensBadge { text: qsTr("tokens:%1/%2").arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold) - color: root.codeColor - fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white" } } @@ -84,8 +87,10 @@ ChatRootView { delegate: ChatItem { required property var model + 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 @@ -143,32 +148,23 @@ ChatRootView { } } - RowLayout { - Layout.fillWidth: true - spacing: 5 - - Button { - id: sendButton + AttachedFilesPlace { + id: attachedFilesPlace - Layout.alignment: Qt.AlignBottom - text: qsTr("Send") - onClicked: root.sendChatMessage() - } - - Button { - id: stopButton + Layout.fillWidth: true + attachedFilesModel: root.attachmentFiles + } - Layout.alignment: Qt.AlignBottom - text: qsTr("Stop") - onClicked: root.cancelRequest() - } + BottomBar { + id: bottomBar - CheckBox { - id: sharingCurrentFile + Layout.preferredWidth: parent.width + Layout.preferredHeight: 40 - text: "Share current file with models" - checked: root.isSharingCurrentFile - } + sendButton.onClicked: root.sendChatMessage() + stopButton.onClicked: root.cancelRequest() + sharingCurrentFile.checked: root.isSharingCurrentFile + attachFiles.onClicked: root.showAttachFilesDialog() } } @@ -181,7 +177,7 @@ ChatRootView { } function sendChatMessage() { - root.sendMessage(messageInput.text, sharingCurrentFile.checked) + root.sendMessage(messageInput.text, bottomBar.sharingCurrentFile.checked) messageInput.text = "" scrollToBottom() } diff --git a/ChatView/qml/controls/QoAButton.qml b/ChatView/qml/controls/QoAButton.qml new file mode 100644 index 0000000..263f4bb --- /dev/null +++ b/ChatView/qml/controls/QoAButton.qml @@ -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 . + */ + +import QtQuick +import QtQuick.Controls.Basic + +Button { + id: control + + padding: 4 + + icon.width: 16 + icon.height: 16 + + contentItem.height: 20 + + background: Rectangle { + id: bg + + implicitHeight: 20 + + color: !control.enabled || !control.down ? control.palette.button : control.palette.dark + border.color: !control.enabled || (!control.hovered && !control.visualFocus) ? control.palette.mid : control.palette.highlight + border.width: 1 + radius: 4 + + Rectangle { + anchors.fill: bg + radius: bg.radius + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.alpha(control.palette.highlight, 0.4) } + GradientStop { position: 1.0; color: Qt.alpha(control.palette.highlight, 0.2) } + } + opacity: control.hovered ? 0.3 : 0.01 + Behavior on opacity {NumberAnimation{duration: 250}} + } + } +} diff --git a/ChatView/qml/parts/AttachedFilesPlace.qml b/ChatView/qml/parts/AttachedFilesPlace.qml new file mode 100644 index 0000000..a930d94 --- /dev/null +++ b/ChatView/qml/parts/AttachedFilesPlace.qml @@ -0,0 +1,90 @@ +/* + * 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 . + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ChatView + +Flow { + id: attachFilesPlace + + property alias attachedFilesModel: attachRepeater.model + + spacing: 5 + leftPadding: 5 + rightPadding: 5 + + Repeater { + id: attachRepeater + + delegate: Rectangle { + required property int index + required property string modelData + + height: 30 + width: fileNameText.width + closeButton.width + 20 + radius: 4 + color: palette.button + + Row { + spacing: 5 + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 5 + + Text { + id: fileNameText + + anchors.verticalCenter: parent.verticalCenter + color: palette.buttonText + + text: { + const parts = modelData.split('/'); + return parts[parts.length - 1]; + } + } + + MouseArea { + id: closeButton + + anchors.verticalCenter: parent.verticalCenter + width: closeIcon.width + height: closeButton.width + + onClicked: { + const newList = [...root.attachmentFiles]; + newList.splice(index, 1); + root.attachmentFiles = newList; + } + + Image { + id: closeIcon + + anchors.centerIn: parent + source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg" + : "qrc:/qt/qml/ChatView/icons/close-light.svg" + width: 6 + height: 6 + } + } + } + } + } +} diff --git a/ChatView/qml/parts/BottomBar.qml b/ChatView/qml/parts/BottomBar.qml new file mode 100644 index 0000000..75005be --- /dev/null +++ b/ChatView/qml/parts/BottomBar.qml @@ -0,0 +1,83 @@ +/* + * 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 . + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ChatView + +Rectangle { + id: root + + property alias sendButton: sendButtonId + property alias stopButton: stopButtonId + property alias sharingCurrentFile: sharingCurrentFileId + property alias attachFiles: attachFilesId + + color: palette.window.hslLightness > 0.5 ? + Qt.darker(palette.window, 1.1) : + Qt.lighter(palette.window, 1.1) + + RowLayout { + id: bottomBar + + anchors { + left: parent.left + leftMargin: 5 + right: parent.right + rightMargin: 5 + verticalCenter: parent.verticalCenter + } + + spacing: 10 + + QoAButton { + id: sendButtonId + + text: qsTr("Send") + } + + QoAButton { + id: stopButtonId + + 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" + height: 15 + width: 8 + } + text: qsTr("Attach files") + } + + Item { + Layout.fillWidth: true + } + } +} diff --git a/ChatView/qml/parts/TopBar.qml b/ChatView/qml/parts/TopBar.qml new file mode 100644 index 0000000..721ab24 --- /dev/null +++ b/ChatView/qml/parts/TopBar.qml @@ -0,0 +1,73 @@ +/* + * 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 . + */ + +import QtQuick +import QtQuick.Layouts +import ChatView + +Rectangle { + id: root + + property alias saveButton: saveButtonId + property alias loadButton: loadButtonId + property alias clearButton: clearButtonId + property alias tokensBadge: tokensBadgeId + + color: palette.window.hslLightness > 0.5 ? + Qt.darker(palette.window, 1.1) : + Qt.lighter(palette.window, 1.1) + + RowLayout { + anchors { + left: parent.left + leftMargin: 5 + right: parent.right + rightMargin: 5 + verticalCenter: parent.verticalCenter + } + + spacing: 10 + + QoAButton { + id: saveButtonId + + text: qsTr("Save") + } + + QoAButton { + id: loadButtonId + + text: qsTr("Load") + } + + QoAButton { + id: clearButtonId + + text: qsTr("Clear") + } + + Item { + Layout.fillWidth: true + } + + Badge { + id: tokensBadgeId + } + } +} diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index 2d419e4..1c547e7 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -194,11 +194,18 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) if (!updatedContext.fileContext.isEmpty()) systemPrompt.append(updatedContext.fileContext); + QString userMessage; + if (completeSettings.useUserMessageTemplateForCC() && promptTemplate->type() == LLMCore::TemplateType::Chat) { + userMessage = completeSettings.userMessageTemplateForCC().arg(updatedContext.prefix, updatedContext.suffix); + } else { + userMessage = updatedContext.prefix; + } + auto message = LLMCore::MessageBuilder() .addSystemMessage(systemPrompt) - .addUserMessage(updatedContext.prefix) + .addUserMessage(userMessage) .addSuffix(updatedContext.suffix) - .addtTokenizer(promptTemplate); + .addTokenizer(promptTemplate); message.saveTo( config.providerRequest, @@ -235,7 +242,7 @@ LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &reque int cursorPosition = position["character"].toInt(); int lineNumber = position["line"].toInt(); - DocumentContextReader reader(textDocument); + Context::DocumentContextReader reader(textDocument); return reader.prepareContext(lineNumber, cursorPosition); } diff --git a/QodeAssist.json.in b/QodeAssist.json.in index 3a2a594..84c1cc8 100644 --- a/QodeAssist.json.in +++ b/QodeAssist.json.in @@ -1,7 +1,7 @@ { "Id" : "qodeassist", "Name" : "QodeAssist", - "Version" : "0.4.3", + "Version" : "0.4.4", "Vendor" : "Petr Mironychev", "VendorId" : "petrmironychev", "Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd", diff --git a/QodeAssistClient.cpp b/QodeAssistClient.cpp index 3055161..1b2f01b 100644 --- a/QodeAssistClient.cpp +++ b/QodeAssistClient.cpp @@ -88,7 +88,8 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document) return; if (Settings::codeCompletionSettings().useProjectChangesCache()) - ChangesManager::instance().addChange(document, position, charsRemoved, charsAdded); + Context::ChangesManager::instance() + .addChange(document, position, charsRemoved, charsAdded); TextEditorWidget *widget = textEditor->editorWidget(); if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors()) diff --git a/context/CMakeLists.txt b/context/CMakeLists.txt index 2bc8709..2a88e7b 100644 --- a/context/CMakeLists.txt +++ b/context/CMakeLists.txt @@ -1,6 +1,8 @@ add_library(Context STATIC DocumentContextReader.hpp DocumentContextReader.cpp ChangesManager.h ChangesManager.cpp + ContextManager.hpp ContextManager.cpp + ContentFile.hpp ) target_link_libraries(Context diff --git a/context/ChangesManager.cpp b/context/ChangesManager.cpp index deabfc4..d79147a 100644 --- a/context/ChangesManager.cpp +++ b/context/ChangesManager.cpp @@ -20,7 +20,7 @@ #include "ChangesManager.h" #include "CodeCompletionSettings.hpp" -namespace QodeAssist { +namespace QodeAssist::Context { ChangesManager &ChangesManager::instance() { @@ -79,4 +79,4 @@ QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument * return context; } -} // namespace QodeAssist +} // namespace QodeAssist::Context diff --git a/context/ChangesManager.h b/context/ChangesManager.h index d9c4c91..85f9501 100644 --- a/context/ChangesManager.h +++ b/context/ChangesManager.h @@ -25,7 +25,7 @@ #include #include -namespace QodeAssist { +namespace QodeAssist::Context { class ChangesManager : public QObject { @@ -58,4 +58,4 @@ class ChangesManager : public QObject QHash> m_documentChanges; }; -} // namespace QodeAssist +} // namespace QodeAssist::Context diff --git a/context/ContentFile.hpp b/context/ContentFile.hpp new file mode 100644 index 0000000..4c79829 --- /dev/null +++ b/context/ContentFile.hpp @@ -0,0 +1,32 @@ +/* + * 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 . + */ + +#pragma once + +#include + +namespace QodeAssist::Context { + +struct ContentFile +{ + QString filename; + QString content; +}; + +} // namespace QodeAssist::Context diff --git a/context/ContextManager.cpp b/context/ContextManager.cpp new file mode 100644 index 0000000..cce1aba --- /dev/null +++ b/context/ContextManager.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "ContextManager.hpp" + +#include +#include +#include + +namespace QodeAssist::Context { + +ContextManager &ContextManager::instance() +{ + static ContextManager manager; + return manager; +} + +ContextManager::ContextManager(QObject *parent) + : QObject(parent) +{} + +QString ContextManager::readFile(const QString &filePath) const +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return QString(); + + QTextStream in(&file); + return in.readAll(); +} + +QList ContextManager::getContentFiles(const QStringList &filePaths) const +{ + QList files; + for (const QString &path : filePaths) { + ContentFile contentFile = createContentFile(path); + files.append(contentFile); + } + return files; +} + +ContentFile ContextManager::createContentFile(const QString &filePath) const +{ + ContentFile contentFile; + QFileInfo fileInfo(filePath); + contentFile.filename = fileInfo.fileName(); + contentFile.content = readFile(filePath); + return contentFile; +} + +} // namespace QodeAssist::Context diff --git a/context/ContextManager.hpp b/context/ContextManager.hpp new file mode 100644 index 0000000..78e60f7 --- /dev/null +++ b/context/ContextManager.hpp @@ -0,0 +1,46 @@ +/* + * 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 . + */ + +#pragma once + +#include +#include + +#include "ContentFile.hpp" + +namespace QodeAssist::Context { + +class ContextManager : public QObject +{ + Q_OBJECT + +public: + static ContextManager &instance(); + QString readFile(const QString &filePath) const; + QList getContentFiles(const QStringList &filePaths) const; + +private: + explicit ContextManager(QObject *parent = nullptr); + ~ContextManager() = default; + ContextManager(const ContextManager &) = delete; + ContextManager &operator=(const ContextManager &) = delete; + ContentFile createContentFile(const QString &filePath) const; +}; + +} // namespace QodeAssist::Context diff --git a/context/DocumentContextReader.cpp b/context/DocumentContextReader.cpp index 7f94b44..e6f2374 100644 --- a/context/DocumentContextReader.cpp +++ b/context/DocumentContextReader.cpp @@ -47,7 +47,7 @@ const QRegularExpression &getCommentRegex() return commentRegex; } -namespace QodeAssist { +namespace QodeAssist::Context { DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument) : m_textDocument(textDocument) @@ -247,4 +247,4 @@ QString DocumentContextReader::getContextAfter(int lineNumber, int cursorPositio } } -} // namespace QodeAssist +} // namespace QodeAssist::Context diff --git a/context/DocumentContextReader.hpp b/context/DocumentContextReader.hpp index e2e6268..ae46e27 100644 --- a/context/DocumentContextReader.hpp +++ b/context/DocumentContextReader.hpp @@ -24,7 +24,7 @@ #include -namespace QodeAssist { +namespace QodeAssist::Context { struct CopyrightInfo { @@ -61,4 +61,4 @@ class DocumentContextReader CopyrightInfo m_copyrightInfo; }; -} // namespace QodeAssist +} // namespace QodeAssist::Context diff --git a/llmcore/MessageBuilder.cpp b/llmcore/MessageBuilder.cpp index 9c65840..cf25fde 100644 --- a/llmcore/MessageBuilder.cpp +++ b/llmcore/MessageBuilder.cpp @@ -40,7 +40,7 @@ QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addSuf return *this; } -QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addtTokenizer( +QodeAssist::LLMCore::MessageBuilder &QodeAssist::LLMCore::MessageBuilder::addTokenizer( PromptTemplate *promptTemplate) { m_promptTemplate = promptTemplate; diff --git a/llmcore/MessageBuilder.hpp b/llmcore/MessageBuilder.hpp index 8b0b734..f37bf6c 100644 --- a/llmcore/MessageBuilder.hpp +++ b/llmcore/MessageBuilder.hpp @@ -53,7 +53,7 @@ class MessageBuilder MessageBuilder &addSuffix(const QString &content); - MessageBuilder &addtTokenizer(PromptTemplate *promptTemplate); + MessageBuilder &addTokenizer(PromptTemplate *promptTemplate); QString roleToString(MessageRole role) const; diff --git a/settings/ChatAssistantSettings.cpp b/settings/ChatAssistantSettings.cpp index b5040ac..14fb9cf 100644 --- a/settings/ChatAssistantSettings.cpp +++ b/settings/ChatAssistantSettings.cpp @@ -47,7 +47,7 @@ ChatAssistantSettings::ChatAssistantSettings() 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(1000, 16000); + chatTokensThreshold.setRange(1, 900000); chatTokensThreshold.setDefaultValue(8000); sharingCurrentFile.setSettingsKey(Constants::CA_SHARING_CURRENT_FILE); diff --git a/settings/CodeCompletionSettings.cpp b/settings/CodeCompletionSettings.cpp index 87e4b2e..1d02973 100644 --- a/settings/CodeCompletionSettings.cpp +++ b/settings/CodeCompletionSettings.cpp @@ -89,7 +89,7 @@ CodeCompletionSettings::CodeCompletionSettings() maxTokens.setSettingsKey(Constants::CC_MAX_TOKENS); maxTokens.setLabelText(Tr::tr("Max Tokens:")); - maxTokens.setRange(-1, 10000); + maxTokens.setRange(-1, 900000); maxTokens.setDefaultValue(50); // Advanced Parameters @@ -164,6 +164,15 @@ CodeCompletionSettings::CodeCompletionSettings() "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."); + useUserMessageTemplateForCC.setSettingsKey(Constants::CC_USE_USER_TEMPLATE); + useUserMessageTemplateForCC.setDefaultValue(true); + useUserMessageTemplateForCC.setLabelText(Tr::tr("Use User Template for code completion message for non-FIM models")); + + userMessageTemplateForCC.setSettingsKey(Constants::CC_USER_TEMPLATE); + userMessageTemplateForCC.setDisplayStyle(Utils::StringAspect::TextEditDisplay); + userMessageTemplateForCC.setDefaultValue("Here is the code context with insertion points: " + "\nBefore: %1After: %2\n "); + useFilePathInContext.setSettingsKey(Constants::CC_USE_FILE_PATH_IN_CONTEXT); useFilePathInContext.setDefaultValue(true); useFilePathInContext.setLabelText(Tr::tr("Use File Path in Context")); @@ -228,6 +237,8 @@ CodeCompletionSettings::CodeCompletionSettings() auto contextItem = Column{Row{contextGrid, Stretch{1}}, Row{useSystemPrompt, Stretch{1}}, systemPrompt, + Row{useUserMessageTemplateForCC, Stretch{1}}, + userMessageTemplateForCC, Row{useFilePathInContext, Stretch{1}}, Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}}; diff --git a/settings/CodeCompletionSettings.hpp b/settings/CodeCompletionSettings.hpp index 5ca671d..4de0f9f 100644 --- a/settings/CodeCompletionSettings.hpp +++ b/settings/CodeCompletionSettings.hpp @@ -66,6 +66,8 @@ class CodeCompletionSettings : public Utils::AspectContainer Utils::IntegerAspect readStringsAfterCursor{this}; Utils::BoolAspect useSystemPrompt{this}; Utils::StringAspect systemPrompt{this}; + Utils::BoolAspect useUserMessageTemplateForCC{this}; + Utils::StringAspect userMessageTemplateForCC{this}; Utils::BoolAspect useFilePathInContext{this}; Utils::BoolAspect useProjectChangesCache{this}; Utils::IntegerAspect maxChangesCacheSize{this}; diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 1c163b0..5a6e051 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -95,6 +95,8 @@ const char CC_READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.ccReadStringsAfterCursor const char CC_USE_SYSTEM_PROMPT[] = "QodeAssist.ccUseSystemPrompt"; const char CC_USE_FILE_PATH_IN_CONTEXT[] = "QodeAssist.ccUseFilePathInContext"; const char CC_SYSTEM_PROMPT[] = "QodeAssist.ccSystemPrompt"; +const char CC_USE_USER_TEMPLATE[] = "QodeAssist.ccUseUserTemplate"; +const char CC_USER_TEMPLATE[] = "QodeAssist.ccUserTemplate"; const char CC_USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.ccUseProjectChangesCache"; const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize"; const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt"; diff --git a/templates/Claude.hpp b/templates/Claude.hpp index de38ba7..2e66026 100644 --- a/templates/Claude.hpp +++ b/templates/Claude.hpp @@ -25,48 +25,15 @@ namespace QodeAssist::Templates { -class ClaudeCodeCompletion : public LLMCore::PromptTemplate +class Claude : public LLMCore::PromptTemplate { public: LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; } - QString name() const override { return "Claude Code Completion"; } - QString promptTemplate() const override { return {}; } - QStringList stopWords() const override { return QStringList(); } - void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override - { - QJsonArray messages = request["messages"].toArray(); - - for (int i = 0; i < messages.size(); ++i) { - QJsonObject message = messages[i].toObject(); - QString role = message["role"].toString(); - QString content = message["content"].toString(); - - if (message["role"] == "user") { - message["content"] - = QString("Here is the code context with insertion points: " - "\nBefore: %1\nAfter: %2\n ") - .arg(context.prefix, context.suffix); - } else { - message["content"] = QString("%1").arg(content); - } - - messages[i] = message; - } - - request["messages"] = messages; - } - QString description() const override { return "Claude Chat for code completion"; } -}; - -class ClaudeChat : public LLMCore::PromptTemplate -{ -public: - LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; } - QString name() const override { return "Claude Chat"; } + QString name() const override { return "Claude"; } 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 "Claude Chat"; } + QString description() const override { return "Claude"; } }; } // namespace QodeAssist::Templates diff --git a/templates/Templates.hpp b/templates/Templates.hpp index b65ce8b..f7e4f01 100644 --- a/templates/Templates.hpp +++ b/templates/Templates.hpp @@ -50,8 +50,7 @@ inline void registerTemplates() templateManager.registerTemplate(); templateManager.registerTemplate(); templateManager.registerTemplate(); - templateManager.registerTemplate(); - templateManager.registerTemplate(); + templateManager.registerTemplate(); } } // namespace QodeAssist::Templates