import { useCallback, useEffect, useState } from 'react'; import axios from 'axios'; import { APP_ENDPOINTS } from '../../config/env'; import { resolveApiErrorMessage } from '../http/apiErrors'; import type { WorkspaceFileResponse, WorkspacePreviewMode, WorkspacePreviewState } from './types'; import { buildWorkspaceDownloadHref, buildWorkspacePreviewHref, buildWorkspaceRawHref, resolveWorkspaceDocumentPath, } from './workspaceMarkdown'; import { isAudioPath, isHtmlPath, isImagePath, isVideoPath, workspaceFileAction, } from './utils'; import type { WorkspaceNotifyOptions } from './workspaceShared'; export interface WorkspacePreviewLabels { fileReadFail: string; fileEditDisabled: string; fileSaveFail: string; fileSaved: string; urlCopied: string; urlCopyFail: string; } interface UseWorkspacePreviewOptions { selectedBotId: string; workspaceCurrentPath: string; workspaceDownloadExtensionSet: ReadonlySet; loadWorkspaceTree: (botId: string, path?: string) => Promise; notify: (message: string, options?: WorkspaceNotifyOptions) => void; t: WorkspacePreviewLabels; isZh: boolean; } function buildMediaPreviewState(path: string, mode: 'image' | 'html' | 'video' | 'audio'): WorkspacePreviewState { const fileExt = (path.split('.').pop() || '').toLowerCase(); return { path, content: '', truncated: false, ext: fileExt ? `.${fileExt}` : '', isMarkdown: false, isImage: mode === 'image', isHtml: mode === 'html', isVideo: mode === 'video', isAudio: mode === 'audio', }; } export function useWorkspacePreview({ selectedBotId, workspaceCurrentPath, workspaceDownloadExtensionSet, loadWorkspaceTree, notify, t, isZh, }: UseWorkspacePreviewOptions) { const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false); const [workspacePreview, setWorkspacePreview] = useState(null); const [workspacePreviewMode, setWorkspacePreviewMode] = useState('preview'); const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false); const [workspacePreviewSaving, setWorkspacePreviewSaving] = useState(false); const [workspacePreviewDraft, setWorkspacePreviewDraft] = useState(''); const workspacePreviewCanEdit = Boolean(workspacePreview?.isMarkdown && !workspacePreview?.truncated); const workspacePreviewEditorEnabled = workspacePreviewCanEdit && workspacePreviewMode === 'edit'; const workspacePreviewPath = workspacePreview?.path || ''; const workspacePreviewContent = workspacePreview?.content || ''; const getWorkspaceDownloadHref = useCallback((filePath: string, forceDownload: boolean = true) => buildWorkspaceDownloadHref(selectedBotId, filePath, forceDownload), [selectedBotId]); const getWorkspaceRawHref = useCallback((filePath: string, forceDownload: boolean = false) => buildWorkspaceRawHref(selectedBotId, filePath, forceDownload), [selectedBotId]); const getWorkspacePreviewHref = useCallback((filePath: string) => { const normalized = String(filePath || '').trim(); if (!normalized) return ''; return buildWorkspacePreviewHref(selectedBotId, normalized, { preferRaw: isHtmlPath(normalized) }); }, [selectedBotId]); const closeWorkspacePreview = useCallback(() => { setWorkspacePreview(null); setWorkspacePreviewMode('preview'); setWorkspacePreviewFullscreen(false); setWorkspacePreviewSaving(false); setWorkspacePreviewDraft(''); }, []); const copyTextToClipboard = useCallback(async (textRaw: string, successMsg: string, failMsg: string) => { const text = String(textRaw || ''); if (!text.trim()) return; try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); } else { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); } notify(successMsg, { tone: 'success' }); } catch { notify(failMsg, { tone: 'error' }); } }, [notify]); const openWorkspaceFilePreview = useCallback(async (path: string) => { const normalizedPath = String(path || '').trim(); if (!selectedBotId || !normalizedPath) return; if (workspaceFileAction(normalizedPath, workspaceDownloadExtensionSet) === 'download') { window.open(getWorkspaceDownloadHref(normalizedPath, true), '_blank', 'noopener,noreferrer'); return; } if (isImagePath(normalizedPath)) { setWorkspacePreview(buildMediaPreviewState(normalizedPath, 'image')); return; } if (isHtmlPath(normalizedPath)) { setWorkspacePreview(buildMediaPreviewState(normalizedPath, 'html')); return; } if (isVideoPath(normalizedPath)) { setWorkspacePreview(buildMediaPreviewState(normalizedPath, 'video')); return; } if (isAudioPath(normalizedPath)) { setWorkspacePreview(buildMediaPreviewState(normalizedPath, 'audio')); return; } setWorkspaceFileLoading(true); try { const res = await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/file`, { params: { path, max_bytes: 400000 }, }); const filePath = res.data.path || path; const textExt = (filePath.split('.').pop() || '').toLowerCase(); let content = res.data.content || ''; if (textExt === 'json') { try { content = JSON.stringify(JSON.parse(content), null, 2); } catch { // Keep original content when JSON is not strictly parseable. } } setWorkspacePreview({ path: filePath, content, truncated: Boolean(res.data.truncated), ext: textExt ? `.${textExt}` : '', isMarkdown: textExt === 'md' || Boolean(res.data.is_markdown), isImage: false, isHtml: false, isVideo: false, isAudio: false, }); } catch (error: unknown) { const msg = resolveApiErrorMessage(error, t.fileReadFail); notify(msg, { tone: 'error' }); } finally { setWorkspaceFileLoading(false); } }, [getWorkspaceDownloadHref, notify, selectedBotId, t.fileReadFail, workspaceDownloadExtensionSet]); const saveWorkspacePreviewMarkdown = useCallback(async () => { if (!selectedBotId || !workspacePreview?.isMarkdown) return; if (workspacePreview.truncated) { notify(t.fileEditDisabled, { tone: 'warning' }); return; } setWorkspacePreviewSaving(true); try { const res = await axios.put( `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/file`, { content: workspacePreviewDraft }, { params: { path: workspacePreview.path } }, ); const filePath = res.data.path || workspacePreview.path; const textExt = (filePath.split('.').pop() || '').toLowerCase(); const content = res.data.content || workspacePreviewDraft; setWorkspacePreview({ ...workspacePreview, path: filePath, content, truncated: false, ext: textExt ? `.${textExt}` : '', isMarkdown: textExt === 'md' || textExt === 'markdown' || Boolean(res.data.is_markdown), }); notify(t.fileSaved, { tone: 'success' }); void loadWorkspaceTree(selectedBotId, workspaceCurrentPath); } catch (error: unknown) { notify(resolveApiErrorMessage(error, t.fileSaveFail), { tone: 'error' }); } finally { setWorkspacePreviewSaving(false); } }, [ loadWorkspaceTree, notify, selectedBotId, t.fileEditDisabled, t.fileSaveFail, t.fileSaved, workspaceCurrentPath, workspacePreview, workspacePreviewDraft, ]); const triggerWorkspaceFileDownload = useCallback((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 = getWorkspaceDownloadHref(normalized, true); link.download = filename; link.rel = 'noopener noreferrer'; document.body.appendChild(link); link.click(); link.remove(); }, [getWorkspaceDownloadHref, selectedBotId]); const copyWorkspacePreviewUrl = useCallback(async (filePath: string) => { const normalized = String(filePath || '').trim(); if (!selectedBotId || !normalized) return; const hrefRaw = getWorkspacePreviewHref(normalized); const href = (() => { try { return new URL(hrefRaw, window.location.origin).href; } catch { return hrefRaw; } })(); await copyTextToClipboard(href, t.urlCopied, t.urlCopyFail); }, [copyTextToClipboard, getWorkspacePreviewHref, selectedBotId, t.urlCopied, t.urlCopyFail]); const copyWorkspacePreviewPath = useCallback(async (filePath: string) => { const normalized = String(filePath || '').trim(); if (!normalized) return; await copyTextToClipboard( normalized, isZh ? '文件路径已复制' : 'File path copied', isZh ? '文件路径复制失败' : 'Failed to copy file path', ); }, [copyTextToClipboard, isZh]); const resolveWorkspaceMediaSrc = useCallback((srcRaw: string, baseFilePath?: string): string => { const src = String(srcRaw || '').trim(); if (!src || !selectedBotId) return src; const resolvedWorkspacePath = resolveWorkspaceDocumentPath(src, baseFilePath); if (resolvedWorkspacePath) { return getWorkspacePreviewHref(resolvedWorkspacePath); } const lower = src.toLowerCase(); if (lower.startsWith('data:') || lower.startsWith('blob:') || lower.startsWith('http://') || lower.startsWith('https://')) { return src; } return src; }, [getWorkspacePreviewHref, selectedBotId]); const resetWorkspacePreviewState = useCallback(() => { setWorkspaceFileLoading(false); setWorkspacePreview(null); setWorkspacePreviewMode('preview'); setWorkspacePreviewFullscreen(false); setWorkspacePreviewSaving(false); setWorkspacePreviewDraft(''); }, []); useEffect(() => { if (!workspacePreviewPath) { setWorkspacePreviewMode('preview'); setWorkspacePreviewSaving(false); setWorkspacePreviewDraft(''); return; } setWorkspacePreviewSaving(false); setWorkspacePreviewDraft(workspacePreviewContent); }, [workspacePreviewContent, workspacePreviewPath]); return { closeWorkspacePreview, copyWorkspacePreviewPath, copyWorkspacePreviewUrl, getWorkspaceDownloadHref, getWorkspacePreviewHref, getWorkspaceRawHref, openWorkspaceFilePreview, resetWorkspacePreviewState, resolveWorkspaceMediaSrc, saveWorkspacePreviewMarkdown, setWorkspacePreviewDraft, setWorkspacePreviewFullscreen, setWorkspacePreviewMode, triggerWorkspaceFileDownload, workspaceFileLoading, workspacePreview, workspacePreviewCanEdit, workspacePreviewDraft, workspacePreviewEditorEnabled, workspacePreviewFullscreen, workspacePreviewMode, workspacePreviewSaving, }; }