refac
This commit is contained in:
1 parent
98ee2bdfd3
commit
fd8cc2ba4a
14 files changed
+117
-18
No files matched your search
@@ -277,6 +277,7 @@ def build_terminal_file_tool_result(
|
|||||||
return None
|
return None
|
||||||
mime_type, _ = mimetypes.guess_type(path)
|
mime_type, _ = mimetypes.guess_type(path)
|
||||||
mime_type = mime_type or 'application/octet-stream'
|
mime_type = mime_type or 'application/octet-stream'
|
||||||
|
page = tool_result.get('page') or tool_function_params.get('page')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**tool_result,
|
**tool_result,
|
||||||
@@ -292,6 +293,7 @@ def build_terminal_file_tool_result(
|
|||||||
'name': tool_result.get('name') or os.path.basename(path),
|
'name': tool_result.get('name') or os.path.basename(path),
|
||||||
'mime_type': tool_result.get('mime_type') or tool_result.get('content_type') or mime_type,
|
'mime_type': tool_result.get('mime_type') or tool_result.get('content_type') or mime_type,
|
||||||
'content_type': tool_result.get('content_type') or tool_result.get('mime_type') or mime_type,
|
'content_type': tool_result.get('content_type') or tool_result.get('mime_type') or mime_type,
|
||||||
|
**({'page': page} if page else {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1242,11 +1244,15 @@ async def terminal_event_handler(
|
|||||||
pass
|
pass
|
||||||
if isinstance(parsed, dict) and parsed.get('exists') is False:
|
if isinstance(parsed, dict) and parsed.get('exists') is False:
|
||||||
return
|
return
|
||||||
|
page = tool_function_params.get('page')
|
||||||
|
|
||||||
await event_emitter(
|
await event_emitter(
|
||||||
{
|
{
|
||||||
'type': f'terminal:{tool_function_name}',
|
'type': f'terminal:{tool_function_name}',
|
||||||
'data': {'path': path},
|
'data': {
|
||||||
|
'path': path,
|
||||||
|
**({'page': page} if page else {}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif tool_function_name in ('write_file', 'replace_file_content'):
|
elif tool_function_name in ('write_file', 'replace_file_content'):
|
||||||
|
|||||||
@@ -968,6 +968,9 @@ def add_terminal_display_file_inline_param(spec: dict) -> dict:
|
|||||||
properties['inline'] = {
|
properties['inline'] = {
|
||||||
'type': 'boolean',
|
'type': 'boolean',
|
||||||
'description': 'Show the file inline in the chat message instead of opening the file viewer.',
|
'description': 'Show the file inline in the chat message instead of opening the file viewer.',
|
||||||
|
}
|
||||||
|
'minimum': 1,
|
||||||
|
'description': 'For PDF, DOCX, and PPTX files, open the preview at this 1-based page or slide number.',
|
||||||
}
|
}
|
||||||
return spec
|
return spec
|
||||||
|
|
||||||
|
|||||||
@@ -1139,7 +1139,7 @@
|
|||||||
const terminalEventHandler = (type: string, data: any) => {
|
const terminalEventHandler = (type: string, data: any) => {
|
||||||
if (type === 'terminal:display_file') {
|
if (type === 'terminal:display_file') {
|
||||||
if (!data?.path) return;
|
if (!data?.path) return;
|
||||||
displayFileHandler(data.path, { showControls, showFileNavPath });
|
displayFileHandler(data.path, { showControls, showFileNavPath }, { page: data?.page });
|
||||||
} else if (type === 'terminal:write_file' || type === 'terminal:replace_file_content') {
|
} else if (type === 'terminal:write_file' || type === 'terminal:replace_file_content') {
|
||||||
if (!data?.path) return;
|
if (!data?.path) return;
|
||||||
showFileNavDir.set(data.path);
|
showFileNavDir.set(data.path);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
} from '$lib/apis/terminal';
|
} from '$lib/apis/terminal';
|
||||||
import { isCodeFile } from '$lib/utils/codeHighlight';
|
import { isCodeFile } from '$lib/utils/codeHighlight';
|
||||||
import { isSavedChatId, isTemporaryChatId } from '$lib/utils/chatId';
|
import { isSavedChatId, isTemporaryChatId } from '$lib/utils/chatId';
|
||||||
|
import { normalizeDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||||
|
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
@@ -302,6 +303,7 @@
|
|||||||
let fileLoading = false;
|
let fileLoading = false;
|
||||||
let filePreviewRef: FilePreview;
|
let filePreviewRef: FilePreview;
|
||||||
let fileSearchTarget: FileSearchTarget | null = null;
|
let fileSearchTarget: FileSearchTarget | null = null;
|
||||||
|
let documentTargetPage: number | null = null;
|
||||||
|
|
||||||
// ── Office preview state ────────────────────────────────────────────
|
// ── Office preview state ────────────────────────────────────────────
|
||||||
let fileOfficeHtml: string | null = null;
|
let fileOfficeHtml: string | null = null;
|
||||||
@@ -691,6 +693,7 @@
|
|||||||
// ── File preview management ──────────────────────────────────────────
|
// ── File preview management ──────────────────────────────────────────
|
||||||
const clearFilePreview = () => {
|
const clearFilePreview = () => {
|
||||||
fileSearchTarget = null;
|
fileSearchTarget = null;
|
||||||
|
documentTargetPage = null;
|
||||||
fileContent = null;
|
fileContent = null;
|
||||||
if (fileImageUrl) {
|
if (fileImageUrl) {
|
||||||
URL.revokeObjectURL(fileImageUrl);
|
URL.revokeObjectURL(fileImageUrl);
|
||||||
@@ -810,7 +813,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEntry = async (entry: FileEntry) => {
|
const openEntry = async (entry: FileEntry, options: { page?: unknown } = {}) => {
|
||||||
const fullPath =
|
const fullPath =
|
||||||
'fullPath' in entry ? (entry as BrowserRow).fullPath : entryPath(currentPath, entry);
|
'fullPath' in entry ? (entry as BrowserRow).fullPath : entryPath(currentPath, entry);
|
||||||
const parentPath = 'parentPath' in entry ? (entry as BrowserRow).parentPath : currentPath;
|
const parentPath = 'parentPath' in entry ? (entry as BrowserRow).parentPath : currentPath;
|
||||||
@@ -832,6 +835,7 @@
|
|||||||
selectedFile = filePath;
|
selectedFile = filePath;
|
||||||
fileLoading = true;
|
fileLoading = true;
|
||||||
clearFilePreview();
|
clearFilePreview();
|
||||||
|
documentTargetPage = normalizeDocumentTargetPage(options.page);
|
||||||
|
|
||||||
if (isImage(filePath)) {
|
if (isImage(filePath)) {
|
||||||
const result = await downloadFileBlob(
|
const result = await downloadFileBlob(
|
||||||
@@ -1317,10 +1321,12 @@
|
|||||||
|
|
||||||
let handledDisplayFile = false;
|
let handledDisplayFile = false;
|
||||||
|
|
||||||
const unsubFileNav = showFileNavPath.subscribe(async (filePath) => {
|
const unsubFileNav = showFileNavPath.subscribe(async (request) => {
|
||||||
if (!filePath || !selectedTerminal) return;
|
if (!request || !selectedTerminal) return;
|
||||||
handledDisplayFile = true;
|
handledDisplayFile = true;
|
||||||
showFileNavPath.set(null);
|
showFileNavPath.set(null);
|
||||||
|
let filePath = typeof request === 'string' ? request : request.path;
|
||||||
|
const targetPage = typeof request === 'string' ? null : request.page;
|
||||||
filePath = normalizePath(filePath);
|
filePath = normalizePath(filePath);
|
||||||
if (!isInsideFileRoot(filePath)) {
|
if (!isInsideFileRoot(filePath)) {
|
||||||
await loadDir(fileRoot?.path ?? '/');
|
await loadDir(fileRoot?.path ?? '/');
|
||||||
@@ -1337,10 +1343,10 @@
|
|||||||
|
|
||||||
const entry = entries.find((e) => e.name === fileName);
|
const entry = entries.find((e) => e.name === fileName);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
await openEntry(entry);
|
await openEntry(entry, { page: targetPage });
|
||||||
} else {
|
} else {
|
||||||
// File may not be in listing; open it directly
|
// File may not be in listing; open it directly
|
||||||
await openEntry({ name: fileName, type: 'file', size: 0 });
|
await openEntry({ name: fileName, type: 'file', size: 0 }, { page: targetPage });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1741,6 +1747,7 @@
|
|||||||
{fileContent}
|
{fileContent}
|
||||||
{fileOfficeHtml}
|
{fileOfficeHtml}
|
||||||
{fileOfficeSlides}
|
{fileOfficeSlides}
|
||||||
|
targetPage={documentTargetPage}
|
||||||
{excelSheetNames}
|
{excelSheetNames}
|
||||||
{selectedExcelSheet}
|
{selectedExcelSheet}
|
||||||
searchTarget={fileSearchTarget}
|
searchTarget={fileSearchTarget}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
export let fileOfficeHtml: string | null = null;
|
export let fileOfficeHtml: string | null = null;
|
||||||
export let fileOfficeSlides: string[] | null = null;
|
export let fileOfficeSlides: string[] | null = null;
|
||||||
export let currentSlide = 0;
|
export let currentSlide = 0;
|
||||||
|
export let targetPage: number | null = null;
|
||||||
export let excelSheetNames: string[] = [];
|
export let excelSheetNames: string[] = [];
|
||||||
export let selectedExcelSheet = '';
|
export let selectedExcelSheet = '';
|
||||||
export let onSheetChange: ((sheet: string) => void) | null = null;
|
export let onSheetChange: ((sheet: string) => void) | null = null;
|
||||||
@@ -320,11 +321,11 @@
|
|||||||
</audio>
|
</audio>
|
||||||
</div>
|
</div>
|
||||||
{:else if filePdfData !== null}
|
{:else if filePdfData !== null}
|
||||||
<PDFViewer bind:this={pdfViewerRef} data={filePdfData} className="w-full h-full" />
|
<PDFViewer bind:this={pdfViewerRef} data={filePdfData} {targetPage} className="w-full h-full" />
|
||||||
{:else if fileSqliteData !== null}
|
{:else if fileSqliteData !== null}
|
||||||
<SqliteView data={fileSqliteData} />
|
<SqliteView data={fileSqliteData} />
|
||||||
{:else if fileDocxData !== null}
|
{:else if fileDocxData !== null}
|
||||||
<DocxPreview data={fileDocxData} className="w-full h-full" />
|
<DocxPreview data={fileDocxData} {targetPage} className="w-full h-full" />
|
||||||
{:else if fileOfficeHtml !== null}
|
{:else if fileOfficeHtml !== null}
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<div class="office-preview overflow-auto flex-1 min-h-0">
|
<div class="office-preview overflow-auto flex-1 min-h-0">
|
||||||
@@ -353,6 +354,7 @@
|
|||||||
bind:this={pptxPreviewRef}
|
bind:this={pptxPreviewRef}
|
||||||
slides={fileOfficeSlides}
|
slides={fileOfficeSlides}
|
||||||
bind:currentSlide
|
bind:currentSlide
|
||||||
|
{targetPage}
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
/>
|
/>
|
||||||
{:else if fileContent !== null}
|
{:else if fileContent !== null}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import FilePreview from '$lib/components/chat/FileNav/FilePreview.svelte';
|
import FilePreview from '$lib/components/chat/FileNav/FilePreview.svelte';
|
||||||
import Icon from '$lib/components/chat/FileNav/Icon.svelte';
|
import Icon from '$lib/components/chat/FileNav/Icon.svelte';
|
||||||
import { fileIconName } from '$lib/components/chat/FileNav/fileIcon';
|
import { fileIconName } from '$lib/components/chat/FileNav/fileIcon';
|
||||||
|
import { normalizeDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||||
|
|
||||||
export let item: any;
|
export let item: any;
|
||||||
export let chatId = '';
|
export let chatId = '';
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
|
|
||||||
$: path = String(item?.full_path || item?.path || '');
|
$: path = String(item?.full_path || item?.path || '');
|
||||||
$: name = String(item?.name || path.split('/').filter(Boolean).at(-1) || 'file');
|
$: name = String(item?.name || path.split('/').filter(Boolean).at(-1) || 'file');
|
||||||
|
$: targetPage = normalizeDocumentTargetPage(item?.page);
|
||||||
$: selector = item?.terminal_selector;
|
$: selector = item?.terminal_selector;
|
||||||
$: terminal = resolveTerminal();
|
$: terminal = resolveTerminal();
|
||||||
$: unavailable = !terminal;
|
$: unavailable = !terminal;
|
||||||
@@ -165,7 +167,7 @@
|
|||||||
function openInFiles() {
|
function openInFiles() {
|
||||||
if (unavailable || !path) return;
|
if (unavailable || !path) return;
|
||||||
showControls.set(true);
|
showControls.set(true);
|
||||||
showFileNavPath.set(path);
|
showFileNavPath.set(targetPage ? { path, page: targetPage } : path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadFile() {
|
async function downloadFile() {
|
||||||
@@ -249,6 +251,7 @@
|
|||||||
{fileOfficeHtml}
|
{fileOfficeHtml}
|
||||||
{fileOfficeSlides}
|
{fileOfficeSlides}
|
||||||
{currentSlide}
|
{currentSlide}
|
||||||
|
{targetPage}
|
||||||
{excelSheetNames}
|
{excelSheetNames}
|
||||||
{selectedExcelSheet}
|
{selectedExcelSheet}
|
||||||
onSheetChange={loadExcelSheet}
|
onSheetChange={loadExcelSheet}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ function getInlineFileFromToolOutput(callItem?: OutputItem, resultItem?: OutputI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result.page === undefined && args.page !== undefined ? { ...result, page: args.page } : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildToolCallToken(item: OutputItem, toolOutputByCallId: Record<string, OutputItem>) {
|
function buildToolCallToken(item: OutputItem, toolOutputByCallId: Record<string, OutputItem>) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { getContext, onDestroy, onMount, tick } from 'svelte';
|
import { getContext, onDestroy, onMount, tick } from 'svelte';
|
||||||
import type { Readable } from 'svelte/store';
|
import type { Readable } from 'svelte/store';
|
||||||
|
import { clampDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||||
|
|
||||||
import Spinner from './Spinner.svelte';
|
import Spinner from './Spinner.svelte';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
|
|
||||||
export let data: ArrayBuffer | null = null;
|
export let data: ArrayBuffer | null = null;
|
||||||
export let className = '';
|
export let className = '';
|
||||||
|
export let targetPage: number | null = null;
|
||||||
|
|
||||||
let outerContainer: HTMLDivElement;
|
let outerContainer: HTMLDivElement;
|
||||||
let containerEl: HTMLDivElement;
|
let containerEl: HTMLDivElement;
|
||||||
@@ -84,6 +86,16 @@
|
|||||||
updateFitScale();
|
updateFitScale();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrollToTargetPage = async () => {
|
||||||
|
if (!containerEl) return;
|
||||||
|
const pages = containerEl.querySelectorAll('section.docx');
|
||||||
|
const page = clampDocumentTargetPage(targetPage, pages.length);
|
||||||
|
if (!page) return;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
(pages[page - 1] as HTMLElement | undefined)?.scrollIntoView({ block: 'start' });
|
||||||
|
};
|
||||||
|
|
||||||
const renderDocx = async (arrayBuffer: ArrayBuffer | null) => {
|
const renderDocx = async (arrayBuffer: ArrayBuffer | null) => {
|
||||||
const currentRender = ++renderId;
|
const currentRender = ++renderId;
|
||||||
clearPreview();
|
clearPreview();
|
||||||
@@ -112,6 +124,7 @@
|
|||||||
});
|
});
|
||||||
await tick();
|
await tick();
|
||||||
updateFitScale();
|
updateFitScale();
|
||||||
|
await scrollToTargetPage();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error rendering DOCX preview:', e);
|
console.error('Error rendering DOCX preview:', e);
|
||||||
|
|
||||||
@@ -130,6 +143,10 @@
|
|||||||
|
|
||||||
$: if (mounted) renderDocx(data);
|
$: if (mounted) renderDocx(data);
|
||||||
|
|
||||||
|
$: if (!loading && targetPage && !fallbackHtml) {
|
||||||
|
void scrollToTargetPage();
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mounted = true;
|
mounted = true;
|
||||||
void tick().then(() => {
|
void tick().then(() => {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy, tick } from 'svelte';
|
||||||
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||||
import panzoom, { type PanZoom } from 'panzoom';
|
import panzoom, { type PanZoom } from 'panzoom';
|
||||||
|
import { clampDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||||
import Spinner from './Spinner.svelte';
|
import Spinner from './Spinner.svelte';
|
||||||
|
|
||||||
export let url: string | null = null;
|
export let url: string | null = null;
|
||||||
export let data: ArrayBuffer | Uint8Array | null = null;
|
export let data: ArrayBuffer | Uint8Array | null = null;
|
||||||
export let className = 'w-full h-[70vh]';
|
export let className = 'w-full h-[70vh]';
|
||||||
|
export let targetPage: number | null = null;
|
||||||
|
|
||||||
type PdfDocument = import('pdfjs-dist').PDFDocumentProxy;
|
type PdfDocument = import('pdfjs-dist').PDFDocumentProxy;
|
||||||
type PdfTextLayer = InstanceType<typeof import('pdfjs-dist').TextLayer>;
|
type PdfTextLayer = InstanceType<typeof import('pdfjs-dist').TextLayer>;
|
||||||
@@ -98,6 +100,17 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrollToTargetPage = async () => {
|
||||||
|
if (!outerContainer || !sceneElement || !pdfDoc) return;
|
||||||
|
const page = clampDocumentTargetPage(targetPage, pdfDoc.numPages);
|
||||||
|
if (!page) return;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
const pageWrapper = sceneElement.querySelectorAll('.pdf-page-wrapper')[page - 1] as
|
||||||
|
HTMLElement | undefined;
|
||||||
|
pageWrapper?.scrollIntoView({ block: 'start' });
|
||||||
|
};
|
||||||
|
|
||||||
// Re-render existing canvases at a new zoom level (preserves panzoom transform)
|
// Re-render existing canvases at a new zoom level (preserves panzoom transform)
|
||||||
const rerenderPages = async (forZoom: number) => {
|
const rerenderPages = async (forZoom: number) => {
|
||||||
if (!pdfDoc || !sceneElement) return;
|
if (!pdfDoc || !sceneElement) return;
|
||||||
@@ -223,6 +236,7 @@
|
|||||||
|
|
||||||
lastRenderedZoom = 1;
|
lastRenderedZoom = 1;
|
||||||
initPanzoom();
|
initPanzoom();
|
||||||
|
await scrollToTargetPage();
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadPdf = async () => {
|
const loadPdf = async () => {
|
||||||
@@ -258,6 +272,10 @@
|
|||||||
loadPdf();
|
loadPdf();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$: if (!loading && pdfDoc && targetPage) {
|
||||||
|
void scrollToTargetPage();
|
||||||
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (rerenderTimer) clearTimeout(rerenderTimer);
|
if (rerenderTimer) clearTimeout(rerenderTimer);
|
||||||
pzInstance?.dispose();
|
pzInstance?.dispose();
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount, tick } from 'svelte';
|
import { onDestroy, onMount, tick } from 'svelte';
|
||||||
import panzoom, { type PanZoom } from 'panzoom';
|
import panzoom, { type PanZoom } from 'panzoom';
|
||||||
|
import { clampDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||||
|
|
||||||
export let slides: string[] = [];
|
export let slides: string[] = [];
|
||||||
export let currentSlide = 0;
|
export let currentSlide = 0;
|
||||||
export let className = '';
|
export let className = '';
|
||||||
|
export let targetPage: number | null = null;
|
||||||
|
|
||||||
let rootEl: HTMLDivElement;
|
let rootEl: HTMLDivElement;
|
||||||
let stageEl: HTMLElement;
|
let stageEl: HTMLElement;
|
||||||
@@ -22,6 +24,8 @@
|
|||||||
let wheelDelta = 0;
|
let wheelDelta = 0;
|
||||||
let lastWheelNavigationAt = 0;
|
let lastWheelNavigationAt = 0;
|
||||||
let lastScrolledSlide = -1;
|
let lastScrolledSlide = -1;
|
||||||
|
let appliedTargetPage: number | null = null;
|
||||||
|
let appliedTargetSlides: string[] | null = null;
|
||||||
let thumbnailButtons: Array<HTMLButtonElement | undefined> = [];
|
let thumbnailButtons: Array<HTMLButtonElement | undefined> = [];
|
||||||
const slideShortcutKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
const slideShortcutKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
||||||
const wheelNavigationThreshold = 80;
|
const wheelNavigationThreshold = 80;
|
||||||
@@ -158,7 +162,7 @@
|
|||||||
const transform = pzInstance?.getTransform();
|
const transform = pzInstance?.getTransform();
|
||||||
if (transform && Math.abs(transform.scale - 1) >= 0.01) {
|
if (transform && Math.abs(transform.scale - 1) >= 0.01) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
pzInstance?.moveBy(-e.deltaX, -e.deltaY);
|
pzInstance?.moveBy(-e.deltaX, -e.deltaY, false);
|
||||||
zoomLevel = pzInstance?.getTransform().scale ?? 1;
|
zoomLevel = pzInstance?.getTransform().scale ?? 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -203,6 +207,18 @@
|
|||||||
void tick().then(scrollSelectedThumbnailIntoView);
|
void tick().then(scrollSelectedThumbnailIntoView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (
|
||||||
|
mounted &&
|
||||||
|
targetPage &&
|
||||||
|
slides.length > 0 &&
|
||||||
|
(targetPage !== appliedTargetPage || slides !== appliedTargetSlides)
|
||||||
|
) {
|
||||||
|
appliedTargetPage = targetPage;
|
||||||
|
appliedTargetSlides = slides;
|
||||||
|
const page = clampDocumentTargetPage(targetPage, slides.length);
|
||||||
|
if (page) selectSlide(page - 1);
|
||||||
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
pzInstance?.dispose();
|
pzInstance?.dispose();
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ export const showOverview = writable(false);
|
|||||||
export const showArtifacts = writable(false);
|
export const showArtifacts = writable(false);
|
||||||
export const showCallOverlay = writable(false);
|
export const showCallOverlay = writable(false);
|
||||||
export const showFileNav = writable(false);
|
export const showFileNav = writable(false);
|
||||||
export const showFileNavPath: Writable<string | null> = writable(null);
|
export type FileNavOpenRequest = string | { path: string; page?: number | null };
|
||||||
|
export const showFileNavPath: Writable<FileNavOpenRequest | null> = writable(null);
|
||||||
export const showFileNavDir: Writable<string | null> = writable(null);
|
export const showFileNavDir: Writable<string | null> = writable(null);
|
||||||
export const selectedTerminalId: Writable<string | null> = writable(null);
|
export const selectedTerminalId: Writable<string | null> = writable(null);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export const normalizeDocumentTargetPage = (page: unknown): number | null => {
|
||||||
|
if (page === undefined || page === null || page === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = Number(page);
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPage = Math.trunc(value);
|
||||||
|
return targetPage > 0 ? targetPage : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clampDocumentTargetPage = (page: number | null | undefined, pageCount: number) => {
|
||||||
|
if (!page || pageCount < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(Math.max(1, page), pageCount);
|
||||||
|
};
|
||||||
@@ -3,6 +3,8 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
import sha256 from 'js-sha256';
|
import sha256 from 'js-sha256';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
import type { FileNavOpenRequest } from '$lib/stores';
|
||||||
|
import { normalizeDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
@@ -2321,10 +2323,12 @@ export const formatSkillName = (name) => {
|
|||||||
*/
|
*/
|
||||||
export const displayFileHandler = (
|
export const displayFileHandler = (
|
||||||
path: string,
|
path: string,
|
||||||
stores: { showControls: Writable<boolean>; showFileNavPath: Writable<string | null> }
|
stores: { showControls: Writable<boolean>; showFileNavPath: Writable<FileNavOpenRequest | null> },
|
||||||
|
options: { page?: unknown } = {}
|
||||||
) => {
|
) => {
|
||||||
if (path) {
|
if (path) {
|
||||||
stores.showControls.set(true);
|
stores.showControls.set(true);
|
||||||
stores.showFileNavPath.set(path);
|
const page = normalizeDocumentTargetPage(options.page);
|
||||||
|
stores.showFileNavPath.set(page ? { path, page } : path);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -481,7 +481,8 @@
|
|||||||
full_path: result?.full_path ?? path,
|
full_path: result?.full_path ?? path,
|
||||||
name,
|
name,
|
||||||
mime_type: contentType,
|
mime_type: contentType,
|
||||||
content_type: contentType
|
content_type: contentType,
|
||||||
|
page: result?.page ?? params?.page
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -519,7 +520,7 @@
|
|||||||
|
|
||||||
if (data?.name === 'display_file' && params?.path && !inlineDisplayFile) {
|
if (data?.name === 'display_file' && params?.path && !inlineDisplayFile) {
|
||||||
if (result?.exists !== false) {
|
if (result?.exists !== false) {
|
||||||
displayFileHandler(params.path, { showControls, showFileNavPath });
|
displayFileHandler(params.path, { showControls, showFileNavPath }, { page: params?.page });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user