This commit is contained in:
Timothy Jaeryang Baek
2026-02-25 16:13:18 -06:00
parent bee13f72ad
commit 8b2160f2f7
4 changed files with 160 additions and 27 deletions

View File

@@ -68,22 +68,14 @@ export const downloadFileBlob = async (
apiKey: string,
path: string
): Promise<{ blob: Blob; filename: string } | null> => {
const url = `${baseUrl.replace(/\/$/, '')}/files/read?path=${encodeURIComponent(path)}`;
const url = `${baseUrl.replace(/\/$/, '')}/files/view?path=${encodeURIComponent(path)}`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` }
}).catch(() => null);
if (!res || !res.ok) return null;
const contentType = res.headers.get('content-type') ?? '';
const filename = path.split('/').pop() ?? 'file';
if (contentType.includes('application/json')) {
const json = await res.json().catch(() => null);
const blob = new Blob([json?.content ?? ''], { type: 'text/plain' });
return { blob, filename };
}
const blob = await res.blob();
return { blob, filename };
};

View File

@@ -26,6 +26,7 @@
import GarbageBin from '../icons/GarbageBin.svelte';
import Spinner from '../common/Spinner.svelte';
import Tooltip from '../common/Tooltip.svelte';
import PDFViewer from '../common/PDFViewer.svelte';
import ConfirmDialog from '../common/ConfirmDialog.svelte';
const i18n = getContext('i18n');
@@ -40,7 +41,7 @@
let selectedFile: string | null = null;
let fileContent: string | null = null;
let fileImageUrl: string | null = null;
let filePdfUrl: string | null = null;
let filePdfData: ArrayBuffer | null = null;
let fileLoading = false;
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']);
@@ -88,10 +89,7 @@
URL.revokeObjectURL(fileImageUrl);
fileImageUrl = null;
}
if (filePdfUrl) {
URL.revokeObjectURL(filePdfUrl);
filePdfUrl = null;
}
filePdfData = null;
};
const loadDir = async (path: string) => {
@@ -132,10 +130,7 @@
if (result) fileImageUrl = URL.createObjectURL(result.blob);
} else if (isPdf(filePath)) {
const result = await downloadFileBlob(terminalUrl, terminalKey, filePath);
if (result)
filePdfUrl = URL.createObjectURL(
new Blob([await result.blob.arrayBuffer()], { type: 'application/pdf' })
);
if (result) filePdfData = await result.blob.arrayBuffer();
} else {
fileContent = await readFile(terminalUrl, terminalKey, filePath);
}
@@ -236,20 +231,29 @@
};
const onBlur = () => (shiftKey = false);
// Auto-reload directory when the browser tab regains focus
const onVisibilityChange = () => {
if (document.visibilityState === 'visible' && !selectedFile && configured && !loading) {
loadDir(currentPath);
}
};
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
window.addEventListener('blur', onBlur);
document.addEventListener('visibilitychange', onVisibilityChange);
return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('blur', onBlur);
document.removeEventListener('visibilitychange', onVisibilityChange);
};
});
onDestroy(() => {
if (fileImageUrl) URL.revokeObjectURL(fileImageUrl);
if (filePdfUrl) URL.revokeObjectURL(filePdfUrl);
});
</script>
@@ -338,6 +342,26 @@
</div>
{#if !selectedFile}
<Tooltip content={$i18n.t('Refresh')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
on:click={() => loadDir(currentPath)}
aria-label={$i18n.t('Refresh')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-3.5 {loading ? 'animate-spin' : ''}"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.451a.75.75 0 0 0 0-1.5H4.5a.75.75 0 0 0-.75.75v3.75a.75.75 0 0 0 1.5 0v-2.127l.13.13a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.624-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.75a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 .75-.75V3.42a.75.75 0 0 0-1.5 0v2.126l-.13-.129A7 7 0 0 0 3.239 8.555a.75.75 0 0 0 1.449.39Z"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
<Tooltip content={$i18n.t('New Folder')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
@@ -376,15 +400,15 @@
</button>
<!-- File preview -->
{#if fileLoading}
<div class="flex justify-center pt-8"><Spinner className="size-5" /></div>
<div class="flex justify-center pt-8"><Spinner className="size-4" /></div>
{:else if fileImageUrl !== null}
<img
src={fileImageUrl}
alt={selectedFile?.split('/').pop()}
class="w-full h-auto object-contain p-3"
/>
{:else if filePdfUrl !== null}
<embed src={filePdfUrl} type="application/pdf" class="w-full h-full min-h-[400px]" />
{:else if filePdfData !== null}
<PDFViewer data={filePdfData} className="w-full h-full min-h-[400px]" />
{:else if fileContent !== null}
<pre
class="text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all leading-relaxed p-3">{fileContent}</pre>
@@ -401,7 +425,7 @@
{$i18n.t('Uploading...')}
</div>
{:else if loading}
<div class="flex justify-center pt-8"><Spinner className="size-5" /></div>
<div class="flex justify-center pt-8"><Spinner className="size-4" /></div>
{:else if error}
<div class="p-4 text-xs text-red-500 dark:text-red-400">{error}</div>
{:else if entries.length === 0 && !creatingFolder}

View File

@@ -24,6 +24,7 @@
import Tooltip from './Tooltip.svelte';
import dayjs from 'dayjs';
import Spinner from './Spinner.svelte';
import PDFViewer from './PDFViewer.svelte';
export let item;
export let show = false;
@@ -443,10 +444,9 @@
playsinline
/>
{:else if isPDF}
<iframe
title={item?.name}
src={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
class="w-full h-[70vh] border-0 rounded-lg"
<PDFViewer
url={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
className="w-full h-[70vh] border-0 rounded-lg"
/>
{:else if isExcel}
{#if excelError}

View File

@@ -0,0 +1,117 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url';
import Spinner from './Spinner.svelte';
export let url: string | null = null;
export let data: ArrayBuffer | Uint8Array | null = null;
export let className = 'w-full h-[70vh]';
let container: HTMLDivElement;
let loading = true;
let error = '';
let pdfDoc: any = null;
const renderAllPages = async () => {
if (!pdfDoc || !container) return;
// Clear previous canvases
container.innerHTML = '';
for (let i = 1; i <= pdfDoc.numPages; i++) {
const page = await pdfDoc.getPage(i);
const viewport = page.getViewport({ scale: 1 });
// Scale to fit container width
const containerWidth = container.clientWidth || 800;
const scale = containerWidth / viewport.width;
const scaledViewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = scaledViewport.width;
canvas.height = scaledViewport.height;
canvas.style.width = '100%';
canvas.style.height = 'auto';
canvas.style.display = 'block';
if (i > 1) {
canvas.style.marginTop = '4px';
}
container.appendChild(canvas);
const ctx = canvas.getContext('2d');
await page.render({
canvasContext: ctx,
viewport: scaledViewport
}).promise;
}
};
const loadPdf = async () => {
if (!url && !data) return;
loading = true;
error = '';
try {
const pdfjs = await import('pdfjs-dist');
pdfjs.GlobalWorkerOptions.workerSrc = pdfWorkerUrl;
let pdfData: ArrayBuffer | Uint8Array;
if (data) {
pdfData = data;
} else {
// Fetch with credentials so auth cookies are sent
const res = await fetch(url!, { credentials: 'include' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
pdfData = await res.arrayBuffer();
}
pdfDoc = await pdfjs.getDocument({ data: pdfData }).promise;
await renderAllPages();
} catch (e) {
console.error('PDF render error:', e);
error = 'Failed to load PDF.';
} finally {
loading = false;
}
};
// Re-render on resize
let resizeObserver: ResizeObserver | null = null;
onMount(() => {
loadPdf();
resizeObserver = new ResizeObserver(() => {
if (pdfDoc && !loading) {
renderAllPages();
}
});
if (container) {
resizeObserver.observe(container);
}
});
onDestroy(() => {
resizeObserver?.disconnect();
if (pdfDoc) {
pdfDoc.destroy();
pdfDoc = null;
}
});
</script>
<div class="overflow-auto {className}" bind:this={container}>
{#if loading}
<div class="flex items-center justify-center h-full">
<Spinner className="size-5" />
</div>
{/if}
{#if error}
<div class="flex items-center justify-center h-full text-sm text-red-500">
{error}
</div>
{/if}
</div>