feat(ui): 添加API使用指南页面

- 新增usage页面组件,包含API使用文档和示例代码
- 添加BookOpen图标用于使用指南菜单项
- 在侧边栏导航中增加"使用指南"链接,指向/usage路径
- 集成到路由系统,支持访问/usage路径
- 提供多种编程语言的API调用示例(bash、python、typescript)
- 支持流式输出和非流式输出的API调用方式
- 集成OpenAI SDK使用示例
This commit is contained in:
PoRi
2026-04-25 17:40:20 +08:00
parent b5305dbfb0
commit a15d39187c
5 changed files with 467 additions and 0 deletions
+5
View File
@@ -5,3 +5,8 @@ This template should help get you started developing with Tauri, React and Types
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
ui 使用shadcn-admin ,基本上是直接粘贴过来的,
这个项目只有两个
@@ -1,6 +1,7 @@
import {
Package,
Play,
BookOpen,
Settings,
UserCog,
Wrench,
@@ -37,6 +38,11 @@ export const sidebarData: SidebarData = {
url: '/launch',
icon: Play,
},
{
title: '使用指南',
url: '/usage',
icon: BookOpen,
},
],
},
{
+429
View File
@@ -0,0 +1,429 @@
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>
</>
)
}
+21
View File
@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as AuthenticatedRouteRouteImport } from './routes/_authenticated/route'
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
import { Route as AuthenticatedUsageRouteImport } from './routes/_authenticated/usage'
import { Route as AuthenticatedLaunchRouteImport } from './routes/_authenticated/launch'
import { Route as errors503RouteImport } from './routes/(errors)/503'
import { Route as errors500RouteImport } from './routes/(errors)/500'
@@ -38,6 +39,11 @@ const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
path: '/',
getParentRoute: () => AuthenticatedRouteRoute,
} as any)
const AuthenticatedUsageRoute = AuthenticatedUsageRouteImport.update({
id: '/usage',
path: '/usage',
getParentRoute: () => AuthenticatedRouteRoute,
} as any)
const AuthenticatedLaunchRoute = AuthenticatedLaunchRouteImport.update({
id: '/launch',
path: '/launch',
@@ -144,6 +150,7 @@ export interface FileRoutesByFullPath {
'/500': typeof errors500Route
'/503': typeof errors503Route
'/launch': typeof AuthenticatedLaunchRoute
'/usage': typeof AuthenticatedUsageRoute
'/settings/account': typeof AuthenticatedSettingsAccountRoute
'/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute
'/settings/display': typeof AuthenticatedSettingsDisplayRoute
@@ -162,6 +169,7 @@ export interface FileRoutesByTo {
'/500': typeof errors500Route
'/503': typeof errors503Route
'/launch': typeof AuthenticatedLaunchRoute
'/usage': typeof AuthenticatedUsageRoute
'/': typeof AuthenticatedIndexRoute
'/settings/account': typeof AuthenticatedSettingsAccountRoute
'/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute
@@ -184,6 +192,7 @@ export interface FileRoutesById {
'/(errors)/500': typeof errors500Route
'/(errors)/503': typeof errors503Route
'/_authenticated/launch': typeof AuthenticatedLaunchRoute
'/_authenticated/usage': typeof AuthenticatedUsageRoute
'/_authenticated/': typeof AuthenticatedIndexRoute
'/_authenticated/settings/account': typeof AuthenticatedSettingsAccountRoute
'/_authenticated/settings/appearance': typeof AuthenticatedSettingsAppearanceRoute
@@ -207,6 +216,7 @@ export interface FileRouteTypes {
| '/500'
| '/503'
| '/launch'
| '/usage'
| '/settings/account'
| '/settings/appearance'
| '/settings/display'
@@ -225,6 +235,7 @@ export interface FileRouteTypes {
| '/500'
| '/503'
| '/launch'
| '/usage'
| '/'
| '/settings/account'
| '/settings/appearance'
@@ -246,6 +257,7 @@ export interface FileRouteTypes {
| '/(errors)/500'
| '/(errors)/503'
| '/_authenticated/launch'
| '/_authenticated/usage'
| '/_authenticated/'
| '/_authenticated/settings/account'
| '/_authenticated/settings/appearance'
@@ -284,6 +296,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedIndexRouteImport
parentRoute: typeof AuthenticatedRouteRoute
}
'/_authenticated/usage': {
id: '/_authenticated/usage'
path: '/usage'
fullPath: '/usage'
preLoaderRoute: typeof AuthenticatedUsageRouteImport
parentRoute: typeof AuthenticatedRouteRoute
}
'/_authenticated/launch': {
id: '/_authenticated/launch'
path: '/launch'
@@ -432,12 +451,14 @@ const AuthenticatedSettingsRouteRouteWithChildren =
interface AuthenticatedRouteRouteChildren {
AuthenticatedSettingsRouteRoute: typeof AuthenticatedSettingsRouteRouteWithChildren
AuthenticatedLaunchRoute: typeof AuthenticatedLaunchRoute
AuthenticatedUsageRoute: typeof AuthenticatedUsageRoute
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
}
const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
AuthenticatedSettingsRouteRoute: AuthenticatedSettingsRouteRouteWithChildren,
AuthenticatedLaunchRoute: AuthenticatedLaunchRoute,
AuthenticatedUsageRoute: AuthenticatedUsageRoute,
AuthenticatedIndexRoute: AuthenticatedIndexRoute,
}
@@ -0,0 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { UsagePage } from '@/features/usage'
export const Route = createFileRoute('/_authenticated/usage')({
component: UsagePage,
})