From 2f3fff3536dd078f043c562afb63ec63b17ba2f1 Mon Sep 17 00:00:00 2001 From: flowingfate Date: Mon, 18 May 2026 11:53:38 +0800 Subject: [PATCH] refactor: introduce react-router --- packages/workflow-dashboard/index.html | 8 + packages/workflow-dashboard/package.json | 1 + packages/workflow-dashboard/src/app.tsx | 61 +-- .../src/components/client-redirect.tsx | 31 ++ .../src/components/login.tsx | 110 ++-- .../src/components/markdown.tsx | 44 +- .../src/components/record-card.tsx | 115 ++-- .../src/components/run-dialog.tsx | 125 ++--- .../src/components/sidebar.tsx | 174 +++--- .../src/components/status-bar.tsx | 55 +- .../src/components/thread-detail.tsx | 138 ++--- .../src/components/thread-list.tsx | 79 +-- .../src/components/ui/badge.tsx | 30 ++ .../src/components/ui/button.tsx | 45 ++ .../src/components/ui/card.tsx | 36 ++ .../src/components/ui/collapsible.tsx | 7 + .../src/components/ui/dialog.tsx | 104 ++++ .../src/components/ui/input.tsx | 17 + .../src/components/ui/resizable-panel.tsx | 73 +++ .../src/components/ui/scroll-area.tsx | 42 ++ .../src/components/ui/select.tsx | 148 ++++++ .../src/components/ui/separator.tsx | 25 + .../src/components/ui/table.tsx | 69 +++ .../src/components/ui/textarea.tsx | 16 + .../src/components/ui/tooltip.tsx | 28 + .../src/components/workflow-detail.tsx | 495 +++++++++--------- .../workflow-graph/condition-edge.tsx | 8 +- .../components/workflow-graph/role-node.tsx | 36 +- .../workflow-graph/terminal-node.tsx | 24 +- .../workflow-graph/workflow-graph.tsx | 18 +- .../src/components/workflow-list.tsx | 67 ++- .../src/hooks/use-theme.tsx | 74 +++ packages/workflow-dashboard/src/index.css | 109 +++- packages/workflow-dashboard/src/lib/utils.ts | 6 + packages/workflow-dashboard/src/main.tsx | 8 +- packages/workflow-dashboard/src/router.tsx | 45 ++ .../workflow-dashboard/src/use-hash-route.ts | 110 ---- packages/workflow-dashboard/src/vite-env.d.ts | 1 + packages/workflow-dashboard/tsconfig.json | 4 +- 39 files changed, 1677 insertions(+), 909 deletions(-) create mode 100644 packages/workflow-dashboard/src/components/client-redirect.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/badge.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/button.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/card.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/collapsible.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/dialog.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/input.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/resizable-panel.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/scroll-area.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/select.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/separator.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/table.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/textarea.tsx create mode 100644 packages/workflow-dashboard/src/components/ui/tooltip.tsx create mode 100644 packages/workflow-dashboard/src/hooks/use-theme.tsx create mode 100644 packages/workflow-dashboard/src/lib/utils.ts create mode 100644 packages/workflow-dashboard/src/router.tsx delete mode 100644 packages/workflow-dashboard/src/use-hash-route.ts create mode 100644 packages/workflow-dashboard/src/vite-env.d.ts diff --git a/packages/workflow-dashboard/index.html b/packages/workflow-dashboard/index.html index 6621a30..da26862 100644 --- a/packages/workflow-dashboard/index.html +++ b/packages/workflow-dashboard/index.html @@ -4,6 +4,14 @@ Workflow Dashboard +
diff --git a/packages/workflow-dashboard/package.json b/packages/workflow-dashboard/package.json index 0ec2e1e..c6027d7 100644 --- a/packages/workflow-dashboard/package.json +++ b/packages/workflow-dashboard/package.json @@ -17,6 +17,7 @@ "react": "^19.2.6", "react-dom": "^19.2.6", "react-markdown": "^10.1.0", + "react-router": "^7.15.1", "shiki": "^4.0.2" }, "devDependencies": { diff --git a/packages/workflow-dashboard/src/app.tsx b/packages/workflow-dashboard/src/app.tsx index b96d11e..3ace0e7 100644 --- a/packages/workflow-dashboard/src/app.tsx +++ b/packages/workflow-dashboard/src/app.tsx @@ -1,75 +1,38 @@ import { useState } from "react"; +import { Navigate, Outlet, useParams } from "react-router"; import { clearApiKey, hasApiKey } from "./api.ts"; -import { LoginPage } from "./components/login.tsx"; import { RunDialog } from "./components/run-dialog.tsx"; import { Sidebar } from "./components/sidebar.tsx"; import { StatusBar } from "./components/status-bar.tsx"; -import { ThreadDetail } from "./components/thread-detail.tsx"; -import { ThreadList } from "./components/thread-list.tsx"; -import { WorkflowDetail } from "./components/workflow-detail.tsx"; -import { WorkflowList } from "./components/workflow-list.tsx"; -import { useHashRoute } from "./use-hash-route.ts"; +import { useTheme } from "./hooks/use-theme.tsx"; -export function App() { +export function Layout() { const [authed, setAuthed] = useState(hasApiKey()); - const { view, client, threadId, workflowName, setView, setClient, setThreadId, setWorkflowName } = - useHashRoute(); + const { client } = useParams(); const [showRun, setShowRun] = useState(false); + const { theme, toggleTheme } = useTheme(); if (!authed) { - return setAuthed(true)} />; + return ; } return ( -
+
{ clearApiKey(); setAuthed(false); }} + theme={theme} + onToggleTheme={toggleTheme} />
- setShowRun(true)} /> + setShowRun(true)} />
- {!client && ( -
-

- Select an client from the sidebar to get started. -

-
- )} - {client && view === "threads" && threadId === null && ( - - )} - {client && view === "threads" && threadId !== null && ( - setThreadId(null)} /> - )} - {client && view === "workflows" && workflowName === null && ( - - )} - {client && view === "workflows" && workflowName !== null && ( - setWorkflowName(null)} - /> - )} +
- {showRun && client && ( - setShowRun(false)} - onCreated={(id) => { - setShowRun(false); - setThreadId(id); - }} - /> - )} + {client && }
); } diff --git a/packages/workflow-dashboard/src/components/client-redirect.tsx b/packages/workflow-dashboard/src/components/client-redirect.tsx new file mode 100644 index 0000000..6381e16 --- /dev/null +++ b/packages/workflow-dashboard/src/components/client-redirect.tsx @@ -0,0 +1,31 @@ +import { Loader2, Users } from "lucide-react"; +import { Navigate } from "react-router"; +import { listClients } from "../api.ts"; +import { useFetch } from "../hooks.ts"; + +export function ClientRedirect() { + const { status, data } = useFetch(() => listClients(), []); + + if (status === "loading") { + return ( +
+ +

Loading clients...

+
+ ); + } + + if (status === "ok" && data.length > 0) { + return ; + } + + return ( +
+ +

No client selected

+

+ Select a client from the sidebar to get started. +

+
+ ); +} diff --git a/packages/workflow-dashboard/src/components/login.tsx b/packages/workflow-dashboard/src/components/login.tsx index 6ee1afb..7e6eeb9 100644 --- a/packages/workflow-dashboard/src/components/login.tsx +++ b/packages/workflow-dashboard/src/components/login.tsx @@ -1,14 +1,18 @@ +import { AlertCircle, Loader2, Moon, Settings, Sun } from "lucide-react"; import { useState } from "react"; +import { useNavigate } from "react-router"; import { setApiKey } from "../api.ts"; +import { useTheme } from "../hooks/use-theme.tsx"; +import { Button } from "./ui/button.tsx"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card.tsx"; +import { Input } from "./ui/input.tsx"; -type Props = { - onLogin: () => void; -}; - -export function LoginPage({ onLogin }: Props) { +export function LoginPage() { + const navigate = useNavigate(); const [key, setKey] = useState(""); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const { theme, toggleTheme } = useTheme(); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); @@ -17,7 +21,6 @@ export function LoginPage({ onLogin }: Props) { setLoading(true); setError(null); - // Test the key by hitting the endpoints list const gatewayUrl = import.meta.env.VITE_GATEWAY_URL || ""; try { const res = await fetch(`${gatewayUrl}/api/gateway/endpoints`, { @@ -40,56 +43,59 @@ export function LoginPage({ onLogin }: Props) { } setApiKey(key.trim()); - onLogin(); + navigate("/", { replace: true }); } return ( -
-
+ - -
+ {theme === "dark" ? : } + + + + + + Workflow Dashboard + + Enter your API key to continue + + +
+ setKey(e.target.value)} + placeholder="API Key" + className="transition-all duration-200" + /> + {error && ( +

+ + {error} +

+ )} + +
+
+
); } diff --git a/packages/workflow-dashboard/src/components/markdown.tsx b/packages/workflow-dashboard/src/components/markdown.tsx index d369e39..4407e90 100644 --- a/packages/workflow-dashboard/src/components/markdown.tsx +++ b/packages/workflow-dashboard/src/components/markdown.tsx @@ -52,19 +52,23 @@ function CodeBlock({ className, children }: { className?: string; children?: Rea if (html !== null) { return ( -
+
+ {lang !== "text" && ( + + {lang} + + )} +
+
); } return ( -
+    
       {code}
     
); @@ -80,8 +84,7 @@ export function Markdown({ content }: { content: string }) { if (isInline) { return ( {children} @@ -91,7 +94,7 @@ export function Markdown({ content }: { content: string }) { return {children}; }, p({ children }) { - return

{children}

; + return

{children}

; }, ul({ children }) { return
    {children}
; @@ -100,20 +103,25 @@ export function Markdown({ content }: { content: string }) { return
    {children}
; }, h1({ children }) { - return

{children}

; + return ( +

+ {children} +

+ ); }, h2({ children }) { - return

{children}

; + return ( +

+ {children} +

+ ); }, h3({ children }) { return

{children}

; }, blockquote({ children }) { return ( -
+
{children}
); diff --git a/packages/workflow-dashboard/src/components/record-card.tsx b/packages/workflow-dashboard/src/components/record-card.tsx index 5127135..1c9e007 100644 --- a/packages/workflow-dashboard/src/components/record-card.tsx +++ b/packages/workflow-dashboard/src/components/record-card.tsx @@ -1,14 +1,26 @@ +import { CheckCircle2, Clock, MessageSquare, Rocket, User, XCircle } from "lucide-react"; import type { RoleRecord, ThreadRecord, ThreadStartRecord, WorkflowResultRecord } from "../api.ts"; +import { cn } from "../lib/utils.ts"; import { Markdown } from "./markdown.tsx"; +import { Badge } from "./ui/badge.tsx"; +import { Card } from "./ui/card.tsx"; -const ROLE_COLORS: Record = { - preparer: "#8b5cf6", - client: "#3b82f6", - extractor: "#f59e0b", -}; +const ROLE_HUES = [262, 210, 35, 150, 330, 180, 15, 280, 55, 195, 345, 120, 240, 75, 305]; -function roleColor(role: string): string { - return ROLE_COLORS[role] ?? "var(--color-accent)"; +function roleHue(role: string): number { + let hash = 0; + for (let i = 0; i < role.length; i++) { + hash = (hash * 31 + role.charCodeAt(i)) | 0; + } + return ROLE_HUES[Math.abs(hash) % ROLE_HUES.length]; +} + +function roleBadgeStyle(role: string): { backgroundColor: string; borderColor: string } { + const hue = roleHue(role); + return { + backgroundColor: `oklch(0.58 0.12 ${hue} / 0.85)`, + borderColor: `oklch(0.58 0.12 ${hue} / 0.25)`, + }; } function formatTime(ts: number | null): string | null { @@ -18,99 +30,86 @@ function formatTime(ts: number | null): string | null { function StartCard({ record }: { record: ThreadStartRecord }) { return ( -
+ +
- πŸš€ - - {record.workflow} - - + + {record.workflow} + {record.status} - +
{record.prompt !== null && ( -
-
+
+
+ Prompt
)} -
+ ); } function RoleMessage({ record, highlighted }: { record: RoleRecord; highlighted: boolean }) { - const color = roleColor(record.role); + const style = roleBadgeStyle(record.role); return ( -
+ {record.role} {formatTime(record.timestamp) !== null && ( - + + {formatTime(record.timestamp)} )}
-
+ ); } function ResultCard({ record }: { record: WorkflowResultRecord }) { const success = record.returnCode === 0; return ( -
- {success ? "βœ…" : "❌"} + {success ? ( + + ) : ( + + )} {success ? "Completed" : "Failed"} - + exit {record.returnCode} - + {formatTime(record.timestamp) !== null && ( - + + {formatTime(record.timestamp)} )}
-
+ ); } diff --git a/packages/workflow-dashboard/src/components/run-dialog.tsx b/packages/workflow-dashboard/src/components/run-dialog.tsx index 843ca13..6d94050 100644 --- a/packages/workflow-dashboard/src/components/run-dialog.tsx +++ b/packages/workflow-dashboard/src/components/run-dialog.tsx @@ -1,14 +1,27 @@ import { useState } from "react"; +import { useNavigate } from "react-router"; import { listWorkflows, runThread } from "../api.ts"; import { useFetch } from "../hooks.ts"; +import { Button } from "./ui/button.tsx"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "./ui/dialog.tsx"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select.tsx"; +import { Textarea } from "./ui/textarea.tsx"; type Props = { client: string; - onClose: () => void; - onCreated: (threadId: string) => void; + open: boolean; + onOpenChange: (open: boolean) => void; }; -export function RunDialog({ client, onClose, onCreated }: Props) { +export function RunDialog({ client, open, onOpenChange }: Props) { + const navigate = useNavigate(); const workflows = useFetch(() => listWorkflows(client), [client]); const [workflow, setWorkflow] = useState(""); const [prompt, setPrompt] = useState(""); @@ -22,7 +35,8 @@ export function RunDialog({ client, onClose, onCreated }: Props) { setError(null); try { const result = await runThread(client, workflow, prompt); - onCreated(result.threadId); + onOpenChange(false); + navigate(`/${client}/threads/${result.threadId}`); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setSubmitting(false); @@ -30,95 +44,54 @@ export function RunDialog({ client, onClose, onCreated }: Props) { } return ( -
-
-

Run Thread on {client}

+ + + + Run Thread + Start a new thread on {client} +
-
-