initial commit for llm chat tab in sidebar
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
#include "chatstore.hpp"
|
||||
|
||||
#include "chat.hpp"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFileInfoList>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
ChatStore::ChatStore(QObject* parent) : QObject(parent) {
|
||||
load();
|
||||
}
|
||||
|
||||
QString ChatStore::dir() {
|
||||
const QString base =
|
||||
QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
|
||||
QStringLiteral("/zshell");
|
||||
return base + QStringLiteral("/chats");
|
||||
}
|
||||
|
||||
QString ChatStore::pathFor(const QString& id) {
|
||||
return dir() + QStringLiteral("/") + id + QStringLiteral(".json");
|
||||
}
|
||||
|
||||
bool ChatStore::writeFile(const QString& path, const QJsonObject& doc) {
|
||||
QDir().mkpath(QFileInfo(path).absolutePath());
|
||||
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
qWarning() << "ChatStore: failed to open" << path << "for writing:"
|
||||
<< file.errorString();
|
||||
return false;
|
||||
}
|
||||
if (file.write(QJsonDocument(doc).toJson(QJsonDocument::Indented)) < 0 ||
|
||||
!file.commit()) {
|
||||
qWarning() << "ChatStore: failed to write" << path << ":"
|
||||
<< file.errorString();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int ChatStore::count() const {
|
||||
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;
|
||||
}
|
||||
|
||||
ChatSession* ChatStore::at(int index) const {
|
||||
if (index < 0 || index >= m_sessions.size())
|
||||
return nullptr;
|
||||
return m_sessions.at(index);
|
||||
}
|
||||
|
||||
ChatSession* ChatStore::insert(int index) {
|
||||
const qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
auto* session = new ChatSession(QString::number(now), this);
|
||||
session->setPath(pathFor(session->id()));
|
||||
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;
|
||||
}
|
||||
|
||||
void ChatStore::remove(int index) {
|
||||
removeSession(at(index));
|
||||
}
|
||||
|
||||
void ChatStore::remove(ChatSession* chat) {
|
||||
removeSession(chat);
|
||||
}
|
||||
|
||||
void ChatStore::removeSession(ChatSession* session) {
|
||||
if (!session || !m_sessions.contains(session))
|
||||
return;
|
||||
if (isStreaming(session)) {
|
||||
qWarning() << "ChatStore: cannot remove a chat that is streaming";
|
||||
return;
|
||||
}
|
||||
const QList<ChatSession*> before = m_sessions;
|
||||
QFile::remove(session->path());
|
||||
if (auto* chat = qobject_cast<Chat*>(parent()))
|
||||
if (chat->m_active == session)
|
||||
chat->m_active = nullptr;
|
||||
m_sessions.removeOne(session);
|
||||
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)
|
||||
return;
|
||||
m_sessions.move(from, to);
|
||||
Q_EMIT valuesChanged();
|
||||
}
|
||||
|
||||
void ChatStore::clear() {
|
||||
const QList<ChatSession*> before = m_sessions;
|
||||
QList<ChatSession*> keep;
|
||||
for (ChatSession* session : std::as_const(m_sessions)) {
|
||||
if (isStreaming(session)) {
|
||||
keep.append(session);
|
||||
continue;
|
||||
}
|
||||
QFile::remove(session->path());
|
||||
session->deleteLater();
|
||||
}
|
||||
m_sessions = keep;
|
||||
notify(before);
|
||||
}
|
||||
|
||||
ChatSession* ChatStore::sessionById(const QString& id) {
|
||||
for (auto* session : m_sessions)
|
||||
if (session->id() == id)
|
||||
return session;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ChatStore::persist(ChatSession* session) {
|
||||
if (!session || !m_sessions.contains(session))
|
||||
return;
|
||||
session->setUpdatedAt(QDateTime::currentMSecsSinceEpoch());
|
||||
if (!writeFile(session->path(), session->document()))
|
||||
return;
|
||||
sortAndNotify();
|
||||
}
|
||||
|
||||
bool ChatStore::isStreaming(ChatSession* session) const {
|
||||
const auto* chat = qobject_cast<const Chat*>(parent());
|
||||
return chat && chat->streamingSession() == session;
|
||||
}
|
||||
|
||||
void ChatStore::sortAndNotify() {
|
||||
const QList<ChatSession*> before = m_sessions;
|
||||
std::stable_sort(
|
||||
m_sessions.begin(),
|
||||
m_sessions.end(),
|
||||
[](const ChatSession* a, const ChatSession* b) {
|
||||
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();
|
||||
}
|
||||
|
||||
void ChatStore::load() {
|
||||
const QFileInfoList files = QDir(dir()).entryInfoList(
|
||||
QStringList{QStringLiteral("*.json")},
|
||||
QDir::Files,
|
||||
QDir::Time);
|
||||
for (const QFileInfo& info : files) {
|
||||
QFile file(info.absoluteFilePath());
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
continue;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
file.close();
|
||||
if (!doc.isObject())
|
||||
continue;
|
||||
const QJsonObject obj = doc.object();
|
||||
auto* session =
|
||||
new ChatSession(obj["id"].toString(info.completeBaseName()), this);
|
||||
session->setPath(info.absoluteFilePath());
|
||||
session->setMeta(
|
||||
obj["title"].toString(),
|
||||
obj["createdAt"].toVariant().toLongLong(),
|
||||
obj["updatedAt"].toVariant().toLongLong(),
|
||||
int(obj["messages"].toArray().size()));
|
||||
m_sessions.append(session);
|
||||
}
|
||||
sortAndNotify();
|
||||
migrateLegacy();
|
||||
}
|
||||
|
||||
void ChatStore::migrateLegacy() {
|
||||
const QString path = dir() + QStringLiteral("/chat.json");
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
file.close();
|
||||
const QJsonArray arr = doc.isArray() ? doc.array() : QJsonArray();
|
||||
|
||||
QString title;
|
||||
qint64 first = 0;
|
||||
qint64 last = 0;
|
||||
QJsonArray messages;
|
||||
for (const QJsonValue& value : arr) {
|
||||
const QJsonObject msg = value.toObject();
|
||||
const QString content = msg["content"].toString();
|
||||
if (content.isEmpty())
|
||||
continue;
|
||||
if (first == 0)
|
||||
first = msg["timestamp"].toVariant().toLongLong();
|
||||
last = msg["timestamp"].toVariant().toLongLong();
|
||||
if (title.isEmpty() && msg["role"].toString() == "user")
|
||||
title = Chat::titleFrom(content);
|
||||
messages.append(msg);
|
||||
}
|
||||
if (messages.isEmpty())
|
||||
return;
|
||||
|
||||
const QString id = QString::number(QDateTime::currentMSecsSinceEpoch());
|
||||
auto* session = new ChatSession(id, this);
|
||||
session->setPath(pathFor(id));
|
||||
|
||||
QJsonObject out;
|
||||
out[QStringLiteral("id")] = id;
|
||||
out[QStringLiteral("title")] = title;
|
||||
out[QStringLiteral("createdAt")] = first;
|
||||
out[QStringLiteral("updatedAt")] = last;
|
||||
out[QStringLiteral("messages")] = messages;
|
||||
if (!writeFile(session->path(), out))
|
||||
return;
|
||||
QFile::remove(path);
|
||||
|
||||
session->setMeta(title, first, last, int(messages.size()));
|
||||
m_sessions.append(session);
|
||||
sortAndNotify();
|
||||
}
|
||||
|
||||
} // namespace ZShell
|
||||
Reference in New Issue
Block a user