#include "chatmodel.h" #include "markdown.h" #include #include ChatModel::ChatModel(ApiClient *api, QObject *parent) : QAbstractListModel(parent) , m_api(api) , m_busy(false) , m_finalizedThisRun(false) { connect(m_api, &ApiClient::sessionOpened, this, &ChatModel::onSessionOpened); connect(m_api, &ApiClient::chatStarted, this, &ChatModel::onChatStarted); connect(m_api, &ApiClient::chatStartFailed, this, &ChatModel::onChatStartFailed); connect(m_api, &ApiClient::streamDelta, this, &ChatModel::onStreamDelta); connect(m_api, &ApiClient::streamDone, this, &ChatModel::onStreamDone); connect(m_api, &ApiClient::streamEnded, this, &ChatModel::onStreamEnded); connect(m_api, &ApiClient::streamError, this, &ChatModel::onStreamError); } int ChatModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; return m_msgs.size(); } QVariant ChatModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_msgs.size()) return QVariant(); const Msg &m = m_msgs.at(index.row()); switch (role) { case RoleRole: return m.role; case ContentRole: return m.content; case ContentRichRole: // HTML per il rich text QML (markdown formattato lato C++, testabile). return Markdown::toRichText(m.content); case StreamingRole: return m.streaming; case TimestampRole: return m.ts; default: return QVariant(); } } QHash ChatModel::roleNames() const { QHash roles; roles.insert(RoleRole, "role"); roles.insert(ContentRole, "content"); roles.insert(ContentRichRole, "contentRich"); roles.insert(StreamingRole, "streaming"); roles.insert(TimestampRole, "timestamp"); return roles; } // ------------------------------------------------------------ flusso chat -- void ChatModel::openSession(const QString &id) { m_sessionId = id; m_title.clear(); setFromMessages(QVariantList()); emit sessionInfoChanged(); m_api->openSession(id); } void ChatModel::newSession() { m_title.clear(); setFromMessages(QVariantList()); emit sessionInfoChanged(); m_api->newSession(); } void ChatModel::resync() { if (!m_sessionId.isEmpty()) m_api->openSession(m_sessionId); } void ChatModel::send(const QString &text) { const QString trimmed = text.trimmed(); if (trimmed.isEmpty()) return; // Rifiuti VISIBILI: prima erano silenziosi, e l'invio automatico poteva // "non fare nulla" senza che si capisse il perché. if (m_sessionId.isEmpty()) { qDebug() << "[chat] send rifiutato: nessuna sessione aperta"; emit notice(tr("No session open")); return; } if (m_busy) { qDebug() << "[chat] send rifiutato: un turno e' gia' in corso"; emit notice(tr("Wait for the current reply")); return; } qDebug() << "[chat] invio:" << trimmed.size() << "caratteri"; m_finalizedThisRun = false; appendMessage(QStringLiteral("user"), trimmed, false); beginAssistant(); setBusy(true); m_api->startChat(m_sessionId, trimmed); } void ChatModel::cancel() { m_api->cancelRun(); } QString ChatModel::lastAssistantText() const { for (int i = m_msgs.size() - 1; i >= 0; --i) { const Msg &m = m_msgs.at(i); if (m.role == QLatin1String("assistant") && !m.content.trimmed().isEmpty()) return m.content; } return QString(); } void ChatModel::onSessionOpened() { const QVariantMap s = m_api->session(); const QString sid = s.value(QStringLiteral("session_id")).toString(); if (!sid.isEmpty()) m_sessionId = sid; m_title = s.value(QStringLiteral("title")).toString(); setFromMessages(s.value(QStringLiteral("messages")).toList()); emit sessionInfoChanged(); // Ripresa: la sessione aveva un run attivo e lo stream e' stato riagganciato. if (m_api->streaming()) { beginAssistant(); setBusy(true); } } void ChatModel::onChatStarted(const QString &streamId) { Q_UNUSED(streamId) if (m_msgs.isEmpty() || m_msgs.last().role != QLatin1String("assistant")) beginAssistant(); setBusy(true); } void ChatModel::onChatStartFailed(const QString &error) { // Rimuove il placeholder assistente rimasto vuoto. if (m_msgs.size() > 0 && m_msgs.last().role == QLatin1String("assistant") && m_msgs.last().content.isEmpty()) { beginRemoveRows(QModelIndex(), m_msgs.size() - 1, m_msgs.size() - 1); m_msgs.removeLast(); endRemoveRows(); emit rowCountChanged(); } setBusy(false); emit notice(error); } void ChatModel::onStreamDelta(const QString &text) { if (m_msgs.isEmpty() || m_msgs.last().role != QLatin1String("assistant")) beginAssistant(); Msg &last = m_msgs.last(); if (!last.streaming) { last.streaming = true; } last.content += text; const QModelIndex idx = index(m_msgs.size() - 1, 0); emit dataChanged(idx, idx, QVector() << ContentRole << ContentRichRole << StreamingRole); emit streamTick(); } void ChatModel::onStreamDone(const QVariantMap &session) { m_finalizedThisRun = true; m_title = session.value(QStringLiteral("title")).toString(); setFromMessages(session.value(QStringLiteral("messages")).toList()); emit sessionInfoChanged(); setBusy(false); emit assistantFinished(); } void ChatModel::onStreamEnded() { if (!m_msgs.isEmpty() && m_msgs.last().streaming) { Msg &last = m_msgs.last(); last.streaming = false; // Placeholder rimasto vuoto (run interrotto prima del primo token). if (last.role == QLatin1String("assistant") && last.content.isEmpty()) { beginRemoveRows(QModelIndex(), m_msgs.size() - 1, m_msgs.size() - 1); m_msgs.removeLast(); endRemoveRows(); emit rowCountChanged(); } else { const QModelIndex idx = index(m_msgs.size() - 1, 0); emit dataChanged(idx, idx, QVector() << StreamingRole); } } setBusy(false); // Aggiorna la lista sessioni (titolo/ordinamento possono essere cambiati). m_api->refreshSessions(); } void ChatModel::onStreamError(const QString &error) { emit notice(error); // Lo stream puo' cadere per sospensione dell'app: risincronizza dallo stato // del server tra un attimo (se nel frattempo non e' gia' ripartito qualcosa). QTimer::singleShot(1200, this, [this]() { if (!m_api->streaming() && !m_sessionId.isEmpty()) m_api->openSession(m_sessionId); }); } // ------------------------------------------------------------- modello ---- void ChatModel::setFromMessages(const QVariantList &messages) { beginResetModel(); m_msgs.clear(); for (int i = 0; i < messages.size(); ++i) { const QVariantMap m = messages.at(i).toMap(); const QString role = m.value(QStringLiteral("role")).toString(); if (role != QLatin1String("user") && role != QLatin1String("assistant")) continue; const QString content = m.value(QStringLiteral("content")).toString(); // Messaggi assistente vuoti (solo chiamate a strumenti): non mostrati. if (role == QLatin1String("assistant") && content.trimmed().isEmpty()) continue; Msg msg; msg.role = role; msg.content = content; msg.streaming = false; msg.ts = m.value(QStringLiteral("timestamp")).toDouble(); m_msgs.append(msg); } endResetModel(); emit rowCountChanged(); } void ChatModel::appendMessage(const QString &role, const QString &content, bool streaming) { beginInsertRows(QModelIndex(), m_msgs.size(), m_msgs.size()); Msg msg; msg.role = role; msg.content = content; msg.streaming = streaming; msg.ts = 0; m_msgs.append(msg); endInsertRows(); emit rowCountChanged(); } void ChatModel::beginAssistant() { appendMessage(QStringLiteral("assistant"), QString(), true); } void ChatModel::setBusy(bool on) { if (on == m_busy) return; m_busy = on; emit busyChanged(); }