mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2024-08-17 17:20:40 +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
|
||||
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}
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
#include "responsetext.h"
|
||||
#include "chatviewtextprocessor.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QChar>
|
||||
@ -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<CodeCopy> newCopies;
|
||||
|
||||
// Track the position in the document to handle non-code blocks
|
||||
int lastIndex = 0;
|
||||
QVector<QTextFrame*> 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<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
|
||||
#define RESPONSETEXT_H
|
||||
#ifndef CHATVIEWTEXTPROCESSOR_H
|
||||
#define CHATVIEWTEXTPROCESSOR_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QObject>
|
||||
@ -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<CodeCopy> m_copies;
|
||||
QColor m_linkColor;
|
||||
QColor m_headerColor;
|
||||
bool m_shouldProcessText = false;
|
||||
bool m_isProcessingText = false;
|
||||
};
|
||||
|
||||
#endif // RESPONSETEXT_H
|
||||
#endif // CHATVIEWTEXTPROCESSOR_H
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user