2026-03-26 03:18:44 +00:00
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Alert,
|
|
|
|
|
|
Avatar,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
DatePicker,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Spin,
|
|
|
|
|
|
Statistic,
|
|
|
|
|
|
Switch,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
message,
|
|
|
|
|
|
} from "antd";
|
|
|
|
|
|
import {
|
|
|
|
|
|
AudioOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
LinkOutlined,
|
|
|
|
|
|
MessageOutlined,
|
|
|
|
|
|
QuestionCircleOutlined,
|
|
|
|
|
|
RocketOutlined,
|
|
|
|
|
|
TeamOutlined,
|
|
|
|
|
|
UserOutlined,
|
|
|
|
|
|
} from "@ant-design/icons";
|
|
|
|
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
|
import PageHeader from "../../components/shared/PageHeader";
|
|
|
|
|
|
import { listUsers } from "../../api";
|
|
|
|
|
|
import { getAiModelDefault, getAiModelPage, type AiModelVO } from "../../api/business/aimodel";
|
|
|
|
|
|
import { getHotWordPage, type HotWordVO } from "../../api/business/hotword";
|
|
|
|
|
|
import { getPromptPage, type PromptTemplateVO } from "../../api/business/prompt";
|
2026-04-03 06:38:36 +00:00
|
|
|
|
import { createRealtimeMeeting, type CreateRealtimeMeetingCommand } from "../../api/business/meeting";
|
2026-03-26 03:18:44 +00:00
|
|
|
|
import type { SysUser } from "../../types";
|
|
|
|
|
|
|
|
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
|
|
|
|
|
|
|
type RealtimeMeetingSessionDraft = {
|
|
|
|
|
|
meetingId: number;
|
|
|
|
|
|
meetingTitle: string;
|
|
|
|
|
|
asrModelName: string;
|
|
|
|
|
|
summaryModelName: string;
|
2026-03-30 09:56:30 +00:00
|
|
|
|
asrModelId: number;
|
2026-03-26 03:18:44 +00:00
|
|
|
|
mode: string;
|
2026-03-30 09:56:30 +00:00
|
|
|
|
language: string;
|
2026-03-26 03:18:44 +00:00
|
|
|
|
useSpkId: number;
|
2026-03-30 09:56:30 +00:00
|
|
|
|
enablePunctuation: boolean;
|
|
|
|
|
|
enableItn: boolean;
|
|
|
|
|
|
enableTextRefine: boolean;
|
|
|
|
|
|
saveAudio: boolean;
|
2026-03-26 03:18:44 +00:00
|
|
|
|
hotwords: Array<{ hotword: string; weight: number }>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function resolveWsUrl(model?: AiModelVO | null) {
|
|
|
|
|
|
if (model?.wsUrl) {
|
|
|
|
|
|
return model.wsUrl;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (model?.baseUrl) {
|
|
|
|
|
|
return model.baseUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-30 09:56:30 +00:00
|
|
|
|
function buildRealtimeProxyPreviewUrl() {
|
|
|
|
|
|
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
|
|
|
|
return `${protocol}://${window.location.host}/ws/meeting/realtime`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 03:18:44 +00:00
|
|
|
|
function getSessionKey(meetingId: number) {
|
|
|
|
|
|
return `realtimeMeetingSession:${meetingId}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function RealtimeAsr() {
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
|
|
const [asrModels, setAsrModels] = useState<AiModelVO[]>([]);
|
|
|
|
|
|
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
|
|
|
|
|
const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
|
|
|
|
|
|
const [hotwordList, setHotwordList] = useState<HotWordVO[]>([]);
|
|
|
|
|
|
const [userList, setUserList] = useState<SysUser[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
const watchedAsrModelId = Form.useWatch("asrModelId", form);
|
|
|
|
|
|
const watchedSummaryModelId = Form.useWatch("summaryModelId", form);
|
|
|
|
|
|
const watchedHotWords = Form.useWatch("hotWords", form) || [];
|
|
|
|
|
|
const watchedParticipants = Form.useWatch("participants", form) || [];
|
|
|
|
|
|
const watchedUseSpkId = Form.useWatch("useSpkId", form);
|
|
|
|
|
|
|
|
|
|
|
|
const selectedAsrModel = useMemo(
|
|
|
|
|
|
() => asrModels.find((item) => item.id === watchedAsrModelId) || null,
|
|
|
|
|
|
[asrModels, watchedAsrModelId],
|
|
|
|
|
|
);
|
|
|
|
|
|
const selectedSummaryModel = useMemo(
|
|
|
|
|
|
() => llmModels.find((item) => item.id === watchedSummaryModelId) || null,
|
|
|
|
|
|
[llmModels, watchedSummaryModelId],
|
|
|
|
|
|
);
|
|
|
|
|
|
const selectedHotwordCount = watchedHotWords.length > 0 ? watchedHotWords.length : hotwordList.length;
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadInitialData = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [asrRes, llmRes, promptRes, hotwordRes, users, defaultAsr, defaultLlm] = await Promise.all([
|
|
|
|
|
|
getAiModelPage({ current: 1, size: 100, type: "ASR" }),
|
|
|
|
|
|
getAiModelPage({ current: 1, size: 100, type: "LLM" }),
|
|
|
|
|
|
getPromptPage({ current: 1, size: 100 }),
|
|
|
|
|
|
getHotWordPage({ current: 1, size: 1000 }),
|
|
|
|
|
|
listUsers(),
|
|
|
|
|
|
getAiModelDefault("ASR"),
|
|
|
|
|
|
getAiModelDefault("LLM"),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const activeAsrModels = asrRes.data.data.records.filter((item) => item.status === 1);
|
|
|
|
|
|
const activeLlmModels = llmRes.data.data.records.filter((item) => item.status === 1);
|
|
|
|
|
|
const activePrompts = promptRes.data.data.records.filter((item) => item.status === 1);
|
|
|
|
|
|
const activeHotwords = hotwordRes.data.data.records.filter((item) => item.status === 1);
|
|
|
|
|
|
|
|
|
|
|
|
setAsrModels(activeAsrModels);
|
|
|
|
|
|
setLlmModels(activeLlmModels);
|
|
|
|
|
|
setPrompts(activePrompts);
|
|
|
|
|
|
setHotwordList(activeHotwords);
|
|
|
|
|
|
setUserList(users || []);
|
|
|
|
|
|
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
title: `实时会议 ${dayjs().format("MM-DD HH:mm")}`,
|
|
|
|
|
|
meetingTime: dayjs(),
|
|
|
|
|
|
asrModelId: defaultAsr.data.data?.id,
|
|
|
|
|
|
summaryModelId: defaultLlm.data.data?.id,
|
|
|
|
|
|
promptId: activePrompts[0]?.id,
|
|
|
|
|
|
useSpkId: 1,
|
|
|
|
|
|
mode: "2pass",
|
2026-03-30 09:56:30 +00:00
|
|
|
|
language: "auto",
|
|
|
|
|
|
enablePunctuation: true,
|
|
|
|
|
|
enableItn: true,
|
|
|
|
|
|
enableTextRefine: false,
|
|
|
|
|
|
saveAudio: false,
|
2026-03-26 03:18:44 +00:00
|
|
|
|
});
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error("加载实时会议配置失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
void loadInitialData();
|
|
|
|
|
|
}, [form]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleCreate = async () => {
|
|
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
|
const wsUrl = resolveWsUrl(selectedAsrModel);
|
|
|
|
|
|
if (!wsUrl) {
|
|
|
|
|
|
message.error("当前 ASR 模型没有配置 WebSocket 地址");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const selectedHotwords = (values.hotWords?.length
|
|
|
|
|
|
? hotwordList.filter((item) => values.hotWords.includes(item.word))
|
|
|
|
|
|
: hotwordList
|
|
|
|
|
|
).map((item) => ({
|
|
|
|
|
|
hotword: item.word,
|
|
|
|
|
|
weight: Number(item.weight || 2) / 10,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2026-04-03 06:38:36 +00:00
|
|
|
|
const payload: CreateRealtimeMeetingCommand = {
|
2026-03-26 03:18:44 +00:00
|
|
|
|
...values,
|
|
|
|
|
|
meetingTime: values.meetingTime.format("YYYY-MM-DD HH:mm:ss"),
|
|
|
|
|
|
participants: values.participants?.join(",") || "",
|
|
|
|
|
|
tags: values.tags?.join(",") || "",
|
2026-04-03 06:38:36 +00:00
|
|
|
|
mode: values.mode || "2pass",
|
|
|
|
|
|
language: values.language || "auto",
|
2026-03-26 03:18:44 +00:00
|
|
|
|
useSpkId: values.useSpkId ? 1 : 0,
|
2026-04-03 06:38:36 +00:00
|
|
|
|
enablePunctuation: values.enablePunctuation !== false,
|
|
|
|
|
|
enableItn: values.enableItn !== false,
|
|
|
|
|
|
enableTextRefine: !!values.enableTextRefine,
|
|
|
|
|
|
saveAudio: !!values.saveAudio,
|
2026-03-26 03:18:44 +00:00
|
|
|
|
hotWords: values.hotWords,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const res = await createRealtimeMeeting(payload);
|
|
|
|
|
|
const createdMeeting = res.data.data;
|
|
|
|
|
|
const sessionDraft: RealtimeMeetingSessionDraft = {
|
|
|
|
|
|
meetingId: createdMeeting.id,
|
|
|
|
|
|
meetingTitle: createdMeeting.title,
|
|
|
|
|
|
asrModelName: selectedAsrModel?.modelName || "ASR",
|
|
|
|
|
|
summaryModelName: selectedSummaryModel?.modelName || "LLM",
|
2026-03-30 09:56:30 +00:00
|
|
|
|
asrModelId: selectedAsrModel?.id || values.asrModelId,
|
2026-03-26 03:18:44 +00:00
|
|
|
|
mode: values.mode || "2pass",
|
2026-03-30 09:56:30 +00:00
|
|
|
|
language: values.language || "auto",
|
2026-03-26 03:18:44 +00:00
|
|
|
|
useSpkId: values.useSpkId ? 1 : 0,
|
2026-03-30 09:56:30 +00:00
|
|
|
|
enablePunctuation: values.enablePunctuation !== false,
|
|
|
|
|
|
enableItn: values.enableItn !== false,
|
|
|
|
|
|
enableTextRefine: !!values.enableTextRefine,
|
|
|
|
|
|
saveAudio: !!values.saveAudio,
|
2026-03-26 03:18:44 +00:00
|
|
|
|
hotwords: selectedHotwords,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
sessionStorage.setItem(getSessionKey(createdMeeting.id), JSON.stringify(sessionDraft));
|
|
|
|
|
|
message.success("会议已创建,进入实时识别");
|
|
|
|
|
|
navigate(`/meeting-live-session/${createdMeeting.id}`);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error("创建实时会议失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ height: "100%", display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<PageHeader title="实时识别会议" subtitle="先完成配置,再进入会中识别页面,减少会中页面干扰。" />
|
2026-03-26 03:18:44 +00:00
|
|
|
|
|
|
|
|
|
|
<div style={{ flex: 1, minHeight: 0, overflow: "hidden" }}>
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<Card bordered={false} style={{ borderRadius: 18 }}>
|
|
|
|
|
|
<div style={{ textAlign: "center", padding: "88px 0" }}>
|
|
|
|
|
|
<Spin />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Row gutter={16} style={{ height: "100%" }}>
|
|
|
|
|
|
<Col xs={24} xl={17} style={{ height: "100%" }}>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
bordered={false}
|
|
|
|
|
|
style={{ height: "100%", borderRadius: 18, boxShadow: "0 8px 22px rgba(15,23,42,0.05)" }}
|
|
|
|
|
|
bodyStyle={{ height: "100%", padding: 16, display: "flex", flexDirection: "column" }}
|
|
|
|
|
|
>
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: 12,
|
|
|
|
|
|
padding: 14,
|
|
|
|
|
|
borderRadius: 16,
|
|
|
|
|
|
background: "linear-gradient(135deg, #f8fbff 0%, #eef6ff 58%, #ffffff 100%)",
|
|
|
|
|
|
border: "1px solid #dbeafe",
|
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
<Space direction="vertical" size={6}>
|
|
|
|
|
|
<Space size={10}>
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: 42,
|
|
|
|
|
|
height: 42,
|
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
|
background: "#1677ff",
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
<AudioOutlined />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<Title level={4} style={{ margin: 0 }}>
|
|
|
|
|
|
创建实时会议
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
<Text type="secondary">会前完成配置后再进入会中识别页。</Text>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
<Space wrap size={[8, 8]}>
|
|
|
|
|
|
<Tag color="blue">单页配置</Tag>
|
|
|
|
|
|
<Tag color="cyan">会中专注转写</Tag>
|
|
|
|
|
|
<Tag color="gold">异常关闭自动兜底</Tag>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<Form
|
|
|
|
|
|
form={form}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
initialValues={{
|
|
|
|
|
|
mode: "2pass",
|
|
|
|
|
|
language: "auto",
|
|
|
|
|
|
useSpkId: 1,
|
|
|
|
|
|
enablePunctuation: true,
|
|
|
|
|
|
enableItn: true,
|
|
|
|
|
|
enableTextRefine: false,
|
|
|
|
|
|
saveAudio: false,
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}
|
|
|
|
|
|
>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
<div style={{ flex: 1, minHeight: 0 }}>
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item name="title" label="会议标题" rules={[{ required: true, message: "请输入会议标题" }]}>
|
|
|
|
|
|
<Input placeholder="例如:产品例会实时记录" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="meetingTime"
|
|
|
|
|
|
label="会议时间"
|
|
|
|
|
|
rules={[{ required: true, message: "请选择会议时间" }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<DatePicker showTime style={{ width: "100%" }} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item name="participants" label="参会人员">
|
|
|
|
|
|
<Select mode="multiple" showSearch optionFilterProp="children" placeholder="选择参会人员">
|
|
|
|
|
|
{userList.map((user) => (
|
|
|
|
|
|
<Option key={user.userId} value={user.userId}>
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Avatar size="small" icon={<UserOutlined />} />
|
|
|
|
|
|
{user.displayName || user.username}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item name="tags" label="会议标签">
|
|
|
|
|
|
<Select mode="tags" placeholder="输入标签后回车" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="asrModelId"
|
|
|
|
|
|
label="识别模型 (ASR)"
|
|
|
|
|
|
rules={[{ required: true, message: "请选择 ASR 模型" }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select placeholder="选择实时识别模型">
|
|
|
|
|
|
{asrModels.map((model) => (
|
|
|
|
|
|
<Option key={model.id} value={model.id}>
|
|
|
|
|
|
{model.modelName}
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="summaryModelId"
|
|
|
|
|
|
label="总结模型 (LLM)"
|
|
|
|
|
|
rules={[{ required: true, message: "请选择总结模型" }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select placeholder="选择总结模型">
|
|
|
|
|
|
{llmModels.map((model) => (
|
|
|
|
|
|
<Option key={model.id} value={model.id}>
|
|
|
|
|
|
{model.modelName}
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="promptId"
|
|
|
|
|
|
label="总结模板"
|
|
|
|
|
|
rules={[{ required: true, message: "请选择总结模板" }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select placeholder="选择总结模板">
|
|
|
|
|
|
{prompts.map((prompt) => (
|
|
|
|
|
|
<Option key={prompt.id} value={prompt.id}>
|
|
|
|
|
|
{prompt.templateName}
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="hotWords"
|
|
|
|
|
|
label={
|
|
|
|
|
|
<span>
|
|
|
|
|
|
热词增强{" "}
|
|
|
|
|
|
<Tooltip title="不选择时将带上系统当前启用的热词">
|
|
|
|
|
|
<QuestionCircleOutlined />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select mode="multiple" allowClear placeholder="可选热词">
|
|
|
|
|
|
{hotwordList.map((item) => (
|
|
|
|
|
|
<Option key={item.word} value={item.word}>
|
|
|
|
|
|
{item.word}
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<Form.Item name="language" hidden>
|
|
|
|
|
|
<Input />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="enablePunctuation" hidden valuePropName="checked">
|
|
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="enableItn" hidden valuePropName="checked">
|
|
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="saveAudio" hidden valuePropName="checked">
|
|
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<Row gutter={16} align="middle">
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item name="mode" label="识别模式">
|
|
|
|
|
|
<Select>
|
|
|
|
|
|
<Option value="2pass">2pass</Option>
|
|
|
|
|
|
<Option value="online">online</Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="useSpkId"
|
|
|
|
|
|
label={
|
|
|
|
|
|
<span>
|
|
|
|
|
|
说话人区分
|
|
|
|
|
|
<Tooltip title="开启后会尝试区分不同发言人">
|
|
|
|
|
|
<QuestionCircleOutlined />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
valuePropName="checked"
|
|
|
|
|
|
getValueProps={(value) => ({ checked: value === 1 || value === true })}
|
|
|
|
|
|
normalize={(value) => (value ? 1 : 0)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} md={8}>
|
|
|
|
|
|
<Form.Item label="WebSocket 地址">
|
|
|
|
|
|
<Input value={buildRealtimeProxyPreviewUrl()} prefix={<LinkOutlined />} readOnly />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col xs={24} md={8}>
|
|
|
|
|
|
<Form.Item name="enableTextRefine" label="文本修正" valuePropName="checked">
|
|
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</div>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
</Form>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
|
|
<Col xs={24} xl={7} style={{ height: "100%" }}>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
bordered={false}
|
|
|
|
|
|
style={{ height: "100%", borderRadius: 18, boxShadow: "0 8px 22px rgba(15,23,42,0.05)" }}
|
|
|
|
|
|
bodyStyle={{ height: "100%", padding: 16, display: "flex", flexDirection: "column" }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Row gutter={[12, 12]} style={{ marginBottom: 12 }}>
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Statistic title="参会人数" value={watchedParticipants.length} prefix={<TeamOutlined />} />
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Statistic title="热词数量" value={selectedHotwordCount} prefix={<MessageOutlined />} />
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Statistic
|
|
|
|
|
|
title="说话人区分"
|
|
|
|
|
|
value={watchedUseSpkId ? "开启" : "关闭"}
|
|
|
|
|
|
prefix={<CheckCircleOutlined />}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Statistic
|
|
|
|
|
|
title="识别链路"
|
|
|
|
|
|
value={selectedAsrModel ? "已就绪" : "待配置"}
|
|
|
|
|
|
prefix={<AudioOutlined />}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Space direction="vertical" size={12} style={{ width: "100%", flex: 1, minHeight: 0 }}>
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<div>
|
|
|
|
|
|
<Text strong style={{ fontSize: 15 }}>
|
|
|
|
|
|
本次识别摘要
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ padding: 14, borderRadius: 14, background: "#fafcff", border: "1px solid #edf2ff" }}>
|
|
|
|
|
|
<Space direction="vertical" size={8} style={{ width: "100%" }}>
|
|
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
|
|
|
|
<Text type="secondary">ASR</Text>
|
|
|
|
|
|
<Text strong>{selectedAsrModel?.modelName || "-"}</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
|
|
|
|
<Text type="secondary">LLM</Text>
|
|
|
|
|
|
<Text strong>{selectedSummaryModel?.modelName || "-"}</Text>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
</div>
|
2026-03-30 09:56:30 +00:00
|
|
|
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
|
|
|
|
<Text type="secondary">WebSocket</Text>
|
|
|
|
|
|
<Text ellipsis style={{ maxWidth: 220 }}>
|
|
|
|
|
|
{buildRealtimeProxyPreviewUrl()}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
message="异常关闭保护"
|
|
|
|
|
|
description="会中页持续写库,并在关闭时自动兜底结束。"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div style={{ marginTop: "auto", padding: 12, borderRadius: 14, background: "#f6ffed", border: "1px solid #b7eb8f" }}>
|
|
|
|
|
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 10 }}>
|
|
|
|
|
|
<Text type="secondary">创建成功后会直接进入识别页,不会在当前页占用麦克风。</Text>
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button onClick={() => navigate("/meetings")}>返回会议中心</Button>
|
|
|
|
|
|
<Button type="primary" icon={<RocketOutlined />} loading={submitting} onClick={() => void handleCreate()}>
|
|
|
|
|
|
创建并进入识别
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
</div>
|
2026-03-30 09:56:30 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
2026-03-26 03:18:44 +00:00
|
|
|
|
</Card>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|