Update llm assisted annotation
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
html, body { min-height: 100%; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
@@ -38,13 +38,13 @@ button, a, select, input, textarea { outline-offset: 3px; }
|
||||
:focus-visible { outline: 2px solid var(--accent); }
|
||||
|
||||
.topbar {
|
||||
min-height: 72px;
|
||||
padding: 12px 20px;
|
||||
height: 56px;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(280px, 440px) minmax(220px, 1fr);
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
gap: 18px;
|
||||
background: rgba(7, 16, 29, 0.92);
|
||||
backdrop-filter: blur(14px);
|
||||
position: sticky;
|
||||
@@ -54,8 +54,8 @@ button, a, select, input, textarea { outline-offset: 3px; }
|
||||
|
||||
.brand { display: flex; align-items: center; gap: 12px; }
|
||||
.brand-mark {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 2px solid var(--accent);
|
||||
border-radius: 9px;
|
||||
position: relative;
|
||||
@@ -66,16 +66,16 @@ button, a, select, input, textarea { outline-offset: 3px; }
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
top: 14px;
|
||||
top: 12px;
|
||||
height: 2px;
|
||||
background: var(--accent);
|
||||
box-shadow: 5px -6px 0 -0.5px var(--accent), 11px 5px 0 -0.5px var(--accent);
|
||||
transform: skewY(-35deg);
|
||||
}
|
||||
.brand h1 { font-size: 1rem; margin: 0; letter-spacing: 0.01em; }
|
||||
.brand p { color: var(--muted); font-size: 0.8rem; margin: 2px 0 0; }
|
||||
.brand p { color: var(--muted); font-size: 0.72rem; margin: 1px 0 0; }
|
||||
|
||||
.progress-block { display: grid; gap: 7px; }
|
||||
.progress-block { display: grid; gap: 5px; }
|
||||
.progress-copy { display: flex; justify-content: space-between; gap: 20px; font-size: 0.86rem; }
|
||||
.progress-copy span { color: var(--muted); }
|
||||
.progress-track { height: 5px; border-radius: 8px; background: #17243a; overflow: hidden; }
|
||||
@@ -87,14 +87,15 @@ button, a, select, input, textarea { outline-offset: 3px; }
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 8px;
|
||||
padding: 8px 11px;
|
||||
padding: 6px 9px;
|
||||
font-size: 0.84rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
.export-actions a:hover { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
.filters {
|
||||
padding: 12px 20px;
|
||||
height: 54px;
|
||||
padding: 6px 14px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(130px, 175px)) minmax(190px, 1fr) auto;
|
||||
gap: 10px;
|
||||
@@ -103,7 +104,7 @@ button, a, select, input, textarea { outline-offset: 3px; }
|
||||
background: #091321;
|
||||
}
|
||||
|
||||
.filters label, .inline-fields label, .text-field { display: grid; gap: 6px; }
|
||||
.filters label, .inline-fields label, .text-field, .confidence-field { display: grid; gap: 3px; }
|
||||
.filters label > span, .inline-fields label > span, .text-field > span {
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
@@ -118,17 +119,17 @@ select, input[type="search"], input[type="text"], .text-field input, textarea {
|
||||
background: #0a1525;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 8px;
|
||||
padding: 9px 10px;
|
||||
min-height: 39px;
|
||||
padding: 5px 8px;
|
||||
min-height: 30px;
|
||||
}
|
||||
textarea { resize: vertical; line-height: 1.45; }
|
||||
::placeholder { color: #65758d; }
|
||||
|
||||
.button {
|
||||
min-height: 39px;
|
||||
min-height: 30px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
padding: 5px 10px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
background: var(--panel-2);
|
||||
@@ -139,12 +140,13 @@ textarea { resize: vertical; line-height: 1.45; }
|
||||
.button:disabled { cursor: wait; opacity: 0.55; }
|
||||
|
||||
.workspace {
|
||||
min-height: calc(100vh - 142px);
|
||||
padding: 14px;
|
||||
height: calc(100vh - 110px);
|
||||
min-height: 0;
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 0.66fr) minmax(460px, 1.65fr) minmax(365px, 1fr);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.panel {
|
||||
@@ -153,10 +155,12 @@ textarea { resize: vertical; line-height: 1.45; }
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.queue-panel, .review-panel { position: sticky; top: 152px; max-height: calc(100vh - 166px); }
|
||||
.queue-panel, .review-panel { height: 100%; }
|
||||
.queue-panel { display: flex; flex-direction: column; }
|
||||
.review-panel { overflow-y: auto; }
|
||||
.review-panel { display: flex; flex-direction: column; }
|
||||
.evidence-panel { height: 100%; display: flex; flex-direction: column; }
|
||||
|
||||
.panel-heading, .event-header, .section-heading {
|
||||
display: flex;
|
||||
@@ -164,7 +168,7 @@ textarea { resize: vertical; line-height: 1.45; }
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
.panel-heading { padding: 15px 16px 12px; border-bottom: 1px solid var(--line); }
|
||||
.panel-heading { padding: 9px 12px 8px; border-bottom: 1px solid var(--line); }
|
||||
.panel-heading h2, .section-heading h3 { margin: 2px 0 0; font-size: 1rem; }
|
||||
.eyebrow { color: var(--accent); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.12em; }
|
||||
.count-pill, .badge {
|
||||
@@ -180,18 +184,18 @@ textarea { resize: vertical; line-height: 1.45; }
|
||||
}
|
||||
.badge.accent { color: var(--accent); border-color: rgba(94, 234, 212, 0.45); background: var(--accent-dim); }
|
||||
|
||||
.queue-list { overflow-y: auto; padding: 7px; }
|
||||
.queue-list { min-height: 0; overflow-y: auto; padding: 5px; }
|
||||
.queue-item {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9px;
|
||||
padding: 10px;
|
||||
padding: 7px 8px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 3px;
|
||||
}
|
||||
.queue-item:hover { background: rgba(255,255,255,0.035); }
|
||||
.queue-item.active { border-color: var(--accent); background: var(--accent-dim); }
|
||||
@@ -201,129 +205,190 @@ textarea { resize: vertical; line-height: 1.45; }
|
||||
.queue-status { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%; background: #516078; }
|
||||
.queue-meta { color: var(--muted); font-size: 0.74rem; display: flex; gap: 8px; }
|
||||
.queue-label { font-size: 0.73rem; color: #cbd7e7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.queue-limit { margin: 0; padding: 10px 14px; color: var(--muted); font-size: 0.75rem; border-top: 1px solid var(--line); }
|
||||
.queue-limit { margin: 0; padding: 6px 10px; color: var(--muted); font-size: 0.7rem; border-top: 1px solid var(--line); }
|
||||
|
||||
.event-header { padding: 16px 18px; }
|
||||
.event-header h2 { margin: 8px 0 3px; font-size: 1.2rem; }
|
||||
.event-header { padding: 9px 14px 7px; }
|
||||
.event-header h2 { margin: 5px 0 1px; font-size: 1.05rem; }
|
||||
.event-header p { margin: 0; color: var(--muted); font-size: 0.82rem; }
|
||||
.event-badges { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.llm-suggestion {
|
||||
margin: 0 18px 14px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(94, 234, 212, 0.3);
|
||||
border-radius: 10px;
|
||||
background: rgba(94, 234, 212, 0.07);
|
||||
color: #c9f8ef;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.llm-suggestion strong { display: block; color: var(--accent); margin-bottom: 3px; }
|
||||
.decision-guide { margin: 0 14px 8px; display: grid; grid-template-columns: minmax(170px, .52fr) minmax(0, 1.48fr); gap: 7px; }
|
||||
.review-question { padding: 9px 11px; border: 1px solid rgba(251,191,36,.28); border-left: 3px solid var(--amber); border-radius: 8px; background: rgba(251,191,36,.055); display: grid; align-content: center; gap: 5px; }
|
||||
.review-question span { color: var(--amber); font-size: .62rem; font-weight: 850; letter-spacing: .12em; text-transform: uppercase; }
|
||||
.review-question strong { font-size: .78rem; line-height: 1.35; }
|
||||
.ai-decision-card { padding: 8px 10px; border: 1px solid rgba(94,234,212,.38); border-radius: 8px; background: linear-gradient(115deg, rgba(94,234,212,.1), rgba(17,29,48,.38)); box-shadow: inset 3px 0 0 var(--accent); display: grid; gap: 6px; }
|
||||
.ai-decision-heading { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
|
||||
.ai-decision-heading span { color: var(--accent); font-size: .66rem; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.ai-decision-heading span i { display: inline-block; width: 6px; height: 6px; margin-right: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 4px rgba(94,234,212,.1); }
|
||||
.ai-decision-heading em { color: var(--muted); font-style: normal; font-size: .68rem; font-variant-numeric: tabular-nums; }
|
||||
.ai-decision-main { display: grid; grid-template-columns: auto auto minmax(0,1fr); align-items: center; gap: 9px; }
|
||||
.ai-verdict { min-width: 116px; padding: 7px 9px; border-radius: 6px; background: rgba(251,191,36,.12); color: #ffd66e; font-size: .82rem; letter-spacing: .02em; text-align: center; }
|
||||
.decision-arrow { color: #5e718b; font-size: 1rem; }
|
||||
.ai-decision-main b { display: block; color: var(--text); font-size: .82rem; }
|
||||
.ai-decision-main p { margin: 2px 0 0; color: #c3d2e4; font-size: .72rem; line-height: 1.3; }
|
||||
.ai-rationale { padding-top: 5px; border-top: 1px solid rgba(94,234,212,.16); display: grid; grid-template-columns: auto minmax(0,1fr); gap: 7px; }
|
||||
.ai-rationale span { color: var(--muted); font-size: .66rem; white-space: nowrap; }
|
||||
.ai-rationale p { margin: 0; color: #d7e6f5; font-size: .7rem; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.ai-decision-card[data-verdict="POSITIVE"] { border-color: rgba(52,211,153,.48); box-shadow: inset 3px 0 0 var(--success); }
|
||||
.ai-decision-card[data-verdict="POSITIVE"] .ai-verdict { color: #86efac; background: rgba(52,211,153,.13); }
|
||||
.ai-decision-card[data-verdict="NOT_APPLICABLE"] { border-color: var(--line-strong); box-shadow: inset 3px 0 0 #71829a; }
|
||||
.ai-decision-card[data-verdict="NOT_APPLICABLE"] .ai-verdict { color: #c4d0df; background: rgba(113,130,154,.14); }
|
||||
.ai-decision-card[data-verdict="UNSURE"] { border-color: rgba(167,139,250,.48); box-shadow: inset 3px 0 0 #a78bfa; }
|
||||
.ai-decision-card[data-verdict="UNSURE"] .ai-verdict { color: #c4b5fd; background: rgba(167,139,250,.13); }
|
||||
.ai-decision-card[data-verdict="EXCLUDE"] { border-color: rgba(251,113,133,.48); box-shadow: inset 3px 0 0 var(--danger); }
|
||||
.ai-decision-card[data-verdict="EXCLUDE"] .ai-verdict { color: #fda4af; background: rgba(251,113,133,.13); }
|
||||
.event-position { color: var(--muted); font-size: 0.8rem; }
|
||||
|
||||
.timeline-section { padding: 0 18px 16px; display: grid; gap: 10px; }
|
||||
.timeline-section { padding: 0 14px 7px; display: grid; gap: 6px; }
|
||||
.timeline-section .section-heading { margin-top: 2px; }
|
||||
.local-heading { margin-top: 8px !important; }
|
||||
.timeline-heading { align-items: end; }
|
||||
.timeline-controls { display: flex; align-items: center; gap: 3px; padding: 3px; border: 1px solid var(--line); border-radius: 8px; background: #07101d; }
|
||||
.timeline-controls button { min-width: 30px; height: 28px; padding: 0 8px; color: #bed0e6; border: 0; border-right: 1px solid var(--line); background: transparent; cursor: pointer; font-size: .72rem; font-weight: 750; }
|
||||
.timeline-controls button:last-child { border-right: 0; }
|
||||
.timeline-controls button:hover { color: var(--accent); background: var(--accent-dim); }
|
||||
.timeline-readout { display: flex; align-items: baseline; gap: 9px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.timeline-readout strong { color: var(--amber); font-size: .78rem; min-width: 28px; }
|
||||
.timeline-readout span { font-size: .72rem; }
|
||||
.timeline-readout small { margin-left: auto; font-size: .65rem; }
|
||||
.local-heading { margin-top: 2px !important; }
|
||||
.session-overview-details { border-bottom: 1px solid var(--line); }
|
||||
.session-overview-details > summary { min-height: 30px; display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--muted); cursor: pointer; list-style: none; font-size: .74rem; }
|
||||
.session-overview-details > summary::-webkit-details-marker { display: none; }
|
||||
.session-overview-details > summary span { display: flex; align-items: baseline; gap: 8px; }
|
||||
.session-overview-details > summary b { color: #cbd7e7; }
|
||||
.session-overview-details > summary small { font-size: .67rem; }
|
||||
.session-overview-details > summary em { font-style: normal; font-size: .66rem; }
|
||||
.session-overview-details[open] > summary em { color: var(--accent); }
|
||||
.session-overview-body { display: grid; gap: 7px; padding: 6px 0 9px; }
|
||||
.session-timeline { min-height: 34px; border: 1px solid var(--line); border-radius: 8px; position: relative; background: #07101d; padding: 6px 7px 28px; display: grid; gap: 3px; overflow: hidden; }
|
||||
.session-lane { display: grid; grid-template-columns: 28px 1fr; gap: 5px; align-items: center; min-height: 10px; }
|
||||
.session-lane { display: grid; grid-template-columns: 52px 1fr; gap: 5px; align-items: center; min-height: 10px; }
|
||||
.session-lane > strong { color: var(--muted); text-align: right; font-size: .58rem; }
|
||||
.session-track { height: 8px; position: relative; background: rgba(255,255,255,.025); }
|
||||
.session-speech { position: absolute; top: 1px; height: 6px; min-width: 1px; background: #60a5fa; opacity: .75; }
|
||||
.session-speech.wearer { background: var(--accent); }
|
||||
.session-events { position: absolute; left: 40px; right: 7px; bottom: 4px; height: 20px; border-top: 1px solid var(--line-strong); }
|
||||
.session-marker { position: absolute; top: 3px; width: 7px; height: 15px; margin-left: -3px; padding: 0; border: 0; border-radius: 4px; background: var(--amber); cursor: pointer; opacity: .68; }
|
||||
.session-events { position: absolute; left: 64px; right: 7px; bottom: 4px; height: 20px; border-top: 1px solid var(--line-strong); }
|
||||
.session-marker { position: absolute; top: 3px; width: 7px; min-width: 7px; max-width: 100%; height: 15px; margin-left: -3px; padding: 0; border: 0; border-radius: 4px; background: var(--amber); cursor: pointer; opacity: .68; transition: opacity 120ms ease, transform 120ms ease; }
|
||||
.session-marker:hover { opacity: 1; transform: translateY(-1px); z-index: 4; }
|
||||
.session-marker.int { background: #a78bfa; }
|
||||
.session-marker.current { opacity: 1; box-shadow: 0 0 0 2px white; z-index: 2; }
|
||||
.speaker-timeline { display: grid; gap: 6px; color: var(--muted); font-size: .76rem; }
|
||||
.speaker-lane { display: grid; grid-template-columns: 34px 1fr; align-items: center; gap: 8px; }
|
||||
.speaker-timeline { display: grid; gap: 4px; color: var(--muted); font-size: .76rem; }
|
||||
.speaker-lane { display: grid; grid-template-columns: 58px 1fr; align-items: center; gap: 8px; }
|
||||
.speaker-lane > strong { color: var(--muted); text-align: right; font-size: .72rem; }
|
||||
.speaker-track { height: 22px; position: relative; border: 1px solid var(--line); border-radius: 5px; overflow: hidden; background: #07101d; }
|
||||
.speaker-track { height: 19px; position: relative; border: 1px solid var(--line); border-radius: 5px; overflow: hidden; background: #07101d; }
|
||||
.speaker-track > i { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--amber); z-index: 3; }
|
||||
.speech-segment { position: absolute; top: 5px; height: 10px; min-width: 2px; border-radius: 3px; background: #60a5fa; }
|
||||
.speech-segment { position: absolute; top: 3px; height: 11px; min-width: 4px; padding: 0; border: 1px solid transparent; border-radius: 3px; background: #60a5fa; cursor: pointer; }
|
||||
.speech-segment.wearer { background: var(--accent); }
|
||||
.speech-segment:hover, .speech-segment:focus-visible, .speech-segment.active { border-color: #fff; box-shadow: 0 0 0 2px rgba(255,255,255,.28); z-index: 4; outline: none; }
|
||||
.speaker-evidence { margin: 5px 0 0 66px; min-height: 42px; display: grid; grid-template-columns: auto 1fr; column-gap: 8px; align-items: baseline; padding: 6px 9px; border: 1px solid rgba(94,234,212,.25); border-left: 3px solid var(--accent); border-radius: 6px; background: rgba(94,234,212,.055); }
|
||||
.speaker-evidence > strong { color: var(--accent); font-size: .76rem; white-space: nowrap; }
|
||||
.speaker-evidence > span { color: var(--muted); font-size: .72rem; font-variant-numeric: tabular-nums; }
|
||||
.speaker-evidence > p { grid-column: 1 / -1; margin: 3px 0 0; color: #dce9f8; font-size: .78rem; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
|
||||
.video-shell { background: #02050a; aspect-ratio: 16 / 9; position: relative; border-block: 1px solid var(--line); }
|
||||
.evidence-grid { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(270px, .8fr); border-top: 1px solid var(--line); }
|
||||
.media-column, .transcript-column { min-width: 0; min-height: 0; }
|
||||
.media-column { display: flex; flex-direction: column; background: #02050a; }
|
||||
.transcript-column { display: flex; flex-direction: column; border-left: 1px solid var(--line); }
|
||||
|
||||
.video-shell { flex: 1; min-height: 0; background: #02050a; position: relative; }
|
||||
.video-shell video { width: 100%; height: 100%; display: block; object-fit: contain; }
|
||||
.empty-state { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; gap: 7px; color: var(--muted); }
|
||||
.empty-state strong { color: var(--text); }
|
||||
.time-marker { position: absolute; pointer-events: none; left: 50%; top: 10px; bottom: 44px; border-left: 1px dashed rgba(251,191,36,.7); opacity: 0; }
|
||||
.video-shell:hover .time-marker { opacity: .55; }
|
||||
|
||||
.transport { padding: 10px 14px; display: flex; gap: 7px; align-items: center; border-bottom: 1px solid var(--line); }
|
||||
.transport button { color: var(--muted); border: 1px solid var(--line); background: #0a1525; border-radius: 7px; padding: 6px 9px; cursor: pointer; }
|
||||
.transport { flex: 0 0 auto; padding: 6px 8px; display: flex; gap: 5px; align-items: center; border-top: 1px solid var(--line); }
|
||||
.transport button { color: var(--muted); border: 1px solid var(--line); background: #0a1525; border-radius: 6px; padding: 4px 6px; cursor: pointer; font-size: .72rem; }
|
||||
.transport button:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.transport .jump-event { color: var(--amber); }
|
||||
#timeReadout { margin-left: auto; color: var(--muted); font-variant-numeric: tabular-nums; font-size: 0.8rem; }
|
||||
|
||||
.transcript-section { padding: 16px 18px 8px; }
|
||||
.section-heading { margin-bottom: 10px; }
|
||||
.transcript-section { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 9px 10px 5px; }
|
||||
.section-heading { margin-bottom: 6px; }
|
||||
.section-heading > span { color: var(--muted); font-size: .78rem; }
|
||||
.transcript-list { display: grid; gap: 7px; max-height: 270px; overflow-y: auto; padding-right: 3px; }
|
||||
.transcript-row { display: grid; grid-template-columns: 72px minmax(0,1fr); gap: 10px; padding: 9px 10px; border: 1px solid var(--line); border-radius: 8px; background: rgba(2, 8, 18, 0.36); }
|
||||
.transcript-list { flex: 1; min-height: 0; display: grid; align-content: start; gap: 5px; overflow-y: auto; padding-right: 3px; }
|
||||
.transcript-row { display: grid; grid-template-columns: 64px minmax(0,1fr); gap: 7px; padding: 6px 7px; border: 1px solid var(--line); border-radius: 7px; background: rgba(2, 8, 18, 0.36); }
|
||||
.transcript-row.anchor { border-color: rgba(251,191,36,.65); background: rgba(251,191,36,.08); }
|
||||
.transcript-row[data-uid] { cursor: pointer; }
|
||||
.transcript-row.linked { border-color: rgba(94,234,212,.75); background: rgba(94,234,212,.09); box-shadow: inset 3px 0 0 var(--accent); }
|
||||
.transcript-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
.speaker { color: var(--accent); font-size: .75rem; font-weight: 800; }
|
||||
.speaker small { color: var(--muted); display: block; margin-top: 3px; font-weight: 500; }
|
||||
.utterance { line-height: 1.46; font-size: .9rem; }
|
||||
.utterance { line-height: 1.35; font-size: .82rem; }
|
||||
.utterance small { display: block; color: var(--muted); margin-top: 4px; font-size: .72rem; }
|
||||
.empty-copy { color: var(--muted); padding: 22px; text-align: center; border: 1px dashed var(--line-strong); border-radius: 8px; }
|
||||
|
||||
.raw-details { margin: 10px 18px 18px; border-top: 1px solid var(--line); padding-top: 12px; }
|
||||
.raw-details { flex: 0 0 auto; margin: 4px 10px 8px; border-top: 1px solid var(--line); padding-top: 6px; }
|
||||
.raw-details summary { color: var(--muted); cursor: pointer; font-size: .82rem; }
|
||||
.raw-details pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 260px; overflow-y: auto; color: #bfd0e7; font-size: .74rem; line-height: 1.5; }
|
||||
|
||||
.review-heading { background: rgba(94,234,212,.035); }
|
||||
.save-state { color: var(--muted); font-size: .75rem; }
|
||||
.save-state.saved { color: var(--success); }
|
||||
#reviewForm { padding: 13px 15px 16px; display: grid; gap: 14px; }
|
||||
#reviewForm { min-height: 0; overflow-y: auto; padding: 9px 11px 10px; display: grid; gap: 9px; }
|
||||
fieldset { margin: 0; padding: 0; border: 0; display: grid; gap: 8px; }
|
||||
legend { font-size: .84rem; font-weight: 750; margin-bottom: 8px; }
|
||||
legend { font-size: .8rem; font-weight: 750; margin-bottom: 5px; }
|
||||
.required { color: var(--muted); font-size: .66rem; font-weight: 500; }
|
||||
.choice-grid { display: grid; gap: 7px; }
|
||||
.choice-grid.three { grid-template-columns: repeat(3, 1fr); }
|
||||
.choice-grid.two { grid-template-columns: repeat(2, 1fr); }
|
||||
.choice-grid.action-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.choice-grid label { cursor: pointer; min-width: 0; }
|
||||
.choice-grid input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.choice-grid label span { min-height: 52px; border: 1px solid var(--line); border-radius: 8px; padding: 8px; display: grid; align-content: center; gap: 2px; background: rgba(2,8,18,.28); }
|
||||
.choice-grid label span { min-height: 44px; border: 1px solid var(--line); border-radius: 8px; padding: 6px 7px; display: grid; align-content: center; gap: 1px; background: rgba(2,8,18,.28); }
|
||||
.choice-grid.compact label span { min-height: 40px; }
|
||||
.choice-grid label:hover span { border-color: var(--line-strong); }
|
||||
.choice-grid input:checked + span { border-color: var(--accent); background: var(--accent-dim); box-shadow: inset 0 0 0 1px rgba(94,234,212,.18); }
|
||||
.choice-grid input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.choice-grid b { font-size: .8rem; overflow: hidden; text-overflow: ellipsis; }
|
||||
.choice-grid small { color: var(--muted); font-size: .67rem; line-height: 1.25; }
|
||||
.choice-grid .accept-option { grid-column: 1 / -1; }
|
||||
.choice-grid .accept-option span { min-height: 62px; padding: 8px 10px; border-color: rgba(94,234,212,.34); background: linear-gradient(100deg, rgba(94,234,212,.09), rgba(2,8,18,.24)); box-shadow: inset 3px 0 0 rgba(94,234,212,.72); }
|
||||
.choice-grid .accept-option em { color: var(--accent); font-size: .6rem; font-style: normal; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.choice-grid .accept-option b { color: #ecfffb; font-size: .9rem; }
|
||||
.choice-grid .accept-option small { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.choice-grid .accept-option input:checked + span { border-color: var(--accent); background: rgba(94,234,212,.17); box-shadow: inset 4px 0 0 var(--accent), 0 0 0 1px rgba(94,234,212,.12); }
|
||||
.choice-grid input:disabled + span { cursor: not-allowed; opacity: .42; filter: grayscale(.45); }
|
||||
.inline-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
|
||||
.confidence-field { color: var(--muted); font-size: .72rem; font-weight: 700; }
|
||||
.confidence-field select { color: var(--text); font-size: .82rem; }
|
||||
.inline-fields select { font-size: .82rem; }
|
||||
.text-field input, .text-field textarea { font-size: .84rem; }
|
||||
.form-error { color: #fecdd3; background: rgba(251,113,133,.1); border: 1px solid rgba(251,113,133,.4); border-radius: 8px; padding: 9px; font-size: .8rem; }
|
||||
.review-actions { display: grid; grid-template-columns: .9fr 1.4fr; gap: 8px; }
|
||||
.review-actions { display: grid; grid-template-columns: 1fr; gap: 8px; }
|
||||
.conditional-fields { padding: 11px; border: 1px solid var(--line); border-radius: 9px; background: rgba(94,234,212,.035); display: grid; gap: 9px; }
|
||||
.conditional-fields label, .boundary-details label { display: grid; gap: 6px; }
|
||||
.conditional-fields label > span, .boundary-details label > span { color: var(--muted); font-size: .72rem; font-weight: 700; }
|
||||
.boundary-details { border-top: 1px solid var(--line); padding-top: 10px; }
|
||||
.boundary-details summary { color: var(--muted); cursor: pointer; font-size: .78rem; margin-bottom: 9px; }
|
||||
.boundary-details, .optional-details { border-top: 1px solid var(--line); padding-top: 7px; }
|
||||
.boundary-details summary, .optional-details summary { color: var(--muted); cursor: pointer; font-size: .75rem; margin-bottom: 6px; }
|
||||
kbd { border: 1px solid rgba(3,24,22,.35); border-bottom-width: 2px; border-radius: 4px; padding: 1px 4px; font-size: .68rem; font-family: inherit; }
|
||||
.shortcuts { border-top: 1px solid var(--line); padding: 11px 15px 14px; display: flex; flex-wrap: wrap; gap: 8px 14px; color: var(--muted); font-size: .71rem; }
|
||||
.shortcuts { flex: 0 0 auto; border-top: 1px solid var(--line); padding: 7px 11px 8px; display: flex; flex-wrap: wrap; gap: 5px 10px; color: var(--muted); font-size: .68rem; }
|
||||
.shortcuts kbd { color: #cbd7e7; border-color: var(--line-strong); margin-right: 2px; }
|
||||
|
||||
.toast { position: fixed; left: 50%; bottom: 22px; transform: translate(-50%, 20px); background: #d9fff9; color: #05251f; border-radius: 9px; padding: 10px 15px; font-weight: 750; font-size: .84rem; box-shadow: var(--shadow); opacity: 0; pointer-events: none; transition: 160ms ease; z-index: 40; }
|
||||
.toast.visible { opacity: 1; transform: translate(-50%, 0); }
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
html, body { height: auto; overflow: auto; }
|
||||
.topbar { grid-template-columns: 1fr minmax(250px, 380px); }
|
||||
.export-actions { display: none; }
|
||||
.workspace { grid-template-columns: 220px minmax(430px, 1fr); }
|
||||
.review-panel { position: static; max-height: none; grid-column: 1 / -1; }
|
||||
.queue-panel { top: 152px; }
|
||||
.workspace { height: auto; min-height: calc(100vh - 110px); grid-template-columns: 220px minmax(430px, 1fr); }
|
||||
.review-panel { height: auto; grid-column: 1 / -1; }
|
||||
.queue-panel, .evidence-panel { min-height: 720px; }
|
||||
.filters { grid-template-columns: repeat(3, 1fr); }
|
||||
.search-field { grid-column: span 2; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.topbar { position: static; grid-template-columns: 1fr; gap: 12px; }
|
||||
.filters { grid-template-columns: 1fr 1fr; padding: 10px; }
|
||||
.topbar { position: static; height: auto; grid-template-columns: 1fr; gap: 12px; }
|
||||
.filters { height: auto; grid-template-columns: 1fr 1fr; padding: 10px; }
|
||||
.search-field { grid-column: 1 / -1; }
|
||||
.workspace { grid-template-columns: 1fr; padding: 10px; }
|
||||
.queue-panel, .review-panel { position: static; max-height: none; }
|
||||
.queue-panel { max-height: 340px; }
|
||||
.evidence-grid { grid-template-columns: 1fr; }
|
||||
.decision-guide { grid-template-columns: 1fr; }
|
||||
.transcript-column { min-height: 420px; border-left: 0; border-top: 1px solid var(--line); }
|
||||
.transport { flex-wrap: wrap; }
|
||||
#timeReadout { width: 100%; margin: 2px 0 0; }
|
||||
.choice-grid.three { grid-template-columns: 1fr; }
|
||||
.choice-grid.action-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ const state = {
|
||||
currentDetail: null,
|
||||
saving: false,
|
||||
sessionSpeech: {},
|
||||
timelineSession: null,
|
||||
timelineZoom: 8,
|
||||
timelineCenter: null,
|
||||
};
|
||||
|
||||
const elements = Object.fromEntries(
|
||||
@@ -15,11 +18,14 @@ const elements = Object.fromEntries(
|
||||
"progressText", "progressMeta", "progressBar", "sessionFilter", "splitFilter",
|
||||
"familyFilter", "statusFilter", "searchFilter", "clearFilters", "queueTitle",
|
||||
"queueCount", "queueList", "queueLimit", "familyBadge", "splitBadge",
|
||||
"autoConfidenceBadge", "eventTitle", "eventSubtitle", "eventPosition",
|
||||
"llmSuggestion", "llmSuggestionText", "sessionTimeline", "speakerTimeline",
|
||||
"eventTitle", "eventSubtitle", "eventPosition", "reviewQuestion",
|
||||
"llmSuggestion", "aiConfidence", "aiVerdict", "aiLabel", "aiMeaning", "aiRationale",
|
||||
"acceptRadio", "acceptTitle", "acceptSummary", "sessionTimeline", "speakerTimeline", "speakerEvidence",
|
||||
"timelineZoomLabel", "timelineRange", "timelinePanLeft", "timelineZoomOut",
|
||||
"timelineFocus", "timelineZoomIn", "timelinePanRight", "timelineReset",
|
||||
"eventVideo", "mediaEmpty", "jumpEvent", "timeReadout", "clipLabel",
|
||||
"transcriptList", "rawCandidate", "reviewForm", "reviewerInput", "formError",
|
||||
"saveButton", "uncertainButton", "saveState", "toast", "relabelFields",
|
||||
"transcriptList", "rawCandidate", "reviewForm", "formError",
|
||||
"saveButton", "saveState", "toast", "relabelFields",
|
||||
"excludeFields", "eventTypeField", "subtypeField",
|
||||
].map((id) => [id, document.getElementById(id)])
|
||||
);
|
||||
@@ -32,6 +38,48 @@ const familyNames = {
|
||||
interaction: "LEGACY INTERACTION",
|
||||
};
|
||||
|
||||
const reviewQuestions = {
|
||||
eot: "Other 释放 floor 后,wearer 是否真正持续接话?",
|
||||
int: "Other 是否在 wearer 发言中接管,使 wearer 让出 floor?",
|
||||
onset: "这次 wearer onset 是否代表新的 floor entry?",
|
||||
offset: "这次 wearer offset 是否代表让出 floor?",
|
||||
interaction: "这次说话人变化是否属于目标 floor event?",
|
||||
};
|
||||
|
||||
const decisionMeanings = {
|
||||
EOT_RESPONSE: "Other 释放 floor 后,wearer 持续接话。",
|
||||
INT_YIELD: "Other 在 wearer 发言中接管,wearer 随后让出 floor。",
|
||||
EOT_MIDTURN_PAUSE: "Other 只是短暂停顿后继续,没有释放 floor 给 wearer。",
|
||||
EOT_OTHER_TO_OTHER: "这是 non-wearer 之间的换人,不是 wearer 接话。",
|
||||
EOT_WEARER_BACKCHANNEL: "Wearer 只是反馈、笑声或短回应,没有接过 floor。",
|
||||
INT_OTHER_BACKCHANNEL: "Other 只是反馈或笑声,没有接管 wearer 的 floor。",
|
||||
INT_NON_TAKEOVER_OVERLAP: "虽然发生重叠,但 other 没有形成持续接管。",
|
||||
INT_WEARER_HOLDS_FLOOR: "Wearer 在重叠后仍持有或继续自己的 floor。",
|
||||
OTHER_HARD_NEGATIVE: "这是有效困难负例,但不属于更具体的负例类型。",
|
||||
NOT_APPLICABLE: "这是真实活动边界,但不属于当前 EOT/INT 评测问题。",
|
||||
UNSURE: "现有时间和转写证据不足,需要人工直接判断标签。",
|
||||
EXCLUDE: "该区域存在数据质量问题,不应进入可靠评分。",
|
||||
};
|
||||
|
||||
function suggestionView(suggestion) {
|
||||
const verdict = suggestion.verdict || suggestion.protocol_disposition || "UNSURE";
|
||||
const label = suggestion.event_type || suggestion.subtype || suggestion.hard_negative_type || verdict;
|
||||
const rawConfidence = suggestion.confidence ?? suggestion.final_confidence;
|
||||
const numericConfidence = Number(rawConfidence);
|
||||
const confidence = Number.isFinite(numericConfidence)
|
||||
? `${Math.round(numericConfidence * 100)}%`
|
||||
: escapeText(rawConfidence || "—");
|
||||
const rawRationale = suggestion.reason || suggestion.rationale || "未提供文字依据。";
|
||||
const rationale = [...new Set(String(rawRationale).split(" | ").map((item) => item.trim()).filter(Boolean))].join(" · ");
|
||||
return {
|
||||
verdict,
|
||||
label,
|
||||
confidence,
|
||||
rationale,
|
||||
meaning: decisionMeanings[label] || decisionMeanings[verdict] || "请结合时间轴与转写核对该标签。",
|
||||
};
|
||||
}
|
||||
|
||||
function escapeText(value) {
|
||||
return String(value ?? "");
|
||||
}
|
||||
@@ -43,6 +91,40 @@ function formatTime(value) {
|
||||
return `${String(minutes).padStart(2, "0")}:${(seconds % 60).toFixed(1).padStart(4, "0")}`;
|
||||
}
|
||||
|
||||
function participantIdentity(row, verbose = false) {
|
||||
const participant = `P${row.participant_id ?? "?"}`;
|
||||
if (row.participant_role === "wearer") {
|
||||
return verbose ? `W · ${participant} · wearer` : `W · ${participant}`;
|
||||
}
|
||||
return verbose ? `${participant} · other` : participant;
|
||||
}
|
||||
|
||||
function matchingElements(selector, uid) {
|
||||
return [...document.querySelectorAll(selector)].filter((element) => element.dataset.uid === uid);
|
||||
}
|
||||
|
||||
function showSpeakerEvidence(row, activeSegment = null, revealTranscript = false) {
|
||||
document.querySelectorAll(".speech-segment.active").forEach((element) => element.classList.remove("active"));
|
||||
document.querySelectorAll(".transcript-row.linked").forEach((element) => element.classList.remove("linked"));
|
||||
if (activeSegment) activeSegment.classList.add("active");
|
||||
|
||||
elements.speakerEvidence.replaceChildren();
|
||||
const identity = document.createElement("strong");
|
||||
identity.textContent = participantIdentity(row, true);
|
||||
const time = document.createElement("span");
|
||||
time.textContent = `${formatTime(row.start_seconds)}–${formatTime(row.end_seconds)}`;
|
||||
const transcription = document.createElement("p");
|
||||
transcription.textContent = row.transcription || "(该 activity 没有转写文本)";
|
||||
elements.speakerEvidence.append(identity, time, transcription);
|
||||
|
||||
if (!row.uid) return;
|
||||
const transcriptRows = matchingElements(".transcript-row[data-uid]", row.uid);
|
||||
for (const transcriptRow of transcriptRows) transcriptRow.classList.add("linked");
|
||||
if (revealTranscript && transcriptRows[0]) {
|
||||
transcriptRows[0].scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
|
||||
function toast(message) {
|
||||
elements.toast.textContent = message;
|
||||
elements.toast.classList.add("visible");
|
||||
@@ -120,7 +202,7 @@ function renderQueue() {
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "queue-meta";
|
||||
meta.textContent = `${item.session} · ${familyNames[item.family] || item.family} · ${formatTime(item.event_time_seconds)}`;
|
||||
meta.textContent = `${familyNames[item.family] || item.family} · ${formatTime(item.event_time_seconds)}`;
|
||||
const label = document.createElement("div");
|
||||
label.className = "queue-label";
|
||||
label.textContent = item.ai_verdict
|
||||
@@ -150,6 +232,10 @@ async function selectCandidate(candidateId) {
|
||||
elements.eventVideo.load();
|
||||
elements.mediaEmpty.hidden = false;
|
||||
elements.llmSuggestion.hidden = true;
|
||||
elements.reviewQuestion.textContent = "—";
|
||||
elements.acceptRadio.disabled = true;
|
||||
elements.acceptTitle.textContent = "没有 AI 结论";
|
||||
elements.acceptSummary.textContent = "请选择 Relabel 完成人工标注";
|
||||
elements.reviewForm.reset();
|
||||
return;
|
||||
}
|
||||
@@ -172,26 +258,39 @@ async function selectCandidate(candidateId) {
|
||||
function renderDetail(detail) {
|
||||
elements.familyBadge.textContent = familyNames[detail.family] || detail.family;
|
||||
elements.splitBadge.textContent = `${detail.split} · ${detail.session}`;
|
||||
elements.autoConfidenceBadge.textContent = detail.automatic_confidence ? `auto ${detail.automatic_confidence}` : "auto confidence —";
|
||||
elements.eventTitle.textContent = detail.candidate_source || detail.automatic_label;
|
||||
elements.eventSubtitle.textContent = `${detail.uid} · ${detail.source_file}:${detail.source_row}`;
|
||||
elements.eventSubtitle.textContent = detail.uid;
|
||||
elements.clipLabel.textContent = detail.clip_stem ? `${detail.session} / ${detail.clip_stem}` : detail.session;
|
||||
elements.rawCandidate.textContent = JSON.stringify(detail.candidate, null, 2);
|
||||
elements.saveState.textContent = detail.review ? "已保存" : "未保存";
|
||||
elements.saveState.classList.toggle("saved", Boolean(detail.review));
|
||||
elements.reviewQuestion.textContent = reviewQuestions[detail.family] || "核对该候选是否属于目标 floor event。";
|
||||
|
||||
const suggestion = detail.llm_suggestion;
|
||||
elements.llmSuggestion.hidden = !suggestion;
|
||||
if (suggestion) {
|
||||
const disposition = suggestion.verdict || suggestion.protocol_disposition || "UNSURE";
|
||||
const subtype = suggestion.event_type || suggestion.subtype || suggestion.hard_negative_type || "—";
|
||||
const confidence = suggestion.confidence || suggestion.final_confidence || "—";
|
||||
const rationale = suggestion.reason || suggestion.rationale || "";
|
||||
elements.llmSuggestionText.textContent = `${disposition} · ${subtype} · ${confidence}${rationale ? ` — ${rationale}` : ""}`;
|
||||
const view = suggestionView(suggestion);
|
||||
elements.llmSuggestion.dataset.verdict = view.verdict;
|
||||
elements.aiConfidence.textContent = `置信度 ${view.confidence}`;
|
||||
elements.aiVerdict.textContent = view.verdict.replaceAll("_", " ");
|
||||
elements.aiLabel.textContent = view.label;
|
||||
elements.aiMeaning.textContent = view.meaning;
|
||||
elements.aiRationale.textContent = view.rationale;
|
||||
elements.acceptRadio.disabled = false;
|
||||
elements.acceptTitle.textContent = `接受 ${view.verdict.replaceAll("_", " ")}`;
|
||||
elements.acceptSummary.textContent = `${view.label} · ${view.meaning}`;
|
||||
} else {
|
||||
elements.acceptRadio.disabled = true;
|
||||
elements.acceptTitle.textContent = "没有 AI 结论";
|
||||
elements.acceptSummary.textContent = "请选择 Relabel 完成人工标注";
|
||||
}
|
||||
|
||||
if (state.timelineSession !== detail.session) {
|
||||
state.timelineSession = detail.session;
|
||||
state.timelineZoom = 8;
|
||||
}
|
||||
state.timelineCenter = Number(detail.event_time_seconds);
|
||||
renderSessionTimeline(detail);
|
||||
renderSpeakerTimeline(detail);
|
||||
|
||||
elements.mediaEmpty.hidden = detail.media_available;
|
||||
if (detail.media_available) {
|
||||
@@ -212,9 +311,10 @@ function renderDetail(detail) {
|
||||
for (const row of detail.transcript) {
|
||||
const item = document.createElement("div");
|
||||
item.className = `transcript-row${row.is_anchor ? " anchor" : ""}`;
|
||||
if (row.uid) item.dataset.uid = row.uid;
|
||||
const speaker = document.createElement("div");
|
||||
speaker.className = "speaker";
|
||||
speaker.textContent = `P${row.participant_id ?? "?"}`;
|
||||
speaker.textContent = participantIdentity(row, true);
|
||||
const time = document.createElement("small");
|
||||
time.textContent = `${formatTime(row.start_seconds)}–${formatTime(row.end_seconds)}`;
|
||||
speaker.append(time);
|
||||
@@ -225,9 +325,21 @@ function renderDetail(detail) {
|
||||
target.textContent = `Target: ${typeof row.target_of_speech === "string" ? row.target_of_speech : JSON.stringify(row.target_of_speech ?? "unknown")}`;
|
||||
utterance.append(target);
|
||||
item.append(speaker, utterance);
|
||||
if (row.uid) {
|
||||
item.tabIndex = 0;
|
||||
item.title = "定位到对应的 local speaker activity";
|
||||
const activate = () => {
|
||||
const segment = matchingElements(".speech-segment[data-uid]", row.uid)[0] || null;
|
||||
showSpeakerEvidence(row, segment, false);
|
||||
};
|
||||
item.addEventListener("mouseenter", activate);
|
||||
item.addEventListener("focus", activate);
|
||||
item.addEventListener("click", activate);
|
||||
}
|
||||
elements.transcriptList.append(item);
|
||||
}
|
||||
}
|
||||
renderSpeakerTimeline(detail);
|
||||
updateTimeReadout();
|
||||
}
|
||||
|
||||
@@ -244,29 +356,48 @@ async function renderSessionTimeline(detail) {
|
||||
if (state.currentId !== detail.candidate_id) return;
|
||||
const events = state.candidates.filter((item) => item.session === detail.session);
|
||||
const speech = state.sessionSpeech[detail.session];
|
||||
const end = Math.max(1,
|
||||
const sessionEnd = Math.max(1,
|
||||
...events.map((item) => Number(item.event_time_seconds) || 0),
|
||||
...speech.map((row) => Number(row.end_seconds) || 0));
|
||||
const speakers = [...new Set(speech.map((row) => row.participant_role === "wearer" ? "W" : `P${row.participant_id}`))];
|
||||
const visibleDuration = sessionEnd / state.timelineZoom;
|
||||
const center = Number.isFinite(state.timelineCenter) ? state.timelineCenter : sessionEnd / 2;
|
||||
const viewStart = Math.max(0, Math.min(sessionEnd - visibleDuration, center - visibleDuration / 2));
|
||||
const viewEnd = viewStart + visibleDuration;
|
||||
elements.timelineZoomLabel.textContent = `${state.timelineZoom}×`;
|
||||
elements.timelineRange.textContent = `${formatTime(viewStart)} – ${formatTime(viewEnd)} / ${formatTime(sessionEnd)}`;
|
||||
const position = (seconds) => 100 * (seconds - viewStart) / visibleDuration;
|
||||
const speakers = [...new Set(speech.map((row) => participantIdentity(row)))];
|
||||
for (const speakerId of speakers) {
|
||||
const lane = document.createElement("div"); lane.className = "session-lane";
|
||||
const label = document.createElement("strong"); label.textContent = speakerId;
|
||||
const track = document.createElement("div"); track.className = "session-track";
|
||||
for (const row of speech.filter((row) => (row.participant_role === "wearer" ? "W" : `P${row.participant_id}`) === speakerId)) {
|
||||
for (const row of speech.filter((row) =>
|
||||
participantIdentity(row) === speakerId &&
|
||||
Number(row.end_seconds) > viewStart && Number(row.start_seconds) < viewEnd)) {
|
||||
const segment = document.createElement("span");
|
||||
segment.className = `session-speech${speakerId === "W" ? " wearer" : ""}`;
|
||||
segment.style.left = `${100 * Number(row.start_seconds) / end}%`;
|
||||
segment.style.width = `${Math.max(0.08, 100 * (Number(row.end_seconds) - Number(row.start_seconds)) / end)}%`;
|
||||
segment.className = `session-speech${row.participant_role === "wearer" ? " wearer" : ""}`;
|
||||
const start = Math.max(viewStart, Number(row.start_seconds));
|
||||
const end = Math.min(viewEnd, Number(row.end_seconds));
|
||||
segment.style.left = `${position(start)}%`;
|
||||
segment.style.width = `${Math.max(0.15, 100 * (end - start) / visibleDuration)}%`;
|
||||
track.append(segment);
|
||||
}
|
||||
lane.append(label, track); elements.sessionTimeline.append(lane);
|
||||
}
|
||||
const eventTrack = document.createElement("div"); eventTrack.className = "session-events";
|
||||
for (const item of events) {
|
||||
for (const item of events.filter((item) => {
|
||||
const start = Number(item.region_start_seconds ?? item.event_time_seconds);
|
||||
const end = Number(item.region_end_seconds ?? item.event_time_seconds);
|
||||
return end >= viewStart && start <= viewEnd;
|
||||
})) {
|
||||
const marker = document.createElement("button");
|
||||
marker.type = "button";
|
||||
marker.className = `session-marker ${item.family}${item.candidate_id === detail.candidate_id ? " current" : ""}`;
|
||||
marker.style.left = `${Math.min(100, 100 * (Number(item.event_time_seconds) || 0) / end)}%`;
|
||||
const eventTime = Number(item.event_time_seconds) || 0;
|
||||
const regionStart = Math.max(viewStart, Number(item.region_start_seconds ?? eventTime));
|
||||
const regionEnd = Math.min(viewEnd, Number(item.region_end_seconds ?? eventTime));
|
||||
marker.style.left = `${Math.max(0, Math.min(100, position(regionStart)))}%`;
|
||||
marker.style.width = `${Math.max(0.35, 100 * Math.max(0, regionEnd - regionStart) / visibleDuration)}%`;
|
||||
marker.title = `${formatTime(item.event_time_seconds)} · ${item.candidate_source || item.automatic_label} · ${item.ai_verdict || "未粗标"}`;
|
||||
marker.addEventListener("click", () => selectCandidate(item.candidate_id));
|
||||
eventTrack.append(marker);
|
||||
@@ -274,6 +405,28 @@ async function renderSessionTimeline(detail) {
|
||||
elements.sessionTimeline.append(eventTrack);
|
||||
}
|
||||
|
||||
function changeTimelineZoom(multiplier) {
|
||||
state.timelineZoom = Math.max(1, Math.min(64, state.timelineZoom * multiplier));
|
||||
if (state.currentDetail) renderSessionTimeline(state.currentDetail);
|
||||
}
|
||||
|
||||
function panTimeline(direction) {
|
||||
if (!state.currentDetail) return;
|
||||
const speech = state.sessionSpeech[state.currentDetail.session] || [];
|
||||
const events = state.candidates.filter((item) => item.session === state.currentDetail.session);
|
||||
const sessionEnd = Math.max(1, ...speech.map((row) => Number(row.end_seconds) || 0),
|
||||
...events.map((item) => Number(item.event_time_seconds) || 0));
|
||||
state.timelineCenter = (state.timelineCenter ?? sessionEnd / 2) + direction * sessionEnd / state.timelineZoom * 0.7;
|
||||
renderSessionTimeline(state.currentDetail);
|
||||
}
|
||||
|
||||
function focusTimeline() {
|
||||
if (!state.currentDetail) return;
|
||||
state.timelineCenter = Number(state.currentDetail.event_time_seconds);
|
||||
if (state.timelineZoom === 1) state.timelineZoom = 8;
|
||||
renderSessionTimeline(state.currentDetail);
|
||||
}
|
||||
|
||||
function renderSpeakerTimeline(detail) {
|
||||
elements.speakerTimeline.replaceChildren();
|
||||
const rows = detail.candidate.speaker_timeline || detail.candidate.context || [];
|
||||
@@ -285,30 +438,41 @@ function renderSpeakerTimeline(detail) {
|
||||
const left = Math.min(...rows.map((row) => Number(row.start_seconds)), eventTime - 3);
|
||||
const right = Math.max(...rows.map((row) => Number(row.end_seconds)), eventTime + 3);
|
||||
const span = Math.max(0.001, right - left);
|
||||
const speakers = [...new Set(rows.map((row) => `${row.participant_role === "wearer" ? "W" : `P${row.participant_id}`}`))];
|
||||
const speakers = [...new Set(rows.map((row) => participantIdentity(row)))];
|
||||
for (const speakerId of speakers) {
|
||||
const lane = document.createElement("div");
|
||||
lane.className = "speaker-lane";
|
||||
const label = document.createElement("strong"); label.textContent = speakerId;
|
||||
const track = document.createElement("div"); track.className = "speaker-track";
|
||||
for (const row of rows.filter((row) => (row.participant_role === "wearer" ? "W" : `P${row.participant_id}`) === speakerId)) {
|
||||
const segment = document.createElement("span");
|
||||
segment.className = `speech-segment${speakerId === "W" ? " wearer" : ""}`;
|
||||
for (const row of rows.filter((row) => participantIdentity(row) === speakerId)) {
|
||||
const segment = document.createElement("button");
|
||||
segment.type = "button";
|
||||
segment.className = `speech-segment${row.participant_role === "wearer" ? " wearer" : ""}`;
|
||||
segment.style.left = `${100 * (Number(row.start_seconds) - left) / span}%`;
|
||||
segment.style.width = `${Math.max(0.8, 100 * (Number(row.end_seconds) - Number(row.start_seconds)) / span)}%`;
|
||||
segment.title = `${formatTime(row.start_seconds)}–${formatTime(row.end_seconds)} ${row.transcription || ""}`;
|
||||
if (row.uid) segment.dataset.uid = row.uid;
|
||||
segment.title = `${participantIdentity(row, true)} · ${formatTime(row.start_seconds)}–${formatTime(row.end_seconds)} · ${row.transcription || "无转写"}`;
|
||||
segment.setAttribute("aria-label", segment.title);
|
||||
segment.addEventListener("mouseenter", () => showSpeakerEvidence(row, segment, false));
|
||||
segment.addEventListener("focus", () => showSpeakerEvidence(row, segment, false));
|
||||
segment.addEventListener("click", () => showSpeakerEvidence(row, segment, true));
|
||||
track.append(segment);
|
||||
}
|
||||
const anchor = document.createElement("i"); anchor.style.left = `${100 * (eventTime - left) / span}%`; track.append(anchor);
|
||||
lane.append(label, track); elements.speakerTimeline.append(lane);
|
||||
}
|
||||
const defaultRow = rows.find((row) =>
|
||||
Number(row.start_seconds) <= eventTime && Number(row.end_seconds) >= eventTime
|
||||
) || rows[0];
|
||||
const defaultSegment = defaultRow?.uid
|
||||
? matchingElements(".speech-segment[data-uid]", defaultRow.uid)[0] || null
|
||||
: null;
|
||||
if (defaultRow) showSpeakerEvidence(defaultRow, defaultSegment, false);
|
||||
}
|
||||
|
||||
function restoreForm(review) {
|
||||
elements.reviewForm.reset();
|
||||
elements.formError.hidden = true;
|
||||
const reviewer = review?.reviewer || localStorage.getItem("floor-reviewer") || "";
|
||||
elements.reviewerInput.value = reviewer;
|
||||
if (!review) {
|
||||
elements.reviewForm.elements.confidence.value = "medium";
|
||||
updateConditionalFields();
|
||||
@@ -335,7 +499,6 @@ function formPayload() {
|
||||
exclude_reason: nullable("exclude_reason"),
|
||||
boundary_correction_seconds: correction === "" ? null : Number(correction),
|
||||
confidence: data.get("confidence"),
|
||||
reviewer: String(data.get("reviewer") || "").trim(),
|
||||
notes: String(data.get("notes") || "").trim(),
|
||||
};
|
||||
}
|
||||
@@ -348,7 +511,6 @@ function validateReview(review) {
|
||||
if (review.human_action === "relabel" && review.human_verdict === "HARD_NEGATIVE" && !review.human_subtype) missing.push("hard-negative subtype");
|
||||
if (review.human_action === "exclude" && !review.exclude_reason) missing.push("排除原因");
|
||||
if (review.human_action === "accept" && !state.currentDetail?.candidate?.ai_verdict && !state.currentDetail?.llm_suggestion?.verdict) missing.push("AI 预标注(尚未运行,需 Relabel)");
|
||||
if (!review.reviewer) missing.push("审核者");
|
||||
return missing;
|
||||
}
|
||||
|
||||
@@ -381,7 +543,6 @@ async function saveCurrent({ advance = true } = {}) {
|
||||
body: JSON.stringify({ candidate_id: state.currentId, review }),
|
||||
});
|
||||
state.reviews[state.currentId] = review;
|
||||
localStorage.setItem("floor-reviewer", review.reviewer);
|
||||
elements.saveState.textContent = "已保存";
|
||||
elements.saveState.classList.add("saved");
|
||||
updateProgress();
|
||||
@@ -456,13 +617,23 @@ function bindEvents() {
|
||||
event.preventDefault();
|
||||
saveCurrent();
|
||||
});
|
||||
elements.uncertainButton.addEventListener("click", () => {
|
||||
elements.reviewForm.elements.human_action.value = "second_review";
|
||||
elements.reviewForm.elements.confidence.value = "low";
|
||||
updateConditionalFields();
|
||||
});
|
||||
elements.reviewForm.elements.human_action.forEach((control) => control.addEventListener("change", updateConditionalFields));
|
||||
elements.reviewForm.elements.human_verdict.addEventListener("change", updateConditionalFields);
|
||||
elements.timelineZoomIn.addEventListener("click", () => changeTimelineZoom(2));
|
||||
elements.timelineZoomOut.addEventListener("click", () => changeTimelineZoom(0.5));
|
||||
elements.timelinePanLeft.addEventListener("click", () => panTimeline(-1));
|
||||
elements.timelinePanRight.addEventListener("click", () => panTimeline(1));
|
||||
elements.timelineFocus.addEventListener("click", focusTimeline);
|
||||
elements.timelineReset.addEventListener("click", () => {
|
||||
state.timelineZoom = 1;
|
||||
state.timelineCenter = Number(state.currentDetail?.event_time_seconds ?? 0);
|
||||
renderSessionTimeline(state.currentDetail);
|
||||
});
|
||||
elements.sessionTimeline.addEventListener("wheel", (event) => {
|
||||
if (!event.shiftKey) return;
|
||||
event.preventDefault();
|
||||
changeTimelineZoom(event.deltaY < 0 ? 2 : 0.5);
|
||||
}, { passive: false });
|
||||
elements.jumpEvent.addEventListener("click", jumpToEvent);
|
||||
document.querySelectorAll("[data-seek]").forEach((button) =>
|
||||
button.addEventListener("click", () => seek(Number(button.dataset.seek)))
|
||||
|
||||
@@ -29,33 +29,55 @@
|
||||
</aside>
|
||||
|
||||
<section class="evidence-panel panel">
|
||||
<div class="event-header"><div><div class="event-badges"><span id="familyBadge" class="badge accent">—</span><span id="splitBadge" class="badge">—</span><span id="autoConfidenceBadge" class="badge">—</span></div><h2 id="eventTitle">请选择候选</h2><p id="eventSubtitle">—</p></div><div class="event-position"><strong id="eventPosition">0 / 0</strong></div></div>
|
||||
<div class="event-header"><div><div class="event-badges"><span id="familyBadge" class="badge accent">—</span><span id="splitBadge" class="badge">—</span></div><h2 id="eventTitle">请选择候选</h2><p id="eventSubtitle">—</p></div><div class="event-position"><strong id="eventPosition">0 / 0</strong></div></div>
|
||||
|
||||
<div id="llmSuggestion" class="llm-suggestion" hidden><strong>AI 预标注(不是最终标签)</strong><span id="llmSuggestionText">—</span></div>
|
||||
|
||||
<section class="timeline-section">
|
||||
<div class="section-heading"><div><span class="eyebrow">SESSION OVERVIEW</span><h3>本 Session 候选分布</h3></div><span>点击 marker 跳转</span></div>
|
||||
<div id="sessionTimeline" class="session-timeline"></div>
|
||||
<div class="section-heading local-heading"><div><span class="eyebrow">LOCAL SPEAKER ACTIVITY</span><h3>事件附近 participant timeline</h3></div><span>原始标注,只读</span></div>
|
||||
<div id="speakerTimeline" class="speaker-timeline"></div>
|
||||
<section class="decision-guide" aria-label="当前审核目标">
|
||||
<div class="review-question"><span>本条需要判断</span><strong id="reviewQuestion">—</strong></div>
|
||||
<div id="llmSuggestion" class="ai-decision-card" hidden>
|
||||
<div class="ai-decision-heading"><span><i></i>AI 建议</span><em id="aiConfidence">置信度 —</em></div>
|
||||
<div class="ai-decision-main">
|
||||
<strong id="aiVerdict" class="ai-verdict">—</strong>
|
||||
<span class="decision-arrow">→</span>
|
||||
<div><b id="aiLabel">—</b><p id="aiMeaning">—</p></div>
|
||||
</div>
|
||||
<div class="ai-rationale"><span>判断依据</span><p id="aiRationale">—</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="video-shell"><video id="eventVideo" controls preload="metadata"></video><div id="mediaEmpty" class="empty-state" hidden><strong>未找到视频</strong><span>检查 dataset root 与 clip。</span></div></div>
|
||||
<div class="transport"><button type="button" data-seek="-2">−2 s</button><button type="button" data-seek="-0.2">−0.2 s</button><button id="jumpEvent" class="jump-event" type="button">回到事件</button><button type="button" data-seek="0.2">+0.2 s</button><button type="button" data-seek="2">+2 s</button><span id="timeReadout">—</span></div>
|
||||
<section class="timeline-section">
|
||||
<details class="session-overview-details">
|
||||
<summary><span><b>Session overview</b><small>全局候选与说话活动</small></span><em>按需展开</em></summary>
|
||||
<div class="session-overview-body">
|
||||
<div class="section-heading timeline-heading"><div><span class="eyebrow">SESSION OVERVIEW</span><h3>本 Session 候选分布</h3></div><div class="timeline-controls" aria-label="时间轴缩放"><button id="timelinePanLeft" type="button" title="向前平移">←</button><button id="timelineZoomOut" type="button" title="缩小">−</button><button id="timelineFocus" type="button" title="聚焦当前事件">聚焦</button><button id="timelineZoomIn" type="button" title="放大">+</button><button id="timelinePanRight" type="button" title="向后平移">→</button><button id="timelineReset" type="button" title="显示完整 Session">全局</button></div></div>
|
||||
<div class="timeline-readout"><strong id="timelineZoomLabel">8×</strong><span id="timelineRange">—</span><small>marker 跳转 · Shift + 滚轮缩放</small></div>
|
||||
<div id="sessionTimeline" class="session-timeline"></div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="section-heading local-heading"><div><span class="eyebrow">LOCAL SPEAKER ACTIVITY</span><h3>事件附近 participant timeline</h3></div><span>W = wearer · P# = Participant_ID</span></div>
|
||||
<div id="speakerTimeline" class="speaker-timeline"></div>
|
||||
<div id="speakerEvidence" class="speaker-evidence" aria-live="polite"><span>悬停或点击任意 activity 区段,转写会固定显示在这里。</span></div>
|
||||
</section>
|
||||
|
||||
<section class="transcript-section"><div class="section-heading"><div><span class="eyebrow">TRANSCRIPT</span><h3>事件上下文</h3></div><span id="clipLabel">—</span></div><div id="transcriptList" class="transcript-list"><div class="empty-copy">选择事件后显示 transcript。</div></div></section>
|
||||
<details class="raw-details"><summary>查看 structured timing 与原始候选</summary><pre id="rawCandidate">{}</pre></details>
|
||||
<div class="evidence-grid">
|
||||
<div class="media-column">
|
||||
<div class="video-shell"><video id="eventVideo" controls preload="metadata"></video><div id="mediaEmpty" class="empty-state" hidden><strong>未找到视频</strong><span>检查 dataset root 与 clip。</span></div></div>
|
||||
<div class="transport"><button type="button" data-seek="-2">−2 s</button><button type="button" data-seek="-0.2">−0.2 s</button><button id="jumpEvent" class="jump-event" type="button">事件点</button><button type="button" data-seek="0.2">+0.2 s</button><button type="button" data-seek="2">+2 s</button><span id="timeReadout">—</span></div>
|
||||
</div>
|
||||
<div class="transcript-column">
|
||||
<section class="transcript-section"><div class="section-heading"><div><span class="eyebrow">TRANSCRIPT</span><h3>事件上下文</h3></div><span id="clipLabel">—</span></div><div id="transcriptList" class="transcript-list"><div class="empty-copy">选择事件后显示 transcript。</div></div></section>
|
||||
<details class="raw-details"><summary>Structured timing / 原始候选</summary><pre id="rawCandidate">{}</pre></details>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="review-panel panel">
|
||||
<div class="panel-heading review-heading"><div><span class="eyebrow">HUMAN REVIEW</span><h2>只做一个主操作</h2></div><span id="saveState" class="save-state">未保存</span></div>
|
||||
<div class="panel-heading review-heading"><div><span class="eyebrow">HUMAN REVIEW</span><h2>选择你的操作</h2></div><span id="saveState" class="save-state">未保存</span></div>
|
||||
<form id="reviewForm">
|
||||
<fieldset><legend>确认 AI 判断,或改标 <span class="required">必填</span></legend>
|
||||
<fieldset><legend>AI 建议正确就接受,否则改标 <span class="required">必填</span></legend>
|
||||
<div class="choice-grid two action-grid">
|
||||
<label><input type="radio" name="human_action" value="accept" /><span><b>Accept</b><small>接受 AI verdict/type</small></span></label>
|
||||
<label class="accept-option"><input id="acceptRadio" type="radio" name="human_action" value="accept" /><span><em>AI 建议</em><b id="acceptTitle">接受 AI 结论</b><small id="acceptSummary">载入候选后显示具体标签</small></span></label>
|
||||
<label><input type="radio" name="human_action" value="relabel" /><span><b>Relabel</b><small>展开并选择正确标签</small></span></label>
|
||||
<label><input type="radio" name="human_action" value="exclude" /><span><b>Exclude</b><small>数据不可可靠评分</small></span></label>
|
||||
<label><input type="radio" name="human_action" value="second_review" /><span><b>Unsure / 二审</b><small>证据不足,交给二审</small></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -69,10 +91,10 @@
|
||||
|
||||
<details class="boundary-details"><summary>仅在原 annotation 明显错误时校正边界</summary><label><span>可选校正(原时间始终保留)</span><select name="boundary_correction_seconds"><option value="">不校正</option><option value="-0.2">−0.2 s</option><option value="0.2">+0.2 s</option></select></label></details>
|
||||
|
||||
<div class="inline-fields"><label><span>置信度</span><select name="confidence"><option value="high">高</option><option value="medium" selected>中</option><option value="low">低</option></select></label><label><span>审核者</span><input id="reviewerInput" name="reviewer" maxlength="100" placeholder="姓名或代号" required /></label></div>
|
||||
<label class="text-field"><span>备注(可选)</span><textarea name="notes" maxlength="4000" rows="3" placeholder="只记录必要的歧义或证据"></textarea></label>
|
||||
<label class="confidence-field"><span>置信度</span><select name="confidence"><option value="high">高</option><option value="medium" selected>中</option><option value="low">低</option></select></label>
|
||||
<details class="optional-details"><summary>备注(可选)</summary><label class="text-field"><textarea name="notes" maxlength="4000" rows="2" placeholder="只记录必要的歧义或证据"></textarea></label></details>
|
||||
<div id="formError" class="form-error" role="alert" hidden></div>
|
||||
<div class="review-actions"><button id="uncertainButton" class="button ghost" type="button">送二审</button><button id="saveButton" class="button primary" type="submit">保存并看下一个 <kbd>⌘↵</kbd></button></div>
|
||||
<div class="review-actions"><button id="saveButton" class="button primary" type="submit">保存并看下一个 <kbd>⌘↵</kbd></button></div>
|
||||
</form>
|
||||
<div class="shortcuts"><span><kbd>←</kbd><kbd>→</kbd> 前后事件</span><span><kbd>[</kbd><kbd>]</kbd> ±200 ms</span><span><kbd>Space</kbd> 播放/暂停</span></div>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user