This commit is contained in:
Mariano Gabriel
2026-07-05 11:26:45 -03:00
parent 300806b936
commit 7b276e8f3d
6 changed files with 402 additions and 64 deletions

View File

@@ -9,8 +9,10 @@
"version": "0.1.0",
"dependencies": {
"jszip": "^3.10",
"mammoth": "^1.8",
"marked": "^14",
"vue": "^3.5"
"vue": "^3.5",
"xlsx": "^0.18"
},
"devDependencies": {
"@types/node": "^22",
@@ -1060,6 +1062,24 @@
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
"license": "MIT"
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/alien-signals": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
@@ -1067,6 +1087,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1074,6 +1103,32 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
@@ -1084,12 +1139,46 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -1103,6 +1192,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/dingbat-to-unicode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause"
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -1181,6 +1285,15 @@
}
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1245,6 +1358,17 @@
"immediate": "~3.0.5"
}
},
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1254,6 +1378,30 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mammoth": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"bluebird": "~3.4.0",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"path-is-absolute": "^1.0.0",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/marked": {
"version": "14.1.4",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz",
@@ -1307,6 +1455,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -1320,6 +1474,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1454,6 +1617,24 @@
"node": ">=0.10.0"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -1494,6 +1675,12 @@
"node": ">=14.17"
}
},
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -1626,6 +1813,54 @@
"peerDependencies": {
"typescript": ">=5.0.0"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
}
}
}

View File

@@ -11,8 +11,10 @@
},
"dependencies": {
"jszip": "^3.10",
"mammoth": "^1.8",
"marked": "^14",
"vue": "^3.5"
"vue": "^3.5",
"xlsx": "^0.18"
},
"devDependencies": {
"@types/node": "^22",

View File

@@ -2,35 +2,18 @@
import { computed } from 'vue'
import { marked } from 'marked'
import SplitPane from '@framework/components/SplitPane.vue'
import NativeViewer from '@/components/NativeViewer.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'
// Left pane (extracted search text) only exists for extracted heavy formats,
// and not for pptx (whose viewer already shows the per-slide text).
const hasExtracted = computed(() =>
node.value?.mode === 'extracted' && node.value.ext !== 'pptx' && d.value?.content != null)
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 extractedHtml = computed(() => (d.value?.content ? marked.parse(d.value.content) : ''))
const metaRows = computed(() => {
const m = d.value?.meta as Record<string, any> | null
@@ -54,11 +37,10 @@ const metaRows = computed(() => {
<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">
<SplitPane v-if="hasExtracted" class="body" :initial-size="1" :min="0.25" :max="3">
<template #first>
<div class="pane">
<div class="pane-label">Extracted text · search index (not the document)</div>
@@ -66,35 +48,13 @@ const metaRows = computed(() => {
</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>
<NativeViewer :node="node" :original-url="d!.originalUrl" :content="d?.content" :text="d?.text" />
</template>
</SplitPane>
<!-- non-extracted: single native viewer of the original -->
<!-- link / pptx / meeting: single native viewer -->
<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>
<NativeViewer :node="node" :original-url="d!.originalUrl" :content="d?.content" :text="d?.text" />
</div>
<footer v-if="metaRows.length" class="meta">
@@ -112,7 +72,6 @@ const metaRows = computed(() => {
}
.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);
@@ -120,22 +79,13 @@ const metaRows = computed(() => {
.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; }
.body.single { overflow: hidden; }
.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); }

View File

@@ -0,0 +1,112 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { marked } from 'marked'
import type { FileNode } from '@/store'
const props = defineProps<{
node: FileNode
originalUrl: string
content?: string | null // extracted text (pptx fallback render)
text?: string | null // inline text for text-native originals
}>()
type Kind = 'pdf' | 'image' | 'html' | 'markdown' | 'text' | 'docx' | 'xlsx' | 'pptx' | 'video' | 'none'
const kind = computed<Kind>(() => {
const e = props.node.ext
if (e === 'pdf') return 'pdf'
if (['png', 'jpg', 'jpeg'].includes(e)) return 'image'
if (['html', 'htm'].includes(e)) return 'html'
if (['md', 'markdown'].includes(e)) return 'markdown'
if (['txt', 'json', 'yaml', 'yml', 'csv'].includes(e)) return 'text'
if (e === 'docx') return 'docx'
if (e === 'xlsx') return 'xlsx'
if (e === 'pptx') return 'pptx'
if (e === 'mp4') return 'video'
return 'none'
})
// Async-rendered HTML for docx (mammoth) and xlsx (SheetJS).
const rendered = ref('')
const loading = ref(false)
const loadError = ref('')
async function loadRich(): Promise<void> {
rendered.value = ''
loadError.value = ''
if (kind.value !== 'docx' && kind.value !== 'xlsx') return
loading.value = true
try {
const res = await fetch(props.originalUrl)
const buf = await res.arrayBuffer()
if (kind.value === 'docx') {
const mammoth = await import('mammoth/mammoth.browser')
rendered.value = (await mammoth.convertToHtml({ arrayBuffer: buf })).value
} else {
const XLSX = await import('xlsx')
const wb = XLSX.read(new Uint8Array(buf), { type: 'array' })
rendered.value = wb.SheetNames.map((name) =>
`<h3>${name}</h3>` + XLSX.utils.sheet_to_html(wb.Sheets[name])).join('\n')
}
} catch (e) {
loadError.value = String(e)
} finally {
loading.value = false
}
}
watch(() => props.node.path, loadRich, { immediate: true })
const mdHtml = computed(() => (props.text ? marked.parse(props.text) : ''))
const pptxHtml = computed(() => (props.content ? marked.parse(props.content) : ''))
</script>
<template>
<div class="nv">
<iframe v-if="kind === 'pdf'" class="frame" :src="originalUrl" title="pdf" />
<div v-else-if="kind === 'image'" class="center"><img :src="originalUrl" :alt="node.name" /></div>
<iframe v-else-if="kind === 'html'" class="frame" :src="originalUrl" sandbox="" title="html" />
<div v-else-if="kind === 'markdown'" class="doc md" v-html="mdHtml" />
<pre v-else-if="kind === 'text'" class="mono">{{ text }}</pre>
<div v-else-if="kind === 'video'" class="center">
<video class="video" :src="originalUrl" controls />
<p class="dim">Meeting its transcript is produced by meetus, not doocus.</p>
</div>
<!-- docx / xlsx rendered client-side -->
<div v-else-if="kind === 'docx' || kind === 'xlsx'" class="doc rich">
<div v-if="loading" class="dim">Rendering</div>
<div v-else-if="loadError" class="dim">Preview failed: {{ loadError }}</div>
<div v-else v-html="rendered" />
</div>
<!-- pptx: no faithful client render; show the per-slide extracted text -->
<div v-else-if="kind === 'pptx'" class="doc md">
<div class="dim label">Slide text (no visual render)</div>
<div v-html="pptxHtml" />
</div>
<div v-else class="center dim">No preview for .{{ node.ext }}.</div>
</div>
</template>
<style scoped>
.nv { height: 100%; overflow: auto; }
.frame { width: 100%; height: 100%; border: 0; }
.center { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-3); }
.center img { max-width: 100%; }
.video { max-width: 100%; max-height: 70vh; }
.doc { padding: var(--space-4); }
.doc.rich :deep(table) { border-collapse: collapse; }
.doc.rich :deep(td), .doc.rich :deep(th) { border: 1px solid var(--surface-3); padding: 2px 6px; }
.doc.rich :deep(h3) { margin: var(--space-3) 0 var(--space-1); }
.md :deep(img) { max-width: 100%; }
.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); }
.dim { color: var(--text-secondary); }
.label { font-size: 11px; margin-bottom: var(--space-2); }
</style>

View File

@@ -5,3 +5,10 @@ declare module '*.vue' {
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
export default component
}
// mammoth ships a browser bundle without bundled types for that entry point.
declare module 'mammoth/mammoth.browser' {
export function convertToHtml(
input: { arrayBuffer: ArrayBuffer },
): Promise<{ value: string; messages: unknown[] }>
}