agent-starter-pack 0.5.3__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/METADATA +1 -1
  2. {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/RECORD +59 -33
  3. agents/adk_base/notebooks/adk_app_testing.ipynb +1 -1
  4. agents/adk_gemini_fullstack/README.md +148 -0
  5. agents/adk_gemini_fullstack/app/agent.py +363 -0
  6. src/frontends/streamlit_adk/frontend/style/app_markdown.py → agents/adk_gemini_fullstack/app/config.py +19 -23
  7. agents/adk_gemini_fullstack/notebooks/adk_app_testing.ipynb +353 -0
  8. agents/adk_gemini_fullstack/notebooks/evaluating_adk_agent.ipynb +1528 -0
  9. agents/adk_gemini_fullstack/template/.templateconfig.yaml +37 -0
  10. agents/adk_gemini_fullstack/tests/integration/test_agent.py +58 -0
  11. agents/agentic_rag/notebooks/adk_app_testing.ipynb +1 -1
  12. src/base_template/Makefile +21 -2
  13. src/base_template/README.md +8 -3
  14. src/base_template/pyproject.toml +1 -4
  15. src/cli/commands/create.py +17 -10
  16. src/cli/utils/template.py +13 -10
  17. src/deployment_targets/cloud_run/app/server.py +7 -1
  18. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +1 -1
  19. src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +321 -0
  20. src/frontends/adk_gemini_fullstack/frontend/components.json +21 -0
  21. src/frontends/adk_gemini_fullstack/frontend/eslint.config.js +28 -0
  22. src/frontends/adk_gemini_fullstack/frontend/index.html +12 -0
  23. src/frontends/adk_gemini_fullstack/frontend/package-lock.json +5829 -0
  24. src/frontends/adk_gemini_fullstack/frontend/package.json +46 -0
  25. src/frontends/adk_gemini_fullstack/frontend/public/vite.svg +1 -0
  26. src/frontends/adk_gemini_fullstack/frontend/src/App.tsx +565 -0
  27. src/frontends/adk_gemini_fullstack/frontend/src/components/ActivityTimeline.tsx +244 -0
  28. src/frontends/adk_gemini_fullstack/frontend/src/components/ChatMessagesView.tsx +419 -0
  29. src/frontends/adk_gemini_fullstack/frontend/src/components/InputForm.tsx +60 -0
  30. src/frontends/adk_gemini_fullstack/frontend/src/components/WelcomeScreen.tsx +56 -0
  31. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/badge.tsx +46 -0
  32. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/button.tsx +59 -0
  33. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/card.tsx +92 -0
  34. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/input.tsx +21 -0
  35. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/scroll-area.tsx +56 -0
  36. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/select.tsx +183 -0
  37. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/tabs.tsx +64 -0
  38. src/frontends/adk_gemini_fullstack/frontend/src/components/ui/textarea.tsx +18 -0
  39. src/frontends/adk_gemini_fullstack/frontend/src/global.css +154 -0
  40. src/frontends/adk_gemini_fullstack/frontend/src/main.tsx +13 -0
  41. src/frontends/adk_gemini_fullstack/frontend/src/utils.ts +7 -0
  42. src/frontends/adk_gemini_fullstack/frontend/src/vite-env.d.ts +1 -0
  43. src/frontends/adk_gemini_fullstack/frontend/tsconfig.json +28 -0
  44. src/frontends/adk_gemini_fullstack/frontend/tsconfig.node.json +24 -0
  45. src/frontends/adk_gemini_fullstack/frontend/vite.config.ts +37 -0
  46. src/resources/locks/uv-adk_base-agent_engine.lock +24 -24
  47. src/resources/locks/uv-adk_base-cloud_run.lock +24 -24
  48. src/resources/locks/uv-adk_gemini_fullstack-agent_engine.lock +3217 -0
  49. src/resources/locks/uv-adk_gemini_fullstack-cloud_run.lock +3513 -0
  50. src/resources/locks/uv-agentic_rag-agent_engine.lock +88 -85
  51. src/resources/locks/uv-agentic_rag-cloud_run.lock +124 -119
  52. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +94 -91
  53. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +130 -125
  54. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +91 -88
  55. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +130 -125
  56. src/resources/locks/uv-live_api-cloud_run.lock +121 -116
  57. src/frontends/streamlit_adk/frontend/side_bar.py +0 -214
  58. src/frontends/streamlit_adk/frontend/streamlit_app.py +0 -314
  59. src/frontends/streamlit_adk/frontend/utils/chat_utils.py +0 -84
  60. src/frontends/streamlit_adk/frontend/utils/local_chat_history.py +0 -110
  61. src/frontends/streamlit_adk/frontend/utils/message_editing.py +0 -61
  62. src/frontends/streamlit_adk/frontend/utils/multimodal_utils.py +0 -223
  63. src/frontends/streamlit_adk/frontend/utils/stream_handler.py +0 -311
  64. src/frontends/streamlit_adk/frontend/utils/title_summary.py +0 -129
  65. {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/WHEEL +0 -0
  66. {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/entry_points.txt +0 -0
  67. {agent_starter_pack-0.5.3.dist-info → agent_starter_pack-0.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@langchain/core": "^0.3.55",
14
+ "@langchain/langgraph-sdk": "^0.0.74",
15
+ "@radix-ui/react-scroll-area": "^1.2.8",
16
+ "@radix-ui/react-select": "^2.2.4",
17
+ "@radix-ui/react-slot": "^1.2.2",
18
+ "@radix-ui/react-tabs": "^1.1.11",
19
+ "@radix-ui/react-tooltip": "^1.2.6",
20
+ "@tailwindcss/vite": "^4.1.5",
21
+ "class-variance-authority": "^0.7.1",
22
+ "clsx": "^2.1.1",
23
+ "lucide-react": "^0.508.0",
24
+ "react": "^19.0.0",
25
+ "react-dom": "^19.0.0",
26
+ "react-markdown": "^9.0.3",
27
+ "react-router-dom": "^7.5.3",
28
+ "tailwind-merge": "^3.2.0",
29
+ "tailwindcss": "^4.1.5"
30
+ },
31
+ "devDependencies": {
32
+ "@eslint/js": "^9.22.0",
33
+ "@types/node": "^22.15.17",
34
+ "@types/react": "^19.1.2",
35
+ "@types/react-dom": "^19.1.3",
36
+ "@vitejs/plugin-react-swc": "^3.9.0",
37
+ "eslint": "^9.22.0",
38
+ "eslint-plugin-react-hooks": "^5.2.0",
39
+ "eslint-plugin-react-refresh": "^0.4.19",
40
+ "globals": "^16.0.0",
41
+ "tw-animate-css": "^1.2.9",
42
+ "typescript": "~5.7.2",
43
+ "typescript-eslint": "^8.26.1",
44
+ "vite": "^6.3.4"
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1,565 @@
1
+ import { useState, useRef, useCallback, useEffect } from "react";
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { WelcomeScreen } from "@/components/WelcomeScreen";
4
+ import { ChatMessagesView } from "@/components/ChatMessagesView";
5
+
6
+ // Update DisplayData to be a string type
7
+ type DisplayData = string | null;
8
+ interface MessageWithAgent {
9
+ type: "human" | "ai";
10
+ content: string;
11
+ id: string;
12
+ agent?: string;
13
+ finalReportWithCitations?: boolean;
14
+ }
15
+
16
+ interface AgentMessage {
17
+ parts: { text: string }[];
18
+ role: string;
19
+ }
20
+
21
+ interface AgentResponse {
22
+ content: AgentMessage;
23
+ usageMetadata: {
24
+ candidatesTokenCount: number;
25
+ promptTokenCount: number;
26
+ totalTokenCount: number;
27
+ };
28
+ author: string;
29
+ actions: {
30
+ stateDelta: {
31
+ research_plan?: string;
32
+ final_report_with_citations?: boolean;
33
+ };
34
+ };
35
+ }
36
+
37
+ interface ProcessedEvent {
38
+ title: string;
39
+ data: any;
40
+ }
41
+
42
+ export default function App() {
43
+ const [userId, setUserId] = useState<string | null>(null);
44
+ const [sessionId, setSessionId] = useState<string | null>(null);
45
+ const [appName, setAppName] = useState<string | null>(null);
46
+ const [messages, setMessages] = useState<MessageWithAgent[]>([]);
47
+ const [displayData, setDisplayData] = useState<DisplayData | null>(null);
48
+ const [isLoading, setIsLoading] = useState(false);
49
+ const [messageEvents, setMessageEvents] = useState<Map<string, ProcessedEvent[]>>(new Map());
50
+ const [websiteCount, setWebsiteCount] = useState<number>(0);
51
+ const [isBackendReady, setIsBackendReady] = useState(false);
52
+ const [isCheckingBackend, setIsCheckingBackend] = useState(true);
53
+ const currentAgentRef = useRef('');
54
+ const accumulatedTextRef = useRef("");
55
+ const scrollAreaRef = useRef<HTMLDivElement>(null);
56
+
57
+ const retryWithBackoff = async (
58
+ fn: () => Promise<any>,
59
+ maxRetries: number = 10,
60
+ maxDuration: number = 120000 // 2 minutes
61
+ ): Promise<any> => {
62
+ const startTime = Date.now();
63
+ let lastError: Error;
64
+
65
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
66
+ if (Date.now() - startTime > maxDuration) {
67
+ throw new Error(`Retry timeout after ${maxDuration}ms`);
68
+ }
69
+
70
+ try {
71
+ return await fn();
72
+ } catch (error) {
73
+ lastError = error as Error;
74
+ const delay = Math.min(1000 * Math.pow(2, attempt), 5000); // Exponential backoff, max 5s
75
+ console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, error);
76
+ await new Promise(resolve => setTimeout(resolve, delay));
77
+ }
78
+ }
79
+
80
+ throw lastError!;
81
+ };
82
+
83
+ const createSession = async (): Promise<{userId: string, sessionId: string, appName: string}> => {
84
+ const generatedSessionId = uuidv4();
85
+ const response = await fetch(`/api/apps/app/users/u_999/sessions/${generatedSessionId}`, {
86
+ method: "POST",
87
+ headers: {
88
+ "Content-Type": "application/json"
89
+ }
90
+ });
91
+
92
+ if (!response.ok) {
93
+ throw new Error(`Failed to create session: ${response.status} ${response.statusText}`);
94
+ }
95
+
96
+ const data = await response.json();
97
+ return {
98
+ userId: data.userId,
99
+ sessionId: data.id,
100
+ appName: data.appName
101
+ };
102
+ };
103
+
104
+ const checkBackendHealth = async (): Promise<boolean> => {
105
+ try {
106
+ // Use the docs endpoint or root endpoint to check if backend is ready
107
+ const response = await fetch("/api/docs", {
108
+ method: "GET",
109
+ headers: {
110
+ "Content-Type": "application/json"
111
+ }
112
+ });
113
+ return response.ok;
114
+ } catch (error) {
115
+ console.log("Backend not ready yet:", error);
116
+ return false;
117
+ }
118
+ };
119
+
120
+ // Function to extract text and metadata from SSE data
121
+ const extractDataFromSSE = (data: string) => {
122
+ try {
123
+ const parsed = JSON.parse(data);
124
+ console.log('[SSE PARSED EVENT]:', JSON.stringify(parsed, null, 2)); // DEBUG: Log parsed event
125
+
126
+ let textParts: string[] = [];
127
+ let agent = '';
128
+ let finalReportWithCitations = undefined;
129
+ let functionCall = null;
130
+ let functionResponse = null;
131
+ let sources = null;
132
+
133
+ // Check if content.parts exists and has text
134
+ if (parsed.content && parsed.content.parts) {
135
+ textParts = parsed.content.parts
136
+ .filter((part: any) => part.text)
137
+ .map((part: any) => part.text);
138
+
139
+ // Check for function calls
140
+ const functionCallPart = parsed.content.parts.find((part: any) => part.functionCall);
141
+ if (functionCallPart) {
142
+ functionCall = functionCallPart.functionCall;
143
+ }
144
+
145
+ // Check for function responses
146
+ const functionResponsePart = parsed.content.parts.find((part: any) => part.functionResponse);
147
+ if (functionResponsePart) {
148
+ functionResponse = functionResponsePart.functionResponse;
149
+ }
150
+ }
151
+
152
+ // Extract agent information
153
+ if (parsed.author) {
154
+ agent = parsed.author;
155
+ console.log('[SSE EXTRACT] Agent:', agent); // DEBUG: Log agent
156
+ }
157
+
158
+ if (
159
+ parsed.actions &&
160
+ parsed.actions.stateDelta &&
161
+ parsed.actions.stateDelta.final_report_with_citations
162
+ ) {
163
+ finalReportWithCitations = parsed.actions.stateDelta.final_report_with_citations;
164
+ }
165
+
166
+ // Extract website count from research agents
167
+ let sourceCount = 0;
168
+ if ((parsed.author === 'section_researcher' || parsed.author === 'enhanced_search_executor')) {
169
+ console.log('[SSE EXTRACT] Relevant agent for source count:', parsed.author); // DEBUG
170
+ if (parsed.actions?.stateDelta?.url_to_short_id) {
171
+ console.log('[SSE EXTRACT] url_to_short_id found:', parsed.actions.stateDelta.url_to_short_id); // DEBUG
172
+ sourceCount = Object.keys(parsed.actions.stateDelta.url_to_short_id).length;
173
+ console.log('[SSE EXTRACT] Calculated sourceCount:', sourceCount); // DEBUG
174
+ } else {
175
+ console.log('[SSE EXTRACT] url_to_short_id NOT found for agent:', parsed.author); // DEBUG
176
+ }
177
+ }
178
+
179
+ // Extract sources if available
180
+ if (parsed.actions?.stateDelta?.sources) {
181
+ sources = parsed.actions.stateDelta.sources;
182
+ console.log('[SSE EXTRACT] Sources found:', sources); // DEBUG
183
+ }
184
+
185
+
186
+ return { textParts, agent, finalReportWithCitations, functionCall, functionResponse, sourceCount, sources };
187
+ } catch (error) {
188
+ // Log the error and a truncated version of the problematic data for easier debugging.
189
+ const truncatedData = data.length > 200 ? data.substring(0, 200) + "..." : data;
190
+ console.error('Error parsing SSE data. Raw data (truncated): "', truncatedData, '". Error details:', error);
191
+ return { textParts: [], agent: '', finalReportWithCitations: undefined, functionCall: null, functionResponse: null, sourceCount: 0, sources: null };
192
+ }
193
+ };
194
+
195
+ // Define getEventTitle here or ensure it's in scope from where it's used
196
+ const getEventTitle = (agentName: string): string => {
197
+ switch (agentName) {
198
+ case "plan_generator":
199
+ return "Planning Research Strategy";
200
+ case "section_planner":
201
+ return "Structuring Report Outline";
202
+ case "section_researcher":
203
+ return "Initial Web Research";
204
+ case "research_evaluator":
205
+ return "Evaluating Research Quality";
206
+ case "EscalationChecker":
207
+ return "Quality Assessment";
208
+ case "enhanced_search_executor":
209
+ return "Enhanced Web Research";
210
+ case "research_pipeline":
211
+ return "Executing Research Pipeline";
212
+ case "iterative_refinement_loop":
213
+ return "Refining Research";
214
+ case "interactive_planner_agent":
215
+ case "root_agent":
216
+ return "Interactive Planning";
217
+ default:
218
+ return `Processing (${agentName || 'Unknown Agent'})`;
219
+ }
220
+ };
221
+
222
+ const processSseEventData = (jsonData: string, aiMessageId: string) => {
223
+ const { textParts, agent, finalReportWithCitations, functionCall, functionResponse, sourceCount, sources } = extractDataFromSSE(jsonData);
224
+
225
+ if (sourceCount > 0) {
226
+ console.log('[SSE HANDLER] Updating websiteCount. Current sourceCount:', sourceCount);
227
+ setWebsiteCount(prev => Math.max(prev, sourceCount));
228
+ }
229
+
230
+ if (agent && agent !== currentAgentRef.current) {
231
+ currentAgentRef.current = agent;
232
+ }
233
+
234
+ if (functionCall) {
235
+ const functionCallTitle = `Function Call: ${functionCall.name}`;
236
+ console.log('[SSE HANDLER] Adding Function Call timeline event:', functionCallTitle);
237
+ setMessageEvents(prev => new Map(prev).set(aiMessageId, [...(prev.get(aiMessageId) || []), {
238
+ title: functionCallTitle,
239
+ data: { type: 'functionCall', name: functionCall.name, args: functionCall.args, id: functionCall.id }
240
+ }]));
241
+ }
242
+
243
+ if (functionResponse) {
244
+ const functionResponseTitle = `Function Response: ${functionResponse.name}`;
245
+ console.log('[SSE HANDLER] Adding Function Response timeline event:', functionResponseTitle);
246
+ setMessageEvents(prev => new Map(prev).set(aiMessageId, [...(prev.get(aiMessageId) || []), {
247
+ title: functionResponseTitle,
248
+ data: { type: 'functionResponse', name: functionResponse.name, response: functionResponse.response, id: functionResponse.id }
249
+ }]));
250
+ }
251
+
252
+ if (textParts.length > 0 && agent !== "report_composer_with_citations") {
253
+ if (agent !== "interactive_planner_agent") {
254
+ const eventTitle = getEventTitle(agent);
255
+ console.log('[SSE HANDLER] Adding Text timeline event for agent:', agent, 'Title:', eventTitle, 'Data:', textParts.join(" "));
256
+ setMessageEvents(prev => new Map(prev).set(aiMessageId, [...(prev.get(aiMessageId) || []), {
257
+ title: eventTitle,
258
+ data: { type: 'text', content: textParts.join(" ") }
259
+ }]));
260
+ } else { // interactive_planner_agent text updates the main AI message
261
+ for (const text of textParts) {
262
+ accumulatedTextRef.current += text + " ";
263
+ setMessages(prev => prev.map(msg =>
264
+ msg.id === aiMessageId ? { ...msg, content: accumulatedTextRef.current.trim(), agent: currentAgentRef.current || msg.agent } : msg
265
+ ));
266
+ setDisplayData(accumulatedTextRef.current.trim());
267
+ }
268
+ }
269
+ }
270
+
271
+ if (sources) {
272
+ console.log('[SSE HANDLER] Adding Retrieved Sources timeline event:', sources);
273
+ setMessageEvents(prev => new Map(prev).set(aiMessageId, [...(prev.get(aiMessageId) || []), {
274
+ title: "Retrieved Sources", data: { type: 'sources', content: sources }
275
+ }]));
276
+ }
277
+
278
+ if (agent === "report_composer_with_citations" && finalReportWithCitations) {
279
+ const finalReportMessageId = Date.now().toString() + "_final";
280
+ setMessages(prev => [...prev, { type: "ai", content: finalReportWithCitations as string, id: finalReportMessageId, agent: currentAgentRef.current, finalReportWithCitations: true }]);
281
+ setDisplayData(finalReportWithCitations as string);
282
+ }
283
+ };
284
+
285
+ const handleSubmit = useCallback(async (query: string, model: string, effort: string) => {
286
+ if (!query.trim()) return;
287
+
288
+ setIsLoading(true);
289
+ try {
290
+ // Create session if it doesn't exist
291
+ let currentUserId = userId;
292
+ let currentSessionId = sessionId;
293
+ let currentAppName = appName;
294
+
295
+ if (!currentSessionId || !currentUserId || !currentAppName) {
296
+ console.log('Creating new session...');
297
+ const sessionData = await retryWithBackoff(createSession);
298
+ currentUserId = sessionData.userId;
299
+ currentSessionId = sessionData.sessionId;
300
+ currentAppName = sessionData.appName;
301
+
302
+ setUserId(currentUserId);
303
+ setSessionId(currentSessionId);
304
+ setAppName(currentAppName);
305
+ console.log('Session created successfully:', { currentUserId, currentSessionId, currentAppName });
306
+ }
307
+
308
+ // Add user message to chat
309
+ const userMessageId = Date.now().toString();
310
+ setMessages(prev => [...prev, { type: "human", content: query, id: userMessageId }]);
311
+
312
+ // Create AI message placeholder
313
+ const aiMessageId = Date.now().toString() + "_ai";
314
+ currentAgentRef.current = ''; // Reset current agent
315
+ accumulatedTextRef.current = ''; // Reset accumulated text
316
+
317
+ setMessages(prev => [...prev, {
318
+ type: "ai",
319
+ content: "",
320
+ id: aiMessageId,
321
+ agent: '',
322
+ }]);
323
+
324
+ // Send the message with retry logic
325
+ const sendMessage = async () => {
326
+ const response = await fetch("/api/run_sse", {
327
+ method: "POST",
328
+ headers: {
329
+ "Content-Type": "application/json",
330
+ },
331
+ body: JSON.stringify({
332
+ appName: currentAppName,
333
+ userId: currentUserId,
334
+ sessionId: currentSessionId,
335
+ newMessage: {
336
+ parts: [{ text: query }],
337
+ role: "user"
338
+ },
339
+ streaming: false
340
+ }),
341
+ });
342
+
343
+ if (!response.ok) {
344
+ throw new Error(`Failed to send message: ${response.status} ${response.statusText}`);
345
+ }
346
+
347
+ return response;
348
+ };
349
+
350
+ const response = await retryWithBackoff(sendMessage);
351
+
352
+ // Handle SSE streaming
353
+ const reader = response.body?.getReader();
354
+ const decoder = new TextDecoder();
355
+ let lineBuffer = "";
356
+ let eventDataBuffer = "";
357
+
358
+ if (reader) {
359
+ // eslint-disable-next-line no-constant-condition
360
+ while (true) {
361
+ const { done, value } = await reader.read();
362
+
363
+ if (value) {
364
+ lineBuffer += decoder.decode(value, { stream: true });
365
+ }
366
+
367
+ let eolIndex;
368
+ // Process all complete lines in the buffer, or the remaining buffer if 'done'
369
+ while ((eolIndex = lineBuffer.indexOf('\n')) >= 0 || (done && lineBuffer.length > 0)) {
370
+ let line: string;
371
+ if (eolIndex >= 0) {
372
+ line = lineBuffer.substring(0, eolIndex);
373
+ lineBuffer = lineBuffer.substring(eolIndex + 1);
374
+ } else { // Only if done and lineBuffer has content without a trailing newline
375
+ line = lineBuffer;
376
+ lineBuffer = "";
377
+ }
378
+
379
+ if (line.trim() === "") { // Empty line: dispatch event
380
+ if (eventDataBuffer.length > 0) {
381
+ // Remove trailing newline before parsing
382
+ const jsonDataToParse = eventDataBuffer.endsWith('\n') ? eventDataBuffer.slice(0, -1) : eventDataBuffer;
383
+ console.log('[SSE DISPATCH EVENT]:', jsonDataToParse.substring(0, 200) + "..."); // DEBUG
384
+ processSseEventData(jsonDataToParse, aiMessageId);
385
+ eventDataBuffer = ""; // Reset for next event
386
+ }
387
+ } else if (line.startsWith('data:')) {
388
+ eventDataBuffer += line.substring(5).trimStart() + '\n'; // Add newline as per spec for multi-line data
389
+ } else if (line.startsWith(':')) {
390
+ // Comment line, ignore
391
+ } // Other SSE fields (event, id, retry) can be handled here if needed
392
+ }
393
+
394
+ if (done) {
395
+ // If the loop exited due to 'done', and there's still data in eventDataBuffer
396
+ // (e.g., stream ended after data lines but before an empty line delimiter)
397
+ if (eventDataBuffer.length > 0) {
398
+ const jsonDataToParse = eventDataBuffer.endsWith('\n') ? eventDataBuffer.slice(0, -1) : eventDataBuffer;
399
+ console.log('[SSE DISPATCH FINAL EVENT]:', jsonDataToParse.substring(0,200) + "..."); // DEBUG
400
+ processSseEventData(jsonDataToParse, aiMessageId);
401
+ eventDataBuffer = ""; // Clear buffer
402
+ }
403
+ break; // Exit the while(true) loop
404
+ }
405
+ }
406
+ }
407
+
408
+ setIsLoading(false);
409
+
410
+ } catch (error) {
411
+ console.error("Error:", error);
412
+ // Update the AI message placeholder with an error message
413
+ const aiMessageId = Date.now().toString() + "_ai_error";
414
+ setMessages(prev => [...prev, {
415
+ type: "ai",
416
+ content: `Sorry, there was an error processing your request: ${error instanceof Error ? error.message : 'Unknown error'}`,
417
+ id: aiMessageId
418
+ }]);
419
+ setIsLoading(false);
420
+ }
421
+ }, [processSseEventData]);
422
+
423
+ useEffect(() => {
424
+ if (scrollAreaRef.current) {
425
+ const scrollViewport = scrollAreaRef.current.querySelector(
426
+ "[data-radix-scroll-area-viewport]"
427
+ );
428
+ if (scrollViewport) {
429
+ scrollViewport.scrollTop = scrollViewport.scrollHeight;
430
+ }
431
+ }
432
+ }, [messages]);
433
+
434
+ useEffect(() => {
435
+ const checkBackend = async () => {
436
+ setIsCheckingBackend(true);
437
+
438
+ // Check if backend is ready with retry logic
439
+ const maxAttempts = 60; // 2 minutes with 2-second intervals
440
+ let attempts = 0;
441
+
442
+ while (attempts < maxAttempts) {
443
+ const isReady = await checkBackendHealth();
444
+ if (isReady) {
445
+ setIsBackendReady(true);
446
+ setIsCheckingBackend(false);
447
+ return;
448
+ }
449
+
450
+ attempts++;
451
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds between checks
452
+ }
453
+
454
+ // If we get here, backend didn't come up in time
455
+ setIsCheckingBackend(false);
456
+ console.error("Backend failed to start within 2 minutes");
457
+ };
458
+
459
+ checkBackend();
460
+ }, []);
461
+
462
+ const handleCancel = useCallback(() => {
463
+ setMessages([]);
464
+ setDisplayData(null);
465
+ setMessageEvents(new Map());
466
+ setWebsiteCount(0);
467
+ window.location.reload();
468
+ }, []);
469
+
470
+ // Scroll to bottom when messages update
471
+ const scrollToBottom = useCallback(() => {
472
+ if (scrollAreaRef.current) {
473
+ const scrollViewport = scrollAreaRef.current.querySelector(
474
+ "[data-radix-scroll-area-viewport]"
475
+ );
476
+ if (scrollViewport) {
477
+ scrollViewport.scrollTop = scrollViewport.scrollHeight;
478
+ }
479
+ }
480
+ }, []);
481
+
482
+ const BackendLoadingScreen = () => (
483
+ <div className="flex-1 flex flex-col items-center justify-center p-4 overflow-hidden relative">
484
+ <div className="w-full max-w-2xl z-10
485
+ bg-neutral-900/50 backdrop-blur-md
486
+ p-8 rounded-2xl border border-neutral-700
487
+ shadow-2xl shadow-black/60">
488
+
489
+ <div className="text-center space-y-6">
490
+ <h1 className="text-4xl font-bold text-white flex items-center justify-center gap-3">
491
+ ✨ Gemini FullStack - ADK 🚀
492
+ </h1>
493
+
494
+ <div className="flex flex-col items-center space-y-4">
495
+ {/* Spinning animation */}
496
+ <div className="relative">
497
+ <div className="w-16 h-16 border-4 border-neutral-600 border-t-blue-500 rounded-full animate-spin"></div>
498
+ <div className="absolute inset-0 w-16 h-16 border-4 border-transparent border-r-purple-500 rounded-full animate-spin" style={{animationDirection: 'reverse', animationDuration: '1.5s'}}></div>
499
+ </div>
500
+
501
+ <div className="space-y-2">
502
+ <p className="text-xl text-neutral-300">
503
+ Waiting for backend to be ready...
504
+ </p>
505
+ <p className="text-sm text-neutral-400">
506
+ This may take a moment on first startup
507
+ </p>
508
+ </div>
509
+
510
+ {/* Animated dots */}
511
+ <div className="flex space-x-1">
512
+ <div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{animationDelay: '0ms'}}></div>
513
+ <div className="w-2 h-2 bg-purple-500 rounded-full animate-bounce" style={{animationDelay: '150ms'}}></div>
514
+ <div className="w-2 h-2 bg-pink-500 rounded-full animate-bounce" style={{animationDelay: '300ms'}}></div>
515
+ </div>
516
+ </div>
517
+ </div>
518
+ </div>
519
+ </div>
520
+ );
521
+
522
+ return (
523
+ <div className="flex h-screen bg-neutral-800 text-neutral-100 font-sans antialiased">
524
+ <main className="flex-1 flex flex-col overflow-hidden w-full">
525
+ <div className={`flex-1 overflow-y-auto ${(messages.length === 0 || isCheckingBackend) ? "flex" : ""}`}>
526
+ {isCheckingBackend ? (
527
+ <BackendLoadingScreen />
528
+ ) : !isBackendReady ? (
529
+ <div className="flex-1 flex flex-col items-center justify-center p-4">
530
+ <div className="text-center space-y-4">
531
+ <h2 className="text-2xl font-bold text-red-400">Backend Unavailable</h2>
532
+ <p className="text-neutral-300">
533
+ Unable to connect to backend services at localhost:8000
534
+ </p>
535
+ <button
536
+ onClick={() => window.location.reload()}
537
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
538
+ >
539
+ Retry
540
+ </button>
541
+ </div>
542
+ </div>
543
+ ) : messages.length === 0 ? (
544
+ <WelcomeScreen
545
+ handleSubmit={handleSubmit}
546
+ isLoading={isLoading}
547
+ onCancel={handleCancel}
548
+ />
549
+ ) : (
550
+ <ChatMessagesView
551
+ messages={messages}
552
+ isLoading={isLoading}
553
+ scrollAreaRef={scrollAreaRef}
554
+ onSubmit={handleSubmit}
555
+ onCancel={handleCancel}
556
+ displayData={displayData}
557
+ messageEvents={messageEvents}
558
+ websiteCount={websiteCount}
559
+ />
560
+ )}
561
+ </div>
562
+ </main>
563
+ </div>
564
+ );
565
+ }