262 lines
11 KiB
React
262 lines
11 KiB
React
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|||
|
|
import {
|
|||
|
|
App,
|
|||
|
|
Button,
|
|||
|
|
Card,
|
|||
|
|
Checkbox,
|
|||
|
|
Col,
|
|||
|
|
Drawer,
|
|||
|
|
Empty,
|
|||
|
|
Row,
|
|||
|
|
Segmented,
|
|||
|
|
Space,
|
|||
|
|
Tag,
|
|||
|
|
Tabs,
|
|||
|
|
Typography,
|
|||
|
|
} from 'antd';
|
|||
|
|
import {
|
|||
|
|
ArrowDownOutlined,
|
|||
|
|
ArrowUpOutlined,
|
|||
|
|
EyeOutlined,
|
|||
|
|
ReloadOutlined,
|
|||
|
|
SaveOutlined,
|
|||
|
|
StarFilled,
|
|||
|
|
} from '@ant-design/icons';
|
|||
|
|
import apiClient from '../utils/apiClient';
|
|||
|
|
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
|
|||
|
|
import PromptManagementPage from './PromptManagementPage';
|
|||
|
|
import MarkdownRenderer from '../components/MarkdownRenderer';
|
|||
|
|
|
|||
|
|
const { Title, Text, Paragraph } = Typography;
|
|||
|
|
|
|||
|
|
const TASK_TYPES = [
|
|||
|
|
{ label: '会议总结', value: 'MEETING_TASK' },
|
|||
|
|
{ label: '知识库整合', value: 'KNOWLEDGE_TASK' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const TASK_TYPE_OPTIONS = TASK_TYPES.map((item) => ({ label: item.label, value: item.value }));
|
|||
|
|
|
|||
|
|
const PromptConfigPage = ({ user }) => {
|
|||
|
|
const { message } = App.useApp();
|
|||
|
|
const [taskType, setTaskType] = useState('MEETING_TASK');
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [saving, setSaving] = useState(false);
|
|||
|
|
const [availablePrompts, setAvailablePrompts] = useState([]);
|
|||
|
|
const [selectedPromptIds, setSelectedPromptIds] = useState([]);
|
|||
|
|
const [viewingPrompt, setViewingPrompt] = useState(null);
|
|||
|
|
|
|||
|
|
const loadConfig = async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)));
|
|||
|
|
setAvailablePrompts(res.data.available_prompts || []);
|
|||
|
|
setSelectedPromptIds(res.data.selected_prompt_ids || []);
|
|||
|
|
} catch {
|
|||
|
|
message.error('加载提示词配置失败');
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadConfig();
|
|||
|
|
}, [taskType]);
|
|||
|
|
|
|||
|
|
const selectedPromptCards = useMemo(() => {
|
|||
|
|
const map = new Map(availablePrompts.map((item) => [item.id, item]));
|
|||
|
|
return selectedPromptIds.map((id) => map.get(id)).filter(Boolean);
|
|||
|
|
}, [availablePrompts, selectedPromptIds]);
|
|||
|
|
|
|||
|
|
const togglePromptSelected = (promptId, checked) => {
|
|||
|
|
setSelectedPromptIds((prev) => {
|
|||
|
|
if (checked) {
|
|||
|
|
if (prev.includes(promptId)) return prev;
|
|||
|
|
return [...prev, promptId];
|
|||
|
|
}
|
|||
|
|
return prev.filter((id) => id !== promptId);
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const moveSelectedPrompt = (promptId, direction) => {
|
|||
|
|
setSelectedPromptIds((prev) => {
|
|||
|
|
const index = prev.findIndex((id) => id === promptId);
|
|||
|
|
if (index === -1) return prev;
|
|||
|
|
const target = direction === 'up' ? index - 1 : index + 1;
|
|||
|
|
if (target < 0 || target >= prev.length) return prev;
|
|||
|
|
const next = [...prev];
|
|||
|
|
[next[index], next[target]] = [next[target], next[index]];
|
|||
|
|
return next;
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const saveConfig = async () => {
|
|||
|
|
setSaving(true);
|
|||
|
|
try {
|
|||
|
|
const items = selectedPromptIds.map((promptId, index) => ({
|
|||
|
|
prompt_id: promptId,
|
|||
|
|
is_enabled: true,
|
|||
|
|
sort_order: index + 1,
|
|||
|
|
}));
|
|||
|
|
await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)), { items });
|
|||
|
|
message.success('提示词配置已保存');
|
|||
|
|
loadConfig();
|
|||
|
|
} catch (error) {
|
|||
|
|
message.error(error?.response?.data?.detail || '保存失败');
|
|||
|
|
} finally {
|
|||
|
|
setSaving(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<Card className="console-surface" bordered={false} style={{ marginBottom: 16 }}>
|
|||
|
|
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
|
|||
|
|
<div>
|
|||
|
|
<Title level={3} style={{ margin: 0 }}>提示词配置</Title>
|
|||
|
|
<Text type="secondary">从系统提示词和个人提示词中勾选启用,并设置显示顺序。</Text>
|
|||
|
|
</div>
|
|||
|
|
</Space>
|
|||
|
|
</Card>
|
|||
|
|
<Card className="console-surface" bordered={false} bodyStyle={{ padding: 12 }}>
|
|||
|
|
<Tabs
|
|||
|
|
className="console-tabs"
|
|||
|
|
defaultActiveKey="config"
|
|||
|
|
destroyInactiveTabPane
|
|||
|
|
items={[
|
|||
|
|
{
|
|||
|
|
key: 'config',
|
|||
|
|
label: '提示词配置',
|
|||
|
|
children: (
|
|||
|
|
<div className="console-tab-panel">
|
|||
|
|
<div className="console-tab-toolbar">
|
|||
|
|
<Segmented
|
|||
|
|
className="console-segmented prompt-config-segmented"
|
|||
|
|
value={taskType}
|
|||
|
|
onChange={setTaskType}
|
|||
|
|
options={TASK_TYPE_OPTIONS}
|
|||
|
|
/>
|
|||
|
|
<Space>
|
|||
|
|
<Button icon={<ReloadOutlined />} onClick={loadConfig}>刷新</Button>
|
|||
|
|
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={saveConfig}>保存配置</Button>
|
|||
|
|
</Space>
|
|||
|
|
</div>
|
|||
|
|
<Row gutter={16}>
|
|||
|
|
<Col xs={24} xl={14}>
|
|||
|
|
<Card title="全部可用提示词" loading={loading} bordered={false} className="console-card-panel">
|
|||
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|||
|
|
{availablePrompts.length ? availablePrompts.map((item) => {
|
|||
|
|
const isSystem = Number(item.is_system) === 1;
|
|||
|
|
return (
|
|||
|
|
<Card
|
|||
|
|
key={item.id}
|
|||
|
|
size="small"
|
|||
|
|
style={{
|
|||
|
|
borderRadius: 10,
|
|||
|
|
background: isSystem ? '#eff6ff' : '#ffffff',
|
|||
|
|
borderColor: isSystem ? '#93c5fd' : undefined,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|||
|
|
<Space style={{ justifyContent: 'space-between', width: '100%' }}>
|
|||
|
|
<Checkbox
|
|||
|
|
checked={selectedPromptIds.includes(item.id)}
|
|||
|
|
onChange={(e) => togglePromptSelected(item.id, e.target.checked)}
|
|||
|
|
>
|
|||
|
|
<Text strong>{item.name}</Text>
|
|||
|
|
</Checkbox>
|
|||
|
|
<Space>
|
|||
|
|
{isSystem ? <Tag color="blue">系统</Tag> : <Tag>个人</Tag>}
|
|||
|
|
{isSystem && item.is_default ? <Tag color="gold" icon={<StarFilled />}>默认</Tag> : null}
|
|||
|
|
<Button
|
|||
|
|
type="text"
|
|||
|
|
size="small"
|
|||
|
|
icon={<EyeOutlined />}
|
|||
|
|
onClick={() => setViewingPrompt(item)}
|
|||
|
|
/>
|
|||
|
|
</Space>
|
|||
|
|
</Space>
|
|||
|
|
{item.desc && (
|
|||
|
|
<Paragraph type="secondary" ellipsis={{ rows: 2 }} style={{ marginBottom: 0 }}>
|
|||
|
|
{item.desc}
|
|||
|
|
</Paragraph>
|
|||
|
|
)}
|
|||
|
|
</Space>
|
|||
|
|
</Card>
|
|||
|
|
);
|
|||
|
|
}) : <Empty description="暂无可用提示词" />}
|
|||
|
|
</Space>
|
|||
|
|
</Card>
|
|||
|
|
</Col>
|
|||
|
|
<Col xs={24} xl={10}>
|
|||
|
|
<Card title="已启用顺序" loading={loading} bordered={false} className="console-card-panel">
|
|||
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|||
|
|
{selectedPromptCards.length ? selectedPromptCards.map((item, index) => (
|
|||
|
|
<Card key={item.id} size="small" style={{ borderRadius: 10 }}>
|
|||
|
|
<Space style={{ justifyContent: 'space-between', width: '100%' }}>
|
|||
|
|
<Space>
|
|||
|
|
<Tag color="geekblue">#{index + 1}</Tag>
|
|||
|
|
<Text>{item.name}</Text>
|
|||
|
|
</Space>
|
|||
|
|
<Space>
|
|||
|
|
<Button size="small" icon={<ArrowUpOutlined />} onClick={() => moveSelectedPrompt(item.id, 'up')} />
|
|||
|
|
<Button size="small" icon={<ArrowDownOutlined />} onClick={() => moveSelectedPrompt(item.id, 'down')} />
|
|||
|
|
</Space>
|
|||
|
|
</Space>
|
|||
|
|
</Card>
|
|||
|
|
)) : <Empty description="请从左侧勾选提示词" />}
|
|||
|
|
</Space>
|
|||
|
|
</Card>
|
|||
|
|
</Col>
|
|||
|
|
</Row>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'library',
|
|||
|
|
label: '个人提示词仓库',
|
|||
|
|
children: <PromptManagementPage user={user} mode="personal" embedded />,
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
/>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* ── View Drawer (read-only) ── */}
|
|||
|
|
<Drawer
|
|||
|
|
title="查看提示词定义"
|
|||
|
|
placement="right"
|
|||
|
|
width={760}
|
|||
|
|
open={Boolean(viewingPrompt)}
|
|||
|
|
onClose={() => setViewingPrompt(null)}
|
|||
|
|
>
|
|||
|
|
{viewingPrompt && (
|
|||
|
|
<div>
|
|||
|
|
<div style={{ marginBottom: 16 }}>
|
|||
|
|
<Text type="secondary" style={{ fontSize: 12 }}>名称</Text>
|
|||
|
|
<div style={{ fontSize: 16, fontWeight: 600 }}>{viewingPrompt.name}</div>
|
|||
|
|
</div>
|
|||
|
|
<Space style={{ marginBottom: 16 }}>
|
|||
|
|
<Tag color="geekblue">{TASK_TYPES.find((x) => x.value === viewingPrompt.task_type)?.label || viewingPrompt.task_type}</Tag>
|
|||
|
|
{Number(viewingPrompt.is_system) === 1 ? <Tag color="blue">系统</Tag> : <Tag>个人</Tag>}
|
|||
|
|
{Number(viewingPrompt.is_system) === 1 && Number(viewingPrompt.is_default) === 1 && <Tag color="gold" icon={<StarFilled />}>默认</Tag>}
|
|||
|
|
</Space>
|
|||
|
|
{viewingPrompt.desc && (
|
|||
|
|
<div style={{ marginBottom: 16 }}>
|
|||
|
|
<Text type="secondary" style={{ fontSize: 12 }}>描述</Text>
|
|||
|
|
<div style={{ color: 'rgba(0,0,0,0.72)' }}>{viewingPrompt.desc}</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<div>
|
|||
|
|
<Text type="secondary" style={{ fontSize: 12, marginBottom: 8, display: 'block' }}>提示词内容</Text>
|
|||
|
|
<div style={{ padding: 16, border: '1px solid #e5e7eb', borderRadius: 8, background: '#fafbfc', maxHeight: 520, overflowY: 'auto' }}>
|
|||
|
|
<MarkdownRenderer content={viewingPrompt.content || ''} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</Drawer>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default PromptConfigPage;
|