import { useEffect, useState, useRef, HTMLAttributes } from "react" import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "./xui/overlayless-sheet" import { IconButton } from "./xui/icon-button" import { createFM } from "@/api/fm" import { ModelCreateFMResponse, FMEntry, FMOpcode, FMIdentifier, FMWorkerData, FMWorkerOpcode } from "@/types" import useWebSocket from "react-use-websocket" import { toast } from "sonner" import { ColumnDef } from "@tanstack/react-table" import { Folder, File } from "lucide-react" import { fm, formatPath, fmWorker as worker } from "@/lib/utils" import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from "@/components/ui/alert-dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Row, flexRender } from "@tanstack/react-table" import { TableRow, TableCell } from "./ui/table" import { DataTable } from "./xui/virtulized-data-table" import { Input } from "@/components/ui/input" import { Filepath } from "./xui/filepath" interface FMProps { wsUrl: string; } const arraysEqual = (a: Uint8Array, b: Uint8Array) => { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } const FMComponent: React.FC = ({ wsUrl, ...props }) => { const fmRef = useRef(null); const [dOpen, setdOpen] = useState(false); const [uOpen, setuOpen] = useState(false); const columns: ColumnDef[] = [ { id: "type", header: () => Type, accessorFn: row => row.type, cell: ({ row }) => ( row.original.type == 0 ? : ), }, { header: () => Name, id: "name", accessorFn: row => row.name, cell: ({ row }) => ( {row.original.name} ), size: 5000, }, { header: () => Action, id: "download", cell: ({ row }) => { return ( { if (!dOpen) setdOpen(true); downloadFile(row.original.name); } } /> ) }, } ] const tableRowComponent = (rows: Row[]) => function getTableRow(props: HTMLAttributes) { // @ts-expect-error data-index is a valid attribute const index = props["data-index"]; const row = rows[index]; if (!row) return null; return ( { if (row.original.type === 1) { setPath(`${currentPath}/${row.original.name}`); } }} className={row.original.type === 1 ? "cursor-pointer" : "cursor-default"} {...props} > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ); }; const [fmEntires, setFMEntries] = useState([]); const firstChunk = useRef(true); const handleReady = useRef(false); const currentBasename = useRef('temp'); const waitForHandleReady = async () => { while (!handleReady.current) { await new Promise(resolve => setTimeout(resolve, 10)); } }; worker.onmessage = async (event: MessageEvent) => { switch (event.data.type) { case FMWorkerOpcode.Error: { console.error('Error from worker', event.data.error); break; } case FMWorkerOpcode.Progress: { handleReady.current = true; break; } case FMWorkerOpcode.Result: { handleReady.current = false; if (event.data.blob && event.data.fileName) { const url = URL.createObjectURL(event.data.blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = event.data.fileName; anchor.click(); URL.revokeObjectURL(url); } firstChunk.current = true; if (dOpen) setdOpen(false); break; } } } const { sendMessage, getWebSocket } = useWebSocket(wsUrl, { share: false, onOpen: () => { listFile(); }, onClose: (e) => { console.log('WebSocket connection closed:', e); }, onError: (e) => { console.error(e); toast("Websocket error", { description: "View console for details.", }) }, onMessage: async (e) => { try { const buf: ArrayBufferLike = e.data; if (firstChunk.current) { const identifier = new Uint8Array(buf, 0, 4); if (arraysEqual(identifier, FMIdentifier.file)) { worker.postMessage({ operation: 1, arrayBuffer: buf, fileName: currentBasename.current }); firstChunk.current = false; } else if (arraysEqual(identifier, FMIdentifier.fileName)) { const { path, fmList } = await fm.parseFMList(buf); setPath(path); setFMEntries(fmList); } else if (arraysEqual(identifier, FMIdentifier.error)) { const errBytes = buf.slice(4); const errMsg = new TextDecoder('utf-8').decode(errBytes); throw new Error(errMsg); } else if (arraysEqual(identifier, FMIdentifier.complete)) { // Upload completed if (uOpen) setuOpen(false); listFile(); } else { throw new Error("Unknown identifier"); } } else { await waitForHandleReady(); worker.postMessage({ operation: 2, arrayBuffer: buf, fileName: currentBasename.current }); } } catch (error) { console.error('Error processing received data:', error); toast("FM error", { description: "View console for details.", }) if (dOpen) setdOpen(false); if (uOpen) setuOpen(false); } } }); const socket = getWebSocket(); useEffect(() => { if (socket && 'binaryType' in socket) socket.binaryType = 'arraybuffer'; }, [socket]) const [currentPath, setPath] = useState(''); useEffect(() => { listFile(); }, [currentPath]) const listFile = () => { const prefix = new Int8Array([FMOpcode.List]); const pathMsg = new TextEncoder().encode(currentPath); const msg = new Int8Array(prefix.length + pathMsg.length); msg.set(prefix); msg.set(pathMsg, prefix.length); sendMessage(msg); } const downloadFile = (basename: string) => { currentBasename.current = basename; const prefix = new Int8Array([FMOpcode.Download]); const filePathMessage = new TextEncoder().encode(`${currentPath}/${basename}`); const msg = new Int8Array(prefix.length + filePathMessage.length); msg.set(prefix); msg.set(filePathMessage, prefix.length); sendMessage(msg); } const uploadFile = async (file: File) => { const chunkSize = 1048576; // 1MB chunk let offset = 0; // Send header const header = fm.buildUploadHeader({ path: currentPath, file: file }); sendMessage(header); // Send data chunks while (offset < file.size) { const chunk = file.slice(offset, offset + chunkSize); const arrayBuffer = await fm.readFileAsArrayBuffer(chunk); if (arrayBuffer) sendMessage(arrayBuffer); offset += chunkSize; } } const fileInputRef = useRef(null); const [gotoPath, setGotoPath] = useState(''); return ( Refresh { await navigator.clipboard.writeText(formatPath(currentPath)); } }>Copy path Goto Goto { setGotoPath(e.target.value) }} /> Cancel { setPath(gotoPath) }}>Confirm Pseudo File Manager { const files = e.target.files; if (files && files.length > 0) { if (!uOpen) setuOpen(true); await uploadFile(files[0]); } } } /> { if (fileInputRef.current) fileInputRef.current.click(); } } /> Downloading... Uploading... ); } export const FMCard = ({ id }: { id?: string }) => { const [open, setOpen] = useState(false); const [fm, setFM] = useState(null); const [init, setInit] = useState(false); const fetchFM = async () => { if (id) { try { setInit(false); const createdFM = await createFM(id); setFM(createdFM); } catch (e) { toast("FM API Error", { description: "View console for details.", }) console.error("fetch error", e); return; } setInit(true); } } return ( { if (isOpen) setOpen(true); }}> {fm?.session_id && init ? : The server does not exist, or have not been connected yet. } ) }
The server does not exist, or have not been connected yet.