mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2024-09-11 21:27:37 +03:00
Improved markdown support:
* Correctly displays inline code blocks with syntax highlighting turned on as well as markdown at the same time * Adds a context menu item for toggling markdown on and off which also which essentially turns on/off all text processing * Uses QTextDocument::MarkdownNoHTML to handle markdown in QTextDocument which allows display of html tags like normal, but unfortunately does not allow display of markdown tables as markdown Signed-off-by: Adam Treat <treat.adam@gmail.com>
This commit is contained in:
parent
d92252cab1
commit
6f52f602ef
@ -109,6 +109,7 @@ qt_add_executable(chat
|
|||||||
chatllm.h chatllm.cpp
|
chatllm.h chatllm.cpp
|
||||||
chatmodel.h chatlistmodel.h chatlistmodel.cpp
|
chatmodel.h chatlistmodel.h chatlistmodel.cpp
|
||||||
chatapi.h chatapi.cpp
|
chatapi.h chatapi.cpp
|
||||||
|
chatviewtextprocessor.h chatviewtextprocessor.cpp
|
||||||
database.h database.cpp
|
database.h database.cpp
|
||||||
download.h download.cpp
|
download.h download.cpp
|
||||||
embllm.cpp embllm.h
|
embllm.cpp embllm.h
|
||||||
@ -119,7 +120,6 @@ qt_add_executable(chat
|
|||||||
network.h network.cpp
|
network.h network.cpp
|
||||||
server.h server.cpp
|
server.h server.cpp
|
||||||
logger.h logger.cpp
|
logger.h logger.cpp
|
||||||
responsetext.h responsetext.cpp
|
|
||||||
${APP_ICON_RESOURCE}
|
${APP_ICON_RESOURCE}
|
||||||
${CHAT_EXE_RESOURCES}
|
${CHAT_EXE_RESOURCES}
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#include "responsetext.h"
|
#include "chatviewtextprocessor.h"
|
||||||
|
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
#include <QChar>
|
#include <QChar>
|
||||||
@ -35,7 +35,8 @@ enum Language {
|
|||||||
Csharp,
|
Csharp,
|
||||||
Latex,
|
Latex,
|
||||||
Html,
|
Html,
|
||||||
Php
|
Php,
|
||||||
|
Markdown
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO (Adam) These should be themeable and not hardcoded since they are quite harsh on the eyes in
|
// 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
|
// 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
|
// indices and the replacement indices and then use the original text that is stored in memory in the
|
||||||
// chat class to populate the clipboard.
|
// chat class to populate the clipboard.
|
||||||
ResponseText::ResponseText(QObject *parent)
|
ChatViewTextProcessor::ChatViewTextProcessor(QObject *parent)
|
||||||
: QObject{parent}
|
: QObject{parent}
|
||||||
, m_textDocument(nullptr)
|
, m_textDocument(nullptr)
|
||||||
, m_syntaxHighlighter(new SyntaxHighlighter(this))
|
, m_syntaxHighlighter(new SyntaxHighlighter(this))
|
||||||
, m_isProcessingText(false)
|
, m_isProcessingText(false)
|
||||||
|
, m_shouldProcessText(true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
QQuickTextDocument* ResponseText::textDocument() const
|
QQuickTextDocument* ChatViewTextProcessor::textDocument() const
|
||||||
{
|
{
|
||||||
return m_textDocument;
|
return m_textDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResponseText::setTextDocument(QQuickTextDocument* textDocument)
|
void ChatViewTextProcessor::setTextDocument(QQuickTextDocument* textDocument)
|
||||||
{
|
{
|
||||||
if (m_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_textDocument = textDocument;
|
||||||
m_syntaxHighlighter->setDocument(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();
|
handleTextChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ResponseText::tryCopyAtPosition(int position) const
|
bool ChatViewTextProcessor::tryCopyAtPosition(int position) const
|
||||||
{
|
{
|
||||||
for (const auto © : m_copies) {
|
for (const auto © : m_copies) {
|
||||||
if (position >= copy.startPos && position <= copy.endPos) {
|
if (position >= copy.startPos && position <= copy.endPos) {
|
||||||
@ -904,9 +906,67 @@ bool ResponseText::tryCopyAtPosition(int position) const
|
|||||||
return false;
|
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;
|
return;
|
||||||
|
|
||||||
m_isProcessingText = true;
|
m_isProcessingText = true;
|
||||||
@ -917,6 +977,7 @@ void ResponseText::handleTextChanged()
|
|||||||
(void)doc->documentLayout()->documentSize();
|
(void)doc->documentLayout()->documentSize();
|
||||||
|
|
||||||
handleCodeBlocks();
|
handleCodeBlocks();
|
||||||
|
handleMarkdown();
|
||||||
|
|
||||||
// We insert an invisible char at the end to make sure the document goes back to the default
|
// We insert an invisible char at the end to make sure the document goes back to the default
|
||||||
// text format
|
// text format
|
||||||
@ -926,18 +987,7 @@ void ResponseText::handleTextChanged()
|
|||||||
m_isProcessingText = false;
|
m_isProcessingText = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void replaceAndInsertMarkdown(int startIndex, int endIndex, QTextDocument *doc)
|
void ChatViewTextProcessor::handleCodeBlocks()
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
QTextDocument* doc = m_textDocument->textDocument();
|
QTextDocument* doc = m_textDocument->textDocument();
|
||||||
QTextCursor cursor(doc);
|
QTextCursor cursor(doc);
|
||||||
@ -997,21 +1047,15 @@ void ResponseText::handleCodeBlocks()
|
|||||||
matchesCode.append(iCode.next());
|
matchesCode.append(iCode.next());
|
||||||
|
|
||||||
QVector<CodeCopy> newCopies;
|
QVector<CodeCopy> newCopies;
|
||||||
|
QVector<QTextFrame*> frames;
|
||||||
// Track the position in the document to handle non-code blocks
|
|
||||||
int lastIndex = 0;
|
|
||||||
|
|
||||||
for(int index = matchesCode.count() - 1; index >= 0; --index) {
|
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].capturedStart());
|
||||||
cursor.setPosition(matchesCode[index].capturedEnd(), QTextCursor::KeepAnchor);
|
cursor.setPosition(matchesCode[index].capturedEnd(), QTextCursor::KeepAnchor);
|
||||||
cursor.removeSelectedText();
|
cursor.removeSelectedText();
|
||||||
|
|
||||||
|
int startPos = cursor.position();
|
||||||
|
|
||||||
QTextFrameFormat frameFormat = frameFormatBase;
|
QTextFrameFormat frameFormat = frameFormatBase;
|
||||||
QString capturedText = matchesCode[index].captured(1);
|
QString capturedText = matchesCode[index].captured(1);
|
||||||
QString codeLanguage;
|
QString codeLanguage;
|
||||||
@ -1100,12 +1144,66 @@ void ResponseText::handleCodeBlocks()
|
|||||||
|
|
||||||
cursor = mainFrame->lastCursorPosition();
|
cursor = mainFrame->lastCursorPosition();
|
||||||
cursor.setCharFormat(QTextCharFormat());
|
cursor.setCharFormat(QTextCharFormat());
|
||||||
|
|
||||||
lastIndex = matchesCode[index].capturedEnd();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex < doc->characterCount())
|
|
||||||
replaceAndInsertMarkdown(lastIndex, doc->characterCount() - 1, doc);
|
|
||||||
|
|
||||||
m_copies = newCopies;
|
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<QPair<int, int>> 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<int, int> &a, const QPair<int, int> &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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
#ifndef RESPONSETEXT_H
|
#ifndef CHATVIEWTEXTPROCESSOR_H
|
||||||
#define RESPONSETEXT_H
|
#define CHATVIEWTEXTPROCESSOR_H
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
@ -37,13 +37,14 @@ struct CodeCopy {
|
|||||||
QString text;
|
QString text;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ResponseText : public QObject
|
class ChatViewTextProcessor : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QQuickTextDocument* textDocument READ textDocument WRITE setTextDocument NOTIFY textDocumentChanged())
|
Q_PROPERTY(QQuickTextDocument* textDocument READ textDocument WRITE setTextDocument NOTIFY textDocumentChanged())
|
||||||
|
Q_PROPERTY(bool shouldProcessText READ shouldProcessText WRITE setShouldProcessText NOTIFY shouldProcessTextChanged())
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
public:
|
public:
|
||||||
explicit ResponseText(QObject *parent = nullptr);
|
explicit ChatViewTextProcessor(QObject *parent = nullptr);
|
||||||
|
|
||||||
QQuickTextDocument* textDocument() const;
|
QQuickTextDocument* textDocument() const;
|
||||||
void setTextDocument(QQuickTextDocument* textDocument);
|
void setTextDocument(QQuickTextDocument* textDocument);
|
||||||
@ -53,12 +54,17 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE bool tryCopyAtPosition(int position) const;
|
Q_INVOKABLE bool tryCopyAtPosition(int position) const;
|
||||||
|
|
||||||
|
bool shouldProcessText() const;
|
||||||
|
void setShouldProcessText(bool b);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void textDocumentChanged();
|
void textDocumentChanged();
|
||||||
|
void shouldProcessTextChanged();
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void handleTextChanged();
|
void handleTextChanged();
|
||||||
void handleCodeBlocks();
|
void handleCodeBlocks();
|
||||||
|
void handleMarkdown();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QQuickTextDocument *m_textDocument;
|
QQuickTextDocument *m_textDocument;
|
||||||
@ -67,7 +73,8 @@ private:
|
|||||||
QVector<CodeCopy> m_copies;
|
QVector<CodeCopy> m_copies;
|
||||||
QColor m_linkColor;
|
QColor m_linkColor;
|
||||||
QColor m_headerColor;
|
QColor m_headerColor;
|
||||||
|
bool m_shouldProcessText = false;
|
||||||
bool m_isProcessingText = false;
|
bool m_isProcessingText = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // RESPONSETEXT_H
|
#endif // CHATVIEWTEXTPROCESSOR_H
|
@ -824,7 +824,7 @@ Rectangle {
|
|||||||
id: tapHandler
|
id: tapHandler
|
||||||
onTapped: function(eventPoint, button) {
|
onTapped: function(eventPoint, button) {
|
||||||
var clickedPos = myTextArea.positionAt(eventPoint.position.x, eventPoint.position.y);
|
var clickedPos = myTextArea.positionAt(eventPoint.position.x, eventPoint.position.y);
|
||||||
var success = responseText.tryCopyAtPosition(clickedPos);
|
var success = textProcessor.tryCopyAtPosition(clickedPos);
|
||||||
if (success)
|
if (success)
|
||||||
copyCodeMessage.open();
|
copyCodeMessage.open();
|
||||||
}
|
}
|
||||||
@ -862,16 +862,24 @@ Rectangle {
|
|||||||
myTextArea.deselect()
|
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 {
|
ChatViewTextProcessor {
|
||||||
id: responseText
|
id: textProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
responseText.setLinkColor(theme.linkColor);
|
textProcessor.setLinkColor(theme.linkColor);
|
||||||
responseText.setHeaderColor(name === qsTr("Response: ") ? theme.darkContrast : theme.lightContrast);
|
textProcessor.setHeaderColor(name === qsTr("Response: ") ? theme.darkContrast : theme.lightContrast);
|
||||||
responseText.textDocument = textDocument
|
textProcessor.textDocument = textDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
Accessible.role: Accessible.Paragraph
|
Accessible.role: Accessible.Paragraph
|
||||||
|
Loading…
Reference in New Issue
Block a user