imetting/frontend/src/pages/PromptConfigPage.jsx

258 lines
11 KiB
React
Raw Normal View History

2026-04-08 09:29:06 +00:00
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2026-03-26 06:55:12 +00:00
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';
2026-04-08 11:19:33 +00:00
import httpService from '../services/httpService';
2026-03-26 06:55:12 +00:00
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
import PromptManagementPage from './PromptManagementPage';
import MarkdownRenderer from '../components/MarkdownRenderer';
2026-04-03 16:25:53 +00:00
import ActionButton from '../components/ActionButton';
2026-03-26 06:55:12 +00:00
const { Title, Text, Paragraph } = Typography;
const TASK_TYPES = [
2026-04-03 16:25:53 +00:00
{ label: '总结模版', value: 'MEETING_TASK' },
{ label: '知识库模版', value: 'KNOWLEDGE_TASK' },
2026-03-26 06:55:12 +00:00
];
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);
2026-04-08 09:29:06 +00:00
const loadConfig = useCallback(async () => {
2026-03-26 06:55:12 +00:00
setLoading(true);
try {
2026-04-08 11:19:33 +00:00
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)));
2026-03-26 06:55:12 +00:00
setAvailablePrompts(res.data.available_prompts || []);
setSelectedPromptIds(res.data.selected_prompt_ids || []);
} catch {
message.error('加载提示词配置失败');
} finally {
setLoading(false);
}
2026-04-08 09:29:06 +00:00
}, [message, taskType]);
2026-03-26 06:55:12 +00:00
useEffect(() => {
loadConfig();
2026-04-08 09:29:06 +00:00
}, [loadConfig]);
2026-03-26 06:55:12 +00:00
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,
}));
2026-04-08 11:19:33 +00:00
await httpService.put(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)), { items });
2026-03-26 06:55:12 +00:00
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 }}>
2026-03-26 06:55:12 +00:00
<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 } }}>
2026-03-26 06:55:12 +00:00
<Tabs
className="console-tabs"
defaultActiveKey="config"
destroyInactiveTabPane
items={[
{
key: 'config',
2026-04-08 11:19:33 +00:00
label: '客户端提示词配置',
2026-03-26 06:55:12 +00:00
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">
2026-03-26 06:55:12 +00:00
<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}
2026-04-03 16:25:53 +00:00
<ActionButton tone="view" variant="iconSm" tooltip="查看模板" icon={<EyeOutlined />} onClick={() => setViewingPrompt(item)} />
2026-03-26 06:55:12 +00:00
</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">
2026-03-26 06:55:12 +00:00
<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;