mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-07-19 05:24:59 -04:00
Upgrade to version 0.3.0
new QML Chat Qwen chat model
This commit is contained in:
@ -1,24 +1,33 @@
|
||||
qt_add_library(QodeAssistChatView STATIC)
|
||||
|
||||
find_package(Qt6 COMPONENTS Core Widgets Quick QuickWidgets Network REQUIRED)
|
||||
|
||||
qt_add_qml_module(QodeAssistChatView
|
||||
URI ChatView
|
||||
VERSION 1.0
|
||||
QML_FILES
|
||||
qml/RootItem.qml
|
||||
qml/ChatItem.qml
|
||||
qml/Badge.qml
|
||||
qml/dialog/CodeBlock.qml
|
||||
qml/dialog/TextBlock.qml
|
||||
SOURCES
|
||||
BaseChatWidget.hpp BaseChatWidget.cpp
|
||||
ChatWidget.hpp ChatWidget.cpp
|
||||
ChatModel.hpp ChatModel.cpp
|
||||
ChatRootView.hpp ChatRootView.cpp
|
||||
ClientInterface.hpp ClientInterface.cpp
|
||||
MessagePart.hpp
|
||||
ChatUtils.h ChatUtils.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
PRIVATE
|
||||
PUBLIC
|
||||
Qt::Widgets
|
||||
Qt::Quick
|
||||
Qt::QuickWidgets
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
LLMCore
|
||||
QodeAssistSettings
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
|
@ -18,12 +18,24 @@
|
||||
*/
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include <QtCore/qjsonobject.h>
|
||||
#include <QtQml>
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatModel::ChatModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_totalTokens(0)
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(&settings.chatTokensThreshold,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatModel::tokensThresholdChanged);
|
||||
}
|
||||
|
||||
int ChatModel::rowCount(const QModelIndex &parent) const
|
||||
@ -40,8 +52,9 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
switch (static_cast<Roles>(role)) {
|
||||
case Roles::RoleType:
|
||||
return QVariant::fromValue(message.role);
|
||||
case Roles::Content:
|
||||
case Roles::Content: {
|
||||
return message.content;
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@ -55,18 +68,112 @@ QHash<int, QByteArray> ChatModel::roleNames() const
|
||||
return roles;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void ChatModel::addMessage(const QString &content, ChatRole role)
|
||||
{
|
||||
int tokenCount = estimateTokenCount(content);
|
||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||
m_messages.append({role, content});
|
||||
m_messages.append({role, content, tokenCount});
|
||||
m_totalTokens += tokenCount;
|
||||
endInsertRows();
|
||||
trim();
|
||||
emit totalTokensChanged();
|
||||
}
|
||||
|
||||
void ChatModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_messages.clear();
|
||||
m_totalTokens = 0;
|
||||
endResetModel();
|
||||
emit totalTokensChanged();
|
||||
}
|
||||
|
||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||
{
|
||||
QList<MessagePart> parts;
|
||||
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
||||
int lastIndex = 0;
|
||||
auto blockMatches = codeBlockRegex.globalMatch(content);
|
||||
|
||||
while (blockMatches.hasNext()) {
|
||||
auto match = blockMatches.next();
|
||||
if (match.capturedStart() > lastIndex) {
|
||||
QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
||||
if (!textBetween.isEmpty()) {
|
||||
parts.append({MessagePart::Text, textBetween, ""});
|
||||
}
|
||||
}
|
||||
parts.append({MessagePart::Code, match.captured(2).trimmed(), match.captured(1)});
|
||||
lastIndex = match.capturedEnd();
|
||||
}
|
||||
|
||||
if (lastIndex < content.length()) {
|
||||
QString remainingText = content.mid(lastIndex).trimmed();
|
||||
if (!remainingText.isEmpty()) {
|
||||
parts.append({MessagePart::Text, remainingText, ""});
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
QJsonArray ChatModel::prepareMessagesForRequest(LLMCore::ContextData context) const
|
||||
{
|
||||
QJsonArray messages;
|
||||
|
||||
messages.append(QJsonObject{{"role", "system"}, {"content", context.systemPrompt}});
|
||||
|
||||
for (const auto &message : m_messages) {
|
||||
QString role;
|
||||
switch (message.role) {
|
||||
case ChatRole::User:
|
||||
role = "user";
|
||||
break;
|
||||
case ChatRole::Assistant:
|
||||
role = "assistant";
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
messages.append(QJsonObject{{"role", role}, {"content", message.content}});
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
int ChatModel::totalTokens() const
|
||||
{
|
||||
return m_totalTokens;
|
||||
}
|
||||
|
||||
int ChatModel::tokensThreshold() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.chatTokensThreshold();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
@ -19,25 +19,35 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ContextData.hpp"
|
||||
#include "MessagePart.hpp"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
enum class ChatRole { System, User, Assistant };
|
||||
|
||||
struct Message
|
||||
{
|
||||
ChatRole role;
|
||||
QString content;
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
public:
|
||||
enum Roles { RoleType = Qt::UserRole, Content };
|
||||
|
||||
enum ChatRole { System, User, Assistant };
|
||||
Q_ENUM(ChatRole)
|
||||
|
||||
struct Message
|
||||
{
|
||||
ChatRole role;
|
||||
QString content;
|
||||
int tokenCount;
|
||||
};
|
||||
|
||||
explicit ChatModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
@ -46,11 +56,28 @@ public:
|
||||
|
||||
Q_INVOKABLE void addMessage(const QString &content, ChatRole role);
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||
|
||||
QVector<Message> getChatHistory() const;
|
||||
QJsonArray prepareMessagesForRequest(LLMCore::ContextData context) const;
|
||||
|
||||
int totalTokens() const;
|
||||
int tokensThreshold() const;
|
||||
|
||||
QString currentModel() const;
|
||||
|
||||
signals:
|
||||
void totalTokensChanged();
|
||||
void tokensThresholdChanged();
|
||||
|
||||
private:
|
||||
void trim();
|
||||
int estimateTokenCount(const QString &text) const;
|
||||
|
||||
QVector<Message> m_messages;
|
||||
int m_totalTokens = 0;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatRole)
|
||||
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
|
||||
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
||||
|
@ -18,17 +18,110 @@
|
||||
*/
|
||||
|
||||
#include "ChatRootView.hpp"
|
||||
#include <QtGui/qclipboard.h>
|
||||
#include <utils/theme/theme.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
, m_chatModel(new ChatModel(this))
|
||||
{}
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(&settings.chatModelName,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::currentTemplateChanged);
|
||||
generateColors();
|
||||
}
|
||||
|
||||
ChatModel *ChatRootView::chatModel() const
|
||||
{
|
||||
return m_chatModel;
|
||||
}
|
||||
|
||||
QColor ChatRootView::backgroundColor() const
|
||||
{
|
||||
return Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
||||
}
|
||||
|
||||
void ChatRootView::sendMessage(const QString &message) const
|
||||
{
|
||||
m_clientInterface->sendMessage(message);
|
||||
}
|
||||
|
||||
void ChatRootView::copyToClipboard(const QString &text)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
void ChatRootView::generateColors()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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::currentTemplate() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.chatModelName();
|
||||
}
|
||||
|
||||
QColor ChatRootView::primaryColor() const
|
||||
{
|
||||
return m_primaryColor;
|
||||
}
|
||||
|
||||
QColor ChatRootView::secondaryColor() const
|
||||
{
|
||||
return m_secondaryColor;
|
||||
}
|
||||
|
||||
QColor ChatRootView::codeColor() const
|
||||
{
|
||||
return m_codeColor;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
@ -22,25 +22,54 @@
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(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)
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
public:
|
||||
ChatRootView(QQuickItem *parent = nullptr);
|
||||
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
|
||||
QColor backgroundColor() const;
|
||||
QColor primaryColor() const;
|
||||
QColor secondaryColor() const;
|
||||
|
||||
QColor codeColor() const;
|
||||
|
||||
public slots:
|
||||
void sendMessage(const QString &message) const;
|
||||
void copyToClipboard(const QString &text);
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
void currentTemplateChanged();
|
||||
|
||||
private:
|
||||
ChatModel *m_chatModel = nullptr;
|
||||
void generateColors();
|
||||
QColor generateColor(const QColor &baseColor,
|
||||
float hueShift,
|
||||
float saturationMod,
|
||||
float lightnessMod);
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
ClientInterface *m_clientInterface;
|
||||
QString m_currentTemplate;
|
||||
QColor m_primaryColor;
|
||||
QColor m_secondaryColor;
|
||||
QColor m_codeColor;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
14
chatview/ChatUtils.cpp
Normal file
14
chatview/ChatUtils.cpp
Normal file
@ -0,0 +1,14 @@
|
||||
#include "ChatUtils.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
void ChatUtils::copyToClipboard(const QString &text)
|
||||
{
|
||||
qDebug() << "call clipboard" << text;
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
21
chatview/ChatUtils.h
Normal file
21
chatview/ChatUtils.h
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
// Q_NAMESPACE
|
||||
|
||||
class ChatUtils : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_NAMED_ELEMENT(ChatUtils)
|
||||
|
||||
public:
|
||||
explicit ChatUtils(QObject *parent = nullptr)
|
||||
: QObject(parent) {};
|
||||
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
@ -17,19 +17,27 @@
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "BaseChatWidget.hpp"
|
||||
#include "ChatWidget.hpp"
|
||||
|
||||
#include <QQmlEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
BaseChatWidget::BaseChatWidget(QWidget *parent) : QQuickWidget(parent)
|
||||
ChatWidget::ChatWidget(QWidget *parent)
|
||||
: QQuickWidget(parent)
|
||||
{
|
||||
setSource(QUrl("qrc:/ChatView/qml/RootItem.qml"));
|
||||
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
|
||||
engine()->rootContext()->setContextObject(this);
|
||||
}
|
||||
|
||||
void ChatWidget::clear()
|
||||
{
|
||||
QMetaObject::invokeMethod(rootObject(), "clearChat");
|
||||
}
|
||||
|
||||
void ChatWidget::scrollToBottom()
|
||||
{
|
||||
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
||||
}
|
||||
}
|
@ -23,13 +23,19 @@
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class BaseChatWidget : public QQuickWidget
|
||||
class ChatWidget : public QQuickWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BaseChatWidget(QWidget *parent = nullptr);
|
||||
~BaseChatWidget() = default;
|
||||
explicit ChatWidget(QWidget *parent = nullptr);
|
||||
~ChatWidget() = default;
|
||||
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE void scrollToBottom();
|
||||
|
||||
signals:
|
||||
void clearPressed();
|
||||
};
|
||||
|
||||
}
|
117
chatview/ClientInterface.cpp
Normal file
117
chatview/ClientInterface.cpp
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 "ClientInterface.hpp"
|
||||
#include "ContextSettings.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_requestHandler(new LLMCore::RequestHandler(this))
|
||||
, m_chatModel(chatModel)
|
||||
{
|
||||
connect(m_requestHandler,
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
[this](const QString &completion, const QJsonObject &, bool isComplete) {
|
||||
handleLLMResponse(completion, isComplete);
|
||||
});
|
||||
|
||||
connect(m_requestHandler,
|
||||
&LLMCore::RequestHandler::requestFinished,
|
||||
this,
|
||||
[this](const QString &, bool success, const QString &errorString) {
|
||||
if (!success) {
|
||||
emit errorOccurred(errorString);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientInterface::~ClientInterface() = default;
|
||||
|
||||
void ClientInterface::sendMessage(const QString &message)
|
||||
{
|
||||
LOG_MESSAGE("Sending message: " + message);
|
||||
LOG_MESSAGE("chatProvider " + Settings::generalSettings().chatLlmProviders.stringValue());
|
||||
LOG_MESSAGE("chatTemplate " + Settings::generalSettings().chatPrompts.stringValue());
|
||||
|
||||
auto chatTemplate = LLMCore::PromptTemplateManager::instance().getCurrentChatTemplate();
|
||||
auto chatProvider = LLMCore::ProvidersManager::instance().getCurrentChatProvider();
|
||||
|
||||
LLMCore::ContextData context;
|
||||
context.prefix = message;
|
||||
context.suffix = "";
|
||||
if (Settings::contextSettings().useChatSystemPrompt())
|
||||
context.systemPrompt = Settings::contextSettings().chatSystemPrompt();
|
||||
|
||||
QJsonObject providerRequest;
|
||||
providerRequest["model"] = Settings::generalSettings().chatModelName();
|
||||
providerRequest["stream"] = true;
|
||||
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(context);
|
||||
|
||||
chatTemplate->prepareRequest(providerRequest, context);
|
||||
chatProvider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::Chat;
|
||||
config.provider = chatProvider;
|
||||
config.promptTemplate = chatTemplate;
|
||||
config.url = QString("%1%2").arg(Settings::generalSettings().chatUrl(),
|
||||
Settings::generalSettings().chatEndPoint());
|
||||
config.providerRequest = providerRequest;
|
||||
config.multiLineCompletion = Settings::generalSettings().multiLineCompletion();
|
||||
|
||||
QJsonObject request;
|
||||
request["id"] = QUuid::createUuid().toString();
|
||||
|
||||
m_accumulatedResponse.clear();
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User);
|
||||
m_requestHandler->sendLLMRequest(config, request);
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
{
|
||||
m_chatModel->clear();
|
||||
m_accumulatedResponse.clear();
|
||||
LOG_MESSAGE("Chat history cleared");
|
||||
}
|
||||
|
||||
void ClientInterface::handleLLMResponse(const QString &response, bool isComplete)
|
||||
{
|
||||
m_accumulatedResponse += response;
|
||||
|
||||
if (isComplete) {
|
||||
LOG_MESSAGE("Message completed. Final response: " + m_accumulatedResponse);
|
||||
emit messageReceived(m_accumulatedResponse.trimmed());
|
||||
|
||||
m_chatModel->addMessage(m_accumulatedResponse.trimmed(), ChatModel::ChatRole::Assistant);
|
||||
m_accumulatedResponse.clear();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
54
chatview/ClientInterface.hpp
Normal file
54
chatview/ClientInterface.hpp
Normal 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "RequestHandler.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ClientInterface : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
~ClientInterface();
|
||||
|
||||
void sendMessage(const QString &message);
|
||||
void clearMessages();
|
||||
|
||||
signals:
|
||||
void messageReceived(const QString &message);
|
||||
void errorOccurred(const QString &error);
|
||||
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, bool isComplete);
|
||||
|
||||
LLMCore::RequestHandler *m_requestHandler;
|
||||
QString m_accumulatedResponse;
|
||||
ChatModel *m_chatModel;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
51
chatview/MessagePart.hpp
Normal file
51
chatview/MessagePart.hpp
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
Q_NAMESPACE
|
||||
|
||||
class MessagePart
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(PartType type MEMBER type CONSTANT FINAL)
|
||||
Q_PROPERTY(QString text MEMBER text CONSTANT FINAL)
|
||||
Q_PROPERTY(QString language MEMBER language CONSTANT FINAL)
|
||||
QML_VALUE_TYPE(messagePart)
|
||||
public:
|
||||
enum PartType { Code, Text };
|
||||
Q_ENUM(PartType)
|
||||
|
||||
PartType type;
|
||||
QString text;
|
||||
QString language;
|
||||
};
|
||||
|
||||
class MessagePartType : public MessagePart
|
||||
{
|
||||
Q_GADGET
|
||||
};
|
||||
|
||||
QML_NAMED_ELEMENT(MessagePart)
|
||||
QML_FOREIGN_NAMESPACE(QodeAssist::Chat::MessagePartType)
|
||||
} // namespace QodeAssist::Chat
|
40
chatview/qml/Badge.qml
Normal file
40
chatview/qml/Badge.qml
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias text: badgeText.text
|
||||
property alias fontColor: badgeText.color
|
||||
|
||||
width: badgeText.implicitWidth + radius
|
||||
height: badgeText.implicitHeight + 6
|
||||
color: "lightgreen"
|
||||
radius: height / 2
|
||||
border.width: 1
|
||||
border.color: "gray"
|
||||
|
||||
Text {
|
||||
id: badgeText
|
||||
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
91
chatview/qml/ChatItem.qml
Normal file
91
chatview/qml/ChatItem.qml
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 ChatView
|
||||
import "./dialog"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias msgModel: msgCreator.model
|
||||
property color fontColor
|
||||
property color codeBgColor
|
||||
property color selectionColor
|
||||
|
||||
height: msgColumn.height
|
||||
radius: 8
|
||||
|
||||
Column {
|
||||
id: msgColumn
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
id: msgCreator
|
||||
delegate: Loader {
|
||||
property var itemData: modelData
|
||||
|
||||
width: parent.width
|
||||
sourceComponent: {
|
||||
switch(modelData.type) {
|
||||
case MessagePart.Text: return textComponent;
|
||||
case MessagePart.Code: return codeBlockComponent;
|
||||
default: return textComponent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComponent
|
||||
|
||||
TextBlock {
|
||||
height: implicitHeight + 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 10
|
||||
text: itemData.text
|
||||
color: fontColor
|
||||
selectionColor: root.selectionColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Component {
|
||||
id: codeBlockComponent
|
||||
|
||||
CodeBlock {
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
right: parent.right
|
||||
rightMargin: 10
|
||||
}
|
||||
|
||||
code: itemData.text
|
||||
language: itemData.language
|
||||
color: root.codeBgColor
|
||||
selectionColor: root.selectionColor
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,26 @@
|
||||
/*
|
||||
* 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.Controls.Basic as QQC
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
|
||||
ChatRootView {
|
||||
@ -6,8 +28,134 @@ ChatRootView {
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
color: "gray"
|
||||
color: root.backgroundColor
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
spacing: 10
|
||||
|
||||
ListView {
|
||||
id: chatListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
leftMargin: 5
|
||||
model: root.chatModel
|
||||
clip: true
|
||||
spacing: 10
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cacheBuffer: 2000
|
||||
|
||||
delegate: ChatItem {
|
||||
width: ListView.view.width - scroll.width
|
||||
msgModel: root.chatModel.processMessageContent(model.content)
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
header: Item {
|
||||
width: ListView.view.width - scroll.width
|
||||
height: 30
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: scroll
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (atYEnd) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
QQC.TextField {
|
||||
id: messageInput
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 60
|
||||
Layout.minimumHeight: 30
|
||||
rightInset: -(parent.width - sendButton.width - clearButton.width)
|
||||
placeholderText: qsTr("Type your message here...")
|
||||
placeholderTextColor: "#888"
|
||||
color: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
||||
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)
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
onAccepted: sendButton.clicked()
|
||||
}
|
||||
|
||||
Button {
|
||||
id: sendButton
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.minimumHeight: 30
|
||||
text: qsTr("Send")
|
||||
onClicked: {
|
||||
if (messageInput.text.trim() !== "") {
|
||||
root.sendMessage(messageInput.text);
|
||||
messageInput.text = ""
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: clearButton
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.minimumHeight: 30
|
||||
text: qsTr("Clear")
|
||||
onClicked: clearChat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: bar
|
||||
|
||||
layoutDirection: Qt.RightToLeft
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: scroll.width
|
||||
}
|
||||
spacing: 10
|
||||
|
||||
Badge {
|
||||
text: "%1/%2".arg(root.chatModel.totalTokens).arg(root.chatModel.tokensThreshold)
|
||||
color: root.codeColor
|
||||
fontColor: root.primaryColor.hslLightness > 0.5 ? "black" : "white"
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
root.chatModel.clear()
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
Qt.callLater(chatListView.positionViewAtEnd)
|
||||
}
|
||||
}
|
||||
|
86
chatview/qml/dialog/CodeBlock.qml
Normal file
86
chatview/qml/dialog/CodeBlock.qml
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 ChatView
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string code: ""
|
||||
property string language: ""
|
||||
property color selectionColor
|
||||
|
||||
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
|
||||
: Qt.lighter(root.color, 1.3)
|
||||
border.width: 2
|
||||
radius: 4
|
||||
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: codeText.implicitHeight + 20
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: codeText
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
text: root.code
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
font.family: "monospace"
|
||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: root.selectionColor
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 5
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
text: root.language
|
||||
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
|
||||
: Qt.lighter(root.color, 1.1)
|
||||
font.pointSize: 8
|
||||
}
|
||||
|
||||
Button {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 5
|
||||
text: "Copy"
|
||||
onClicked: {
|
||||
utils.copyToClipboard(root.code)
|
||||
text = qsTr("Copied")
|
||||
copyTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyTimer
|
||||
interval: 2000
|
||||
onTriggered: parent.text = qsTr("Copy")
|
||||
}
|
||||
}
|
||||
}
|
29
chatview/qml/dialog/TextBlock.qml
Normal file
29
chatview/qml/dialog/TextBlock.qml
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
TextEdit {
|
||||
id: root
|
||||
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.StyledText
|
||||
}
|
Reference in New Issue
Block a user