Skip to content

Commit 721884f

Browse files
refactor: routing logic moved from frontend to backend
2 parents 7ca4b64 + ba226ff commit 721884f

File tree

8 files changed

+2650
-1629
lines changed

8 files changed

+2650
-1629
lines changed

content-gen/src/app/frontend/src/App.tsx

Lines changed: 344 additions & 444 deletions
Large diffs are not rendered by default.

content-gen/src/app/frontend/src/api/index.ts

Lines changed: 90 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -3,174 +3,64 @@
33
*/
44

55
import type {
6-
CreativeBrief,
7-
Product,
86
AgentResponse,
9-
ParsedBriefResponse,
107
AppConfig,
8+
MessageRequest,
9+
MessageResponse,
1110
} from '../types';
1211

1312
const 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
*/
16862
export 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
}

content-gen/src/app/frontend/src/types/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,26 @@ export interface AppConfig {
124124
enable_compliance_check: boolean;
125125
max_file_size_mb: number;
126126
}
127+
128+
/**
129+
* Request payload for /api/chat endpoint
130+
*/
131+
export interface MessageRequest {
132+
conversation_id: string;
133+
user_id: string;
134+
message?: string;
135+
action?: string;
136+
selected_products?: Product[];
137+
brief?: CreativeBrief;
138+
has_generated_content?: boolean;
139+
}
140+
141+
/**
142+
* Response from /api/chat endpoint
143+
*/
144+
export interface MessageResponse {
145+
action_type: string;
146+
message: string;
147+
data?: Record<string, unknown>;
148+
conversation_id: string;
149+
}

0 commit comments

Comments
 (0)