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