1- import { ArrowDownTrayIcon , ArrowsPointingInIcon , ArrowsPointingOutIcon , ArrowTrendingUpIcon , ClipboardIcon } from "@heroicons/react/20/solid" ;
1+ import { ArrowDownTrayIcon , ArrowsPointingInIcon , ArrowsPointingOutIcon , ArrowTrendingUpIcon , ClipboardIcon , TableCellsIcon } from "@heroicons/react/20/solid" ;
2+ import { useFetcher } from "@remix-run/react" ;
3+ import type { action as titleAction } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-title" ;
24import type { OutputColumnMetadata , WhereClauseFallback } from "@internal/clickhouse" ;
35import {
46 redirect ,
@@ -374,12 +376,23 @@ const QueryEditorForm = forwardRef<
374376 history : QueryHistoryItem [ ] ;
375377 fetcher : ReturnType < typeof useTypedFetcher < typeof action > > ;
376378 isAdmin : boolean ;
379+ onQuerySubmit ?: ( ) => void ;
380+ onHistorySelected ?: ( item : QueryHistoryItem ) => void ;
377381 }
378- > ( function QueryEditorForm ( { defaultQuery, defaultScope, defaultTimeFilter, history, fetcher, isAdmin } , ref ) {
382+ > ( function QueryEditorForm ( { defaultQuery, defaultScope, defaultTimeFilter, history, fetcher, isAdmin, onQuerySubmit , onHistorySelected } , ref ) {
379383 const isLoading = fetcher . state === "submitting" || fetcher . state === "loading" ;
380384 const [ query , setQuery ] = useState ( defaultQuery ) ;
381385 const [ scope , setScope ] = useState < QueryScope > ( defaultScope ) ;
382386 const formRef = useRef < HTMLFormElement > ( null ) ;
387+ const prevFetcherState = useRef ( fetcher . state ) ;
388+
389+ // Notify parent when query is submitted (for title generation)
390+ useEffect ( ( ) => {
391+ if ( prevFetcherState . current !== "submitting" && fetcher . state === "submitting" ) {
392+ onQuerySubmit ?.( ) ;
393+ }
394+ prevFetcherState . current = fetcher . state ;
395+ } , [ fetcher . state , onQuerySubmit ] ) ;
383396
384397 // Get time filter values - initialize from props (which may come from history)
385398 const [ period , setPeriod ] = useState < string | undefined > ( defaultTimeFilter ?. period ) ;
@@ -414,7 +427,9 @@ const QueryEditorForm = forwardRef<
414427 setPeriod ( item . filterPeriod ?? undefined ) ;
415428 setFrom ( item . filterFrom ? toISOString ( item . filterFrom ) : undefined ) ;
416429 setTo ( item . filterTo ? toISOString ( item . filterTo ) : undefined ) ;
417- } , [ ] ) ;
430+ // Notify parent about history selection (for title)
431+ onHistorySelected ?.( item ) ;
432+ } , [ onHistorySelected ] ) ;
418433
419434 return (
420435 < div className = "flex h-full flex-col gap-2 bg-charcoal-900 pb-2" >
@@ -514,6 +529,10 @@ export default function Page() {
514529 const results = fetcher . data ;
515530 const { replace : replaceSearchParams } = useSearchParams ( ) ;
516531
532+ const organization = useOrganization ( ) ;
533+ const project = useProject ( ) ;
534+ const environment = useEnvironment ( ) ;
535+
517536 // Use most recent history item if available, otherwise fall back to defaults
518537 const initialQuery = history . length > 0 ? history [ 0 ] . query : defaultQuery ;
519538 const initialScope : QueryScope = history . length > 0 ? history [ 0 ] . scope : "environment" ;
@@ -533,6 +552,44 @@ export default function Page() {
533552 const [ sidebarTab , setSidebarTab ] = useState < string > ( "ai" ) ;
534553 const [ aiFixRequest , setAiFixRequest ] = useState < { prompt : string ; key : number } | null > ( null ) ;
535554
555+ // Title generation state
556+ const titleFetcher = useFetcher < typeof titleAction > ( ) ;
557+ const isTitleLoading = titleFetcher . state !== "idle" ;
558+ const generatedTitle = titleFetcher . data ?. title ;
559+ const [ historyTitle , setHistoryTitle ] = useState < string | null > (
560+ history . length > 0 ? history [ 0 ] . title ?? null : null
561+ ) ;
562+
563+ // Effective title: history title takes precedence, then generated
564+ const queryTitle = historyTitle ?? generatedTitle ?? null ;
565+
566+ // Track whether we should generate a title for the current results
567+ const [ shouldGenerateTitle , setShouldGenerateTitle ] = useState ( false ) ;
568+
569+ // Trigger title generation when query succeeds (only for new queries, not history)
570+ useEffect ( ( ) => {
571+ if (
572+ results ?. rows &&
573+ ! results . error &&
574+ shouldGenerateTitle &&
575+ ! historyTitle &&
576+ titleFetcher . state === "idle"
577+ ) {
578+ const currentQuery = editorRef . current ?. getQuery ( ) ;
579+ if ( currentQuery ) {
580+ titleFetcher . submit (
581+ { query : currentQuery } ,
582+ {
583+ method : "POST" ,
584+ action : `/resources/orgs/${ organization . slug } /projects/${ project . slug } /env/${ environment . slug } /query/ai-title` ,
585+ encType : "application/json" ,
586+ }
587+ ) ;
588+ setShouldGenerateTitle ( false ) ;
589+ }
590+ }
591+ } , [ results , shouldGenerateTitle , historyTitle , titleFetcher , organization . slug , project . slug , environment . slug ] ) ;
592+
536593 const handleTryFixError = useCallback ( ( errorMessage : string ) => {
537594 setSidebarTab ( "ai" ) ;
538595 setAiFixRequest ( ( prev ) => ( {
@@ -575,6 +632,18 @@ export default function Page() {
575632 setChartConfig ( config ) ;
576633 } , [ ] ) ;
577634
635+ // Handle query submission - prepare for title generation
636+ const handleQuerySubmit = useCallback ( ( ) => {
637+ setHistoryTitle ( null ) ; // Clear history title when running a new query
638+ setShouldGenerateTitle ( true ) ; // Enable title generation for new results
639+ } , [ ] ) ;
640+
641+ // Handle history selection - use existing title if available
642+ const handleHistorySelected = useCallback ( ( item : QueryHistoryItem ) => {
643+ setHistoryTitle ( item . title ?? null ) ;
644+ setShouldGenerateTitle ( false ) ; // Don't generate title for history items
645+ } , [ ] ) ;
646+
578647 return (
579648 < PageContainer >
580649 < NavBar >
@@ -594,6 +663,8 @@ export default function Page() {
594663 history = { history }
595664 fetcher = { fetcher }
596665 isAdmin = { isAdmin }
666+ onQuerySubmit = { handleQuerySubmit }
667+ onHistorySelected = { handleHistorySelected }
597668 />
598669 </ ResizablePanel >
599670 < ResizableHandle id = "query-editor-handle" />
@@ -701,7 +772,19 @@ export default function Page() {
701772 </ Callout >
702773 ) }
703774 < div className = "h-full bg-charcoal-900 p-2" >
704- < Card className = "h-full overflow-hidden p-0" >
775+ < Card className = "h-full overflow-hidden px-0 pb-0" >
776+ < Card . Header >
777+ < div className = "flex items-center gap-1.5" >
778+ < TableCellsIcon className = "size-5 text-indigo-500" />
779+ { isTitleLoading ? (
780+ < span className = "flex items-center gap-2 text-text-dimmed" >
781+ < Spinner className = "size-3" /> Generating title...
782+ </ span >
783+ ) : (
784+ queryTitle ?? "Results"
785+ ) }
786+ </ div >
787+ </ Card . Header >
705788 < Card . Content className = "min-h-0 flex-1 overflow-hidden p-0" >
706789 < TSQLResultsTable
707790 rows = { results . rows }
@@ -728,6 +811,8 @@ export default function Page() {
728811 columns = { results . columns }
729812 chartConfig = { chartConfig }
730813 onChartConfigChange = { handleChartConfigChange }
814+ queryTitle = { queryTitle }
815+ isTitleLoading = { isTitleLoading }
731816 />
732817 ) : (
733818 < Paragraph variant = "small" className = "p-4 text-text-dimmed" >
@@ -853,14 +938,26 @@ function ResultsChart({
853938 columns,
854939 chartConfig,
855940 onChartConfigChange,
941+ queryTitle,
942+ isTitleLoading,
856943} : {
857944 rows : Record < string , unknown > [ ] ;
858945 columns : OutputColumnMetadata [ ] ;
859946 chartConfig : ChartConfiguration ;
860947 onChartConfigChange : ( config : ChartConfiguration ) => void ;
948+ queryTitle : string | null ;
949+ isTitleLoading : boolean ;
861950} ) {
862951 const [ isOpen , setIsOpen ] = useState ( false ) ;
863952
953+ const titleContent = isTitleLoading ? (
954+ < span className = "flex items-center gap-2 text-text-dimmed" >
955+ < Spinner className = "size-3" /> Generating title...
956+ </ span >
957+ ) : (
958+ queryTitle ?? "Chart"
959+ ) ;
960+
864961 return (
865962 < > < ResizablePanelGroup className = "h-full overflow-hidden" >
866963 < ResizablePanel id = "chart-results" >
@@ -869,7 +966,7 @@ function ResultsChart({
869966 < Card . Header >
870967 < div className = "flex items-center gap-1.5" >
871968 < ArrowTrendingUpIcon className = "size-5 text-indigo-500" />
872- Chart
969+ { titleContent }
873970 </ div >
874971 < Card . Accessory >
875972 < Button variant = "minimal/small" LeadingIcon = { ArrowsPointingOutIcon } onClick = { ( ) => setIsOpen ( true ) } />
@@ -890,7 +987,7 @@ function ResultsChart({
890987 < Dialog open = { isOpen } onOpenChange = { setIsOpen } >
891988 < DialogContent fullscreen >
892989 < DialogHeader >
893- Chart
990+ { queryTitle ?? " Chart" }
894991 </ DialogHeader >
895992 < div className = "h-full min-h-0 flex-1 overflow-hidden w-full pt-4" >
896993 < QueryResultsChart rows = { rows } columns = { columns } config = { chartConfig } fullLegend = { true } />
0 commit comments