doocus update and meeting list export

This commit is contained in:
Mariano Gabriel
2026-07-05 11:15:57 -03:00
parent fa3a0e3d1a
commit 300806b936
17 changed files with 614 additions and 628 deletions

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { computed } from 'vue'
import { marked } from 'marked'
import SplitPane from '@framework/components/SplitPane.vue'
import { store, humanBytes } from '@/store'
const d = computed(() => store.detail)
const node = computed(() => d.value?.node)
type ViewerKind = 'pdf' | 'image' | 'html' | 'markdown' | 'text' | 'office' | 'video' | 'download'
const viewerKind = computed<ViewerKind>(() => {
const ext = node.value?.ext ?? ''
if (ext === 'pdf') return 'pdf'
if (['png', 'jpg', 'jpeg'].includes(ext)) return 'image'
if (['html', 'htm'].includes(ext)) return 'html'
if (['md', 'markdown'].includes(ext)) return 'markdown'
if (['txt', 'json', 'yaml', 'yml', 'csv'].includes(ext)) return 'text'
if (['docx', 'pptx', 'xlsx'].includes(ext)) return 'office'
if (ext === 'mp4') return 'video'
return 'download'
})
// Left pane (extracted search text) only exists for extracted heavy formats.
const hasExtracted = computed(() => node.value?.mode === 'extracted' && d.value?.content != null)
const extractedHtml = computed(() =>
d.value?.content ? marked.parse(d.value.content) : '')
// Right pane text (for text-native originals rendered inline).
const inlineText = computed(() => d.value?.text ?? '')
const inlineMarkdown = computed(() =>
viewerKind.value === 'markdown' && inlineText.value ? marked.parse(inlineText.value) : '')
const metaRows = computed(() => {
const m = d.value?.meta as Record<string, any> | null
if (!m) return []
const out: Array<[string, string]> = []
const push = (k: string, v: unknown) => {
if (v == null || v === '') return
out.push([k, Array.isArray(v) ? v.join(', ') : typeof v === 'object' ? JSON.stringify(v) : String(v)])
}
push('title', m.title); push('author', m.author); push('created', m.created)
push('pages', m.pages); push('slides', m.slides); push('sheets', m.sheets)
push('words', m.word_count); push('extractor', m.extractor)
return out
})
</script>
<template>
<div v-if="!node" class="empty">Select a file to view it.</div>
<div v-else class="detail">
<header class="head">
<span class="badge" :data-mode="node.mode">{{ node.mode }}</span>
<span class="name">{{ node.path }}</span>
<span class="dim">{{ node.ext }} · {{ humanBytes(node.bytes) }}</span>
<a class="dl" :href="d?.originalUrl" :download="node.name"> original</a>
</header>
<!-- extracted: search text (left, secondary) | native original (right) -->
<SplitPane v-if="hasExtracted" class="body" :initial-size="1" :min="0.3" :max="3">
<template #first>
<div class="pane">
<div class="pane-label">Extracted text · search index (not the document)</div>
<div class="md" v-html="extractedHtml" />
</div>
</template>
<template #second>
<div class="pane viewer">
<template v-if="viewerKind === 'pdf'">
<iframe class="frame" :src="d?.originalUrl" title="pdf" />
</template>
<div v-else class="office">
<p>No inline preview for <b>.{{ node.ext }}</b>.</p>
<a class="btn" :href="d?.originalUrl" :download="node.name">Download original</a>
<p class="dim">The extracted text on the left is a search aid, not a faithful render.</p>
</div>
</div>
</template>
</SplitPane>
<!-- non-extracted: single native viewer of the original -->
<div v-else class="body single">
<div v-if="viewerKind === 'image'" class="viewer center">
<img :src="d?.originalUrl" :alt="node.name" />
</div>
<iframe v-else-if="viewerKind === 'html'" class="frame" :src="d?.originalUrl" sandbox="" title="html" />
<div v-else-if="viewerKind === 'markdown'" class="md pad" v-html="inlineMarkdown" />
<pre v-else-if="viewerKind === 'text'" class="mono">{{ inlineText }}</pre>
<div v-else-if="viewerKind === 'video'" class="viewer center meeting">
<video class="video" :src="d?.originalUrl" controls />
<p class="dim">Meeting its transcript is produced by meetus, not doocus.</p>
</div>
<div v-else class="office">
<p>No inline preview for <b>.{{ node.ext }}</b>.</p>
<a class="btn" :href="d?.originalUrl" :download="node.name">Download original</a>
</div>
</div>
<footer v-if="metaRows.length" class="meta">
<span v-for="[k, v] in metaRows" :key="k" class="chip"><b>{{ k }}</b> {{ v }}</span>
</footer>
</div>
</template>
<style scoped>
.empty { padding: var(--space-4); color: var(--text-secondary); }
.detail { display: flex; flex-direction: column; height: 100%; }
.head {
display: flex; align-items: center; gap: var(--space-3);
padding: var(--space-2) var(--space-3); border-bottom: var(--panel-border); flex-shrink: 0;
}
.head .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
.head .dim, .dim { color: var(--text-secondary); font-size: var(--font-size-sm); }
.head .dl { margin-left: auto; }
.badge {
font-size: 10px; text-transform: uppercase; padding: 1px 5px; border-radius: 3px;
background: var(--surface-3); color: var(--text-secondary);
}
.badge[data-mode='extracted'] { background: var(--status-live, #2a6); color: #fff; }
.badge[data-mode='meeting'] { background: var(--status-processing, #a60); color: #fff; }
.body { flex: 1; min-height: 0; }
.body.single { overflow: auto; }
.pane { height: 100%; display: flex; flex-direction: column; overflow: hidden; }
.pane-label {
font-size: 11px; color: var(--text-secondary); padding: var(--space-1) var(--space-3);
border-bottom: 1px solid var(--surface-2); flex-shrink: 0;
}
.md { padding: var(--space-3); overflow: auto; }
.md.pad { padding: var(--space-4); }
.viewer { height: 100%; overflow: auto; }
.viewer.center { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-3); }
.frame { width: 100%; height: 100%; border: 0; }
.viewer img { max-width: 100%; }
.video { max-width: 100%; max-height: 70vh; }
.mono { margin: 0; padding: var(--space-4); white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-mono, monospace); font-size: var(--font-size-sm); }
.office { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); align-items: flex-start; }
.btn { padding: var(--space-1) var(--space-3); background: var(--surface-2); border: var(--panel-border); border-radius: var(--panel-radius); }
.meta { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-top: var(--panel-border); flex-shrink: 0; }
.chip { font-size: var(--font-size-sm); color: var(--text-secondary); }
.chip b { color: var(--text-primary); }
</style>