Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Machine Form UI #2

Merged
merged 38 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c051ecd
Define min-h-screen on body
punitda Jul 18, 2024
cc23fa5
Create hero component for landing page
punitda Jul 18, 2024
6cb0cbd
Add create-machine page
punitda Jul 18, 2024
c41bc36
Update home page to use Hero component
punitda Jul 18, 2024
9b460d7
Add accordion component
punitda Jul 19, 2024
a89c515
Use button component for consistent design
punitda Jul 19, 2024
232c533
Fix data-disabled cmdk issue in Command component
punitda Jul 19, 2024
e46d3e4
Update nvmrc to use "v20.10.0"
punitda Jul 19, 2024
e87420e
Add heroicons package to dependencies
punitda Jul 19, 2024
ebcabf2
Create FormNav component
punitda Jul 19, 2024
6724de6
Create CustomNodeForm component
punitda Jul 19, 2024
6a990ad
Add multiple custom nodes selection
punitda Jul 20, 2024
d293d0e
Create UploadWorkflowFile form
punitda Jul 20, 2024
e52d915
Add info icon and popover for explaining workflow file
punitda Jul 20, 2024
bee2598
Define model types
punitda Jul 20, 2024
95d2ae1
Create ModelsForm
punitda Jul 20, 2024
b76cfcb
Update CustomNodeProps Form to include nav buttons
punitda Jul 20, 2024
fd9019d
WIP CreateMachineForm
punitda Jul 20, 2024
6c74e38
Gpu types
punitda Jul 20, 2024
528a245
Create GPU form component
punitda Jul 20, 2024
50570c2
Add gpu form step nav
punitda Jul 20, 2024
9052878
Fetch custom nodes and models from Github
punitda Jul 21, 2024
868dd35
Pass data down fetched from loader to Forms
punitda Jul 21, 2024
ff49e37
Add useDebounce hook
punitda Jul 22, 2024
9ac35ec
Remove console log
punitda Jul 22, 2024
df5b209
Create CivitaAIModelComboBox with loader to call search API
punitda Jul 22, 2024
e1085b7
Call search api on form load
punitda Jul 22, 2024
43b1ce3
Persist selected nodes and models between nav steps
punitda Jul 22, 2024
b9428b9
Map nodes, models to output format
punitda Jul 23, 2024
5939c87
Add placeholder for machine name
punitda Jul 23, 2024
416e2cf
Add missing type
punitda Jul 23, 2024
995d1f5
Separate action for upload workflow file
punitda Jul 23, 2024
d8b07c3
Add Textarea component
punitda Jul 23, 2024
55b19e9
Pass selected nodes and models to GpuForm and add fetcher.Form
punitda Jul 23, 2024
c39861e
temp workaround for civitai search error
punitda Jul 23, 2024
5e1e054
Change back button to use variant "outline"
punitda Jul 23, 2024
cc6ead2
Remove console logs in Gpu form
punitda Jul 23, 2024
92d2708
Upload workflow file and generate custom nodes
punitda Jul 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18.18.2
v20.10.0
258 changes: 258 additions & 0 deletions web/app/components/custom-node-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import { Label } from "~/components/ui/label";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";

import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "~/components/ui/command";

import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { useEffect, useState } from "react";
import { CustomNode } from "~/lib/types";

import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "~/lib/utils";
import { useFetcher } from "@remix-run/react";

import { InformationCircleIcon } from "@heroicons/react/24/outline";
import { action } from "~/routes/upload-workflow-file/route";

export interface CustomNodeFormProps {
nodes: CustomNode[];
selectedCustomNodes: CustomNode[];
onNodesSelected: (node: CustomNode[]) => void;
onNextStep: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
export default function CustomNodeForm({
nodes,
selectedCustomNodes,
onNodesSelected,
onNextStep,
}: CustomNodeFormProps) {
return (
<div>
<div className="px-4 py-6 sm:p-8">
<div className="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div className="sm:col-span-4">
<UploadWorkflowFileForm onNodesSelected={onNodesSelected} />
</div>
<div className="relative sm:col-span-4">
<div
aria-hidden="true"
className="absolute inset-0 flex items-center"
>
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center">
<span className="bg-white px-2 text-sm text-primary">OR</span>
</div>
</div>
<div className="sm:col-span-4">
<Label>Add Custom Nodes</Label>
<div className="mt-2">
<CustomNodesComboBox
nodes={nodes}
selectedNodes={selectedCustomNodes}
onNodesSelected={onNodesSelected}
/>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
<Button onClick={onNextStep}>Next</Button>
</div>
</div>
);
}

interface CustomNodesComboxBoxProps {
nodes: CustomNode[];
selectedNodes: CustomNode[];
onNodesSelected: (nodes: CustomNode[]) => void;
}

function CustomNodesComboBox({
nodes,
selectedNodes,
onNodesSelected,
}: CustomNodesComboxBoxProps) {
const [open, setOpen] = useState(false);

function onSelected(node: CustomNode) {
const prevSelectedNodes = selectedNodes;
if (
prevSelectedNodes.some(
(selectedModel) =>
selectedModel.reference + selectedModel.title ===
node.reference + node.title
)
) {
onNodesSelected(
prevSelectedNodes.filter(
(selectedModel) =>
selectedModel.reference + selectedModel.title !==
node.reference + node.title
)
);
} else {
onNodesSelected([...prevSelectedNodes, node]);
}
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{selectedNodes.length == 0
? "Select nodes"
: `Selected nodes : ${selectedNodes.length}`}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[400px]">
<Command>
<CommandInput placeholder="Search Node..." />
<CommandEmpty>No node found.</CommandEmpty>
<CommandList>
<CommandGroup>
{nodes.map((node) => (
<CommandItem
key={node.reference + node.title}
value={node.reference}
onSelect={() => {
onSelected(node);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedNodes.some(
(selectedNode) =>
selectedNode.reference === node.reference
)
? "opacity-100"
: "opacity-0"
)}
/>
{node.title}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

interface UploadWorkflowFileFormProps {
onNodesSelected: (nodes: CustomNode[]) => void;
}

function UploadWorkflowFileForm({
onNodesSelected,
}: UploadWorkflowFileFormProps) {
const fetcher = useFetcher<typeof action>();

const isUploading = fetcher.state !== "idle";
const [fileAdded, setFileAdded] = useState(false);

function onChange(e: React.ChangeEvent<HTMLInputElement>) {
e.preventDefault();
const fileSize = e.target?.files?.length;
if (fileSize != null && fileSize > 0) {
setFileAdded(true);
}
}

useEffect(() => {
if (fetcher.data?.nodes?.length ?? 0 > 0) {
onNodesSelected(fetcher.data?.nodes ?? []);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher.data?.nodes]);
return (
<fetcher.Form
action="/upload-workflow-file"
method="post"
encType="multipart/form-data"
>
<div className="flex items-center space-x-2">
<Label htmlFor="workflow-file">Upload workflow file</Label>
<Popover>
<PopoverTrigger asChild>
<InformationCircleIcon className="size-6 text-primary/70" />
</PopoverTrigger>
<PopoverContent className="w-80">
<p className="text-primary/90 text-sm mt-1">
If you are not sure which custom nodes are used in your workflow,
please upload the workflow file and we can generate the list of
custom nodes required to be installed :)
</p>
</PopoverContent>
</Popover>
</div>

<div className="flex w-full max-w-sm items-center space-x-2 mt-2">
<Input
id="workflow-file"
name="workflow-file"
accept=".json"
type="file"
onChange={onChange}
/>
<div className="relative flex items-center">
<Button
type="submit"
disabled={isUploading || !fileAdded}
className="px-8"
>
Upload
</Button>
{isUploading ? (
<svg
className="animate-spin mr-1 h-4 w-4 text-background absolute right-1"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : null}
</div>
</div>
{fetcher?.data?.nodes ? (
<p className="text-sm mt-2">
<span className="font-semibold">{fetcher?.data?.nodes.length}</span>
<span> nodes selected from the workflow file</span>
</p>
) : null}
</fetcher.Form>
);
}
84 changes: 84 additions & 0 deletions web/app/components/form-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { CheckIcon } from "@heroicons/react/24/solid";
import { FormStep } from "~/lib/types";

interface FormNavProps {
steps: FormStep[];
}

export default function FormNav({ steps }: FormNavProps) {
return (
<nav aria-label="Progress">
<ol className="divide-y divide-gray-300 rounded-md border-background border-gray-300 md:flex md:divide-y-0">
{steps.map((step, stepIdx) => (
<li key={step.name} className="relative md:flex md:flex-1">
{step.status === "complete" ? (
<a href={step.href} className="group flex w-full items-center">
<span className="flex items-center px-6 py-4 text-sm font-medium">
<span className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-primary/90 group-hover:bg-primary">
<CheckIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
</span>
<span className="ml-4 text-sm font-medium text-primary/90">
{step.name}
</span>
</span>
</a>
) : step.status === "current" ? (
<a
href={step.href}
aria-current="step"
className="flex items-center px-6 py-4 text-sm font-medium"
>
<span className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border-2 border-primary">
<span className="text-primary">{step.id}</span>
</span>
<span className="ml-4 text-sm font-medium text-primary">
{step.name}
</span>
</a>
) : (
<a href={step.href} className="group flex items-center">
<span className="flex items-center px-6 py-4 text-sm font-medium">
<span className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border-2 border-secondary">
<span className="text-gray-500 group-hover:text-primary">
{step.id}
</span>
</span>
<span className="ml-4 text-sm font-medium text-gray-500 group-hover:text-primary">
{step.name}
</span>
</span>
</a>
)}

{stepIdx !== steps.length - 1 ? (
<>
{/* Arrow separator for lg screens and up */}
<div
aria-hidden="true"
className="absolute right-0 top-0 hidden h-full w-5 md:block"
>
<svg
fill="none"
viewBox="0 0 22 80"
preserveAspectRatio="none"
className="h-full w-full text-border"
>
<path
d="M0 -2L20 40L0 82"
stroke="currentcolor"
vectorEffect="non-scaling-stroke"
strokeLinejoin="round"
/>
</svg>
</div>
</>
) : null}
</li>
))}
</ol>
</nav>
);
}
Loading