diff --git a/frontend/src/modules/dashboard/BotDashboardModule.css b/frontend/src/modules/dashboard/BotDashboardModule.css index 5d165e1..24f4821 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.css +++ b/frontend/src/modules/dashboard/BotDashboardModule.css @@ -1395,6 +1395,20 @@ width: min(1080px, 95vw); } +.modal-preview-fullscreen { + width: 100vw; + max-width: 100vw; + height: 100vh; + max-height: 100vh; + margin: 0; + border-radius: 0; +} + +.modal-preview-fullscreen .workspace-preview-body { + min-height: calc(100vh - 170px); + max-height: calc(100vh - 170px); +} + .workspace-preview-body { border: 1px solid var(--line); border-radius: 10px; diff --git a/frontend/src/modules/dashboard/BotDashboardModule.tsx b/frontend/src/modules/dashboard/BotDashboardModule.tsx index 1a938df..09d0f94 100644 --- a/frontend/src/modules/dashboard/BotDashboardModule.tsx +++ b/frontend/src/modules/dashboard/BotDashboardModule.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type ChangeEvent, type KeyboardEvent, type ReactNode } from 'react'; import axios from 'axios'; -import { Activity, Boxes, Check, Clock3, EllipsisVertical, Eye, EyeOff, FileText, FolderOpen, Hammer, MessageSquareText, Paperclip, Plus, Power, PowerOff, RefreshCw, Repeat2, Save, Settings2, SlidersHorizontal, TriangleAlert, Trash2, UserRound, Waypoints, X } from 'lucide-react'; +import { Activity, Boxes, Check, Clock3, EllipsisVertical, Eye, EyeOff, FileText, FolderOpen, Hammer, Maximize2, MessageSquareText, Minimize2, Paperclip, Plus, Power, PowerOff, RefreshCw, Repeat2, Save, Settings2, SlidersHorizontal, TriangleAlert, Trash2, UserRound, Waypoints, X } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; @@ -208,7 +208,7 @@ function normalizeRuntimeState(s?: string) { function isPreviewableWorkspaceFile(node: WorkspaceNode) { if (node.type !== 'file') return false; const ext = (node.ext || '').toLowerCase(); - return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp'].includes(ext); + return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].includes(ext); } function isPdfPath(path: string) { @@ -220,9 +220,16 @@ function isImagePath(path: string) { return normalized.endsWith('.png') || normalized.endsWith('.jpg') || normalized.endsWith('.jpeg') || normalized.endsWith('.webp'); } +function isOfficePath(path: string) { + const normalized = String(path || '').trim().toLowerCase(); + return ['.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].some((ext) => + normalized.endsWith(ext), + ); +} + function isPreviewableWorkspacePath(path: string) { const normalized = String(path || '').trim().toLowerCase(); - return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp'].some((ext) => + return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.odt', '.ods', '.odp', '.wps'].some((ext) => normalized.endsWith(ext), ); } @@ -252,7 +259,7 @@ function decorateWorkspacePathsForMarkdown(text: string) { '[$1]($2)', ); const workspacePathPattern = - /\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp)\b/gi; + /\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b/gi; return normalizedExistingLinks.replace(workspacePathPattern, (fullPath) => { const normalized = normalizeDashboardAttachmentPath(fullPath); if (!normalized) return fullPath; @@ -366,6 +373,7 @@ export function BotDashboardModule({ const [workspaceParentPath, setWorkspaceParentPath] = useState(null); const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false); const [workspacePreview, setWorkspacePreview] = useState(null); + const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false); const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(true); const [pendingAttachments, setPendingAttachments] = useState([]); const [isUploadingAttachments, setIsUploadingAttachments] = useState(false); @@ -396,9 +404,32 @@ export function BotDashboardModule({ const [expandedProgressByKey, setExpandedProgressByKey] = useState>({}); const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false); const runtimeMenuRef = useRef(null); + const buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) => + `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(filePath)}${forceDownload ? '&download=1' : ''}`; + const closeWorkspacePreview = () => { + setWorkspacePreview(null); + setWorkspacePreviewFullscreen(false); + }; + const triggerWorkspaceFileDownload = (filePath: string) => { + if (!selectedBotId) return; + const normalized = String(filePath || '').trim(); + if (!normalized) return; + const filename = normalized.split('/').pop() || 'workspace-file'; + const link = document.createElement('a'); + link.href = buildWorkspaceDownloadHref(normalized, true); + link.download = filename; + link.rel = 'noopener noreferrer'; + document.body.appendChild(link); + link.click(); + link.remove(); + }; const openWorkspacePathFromChat = (path: string) => { const normalized = String(path || '').trim(); if (!normalized) return; + if (isPdfPath(normalized) || isOfficePath(normalized)) { + triggerWorkspaceFileDownload(normalized); + return; + } if (!isPreviewableWorkspacePath(normalized)) { notify(fileNotPreviewableLabel, { tone: 'warning' }); return; @@ -409,7 +440,7 @@ export function BotDashboardModule({ const source = String(text || ''); if (!source) return [source]; const pattern = - /\[(\/root\/\.nanobot\/workspace\/[^\]]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp))\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp)\b|https:\/\/workspace\.local\/open\/[^\s)]+/gi; + /\[(\/root\/\.nanobot\/workspace\/[^\]]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps))\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|json|log|txt|csv|pdf|png|jpg|jpeg|webp|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b|https:\/\/workspace\.local\/open\/[^\s)]+/gi; const nodes: ReactNode[] = []; let lastIndex = 0; let matchIndex = 0; @@ -485,11 +516,7 @@ export function BotDashboardModule({ href="#" onClick={(event) => { event.preventDefault(); - if (!isPreviewableWorkspacePath(workspacePath)) { - notify(fileNotPreviewableLabel, { tone: 'warning' }); - return; - } - void openWorkspaceFilePreview(workspacePath); + openWorkspacePathFromChat(workspacePath); }} {...props} > @@ -696,17 +723,20 @@ export function BotDashboardModule({
{(item.attachments || []).map((rawPath) => { const filePath = normalizeDashboardAttachmentPath(rawPath); - const isPdf = isPdfPath(filePath); - const href = `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(filePath)}${isPdf ? '&download=1' : ''}`; const filename = filePath.split('/').pop() || filePath; return ( { + event.preventDefault(); + if (isPdfPath(filePath) || isOfficePath(filePath)) { + triggerWorkspaceFileDownload(filePath); + return; + } + openWorkspacePathFromChat(filePath); + }} > {filename} @@ -822,9 +852,9 @@ export function BotDashboardModule({ const openWorkspaceFilePreview = async (path: string) => { if (!selectedBotId || !path) return; const normalizedPath = String(path || '').trim(); - if (isPdfPath(normalizedPath)) { - const href = `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(normalizedPath)}&download=1`; - window.open(href, '_blank', 'noopener,noreferrer'); + setWorkspacePreviewFullscreen(false); + if (isPdfPath(normalizedPath) || isOfficePath(normalizedPath)) { + triggerWorkspaceFileDownload(normalizedPath); return; } if (isImagePath(normalizedPath)) { @@ -1565,14 +1595,14 @@ export function BotDashboardModule({ } const previewable = isPreviewableWorkspaceFile(node); - const pdfFile = String(node.ext || '').toLowerCase() === '.pdf'; + const downloadOnlyFile = isPdfPath(node.path) || isOfficePath(node.path); rendered.push( {t.download} - +