@@ -3,8 +3,10 @@ import { toast } from 'sonner';
33import { supabase } from '@/integrations/supabase/client' ;
44import { ChatKit , useChatKit } from '@openai/chatkit-react' ;
55import { agentsService } from '@/services/agentsService' ;
6+ import { threadsService } from '@/services/threadsService' ;
67import { getBackendType , setBackendType , getBackendBaseUrl , getChatKitUrl , type BackendType } from '@/services/backendConfig' ;
78import type { AgentRecord } from '@/types/agent' ;
9+ import type { ThreadMetadata } from '@/types/thread' ;
810import {
911 IonPage ,
1012 IonHeader ,
@@ -25,6 +27,8 @@ const Chat = () => {
2527 const [ selectedAgent , setSelectedAgent ] = useState < AgentRecord | null > ( null ) ;
2628 const [ loadingAgents , setLoadingAgents ] = useState ( false ) ;
2729 const [ currentThreadId , setCurrentThreadId ] = useState < string | null > ( null ) ;
30+ const [ threads , setThreads ] = useState < ThreadMetadata [ ] > ( [ ] ) ;
31+ const [ loadingThreads , setLoadingThreads ] = useState ( false ) ;
2832
2933 // Settings state
3034 const [ darkMode , setDarkMode ] = useState ( true ) ;
@@ -74,29 +78,36 @@ const Chat = () => {
7478 }
7579 } ;
7680
77- // Load the most recent thread for the current user
78- const loadMostRecentThread = useCallback ( async ( ) => {
79- try {
80- const authHeaders = await getAuthHeaders ( ) ;
81- const response = await fetch ( `${ getServerBaseUrl ( ) } /threads/list?limit=1&order=desc` , {
82- method : 'GET' ,
83- headers : authHeaders ,
84- } ) ;
81+ // Load threads for the current agent
82+ const loadThreads = useCallback ( async ( skipAutoSelect : boolean = false ) => {
83+ if ( ! selectedAgent ) {
84+ setThreads ( [ ] ) ;
85+ return ;
86+ }
8587
86- if ( response . ok ) {
87- const data = await response . json ( ) ;
88- if ( data . data && data . data . length > 0 ) {
89- const mostRecentThread = data . data [ 0 ] ;
90- console . log ( 'Loading most recent thread:' , mostRecentThread . id ) ;
91- setCurrentThreadId ( mostRecentThread . id ) ;
92- return mostRecentThread . id ;
93- }
88+ try {
89+ setLoadingThreads ( true ) ;
90+ const response = await threadsService . listThreads ( backendType , 50 ) ;
91+ setThreads ( response . data ) ;
92+
93+ // Auto-select the most recent thread if available and no thread is selected
94+ // Skip auto-selection if explicitly requested (e.g., when creating a new thread)
95+ if ( ! skipAutoSelect ) {
96+ setCurrentThreadId ( ( prevThreadId ) => {
97+ if ( response . data . length > 0 && ! prevThreadId ) {
98+ return response . data [ 0 ] . id ;
99+ }
100+ return prevThreadId ;
101+ } ) ;
94102 }
95103 } catch ( error ) {
96- console . error ( 'Error loading most recent thread:' , error ) ;
104+ console . error ( 'Error loading threads:' , error ) ;
105+ toast . error ( 'Failed to load threads' ) ;
106+ setThreads ( [ ] ) ;
107+ } finally {
108+ setLoadingThreads ( false ) ;
97109 }
98- return null ;
99- } , [ backendType ] ) ;
110+ } , [ selectedAgent , backendType ] ) ;
100111
101112 // Check for existing session or sign in anonymously on component mount
102113 useEffect ( ( ) => {
@@ -112,8 +123,6 @@ const Chat = () => {
112123 // We have a valid session, use it
113124 setIsAuthenticated ( true ) ;
114125 await loadAgents ( ) ;
115- // Load the most recent thread to continue the conversation
116- await loadMostRecentThread ( ) ;
117126 } else {
118127 // No valid session, sign in anonymously
119128 const { data, error } = await supabase . auth . signInAnonymously ( ) ;
@@ -123,8 +132,6 @@ const Chat = () => {
123132 } else {
124133 setIsAuthenticated ( true ) ;
125134 await loadAgents ( ) ;
126- // Load the most recent thread to continue the conversation
127- await loadMostRecentThread ( ) ;
128135 }
129136 }
130137 } catch ( error ) {
@@ -151,14 +158,20 @@ const Chat = () => {
151158 return ( ) => {
152159 subscription . unsubscribe ( ) ;
153160 } ;
154- } , [ loadAgents , loadMostRecentThread ] ) ;
161+ } , [ loadAgents ] ) ;
155162
156- // Load agent details when selected agent changes
163+ // Load agent details and threads when selected agent changes
157164 useEffect ( ( ) => {
158165 if ( selectedAgent ?. id ) {
159166 loadAgentDetails ( selectedAgent . id ) ;
167+ // Reset thread selection when agent changes
168+ setCurrentThreadId ( null ) ;
169+ loadThreads ( ) ;
170+ } else {
171+ setThreads ( [ ] ) ;
172+ setCurrentThreadId ( null ) ;
160173 }
161- } , [ selectedAgent ?. id ] ) ;
174+ } , [ selectedAgent ?. id , loadThreads ] ) ;
162175
163176 // Handle backend type change
164177 const handleBackendChange = ( newBackendType : BackendType ) => {
@@ -195,15 +208,31 @@ const Chat = () => {
195208 const agent = agents . find ( ( a ) => a . id === agentId ) ;
196209 if ( agent ) {
197210 setSelectedAgent ( agent ) ;
211+ // Reset thread when switching agents
212+ setCurrentThreadId ( null ) ;
198213 }
199214 } ;
200215
201216 // ChatKit configuration - use a generic agents endpoint and route dynamically
202217 const chatKitUrl = getChatKitUrl ( backendType ) ;
203218
204- const { control } = useChatKit ( {
219+ const chatKitHook = useChatKit ( {
205220 onThreadChange : ( { threadId } ) => {
206- setCurrentThreadId ( threadId ) ;
221+ // Update thread ID when ChatKit changes it (e.g., when a new message creates a thread)
222+ setCurrentThreadId ( ( prevThreadId ) => {
223+ // Only update if threadId actually changed (avoid infinite loops)
224+ if ( threadId !== prevThreadId ) {
225+ // Reload threads when a new thread is created, but don't auto-select
226+ // since we're already on the new thread
227+ if ( threadId ) {
228+ setTimeout ( ( ) => {
229+ loadThreads ( true ) ; // Skip auto-selection since we're already on this thread
230+ } , 500 ) ;
231+ }
232+ return threadId ;
233+ }
234+ return prevThreadId ;
235+ } ) ;
207236 } ,
208237 onClientTool : async ( invocation ) => {
209238 if ( invocation . name === "switch_theme" ) {
@@ -268,25 +297,11 @@ const Chat = () => {
268297 placeholder : `Message your ${ selectedAgent ?. name } AI agent...` ,
269298 // tools: [{ id: "rate", label: "Rate", icon: "star", pinned: true }],
270299 } ,
300+ history : {
301+ enabled : false , // Disable built-in history management
302+ } ,
271303 header : {
272- leftAction : {
273- icon : 'sidebar-left' ,
274- onClick : async ( ) => {
275- // Open the left settings menu
276- if ( leftMenuRef . current ) {
277- await leftMenuRef . current . open ( ) ;
278- }
279- } ,
280- } ,
281- // rightAction: {
282- // icon: 'sidebar-right',
283- // onClick: async () => {
284- // // Open the right menu
285- // if (rightMenuRef.current) {
286- // await rightMenuRef.current.open();
287- // }
288- // },
289- // },
304+ enabled : false , // Disable built-in header to use our custom one
290305 } ,
291306 startScreen : {
292307 // greeting: selectedAgent ? `Welcome to Timestep AI! You're chatting with ${selectedAgent.name}` : "Welcome to Timestep AI!",
@@ -308,6 +323,35 @@ const Chat = () => {
308323 } ,
309324 } ) ;
310325
326+ // Extract control and setThreadId from the hook
327+ const { control, setThreadId } = chatKitHook ;
328+
329+ // Handle thread switching
330+ const handleThreadChange = ( threadId : string | null ) => {
331+ // Just update state - the useEffect will handle calling setThreadId
332+ setCurrentThreadId ( threadId ) ;
333+ } ;
334+
335+ // Handle creating a new thread
336+ const handleCreateNewThread = ( ) => {
337+ // Just update state - the useEffect will handle calling setThreadId
338+ setCurrentThreadId ( null ) ;
339+ // Reload threads to get the new one when it's created, but skip auto-selection
340+ // so we don't automatically switch away from the new thread
341+ setTimeout ( ( ) => {
342+ loadThreads ( true ) ; // Skip auto-selection
343+ } , 1000 ) ;
344+ } ;
345+
346+ // Set initial thread when component mounts or thread changes
347+ // Only call setThreadId when we have a valid thread ID (not null) or when explicitly setting to null for new thread
348+ useEffect ( ( ) => {
349+ if ( setThreadId && currentThreadId !== undefined && selectedAgent ) {
350+ // Only set thread ID if we have a valid thread ID or explicitly want to create a new thread
351+ setThreadId ( currentThreadId ) ;
352+ }
353+ } , [ setThreadId , currentThreadId , selectedAgent ] ) ;
354+
311355 if ( ! isAuthenticated || loadingAgents ) {
312356 return (
313357 < IonPage >
@@ -368,8 +412,13 @@ const Chat = () => {
368412 backendType = { backendType }
369413 selectedAgent = { selectedAgent }
370414 agents = { agents }
415+ threads = { threads }
416+ selectedThreadId = { currentThreadId }
417+ loadingThreads = { loadingThreads }
371418 onBackendChange = { handleBackendChange }
372419 onAgentChange = { handleAgentChange }
420+ onThreadChange = { handleThreadChange }
421+ onCreateNewThread = { handleCreateNewThread }
373422 />
374423 </ IonButtons >
375424 < IonTitle > Timestep AI</ IonTitle >
0 commit comments