Files
planet/frontend/src/pages/Playground/Playground.tsx

351 lines
13 KiB
TypeScript
Raw 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 { useEffect, useState } from 'react'
import axios from 'axios'
import { SyncOutlined } from '@ant-design/icons'
import {
Alert,
Button,
Card,
Collapse,
Form,
Input,
Space,
Spin,
Tabs,
Tag,
Typography,
message,
} from 'antd'
import AppLayout from '../../components/AppLayout/AppLayout'
const { Title, Text } = Typography
const API_BASE_URL = (import.meta as any).env?.VITE_API_URL || '/api/v1'
const PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY = 'playground-provider-status'
interface AIProviderStatus {
provider: string
api?: string | null
enabled: boolean
configured: boolean
model?: string | null
base_url?: string | null
}
interface AIContentBlock {
type: string
text?: string | null
thinking?: string | null
}
interface SituationalAnalysisResponse {
provider: string
model: string
content: string
content_blocks: AIContentBlock[]
text_blocks: string[]
thinking_blocks: string[]
raw_response: Record<string, unknown>
}
interface PlaygroundFormValues {
title: string
objective: string
observations?: string
constraints?: string
}
function splitLines(value?: string) {
return (value || '')
.split('\n')
.map((item) => item.trim())
.filter(Boolean)
}
function Playground() {
const [form] = Form.useForm<PlaygroundFormValues>()
const [messageApi, contextHolder] = message.useMessage()
const [statusLoading, setStatusLoading] = useState(false)
const [analyzing, setAnalyzing] = useState(false)
const [providerStatus, setProviderStatus] = useState<AIProviderStatus | null>(null)
const [analysis, setAnalysis] = useState<SituationalAnalysisResponse | null>(null)
const [activeTab, setActiveTab] = useState<'request' | 'result'>('request')
const [helpExpanded, setHelpExpanded] = useState(true)
const loadProviderStatus = async (force = false) => {
if (!force) {
const cached = sessionStorage.getItem(PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY)
if (cached) {
try {
setProviderStatus(JSON.parse(cached) as AIProviderStatus)
return
} catch {
sessionStorage.removeItem(PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY)
}
}
}
setStatusLoading(true)
try {
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))
} catch {
messageApi.error('AI Provider 状态获取失败')
} finally {
setStatusLoading(false)
}
}
useEffect(() => {
void loadProviderStatus()
}, [])
const handleAnalyze = async (values: PlaygroundFormValues) => {
setAnalyzing(true)
setActiveTab('result')
try {
const res = await axios.post<SituationalAnalysisResponse>(
`${API_BASE_URL}/ai/situational-awareness/analyze`,
{
title: values.title.trim(),
objective: values.objective.trim(),
observations: splitLines(values.observations),
constraints: splitLines(values.constraints),
context: {
source: 'playground',
},
},
)
setAnalysis(res.data)
messageApi.success('分析已完成')
} catch {
messageApi.error('分析失败,请检查 AI Provider 配置或稍后再试')
setActiveTab('request')
} finally {
setAnalyzing(false)
}
}
return (
<AppLayout>
{contextHolder}
<div className="page-shell playground-page">
<div className="page-shell__header playground-page__header">
<div>
<Title level={3} style={{ marginBottom: 4 }}>AI Playground</Title>
<Text type="secondary">
AI AI Provider
</Text>
</div>
</div>
<div className="page-shell__body playground-page__body">
<div className="playground-shell">
<div className="playground-shell__sidebar">
<Card
className="playground-card playground-card--provider"
title="Provider 状态"
extra={(
<Button
type="text"
shape="circle"
icon={<SyncOutlined spin={statusLoading} />}
onClick={() => void loadProviderStatus(true)}
aria-label="刷新 Provider 状态"
className="playground-card__icon-button"
/>
)}
>
<div className="playground-card__scroll">
<div className="playground-panel__section">
<Spin spinning={statusLoading}>
{providerStatus ? (
<div className="playground-provider-panel">
<div className="playground-kv">
<Text type="secondary">Provider</Text>
<Text strong>{providerStatus.provider || '-'}</Text>
</div>
<div className="playground-kv">
<Text type="secondary"></Text>
<Text strong>{providerStatus.model || '-'}</Text>
</div>
<div className="playground-kv">
<Text type="secondary">API</Text>
<Text code>{providerStatus.api || '-'}</Text>
</div>
<div className="playground-kv">
<Text type="secondary"></Text>
<div className="playground-status-tags">
<Tag color={providerStatus.enabled ? 'green' : 'default'}>
{providerStatus.enabled ? 'enabled' : 'disabled'}
</Tag>
<Tag color={providerStatus.configured ? 'blue' : 'volcano'}>
{providerStatus.configured ? 'configured' : 'not configured'}
</Tag>
</div>
</div>
<div className="playground-kv playground-kv--top">
<Text type="secondary">Base URL</Text>
<Text code className="playground-code-block">
{providerStatus.base_url || '-'}
</Text>
</div>
</div>
) : (
<Alert type="warning" showIcon message="尚未获取到 AI Provider 状态" />
)}
</Spin>
</div>
</div>
</Card>
<Collapse
className={`playground-help${helpExpanded ? ' playground-help--expanded' : ' playground-help--collapsed'}`}
defaultActiveKey={['help']}
onChange={(keys) => setHelpExpanded(Array.isArray(keys) ? keys.includes('help') : keys === 'help')}
items={[
{
key: 'help',
label: '测试说明',
children: (
<Alert
type="info"
showIcon
className="playground-note"
message="用于验证 AI Provider 是否可用,以及 backend -> aiprovider 链路是否通畅。"
description={(
<span>
<Text code>frontend /playground</Text>
{' -> '}
<Text code>backend /api/v1/ai/*</Text>
{' -> '}
<Text code>aiprovider /v1/*</Text>
</span>
)}
/>
),
},
]}
/>
</div>
<Card className="playground-card playground-card--workspace">
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as 'request' | 'result')}
className="playground-tabs"
items={[
{
key: 'request',
label: '请求',
children: (
<div className="playground-tabpane">
<Form
form={form}
layout="vertical"
initialValues={{
title: 'BGP 告警态势简报',
objective: '总结当前告警的主要风险、优先级与建议动作。',
observations: '出现新的高危告警\n部分观测站最近 24h 事件增多',
constraints: '结论要简洁\n优先给出操作建议',
}}
onFinish={handleAnalyze}
>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input maxLength={200} placeholder="例如BGP 告警态势简报" />
</Form.Item>
<Form.Item
label="目标"
name="objective"
rules={[{ required: true, message: '请输入分析目标' }]}
>
<Input.TextArea rows={4} maxLength={1000} placeholder="希望模型帮助完成什么分析" />
</Form.Item>
<div className="playground-form__grid playground-form__grid--bottom">
<Form.Item label="观察项" name="observations">
<Input.TextArea rows={6} placeholder="每行一条观察,例如:某观测站事件显著增加" />
</Form.Item>
<Form.Item label="约束条件" name="constraints">
<Input.TextArea rows={6} placeholder="每行一条限制,例如:回答保持简洁" />
</Form.Item>
</div>
<div className="playground-form__actions">
<Button type="primary" htmlType="submit" loading={analyzing}>
</Button>
<Button
onClick={() => {
form.resetFields()
setAnalysis(null)
}}
>
</Button>
</div>
</Form>
</div>
),
},
{
key: 'result',
label: '结果',
children: (
<div className="playground-tabpane">
{analyzing ? (
<div className="playground-result__loading">
<Spin />
<Text type="secondary">AI ...</Text>
</div>
) : 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>
<div className="playground-result__content">
<pre>{analysis.content}</pre>
</div>
{analysis.text_blocks.length ? (
<div className="playground-result__blocks">
<Text strong></Text>
<div className="playground-result__blocks-scroll">
{analysis.text_blocks.map((block, index) => (
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
<pre>{block}</pre>
</Card>
))}
</div>
</div>
) : null}
</Space>
) : (
<Alert
type="info"
showIcon
message="这里会显示 AI 返回的分析结果"
description="先切到“请求”填写内容并点击“开始分析”。"
/>
)}
</div>
),
},
]}
/>
</Card>
</div>
</div>
</div>
</AppLayout>
)
}
export default Playground