Add new solution for context links that does not force regular markdown (#938)

in responses which is disruptive to code completions in responses.
This commit is contained in:
AT 2023-06-10 10:15:38 -04:00 committed by GitHub
parent d3ba1295a7
commit a9c2f47303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 205 additions and 5 deletions

View File

@ -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}
)

View File

@ -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"

View File

@ -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 <a href=\"https://discord.gg/4M2QFmTt2k\">https://discord.gg/4M2QFmTt2k</a>")
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 <a href=\"https://home.nomic.ai\">Nomic AI</a> 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") }

View File

@ -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
}

View File

@ -0,0 +1,109 @@
#include "responsetext.h"
#include <QTextCharFormat>
#include <QTextDocument>
#include <QTextDocumentFragment>
#include <QFontMetricsF>
#include <QTextTableCell>
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<QRegularExpressionMatch> matches;
while (i.hasNext())
matches.append(i.next());
QVector<ContextLink> 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;
}

View File

@ -0,0 +1,63 @@
#ifndef RESPONSETEXT_H
#define RESPONSETEXT_H
#include <QObject>
#include <QQmlEngine>
#include <QQuickTextDocument>
#include <QSyntaxHighlighter>
#include <QRegularExpression>
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<HighlightingRule> 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<ContextLink> m_links;
QColor m_linkColor;
bool m_isProcessingText = false;
};
#endif // RESPONSETEXT_H