258 lines
11 KiB
JavaScript
258 lines
11 KiB
JavaScript
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';
|
||
import ActionButton from '../components/ActionButton';
|
||
|
||
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" variant="borderless" 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" variant="borderless" styles={{ body: { 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} variant="borderless" 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}
|
||
<ActionButton tone="view" variant="iconSm" tooltip="查看模板" 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} variant="borderless" 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;
|