Add metadata validation script and module documentation
- Introduced a new Python script `check_agents_metadata.py` for validating agent metadata, including YAML parsing, rating ranges, and cross-references. - Added usage instructions and exit codes for the script. - Created a new markdown file `模块目录和功能说明.md` to outline the directory structure and functionality of the modules. - Added a text file `说明此文件不可AI修改.txt` to specify that certain files should not be modified by AI, including important information about the `dstalk` framework and its modules.
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
// ============================================================================
|
||||
// dstalk-gui — SDL3 聊天客户端
|
||||
// ============================================================================
|
||||
// 使用 SDL3 内置的 SDL_RenderDebugText() 渲染文本(8x8 像素),
|
||||
// 通过 SDL_SetRenderScale 2 倍缩放至有效的 16x16 像素。
|
||||
//
|
||||
// 该文件是独立的——不需要额外的源文件。
|
||||
// ============================================================================
|
||||
/*
|
||||
* @file main.cpp
|
||||
* @brief SDL3-based GUI frontend for dstalk (stub/minimal implementation).
|
||||
* dstalk 的 SDL3 图形界面前端(最小化实现)。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*
|
||||
* Uses SDL3's built-in SDL_RenderDebugText() for 8x8 pixel text, scaled 2x to
|
||||
* effective 16x16 pixels via SDL_SetRenderScale. Self-contained single-file GUI.
|
||||
* 使用 SDL3 内置的 SDL_RenderDebugText() 渲染 8x8 像素文本,通过 SDL_SetRenderScale
|
||||
* 缩放 2 倍达到 16x16 像素效果。自包含的单文件 GUI。
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -19,46 +22,48 @@
|
||||
|
||||
#include "dstalk/dstalk_host.h"
|
||||
|
||||
// ---- 服务 vtable 指针 ----
|
||||
// ---- 服务 vtable 指针 / Service vtable pointers ----
|
||||
// Global pointers to service vtables queried from the host on startup.
|
||||
// 在启动时从主机查询获取的服务 vtable 全局指针。
|
||||
static const dstalk_ai_service_t* g_ai_svc = nullptr;
|
||||
static const dstalk_session_service_t* g_session_svc = nullptr;
|
||||
|
||||
// ---- 常量 ----
|
||||
// ---- 常量 / Constants ----
|
||||
|
||||
static constexpr int WINDOW_W = 1024;
|
||||
static constexpr int WINDOW_H = 768;
|
||||
static constexpr float RENDER_SCALE = 2.0f;
|
||||
|
||||
// 逻辑坐标尺寸(物理像素 / RENDER_SCALE)
|
||||
// 逻辑坐标尺寸(物理像素 / RENDER_SCALE) / Logical coordinate dimensions (physical pixels / RENDER_SCALE)
|
||||
static constexpr int LOGICAL_W = WINDOW_W / 2; // 512
|
||||
static constexpr int LOGICAL_H = WINDOW_H / 2; // 384
|
||||
|
||||
static constexpr int CHAR_W = 8; // SDL_RenderDebugText 原生字符宽度(逻辑像素)
|
||||
static constexpr int CHAR_H = 8; // 原生字符高度(逻辑像素)
|
||||
static constexpr int TITLE_H = 16; // 标题栏高度(逻辑像素)
|
||||
static constexpr int PADDING = 4; // 内边距(逻辑像素)
|
||||
static constexpr int CHAR_W = 8; // SDL_RenderDebugText 原生字符宽度(逻辑像素) / native char width (logical pixels)
|
||||
static constexpr int CHAR_H = 8; // 原生字符高度(逻辑像素) / native char height (logical pixels)
|
||||
static constexpr int TITLE_H = 16; // 标题栏高度(逻辑像素) / title bar height (logical pixels)
|
||||
static constexpr int PADDING = 4; // 内边距(逻辑像素) / padding (logical pixels)
|
||||
|
||||
// 侧边栏
|
||||
static constexpr int SIDEBAR_W = 80; // 侧边栏宽度(逻辑像素,渲染为 160 物理像素)
|
||||
// 侧边栏 / Sidebar
|
||||
static constexpr int SIDEBAR_W = 80; // 侧边栏宽度(逻辑像素,渲染为 160 物理像素) / sidebar width (logical, renders as 160 physical px)
|
||||
|
||||
// 状态栏
|
||||
static constexpr int STATUS_H = 20; // 状态栏高度(逻辑像素,渲染为 40 物理像素)
|
||||
// 状态栏 / Status bar
|
||||
static constexpr int STATUS_H = 20; // 状态栏高度(逻辑像素,渲染为 40 物理像素) / status bar height (logical, renders as 40 physical px)
|
||||
|
||||
// 输入区域动态高度
|
||||
static constexpr int INPUT_H_MIN = 40; // 最小高度(逻辑像素)
|
||||
static constexpr int INPUT_H_MAX = 120; // 最大高度(逻辑像素)
|
||||
// 输入区域动态高度 / Input area dynamic height
|
||||
static constexpr int INPUT_H_MIN = 40; // 最小高度(逻辑像素) / min height (logical pixels)
|
||||
static constexpr int INPUT_H_MAX = 120; // 最大高度(逻辑像素) / max height (logical pixels)
|
||||
|
||||
// 消息区域(Y 起点不变,宽度和高度动态计算)
|
||||
// 消息区域(Y 起点不变,宽度和高度动态计算) / Message area (Y origin fixed, width and height calculated dynamically)
|
||||
static constexpr int MSG_Y = TITLE_H;
|
||||
|
||||
// 颜色(ARGB 格式,用于 SDL_SetRenderDrawColor)
|
||||
// 颜色(ARGB 格式,用于 SDL_SetRenderDrawColor) / Colors (ARGB format, for SDL_SetRenderDrawColor)
|
||||
static constexpr SDL_Color COL_BG = {0x1E, 0x1E, 0x2E, 0xFF};
|
||||
static constexpr SDL_Color COL_TITLE_BG = {0x2D, 0x2D, 0x44, 0xFF};
|
||||
static constexpr SDL_Color COL_INPUT_BG = {0x2A, 0x2A, 0x3E, 0xFF};
|
||||
static constexpr SDL_Color COL_USER = {0x00, 0xFF, 0xFF, 0xFF}; // 青色
|
||||
static constexpr SDL_Color COL_AI = {0x00, 0xFF, 0x80, 0xFF}; // 绿色
|
||||
static constexpr SDL_Color COL_SYS = {0xFF, 0xFF, 0x00, 0xFF}; // 黄色
|
||||
static constexpr SDL_Color COL_BTN = {0x50, 0x50, 0x80, 0xFF}; // 按钮
|
||||
static constexpr SDL_Color COL_USER = {0x00, 0xFF, 0xFF, 0xFF}; // 青色 / cyan
|
||||
static constexpr SDL_Color COL_AI = {0x00, 0xFF, 0x80, 0xFF}; // 绿色 / green
|
||||
static constexpr SDL_Color COL_SYS = {0xFF, 0xFF, 0x00, 0xFF}; // 黄色 / yellow
|
||||
static constexpr SDL_Color COL_BTN = {0x50, 0x50, 0x80, 0xFF}; // 按钮 / button
|
||||
static constexpr SDL_Color COL_WHITE = {0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static constexpr SDL_Color COL_CURSOR = {0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static constexpr SDL_Color COL_SEP = {0x50, 0x50, 0x70, 0xFF};
|
||||
@@ -68,8 +73,9 @@ static constexpr SDL_Color COL_SIDEBAR_BTN = {0x40, 0x40, 0x68, 0xFF};
|
||||
static constexpr SDL_Color COL_STATUSBAR_BG= {0x2D, 0x2D, 0x44, 0xFF};
|
||||
static constexpr SDL_Color COL_DIM = {0x80, 0x80, 0x80, 0xFF};
|
||||
|
||||
// ---- 数据结构 ----
|
||||
// ---- 数据结构 / Data structures ----
|
||||
|
||||
// 单条聊天消息 / Represents a single chat message with role and text content.
|
||||
struct ChatMessage {
|
||||
enum Role { USER, ASSISTANT, SYSTEM } role;
|
||||
std::string content;
|
||||
@@ -77,62 +83,66 @@ struct ChatMessage {
|
||||
ChatMessage(Role r, std::string c) : role(r), content(std::move(c)) {}
|
||||
};
|
||||
|
||||
// 保存所有可变 UI 状态 / Holds all mutable UI state: message list, input buffer, scroll, streaming flag, etc.
|
||||
struct GuiState {
|
||||
std::vector<ChatMessage> messages;
|
||||
std::string inputBuffer;
|
||||
int scrollOffset = 0; // 从底部滚动的逻辑像素
|
||||
int scrollOffset = 0; // 从底部滚动的逻辑像素 / logical pixels scrolled from bottom
|
||||
bool streaming = false;
|
||||
bool running = true;
|
||||
int cursorPos = 0; // 输入缓冲区中的光标位置
|
||||
int cursorPos = 0; // 输入缓冲区中的光标位置 / cursor position in input buffer
|
||||
bool cursorVisible = true;
|
||||
Uint64 lastCursorBlink = 0;
|
||||
float maxScroll = 0; // 可用的最大滚动距离(逻辑像素)
|
||||
float maxScroll = 0; // 可用的最大滚动距离(逻辑像素) / max available scroll distance (logical pixels)
|
||||
|
||||
// P0 新增字段
|
||||
std::vector<std::string> input_history; // 输入历史(最多 20 条)
|
||||
int history_index = -1; // 当前历史位置(-1 = 新输入)
|
||||
std::string saved_input; // 浏览历史时暂存当前输入
|
||||
bool sidebar_visible = true; // 侧边栏可见性
|
||||
std::string model_name = "deepseek-chat";// 当前模型名
|
||||
// P0 新增字段 / P0 new fields
|
||||
std::vector<std::string> input_history; // 输入历史(最多 20 条) / input history (max 20 entries)
|
||||
int history_index = -1; // 当前历史位置(-1 = 新输入) / current history position (-1 = new input)
|
||||
std::string saved_input; // 浏览历史时暂存当前输入 / saved current input while browsing history
|
||||
bool sidebar_visible = true; // 侧边栏可见性 / sidebar visibility
|
||||
std::string model_name = "deepseek-chat";// 当前模型名 / current model name
|
||||
};
|
||||
|
||||
// 持有上下文指针,用于将回调传递给流式 API
|
||||
// 将 GuiState 与 SDL 窗口/渲染器句柄及逐帧标志打包。
|
||||
// 作为 userdata 传递给流式回调,使其可以更新缓冲区并重新渲染。
|
||||
// Bundles GuiState with SDL window/renderer handles and per-frame flags.
|
||||
// Passed as userdata to the streaming callback so it can update the buffer and re-render.
|
||||
struct AppContext {
|
||||
GuiState state;
|
||||
SDL_Window* window = nullptr;
|
||||
SDL_Renderer* renderer = nullptr;
|
||||
bool sendPending = false; // 按下 Enter 后设置为 true
|
||||
std::string streamBuffer; // 存储当前流式消息
|
||||
bool sendPending = false; // 按下 Enter 后设置为 true / set to true after pressing Enter
|
||||
std::string streamBuffer; // 存储当前流式消息 / stores current streaming message
|
||||
};
|
||||
|
||||
// ---- 辅助函数 ----
|
||||
// ---- 辅助函数 / Helper functions ----
|
||||
|
||||
// 获取一个逻辑坐标的 SDL 矩形
|
||||
// 在逻辑坐标系中创建 SDL_FRect / Create an SDL_FRect in logical coordinates.
|
||||
static SDL_FRect mkRect(float x, float y, float w, float h) {
|
||||
SDL_FRect r;
|
||||
r.x = x; r.y = y; r.w = w; r.h = h;
|
||||
return r;
|
||||
}
|
||||
|
||||
// 使用给定的颜色设置绘制颜色
|
||||
// 使用 SDL_Color 设置渲染器的绘制颜色 / Set the renderer's draw color from an SDL_Color.
|
||||
static void setColor(SDL_Renderer* r, SDL_Color c) {
|
||||
SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a);
|
||||
}
|
||||
|
||||
// 使用颜色绘制填充矩形
|
||||
// 以纯色填充矩形(逻辑坐标) / Fill a rectangle with a solid color (logical coordinates).
|
||||
static void fillRect(SDL_Renderer* r, SDL_FRect rect, SDL_Color c) {
|
||||
setColor(r, c);
|
||||
SDL_RenderFillRect(r, &rect);
|
||||
}
|
||||
|
||||
// 在给定位置(逻辑坐标)绘制一个调试文本字符串,并设定颜色
|
||||
// 在指定逻辑位置以指定颜色绘制调试文本 / Draw a debug-text string at a given logical position with the specified color.
|
||||
static void drawText(SDL_Renderer* r, float x, float y,
|
||||
const char* text, SDL_Color color) {
|
||||
setColor(r, color);
|
||||
SDL_RenderDebugText(r, x, y, text);
|
||||
}
|
||||
|
||||
// 绘制一个可见的调试文本字符,避免为空字符串调用 SDL_RenderDebugText
|
||||
// 仅当字符串非空时绘制调试文本(避免 SDL_RenderDebugText 问题) / Draw debug text only if the string is non-empty (avoids SDL_RenderDebugText issues).
|
||||
static void drawTextSafe(SDL_Renderer* r, float x, float y,
|
||||
const char* text) {
|
||||
if (text && text[0] != '\0') {
|
||||
@@ -140,7 +150,7 @@ static void drawTextSafe(SDL_Renderer* r, float x, float y,
|
||||
}
|
||||
}
|
||||
|
||||
// 计算输入区域的动态高度(根据输入内容中的换行数)
|
||||
// 根据换行符数量计算输入区域的动态高度 / Compute the dynamic height of the input area based on the number of newlines.
|
||||
static int calcInputHeight(const std::string& input) {
|
||||
int lines = 1;
|
||||
for (char ch : input) {
|
||||
@@ -150,14 +160,13 @@ static int calcInputHeight(const std::string& input) {
|
||||
std::max(INPUT_H_MIN, lines * CHAR_H + PADDING * 2));
|
||||
}
|
||||
|
||||
// ---- 文本换行 ----
|
||||
// ---- 文本换行 / Text wrapping ----
|
||||
|
||||
// 将一段文本按字符数换行。保留嵌入的 '\n',并在单词边界处尽可能按字符数换行。
|
||||
// 返回逻辑文本行列表。
|
||||
// 按 maxChars 换行文本,保留嵌入的换行符 / Word-wrap text to fit within maxChars per line, respecting embedded newlines.
|
||||
static std::vector<std::string> wrapText(const std::string& text, int maxChars) {
|
||||
std::vector<std::string> lines;
|
||||
|
||||
// 首先按嵌入的换行符分割
|
||||
// 首先按嵌入的换行符分割 / First split by embedded newlines
|
||||
std::string remaining = text;
|
||||
while (!remaining.empty()) {
|
||||
std::string segment;
|
||||
@@ -170,13 +179,13 @@ static std::vector<std::string> wrapText(const std::string& text, int maxChars)
|
||||
remaining.clear();
|
||||
}
|
||||
|
||||
// 将片段按单词换行以适应 maxChars
|
||||
// 将片段按单词换行以适应 maxChars / Wrap segment by word to fit maxChars
|
||||
while (!segment.empty()) {
|
||||
if (static_cast<int>(segment.size()) <= maxChars) {
|
||||
lines.push_back(segment);
|
||||
break;
|
||||
}
|
||||
// 在 maxChars 位置寻找空格/单词边界
|
||||
// 在 maxChars 位置寻找空格/单词边界 / Find space/word boundary at maxChars position
|
||||
int splitAt = maxChars;
|
||||
for (int i = maxChars; i > 0; --i) {
|
||||
char ch = segment[i];
|
||||
@@ -187,7 +196,7 @@ static std::vector<std::string> wrapText(const std::string& text, int maxChars)
|
||||
break;
|
||||
}
|
||||
if ((ch & 0x80) != 0) {
|
||||
// UTF-8 多字节字符——不在中间分割
|
||||
// UTF-8 多字节字符——不在中间分割 / UTF-8 multi-byte char — don't split in the middle
|
||||
}
|
||||
}
|
||||
if (splitAt <= 0 || splitAt > maxChars) {
|
||||
@@ -195,7 +204,7 @@ static std::vector<std::string> wrapText(const std::string& text, int maxChars)
|
||||
}
|
||||
|
||||
lines.push_back(segment.substr(0, splitAt));
|
||||
// 去除下一行的前导空格
|
||||
// 去除下一行的前导空格 / Trim leading spaces for the next line
|
||||
size_t start = splitAt;
|
||||
while (start < segment.size() &&
|
||||
(segment[start] == ' ' || segment[start] == '\t')) {
|
||||
@@ -207,8 +216,7 @@ static std::vector<std::string> wrapText(const std::string& text, int maxChars)
|
||||
return lines;
|
||||
}
|
||||
|
||||
// 计算所有消息的总渲染高度(逻辑像素)。
|
||||
// 注意:这使用当前的侧边栏状态来决定宽度;调用者应在侧边栏宽度正确时调用。
|
||||
// 计算所有消息在换行后的总渲染高度(逻辑像素) / Calculate the total rendered height (in logical pixels) of all messages after wrapping.
|
||||
static int calcTotalMsgHeight(GuiState& state, int charsPerLine) {
|
||||
int totalH = 0;
|
||||
for (auto& msg : state.messages) {
|
||||
@@ -219,8 +227,10 @@ static int calcTotalMsgHeight(GuiState& state, int charsPerLine) {
|
||||
return totalH;
|
||||
}
|
||||
|
||||
// ---- 侧边栏渲染 ----
|
||||
// ---- 侧边栏渲染 / Sidebar rendering ----
|
||||
|
||||
// 渲染左侧边栏:背景、会话列表和"+ New Chat"按钮。
|
||||
// Render the left sidebar: background, session list, and "+ New Chat" button.
|
||||
static void renderSidebar(AppContext& ctx) {
|
||||
GuiState& gs = ctx.state;
|
||||
SDL_Renderer* r = ctx.renderer;
|
||||
@@ -228,32 +238,34 @@ static void renderSidebar(AppContext& ctx) {
|
||||
float sbY = static_cast<float>(TITLE_H);
|
||||
float sbH = static_cast<float>(LOGICAL_H) - TITLE_H - STATUS_H;
|
||||
|
||||
// 背景
|
||||
// 背景 / Background
|
||||
fillRect(r, mkRect(0, sbY, sbW, sbH), COL_SIDEBAR_BG);
|
||||
|
||||
// 右侧分隔线
|
||||
// 右侧分隔线 / Right separator line
|
||||
setColor(r, COL_SEP);
|
||||
SDL_RenderLine(r, sbW, sbY, sbW, sbY + sbH);
|
||||
|
||||
// "Chats" 标题
|
||||
// "Chats" 标题 / "Chats" title
|
||||
drawText(r, static_cast<float>(PADDING), sbY + PADDING, "Chats", COL_WHITE);
|
||||
|
||||
// 会话列表(当前只有 "default")
|
||||
// 会话列表(当前只有 "default") / Session list (currently only "default")
|
||||
float listY = sbY + TITLE_H;
|
||||
// "default" 条目(活动状态高亮)
|
||||
// "default" 条目(活动状态高亮) / "default" entry (active state highlighted)
|
||||
float itemH = static_cast<float>(CHAR_H + PADDING);
|
||||
fillRect(r, mkRect(PADDING, listY, sbW - PADDING * 2, itemH), COL_SIDEBAR_ACT);
|
||||
drawText(r, PADDING * 2.0f, listY + PADDING / 2.0f, "default", COL_AI);
|
||||
|
||||
// "+ New Chat" 按钮(侧边栏底部)
|
||||
// "+ New Chat" 按钮(侧边栏底部) / "+ New Chat" button (sidebar bottom)
|
||||
float btnY = sbY + sbH - CHAR_H - PADDING * 2;
|
||||
float btnH = static_cast<float>(CHAR_H + PADDING);
|
||||
fillRect(r, mkRect(PADDING, btnY, sbW - PADDING * 2, btnH), COL_SIDEBAR_BTN);
|
||||
drawText(r, PADDING * 2.0f, btnY + PADDING / 2.0f, "+ New Chat", COL_WHITE);
|
||||
}
|
||||
|
||||
// ---- 状态栏渲染 ----
|
||||
// ---- 状态栏渲染 / Status bar rendering ----
|
||||
|
||||
// 渲染底部状态栏:模型名、消息数和流式状态。
|
||||
// Render the bottom status bar: model name, message count, and streaming state.
|
||||
static void renderStatusBar(AppContext& ctx) {
|
||||
GuiState& gs = ctx.state;
|
||||
SDL_Renderer* r = ctx.renderer;
|
||||
@@ -261,20 +273,20 @@ static void renderStatusBar(AppContext& ctx) {
|
||||
float lh = static_cast<float>(LOGICAL_H);
|
||||
float barY = lh - STATUS_H;
|
||||
|
||||
// 背景
|
||||
// 背景 / Background
|
||||
fillRect(r, mkRect(0, barY, lw, static_cast<float>(STATUS_H)), COL_STATUSBAR_BG);
|
||||
|
||||
// 顶部分隔线
|
||||
// 顶部分隔线 / Top separator line
|
||||
setColor(r, COL_SEP);
|
||||
SDL_RenderLine(r, 0, barY, lw, barY);
|
||||
|
||||
// 统计消息数(排除系统消息)
|
||||
// 统计消息数(排除系统消息) / Count messages (excluding system messages)
|
||||
int msgCount = 0;
|
||||
for (auto& msg : gs.messages) {
|
||||
if (msg.role != ChatMessage::SYSTEM) msgCount++;
|
||||
}
|
||||
|
||||
// 状态文本:模型名 | 消息条数 | 流式状态
|
||||
// 状态文本:模型名 | 消息条数 | 流式状态 / Status text: model name | message count | streaming state
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "%s | %d messages | %s",
|
||||
gs.model_name.c_str(), msgCount,
|
||||
@@ -283,8 +295,10 @@ static void renderStatusBar(AppContext& ctx) {
|
||||
barY + (STATUS_H - CHAR_H) / 2.0f, buf, COL_WHITE);
|
||||
}
|
||||
|
||||
// ---- 主渲染 ----
|
||||
// ---- 主渲染 / Main rendering ----
|
||||
|
||||
// 渲染一帧:标题栏、侧边栏、消息区(滚动)、输入区、光标、发送按钮、状态栏。
|
||||
// Render one full frame: title bar, sidebar, message area (with scrolling), input area, cursor, send button, status bar.
|
||||
static void renderFrame(AppContext& ctx) {
|
||||
GuiState& gs = ctx.state;
|
||||
SDL_Renderer* r = ctx.renderer;
|
||||
@@ -301,33 +315,33 @@ static void renderFrame(AppContext& ctx) {
|
||||
int charsPerLine = std::max(20,
|
||||
static_cast<int>(msgAreaW - PADDING * 2) / CHAR_W);
|
||||
|
||||
// 1. 设置渲染缩放以获得 2 倍文本大小
|
||||
// 1. 设置渲染缩放以获得 2 倍文本大小 / Set render scale for 2x text size
|
||||
SDL_SetRenderScale(r, RENDER_SCALE, RENDER_SCALE);
|
||||
|
||||
// 2. 清除背景
|
||||
// 2. 清除背景 / Clear background
|
||||
setColor(r, COL_BG);
|
||||
SDL_RenderClear(r);
|
||||
|
||||
// 3. 标题栏(全宽)
|
||||
// 3. 标题栏(全宽)/ Title bar (full width)
|
||||
fillRect(r, mkRect(0, 0, lw, static_cast<float>(TITLE_H)), COL_TITLE_BG);
|
||||
drawText(r, static_cast<float>(PADDING), static_cast<float>(PADDING),
|
||||
"dstalk - AI Chat", COL_WHITE);
|
||||
// 右侧的状态指示器
|
||||
// 右侧的状态指示器 / Status indicator on the right
|
||||
const char* status = gs.streaming ? "[streaming...]" : "[ready]";
|
||||
float statusW = static_cast<float>(strlen(status)) * CHAR_W + PADDING;
|
||||
drawText(r, lw - statusW, static_cast<float>(PADDING), status, COL_WHITE);
|
||||
|
||||
// 4. 标题栏分隔线
|
||||
// 4. 标题栏分隔线 / Title bar separator line
|
||||
setColor(r, COL_SEP);
|
||||
SDL_RenderLine(r, 0, static_cast<float>(TITLE_H),
|
||||
lw, static_cast<float>(TITLE_H));
|
||||
|
||||
// 5. 侧边栏(可折叠)
|
||||
// 5. 侧边栏(可折叠)/ Sidebar (collapsible)
|
||||
if (gs.sidebar_visible) {
|
||||
renderSidebar(ctx);
|
||||
}
|
||||
|
||||
// 6. 消息区域(带滚动)
|
||||
// 6. 消息区域(带滚动)/ Message area (with scrolling)
|
||||
SDL_Rect msgClip;
|
||||
msgClip.x = static_cast<int>(msgAreaX * RENDER_SCALE);
|
||||
msgClip.y = static_cast<int>(msgAreaY * RENDER_SCALE);
|
||||
@@ -335,13 +349,13 @@ static void renderFrame(AppContext& ctx) {
|
||||
msgClip.h = static_cast<int>(msgAreaH * RENDER_SCALE);
|
||||
SDL_SetRenderClipRect(r, &msgClip);
|
||||
|
||||
// 计算总消息高度和滚动限制
|
||||
// 计算总消息高度和滚动限制 / Calculate total message height and scroll limits
|
||||
int totalMsgH = calcTotalMsgHeight(gs, charsPerLine);
|
||||
gs.maxScroll = std::max(0.0f, static_cast<float>(totalMsgH) - msgAreaH);
|
||||
if (gs.scrollOffset < 0) gs.scrollOffset = 0;
|
||||
if (gs.scrollOffset > gs.maxScroll) gs.scrollOffset = static_cast<int>(gs.maxScroll);
|
||||
|
||||
// 绘制消息:起始 Y 从消息区域顶部减去 scrollOffset
|
||||
// 绘制消息:起始 Y 从消息区域顶部减去 scrollOffset / Draw messages: start Y from message area top minus scrollOffset
|
||||
float drawY = msgAreaY - gs.scrollOffset;
|
||||
float unusedSpace = msgAreaH - static_cast<float>(totalMsgH);
|
||||
float bottomOffset = std::max(0.0f, unusedSpace);
|
||||
@@ -359,7 +373,7 @@ static void renderFrame(AppContext& ctx) {
|
||||
default: col = COL_SYS; prefix = "Sys> "; break;
|
||||
}
|
||||
|
||||
// 如果该消息可见,则绘制
|
||||
// 如果该消息可见,则绘制 / Draw if this message is visible
|
||||
float msgBottom = drawY + msgH;
|
||||
if (msgBottom > msgAreaY && drawY < msgAreaY + msgAreaH) {
|
||||
float lineY = drawY + 2;
|
||||
@@ -383,14 +397,14 @@ static void renderFrame(AppContext& ctx) {
|
||||
|
||||
SDL_SetRenderClipRect(r, nullptr);
|
||||
|
||||
// 7. 输入区域分隔线
|
||||
// 7. 输入区域分隔线 / Input area separator line
|
||||
setColor(r, COL_SEP);
|
||||
SDL_RenderLine(r, msgAreaX, inputY, lw, inputY);
|
||||
|
||||
// 8. 输入区域背景
|
||||
// 8. 输入区域背景 / Input area background
|
||||
fillRect(r, mkRect(msgAreaX, inputY, msgAreaW, static_cast<float>(inputH)), COL_INPUT_BG);
|
||||
|
||||
// 9. 输入文本(支持多行显示)
|
||||
// 9. 输入文本(支持多行显示)/ Input text (multi-line support)
|
||||
if (!gs.inputBuffer.empty()) {
|
||||
std::string remaining = gs.inputBuffer;
|
||||
int lineIdx = 0;
|
||||
@@ -416,7 +430,7 @@ static void renderFrame(AppContext& ctx) {
|
||||
textY, "Type here...");
|
||||
}
|
||||
|
||||
// 10. 光标(多行感知)
|
||||
// 10. 光标(多行感知)/ Cursor (multi-line aware)
|
||||
if (!gs.streaming) {
|
||||
Uint64 now = SDL_GetTicks();
|
||||
if (now - gs.lastCursorBlink > 530) {
|
||||
@@ -424,7 +438,7 @@ static void renderFrame(AppContext& ctx) {
|
||||
gs.lastCursorBlink = now;
|
||||
}
|
||||
if (gs.cursorVisible && gs.cursorPos <= static_cast<int>(gs.inputBuffer.size())) {
|
||||
// 计算光标所在行和列
|
||||
// 计算光标所在行和列 / Calculate cursor line and column
|
||||
int curLine = 0;
|
||||
int charsBeforeLine = 0;
|
||||
for (int i = 0; i < gs.cursorPos; i++) {
|
||||
@@ -444,7 +458,7 @@ static void renderFrame(AppContext& ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
// 11. 发送/停止按钮
|
||||
// 11. 发送/停止按钮 / Send/Stop button
|
||||
float btnW = 5 * CHAR_W + PADDING;
|
||||
float btnH = CHAR_H + PADDING;
|
||||
float btnX = lw - btnW - PADDING;
|
||||
@@ -458,26 +472,27 @@ static void renderFrame(AppContext& ctx) {
|
||||
drawText(r, btnTextX, btnTextY, "[Send]", COL_WHITE);
|
||||
}
|
||||
|
||||
// 12. 状态栏
|
||||
// 12. 状态栏 / Status bar
|
||||
renderStatusBar(ctx);
|
||||
|
||||
// 13. Present
|
||||
// 13. Present / Present
|
||||
SDL_RenderPresent(r);
|
||||
}
|
||||
|
||||
// ---- 事件处理 ----
|
||||
// ---- 事件处理 / Event handling ----
|
||||
|
||||
// 尝试发送当前输入缓冲区的内容;返回 true 表示消息已排队
|
||||
// 验证当前输入缓冲区并将其作为用户消息加入队列;成功发送则返回 true。
|
||||
// Validate and queue the current input buffer as a user message; returns true if sent.
|
||||
static bool trySendMessage(GuiState& gs) {
|
||||
std::string text = gs.inputBuffer;
|
||||
// 去除前导/尾随空白,但保留内容空白
|
||||
// 去除前导/尾随空白,但保留内容空白 / Trim leading/trailing whitespace but preserve content whitespace
|
||||
size_t start = text.find_first_not_of(" \t\r\n");
|
||||
size_t end = text.find_last_not_of(" \t\r\n");
|
||||
if (start == std::string::npos) return false; // 空输入
|
||||
if (start == std::string::npos) return false; // 空输入 / empty input
|
||||
text = text.substr(start, end - start + 1);
|
||||
if (text.empty()) return false;
|
||||
|
||||
// 保存原始输入到历史(最多保留 20 条)
|
||||
// 保存原始输入到历史(最多保留 20 条) / Save original input to history (max 20 entries)
|
||||
gs.input_history.push_back(gs.inputBuffer);
|
||||
if (gs.input_history.size() > 20)
|
||||
gs.input_history.erase(gs.input_history.begin());
|
||||
@@ -489,7 +504,8 @@ static bool trySendMessage(GuiState& gs) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果输入区域中的 Send/Stop 按钮被点击,返回 true
|
||||
// 检查物理像素坐标是否落在发送/停止按钮区域内。
|
||||
// Return true if the given physical-pixel coordinates fall within the Send/Stop button.
|
||||
static bool isSendButtonHit(AppContext& ctx, float physX, float physY) {
|
||||
float lx = physX / RENDER_SCALE;
|
||||
float ly = physY / RENDER_SCALE;
|
||||
@@ -506,8 +522,10 @@ static bool isSendButtonHit(AppContext& ctx, float physX, float physY) {
|
||||
ly >= btnY && ly <= btnY + btnH;
|
||||
}
|
||||
|
||||
// ---- 流式回调 ----
|
||||
// ---- 流式回调 / Streaming callback ----
|
||||
|
||||
// 流式 token 回调:将 token 追加到 streamBuffer,更新最后一条助手消息,然后重新渲染。
|
||||
// Streaming token callback: appends token to streamBuffer, updates last assistant message, then re-renders.
|
||||
static int streamTokenCallback(const char* token, void* userdata) {
|
||||
AppContext* ctx = static_cast<AppContext*>(userdata);
|
||||
GuiState& gs = ctx->state;
|
||||
@@ -520,7 +538,7 @@ static int streamTokenCallback(const char* token, void* userdata) {
|
||||
}
|
||||
}
|
||||
|
||||
// 泵送事件以保持窗口响应
|
||||
// 泵送事件以保持窗口响应 / Pump events to keep the window responsive
|
||||
SDL_PumpEvents();
|
||||
|
||||
SDL_Event ev;
|
||||
@@ -547,15 +565,17 @@ static int streamTokenCallback(const char* token, void* userdata) {
|
||||
}
|
||||
}
|
||||
|
||||
// 重新渲染以显示进度的令牌
|
||||
// 重新渲染以显示进度的令牌 / Re-render to show the token progress
|
||||
gs.scrollOffset = 0;
|
||||
renderFrame(*ctx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- 主事件处理函数 ----
|
||||
// ---- 主事件处理函数 / Main event processing function ----
|
||||
|
||||
// 分发单个 SDL 事件以更新 GuiState(键盘输入、鼠标点击、滚动、文本输入)。
|
||||
// Dispatch a single SDL event to update GuiState (keyboard input, mouse clicks, scroll, text input).
|
||||
static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
GuiState& gs = ctx.state;
|
||||
|
||||
@@ -571,23 +591,23 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
bool shift = (mod & SDL_KMOD_SHIFT) != 0;
|
||||
|
||||
if (gs.streaming) {
|
||||
// 流式传输期间,按 Escape 键取消
|
||||
// 流式传输期间,按 Escape 键取消 / While streaming, press Escape to cancel
|
||||
if (key == SDLK_ESCAPE) {
|
||||
gs.streaming = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Tab 切换侧边栏显示/隐藏
|
||||
// Tab 切换侧边栏显示/隐藏 / Tab toggles sidebar visibility
|
||||
if (key == SDLK_TAB) {
|
||||
gs.sidebar_visible = !gs.sidebar_visible;
|
||||
break;
|
||||
}
|
||||
|
||||
// 输入历史浏览(↑/↓)
|
||||
// 输入历史浏览(↑/↓) / Input history browsing (Up/Down)
|
||||
if (key == SDLK_UP && !gs.input_history.empty()) {
|
||||
if (gs.history_index == -1) {
|
||||
// 首次进入历史浏览,保存当前输入
|
||||
// 首次进入历史浏览,保存当前输入 / First time browsing history, save current input
|
||||
gs.saved_input = gs.inputBuffer;
|
||||
gs.history_index = static_cast<int>(gs.input_history.size()) - 1;
|
||||
} else if (gs.history_index > 0) {
|
||||
@@ -606,7 +626,7 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
if (gs.history_index >= 0) {
|
||||
gs.inputBuffer = gs.input_history[gs.history_index];
|
||||
} else {
|
||||
// 回到新输入,恢复暂存的输入
|
||||
// 回到新输入,恢复暂存的输入 / Back to new input, restore saved input
|
||||
gs.inputBuffer = gs.saved_input;
|
||||
}
|
||||
gs.cursorPos = static_cast<int>(gs.inputBuffer.size());
|
||||
@@ -620,7 +640,7 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
case SDLK_RETURN:
|
||||
case SDLK_KP_ENTER:
|
||||
if (shift) {
|
||||
// Shift+Enter:插入换行符(不发送)
|
||||
// Shift+Enter:插入换行符(不发送) / Shift+Enter: insert newline (don't send)
|
||||
gs.inputBuffer.insert(gs.cursorPos, "\n");
|
||||
gs.cursorPos++;
|
||||
gs.cursorVisible = true;
|
||||
@@ -670,7 +690,7 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
break;
|
||||
case SDLK_V:
|
||||
if (ctrl) {
|
||||
// Ctrl+V:从剪贴板粘贴
|
||||
// Ctrl+V:从剪贴板粘贴 / Ctrl+V: paste from clipboard
|
||||
if (SDL_HasClipboardText()) {
|
||||
char* clip = SDL_GetClipboardText();
|
||||
if (clip) {
|
||||
@@ -685,7 +705,7 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
break;
|
||||
case SDLK_C:
|
||||
if (ctrl) {
|
||||
// Ctrl+C:复制到剪贴板(复制最后一条助手消息)
|
||||
// Ctrl+C:复制到剪贴板(复制最后一条助手消息) / Ctrl+C: copy to clipboard (copy last assistant message)
|
||||
if (!gs.messages.empty()) {
|
||||
for (int i = static_cast<int>(gs.messages.size()) - 1; i >= 0; --i) {
|
||||
if (gs.messages[i].role != ChatMessage::USER) {
|
||||
@@ -701,7 +721,7 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
break;
|
||||
case SDLK_L:
|
||||
if (ctrl) {
|
||||
// Ctrl+L:清除聊天
|
||||
// Ctrl+L:清除聊天 / Ctrl+L: clear chat
|
||||
if (g_session_svc) g_session_svc->clear();
|
||||
gs.messages.clear();
|
||||
gs.messages.push_back(ChatMessage(
|
||||
@@ -711,7 +731,7 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
break;
|
||||
case SDLK_S:
|
||||
if (ctrl) {
|
||||
// Ctrl+S:保存会话
|
||||
// Ctrl+S:保存会话 / Ctrl+S: save session
|
||||
if (g_session_svc && g_session_svc->save("session.json") == 0) {
|
||||
gs.messages.push_back(ChatMessage(
|
||||
ChatMessage::SYSTEM, "Session saved to session.json"));
|
||||
@@ -724,7 +744,7 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
break;
|
||||
case SDLK_O:
|
||||
if (ctrl) {
|
||||
// Ctrl+O:加载会话
|
||||
// Ctrl+O:加载会话 / Ctrl+O: load session
|
||||
if (g_session_svc && g_session_svc->load("session.json") == 0) {
|
||||
gs.messages.push_back(ChatMessage(
|
||||
ChatMessage::SYSTEM, "Session loaded from session.json"));
|
||||
@@ -743,7 +763,7 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
|
||||
case SDL_EVENT_TEXT_INPUT:
|
||||
if (!gs.streaming) {
|
||||
// 将文本插入光标位置
|
||||
// 将文本插入光标位置 / Insert text at cursor position
|
||||
gs.inputBuffer.insert(gs.cursorPos, ev.text.text);
|
||||
gs.cursorPos += static_cast<int>(strlen(ev.text.text));
|
||||
gs.cursorVisible = true;
|
||||
@@ -772,6 +792,8 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
case SDL_EVENT_WINDOW_RESIZED: {
|
||||
// 当窗口大小改变时,不更新我们的常量——保持 1024x768 的逻辑尺寸。
|
||||
// SDL 将自动缩放输出。
|
||||
// When window resizes, don't update our constants — keep 1024x768 logical size.
|
||||
// SDL will auto-scale the output.
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -780,10 +802,12 @@ static void processEvent(AppContext& ctx, SDL_Event& ev) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 入口 ----
|
||||
// ---- 入口 / Entry point ----
|
||||
|
||||
// 入口:初始化 dstalk host 和 SDL3,运行主事件/渲染循环,然后清理。
|
||||
// Entry point: initializes dstalk host and SDL3, runs the main event/render loop, then cleans up.
|
||||
int main(int argc, char* argv[]) {
|
||||
// ----- 初始化 dstalk -----
|
||||
// ----- 初始化 dstalk / Initialize dstalk -----
|
||||
if (dstalk_init(nullptr) != 0) {
|
||||
std::fprintf(stderr, "[dstalk] Init failed\n");
|
||||
return 1;
|
||||
@@ -796,7 +820,7 @@ int main(int argc, char* argv[]) {
|
||||
if (!g_ai_svc) dstalk_log(3, "AI service not found (check plugins directory)");
|
||||
if (!g_session_svc) dstalk_log(3, "Session service not found");
|
||||
|
||||
// ----- 初始化 SDL -----
|
||||
// ----- 初始化 SDL / Initialize SDL -----
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
std::fprintf(stderr, "[dstalk] SDL init failed: %s\n", SDL_GetError());
|
||||
dstalk_shutdown();
|
||||
@@ -822,10 +846,10 @@ int main(int argc, char* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 启用文本输入事件
|
||||
// 启用文本输入事件 / Enable text input events
|
||||
SDL_StartTextInput(window);
|
||||
|
||||
// ----- 应用程序状态 -----
|
||||
// ----- 应用程序状态 / Application state -----
|
||||
AppContext ctx;
|
||||
ctx.window = window;
|
||||
ctx.renderer = renderer;
|
||||
@@ -834,29 +858,29 @@ int main(int argc, char* argv[]) {
|
||||
"Ctrl+L clear, Ctrl+S save, Ctrl+O load. "
|
||||
"Shift+Enter for newline, Up/Down for history, Tab toggle sidebar."));
|
||||
|
||||
// ----- 主循环 -----
|
||||
// ----- 主循环 / Main loop -----
|
||||
SDL_Event event;
|
||||
while (ctx.state.running) {
|
||||
// 处理所有待处理事件
|
||||
// 处理所有待处理事件 / Process all pending events
|
||||
while (SDL_PollEvent(&event)) {
|
||||
processEvent(ctx, event);
|
||||
if (!ctx.state.running) break;
|
||||
}
|
||||
if (!ctx.state.running) break;
|
||||
|
||||
// 检查待发送的消息
|
||||
// 检查待发送的消息 / Check for pending message to send
|
||||
if (ctx.sendPending && !ctx.state.streaming) {
|
||||
ctx.sendPending = false;
|
||||
if (trySendMessage(ctx.state)) {
|
||||
// 开始流式传输
|
||||
// 开始流式传输 / Start streaming
|
||||
ctx.state.streaming = true;
|
||||
ctx.streamBuffer.clear();
|
||||
// 为流式响应添加占位消息
|
||||
// 为流式响应添加占位消息 / Add placeholder message for streaming response
|
||||
ctx.state.messages.push_back(
|
||||
ChatMessage(ChatMessage::ASSISTANT, ""));
|
||||
ctx.state.scrollOffset = 0;
|
||||
|
||||
// 对最后一条消息调用流式 API(通过插件服务 vtable)
|
||||
// 对最后一条消息调用流式 API(通过插件服务 vtable) / Call streaming API for the last message (via plugin service vtable)
|
||||
std::string& userMsg =
|
||||
ctx.state.messages[ctx.state.messages.size() - 2].content;
|
||||
int rc = -1;
|
||||
@@ -871,7 +895,7 @@ int main(int argc, char* argv[]) {
|
||||
g_ai_svc->free_result(&result);
|
||||
}
|
||||
|
||||
// 流式传输完成(或被取消)
|
||||
// 流式传输完成(或被取消) / Streaming completed (or cancelled)
|
||||
if (rc != 0) {
|
||||
if (!ctx.state.messages.empty() &&
|
||||
ctx.state.messages.back().role == ChatMessage::ASSISTANT) {
|
||||
@@ -884,14 +908,14 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染当前帧
|
||||
// 渲染当前帧 / Render current frame
|
||||
renderFrame(ctx);
|
||||
|
||||
// 短暂休眠以降低 CPU 使用率
|
||||
// 短暂休眠以降低 CPU 使用率 / Brief sleep to reduce CPU usage
|
||||
SDL_Delay(16); // ~60 FPS
|
||||
}
|
||||
|
||||
// ----- 清理 -----
|
||||
// ----- 清理 / Cleanup -----
|
||||
SDL_StopTextInput(window);
|
||||
SDL_DestroyRenderer(renderer);
|
||||
SDL_DestroyWindow(window);
|
||||
|
||||
Reference in New Issue
Block a user