diff --git a/app/api/upload/transloadit/route.ts b/app/api/upload/transloadit/route.ts new file mode 100644 index 0000000..cea778b --- /dev/null +++ b/app/api/upload/transloadit/route.ts @@ -0,0 +1,36 @@ +import crypto from "node:crypto"; +import { NextResponse } from "next/server"; + +export async function GET() { + const authKey = process.env.TRANSLOADIT_KEY; + const authSecret = process.env.TRANSLOADIT_SECRET; + + if (!authKey || !authSecret) { + return NextResponse.json( + { error: "Transloadit not configured" }, + { status: 500 }, + ); + } + + const rawParams = { + auth: { + key: authKey, + expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + }, + steps: { + ":original": { + robot: "/upload/handle", + result: true, + }, + }, + }; + + const paramsStr = JSON.stringify(rawParams); + const encoded = Buffer.from(paramsStr).toString("base64"); + const signature = crypto + .createHmac("sha1", authSecret) + .update(encoded) + .digest("hex"); + + return NextResponse.json({ params: encoded, signature }); +} diff --git a/app/app/flow/page.tsx b/app/app/flow/page.tsx index 2ffa7ee..43725fd 100644 --- a/app/app/flow/page.tsx +++ b/app/app/flow/page.tsx @@ -2,7 +2,9 @@ import { useUser } from "@clerk/nextjs"; import { - FileUp, + Copy, + Download, + ExternalLink, MoreHorizontal, Pencil, Plus, @@ -138,6 +140,42 @@ export default function FlowPage() { input.click(); }; + const duplicateWorkflow = async (id: string, name: string) => { + try { + const res = await fetch(`/api/workflows/${id}/export`); + if (!res.ok) return; + const data = await res.json(); + data.name = `${name} (copy)`; + const dup = await fetch("/api/workflows/import", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (dup.ok) { + const wf = await dup.json(); + setWorkflows((prev) => [...prev, wf]); + } + } catch (err) { + console.log("[FLOW] duplicate error:", err); + } + }; + + const exportWorkflow = async (id: string, name: string) => { + try { + const res = await fetch(`/api/workflows/${id}/export`); + if (!res.ok) return; + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${name}.json`; + a.click(); + URL.revokeObjectURL(url); + } catch (err) { + console.log("[FLOW] export error:", err); + } + }; + const filtered = workflows.filter((w) => w.name.toLowerCase().includes(searchQuery.toLowerCase()), ); @@ -159,14 +197,14 @@ export default function FlowPage() { } return ( -
+
{/* Header */}
-

+

Flow

-

+

Build workflows or run models directly

@@ -174,15 +212,15 @@ export default function FlowPage() { @@ -191,53 +229,30 @@ export default function FlowPage() { {/* System Workflows */}
-

+

System Workflows

-

+

Prebuilt workflow templates - click to open and start using.

-
- {/* Sample marketing workflow */} - +
{/* Racing car template */} - )} +
+ {wf.name} router.push(`/app/workflows/${wf.id}`)} + /> + + {/* Menu on upper right of image */} +
+ + + + + + router.push(`/app/workflows/${wf.id}`)} + > + + Open + + { + setRenaming(wf.id); + setRenameValue(wf.name); + }} + > + + Rename + + duplicateWorkflow(wf.id, wf.name)} + > + + Duplicate + + exportWorkflow(wf.id, wf.name)} + > + + Export JSON + + + setDeleteTarget(wf)} + > + + Delete + + + +
+ {wf.isRunning && ( -
+
@@ -330,66 +380,33 @@ export default function FlowPage() {
{/* Card content */} -
-
- {renaming === wf.id ? ( - setRenameValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") renameWorkflow(wf.id); - if (e.key === "Escape") setRenaming(null); - }} - onBlur={() => renameWorkflow(wf.id)} - /> - ) : ( - <> - - - Edited {timeAgo(wf.updatedAt)} - - - )} -
- - - - - - - + {renaming === wf.id ? ( + setRenameValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") renameWorkflow(wf.id); + if (e.key === "Escape") setRenaming(null); + }} + onBlur={() => renameWorkflow(wf.id)} + /> + ) : ( + <> + + + Edited {timeAgo(wf.updatedAt)} + + + )}
))} diff --git a/app/app/workflows/[id]/canvas/page.tsx b/app/app/workflows/[id]/canvas/page.tsx index 611a6bf..530077f 100644 --- a/app/app/workflows/[id]/canvas/page.tsx +++ b/app/app/workflows/[id]/canvas/page.tsx @@ -24,8 +24,8 @@ import { Redo2, Save, Search, - Upload, Undo2, + Upload, } from "lucide-react"; import NodePropertiesPanel from "@/components/NodePropertiesPanel"; import CropImageNode from "@/components/nodes/CropImageNode"; @@ -240,6 +240,7 @@ function CanvasEditorInner() { loadWorkflow, undo, redo, + setSelectedNodes, } = useCanvasStore(); const { startRun, @@ -359,15 +360,31 @@ function CanvasEditorInner() { } }, [workflowId]); + // Track selection for single/multi-select runs + const onSelectionChange = useCallback( + ({ nodes: selected }: { nodes: Node[] }) => { + setSelectedNodes(selected.map((n) => n.id)); + }, + [setSelectedNodes], + ); + // Run execution const runWorkflow = useCallback(async () => { - const nodeIds = useCanvasStore.getState().nodes.map((n) => n.id); + const state = useCanvasStore.getState(); + const nodeIds = + state.selectedNodeIds.length > 0 + ? state.selectedNodeIds + : state.nodes.map((n) => n.id); try { const res = await fetch(`/api/workflows/${workflowId}/run`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), + body: JSON.stringify( + nodeIds.length < useCanvasStore.getState().nodes.length + ? { selectedNodeIds: nodeIds } + : {}, + ), }); if (!res.ok) return; @@ -616,6 +633,7 @@ function CanvasEditorInner() { isValidConnection={isValidConnection} deleteKeyCode="Delete" multiSelectionKeyCode="Shift" + onSelectionChange={onSelectionChange} fitView colorMode="light" className="bg-white text-gray-900" diff --git a/app/app/workflows/[id]/page.tsx b/app/app/workflows/[id]/page.tsx index 6dc2652..aedd4c1 100644 --- a/app/app/workflows/[id]/page.tsx +++ b/app/app/workflows/[id]/page.tsx @@ -15,6 +15,7 @@ import { AlignLeft, ArrowLeft, Check, + ChevronDown, Copy, History, Loader2, @@ -22,7 +23,6 @@ import { Play, Search, Sparkles, - Terminal, } from "lucide-react"; import CropImageNode from "@/components/nodes/CropImageNode"; import GeminiNode from "@/components/nodes/GeminiNode"; @@ -43,6 +43,246 @@ const nodeTypes = { GEMINI: GeminiNode, }; +interface CodeBlockProps { + code: string; + language: string; +} + +function CodeBlock({ code, language }: CodeBlockProps) { + const highlight = (text: string, lang: string) => { + if (lang === "python") { + const parts = text.split( + /(\"[^\"]*\"|'[^']*'|#.*|"""[\s\S]*?"""|\b(?:import|time|json|def|while|True|False|if|elif|return|raise|in|Exception|print|requests|post|get|json|sleep|as|and|or|not)\b)/g, + ); + return parts.map((part, index) => { + if (part.startsWith("#") || part.startsWith('"""')) { + return ( + + {part} + + ); + } + if ( + (part.startsWith('"') && part.endsWith('"')) || + (part.startsWith("'") && part.endsWith("'")) + ) { + return ( + + {part} + + ); + } + if ( + /^\b(import|time|json|def|while|True|False|if|elif|return|raise|in|Exception|print|requests|post|get|json|sleep|as|and|or|not)\b$/.test( + part, + ) + ) { + if ( + ["def", "return", "while", "if", "elif", "raise", "in", "import", "as"].includes( + part, + ) + ) { + return ( + + {part} + + ); + } + if (part === "True" || part === "False") { + return ( + + {part} + + ); + } + return ( + + {part} + + ); + } + return ( + + {part} + + ); + }); + } else if (lang === "javascript") { + const parts = text.split( + /(\"[^\"]*\"|'[^']*'|`[^`]*`|\/\/.*|\b(?:const|let|var|function|async|await|while|true|false|if|else|return|throw|try|catch|new|Promise|setTimeout|resolve|reject|console|log|error|fetch|then|stringify|json)\b)/g, + ); + return parts.map((part, index) => { + if (part.startsWith("//")) { + return ( + + {part} + + ); + } + if ( + (part.startsWith('"') && part.endsWith('"')) || + (part.startsWith("'") && part.endsWith("'")) || + (part.startsWith("`") && part.endsWith("`")) + ) { + return ( + + {part} + + ); + } + if ( + /^\b(const|let|var|function|async|await|while|true|false|if|else|return|throw|try|catch|new|Promise|setTimeout|resolve|reject|console|log|error|fetch|then|stringify|json)\b$/.test( + part, + ) + ) { + if ( + [ + "const", + "let", + "var", + "function", + "async", + "await", + "return", + "if", + "else", + "try", + "catch", + "throw", + "new", + ].includes(part) + ) { + return ( + + {part} + + ); + } + if (part === "true" || part === "false") { + return ( + + {part} + + ); + } + return ( + + {part} + + ); + } + return ( + + {part} + + ); + }); + } else if (lang === "curl") { + const parts = text.split( + /(\"[^\"]*\"|'[^']*'|\\\n|\b(?:curl|POST|GET|-X|-H|-d|Authorization|Bearer|Content-Type|application\/json)\b)/g, + ); + return parts.map((part, index) => { + if ( + (part.startsWith('"') && part.endsWith('"')) || + (part.startsWith("'") && part.endsWith("'")) + ) { + return ( + + {part} + + ); + } + if (part === "curl") { + return ( + + {part} + + ); + } + if (part === "POST" || part === "GET") { + return ( + + {part} + + ); + } + if (part.startsWith("-")) { + return ( + + {part} + + ); + } + return ( + + {part} + + ); + }); + } else if (lang === "json") { + const parts = text.split( + /(\"[^\"]*\"\s*:|\[|\]|\{|\}|\"[^\"]*\"|[0-9\.]+|true|false|null)/g, + ); + return parts.map((part, index) => { + if (part.endsWith(":")) { + return ( + + {part} + + ); + } + if (part.startsWith('"') && part.endsWith('"')) { + return ( + + {part} + + ); + } + if (/^(true|false|null)$/.test(part)) { + return ( + + {part} + + ); + } + if (/^[0-9\.]+$/.test(part)) { + return ( + + {part} + + ); + } + return ( + + {part} + + ); + }); + } + return {text}; + }; + + const lines = code.split("\n"); + return ( +
+ {/* Line Numbers */} +
+ {lines.map((_, i) => ( +
{i + 1}
+ ))} +
+ {/* Code Content */} +
+ {lines.map((line, i) => ( +
+ {line.trim() === "" ? "\u00A0" : highlight(line, language)} +
+ ))} +
+
+ ); +} + function WorkflowDetailsInner() { const params = useParams(); const router = useRouter(); @@ -61,7 +301,9 @@ function WorkflowDetailsInner() { const [inputValues, setInputValues] = useState>({}); const [copiedApi, setCopiedApi] = useState(false); const [copiedOutput, setCopiedOutput] = useState(false); - const [selectedApiLang, setSelectedApiLang] = useState("curl"); + const [selectedApiLang, setSelectedApiLang] = useState("python"); + const [inDetails, setInDetails] = useState(true); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [historySearchQuery, setHistorySearchQuery] = useState(""); const [historyTab, setHistoryTab] = useState<"ui" | "api">("ui"); @@ -504,29 +746,187 @@ function WorkflowDetailsInner() { }; // API code snippets - const curlSnippet = `curl -X POST http://localhost:3000/api/workflows/${workflowId}/run \\ + const requestNodeId = requestInputsNode?.id || "node_request_inputs_id"; + const pythonValues = inputFields.length > 0 + ? inputFields.map((f: any) => ` "${f.name}": "your text here"`).join(",\n") + : ""; + const jsValues = inputFields.length > 0 + ? inputFields.map((f: any) => ` "${f.name}": "your text here"`).join(",\n") + : ""; + const curlValues = inputFields.length > 0 + ? inputFields.map((f: any) => ` "${f.name}": "your text here"`).join(",\n") + : ""; + + const inDetailsQuery = inDetails ? "?inDetails=true" : ""; + + const sampleResponseTrue = `{ + "id": "run_abc123...", + "status": "COMPLETED", + "finishedAt": "2025-01-01T00:00:12.000Z", + "createdAt": "2025-01-01T00:00:00.000Z", + "error": null, + "estimatedCredits": 1, + "actualCredits": 1, + "nodeRunCount": 2, + "singleNodeId": null, + "nodeRuns": [ + { + "id": "nr_req123...", + "nodeId": "node_req...", + "nodeType": "request", + "status": "COMPLETED", + "startedAt": "2025-01-01T00:00:00.000Z", + "finishedAt": "2025-01-01T00:00:00.000Z", + "error": null, + "input": null, + "output": { + "prompt": "a horse running in fields" + } + }, + { + "id": "nr_proc123...", + "nodeId": "node_proc...", + "nodeType": "flux_pro", + "status": "COMPLETED", + "startedAt": "2025-01-01T00:00:01.000Z", + "finishedAt": "2025-01-01T00:00:11.000Z", + "error": null, + "input": { + "prompt": "a horse running in fields" + }, + "output": { + "image": "https://api.magica.com/outputs/img_123.jpg" + } + } + ] +}`; + + const sampleResponseFalse = `{ + "id": "run_abc123...", + "status": "COMPLETED", + "finishedAt": "2025-01-01T00:00:12.000Z", + "createdAt": "2025-01-01T00:00:00.000Z", + "error": null, + "estimatedCredits": 1, + "actualCredits": 1, + "nodeRunCount": 2, + "singleNodeId": null +}`; + + const curlSnippet = `curl -X POST https://api.magica.com/api/v1/runs \\ -H "Content-Type: application/json" \\ - -d '{}'`; + -H "Authorization: Bearer YOUR_API_KEY" \\ + -d '{ + "workflowId": "${workflowId}", + "values": { + "${requestNodeId}": { + ${curlValues} + } + } + }' + +# 2. Poll the workflow run status +curl -H "Authorization: Bearer YOUR_API_KEY" \\ + https://api.magica.com/api/v1/runs/run_abc123...${inDetailsQuery}`; + + const jsSnippet = `const api_key = "YOUR_API_KEY"; +const url = "https://api.magica.com/api/v1/runs"; + +const data = { + workflowId: "${workflowId}", + values: { + "${requestNodeId}": { +${jsValues} + } + } +}; - const jsSnippet = `fetch("http://localhost:3000/api/workflows/${workflowId}/run", { +// Start the run +fetch(url, { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "Authorization": \`Bearer \${api_key}\` }, - body: JSON.stringify({}) + body: JSON.stringify(data) }) .then(res => res.json()) - .then(data => console.log("Run triggered successfully:", data));`; + .then(result => { + const runId = result.runId; + console.log(\`Run started: \${runId}\`); + return pollForResult(runId); + }) + .then(final => console.log(JSON.stringify(final, null, 2))) + .catch(err => console.error(err)); + +async function pollForResult(runId) { + const pollUrl = \`https://api.magica.com/api/v1/runs/\${runId}${inDetailsQuery}\`; + while (true) { + const res = await fetch(pollUrl, { + headers: { + "Authorization": \`Bearer \${api_key}\` + } + }); + const result = await res.json(); + if (result.status === 'COMPLETED') { + return result; + } else if (result.status === 'FAILED' || result.status === 'CANCELED') { + throw new Error(\`Run failed: \${result.error || 'unknown error'}\`); + } + await new Promise(resolve => setTimeout(resolve, 7000)); + } +}`; const pySnippet = `import requests +import time +import json + +api_key = "YOUR_API_KEY" +url = "https://api.magica.com/api/v1/runs" + +data = { + "workflowId": "${workflowId}", + "values": { + "${requestNodeId}": { +${pythonValues} + } + } +} -url = "http://localhost:3000/api/workflows/${workflowId}/run" -response = requests.post(url, json={}) +def poll_for_result(run_id): + """Poll the API until the generation is complete""" + poll_url = f"https://api.magica.com/api/v1/runs/{run_id}${inDetailsQuery}" + while True: + response = requests.get( + poll_url, + headers={'Authorization': f'Bearer {api_key}'} + ) + result = response.json() + + if result['status'] == 'COMPLETED': + return result + elif result['status'] in ['FAILED', 'CANCELED']: + raise Exception(f"Run failed: {result.get('error', 'unknown error')}") + + time.sleep(7) + +# Start the run +response = requests.post( + url, + json=data, + headers={ + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}' + } +) + +result = response.json() +run_id = result['runId'] +print(f"Run started: {run_id}") -if response.status_code == 201: - print("Run triggered successfully:", response.json()) -else: - print("Execution failed to trigger:", response.status_code, response.text)`; +# Poll for result +final = poll_for_result(run_id) +print(json.dumps(final, indent=2))`; const apiSnippets: Record = { curl: curlSnippet, @@ -541,7 +941,15 @@ else: }; if (loading) { - return ; + return ( + + ); } return ( @@ -915,105 +1323,211 @@ else: {/* API Tab Content */} {activeTab === "api" && ( -
-
-
- +
+ {/* Left Column: Code Panel */} +
+ {/* Header */} +
+ {/* Language Select Dropdown */} +
+ + {isDropdownOpen && ( + <> + + ))} +
+ + )} +
+ + {/* Copy Button */} +
-
-

- API Integration -

-

- Trigger this workflow programmatically from your own - applications or scripts. -

+ + {/* Code Panel Block */} +
+
-
- {/* Method and Endpoint */} -
- - POST - - - http://localhost:3000/api/workflows/{workflowId}/run - + {/* Right Column: Documentation Card List */} +
+ {/* API Endpoint Card */} +
+

+ API Endpoint +

+
+ + POST + + + https://api.magica.com/api/v1/runs + +
- {/* Code Blocks */} -
- {/* Selector Header */} -
-
- {["curl", "javascript", "python"].map((lang) => ( - - ))} + {/* Response Format Card */} +
+

+ Response Format +

+
+

+ The start endpoint returns a{" "} + + runId + + . Poll{" "} + + GET /v1/runs/{"{"}runId{"}"} + {" "} + to check status. +

+
+
-
- - {/* Snip content */} -
-                  {apiSnippets[selectedApiLang]}
-                
- {/* Payload Schema info */} -
-

- Request Body Params + {/* Polling Format Card */} +
+

+ Polling Format

-
- - - - - - - - - - - - - - - - - -
FieldTypeRequiredDescription
- selectedNodeIds - string[]No - Subset of nodes IDs to trigger. If omitted, triggers - the full pipeline execution. -
+
+

+ Poll{" "} + + GET /v1/runs/{"{"}runId{"}"} + {" "} + until status is a terminal value: +

+ + {/* Status badges row */} +
+ + Queued + + + Running + + + Completed + + + Canceled + + + Failed + +
+ + {/* GET request box */} +
+ + GET + + + /v1/runs/{"{"}runId{"}"} + {inDetails ? "?inDetails=true" : ""} + +
+ + {/* Toggle Switch */} +
+ +
+ + inDetails + + + {inDetails + ? "true - all node runs" + : "false - only workflow status"} + +
+
+ + {/* Sample Response Section */} +
+ + Sample response: + +
+ +
+
@@ -1079,56 +1593,56 @@ else: zoomable /> -

-
{/* Floating Workflow Navigator Widget matching Screen 1 & 2 */} -
- {(() => { - const sortedNodes = [...nodes].sort( - (a, b) => a.position.x - b.position.x, - ); - if (sortedNodes.length === 0) { - return ( - - No Nodes - + {activeTab !== "api" && ( +
+ {(() => { + const sortedNodes = [...nodes].sort( + (a, b) => a.position.x - b.position.x, ); - } - return sortedNodes.map((node) => { - // Determine height based on node type - let height = 12; - if (node.type === "REQUEST_INPUTS") height = 10; - else if (node.type === "RESPONSE") height = 14; - else if (node.type === "GEMINI") height = 28; - else if (node.type === "CROP_IMAGE") height = 20; - else height = 18; - - return ( -
+ if (sortedNodes.length === 0) { + return ( + + No Nodes + + ); + } + return sortedNodes.map((node) => { + // Determine height based on node type + let height = 12; + if (node.type === "REQUEST_INPUTS") height = 10; + else if (node.type === "RESPONSE") height = 14; + else if (node.type === "GEMINI") height = 28; + else if (node.type === "CROP_IMAGE") height = 20; + else height = 18; + + return ( +
+ )}
); diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 1900d40..e92ae4a 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -74,7 +74,7 @@ export default function DashboardPage() { const res = await fetch("/api/workflows", { method: "POST", body: "{}" }); if (res.ok) { const wf = await res.json(); - router.push(`/workflow/${wf.id}`); + router.push(`/app/workflows/${wf.id}`); } }; @@ -196,7 +196,7 @@ export default function DashboardPage() { router.push(`/workflow/${wf.id}`)} + onClick={() => router.push(`/app/workflows/${wf.id}`)} > Open diff --git a/app/globals.css b/app/globals.css index 7ad0381..bbf79fd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -7,9 +7,12 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-inter), var(--font-geist-sans); + --font-sans: + "Google Sans Flex", "Google Sans", var(--font-inter), var(--font-geist-sans); + --font-static-sans: var(--font-inter), var(--font-geist-sans); --font-mono: var(--font-geist-mono); - --font-heading: var(--font-inter), var(--font-geist-sans); + --font-heading: + "Google Sans Flex", "Google Sans", var(--font-inter), var(--font-geist-sans); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -93,7 +96,7 @@ } :root { - --background: oklch(1 0 0); + --background: #f9f9f9; --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); @@ -117,7 +120,7 @@ --chart-4: oklch(0.371 0 0); --chart-5: oklch(0.269 0 0); --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); + --sidebar: #f0f0f0; --sidebar-foreground: oklch(0.4 0 0); --sidebar-primary: oklch(0.205 0 0); --sidebar-primary-foreground: oklch(0.985 0 0); @@ -178,11 +181,13 @@ } /* Hide scrollbar utility */ -.hide-scrollbar { +.hide-scrollbar, +.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } -.hide-scrollbar::-webkit-scrollbar { +.hide-scrollbar::-webkit-scrollbar, +.no-scrollbar::-webkit-scrollbar { display: none; } diff --git a/app/layout.tsx b/app/layout.tsx index 802a9c8..eca9de6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,20 @@ import { ClerkProvider } from "@clerk/nextjs"; import type { Metadata } from "next"; import { Geist, Geist_Mono, Inter } from "next/font/google"; import { TooltipProvider } from "@/components/ui/tooltip"; + +// Google Sans Flex (variable — flex everywhere except PromoBanner) +import "@fontsource/google-sans-flex/400.css"; +import "@fontsource/google-sans-flex/500.css"; +import "@fontsource/google-sans-flex/600.css"; +import "@fontsource/google-sans-flex/700.css"; +import "@fontsource/google-sans-flex/800.css"; + +// Google Sans (static — for PromoBanner) +import "@fontsource/google-sans/400.css"; +import "@fontsource/google-sans/500.css"; +import "@fontsource/google-sans/600.css"; +import "@fontsource/google-sans/700.css"; + import "./globals.css"; const inter = Inter({ @@ -34,11 +48,11 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - + + {children} diff --git a/bun.lock b/bun.lock index e6adf22..394feb6 100644 --- a/bun.lock +++ b/bun.lock @@ -6,11 +6,15 @@ "dependencies": { "@base-ui/react": "^1.6.0", "@clerk/nextjs": "^7.5.8", + "@fontsource/google-sans": "^5.2.1", + "@fontsource/google-sans-flex": "^5.2.4", "@google/generative-ai": "0.24.1", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", "@t3-oss/env-nextjs": "^0.13.11", "@trigger.dev/sdk": "4.4.6", + "@uppy/core": "^5.2.0", + "@uppy/transloadit": "^5.5.1", "@xyflow/react": "12.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -159,6 +163,10 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@fontsource/google-sans": ["@fontsource/google-sans@5.2.1", "", {}, "sha512-FteXH18YS0288Bcrjefwyyp7hLwxd8yQBEcmjU/P0BED9c8n8kxwgna3gtMtYyFQx/wIkpyrzj8RaYQyl/4quw=="], + + "@fontsource/google-sans-flex": ["@fontsource/google-sans-flex@5.2.4", "", {}, "sha512-S7WKovlLsdcmC7DdkYRYNMul1EAmj6PQ9ZYTLgihazDBzYAyRti3MbaZeOqYhBl1kAY6kyck70+wAyZFEtz8TQ=="], + "@google-cloud/precise-date": ["@google-cloud/precise-date@4.0.0", "", {}, "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA=="], "@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="], @@ -413,8 +421,12 @@ "@transloadit/abbr": ["@transloadit/abbr@1.0.0", "", {}, "sha512-Hg5xdbpsDfUiUc62fIAF6L86+o52pY37/eKCOKqvJCJfSJ0ET6AG5FBIg6o2tTiFF7si5lhGIHONs0/IAEwc2Q=="], + "@transloadit/prettier-bytes": ["@transloadit/prettier-bytes@0.3.5", "", {}, "sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA=="], + "@transloadit/sev-logger": ["@transloadit/sev-logger@0.1.9", "", { "dependencies": { "@transloadit/abbr": "^1.0.0" } }, "sha512-TALqS5mOo+5TmwNdtRfsfOhtjhfCuXllVffNoiGEpewmbwsxBfrTdcE/9/Ayst9rfT0HST0KU1l71lxltLMEwg=="], + "@transloadit/types": ["@transloadit/types@4.3.3", "", {}, "sha512-SWPS65ZkCzTfIZErICXcugUkFvjVEtDHi9CZtI2IZ6/7UyG0llqdQbrge0F10PVFX2/20m3lyxsJVPKhQzAKlQ=="], + "@transloadit/utils": ["@transloadit/utils@4.3.0", "", {}, "sha512-fjfowdo+RCh1VQe2aH/dfgRUWNT3Vq9863jYCH4neWA4pyyyl+Y4zA11fKmncs+NlGLsQlbkiQYCeTFwVa+6Uw=="], "@trigger.dev/build": ["@trigger.dev/build@4.4.6", "", { "dependencies": { "@prisma/config": "^6.10.0", "@trigger.dev/core": "4.4.6", "mlly": "^1.7.1", "pkg-types": "^1.1.3", "resolve": "^1.22.8", "tinyglobby": "^0.2.2", "tsconfck": "3.1.3" } }, "sha512-eHPPaeuFe9GZDndQzP4QUlxocyIJWYSx0FMx1GEiAnEVKwXWUqiW72DRFH7cr9v7IQnI9YbAWRuWvyMPHSVLwg=="], @@ -453,8 +465,22 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/retry": ["@types/retry@0.12.2", "", {}, "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="], + "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], + "@uppy/companion-client": ["@uppy/companion-client@5.1.1", "", { "dependencies": { "@uppy/utils": "^7.1.1", "namespace-emitter": "^2.0.1", "p-retry": "^6.1.0" }, "peerDependencies": { "@uppy/core": "^5.1.1" } }, "sha512-DzrOWTbIZHvtgAFXBMYHk2wD27NjpBSVhY2tEiEIUhPd2CxbFRZjHM/N3HOt3VwZEAP471QWFLlJRWPcIY3A2Q=="], + + "@uppy/core": ["@uppy/core@5.2.0", "", { "dependencies": { "@transloadit/prettier-bytes": "^0.3.4", "@uppy/store-default": "^5.0.0", "@uppy/utils": "^7.1.4", "lodash": "^4.17.21", "mime-match": "^1.0.2", "namespace-emitter": "^2.0.1", "nanoid": "^5.0.9", "preact": "^10.5.13" } }, "sha512-uvfNyz4cnaplt7LYJmEZHuqOuav0tKp4a9WKJIaH6iIj7XiqYvS2J5SEByexAlUFlzefOAyjzj4Ja2dd/8aMrw=="], + + "@uppy/store-default": ["@uppy/store-default@5.0.0", "", {}, "sha512-hQtCSQ1yGiaval/wVYUWquYGDJ+bpQ7e4FhUUAsRQz1x1K+o7NBtjfp63O9I4Ks1WRoKunpkarZ+as09l02cPw=="], + + "@uppy/transloadit": ["@uppy/transloadit@5.5.1", "", { "dependencies": { "@transloadit/types": "^4.1.3", "@uppy/tus": "^5.1.1", "@uppy/utils": "^7.2.0", "component-emitter": "^2.0.0" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-nzgZ/u90hMzbsCj/44pVepUXtbOvpIbWAHLRLbSTUbQUfwR6o8o5s59IFmFUQ3uLYdunW4DDaOkEdNb7hRLmEw=="], + + "@uppy/tus": ["@uppy/tus@5.1.1", "", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/utils": "^7.1.5", "tus-js-client": "^4.2.3" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-316kLQfO5H/uUJIMhBYhBrTpeN0Q+d6ykW3pomCvdTkFGCvg20rF3oH/owE3lf2UZZN7ZqBk+wHO0WlQePoklg=="], + + "@uppy/utils": ["@uppy/utils@7.2.0", "", { "dependencies": { "lodash": "^4.17.23", "preact": "^10.26.10" } }, "sha512-6lC246qszMv6bTyl/+QyHwrudgeguWkA94ME1wHn+a6uRAvmtAEaUManIfGqTJfoKvWAiCJqdJPl5xRJjhAloQ=="], + "@xyflow/react": ["@xyflow/react@12.11.0", "", { "dependencies": { "@xyflow/system": "0.0.77", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "@types/react": ">=17", "@types/react-dom": ">=17", "react": ">=17", "react-dom": ">=17" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA=="], "@xyflow/system": ["@xyflow/system@0.0.77", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg=="], @@ -559,6 +585,8 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "component-emitter": ["component-emitter@2.0.0", "", {}, "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "conf": ["conf@10.2.0", "", { "dependencies": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "atomically": "^1.7.0", "debounce-fn": "^4.0.0", "dot-prop": "^6.0.1", "env-paths": "^2.2.1", "json-schema-typed": "^7.0.3", "onetime": "^5.1.2", "pkg-up": "^3.1.0", "semver": "^7.3.5" } }, "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg=="], @@ -843,6 +871,8 @@ "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + "is-network-error": ["is-network-error@1.3.2", "", {}, "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="], @@ -969,6 +999,8 @@ "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "mime-match": ["mime-match@1.0.2", "", { "dependencies": { "wildcard": "^1.1.0" } }, "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg=="], + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], @@ -993,7 +1025,9 @@ "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], - "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], + "namespace-emitter": ["namespace-emitter@2.0.1", "", {}, "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g=="], + + "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -1043,6 +1077,8 @@ "p-queue": ["p-queue@9.3.0", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang=="], + "p-retry": ["p-retry@6.2.1", "", { "dependencies": { "@types/retry": "0.12.2", "is-network-error": "^1.0.0", "retry": "^0.13.1" } }, "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ=="], + "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], @@ -1111,6 +1147,8 @@ "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "preact": ["preact@10.29.3", "", {}, "sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], "prisma": ["prisma@7.8.0", "", { "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.8.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw=="], @@ -1341,6 +1379,8 @@ "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "wildcard": ["wildcard@1.1.2", "", {}, "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], @@ -1469,8 +1509,12 @@ "nypm/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + "postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], + "prisma/@prisma/config": ["@prisma/config@7.8.0", "", { "dependencies": { "c12": "3.3.4", "deepmerge-ts": "7.1.5", "effect": "3.20.0", "empathic": "2.0.0" } }, "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], @@ -1537,6 +1581,8 @@ "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "next/postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], + "prisma/@prisma/config/c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="], "prisma/@prisma/config/effect": ["effect@3.20.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw=="], diff --git a/components/NodePropertiesPanel.tsx b/components/NodePropertiesPanel.tsx index b619665..48975ec 100644 --- a/components/NodePropertiesPanel.tsx +++ b/components/NodePropertiesPanel.tsx @@ -1,7 +1,7 @@ "use client"; import type { Node } from "@xyflow/react"; -import { Plus, Trash2, X } from "lucide-react"; +import { Loader2, Plus, Trash2, X } from "lucide-react"; import { useState } from "react"; import { useCanvasStore } from "@/lib/stores"; @@ -12,6 +12,30 @@ interface Props { type Field = { name: string; type: string; value?: string }; +async function uploadToTransloadit(file: File): Promise { + const res = await fetch("/api/upload/transloadit"); + if (!res.ok) throw new Error("Failed to get upload params"); + const { params, signature } = await res.json(); + + const form = new FormData(); + form.append("params", params); + form.append("signature", signature); + form.append("my_file", file); + + const assemblyRes = await fetch( + "https://api2.transloadit.com/assemblies?wait=true", + { + method: "POST", + body: form, + }, + ); + if (!assemblyRes.ok) throw new Error("Upload failed"); + + const result = await assemblyRes.json(); + const uploaded = result?.results?.[":original"]?.[0]; + return uploaded?.ssl_url ?? uploaded?.url ?? ""; +} + function RequestInputsForm({ config, onChange, @@ -22,6 +46,7 @@ function RequestInputsForm({ const fields = (config.fields as Field[]) ?? []; const [newName, setNewName] = useState(""); const [newType, setNewType] = useState("text"); + const [uploadingIdx, setUploadingIdx] = useState(null); const addField = () => { if (!newName.trim()) return; @@ -87,20 +112,34 @@ function RequestInputsForm({ accept="image/*" className="hidden" id={`upload-${i}`} - onChange={(e) => { + onChange={async (e) => { const file = e.target.files?.[0]; - if (file) { - const url = URL.createObjectURL(file); - updateField(i, "value", url); + if (!file) return; + setUploadingIdx(i); + try { + const url = await uploadToTransloadit(file); + if (url) updateField(i, "value", url); + } catch (err) { + console.log("[UPLOAD] Transloadit error:", err); + } finally { + setUploadingIdx(null); } }} />
) : ( diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 1645370..8cb01d7 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -1,23 +1,25 @@ "use client"; -import { useClerk, useUser } from "@clerk/nextjs"; +import { UserButton } from "@clerk/nextjs"; import { - BarChart3, BookOpen, + Boxes, ChevronDown, - FolderClosed, + ChevronUp, + FolderOpen, Gift, - MessageSquare, + Library, + MessageSquareMore, + PanelLeft, Plus, Search, Settings, - Users, - Workflow, + Share2, } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Sidebar, @@ -29,7 +31,6 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, - SidebarSeparator, SidebarTrigger, useSidebar, } from "@/components/ui/sidebar"; @@ -42,31 +43,25 @@ import { const mainNav = [ { href: "#", label: "New task", icon: Plus }, { href: "#", label: "Search Task", icon: Search }, - { href: "#", label: "Task", icon: MessageSquare }, - { href: "#", label: "Projects", icon: FolderClosed }, - { href: "#", label: "Library", icon: BarChart3 }, - { href: "/app/flow", label: "Flow", icon: Workflow }, - { href: "#", label: "Tools", icon: Users }, + { href: "#", label: "Task", icon: MessageSquareMore }, + { href: "#", label: "Projects", icon: FolderOpen }, + { href: "#", label: "Library", icon: Library }, + { href: "/app/flow", label: "Flow", icon: Share2 }, + { href: "#", label: "Tools", icon: Boxes }, { href: "#", label: "API / MCP", icon: BookOpen }, ]; export function AppSidebar() { const pathname = usePathname(); - const { user } = useUser(); - const { signOut } = useClerk(); const { state, toggleSidebar } = useSidebar(); const collapsed = state === "collapsed"; - - const initials = - user?.firstName?.[0] ?? user?.emailAddresses?.[0]?.emailAddress?.[0] ?? "?"; - const displayName = - [user?.firstName, user?.lastName].filter(Boolean).join(" ") || "User"; + const [showSettingsOffer, setShowSettingsOffer] = useState(true); return ( {/* Header: Logo + Collapse Trigger */} - -
+ +
{collapsed ? ( } > @@ -83,8 +78,11 @@ export function AppSidebar() { alt="Magica" width={24} height={24} - className="invert dark:invert-0" + className="invert dark:invert-0 transition-opacity duration-150 group-hover/logo:opacity-0" /> +
+ +
Open sidebar @@ -95,16 +93,33 @@ export function AppSidebar() {
) : ( <> - + Magica - + + } + /> + + Close sidebar + + . + + + )}
@@ -114,7 +129,7 @@ export function AppSidebar() { - + {mainNav.map((item) => { const Icon = item.icon; const isActive = @@ -125,7 +140,7 @@ export function AppSidebar() { isActive={isActive} tooltip={item.label} render={} - className="h-[46px] text-[15.5px] px-4 gap-4 rounded-xl [&_svg]:size-[19.5px] font-semibold text-gray-700 data-active:text-gray-900 data-active:bg-gray-100/70 transition-all" + className="h-11 text-[16px] px-4 gap-4 rounded-xl [&_svg]:size-[22px] [&_svg]:text-[#3D3D41] font-normal text-gray-700 data-active:text-gray-950 data-active:bg-[#DCDCE0] transition-all group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0" > {item.label} @@ -139,85 +154,94 @@ export function AppSidebar() { {/* No tasks yet placeholder */} {!collapsed && ( -
- +
+ No tasks yet
)} - {/* Footer */} - - {/* Settings */} - - - {collapsed ? ( - } - className="h-[46px] text-[15.5px] px-4 gap-4 rounded-xl [&_svg]:size-[19.5px] font-semibold text-gray-700 hover:text-gray-900 transition-all" - > - - - ) : ( - - - Settings - - )} - - - - - - {/* Claim Offer */} - - - {collapsed ? ( - + {/* Settings + Claim Offer */} + {(!collapsed && showSettingsOffer) || collapsed ? ( + <> + + + {collapsed ? ( + + ) : ( + + + Settings + + )} + + + + + {collapsed ? ( + + + + ) : ( +
+ + +
+ )} +
+
+ + ) : ( + !collapsed && ( +
+ - -
- )} -
-
+ + +
+ ) + )} - {/* User Profile */} - - - signOut({ redirectUrl: "/" })} - className="h-13.5 text-[15.5px] [&_svg]:size-[19.5px] gap-4 font-semibold text-gray-750 hover:text-gray-950 transition-all rounded-xl" - > - - - {initials} - -
- - {displayName} - -
- -
-
-
+ {/* User Profile — Clerk UserButton */} +
+ +
); diff --git a/components/nodes/NodeShell.tsx b/components/nodes/NodeShell.tsx index 72a8afb..d502234 100644 --- a/components/nodes/NodeShell.tsx +++ b/components/nodes/NodeShell.tsx @@ -1,9 +1,9 @@ "use client"; -import type { ReactNode } from "react"; import { Play } from "lucide-react"; +import { type ReactNode, useCallback, useRef } from "react"; +import { useCanvasStore, useExecutionStore } from "@/lib/stores"; import { cn } from "@/lib/utils"; -import { useExecutionStore } from "@/lib/stores"; interface NodeShellProps { id: string; @@ -67,13 +67,70 @@ export function NodeShell({ const nodeStatus = useExecutionStore((s) => s.nodeStatuses[id]); const isRunning = nodeStatus === "running"; const c = colorConfig[color]; + const pollRef = useRef | null>(null); + + const handleRun = useCallback(async () => { + const workflowId = + window.location.pathname.match(/\/workflows\/([^/]+)/)?.[1]; + if (!workflowId) return; + + const startRun = useExecutionStore.getState().startRun; + const updateNodeStatus = useExecutionStore.getState().updateNodeStatus; + const setNodeResult = useExecutionStore.getState().setNodeResult; + const setNodeError = useExecutionStore.getState().setNodeError; + const completeRun = useExecutionStore.getState().completeRun; + + try { + const res = await fetch(`/api/workflows/${workflowId}/run`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ selectedNodeIds: [id] }), + }); + if (!res.ok) return; + + const run = await res.json(); + startRun(run.id, [id]); + + pollRef.current = setInterval(async () => { + try { + const statusRes = await fetch( + `/api/workflows/${workflowId}/runs/${run.id}`, + ); + if (!statusRes.ok) { + if (pollRef.current) clearInterval(pollRef.current); + completeRun(run.id); + return; + } + const data = await statusRes.json(); + if (["SUCCESS", "FAILED", "PARTIAL"].includes(data.status)) { + if (pollRef.current) clearInterval(pollRef.current); + for (const nr of data.nodeResults ?? []) { + updateNodeStatus(run.id, nr.nodeId, nr.status.toLowerCase()); + if (nr.output != null) { + setNodeResult(run.id, nr.nodeId, nr.output); + } + if (nr.error != null) { + setNodeError(run.id, nr.nodeId, nr.error); + } + } + completeRun(run.id); + } + } catch { + if (pollRef.current) clearInterval(pollRef.current); + completeRun(run.id); + } + }, 2000); + } catch (err) { + console.log("[NODE RUN] error:", err); + } + }, [id]); return (
)} - +
diff --git a/components/nodes/ResponseNode.tsx b/components/nodes/ResponseNode.tsx index 1421d76..f707a94 100644 --- a/components/nodes/ResponseNode.tsx +++ b/components/nodes/ResponseNode.tsx @@ -55,9 +55,10 @@ function ResponseNode({ id, data, selected }: NodeProps) { value && typeof value === "object" && "response" in (value as Record) - ? String( - (value as Record).response, - ).slice(0, 80) + ? String((value as Record).response).slice( + 0, + 80, + ) : typeof value === "string" ? value.slice(0, 80) : "Output received"; diff --git a/components/promo-banner.tsx b/components/promo-banner.tsx index d6d5c46..237af8a 100644 --- a/components/promo-banner.tsx +++ b/components/promo-banner.tsx @@ -27,13 +27,12 @@ export function PromoBanner() { const pad = (n: number) => String(n).padStart(2, "0"); return ( -
+
- Pay once, get a{" "} - LIFETIME deal - forever — for only $499 + Pay once, get a LIFETIME deal forever + — for only $499 - + Click here diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx index 593e8c6..7a9de30 100644 --- a/components/ui/sidebar.tsx +++ b/components/ui/sidebar.tsx @@ -26,10 +26,10 @@ import { cn } from "@/lib/utils"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = "17.5rem"; +const SIDEBAR_WIDTH = "18.5rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; -const SIDEBAR_KEYBOARD_SHORTCUT = "b"; +const SIDEBAR_KEYBOARD_SHORTCUT = "."; type SidebarContextProps = { state: "expanded" | "collapsed"; @@ -262,15 +262,18 @@ function SidebarTrigger({ data-sidebar="trigger" data-slot="sidebar-trigger" variant="ghost" - size="icon-sm" - className={cn(className)} + size="icon" + className={cn( + "h-11 w-11 rounded-xl text-gray-500 hover:text-gray-900 cursor-pointer", + className, + )} onClick={(event) => { onClick?.(event); toggleSidebar(); }} {...props} > - + Toggle Sidebar ); diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx index 086477b..64413cb 100644 --- a/components/ui/tooltip.tsx +++ b/components/ui/tooltip.tsx @@ -50,13 +50,13 @@ function TooltipContent({ {children} - + diff --git a/lib/stores/execution-store.ts b/lib/stores/execution-store.ts index 54a1824..a089ddf 100644 --- a/lib/stores/execution-store.ts +++ b/lib/stores/execution-store.ts @@ -135,15 +135,13 @@ export const useExecutionStore = create((set) => ({ completeRun: (runId) => { set((state) => { const activeRunIds = state.activeRunIds.filter((id) => id !== runId); - const runs = removeRun(state.runs, runId); const nextActiveId = activeRunIds.length > 0 ? activeRunIds[activeRunIds.length - 1] : null; return { activeRunIds, - activeRunId: nextActiveId, + activeRunId: nextActiveId ?? runId, isRunning: activeRunIds.length > 0, - runs, - ...syncActiveRun(runs, nextActiveId), + ...syncActiveRun(state.runs, nextActiveId ?? runId), }; }); }, diff --git a/package.json b/package.json index e279b0b..2fa1a10 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,15 @@ "dependencies": { "@base-ui/react": "^1.6.0", "@clerk/nextjs": "^7.5.8", + "@fontsource/google-sans": "^5.2.1", + "@fontsource/google-sans-flex": "^5.2.4", "@google/generative-ai": "0.24.1", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", "@t3-oss/env-nextjs": "^0.13.11", "@trigger.dev/sdk": "4.4.6", + "@uppy/core": "^5.2.0", + "@uppy/transloadit": "^5.5.1", "@xyflow/react": "12.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/public/porsche-rear.png b/public/porsche-rear.png new file mode 100644 index 0000000..4e7ad3e Binary files /dev/null and b/public/porsche-rear.png differ