298 lines
13 KiB
Markdown
298 lines
13 KiB
Markdown
|
|
给你一份可直接落地的前端实现:我已把完整的 React/Next.js 单文件示例(含自定义 Tool UI、样式与 Data Stream 运行时接入)放到右侧画布里。按下面步骤即可跑起来:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
"use client";
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------
|
|||
|
|
// assistant-ui × LangGraph(FastAPI) 流式前端 (Data Stream 协议)
|
|||
|
|
// ------------------------------------------------------------
|
|||
|
|
// 说明:
|
|||
|
|
// 1) 该文件可作为 Next.js `app/page.tsx` 或任意 React 页面组件使用。
|
|||
|
|
// 2) 依赖:
|
|||
|
|
// npm i @assistant-ui/react @assistant-ui/react-ui @assistant-ui/react-data-stream \
|
|||
|
|
// @assistant-ui/react-markdown remark-gfm @radix-ui/react-tooltip \
|
|||
|
|
// @radix-ui/react-slot lucide-react class-variance-authority clsx tailwindcss-animate
|
|||
|
|
// 3) 样式:
|
|||
|
|
// - 在 tailwind.config.ts 中加入插件:
|
|||
|
|
// plugins: [
|
|||
|
|
// require("tailwindcss-animate"),
|
|||
|
|
// require("@assistant-ui/react-ui/tailwindcss")({ components: ["thread", "thread-list"], shadcn: true })
|
|||
|
|
// ]
|
|||
|
|
// - 在全局布局文件(如 app/layout.tsx)中引入:
|
|||
|
|
// import "@assistant-ui/react-ui/styles/index.css";
|
|||
|
|
// 4) 运行约定:后端 FastAPI 暴露 POST /api/chat,返回基于 Data Stream 协议的 SSE。
|
|||
|
|
// - 响应头需包含:'x-vercel-ai-ui-message-stream': 'v1'
|
|||
|
|
// - 事件类型至少包含:start、text-start / text-delta / text-end、
|
|||
|
|
// tool-input-start / tool-input-delta / tool-input-available、
|
|||
|
|
// tool-output-available、start-step、finish-step、finish、[DONE]
|
|||
|
|
// - 这些事件来自 LangGraph 的 run/工具事件映射(由后端转成 Data Stream 协议)。
|
|||
|
|
// ------------------------------------------------------------
|
|||
|
|
|
|||
|
|
import React, { useMemo } from "react";
|
|||
|
|
import {
|
|||
|
|
AssistantRuntimeProvider,
|
|||
|
|
makeAssistantToolUI,
|
|||
|
|
} from "@assistant-ui/react";
|
|||
|
|
import { useDataStreamRuntime } from "@assistant-ui/react-data-stream";
|
|||
|
|
import { Thread } from "@assistant-ui/react-ui";
|
|||
|
|
import { Check, Globe, Search, Terminal } from "lucide-react";
|
|||
|
|
|
|||
|
|
// ---------------------------
|
|||
|
|
// 1) 自定义 Tool UI(可选)
|
|||
|
|
// ---------------------------
|
|||
|
|
// 将 LangGraph 工具事件以特定工具名注册到前端 UI 中,
|
|||
|
|
// toolName 需与后端发送的工具名完全一致。
|
|||
|
|
|
|||
|
|
// Web 搜索工具 UI(示例:toolName: "web_search")
|
|||
|
|
const WebSearchToolUI = makeAssistantToolUI<{ query: string }, { results: Array<{ title: string; url: string; snippet?: string }>; took_ms?: number }>({
|
|||
|
|
toolName: "web_search",
|
|||
|
|
render: ({ args, result, status }) => {
|
|||
|
|
return (
|
|||
|
|
<div className="rounded-2xl border bg-card text-card-foreground p-3 my-2">
|
|||
|
|
<div className="flex items-center gap-2 text-sm font-medium opacity-80">
|
|||
|
|
<Search className="h-4 w-4" />
|
|||
|
|
<span>Web 搜索</span>
|
|||
|
|
<span className="opacity-60">— {args?.query ?? ""}</span>
|
|||
|
|
</div>
|
|||
|
|
{status.type === "running" && (
|
|||
|
|
<p className="text-sm mt-2 opacity-80">正在搜索…</p>
|
|||
|
|
)}
|
|||
|
|
{status.type === "requires_action" && (
|
|||
|
|
<p className="text-sm mt-2 opacity-80">等待后端确认…</p>
|
|||
|
|
)}
|
|||
|
|
{status.type === "incomplete" && (
|
|||
|
|
<p className="text-sm mt-2 text-destructive">搜索失败</p>
|
|||
|
|
)}
|
|||
|
|
{status.type === "complete" && result && (
|
|||
|
|
<ul className="mt-2 space-y-2">
|
|||
|
|
{result.results?.slice(0, 6).map((r, i) => (
|
|||
|
|
<li key={i} className="text-sm">
|
|||
|
|
<a className="underline underline-offset-4" href={r.url} target="_blank" rel="noreferrer">
|
|||
|
|
{r.title}
|
|||
|
|
</a>
|
|||
|
|
{r.snippet && <p className="opacity-80 mt-1">{r.snippet}</p>}
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
{typeof result.took_ms === "number" && (
|
|||
|
|
<li className="text-xs opacity-60">耗时 {result.took_ms}ms</li>
|
|||
|
|
)}
|
|||
|
|
</ul>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// URL 抓取工具 UI(示例:toolName: "fetch_url")
|
|||
|
|
const FetchUrlToolUI = makeAssistantToolUI<{ url: string }, { title?: string; content?: string } | { error: string }>({
|
|||
|
|
toolName: "fetch_url",
|
|||
|
|
render: ({ args, result, status }) => {
|
|||
|
|
return (
|
|||
|
|
<div className="rounded-2xl border bg-card text-card-foreground p-3 my-2">
|
|||
|
|
<div className="flex items-center gap-2 text-sm font-medium opacity-80">
|
|||
|
|
<Globe className="h-4 w-4" />
|
|||
|
|
<span>抓取网页</span>
|
|||
|
|
<span className="opacity-60">— {args?.url ?? ""}</span>
|
|||
|
|
</div>
|
|||
|
|
{status.type === "running" && (
|
|||
|
|
<p className="text-sm mt-2 opacity-80">抓取中…</p>
|
|||
|
|
)}
|
|||
|
|
{status.type === "complete" && result && "error" in result && (
|
|||
|
|
<p className="text-sm mt-2 text-destructive">错误:{result.error}</p>
|
|||
|
|
)}
|
|||
|
|
{status.type === "complete" && result && !("error" in result) && (
|
|||
|
|
<div className="mt-2 text-sm space-y-1">
|
|||
|
|
{result.title && <p className="font-medium">{result.title}</p>}
|
|||
|
|
{result.content && (
|
|||
|
|
<p className="opacity-80 line-clamp-4" title={result.content}>
|
|||
|
|
{result.content}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Python 代码执行 UI(示例:toolName: "python" 或 "run_python")
|
|||
|
|
const PythonToolUI = makeAssistantToolUI<{ code: string }, { stdout?: string; stderr?: string; elapsed_ms?: number }>({
|
|||
|
|
toolName: "python",
|
|||
|
|
render: ({ args, result, status }) => {
|
|||
|
|
return (
|
|||
|
|
<div className="rounded-2xl border bg-card text-card-foreground p-3 my-2">
|
|||
|
|
<div className="flex items-center gap-2 text-sm font-medium opacity-80">
|
|||
|
|
<Terminal className="h-4 w-4" />
|
|||
|
|
<span>执行 Python</span>
|
|||
|
|
</div>
|
|||
|
|
<pre className="bg-muted/60 rounded-xl p-3 mt-2 text-xs overflow-auto max-h-64">
|
|||
|
|
{args?.code}
|
|||
|
|
</pre>
|
|||
|
|
{status.type === "running" && (
|
|||
|
|
<p className="text-sm mt-2 opacity-80">运行中…</p>
|
|||
|
|
)}
|
|||
|
|
{status.type === "complete" && result && (
|
|||
|
|
<div className="mt-2 text-sm space-y-2">
|
|||
|
|
{result.stdout && (
|
|||
|
|
<div>
|
|||
|
|
<p className="font-medium">stdout</p>
|
|||
|
|
<pre className="bg-muted/60 rounded-xl p-3 mt-1 text-xs overflow-auto max-h-64">{result.stdout}</pre>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{result.stderr && (
|
|||
|
|
<div>
|
|||
|
|
<p className="font-medium">stderr</p>
|
|||
|
|
<pre className="bg-muted/60 rounded-xl p-3 mt-1 text-xs overflow-auto max-h-64 text-red-600">{result.stderr}</pre>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{typeof result.elapsed_ms === "number" && (
|
|||
|
|
<div className="flex items-center gap-2 text-xs opacity-60">
|
|||
|
|
<Check className="h-3 w-3" /> 用时 {result.elapsed_ms}ms
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---------------------------
|
|||
|
|
// 2) Runtime Provider(Data Stream 协议,SSE)
|
|||
|
|
// ---------------------------
|
|||
|
|
// useDataStreamRuntime 会:
|
|||
|
|
// - 在发送消息后,自动通过 EventSource 连接到 /api/chat 的 SSE 流;
|
|||
|
|
// - 解析 Data Stream 协议事件并更新到线程消息;
|
|||
|
|
// - 支持多步/工具调用的可视化(配合上方 Tool UIs)。
|
|||
|
|
|
|||
|
|
function AssistantProvider({ children }: { children: React.ReactNode }) {
|
|||
|
|
const runtime = useDataStreamRuntime({
|
|||
|
|
api: "/api/chat", // 对应 FastAPI 的 POST /api/chat
|
|||
|
|
// 如果需要自定义 headers/cookies,可传入 fetcher:
|
|||
|
|
// fetcher: (input, init) => fetch(input, { ...init, credentials: "include" })
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<AssistantRuntimeProvider runtime={runtime}>{children}</AssistantRuntimeProvider>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------
|
|||
|
|
// 3) 主界面:Thread(预设主题,开箱即用)
|
|||
|
|
// ---------------------------
|
|||
|
|
// 你也可以改用更细粒度的 primitives 自定义外观;此处采用 @assistant-ui/react-ui 的 Thread 组件。
|
|||
|
|
|
|||
|
|
export default function App() {
|
|||
|
|
// 你可通过 URL 参数或路由传入 threadId 等信息(示例保留默认主线程)。
|
|||
|
|
const header = useMemo(
|
|||
|
|
() => (
|
|||
|
|
<div className="border-b bg-background/60 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|||
|
|
<div className="mx-auto max-w-3xl px-4 py-3">
|
|||
|
|
<div className="text-sm opacity-70">LangGraph Agent · FastAPI · Streaming</div>
|
|||
|
|
<h1 className="text-lg font-semibold">assistant-ui × LangGraph(FastAPI) 演示</h1>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
[]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="h-screen w-full flex flex-col">
|
|||
|
|
{header}
|
|||
|
|
<div className="mx-auto max-w-3xl w-full grow px-4">
|
|||
|
|
<div className="h-full py-4">
|
|||
|
|
<AssistantProvider>
|
|||
|
|
{/* 注册前端 Tool UI(顺序无关,可按需增减;未注册的工具将 fallback 到纯文本或你自定义的 ToolFallback)*/}
|
|||
|
|
<WebSearchToolUI />
|
|||
|
|
<FetchUrlToolUI />
|
|||
|
|
<PythonToolUI />
|
|||
|
|
|
|||
|
|
{/* 线程组件:包含消息视图 + 输入框,默认支持 Markdown、高亮、附件、撤回/编辑、自动滚动等 */}
|
|||
|
|
<Thread className="h-full rounded-2xl border" placeholder="问我任何问题,或让代理调用工具…" />
|
|||
|
|
</AssistantProvider>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------
|
|||
|
|
// 4) 与 FastAPI/LangGraph 的接口契约(仅供参考,非前端代码)
|
|||
|
|
// ------------------------------------------------------------
|
|||
|
|
// FastAPI 需要在 /api/chat 接收形如 { messages: UIMessage[] } 的 JSON,并返回 SSE:
|
|||
|
|
// Content-Type: text/event-stream
|
|||
|
|
// x-vercel-ai-ui-message-stream: v1
|
|||
|
|
// 核心事件示例(每行以 `data: <json>\n\n` 形式发送;最后 `data: [DONE]\n\n` 终止):
|
|||
|
|
// data: {"type":"start","messageId":"..."}
|
|||
|
|
// data: {"type":"text-start","id":"..."}
|
|||
|
|
// data: {"type":"text-delta","id":"...","delta":"Hello"}
|
|||
|
|
// data: {"type":"text-end","id":"..."}
|
|||
|
|
// data: {"type":"tool-input-start","toolCallId":"...","toolName":"python"}
|
|||
|
|
// data: {"type":"tool-input-delta","toolCallId":"...","inputTextDelta":"print(1)"}
|
|||
|
|
// data: {"type":"tool-input-available","toolCallId":"...","toolName":"python","input":{"code":"print(1)"}}
|
|||
|
|
// data: {"type":"tool-output-available","toolCallId":"...","output":{"stdout":"1\n"}}
|
|||
|
|
// data: {"type":"finish-step"}
|
|||
|
|
// data: {"type":"finish"}
|
|||
|
|
// data: [DONE]
|
|||
|
|
// 这些事件可由 LangGraph 的 streaming 回调/事件(如 on_event/on_tool_start/on_tool_end)转换而来。
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
# 怎么用
|
|||
|
|
|
|||
|
|
1. 安装依赖
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm i @assistant-ui/react @assistant-ui/react-ui @assistant-ui/react-data-stream \
|
|||
|
|
@assistant-ui/react-markdown remark-gfm @radix-ui/react-tooltip \
|
|||
|
|
@radix-ui/react-slot lucide-react class-variance-authority clsx tailwindcss-animate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. 加样式
|
|||
|
|
|
|||
|
|
* `tailwind.config.ts` 添加:
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
plugins: [
|
|||
|
|
require("tailwindcss-animate"),
|
|||
|
|
require("@assistant-ui/react-ui/tailwindcss")({ components: ["thread","thread-list"], shadcn: true })
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
* 在全局布局(如 `app/layout.tsx`)里加入:
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
import "@assistant-ui/react-ui/styles/index.css";
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. 放代码
|
|||
|
|
把我提供的文件作为 `app/page.tsx`(或你的任意页面组件)使用即可。
|
|||
|
|
|
|||
|
|
4. 与后端的对接约定(FastAPI + LangGraph)
|
|||
|
|
|
|||
|
|
* 前端默认向 `POST /api/chat` 发送 `{ messages: UIMessage[] }`,并期望 **SSE** 流式返回,协议为 **Vercel AI SDK 的 Data Stream 协议**:响应头 **必须** 包含 `x-vercel-ai-ui-message-stream: v1`。
|
|||
|
|
* 你的 FastAPI 需要把 LangGraph 的运行/工具事件映射为以下事件并逐条写出(SSE `data:` 行,一行一个 JSON,末尾 `[DONE]` 终止),例如:
|
|||
|
|
|
|||
|
|
* `start`、`text-start` / `text-delta` / `text-end`
|
|||
|
|
* `tool-input-start` / `tool-input-delta` / `tool-input-available`
|
|||
|
|
* `tool-output-available`
|
|||
|
|
* `start-step` / `finish-step`、`finish`、`[DONE]`
|
|||
|
|
|
|||
|
|
画布代码里我已注册了常见工具 UI(`web_search`、`fetch_url`、`python`),多步/工具调用会自动以卡片形式流式显示、并和模型文本交替渲染。
|
|||
|
|
|
|||
|
|
# 关键参考(确认兼容性)
|
|||
|
|
|
|||
|
|
* assistant-ui 官方入门与组件:([assistant-ui.com][1])
|
|||
|
|
* 预置样式包与示例 Thread 用法:([assistant-ui.com][2])
|
|||
|
|
* Data Stream 运行时(前端)与 API:([assistant-ui.com][3])
|
|||
|
|
* Data Stream 协议(后端需要按此协议推送 SSE):事件清单与 `x-vercel-ai-ui-message-stream: v1` 头要求见 ([AI SDK][4])
|
|||
|
|
* LangGraph/assistant-ui 官方整合说明与示例:([assistant-ui.com][5], [LangChain Blog][6])
|
|||
|
|
|
|||
|
|
如果你愿意,我也可以给你一份最小可用的 FastAPI 端 `/api/chat` 实现(把 LangGraph 的事件转成 Data Stream 协议 SSE),直接贴到你服务里就能和前端对上。
|
|||
|
|
|
|||
|
|
[1]: https://www.assistant-ui.com/docs?utm_source=chatgpt.com "Getting Started"
|
|||
|
|
[2]: https://www.assistant-ui.com/docs/legacy/styled/Thread "Thread | assistant-ui"
|
|||
|
|
[3]: https://www.assistant-ui.com/docs/api-reference/integrations/react-data-stream?utm_source=chatgpt.com "assistant-ui/react-data-stream"
|
|||
|
|
[4]: https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol "AI SDK UI: Stream Protocols"
|
|||
|
|
[5]: https://www.assistant-ui.com/docs/runtimes/langgraph?utm_source=chatgpt.com "Getting Started"
|
|||
|
|
[6]: https://blog.langchain.dev/assistant-ui/?utm_source=chatgpt.com "Build stateful conversational AI agents with LangGraph and ..."
|