mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-14 02:53:01 -05:00
feat: Add temp file storage for chat (#279)
* fix: Add signature to chat history * feat: Add file storage for chat
This commit is contained in:
@ -63,6 +63,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
ChatView.hpp ChatView.cpp
|
ChatView.hpp ChatView.cpp
|
||||||
ChatData.hpp
|
ChatData.hpp
|
||||||
FileItem.hpp FileItem.cpp
|
FileItem.hpp FileItem.cpp
|
||||||
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
|
|||||||
206
ChatView/ChatFileManager.cpp
Normal file
206
ChatView/ChatFileManager.cpp
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* 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 "ChatFileManager.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatFileManager::ChatFileManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_intermediateStorageDir(getIntermediateStorageDir())
|
||||||
|
{}
|
||||||
|
|
||||||
|
ChatFileManager::~ChatFileManager() = default;
|
||||||
|
|
||||||
|
QStringList ChatFileManager::processDroppedFiles(const QStringList &filePaths)
|
||||||
|
{
|
||||||
|
QStringList processedPaths;
|
||||||
|
processedPaths.reserve(filePaths.size());
|
||||||
|
|
||||||
|
for (const QString &filePath : filePaths) {
|
||||||
|
if (!isFileAccessible(filePath)) {
|
||||||
|
const QString error = tr("File is not accessible: %1").arg(filePath);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit fileOperationFailed(error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString copiedPath = copyToIntermediateStorage(filePath);
|
||||||
|
if (!copiedPath.isEmpty()) {
|
||||||
|
processedPaths.append(copiedPath);
|
||||||
|
emit fileCopiedToStorage(filePath, copiedPath);
|
||||||
|
LOG_MESSAGE(QString("File copied to storage: %1 -> %2").arg(filePath, copiedPath));
|
||||||
|
} else {
|
||||||
|
const QString error = tr("Failed to copy file: %1").arg(filePath);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit fileOperationFailed(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::setChatFilePath(const QString &chatFilePath)
|
||||||
|
{
|
||||||
|
m_chatFilePath = chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::chatFilePath() const
|
||||||
|
{
|
||||||
|
return m_chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::clearIntermediateStorage()
|
||||||
|
{
|
||||||
|
QDir dir(m_intermediateStorageDir);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
for (const QFileInfo &fileInfo : files) {
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
|
||||||
|
if (file.remove()) {
|
||||||
|
LOG_MESSAGE(QString("Removed intermediate file: %1").arg(fileInfo.fileName()));
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("Failed to remove intermediate file: %1")
|
||||||
|
.arg(fileInfo.fileName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatFileManager::isFileAccessible(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
return fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::cleanupGlobalIntermediateStorage()
|
||||||
|
{
|
||||||
|
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
|
||||||
|
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
|
||||||
|
|
||||||
|
QDir dir(intermediatePath);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
int removedCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
|
||||||
|
for (const QFileInfo &fileInfo : files) {
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
|
||||||
|
if (file.remove()) {
|
||||||
|
removedCount++;
|
||||||
|
} else {
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedCount > 0 || failedCount > 0) {
|
||||||
|
LOG_MESSAGE(QString("ChatFileManager global cleanup: removed=%1, failed=%2")
|
||||||
|
.arg(removedCount)
|
||||||
|
.arg(failedCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::copyToIntermediateStorage(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
if (!fileInfo.exists() || !fileInfo.isFile()) {
|
||||||
|
LOG_MESSAGE(QString("Source file does not exist or is not a file: %1").arg(filePath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInfo.size() == 0) {
|
||||||
|
LOG_MESSAGE(QString("Source file is empty: %1").arg(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString newFileName = generateIntermediateFileName(filePath);
|
||||||
|
const QString destinationPath = QDir(m_intermediateStorageDir).filePath(newFileName);
|
||||||
|
|
||||||
|
if (QFileInfo::exists(destinationPath)) {
|
||||||
|
QFile::remove(destinationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!QFile::copy(filePath, destinationPath)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to copy file: %1 -> %2").arg(filePath, destinationPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile copiedFile(destinationPath);
|
||||||
|
if (!copiedFile.exists()) {
|
||||||
|
LOG_MESSAGE(QString("Copied file does not exist after copy: %1").arg(destinationPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
copiedFile.setPermissions(QFile::ReadUser | QFile::WriteUser);
|
||||||
|
|
||||||
|
return destinationPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::getIntermediateStorageDir()
|
||||||
|
{
|
||||||
|
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
|
||||||
|
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
|
||||||
|
|
||||||
|
QDir dir;
|
||||||
|
if (!dir.exists(intermediatePath) && !dir.mkpath(intermediatePath)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create intermediate storage directory: %1")
|
||||||
|
.arg(intermediatePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return intermediatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::generateIntermediateFileName(const QString &originalPath)
|
||||||
|
{
|
||||||
|
const QFileInfo fileInfo(originalPath);
|
||||||
|
const QString extension = fileInfo.suffix();
|
||||||
|
QString baseName = fileInfo.completeBaseName().left(30);
|
||||||
|
|
||||||
|
static const QRegularExpression specialChars("[^a-zA-Z0-9_-]");
|
||||||
|
baseName.replace(specialChars, "_");
|
||||||
|
|
||||||
|
if (baseName.isEmpty()) {
|
||||||
|
baseName = "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
|
||||||
|
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces).left(8);
|
||||||
|
|
||||||
|
return QString("%1_%2_%3.%4").arg(baseName, timestamp, uuid, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
|
|
||||||
59
ChatView/ChatFileManager.hpp
Normal file
59
ChatView/ChatFileManager.hpp
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QMap>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatFileManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatFileManager(QObject *parent = nullptr);
|
||||||
|
~ChatFileManager();
|
||||||
|
|
||||||
|
QStringList processDroppedFiles(const QStringList &filePaths);
|
||||||
|
void setChatFilePath(const QString &chatFilePath);
|
||||||
|
QString chatFilePath() const;
|
||||||
|
void clearIntermediateStorage();
|
||||||
|
|
||||||
|
static bool isFileAccessible(const QString &filePath);
|
||||||
|
static void cleanupGlobalIntermediateStorage();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void fileOperationFailed(const QString &error);
|
||||||
|
void fileCopiedToStorage(const QString &originalPath, const QString &newPath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString copyToIntermediateStorage(const QString &filePath);
|
||||||
|
QString getIntermediateStorageDir();
|
||||||
|
QString generateIntermediateFileName(const QString &originalPath);
|
||||||
|
|
||||||
|
QString m_chatFilePath;
|
||||||
|
QString m_intermediateStorageDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
|
|
||||||
@ -22,6 +22,7 @@
|
|||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
@ -53,6 +54,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
, m_chatModel(new ChatModel(this))
|
, m_chatModel(new ChatModel(this))
|
||||||
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
|
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
|
||||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
||||||
|
, m_fileManager(new ChatFileManager(this))
|
||||||
, m_isRequestInProgress(false)
|
, m_isRequestInProgress(false)
|
||||||
{
|
{
|
||||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
@ -230,6 +232,11 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
&Utils::BaseAspect::changed,
|
&Utils::BaseAspect::changed,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::isThinkingSupportChanged);
|
&ChatRootView::isThinkingSupportChanged);
|
||||||
|
|
||||||
|
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
||||||
|
m_lastErrorMessage = error;
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatModel *ChatRootView::chatModel() const
|
ChatModel *ChatRootView::chatModel() const
|
||||||
@ -265,6 +272,8 @@ void ChatRootView::sendMessage(const QString &message)
|
|||||||
|
|
||||||
m_clientInterface
|
m_clientInterface
|
||||||
->sendMessage(message, m_attachmentFiles, m_linkedFiles, useTools(), useThinking());
|
->sendMessage(message, m_attachmentFiles, m_linkedFiles, useTools(), useThinking());
|
||||||
|
|
||||||
|
m_fileManager->clearIntermediateStorage();
|
||||||
clearAttachmentFiles();
|
clearAttachmentFiles();
|
||||||
setRequestProgressStatus(true);
|
setRequestProgressStatus(true);
|
||||||
}
|
}
|
||||||
@ -282,18 +291,23 @@ void ChatRootView::cancelRequest()
|
|||||||
|
|
||||||
void ChatRootView::clearAttachmentFiles()
|
void ChatRootView::clearAttachmentFiles()
|
||||||
{
|
{
|
||||||
if (!m_attachmentFiles.isEmpty()) {
|
if (m_attachmentFiles.isEmpty()) {
|
||||||
m_attachmentFiles.clear();
|
return;
|
||||||
emit attachmentFilesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_attachmentFiles.clear();
|
||||||
|
emit attachmentFilesChanged();
|
||||||
|
m_fileManager->clearIntermediateStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::clearLinkedFiles()
|
void ChatRootView::clearLinkedFiles()
|
||||||
{
|
{
|
||||||
if (!m_linkedFiles.isEmpty()) {
|
if (m_linkedFiles.isEmpty()) {
|
||||||
m_linkedFiles.clear();
|
return;
|
||||||
emit linkedFilesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_linkedFiles.clear();
|
||||||
|
emit linkedFilesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::getChatsHistoryDir() const
|
QString ChatRootView::getChatsHistoryDir() const
|
||||||
@ -304,8 +318,8 @@ QString ChatRootView::getChatsHistoryDir() const
|
|||||||
Settings::ProjectSettings projectSettings(project);
|
Settings::ProjectSettings projectSettings(project);
|
||||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||||
} else {
|
} else {
|
||||||
path = QString("%1/qodeassist/chat_history")
|
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
path = baseDir.filePath("qodeassist/chat_history");
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir dir(path);
|
QDir dir(path);
|
||||||
@ -342,6 +356,12 @@ void ChatRootView::loadHistory(const QString &filePath)
|
|||||||
setRecentFilePath(filePath);
|
setRecentFilePath(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_fileManager->clearIntermediateStorage();
|
||||||
|
m_attachmentFiles.clear();
|
||||||
|
m_linkedFiles.clear();
|
||||||
|
emit attachmentFilesChanged();
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
|
||||||
m_currentMessageRequestId.clear();
|
m_currentMessageRequestId.clear();
|
||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
updateCurrentMessageEditsStats();
|
updateCurrentMessageEditsStats();
|
||||||
@ -499,8 +519,10 @@ void ChatRootView::addFilesToAttachList(const QStringList &filePaths)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QStringList processedPaths = m_fileManager->processDroppedFiles(filePaths);
|
||||||
|
|
||||||
bool filesAdded = false;
|
bool filesAdded = false;
|
||||||
for (const QString &filePath : filePaths) {
|
for (const QString &filePath : processedPaths) {
|
||||||
if (!m_attachmentFiles.contains(filePath)) {
|
if (!m_attachmentFiles.contains(filePath)) {
|
||||||
m_attachmentFiles.append(filePath);
|
m_attachmentFiles.append(filePath);
|
||||||
filesAdded = true;
|
filesAdded = true;
|
||||||
@ -514,10 +536,15 @@ void ChatRootView::addFilesToAttachList(const QStringList &filePaths)
|
|||||||
|
|
||||||
void ChatRootView::removeFileFromAttachList(int index)
|
void ChatRootView::removeFileFromAttachList(int index)
|
||||||
{
|
{
|
||||||
if (index >= 0 && index < m_attachmentFiles.size()) {
|
if (index < 0 || index >= m_attachmentFiles.size()) {
|
||||||
m_attachmentFiles.removeAt(index);
|
return;
|
||||||
emit attachmentFilesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString removedFile = m_attachmentFiles.at(index);
|
||||||
|
m_attachmentFiles.removeAt(index);
|
||||||
|
emit attachmentFilesChanged();
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Removed attachment file: %1").arg(removedFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::showLinkFilesDialog()
|
void ChatRootView::showLinkFilesDialog()
|
||||||
@ -557,7 +584,6 @@ void ChatRootView::addFilesToLinkList(const QStringList &filePaths)
|
|||||||
|
|
||||||
if (!imageFiles.isEmpty()) {
|
if (!imageFiles.isEmpty()) {
|
||||||
addFilesToAttachList(imageFiles);
|
addFilesToAttachList(imageFiles);
|
||||||
|
|
||||||
m_lastInfoMessage
|
m_lastInfoMessage
|
||||||
= tr("Images automatically moved to Attach zone (%n file(s))", "", imageFiles.size());
|
= tr("Images automatically moved to Attach zone (%n file(s))", "", imageFiles.size());
|
||||||
emit lastInfoMessageChanged();
|
emit lastInfoMessageChanged();
|
||||||
@ -570,10 +596,15 @@ void ChatRootView::addFilesToLinkList(const QStringList &filePaths)
|
|||||||
|
|
||||||
void ChatRootView::removeFileFromLinkList(int index)
|
void ChatRootView::removeFileFromLinkList(int index)
|
||||||
{
|
{
|
||||||
if (index >= 0 && index < m_linkedFiles.size()) {
|
if (index < 0 || index >= m_linkedFiles.size()) {
|
||||||
m_linkedFiles.removeAt(index);
|
return;
|
||||||
emit linkedFilesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString removedFile = m_linkedFiles.at(index);
|
||||||
|
m_linkedFiles.removeAt(index);
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Removed linked file: %1").arg(removedFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::showAddImageDialog()
|
void ChatRootView::showAddImageDialog()
|
||||||
@ -587,19 +618,7 @@ void ChatRootView::showAddImageDialog()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dialog.exec() == QDialog::Accepted) {
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
QStringList newFilePaths = dialog.selectedFiles();
|
addFilesToAttachList(dialog.selectedFiles());
|
||||||
if (!newFilePaths.isEmpty()) {
|
|
||||||
bool filesAdded = false;
|
|
||||||
for (const QString &filePath : std::as_const(newFilePaths)) {
|
|
||||||
if (!m_attachmentFiles.contains(filePath)) {
|
|
||||||
m_attachmentFiles.append(filePath);
|
|
||||||
filesAdded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filesAdded) {
|
|
||||||
emit attachmentFilesChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,8 +664,8 @@ void ChatRootView::openChatHistoryFolder()
|
|||||||
Settings::ProjectSettings projectSettings(project);
|
Settings::ProjectSettings projectSettings(project);
|
||||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||||
} else {
|
} else {
|
||||||
path = QString("%1/qodeassist/chat_history")
|
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
path = baseDir.filePath("qodeassist/chat_history");
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir dir(path);
|
QDir dir(path);
|
||||||
@ -666,7 +685,7 @@ void ChatRootView::openRulesFolder()
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString projectPath = project->projectDirectory().toFSPathString();
|
QString projectPath = project->projectDirectory().toFSPathString();
|
||||||
QString rulesPath = projectPath + "/.qodeassist/rules";
|
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
|
||||||
|
|
||||||
QDir dir(rulesPath);
|
QDir dir(rulesPath);
|
||||||
if (!dir.exists()) {
|
if (!dir.exists()) {
|
||||||
@ -762,6 +781,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
|||||||
if (m_recentFilePath != filePath) {
|
if (m_recentFilePath != filePath) {
|
||||||
m_recentFilePath = filePath;
|
m_recentFilePath = filePath;
|
||||||
m_clientInterface->setChatFilePath(filePath);
|
m_clientInterface->setChatFilePath(filePath);
|
||||||
|
m_fileManager->setChatFilePath(filePath);
|
||||||
emit chatFileNameChanged();
|
emit chatFileNameChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#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>
|
||||||
|
|
||||||
@ -199,6 +200,7 @@ private:
|
|||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
LLMCore::PromptProviderChat m_promptProvider;
|
LLMCore::PromptProviderChat m_promptProvider;
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
|
ChatFileManager *m_fileManager;
|
||||||
QString m_currentTemplate;
|
QString m_currentTemplate;
|
||||||
QString m_recentFilePath;
|
QString m_recentFilePath;
|
||||||
QStringList m_attachmentFiles;
|
QStringList m_attachmentFiles;
|
||||||
|
|||||||
@ -60,6 +60,7 @@ ChatRootView {
|
|||||||
|
|
||||||
SplitDropZone {
|
SplitDropZone {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
z: 99
|
||||||
|
|
||||||
onFilesDroppedToAttach: (urlStrings) => {
|
onFilesDroppedToAttach: (urlStrings) => {
|
||||||
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
||||||
|
|||||||
@ -23,10 +23,12 @@ import QtQuick.Controls
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
signal filesDroppedToAttach(var urlStrings) // Array of URL strings (file://...)
|
signal filesDroppedToAttach(var urlStrings)
|
||||||
signal filesDroppedToLink(var urlStrings) // Array of URL strings (file://...)
|
signal filesDroppedToLink(var urlStrings)
|
||||||
|
|
||||||
property string activeZone: ""
|
property string activeZone: ""
|
||||||
|
property int filesCount: 0
|
||||||
|
property bool isDragActive: false
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: splitDropOverlay
|
id: splitDropOverlay
|
||||||
@ -34,12 +36,39 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: false
|
visible: false
|
||||||
z: 999
|
z: 999
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
|
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
topMargin: 30
|
||||||
|
}
|
||||||
|
width: fileCountText.width + 40
|
||||||
|
height: 50
|
||||||
|
color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.9)
|
||||||
|
radius: 25
|
||||||
|
visible: root.filesCount > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: fileCountText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("%n file(s) to drop", "", root.filesCount)
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
color: palette.highlightedText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: leftZone
|
id: leftZone
|
||||||
|
|
||||||
@ -76,19 +105,20 @@ Item {
|
|||||||
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("(for one-time use)")
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.italic: true
|
||||||
|
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
ColorAnimation { duration: 150 }
|
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||||
}
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@ -127,19 +157,20 @@ Item {
|
|||||||
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("(added to context)")
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.italic: true
|
||||||
|
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
ColorAnimation { duration: 150 }
|
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||||
}
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@ -193,42 +224,67 @@ Item {
|
|||||||
|
|
||||||
onEntered: (drag) => {
|
onEntered: (drag) => {
|
||||||
if (drag.hasUrls) {
|
if (drag.hasUrls) {
|
||||||
|
root.isDragActive = true
|
||||||
|
root.filesCount = drag.urls.length
|
||||||
splitDropOverlay.visible = true
|
splitDropOverlay.visible = true
|
||||||
|
splitDropOverlay.opacity = 1
|
||||||
root.activeZone = ""
|
root.activeZone = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: {
|
onExited: {
|
||||||
splitDropOverlay.visible = false
|
root.isDragActive = false
|
||||||
root.activeZone = ""
|
root.filesCount = 0
|
||||||
|
splitDropOverlay.opacity = 0
|
||||||
|
|
||||||
|
Qt.callLater(function() {
|
||||||
|
if (!root.isDragActive) {
|
||||||
|
splitDropOverlay.visible = false
|
||||||
|
root.activeZone = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onPositionChanged: (drag) => {
|
onPositionChanged: (drag) => {
|
||||||
if (drag.x < globalDropArea.width / 2) {
|
if (drag.hasUrls) {
|
||||||
root.activeZone = "left"
|
root.activeZone = drag.x < globalDropArea.width / 2 ? "left" : "right"
|
||||||
} else {
|
|
||||||
root.activeZone = "right"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDropped: (drop) => {
|
onDropped: (drop) => {
|
||||||
var targetZone = root.activeZone
|
const targetZone = root.activeZone
|
||||||
splitDropOverlay.visible = false
|
root.isDragActive = false
|
||||||
root.activeZone = ""
|
root.filesCount = 0
|
||||||
|
splitDropOverlay.opacity = 0
|
||||||
|
|
||||||
if (drop.hasUrls && drop.urls.length > 0) {
|
Qt.callLater(function() {
|
||||||
// Convert URLs to array of strings for C++ processing
|
splitDropOverlay.visible = false
|
||||||
var urlStrings = []
|
root.activeZone = ""
|
||||||
for (var i = 0; i < drop.urls.length; i++) {
|
})
|
||||||
urlStrings.push(drop.urls[i].toString())
|
|
||||||
}
|
if (!drop.hasUrls || drop.urls.length === 0) {
|
||||||
|
return
|
||||||
if (targetZone === "right") {
|
}
|
||||||
root.filesDroppedToLink(urlStrings)
|
|
||||||
} else {
|
var urlStrings = []
|
||||||
root.filesDroppedToAttach(urlStrings)
|
for (var i = 0; i < drop.urls.length; i++) {
|
||||||
|
var urlString = drop.urls[i].toString()
|
||||||
|
if (urlString.startsWith("file://") || urlString.indexOf("://") === -1) {
|
||||||
|
urlStrings.push(urlString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (urlStrings.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
drop.accept(Qt.CopyAction)
|
||||||
|
|
||||||
|
if (targetZone === "right") {
|
||||||
|
root.filesDroppedToLink(urlStrings)
|
||||||
|
} else {
|
||||||
|
root.filesDroppedToAttach(urlStrings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +62,7 @@
|
|||||||
#include "widgets/CustomInstructionsManager.hpp"
|
#include "widgets/CustomInstructionsManager.hpp"
|
||||||
#include "widgets/QuickRefactorDialog.hpp"
|
#include "widgets/QuickRefactorDialog.hpp"
|
||||||
#include <ChatView/ChatView.hpp>
|
#include <ChatView/ChatView.hpp>
|
||||||
|
#include <ChatView/ChatFileManager.hpp>
|
||||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||||
#include <coreplugin/actionmanager/actionmanager.h>
|
#include <coreplugin/actionmanager/actionmanager.h>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
@ -87,6 +88,8 @@ public:
|
|||||||
|
|
||||||
~QodeAssistPlugin() final
|
~QodeAssistPlugin() final
|
||||||
{
|
{
|
||||||
|
Chat::ChatFileManager::cleanupGlobalIntermediateStorage();
|
||||||
|
|
||||||
delete m_qodeAssistClient;
|
delete m_qodeAssistClient;
|
||||||
if (m_chatOutputPane) {
|
if (m_chatOutputPane) {
|
||||||
delete m_chatOutputPane;
|
delete m_chatOutputPane;
|
||||||
@ -249,6 +252,8 @@ public:
|
|||||||
editorContextMenu->addAction(closeChatViewAction.command(),
|
editorContextMenu->addAction(closeChatViewAction.command(),
|
||||||
Core::Constants::G_DEFAULT_THREE);
|
Core::Constants::G_DEFAULT_THREE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Chat::ChatFileManager::cleanupGlobalIntermediateStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void extensionsInitialized() final {}
|
void extensionsInitialized() final {}
|
||||||
|
|||||||
Reference in New Issue
Block a user