feat(ui): 添加API使用指南页面
- 新增usage页面组件,包含API使用文档和示例代码 - 添加BookOpen图标用于使用指南菜单项 - 在侧边栏导航中增加"使用指南"链接,指向/usage路径 - 集成到路由系统,支持访问/usage路径 - 提供多种编程语言的API调用示例(bash、python、typescript) - 支持流式输出和非流式输出的API调用方式 - 集成OpenAI SDK使用示例
This commit is contained in:
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
Reference in New Issue
Block a user