33 */
44
55import type {
6- CreativeBrief ,
7- Product ,
86 AgentResponse ,
9- ParsedBriefResponse ,
107 AppConfig ,
8+ MessageRequest ,
9+ MessageResponse ,
1110} from '../types' ;
1211
1312const API_BASE = '/api' ;
1413
1514/**
16- * Get application configuration including feature flags
17- */
18- export async function getAppConfig ( ) : Promise < AppConfig > {
19- const response = await fetch ( `${ API_BASE } /config` ) ;
20-
21- if ( ! response . ok ) {
22- throw new Error ( `Failed to get config: ${ response . statusText } ` ) ;
23- }
24-
25- return response . json ( ) ;
26- }
27-
28- /**
29- * Parse a free-text creative brief into structured format
15+ * Send a message or action to the /api/chat endpoint
3016 */
31- export async function parseBrief (
32- briefText : string ,
33- conversationId ?: string ,
34- userId ?: string ,
17+ export async function sendMessage (
18+ request : MessageRequest ,
3519 signal ?: AbortSignal
36- ) : Promise < ParsedBriefResponse > {
37- const response = await fetch ( `${ API_BASE } /brief/parse ` , {
20+ ) : Promise < MessageResponse > {
21+ const response = await fetch ( `${ API_BASE } /chat ` , {
3822 signal,
3923 method : 'POST' ,
4024 headers : { 'Content-Type' : 'application/json' } ,
41- body : JSON . stringify ( {
42- brief_text : briefText ,
43- conversation_id : conversationId ,
44- user_id : userId || 'anonymous' ,
45- } ) ,
46- } ) ;
47-
48- if ( ! response . ok ) {
49- throw new Error ( `Failed to parse brief: ${ response . statusText } ` ) ;
50- }
51-
52- return response . json ( ) ;
53- }
54-
55- /**
56- * Confirm a parsed creative brief
57- */
58- export async function confirmBrief (
59- brief : CreativeBrief ,
60- conversationId ?: string ,
61- userId ?: string
62- ) : Promise < { status : string ; conversation_id : string ; brief : CreativeBrief } > {
63- const response = await fetch ( `${ API_BASE } /brief/confirm` , {
64- method : 'POST' ,
65- headers : { 'Content-Type' : 'application/json' } ,
66- body : JSON . stringify ( {
67- brief,
68- conversation_id : conversationId ,
69- user_id : userId || 'anonymous' ,
70- } ) ,
25+ body : JSON . stringify ( request ) ,
7126 } ) ;
7227
7328 if ( ! response . ok ) {
74- throw new Error ( `Failed to confirm brief : ${ response . statusText } ` ) ;
29+ throw new Error ( `Message request failed : ${ response . statusText } ` ) ;
7530 }
7631
7732 return response . json ( ) ;
7833}
7934
8035/**
81- * Select or modify products via natural language
36+ * Get application configuration including feature flags
8237 */
83- export async function selectProducts (
84- request : string ,
85- currentProducts : Product [ ] ,
86- conversationId ?: string ,
87- userId ?: string ,
88- signal ?: AbortSignal
89- ) : Promise < { products : Product [ ] ; action : string ; message : string ; conversation_id : string } > {
90- const response = await fetch ( `${ API_BASE } /products/select` , {
91- signal,
92- method : 'POST' ,
93- headers : { 'Content-Type' : 'application/json' } ,
94- body : JSON . stringify ( {
95- request,
96- current_products : currentProducts ,
97- conversation_id : conversationId ,
98- user_id : userId || 'anonymous' ,
99- } ) ,
100- } ) ;
38+ export async function getAppConfig ( ) : Promise < AppConfig > {
39+ const response = await fetch ( `${ API_BASE } /config` ) ;
10140
10241 if ( ! response . ok ) {
103- throw new Error ( `Failed to select products : ${ response . statusText } ` ) ;
42+ throw new Error ( `Failed to get config : ${ response . statusText } ` ) ;
10443 }
10544
10645 return response . json ( ) ;
10746}
10847
10948/**
110- * Stream chat messages from the agent orchestration
49+ * Request for content generation
11150 */
112- export async function * streamChat (
113- message : string ,
114- conversationId ?: string ,
115- userId ?: string ,
116- signal ?: AbortSignal
117- ) : AsyncGenerator < AgentResponse > {
118- const response = await fetch ( `${ API_BASE } /chat` , {
119- signal,
120- method : 'POST' ,
121- headers : { 'Content-Type' : 'application/json' } ,
122- body : JSON . stringify ( {
123- message,
124- conversation_id : conversationId ,
125- user_id : userId || 'anonymous' ,
126- } ) ,
127- } ) ;
128-
129- if ( ! response . ok ) {
130- throw new Error ( `Chat request failed: ${ response . statusText } ` ) ;
131- }
132-
133- const reader = response . body ?. getReader ( ) ;
134- if ( ! reader ) {
135- throw new Error ( 'No response body' ) ;
136- }
137-
138- const decoder = new TextDecoder ( ) ;
139- let buffer = '' ;
140-
141- while ( true ) {
142- const { done, value } = await reader . read ( ) ;
143- if ( done ) break ;
144-
145- buffer += decoder . decode ( value , { stream : true } ) ;
146- const lines = buffer . split ( '\n\n' ) ;
147- buffer = lines . pop ( ) || '' ;
148-
149- for ( const line of lines ) {
150- if ( line . startsWith ( 'data: ' ) ) {
151- const data = line . slice ( 6 ) ;
152- if ( data === '[DONE]' ) {
153- return ;
154- }
155- try {
156- yield JSON . parse ( data ) as AgentResponse ;
157- } catch {
158- console . error ( 'Failed to parse SSE data:' , data ) ;
159- }
160- }
161- }
162- }
51+ export interface GenerateRequest {
52+ conversation_id : string ;
53+ user_id : string ;
54+ brief : Record < string , unknown > ;
55+ products : Array < Record < string , unknown > > ;
56+ generate_images : boolean ;
16357}
16458
16559/**
16660 * Generate content from a confirmed brief
16761 */
16862export async function * streamGenerateContent (
169- brief : CreativeBrief ,
170- products ?: Product [ ] ,
171- generateImages : boolean = true ,
172- conversationId ?: string ,
173- userId ?: string ,
63+ request : GenerateRequest ,
17464 signal ?: AbortSignal
17565) : AsyncGenerator < AgentResponse > {
17666 // Use polling-based approach for reliability with long-running tasks
@@ -179,11 +69,11 @@ export async function* streamGenerateContent(
17969 method : 'POST' ,
18070 headers : { 'Content-Type' : 'application/json' } ,
18171 body : JSON . stringify ( {
182- brief,
183- products : products || [ ] ,
184- generate_images : generateImages ,
185- conversation_id : conversationId ,
186- user_id : userId || 'anonymous' ,
72+ brief : request . brief ,
73+ products : request . products || [ ] ,
74+ generate_images : request . generate_images ,
75+ conversation_id : request . conversation_id ,
76+ user_id : request . user_id || 'anonymous' ,
18777 } ) ,
18878 } ) ;
18979
@@ -287,65 +177,76 @@ export async function* streamGenerateContent(
287177
288178 throw new Error ( 'Generation timed out after 10 minutes' ) ;
289179}
180+
290181/**
291- * Regenerate image with a modification request
292- * Used when user wants to change the generated image after initial content generation
182+ * Poll for task completion using task_id
183+ * Used for both content generation and image regeneration
293184 */
294- export async function * streamRegenerateImage (
295- modificationRequest : string ,
296- brief : CreativeBrief ,
297- products ?: Product [ ] ,
298- previousImagePrompt ?: string ,
299- conversationId ?: string ,
300- userId ?: string ,
185+ export async function * pollTaskStatus (
186+ taskId : string ,
301187 signal ?: AbortSignal
302188) : AsyncGenerator < AgentResponse > {
303- const response = await fetch ( `${ API_BASE } /regenerate` , {
304- signal,
305- method : 'POST' ,
306- headers : { 'Content-Type' : 'application/json' } ,
307- body : JSON . stringify ( {
308- modification_request : modificationRequest ,
309- brief,
310- products : products || [ ] ,
311- previous_image_prompt : previousImagePrompt ,
312- conversation_id : conversationId ,
313- user_id : userId || 'anonymous' ,
314- } ) ,
315- } ) ;
316-
317- if ( ! response . ok ) {
318- throw new Error ( `Regeneration request failed: ${ response . statusText } ` ) ;
319- }
320-
321- const reader = response . body ?. getReader ( ) ;
322- if ( ! reader ) {
323- throw new Error ( 'No response body' ) ;
324- }
325-
326- const decoder = new TextDecoder ( ) ;
327- let buffer = '' ;
328-
329- while ( true ) {
330- const { done, value } = await reader . read ( ) ;
331- if ( done ) break ;
332-
333- buffer += decoder . decode ( value , { stream : true } ) ;
334- const lines = buffer . split ( '\n\n' ) ;
335- buffer = lines . pop ( ) || '' ;
336-
337- for ( const line of lines ) {
338- if ( line . startsWith ( 'data: ' ) ) {
339- const data = line . slice ( 6 ) ;
340- if ( data === '[DONE]' ) {
341- return ;
342- }
343- try {
344- yield JSON . parse ( data ) as AgentResponse ;
345- } catch {
346- console . error ( 'Failed to parse SSE data:' , data ) ;
189+ let attempts = 0 ;
190+ const maxAttempts = 600 ; // 10 minutes max with 1-second polling
191+ const pollInterval = 1000 ; // 1 second
192+
193+ while ( attempts < maxAttempts ) {
194+ if ( signal ?. aborted ) {
195+ throw new DOMException ( 'Operation cancelled by user' , 'AbortError' ) ;
196+ }
197+
198+ await new Promise ( resolve => setTimeout ( resolve , pollInterval ) ) ;
199+ attempts ++ ;
200+
201+ if ( signal ?. aborted ) {
202+ throw new DOMException ( 'Operation cancelled by user' , 'AbortError' ) ;
203+ }
204+
205+ try {
206+ const statusResponse = await fetch ( `${ API_BASE } /generate/status/${ taskId } ` , { signal } ) ;
207+ if ( ! statusResponse . ok ) {
208+ throw new Error ( `Failed to get task status: ${ statusResponse . statusText } ` ) ;
209+ }
210+
211+ const statusData = await statusResponse . json ( ) ;
212+
213+ if ( statusData . status === 'completed' ) {
214+ yield {
215+ type : 'agent_response' ,
216+ content : JSON . stringify ( statusData . result ) ,
217+ is_final : true ,
218+ } as AgentResponse ;
219+ return ;
220+ } else if ( statusData . status === 'failed' ) {
221+ throw new Error ( statusData . error || 'Task failed' ) ;
222+ } else {
223+ // Yield heartbeat with progress
224+ const elapsedSeconds = attempts ;
225+ let stageMessage : string ;
226+
227+ if ( elapsedSeconds < 10 ) {
228+ stageMessage = 'Starting regeneration...' ;
229+ } else if ( elapsedSeconds < 30 ) {
230+ stageMessage = 'Generating new image...' ;
231+ } else if ( elapsedSeconds < 50 ) {
232+ stageMessage = 'Processing image...' ;
233+ } else {
234+ stageMessage = 'Finalizing...' ;
347235 }
236+
237+ yield {
238+ type : 'heartbeat' ,
239+ content : stageMessage ,
240+ elapsed : elapsedSeconds ,
241+ is_final : false ,
242+ } as AgentResponse ;
243+ }
244+ } catch ( error ) {
245+ if ( attempts >= maxAttempts ) {
246+ throw error ;
348247 }
349248 }
350249 }
250+
251+ throw new Error ( 'Task timed out after 10 minutes' ) ;
351252}
0 commit comments