Upgrade to version 0.4.4

* feat: Add attachments for message
* feat: Support QtC color palette for chat view
* feat: Improve code completion from non-FIM models
* refactor: Removed trimming messages
* chore: Bump version to 0.4.4
This commit is contained in:
Petr Mironychev 2025-01-08 02:05:25 +01:00 committed by GitHub
parent 35012865c7
commit 511f5b36eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 734 additions and 147 deletions

View File

@ -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

View File

@ -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<int, QByteArray> ChatModel::roleNames() const
QHash<int, QByteArray> 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<Context::ContentFile> &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::Message> 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<MessagePart> 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;

View File

@ -26,6 +26,8 @@
#include <QJsonArray>
#include <QtQmlIntegration>
#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<Context::ContentFile> attachments;
};
explicit ChatModel(QObject *parent = nullptr);
@ -55,7 +59,11 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> 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<Context::ContentFile> &attachments = {});
Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
@ -74,7 +82,6 @@ signals:
void modelReseted();
private:
void trim();
int estimateTokenCount(const QString &text) const;
QVector<Message> m_messages;

View File

@ -21,6 +21,7 @@
#include <QClipboard>
#include <QFileDialog>
#include <QMessageBox>
#include <coreplugin/icore.h>
#include <projectexplorer/project.h>
@ -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

View File

@ -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 @@ public:
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 @@ private:
QColor m_secondaryColor;
QColor m_codeColor;
QString m_recentFilePath;
QStringList m_attachmentFiles;
};
} // namespace QodeAssist::Chat

View File

@ -33,6 +33,7 @@
#include <texteditor/texteditor.h>
#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<QString> &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();

View File

@ -36,7 +36,10 @@ public:
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
~ClientInterface();
void sendMessage(const QString &message, bool includeCurrentFile = false);
void sendMessage(
const QString &message,
const QList<QString> &attachments = {},
bool includeCurrentFile = false);
void clearMessages();
void cancelRequest();

View File

@ -0,0 +1,10 @@
<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"/>
</g>
<defs>
<clipPath id="clip0_37_14">
<rect width="24" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 555 B

View File

@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_41_14)">
<path d="M0 0L24 24M0 24L24 0" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_41_14">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 353 B

View File

@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_41_14)">
<path d="M0 0L24 24M0 24L24 0" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_41_14">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 353 B

View File

@ -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
}
}

View File

@ -17,28 +17,28 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
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 {

View File

@ -17,56 +17,59 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
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
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
Button {
text: qsTr("Save")
onClicked: root.showSaveDialog()
}
Button {
text: qsTr("Load")
onClicked: root.showLoadDialog()
}
Button {
text: qsTr("Clear")
onClicked: root.clearChat()
}
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 {
AttachedFilesPlace {
id: attachedFilesPlace
Layout.fillWidth: true
spacing: 5
attachedFilesModel: root.attachmentFiles
}
Button {
id: sendButton
BottomBar {
id: bottomBar
Layout.alignment: Qt.AlignBottom
text: qsTr("Send")
onClicked: root.sendChatMessage()
}
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
Button {
id: stopButton
Layout.alignment: Qt.AlignBottom
text: qsTr("Stop")
onClicked: root.cancelRequest()
}
CheckBox {
id: sharingCurrentFile
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()
}

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/>.
*/
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}}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
}

View File

@ -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);
}

View File

@ -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",

View File

@ -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())

View File

@ -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

View File

@ -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

View File

@ -25,7 +25,7 @@
#include <QTimer>
#include <texteditor/textdocument.h>
namespace QodeAssist {
namespace QodeAssist::Context {
class ChangesManager : public QObject
{
@ -58,4 +58,4 @@ private:
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
};
} // namespace QodeAssist
} // namespace QodeAssist::Context

32
context/ContentFile.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
namespace QodeAssist::Context {
struct ContentFile
{
QString filename;
QString content;
};
} // namespace QodeAssist::Context

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "ContextManager.hpp"
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
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<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
{
QList<ContentFile> 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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QString>
#include "ContentFile.hpp"
namespace QodeAssist::Context {
class ContextManager : public QObject
{
Q_OBJECT
public:
static ContextManager &instance();
QString readFile(const QString &filePath) const;
QList<ContentFile> 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

View File

@ -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

View File

@ -24,7 +24,7 @@
#include <llmcore/ContextData.hpp>
namespace QodeAssist {
namespace QodeAssist::Context {
struct CopyrightInfo
{
@ -61,4 +61,4 @@ private:
CopyrightInfo m_copyrightInfo;
};
} // namespace QodeAssist
} // namespace QodeAssist::Context

View File

@ -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;

View File

@ -53,7 +53,7 @@ public:
MessageBuilder &addSuffix(const QString &content);
MessageBuilder &addtTokenizer(PromptTemplate *promptTemplate);
MessageBuilder &addTokenizer(PromptTemplate *promptTemplate);
QString roleToString(MessageRole role) const;

View File

@ -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);

View File

@ -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: <code_context>"
"\nBefore: %1After: %2\n </code_context>");
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}}};

View File

@ -66,6 +66,8 @@ public:
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};

View File

@ -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";

View File

@ -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: <code_context>"
"\nBefore: %1\nAfter: %2\n </code_context>")
.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

View File

@ -50,8 +50,7 @@ inline void registerTemplates()
templateManager.registerTemplate<ChatML>();
templateManager.registerTemplate<Alpaca>();
templateManager.registerTemplate<Llama2>();
templateManager.registerTemplate<ClaudeCodeCompletion>();
templateManager.registerTemplate<ClaudeChat>();
templateManager.registerTemplate<Claude>();
}
} // namespace QodeAssist::Templates