attempt to use separate folder for docs and meetings
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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} | ||||