attempt to use separate folder for docs and meetings

This commit is contained in:
Mariano Gabriel
2026-07-06 01:18:17 -03:00
parent bb84558526
commit 31ad8e5928
10 changed files with 261 additions and 62 deletions

View File

@@ -5,7 +5,21 @@ import { useStore } from '../store'
const props = withDefaults(defineProps<{ allowEdit?: boolean }>(), { allowEdit: true })
const { state, activeSegmentIndex, seek } = useStore()
const { state, activeSegmentIndex, seek, setSelectMode, setSegmentIncluded } = useStore()
// Turn-level include (a turn = a speaker's continuous run of segments).
function turnIncluded(items: Array<{ i: number }>): boolean {
return items.length > 0 && items.every((it) => state.segments[it.i]?.included)
}
function toggleTurn(items: Array<{ i: number }>): void {
const val = !turnIncluded(items)
for (const it of items) setSegmentIncluded(it.i, val)
}
function toggleSelect(): void {
const on = !state.selectMode
setSelectMode(on)
if (on) viewMode.value = 'read' // selection lives in the read view
}
// View preferences (persisted). When edit is disallowed (e.g. embedded review),
// the transcript is read-only regardless of the saved preference.
@@ -93,6 +107,7 @@ watch(activeSegmentIndex, async (i) => {
</div>
<button v-if="allowEdit && effectiveMode === 'edit'" :class="{ on: compact }" @click="compact = !compact">Compact</button>
<button v-else :class="{ on: showTimes }" @click="showTimes = !showTimes">Times</button>
<button :class="{ on: state.selectMode }" title="Pick segments (+ their frames) for the package" @click="toggleSelect">Select</button>
<span class="count">{{ state.segments.length }} segments</span>
</template>
@@ -127,6 +142,14 @@ watch(activeSegmentIndex, async (i) => {
<p v-if="!state.segments.length" class="empty">No transcript for this run.</p>
<div v-for="(turn, ti) in readingTurns" :key="ti" class="turn">
<div class="turn-head">
<input
v-if="state.selectMode"
type="checkbox"
class="turn-pick"
:checked="turnIncluded(turn.items)"
title="Include this part (+ its frames) in the package"
@change="toggleTurn(turn.items)"
/>
<button class="ts" title="Seek here" @click="seek(turn.start)">{{ fmt(turn.start) }}</button>
<span class="speaker-lbl">{{ turn.speaker }}</span>
</div>
@@ -141,7 +164,7 @@ watch(activeSegmentIndex, async (i) => {
><span
:ref="(el) => setSeg(it.i, el as Element | null)"
class="rseg"
:class="{ active: it.i === activeSegmentIndex }"
:class="{ active: it.i === activeSegmentIndex, included: state.selectMode && state.segments[it.i]?.included }"
@click="seek(it.start)"
>{{ it.text.trim() + ' ' }}</span>
</template>
@@ -274,6 +297,10 @@ watch(activeSegmentIndex, async (i) => {
background: var(--surface-2);
box-shadow: 0 0 0 1px var(--status-processing);
}
.rseg.included {
background: color-mix(in srgb, var(--status-live, #2a6) 22%, transparent);
}
.turn-pick { margin-right: var(--space-1); }
.inline-ts {
font-family: var(--font-mono);
font-size: 10px;

View File

@@ -23,6 +23,7 @@ export interface SegmentState {
origSpeaker: string | null
text: string
speaker: string | null
included: boolean // in "select" mode, whether this segment is in the package
}
export interface FrameState {
@@ -56,6 +57,8 @@ interface State {
duration: number
playing: boolean
selectMode: boolean // package = only the included segments + their frames
saveStatus: SaveStatus
}
@@ -78,6 +81,8 @@ const state = reactive<State>({
duration: 0,
playing: false,
selectMode: false,
saveStatus: 'idle',
})
@@ -140,6 +145,7 @@ export function buildTranscript(): string {
const items: Item[] = []
for (const s of state.segments) {
if (!s.text.trim()) continue
if (state.selectMode && !s.included) continue // select mode: only chosen segments
items.push({ type: 'audio', time: s.start, speaker: s.speaker || 'SPEAKER', text: s.text.trim() })
}
for (const f of state.frames) {
@@ -221,6 +227,8 @@ async function openRun(id: string): Promise<void> {
const review = data.review && typeof data.review === 'object' ? data.review : null
const segOverrides = review?.segments ?? {}
const includedSet = new Set<number>(review?.selection?.segments ?? [])
state.selectMode = includedSet.size > 0
state.segments = (data.segments ?? []).map((s: any, i: number): SegmentState => {
const ov = segOverrides[i] ?? segOverrides[String(i)] ?? {}
return {
@@ -230,6 +238,7 @@ async function openRun(id: string): Promise<void> {
origSpeaker: s.speaker ?? null,
text: ov.text ?? s.text,
speaker: ov.speaker ?? s.speaker ?? null,
included: includedSet.has(i),
}
})
@@ -268,6 +277,8 @@ function buildReview(): unknown {
updatedAt: new Date().toISOString(),
segments,
frames: { selected: state.frames.filter((f) => f.selected).map((f) => f.file) },
// Select-mode: which segments are included in the package (indices).
selection: { segments: state.segments.map((s, i) => (s.included ? i : -1)).filter((i) => i >= 0) },
}
}
@@ -291,7 +302,7 @@ async function flushSave(): Promise<void> {
// Watch the editable surface and the frame selection; debounce writes.
watch(
() => [
state.segments.map((s) => `${s.text}${s.speaker ?? ''}`).join(''),
state.segments.map((s) => `${s.text} ${s.speaker ?? ''} ${s.included ? '1' : '0'}`).join(''),
state.frames.map((f) => (f.selected ? f.file : '')).join(''),
],
() => {
@@ -312,6 +323,23 @@ function setSelectAll(selected: boolean): void {
state.frames.forEach((f) => { f.selected = selected })
}
/** Include/exclude a segment in the package, and the frames captured in its time
* window along with it. */
function setSegmentIncluded(i: number, included: boolean): void {
const s = state.segments[i]
if (!s) return
s.included = included
state.frames.forEach((f) => {
if (f.time != null && f.time >= s.start && f.time < s.end) f.selected = included
})
}
/** Toggle the whole select-mode; leaving it clears any partial selection. */
function setSelectMode(on: boolean): void {
state.selectMode = on
if (!on) state.segments.forEach((s) => { s.included = false })
}
export function useStore() {
return {
state,
@@ -324,6 +352,8 @@ export function useStore() {
setOutputDir,
seek,
setSelectAll,
setSegmentIncluded,
setSelectMode,
buildTranscript,
}
}