#include "ctai_history_textedit.h" ctai_history_textedit::ctai_history_textedit(QWidget *parent) : QWidget(parent) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } ctai_history_textedit::~ctai_history_textedit() { } void ctai_history_textedit::init_layout(msg_type msg_type_mode) { // 主垂直布局 mainLayout = new QVBoxLayout(); // 1. 消息头垂直 headerLayout = new QVBoxLayout(); header_info_Layout = new QHBoxLayout(); m_msg_header = new QLineEdit(); m_msg_header->setObjectName("m_msg_header"); m_msg_header->setReadOnly(true); if (msg_type_mode == SYSTEM) { // SYSTEM消息头水平 m_msg_system_header_ico = new QPushButton(); m_msg_system_header_ico->setObjectName("m_msg_system_header_ico"); header_info_Layout->addWidget(m_msg_system_header_ico); header_info_Layout->addWidget(m_msg_header); header_info_Layout->setContentsMargins(0, 0, 0, 0); headerLayout->addLayout(header_info_Layout); // 2.tokens功能按钮 m_msg_tokens = new QPushButton(); m_history_to_send = new QPushButton(); m_msg_system_del = new QPushButton(); m_restart_to_send = new QPushButton(); m_msg_tokens->setObjectName("m_msg_tokens"); m_history_to_send->setObjectName("m_history_to_send"); m_msg_system_del->setObjectName("m_msg_system_del"); m_restart_to_send->setObjectName("m_restart_to_send"); header_info_Layout->addWidget(m_history_to_send); header_info_Layout->addWidget(m_msg_tokens); header_info_Layout->addWidget(m_restart_to_send); header_info_Layout->addWidget(m_msg_system_del); // 3.消息功能区 header_opts_Layout = new QHBoxLayout(); m_msg_copy = new QPushButton(); m_msg_save = new QPushButton(); m_msg_menu = new QPushButton(); m_msg_fold = new QPushButton(); m_msg_copy->setObjectName("m_msg_copy"); m_msg_save->setObjectName("m_msg_save"); m_msg_menu->setObjectName("m_msg_menu"); m_msg_fold->setObjectName("m_msg_fold"); m_msg_fold->setIcon(QIcon(":res/img/btn/btn_info_up.png")); m_msg_fold->setIconSize(QSize(25, 25)); header_opts_Layout->addItem(sparcer_item); header_opts_Layout->addWidget(m_msg_copy); header_opts_Layout->addWidget(m_msg_save); header_opts_Layout->addWidget(m_msg_menu); header_opts_Layout->addWidget(m_msg_fold); header_opts_Layout->addItem(sparcer_item); header_opts_Layout->setContentsMargins(0, 0, 0, 0); headerLayout->addLayout(header_opts_Layout); } else { // SYSTEM MSG ICO m_msg_user_header_ico = new QPushButton(); m_msg_user_header_ico->setObjectName("m_msg_system_header_ico"); header_info_Layout->addWidget(m_msg_user_header_ico); header_info_Layout->addWidget(m_msg_header); header_info_Layout->setContentsMargins(0, 0, 0, 0); headerLayout->addLayout(header_info_Layout); // USER DELETE BUTTON m_msg_user_del = new QPushButton(); m_msg_user_del->setObjectName("m_msg_user_del"); m_history_to_send = new QPushButton(); m_history_to_send->setObjectName("m_history_to_send"); m_restart_to_send = new QPushButton(); m_restart_to_send->setObjectName("m_restart_to_send"); header_info_Layout->addWidget(m_history_to_send); header_info_Layout->addWidget(m_restart_to_send); header_info_Layout->addWidget(m_msg_user_del); } // 4.分割线区域 hLine = new QFrame; hLine->setFrameShape(QFrame::HLine); // 关键属性 hLine->setFrameShadow(QFrame::Sunken); // 凹陷效果 hLine->setLineWidth(2); // 线宽 // 5. 历史信息QTextEdit historyLayout = new QVBoxLayout(); m_msg_history = new QTextEdit(); m_msg_history->setAcceptRichText(true); m_msg_history->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_msg_history->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); m_msg_history->setObjectName("m_msg_history"); m_msg_history->setReadOnly(true); historyLayout->addWidget(m_msg_history); historyLayout->addWidget(hLine); historyLayout->setContentsMargins(0, 0, 0, 0); // 组合布局 mainLayout->addLayout(headerLayout); mainLayout->addLayout(historyLayout); mainLayout->addItem(bottom_spacer); mainLayout->setStretch(0, 1); mainLayout->setSpacing(1); mainLayout->setContentsMargins(0, 0, 0, 0); setLayout(mainLayout); connect_signals(msg_type_mode); } void ctai_history_textedit::connect_signals(msg_type msg_type_mode) { if (msg_type_mode == SYSTEM) { connect(m_msg_system_del, SIGNAL(clicked()), this, SLOT(on_delete_clicked())); connect(m_msg_copy, SIGNAL(clicked()), this, SLOT(on_copy_clicked())); connect(m_msg_save, SIGNAL(clicked()), this, SLOT(on_save_clicked())); connect(m_msg_menu, SIGNAL(clicked()), this, SLOT(on_menu_clicked())); connect(m_msg_fold, SIGNAL(clicked()), this, SLOT(on_fold_clicked())); connect(m_msg_tokens, SIGNAL(clicked()), this, SLOT(on_tokens_clicked())); } else { connect(m_msg_user_del, SIGNAL(clicked()), this, SLOT(on_delete_clicked())); } connect(m_msg_history->document(), SIGNAL(contentsChanged()), this, SLOT(on_text_height())); } // tokens按钮实现功能的槽函数 void ctai_history_textedit::on_tokens_clicked() { if (!m_msg_tokens_menu) { m_msg_tokens_menu = new QMenu(); } if (!m_menu_prompt_tokens) { m_menu_prompt_tokens = new QAction(m_tokens_data.prompt_tokens); } if (!m_menu_completion_tokens) { m_menu_completion_tokens = new QAction(m_tokens_data.completion_tokens); } if (!m_menu_total_tokens) { m_menu_total_tokens = new QAction(m_tokens_data.total_tokens); } if (!m_menu_cache_hit_tokens) { m_menu_cache_hit_tokens = new QAction(m_tokens_data.cache_hit_tokens); } if (!m_menu_cache_miss_tokens) { m_menu_cache_miss_tokens = new QAction(m_tokens_data.cache_miss_tokens); } m_msg_tokens_menu->addAction(m_menu_prompt_tokens); m_msg_tokens_menu->addAction(m_menu_completion_tokens); m_msg_tokens_menu->addAction(m_menu_cache_hit_tokens); m_msg_tokens_menu->addAction(m_menu_cache_miss_tokens); m_msg_tokens_menu->addAction(m_menu_total_tokens); m_msg_tokens_menu->exec(QCursor::pos()); } // save按钮实现功能的槽函数 void ctai_history_textedit::on_save_clicked() { if (!m_msg_save_menu) { m_msg_save_menu = new QMenu(); } if (!m_msg_save_text) { m_msg_save_text = new QAction(tr("另存为文本")); connect(m_msg_save_text, SIGNAL(triggered(bool)), this, SLOT(on_save_text())); } if (!m_msg_save_html) { m_msg_save_html = new QAction(tr("另存为HTML")); connect(m_msg_save_html, SIGNAL(triggered(bool)), this, SLOT(on_save_html())); } if (!m_msg_save_markdown) { m_msg_save_markdown = new QAction(tr("另存为Markdown")); connect(m_msg_save_markdown, SIGNAL(triggered(bool)), this, SLOT(on_save_markdown())); } if (!m_msg_save_pdf) { m_msg_save_pdf = new QAction(tr("另存为PDF")); connect(m_msg_save_pdf, SIGNAL(triggered(bool)), this, SLOT(on_save_pdf())); } m_msg_save_menu->addAction(m_msg_save_text); m_msg_save_menu->addAction(m_msg_save_html); m_msg_save_menu->addAction(m_msg_save_markdown); m_msg_save_menu->addAction(m_msg_save_pdf); m_msg_save_menu->exec(QCursor::pos()); } // 实现折叠功能的槽函数 void ctai_history_textedit::on_fold_clicked() { m_is_folded = !m_is_folded; m_msg_history->setFixedHeight(m_is_folded ? 0 : m_original_height); m_msg_fold->setIcon(QIcon(m_is_folded ? ":res/img/btn/btn_info_down.png" : ":res/img/btn/btn_info_up.png")); updateGeometry(); emit row_height_changed(m_is_folded); } void ctai_history_textedit::on_text_height() { // 防抖动处理:如果上次更新在100ms内,则延迟处理 static QTimer debounceTimer; debounceTimer.setSingleShot(true); if (debounceTimer.isActive()) { return; } // 直接使用documentLayout获取高度,避免多次计算 int textHeight = m_msg_history->document()->documentLayout()->documentSize().height(); if (textHeight > 0) { // 计算实际需要的高度 (包含边距和视口边框) int newHeight = textHeight + m_msg_history->document()->documentMargin() * 2 + m_msg_history->frameWidth() * 2; // 添加高度阈值判断,避免微小变化触发更新 static const int HEIGHT_THRESHOLD = 5; // 5像素的阈值 if (abs(m_msg_history->height() - newHeight) > HEIGHT_THRESHOLD) { m_original_height = newHeight; bool wasBlocked = m_msg_history->signalsBlocked(); m_msg_history->blockSignals(true); m_msg_history->setFixedHeight(newHeight); updateGeometry(); m_msg_history->blockSignals(wasBlocked); emit row_height_changed(false); // 延迟发送布局更新请求 if (QWidget *parent = parentWidget()) { QTimer::singleShot(100, [parent]() { QEvent e(QEvent::LayoutRequest); QApplication::sendEvent(parent, &e); }); } } } // 设置下一次检查的延迟 debounceTimer.start(100); } void ctai_history_textedit::add_user_message(const model_data &message) { QString disp_data; disp_data = ctai_markdown::md_to_html(QSL(message.send_user_data)); m_msg_sned_id = QSL(message.send_user_id); m_msg_history->setHtml(disp_data); } void ctai_history_textedit::add_system_message(const model_data &message) { QString disp_data; m_msg_sned_id = QSL(message.postback_send_id); std::lock_guard lock(m_mutex); if (message.postback_stream_mode) { // 流式模式下追加内容 m_current_content += message.postback_model_data; m_msg_history->setHtml(ctai_markdown::md_to_html(m_current_content)); } else { // 非流式模式直接设置全部内容 disp_data = ctai_markdown::md_to_html(QSL(message.postback_model_data)); m_msg_history->setHtml(disp_data); m_current_content = QSL(message.postback_model_data); } } // 增加tokens信息 void ctai_history_textedit::update_tokens_message(const model_data &message) { if (message.msg_type_mode == SYSTEM) { m_tokens_data.prompt_tokens = "提示词消耗tokens:" + QSN(message.postback_prompt_tokens); m_tokens_data.completion_tokens = "生成信息消耗:tokens:" + QSN(message.postback_completion_tokens); m_tokens_data.total_tokens = "总消耗tokens:" + QSN(message.postback_total_tokens); m_tokens_data.cache_hit_tokens = "提示词缓存命中消耗tokens:" + QSN(message.postback_prompt_cache_hit_tokens); m_tokens_data.cache_miss_tokens = "提示词缓存未命中消耗tokens:" + QSN(message.postback_prompt_cache_miss_tokens); } } // 增加头信息 void ctai_history_textedit::add_header_message(const model_data &message) { QString disp_header; if (message.msg_type_mode == SYSTEM) { disp_header = "ID:" + QSL(message.postback_send_id) + " | FP:" + QSL(message.postback_system_fingerprint) + " | 时间:" + QSL(message.postback_time); } else { disp_header = "ID:" + QSL(message.send_user_id) + " | Time:" + QSL(message.send_user_time); } m_msg_header->setText(disp_header); } void ctai_history_textedit::on_delete_clicked() { // 发送删除请求信号 emit delete_requested(m_msg_sned_id); } void ctai_history_textedit::on_copy_clicked() { // 获取文本并复制到剪贴板 QString text = m_msg_history->toPlainText(); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(text); } void ctai_history_textedit::on_save_text() { QString fileName = QFileDialog::getSaveFileName(this, tr("保存消息"), m_msg_sned_id + ".txt", tr("文本文件 (*.txt)")); if (fileName.isEmpty()) return; QFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("保存失败"), tr("无法保存文件 %1:\n%2").arg(fileName).arg(file.errorString())); return; } QTextStream out(&file); out << m_msg_history->toPlainText() << "\n\n"; } void ctai_history_textedit::on_save_html() { } void ctai_history_textedit::on_save_markdown() { } void ctai_history_textedit::on_save_pdf() { } void ctai_history_textedit::on_menu_clicked() { QMenu menu(this); // 添加菜单项 QAction *actCopy = menu.addAction(tr("复制")); QAction *actSave = menu.addAction(tr("保存")); menu.addSeparator(); QAction *actDelete = menu.addAction(tr("删除")); // 显示菜单 QPoint pos = m_msg_menu->mapToGlobal(m_msg_menu->rect().bottomRight()); QAction *act = menu.exec(pos); // 处理菜单选择 if (act == actCopy) { on_copy_clicked(); } else if (actSave) { on_save_clicked(); } else if (act == actDelete) { on_delete_clicked(); } }