From a9c2f473032fe8a47226581601cb5a6fa3cbfd6f Mon Sep 17 00:00:00 2001 From: AT Date: Sat, 10 Jun 2023 10:15:38 -0400 Subject: [PATCH] Add new solution for context links that does not force regular markdown (#938) in responses which is disruptive to code completions in responses. --- gpt4all-chat/CMakeLists.txt | 1 + gpt4all-chat/main.qml | 29 +++++++- gpt4all-chat/qml/AboutDialog.qml | 4 +- gpt4all-chat/qml/Theme.qml | 4 +- gpt4all-chat/responsetext.cpp | 109 +++++++++++++++++++++++++++++++ gpt4all-chat/responsetext.h | 63 ++++++++++++++++++ 6 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 gpt4all-chat/responsetext.cpp create mode 100644 gpt4all-chat/responsetext.h diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt index bc513a5e..552cc6e2 100644 --- a/gpt4all-chat/CMakeLists.txt +++ b/gpt4all-chat/CMakeLists.txt @@ -76,6 +76,7 @@ qt_add_executable(chat llm.h llm.cpp server.h server.cpp logger.h logger.cpp + responsetext.h responsetext.cpp sysinfo.h ${METAL_SHADER_FILE} ) diff --git a/gpt4all-chat/main.qml b/gpt4all-chat/main.qml index da4d30bb..67a5a0f7 100644 --- a/gpt4all-chat/main.qml +++ b/gpt4all-chat/main.qml @@ -7,6 +7,7 @@ import Qt5Compat.GraphicalEffects import llm import download import network +import gpt4all Window { id: window @@ -580,11 +581,12 @@ Window { Accessible.description: qsTr("This is the list of prompt/response pairs comprising the actual conversation with the model") delegate: TextArea { + id: myTextArea text: value + references width: listView.width color: theme.textColor wrapMode: Text.WordWrap - textFormat: TextEdit.MarkdownText + textFormat: TextEdit.PlainText focus: false readOnly: true font.pixelSize: theme.fontSizeLarge @@ -597,6 +599,31 @@ Window { : (currentChat.isServer ? theme.backgroundDark : theme.backgroundLight) } + MouseArea { + id: mouseArea + anchors.fill: parent + propagateComposedEvents: true + onClicked: { + var clickedPos = myTextArea.positionAt(mouse.x, mouse.y); + var link = responseText.getLinkAtPosition(clickedPos); + if (!link.startsWith("context://")) + return + var integer = parseInt(link.split("://")[1]); + referenceContextDialog.text = referencesContext[integer - 1]; + referenceContextDialog.open(); + mouse.accepted = true; + } + } + + ResponseText { + id: responseText + } + + Component.onCompleted: { + responseText.textDocument = textDocument + responseText.setLinkColor(theme.linkColor); + } + Accessible.role: Accessible.Paragraph Accessible.name: name Accessible.description: name === qsTr("Response: ") ? "The response by the model" : "The prompt by the user" diff --git a/gpt4all-chat/qml/AboutDialog.qml b/gpt4all-chat/qml/AboutDialog.qml index 35d1ee43..fe6455bf 100644 --- a/gpt4all-chat/qml/AboutDialog.qml +++ b/gpt4all-chat/qml/AboutDialog.qml @@ -76,7 +76,7 @@ Dialog { Label { id: discordLink width: parent.width - textFormat: Text.RichText + textFormat: Text.StyledText wrapMode: Text.WordWrap text: qsTr("Check out our discord channel https://discord.gg/4M2QFmTt2k") onLinkActivated: { Qt.openUrlExternally("https://discord.gg/4M2QFmTt2k") } @@ -90,7 +90,7 @@ Dialog { Label { id: nomicProps width: parent.width - textFormat: Text.RichText + textFormat: Text.StyledText wrapMode: Text.WordWrap text: qsTr("Thank you to Nomic AI and the community for contributing so much great data, code, ideas, and energy to the growing open source AI ecosystem!") onLinkActivated: { Qt.openUrlExternally("https://home.nomic.ai") } diff --git a/gpt4all-chat/qml/Theme.qml b/gpt4all-chat/qml/Theme.qml index f35dfc5e..c4f90c59 100644 --- a/gpt4all-chat/qml/Theme.qml +++ b/gpt4all-chat/qml/Theme.qml @@ -18,8 +18,8 @@ QtObject { property color dialogBorder: "#d1d5db" property color userColor: "#ec86bf" property color assistantColor: "#10a37f" - property color linkColor: "white" - property color tabBorder: "#2C2D35" + property color linkColor: "#55aaff" + property color tabBorder: "#2c2d35" property real fontSizeLarge: Qt.application.font.pixelSize property real fontSizeLarger: Qt.application.font.pixelSize + 2 } diff --git a/gpt4all-chat/responsetext.cpp b/gpt4all-chat/responsetext.cpp new file mode 100644 index 00000000..aba3727a --- /dev/null +++ b/gpt4all-chat/responsetext.cpp @@ -0,0 +1,109 @@ +#include "responsetext.h" + +#include +#include +#include +#include +#include + +SyntaxHighlighter::SyntaxHighlighter(QObject *parent) + : QSyntaxHighlighter(parent) +{ +} + +SyntaxHighlighter::~SyntaxHighlighter() +{ +} + +void SyntaxHighlighter::highlightBlock(const QString &text) +{ + for (const HighlightingRule &rule : qAsConst(m_highlightingRules)) { + QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); + while (matchIterator.hasNext()) { + QRegularExpressionMatch match = matchIterator.next(); + setFormat(match.capturedStart(), match.capturedLength(), rule.format); + } + } +} + +ResponseText::ResponseText(QObject *parent) + : QObject{parent} + , m_textDocument(nullptr) + , m_syntaxHighlighter(new SyntaxHighlighter(this)) + , m_isProcessingText(false) +{ +} + +QQuickTextDocument* ResponseText::textDocument() const +{ + return m_textDocument; +} + +void ResponseText::setTextDocument(QQuickTextDocument* textDocument) +{ + if (m_textDocument) + disconnect(m_textDocument->textDocument(), &QTextDocument::contentsChanged, this, &ResponseText::handleTextChanged); + + m_textDocument = textDocument; + m_syntaxHighlighter->setDocument(m_textDocument->textDocument()); + connect(m_textDocument->textDocument(), &QTextDocument::contentsChanged, this, &ResponseText::handleTextChanged); +} + +QString ResponseText::getLinkAtPosition(int position) const +{ + int i = 0; + for (const auto &link : m_links) { + if (position >= link.startPos && position < link.endPos) + return link.href; + } + return QString(); +} + +void ResponseText::handleTextChanged() +{ + if (!m_textDocument || m_isProcessingText) + return; + + m_isProcessingText = true; + QTextDocument* doc = m_textDocument->textDocument(); + QTextCursor cursor(doc); + + QTextCharFormat linkFormat; + linkFormat.setForeground(m_linkColor); + linkFormat.setFontUnderline(true); + + // Loop through the document looking for context links + QRegularExpression re("\\[Context\\]\\((context://\\d+)\\)"); + QRegularExpressionMatchIterator i = re.globalMatch(doc->toPlainText()); + + QList matches; + while (i.hasNext()) + matches.append(i.next()); + + QVector newLinks; + + // Calculate new positions and store them in newLinks + int positionOffset = 0; + for(const auto &match : matches) { + ContextLink newLink; + newLink.href = match.captured(1); + newLink.text = "Context"; + newLink.startPos = match.capturedStart() - positionOffset; + newLink.endPos = newLink.startPos + newLink.text.length(); + newLinks.append(newLink); + positionOffset += match.capturedLength() - newLink.text.length(); + } + + // Replace the context links with the word "Context" in reverse order + for(int index = matches.count() - 1; index >= 0; --index) { + cursor.setPosition(matches.at(index).capturedStart()); + cursor.setPosition(matches.at(index).capturedEnd(), QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + cursor.setCharFormat(linkFormat); + cursor.insertText(newLinks.at(index).text); + cursor.setCharFormat(QTextCharFormat()); + } + + m_links = newLinks; + m_isProcessingText = false; +} diff --git a/gpt4all-chat/responsetext.h b/gpt4all-chat/responsetext.h new file mode 100644 index 00000000..90537aa1 --- /dev/null +++ b/gpt4all-chat/responsetext.h @@ -0,0 +1,63 @@ +#ifndef RESPONSETEXT_H +#define RESPONSETEXT_H + +#include +#include +#include +#include +#include + +struct HighlightingRule +{ + QRegularExpression pattern; + QTextCharFormat format; +}; + +class SyntaxHighlighter : public QSyntaxHighlighter { + Q_OBJECT +public: + SyntaxHighlighter(QObject *parent); + ~SyntaxHighlighter(); + + void highlightBlock(const QString &text) override; + +private: + QVector m_highlightingRules; +}; + +struct ContextLink { + int startPos = -1; + int endPos = -1; + QString text; + QString href; +}; + +class ResponseText : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQuickTextDocument* textDocument READ textDocument WRITE setTextDocument NOTIFY textDocumentChanged()) + QML_ELEMENT +public: + explicit ResponseText(QObject *parent = nullptr); + + QQuickTextDocument* textDocument() const; + void setTextDocument(QQuickTextDocument* textDocument); + + Q_INVOKABLE void setLinkColor(const QColor &c) { m_linkColor = c; } + Q_INVOKABLE QString getLinkAtPosition(int position) const; + +Q_SIGNALS: + void textDocumentChanged(); + +private Q_SLOTS: + void handleTextChanged(); + +private: + QQuickTextDocument *m_textDocument; + SyntaxHighlighter *m_syntaxHighlighter; + QVector m_links; + QColor m_linkColor; + bool m_isProcessingText = false; +}; + +#endif // RESPONSETEXT_H