diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt index f0b71fc6..2b978519 100644 --- a/gpt4all-chat/CMakeLists.txt +++ b/gpt4all-chat/CMakeLists.txt @@ -109,6 +109,7 @@ qt_add_executable(chat chatllm.h chatllm.cpp chatmodel.h chatlistmodel.h chatlistmodel.cpp chatapi.h chatapi.cpp + chatviewtextprocessor.h chatviewtextprocessor.cpp database.h database.cpp download.h download.cpp embllm.cpp embllm.h @@ -119,7 +120,6 @@ qt_add_executable(chat network.h network.cpp server.h server.cpp logger.h logger.cpp - responsetext.h responsetext.cpp ${APP_ICON_RESOURCE} ${CHAT_EXE_RESOURCES} ) diff --git a/gpt4all-chat/responsetext.cpp b/gpt4all-chat/chatviewtextprocessor.cpp similarity index 89% rename from gpt4all-chat/responsetext.cpp rename to gpt4all-chat/chatviewtextprocessor.cpp index ea39c433..9d4e3643 100644 --- a/gpt4all-chat/responsetext.cpp +++ b/gpt4all-chat/chatviewtextprocessor.cpp @@ -1,4 +1,4 @@ -#include "responsetext.h" +#include "chatviewtextprocessor.h" #include #include @@ -35,7 +35,8 @@ enum Language { Csharp, Latex, Html, - Php + Php, + Markdown }; // TODO (Adam) These should be themeable and not hardcoded since they are quite harsh on the eyes in @@ -868,31 +869,32 @@ void SyntaxHighlighter::highlightBlock(const QString &text) // not the replaced text. A possible solution is to have this class keep a mapping of the original // indices and the replacement indices and then use the original text that is stored in memory in the // chat class to populate the clipboard. -ResponseText::ResponseText(QObject *parent) +ChatViewTextProcessor::ChatViewTextProcessor(QObject *parent) : QObject{parent} , m_textDocument(nullptr) , m_syntaxHighlighter(new SyntaxHighlighter(this)) , m_isProcessingText(false) + , m_shouldProcessText(true) { } -QQuickTextDocument* ResponseText::textDocument() const +QQuickTextDocument* ChatViewTextProcessor::textDocument() const { return m_textDocument; } -void ResponseText::setTextDocument(QQuickTextDocument* textDocument) +void ChatViewTextProcessor::setTextDocument(QQuickTextDocument* textDocument) { if (m_textDocument) - disconnect(m_textDocument->textDocument(), &QTextDocument::contentsChanged, this, &ResponseText::handleTextChanged); + disconnect(m_textDocument->textDocument(), &QTextDocument::contentsChanged, this, &ChatViewTextProcessor::handleTextChanged); m_textDocument = textDocument; m_syntaxHighlighter->setDocument(m_textDocument->textDocument()); - connect(m_textDocument->textDocument(), &QTextDocument::contentsChanged, this, &ResponseText::handleTextChanged); + connect(m_textDocument->textDocument(), &QTextDocument::contentsChanged, this, &ChatViewTextProcessor::handleTextChanged); handleTextChanged(); } -bool ResponseText::tryCopyAtPosition(int position) const +bool ChatViewTextProcessor::tryCopyAtPosition(int position) const { for (const auto © : m_copies) { if (position >= copy.startPos && position <= copy.endPos) { @@ -904,9 +906,67 @@ bool ResponseText::tryCopyAtPosition(int position) const return false; } -void ResponseText::handleTextChanged() +bool ChatViewTextProcessor::shouldProcessText() const { - if (!m_textDocument || m_isProcessingText) + return m_shouldProcessText; +} + +void ChatViewTextProcessor::setShouldProcessText(bool b) +{ + if (m_shouldProcessText == b) + return; + m_shouldProcessText = b; + emit shouldProcessTextChanged(); + handleTextChanged(); +} + +void traverseDocument(QTextDocument *doc) +{ + QTextFrame *rootFrame = doc->rootFrame(); + QTextFrame::iterator rootIt; + + for (rootIt = rootFrame->begin(); !rootIt.atEnd(); ++rootIt) { + QTextFrame *childFrame = rootIt.currentFrame(); + QTextBlock childBlock = rootIt.currentBlock(); + + if (childFrame) { + qDebug() << "Frame from" << childFrame->firstPosition() << "to" << childFrame->lastPosition(); + + // Iterate over blocks within the frame + QTextFrame::iterator frameIt; + for (frameIt = childFrame->begin(); !frameIt.atEnd(); ++frameIt) { + QTextBlock block = frameIt.currentBlock(); + if (block.isValid()) { + qDebug() << " Block position:" << block.position(); + qDebug() << " Block text:" << block.text(); + + // Iterate over lines within the block + for (QTextBlock::iterator blockIt = block.begin(); !(blockIt.atEnd()); ++blockIt) { + QTextFragment fragment = blockIt.fragment(); + if (fragment.isValid()) { + qDebug() << " Fragment text:" << fragment.text(); + } + } + } + } + } else if (childBlock.isValid()) { + qDebug() << "Block position:" << childBlock.position(); + qDebug() << "Block text:" << childBlock.text(); + + // Iterate over lines within the block + for (QTextBlock::iterator blockIt = childBlock.begin(); !(blockIt.atEnd()); ++blockIt) { + QTextFragment fragment = blockIt.fragment(); + if (fragment.isValid()) { + qDebug() << " Fragment text:" << fragment.text(); + } + } + } + } +} + +void ChatViewTextProcessor::handleTextChanged() +{ + if (!m_textDocument || m_isProcessingText || !m_shouldProcessText) return; m_isProcessingText = true; @@ -917,6 +977,7 @@ void ResponseText::handleTextChanged() (void)doc->documentLayout()->documentSize(); handleCodeBlocks(); + handleMarkdown(); // We insert an invisible char at the end to make sure the document goes back to the default // text format @@ -926,18 +987,7 @@ void ResponseText::handleTextChanged() m_isProcessingText = false; } -void replaceAndInsertMarkdown(int startIndex, int endIndex, QTextDocument *doc) -{ - QTextCursor cursor(doc); - cursor.setPosition(startIndex); - cursor.setPosition(endIndex, QTextCursor::KeepAnchor); - QTextDocumentFragment fragment(cursor); - const QString plainText = fragment.toPlainText(); - cursor.removeSelectedText(); - cursor.insertMarkdown(plainText); -} - -void ResponseText::handleCodeBlocks() +void ChatViewTextProcessor::handleCodeBlocks() { QTextDocument* doc = m_textDocument->textDocument(); QTextCursor cursor(doc); @@ -997,21 +1047,15 @@ void ResponseText::handleCodeBlocks() matchesCode.append(iCode.next()); QVector newCopies; - - // Track the position in the document to handle non-code blocks - int lastIndex = 0; + QVector frames; for(int index = matchesCode.count() - 1; index >= 0; --index) { - - int nonCodeStart = lastIndex; - int nonCodeEnd = matchesCode[index].capturedStart(); - if (nonCodeEnd > nonCodeStart) - replaceAndInsertMarkdown(nonCodeStart, nonCodeEnd, doc); - cursor.setPosition(matchesCode[index].capturedStart()); cursor.setPosition(matchesCode[index].capturedEnd(), QTextCursor::KeepAnchor); cursor.removeSelectedText(); + int startPos = cursor.position(); + QTextFrameFormat frameFormat = frameFormatBase; QString capturedText = matchesCode[index].captured(1); QString codeLanguage; @@ -1100,12 +1144,66 @@ void ResponseText::handleCodeBlocks() cursor = mainFrame->lastCursorPosition(); cursor.setCharFormat(QTextCharFormat()); - - lastIndex = matchesCode[index].capturedEnd(); } - if (lastIndex < doc->characterCount()) - replaceAndInsertMarkdown(lastIndex, doc->characterCount() - 1, doc); - m_copies = newCopies; } + +void replaceAndInsertMarkdown(int startIndex, int endIndex, QTextDocument *doc) +{ + QTextCursor cursor(doc); + cursor.setPosition(startIndex); + cursor.setPosition(endIndex, QTextCursor::KeepAnchor); + QTextDocumentFragment fragment(cursor); + const QString plainText = fragment.toPlainText(); + cursor.removeSelectedText(); + cursor.insertMarkdown(plainText, QTextDocument::MarkdownNoHTML); + cursor.block().setUserState(Markdown); +} + +void ChatViewTextProcessor::handleMarkdown() +{ + QTextDocument* doc = m_textDocument->textDocument(); + QTextCursor cursor(doc); + + QVector> codeBlockPositions; + + QTextFrame *rootFrame = doc->rootFrame(); + QTextFrame::iterator rootIt; + + bool hasAlreadyProcessedMarkdown = false; + for (rootIt = rootFrame->begin(); !rootIt.atEnd(); ++rootIt) { + QTextFrame *childFrame = rootIt.currentFrame(); + QTextBlock childBlock = rootIt.currentBlock(); + if (childFrame) { + codeBlockPositions.append(qMakePair(childFrame->firstPosition()-1, childFrame->lastPosition()+1)); + + for (QTextFrame::iterator frameIt = childFrame->begin(); !frameIt.atEnd(); ++frameIt) { + QTextBlock block = frameIt.currentBlock(); + if (block.isValid() && block.userState() == Markdown) + hasAlreadyProcessedMarkdown = true; + } + } else if (childBlock.isValid() && childBlock.userState() == Markdown) + hasAlreadyProcessedMarkdown = true; + } + + + if (!hasAlreadyProcessedMarkdown) { + std::sort(codeBlockPositions.begin(), codeBlockPositions.end(), [](const QPair &a, const QPair &b) { + return a.first > b.first; + }); + + int lastIndex = doc->characterCount() - 1; + for (const auto &pos : codeBlockPositions) { + int nonCodeStart = pos.second; + int nonCodeEnd = lastIndex; + if (nonCodeEnd > nonCodeStart) { + replaceAndInsertMarkdown(nonCodeStart, nonCodeEnd, doc); + } + lastIndex = pos.first; + } + + if (lastIndex > 0) + replaceAndInsertMarkdown(0, lastIndex, doc); + } +} diff --git a/gpt4all-chat/responsetext.h b/gpt4all-chat/chatviewtextprocessor.h similarity index 75% rename from gpt4all-chat/responsetext.h rename to gpt4all-chat/chatviewtextprocessor.h index 88f463bd..7f2cc73e 100644 --- a/gpt4all-chat/responsetext.h +++ b/gpt4all-chat/chatviewtextprocessor.h @@ -1,5 +1,5 @@ -#ifndef RESPONSETEXT_H -#define RESPONSETEXT_H +#ifndef CHATVIEWTEXTPROCESSOR_H +#define CHATVIEWTEXTPROCESSOR_H #include #include @@ -37,13 +37,14 @@ struct CodeCopy { QString text; }; -class ResponseText : public QObject +class ChatViewTextProcessor : public QObject { Q_OBJECT Q_PROPERTY(QQuickTextDocument* textDocument READ textDocument WRITE setTextDocument NOTIFY textDocumentChanged()) + Q_PROPERTY(bool shouldProcessText READ shouldProcessText WRITE setShouldProcessText NOTIFY shouldProcessTextChanged()) QML_ELEMENT public: - explicit ResponseText(QObject *parent = nullptr); + explicit ChatViewTextProcessor(QObject *parent = nullptr); QQuickTextDocument* textDocument() const; void setTextDocument(QQuickTextDocument* textDocument); @@ -53,12 +54,17 @@ public: Q_INVOKABLE bool tryCopyAtPosition(int position) const; + bool shouldProcessText() const; + void setShouldProcessText(bool b); + Q_SIGNALS: void textDocumentChanged(); + void shouldProcessTextChanged(); private Q_SLOTS: void handleTextChanged(); void handleCodeBlocks(); + void handleMarkdown(); private: QQuickTextDocument *m_textDocument; @@ -67,7 +73,8 @@ private: QVector m_copies; QColor m_linkColor; QColor m_headerColor; + bool m_shouldProcessText = false; bool m_isProcessingText = false; }; -#endif // RESPONSETEXT_H +#endif // CHATVIEWTEXTPROCESSOR_H diff --git a/gpt4all-chat/qml/ChatView.qml b/gpt4all-chat/qml/ChatView.qml index b9a017c3..38a66c1f 100644 --- a/gpt4all-chat/qml/ChatView.qml +++ b/gpt4all-chat/qml/ChatView.qml @@ -824,7 +824,7 @@ Rectangle { id: tapHandler onTapped: function(eventPoint, button) { var clickedPos = myTextArea.positionAt(eventPoint.position.x, eventPoint.position.y); - var success = responseText.tryCopyAtPosition(clickedPos); + var success = textProcessor.tryCopyAtPosition(clickedPos); if (success) copyCodeMessage.open(); } @@ -862,16 +862,24 @@ Rectangle { myTextArea.deselect() } } + MenuItem { + text: textProcessor.shouldProcessText ? qsTr("Disable markdown") : qsTr("Enable markdown") + height: enabled ? implicitHeight : 0 + onTriggered: { + textProcessor.shouldProcessText = !textProcessor.shouldProcessText; + myTextArea.text = value + } + } } - ResponseText { - id: responseText + ChatViewTextProcessor { + id: textProcessor } Component.onCompleted: { - responseText.setLinkColor(theme.linkColor); - responseText.setHeaderColor(name === qsTr("Response: ") ? theme.darkContrast : theme.lightContrast); - responseText.textDocument = textDocument + textProcessor.setLinkColor(theme.linkColor); + textProcessor.setHeaderColor(name === qsTr("Response: ") ? theme.darkContrast : theme.lightContrast); + textProcessor.textDocument = textDocument } Accessible.role: Accessible.Paragraph