打造专属Claude红色主题工作台:从Next.js到流式输出实践指南

发布时间:2026/9/15 4:17:26
打造专属Claude红色主题工作台:从Next.js到流式输出实践指南 Claude-Red这个项目名听着挺有攻击性其实它就是一个我最近在业余时间折腾出来的红色深色主题的 Claude 大模型对话工作台。起因很简单每天高频用 API 调 Claude 写代码、改文案、做表格原生 Playground 和各类套壳前端都不太顺手——要么界面太白晃眼要么会话管理太弱要么密钥直接暴露在浏览器里。于是干脆自己动手做了一个专门围绕 Claude 的定制界面把红色做成品牌主色把常用提示词、会话归档、流式输出全部整合进去整个过程踩了不少坑也积累了一批可以直接抄作业的实现方案。这个内容适合谁看我先说清楚如果你平时在用 Claude API 做开发或者想给自己的大模型应用做一个有点辨识度的前端界面再或者单纯想了解一个完整的前后端联调项目从零到一怎么做都可以顺着这条思路往下走。我会把技术选型、界面设计、API 接入、流式输出、常见故障排查全部拆开讲代码部分也会给到关键实现片段方便你直接复现或者改造。1. 项目初衷与整体设计为什么偏偏是红色又为什么自己做1.1 市面上现成界面到底差在哪我很早就发现官方 Playground 适合临时试 prompt但不适合日常使用。比如它没有真正的“多会话树”常用的角色设定每次都要重新粘连续多轮对话一旦超过上下文窗口提示词就莫名其妙被截断。市面上的套壳应用我也试了一圈不少确实做得很好看但要么是闭源在线服务数据要过一遍第三方服务器要么前端直接调大模型 API密钥就明文躺在 localStorage 里每次打开浏览器审查元素都能看到心里实在不踏实。所以我的需求其实很明确第一界面要让自己看得舒服深色为主红色作为强调色第二密钥要尽量留在服务端前端只请求自己的后端接口第三必须有清晰的会话历史、提示词预设、流式输出第四整套东西要能本地部署用 Docker 一键跑起来。把这些需求列出来之后自己写的冲动就压不住了。1.2 “Red”不只是颜色红色主题背后的设计动机很多人看到“Claude-Red”第一反应是“红色是不是代表危险、报错、异常”但实际上在这个项目里红色承担的是“品牌识别”和“焦点引导”两个功能。日常深色界面如果到处都是白色、灰色长时间盯着屏幕容易视觉疲劳而如果大面积用高饱和颜色又会让内容区域失去层次。我的最终方案是用深灰黑作为大面积底色把红色集中在左侧栏 Logo、当前会话高亮、按钮主操作、用户消息气泡这类“需要你注意”的位置。这样红色就成了视线锚点而不是干扰源。具体配色我参考了现代 UI 设计里常见的“红阶”思路主色用偏珊瑚红的#E5484D悬停和激活态用稍暗的#DA3B42大面积色块用接近黑色的#161414次级背景用#1D1C1C。这套颜色在 OLED 屏幕上尤其好看深色区域不刺眼红色也不会显得脏。后面在 3.2 节我会详细给出这套 CSS 变量和实际代码。2. 技术选型与架构设计哪些库值得用哪些坑可以避开2.1 为什么选 Next.js 做底座而不是 Vite 或纯 React项目的第一版我用的是 Vite React 纯前端方案做界面确实快但很快撞上两个问题第一浏览器直接请求大模型 API 会碰到 CORS 限制必须再起一层服务做转发第二如果未来要加用户登录、团队共享、知识库纯静态部署会非常别扭。于是我重构的时候换成了 Next.js App Router理由很直接——同一个项目里同时写前端页面和后端 API 路由部署也只需要一个 Node 服务不需要额外开一个转发服务。Next.js 的 Route Handler 可以直接导出一个POST函数来处理/api/claude请求前端 fetch 同源地址浏览器层面没有跨域问题密钥也能安全放在服务端环境变量里。另外 Next.js 对 TypeScript 的支持很成熟在做消息类型定义时会省不少事。如果你只是想要一个纯本地的简易工具Vite 方案也够用但只要涉及密钥管理和多人使用我建议直接上 Next.js后面会发现这一步是在给未来的扩展性铺路。2.2 API 调用和流式响应的基本模型Claude 的 Messages API 是目前新版模型的调用入口核心是向https://api.anthropic.com/v1/messages发 POST 请求带上x-api-key、anthropic-version两个关键头请求体里用messages数组传会话记录。如果要流式返回就把stream设为true服务端会返回一个 SSE 格式的文本流客户端可以通过content_block_delta事件不断读取增量内容。整个流程不复杂但很多人第一次接的时候会卡在“流式输出在 Next.js 里怎么转发”这个问题上。我这里的做法是在后端 Route Handler 里直接用fetch请求上游接口拿到ReadableStream之后用TextEncoder把它转成一个新的ReadableStream然后以text/event-stream的格式返回给前端。这样前端始终只对着自己的后端说话后端再和上游通信安全性和可维护性都更好。后面 3.3 节我会给完整代码。2.3 会话管理和提示词预设的数据结构Claude-Red 的会话存储我一开始想用数据库后来斟酌了一下单机自用场景直接用文件或 localStorage 就够了没必要引入 MongoDB 或 SQLite。最终我做了双层设计浏览器端用 localStorage 保存会话列表和消息内容方便刷新后秒恢复服务端只负责转发请求和流式返回不做持久化。这样做有一个明显好处——我不用处理数据库迁移整个项目压缩到极简。会话的数据结构大概是这样的每条会话包含id、title、createdAt、updatedAt以及一个messages数组。messages里的每条消息统一为{ id, role, content, timestamp }其中role是user或assistant。提示词预设则是独立的一个 JSON 数组每个预设包含name、description、prompt点击后自动填充到输入框并带到请求里。这套结构非常朴素但足够撑起日常使用场景。3. 实操过程与核心实现从空目录到一个能跑的红色工作台3.1 五分钟搭好 Next.js 项目骨架如果不想从零敲配置直接用官方脚手架就能起步。我推荐用create-next-app创建项目执行时记得开启 TypeScript、Tailwind CSS 和 App Router 选项后面写样式和路由会方便很多。核心命令如下npx create-next-applatest claude-red --typescript --tailwind --app cd claude-red npm install anthropic依赖方面官方anthropicSDK 可以直接引用但我在实际项目里更习惯用原生 fetch 写转发层原因很简单少一层封装报错信息更直观排查问题时能直接看到上游返回的原始响应。项目目录我的习惯是这样组织的claude-red/ app/ api/ claude/ route.ts # 后端流式转发 page.tsx # 主聊天界面 layout.tsx # 全局布局与主题元信息 globals.css # 红色主题 CSS 变量 components/ Sidebar.tsx # 会话列表与预设入口 ChatWindow.tsx # 聊天主区域 MessageItem.tsx # 单条消息渲染 PromptLibrary.tsx # 提示词预设库 lib/ store.ts # localStorage 会话工具 types.ts # 消息与会话类型定义 .env.local # 服务端密钥环境变量这个结构不算复杂胜在边界清晰。前端组件只关心展示和交互后端路由只关心转发lib/store.ts负责读写本地存储后续想换数据库也只需要替换这一个模块。3.2 红色主题系统怎么配置最实用主题部分我建议用 CSS 变量统一管理而不是在组件里写死颜色。这样换肤、调色、夜间模式都能在一个文件里解决。以下是我在globals.css里用的核心变量:root { --bg-primary: #161414; --bg-secondary: #1D1C1C; --bg-elevated: #262424; --border-color: rgba(255, 255, 255, 0.08); --text-primary: #EDEDED; --text-secondary: #A8A0A0; --accent-red: #E5484D; --accent-red-hover: #DA3B42; --accent-red-muted: rgba(229, 72, 77, 0.15); --user-bubble: #2A1416; --assistant-bubble: #1F1E1E; --radius-md: 12px; --radius-lg: 18px; }实际使用的时候红色不要大面积铺开这是我反复调了很久得出的经验。默认主题下左侧栏宽度约 260px背景色就是--bg-secondary激活会话那一项用--accent-red-muted打底左侧再加一条 3px 的红色竖条。主按钮和用户气泡用#E5484D但用户气泡可以和文字对比度之间做个平衡不然白字落在鲜红色上会显得刺眼。我的做法是气泡背景使用暗红#2A1416真正的大红只出现在发送按钮和 Logo 上。页面整体布局上我用了左侧栏加右侧聊天区的双栏结构。左侧栏里依次是新建会话按钮、会话历史列表、提示词预设入口右侧从上到下是当前会话标题、消息流、底部输入区。输入区固定放在视图底部用position: sticky或者 flex 布局都行但移动端要特别注意键盘弹出问题后面排查章节我会专门提。3.3 接入 Claude API 并处理流式输出后端路由是整个项目的核心我最终用的是原生 fetch 加 SSE 转发。下面给出app/api/claude/route.ts的代码片段这套实现我实测下来比较稳定import { NextRequest } from next/server; const CLAUDE_API https://api.anthropic.com/v1/messages; const MODEL process.env.CLAUDE_MODEL || claude-sonnet-4-20250514; const MAX_TOKENS Number(process.env.MAX_TOKENS || 4096); export async function POST(req: NextRequest) { const { messages, system, temperature 0.7 } await req.json(); const upstreamRes await fetch(CLAUDE_API, { method: POST, headers: { content-type: application/json, x-api-key: process.env.ANTHROPIC_API_KEY || , anthropic-version: 2023-06-01, }, body: JSON.stringify({ model: MODEL, max_tokens: MAX_TOKENS, system: system || undefined, messages, temperature, stream: true, }), }); if (!upstreamRes.ok || !upstreamRes.body) { const errorText await upstreamRes.text(); return new Response(errorText, { status: upstreamRes.status }); } const encoder new TextEncoder(); const decoder new TextDecoder(); const stream new ReadableStream({ async start(controller) { const reader upstreamRes.body!.getReader(); try { while (true) { const { done, value } await reader.read(); if (done) break; const chunk decoder.decode(value, { stream: true }); controller.enqueue(encoder.encode(chunk)); } } catch (err) { controller.error(err); } finally { reader.releaseLock(); controller.close(); } }, }); return new Response(stream, { headers: { content-type: text/event-stream; charsetutf-8, cache-control: no-cache, connection: keep-alive, }, }); }这段代码的要点是接收前端传来的完整messages数组和可选的system提示词在服务端拼好请求把上游响应体直接转发给前端。因为流式返回是 SSE 格式前端解析时要做两件事第一通过getReader()读取字节流第二把收到的文本按空行拆分成事件块逐行解析event和data字段。前端读取流式响应的方法我封装了这样一个异步函数async function streamClaudeResponse(body: any) { const res await fetch(/api/claude, { method: POST, headers: { content-type: application/json }, body: JSON.stringify(body), }); if (!res.ok) { const errText await res.text(); throw new Error(errText || 请求失败请检查后端日志); } const reader res.body!.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const events buffer.split(\n\n); buffer events.pop() || ; for (const event of events) { for (const line of event.split(\n)) { if (!line.startsWith(data:)) continue; const data line.slice(5).trim(); if (!data) continue; if (data [DONE]) return; const parsed JSON.parse(data); if (parsed.type content_block_delta parsed.delta?.text) { // 将 parsed.delta.text 追加到当前助手消息 } } } } }这里有一个很容易踩的坑SSE 的data字段可能被拆成多个字节块传输尤其是中文内容较多时。如果每次只按行处理而不保留buffer极容易出现“JSON.parse 时报 Unexpected token”的错误。核心解法就是上面代码里的buffer模式——先把收到的文本追加到缓冲区再按双换行切分切完后剩余部分继续留到下一轮。实测下来这个逻辑应对各种网络波动都不会丢内容。3.4 会话持久化、输入区与提示词预设的联动会话持久化我写了一个很轻量的lib/store.ts核心 API 只有三个loadConversations()、saveConversation(convo)、deleteConversation(id)。所有会话统一存到 localStorage 的claude-red.conversations字段中。这里有两个需要注意的点第一写入前一定要判断 localStorage 是否可用比如在隐私模式下直接访问某些浏览器属性会抛异常第二每次保存建议防抖一下避免用户连续发送消息时频繁触发setItem造成掉帧。const STORAGE_KEY claude-red.conversations; export function loadConversations(): Conversation[] { try { const raw localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } } export function saveConversations(list: Conversation[]) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(list)); } catch (err) { console.error(保存会话失败, err); } }输入区我建议做成多行自适应而不是单行 input。因为日常问问题经常会一段一段贴日志单行输入框会非常折磨。实现思路很简单给textarea设置rows{1}然后在onInput里根据scrollHeight动态调整高度最大高度限制在 200px 左右超出后出现滚动条。Enter 键发送Shift Enter 换行这是几乎所有对话类产品的通用交互用户不需要学习成本。提示词预设库是我个人觉得这个项目最提升效率的部分。我在lib/preset.ts里预置了十来种常用场景比如代码审查、SQL 生成、文案润色、周报总结、学习笔记整理。每个预设都是一个{ name, description, prompt }对象。用户点击某个预设后输入框会被填充成对应的提示词模板同时自动追加一条说明“当前角色已切换为 XX”。这样做的好处是不用每次手写大段角色设定尤其是写代码审查提示词时能节省大量时间。我实际使用中最多的是代码审查预设prompt 大概是这么一段你是一位资深后端工程师请从代码风格、性能隐患、边界条件、错误处理四个维度 逐行审查我提供的代码片段。每个问题请注明严重程度高/中/低 并且用中文给出可落地的修改建议。整个联动链路就是点击预设 - 填充输入框 - 发送 - 后端转发 - 流式渲染到当前会话。逻辑不复杂但非常实用。3.5 本地部署与环境变量配置项目需要配置的环境变量有三个ANTHROPIC_API_KEY是必须的CLAUDE_MODEL和MAX_TOKENS可选。.env.local文件长这样ANTHROPIC_API_KEYsk-ant-你的密钥 CLAUDE_MODELclaude-sonnet-4-20250514 MAX_TOKENS4096注意.env.local绝对不要提交到 Git我在.gitignore里特意加了一行。部署时如果直接在服务器上运行执行npm run build npm start就好如果想用 Docker我写了很简单的Dockerfile基于 Node 20 的 alpine 镜像把node_modules安装好后直接跑npm start暴露 3000 端口。这个项目本身没有数据库容器可以随时销毁重建状态都留在浏览器端运维成本几乎为零。4. 常见问题与排查技巧实录4.1 流式响应中途断开怎么处理这是我使用过程中遇到最多的问题尤其在网络不太稳定的场景上游返回到一半连接突然断了。前端表现是消息停在半截没有报错。排查思路是先确认是不是上游在发送message_stop之前就把连接关闭了再确认是不是本地代理或防火墙做了空闲超时最后看前端解析的 buffer 里有没有残留数据。我的处理策略分两层。第一层是前端增加超时控制用AbortController给整个请求设一个 60 秒的超时超过后自动放弃本次请求并给出提示第二层是增加“重新生成”按钮断流后用户可以直接点击重试当前 prompt。如果是在公网部署还可以考虑在服务端加心跳包: ping注释行SSE 规范的注释行可以维持连接活跃防止中间网关断开空闲连接。如果你是在本地跑这个处理基本用不上但在服务器上非常管用。4.2 密钥安全与前端请求范围控制很多人一开始图省事直接把apiKey写在 React 组件里或者放在前端请求头里这是非常危险的做法。因为浏览器环境里的一切都是可读的任何人打开开发者工具就能把你的密钥拿走。Claude-Red 的方案很简单密钥只存在于服务端环境变量前端代码里你不看任何sk-ant-开头的字符串所有请求走同源/api/claude。另外我还做了一层限制在 Route Handler 里检查请求来源只有本地回环地址或指定域名才允许访问。虽然 Next.js 部署在服务端时不太容易被别人直接扫描到环境变量但多加一层校验总会更稳。如果你后续要开放给别人使用建议直接接一套简单的登录鉴权或者至少配置反向代理的 IP 白名单。4.3 中文乱码和 Markdown 渲染不干净中文乱码问题我早期也遇到过。排查下来发现TextDecoder默认是按 UTF-8 解码问题通常出在两个地方一是服务端返回时没有指定charsetutf-8某些网络中间件可能会用默认编码解析导致前端收到的是乱码字节二是移动端网络较差时字节流被切成不完整的多字节序列如果每次光按字符切分没有保留尾部残留就会把中文的 UTF-8 尾字节截断。解决方案就是我 3.3 节代码里展示的 buffer 模式可以最大程度避免多字节字符被切碎的问题。Markdown 渲染我建议直接使用react-markdown配合remark-gfm插件支持表格、任务列表和删除线。代码高亮可以用react-syntax-highlighter或者shiki但注意不要在用户打字过程中实时渲染整条消息否则长文本场景会造成明显卡顿更好的办法是收到content_block_stop事件后再统一渲染。4.4 移动端适配和性能取舍Claude-Red 从设计之初就是优先桌面端的但后来我确实也遇到需要在手机上应急看对话的情况。移动端最大的问题有两个一是键盘弹出后输入框被挡住二是侧边栏占据大半个屏幕。键盘问题我用100dvh替代100vh来设置主容器高度这样浏览器会识别动态视口高度键盘弹起后布局不会被压缩成一团。侧边栏我改造成抽屉模式默认隐藏点击左上角菜单按钮再滑出。性能方面最大的开销来自长消息的 Markdown 渲染。如果一条消息有几千行代码直接渲染会导致 UI 阻塞。我的做法是给MessageItem增加一个简单的虚拟滚动逻辑只渲染当前视口内和视口前后各两条消息其余用固定高度的占位 div 替代。实测下来200 多条消息的古老会话也能流畅滚动。如果不想自己写虚拟滚动用tanstack/react-virtual这个库也很简单。4.5 常见错误速查表我把平时最容易遇到的报错整理成了一张表遇到问题时先对照排查错误现象常见原因处理办法401 invalid x-api-key服务端环境变量没配或密钥已失效检查.env.local中ANTHROPIC_API_KEY确认后重启服务404 model not found模型名写错或账号无该模型权限在 Anthropic 后台确认可用模型更新CLAUDE_MODEL429 rate limit exceeded触发速率限制降低请求频率检查MAX_TOKENS是否设置过大前端一直转圈但不输出流式响应解析失败打开浏览器 Network 面板查看/api/claude响应体是否有 SSE 事件刷新页面后会话消失localStorage 写入失败或清空检查浏览器隐私模式确认无异常抛出中文乱码TextDecoder 未保留缓冲按 buffer 模式切分 SSE参考 3.3 节代码服务端崩溃环境变量缺失或内存不足确认 Node 版本检查日志中未捕获异常5. 经验总结与后续可以怎么玩这套 Claude-Red 做了大概两个星期最深的体会是界面定制这件事技术难度从来不是最高的真正难的是把日常使用中那些“不舒服”一个一个抠出来再用合理的方式解决掉。比如密钥管理、流式断线、会话归档这些听起来都像是小问题但叠加在一起就决定了这个工具你到底愿不愿意天天用。如果你也想做一个类似的项目我的建议是不要一上来就堆功能。先把“能安全地聊天、能看到流式输出、关掉浏览器再打开记录还在”这三件事做好就已经超过 80% 的半成品工具了。之后再根据真正常用的两三个场景比如代码审查或者文案改写做提示词预设把使用效率拉到最高。后续我打算在这个基础上加几个方向一个是把会话数据从 localStorage 迁到 SQLite支持多设备同步另一个是想接入简单的知识库检索让 Claude-Red 能直接引用本地文档回答问题可以做基于文件系统的向量检索方案。每一个方向都会踩不少坑等我把其中某一块真正跑通后再单独写一篇来展开。最后再分享一个小经验红色主色的大面积使用容易让人产生视觉压迫感我第一次把整个应用刷成红色的时候用了不到半天就觉得眼睛累了。后面才意识到深色界面里的强调色应该像荧光笔一样只在关键位置出现而不是让整张屏幕都在发光。把配色换成“深灰底 局部红”的方案之后整个项目的气质一下子就稳住了。这个原则不局限于这个项目任何深色主题的自定义界面都可以参考。