embedded meetus

This commit is contained in:
Mariano Gabriel
2026-07-05 21:35:09 -03:00
parent a92615d6bc
commit 8ff29fc776
8 changed files with 167 additions and 138 deletions

View File

@@ -1,14 +1,7 @@
<script setup lang="ts">
import SplitPane from '@framework/components/SplitPane.vue'
import Panel from '@framework/components/Panel.vue'
import VideoPlayer from '@framework/components/VideoPlayer.vue'
import RunPicker from '@/components/RunPicker.vue'
import TranscriptEditor from '@/components/TranscriptEditor.vue'
import FrameSelector from '@/components/FrameSelector.vue'
import ExportBar from '@/components/ExportBar.vue'
import { useStore } from '@/store'
const { state } = useStore()
import ReviewBody from '@/components/ReviewBody.vue'
</script>
<template>
@@ -20,34 +13,7 @@ const { state } = useStore()
</header>
<div class="body">
<SplitPane direction="horizontal" :initialSize="1.4" sizeMode="ratio" :min="0.4" :max="4">
<template #first>
<SplitPane direction="vertical" :initialSize="1.3" sizeMode="ratio" :min="0.3" :max="4">
<template #first>
<Panel title="Video" :status="state.playing ? 'live' : 'idle'">
<VideoPlayer
v-if="state.videoUrl"
:src="state.videoUrl"
v-model:currentTime="state.currentTime"
v-model:playing="state.playing"
@duration="state.duration = $event"
/>
<div v-else class="placeholder">
{{ state.currentRunId
? 'Source video not available at the path recorded in manifest.json.'
: 'Select a meeting to begin.' }}
</div>
</Panel>
</template>
<template #second>
<FrameSelector />
</template>
</SplitPane>
</template>
<template #second>
<TranscriptEditor />
</template>
</SplitPane>
<ReviewBody :allow-edit="true" />
</div>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, watch, nextTick, computed } from 'vue'
import Panel from '@framework/components/Panel.vue'
import { useStore } from '@/store'
import { useStore } from '../store'
const { state, activeFrameIndex, selectedFrames, seek, setSelectAll } = useStore()

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
/**
* The meetus review — pure composition of video + frames + transcript over the
* shared store. Uses only relative + @framework imports so it is portable: the
* doocus app embeds this same component (pointed at its own meetus-shaped API).
* Config is by prop; all logic lives in the store and child components.
*/
import SplitPane from '@framework/components/SplitPane.vue'
import Panel from '@framework/components/Panel.vue'
import VideoPlayer from '@framework/components/VideoPlayer.vue'
import TranscriptEditor from './TranscriptEditor.vue'
import FrameSelector from './FrameSelector.vue'
import { useStore } from '../store'
withDefaults(defineProps<{ allowEdit?: boolean; embedded?: boolean }>(), {
allowEdit: true, embedded: false,
})
const { state } = useStore()
</script>
<template>
<div class="review-body">
<SplitPane direction="horizontal" :initialSize="1.4" sizeMode="ratio" :min="0.4" :max="4">
<template #first>
<SplitPane direction="vertical" :initialSize="1.3" sizeMode="ratio" :min="0.3" :max="4">
<template #first>
<Panel title="Video" :status="state.playing ? 'live' : 'idle'">
<VideoPlayer
v-if="state.videoUrl"
:src="state.videoUrl"
v-model:currentTime="state.currentTime"
v-model:playing="state.playing"
@duration="state.duration = $event"
/>
<div v-else class="placeholder">
{{ state.currentRunId
? 'Source video not available at the path recorded in manifest.json.'
: 'Select a meeting to begin.' }}
</div>
</Panel>
</template>
<template #second><FrameSelector /></template>
</SplitPane>
</template>
<template #second><TranscriptEditor :allow-edit="allowEdit" /></template>
</SplitPane>
</div>
</template>
<style scoped>
.review-body { height: 100%; }
.placeholder {
display: flex; align-items: center; justify-content: center; height: 100%;
padding: var(--space-6); text-align: center; color: var(--text-secondary);
}
</style>

View File

@@ -1,13 +1,17 @@
<script setup lang="ts">
import { ref, watch, nextTick, computed, onMounted } from 'vue'
import Panel from '@framework/components/Panel.vue'
import { useStore } from '@/store'
import { useStore } from '../store'
const props = withDefaults(defineProps<{ allowEdit?: boolean }>(), { allowEdit: true })
const { state, activeSegmentIndex, seek } = useStore()
// View preferences (persisted).
// View preferences (persisted). When edit is disallowed (e.g. embedded review),
// the transcript is read-only regardless of the saved preference.
type ViewMode = 'edit' | 'read'
const viewMode = ref<ViewMode>((localStorage.getItem('meetus.viewMode') as ViewMode) || 'edit')
const effectiveMode = computed<ViewMode>(() => (props.allowEdit ? viewMode.value : 'read'))
const compact = ref(localStorage.getItem('meetus.compact') === '1')
const showTimes = ref(localStorage.getItem('meetus.readTimes') !== '0')
watch(viewMode, (v) => localStorage.setItem('meetus.viewMode', v))
@@ -83,17 +87,17 @@ watch(activeSegmentIndex, async (i) => {
<template>
<Panel title="Transcript">
<template #actions>
<div class="seg-control">
<button :class="{ on: viewMode === 'edit' }" @click="viewMode = 'edit'">Edit</button>
<button :class="{ on: viewMode === 'read' }" @click="viewMode = 'read'">Read</button>
<div v-if="allowEdit" class="seg-control">
<button :class="{ on: effectiveMode === 'edit' }" @click="viewMode = 'edit'">Edit</button>
<button :class="{ on: effectiveMode === 'read' }" @click="viewMode = 'read'">Read</button>
</div>
<button v-if="viewMode === 'edit'" :class="{ on: compact }" @click="compact = !compact">Compact</button>
<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>
<span class="count">{{ state.segments.length }} segments</span>
</template>
<!-- EDIT: per-segment blocks (comfortable or compact density) -->
<div v-if="viewMode === 'edit'" class="seg-list" :class="{ compact }">
<div v-if="effectiveMode === 'edit'" class="seg-list" :class="{ compact }">
<p v-if="!state.segments.length" class="empty">No transcript for this run.</p>
<div
v-for="(seg, i) in state.segments"
@@ -139,7 +143,7 @@ watch(activeSegmentIndex, async (i) => {
class="rseg"
:class="{ active: it.i === activeSegmentIndex }"
@click="seek(it.start)"
>{{ it.text }} </span>
>{{ it.text.trim() + ' ' }}</span>
</template>
</p>
</div>