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
+12
View File
@@ -293,6 +293,18 @@ kbd { min-width: 17px; padding: 0 4px; border: 1px solid #dce5ce; border-bottom-
.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) {
.workspace-grid { grid-template-columns: 210px minmax(0, 1fr); }
.sidebar { grid-row: span 2; }
+3 -1
View File
@@ -69,6 +69,7 @@ export default function Workbench() {
const objectUrls = useRef(new Set<string>());
const versionRef = useRef(0);
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 commit = useCallback((next: Dataset | ((current: Dataset) => Dataset)) => {
@@ -109,6 +110,7 @@ export default function Workbench() {
finally { setLoading(false); }
}, [notify]);
useEffect(() => { void loadWorkspace(); }, [loadWorkspace]);
useEffect(() => { callListRef.current?.querySelector(".call-item.selected")?.scrollIntoView({ block: "nearest" }); }, [callId]);
useEffect(() => {
if (!initialized || !dirty) return;
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 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>
</aside>
+3 -60
View File
@@ -141,9 +141,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -160,9 +157,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -179,9 +173,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -198,9 +189,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -217,9 +205,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -236,9 +221,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -255,9 +237,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -274,9 +253,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -293,9 +269,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -318,9 +291,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -343,9 +313,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -368,9 +335,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -393,9 +357,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -418,9 +379,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -443,9 +401,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -468,9 +423,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -623,9 +575,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -642,9 +591,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -661,9 +607,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -680,9 +623,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -749,6 +689,7 @@
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -928,6 +869,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -937,6 +879,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
"integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.28.0"
},