Files
PoRi a15d39187c feat(ui): 添加API使用指南页面
- 新增usage页面组件,包含API使用文档和示例代码
- 添加BookOpen图标用于使用指南菜单项
- 在侧边栏导航中增加"使用指南"链接,指向/usage路径
- 集成到路由系统,支持访问/usage路径
- 提供多种编程语言的API调用示例(bash、python、typescript)
- 支持流式输出和非流式输出的API调用方式
- 集成OpenAI SDK使用示例
2026-04-25 17:40:20 +08:00

430 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react"
import { BookOpen, Copy, Check, Code, Terminal, FileType } from "lucide-react"
import { Card } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Header } from "@/components/layout/header"
import { Main } from "@/components/layout/main"
import { ProfileDropdown } from "@/components/profile-dropdown"
import { ThemeSwitch } from "@/components/theme-switch"
interface CodeBlockProps {
code: string
language?: string
}
function CodeBlock({ code, language = "bash" }: CodeBlockProps) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(code)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
const langIcons: Record<string, React.ReactNode> = {
bash: <Terminal className="w-3.5 h-3.5" />,
python: <FileType className="w-3.5 h-3.5" />,
typescript: <Code className="w-3.5 h-3.5" />,
}
return (
<div className="relative group">
<div className="flex items-center justify-between px-4 py-1.5 bg-muted/50 border-b border-border/50 rounded-t-lg">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
{langIcons[language] || null}
{language}
</div>
<button
onClick={handleCopy}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{copied ? (
<>
<Check className="w-3.5 h-3.5 text-green-500" />
已复制
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
复制
</>
)}
</button>
</div>
<pre className="bg-[#1e1e2e] text-gray-300 p-4 rounded-b-lg overflow-x-auto text-sm leading-relaxed">
<code>{code}</code>
</pre>
</div>
)
}
function ApiSection({
title,
method,
path,
description,
children,
}: {
title: string
method: string
path: string
description: string
children: React.ReactNode
}) {
const methodColors: Record<string, string> = {
GET: "bg-green-500/10 text-green-600 border-green-200 dark:border-green-800 dark:text-green-400",
POST: "bg-blue-500/10 text-blue-600 border-blue-200 dark:border-blue-800 dark:text-blue-400",
}
return (
<div className="space-y-3">
<h3 className="text-base font-semibold">{title}</h3>
<p className="text-sm text-muted-foreground">{description}</p>
<div className="flex items-center gap-2 font-mono text-sm">
<span className={`px-2 py-0.5 rounded border text-xs font-medium ${methodColors[method] || ""}`}>
{method}
</span>
<span className="text-foreground">{path}</span>
</div>
{children}
</div>
)
}
export function UsagePage() {
const [baseUrl, setBaseUrl] = useState("http://127.0.0.1:10100")
return (
<>
<Header>
<div className="flex items-center gap-2 ms-auto">
<ThemeSwitch />
<ProfileDropdown />
</div>
</Header>
<Main>
<div className="mb-6">
<h1 className="text-2xl font-bold tracking-tight flex items-center gap-2">
<BookOpen className="w-6 h-6" />
API 使用指南
</h1>
<p className="text-muted-foreground text-sm mt-1">
如何通过 HTTP 接口调用本地模型推理服务
</p>
</div>
{/* 服务地址配置 */}
<Card className="p-4 mb-6">
<div className="flex items-end gap-4">
<div className="flex-1 space-y-2">
<Label htmlFor="base-url">服务地址</Label>
<Input
id="base-url"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
placeholder="http://127.0.0.1:10100"
/>
</div>
<p className="text-xs text-muted-foreground pb-1">
请确保服务已启动,地址与「启动服务」页面配置一致
</p>
</div>
</Card>
<div className="space-y-8">
{/* 1. 查看可用模型 */}
<Card className="p-5 space-y-4">
<ApiSection
title="查看可用模型"
method="GET"
path="/v1/models"
description="获取当前服务中可用的模型列表"
>
<div className="space-y-3">
<CodeBlock
language="bash"
code={`curl ${baseUrl}/v1/models`}
/>
<CodeBlock
language="python"
code={`import requests
response = requests.get("${baseUrl}/v1/models")
models = response.json()
print(models)`}
/>
<CodeBlock
language="typescript"
code={`const response = await fetch("${baseUrl}/v1/models")
const models = await response.json()
console.log(models)`}
/>
</div>
</ApiSection>
</Card>
{/* 2. Chat Completions */}
<Card className="p-5 space-y-4">
<ApiSection
title="对话补全"
method="POST"
path="/v1/chat/completions"
description="向模型发送对话消息,获取推理回复。兼容 OpenAI API 格式。"
>
<div className="space-y-3">
<CodeBlock
language="bash"
code={`curl ${baseUrl}/v1/chat/completions \\
-H "Content-Type: application/json" \\
-d '{
"model": "your-model-id",
"messages": [
{"role": "system", "content": "你是一个有用的助手"},
{"role": "user", "content": "你好,请介绍一下你自己"}
],
"temperature": 0.7,
"max_tokens": 2048,
"stream": false
}'`}
/>
<CodeBlock
language="python"
code={`import requests
response = requests.post(
"${baseUrl}/v1/chat/completions",
json={
"model": "your-model-id",
"messages": [
{"role": "system", "content": "你是一个有用的助手"},
{"role": "user", "content": "你好,请介绍一下你自己"}
],
"temperature": 0.7,
"max_tokens": 2048,
"stream": False,
},
)
result = response.json()
print(result["choices"][0]["message"]["content"])`}
/>
<CodeBlock
language="typescript"
code={`const response = await fetch("${baseUrl}/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "your-model-id",
messages: [
{ role: "system", content: "你是一个有用的助手" },
{ role: "user", content: "你好,请介绍一下你自己" }
],
temperature: 0.7,
max_tokens: 2048,
stream: false,
}),
})
const result = await response.json()
console.log(result.choices[0].message.content)`}
/>
</div>
<div className="mt-4 p-3 bg-muted/50 rounded-md text-sm space-y-2">
<p className="font-medium">请求参数说明</p>
<table className="w-full text-xs">
<thead>
<tr className="border-b">
<th className="text-left py-1 pr-2">参数</th>
<th className="text-left py-1 pr-2">类型</th>
<th className="text-left py-1">说明</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-muted">
<td className="py-1 pr-2 font-mono">model</td>
<td className="py-1 pr-2">string</td>
<td className="py-1">模型 ID</td>
</tr>
<tr className="border-b border-muted">
<td className="py-1 pr-2 font-mono">messages</td>
<td className="py-1 pr-2">array</td>
<td className="py-1">对话消息列表</td>
</tr>
<tr className="border-b border-muted">
<td className="py-1 pr-2 font-mono">temperature</td>
<td className="py-1 pr-2">number</td>
<td className="py-1">采样温度 (0~2),默认 1.0</td>
</tr>
<tr className="border-b border-muted">
<td className="py-1 pr-2 font-mono">max_tokens</td>
<td className="py-1 pr-2">number</td>
<td className="py-1">最大生成 token 数</td>
</tr>
<tr>
<td className="py-1 pr-2 font-mono">stream</td>
<td className="py-1 pr-2">boolean</td>
<td className="py-1">是否流式输出,默认 false</td>
</tr>
</tbody>
</table>
</div>
</ApiSection>
</Card>
{/* 3. 流式输出 */}
<Card className="p-5 space-y-4">
<ApiSection
title="流式对话"
method="POST"
path="/v1/chat/completions (stream)"
description="使用 SSE (Server-Sent Events) 实现流式输出,逐 token 返回推理结果。"
>
<div className="space-y-3">
<CodeBlock
language="bash"
code={`curl ${baseUrl}/v1/chat/completions \\
-H "Content-Type: application/json" \\
-d '{
"model": "your-model-id",
"messages": [
{"role": "user", "content": "用 Python 写一个递归遍历目录的例子"}
],
"stream": true
}'`}
/>
<CodeBlock
language="python"
code={`import requests
response = requests.post(
"${baseUrl}/v1/chat/completions",
json={
"model": "your-model-id",
"messages": [
{"role": "user", "content": "用 Python 写一个递归遍历目录的例子"}
],
"stream": True,
},
stream=True,
)
for line in response.iter_lines():
if line:
text = line.decode("utf-8").removeprefix("data: ")
if text != "[DONE]":
import json
chunk = json.loads(text)
delta = chunk["choices"][0].get("delta", {}).get("content", "")
print(delta, end="", flush=True)`}
/>
<CodeBlock
language="typescript"
code={`const response = await fetch("${baseUrl}/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "your-model-id",
messages: [
{ role: "user", content: "用 Python 写一个递归遍历目录的例子" }
],
stream: true,
}),
})
const reader = response.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) {
const text = line.replace(/^data: /, "").trim()
if (!text || text === "[DONE]") continue
const chunk = JSON.parse(text)
const content = chunk.choices?.[0]?.delta?.content || ""
process.stdout.write(content)
}
}`}
/>
</div>
</ApiSection>
</Card>
{/* 4. 使用 OpenAI SDK */}
<Card className="p-5 space-y-4">
<h3 className="text-base font-semibold">使用 OpenAI SDK 调用</h3>
<p className="text-sm text-muted-foreground">
由于服务兼容 OpenAI API 格式,你可以直接使用 OpenAI 官方 SDK,只需修改 base URL 即可。
</p>
<div className="space-y-3">
<CodeBlock
language="python"
code={`from openai import OpenAI
client = OpenAI(
base_url="${baseUrl}/v1",
api_key="not-needed", # 本地服务不需要 API Key
)
response = client.chat.completions.create(
model="your-model-id",
messages=[
{"role": "user", "content": "你好"}
],
stream=True,
)
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")`}
/>
<CodeBlock
language="typescript"
code={`import OpenAI from "openai"
const client = new OpenAI({
baseURL: "${baseUrl}/v1",
apiKey: "not-needed",
})
const stream = await client.chat.completions.create({
model: "your-model-id",
messages: [{ role: "user", content: "你好" }],
stream: true,
})
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || ""
process.stdout.write(content)
}`}
/>
</div>
</Card>
{/* 5. 健康检查 */}
<Card className="p-5 space-y-4">
<ApiSection
title="健康检查"
method="GET"
path="/health"
description="检查服务运行状态"
>
<CodeBlock
language="bash"
code={`curl ${baseUrl}/health`}
/>
</ApiSection>
</Card>
</div>
</Main>
</>
)
}