#ifndef CHAT_BLOCK_MODEL_H #define CHAT_BLOCK_MODEL_H #include #include #include // A single block in the chat output struct ChatBlock { enum Type { User, // [user] markdown Assistant, // [assistant] markdown Reasoning, // [reasoning] content Call, // [call:name] args Tool, // [tool:name] output Error, // [error] text Info // [info] text }; Type type; QString name; // tool/call name, empty for others QString responseId; // stable assistant response ID QString reaction; // emoji reaction text (empty if not reacted) QString content; // raw text content mutable QVariantList cachedSubBlocks; // cached parseSubBlocks result mutable QString cachedContent; // content when subBlocks were cached }; // A sub-block within a markdown block (prose, code fence, diff) struct ContentBlock { enum Type { Prose, // regular markdown text Code, // fenced code block Diff, // unified diff (detected by content pattern) Diagram // mermaid/plantuml rendered as image }; Type type; QString language; // for Code and Diagram blocks QString content; }; class ChatBlockModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString rawText READ rawText WRITE setRawText NOTIFY rawTextChanged) Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged) Q_PROPERTY(bool filtering READ filtering NOTIFY filterTextChanged) public: enum Roles { BlockTypeRole = Qt::UserRole + 1, NameRole, ContentRole, SubBlocksRole, // QVariantList of sub-block maps FormattedContentRole, // pre-formatted HTML for call/tool expanded view ResponseIdRole, ReactionRole, }; explicit ChatBlockModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; QString rawText() const { return m_rawText; } void setRawText(const QString &text); QString filterText() const { return m_filterText; } void setFilterText(const QString &text); bool filtering() const { return !m_filterText.isEmpty(); } Q_INVOKABLE QString highlightedHtml(const QString &code, const QString &language) const; Q_INVOKABLE QString prettyJson(const QString &text) const; Q_INVOKABLE void copyToClipboard(const QString &text) const; Q_INVOKABLE QString linkifyFilePaths(const QString &markdown) const; Q_INVOKABLE void openLink(const QString &link) const; Q_INVOKABLE bool executeInTerminal(const QString &code, const QString &language, const QString &workingDirectory = QString()) const; Q_INVOKABLE bool loadEarlier(); // returns true if more blocks were loaded Q_INVOKABLE void setReactions(const QVariantMap &reactions); Q_INVOKABLE QString diagramUrl(const QString &source, const QString &language) const; Q_INVOKABLE bool invalidateDiagram(const QString &source, const QString &language) const; signals: void rawTextChanged(); void filterTextChanged(); private: void reparse(); void updateVisibleWindow(); void applyFilter(); QList parseBlocks(const QString &text) const; QVariantList parseSubBlocks(const QString &content) const; QString m_rawText; QString m_filterText; QList m_allBlocks; // all parsed blocks QList m_blocks; // visible window (tail of m_allBlocks) QList m_filteredBlocks; // filtered subset when filterText is active int m_visibleStart = 0; // index into m_allBlocks where visible window starts mutable QHash m_highlightCache; }; #endif // CHAT_BLOCK_MODEL_H