chore: restructure llm plugin to be more coherent & fix various bugs in llm ui
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m35s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 54s
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m35s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 54s
This commit is contained in:
+240
-146
@@ -1,6 +1,8 @@
|
||||
#include "chatstore.hpp"
|
||||
|
||||
#include "attachment.hpp"
|
||||
#include "llmclient.hpp"
|
||||
#include "markdownblock.hpp"
|
||||
#include "message.hpp"
|
||||
#include "segment.hpp"
|
||||
|
||||
@@ -11,15 +13,12 @@
|
||||
#include <QFileInfo>
|
||||
#include <QPointer>
|
||||
#include <QStandardPaths>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlError>
|
||||
#include <QSqlQuery>
|
||||
#include <QThreadPool>
|
||||
#include <QVector>
|
||||
#include <QUuid>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ZShell::llm {
|
||||
|
||||
namespace {
|
||||
@@ -76,22 +75,25 @@ struct AttachmentRow {
|
||||
struct MessageRow {
|
||||
bool user = false;
|
||||
qint64 timestamp = 0;
|
||||
QString content;
|
||||
QVector<GenerationRow> generations;
|
||||
QVector<AttachmentRow> attachments;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatStore::ChatStore(QObject* parent)
|
||||
: QObject(parent), m_connectionName(QUuid::createUuid().toString()) {
|
||||
ChatStore::ChatStore(LlmClient* client, QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_client(client)
|
||||
, m_connectionName(QUuid::createUuid().toString()) {
|
||||
openDb();
|
||||
load();
|
||||
}
|
||||
|
||||
ChatStore::~ChatStore() {
|
||||
if (m_connectionName.isEmpty()) return;
|
||||
QSqlDatabase db = QSqlDatabase::database(m_connectionName, false);
|
||||
db.close();
|
||||
QSqlDatabase handle = QSqlDatabase::database(m_connectionName, false);
|
||||
handle.close();
|
||||
QSqlDatabase::removeDatabase(m_connectionName);
|
||||
}
|
||||
|
||||
@@ -105,20 +107,20 @@ void ChatStore::openDb() {
|
||||
QStringLiteral("/zshell/chats.sqlite");
|
||||
QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
|
||||
|
||||
QSqlDatabase db =
|
||||
QSqlDatabase handle =
|
||||
QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
|
||||
db.setDatabaseName(m_dbPath);
|
||||
if (!db.open()) {
|
||||
handle.setDatabaseName(m_dbPath);
|
||||
if (!handle.open()) {
|
||||
qWarning() << "ChatStore: failed to open database" << m_dbPath << ":"
|
||||
<< db.lastError().text();
|
||||
<< handle.lastError().text();
|
||||
return;
|
||||
}
|
||||
{
|
||||
QSqlQuery pragma(db);
|
||||
QSqlQuery pragma(handle);
|
||||
pragma.exec(QStringLiteral("PRAGMA foreign_keys = ON"));
|
||||
}
|
||||
{
|
||||
QSqlQuery query(db);
|
||||
QSqlQuery query(handle);
|
||||
query.exec(QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS sessions (\n"
|
||||
" id TEXT PRIMARY KEY,\n"
|
||||
@@ -130,18 +132,23 @@ void ChatStore::openDb() {
|
||||
")"));
|
||||
}
|
||||
{
|
||||
QSqlQuery query(db);
|
||||
QSqlQuery query(handle);
|
||||
query.exec(QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS messages (\n"
|
||||
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||
" session_id TEXT NOT NULL REFERENCES sessions (id) "
|
||||
"ON DELETE CASCADE,\n"
|
||||
" role TEXT NOT NULL,\n"
|
||||
" content TEXT NOT NULL DEFAULT '',\n"
|
||||
" timestamp INTEGER NOT NULL\n"
|
||||
")"));
|
||||
if (!query.exec(QStringLiteral(
|
||||
"ALTER TABLE messages ADD COLUMN content TEXT "
|
||||
"NOT NULL DEFAULT ''"))) {
|
||||
}
|
||||
}
|
||||
{
|
||||
QSqlQuery query(db);
|
||||
QSqlQuery query(handle);
|
||||
query.exec(QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS generations (\n"
|
||||
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||
@@ -152,7 +159,7 @@ void ChatStore::openDb() {
|
||||
")"));
|
||||
}
|
||||
{
|
||||
QSqlQuery query(db);
|
||||
QSqlQuery query(handle);
|
||||
query.exec(QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS segments (\n"
|
||||
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||
@@ -170,7 +177,7 @@ void ChatStore::openDb() {
|
||||
")"));
|
||||
}
|
||||
{
|
||||
QSqlQuery query(db);
|
||||
QSqlQuery query(handle);
|
||||
query.exec(QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS attachments (\n"
|
||||
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||
@@ -184,7 +191,7 @@ void ChatStore::openDb() {
|
||||
")"));
|
||||
}
|
||||
{
|
||||
QSqlQuery query(db);
|
||||
QSqlQuery query(handle);
|
||||
query.exec(QStringLiteral(
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_session "
|
||||
"ON messages (session_id)"));
|
||||
@@ -200,93 +207,156 @@ void ChatStore::openDb() {
|
||||
}
|
||||
}
|
||||
|
||||
int ChatStore::count() const {
|
||||
int ChatStore::rowCount(const QModelIndex& parent) const {
|
||||
if (parent.isValid()) return 0;
|
||||
return static_cast<int>(m_sessions.size());
|
||||
}
|
||||
|
||||
QVariantList ChatStore::values() const {
|
||||
QVariantList vals;
|
||||
vals.reserve(m_sessions.size());
|
||||
for (const auto* session : m_sessions)
|
||||
vals.append(QVariant::fromValue(session));
|
||||
return vals;
|
||||
QVariant ChatStore::data(const QModelIndex& index, int role) const {
|
||||
if (role != Qt::UserRole || !index.isValid() || index.row() < 0 ||
|
||||
index.row() >= m_sessions.size())
|
||||
return {};
|
||||
return QVariant::fromValue(m_sessions.at(index.row()));
|
||||
}
|
||||
|
||||
ChatSession* ChatStore::at(int index) const {
|
||||
if (index < 0 || index >= m_sessions.size()) return nullptr;
|
||||
return m_sessions.at(index);
|
||||
QHash<int, QByteArray> ChatStore::roleNames() const {
|
||||
return {{Qt::UserRole, "modelData"}};
|
||||
}
|
||||
|
||||
ChatSession* ChatStore::insert(int index) {
|
||||
int ChatStore::rowOf(const ChatSession* session) const {
|
||||
for (int i = 0; i < m_sessions.size(); ++i)
|
||||
if (m_sessions.at(i) == session) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ChatStore::firstAfter(const ChatSession* a, const ChatSession* b) {
|
||||
if (a->pinned() != b->pinned()) return a->pinned();
|
||||
return a->updatedAtMs() > b->updatedAtMs();
|
||||
}
|
||||
|
||||
int ChatStore::sortedRow(const ChatSession* session) const {
|
||||
int row = 0;
|
||||
while (row < m_sessions.size() && !firstAfter(session, m_sessions.at(row)))
|
||||
++row;
|
||||
return row;
|
||||
}
|
||||
|
||||
void ChatStore::insertRow(ChatSession* session) {
|
||||
const int row = sortedRow(session);
|
||||
beginInsertRows(QModelIndex(), row, row);
|
||||
m_sessions.insert(row, session);
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void ChatStore::moveRow(int from, int to) {
|
||||
if (from < 0 || from >= m_sessions.size() || to < 0 ||
|
||||
to > m_sessions.size() || from == to || to == from + 1)
|
||||
return;
|
||||
if (!beginMoveRows(QModelIndex(), from, from, QModelIndex(), to)) return;
|
||||
m_sessions.move(from, to > from ? to - 1 : to);
|
||||
endMoveRows();
|
||||
}
|
||||
|
||||
void ChatStore::reposition(ChatSession* session) {
|
||||
if (m_loading) return;
|
||||
const int row = rowOf(session);
|
||||
if (row < 0) return;
|
||||
const int target = sortedRow(session);
|
||||
if (target == row) return;
|
||||
moveRow(row, target);
|
||||
}
|
||||
|
||||
ChatSession* ChatStore::draft() {
|
||||
if (m_draft) return m_draft;
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
const QString id = QUuid::createUuid().toString();
|
||||
{
|
||||
QSqlQuery query(db());
|
||||
query.prepare(
|
||||
"INSERT INTO sessions (id, title, created_at, updated_at) "
|
||||
"VALUES (:id, '', :created_at, :updated_at)");
|
||||
query.bindValue(":id", id);
|
||||
query.bindValue(":created_at", now);
|
||||
query.bindValue(":updated_at", now);
|
||||
if (!query.exec())
|
||||
qWarning() << "ChatStore: failed to insert session" << id << ":"
|
||||
<< query.lastError().text();
|
||||
m_draft = new ChatSession(this, QUuid::createUuid().toString(), this);
|
||||
m_draft->hydrate(QString(), QString(), now, now, 0, false);
|
||||
return m_draft;
|
||||
}
|
||||
|
||||
void ChatStore::commitDraft(ChatSession* session) {
|
||||
if (!session || session->isCommitted()) return;
|
||||
if (m_draft == session) m_draft = nullptr;
|
||||
|
||||
if (!writeSessionRow(session)) return;
|
||||
session->setCommitted(true);
|
||||
insertRow(session);
|
||||
}
|
||||
|
||||
bool ChatStore::writeSessionRow(ChatSession* session) {
|
||||
QSqlQuery query(db());
|
||||
query.prepare(
|
||||
"INSERT INTO sessions (id, title, icon, created_at, updated_at, "
|
||||
"pinned) VALUES (:id, :title, :icon, :created_at, :updated_at, "
|
||||
":pinned)");
|
||||
query.bindValue(":id", session->id());
|
||||
query.bindValue(":title", sqlText(session->title()));
|
||||
query.bindValue(":icon", sqlText(session->icon()));
|
||||
query.bindValue(":created_at", session->createdAtMs());
|
||||
query.bindValue(":updated_at", session->updatedAtMs());
|
||||
query.bindValue(":pinned", session->pinned() ? 1 : 0);
|
||||
if (!query.exec()) {
|
||||
qWarning() << "ChatStore: failed to insert session" << session->id()
|
||||
<< ":" << query.lastError().text();
|
||||
return false;
|
||||
}
|
||||
auto* session = new ChatSession(id, this);
|
||||
session->setMeta(QString(), now, now, 0);
|
||||
const int pos = index >= 0 && index <= m_sessions.size() ? index : 0;
|
||||
m_sessions.insert(pos, session);
|
||||
Q_EMIT countChanged();
|
||||
Q_EMIT valuesChanged();
|
||||
return session;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatStore::remove(int index) {
|
||||
removeSession(at(index));
|
||||
}
|
||||
void ChatStore::removeRow(ChatSession* session) {
|
||||
if (!session) return;
|
||||
const int row = rowOf(session);
|
||||
|
||||
void ChatStore::remove(ChatSession* chat) {
|
||||
removeSession(chat);
|
||||
}
|
||||
|
||||
void ChatStore::removeSession(ChatSession* session) {
|
||||
if (!session || !m_sessions.contains(session)) return;
|
||||
const QList<ChatSession*> before = m_sessions;
|
||||
Q_EMIT sessionRemoved(session);
|
||||
|
||||
{
|
||||
QSqlQuery query(db());
|
||||
query.prepare("DELETE FROM sessions WHERE id = :id");
|
||||
query.bindValue(":id", session->id());
|
||||
query.exec();
|
||||
}
|
||||
QDir(ChatSession::sandboxDirFor(session->id())).removeRecursively();
|
||||
m_sessions.removeOne(session);
|
||||
|
||||
if (row >= 0) {
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
m_sessions.removeAt(row);
|
||||
endRemoveRows();
|
||||
}
|
||||
session->deleteLater();
|
||||
notify(before);
|
||||
}
|
||||
|
||||
void ChatStore::move(int from, int to) {
|
||||
if (from < 0 || from >= m_sessions.size() || to < 0 ||
|
||||
to >= m_sessions.size() || from == to)
|
||||
void ChatStore::remove(ChatSession* chat) {
|
||||
if (!chat) return;
|
||||
m_pendingPersists.remove(chat);
|
||||
m_pendingClears.remove(chat);
|
||||
if (m_draft == chat) m_draft = nullptr;
|
||||
|
||||
if (!chat->isCommitted()) {
|
||||
Q_EMIT sessionRemoved(chat);
|
||||
chat->deleteLater();
|
||||
return;
|
||||
m_sessions.move(from, to);
|
||||
Q_EMIT valuesChanged();
|
||||
}
|
||||
|
||||
removeRow(chat);
|
||||
}
|
||||
|
||||
void ChatStore::clear() {
|
||||
const QList<ChatSession*> sessions = m_sessions;
|
||||
for (ChatSession* session : sessions)
|
||||
removeSession(session);
|
||||
remove(session);
|
||||
}
|
||||
|
||||
ChatSession* ChatStore::sessionById(const QString& id) {
|
||||
for (auto* session : m_sessions)
|
||||
if (session->id() == id) return session;
|
||||
return nullptr;
|
||||
void ChatStore::requestClear(ChatSession* session) {
|
||||
if (!session) return;
|
||||
m_pendingClears.insert(session);
|
||||
if (session->isLoaded() && m_client->streamingSession() != session)
|
||||
applyPendingClear(session);
|
||||
}
|
||||
|
||||
void ChatStore::setLlmClient(LlmClient* client) {
|
||||
m_llmClient = client;
|
||||
void ChatStore::applyPendingClear(ChatSession* session) {
|
||||
if (!session || !m_pendingClears.remove(session)) return;
|
||||
if (!session->isLoaded()) return;
|
||||
session->clearMessages();
|
||||
persist(session);
|
||||
}
|
||||
|
||||
void ChatStore::persist(ChatSession* session) {
|
||||
@@ -297,21 +367,23 @@ void ChatStore::persist(ChatSession* session) {
|
||||
}
|
||||
session->setUpdatedAt(QDateTime::currentMSecsSinceEpoch());
|
||||
if (!saveSession(session)) return;
|
||||
sortAndNotify();
|
||||
reposition(session);
|
||||
}
|
||||
|
||||
void ChatStore::saveMeta(ChatSession* session) {
|
||||
void ChatStore::saveFlags(ChatSession* session) {
|
||||
if (!session || !m_sessions.contains(session)) return;
|
||||
QSqlQuery query(db());
|
||||
query.prepare(
|
||||
"UPDATE sessions SET title = :title, icon = :icon "
|
||||
"WHERE id = :id");
|
||||
"UPDATE sessions SET title = :title, icon = :icon, "
|
||||
"pinned = :pinned WHERE id = :id");
|
||||
query.bindValue(":title", sqlText(session->title()));
|
||||
query.bindValue(":icon", sqlText(session->icon()));
|
||||
query.bindValue(":pinned", session->pinned() ? 1 : 0);
|
||||
query.bindValue(":id", session->id());
|
||||
if (!query.exec())
|
||||
qWarning() << "ChatStore: failed to save meta for" << session->id()
|
||||
qWarning() << "ChatStore: failed to save flags for" << session->id()
|
||||
<< ":" << query.lastError().text();
|
||||
reposition(session);
|
||||
}
|
||||
|
||||
bool ChatStore::saveSession(ChatSession* session) {
|
||||
@@ -326,10 +398,12 @@ bool ChatStore::saveSession(ChatSession* session) {
|
||||
{
|
||||
QSqlQuery query(handle);
|
||||
query.prepare(
|
||||
"UPDATE sessions SET title = :title, updated_at = :updated_at "
|
||||
"WHERE id = :id");
|
||||
"UPDATE sessions SET title = :title, icon = :icon, "
|
||||
"updated_at = :updated_at, pinned = :pinned WHERE id = :id");
|
||||
query.bindValue(":title", sqlText(session->title()));
|
||||
query.bindValue(":icon", sqlText(session->icon()));
|
||||
query.bindValue(":updated_at", session->updatedAtMs());
|
||||
query.bindValue(":pinned", session->pinned() ? 1 : 0);
|
||||
query.bindValue(":id", session->id());
|
||||
ok = query.exec();
|
||||
}
|
||||
@@ -342,8 +416,8 @@ bool ChatStore::saveSession(ChatSession* session) {
|
||||
if (ok) {
|
||||
QSqlQuery messageInsert(handle);
|
||||
ok = messageInsert.prepare(
|
||||
"INSERT INTO messages (session_id, role, timestamp) "
|
||||
"VALUES (:id, :role, :timestamp)");
|
||||
"INSERT INTO messages (session_id, role, content, timestamp) "
|
||||
"VALUES (:id, :role, :content, :timestamp)");
|
||||
QSqlQuery generationInsert(handle);
|
||||
ok = ok &&
|
||||
generationInsert.prepare(
|
||||
@@ -371,6 +445,7 @@ bool ChatStore::saveSession(ChatSession* session) {
|
||||
message->role() == ChatMessage::Role::User
|
||||
? QStringLiteral("user")
|
||||
: QStringLiteral("assistant"));
|
||||
messageInsert.bindValue(":content", sqlText(message->content()));
|
||||
messageInsert.bindValue(":timestamp", message->timestamp());
|
||||
if (!messageInsert.exec()) {
|
||||
ok = false;
|
||||
@@ -401,16 +476,24 @@ bool ChatStore::saveSession(ChatSession* session) {
|
||||
segmentInsert.bindValue(":gid", generationId);
|
||||
segmentInsert.bindValue(
|
||||
":type", segmentTypeName(segment->type()));
|
||||
segmentInsert.bindValue(":text", sqlText(segment->text()));
|
||||
segmentInsert.bindValue(":name", sqlText(segment->name()));
|
||||
const auto* text =
|
||||
qobject_cast<const LlmTextSegment*>(segment);
|
||||
segmentInsert.bindValue(
|
||||
":tool_call_id", sqlText(segment->toolCallId()));
|
||||
":text", sqlText(text ? text->text() : QString()));
|
||||
const auto* call =
|
||||
qobject_cast<const LlmToolCall*>(segment);
|
||||
segmentInsert.bindValue(
|
||||
":arguments", sqlText(segment->arguments()));
|
||||
":name", sqlText(call ? call->name() : QString()));
|
||||
segmentInsert.bindValue(
|
||||
":result", sqlText(segment->result()));
|
||||
":tool_call_id",
|
||||
sqlText(call ? call->toolCallId() : QString()));
|
||||
segmentInsert.bindValue(
|
||||
":status", static_cast<int>(segment->status()));
|
||||
":arguments",
|
||||
sqlText(call ? call->arguments() : QString()));
|
||||
segmentInsert.bindValue(
|
||||
":result", sqlText(call ? call->result() : QString()));
|
||||
segmentInsert.bindValue(
|
||||
":status", call ? static_cast<int>(call->status()) : 0);
|
||||
segmentInsert.bindValue(":elapsed_ms", segment->elapsedMs());
|
||||
segmentInsert.bindValue(":timestamp", segment->timestamp());
|
||||
if (!segmentInsert.exec()) {
|
||||
@@ -448,12 +531,13 @@ bool ChatStore::saveSession(ChatSession* session) {
|
||||
qWarning() << "ChatStore: saveSession" << id
|
||||
<< "commit failed, rolling back";
|
||||
handle.rollback();
|
||||
ok = false;
|
||||
qWarning() << "ChatStore: failed to save session" << session->id()
|
||||
<< ":" << handle.lastError().text();
|
||||
return false;
|
||||
}
|
||||
return ok;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
if (!session) return;
|
||||
const QString sessionId = session->id();
|
||||
@@ -467,15 +551,15 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
QList<MessageRow> rows;
|
||||
const QString connName = QUuid::createUuid().toString();
|
||||
{
|
||||
QSqlDatabase db =
|
||||
QSqlDatabase handle =
|
||||
QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connName);
|
||||
db.setDatabaseName(path);
|
||||
if (db.open()) {
|
||||
QSqlQuery busy(db);
|
||||
handle.setDatabaseName(path);
|
||||
if (handle.open()) {
|
||||
QSqlQuery busy(handle);
|
||||
busy.exec(QStringLiteral("PRAGMA busy_timeout = 3000"));
|
||||
QSqlQuery query(db);
|
||||
QSqlQuery query(handle);
|
||||
query.prepare(
|
||||
"SELECT id, role, timestamp FROM messages "
|
||||
"SELECT id, role, content, timestamp FROM messages "
|
||||
"WHERE session_id = :id ORDER BY rowid DESC");
|
||||
query.bindValue(":id", sessionId);
|
||||
if (!query.exec()) {
|
||||
@@ -487,8 +571,9 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
MessageRow message;
|
||||
message.user = query.value(1).toString() ==
|
||||
QLatin1String("user");
|
||||
message.timestamp = query.value(2).toLongLong();
|
||||
QSqlQuery generationQuery(db);
|
||||
message.content = query.value(2).toString();
|
||||
message.timestamp = query.value(3).toLongLong();
|
||||
QSqlQuery generationQuery(handle);
|
||||
generationQuery.prepare(
|
||||
"SELECT id, timestamp, is_active FROM "
|
||||
"generations WHERE message_id = :mid "
|
||||
@@ -501,7 +586,7 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
generationQuery.value(1).toLongLong();
|
||||
generation.active =
|
||||
generationQuery.value(2).toInt() != 0;
|
||||
QSqlQuery segmentQuery(db);
|
||||
QSqlQuery segmentQuery(handle);
|
||||
segmentQuery.prepare(
|
||||
"SELECT type, text, name, tool_call_id, "
|
||||
"arguments, result, status, elapsed_ms, "
|
||||
@@ -549,7 +634,7 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
<< messageId << ":"
|
||||
<< generationQuery.lastError().text();
|
||||
}
|
||||
QSqlQuery attachmentQuery(db);
|
||||
QSqlQuery attachmentQuery(handle);
|
||||
attachmentQuery.prepare(
|
||||
"SELECT name, path, kind, mime, size FROM "
|
||||
"attachments WHERE message_id = :mid "
|
||||
@@ -580,10 +665,10 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
rows.append(message);
|
||||
}
|
||||
}
|
||||
db.close();
|
||||
handle.close();
|
||||
} else {
|
||||
qWarning() << "ChatStore: failed to open database for load:"
|
||||
<< db.lastError().text();
|
||||
<< handle.lastError().text();
|
||||
}
|
||||
}
|
||||
QSqlDatabase::removeDatabase(connName);
|
||||
@@ -595,7 +680,7 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
ChatSession* s = session;
|
||||
if (!st || !s) return;
|
||||
|
||||
auto* model = s->model();
|
||||
auto* model = s->messagesModel();
|
||||
if (!model) return;
|
||||
QList<ChatMessage*> messages;
|
||||
for (const MessageRow& row : rows) {
|
||||
@@ -603,6 +688,7 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
row.user ? ChatMessage::Role::User
|
||||
: ChatMessage::Role::Assistant,
|
||||
row.timestamp);
|
||||
|
||||
if (!row.attachments.isEmpty()) {
|
||||
QList<Attachment*> attachments;
|
||||
for (const AttachmentRow& attachmentRow :
|
||||
@@ -619,27 +705,54 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
}
|
||||
message->setAttachments(attachments);
|
||||
}
|
||||
|
||||
if (row.user) {
|
||||
QString content = row.content;
|
||||
if (content.isEmpty() && !row.generations.isEmpty()) {
|
||||
for (const SegmentRow& segment :
|
||||
row.generations.first().segments)
|
||||
if (segmentTypeFromName(segment.type) ==
|
||||
LlmSegment::Type::Content) {
|
||||
content = segment.text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!content.isEmpty()) message->setContent(content);
|
||||
messages.append(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
int activeIndex = 0;
|
||||
for (int i = 0; i < row.generations.size(); ++i) {
|
||||
const GenerationRow& generationRow =
|
||||
row.generations.at(i);
|
||||
auto* generation =
|
||||
message->addGeneration(generationRow.timestamp);
|
||||
if (!generation) continue;
|
||||
for (const SegmentRow& segmentRow :
|
||||
generationRow.segments) {
|
||||
auto* segment = new LlmSegment(
|
||||
segmentTypeFromName(segmentRow.type),
|
||||
segmentRow.timestamp,
|
||||
generation);
|
||||
segment->setText(segmentRow.text);
|
||||
segment->setName(segmentRow.name);
|
||||
segment->setToolCallId(segmentRow.toolCallId);
|
||||
segment->appendArguments(segmentRow.arguments);
|
||||
segment->setResult(segmentRow.result);
|
||||
segment->setStatus(
|
||||
static_cast<LlmSegment::Status>(
|
||||
segmentRow.status));
|
||||
segment->restore(segmentRow.elapsedMs);
|
||||
const LlmSegment::Type type =
|
||||
segmentTypeFromName(segmentRow.type);
|
||||
LlmSegment* segment = nullptr;
|
||||
if (type == LlmSegment::Type::ToolCall) {
|
||||
auto* call = new LlmToolCall(
|
||||
segmentRow.timestamp, generation);
|
||||
call->setName(segmentRow.name);
|
||||
call->setToolCallId(segmentRow.toolCallId);
|
||||
call->appendArguments(segmentRow.arguments);
|
||||
call->setResult(segmentRow.result);
|
||||
call->setStatus(
|
||||
static_cast<LlmToolCall::Status>(
|
||||
segmentRow.status));
|
||||
call->restore(segmentRow.elapsedMs);
|
||||
segment = call;
|
||||
} else {
|
||||
auto* text = new LlmTextSegment(
|
||||
type, segmentRow.timestamp, generation);
|
||||
text->setText(segmentRow.text);
|
||||
text->restore(segmentRow.elapsedMs);
|
||||
segment = text;
|
||||
}
|
||||
generation->addSegment(segment);
|
||||
}
|
||||
if (generationRow.active) activeIndex = i;
|
||||
@@ -658,54 +771,35 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
|
||||
|
||||
s->markLoaded();
|
||||
|
||||
if (s->takeClearPending()) {
|
||||
s->clear();
|
||||
} else if (st->m_pendingPersists.remove(s)) {
|
||||
st->persist(s);
|
||||
}
|
||||
st->applyPendingClear(s);
|
||||
if (st->m_pendingPersists.remove(s)) st->persist(s);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void ChatStore::load() {
|
||||
m_loading = true;
|
||||
QSqlQuery query(db());
|
||||
query.exec(
|
||||
"SELECT s.id, s.title, s.icon, s.created_at, s.updated_at, s.pinned, "
|
||||
"(SELECT COUNT(*) FROM messages m WHERE m.session_id = s.id) AS cnt "
|
||||
"FROM sessions s ORDER BY s.pinned DESC, s.updated_at DESC");
|
||||
while (query.next()) {
|
||||
auto* session = new ChatSession(query.value(0).toString(), this);
|
||||
session->setMeta(
|
||||
auto* session = new ChatSession(this, query.value(0).toString(), this);
|
||||
session->hydrate(
|
||||
query.value(1).toString(),
|
||||
query.value(2).toString(),
|
||||
query.value(3).toLongLong(),
|
||||
query.value(4).toLongLong(),
|
||||
query.value(6).toInt());
|
||||
session->setIcon(query.value(2).toString());
|
||||
session->setPinned(query.value(5).toInt() != 0);
|
||||
query.value(6).toInt(),
|
||||
query.value(5).toInt() != 0);
|
||||
m_sessions.append(session);
|
||||
}
|
||||
sortAndNotify();
|
||||
}
|
||||
m_loading = false;
|
||||
|
||||
void ChatStore::sortAndNotify() {
|
||||
const QList<ChatSession*> before = m_sessions;
|
||||
std::stable_sort(
|
||||
m_sessions.begin(),
|
||||
m_sessions.end(),
|
||||
[](const ChatSession* a, const ChatSession* b) {
|
||||
if (a->pinned() != b->pinned()) return a->pinned() > b->pinned();
|
||||
return a->updatedAtMs() > b->updatedAtMs();
|
||||
});
|
||||
notify(before);
|
||||
}
|
||||
|
||||
void ChatStore::notify(const QList<ChatSession*>& before) {
|
||||
if (before.size() != m_sessions.size()) Q_EMIT countChanged();
|
||||
bool same = before.size() == m_sessions.size();
|
||||
for (int i = 0; same && i < m_sessions.size(); ++i)
|
||||
if (before.at(i) != m_sessions.at(i)) same = false;
|
||||
if (!same) Q_EMIT valuesChanged();
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
} // namespace ZShell::llm
|
||||
|
||||
Reference in New Issue
Block a user