news 2026/9/15 4:17:15

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
打造专属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-keyanthropic-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 保存会话列表和消息内容,方便刷新后秒恢复;服务端只负责转发请求和流式返回,不做持久化。这样做有一个明显好处——我不用处理数据库迁移,整个项目压缩到极简。

会话的数据结构大概是这样的:每条会话包含idtitlecreatedAtupdatedAt,以及一个messages数组。messages里的每条消息统一为{ id, role, content, timestamp },其中roleuserassistant。提示词预设则是独立的一个 JSON 数组,每个预设包含namedescriptionprompt,点击后自动填充到输入框并带到请求里。这套结构非常朴素,但足够撑起日常使用场景。

3. 实操过程与核心实现:从空目录到一个能跑的红色工作台

3.1 五分钟搭好 Next.js 项目骨架

如果不想从零敲配置,直接用官方脚手架就能起步。我推荐用create-next-app创建项目,执行时记得开启 TypeScript、Tailwind CSS 和 App Router 选项,后面写样式和路由会方便很多。核心命令如下:

npx create-next-app@latest 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; charset=utf-8", "cache-control": "no-cache", connection: "keep-alive", }, }); }

这段代码的要点是:接收前端传来的完整messages数组和可选的system提示词,在服务端拼好请求,把上游响应体直接转发给前端。因为流式返回是 SSE 格式,前端解析时要做两件事:第一,通过getReader()读取字节流;第二,把收到的文本按空行拆分成事件块,逐行解析eventdata字段。

前端读取流式响应的方法,我封装了这样一个异步函数:

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_MODELMAX_TOKENS可选。.env.local文件长这样:

ANTHROPIC_API_KEY=sk-ant-你的密钥 CLAUDE_MODEL=claude-sonnet-4-20250514 MAX_TOKENS=4096

注意.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 解码,问题通常出在两个地方:一是服务端返回时没有指定charset="utf-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.localANTHROPIC_API_KEY,确认后重启服务
404 model not found模型名写错,或账号无该模型权限在 Anthropic 后台确认可用模型,更新CLAUDE_MODEL
429 rate limit exceeded触发速率限制降低请求频率,检查MAX_TOKENS是否设置过大
前端一直转圈但不输出流式响应解析失败打开浏览器 Network 面板,查看/api/claude响应体是否有 SSE 事件
刷新页面后会话消失localStorage 写入失败或清空检查浏览器隐私模式,确认无异常抛出
中文乱码TextDecoder 未保留缓冲按 buffer 模式切分 SSE,参考 3.3 节代码
服务端崩溃环境变量缺失或内存不足确认 Node 版本,检查日志中未捕获异常

5. 经验总结与后续可以怎么玩

这套 Claude-Red 做了大概两个星期,最深的体会是:界面定制这件事,技术难度从来不是最高的,真正难的是把日常使用中那些“不舒服”一个一个抠出来,再用合理的方式解决掉。比如密钥管理、流式断线、会话归档,这些听起来都像是小问题,但叠加在一起,就决定了这个工具你到底愿不愿意天天用。

如果你也想做一个类似的项目,我的建议是不要一上来就堆功能。先把“能安全地聊天、能看到流式输出、关掉浏览器再打开记录还在”这三件事做好,就已经超过 80% 的半成品工具了。之后再根据真正常用的两三个场景,比如代码审查或者文案改写,做提示词预设,把使用效率拉到最高。

后续我打算在这个基础上加几个方向,一个是把会话数据从 localStorage 迁到 SQLite,支持多设备同步;另一个是想接入简单的知识库检索,让 Claude-Red 能直接引用本地文档回答问题,可以做基于文件系统的向量检索方案。每一个方向都会踩不少坑,等我把其中某一块真正跑通后,再单独写一篇来展开。

最后再分享一个小经验:红色主色的大面积使用容易让人产生视觉压迫感,我第一次把整个应用刷成红色的时候,用了不到半天就觉得眼睛累了。后面才意识到,深色界面里的强调色应该像荧光笔一样只在关键位置出现,而不是让整张屏幕都在发光。把配色换成“深灰底 + 局部红”的方案之后,整个项目的气质一下子就稳住了。这个原则不局限于这个项目,任何深色主题的自定义界面都可以参考。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 4:17:09

客流预测大屏前端实战:WebSocket实时推送与ECharts可视化

简介:面向高校计算机相关专业学生与前端开发初学者,这是一份毕业设计“基于深度学习的轨道交通客流实时分析预测系统”的前端工程资源,主要解决客流数据可视化、预测结果展示与交互操作落地等问题。压缩包共83个文件,以tsx、ts、c…

作者头像 李华
网站建设 2026/9/15 4:16:27

铁威马Hyper-WORM技术解析:中小企业数据安全的终极防线

1. 铁威马Hyper-WORM技术解析:中小企业数据安全的终极防线在数据爆炸式增长的时代,企业面临的数据安全挑战日益严峻。特别是对中小企业而言,如何在有限预算内实现合规的数据保护成为关键痛点。铁威马F4-425 Plus存储设备搭载的TOS6系统中&…

作者头像 李华
网站建设 2026/9/15 4:15:14

教育论文的干预设计分步落地:同一份设计,要在三处各成立一次

教育论文里做完一份干预设计,难的往往不是把活动想出来,而是让它在真实的班级里跑起来。五个动作:先写下要改变什么,再把它送到三处各过一遍——上课那一刻、学生那边,以及课后回看这三处——再小范围跑一次才定稿。干…

作者头像 李华
网站建设 2026/9/15 4:15:13

Python HTML处理工具:转义与安全防护实践

1. Python标准库中的HTML处理工具解析作为一门广泛应用于Web开发的编程语言,Python在标准库中内置了对HTML处理的完整支持。这个看似简单的模块实际上包含了Web开发中最基础也最关键的文本处理功能,特别是在需要动态生成HTML内容或处理用户输入时&#x…

作者头像 李华
网站建设 2026/9/15 4:14:32

串口远程透传原理与工业落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 4:14:27

SpringMVC路径映射原理与最佳实践

1. SpringMVC路径映射基础原理在SpringMVC框架中,RequestMapping注解是定义控制器方法如何映射到Web请求的核心机制。这个注解本质上建立了一个路由表,将HTTP请求的URL路径与后端Java方法进行绑定。1.1 注解的基本语法结构RequestMapping的标准用法包含以…

作者头像 李华