1+ import { PencilSquareIcon , SparklesIcon } from "@heroicons/react/20/solid" ;
12import { AnimatePresence , motion } from "framer-motion" ;
23import { Suspense , lazy , useCallback , useEffect , useRef , useState } from "react" ;
34import { AISparkleIcon } from "~/assets/icons/AISparkleIcon" ;
@@ -17,22 +18,31 @@ import { Spinner } from "~/components/primitives/Spinner";
1718import { useEnvironment } from "~/hooks/useEnvironment" ;
1819import { useOrganization } from "~/hooks/useOrganizations" ;
1920import { useProject } from "~/hooks/useProject" ;
21+ import { cn } from "~/utils/cn" ;
2022
2123type StreamEventType =
2224 | { type : "thinking" ; content : string }
2325 | { type : "tool_call" ; tool : string ; args : unknown }
24- | { type : "tool_result" ; tool : string ; result : unknown }
2526 | { type : "result" ; success : true ; query : string }
2627 | { type : "result" ; success : false ; error : string } ;
2728
29+ export type AIQueryMode = "new" | "edit" ;
30+
2831interface AIQueryInputProps {
2932 onQueryGenerated : ( query : string ) => void ;
3033 /** Set this to a prompt to auto-populate and immediately submit */
3134 autoSubmitPrompt ?: string ;
35+ /** The current query in the editor (used for edit mode) */
36+ currentQuery ?: string ;
3237}
3338
34- export function AIQueryInput ( { onQueryGenerated, autoSubmitPrompt } : AIQueryInputProps ) {
39+ export function AIQueryInput ( {
40+ onQueryGenerated,
41+ autoSubmitPrompt,
42+ currentQuery,
43+ } : AIQueryInputProps ) {
3544 const [ prompt , setPrompt ] = useState ( "" ) ;
45+ const [ mode , setMode ] = useState < AIQueryMode > ( "new" ) ;
3646 const [ isLoading , setIsLoading ] = useState ( false ) ;
3747 const [ thinking , setThinking ] = useState ( "" ) ;
3848 const [ error , setError ] = useState < string | null > ( null ) ;
@@ -48,9 +58,20 @@ export function AIQueryInput({ onQueryGenerated, autoSubmitPrompt }: AIQueryInpu
4858
4959 const resourcePath = `/resources/orgs/${ organization . slug } /projects/${ project . slug } /env/${ environment . slug } /query/ai-generate` ;
5060
61+ // Can only use edit mode if there's a current query
62+ const canEdit = Boolean ( currentQuery ?. trim ( ) ) ;
63+
64+ // If mode is edit but there's no current query, switch to new
65+ useEffect ( ( ) => {
66+ if ( mode === "edit" && ! canEdit ) {
67+ setMode ( "new" ) ;
68+ }
69+ } , [ mode , canEdit ] ) ;
70+
5171 const submitQuery = useCallback (
52- async ( queryPrompt : string ) => {
72+ async ( queryPrompt : string , submitMode : AIQueryMode = mode ) => {
5373 if ( ! queryPrompt . trim ( ) || isLoading ) return ;
74+ if ( submitMode === "edit" && ! currentQuery ?. trim ( ) ) return ;
5475
5576 setIsLoading ( true ) ;
5677 setThinking ( "" ) ;
@@ -67,6 +88,10 @@ export function AIQueryInput({ onQueryGenerated, autoSubmitPrompt }: AIQueryInpu
6788 try {
6889 const formData = new FormData ( ) ;
6990 formData . append ( "prompt" , queryPrompt ) ;
91+ formData . append ( "mode" , submitMode ) ;
92+ if ( submitMode === "edit" && currentQuery ) {
93+ formData . append ( "currentQuery" , currentQuery ) ;
94+ }
7095
7196 const response = await fetch ( resourcePath , {
7297 method : "POST" ,
@@ -135,7 +160,7 @@ export function AIQueryInput({ onQueryGenerated, autoSubmitPrompt }: AIQueryInpu
135160 setIsLoading ( false ) ;
136161 }
137162 } ,
138- [ isLoading , resourcePath ]
163+ [ isLoading , resourcePath , mode , currentQuery ]
139164 ) ;
140165
141166 const processStreamEvent = useCallback (
@@ -147,9 +172,6 @@ export function AIQueryInput({ onQueryGenerated, autoSubmitPrompt }: AIQueryInpu
147172 case "tool_call" :
148173 setThinking ( ( prev ) => prev + `\nValidating query...\n` ) ;
149174 break ;
150- case "tool_result" :
151- // Optionally show validation result
152- break ;
153175 case "result" :
154176 if ( event . success ) {
155177 onQueryGenerated ( event . query ) ;
@@ -217,7 +239,11 @@ export function AIQueryInput({ onQueryGenerated, autoSubmitPrompt }: AIQueryInpu
217239 < textarea
218240 ref = { textareaRef }
219241 name = "prompt"
220- placeholder = "e.g. show me failed runs from the last 7 days"
242+ placeholder = {
243+ mode === "edit"
244+ ? "e.g. add a filter for failed runs, change the limit to 50"
245+ : "e.g. show me failed runs from the last 7 days"
246+ }
221247 value = { prompt }
222248 onChange = { ( e ) => setPrompt ( e . target . value ) }
223249 disabled = { isLoading }
@@ -231,16 +257,50 @@ export function AIQueryInput({ onQueryGenerated, autoSubmitPrompt }: AIQueryInpu
231257 } }
232258 />
233259 < div className = "flex justify-end gap-2 px-2 pb-2" >
234- < Button
235- type = "submit"
236- variant = "tertiary/small"
237- disabled = { isLoading || ! prompt . trim ( ) }
238- LeadingIcon = { isLoading ? Spinner : AISparkleIcon }
239- className = "pl-1.5"
240- iconSpacing = "gap-1.5"
241- >
242- { isLoading ? "Generating" : "Generate" }
243- </ Button >
260+ { isLoading ? (
261+ < Button
262+ type = "button"
263+ variant = "tertiary/small"
264+ disabled = { true }
265+ LeadingIcon = { Spinner }
266+ className = "pl-1.5"
267+ iconSpacing = "gap-1.5"
268+ >
269+ { mode === "edit" ? "Editing..." : "Generating..." }
270+ </ Button >
271+ ) : (
272+ < >
273+ < Button
274+ type = "button"
275+ variant = "tertiary/small"
276+ disabled = { ! prompt . trim ( ) }
277+ LeadingIcon = { SparklesIcon }
278+ className = "pl-1.5"
279+ iconSpacing = "gap-1.5"
280+ onClick = { ( ) => {
281+ setMode ( "new" ) ;
282+ submitQuery ( prompt , "new" ) ;
283+ } }
284+ >
285+ New query
286+ </ Button >
287+ < Button
288+ type = "button"
289+ variant = "tertiary/small"
290+ disabled = { ! prompt . trim ( ) || ! canEdit }
291+ LeadingIcon = { PencilSquareIcon }
292+ className = { cn ( "pl-1.5" , ! canEdit && "opacity-50" ) }
293+ iconSpacing = "gap-1.5"
294+ tooltip = { ! canEdit ? "Write a query first to enable editing" : undefined }
295+ onClick = { ( ) => {
296+ setMode ( "edit" ) ;
297+ submitQuery ( prompt , "edit" ) ;
298+ } }
299+ >
300+ Edit query
301+ </ Button >
302+ </ >
303+ ) }
244304 </ div >
245305 </ form >
246306 </ div >
0 commit comments