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

@@ -1,36 +1,31 @@
<script setup lang="ts">
import { ref } from 'vue'
import JSZip from 'jszip'
import {
store, TARGETS, target, packageEstimate, humanBytes,
} from '@/store'
import { store, TARGETS, target, packageEstimate, humanBytes } from '@/store'
const building = ref(false)
const error = ref('')
// Build a zip: per selected doc, a folder holding the original source file plus
// the textified content.md and meta.json. Originals are included because the
// permitted services (Gemini web / NotebookLM) receive them directly; content.md
// keeps each doc greppable and small.
// Build a zip of the selected files: each ORIGINAL (what Gemini/NotebookLM
// actually consume) plus, for extracted formats, the content.md search text.
// Folder structure is preserved. Later this can emit Drive links instead of
// bytes once nodes carry a `url`.
async function buildPackage(): Promise<void> {
if (!store.selected.size) return
building.value = true
error.value = ''
try {
const zip = new JSZip()
for (const id of store.selected) {
const summary = store.docs.find((d) => d.id === id)
const res = await fetch(`/api/docs/${encodeURIComponent(id)}`)
if (!res.ok) throw new Error(`fetch ${id} failed`)
const detail = await res.json()
const folder = zip.folder(id.replace(/[\\/]+/g, '__')) ?? zip
folder.file('content.md', detail.content ?? '')
folder.file('meta.json', JSON.stringify(detail.meta ?? {}, null, 2))
if (detail.originalUrl) {
const bin = await fetch(detail.originalUrl)
if (bin.ok) {
const name = (summary?.name) || 'original'
folder.file(name, await bin.blob())
for (const f of store.files.filter((x) => store.selected.has(x.path))) {
const dir = f.path.includes('/') ? f.path.slice(0, f.path.lastIndexOf('/')) : ''
const folder = dir ? (zip.folder(dir) ?? zip) : zip
const bin = await fetch(`/api/original?path=${encodeURIComponent(f.path)}`)
if (bin.ok) folder.file(f.name, await bin.blob())
if (f.mode === 'extracted') {
const detail = await fetch(`/api/detail?path=${encodeURIComponent(f.path)}`)
if (detail.ok) {
const d = await detail.json()
if (d.content) folder.file(`${f.name}.txt`, d.content)
}
}
}
@@ -47,10 +42,6 @@ async function buildPackage(): Promise<void> {
building.value = false
}
}
function clearSelection(): void {
store.selected.clear()
}
</script>
<template>
@@ -63,17 +54,12 @@ function clearSelection(): void {
</label>
<div class="estimate" :class="{ over: packageEstimate.overFiles || packageEstimate.overBytes }">
{{ packageEstimate.docCount }} docs ·
<span :class="{ bad: packageEstimate.overFiles }">
{{ packageEstimate.files }}/{{ target.maxFiles }} files
</span>
·
<span :class="{ bad: packageEstimate.overBytes }">
{{ humanBytes(packageEstimate.bytes) }} / {{ humanBytes(target.maxBytes) }}
</span>
{{ packageEstimate.docCount }} sel ·
<span :class="{ bad: packageEstimate.overFiles }">{{ packageEstimate.files }}/{{ target.maxFiles }} files</span> ·
<span :class="{ bad: packageEstimate.overBytes }">{{ humanBytes(packageEstimate.bytes) }} / {{ humanBytes(target.maxBytes) }}</span>
</div>
<button :disabled="!store.selected.size" @click="clearSelection">Clear</button>
<button :disabled="!store.selected.size" @click="store.selected.clear()">Clear</button>
<button :disabled="!store.selected.size || building" @click="buildPackage">
{{ building ? 'Building' : 'Download package' }}
</button>
@@ -82,29 +68,10 @@ function clearSelection(): void {
</template>
<style scoped>
.export-bar {
display: flex;
align-items: center;
gap: var(--space-3);
font-size: var(--font-size-sm);
}
.target {
display: flex;
align-items: center;
gap: var(--space-1);
color: var(--text-secondary);
}
.estimate {
color: var(--text-secondary);
}
.estimate.over {
color: var(--status-error, #e0a030);
}
.estimate .bad {
color: var(--status-error, #e0a030);
font-weight: 600;
}
.err {
color: var(--status-error, #e0a030);
}
.export-bar { display: flex; align-items: center; gap: var(--space-3); font-size: var(--font-size-sm); }
.target { display: flex; align-items: center; gap: var(--space-1); color: var(--text-secondary); }
.estimate { color: var(--text-secondary); }
.estimate.over { color: var(--status-error, #e0a030); }
.estimate .bad { color: var(--status-error, #e0a030); font-weight: 600; }
.err { color: var(--status-error, #e0a030); }
</style>