import { createFM } from "@/api/fm" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog" import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { useMediaQuery } from "@/hooks/useMediaQuery" import { copyToClipboard, fm, formatPath, fmWorker as worker } from "@/lib/utils" import { FMEntry, FMIdentifier, FMOpcode, FMWorkerData, FMWorkerOpcode, ModelCreateFMResponse, } from "@/types" import { ColumnDef } from "@tanstack/react-table" import { Row, flexRender } from "@tanstack/react-table" import { File, Folder } from "lucide-react" import { HTMLAttributes, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { TableCell, TableRow } from "./ui/table" import { Filepath } from "./xui/filepath" import { IconButton } from "./xui/icon-button" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from "./xui/overlayless-sheet" import { DataTable } from "./xui/virtulized-data-table" 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 { t } = useTranslation() const fmRef = useRef(null) const wsRef = useRef(null) useEffect(() => { return () => { wsRef.current?.close() } }, []) const [dOpen, setdOpen] = useState(false) const [uOpen, setuOpen] = useState(false) const columns: ColumnDef[] = [ { id: "type", header: () => {t("Type")}, accessorFn: (row) => row.type, cell: ({ row }) => (row.original.type == 0 ? : ), }, { header: () => {t("Name")}, id: "name", accessorFn: (row) => row.name, cell: ({ row }) => (
{row.original.name}
), size: 5000, }, { header: () => {t("Actions")}, 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 [currentPath, setPath] = useState("") useEffect(() => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { listFile() } }, [wsRef.current, currentPath]) useEffect(() => { const ws = new WebSocket(wsUrl) wsRef.current = ws ws.binaryType = "arraybuffer" ws.onopen = () => { listFile() } ws.onclose = (e) => { console.log("WebSocket connection closed:", e) } ws.onerror = (e) => { console.error(e) toast("Websocket" + " " + t("Error"), { description: t("Results.UnExpectedError"), }) } ws.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(t("Results.UnknownIdentifier")) } } else { await waitForHandleReady() worker.postMessage({ operation: 2, arrayBuffer: buf, fileName: currentBasename.current, }) } } catch (error) { console.error("Error processing received data:", error) toast("FM" + " " + t("Error"), { description: t("Results.UnExpectedError"), }) if (dOpen) setdOpen(false) if (uOpen) setuOpen(false) } } }, [wsUrl]) 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) wsRef.current?.send(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) wsRef.current?.send(msg) } const uploadFile = async (file: File) => { const chunkSize = 1048576 // 1MB chunk let offset = 0 // Send header const header = fm.buildUploadHeader({ path: currentPath, file: file }) wsRef.current?.send(header) // Send data chunks while (offset < file.size) { const chunk = file.slice(offset, offset + chunkSize) const arrayBuffer = await fm.readFileAsArrayBuffer(chunk) if (arrayBuffer) wsRef.current?.send(arrayBuffer) offset += chunkSize } } const fileInputRef = useRef(null) const [gotoPath, setGotoPath] = useState("") return (
{t("Refresh")} { try { await copyToClipboard(formatPath(currentPath)) } catch (error: any) { toast("FM" + " " + t("Error"), { description: error.message, }) console.log("copy error: ", error) } }} > {t("CopyPath")} {t("Goto")} {t("Goto")} { setGotoPath(e.target.value) }} /> {t("Close")} { setPath(gotoPath) }} > {t("Confirm")}

{t("FileManager")}

{ const files = e.target.files if (files && files.length > 0) { if (!uOpen) setuOpen(true) await uploadFile(files[0]) } }} /> { if (fileInputRef.current) fileInputRef.current.click() }} />
{t("Downloading")}... {t("Uploading")}...
) } export const FMCard = ({ id }: { id?: string }) => { const { t } = useTranslation() const [open, setOpen] = useState(false) const [fm, setFM] = useState(null) const [init, setInit] = useState(false) const isDesktop = useMediaQuery("(min-width: 640px)") const fetchFM = async () => { if (id) { try { setInit(false) const createdFM = await createFM(id) setFM(createdFM) } catch (e) { toast(t("Error"), { description: t("Results.UnExpectedError"), }) console.error("fetch error", e) return } setInit(true) } } return isDesktop ? ( { if (isOpen) setOpen(true) }} >
{fm?.session_id && init ? ( ) : (

{t("Results.TheServerDoesNotOnline")}

)}
) : (
{fm?.session_id && init ? ( ) : (

{t("Results.TheServerDoesNotOnline")}

)}
) }