add markdown parsing + latex + tree-sitter highlighting for codeblocks in llm responses
This commit is contained in:
@@ -10,34 +10,92 @@ ChatGeneration::ChatGeneration(qint64 timestamp, QObject* parent)
|
||||
m_timer.setInterval(500);
|
||||
m_timer.setTimerType(Qt::CoarseTimer);
|
||||
connect(&m_timer, &QTimer::timeout, this, [this]() {
|
||||
if (!reasoningInFlight() && !contentInFlight()) {
|
||||
m_timer.stop();
|
||||
return;
|
||||
bool anyRunning = false;
|
||||
for (auto* segment : m_segments) {
|
||||
if (!segment->running())
|
||||
continue;
|
||||
anyRunning = true;
|
||||
segment->elapsedMsChanged();
|
||||
}
|
||||
Q_EMIT elapsedMsChanged();
|
||||
if (anyRunning)
|
||||
Q_EMIT elapsedMsChanged();
|
||||
if (!anyRunning && !m_streaming)
|
||||
m_timer.stop();
|
||||
});
|
||||
}
|
||||
|
||||
QString ChatGeneration::content() const {
|
||||
QStringList parts;
|
||||
for (const auto* segment : m_segments) {
|
||||
if (segment->type() != LlmSegment::Type::Content ||
|
||||
segment->text().isEmpty())
|
||||
continue;
|
||||
parts.append(segment->text());
|
||||
}
|
||||
return parts.join(QStringLiteral("\n\n"));
|
||||
}
|
||||
|
||||
QString ChatGeneration::reasoning() const {
|
||||
QStringList parts;
|
||||
for (const auto* segment : m_segments) {
|
||||
if (segment->type() != LlmSegment::Type::Reasoning ||
|
||||
segment->text().isEmpty())
|
||||
continue;
|
||||
parts.append(segment->text());
|
||||
}
|
||||
return parts.join(QStringLiteral("\n\n"));
|
||||
}
|
||||
|
||||
bool ChatGeneration::reasoningActive() const {
|
||||
if (!m_streaming)
|
||||
return false;
|
||||
if (!content().isEmpty())
|
||||
return false;
|
||||
return !hasRunningTool();
|
||||
}
|
||||
|
||||
qint64 ChatGeneration::reasoningElapsedMs() const {
|
||||
if (m_reasoningStartedAt <= 0)
|
||||
return 0;
|
||||
const qint64 end = m_reasoningEndedAt > 0
|
||||
? m_reasoningEndedAt
|
||||
: QDateTime::currentMSecsSinceEpoch();
|
||||
return end - m_reasoningStartedAt;
|
||||
qint64 total = 0;
|
||||
for (const auto* segment : m_segments)
|
||||
if (segment->type() == LlmSegment::Type::Reasoning)
|
||||
total += segment->elapsedMs();
|
||||
return total;
|
||||
}
|
||||
|
||||
qint64 ChatGeneration::contentElapsedMs() const {
|
||||
if (m_contentStartedAt <= 0)
|
||||
return 0;
|
||||
const qint64 end = m_contentEndedAt > 0
|
||||
? m_contentEndedAt
|
||||
: QDateTime::currentMSecsSinceEpoch();
|
||||
return end - m_contentStartedAt;
|
||||
qint64 total = 0;
|
||||
for (const auto* segment : m_segments)
|
||||
if (segment->type() == LlmSegment::Type::Content)
|
||||
total += segment->elapsedMs();
|
||||
return total;
|
||||
}
|
||||
|
||||
qint64 ChatGeneration::toolsElapsedMs() const {
|
||||
qint64 total = 0;
|
||||
for (const auto* segment : m_segments)
|
||||
if (segment->type() == LlmSegment::Type::ToolCall)
|
||||
total += segment->elapsedMs();
|
||||
return total;
|
||||
}
|
||||
|
||||
int ChatGeneration::toolCallCount() const {
|
||||
int count = 0;
|
||||
for (const auto* segment : m_segments)
|
||||
if (segment->type() == LlmSegment::Type::ToolCall)
|
||||
++count;
|
||||
return count;
|
||||
}
|
||||
|
||||
bool ChatGeneration::hasRunningTool() const {
|
||||
for (const auto* segment : m_segments)
|
||||
if (segment->type() == LlmSegment::Type::ToolCall &&
|
||||
segment->running())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void ChatGeneration::updateReasoningActive() {
|
||||
const bool active = m_streaming && m_content.isEmpty();
|
||||
const bool active = reasoningActive();
|
||||
if (m_reasoningActive == active)
|
||||
return;
|
||||
m_reasoningActive = active;
|
||||
@@ -45,60 +103,51 @@ void ChatGeneration::updateReasoningActive() {
|
||||
}
|
||||
|
||||
void ChatGeneration::setContent(const QString& value) {
|
||||
if (m_content == value)
|
||||
return;
|
||||
m_content = value;
|
||||
Q_EMIT contentChanged();
|
||||
}
|
||||
|
||||
void ChatGeneration::setReasoning(const QString& value) {
|
||||
if (m_reasoning == value)
|
||||
return;
|
||||
m_reasoning = value;
|
||||
Q_EMIT reasoningChanged();
|
||||
// Replaces the entire answer: the first content segment takes the
|
||||
// new text, any later content bursts are cleared.
|
||||
LlmSegment* first = nullptr;
|
||||
for (auto* segment : m_segments) {
|
||||
if (segment->type() != LlmSegment::Type::Content)
|
||||
continue;
|
||||
if (!first)
|
||||
first = segment;
|
||||
else
|
||||
segment->setText(QString());
|
||||
}
|
||||
if (!first) {
|
||||
// Do not materialize an empty content segment (e.g. the assistant
|
||||
// placeholder created before the stream starts).
|
||||
if (value.isEmpty())
|
||||
return;
|
||||
first = new LlmSegment(
|
||||
LlmSegment::Type::Content,
|
||||
QDateTime::currentMSecsSinceEpoch(),
|
||||
this);
|
||||
addSegment(first);
|
||||
}
|
||||
first->setText(value);
|
||||
}
|
||||
|
||||
void ChatGeneration::appendContent(const QString& piece) {
|
||||
if (piece.isEmpty())
|
||||
return;
|
||||
if (m_content.isEmpty()) {
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
if (reasoningInFlight())
|
||||
m_reasoningEndedAt = now;
|
||||
m_contentStartedAt = now;
|
||||
if (!m_timer.isActive())
|
||||
m_timer.start();
|
||||
for (auto* segment : m_segments) {
|
||||
if (segment->type() == LlmSegment::Type::Reasoning &&
|
||||
segment->running())
|
||||
segment->close();
|
||||
}
|
||||
m_content += piece;
|
||||
Q_EMIT contentChanged();
|
||||
updateReasoningActive();
|
||||
Q_EMIT elapsedMsChanged();
|
||||
openContentSegment()->appendText(piece);
|
||||
}
|
||||
|
||||
void ChatGeneration::appendReasoning(const QString& piece) {
|
||||
if (piece.isEmpty())
|
||||
return;
|
||||
if (m_reasoning.isEmpty()) {
|
||||
if (m_reasoningStartedAt <= 0)
|
||||
m_reasoningStartedAt = QDateTime::currentMSecsSinceEpoch();
|
||||
if (!m_timer.isActive())
|
||||
m_timer.start();
|
||||
}
|
||||
m_reasoning += piece;
|
||||
Q_EMIT reasoningChanged();
|
||||
updateReasoningActive();
|
||||
Q_EMIT elapsedMsChanged();
|
||||
}
|
||||
|
||||
void ChatGeneration::setElapsedMs(qint64 reasoningMs, qint64 contentMs) {
|
||||
if (reasoningMs > 0) {
|
||||
m_reasoningStartedAt = m_timestamp;
|
||||
m_reasoningEndedAt = m_timestamp + reasoningMs;
|
||||
}
|
||||
if (contentMs > 0) {
|
||||
m_contentStartedAt = m_timestamp;
|
||||
m_contentEndedAt = m_timestamp + contentMs;
|
||||
for (auto* segment : m_segments) {
|
||||
if (segment->type() == LlmSegment::Type::Content &&
|
||||
segment->running())
|
||||
segment->close();
|
||||
}
|
||||
openReasoningSegment()->appendText(piece);
|
||||
}
|
||||
|
||||
void ChatGeneration::setStreaming(bool value) {
|
||||
@@ -107,19 +156,94 @@ void ChatGeneration::setStreaming(bool value) {
|
||||
m_streaming = value;
|
||||
Q_EMIT streamingChanged();
|
||||
if (value) {
|
||||
if (m_reasoningStartedAt <= 0)
|
||||
m_reasoningStartedAt = QDateTime::currentMSecsSinceEpoch();
|
||||
if (!m_timer.isActive())
|
||||
m_timer.start();
|
||||
} else {
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
if (reasoningInFlight())
|
||||
m_reasoningEndedAt = now;
|
||||
if (contentInFlight())
|
||||
m_contentEndedAt = now;
|
||||
closeOpenSegments();
|
||||
m_timer.stop();
|
||||
Q_EMIT elapsedMsChanged();
|
||||
}
|
||||
Q_EMIT elapsedMsChanged();
|
||||
updateReasoningActive();
|
||||
}
|
||||
|
||||
LlmSegment* ChatGeneration::openContentSegment() {
|
||||
for (auto* segment : m_segments)
|
||||
if (segment->type() == LlmSegment::Type::Content &&
|
||||
segment->running())
|
||||
return segment;
|
||||
auto* segment = new LlmSegment(
|
||||
LlmSegment::Type::Content,
|
||||
QDateTime::currentMSecsSinceEpoch(),
|
||||
this);
|
||||
segment->begin();
|
||||
addSegment(segment);
|
||||
return segment;
|
||||
}
|
||||
|
||||
LlmSegment* ChatGeneration::openReasoningSegment() {
|
||||
for (auto* segment : m_segments)
|
||||
if (segment->type() == LlmSegment::Type::Reasoning &&
|
||||
segment->running())
|
||||
return segment;
|
||||
auto* segment = new LlmSegment(
|
||||
LlmSegment::Type::Reasoning,
|
||||
QDateTime::currentMSecsSinceEpoch(),
|
||||
this);
|
||||
segment->begin();
|
||||
addSegment(segment);
|
||||
return segment;
|
||||
}
|
||||
|
||||
LlmSegment* ChatGeneration::beginToolCall(
|
||||
const QString& name, const QString& toolCallId) {
|
||||
closeOpenSegments();
|
||||
auto* segment = new LlmSegment(
|
||||
LlmSegment::Type::ToolCall,
|
||||
QDateTime::currentMSecsSinceEpoch(),
|
||||
this);
|
||||
segment->setName(name);
|
||||
segment->setToolCallId(toolCallId);
|
||||
segment->setStatus(LlmSegment::Status::Running);
|
||||
segment->begin();
|
||||
addSegment(segment);
|
||||
Q_EMIT toolStateChanged();
|
||||
return segment;
|
||||
}
|
||||
|
||||
void ChatGeneration::addSegment(LlmSegment* segment) {
|
||||
if (!segment || m_segments.contains(segment))
|
||||
return;
|
||||
segment->setParent(this);
|
||||
connect(
|
||||
segment, &LlmSegment::textChanged, this, [this, segment]() {
|
||||
if (segment->type() == LlmSegment::Type::Reasoning)
|
||||
Q_EMIT reasoningChanged();
|
||||
else if (segment->type() == LlmSegment::Type::Content)
|
||||
Q_EMIT contentChanged();
|
||||
updateReasoningActive();
|
||||
});
|
||||
connect(
|
||||
segment, &LlmSegment::statusChanged, this, [this]() {
|
||||
Q_EMIT toolStateChanged();
|
||||
});
|
||||
connect(
|
||||
segment, &LlmSegment::resultChanged, this, [this]() {
|
||||
Q_EMIT toolStateChanged();
|
||||
});
|
||||
connect(
|
||||
segment, &LlmSegment::runningChanged, this, [this]() {
|
||||
Q_EMIT elapsedMsChanged();
|
||||
Q_EMIT toolStateChanged();
|
||||
updateReasoningActive();
|
||||
});
|
||||
m_segments.append(segment);
|
||||
Q_EMIT segmentsChanged();
|
||||
}
|
||||
|
||||
void ChatGeneration::closeOpenSegments() {
|
||||
for (auto* segment : m_segments)
|
||||
if (segment->running())
|
||||
segment->close();
|
||||
updateReasoningActive();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user