diff --git a/frontend/src/pages/MeetingLive.tsx b/frontend/src/pages/MeetingLive.tsx
index 5e9ee55..954b146 100644
--- a/frontend/src/pages/MeetingLive.tsx
+++ b/frontend/src/pages/MeetingLive.tsx
@@ -1,23 +1,164 @@
-import React, { useState } from 'react';
-import { Card, Form, Input, Button, Upload, message, Progress, Select, Row, Col } from 'antd';
-import { InboxOutlined, AudioOutlined, UserAddOutlined } from '@ant-design/icons';
+import React, { useState, useEffect } from 'react';
+import {
+ Card, Form, Input, Button, Upload, message, Progress, Select, Modal,
+ Typography, Space, DatePicker, Row, Col, Avatar, Radio
+} from 'antd';
+import {
+ AudioOutlined,
+ CloudUploadOutlined,
+ VideoCameraOutlined,
+ CalendarOutlined,
+ TagOutlined,
+ FileTextOutlined,
+ CheckOutlined,
+ UserOutlined,
+ RightOutlined
+} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
+import dayjs from 'dayjs';
import { api } from '../api';
import PageHeader from '../components/PageHeader/PageHeader';
const { Dragger } = Upload;
const { Option } = Select;
+const { Text, Title } = Typography;
+
+const PromptSelector = ({ value, onChange, prompts }: { value?: number, onChange?: (val: number) => void, prompts: any[] }) => {
+ if (!prompts || prompts.length === 0) {
+ return (
+
+ 暂无可用模版
+
+ );
+ }
+
+ return (
+
+ {prompts.map(prompt => {
+ const isSelected = value === prompt.id;
+ return (
+
onChange?.(prompt.id)}
+ style={{
+ padding: '8px 12px',
+ borderRadius: 6,
+ border: isSelected ? '1px solid #2563eb' : '1px solid #e2e8f0',
+ backgroundColor: isSelected ? '#eff6ff' : '#fff',
+ cursor: 'pointer',
+ transition: 'all 0.2s',
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'column',
+ height: 64,
+ justifyContent: 'center'
+ }}
+ >
+
+
+
+ {prompt.name}
+
+
+ {prompt.is_system ? '系统' : '个人'}
+
+
+
+
+ {prompt.description || '暂无描述'}
+
+
+ {isSelected && (
+
+
+
+ )}
+
+ );
+ })}
+
+ );
+};
const MeetingLive: React.FC = () => {
- const [form] = Form.useForm();
+ const [liveForm] = Form.useForm();
+ const [uploadForm] = Form.useForm();
+
+ const [users, setUsers] = useState([]);
+ const [prompts, setPrompts] = useState([]);
+
+ const [isLiveModalVisible, setIsLiveModalVisible] = useState(false);
+ const [isUploadModalVisible, setIsUploadModalVisible] = useState(false);
+
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
+ const [selectedFile, setSelectedFile] = useState(null);
+
const navigate = useNavigate();
- const handleCreate = async (values: any) => {
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const fetchData = async () => {
try {
- const res = await api.createMeeting(values);
+ const [usersData, promptsData] = await Promise.all([
+ api.listUsers(),
+ api.listPrompts({ scope: 'personal' })
+ ]);
+ setUsers(usersData);
+ setPrompts(promptsData.filter((p: any) => p.is_active === true || p.is_active === 1));
+ } catch (error) {
+ console.error('Failed to fetch data:', error);
+ message.error('加载基础数据失败');
+ }
+ };
+
+ const handleCreateLive = async (values: any) => {
+ try {
+ const payload = {
+ title: values.title,
+ participants: values.participants,
+ prompt_id: values.promptTemplateId,
+ tags: values.tags,
+ meeting_time: values.time ? values.time.valueOf() : dayjs().valueOf(),
+ type: 'live'
+ };
+
+ const res = await api.createMeeting(payload);
message.success('会议创建成功');
+ setIsLiveModalVisible(false);
navigate(`/meeting/history/${res.meeting_id}`);
} catch (error) {
console.error(error);
@@ -25,13 +166,40 @@ const MeetingLive: React.FC = () => {
}
};
- const handleUpload = async (file: File) => {
+ const handleCreateUpload = async (values: any) => {
+ if (!selectedFile) {
+ message.error('请先上传录音文件');
+ return;
+ }
+
setUploading(true);
setProgress(0);
+
try {
- // Mock progress since fetch doesn't support it easily without XHR
+ const formData = new FormData();
+ formData.append('file', selectedFile);
+ formData.append('title', values.title);
+ if (values.participants) {
+ // Backend might expect list or comma separated string depending on implementation
+ // Assuming api.ts handles FormData correctly, we append each participant or join them
+ // If backend uses FastAPI List[str] = Form(...), we append multiple times
+ // If backend uses str = Form(...), we join them.
+ // Safer to try JSON string if complex, but standard form data usually repeats keys
+ // For now, let's append as JSON string or multiple keys?
+ // Let's assume the backend parses JSON for complex fields if passed as string
+ // OR we append each one.
+ // Given existing code elsewhere, usually standard form submission.
+ // Let's iterate.
+ values.participants.forEach((p: any) => formData.append('participants', String(p)));
+ }
+ if (values.promptTemplateId) formData.append('prompt_id', String(values.promptTemplateId));
+ if (values.tags) values.tags.forEach((t: string) => formData.append('tags', t));
+ if (values.time) formData.append('meeting_time', String(values.time.valueOf()));
+ formData.append('type', 'upload');
+
+ // Simulated progress for better UX since fetch doesn't support upload progress natively easily
const timer = setInterval(() => {
- setProgress((prev) => {
+ setProgress(prev => {
if (prev >= 90) {
clearInterval(timer);
return 90;
@@ -40,80 +208,372 @@ const MeetingLive: React.FC = () => {
});
}, 500);
- const res = await api.uploadMeetingAudio(file);
+ const res = await api.createMeeting(formData);
+
clearInterval(timer);
setProgress(100);
- message.success('上传成功,开始转译');
- // Assuming upload returns meeting_id or we navigate to list
- if (res && res.meeting_id) {
- navigate(`/meeting/history/${res.meeting_id}`);
- } else {
- navigate('/meeting/history');
- }
+ message.success('上传并创建成功');
+ setIsUploadModalVisible(false);
+ navigate(`/meeting/history/${res.meeting_id}`);
} catch (error) {
console.error(error);
- message.error('上传失败');
+ message.error('上传失败: ' + (error as any).message);
+ } finally {
setUploading(false);
}
- return false; // Prevent default upload behavior
};
- return (
-
-
-
-
-
-
-
- } />
-
-
-
-
-
- } block>
- 开始会议
-
-
-
-
-
+ const CardButton = ({
+ icon,
+ title,
+ description,
+ color,
+ onClick
+ }: {
+ icon: React.ReactNode,
+ title: string,
+ description: string,
+ color: string,
+ onClick: () => void
+ }) => (
+
+
+ {icon}
+
+ {title}
+ {description}
+
+ 立即开始
+
+
+ );
+
+ const renderFormFields = (isUpload: boolean) => (
+
+
+
+
+ 基本信息
+
+
-
-
+
+ }
+ style={{ borderRadius: 12, padding: '10px 16px' }}
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ tagRender={(props) => (
+
+ {props.label}
+ ×
+
+ )}
+ />
+
+
+ {isUpload && (
+
+
+
+ 上传录音 (必选)
+
+
{
+ setSelectedFile(file);
+ const currentTitle = uploadForm.getFieldValue('title');
+ if (!currentTitle) {
+ uploadForm.setFieldsValue({ title: file.name.replace(/\.[^/.]+$/, "") });
+ }
+ return false;
+ }}
showUploadList={false}
accept="audio/*,video/*"
disabled={uploading}
+ style={{
+ padding: 24,
+ background: selectedFile ? '#eff6ff' : '#f8fafc',
+ border: selectedFile ? '2px solid #2563eb' : '2px dashed #cbd5e1',
+ borderRadius: 16
+ }}
>
-
-
-
- 点击或拖拽文件到此区域上传
-
- 支持 MP3, WAV, M4A 等常见音频格式
-
+ {selectedFile ? (
+
+
+
{selectedFile.name}
+
+ {(selectedFile.size / 1024 / 1024).toFixed(2)} MB
+
+
+
+ ) : (
+ <>
+
+
+
+ 点击或拖拽上传音频
+
+ 支持 MP3, WAV, M4A
+
+ >
+ )}
{uploading && (
-
-
- {progress === 100 ? '上传完成,正在处理...' : '上传中...'}
+
+
+ 正在上传...
+ {progress}%
)}
-
+
+ )}
+
+
+
+
+
+ 参会人员
+
+
+
+
+
+
+
+
+ 会议模版
+
+
+
+
+
+
+
+ );
+
+ return (
+
+
+
+
+
+
+ }
+ title="实时会议"
+ description="开始即时会议录音,实时转写并生成会议纪要"
+ color="#1890ff"
+ onClick={() => setIsLiveModalVisible(true)}
+ />
+
+
+ }
+ title="上传录音"
+ description="上传已有的音频/视频文件,AI 自动整理会议内容"
+ color="#fa8c16"
+ onClick={() => setIsUploadModalVisible(true)}
+ />
+
+ {/* Live Meeting Modal */}
+
+
+
+
+
+
+ }
+ open={isLiveModalVisible}
+ onCancel={() => setIsLiveModalVisible(false)}
+ footer={
+
+
+
+
+ }
+ width={800}
+ destroyOnClose
+ centered
+ styles={{ body: { padding: '24px 0 0 0' } }}
+ >
+
+
+
+ {/* Upload Modal */}
+
+
+
+
+
+
上传会议录音
+
支持 MP3, WAV, M4A 等常见音频格式
+
+
+ }
+ open={isUploadModalVisible}
+ onCancel={() => {
+ if (!uploading) setIsUploadModalVisible(false);
+ }}
+ footer={
+
+
+
+
+ }
+ width={800}
+ destroyOnClose
+ centered
+ maskClosable={!uploading}
+ styles={{ body: { padding: '24px 0 0 0' } }}
+ >
+
+
);
};
diff --git a/frontend/src/pages/PromptManage.tsx b/frontend/src/pages/PromptManage.tsx
index 8127a59..bc777a9 100644
--- a/frontend/src/pages/PromptManage.tsx
+++ b/frontend/src/pages/PromptManage.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { Button, Card, Tag, Space, Form, Input, InputNumber, Switch, message, Select, Modal, Tooltip, Divider } from 'antd';
+import { App, Button, Card, Tag, Space, Form, Input, InputNumber, Switch, Select, Modal, Tooltip, Divider } from 'antd';
import {
PlusOutlined,
EditOutlined,
@@ -18,6 +18,7 @@ import ListTable from '../components/ListTable/ListTable';
const { Option } = Select;
const PromptManage: React.FC = () => {
+ const { message, modal } = App.useApp();
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [categories, setCategories] = useState([]);
@@ -63,7 +64,7 @@ const PromptManage: React.FC = () => {
};
const handleDelete = (id: number) => {
- Modal.confirm({
+ modal.confirm({
title: '确认删除',
content: '确定要删除此个人模板吗?',
onOk: async () => {
@@ -74,10 +75,9 @@ const PromptManage: React.FC = () => {
});
};
- const handleTogglePersonalActive = async (e: React.MouseEvent, record: any) => {
- e.stopPropagation();
- await api.updatePromptConfig(record.id, { is_active: !record.is_active });
- message.success(record.is_active ? '已隐藏' : '已显示');
+ const handleTogglePersonalActive = async (checked: boolean, record: any) => {
+ await api.updatePromptConfig(record.id, { is_active: checked });
+ message.success(checked ? '已启用' : '已停用');
fetchPrompts(searchKeyword);
};
@@ -128,20 +128,17 @@ const PromptManage: React.FC = () => {
}
},
{
- title: '总结显示',
+ title: '是否启用',
dataIndex: 'is_active',
key: 'is_active',
width: 100,
render: (active: boolean, record: any) => (
- : }
- onClick={(e) => handleTogglePersonalActive(e, record)}
- style={{ color: active ? '#1890ff' : '#ccc' }}
- >
- {active ? '显示' : '隐藏'}
-
+ handleTogglePersonalActive(checked, record)}
+ onClick={(_, e) => e.stopPropagation()}
+ />
),
},
{