316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
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<string>;
|
|
loadWorkspaceTree: (botId: string, path?: string) => Promise<void>;
|
|
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<WorkspacePreviewState | null>(null);
|
|
const [workspacePreviewMode, setWorkspacePreviewMode] = useState<WorkspacePreviewMode>('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<WorkspaceFileResponse>(`${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<WorkspaceFileResponse>(
|
|
`${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,
|
|
};
|
|
}
|