mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-22 06:53:03 -05:00
feat: Add summarize chat (#289)
This commit is contained in:
@ -53,6 +53,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/tools-icon-on.svg
|
icons/tools-icon-on.svg
|
||||||
icons/tools-icon-off.svg
|
icons/tools-icon-off.svg
|
||||||
icons/settings-icon.svg
|
icons/settings-icon.svg
|
||||||
|
icons/compress-icon.svg
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
ChatWidget.hpp ChatWidget.cpp
|
ChatWidget.hpp ChatWidget.cpp
|
||||||
@ -66,6 +67,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
ChatData.hpp
|
ChatData.hpp
|
||||||
FileItem.hpp FileItem.cpp
|
FileItem.hpp FileItem.cpp
|
||||||
ChatFileManager.hpp ChatFileManager.cpp
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
|
|||||||
310
ChatView/ChatCompressor.cpp
Normal file
310
ChatView/ChatCompressor.cpp
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 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 "ChatCompressor.hpp"
|
||||||
|
#include "ChatModel.hpp"
|
||||||
|
#include "GeneralSettings.hpp"
|
||||||
|
#include "PromptTemplateManager.hpp"
|
||||||
|
#include "ProvidersManager.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatCompressor::ChatCompressor(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
||||||
|
{
|
||||||
|
if (m_isCompressing) {
|
||||||
|
emit compressionFailed(tr("Compression already in progress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatFilePath.isEmpty()) {
|
||||||
|
emit compressionFailed(tr("No chat file to compress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chatModel || chatModel->rowCount() == 0) {
|
||||||
|
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto providerName = Settings::generalSettings().caProvider();
|
||||||
|
m_provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
|
if (!m_provider) {
|
||||||
|
emit compressionFailed(tr("No provider available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto templateName = Settings::generalSettings().caTemplate();
|
||||||
|
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||||
|
templateName);
|
||||||
|
|
||||||
|
if (!promptTemplate) {
|
||||||
|
emit compressionFailed(tr("No template available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_isCompressing = true;
|
||||||
|
m_chatModel = chatModel;
|
||||||
|
m_originalChatPath = chatFilePath;
|
||||||
|
m_accumulatedSummary.clear();
|
||||||
|
m_currentRequestId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
|
||||||
|
emit compressionStarted();
|
||||||
|
|
||||||
|
connectProviderSignals();
|
||||||
|
|
||||||
|
QUrl requestUrl;
|
||||||
|
QJsonObject payload;
|
||||||
|
|
||||||
|
if (m_provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||||
|
requestUrl = QUrl(QString("%1/models/%2:streamGenerateContent?alt=sse")
|
||||||
|
.arg(Settings::generalSettings().caUrl(),
|
||||||
|
Settings::generalSettings().caModel()));
|
||||||
|
} else {
|
||||||
|
requestUrl = QUrl(QString("%1%2").arg(Settings::generalSettings().caUrl(),
|
||||||
|
m_provider->chatEndpoint()));
|
||||||
|
payload["model"] = Settings::generalSettings().caModel();
|
||||||
|
payload["stream"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildRequestPayload(payload, promptTemplate);
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||||
|
m_provider->sendRequest(m_currentRequestId, requestUrl, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatCompressor::isCompressing() const
|
||||||
|
{
|
||||||
|
return m_isCompressing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::cancelCompression()
|
||||||
|
{
|
||||||
|
if (!m_isCompressing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE("Cancelling compression request");
|
||||||
|
|
||||||
|
if (m_provider && !m_currentRequestId.isEmpty())
|
||||||
|
m_provider->cancelRequest(m_currentRequestId);
|
||||||
|
|
||||||
|
cleanupState();
|
||||||
|
emit compressionFailed(tr("Compression cancelled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
|
||||||
|
{
|
||||||
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_accumulatedSummary += partialText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
|
||||||
|
{
|
||||||
|
Q_UNUSED(fullText)
|
||||||
|
|
||||||
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
||||||
|
|
||||||
|
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||||
|
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
||||||
|
handleCompressionError(tr("Failed to save compressed chat"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
||||||
|
cleanupState();
|
||||||
|
emit compressionCompleted(compressedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
|
||||||
|
{
|
||||||
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Compression request failed: %1").arg(error));
|
||||||
|
handleCompressionError(tr("Compression failed: %1").arg(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::handleCompressionError(const QString &error)
|
||||||
|
{
|
||||||
|
cleanupState();
|
||||||
|
emit compressionFailed(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatCompressor::createCompressedChatPath(const QString &originalPath) const
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(originalPath);
|
||||||
|
QString hash = QString::number(QDateTime::currentMSecsSinceEpoch() % 100000, 16);
|
||||||
|
return QString("%1/%2_%3.%4")
|
||||||
|
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatCompressor::buildCompressionPrompt() const
|
||||||
|
{
|
||||||
|
return QStringLiteral(
|
||||||
|
"Please create a comprehensive summary of our entire conversation above. "
|
||||||
|
"The summary should:\n"
|
||||||
|
"1. Preserve all important context, decisions, and key information\n"
|
||||||
|
"2. Maintain technical details, code snippets, file references, and specific examples\n"
|
||||||
|
"3. Keep the chronological flow of the discussion\n"
|
||||||
|
"4. Be significantly shorter than the original (aim for 30-40% of original length)\n"
|
||||||
|
"5. Be written in clear, structured format\n"
|
||||||
|
"6. Use markdown formatting for better readability\n\n"
|
||||||
|
"Create the summary now:");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::buildRequestPayload(
|
||||||
|
QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate)
|
||||||
|
{
|
||||||
|
LLMCore::ContextData context;
|
||||||
|
|
||||||
|
context.systemPrompt = QStringLiteral(
|
||||||
|
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||||
|
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
||||||
|
|
||||||
|
QVector<LLMCore::Message> messages;
|
||||||
|
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||||
|
if (msg.role == ChatModel::ChatRole::Tool
|
||||||
|
|| msg.role == ChatModel::ChatRole::FileEdit
|
||||||
|
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
LLMCore::Message apiMessage;
|
||||||
|
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
||||||
|
apiMessage.content = msg.content;
|
||||||
|
messages.append(apiMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
LLMCore::Message compressionRequest;
|
||||||
|
compressionRequest.role = "user";
|
||||||
|
compressionRequest.content = buildCompressionPrompt();
|
||||||
|
messages.append(compressionRequest);
|
||||||
|
|
||||||
|
context.history = messages;
|
||||||
|
|
||||||
|
m_provider->prepareRequest(
|
||||||
|
payload, promptTemplate, context, LLMCore::RequestType::Chat, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatCompressor::createCompressedChatFile(
|
||||||
|
const QString &sourcePath, const QString &destPath, const QString &summary)
|
||||||
|
{
|
||||||
|
QFile sourceFile(sourcePath);
|
||||||
|
if (!sourceFile.open(QIODevice::ReadOnly)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to open source chat file: %1").arg(sourcePath));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(sourceFile.readAll(), &parseError);
|
||||||
|
sourceFile.close();
|
||||||
|
|
||||||
|
if (doc.isNull() || !doc.isObject()) {
|
||||||
|
LOG_MESSAGE(QString("Invalid JSON in chat file: %1 (Error: %2)")
|
||||||
|
.arg(sourcePath, parseError.errorString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root = doc.object();
|
||||||
|
|
||||||
|
QJsonObject summaryMessage;
|
||||||
|
summaryMessage["role"] = "assistant";
|
||||||
|
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
|
||||||
|
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
summaryMessage["isRedacted"] = false;
|
||||||
|
summaryMessage["attachments"] = QJsonArray();
|
||||||
|
summaryMessage["images"] = QJsonArray();
|
||||||
|
|
||||||
|
root["messages"] = QJsonArray{summaryMessage};
|
||||||
|
|
||||||
|
if (QFile::exists(destPath))
|
||||||
|
QFile::remove(destPath);
|
||||||
|
|
||||||
|
QFile destFile(destPath);
|
||||||
|
if (!destFile.open(QIODevice::WriteOnly)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create compressed chat file: %1").arg(destPath));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::connectProviderSignals()
|
||||||
|
{
|
||||||
|
m_connections.append(connect(
|
||||||
|
m_provider,
|
||||||
|
&LLMCore::Provider::partialResponseReceived,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onPartialResponseReceived,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
m_provider,
|
||||||
|
&LLMCore::Provider::fullResponseReceived,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onFullResponseReceived,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
m_provider,
|
||||||
|
&LLMCore::Provider::requestFailed,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onRequestFailed,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::disconnectAllSignals()
|
||||||
|
{
|
||||||
|
for (const auto &connection : std::as_const(m_connections))
|
||||||
|
disconnect(connection);
|
||||||
|
m_connections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::cleanupState()
|
||||||
|
{
|
||||||
|
disconnectAllSignals();
|
||||||
|
|
||||||
|
m_isCompressing = false;
|
||||||
|
m_currentRequestId.clear();
|
||||||
|
m_originalChatPath.clear();
|
||||||
|
m_accumulatedSummary.clear();
|
||||||
|
m_chatModel = nullptr;
|
||||||
|
m_provider = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
79
ChatView/ChatCompressor.hpp
Normal file
79
ChatView/ChatCompressor.hpp
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 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 <QJsonObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::LLMCore {
|
||||||
|
class Provider;
|
||||||
|
class PromptTemplate;
|
||||||
|
} // namespace QodeAssist::LLMCore
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatModel;
|
||||||
|
|
||||||
|
class ChatCompressor : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatCompressor(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
||||||
|
|
||||||
|
bool isCompressing() const;
|
||||||
|
void cancelCompression();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void compressionStarted();
|
||||||
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
|
||||||
|
void onFullResponseReceived(const QString &requestId, const QString &fullText);
|
||||||
|
void onRequestFailed(const QString &requestId, const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString createCompressedChatPath(const QString &originalPath) const;
|
||||||
|
QString buildCompressionPrompt() const;
|
||||||
|
bool createCompressedChatFile(
|
||||||
|
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||||
|
void connectProviderSignals();
|
||||||
|
void disconnectAllSignals();
|
||||||
|
void cleanupState();
|
||||||
|
void handleCompressionError(const QString &error);
|
||||||
|
void buildRequestPayload(QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate);
|
||||||
|
|
||||||
|
bool m_isCompressing = false;
|
||||||
|
QString m_currentRequestId;
|
||||||
|
QString m_originalChatPath;
|
||||||
|
QString m_accumulatedSummary;
|
||||||
|
LLMCore::Provider *m_provider = nullptr;
|
||||||
|
ChatModel *m_chatModel = nullptr;
|
||||||
|
|
||||||
|
QList<QMetaObject::Connection> m_connections;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@ -36,6 +36,7 @@
|
|||||||
|
|
||||||
#include "AgentRole.hpp"
|
#include "AgentRole.hpp"
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
|
#include "ChatCompressor.hpp"
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "ConfigurationManager.hpp"
|
#include "ConfigurationManager.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
@ -58,6 +59,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
||||||
, m_fileManager(new ChatFileManager(this))
|
, m_fileManager(new ChatFileManager(this))
|
||||||
, m_isRequestInProgress(false)
|
, m_isRequestInProgress(false)
|
||||||
|
, m_chatCompressor(new ChatCompressor(this))
|
||||||
{
|
{
|
||||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
connect(
|
connect(
|
||||||
@ -245,6 +247,27 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
m_lastErrorMessage = error;
|
m_lastErrorMessage = error;
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ChatCompressor signals
|
||||||
|
connect(m_chatCompressor, &ChatCompressor::compressionStarted, this, [this]() {
|
||||||
|
emit isCompressingChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_chatCompressor, &ChatCompressor::compressionCompleted, this, [this](const QString &compressedChatPath) {
|
||||||
|
emit isCompressingChanged();
|
||||||
|
m_lastInfoMessage = tr("Chat compressed successfully!");
|
||||||
|
emit lastInfoMessageChanged();
|
||||||
|
emit compressionCompleted(compressedChatPath);
|
||||||
|
|
||||||
|
loadHistory(compressedChatPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_chatCompressor, &ChatCompressor::compressionFailed, this, [this](const QString &error) {
|
||||||
|
emit isCompressingChanged();
|
||||||
|
m_lastErrorMessage = error;
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
emit compressionFailed(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatModel *ChatRootView::chatModel() const
|
ChatModel *ChatRootView::chatModel() const
|
||||||
@ -1470,4 +1493,33 @@ void ChatRootView::openAgentRolesSettings()
|
|||||||
Core::ICore::showOptionsDialog(Utils::Id("QodeAssist.AgentRoles"));
|
Core::ICore::showOptionsDialog(Utils::Id("QodeAssist.AgentRoles"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatRootView::compressCurrentChat()
|
||||||
|
{
|
||||||
|
if (m_chatCompressor->isCompressing()) {
|
||||||
|
m_lastErrorMessage = tr("Compression is already in progress");
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_recentFilePath.isEmpty()) {
|
||||||
|
m_lastErrorMessage = tr("No chat file to compress. Please save the chat first.");
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autosave();
|
||||||
|
|
||||||
|
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::cancelCompression()
|
||||||
|
{
|
||||||
|
m_chatCompressor->cancelCompression();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatRootView::isCompressing() const
|
||||||
|
{
|
||||||
|
return m_chatCompressor->isCompressing();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@ -21,14 +21,16 @@
|
|||||||
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
|
||||||
|
#include "ChatFileManager.hpp"
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
#include "ChatFileManager.hpp"
|
|
||||||
#include "llmcore/PromptProviderChat.hpp"
|
#include "llmcore/PromptProviderChat.hpp"
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatCompressor;
|
||||||
|
|
||||||
class ChatRootView : public QQuickItem
|
class ChatRootView : public QQuickItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@ -64,6 +66,7 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
||||||
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
||||||
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||||
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
@ -151,6 +154,9 @@ public:
|
|||||||
QStringList availableConfigurations() const;
|
QStringList availableConfigurations() const;
|
||||||
QString currentConfiguration() const;
|
QString currentConfiguration() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void compressCurrentChat();
|
||||||
|
Q_INVOKABLE void cancelCompression();
|
||||||
|
|
||||||
Q_INVOKABLE void loadAvailableAgentRoles();
|
Q_INVOKABLE void loadAvailableAgentRoles();
|
||||||
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
||||||
Q_INVOKABLE void openAgentRolesSettings();
|
Q_INVOKABLE void openAgentRolesSettings();
|
||||||
@ -169,6 +175,8 @@ public:
|
|||||||
|
|
||||||
bool isThinkingSupport() const;
|
bool isThinkingSupport() const;
|
||||||
|
|
||||||
|
bool isCompressing() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message);
|
void sendMessage(const QString &message);
|
||||||
void copyToClipboard(const QString &text);
|
void copyToClipboard(const QString &text);
|
||||||
@ -205,10 +213,15 @@ signals:
|
|||||||
void isThinkingSupportChanged();
|
void isThinkingSupportChanged();
|
||||||
void availableConfigurationsChanged();
|
void availableConfigurationsChanged();
|
||||||
void currentConfigurationChanged();
|
void currentConfigurationChanged();
|
||||||
|
|
||||||
void availableAgentRolesChanged();
|
void availableAgentRolesChanged();
|
||||||
void currentAgentRoleChanged();
|
void currentAgentRoleChanged();
|
||||||
void baseSystemPromptChanged();
|
void baseSystemPromptChanged();
|
||||||
|
|
||||||
|
void isCompressingChanged();
|
||||||
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||||
QString getChatsHistoryDir() const;
|
QString getChatsHistoryDir() const;
|
||||||
@ -244,6 +257,8 @@ private:
|
|||||||
|
|
||||||
QStringList m_availableAgentRoles;
|
QStringList m_availableAgentRoles;
|
||||||
QString m_currentAgentRole;
|
QString m_currentAgentRole;
|
||||||
|
|
||||||
|
ChatCompressor *m_chatCompressor;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
10
ChatView/icons/compress-icon.svg
Normal file
10
ChatView/icons/compress-icon.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Archive/compress icon: downward arrows pointing to center with horizontal lines -->
|
||||||
|
<line x1="12" y1="3" x2="12" y2="10" />
|
||||||
|
<polyline points="9 7 12 10 15 7" />
|
||||||
|
|
||||||
|
<line x1="12" y1="21" x2="12" y2="14" />
|
||||||
|
<polyline points="9 17 12 14 15 17" />
|
||||||
|
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" stroke-width="3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 487 B |
@ -88,9 +88,12 @@ ChatRootView {
|
|||||||
Layout.preferredWidth: parent.width
|
Layout.preferredWidth: parent.width
|
||||||
Layout.preferredHeight: childrenRect.height + 10
|
Layout.preferredHeight: childrenRect.height + 10
|
||||||
|
|
||||||
|
isCompressing: root.isCompressing
|
||||||
saveButton.onClicked: root.showSaveDialog()
|
saveButton.onClicked: root.showSaveDialog()
|
||||||
loadButton.onClicked: root.showLoadDialog()
|
loadButton.onClicked: root.showLoadDialog()
|
||||||
clearButton.onClicked: root.clearChat()
|
clearButton.onClicked: root.clearChat()
|
||||||
|
compressButton.onClicked: compressConfirmDialog.open()
|
||||||
|
cancelCompressButton.onClicked: root.cancelCompression()
|
||||||
tokensBadge {
|
tokensBadge {
|
||||||
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||||
}
|
}
|
||||||
@ -499,6 +502,22 @@ ChatRootView {
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: compressConfirmDialog
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
title: qsTr("Compress Chat")
|
||||||
|
modal: true
|
||||||
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Create a summarized copy of this chat?\n\nThe summary will be generated by LLM and saved as a new chat file.")
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: root.compressCurrentChat()
|
||||||
|
}
|
||||||
|
|
||||||
Toast {
|
Toast {
|
||||||
id: errorToast
|
id: errorToast
|
||||||
z: 1000
|
z: 1000
|
||||||
|
|||||||
@ -29,6 +29,8 @@ Rectangle {
|
|||||||
property alias saveButton: saveButtonId
|
property alias saveButton: saveButtonId
|
||||||
property alias loadButton: loadButtonId
|
property alias loadButton: loadButtonId
|
||||||
property alias clearButton: clearButtonId
|
property alias clearButton: clearButtonId
|
||||||
|
property alias compressButton: compressButtonId
|
||||||
|
property alias cancelCompressButton: cancelCompressButtonId
|
||||||
property alias tokensBadge: tokensBadgeId
|
property alias tokensBadge: tokensBadgeId
|
||||||
property alias recentPath: recentPathId
|
property alias recentPath: recentPathId
|
||||||
property alias openChatHistory: openChatHistoryId
|
property alias openChatHistory: openChatHistoryId
|
||||||
@ -40,6 +42,8 @@ Rectangle {
|
|||||||
property alias configSelector: configSelectorId
|
property alias configSelector: configSelectorId
|
||||||
property alias roleSelector: roleSelector
|
property alias roleSelector: roleSelector
|
||||||
|
|
||||||
|
property bool isCompressing: false
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
Qt.darker(palette.window, 1.1) :
|
Qt.darker(palette.window, 1.1) :
|
||||||
Qt.lighter(palette.window, 1.1)
|
Qt.lighter(palette.window, 1.1)
|
||||||
@ -243,6 +247,51 @@ Rectangle {
|
|||||||
ToolTip.text: qsTr("Clean chat")
|
ToolTip.text: qsTr("Clean chat")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: compressButtonId
|
||||||
|
|
||||||
|
visible: !root.isCompressing
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: compressingRow
|
||||||
|
visible: root.isCompressing
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: compressBusyIndicator
|
||||||
|
running: root.isCompressing
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Compressing...")
|
||||||
|
color: palette.text
|
||||||
|
font.pixelSize: 12
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: cancelCompressButtonId
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Cancel compression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: openChatHistoryId
|
id: openChatHistoryId
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user