1
+ import React , { useState } from "react" ;
2
+ import {
3
+ useExternalStoreRuntime ,
4
+ ThreadMessageLike ,
5
+ AppendMessage ,
6
+ AssistantRuntimeProvider ,
7
+ ExternalStoreThreadListAdapter ,
8
+ } from "@assistant-ui/react" ;
9
+ import { useThreadContext , MyMessage , ThreadProvider } from "./context/ThreadContext" ;
10
+ import { Thread } from "./assistant-ui/thread" ;
11
+ import { ThreadList } from "./assistant-ui/thread-list" ;
12
+
13
+ // Define thread data interfaces to match ExternalStoreThreadData requirements
14
+ interface RegularThreadData {
15
+ threadId : string ;
16
+ status : "regular" ;
17
+ title : string ;
18
+ }
19
+
20
+ interface ArchivedThreadData {
21
+ threadId : string ;
22
+ status : "archived" ;
23
+ title : string ;
24
+ }
25
+
26
+ type ThreadData = RegularThreadData | ArchivedThreadData ;
27
+
28
+ const generateId = ( ) => Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) ;
29
+
30
+ const callYourAPI = async ( text : string ) => {
31
+ // Simulate API delay
32
+ await new Promise ( resolve => setTimeout ( resolve , 1500 ) ) ;
33
+
34
+ // Simple responses
35
+ return {
36
+ content : "This is a mock response from your backend. You typed: " + text
37
+ } ;
38
+ } ;
39
+
40
+ function ChatWithThreads ( ) {
41
+ const { currentThreadId, setCurrentThreadId, threads, setThreads } =
42
+ useThreadContext ( ) ;
43
+ const [ isRunning , setIsRunning ] = useState ( false ) ;
44
+ const [ threadList , setThreadList ] = useState < ThreadData [ ] > ( [
45
+ { threadId : "default" , status : "regular" , title : "New Chat" } as RegularThreadData ,
46
+ ] ) ;
47
+
48
+ // Get messages for current thread
49
+ const currentMessages = threads . get ( currentThreadId ) || [ ] ;
50
+
51
+ // Convert custom format to ThreadMessageLike
52
+ const convertMessage = ( message : MyMessage ) : ThreadMessageLike => ( {
53
+ role : message . role ,
54
+ content : [ { type : "text" , text : message . text } ] ,
55
+ id : message . id ,
56
+ createdAt : new Date ( message . timestamp ) ,
57
+ } ) ;
58
+
59
+ const onNew = async ( message : AppendMessage ) => {
60
+ // Extract text from AppendMessage content array
61
+ if ( message . content . length !== 1 || message . content [ 0 ] ?. type !== "text" ) {
62
+ throw new Error ( "Only text content is supported" ) ;
63
+ }
64
+
65
+ // Add user message in custom format
66
+ const userMessage : MyMessage = {
67
+ id : generateId ( ) ,
68
+ role : "user" ,
69
+ text : message . content [ 0 ] . text ,
70
+ timestamp : Date . now ( ) ,
71
+ } ;
72
+
73
+ // Update current thread with new user message
74
+ const updatedMessages = [ ...currentMessages , userMessage ] ;
75
+ setThreads ( prev => new Map ( prev ) . set ( currentThreadId , updatedMessages ) ) ;
76
+ setIsRunning ( true ) ;
77
+
78
+ try {
79
+ // Call mock API
80
+ const response = await callYourAPI ( userMessage . text ) ;
81
+
82
+ const assistantMessage : MyMessage = {
83
+ id : generateId ( ) ,
84
+ role : "assistant" ,
85
+ text : response . content ,
86
+ timestamp : Date . now ( ) ,
87
+ } ;
88
+
89
+ // Update current thread with assistant response
90
+ const finalMessages = [ ...updatedMessages , assistantMessage ] ;
91
+ setThreads ( prev => new Map ( prev ) . set ( currentThreadId , finalMessages ) ) ;
92
+ } catch ( error ) {
93
+ // Handle errors gracefully
94
+ const errorMessage : MyMessage = {
95
+ id : generateId ( ) ,
96
+ role : "assistant" ,
97
+ text : `Sorry, I encountered an error: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
98
+ timestamp : Date . now ( ) ,
99
+ } ;
100
+
101
+ const finalMessages = [ ...updatedMessages , errorMessage ] ;
102
+ setThreads ( prev => new Map ( prev ) . set ( currentThreadId , finalMessages ) ) ;
103
+ } finally {
104
+ setIsRunning ( false ) ;
105
+ }
106
+ } ;
107
+
108
+ // Add onEdit functionality
109
+ const onEdit = async ( message : AppendMessage ) => {
110
+ // Extract text from AppendMessage content array
111
+ if ( message . content . length !== 1 || message . content [ 0 ] ?. type !== "text" ) {
112
+ throw new Error ( "Only text content is supported" ) ;
113
+ }
114
+
115
+ // Find the index where to insert the edited message
116
+ const index = currentMessages . findIndex ( ( m ) => m . id === message . parentId ) + 1 ;
117
+
118
+ // Keep messages up to the parent
119
+ const newMessages = [ ...currentMessages . slice ( 0 , index ) ] ;
120
+
121
+ // Add the edited message in custom format
122
+ const editedMessage : MyMessage = {
123
+ id : generateId ( ) ,
124
+ role : "user" ,
125
+ text : message . content [ 0 ] . text ,
126
+ timestamp : Date . now ( ) ,
127
+ } ;
128
+ newMessages . push ( editedMessage ) ;
129
+
130
+ setThreads ( prev => new Map ( prev ) . set ( currentThreadId , newMessages ) ) ;
131
+ setIsRunning ( true ) ;
132
+
133
+ try {
134
+ // Generate new response
135
+ const response = await callYourAPI ( editedMessage . text ) ;
136
+
137
+ const assistantMessage : MyMessage = {
138
+ id : generateId ( ) ,
139
+ role : "assistant" ,
140
+ text : response . content ,
141
+ timestamp : Date . now ( ) ,
142
+ } ;
143
+
144
+ newMessages . push ( assistantMessage ) ;
145
+ setThreads ( prev => new Map ( prev ) . set ( currentThreadId , newMessages ) ) ;
146
+ } catch ( error ) {
147
+ // Handle errors gracefully
148
+ const errorMessage : MyMessage = {
149
+ id : generateId ( ) ,
150
+ role : "assistant" ,
151
+ text : `Sorry, I encountered an error: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
152
+ timestamp : Date . now ( ) ,
153
+ } ;
154
+
155
+ newMessages . push ( errorMessage ) ;
156
+ setThreads ( prev => new Map ( prev ) . set ( currentThreadId , newMessages ) ) ;
157
+ } finally {
158
+ setIsRunning ( false ) ;
159
+ }
160
+ } ;
161
+
162
+ // Thread list adapter for managing multiple threads
163
+ const threadListAdapter : ExternalStoreThreadListAdapter = {
164
+ threadId : currentThreadId ,
165
+ threads : threadList . filter ( ( t ) : t is RegularThreadData => t . status === "regular" ) ,
166
+ archivedThreads : threadList . filter ( ( t ) : t is ArchivedThreadData => t . status === "archived" ) ,
167
+
168
+ onSwitchToNewThread : ( ) => {
169
+ const newId = `thread-${ Date . now ( ) } ` ;
170
+ setThreadList ( ( prev ) => [
171
+ ...prev ,
172
+ {
173
+ threadId : newId ,
174
+ status : "regular" ,
175
+ title : "New Chat" ,
176
+ } as RegularThreadData ,
177
+ ] ) ;
178
+ setThreads ( ( prev ) => new Map ( prev ) . set ( newId , [ ] ) ) ;
179
+ setCurrentThreadId ( newId ) ;
180
+ } ,
181
+
182
+ onSwitchToThread : ( threadId ) => {
183
+ setCurrentThreadId ( threadId ) ;
184
+ } ,
185
+
186
+ onRename : ( threadId , newTitle ) => {
187
+ setThreadList ( ( prev ) =>
188
+ prev . map ( ( t ) =>
189
+ t . threadId === threadId ? { ...t , title : newTitle } : t ,
190
+ ) ,
191
+ ) ;
192
+ } ,
193
+
194
+ onArchive : ( threadId ) => {
195
+ setThreadList ( ( prev ) =>
196
+ prev . map ( ( t ) =>
197
+ t . threadId === threadId ? { ...t , status : "archived" } : t ,
198
+ ) ,
199
+ ) ;
200
+ } ,
201
+
202
+ onDelete : ( threadId ) => {
203
+ setThreadList ( ( prev ) => prev . filter ( ( t ) => t . threadId !== threadId ) ) ;
204
+ setThreads ( ( prev ) => {
205
+ const next = new Map ( prev ) ;
206
+ next . delete ( threadId ) ;
207
+ return next ;
208
+ } ) ;
209
+ if ( currentThreadId === threadId ) {
210
+ setCurrentThreadId ( "default" ) ;
211
+ }
212
+ } ,
213
+ } ;
214
+
215
+ const runtime = useExternalStoreRuntime ( {
216
+ messages : currentMessages ,
217
+ setMessages : ( messages ) => {
218
+ setThreads ( ( prev ) => new Map ( prev ) . set ( currentThreadId , messages ) ) ;
219
+ } ,
220
+ convertMessage,
221
+ isRunning,
222
+ onNew,
223
+ onEdit,
224
+ adapters : {
225
+ threadList : threadListAdapter ,
226
+ } ,
227
+ } ) ;
228
+
229
+ return (
230
+ < AssistantRuntimeProvider runtime = { runtime } >
231
+ < ThreadList />
232
+ < Thread />
233
+ </ AssistantRuntimeProvider >
234
+ ) ;
235
+ }
236
+
237
+ // Main App component with proper context wrapping
238
+ export function ChatApp ( ) {
239
+ return (
240
+ < ThreadProvider >
241
+ < ChatWithThreads />
242
+ </ ThreadProvider >
243
+ ) ;
244
+ }
245
+
246
+ export { ChatWithThreads } ;
0 commit comments