From 4c2629a0bf089e1184097870ac14c73ec0073056 Mon Sep 17 00:00:00 2001 From: Bifang <915779419@qq.com> Date: Mon, 11 May 2026 17:04:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86=E4=BA=86=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../results/json/1778221747643/sub_topic.json | 95 ++-- .../md/1778221747643/meeting_summary.md | 45 +- frontend/assets/app.js | 444 +++++++++++++++++- frontend/assets/styles.css | 33 +- frontend/index.html | 74 +-- template_guides/template1.md | 18 +- 6 files changed, 568 insertions(+), 141 deletions(-) diff --git a/data/results/json/1778221747643/sub_topic.json b/data/results/json/1778221747643/sub_topic.json index 8a60a9a..1b5caa9 100644 --- a/data/results/json/1778221747643/sub_topic.json +++ b/data/results/json/1778221747643/sub_topic.json @@ -1,49 +1,48 @@ + + +```json { - "sub_topics": [ - { - "title": "宽带安装维护与网络指标汇报", - "time_interval": "00:00:01 - 00:02:40", - "overview": "本周汇报宽带上门安装量、维护质量及转化率指标。受天气影响安装量约400户,累计上门超1000户但距3000户目标进度靠后;弱光值降至0.51逼近目标,月度转化率87.35%接近90%,退单率控制在6.53%。" - }, - { - "title": "退单原因分析与PCDN专线治理", - "time_interval": "00:02:41 - 00:05:31", - "overview": "本周剖析宽带退单因素及PCDN专线网络质量恶化应对情况。退单主因为用户不安装及改约,兰单转化率当周56.82%、月度75.29%未达80%目标;PCDN专线因学校出口带宽问题恶化,已上报市公司分析IP并协调建立后台限速机制。" - }, - { - "title": "基站攻坚、专线护航与故障通报", - "time_interval": "00:05:33 - 00:07:56", - "overview": "本周通报基站攻坚、政企专线护航进度及当周故障情况。超频基站故障已处理恢复,5G参数调整试点持续观察;118条专线存量73条已巡检56条,剩余17条确保月底前完成;当周收故障33件,其中9件为光缆等网络问题。" - }, - { - "title": "二级站点拆除与网络质量评估体系", - "time_interval": "00:07:57 - 00:09:35", - "overview": "本周通报当周故障、二级站点拆除进度及新评估体系部署。当周故障中网络相关9件,二级站点剩余75站计划两周内拆除,目标4月15日前全量调整资费;内部已提前收到网络质量综合评估体系文件,下周将分解指标制定目标。" - }, - { - "title": "综合部项目推进与行政事务协调", - "time_interval": "00:09:46 - 00:12:39", - "overview": "本周汇报重点项目跟进、物资采购及渠道产能评估进展。花果山项目无变化,无人机验收本周推动;建委框架清单及投资计划已汇报完成;打印机采购拟采用外派点保障招投标需求;渠道产能评估宣贯会已开,方案本周专题汇报。" - }, - { - "title": "工会经费公示与体育文化节筹备", - "time_interval": "00:12:40 - 00:19:50", - "overview": "本周通报工会经费压降使用方案及第四届体育文化节筹备情况。今年工会经费全面压降,食堂改造及直升机引水方案已获批;体育文化节方阵需25人目前缺编9人,已决议由各部门抽调并于周二至周四排练,参与者享绩效加分。" - }, - { - "title": "招待费管控与区级评优申报", - "time_interval": "00:19:51 - 00:26:58", - "overview": "本周通报2025年招待费预算执行情况及区级评优申报进展。2025年招待费整体超预算14%,综合部超支107%已调整预算,正气部招待需统筹分配;区国资委管辖企业评优已启动,委办与社保局联合经办,需提前对接名额分配以争取优势。" - }, - { - "title": "查企商客业务通报与工作作风整顿", - "time_interval": "00:27:32 - 00:35:21", - "overview": "本周通报查企与商客市场业绩及工作作风纪律要求。查企完成6900万价值提升发展1110户,重点提升低价值宽带;商客收入88.5万环比增1万;领导强调需养成日事日清习惯,针对专线及在线指标需当日量化回复并落实措施。" - }, - { - "title": "市场客服指标复盘与年度考核部署", - "time_interval": "00:35:22 - 00:46:16", - "overview": "本周复盘市场客服指标并部署市公司年度考核工作。市场部需提前谋划二季度声势,本周汇报招聘及渠道进度;满意度暴露正气部管理松懈,80-20指标全市倒数第二需每日通报;考核明确工信部有责及离网率为KPI,要求提前沟通口径强化执行力。" - } - ] -} \ No newline at end of file + "sub_topics": [ + { + "title": "宽带装维进度与九零工程转化退单指标", + "time_interval": "00:00:01 - 00:03:06", + "overview": "会议通报了宽带装维进度,上周上门量达580户,累计超1000户,但距离3000户目标进度偏后。九零工程月度转化率为87.35%,退单率为6.53%,主要受用户原因、天气及改约影响。PCTN蓝单当周转化率为56.82%,月度为75.29%,退单率达44.29%,需持续优化流程。" + }, + { + "title": "PCDN专线学校限速协调与审核流程优化", + "time_interval": "00:03:07 - 00:05:31", + "overview": "针对PCDN专线指标恶化问题,会议指出主要受开学后两所学校IP地址滥用影响,已报市公司分析并拟采取限速措施。同时提出优化审核流程,建议剔除一线审批同意的退单,并将该指标纳入综合运维考核。目前正协调全资建立后台自动限速机制以提升管控效率。" + }, + { + "title": "网络质量攻坚、基站维护与二级站点拆除计划", + "time_interval": "00:05:33 - 00:09:35", + "overview": "网络质量方面,超频基站故障已及时处理恢复,5G基站参数调整试点将观察信号弱化改善效果。二级基站拆除工作进展顺利,剩余75站预计4月15日前全量完成下电与资费调整。此外,118条专线已按计划推进巡检,下周将针对新收到的网络质量综合评估体系分解目标。" + }, + { + "title": "综合部行政事务推进、渠道评估与气象人员管理", + "time_interval": "00:09:46 - 00:12:39", + "overview": "综合部推进了建委清单整理、投资计划汇报及市场终端占比措施制定,并落实了招投标打印设备的保障方案。渠道产能评估已召开宣贯会并梳理方案,预计本周专题汇报。同时强调气象工作人员纪律管理,并指出基金工作清单与宣传工作因兼职原因有所滞后,需加快进度。" + }, + { + "title": "工会经费管控、暖心工程升级与体育文化节筹备", + "time_interval": "00:12:40 - 00:19:50", + "overview": "受经费压降影响,工会将推行节支增效,食堂改造升级及下半年直升机引水方案已获批。全年规划六场大型文体活动,重点筹备第四届体育文化节,当前方阵人员缺口9人,需各部门今日落实并安排下班后排练。参会者可获得绩效加分,以激励积极性。" + }, + { + "title": "主题教育落实、招待费通报与区级奖项申报策略", + "time_interval": "00:19:51 - 00:27:28", + "overview": "主题教育已按要求完成基层学习,2025年招待费超预算14%,综合部超支明显,需统筹对外接待安排。针对河川区担当作为奖项申报,会议建议先向委办及社保局领导摸底意向,若入围希望不大则避免无效投入。目前申报材料已提交区领导,需持续跟进审批进度。" + }, + { + "title": "商客市场复盘、查企进度与管理层执行力要求", + "time_interval": "00:27:29 - 00:35:15", + "overview": "商客市场1月收入88.5万元,同比提升,草街等分局增幅超2万,价值拓展排名第6。查企工作聚焦低价值用户提值与融合套餐推广,本周开展5场活动但签约未达预期。管理层严厉强调工作执行力,要求养成“日事日清”习惯,并限期提交各项指标的具体保障方案。" + }, + { + "title": "满意度考核反思、客服指标管控与年度KPI部署", + "time_interval": "00:35:16 - 00:46:16", + "overview": "针对近期满意度测评排名靠后,会议批评市场部与政企部思想松懈,要求主管亲自抓并落实客户会参与机制。客服端将强化80/20指标管控技巧,每日输出日报,并预警离网率将纳入今年KPI考核。最后强调需提前与市公司沟通年度考核口径,确保公平得分并提升整体执行力。" + } + ] +} +``` \ No newline at end of file diff --git a/data/results/md/1778221747643/meeting_summary.md b/data/results/md/1778221747643/meeting_summary.md index 66ba55e..cf6b4ab 100644 --- a/data/results/md/1778221747643/meeting_summary.md +++ b/data/results/md/1778221747643/meeting_summary.md @@ -2,7 +2,7 @@ # 会议记录 -议 题:合川分公司周例会(2026年第19期) +议 题:合川分公司周例会(2026年第X期) 时 间:2026年5月8日 10:38—10:50 @@ -10,7 +10,7 @@ 主持人:管理员 -参加人:分公司领导、各部门经理、AI云数中心经理 +参加人:分公司领导、各部门负责人及相关岗位人员 议程: @@ -22,34 +22,31 @@ ## 会议内容 -### 一、各部门汇报 - -建维部汇报宽带上门安装量及网络指标,受天气影响安装进度略缓,累计上门超1000户但距3000户目标进度靠后;弱光值降至0.51逼近目标,月度转化率87.35%接近90%,退单率6.53%;PCDN专线因学校出口带宽问题恶化,已上报市公司协调IP分析与后台限速机制;超频基站故障已恢复,118条专线存量巡检有序推进,剩余17条确保月底前完成;二级站点剩余75座计划两周内拆除,新网络质量综合评估体系下周将分解目标。综合部通报无人机验收及建委框架清单已汇报完成,打印机采购拟采用外派点保障招投标,工会经费全面压降,第四届体育文化节方阵缺编9人需各部门抽调,2025年招待费整体超预算14%需统筹管控,区级评优申报需提前对接争取名额。市场部与政企部通报查企完成6900万价值提升发展1110户,商客收入88.5万环比增长,二季度营销活动需提前谋划,满意度及80-20指标全市排名靠后需每日通报,市公司年度考核(工信部有责、离网率)需提前沟通口径并强化执行落实。 +### 一、市场部、政企部、建维部、综合部︱党群纪检部按议程现场按顺序做汇报。综合部按照领导部署通报周例会领导部署工作推进完成情况:各部门按议程现场汇报工作推进情况:建维部通报宽带装维进度(上周上门580户,累计超1000户)、九零工程转化退单指标及二级站点拆除计划;综合部汇报建委清单整理、渠道产能评估、工会经费管控、体育文化节筹备及区级奖项申报策略;市场与政企部复盘商客市场收入与查企进度,并通报满意度测评情况。重点工作按节点推进,部分指标需优化落实。 --- ### 二、部署强调 -#### 分公司领导强调: +#### 分管领导强调: -1. **网络与运维方面:** - - 弱光值与转化率指标需持续压降改善,月度转化率当前87.35%已接近90%目标,需纳入运维阶段性考核跟踪。 - - PCDN专线网络质量恶化主因学校出口带宽,需协调市公司分析IP地址并建立后台自动限速机制,避免被动退单。 - - 二级站点拆除需抢抓进度,剩余75座争取两周内全部完成下电与资费调整,确保4月15日前全量完成。 - - 内部已提前收悉网络质量综合评估体系,下周须针对指标进行分解并制定具体目标。 +1. **工作要求与执行力:** + - 养成“日事日清、日清日结”工作习惯,阶段性汇报进展与困难,严禁工作拖延数月无反馈。 + - 工作安排需第一时间量化回复领导,而非仅依赖周报记录,确保推进闭环。 +2. **指标保障与方案落实:** + - 限期提交专线、商客等核心指标的具体保障措施与责任人,拒绝空谈,确保目标达成。 + - 针对满意度测评靠后问题,批评相关部门思想松懈,主管需亲自抓客户会参与机制,杜绝“亡羊补牢”式管理。 +3. **客服管控与KPI预警:** + - 强化80/20指标管控技巧,市场部需每日输出日报至分管领导。 + - 预警离网率将纳入今年KPI考核,要求摸透市公司考核口径与可控操作空间,提前布局。 -2. **市场与客服方面:** - - 二季度营销活动需提前谋划造势,打破传统淡季思维,重点推进签约、3人融合及AI终端业务,避免业务推进疲软。 - - 满意度及80-20指标目前全市排名靠后,市场部与政企部须吃透市公司考核技巧,每日输出日报通报进度。 - - 正气部满意度管理存在松懈,主管须亲自抓客户会落实,严禁“亡羊补牢”式被动应对,满意度工作须常态化管控。 +--- -3. **综合与行政方面:** - - 工会经费全面压降,需以节电耗用更少的钱办更好的事,食堂改造及直升机引水方案已获批推进,下半年启用节约成本。 - - 第四届体育文化节方阵目前缺编9人,需各部门于本周内抽调人员,周二至周四下班后集中排练,参与者享绩效加分。 - - 2025年招待费整体超预算14%,综合部超支严重已调整预算,其他部门须合理规划时间进度,正气部对外招待需统筹分配客户经理资源。 - - 区级担当作为评优已启动,需提前对接区委办与人力社保局了解名额分配机制,避免盲目申报浪费精力。 +#### 分公司主要领导强调: -4. **考核与作风建设方面:** - - 全体员工须养成“日事日清、日清日结”工作习惯,任务安排须当日量化回复并落实具体措施,严禁拖延数月无进展。 - - 市公司年度考核指标已明确为工信部有责及离网率,各部门须提前沟通考核口径,避免因信息不对称在起跑线落后。 - - 强化执行力建设,管理层至一线员工须明确目标与路径,杜绝“知道怎么做却不去做”的作风问题,确保年度考核稳妥达标。 \ No newline at end of file +1. **执行力与文化塑造:** + - 各级管理人员及一线员工必须具备强执行力,知晓方法却不落实的工作作风不可接受。 + - 各部门需提前摸底市公司年度考核口径与潜在风险,提前沟通争取公平得分,避免起跑线落后。 +2. **协同联动与考核前置:** + - 加强分管领导与牵头部门联动,必要时共同向上沟通,确保考核工作前置。 + - 面对考核指标需主动出击,通过努力改善得分,而非被动等待。 \ No newline at end of file diff --git a/frontend/assets/app.js b/frontend/assets/app.js index 4ca5365..3e0642a 100644 --- a/frontend/assets/app.js +++ b/frontend/assets/app.js @@ -224,7 +224,7 @@ function loadSettingsDraft(cfg) { async function persistSettingsDraft() { const payload = cloneSettingsDraft(state.settingsDraft || {}); if (!payload.api_profiles.length) { - throw new Error("??????????"); + throw new Error("至少保留一个模型配置"); } await api("/api/settings", { method: "PUT", @@ -1133,7 +1133,7 @@ async function reparseTemplateGuideFromModal(userNotes = "") { $("#process-guide-editor").value = result.content || ""; - $("#guide-modal-title").textContent = `???? ? ${templateName}`; + $("#guide-modal-title").textContent = `模板说明 · ${templateName}`; $("#guide-modal-subtitle").textContent = "确认后开始总结,也可以先补充说明或重解析"; setProcessGuideEditMode(false); @@ -1450,6 +1450,416 @@ $("#btn-reparse-guide").addEventListener("click", async () => { openStandaloneReparseModal(); }); +renderMeetingStatus = function(meeting) { + const name = $("#sidebar-meeting-name"); + const meta = $("#sidebar-meeting-meta"); + const tip = $("#selected-meeting-tip"); + const summaryBadge = $("#badge-summary"); + const topicsBadge = $("#badge-topics"); + + if (!meeting) { + name.textContent = "当前会议:未选择"; + meta.textContent = "请从左侧选择一个会议开始处理。"; + tip.textContent = "未选择会议"; + summaryBadge.textContent = "未生成总结"; + topicsBadge.textContent = "未生成主题 JSON"; + summaryBadge.className = "badge muted"; + topicsBadge.className = "badge muted"; + return; + } + + name.textContent = `当前会议:${meeting.name}`; + meta.textContent = + `ID: ${meeting.id} · 导入时间:${meeting.created_at || "未知"} · 原始文件:` + + `${meeting.original_filename || meeting.transcript_filename || "未知"}`; + tip.textContent = `当前处理会议:${meeting.name} (${meeting.id})`; + summaryBadge.textContent = meeting.has_summary ? "已生成总结" : "未生成总结"; + topicsBadge.textContent = meeting.has_topics ? "已生成主题 JSON" : "未生成主题 JSON"; + summaryBadge.className = meeting.has_summary ? "badge" : "badge muted"; + topicsBadge.className = meeting.has_topics ? "badge" : "badge muted"; +}; + +refreshActionButtons = function() { + const canProcess = Boolean(state.meetingId) && !state.processing && !state.guideBusy; + const currentMeeting = meetingById(state.meetingId); + $("#btn-process").disabled = !canProcess; + $("#btn-process").textContent = state.processing + ? "总结中" + : state.guideBusy + ? "等待" + : currentMeeting?.has_summary + ? "重总结" + : "总结"; + + const canEditResult = Boolean(state.meetingId) && !state.processing && !state.guideBusy; + $("#btn-toggle-result-edit").disabled = !canEditResult; + + const resource = state.rightResource; + const canEditSide = Boolean(resource?.editable) && !state.processing && !state.guideBusy; + $("#btn-toggle-side-edit").disabled = !canEditSide; + + updateGuideButton(resource); +}; + +setProcessGuideEditMode = function(editMode) { + state.processGuideEditMode = editMode; + const editor = $("#process-guide-editor"); + const button = $("#btn-process-guide-edit"); + editor.readOnly = !editMode; + editor.classList.toggle("is-editing", editMode); + button.textContent = editMode ? "保存" : "编辑"; +}; + +syncProcessGuideConfirmLabel = function() { + const meeting = meetingById(state.meetingId); + $("#btn-process-guide-confirm").textContent = meeting?.has_summary ? "重总结" : "确认"; +}; + +persistSettingsDraft = async function() { + const payload = cloneSettingsDraft(state.settingsDraft || {}); + if (!payload.api_profiles.length) { + throw new Error("至少保留一个模型配置"); + } + await api("/api/settings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); +}; + +resetReparseModalProgress = function() { + $("#reparse-stream-title").textContent = "正在重新解析模板说明..."; + $("#reparse-stream-content").textContent = ""; +}; + +openStandaloneReparseModal = function() { + $("#reparse-modal-title").textContent = `模板说明补充 · ${state.templateName}`; + $("#reparse-modal-subtitle").textContent = "可为空;会和系统解析模板一起用于重新解析"; + $("#reparse-modal-notes").value = ""; + $("#reparse-modal-form").hidden = false; + $("#reparse-modal-progress").hidden = true; + $("#btn-reparse-modal-cancel").disabled = false; + $("#btn-reparse-modal-confirm").disabled = false; + resetReparseModalProgress(); + openModal("modal-reparse-guide"); + $("#reparse-modal-notes").focus(); +}; + +runStandaloneGuideReparseFlow = async function() { + const templateName = state.templateName; + const userNotes = $("#reparse-modal-notes").value || ""; + + state.guideBusy = true; + setStatus("right", true, "解析中"); + setStandaloneReparseProcessing(true); + resetReparseModalProgress(); + pushStandaloneReparseLine(`模板:${templateName}`); + pushStandaloneReparseLine("已提交重解析请求,正在准备说明..."); + refreshActionButtons(); + + try { + const params = new URLSearchParams(); + if (userNotes.trim()) { + params.set("user_notes", userNotes.trim()); + } + const source = new EventSource( + `/api/templates/${encodeURIComponent(templateName)}/guide/reparse/stream?${params.toString()}`, + ); + + const result = await new Promise((resolve, reject) => { + let contentAcc = ""; + + source.onmessage = (event) => { + if (!event.data) { + return; + } + const payload = JSON.parse(event.data); + if (payload.type === "status") { + $("#reparse-stream-title").textContent = "正在重新解析模板说明..."; + pushStandaloneReparseLine("已连接模型,开始解析..."); + return; + } + if (payload.type === "chunk") { + const chunk = payload.data?.text || ""; + if (chunk) { + contentAcc += chunk; + $("#reparse-stream-content").textContent = contentAcc + .replace(/\r\n/g, "\n") + .split("\n") + .slice(-6) + .join("\n"); + } + return; + } + if (payload.type === "done") { + source.close(); + resolve(payload.data || { name: templateName, content: contentAcc }); + return; + } + if (payload.type === "error") { + source.close(); + reject(new Error(payload.data || "解析失败")); + } + }; + + source.onerror = () => { + source.close(); + reject(new Error("解析连接中断")); + }; + }); + + state.templates = state.templates.map((item) => ( + item.name === templateName ? { ...item, has_guide: true } : item + )); + if (state.rightResource?.name === templateName && state.rightResource.type === "template") { + state.rightResource.hasGuide = true; + } + await refreshRightResourceAfterGuideReparse(templateName, result.content || ""); + closeModal("modal-reparse-guide"); + await openProcessGuideModal(); + $("#process-guide-editor").value = result.content || ""; + } finally { + state.guideBusy = false; + setStatus("right", false, "空闲"); + setStandaloneReparseProcessing(false); + refreshActionButtons(); + } +}; + +resetProcessingStream = function() { + $("#stream-box").style.display = "none"; + $("#stream-title").textContent = ""; + $("#stream-content").textContent = ""; + setStatus("left", false, "空闲"); +}; + +ensureTemplateGuideBeforeProcess = async function() { + const templateMeta = templateMetaByName(state.templateName); + if (templateMeta?.has_guide) { + return; + } + + toast("当前模板还没有解析说明,先为你解析模板。"); + state.guideBusy = true; + setStatus("left", true, "解析中"); + refreshActionButtons(); + + try { + const result = await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide/reparse`, { + method: "POST", + }); + state.templates = state.templates.map((item) => ( + item.name === state.templateName ? { ...item, has_guide: true } : item + )); + if ( + state.rightResource && + state.rightResource.templateName === result.name && + state.rightResource.type === "template" + ) { + state.rightResource.hasGuide = true; + refreshActionButtons(); + } + } finally { + state.guideBusy = false; + setStatus("left", false, "空闲"); + refreshActionButtons(); + } +}; + +loadTemplateGuideForProcess = async function() { + const data = await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide`); + $("#guide-modal-title").textContent = `模板说明 · ${state.templateName}`; + $("#guide-modal-subtitle").textContent = "确认后开始总结,也可以先补充说明或重解析"; + $("#process-guide-editor").value = data.content || ""; + if (!$("#process-extra-notes").dataset.keepValue) { + $("#process-extra-notes").value = ""; + } + setProcessGuideEditMode(false); + syncProcessGuideConfirmLabel(); +}; + +saveProcessGuideFromModal = async function() { + const content = $("#process-guide-editor").value; + await api(`/api/templates/${encodeURIComponent(state.templateName)}/guide`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content }), + }); + + state.templates = state.templates.map((item) => ( + item.name === state.templateName ? { ...item, has_guide: true } : item + )); + + if ( + state.rightResource && + state.rightResource.templateName === state.templateName && + state.rightResource.type === "template-guide" + ) { + state.rightResource.content = content; + } + if ( + state.rightResource && + state.rightResource.name === state.templateName && + state.rightResource.type === "template" + ) { + state.rightResource.hasGuide = true; + } + + setProcessGuideEditMode(false); + refreshActionButtons(); + toast("说明已保存"); +}; + +openProcessGuideModal = async function() { + openModal("modal-process-guide"); + hideReparseGuidePrompt(); + $("#guide-modal-title").textContent = `模板说明 · ${state.templateName}`; + $("#guide-modal-subtitle").textContent = "正在准备模板说明..."; + $("#process-guide-editor").value = "正在加载模板说明,请稍等..."; + $("#process-guide-editor").readOnly = true; + $("#process-guide-editor").classList.remove("is-editing"); + $("#process-extra-notes").dataset.keepValue = "1"; + $("#btn-process-guide-edit").disabled = true; + $("#btn-process-guide-reparse").disabled = true; + $("#btn-process-guide-confirm").disabled = true; + + try { + await ensureTemplateGuideBeforeProcess(); + await loadTemplateGuideForProcess(); + } catch (error) { + closeModal("modal-process-guide"); + throw error; + } finally { + $("#btn-process-guide-edit").disabled = false; + $("#btn-process-guide-reparse").disabled = false; + $("#btn-process-guide-confirm").disabled = false; + delete $("#process-extra-notes").dataset.keepValue; + } +}; + +reparseTemplateGuideFromModal = async function(userNotes = "") { + const templateName = state.templateName; + + state.guideBusy = true; + setStatus("right", true, "解析中"); + $("#btn-process-guide-edit").disabled = true; + $("#btn-process-guide-reparse").disabled = true; + $("#btn-process-guide-confirm").disabled = true; + $("#guide-modal-subtitle").textContent = "正在结合补充说明重新解析模板..."; + refreshActionButtons(); + + try { + if (state.processGuideEditMode) { + await saveProcessGuideFromModal(); + } + + const result = await runTemplateGuideReparse(templateName, userNotes); + $("#process-guide-editor").value = result.content || ""; + $("#guide-modal-title").textContent = `模板说明 · ${templateName}`; + $("#guide-modal-subtitle").textContent = "确认后开始总结,也可以先补充说明或重解析"; + setProcessGuideEditMode(false); + toast(`说明已更新:${templateName}`); + } finally { + state.guideBusy = false; + setStatus("right", false, "空闲"); + $("#btn-process-guide-edit").disabled = false; + $("#btn-process-guide-reparse").disabled = false; + $("#btn-process-guide-confirm").disabled = false; + refreshActionButtons(); + } +}; + +runTemplateGuideReparse = async function(templateName, userNotes = "") { + return streamTemplateGuideReparse(templateName, userNotes, { + onStatus() { + $("#guide-modal-subtitle").textContent = "正在结合补充说明重新解析模板..."; + }, + onChunk(_chunk, contentAcc) { + $("#process-guide-editor").value = contentAcc; + }, + }); +}; + +startMeetingProcess = function(userNotes = "") { + state.processing = true; + setStatus("left", true, "总结中"); + refreshActionButtons(); + showProcessingView(); + $("#stream-box").style.display = "block"; + $("#stream-title").textContent = "第一阶段:结构化主题..."; + $("#stream-content").textContent = ""; + + const params = new URLSearchParams({ template_name: state.templateName }); + if (userNotes.trim()) { + params.set("user_notes", userNotes.trim()); + } + + const source = new EventSource(`/api/meetings/${state.meetingId}/process?${params.toString()}`); + + let resultAcc = ""; + let streamAcc = ""; + + source.onmessage = async (event) => { + if (!event.data) { + return; + } + + const payload = JSON.parse(event.data); + + if (payload.type === "status") { + if (payload.data === "preprocessing") { + $("#stream-title").textContent = "第一阶段:结构化主题..."; + } else if (payload.data === "preprocessing_done") { + $("#stream-title").textContent = "主题提取完成,开始生成会议总结..."; + } else if (payload.data === "summarizing") { + $("#stream-title").textContent = "第二阶段:生成会议总结..."; + streamAcc = ""; + $("#stream-content").textContent = ""; + } + return; + } + + if (payload.type === "chunk") { + const { data } = payload; + streamAcc += data.text || ""; + $("#stream-content").textContent = streamAcc.replace(/\r\n/g, "\n").split("\n").slice(-4).join("\n"); + if (data.stage === 2 && data.chunk_type === "content") { + resultAcc += data.text || ""; + } + return; + } + + if (payload.type === "done") { + source.close(); + state.processing = false; + patchMeeting(state.meetingId, { has_summary: true }); + renderMeetingStatus(meetingById(state.meetingId)); + showResult(payload.data?.result || resultAcc || ""); + toast("会议总结完成"); + refreshActionButtons(); + return; + } + + if (payload.type === "error") { + source.close(); + state.processing = false; + resetProcessingStream(); + $("#processing-indicator").hidden = true; + refreshActionButtons(); + toast(`处理失败:${payload.data}`, "err"); + } + }; + + source.onerror = () => { + source.close(); + state.processing = false; + resetProcessingStream(); + $("#processing-indicator").hidden = true; + refreshActionButtons(); + toast("处理连接中断", "err"); + }; +}; + $("#btn-import").addEventListener("click", () => { $("#import-name").value = ""; $("#import-file").value = ""; @@ -1466,11 +1876,11 @@ $("#btn-confirm-import").addEventListener("click", async () => { const name = $("#import-name").value.trim(); const file = $("#import-file").files[0]; if (!name) { - toast("???????", "err"); + toast("请填写会议名称", "err"); return; } if (!file) { - toast("???????", "err"); + toast("请选择会议文件", "err"); return; } @@ -1481,13 +1891,13 @@ $("#btn-confirm-import").addEventListener("click", async () => { const result = await fetch("/api/meetings/import", { method: "POST", body: formData }); if (!result.ok) { const detail = await result.json().catch(() => ({ detail: "Import failed" })); - toast(`?????${detail.detail}`, "err"); + toast(`导入失败:${detail.detail}`, "err"); return; } const payload = await result.json(); closeModal("modal-import"); - toast(`?????${name}`); + toast(`已导入会议:${name}`); await refresh(); await selectMeeting(payload.id); }); @@ -1496,11 +1906,11 @@ $("#btn-confirm-import-template").addEventListener("click", async () => { const name = $("#import-template-name").value.trim(); const file = $("#import-template-file").files[0]; if (!name) { - toast("???????", "err"); + toast("请填写模板名称", "err"); return; } if (!file) { - toast("???????", "err"); + toast("请选择模板文件", "err"); return; } @@ -1511,13 +1921,13 @@ $("#btn-confirm-import-template").addEventListener("click", async () => { const result = await fetch("/api/templates/import", { method: "POST", body: formData }); if (!result.ok) { const detail = await result.json().catch(() => ({ detail: "Import failed" })); - toast(`?????${detail.detail}`, "err"); + toast(`导入失败:${detail.detail}`, "err"); return; } const payload = await result.json(); closeModal("modal-import-template"); - toast(`??????${payload.name}`); + toast(`已导入模板:${payload.name}`); await refresh(); syncTemplateSelection(payload.name); await openTemplate(payload.name); @@ -1540,7 +1950,7 @@ $("#cfg-key-select").addEventListener("change", async (event) => { renderSettingsKeyOptions(); try { await persistSettingsDraft(); - toast(`??????${$("#cfg-current-model").value}`); + toast(`已切换模型:${$("#cfg-current-model").value}`); } catch (error) { toast(error.message, "err"); } @@ -1553,7 +1963,7 @@ $("#btn-save-settings").addEventListener("click", async () => { const currentName = $("#cfg-key-select").value; const maxTokens = Number($("#cfg-max-tokens").value || 64000); if (!Number.isFinite(maxTokens) || maxTokens < 1) { - toast("max_tokens ????? 0 ???", "err"); + toast("max_tokens 必须大于 0", "err"); return; } state.settingsDraft.api_profiles = state.settingsDraft.api_profiles.map((item) => ( @@ -1562,7 +1972,7 @@ $("#btn-save-settings").addEventListener("click", async () => { try { await persistSettingsDraft(); renderSettingsKeyOptions(); - toast(`??????${$("#cfg-current-model").value}`); + toast(`已保存模型配置:${$("#cfg-current-model").value}`); } catch (error) { toast(error.message, "err"); } @@ -1585,11 +1995,11 @@ $("#btn-confirm-add-model").addEventListener("click", async () => { const apiKey = $("#cfg-add-api-key").value.trim(); const maxTokens = Number($("#cfg-add-max-tokens").value || 64000); if (!modelName || !apiBaseUrl || !apiKey || !Number.isFinite(maxTokens) || maxTokens < 1) { - toast("??????????", "err"); + toast("请完整填写模型配置", "err"); return; } if (state.settingsDraft.api_profiles.some((item) => item.name === modelName)) { - toast("???????", "err"); + toast("模型名称已存在", "err"); return; } @@ -1606,7 +2016,7 @@ $("#btn-confirm-add-model").addEventListener("click", async () => { try { await persistSettingsDraft(); closeModal("modal-add-model"); - toast(`??????${modelName}`); + toast(`已新增模型:${modelName}`); } catch (error) { toast(error.message, "err"); } @@ -1623,7 +2033,7 @@ $("#btn-key-delete").addEventListener("click", async () => { try { await persistSettingsDraft(); - toast(`??????${targetName}`); + toast(`已删除模型:${targetName}`); } catch (error) { toast(error.message, "err"); } diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index 3267b89..8445dc1 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -694,6 +694,9 @@ select.btn { .guide-modal { width: min(760px, 100%); padding: 24px; + display: flex; + flex-direction: column; + gap: 16px; } .guide-modal-head { @@ -701,7 +704,6 @@ select.btn { justify-content: space-between; align-items: flex-start; gap: 16px; - margin-bottom: 16px; } .guide-modal-head h3 { @@ -718,6 +720,7 @@ select.btn { display: grid; gap: 14px; position: relative; + min-height: 0; } .guide-editor { @@ -786,9 +789,10 @@ select.btn { .reparse-modal { width: min(720px, 100%); - min-height: 430px; + min-height: 0; display: flex; flex-direction: column; + gap: 14px; } .reparse-modal-body { @@ -798,19 +802,34 @@ select.btn { } .reparse-modal-textarea { - min-height: 190px; - height: 190px; + min-height: 176px; + height: 176px; margin-top: 0; } .reparse-stream-box { width: 100%; - min-height: 240px; + min-height: 0; } .reparse-stream-content { - min-height: 180px; - max-height: 180px; + min-height: 152px; + max-height: 152px; + overflow: hidden; + scrollbar-width: none; + padding-bottom: 14px; +} + +.reparse-stream-content::-webkit-scrollbar { + display: none; +} + +#modal-process-guide .modal-actions, +#modal-reparse-guide .modal-actions { + justify-content: flex-end; + flex-wrap: wrap; + padding-top: 14px; + border-top: 1px solid rgba(199, 220, 248, 0.9); } .icon-btn { diff --git a/frontend/index.html b/frontend/index.html index b01b046..0e66105 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@