new chatlistview component, file attachments, performance & primitive test

This commit is contained in:
2026-09-30 15:01:28 +02:00
parent 51ed270774
commit d8a3d5732e
36 changed files with 4368 additions and 310 deletions
+96
View File
@@ -65,10 +65,19 @@ struct GenerationRow {
QVector<SegmentRow> segments;
};
struct AttachmentRow {
QString name;
QString path;
int kind = 1;
QString mime;
qint64 size = 0;
};
struct MessageRow {
bool user = false;
qint64 timestamp = 0;
QVector<GenerationRow> generations;
QVector<AttachmentRow> attachments;
};
} // namespace
@@ -160,6 +169,20 @@ void ChatStore::openDb() {
" timestamp INTEGER NOT NULL\n"
")"));
}
{
QSqlQuery query(db);
query.exec(QStringLiteral(
"CREATE TABLE IF NOT EXISTS attachments (\n"
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
" message_id INTEGER NOT NULL REFERENCES messages "
"(id) ON DELETE CASCADE,\n"
" name TEXT NOT NULL,\n"
" path TEXT NOT NULL,\n"
" kind INTEGER NOT NULL DEFAULT 1,\n"
" mime TEXT NOT NULL DEFAULT '',\n"
" size INTEGER NOT NULL DEFAULT 0\n"
")"));
}
{
QSqlQuery query(db);
query.exec(QStringLiteral(
@@ -171,6 +194,9 @@ void ChatStore::openDb() {
query.exec(QStringLiteral(
"CREATE INDEX IF NOT EXISTS idx_segments_generation "
"ON segments (generation_id)"));
query.exec(QStringLiteral(
"CREATE INDEX IF NOT EXISTS idx_attachments_message "
"ON attachments (message_id)"));
}
}
@@ -233,6 +259,7 @@ void ChatStore::removeSession(ChatSession* session) {
query.bindValue(":id", session->id());
query.exec();
}
QDir(ChatSession::sandboxDirFor(session->id())).removeRecursively();
m_sessions.removeOne(session);
session->deleteLater();
notify(before);
@@ -330,6 +357,11 @@ bool ChatStore::saveSession(ChatSession* session) {
"timestamp) VALUES (:gid, :type, :text, :name, "
":tool_call_id, :arguments, :result, :status, :elapsed_ms, "
":timestamp)");
QSqlQuery attachmentInsert(handle);
ok = ok && attachmentInsert.prepare(
"INSERT INTO attachments (message_id, name, path, kind, "
"mime, size) VALUES (:mid, :name, :path, :kind, :mime, "
":size)");
const auto* model = session->messagesModel();
for (int row = model->rowCount() - 1; ok && row >= 0; --row) {
const auto* message = model->at(row);
@@ -390,6 +422,26 @@ bool ChatStore::saveSession(ChatSession* session) {
}
}
}
if (ok)
for (const auto* attachment : message->attachments()) {
attachmentInsert.bindValue(":mid", messageId);
attachmentInsert.bindValue(
":name", sqlText(attachment->name()));
attachmentInsert.bindValue(
":path", sqlText(attachment->path()));
attachmentInsert.bindValue(
":kind", static_cast<int>(attachment->kind()));
attachmentInsert.bindValue(
":mime", sqlText(attachment->mimeType()));
attachmentInsert.bindValue(":size", attachment->size());
if (!attachmentInsert.exec()) {
ok = false;
qWarning() << "ChatStore: saveSession" << id
<< "attachment insert failed:"
<< attachmentInsert.lastError().text();
break;
}
}
}
}
if (!ok || !handle.commit()) {
@@ -497,6 +549,34 @@ void ChatStore::loadMessagesInto(ChatSession* session) {
<< messageId << ":"
<< generationQuery.lastError().text();
}
QSqlQuery attachmentQuery(db);
attachmentQuery.prepare(
"SELECT name, path, kind, mime, size FROM "
"attachments WHERE message_id = :mid "
"ORDER BY rowid");
attachmentQuery.bindValue(":mid", messageId);
if (attachmentQuery.exec()) {
while (attachmentQuery.next()) {
AttachmentRow attachment;
attachment.name =
attachmentQuery.value(0).toString();
attachment.path =
attachmentQuery.value(1).toString();
attachment.kind =
attachmentQuery.value(2).toInt();
attachment.mime =
attachmentQuery.value(3).toString();
attachment.size =
attachmentQuery.value(4).toLongLong();
message.attachments.append(attachment);
}
} else {
qWarning()
<< "ChatStore: failed to load attachments "
"for message"
<< messageId << ":"
<< attachmentQuery.lastError().text();
}
rows.append(message);
}
}
@@ -523,6 +603,22 @@ 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 :
row.attachments) {
attachments.append(new Attachment(
attachmentRow.name,
attachmentRow.path,
attachmentRow.kind == 0
? Attachment::Kind::Image
: Attachment::Kind::File,
attachmentRow.mime,
attachmentRow.size,
message));
}
message->setAttachments(attachments);
}
int activeIndex = 0;
for (int i = 0; i < row.generations.size(); ++i) {
const GenerationRow& generationRow =