新闻详情

新闻详情

首页 / 资讯中心 / 详情

用 cpp-httplib 为 LLM 翻译服务添加 Web UI:静态文件挂载与 SSE 流式交互实战

发布时间:2026/10/2 8:16:42来源:尧图网络
用 cpp-httplib 为 LLM 翻译服务添加 Web UI:静态文件挂载与 SSE 流式交互实战
后端网络【免费下载链接】cpp-httplibA C header-only HTTP/HTTPS server and client library项目地址https://gitcode.com/GitHub_Trending/cp/cpp-httplib点击查看免费下载本教程是 cpp-httplib 构建 LLM 桌面应用的系列指南 的第五章。在前几章中我们已经用 cpp-httplib 实现了翻译 REST API、SSE 令牌流式输出和模型下载/切换管理但唯一的交互方式是 curl。本章将用纯 HTML/CSS/JavaScript不使用任何前端框架在浏览器中搭建完整的翻译界面输入即翻译、逐字输出、模型与语言下拉切换、未下载模型的进度条下载与取消。读完本文你将掌握set_mount_point静态文件服务的底层原理以及浏览器端用fetch()读取 POST SSE 流、用AbortController实现请求取消的完整实战模式。为什么需要 Web UI截至第四章服务器端能力已经齐备/translateJSON 翻译、/translate/streamSSE 流式翻译、/models模型列表、/models/select模型下载与切换。这些接口全部可以用 curl 验证但日常使用显然不现实。本章要实现的最终界面具备三个核心交互输入文本时翻译 token 逐个出现带防抖 debounce从头部下拉菜单切换模型与目标语言选择未下载的模型时弹出带进度条可取消的下载对话框。前端代码刻意保持精简约 100 行纯 CSS 完成布局不使用任何 CSS 框架HTML/CSS/JavaScript 全部放在public/目录中由服务器直接提供。5.1 文件结构本章新增的文件如下HTML、CSS、JavaScript 位于public/目录由服务器托管translate-app/ ├── public/ │ ├── index.html │ ├── style.css │ └── script.js └── src/ └── main.cpp # Add set_mount_point服务器端唯一的改动就是在main.cpp中加入一行set_mount_point。5.2 用 set_mount_point 提供静态文件服务cpp-httplib 的Server::set_mount_point可以把一个本地目录直接映射为 HTTP 路径。先创建public/目录并放入一个空壳index.htmlmkdir public!DOCTYPE html html langja head meta charsetUTF-8 titleTranslate App/title /head body h1Hello!/h1 /body /html然后在服务器代码中、svr.listen()之前加入一行// Add inside main(), before svr.listen() svr.set_mount_point(/, ./public);重新编译并启动服务器浏览器打开http://127.0.0.1:8080就能看到 Hello!。因为是纯静态文件修改index.html后只需刷新浏览器即可生效无需重启服务器。源码层面的实现原理从 httplib.h 的实现可以看到set_mount_point的完整行为inline bool Server::set_mount_point(const std::string mount_point, const std::string dir, Headers headers) { detail::FileStat stat(dir); if (stat.is_dir()) { std::string mnt !mount_point.empty() ? mount_point : /; if (!mnt.empty() mnt[0] /) { std::string resolved_base; if (detail::canonicalize_path(dir.c_str(), resolved_base)) { // ... 平台相关路径分隔符处理 ... base_dirs_.push_back( {std::move(mnt), dir, std::move(resolved_base), std::move(headers)}); return true; } } } return false; }几个关键点校验条件挂载点必须以/开头且目标必须是真实存在的目录FileStat::is_dir()否则返回false签名bool set_mount_point(const std::string mount_point, const std::string dir, Headers headers Headers())第三个参数可以为挂载目录附加自定义响应头声明位于 httplib.h路径规范化目录会被canonicalize_path解析为绝对路径并统一补上尾部斜杠用于后续静态文件请求的安全拼接防止路径穿越移除挂载与之配套的remove_mount_point(mount_point)httplib.h可以在运行时解除某个挂载点别名set_base_dir(dir, mount_point)是set_mount_point的等价别名httplib.h传入空挂载点时默认映射到/。这也是仓库示例程序 example/simplesvr.cc 的做法if (!svr.set_mount_point(/, base_dir))失败时打印提示并退出成功后才svr.listen。5.3 搭建界面布局将index.html替换为最终布局!DOCTYPE html html langja head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1 titleTranslate App/title !-- Set favicon with inline SVG emoji (no image file needed) -- link relicon hrefdata:image/svgxml,svg xmlnshttp://www.w3.org/2000/svg viewBox0 0 100 100text y.9em font-size90/text/svg link relstylesheet href/style.css /head body !-- Header: title model selector language selector -- header strongTranslate App/strong div !-- Options are dynamically populated by script.js via GET /models -- select idmodel-select aria-labelModel/select select idtarget-lang aria-labelTarget language option valuejaJapanese/option option valueenEnglish/option option valuezhChinese/option option valuekoKorean/option option valuefrFrench/option option valuedeGerman/option option valueesSpanish/option /select /div /header !-- Two-column layout: input and translation result -- main textarea idinput-text placeholderEnter text to translate.../textarea output idoutput-text/output /main !-- Modal displayed during model download -- dialog iddownload-dialog h3Downloading model.../h3 progress iddownload-progress max100 value0/progress p iddownload-status/p button iddownload-cancelCancel/button /dialog script src/script.js/script /body /htmlHTML 的设计要点favicon 零文件用data:image/svgxml内联一个 SVG emoji不需要任何图片文件dialog模态框标准 HTML 元素通过showModal()以模态形式显示下载进度output语义元素表示计算输出用于展示翻译结果没有翻译按钮翻译在输入文本时自动触发见 5.4 节模型下拉菜单为空选项由script.js通过GET /models动态填充。CSS 写入public/style.css不使用任何框架:root { --gap: 0.5rem; --color-border: #ccc; --font: system-ui, sans-serif; } * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; font-family: var(--font); } body { display: flex; flex-direction: column; padding: var(--gap); gap: var(--gap); } /* Header: title dropdowns */ header { display: flex; align-items: center; justify-content: space-between; } header div { display: flex; gap: var(--gap); } /* Main: two-column layout */ main { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); min-height: 0; } #input-text { resize: none; padding: 0.75rem; font-family: var(--font); font-size: 1rem; border: 1px solid var(--color-border); border-radius: 4px; } textarea:focus, select:focus { outline: 1px solid #4a9eff; outline-offset: -1px; } #output-text { display: block; padding: 0.75rem; font-size: 1rem; border: 1px solid var(--color-border); border-radius: 4px; white-space: pre-wrap; overflow-y: auto; } /* Download modal */ dialog { border: 1px solid var(--color-border); border-radius: 8px; padding: 1.5rem; max-width: 400px; width: 90%; margin: auto; } dialog::backdrop { background: rgba(0, 0, 0, 0.4); } dialog h3 { margin-bottom: 0.75rem; } dialog progress { width: 100%; height: 1.25rem; } dialog p { margin-top: 0.5rem; text-align: center; color: #666; } dialog button { display: block; margin: 0.75rem auto 0; padding: 0.4rem 1.5rem; cursor: pointer; } /* Block the entire UI during translation or model switching */ body.busy { cursor: wait; } body.busy select, body.busy textarea { pointer-events: none; opacity: 0.6; }布局要点body用 Flexbox 纵向排列main通过flex: 1占据剩余高度输入/输出区域延伸到窗口底部main用 CSS Grid 的1fr 1fr分成左右两栏--gap变量统一所有间距页头、输入框、输出框之间的缝隙宽度一致body.busy类用于翻译或切换模型期间屏蔽整个 UI鼠标变等待光标、控件不可点击且半透明由 JavaScript 负责切换。刷新浏览器即可看到左右并排的输入/输出区域。此时输入还没有任何反应但布局已完成。5.4 连接翻译功能fetch 读取 SSE 流创建public/script.js调用服务器 API。这里有一个关键技术点第三章实现的/translate/stream是POST接口ch03-sse-streaming.md而浏览器原生的EventSource只支持 GET因此必须用fetch()ReadableStream来读取 SSE用fetch()发送 POST 请求通过res.body.getReader()获取流边读块边处理以data:开头的行。由于网络分块可能把一条 SSE 行从中间截断需要维护缓冲区按行处理。自动翻译与防抖界面没有翻译按钮输入文本或切换语言即触发翻译每 300ms 防抖一次避免每个按键都发请求。为在继续输入时取消上一次翻译使用AbortController新输入到达时调用abort()取消上一个fetch并发起新翻译。因为要把取消用的signal传给fetchSSE 读取逻辑全部内联写在translate()里。const inputText document.getElementById(input-text); const outputText document.getElementById(output-text); const targetLang document.getElementById(target-lang); let debounceTimer null; let abortController null; async function translate() { const text inputText.value.trim(); if (!text) { outputText.textContent ; return; } // Cancel any in-progress translation if (abortController) abortController.abort(); abortController new AbortController(); const { signal } abortController; outputText.textContent ; document.body.classList.add(busy); try { const res await fetch(/translate/stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ text, target_lang: targetLang.value }), signal, }); if (!res.ok) { const err await res.json(); throw new Error(err.error || HTTP ${res.status}); } const reader res.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); for (const line of lines) { if (line.startsWith(data: )) { const data line.slice(6); if (data [DONE]) return; const parsed JSON.parse(data); if (parsed parsed.error) { outputText.textContent Error: parsed.error; return; } outputText.textContent parsed; } } } } catch (e) { if (e.name AbortError) return; // Cancelled by new input outputText.textContent Error: e.message; } finally { document.body.classList.remove(busy); } } function scheduleTranslation() { clearTimeout(debounceTimer); debounceTimer setTimeout(translate, 300); } inputText.addEventListener(input, scheduleTranslation); targetLang.addEventListener(change, scheduleTranslation);要点解析不用EventSource的原因除了只支持 GET 的限制还需要把AbortController的signal传给请求错误处理第三章在服务器端给流式响应加了try/catch错误以 JSON 对象形式作为 SSE 事件返回因此前端也要检查parsed.error[DONE]终止符与第三章约定一致读到data: [DONE]即结束流AbortError静默返回fetch被abort()取消时抛出的异常名恰为AbortError这是用户继续输入的正常路径不应显示错误。刷新浏览器并输入文字约 300ms 后 token 会逐个出现修改输入内容时上一次翻译被取消并立即开始新的翻译。服务端对取消的响应浏览器端abort()断开连接后服务端流式提供者如何感知答案是 httplib.h 中的DataSinksink.os是绑定了write回调的std::ostream每次向sink.os 写入都会调用write。客户端断开后写入失败sink.os.good()返回false。第三章的/translate/stream处理器正是这样做的见 ch03-sse-streaming.mdllm.chat(prompt, { sink.os data: json(std::string(token)).dump( -1, , false, json::error_handler_t::replace) \n\n; return sink.os.good(); // Abort inference on disconnect });llm.chat()的流式回调返回false时终止推理从而在浏览器取消请求时立即停止 LLM 生成节省计算资源。5.5 连接模型选择加载模型列表页面加载时调用GET /models初始化下拉菜单。服务端GET /models返回每个模型的name、params、downloaded、selected字段见第四章的完整main.cpp位于本章完整代码中const modelSelect document.getElementById(model-select); // Fetch model list from GET /models and build the dropdown async function loadModels() { const res await fetch(/models); const { models } await res.json(); modelSelect.innerHTML ; // Clear existing options for (const m of models) { const opt document.createElement(option); opt.value m.name; // Mark undownloaded models with a ⬇ icon to distinguish them opt.textContent m.downloaded ? ${m.name} (${m.params}) : ${m.name} (${m.params}) ⬇; opt.selected m.selected; // Select the current model using the selected flag from the server modelSelect.appendChild(opt); } } loadModels(); // Run on page load未下载的模型用⬇图标区分服务器返回的selected布尔值用于回显当前选中的模型。切换模型更改下拉菜单会调用POST /models/select。若需要下载弹出带进度条的dialog取消按钮可中止下载。与翻译一样使用AbortController点击取消按钮调用abort()断开连接服务端通过第四章download_model的sink.os.good()检测到断连并中止下载。const dialog document.getElementById(download-dialog); const progressBar document.getElementById(download-progress); const downloadStatus document.getElementById(download-status); const downloadCancel document.getElementById(download-cancel); let modelAbort null; downloadCancel.addEventListener(click, () { if (modelAbort) modelAbort.abort(); }); modelSelect.addEventListener(change, async () { const name modelSelect.value; document.body.classList.add(busy); modelAbort new AbortController(); const { signal } modelAbort; try { const res await fetch(/models/select, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ model: name }), signal, }); if (!res.ok) { const err await res.json(); throw new Error(err.error || HTTP ${res.status}); } const reader res.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop(); for (const line of lines) { if (line.startsWith(data: )) { const data line.slice(6); if (data [DONE]) return; const event JSON.parse(data); switch (event.status) { case downloading: if (!dialog.open) dialog.showModal(); // Show the modal progressBar.value event.progress; // Update the progress bar downloadStatus.textContent ${event.progress}%; break; case loading: // Removing the value attribute puts progress into animated (indeterminate) state progressBar.removeAttribute(value); downloadStatus.textContent Loading model...; break; case ready: if (dialog.open) dialog.close(); break; case error: if (dialog.open) dialog.close(); alert(Download failed: event.message); break; } } } } await loadModels(); // Refresh the list since the selected flag changed scheduleTranslation(); // Re-translate with the new model } catch (e) { if (e.name AbortError) { // Cancelled -- revert to the original model await loadModels(); } else { alert(Error: e.message); } } finally { document.body.classList.remove(busy); if (dialog.open) dialog.close(); modelAbort null; } });要点SSE 状态机/models/select无论模型是否已下载一律以 SSE 流响应第四章的设计依次推送downloading→loading→ready或error事件不确定进度条progressBar.removeAttribute(value)让progress进入动画不确定状态用于下载完成后加载模型入内存的阶段取消回退下载被取消时重新调用loadModels()把下拉菜单回退到原来的选中模型。5.6 完整代码index.html、style.css、script.js三个文件的最终版本即上文 5.3、5.4、5.5 各节展示的完整代码直接合并即可。服务端main.cpp的改动只有一行——在第四章完整代码的svr.listen()之前加入// --- Static file serving (Chapter 5) -------------------------------------- svr.set_mount_point(/, ./public);放置在模型相关路由之后、信号处理与svr.listen()之前。由于挂载点/只处理静态资源路径不会与已注册的/health、/translate、/translate/stream、/models、/models/select路由冲突——cpp-httplib 会优先匹配显式注册的路由其余路径落入静态文件服务。5.7 测试重新编译并启动服务器cmake --build build -j ./build/translate-server在浏览器打开http://127.0.0.1:8080按以下步骤验证输入文本——约 300ms 后 token 逐个增量出现修改输入——上一次翻译被取消立即开始新的翻译切换语言下拉框——自动重新翻译切换模型下拉框——若模型已下载则立即切换选择未下载的模型——弹出进度条点击 Cancel 可中止下载。至此第四章中所有用 curl 完成的操作都能在浏览器中完成。从服务端视角看/translate/stream和/models/select的 SSE 事件流由 httplib.h 的DataSink逐块推送给浏览器客户端断开即通过sink.os.good()感知并中止从客户端视角看fetch()ReadableStream替代了不支持 POST 的EventSourceAbortController实现了输入防抖取消与下载取消。下一步服务器与 Web UI 已完成。下一章将用 webview/webview 把这个应用封装成不依赖浏览器的桌面应用并把静态文件嵌入二进制使可分发产物成为单一可执行文件。相关教程见 Turning It into a Desktop App with WebView。赞分享后端网络【免费下载链接】cpp-httplibA C header-only HTTP/HTTPS server and client library项目地址https://gitcode.com/GitHub_Trending/cp/cpp-httplib点击查看免费下载相关推荐cpp-httplib 实战为 SSE 翻译服务添加带流式输出的可交互 Web UIcpp httplib 实战为 SSE 翻译服务添加带流式输出的可交互 Web UI 本指南讲解如何为基于 cpp httplib 构建的本地 LLM 翻译应后端网络Vector kafka sink 配置破坏性变更bootstrap_servers 从数组改为逗号分隔字符串Vector kafka sink 配置破坏性变更 bootstrap_servers 从数组改为逗号分隔字符串 导读 本文讲解 Vector 在 0.7.0后端网络cpp-httplib 静态文件服务器实战set_mount_point 挂载点用法与源码解析cpp httplib 静态文件服务器实战set_mount_point 挂载点用法与源码解析 cpp httplib 不仅是一个 HTTP/HTTPS 客户后端网络上一篇免费B站视频下载三步把大会员4K与充电专属视频离线拿到手下一篇jevgrep 提供者支持的可移植性保障契约Preservation Gates 与兼容性验证指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

COMSOL正方晶格光子晶体能带仿真全流程:从边界条件到特征频率详解 2026/10/2 8:59:28

COMSOL正方晶格光子晶体能带仿真全流程:从边界条件到特征频率详解

各位做光学计算和光子器件设计的朋友,肯定对COMSOL Multiphysics不陌生。它在光子晶体能带仿真这块可以说是最常用的工具之一,但也是最容易让人“翻车”的地方。尤其是正方晶格光子晶体,作为入门频率最高、概念最基础的模型,很多人…

阅读更多 →
找搭子+点单社群系统源码:部署实战与订单状态机解析 2026/10/2 8:59:28

找搭子+点单社群系统源码:部署实战与订单状态机解析

简介:一套用于搭建交友社群、找搭子、陪玩点单等场景的企业级系统源码,功能完整度与市场上售价过万的版本相当,面向具备前后端开发经验的技术人员、社群运营者或创业者。压缩包内共2002个文件,以JS和CSS文件为主,辅以M…

阅读更多 →
云边端协同算力体系:从分布式推理到确定性调度 2026/10/2 8:59:22

云边端协同算力体系:从分布式推理到确定性调度

1. 这不是“云边端”口号,而是一场算力分配方式的底层重构最近和几个做工业视觉检测的老朋友吃饭,聊到他们新上线的产线质检系统——原来部署在机房里的GPU服务器,现在被拆成了三块:模型训练扔进公有云集群,中间层推理…

阅读更多 →
南华大学数据库原理实验报告:SQL Server建库到嵌套查询完整复现 2026/10/2 8:59:22

南华大学数据库原理实验报告:SQL Server建库到嵌套查询完整复现

简介:这份南华大学《数据库原理》实验报告面向大数据技术等专业学生,用于课程实验的参考与复盘,帮助读者系统掌握数据库管理系统操作与SQL语言应用。报告围绕SQL Server Management Studio展开,涵盖数据库与数据表的创建、主码外码…

阅读更多 →
MySQL安装到增删改查全教程:环境配置、Workbench操作与SQL实践 2026/10/2 8:59:15

MySQL安装到增删改查全教程:环境配置、Workbench操作与SQL实践

简介:面向MySQL零基础学员的安装与使用教程,覆盖数据库环境搭建与基础操作的全流程,特别适合初次接触关系型数据库、需要完成课程实验或本地开发环境部署的读者。教程从官网下载官方安装向导讲起,针对安装过程中的密码设置、组件下…

阅读更多 →
Spring Boot医院医疗仪器管理系统:从业务梳理到落地实践 2026/10/2 8:59:09

Spring Boot医院医疗仪器管理系统:从业务梳理到落地实践

我第一次接手医院仪器管理系统这个需求时,内心其实有点不屑——医院设备管理不就是个资产台账吗?无非是把Excel搬到网页上,增删改查加个导出,Spring Boot一个单体应用跑起来就完事了。直到我真的坐到设备科办公室,看到…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉