diff --git a/ChatView/ChatModel.cpp b/ChatView/ChatModel.cpp index 28f7f3f..3f8a700 100644 --- a/ChatView/ChatModel.cpp +++ b/ChatView/ChatModel.cpp @@ -78,11 +78,26 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const return message.content; } case Roles::Attachments: { - QStringList filenames; + QVariantList attachmentsList; for (const auto &attachment : message.attachments) { - filenames << attachment.filename; + QVariantMap attachmentMap; + attachmentMap["fileName"] = attachment.filename; + attachmentMap["storedPath"] = attachment.content; + + if (!m_chatFilePath.isEmpty()) { + QFileInfo fileInfo(m_chatFilePath); + QString baseName = fileInfo.completeBaseName(); + QString dirPath = fileInfo.absolutePath(); + QString contentFolder = QDir(dirPath).filePath(baseName + "_content"); + QString fullPath = QDir(contentFolder).filePath(attachment.content); + attachmentMap["filePath"] = fullPath; + } else { + attachmentMap["filePath"] = QString(); + } + + attachmentsList.append(attachmentMap); } - return filenames; + return attachmentsList; } case Roles::IsRedacted: { return message.isRedacted; @@ -99,8 +114,8 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const QFileInfo fileInfo(m_chatFilePath); QString baseName = fileInfo.completeBaseName(); QString dirPath = fileInfo.absolutePath(); - QString imagesFolder = QDir(dirPath).filePath(baseName + "_images"); - QString fullPath = QDir(imagesFolder).filePath(image.storedPath); + QString contentFolder = QDir(dirPath).filePath(baseName + "_content"); + QString fullPath = QDir(contentFolder).filePath(image.storedPath); imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString(); } else { imageMap["imageUrl"] = QString(); @@ -135,15 +150,6 @@ void ChatModel::addMessage( bool isRedacted, const QString &signature) { - QString fullContent = content; - if (!attachments.isEmpty()) { - fullContent += "\n\nAttached files list:"; - for (const auto &attachment : attachments) { - fullContent += QString("\nname: %1\nfile content:\n%2") - .arg(attachment.filename, attachment.content); - } - } - if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id && m_messages.last().role == role) { Message &lastMessage = m_messages.last(); diff --git a/ChatView/ChatSerializer.cpp b/ChatView/ChatSerializer.cpp index 91c458e..a4c0b26 100644 --- a/ChatView/ChatSerializer.cpp +++ b/ChatView/ChatSerializer.cpp @@ -30,7 +30,7 @@ namespace QodeAssist::Chat { -const QString ChatSerializer::VERSION = "0.1"; +const QString ChatSerializer::VERSION = "0.2"; SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath) { @@ -38,11 +38,11 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt return {false, "Failed to create directory structure"}; } - QString imagesFolder = getChatImagesFolder(filePath); + QString contentFolder = getChatContentFolder(filePath); QDir dir; - if (!dir.exists(imagesFolder)) { - if (!dir.mkpath(imagesFolder)) { - LOG_MESSAGE(QString("Warning: Failed to create images folder: %1").arg(imagesFolder)); + if (!dir.exists(contentFolder)) { + if (!dir.mkpath(contentFolder)) { + LOG_MESSAGE(QString("Warning: Failed to create content folder: %1").arg(contentFolder)); } } @@ -103,6 +103,17 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message, messageObj["signature"] = message.signature; } + if (!message.attachments.isEmpty()) { + QJsonArray attachmentsArray; + for (const auto &attachment : message.attachments) { + QJsonObject attachmentObj; + attachmentObj["fileName"] = attachment.filename; + attachmentObj["storedPath"] = attachment.content; + attachmentsArray.append(attachmentObj); + } + messageObj["attachments"] = attachmentsArray; + } + if (!message.images.isEmpty()) { QJsonArray imagesArray; for (const auto &image : message.images) { @@ -127,6 +138,17 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c message.isRedacted = json["isRedacted"].toBool(false); message.signature = json["signature"].toString(); + if (json.contains("attachments")) { + QJsonArray attachmentsArray = json["attachments"].toArray(); + for (const auto &attachmentValue : attachmentsArray) { + QJsonObject attachmentObj = attachmentValue.toObject(); + Context::ContentFile attachment; + attachment.filename = attachmentObj["fileName"].toString(); + attachment.content = attachmentObj["storedPath"].toString(); + message.attachments.append(attachment); + } + } + if (json.contains("images")) { QJsonArray imagesArray = json["images"].toArray(); for (const auto &imageValue : imagesArray) { @@ -192,27 +214,36 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath) bool ChatSerializer::validateVersion(const QString &version) { - return version == VERSION; + if (version == VERSION) { + return true; + } + + if (version == "0.1") { + LOG_MESSAGE("Loading chat from old format 0.1 - images folder structure has changed from _images to _content"); + return true; + } + + return false; } -QString ChatSerializer::getChatImagesFolder(const QString &chatFilePath) +QString ChatSerializer::getChatContentFolder(const QString &chatFilePath) { QFileInfo fileInfo(chatFilePath); QString baseName = fileInfo.completeBaseName(); QString dirPath = fileInfo.absolutePath(); - return QDir(dirPath).filePath(baseName + "_images"); + return QDir(dirPath).filePath(baseName + "_content"); } -bool ChatSerializer::saveImageToStorage(const QString &chatFilePath, - const QString &fileName, - const QString &base64Data, - QString &storedPath) +bool ChatSerializer::saveContentToStorage(const QString &chatFilePath, + const QString &fileName, + const QString &base64Data, + QString &storedPath) { - QString imagesFolder = getChatImagesFolder(chatFilePath); + QString contentFolder = getChatContentFolder(chatFilePath); QDir dir; - if (!dir.exists(imagesFolder)) { - if (!dir.mkpath(imagesFolder)) { - LOG_MESSAGE(QString("Failed to create images folder: %1").arg(imagesFolder)); + if (!dir.exists(contentFolder)) { + if (!dir.mkpath(contentFolder)) { + LOG_MESSAGE(QString("Failed to create content folder: %1").arg(contentFolder)); return false; } } @@ -225,43 +256,43 @@ bool ChatSerializer::saveImageToStorage(const QString &chatFilePath, .arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)) .arg(extension); - QString fullPath = QDir(imagesFolder).filePath(uniqueName); + QString fullPath = QDir(contentFolder).filePath(uniqueName); - QByteArray imageData = QByteArray::fromBase64(base64Data.toUtf8()); + QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8()); QFile file(fullPath); if (!file.open(QIODevice::WriteOnly)) { LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath)); return false; } - if (file.write(imageData) == -1) { - LOG_MESSAGE(QString("Failed to write image data: %1").arg(file.errorString())); + if (file.write(contentData) == -1) { + LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString())); return false; } file.close(); storedPath = uniqueName; - LOG_MESSAGE(QString("Saved image: %1 to %2").arg(fileName, fullPath)); + LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath)); return true; } -QString ChatSerializer::loadImageFromStorage(const QString &chatFilePath, const QString &storedPath) +QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, const QString &storedPath) { - QString imagesFolder = getChatImagesFolder(chatFilePath); - QString fullPath = QDir(imagesFolder).filePath(storedPath); + QString contentFolder = getChatContentFolder(chatFilePath); + QString fullPath = QDir(contentFolder).filePath(storedPath); QFile file(fullPath); if (!file.open(QIODevice::ReadOnly)) { - LOG_MESSAGE(QString("Failed to open image file: %1").arg(fullPath)); + LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath)); return QString(); } - QByteArray imageData = file.readAll(); + QByteArray contentData = file.readAll(); file.close(); - return imageData.toBase64(); + return contentData.toBase64(); } } // namespace QodeAssist::Chat diff --git a/ChatView/ChatSerializer.hpp b/ChatView/ChatSerializer.hpp index 6a67ff1..faa81db 100644 --- a/ChatView/ChatSerializer.hpp +++ b/ChatView/ChatSerializer.hpp @@ -45,13 +45,13 @@ public: static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath); static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath); - // Image management - static QString getChatImagesFolder(const QString &chatFilePath); - static bool saveImageToStorage(const QString &chatFilePath, - const QString &fileName, - const QString &base64Data, - QString &storedPath); - static QString loadImageFromStorage(const QString &chatFilePath, const QString &storedPath); + // Content management (images and text files) + static QString getChatContentFolder(const QString &chatFilePath); + static bool saveContentToStorage(const QString &chatFilePath, + const QString &fileName, + const QString &base64Data, + QString &storedPath); + static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath); private: static const QString VERSION; diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index f8c805c..d38ae37 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -88,7 +88,24 @@ void ClientInterface::sendMessage( } } - auto attachFiles = m_contextManager->getContentFiles(textFiles); + QList storedAttachments; + if (!textFiles.isEmpty() && !m_chatFilePath.isEmpty()) { + auto attachFiles = m_contextManager->getContentFiles(textFiles); + for (const auto &file : attachFiles) { + QString storedPath; + if (ChatSerializer::saveContentToStorage( + m_chatFilePath, file.filename, file.content.toUtf8().toBase64(), storedPath)) { + Context::ContentFile storedFile; + storedFile.filename = file.filename; + storedFile.content = storedPath; + storedAttachments.append(storedFile); + LOG_MESSAGE(QString("Stored text file %1 as %2").arg(file.filename, storedPath)); + } + } + } else if (!textFiles.isEmpty()) { + LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 text file(s)") + .arg(textFiles.size())); + } QList imageAttachments; if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) { @@ -100,7 +117,7 @@ void ClientInterface::sendMessage( QString storedPath; QFileInfo fileInfo(imagePath); - if (ChatSerializer::saveImageToStorage( + if (ChatSerializer::saveContentToStorage( m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) { ChatModel::ImageAttachment imageAttachment; imageAttachment.fileName = fileInfo.fileName(); @@ -116,7 +133,7 @@ void ClientInterface::sendMessage( .arg(imageFiles.size())); } - m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles, imageAttachments); + m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments); auto &chatAssistantSettings = Settings::chatAssistantSettings(); @@ -182,6 +199,19 @@ void ClientInterface::sendMessage( LLMCore::Message apiMessage; apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant"; apiMessage.content = msg.content; + + if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) { + apiMessage.content += "\n\nAttached files:"; + for (const auto &attachment : msg.attachments) { + QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content); + if (!fileContent.isEmpty()) { + QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8())); + apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```") + .arg(attachment.filename, decodedContent); + } + } + } + apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking); apiMessage.isRedacted = msg.isRedacted; apiMessage.signature = msg.signature; @@ -499,7 +529,7 @@ QVector ClientInterface::loadImagesFromStorage( for (const auto &storedImage : storedImages) { QString base64Data - = ChatSerializer::loadImageFromStorage(m_chatFilePath, storedImage.storedPath); + = ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath); if (base64Data.isEmpty()) { LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath)); continue; diff --git a/ChatView/qml/chatparts/ChatItem.qml b/ChatView/qml/chatparts/ChatItem.qml index 404f879..e5e0393 100644 --- a/ChatView/qml/chatparts/ChatItem.qml +++ b/ChatView/qml/chatparts/ChatItem.qml @@ -121,24 +121,11 @@ Rectangle { Repeater { id: attachmentsModel - delegate: Rectangle { + delegate: AttachmentComponent { required property int index required property var modelData - height: attachText.implicitHeight + 8 - width: attachText.implicitWidth + 16 - radius: 4 - color: palette.button - border.width: 1 - border.color: palette.mid - - Text { - id: attachText - - anchors.centerIn: parent - text: modelData - color: palette.text - } + itemData: modelData } } } @@ -239,6 +226,68 @@ Rectangle { codeFontSize: root.codeFontSize } + component AttachmentComponent : Rectangle { + required property var itemData + + height: attachFileText.implicitHeight + 8 + width: attachFileText.implicitWidth + 16 + radius: 4 + color: attachFileMouseArea.containsMouse ? Qt.lighter(palette.button, 1.1) : palette.button + border.width: 1 + border.color: palette.mid + + Behavior on color { ColorAnimation { duration: 100 } } + + FileItem { + id: fileItem + filePath: itemData.filePath || "" + } + + Text { + id: attachFileText + + anchors.centerIn: parent + text: (itemData.fileName || "") + color: palette.buttonText + font.pointSize: root.textFontSize - 1 + } + + MouseArea { + id: attachFileMouseArea + + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + + onClicked: (mouse) => { + if (mouse.button === Qt.LeftButton) { + fileItem.openFileInEditor() + } else if (mouse.button === Qt.RightButton) { + attachmentContextMenu.popup() + } + } + + ToolTip.visible: containsMouse + ToolTip.text: qsTr("Left click: Open in Qt Creator\nRight click: More options") + ToolTip.delay: 500 + } + + Menu { + id: attachmentContextMenu + + MenuItem { + text: qsTr("Open in Qt Creator") + onTriggered: fileItem.openFileInEditor() + } + + MenuItem { + text: qsTr("Open in System Editor") + onTriggered: fileItem.openFileInExternalEditor() + } + } + } + component ImageComponent : Rectangle { required property var itemData @@ -248,10 +297,17 @@ Rectangle { width: Math.min(imageDisplay.implicitWidth, maxImageWidth) + 16 height: imageDisplay.implicitHeight + fileNameText.implicitHeight + 16 radius: 4 - color: palette.base + color: imageMouseArea.containsMouse ? Qt.lighter(palette.base, 1.05) : palette.base border.width: 1 border.color: palette.mid + Behavior on color { ColorAnimation { duration: 100 } } + + FileItem { + id: imageFileItem + filePath: itemData.imageUrl ? itemData.imageUrl.toString().replace("file://", "") : "" + } + ColumnLayout { anchors.fill: parent anchors.margins: 8 @@ -259,6 +315,7 @@ Rectangle { Image { id: imageDisplay + Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.parent.maxImageWidth Layout.maximumHeight: parent.parent.maxImageHeight @@ -289,6 +346,7 @@ Rectangle { Text { id: fileNameText + Layout.fillWidth: true text: itemData.fileName || "" color: palette.text @@ -297,5 +355,40 @@ Rectangle { horizontalAlignment: Text.AlignHCenter } } + + MouseArea { + id: imageMouseArea + + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + + onClicked: (mouse) => { + if (mouse.button === Qt.LeftButton) { + imageFileItem.openFileInEditor() + } else if (mouse.button === Qt.RightButton) { + imageContextMenu.popup() + } + } + + ToolTip.visible: containsMouse + ToolTip.text: qsTr("Left click: Open in System\nRight click: More options") + ToolTip.delay: 500 + } + + Menu { + id: imageContextMenu + + MenuItem { + text: qsTr("Open in Qt Creator") + onTriggered: imageFileItem.openFileInEditor() + } + + MenuItem { + text: qsTr("Open in System Viewer") + onTriggered: imageFileItem.openFileInExternalEditor() + } + } } } diff --git a/context/ContextManager.cpp b/context/ContextManager.cpp index f5d0f2a..3f776e7 100644 --- a/context/ContextManager.cpp +++ b/context/ContextManager.cpp @@ -43,11 +43,17 @@ ContextManager::ContextManager(QObject *parent) QString ContextManager::readFile(const QString &filePath) const { QFile file(filePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + LOG_MESSAGE(QString("Failed to open file for reading: %1 - %2") + .arg(filePath, file.errorString())); return QString(); + } QTextStream in(&file); - return in.readAll(); + QString content = in.readAll(); + file.close(); + + return content; } QList ContextManager::getContentFiles(const QStringList &filePaths) const diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f0f9cea..6e4835b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -33,6 +33,31 @@ If issues persist, you can reset settings to their default values: - API keys are preserved during reset - You will need to re-select your model after reset +## Chat History Migration + +### Images not showing in old chats (version 0.5.x → 0.6.x) + +If you have chat histories from QodeAssist version 0.5.x or earlier, images may not display correctly due to a storage structure change. + +**Solution:** Rename the content folder for each affected chat: + +```bash +# Navigate to your chat history folder +cd ~/path/to/chat_history + +# For each chat file, rename its folder +mv chat_name_images chat_name_content +``` + +**Example:** +```bash +mv my_conversation_2024-11-28_images my_conversation_2024-11-28_content +``` + +**What changed:** +- Old format (v0.1): Stored files in `chat_name_images/` +- New format (v0.2): Stores all content in `chat_name_content/` (both images and text files) + ## Common Issues ### Plugin doesn't appear after installation diff --git a/widgets/QuickRefactorDialog.cpp b/widgets/QuickRefactorDialog.cpp index 6ef490d..4c3bae4 100644 --- a/widgets/QuickRefactorDialog.cpp +++ b/widgets/QuickRefactorDialog.cpp @@ -155,6 +155,9 @@ void QuickRefactorDialog::setupUi() connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); mainLayout->addWidget(buttonBox); + + setTabOrder(m_commandsComboBox, m_textEdit); + setTabOrder(m_textEdit, buttonBox); } void QuickRefactorDialog::createActionButtons()