EOT Studio 桌面布局独立滚动、通话列表自动定位及标注修正

This commit is contained in:
Eric Wang
2026-10-14 11:31:30 +08:00
parent 2219a11f44
commit 7660d16bd2
5 changed files with 20 additions and 63 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ a266b9ef-5e47-4434-a04c-ce4eacd52c19,51080,迈腾2023款330豪华版2.0T。出
a266b9ef-5e47-4434-a04c-ce4eacd52c19,68313,轮胎质量问题规律,排水槽横向竖向龟裂。,0,low,mid_turn_pause,xlsx_draft a266b9ef-5e47-4434-a04c-ce4eacd52c19,68313,轮胎质量问题规律,排水槽横向竖向龟裂。,0,low,mid_turn_pause,xlsx_draft
cb3238dc-3946-42a5-aad8-9b9a54226363,13540,辽宁葫芦岛。,1,low,completed_answer,xlsx_draft cb3238dc-3946-42a5-aad8-9b9a54226363,13540,辽宁葫芦岛。,1,low,completed_answer,xlsx_draft
cb3238dc-3946-42a5-aad8-9b9a54226363,27520,投诉建议。,1,low,completed_answer,xlsx_draft cb3238dc-3946-42a5-aad8-9b9a54226363,27520,投诉建议。,1,low,completed_answer,xlsx_draft
cb3238dc-3946-42a5-aad8-9b9a54226363,60790,我的轮胎行驶28000公里。,0,low,unfinished_expression,xlsx_draft cb3238dc-3946-42a5-aad8-9b9a54226363,60790,我的轮胎行驶28000,0,low,unfinished_expression,xlsx_draft
cb3238dc-3946-42a5-aad8-9b9a54226363,70745,轮胎开裂,0,low,mid_turn_pause,xlsx_draft cb3238dc-3946-42a5-aad8-9b9a54226363,70745,轮胎开裂,0,low,mid_turn_pause,xlsx_draft
e304f740-85d4-427e-9fd6-c5474e9cd737,18842,苏州昆山。,1,low,completed_answer,xlsx_draft e304f740-85d4-427e-9fd6-c5474e9cd737,18842,苏州昆山。,1,low,completed_answer,xlsx_draft
e304f740-85d4-427e-9fd6-c5474e9cd737,29619,投诉。,1,low,completed_answer,xlsx_draft e304f740-85d4-427e-9fd6-c5474e9cd737,29619,投诉。,1,low,completed_answer,xlsx_draft
@@ -30,7 +30,7 @@ e304f740-85d4-427e-9fd6-c5474e9cd737,54600,24年11月份购买的帕萨特,150
e304f740-85d4-427e-9fd6-c5474e9cd737,65819,四条轮胎,最近我在。,0,low,unfinished_expression,xlsx_draft e304f740-85d4-427e-9fd6-c5474e9cd737,65819,四条轮胎,最近我在。,0,low,unfinished_expression,xlsx_draft
872a1e67-873a-4c49-a598-b38a670afcad,47187,帕萨特L2025款 ,0,,mid_turn_pause, 872a1e67-873a-4c49-a598-b38a670afcad,47187,帕萨特L2025款 ,0,,mid_turn_pause,
872a1e67-873a-4c49-a598-b38a670afcad,52326,我的轮胎开裂了。,0,,mid_turn_pause, 872a1e67-873a-4c49-a598-b38a670afcad,52326,我的轮胎开裂了。,0,,mid_turn_pause,
40ae686c-8196-4d7b-b7ad-3e12a427ebb6,48348,爱车型号是大众帕萨特,购车时间2023年10月份,0,,mid_turn_pause, 40ae686c-8196-4d7b-b7ad-3e12a427ebb6,48536,爱车型号是大众帕萨特,购车时间2023年10月份,0,,mid_turn_pause,
872a1e67-873a-4c49-a598-b38a670afcad,44810,帕萨特,0,,mid_turn_pause, 872a1e67-873a-4c49-a598-b38a670afcad,44810,帕萨特,0,,mid_turn_pause,
872a1e67-873a-4c49-a598-b38a670afcad,49500,帕萨特L2025款2025年3月购车,1,,completed_turn, 872a1e67-873a-4c49-a598-b38a670afcad,49500,帕萨特L2025款2025年3月购车,1,,completed_turn,
872a1e67-873a-4c49-a598-b38a670afcad,54318,行驶里程29000公里。,1,,completed_turn, 872a1e67-873a-4c49-a598-b38a670afcad,54318,行驶里程29000公里。,1,,completed_turn,
1 call_id decision_ms user_text_at_decision can_reply_now confidence reason annotator
21 a266b9ef-5e47-4434-a04c-ce4eacd52c19 68313 轮胎质量问题规律,排水槽横向竖向龟裂。 0 low mid_turn_pause xlsx_draft
22 cb3238dc-3946-42a5-aad8-9b9a54226363 13540 辽宁葫芦岛。 1 low completed_answer xlsx_draft
23 cb3238dc-3946-42a5-aad8-9b9a54226363 27520 投诉建议。 1 low completed_answer xlsx_draft
24 cb3238dc-3946-42a5-aad8-9b9a54226363 60790 我的轮胎行驶28000公里。 我的轮胎行驶28000 0 low unfinished_expression xlsx_draft
25 cb3238dc-3946-42a5-aad8-9b9a54226363 70745 轮胎开裂 0 low mid_turn_pause xlsx_draft
26 e304f740-85d4-427e-9fd6-c5474e9cd737 18842 苏州昆山。 1 low completed_answer xlsx_draft
27 e304f740-85d4-427e-9fd6-c5474e9cd737 29619 投诉。 1 low completed_answer xlsx_draft
30 e304f740-85d4-427e-9fd6-c5474e9cd737 65819 四条轮胎,最近我在。 0 low unfinished_expression xlsx_draft
31 872a1e67-873a-4c49-a598-b38a670afcad 47187 帕萨特L2025款 0 mid_turn_pause
32 872a1e67-873a-4c49-a598-b38a670afcad 52326 我的轮胎开裂了。 0 mid_turn_pause
33 40ae686c-8196-4d7b-b7ad-3e12a427ebb6 48348 48536 爱车型号是大众帕萨特,购车时间2023年10月份 0 mid_turn_pause
34 872a1e67-873a-4c49-a598-b38a670afcad 44810 帕萨特 0 mid_turn_pause
35 872a1e67-873a-4c49-a598-b38a670afcad 49500 帕萨特L2025款2025年3月购车 1 completed_turn
36 872a1e67-873a-4c49-a598-b38a670afcad 54318 行驶里程29000公里。 1 completed_turn
+12
View File
@@ -293,6 +293,18 @@ kbd { min-width: 17px; padding: 0 4px; border: 1px solid #dce5ce; border-bottom-
.player-secondary { gap: 7px; } .player-secondary { gap: 7px; }
} }
/* Desktop panes scroll independently: the left call list, the middle
workspace and the right editor each keep their own scrollbar. */
@media (min-width: 901px) {
.app-shell { height: 100vh; height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
.workspace-grid { flex: 1; min-height: 0; }
.sidebar { min-height: 0; overflow: hidden; }
.call-list { flex: 1; min-height: 0; overflow-y: auto; margin-bottom: 14px; scrollbar-width: thin; scrollbar-color: #d3ddc6 transparent; }
.main-workspace { min-height: 0; overflow-y: auto; }
.annotation-sidebar { min-height: 0; overflow: hidden; }
.editor-body, .empty-editor { flex: 1; min-height: 0; overflow-y: auto; }
}
@media (max-width: 1020px) { @media (max-width: 1020px) {
.workspace-grid { grid-template-columns: 210px minmax(0, 1fr); } .workspace-grid { grid-template-columns: 210px minmax(0, 1fr); }
.sidebar { grid-row: span 2; } .sidebar { grid-row: span 2; }
+3 -1
View File
@@ -69,6 +69,7 @@ export default function Workbench() {
const objectUrls = useRef(new Set<string>()); const objectUrls = useRef(new Set<string>());
const versionRef = useRef(0); const versionRef = useRef(0);
const originalGroupsRef = useRef(new Map<string, string>()); const originalGroupsRef = useRef(new Map<string, string>());
const callListRef = useRef<HTMLDivElement>(null);
const notify = useCallback((message: string, kind: Notice["kind"] = "success") => setNotice({ message, kind }), []); const notify = useCallback((message: string, kind: Notice["kind"] = "success") => setNotice({ message, kind }), []);
const commit = useCallback((next: Dataset | ((current: Dataset) => Dataset)) => { const commit = useCallback((next: Dataset | ((current: Dataset) => Dataset)) => {
@@ -109,6 +110,7 @@ export default function Workbench() {
finally { setLoading(false); } finally { setLoading(false); }
}, [notify]); }, [notify]);
useEffect(() => { void loadWorkspace(); }, [loadWorkspace]); useEffect(() => { void loadWorkspace(); }, [loadWorkspace]);
useEffect(() => { callListRef.current?.querySelector(".call-item.selected")?.scrollIntoView({ block: "nearest" }); }, [callId]);
useEffect(() => { useEffect(() => {
if (!initialized || !dirty) return; if (!initialized || !dirty) return;
const timer = setTimeout(() => { try { localStorage.setItem(DRAFT_KEY, JSON.stringify({ transcripts, annotations, updatedAt: Date.now() })); } catch { notify("浏览器草稿空间不足,请保存到项目或导出 CSV", "error"); } }, 350); const timer = setTimeout(() => { try { localStorage.setItem(DRAFT_KEY, JSON.stringify({ transcripts, annotations, updatedAt: Date.now() })); } catch { notify("浏览器草稿空间不足,请保存到项目或导出 CSV", "error"); } }, 350);
@@ -350,7 +352,7 @@ export default function Workbench() {
</div></header> </div></header>
<div className="workspace-grid"><aside className="sidebar"><div className="sidebar-heading"><span className="eyebrow">对话库</span><span className="count-badge">{scopedCalls.length}</span></div><label className="dataset-scope"><span>数据集</span><select aria-label="选择数据集" value={datasetScope} disabled={loading} onChange={event => changeDatasetScope(event.target.value)}>{datasetGroups.map(group => <option key={group} value={group}>{group}</option>)}<option value={ALL_DATASETS}>全部数据</option></select><small>导出可选当前数据集,项目保存保留全部</small></label><label className="search-box"><Icon name="search" size={16} /><input aria-label="搜索通话" value={search} onChange={event => setSearch(event.target.value)} placeholder="搜索名称或通话 ID" /></label><div className="sidebar-tabs"><button className={filter === "all" ? "active" : ""} onClick={() => setFilter("all")}>全部对话</button><button className={filter === "pending" ? "active" : ""} onClick={() => setFilter("pending")}>待处理</button></div> <div className="workspace-grid"><aside className="sidebar"><div className="sidebar-heading"><span className="eyebrow">对话库</span><span className="count-badge">{scopedCalls.length}</span></div><label className="dataset-scope"><span>数据集</span><select aria-label="选择数据集" value={datasetScope} disabled={loading} onChange={event => changeDatasetScope(event.target.value)}>{datasetGroups.map(group => <option key={group} value={group}>{group}</option>)}<option value={ALL_DATASETS}>全部数据</option></select><small>导出可选当前数据集,项目保存保留全部</small></label><label className="search-box"><Icon name="search" size={16} /><input aria-label="搜索通话" value={search} onChange={event => setSearch(event.target.value)} placeholder="搜索名称或通话 ID" /></label><div className="sidebar-tabs"><button className={filter === "all" ? "active" : ""} onClick={() => setFilter("all")}>全部对话</button><button className={filter === "pending" ? "active" : ""} onClick={() => setFilter("pending")}>待处理</button></div>
<div className="call-list">{loading ? <p className="muted loading-label">正在读取工作空间…</p> : visibleCalls.length ? visibleCalls.map(row => <button key={row.id} className={`call-item ${callId === row.id ? "selected" : ""}`} onClick={() => chooseCall(row.id)} title={row.id}><span className="call-icon"><Icon name={row.recording ? "headphones" : "file"} size={17} /></span><span className="call-item-copy"><strong>{row.name}</strong><small>{row.id === "demo" ? "标准示例 · 可自由编辑" : row.id.slice(0, 18) + (row.id.length > 18 ? "…" : "")}</small><span>{row.count} 个判断点{row.pending > 0 && <em> · {row.pending} 待处理</em>}</span></span>{callId === row.id && <span className="selected-dot" />}</button>) : <p className="muted loading-label">没有找到对话</p>}</div> <div className="call-list" ref={callListRef}>{loading ? <p className="muted loading-label">正在读取工作空间…</p> : visibleCalls.length ? visibleCalls.map(row => <button key={row.id} className={`call-item ${callId === row.id ? "selected" : ""}`} onClick={() => chooseCall(row.id)} title={row.id}><span className="call-icon"><Icon name={row.recording ? "headphones" : "file"} size={17} /></span><span className="call-item-copy"><strong>{row.name}</strong><small>{row.id === "demo" ? "标准示例 · 可自由编辑" : row.id.slice(0, 18) + (row.id.length > 18 ? "…" : "")}</small><span>{row.count} 个判断点{row.pending > 0 && <em> · {row.pending} 待处理</em>}</span></span>{callId === row.id && <span className="selected-dot" />}</button>) : <p className="muted loading-label">没有找到对话</p>}</div>
<div className="sidebar-bottom"><div><span>标注进度</span><strong>{completed}<span> / {scopedAnnotations.length}</span></strong></div><div className="progress-track"><span style={{ width: `${scopedAnnotations.length ? completed / scopedAnnotations.length * 100 : 0}%` }} /></div><p>每次判断,留出合适的接话空间。</p><button className="help-link" onClick={() => setHelpOpen(true)}><Icon name="help" size={16} />标注说明与快捷键</button></div> <div className="sidebar-bottom"><div><span>标注进度</span><strong>{completed}<span> / {scopedAnnotations.length}</span></strong></div><div className="progress-track"><span style={{ width: `${scopedAnnotations.length ? completed / scopedAnnotations.length * 100 : 0}%` }} /></div><p>每次判断,留出合适的接话空间。</p><button className="help-link" onClick={() => setHelpOpen(true)}><Icon name="help" size={16} />标注说明与快捷键</button></div>
</aside> </aside>
+3 -60
View File
@@ -141,9 +141,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -160,9 +157,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -179,9 +173,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -198,9 +189,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -217,9 +205,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -236,9 +221,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -255,9 +237,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -274,9 +253,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later", "license": "LGPL-3.0-or-later",
"optional": true, "optional": true,
"os": [ "os": [
@@ -293,9 +269,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -318,9 +291,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -343,9 +313,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -368,9 +335,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -393,9 +357,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -418,9 +379,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -443,9 +401,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -468,9 +423,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -623,9 +575,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -642,9 +591,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -661,9 +607,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -680,9 +623,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -749,6 +689,7 @@
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -928,6 +869,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -937,6 +879,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
"integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.28.0" "scheduler": "^0.28.0"
}, },