imetting/frontend/src/pages/PromptConfigPage.jsx

258 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useCallback, 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 httpService from '../services/httpService';
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 = useCallback(async () => {
setLoading(true);
try {
const res = await httpService.get(buildApiUrl(API_ENDPOINTS.PROMPTS.CONFIG(taskType)));
setAvailablePrompts(res.data.available_prompts || []);
setSelectedPromptIds(res.data.selected_prompt_ids || []);
} catch {
message.error('加载提示词配置失败');
} finally {
setLoading(false);
}
}, [message, taskType]);
useEffect(() => {
loadConfig();
}, [loadConfig]);
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 httpService.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;