fix: expand playground diagnostics and compose fallback

This commit is contained in:
linkong
2026-04-09 16:38:55 +08:00
parent 39f90bd575
commit 306ba7f850
9 changed files with 394 additions and 26 deletions

View File

@@ -380,6 +380,23 @@ body {
padding-bottom: 4px;
}
.playground-preset-strip {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 16px;
padding: 12px;
border: 1px solid rgba(148, 163, 184, 0.22);
border-radius: 14px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
}
.playground-preset-strip__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.playground-form__actions {
display: flex;
gap: 12px;
@@ -555,10 +572,23 @@ body {
.playground-result__meta {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
}
.playground-result__meta-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.playground-result__descriptions .ant-descriptions-view {
border-radius: 14px;
overflow: hidden;
}
.playground-result__content,
.playground-result__blocks .ant-card {
border-radius: 14px;
@@ -591,6 +621,13 @@ body {
gap: 12px;
}
.playground-result__blocks-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.playground-result__blocks-scroll {
max-height: 180px;
overflow: auto;
@@ -619,6 +656,12 @@ body {
min-width: 0;
max-width: none;
}
.playground-result__meta,
.playground-result__blocks-head {
align-items: stretch;
flex-direction: column;
}
}

View File

@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react'
import axios from 'axios'
import { SyncOutlined } from '@ant-design/icons'
import { CopyOutlined, ReloadOutlined, SyncOutlined } from '@ant-design/icons'
import {
Alert,
Button,
Card,
Collapse,
Descriptions,
Form,
Input,
Space,
@@ -53,6 +54,51 @@ interface PlaygroundFormValues {
constraints?: string
}
interface PlaygroundPreset {
key: string
label: string
values: PlaygroundFormValues
}
interface AnalysisRunMeta {
requestId: string | null
durationMs: number | null
completedAt: string | null
}
const PLAYGROUND_PRESETS: PlaygroundPreset[] = [
{
key: 'bgp-brief',
label: 'BGP 简报',
values: {
title: 'BGP 告警态势简报',
objective: '总结当前告警的主要风险、优先级与建议动作。',
observations: '出现新的高危告警\n部分观测站最近 24h 事件增多',
constraints: '结论要简洁\n优先给出操作建议',
},
},
{
key: 'datasource-health',
label: '数据源健康',
values: {
title: '采集器健康检查说明',
objective: '判断当前采集器失败是否属于上游接口失效、限流、结构变更或临时波动。',
observations: '最近 3 次任务失败\n部分数据源响应时间抬升\n个别接口返回结构不稳定',
constraints: '区分事实与推断\n先给排障优先级',
},
},
{
key: 'link-smoke',
label: '链路探测',
values: {
title: 'AI 链路探测',
objective: '验证 backend -> aiprovider -> model provider 调用链路是否正常。',
observations: '当前从 Playground 发起测试\n希望确认 provider 配置和返回结构正常',
constraints: '输出简洁\n包含一段明确结论',
},
},
]
function splitLines(value?: string) {
return (value || '')
.split('\n')
@@ -67,6 +113,12 @@ function Playground() {
const [analyzing, setAnalyzing] = useState(false)
const [providerStatus, setProviderStatus] = useState<AIProviderStatus | null>(null)
const [analysis, setAnalysis] = useState<SituationalAnalysisResponse | null>(null)
const [analysisMeta, setAnalysisMeta] = useState<AnalysisRunMeta>({
requestId: null,
durationMs: null,
completedAt: null,
})
const [providerStatusUpdatedAt, setProviderStatusUpdatedAt] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<'request' | 'result'>('request')
const [helpExpanded, setHelpExpanded] = useState(true)
@@ -87,6 +139,7 @@ function Playground() {
const res = await axios.get<AIProviderStatus>(`${API_BASE_URL}/ai/provider/status`)
setProviderStatus(res.data)
sessionStorage.setItem(PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY, JSON.stringify(res.data))
setProviderStatusUpdatedAt(new Date().toLocaleString())
} catch {
messageApi.error('AI Provider 状态获取失败')
} finally {
@@ -99,6 +152,7 @@ function Playground() {
}, [])
const handleAnalyze = async (values: PlaygroundFormValues) => {
const startedAt = performance.now()
setAnalyzing(true)
setActiveTab('result')
try {
@@ -115,6 +169,11 @@ function Playground() {
},
)
setAnalysis(res.data)
setAnalysisMeta({
requestId: typeof res.headers['x-request-id'] === 'string' ? res.headers['x-request-id'] : null,
durationMs: Math.round(performance.now() - startedAt),
completedAt: new Date().toLocaleString(),
})
messageApi.success('分析已完成')
} catch {
messageApi.error('分析失败,请检查 AI Provider 配置或稍后再试')
@@ -124,6 +183,31 @@ function Playground() {
}
}
const handleApplyPreset = (preset: PlaygroundPreset) => {
form.setFieldsValue(preset.values)
setActiveTab('request')
}
const handleCopyResult = async () => {
if (!analysis?.content) return
try {
await navigator.clipboard.writeText(analysis.content)
messageApi.success('结果已复制')
} catch {
messageApi.error('复制失败,请稍后再试')
}
}
const handleCopyRawResponse = async () => {
if (!analysis) return
try {
await navigator.clipboard.writeText(JSON.stringify(analysis.raw_response, null, 2))
messageApi.success('原始响应已复制')
} catch {
messageApi.error('复制失败,请稍后再试')
}
}
return (
<AppLayout>
{contextHolder}
@@ -188,6 +272,10 @@ function Playground() {
{providerStatus.base_url || '-'}
</Text>
</div>
<div className="playground-kv">
<Text type="secondary"></Text>
<Text>{providerStatusUpdatedAt || '-'}</Text>
</div>
</div>
) : (
<Alert type="warning" showIcon message="尚未获取到 AI Provider 状态" />
@@ -223,6 +311,13 @@ function Playground() {
</span>
)}
/>
<Alert
type="success"
showIcon
className="playground-note"
message="推荐使用左侧预设场景快速联调。"
description="先看 Provider 状态,再选择一个预设模版发起请求,最后在结果区检查 request id、thinking blocks 和 raw response。"
/>
</div>
),
},
@@ -241,15 +336,20 @@ function Playground() {
label: '请求',
children: (
<div className="playground-tabpane">
<div className="playground-preset-strip">
<Text type="secondary"></Text>
<div className="playground-preset-strip__actions">
{PLAYGROUND_PRESETS.map((preset) => (
<Button key={preset.key} onClick={() => handleApplyPreset(preset)}>
{preset.label}
</Button>
))}
</div>
</div>
<Form
form={form}
layout="vertical"
initialValues={{
title: 'BGP 告警态势简报',
objective: '总结当前告警的主要风险、优先级与建议动作。',
observations: '出现新的高危告警\n部分观测站最近 24h 事件增多',
constraints: '结论要简洁\n优先给出操作建议',
}}
initialValues={PLAYGROUND_PRESETS[0].values}
onFinish={handleAnalyze}
>
<Form.Item
@@ -284,11 +384,16 @@ function Playground() {
</Button>
<Button
onClick={() => {
form.resetFields()
form.setFieldsValue(PLAYGROUND_PRESETS[0].values)
setAnalysis(null)
setAnalysisMeta({
requestId: null,
durationMs: null,
completedAt: null,
})
}}
>
</Button>
</div>
</Form>
@@ -308,9 +413,30 @@ function Playground() {
) : analysis ? (
<Space direction="vertical" size={16} className="playground-result__stack">
<div className="playground-result__meta">
<Tag color="blue">{analysis.provider}</Tag>
<Tag>{analysis.model}</Tag>
<div className="playground-result__meta-tags">
<Tag color="blue">{analysis.provider}</Tag>
<Tag>{analysis.model}</Tag>
</div>
<Space wrap>
<Button icon={<CopyOutlined />} onClick={() => void handleCopyResult()}>
</Button>
<Button icon={<ReloadOutlined />} onClick={() => setActiveTab('request')}>
</Button>
</Space>
</div>
<Descriptions
size="small"
bordered
className="playground-result__descriptions"
column={1}
items={[
{ key: 'requestId', label: 'Request ID', children: analysisMeta.requestId || '-' },
{ key: 'duration', label: '耗时', children: analysisMeta.durationMs ? `${analysisMeta.durationMs} ms` : '-' },
{ key: 'completedAt', label: '完成时间', children: analysisMeta.completedAt || '-' },
]}
/>
<div className="playground-result__content">
<pre>{analysis.content}</pre>
</div>
@@ -326,6 +452,31 @@ function Playground() {
</div>
</div>
) : null}
{analysis.thinking_blocks.length ? (
<div className="playground-result__blocks">
<Text strong>Thinking Blocks</Text>
<div className="playground-result__blocks-scroll">
{analysis.thinking_blocks.map((block, index) => (
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
<pre>{block}</pre>
</Card>
))}
</div>
</div>
) : null}
<div className="playground-result__blocks">
<div className="playground-result__blocks-head">
<Text strong>Raw Response</Text>
<Button size="small" icon={<CopyOutlined />} onClick={() => void handleCopyRawResponse()}>
JSON
</Button>
</div>
<div className="playground-result__blocks-scroll">
<Card size="small">
<pre>{JSON.stringify(analysis.raw_response, null, 2)}</pre>
</Card>
</div>
</div>
</Space>
) : (
<Alert