11import { ArrowLeftIcon , RepeatIcon } from "@phosphor-icons/react" ;
22import type { LoopSchemas } from "@posthog/api-client/loops" ;
3- import { Switch } from "@posthog/quill" ;
3+ import {
4+ AlertDialog ,
5+ AlertDialogClose ,
6+ AlertDialogContent ,
7+ AlertDialogDescription ,
8+ AlertDialogFooter ,
9+ AlertDialogHeader ,
10+ AlertDialogTitle ,
11+ Badge ,
12+ Button ,
13+ Switch ,
14+ Textarea ,
15+ } from "@posthog/quill" ;
416import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent" ;
5- import { Badge } from "@posthog/ui/primitives/Badge" ;
6- import { Button } from "@posthog/ui/primitives/Button" ;
717import { toast } from "@posthog/ui/primitives/toast" ;
818import {
919 navigateToEditLoop ,
1020 navigateToLoops ,
1121} from "@posthog/ui/router/navigationBridge" ;
12- import { AlertDialog , Flex , Text } from "@radix-ui/themes" ;
13- import { useState } from "react" ;
22+ import { Flex , Text } from "@radix-ui/themes" ;
23+ import { useRef , useState } from "react" ;
1424import { useLoop } from "../hooks/useLoop" ;
1525import { useLoopDisplayModel } from "../hooks/useLoopDisplayModel" ;
1626import {
@@ -102,24 +112,25 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
102112 < div className = "mx-auto w-full max-w-5xl px-8 py-6" >
103113 < Flex direction = "column" gap = "5" >
104114 < Flex direction = "column" gap = "3" >
105- < button
106- type = "button"
115+ < Button
116+ variant = "link-muted"
117+ size = "xs"
107118 onClick = { navigateToLoops }
108- className = "flex w-fit items-center gap-1.5 border-none bg-transparent p-0 text-[12px] text-gray-11 no-underline hover:text-gray-12 "
119+ className = "w-fit px-0 "
109120 >
110121 < ArrowLeftIcon size = { 13 } />
111122 Loops
112- </ button >
123+ </ Button >
113124
114125 < Flex align = "center" justify = "between" gap = "3" wrap = "wrap" >
115126 < Flex align = "center" gap = "2" wrap = "wrap" >
116127 < Text className = "font-bold text-[22px] text-gray-12 leading-tight tracking-tight" >
117128 { loop . name }
118129 </ Text >
119- < Badge color = { loopStatusColor ( loop ) } >
130+ < Badge variant = { loopStatusBadgeVariant ( loop ) } >
120131 { loopStatusLabel ( loop ) }
121132 </ Badge >
122- < Badge color = "gray" > { loop . visibility } </ Badge >
133+ < Badge > { loop . visibility } </ Badge >
123134 </ Flex >
124135 < Flex align = "center" gap = "2" >
125136 < Switch
@@ -129,27 +140,24 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
129140 onCheckedChange = { handleToggleEnabled }
130141 />
131142 < Button
132- variant = "soft"
133- color = "gray"
134- size = "1"
143+ variant = "outline"
144+ size = "xs"
135145 loading = { runLoop . isPending }
136146 disabled = { runLoop . isPending }
137147 onClick = { handleRunNow }
138148 >
139149 Run now
140150 </ Button >
141151 < Button
142- variant = "soft"
143- color = "gray"
144- size = "1"
152+ variant = "outline"
153+ size = "xs"
145154 onClick = { ( ) => navigateToEditLoop ( loop . id ) }
146155 >
147156 Edit
148157 </ Button >
149158 < Button
150- variant = "soft"
151- color = "red"
152- size = "1"
159+ variant = "destructive"
160+ size = "xs"
153161 onClick = { ( ) => setDeleteOpen ( true ) }
154162 >
155163 Delete
@@ -166,6 +174,8 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
166174
167175 < ConfigSummarySection loop = { loop } />
168176
177+ < InstructionsSection loop = { loop } />
178+
169179 < Flex direction = "column" gap = "2" >
170180 < Flex align = "center" gap = "2" >
171181 < Text className = "font-medium text-[13px] text-gray-12" >
@@ -202,41 +212,51 @@ export function LoopDetailView({ loopId }: { loopId: string }) {
202212 </ Flex >
203213 </ Flex >
204214
205- < AlertDialog . Root open = { deleteOpen } onOpenChange = { setDeleteOpen } >
206- < AlertDialog . Content maxWidth = "420px" size = "1" >
207- < AlertDialog . Title className = "text-sm" > Delete loop</ AlertDialog . Title >
208- < AlertDialog . Description className = "text-[13px]" >
209- < Text color = "gray" className = "text-[13px]" >
210- Permanently delete{ " " }
211- < Text className = "font-medium text-[13px]" > { loop . name } </ Text > ? This
212- stops every trigger and cannot be undone.
213- </ Text >
214- </ AlertDialog . Description >
215- < Flex justify = "end" gap = "3" mt = "3" >
216- < AlertDialog . Cancel >
217- < Button variant = "soft" color = "gray" size = "1" >
218- Cancel
219- </ Button >
220- </ AlertDialog . Cancel >
221- < AlertDialog . Action >
222- < Button
223- variant = "solid"
224- color = "red"
225- size = "1"
226- loading = { deleteLoop . isPending }
227- disabled = { deleteLoop . isPending }
228- onClick = { handleDelete }
229- >
230- Delete
231- </ Button >
232- </ AlertDialog . Action >
233- </ Flex >
234- </ AlertDialog . Content >
235- </ AlertDialog . Root >
215+ < AlertDialog open = { deleteOpen } onOpenChange = { setDeleteOpen } >
216+ < AlertDialogContent className = "max-w-md" >
217+ < AlertDialogHeader >
218+ < AlertDialogTitle > Delete loop</ AlertDialogTitle >
219+ < AlertDialogDescription >
220+ < Text color = "gray" className = "text-[13px]" >
221+ Permanently delete{ " " }
222+ < Text className = "font-medium text-[13px]" > { loop . name } </ Text > ?
223+ This stops every trigger and cannot be undone.
224+ </ Text >
225+ </ AlertDialogDescription >
226+ </ AlertDialogHeader >
227+ < AlertDialogFooter >
228+ < AlertDialogClose
229+ render = {
230+ < Button variant = "outline" size = "sm" >
231+ Cancel
232+ </ Button >
233+ }
234+ />
235+ < Button
236+ variant = "destructive"
237+ size = "sm"
238+ loading = { deleteLoop . isPending }
239+ disabled = { deleteLoop . isPending }
240+ onClick = { handleDelete }
241+ >
242+ Delete
243+ </ Button >
244+ </ AlertDialogFooter >
245+ </ AlertDialogContent >
246+ </ AlertDialog >
236247 </ div >
237248 ) ;
238249}
239250
251+ function loopStatusBadgeVariant (
252+ loop : LoopSchemas . Loop ,
253+ ) : "default" | "destructive" | "success" {
254+ const color = loopStatusColor ( loop ) ;
255+ if ( color === "green" ) return "success" ;
256+ if ( color === "red" ) return "destructive" ;
257+ return "default" ;
258+ }
259+
240260function ConfigSummarySection ( { loop } : { loop : LoopSchemas . Loop } ) {
241261 const displayModel = useLoopDisplayModel ( loop . runtime_adapter , loop . model ) ;
242262
@@ -281,13 +301,74 @@ function ConfigSummarySection({ loop }: { loop: LoopSchemas.Loop }) {
281301 </ Flex >
282302 ) }
283303 </ SummaryRow >
304+ </ Flex >
305+ </ Flex >
306+ ) ;
307+ }
284308
285- < SummaryRow label = "Instructions" >
286- < pre className = "max-h-[200px] overflow-auto whitespace-pre-wrap text-[12px] text-gray-12 [font-family:var(--font-mono)]" >
287- { loop . instructions }
288- </ pre >
289- </ SummaryRow >
309+ function InstructionsSection ( { loop } : { loop : LoopSchemas . Loop } ) {
310+ const updateLoop = useUpdateLoop ( loop . id ) ;
311+ const [ draft , setDraft ] = useState < string | null > ( null ) ;
312+ // Escape reverts and blurs; skip the resulting onBlur save.
313+ const skipCommit = useRef ( false ) ;
314+
315+ const commit = ( value : string ) => {
316+ if ( skipCommit . current ) {
317+ skipCommit . current = false ;
318+ return ;
319+ }
320+ const trimmed = value . trim ( ) ;
321+ if ( ! trimmed ) {
322+ setDraft ( null ) ;
323+ return ;
324+ }
325+ if ( updateLoop . isPending ) return ;
326+ if ( trimmed === loop . instructions . trim ( ) ) {
327+ setDraft ( null ) ;
328+ return ;
329+ }
330+ updateLoop . mutate (
331+ { instructions : trimmed } ,
332+ {
333+ onSuccess : ( ) => {
334+ setDraft ( null ) ;
335+ toast . success ( "Instructions updated" ) ;
336+ } ,
337+ onError : ( error ) => {
338+ setDraft ( null ) ;
339+ toast . error ( "Failed to update instructions" , {
340+ description : error . message ,
341+ } ) ;
342+ } ,
343+ } ,
344+ ) ;
345+ } ;
346+
347+ return (
348+ < Flex direction = "column" gap = "3" >
349+ < Flex align = "center" gap = "2" >
350+ < Text className = "font-medium text-[13px] text-gray-12" >
351+ Instructions
352+ </ Text >
353+ { updateLoop . isPending ? (
354+ < Text className = "text-[11px] text-gray-10" > Saving…</ Text >
355+ ) : null }
290356 </ Flex >
357+ < Textarea
358+ value = { draft ?? loop . instructions }
359+ disabled = { updateLoop . isPending }
360+ aria-label = "Loop instructions"
361+ className = "min-h-[200px] bg-(--color-panel-solid) text-[12.5px] leading-relaxed"
362+ onChange = { ( e ) => setDraft ( e . currentTarget . value ) }
363+ onBlur = { ( e ) => commit ( e . currentTarget . value ) }
364+ onKeyDown = { ( e ) => {
365+ if ( e . key === "Escape" ) {
366+ skipCommit . current = true ;
367+ setDraft ( null ) ;
368+ e . currentTarget . blur ( ) ;
369+ }
370+ } }
371+ />
291372 </ Flex >
292373 ) ;
293374}
0 commit comments