diff --git a/src/chat/chatwindow.cpp b/src/chat/chatwindow.cpp index c3c481d..cca01d4 100644 --- a/src/chat/chatwindow.cpp +++ b/src/chat/chatwindow.cpp @@ -6,6 +6,8 @@ #include "config.h" #include +#include +#include #include #include #include @@ -30,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +57,65 @@ QString escapedHtml(const QString &text) return html; } +/** Lightweight Markdown to HTML for assistant messages. */ +QString renderInline(QString text) +{ + text = text.toHtmlEscaped(); + text.replace(QRegularExpression(QStringLiteral("`([^`]+)`")), + QStringLiteral("\\1")); + text.replace(QRegularExpression(QStringLiteral("\\*\\*([^*]+)\\*\\*")), + QStringLiteral("\\1")); + text.replace(QRegularExpression(QStringLiteral("\\[([^\\]]+)\\]\\((https?://[^)\\s]+)\\)")), + QStringLiteral("\\1")); + text.replace(QRegularExpression(QStringLiteral("(^|[\\s(])(https?://[^\\s<]+)")), + QStringLiteral("\\1\\2")); + return text; +} + +QString markdownToHtml(const QString &markdown) +{ + QString html; + QString code; + bool inCode = false; + + const QStringList lines = markdown.split(QLatin1Char('\n')); + for (const QString &line : lines) { + if (line.trimmed().startsWith(QStringLiteral("```"))) { + if (inCode) { + html += QStringLiteral("
%1
").arg(code.toHtmlEscaped()); + code.clear(); + } + inCode = !inCode; + continue; + } + if (inCode) { + code += line; + code += QLatin1Char('\n'); + continue; + } + + if (line.startsWith(QStringLiteral("### "))) { + html += QStringLiteral("

%1

").arg(renderInline(line.mid(4))); + } else if (line.startsWith(QStringLiteral("## "))) { + html += QStringLiteral("

%1

").arg(renderInline(line.mid(3))); + } else if (line.startsWith(QStringLiteral("# "))) { + html += QStringLiteral("

%1

").arg(renderInline(line.mid(2))); + } else if (line.startsWith(QStringLiteral("- ")) || + line.startsWith(QStringLiteral("* "))) { + html += QStringLiteral("
• %1
").arg(renderInline(line.mid(2))); + } else if (line.trimmed().isEmpty()) { + html += QStringLiteral("
"); + } else { + html += QStringLiteral("
%1
").arg(renderInline(line)); + } + } + + if (inCode) { + html += QStringLiteral("
%1
").arg(code.toHtmlEscaped()); + } + return html; +} + } // namespace class ChatSidebar : public QListWidget @@ -477,7 +539,9 @@ void ChatWindow::renderConversation() html += QStringLiteral( ""); if (m_current < 0 || m_conversations.isEmpty()) { @@ -508,13 +572,16 @@ void ChatWindow::renderConversation() message.role == QStringLiteral("user") ? QStringLiteral("user") : QStringLiteral("assistant"); html += QStringLiteral("
%1
").arg(role); - html += QStringLiteral("
%2
") - .arg(cssClass, escapedHtml(message.content)); + const QString body = message.role == QStringLiteral("assistant") + ? markdownToHtml(message.content) + : escapedHtml(message.content); + html += QStringLiteral("
%2
").arg(cssClass, body); html += QStringLiteral("
"); if (message.role == QStringLiteral("user")) { html += QStringLiteral("编辑").arg(i); } else if (message.role == QStringLiteral("assistant")) { + html += QStringLiteral("复制 · ").arg(i); html += QStringLiteral("分支").arg(i); if (i == conversation.messages.size() - 1) { html += QStringLiteral(" · 重试"); @@ -757,6 +824,13 @@ void ChatWindow::handleAnchor(const QUrl &url) editMessage(target.section(QLatin1Char('/'), -1).toInt()); } else if (target.startsWith(QStringLiteral("gxde-ai-see:fork/"))) { forkFrom(target.section(QLatin1Char('/'), -1).toInt()); + } else if (target.startsWith(QStringLiteral("gxde-ai-see:copy/"))) { + const int index = target.section(QLatin1Char('/'), -1).toInt(); + if (m_current >= 0 && index >= 0 && + index < m_conversations.at(m_current).messages.size()) { + QApplication::clipboard()->setText( + m_conversations.at(m_current).messages.at(index).content); + } } else if (target == QStringLiteral("gxde-ai-see:retry")) { retryLast(); }