diff --git a/src/components/Folder/index.tsx b/src/components/Folder/index.tsx index b12816221..9c204530f 100644 --- a/src/components/Folder/index.tsx +++ b/src/components/Folder/index.tsx @@ -1,644 +1,939 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from 'react'; import { - ChevronsLeft, - Search, - FileText, - CodeXml, - ChevronLeft, - Download, - Folder as FolderIcon, - ChevronRight, - ChevronDown, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; -import FolderComponent from "./FolderComponent"; + ChevronsLeft, + Search, + FileText, + CodeXml, + ChevronLeft, + Download, + Folder as FolderIcon, + ChevronRight, + ChevronDown, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import FolderComponent from './FolderComponent'; -import { MarkDown } from "@/components/ChatBox/MessageItem/MarkDown"; -import { useAuthStore } from "@/store/authStore"; -import { proxyFetchGet } from "@/api/http"; -import { useTranslation } from "react-i18next"; -import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; +import { MarkDown } from '@/components/ChatBox/MessageItem/MarkDown'; +import { useAuthStore } from '@/store/authStore'; +import { proxyFetchGet } from '@/api/http'; +import { useTranslation } from 'react-i18next'; +import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; +import DOMPurify from 'dompurify'; // Type definitions interface FileTreeNode { - name: string; - path: string; - type?: string; - isFolder?: boolean; - icon?: React.ElementType; - children?: FileTreeNode[]; - isRemote?: boolean; + name: string; + path: string; + type?: string; + isFolder?: boolean; + icon?: React.ElementType; + children?: FileTreeNode[]; + isRemote?: boolean; } interface FileInfo { - name: string; - path: string; - type: string; - isFolder?: boolean; - icon?: React.ElementType; - content?: string; - relativePath?: string; - isRemote?: boolean; + name: string; + path: string; + type: string; + isFolder?: boolean; + icon?: React.ElementType; + content?: string; + relativePath?: string; + isRemote?: boolean; } // FileTree component to render nested file structure interface FileTreeProps { - node: FileTreeNode; - level?: number; - selectedFile: FileInfo | null; - expandedFolders: Set; - onToggleFolder: (path: string) => void; - onSelectFile: (file: FileInfo) => void; - isShowSourceCode: boolean; + node: FileTreeNode; + level?: number; + selectedFile: FileInfo | null; + expandedFolders: Set; + onToggleFolder: (path: string) => void; + onSelectFile: (file: FileInfo) => void; + isShowSourceCode: boolean; } const FileTree: React.FC = ({ - node, - level = 0, - selectedFile, - expandedFolders, - onToggleFolder, - onSelectFile, - isShowSourceCode, + node, + level = 0, + selectedFile, + expandedFolders, + onToggleFolder, + onSelectFile, + isShowSourceCode, }) => { - if (!node.children || node.children.length === 0) return null; + if (!node.children || node.children.length === 0) return null; - return ( -
0 ? "ml-4" : ""}> - {node.children.map((child) => { - const isExpanded = expandedFolders.has(child.path); - const fileInfo: FileInfo = { - name: child.name, - path: child.path, - type: child.type || "", - isFolder: child.isFolder, - icon: child.icon, - isRemote: child.isRemote, - }; + return ( +
0 ? 'ml-4' : ''}> + {node.children.map((child) => { + const isExpanded = expandedFolders.has(child.path); + const fileInfo: FileInfo = { + name: child.name, + path: child.path, + type: child.type || '', + isFolder: child.isFolder, + icon: child.icon, + isRemote: child.isRemote, + }; - return ( -
- + + {child.name} + + - {child.isFolder && isExpanded && child.children && ( - - )} -
- ); - })} -
- ); + {child.isFolder && isExpanded && child.children && ( + + )} +
+ ); + })} + + ); }; function downloadByBrowser(url: string) { - window.ipcRenderer - .invoke("download-file", url) - .then((result) => { - if (result.success) { - console.log("download-file success:", result.path); - } else { - console.error("download-file error:", result.error); - } - }) - .catch((error) => { - console.error("download-file error:", error); - }); + window.ipcRenderer + .invoke('download-file', url) + .then((result) => { + if (result.success) { + console.log('download-file success:', result.path); + } else { + console.error('download-file error:', result.error); + } + }) + .catch((error) => { + console.error('download-file error:', error); + }); } export default function Folder({ data }: { data?: Agent }) { - //Get Chatstore for the active project's task - const { chatStore, projectStore } = useChatStoreAdapter(); - if (!chatStore) { - return
Loading...
; - } - - const authStore = useAuthStore(); - const { t } = useTranslation(); - const [selectedFile, setSelectedFile] = useState(null); - const [loading, setLoading] = useState(false); + //Get Chatstore for the active project's task + const { chatStore, projectStore } = useChatStoreAdapter(); + if (!chatStore) { + return
Loading...
; + } - const selectedFileChange = (file: FileInfo, isShowSourceCode?: boolean) => { - if (file.type === "zip") { - // if file is remote, don't call reveal-in-folder - if (file.isRemote) { - downloadByBrowser(file.path); - return; - } - window.ipcRenderer.invoke("reveal-in-folder", file.path); - return; - } - // Don't open folders in preview - they are handled by expand/collapse - if (file.isFolder) { - return; - } - setSelectedFile(file); - setLoading(true); - console.log("file", JSON.parse(JSON.stringify(file))); + const authStore = useAuthStore(); + const { t } = useTranslation(); + const [selectedFile, setSelectedFile] = useState(null); + const [loading, setLoading] = useState(false); - // For PDF files, use data URL instead of custom protocol - if (file.type === "pdf") { - window.ipcRenderer - .invoke("read-file-dataurl", file.path) - .then((dataUrl: string) => { - setSelectedFile({ ...file, content: dataUrl }); - chatStore.setSelectedFile(chatStore.activeTaskId as string, file); - setLoading(false); - }) - .catch((error) => { - console.error("read-file-dataurl error:", error); - setLoading(false); - }); - return; - } + const selectedFileChange = (file: FileInfo, isShowSourceCode?: boolean) => { + if (file.type === 'zip') { + // if file is remote, don't call reveal-in-folder + if (file.isRemote) { + downloadByBrowser(file.path); + return; + } + window.ipcRenderer.invoke('reveal-in-folder', file.path); + return; + } + // Don't open folders in preview - they are handled by expand/collapse + if (file.isFolder) { + return; + } + setSelectedFile(file); + setLoading(true); + console.log('file', JSON.parse(JSON.stringify(file))); - // all other files call open-file interface, the backend handles download and parsing - window.ipcRenderer - .invoke("open-file", file.type, file.path, isShowSourceCode) - .then((res) => { - setSelectedFile({ ...file, content: res }); - chatStore.setSelectedFile(chatStore.activeTaskId as string, file); - setLoading(false); - }) - .catch((error) => { - console.error("open-file error:", error); - setLoading(false); - }); - }; + // For PDF files, use data URL instead of custom protocol + if (file.type === 'pdf') { + window.ipcRenderer + .invoke('read-file-dataurl', file.path) + .then((dataUrl: string) => { + setSelectedFile({ ...file, content: dataUrl }); + chatStore.setSelectedFile(chatStore.activeTaskId as string, file); + setLoading(false); + }) + .catch((error) => { + console.error('read-file-dataurl error:', error); + setLoading(false); + }); + return; + } - const [isShowSourceCode, setIsShowSourceCode] = useState(false); - const isShowSourceCodeChange = () => { - // all files can reload content - selectedFileChange(selectedFile!, !isShowSourceCode); - setIsShowSourceCode(!isShowSourceCode); - }; + // all other files call open-file interface, the backend handles download and parsing + window.ipcRenderer + .invoke('open-file', file.type, file.path, isShowSourceCode) + .then((res) => { + setSelectedFile({ ...file, content: res }); + chatStore.setSelectedFile(chatStore.activeTaskId as string, file); + setLoading(false); + }) + .catch((error) => { + console.error('open-file error:', error); + setLoading(false); + }); + }; - const [isCollapsed, setIsCollapsed] = useState(false); + const [isShowSourceCode, setIsShowSourceCode] = useState(false); + const isShowSourceCodeChange = () => { + // all files can reload content + selectedFileChange(selectedFile!, !isShowSourceCode); + setIsShowSourceCode(!isShowSourceCode); + }; - const buildFileTree = (files: FileInfo[]): FileTreeNode => { - const root: FileTreeNode = { - name: "root", - path: "", - children: [], - isFolder: true, - }; + const [isCollapsed, setIsCollapsed] = useState(false); - const nodeMap = new Map(); - nodeMap.set("", root); + const buildFileTree = (files: FileInfo[]): FileTreeNode => { + const root: FileTreeNode = { + name: 'root', + path: '', + children: [], + isFolder: true, + }; - const sortedFiles = [...files].sort((a, b) => { - const depthA = (a.relativePath || "").split("/").filter(Boolean).length; - const depthB = (b.relativePath || "").split("/").filter(Boolean).length; - return depthA - depthB; - }); + const nodeMap = new Map(); + nodeMap.set('', root); - for (const file of sortedFiles) { - const fullRelativePath = file.relativePath - ? `${file.relativePath}/${file.name}` - : file.name; + const sortedFiles = [...files].sort((a, b) => { + const depthA = (a.relativePath || '').split('/').filter(Boolean).length; + const depthB = (b.relativePath || '').split('/').filter(Boolean).length; + return depthA - depthB; + }); - const parentPath = file.relativePath || ""; - const parentNode = nodeMap.get(parentPath) || root; + for (const file of sortedFiles) { + const fullRelativePath = file.relativePath + ? `${file.relativePath}/${file.name}` + : file.name; - const node: FileTreeNode = { - name: file.name, - path: file.path, - type: file.type, - isFolder: file.isFolder, - icon: file.icon, - children: file.isFolder ? [] : undefined, - isRemote: file.isRemote, - }; + const parentPath = file.relativePath || ''; + const parentNode = nodeMap.get(parentPath) || root; - parentNode.children!.push(node); + const node: FileTreeNode = { + name: file.name, + path: file.path, + type: file.type, + isFolder: file.isFolder, + icon: file.icon, + children: file.isFolder ? [] : undefined, + isRemote: file.isRemote, + }; - if (file.isFolder) { - nodeMap.set(fullRelativePath, node); - } - } + parentNode.children!.push(node); - return root; - }; + if (file.isFolder) { + nodeMap.set(fullRelativePath, node); + } + } - const [fileTree, setFileTree] = useState({ - name: "root", - path: "", - children: [], - isFolder: true, - }); + return root; + }; - const [expandedFolders, setExpandedFolders] = useState>( - new Set() - ); + const [fileTree, setFileTree] = useState({ + name: 'root', + path: '', + children: [], + isFolder: true, + }); - const toggleFolder = (folderPath: string) => { - setExpandedFolders((prev) => { - const newSet = new Set(prev); - if (newSet.has(folderPath)) { - newSet.delete(folderPath); - } else { - newSet.add(folderPath); - } - return newSet; - }); - }; + const [expandedFolders, setExpandedFolders] = useState>( + new Set() + ); - const [fileGroups, setFileGroups] = useState< - { - folder: string; - files: FileInfo[]; - }[] - >([ - { - folder: "Reports", - files: [], - }, - ]); + const toggleFolder = (folderPath: string) => { + setExpandedFolders((prev) => { + const newSet = new Set(prev); + if (newSet.has(folderPath)) { + newSet.delete(folderPath); + } else { + newSet.add(folderPath); + } + return newSet; + }); + }; - const hasFetchedRemote = useRef(false); + const [fileGroups, setFileGroups] = useState< + { + folder: string; + files: FileInfo[]; + }[] + >([ + { + folder: 'Reports', + files: [], + }, + ]); - // Reset hasFetchedRemote when activeTaskId changes - useEffect(() => { - hasFetchedRemote.current = false; - }, [chatStore.activeTaskId]); + const hasFetchedRemote = useRef(false); - useEffect(() => { - const setFileList = async () => { - let res = null; - res = await window.ipcRenderer.invoke( - "get-project-file-list", - authStore.email, - projectStore.activeProjectId as string - ); - let tree: any = null; - if ( - (res && res.length > 0) || - import.meta.env.VITE_USE_LOCAL_PROXY === "true" - ) { - tree = buildFileTree(res || []); - } else { - if (!hasFetchedRemote.current) { - //TODO(file): rename endpoint to use project_id - res = await proxyFetchGet("/api/chat/files", { - task_id: projectStore.activeProjectId as string, - }); - hasFetchedRemote.current = true; - } - console.log("res", res); - if (res) { - res = res.map((item: any) => { - return { - name: item.filename, - type: item.filename.split(".")[1], - path: item.url, - isRemote: true, - }; - }); - tree = buildFileTree(res || []); - } - } - setFileTree(tree); - // Keep the old structure for compatibility - setFileGroups((prev) => { - const chatStoreSelectedFile = - chatStore.tasks[chatStore.activeTaskId as string]?.selectedFile; - if (chatStoreSelectedFile) { - console.log(res, chatStoreSelectedFile); - const file = res.find( - (item: any) => item.name === chatStoreSelectedFile.name - ); - console.log("file", file); - if (file && selectedFile?.path !== chatStoreSelectedFile?.path) { - selectedFileChange(file as FileInfo, isShowSourceCode); - } - } - return [ - { - ...prev[0], - files: res || [], - }, - ]; - }); - }; - setFileList(); - }, [chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning]); + // Reset hasFetchedRemote when activeTaskId changes + useEffect(() => { + hasFetchedRemote.current = false; + }, [chatStore.activeTaskId]); - useEffect(() => { - const chatStoreSelectedFile = - chatStore.tasks[chatStore.activeTaskId as string]?.selectedFile; - if (chatStoreSelectedFile && fileGroups[0]?.files) { - const file = fileGroups[0].files.find( - (item: any) => item.path === chatStoreSelectedFile.path - ); - if (file && selectedFile?.path !== chatStoreSelectedFile?.path) { - selectedFileChange(file as FileInfo, isShowSourceCode); - } - } - }, [ - chatStore.tasks[chatStore.activeTaskId as string]?.selectedFile?.path, - fileGroups, - isShowSourceCode, - chatStore.activeTaskId, - ]); + useEffect(() => { + const setFileList = async () => { + let res = null; + res = await window.ipcRenderer.invoke( + 'get-project-file-list', + authStore.email, + projectStore.activeProjectId as string + ); + let tree: any = null; + if ( + (res && res.length > 0) || + import.meta.env.VITE_USE_LOCAL_PROXY === 'true' + ) { + tree = buildFileTree(res || []); + } else { + if (!hasFetchedRemote.current) { + //TODO(file): rename endpoint to use project_id + res = await proxyFetchGet('/api/chat/files', { + task_id: projectStore.activeProjectId as string, + }); + hasFetchedRemote.current = true; + } + console.log('res', res); + if (res) { + res = res.map((item: any) => { + return { + name: item.filename, + type: item.filename.split('.')[1], + path: item.url, + isRemote: true, + }; + }); + tree = buildFileTree(res || []); + } + } + setFileTree(tree); + // Keep the old structure for compatibility + setFileGroups((prev) => { + const chatStoreSelectedFile = + chatStore.tasks[chatStore.activeTaskId as string]?.selectedFile; + if (chatStoreSelectedFile) { + console.log(res, chatStoreSelectedFile); + const file = res.find( + (item: any) => item.name === chatStoreSelectedFile.name + ); + console.log('file', file); + if (file && selectedFile?.path !== chatStoreSelectedFile?.path) { + selectedFileChange(file as FileInfo, isShowSourceCode); + } + } + return [ + { + ...prev[0], + files: res || [], + }, + ]; + }); + }; + setFileList(); + }, [chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning]); - const handleBack = () => { - chatStore.setActiveWorkSpace(chatStore.activeTaskId as string, "workflow"); - }; + useEffect(() => { + const chatStoreSelectedFile = + chatStore.tasks[chatStore.activeTaskId as string]?.selectedFile; + if (chatStoreSelectedFile && fileGroups[0]?.files) { + const file = fileGroups[0].files.find( + (item: any) => item.path === chatStoreSelectedFile.path + ); + if (file && selectedFile?.path !== chatStoreSelectedFile?.path) { + selectedFileChange(file as FileInfo, isShowSourceCode); + } + } + }, [ + chatStore.tasks[chatStore.activeTaskId as string]?.selectedFile?.path, + fileGroups, + isShowSourceCode, + chatStore.activeTaskId, + ]); - return ( -
- {/* fileList */} -
- {/* head */} -
-
- {!isCollapsed && ( -
- - - {t("chat.agent-folder")} - -
- )} - -
-
+ const handleBack = () => { + chatStore.setActiveWorkSpace(chatStore.activeTaskId as string, 'workflow'); + }; - {/* Search Input*/} - {!isCollapsed && ( -
-
- - -
-
- )} + return ( +
+ {/* fileList */} +
+ {/* head */} +
+
+ {!isCollapsed && ( +
+ + + {t('chat.agent-folder')} + +
+ )} + +
+
- {/* fileList */} -
- {!isCollapsed ? ( -
-
-
- {t("chat.files")} -
- - selectedFileChange(file, isShowSourceCode) - } - isShowSourceCode={isShowSourceCode} - /> -
-
- ) : ( - // Display simplified file icons when collapsed -
- {fileGroups.map((group) => - group.files.map((file) => ( - - )) - )} -
- )} -
-
+ {/* Search Input*/} + {!isCollapsed && ( +
+
+ + +
+
+ )} - {/* content */} -
- {/* head */} - {selectedFile && ( -
-
-
{ - // if file is remote, don't call reveal-in-folder - if (selectedFile.isRemote) { - downloadByBrowser(selectedFile.path); - return; - } - window.ipcRenderer.invoke( - "reveal-in-folder", - selectedFile.path - ); - }} - className="flex-1 min-w-0 overflow-hidden cursor-pointer flex items-center gap-2" - > - - {selectedFile.name} - - -
- -
-
- )} + {/* fileList */} +
+ {!isCollapsed ? ( +
+
+
+ {t('chat.files')} +
+ + selectedFileChange(file, isShowSourceCode) + } + isShowSourceCode={isShowSourceCode} + /> +
+
+ ) : ( + // Display simplified file icons when collapsed +
+ {fileGroups.map((group) => + group.files.map((file) => ( + + )) + )} +
+ )} +
+
- {/* content */} -
-
- {selectedFile ? ( - !loading ? ( - selectedFile.type === "md" && !isShowSourceCode ? ( -
- -
- ) : selectedFile.type === "pdf" ? ( -