|
1 | 1 | import { useState } from "react"; |
2 | | -import { |
3 | | - type WorkflowEvent, |
4 | | - useWorkflowHandler, |
5 | | - useWorkflowRun, |
6 | | -} from "@llamaindex/ui"; |
| 2 | +import { useWorkflowRun, useWorkflowHandler } from "@llamaindex/ui"; |
7 | 3 |
|
8 | 4 | export default function Home() { |
9 | | - const [taskId, setTaskId] = useState<string | null>(null); |
10 | | - const createHandler = useWorkflowRun(); |
| 5 | + const [handlerId, setHandlerId] = useState<string | null>(null); |
| 6 | + const run = useWorkflowRun(); |
| 7 | + const handler = useWorkflowHandler(handlerId ?? ""); |
| 8 | + |
| 9 | + const result = handler.events.find((e) => e.type.endsWith(".StopEvent")) as |
| 10 | + | { type: string; data: { result?: string } } |
| 11 | + | undefined; |
| 12 | + |
11 | 13 | return ( |
12 | | - <div className="aurora-container relative min-h-screen overflow-hidden bg-background text-foreground"> |
13 | | - <main className="relative mx-auto flex min-h-screen max-w-2xl px-6 flex-col gap-4 items-center justify-center my-12"> |
14 | | - <div className="text-center mb-4 text-black/80 dark:text-white/80 text-lg"> |
15 | | - This is a basic starter app for LlamaDeploy. It's running a simple |
16 | | - Human-in-the-loop workflow on the backend, and vite with react on the |
17 | | - frontend with llama-ui to call the workflow. Customize this app with |
18 | | - your own workflow and UI. |
19 | | - </div> |
20 | | - <div className="flex flex-row gap-4 items-start justify-center w-full"> |
21 | | - <HandlerOutput handlerId={taskId} /> |
22 | | - <RunButton |
23 | | - disabled={createHandler.isCreating} |
| 14 | + <div className="relative min-h-screen flex items-center justify-center p-6"> |
| 15 | + <div className="max-w-2xl text-center text-black/80 dark:text-white/80 flex flex-col gap-4"> |
| 16 | + <p className="text-lg"> |
| 17 | + This is a minimal UI starter. Click the button to run the backend |
| 18 | + workflow and display its result. |
| 19 | + </p> |
| 20 | + <div className="flex items-center justify-center gap-3"> |
| 21 | + <button |
| 22 | + type="button" |
| 23 | + disabled={run.isCreating} |
24 | 24 | onClick={() => |
25 | | - createHandler |
26 | | - .runWorkflow("default", { |
27 | | - message: `${new Date().toLocaleTimeString()} PING`, |
28 | | - }) |
29 | | - .then((task) => setTaskId(task.handler_id)) |
| 25 | + run |
| 26 | + .runWorkflow("default", {}) |
| 27 | + .then((h) => setHandlerId(h.handler_id)) |
30 | 28 | } |
| 29 | + className="inline-flex items-center justify-center rounded-xl border px-6 py-3 text-sm font-semibold shadow-sm border-black/10 bg-black/5 text-black hover:bg-black/10 dark:border-white/10 dark:bg-white/10 dark:text-white" |
31 | 30 | > |
32 | | - <GreenDot /> |
33 | | - Run |
34 | | - </RunButton> |
| 31 | + Run Workflow |
| 32 | + </button> |
35 | 33 | </div> |
36 | | - </main> |
37 | | - </div> |
38 | | - ); |
39 | | -} |
40 | | - |
41 | | -const GreenDot = () => { |
42 | | - return ( |
43 | | - <span className="mr-2 size-2 rounded-full bg-emerald-500/80 shadow-[0_0_20px_2px_rgba(16,185,129,0.35)] transition group-hover:bg-emerald-400/90"></span> |
44 | | - ); |
45 | | -}; |
46 | | - |
47 | | -function RunButton({ |
48 | | - disabled, |
49 | | - children, |
50 | | - onClick, |
51 | | -}: { |
52 | | - disabled: boolean; |
53 | | - children: React.ReactNode; |
54 | | - onClick: () => void; |
55 | | -}) { |
56 | | - return ( |
57 | | - <button |
58 | | - type="button" |
59 | | - disabled={disabled} |
60 | | - onClick={onClick} |
61 | | - className="group inline-flex items-center justify-center rounded-xl border px-6 py-3 text-sm font-semibold shadow-sm backdrop-blur transition active:scale-[.99] |
62 | | - border-black/10 bg-black/5 text-black hover:bg-black/10 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-black/20 |
63 | | - dark:border-white/10 dark:bg-white/10 dark:text-white dark:hover:bg-white/15 dark:focus:ring-white/30 cursor-pointer" |
64 | | - > |
65 | | - {children} |
66 | | - </button> |
67 | | - ); |
68 | | -} |
69 | | - |
70 | | -type PongEvent = { type: `${string}.PongEvent`; data: { message: string } }; |
71 | | -type PauseEvent = { type: `${string}.PauseEvent`; data: { timestamp: string } }; |
72 | | - |
73 | | -function isPongEvent(event: WorkflowEvent): event is PongEvent { |
74 | | - return event.type.endsWith(".PongEvent"); |
75 | | -} |
76 | | -function isPauseEvent(event: WorkflowEvent): event is PauseEvent { |
77 | | - return event.type.endsWith(".PauseEvent"); |
78 | | -} |
79 | | - |
80 | | -function HandlerOutput({ handlerId }: { handlerId: string | null }) { |
81 | | - // stream events and result from the workflow |
82 | | - const handler = useWorkflowHandler(handlerId ?? ""); |
83 | | - |
84 | | - // read workflow events here |
85 | | - const pongsOrResume = handler.events.filter( |
86 | | - (event) => isPongEvent(event) || isPauseEvent(event), |
87 | | - ) as (PongEvent | PauseEvent)[]; |
88 | | - const completed = handler.events.find((event) => |
89 | | - event.type.endsWith(".WorkflowCompletedEvent"), |
90 | | - ) as { type: string; data: { timestamp: string } } | undefined; |
91 | | - |
92 | | - return ( |
93 | | - <div className="flex flex-col gap-4 w-full min-h-60 items-center"> |
94 | | - <Output>{completed ? completed.data.timestamp : "Running... "}</Output> |
95 | | - {pongsOrResume.map((pong, index) => ( |
96 | | - <span |
97 | | - className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-black/3 |
98 | | - dark:bg-white/2 text-black/60 dark:text-white/60 rounded border border-black/5 |
99 | | - dark:border-white/5 backdrop-blur-sm" |
100 | | - key={index} |
101 | | - style={{ |
102 | | - animation: "fade-in-left 80ms ease-out both", |
103 | | - willChange: "opacity, transform", |
104 | | - }} |
105 | | - > |
106 | | - {isPongEvent(pong) ? pong.data.message : pong.data.timestamp} |
107 | | - {isPauseEvent(pong) && |
108 | | - index === pongsOrResume.length - 1 && |
109 | | - !completed && ( |
110 | | - <button |
111 | | - onClick={() => |
112 | | - handler.sendEvent({ |
113 | | - type: "app.workflow.ResumeEvent", |
114 | | - data: { should_continue: true }, |
115 | | - }) |
116 | | - } |
117 | | - className="ml-2 px-2 py-0.5 bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20 |
118 | | - text-black/80 dark:text-white/80 text-xs rounded border border-black/10 dark:border-white/10" |
119 | | - > |
120 | | - Resume? |
121 | | - </button> |
122 | | - )} |
123 | | - </span> |
124 | | - ))} |
125 | | - {!completed && pongsOrResume.length > 0 && ( |
126 | | - <button |
127 | | - onClick={() => |
128 | | - handler.sendEvent({ |
129 | | - type: "app.workflow.ResumeEvent", |
130 | | - data: { should_continue: false }, |
131 | | - }) |
132 | | - } |
133 | | - className="ml-2 px-2 py-0.5 bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20 |
134 | | - text-black/80 dark:text-white/80 text-xs rounded border border-black/10 dark:border-white/10" |
135 | | - > |
136 | | - Stop |
137 | | - </button> |
138 | | - )} |
139 | | - </div> |
140 | | - ); |
141 | | -} |
142 | | - |
143 | | -function Output({ children }: { children: React.ReactNode }) { |
144 | | - return ( |
145 | | - <div |
146 | | - aria-live="polite" |
147 | | - className="w-full rounded-xl border bg-black/5 p-4 text-left shadow-[inset_0_1px_0_0_rgba(255,255,255,0.06)] |
148 | | - border-black/10 dark:border-white/10 dark:bg-white/5" |
149 | | - > |
150 | | - <pre className="whitespace-pre-wrap break-words font-mono text-xs text-black/80 dark:text-white/80"> |
151 | | - {children} |
152 | | - </pre> |
| 34 | + <div className="text-sm"> |
| 35 | + {handlerId && ( |
| 36 | + <div> |
| 37 | + Handler: <code>{handlerId}</code> |
| 38 | + </div> |
| 39 | + )} |
| 40 | + {result?.data?.result && ( |
| 41 | + <div className="mt-2"> |
| 42 | + Result: <code>{result.data.result}</code> |
| 43 | + </div> |
| 44 | + )} |
| 45 | + </div> |
| 46 | + </div> |
153 | 47 | </div> |
154 | 48 | ); |
155 | 49 | } |
0 commit comments