|
1 | 1 | import { useState } from "react"; |
2 | | -import { useWorkflowHandler, useWorkflowRun } from "@llamaindex/ui"; |
| 2 | +import { |
| 3 | + type WorkflowEvent, |
| 4 | + useWorkflowHandler, |
| 5 | + useWorkflowRun, |
| 6 | +} from "@llamaindex/ui"; |
3 | 7 |
|
4 | 8 | export default function Home() { |
5 | 9 | const [taskId, setTaskId] = useState<string | null>(null); |
6 | 10 | const createHandler = useWorkflowRun(); |
7 | 11 | return ( |
8 | 12 | <div className="aurora-container relative min-h-screen overflow-hidden bg-background text-foreground"> |
9 | | - <main className="relative mx-auto flex min-h-screen max-w-2xl px-6 flex-col gap-4 items-center justify-center"> |
| 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"> |
10 | 14 | <div className="text-center mb-4 text-black/80 dark:text-white/80 text-lg"> |
11 | 15 | This is a basic starter app for LlamaDeploy. It's running a simple |
12 | | - workflow on the backend, and vite with react on the frontend with |
13 | | - llama-ui to call the workflow. Customize this app with your own |
14 | | - workflow and UI. |
| 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. |
15 | 19 | </div> |
16 | 20 | <div className="flex flex-row gap-4 items-start justify-center w-full"> |
17 | | - {taskId ? ( |
18 | | - <HandlerOutput handlerId={taskId} /> |
19 | | - ) : ( |
20 | | - <Output> |
21 | | - <span className="text-black/60 dark:text-white/60"> |
22 | | - Workflow result will show here. |
23 | | - </span> |
24 | | - </Output> |
25 | | - )} |
| 21 | + <HandlerOutput handlerId={taskId} /> |
26 | 22 | <RunButton |
27 | 23 | disabled={createHandler.isCreating} |
28 | 24 | onClick={() => |
@@ -71,33 +67,75 @@ function RunButton({ |
71 | 67 | ); |
72 | 68 | } |
73 | 69 |
|
74 | | -function HandlerOutput({ handlerId }: { handlerId: string }) { |
| 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 }) { |
75 | 81 | // stream events and result from the workflow |
76 | | - const handler = useWorkflowHandler(handlerId); |
| 82 | + const handler = useWorkflowHandler(handlerId ?? ""); |
77 | 83 |
|
78 | 84 | // read workflow events here |
79 | | - const pongs = handler.events.filter((event) => |
80 | | - event.type.match(/PongEvent$/), |
81 | | - ) as { type: string; data: { message: string } }[]; |
| 85 | + const pongsOrResume = handler.events.filter( |
| 86 | + (event) => isPongEvent(event) || isPauseEvent(event), |
| 87 | + ) as (PongEvent | PauseEvent)[]; |
82 | 88 | const completed = handler.events.find((event) => |
83 | | - event.type.match(/WorkflowCompletedEvent$/), |
| 89 | + event.type.endsWith(".WorkflowCompletedEvent"), |
84 | 90 | ) as { type: string; data: { timestamp: string } } | undefined; |
85 | 91 |
|
86 | 92 | return ( |
87 | | - <div className="flex flex-col gap-4 w-full min-h-60"> |
| 93 | + <div className="flex flex-col gap-4 w-full min-h-60 items-center"> |
88 | 94 | <Output>{completed ? completed.data.timestamp : "Running... "}</Output> |
89 | | - {pongs.map((pong, index) => ( |
| 95 | + {pongsOrResume.map((pong, index) => ( |
90 | 96 | <span |
91 | | - className="text-black/60 dark:text-white/60 text-sm m-0" |
92 | | - key={pong.data.message} |
| 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} |
93 | 101 | style={{ |
94 | 102 | animation: "fade-in-left 80ms ease-out both", |
95 | 103 | willChange: "opacity, transform", |
96 | 104 | }} |
97 | 105 | > |
98 | | - {pong.data.message} |
| 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 | + )} |
99 | 123 | </span> |
100 | 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 | + )} |
101 | 139 | </div> |
102 | 140 | ); |
103 | 141 | } |
|
0 commit comments