mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-26 23:44:48 -06:00
refac
This commit is contained in:
@@ -277,6 +277,7 @@ def build_terminal_file_tool_result(
|
||||
return None
|
||||
mime_type, _ = mimetypes.guess_type(path)
|
||||
mime_type = mime_type or 'application/octet-stream'
|
||||
page = tool_result.get('page') or tool_function_params.get('page')
|
||||
|
||||
return {
|
||||
**tool_result,
|
||||
@@ -292,6 +293,7 @@ def build_terminal_file_tool_result(
|
||||
'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,
|
||||
'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
|
||||
if isinstance(parsed, dict) and parsed.get('exists') is False:
|
||||
return
|
||||
page = tool_function_params.get('page')
|
||||
|
||||
await event_emitter(
|
||||
{
|
||||
'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'):
|
||||
|
||||
@@ -968,6 +968,9 @@ def add_terminal_display_file_inline_param(spec: dict) -> dict:
|
||||
properties['inline'] = {
|
||||
'type': 'boolean',
|
||||
'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
|
||||
|
||||
|
||||
@@ -1139,7 +1139,7 @@
|
||||
const terminalEventHandler = (type: string, data: any) => {
|
||||
if (type === 'terminal:display_file') {
|
||||
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') {
|
||||
if (!data?.path) return;
|
||||
showFileNavDir.set(data.path);
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
} from '$lib/apis/terminal';
|
||||
import { isCodeFile } from '$lib/utils/codeHighlight';
|
||||
import { isSavedChatId, isTemporaryChatId } from '$lib/utils/chatId';
|
||||
import { normalizeDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
@@ -302,6 +303,7 @@
|
||||
let fileLoading = false;
|
||||
let filePreviewRef: FilePreview;
|
||||
let fileSearchTarget: FileSearchTarget | null = null;
|
||||
let documentTargetPage: number | null = null;
|
||||
|
||||
// ── Office preview state ────────────────────────────────────────────
|
||||
let fileOfficeHtml: string | null = null;
|
||||
@@ -691,6 +693,7 @@
|
||||
// ── File preview management ──────────────────────────────────────────
|
||||
const clearFilePreview = () => {
|
||||
fileSearchTarget = null;
|
||||
documentTargetPage = null;
|
||||
fileContent = null;
|
||||
if (fileImageUrl) {
|
||||
URL.revokeObjectURL(fileImageUrl);
|
||||
@@ -810,7 +813,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
const openEntry = async (entry: FileEntry) => {
|
||||
const openEntry = async (entry: FileEntry, options: { page?: unknown } = {}) => {
|
||||
const fullPath =
|
||||
'fullPath' in entry ? (entry as BrowserRow).fullPath : entryPath(currentPath, entry);
|
||||
const parentPath = 'parentPath' in entry ? (entry as BrowserRow).parentPath : currentPath;
|
||||
@@ -832,6 +835,7 @@
|
||||
selectedFile = filePath;
|
||||
fileLoading = true;
|
||||
clearFilePreview();
|
||||
documentTargetPage = normalizeDocumentTargetPage(options.page);
|
||||
|
||||
if (isImage(filePath)) {
|
||||
const result = await downloadFileBlob(
|
||||
@@ -1317,10 +1321,12 @@
|
||||
|
||||
let handledDisplayFile = false;
|
||||
|
||||
const unsubFileNav = showFileNavPath.subscribe(async (filePath) => {
|
||||
if (!filePath || !selectedTerminal) return;
|
||||
const unsubFileNav = showFileNavPath.subscribe(async (request) => {
|
||||
if (!request || !selectedTerminal) return;
|
||||
handledDisplayFile = true;
|
||||
showFileNavPath.set(null);
|
||||
let filePath = typeof request === 'string' ? request : request.path;
|
||||
const targetPage = typeof request === 'string' ? null : request.page;
|
||||
filePath = normalizePath(filePath);
|
||||
if (!isInsideFileRoot(filePath)) {
|
||||
await loadDir(fileRoot?.path ?? '/');
|
||||
@@ -1337,10 +1343,10 @@
|
||||
|
||||
const entry = entries.find((e) => e.name === fileName);
|
||||
if (entry) {
|
||||
await openEntry(entry);
|
||||
await openEntry(entry, { page: targetPage });
|
||||
} else {
|
||||
// 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}
|
||||
{fileOfficeHtml}
|
||||
{fileOfficeSlides}
|
||||
targetPage={documentTargetPage}
|
||||
{excelSheetNames}
|
||||
{selectedExcelSheet}
|
||||
searchTarget={fileSearchTarget}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
export let fileOfficeHtml: string | null = null;
|
||||
export let fileOfficeSlides: string[] | null = null;
|
||||
export let currentSlide = 0;
|
||||
export let targetPage: number | null = null;
|
||||
export let excelSheetNames: string[] = [];
|
||||
export let selectedExcelSheet = '';
|
||||
export let onSheetChange: ((sheet: string) => void) | null = null;
|
||||
@@ -320,11 +321,11 @@
|
||||
</audio>
|
||||
</div>
|
||||
{: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}
|
||||
<SqliteView data={fileSqliteData} />
|
||||
{: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}
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="office-preview overflow-auto flex-1 min-h-0">
|
||||
@@ -353,6 +354,7 @@
|
||||
bind:this={pptxPreviewRef}
|
||||
slides={fileOfficeSlides}
|
||||
bind:currentSlide
|
||||
{targetPage}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
{:else if fileContent !== null}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import FilePreview from '$lib/components/chat/FileNav/FilePreview.svelte';
|
||||
import Icon from '$lib/components/chat/FileNav/Icon.svelte';
|
||||
import { fileIconName } from '$lib/components/chat/FileNav/fileIcon';
|
||||
import { normalizeDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||
|
||||
export let item: any;
|
||||
export let chatId = '';
|
||||
@@ -42,6 +43,7 @@
|
||||
|
||||
$: path = String(item?.full_path || item?.path || '');
|
||||
$: name = String(item?.name || path.split('/').filter(Boolean).at(-1) || 'file');
|
||||
$: targetPage = normalizeDocumentTargetPage(item?.page);
|
||||
$: selector = item?.terminal_selector;
|
||||
$: terminal = resolveTerminal();
|
||||
$: unavailable = !terminal;
|
||||
@@ -165,7 +167,7 @@
|
||||
function openInFiles() {
|
||||
if (unavailable || !path) return;
|
||||
showControls.set(true);
|
||||
showFileNavPath.set(path);
|
||||
showFileNavPath.set(targetPage ? { path, page: targetPage } : path);
|
||||
}
|
||||
|
||||
async function downloadFile() {
|
||||
@@ -249,6 +251,7 @@
|
||||
{fileOfficeHtml}
|
||||
{fileOfficeSlides}
|
||||
{currentSlide}
|
||||
{targetPage}
|
||||
{excelSheetNames}
|
||||
{selectedExcelSheet}
|
||||
onSheetChange={loadExcelSheet}
|
||||
|
||||
@@ -187,7 +187,7 @@ function getInlineFileFromToolOutput(callItem?: OutputItem, resultItem?: OutputI
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.page === undefined && args.page !== undefined ? { ...result, page: args.page } : result;
|
||||
}
|
||||
|
||||
function buildToolCallToken(item: OutputItem, toolOutputByCallId: Record<string, OutputItem>) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { getContext, onDestroy, onMount, tick } from 'svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import { clampDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||
|
||||
import Spinner from './Spinner.svelte';
|
||||
|
||||
@@ -13,6 +14,7 @@
|
||||
|
||||
export let data: ArrayBuffer | null = null;
|
||||
export let className = '';
|
||||
export let targetPage: number | null = null;
|
||||
|
||||
let outerContainer: HTMLDivElement;
|
||||
let containerEl: HTMLDivElement;
|
||||
@@ -84,6 +86,16 @@
|
||||
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 currentRender = ++renderId;
|
||||
clearPreview();
|
||||
@@ -112,6 +124,7 @@
|
||||
});
|
||||
await tick();
|
||||
updateFitScale();
|
||||
await scrollToTargetPage();
|
||||
} catch (e) {
|
||||
console.error('Error rendering DOCX preview:', e);
|
||||
|
||||
@@ -130,6 +143,10 @@
|
||||
|
||||
$: if (mounted) renderDocx(data);
|
||||
|
||||
$: if (!loading && targetPage && !fallbackHtml) {
|
||||
void scrollToTargetPage();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
void tick().then(() => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<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 panzoom, { type PanZoom } from 'panzoom';
|
||||
import { clampDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||
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]';
|
||||
export let targetPage: number | null = null;
|
||||
|
||||
type PdfDocument = import('pdfjs-dist').PDFDocumentProxy;
|
||||
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)
|
||||
const rerenderPages = async (forZoom: number) => {
|
||||
if (!pdfDoc || !sceneElement) return;
|
||||
@@ -223,6 +236,7 @@
|
||||
|
||||
lastRenderedZoom = 1;
|
||||
initPanzoom();
|
||||
await scrollToTargetPage();
|
||||
};
|
||||
|
||||
const loadPdf = async () => {
|
||||
@@ -258,6 +272,10 @@
|
||||
loadPdf();
|
||||
});
|
||||
|
||||
$: if (!loading && pdfDoc && targetPage) {
|
||||
void scrollToTargetPage();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (rerenderTimer) clearTimeout(rerenderTimer);
|
||||
pzInstance?.dispose();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
import panzoom, { type PanZoom } from 'panzoom';
|
||||
import { clampDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||
|
||||
export let slides: string[] = [];
|
||||
export let currentSlide = 0;
|
||||
export let className = '';
|
||||
export let targetPage: number | null = null;
|
||||
|
||||
let rootEl: HTMLDivElement;
|
||||
let stageEl: HTMLElement;
|
||||
@@ -22,6 +24,8 @@
|
||||
let wheelDelta = 0;
|
||||
let lastWheelNavigationAt = 0;
|
||||
let lastScrolledSlide = -1;
|
||||
let appliedTargetPage: number | null = null;
|
||||
let appliedTargetSlides: string[] | null = null;
|
||||
let thumbnailButtons: Array<HTMLButtonElement | undefined> = [];
|
||||
const slideShortcutKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
||||
const wheelNavigationThreshold = 80;
|
||||
@@ -158,7 +162,7 @@
|
||||
const transform = pzInstance?.getTransform();
|
||||
if (transform && Math.abs(transform.scale - 1) >= 0.01) {
|
||||
e.preventDefault();
|
||||
pzInstance?.moveBy(-e.deltaX, -e.deltaY);
|
||||
pzInstance?.moveBy(-e.deltaX, -e.deltaY, false);
|
||||
zoomLevel = pzInstance?.getTransform().scale ?? 1;
|
||||
return;
|
||||
}
|
||||
@@ -203,6 +207,18 @@
|
||||
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(() => {
|
||||
pzInstance?.dispose();
|
||||
resizeObserver?.disconnect();
|
||||
|
||||
@@ -140,7 +140,8 @@ export const showOverview = writable(false);
|
||||
export const showArtifacts = writable(false);
|
||||
export const showCallOverlay = 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 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 DOMPurify from 'dompurify';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import type { FileNavOpenRequest } from '$lib/stores';
|
||||
import { normalizeDocumentTargetPage } from '$lib/utils/documentPreview';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
@@ -2321,10 +2323,12 @@ export const formatSkillName = (name) => {
|
||||
*/
|
||||
export const displayFileHandler = (
|
||||
path: string,
|
||||
stores: { showControls: Writable<boolean>; showFileNavPath: Writable<string | null> }
|
||||
stores: { showControls: Writable<boolean>; showFileNavPath: Writable<FileNavOpenRequest | null> },
|
||||
options: { page?: unknown } = {}
|
||||
) => {
|
||||
if (path) {
|
||||
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,
|
||||
name,
|
||||
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 (result?.exists !== false) {
|
||||
displayFileHandler(params.path, { showControls, showFileNavPath });
|
||||
displayFileHandler(params.path, { showControls, showFileNavPath }, { page: params?.page });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user