#include #include #include #include #include #include #include #include #include #include "llm/LLMClient.hpp" namespace pmc::gui::example { // 带缓冲区的Markdown转换器,累积整个响应并处理Markdown class BufferedMarkdownConverter { private: std::string buffer; // 累积原始Markdown文本 std::string last_processed; // 上次处理的Pango标记 public: BufferedMarkdownConverter() {} // 重置状态 void reset() { buffer.clear(); last_processed.clear(); } // 添加新的Markdown块到缓冲区 void append_chunk(const std::string& chunk) { buffer += chunk; } // 获取当前缓冲区的内容 std::string get_buffer() const { return buffer; } // 处理整个缓冲区并返回Pango标记 std::string process_buffer() { std::string result = buffer; // 1. 首先转义HTML特殊字符 result = std::regex_replace(result, std::regex("&"), "&"); result = std::regex_replace(result, std::regex("<"), "<"); result = std::regex_replace(result, std::regex(">"), ">"); // 2. 转换粗斜体:***text*** -> text // 先处理粗斜体,确保正确嵌套 std::regex bold_italic_regex("\\*\\*\\*(.*?)\\*\\*\\*"); result = std::regex_replace(result, bold_italic_regex, "$1"); // 3. 转换粗体:**text** -> text(但排除已经是粗斜体的部分) std::regex bold_regex("\\*\\*(?!\\*)(.*?)\\*\\*"); result = std::regex_replace(result, bold_regex, "$1"); // 4. 转换斜体:*text* -> text(但排除已经是粗体或粗斜体的部分) std::regex italic_regex("\\*(?!\\*)(.*?)\\*"); result = std::regex_replace(result, italic_regex, "$1"); // 5. 转换内联代码:`code` -> code (使用标签而不是) std::regex code_regex("`(.*?)`"); result = std::regex_replace(result, code_regex, "$1"); // 6. 转换标题:# 标题 -> 标题 std::regex h1_regex("^#\\s+(.*?)$", std::regex::multiline); result = std::regex_replace(result, h1_regex, "$1\n"); std::regex h2_regex("^##\\s+(.*?)$", std::regex::multiline); result = std::regex_replace(result, h2_regex, "$1\n"); std::regex h3_regex("^###\\s+(.*?)$", std::regex::multiline); result = std::regex_replace(result, h3_regex, "$1\n"); // 7. 转换无序列表:* item -> • item std::regex ul_regex("^\\*\\s+(.*?)$", std::regex::multiline); result = std::regex_replace(result, ul_regex, "• $1\n"); // 8. 转换有序列表:1. item -> 1. item std::regex ol_regex("^\\d+\\.\\s+(.*?)$", std::regex::multiline); // 保持原样,只是确保有换行 result = std::regex_replace(result, ol_regex, "$&\n"); // 9. 转换引用:> text -> text std::regex blockquote_regex("^>\\s+(.*?)$", std::regex::multiline); result = std::regex_replace(result, blockquote_regex, "$1\n"); // 10. 转换链接:[text](url) -> text (Pango不支持标签,所以只显示文本) std::regex link_regex("\\[(.*?)\\]\\((.*?)\\)"); result = std::regex_replace(result, link_regex, "$1"); // 11. 转换图片:![alt](url) -> [图片: alt] (Pango不支持图片,所以显示替代文本) std::regex image_regex("!\\[(.*?)\\]\\((.*?)\\)"); result = std::regex_replace(result, image_regex, "[图片: $1]"); // 重要修复:Pango标记语言使用 \n 表示换行,而不是
// 不要转换换行符,保持原样 last_processed = result; return result; } // 获取上次处理的结果 std::string get_last_processed() const { return last_processed; } }; /** * @brief 简单的聊天窗口类 */ class SimpleChatWindow { private: GtkTextMark* current_response_start_mark; // 标记当前响应的开始位置 public: SimpleChatWindow() : current_response_start_mark(nullptr) { // 创建主窗口 window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL)); gtk_window_set_title(window, "PMC Chat"); gtk_window_set_default_size(window, 800, 600); g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), nullptr); // 创建主垂直布局 GtkWidget* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); gtk_container_add(GTK_CONTAINER(window), vbox); // 创建滚动窗口和文本视图(用于显示聊天记录) GtkWidget* scrolled_window = gtk_scrolled_window_new(nullptr, nullptr); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_box_pack_start(GTK_BOX(vbox), scrolled_window, TRUE, TRUE, 0); text_view = GTK_TEXT_VIEW(gtk_text_view_new()); gtk_text_view_set_editable(text_view, FALSE); gtk_text_view_set_cursor_visible(text_view, FALSE); gtk_text_view_set_wrap_mode(text_view, GTK_WRAP_WORD_CHAR); gtk_container_add(GTK_CONTAINER(scrolled_window), GTK_WIDGET(text_view)); // 设置文本视图的边距 gtk_text_view_set_left_margin(text_view, 10); gtk_text_view_set_right_margin(text_view, 10); gtk_text_view_set_top_margin(text_view, 10); gtk_text_view_set_bottom_margin(text_view, 10); // 创建输入区域的水平布局 GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); // 创建输入框 entry = GTK_ENTRY(gtk_entry_new()); gtk_entry_set_placeholder_text(entry, "输入消息..."); gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(entry), TRUE, TRUE, 0); // 创建发送按钮 send_button = GTK_BUTTON(gtk_button_new_with_label("发送")); gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(send_button), FALSE, FALSE, 0); // 连接信号 g_signal_connect(send_button, "clicked", G_CALLBACK(on_send_clicked), this); g_signal_connect(entry, "activate", G_CALLBACK(on_entry_activate), this); // 初始化LLM客户端 std::string api_key; const char* env_key = std::getenv("DEEPSEEK_API_KEY"); if (env_key && env_key[0] != '\0') { api_key = env_key; } else { // 尝试从文件读取 std::string key_file = std::string(std::getenv("HOME")) + "/.cert/deepseek.key"; std::ifstream file(key_file); if (file.is_open()) { std::getline(file, api_key); file.close(); } else { api_key = ""; // 使用空密钥,会在使用时出错 } } llm_client = std::make_unique(api_key); markdown_converter = std::make_unique(); } // 使用带缓冲区的Markdown转换器进行转换 std::string markdown_to_pango(const std::string& markdown) { // 将新块添加到缓冲区 markdown_converter->append_chunk(markdown); // 处理整个缓冲区 std::string result = markdown_converter->process_buffer(); return result; } void show() { gtk_widget_show_all(GTK_WIDGET(window)); } void append_message(const std::string& message) { GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view); GtkTextIter end; gtk_text_buffer_get_end_iter(buffer, &end); // 用户消息不需要Markdown转换,直接显示 gtk_text_buffer_insert(buffer, &end, message.c_str(), -1); gtk_text_buffer_insert(buffer, &end, "\n", -1); // 滚动到底部 GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW(gtk_widget_get_parent(GTK_WIDGET(text_view)))); gtk_adjustment_set_value(adj, gtk_adjustment_get_upper(adj)); } void append_stream_chunk(const std::string& chunk) { GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view); // 如果这是响应的第一个块,创建一个标记来跟踪开始位置 if (current_response_start_mark == nullptr) { GtkTextIter end; gtk_text_buffer_get_end_iter(buffer, &end); // 添加助手前缀 gtk_text_buffer_insert(buffer, &end, "Assistant: ", -1); // 在助手前缀后创建标记 gtk_text_buffer_get_end_iter(buffer, &end); current_response_start_mark = gtk_text_buffer_create_mark(buffer, "current_response_start", &end, TRUE); } // 获取当前响应的开始位置 GtkTextIter start_iter; gtk_text_buffer_get_iter_at_mark(buffer, &start_iter, current_response_start_mark); // 获取缓冲区的结束位置 GtkTextIter end_iter; gtk_text_buffer_get_end_iter(buffer, &end_iter); // 删除当前响应区域(从标记位置到缓冲区末尾) gtk_text_buffer_delete(buffer, &start_iter, &end_iter); // 在处理后的位置插入新的内容 gtk_text_buffer_get_iter_at_mark(buffer, &start_iter, current_response_start_mark); // 关键修复:插入整个累积的缓冲区内容,而不仅仅是当前的chunk // 首先,将新chunk添加到缓冲区 markdown_converter->append_chunk(chunk); // 然后处理整个缓冲区 std::string processed_buffer = markdown_converter->process_buffer(); // 插入处理后的整个缓冲区内容 if (!processed_buffer.empty()) { gtk_text_buffer_insert_markup(buffer, &start_iter, processed_buffer.c_str(), -1); } // 滚动到底部 GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment( GTK_SCROLLED_WINDOW(gtk_widget_get_parent(GTK_WIDGET(text_view)))); gtk_adjustment_set_value(adj, gtk_adjustment_get_upper(adj)); // 累积当前响应 current_response += chunk; } void finish_response() { // 将完整的响应添加到聊天历史 if (!current_response.empty()) { chat_history.push_back({"assistant", current_response}); current_response.clear(); } // 重置Markdown转换器 markdown_converter->reset(); // 清除当前响应标记 if (current_response_start_mark != nullptr) { GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view); gtk_text_buffer_delete_mark(buffer, current_response_start_mark); current_response_start_mark = nullptr; } // 添加换行符 GtkTextBuffer* buffer = gtk_text_view_get_buffer(text_view); GtkTextIter end; gtk_text_buffer_get_end_iter(buffer, &end); gtk_text_buffer_insert(buffer, &end, "\n", -1); } std::string get_input_text() { const char* text = gtk_entry_get_text(entry); return text ? std::string(text) : ""; } void clear_input() { gtk_entry_set_text(entry, ""); } void send_to_llm(const std::string& user_message) { // 将用户消息添加到聊天历史 chat_history.push_back({"user", user_message}); // 创建流式回调函数 auto stream_callback = [this](const std::string& chunk) { // 关键修复:简化完成标记检查 // 只检查明确的完成标记,避免误判 if (chunk == "[[PMC_STREAM_COMPLETE]]" || chunk == "[流式传输完成]]") { std::cout << "DEBUG: Detected completion marker" << std::endl; // 流式传输完成,在主线程中调用finish_response g_idle_add([](gpointer data) -> gboolean { auto* self = static_cast(data); self->finish_response(); return FALSE; // 只执行一次 }, this); return; // 重要:直接返回,不显示这个字符串 } // 使用g_idle_add在主线程中更新UI g_idle_add([](gpointer data) -> gboolean { auto* chunk_data = static_cast*>(data); chunk_data->first->append_stream_chunk(chunk_data->second); delete chunk_data; return FALSE; // 只执行一次 }, new std::pair(this, chunk)); }; // 在新线程中发送流式请求(避免阻塞主线程) std::thread([this, stream_callback]() { try { llm_client->stream_request(chat_history, stream_callback); } catch (const std::exception& e) { // 错误处理:在主线程中显示错误消息 g_idle_add([](gpointer data) -> gboolean { auto* error_data = static_cast*>(data); error_data->first->append_message("错误: " + error_data->second); delete error_data; return FALSE; // 只执行一次 }, new std::pair(this, e.what())); } }).detach(); // 分离线程,让它独立运行 } private: static void on_send_clicked(GtkButton* button, gpointer user_data) { auto* self = static_cast(user_data); std::string text = self->get_input_text(); if (!text.empty()) { // 用户消息不需要Markdown转换,直接显示 self->append_message("You: " + text); self->clear_input(); self->send_to_llm(text); } } static void on_entry_activate(GtkEntry* entry, gpointer user_data) { auto* self = static_cast(user_data); std::string text = self->get_input_text(); if (!text.empty()) { // 用户消息不需要Markdown转换,直接显示 self->append_message("You: " + text); self->clear_input(); self->send_to_llm(text); } } GtkWindow* window; GtkTextView* text_view; GtkEntry* entry; GtkButton* send_button; std::unique_ptr llm_client; std::unique_ptr markdown_converter; std::vector> chat_history; std::string current_response; }; /** * @brief 运行简单的聊天窗口示例 */ void run_simple_chat_example() { // 标准GTK的gtk_init返回void,不是bool gtk_init(nullptr, nullptr); SimpleChatWindow window; window.show(); gtk_main(); } } // namespace pmc::gui::example int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "用法: " << argv[0] << " " << std::endl; std::cerr << "命令: chat - 运行聊天窗口" << std::endl; return 1; } std::string command = argv[1]; if (command == "chat") { pmc::gui::example::run_simple_chat_example(); } else { std::cerr << "未知命令: " << command << std::endl; return 1; } return 0; }