fix: expand playground diagnostics and compose fallback
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user