diff --git a/src/components/workboard/inference-node.tsx b/src/components/workboard/inference-node.tsx index 180cff6..15562e9 100644 --- a/src/components/workboard/inference-node.tsx +++ b/src/components/workboard/inference-node.tsx @@ -1,415 +1,403 @@ -// import { useInterpret, useSelector } from '@xstate/react'; -// import { useState } from 'react'; -import { Handle, type NodeProps, Position } from 'reactflow'; -// import { State, type StateFrom, assign, createMachine } from 'xstate'; -// import { Button } from '~/components/ui/button'; -// import { -// Card, -// CardContent, -// CardDescription, -// CardFooter, -// CardHeader, -// CardTitle, -// } from '~/components/ui/card'; -// import { -// Sheet, -// SheetContent, -// SheetHeader, -// SheetTitle, -// } from '~/components/ui/sheet'; -// import { toast } from '~/components/ui/use-toast'; -// import { -// type FormType, -// InferenceForm, -// } from '~/components/workboard/node-component-forms/inference-form'; +import dayjs from 'dayjs'; +import { AlertTriangleIcon, CoffeeIcon } from 'lucide-react'; +import { useEffect, useState } from 'react'; import { - type NodeData, - type NodeStatus, - useStoreActions, -} from '~/hooks/use-store'; + Handle, + type NodeProps, + Position, + useUpdateNodeInternals, +} from 'reactflow'; +import { toast } from 'sonner'; +import { z } from 'zod'; +import { Alert, AlertDescription, AlertTitle } from '~/components/ui/alert'; +import { Button } from '~/components/ui/button'; +import { Card, CardContent } from '~/components/ui/card'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '~/components/ui/sheet'; +import { type NodeData, useStoreActions } from '~/hooks/use-store'; +import { api } from '~/utils/api'; -// import { api } from '~/utils/api'; +import { + type FormType, + InferenceForm, +} from './node-component-forms/inference-form'; -// import { type NetworkXState } from './network-node'; +export function InferenceNode(nodeProps: NodeProps) { + const formEditState = + nodeProps.selected && + ['active', 'error', 'success'].includes(nodeProps.data.status); + const [open, setOpen] = useState(false); + const [pokemon, setPokemon] = useState(''); -// interface JobEvent { -// type: 'done.invoke'; -// data: { jobId?: string; jobStatus?: string; formData?: FormType }; -// } + useEffect(() => { + setOpen(formEditState); + }, [formEditState]); -// type FormSubmitEvent = -// | { type: 'start'; data: { formData: FormType; connectedNodeName: string } } -// | { type: 'retry'; data: { formData: FormType; connectedNodeName: string } }; + useEffect(() => { + const randomFirstGenPokemon = Math.floor(Math.random() * 151) + 1; + fetch( + 'https://pokeapi.co/api/v2/pokemon/' + randomFirstGenPokemon.toString(), + ) + .then((res) => res.json()) + .then((data: unknown) => { + const parsed = z + .object({ + name: z.string(), + }) + .parse(data); + setPokemon(parsed.name); + }) + .catch((err) => { + console.log(err); + }); + }, []); -// const inferenceMachine = createMachine({ -// /** @xstate-layout N4IgpgJg5mDOIC5QEsB2AzMAnMqDGYAdGgIZ4AuyAbmAMRmVUnlgDaADALqKgAOA9rGSV+qHiAAeiAIwA2AMyEAnCqUB2JQCYAHEoCs2te1kAaEAE9E82ZsJrNsvWoAsavUtnsdzgL4+zaJg4+EQM1HSw5CRY5BzcSCACQiJiCVIIzuxqhM6a0tp67tq60vLWZpYI1rb2jnoKes7S7OzafgEY2LgEhABGAK6w5oS8uBBoULQQokRoVPwA1kSw-b0AtsIAUvy9ceJJwsii4unarTnazvJn7HqamhraFYhOeoROmlrSV9Vl7SCBLohPqDYajVDjVCTbBYfhYEYAG2Y6Dha0IK3WWx2ewSBxSJxkOiUhHkVzU8nJmnkSmkBmeCAeb1qTjUcmk9n0-0BwR6AyGhCw-VQqAmUxmxFQ8yWEqC3SIfOGguFEwQc34eGYR1QcRxfEEh2OaUJuhJZIp8ipNLpFkQskMhBaam09yUZPuei5nR58tBAqFIqhYtQs0lixDsuBCr9yqhqtDGpSOuk8T1yS1BIQ0k+xNJ5sp1NpTxtCHctkdzoM0izzl8-gBXrlIP5SoDk2mwYlUvDQN5vpbKrVCa1Os0KcS+vxRsz2dNeYtBetlQUxL0WWdZJc2gtnojveb-tF7ZDXZlPZ9+5jUDj8yHoh18jHePTU-U2T08izdyyt2a8npziUdhlE+IwyircktB3M8m0VA9Aw1EIEV1cc00NUB0maZxtByPJLjtJRikA0xi2pZxlDURwKJrLCAOkKDvUIGE4VoHByCwcxkKfNDJBeFoHSdLMXGuRp1HpbRpHeFoWmkQCrgcLD6MbFY8AIWBYFodA0DAcghTYLh9gnZ90N4oC10Eq4CgAtR6QoiSiM0XImnfJ0lD8OtUH4CA4HEbk5QM1DUmMhAAFpiMqULFOBUgKHCfyDUCniMk0elrEUe5KNcdxPG8SKejCGg4snIKdDeXJ8go2QVAtBQUoUQh0qcTKPC8S5cvPSpU3ijMsOyMyHgskTrJIrIcgogwKTk8lZFrDpd3akYxgmQqjMS983j6oTLNE4tChXACAIc75qXkNqYOjVtlu4jCvCA1133Yax8iaYp6QMbDih0TRnLJGxTqYrBLoSjCbAku0XCUakFBrelPv4xxdGdZpvtO5TVPgXFDKumQDDeD8HK3ORDDOP9i0qiT3CrMrZCh0k3J8IA */ -// id: 'inference', -// schema: { -// context: {} as { -// jobId: string; -// jobStatus: string; -// inputImages: Array<{ name: string; path: string }>; -// }, -// events: {} as FormSubmitEvent | { type: 'cancel' }, -// }, -// initial: 'active', -// context: { -// jobId: '', -// jobStatus: '', -// inputImages: [], -// }, -// states: { -// active: { -// tags: ['active'], -// on: { -// start: 'busy', -// }, -// }, -// busy: { -// tags: ['busy'], -// initial: 'pending', -// states: { -// pending: { -// invoke: { -// id: 'submitJob', -// src: 'submitJob', -// onDone: { -// target: 'running', -// actions: assign({ -// jobId: (context, event: JobEvent) => event.data.jobId ?? '', -// inputImages: (context, event: JobEvent) => { -// return event.data.formData?.inputImages ?? []; -// }, -// }), -// }, -// onError: { -// target: '#inference.error', -// }, -// }, -// }, -// running: { -// invoke: { -// src: 'jobStatus', -// onDone: [ -// { -// target: '#inference.success', -// cond: 'isCompleted', -// }, -// { -// target: '#inference.error', -// cond: 'isFailed', -// }, -// { -// target: '#inference.error', -// cond: 'isCancelled', -// }, -// { -// target: 'running', -// actions: assign({ -// jobStatus: (context, event: JobEvent) => -// event.data.jobStatus ?? '', -// }), -// }, -// ], -// }, -// on: { -// cancel: { -// actions: 'cancelJob', -// }, -// }, -// }, -// }, -// }, -// error: { -// tags: ['error'], -// on: { -// retry: { -// target: 'busy', -// actions: assign({ jobStatus: '' }), -// }, -// }, -// }, -// success: { -// tags: ['success'], -// }, -// }, -// predictableActionArguments: true, -// }); -// export type InferenceXState = StateFrom; + const [formData, setFormData] = useState(undefined); -// export function InferenceNode(nodeProps: NodeProps) { -// // const createJob = api.remotejob.create.useMutation(); -// const checkJob = api.remotejob.status.useMutation(); -// const cancelJob = api.remotejob.cancel.useMutation(); -// const submitJob = api.remoteProcess.submitInference.useMutation(); -// const { getSourceData } = useStoreActions(); -// const { onUpdateNode } = useStoreActions(); -// const [nodeStatus, setNodeStatus] = useState( -// nodeProps.data.status, -// ); + const updateNodeInternals = useUpdateNodeInternals(); -// const prevXState = State.create( -// nodeProps.data.xState -// ? (JSON.parse(nodeProps.data.xState) as InferenceXState) -// : inferenceMachine.initialState, -// ); + const {} = api.remotejob.checkStatus.useQuery( + { jobId: nodeProps.data.jobId as string }, + { + enabled: nodeProps.data.status === 'busy' && !!nodeProps.data.jobId, + refetchInterval: 5000, + refetchIntervalInBackground: true, + refetchOnWindowFocus: true, + onSuccess: (data) => { + console.log('checkJob.onSuccess', data); + if (!nodeProps.data.jobId) { + console.error('checkJob.onSuccess', 'no job id', data); + } + const date = dayjs().format('YYYY-MM-DD HH:mm:ss'); + if (data.jobStatus === 'COMPLETED') { + toast.success('Job completed'); + onUpdateNode({ + id: nodeProps.id, + data: { + ...nodeProps.data, + status: 'success', + jobId: nodeProps.data.jobId, + jobStatus: data.jobStatus, + message: `Job ${ + nodeProps.data.jobId ?? 'aa' + } finished successfully in ${date}`, + updatedAt: date, + }, + }); + updateNodeInternals(nodeProps.id); + } else if (data.jobStatus === 'FAILED') { + toast.error('Job failed'); + onUpdateNode({ + id: nodeProps.id, + data: { + ...nodeProps.data, + status: 'error', + jobId: nodeProps.data.jobId, + jobStatus: data.jobStatus, + message: `Job ${nodeProps.data.jobId ?? 'aa'} failed in ${date}`, + updatedAt: date, + }, + }); + updateNodeInternals(nodeProps.id); + } else { + onUpdateNode({ + id: nodeProps.id, + data: { + ...nodeProps.data, + status: 'busy', + jobId: nodeProps.data.jobId, + jobStatus: data.jobStatus, + message: `Job ${nodeProps.data.jobId ?? 'aa'} is ${ + data.jobStatus?.toLocaleLowerCase() ?? 'aa' + }, last checked at ${date}`, + updatedAt: date, + }, + }); + updateNodeInternals(nodeProps.id); + } + }, + }, + ); + const { mutateAsync: cancelJob } = api.remotejob.cancel.useMutation(); + const { mutateAsync: submitJob } = + api.remoteProcess.submitInference.useMutation(); + const { onUpdateNode, getSourceData } = useStoreActions(); -// const actor = useInterpret( -// inferenceMachine, -// { -// state: prevXState, -// guards: { -// isCompleted: (context) => { -// return context.jobStatus === 'COMPLETED'; -// }, -// isFailed: (context) => { -// return context.jobStatus === 'FAILED'; -// }, -// isCancelled: (context) => { -// return context.jobStatus === 'CANCELLED'; -// }, -// }, -// actions: { -// cancelJob: (context) => { -// cancelJob.mutate({ jobId: context.jobId }); -// }, -// }, -// services: { -// submitJob: (_, event) => { -// return new Promise((resolve, reject) => { -// const submitEvent = event as FormSubmitEvent; -// if (!submitEvent.data.formData) -// reject(new Error("This event shouldn't submit a job")); -// const formData = submitEvent.data.formData; -// const connectedNodeName = submitEvent.data.connectedNodeName; -// submitJob -// .mutateAsync({ -// workspacePath: nodeProps.data.workspacePath, -// networkName: connectedNodeName, -// formData: formData, -// }) -// .then((data) => { -// resolve({ ...data, formData }); -// }) -// .catch((err) => reject(err)); -// }); -// }, -// jobStatus: (context) => { -// return new Promise((resolve, reject) => { -// // wait 5 seconds before checking the job status -// setTimeout(() => { -// checkJob -// .mutateAsync({ jobId: context.jobId }) -// .then((data) => { -// resolve(data); -// }) -// .catch((err) => reject(err)); -// }, 5000); -// }); -// }, -// }, -// }, -// // observer -// (state) => { -// // subscribes to state changes and check if there is a status change to update the node data -// const newStatus = [...state.tags][0] as NodeStatus; -// if (newStatus !== nodeStatus) { -// // update the local state -// setNodeStatus(newStatus); -// // update the node data in the store -// onUpdateNode({ -// id: nodeProps.id, // this is the component id from the react-flow -// data: { -// ...nodeProps.data, -// status: newStatus, -// xState: JSON.stringify(state), -// }, -// }); -// } -// }, -// ); + const handleSubmitJob = (formData: FormType) => { + const connectedNodeData = getSourceData(nodeProps.id); + if (!connectedNodeData || !connectedNodeData.remotePath) { + toast.info('Please connect a trained network'); + return; + } + submitJob({ + networkName: connectedNodeData.remotePath.split('/').pop() as string, + formData, + workspacePath: nodeProps.data.workspacePath, + }) + .then(({ jobId }) => { + console.log('handleSubmitJob then?', 'jobId', jobId); + if (!jobId) { + console.error('handleSubmitJob then?', 'no jobId'); + toast.error('Error submitting job'); + return; + } + setFormData(formData); + onUpdateNode({ + id: nodeProps.id, + data: { + ...nodeProps.data, + status: 'busy', + remotePath: `${formData.outputDir}/${pokemon}`, + jobId: jobId, + message: `Job ${jobId} submitted in ${dayjs().format( + 'YYYY-MM-DD HH:mm:ss', + )}`, + }, + }); + updateNodeInternals(nodeProps.id); + setOpen(!open); + }) + .catch(() => { + toast.error('Error submitting job'); + }); + }; -// const selector = (state: InferenceXState) => { -// return { -// jobId: state.context.jobId, -// jobStatus: state.context.jobStatus, -// inputImages: state.context.inputImages, -// }; -// }; + if (nodeProps.data.status === 'active') { + return ( + + +
+ +
+

+ {'new inference'} +

+

+ {'Click to create a job'} +

+
+
+ + + + Create + + + + +
+ +
+ ); + } -// const { jobId, jobStatus, inputImages } = useSelector(actor, selector); + if (nodeProps.data.status === 'busy') { + return ( + + +
+ +
+

+ {nodeProps.data?.remotePath?.split('/').pop()} +

+

+ {`${nodeProps.data.jobId || 'jobId'} -- ${ + nodeProps.data.jobStatus || 'checking status..' + }`} +

+
+ + + + {nodeProps.data.message} + + + Details + +
+
+

id

+

{nodeProps.data.jobId}

+
+
+

status

+

+ {nodeProps.data.jobStatus} +

+
+
+

updated at

+

+ {nodeProps.data.updatedAt} +

+
+
+
+ +
+
+
+
+ +
+ ); + } -// const getConnectedNetworkLabel = () => { -// const connectedNodeData = getSourceData(nodeProps.id); -// if (!connectedNodeData?.xState) return; -// const connectedXState = JSON.parse( -// connectedNodeData.xState, -// ) as NetworkXState; -// return connectedXState.context.networkLabel ?? undefined; -// }; + if (nodeProps.data.status === 'success') { + return ( + + +
+ +
+

+ {nodeProps.data?.remotePath?.split('/').pop()} +

+

+ {`${nodeProps.data.jobId || 'jobId'} -- ${ + nodeProps.data.jobStatus || 'jobStatus' + }`} +

+
+
+ + + + {nodeProps.data.message} + + + Details + +
+
+

id

+

{nodeProps.data.jobId}

+
+
+

status

+

+ {nodeProps.data.jobStatus} +

+
+
+

updated at

+

+ {nodeProps.data.updatedAt} +

+
+
+
+
+
+
+ +
+ ); + } -// return ( -// -// -// -// {'inference'} -// {nodeStatus} -// -// {nodeStatus === 'active' && ( -// -// -// -// -// Run Inference -// -// { -// const connectedNetworkLabel = getConnectedNetworkLabel(); -// if (!connectedNetworkLabel) { -// toast({ -// title: 'Error', -// description: 'Please connect a network node.', -// }); -// return; -// } -// actor.send({ -// type: 'start', -// data: { -// formData: formSubmitData, -// connectedNodeName: connectedNetworkLabel, -// }, -// }); -// toast({ -// title: 'You submitted the following values:', -// description: ( -//
-//                         
-//                           {JSON.stringify(
-//                             { ...formSubmitData, ...nodeProps.data },
-//                             null,
-//                             2,
-//                           )}
-//                         
-//                       
-// ), -// }); -// }} -// /> -//
-//
-//
-// )} -// {nodeStatus === 'busy' && ( -// <> -// -//
-//

-// {jobId} -//

-//

-// {jobStatus} -//

-//
-//
-// -// -// -// -// )} -// {nodeStatus === 'error' && ( -// -//
-//

{jobId}

-//

-// {jobStatus} -//

-//
-// -// -// -// Retry -// -// { -// const connectedNetworkLabel = getConnectedNetworkLabel(); -// if (!connectedNetworkLabel) { -// toast({ -// title: 'Error', -// description: 'Please connect a network node.', -// }); -// return; -// } -// actor.send({ -// type: 'retry', -// data: { -// formData: formSubmitData, -// connectedNodeName: connectedNetworkLabel, -// }, -// }); -// toast({ -// title: 'You submitted the following values:', -// description: ( -//
-//                         
-//                           {JSON.stringify(formSubmitData, null, 2)}
-//                         
-//                       
-// ), -// }); -// }} -// /> -//
-//
-//
-// )} -// {nodeStatus === 'success' && ( -// -//
-//

{jobId}

-//

-// {jobStatus} -//

-//
-// -// -// -// Inference Results -// -// {inputImages.map((image, idx) => ( -//
-// {image.name} -//
-// ))} -//
-//
-//
-//
-//
-// )} -//
-// ); -// } + if (nodeProps.data.status === 'error') { + return ( + + +
+ +
+

+ {nodeProps.data?.remotePath?.split('/').pop()} +

+

+ {`${nodeProps.data.jobId || 'jobId'} -- ${ + nodeProps.data.jobStatus || 'jobStatus' + }`} +

+
+
+ + + + + Error + + {nodeProps.data.message || 'Something went wrong'} + + + + Retry + + + + +
+ +
+ ); + } -export function InferenceNode(nodeProps: NodeProps) { return null; } diff --git a/src/components/workboard/node-component-forms/inference-form.tsx b/src/components/workboard/node-component-forms/inference-form.tsx index bff05e9..5eba7c0 100644 --- a/src/components/workboard/node-component-forms/inference-form.tsx +++ b/src/components/workboard/node-component-forms/inference-form.tsx @@ -1,7 +1,7 @@ 'use client'; import { zodResolver } from '@hookform/resolvers/zod'; -import { X } from 'lucide-react'; +import { PlusIcon, TreeDeciduousIcon, X } from 'lucide-react'; import { useFieldArray, useForm } from 'react-hook-form'; import * as z from 'zod'; import { FsTreeDialog } from '~/components/fs-treeview'; @@ -25,7 +25,6 @@ import { } from '~/components/ui/select'; import { Switch } from '~/components/ui/switch'; import { slurmGPUOptions, slurmPartitionOptions } from '~/lib/constants'; -import { cn } from '~/lib/utils'; const slurmOptions = z.object({ partition: z.enum(slurmPartitionOptions), @@ -118,40 +117,54 @@ export function InferenceForm({ form.setValue('outputDir', path); }; + const toUnixPath = (path: string) => + path.replace(/[\\/]+/g, '/').replace(/^([a-zA-Z]+:|\.\/)/, ''); + return (
- void form.handleSubmit(onSubmit)(...args)}> -
+ void form.handleSubmit(onSubmit)(...args)} + > +
+ + + ( - + Output Directory Select the output directory. -
- {!!field.value && } -
+
)} /> - - -
{fields.map((field, index) => ( @@ -161,17 +174,15 @@ export function InferenceForm({ name={`inputImages.${index}.name`} render={({ field }) => ( - - Images - - - Add images to run inference on. - -
- -
@@ -188,29 +199,35 @@ export function InferenceForm({ 'Select the path to a valid image file or paste it down below.', }} > - +
+ + Add images +
( - - -
-
-

- Normalize Images -

-
+ +
+ + Normalize Images + + -
- + +
)} /> @@ -218,24 +235,22 @@ export function InferenceForm({ name="saveProbMap" control={form.control} render={({ field }) => ( - - -
-
-

- Save as Probabilty Map -

-
+ +
+ + Save as Probabilty Map + + -
- + +
)} /> -
+
-