drdata commited on
Commit
76a8f07
·
verified ·
1 Parent(s): 43df02e

Update src/App.tsx

Browse files
Files changed (1) hide show
  1. src/App.tsx +385 -185
src/App.tsx CHANGED
@@ -13,11 +13,8 @@ import {
13
  RotateCcw,
14
  Settings,
15
  X,
16
- PanelRightClose,
17
- PanelRightOpen,
18
  } from "lucide-react";
19
  import { useLLM } from "./hooks/useLLM";
20
- import { useMCP } from "./hooks/useMCP";
21
 
22
  import type { Tool } from "./components/ToolItem";
23
 
@@ -41,15 +38,14 @@ import ToolCallIndicator from "./components/ToolCallIndicator";
41
  import ToolItem from "./components/ToolItem";
42
  import ResultBlock from "./components/ResultBlock";
43
  import ExamplePrompts from "./components/ExamplePrompts";
44
- import { MCPServerManager } from "./components/MCPServerManager";
45
 
46
  import { LoadingScreen } from "./components/LoadingScreen";
47
 
48
  interface RenderInfo {
49
  call: string;
50
- result?: unknown;
51
  renderer?: string;
52
- input?: Record<string, unknown>;
53
  error?: string;
54
  }
55
 
@@ -82,7 +78,7 @@ async function getDB(): Promise<IDBPDatabase> {
82
 
83
  const App: React.FC = () => {
84
  const [systemPrompt, setSystemPrompt] = useState<string>(
85
- DEFAULT_SYSTEM_PROMPT
86
  );
87
  const [isSystemPromptModalOpen, setIsSystemPromptModalOpen] =
88
  useState<boolean>(false);
@@ -93,12 +89,10 @@ const App: React.FC = () => {
93
  const [isGenerating, setIsGenerating] = useState<boolean>(false);
94
  const isMobile = useMemo(isMobileOrTablet, []);
95
  const [selectedModelId, setSelectedModelId] = useState<string>(
96
- isMobile ? "350M" : "1.2B"
97
  );
98
  const [isModelDropdownOpen, setIsModelDropdownOpen] =
99
  useState<boolean>(false);
100
- const [isMCPManagerOpen, setIsMCPManagerOpen] = useState<boolean>(false);
101
- const [isToolsPanelVisible, setIsToolsPanelVisible] = useState<boolean>(true);
102
  const chatContainerRef = useRef<HTMLDivElement>(null);
103
  const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
104
  const toolsContainerRef = useRef<HTMLDivElement>(null);
@@ -113,55 +107,31 @@ const App: React.FC = () => {
113
  clearPastKeyValues,
114
  } = useLLM(selectedModelId);
115
 
116
- // MCP integration
117
- const {
118
- getMCPToolsAsOriginalTools,
119
- callMCPTool,
120
- connectAll: connectAllMCPServers,
121
- } = useMCP();
122
-
123
  const loadTools = useCallback(async (): Promise<void> => {
124
- try {
125
- const db = await getDB();
126
- const allTools: Tool[] = await db.getAll(STORE_NAME);
127
- if (allTools.length === 0) {
128
- const defaultTools: Tool[] = Object.entries(DEFAULT_TOOLS).map(
129
- ([name, code], id) => ({
130
- id,
131
- name,
132
- code,
133
- enabled: true,
134
- isCollapsed: false,
135
- })
136
- );
137
- const tx = db.transaction(STORE_NAME, "readwrite");
138
- await Promise.all(defaultTools.map((tool) => tx.store.put(tool)));
139
- await tx.done;
140
- setTools(defaultTools);
141
- } else {
142
- setTools(allTools.map((t) => ({ ...t, isCollapsed: false })));
143
- }
144
-
145
- // Load MCP tools and merge them
146
- const mcpTools = getMCPToolsAsOriginalTools();
147
- setTools((prevTools) => [...prevTools, ...mcpTools]);
148
-
149
- console.log('✅ Tools loaded:', {
150
- defaultToolCount: allTools.length,
151
- mcpToolCount: mcpTools.length
152
- });
153
- } catch (error) {
154
- console.error('❌ Failed to load tools:', error);
155
  }
156
- }, [getMCPToolsAsOriginalTools]);
157
 
158
  useEffect(() => {
159
  loadTools();
160
- // Connect to MCP servers on startup
161
- connectAllMCPServers().catch((error) => {
162
- console.error("Failed to connect to MCP servers:", error);
163
- });
164
- }, [loadTools, connectAllMCPServers]);
165
 
166
  useEffect(() => {
167
  if (chatContainerRef.current) {
@@ -189,7 +159,6 @@ const App: React.FC = () => {
189
  const clearChat = useCallback(() => {
190
  setMessages([]);
191
  clearPastKeyValues();
192
- console.log('🧹 Chat cleared');
193
  }, [clearPastKeyValues]);
194
 
195
  const addTool = async (): Promise<void> => {
@@ -240,16 +209,16 @@ const App: React.FC = () => {
240
  const toggleToolCollapsed = (id: number): void => {
241
  setTools(
242
  tools.map((tool) =>
243
- tool.id === id ? { ...tool, isCollapsed: !tool.isCollapsed } : tool
244
- )
245
  );
246
  };
247
 
248
  const expandTool = (id: number): void => {
249
  setTools(
250
  tools.map((tool) =>
251
- tool.id === id ? { ...tool, isCollapsed: false } : tool
252
- )
253
  );
254
  };
255
 
@@ -272,80 +241,27 @@ const App: React.FC = () => {
272
  console.log('🔧 Executing tool call:', callString);
273
 
274
  const parsedCall = parsePythonicCalls(callString);
275
- if (!parsedCall) {
276
- const error = `Invalid tool call format: ${callString}`;
277
- console.error('❌', error);
278
- throw new Error(error);
279
- }
280
 
281
  const { name, positionalArgs, keywordArgs } = parsedCall;
282
  const toolToUse = tools.find((t) => t.name === name && t.enabled);
283
-
284
- if (!toolToUse) {
285
- const error = `Tool '${name}' not found or is disabled.`;
286
- console.error('❌', error);
287
- throw new Error(error);
288
- }
289
-
290
- console.log('🔧 Tool found:', { name, enabled: toolToUse.enabled });
291
-
292
- // Check if this is an MCP tool
293
- const isMCPTool = toolToUse.code?.includes("mcpServerId:");
294
- if (isMCPTool) {
295
- console.log('🌐 MCP tool detected');
296
-
297
- const mcpServerMatch = toolToUse.code?.match(/mcpServerId: "([^"]+)"/);
298
- const mcpToolMatch = toolToUse.code?.match(/toolName: "([^"]+)"/);
299
-
300
- if (mcpServerMatch && mcpToolMatch) {
301
- const serverId = mcpServerMatch[1];
302
- const toolName = mcpToolMatch[1];
303
 
304
- const { functionCode } = extractFunctionAndRenderer(toolToUse.code);
305
- const schema = generateSchemaFromCode(functionCode);
306
- const paramNames = Object.keys(schema.parameters.properties);
307
-
308
- const args: Record<string, unknown> = {};
309
-
310
- for (
311
- let i = 0;
312
- i < Math.min(positionalArgs.length, paramNames.length);
313
- i++
314
- ) {
315
- args[paramNames[i]] = positionalArgs[i];
316
- }
317
-
318
- Object.entries(keywordArgs).forEach(([key, value]) => {
319
- args[key] = value;
320
- });
321
-
322
- console.log('🌐 Calling MCP tool:', { serverId, toolName, args });
323
- const result = await callMCPTool(serverId, toolName, args);
324
- console.log('✅ MCP tool result:', result);
325
- return JSON.stringify(result);
326
- }
327
- }
328
-
329
- // Handle local tools
330
- console.log('🔧 Executing local tool');
331
  const { functionCode } = extractFunctionAndRenderer(toolToUse.code);
332
  const schema = generateSchemaFromCode(functionCode);
333
  const paramNames = Object.keys(schema.parameters.properties);
334
 
335
- const finalArgs: unknown[] = [];
336
  const requiredParams = schema.parameters.required || [];
337
 
338
  for (let i = 0; i < paramNames.length; ++i) {
339
  const paramName = paramNames[i];
340
  if (i < positionalArgs.length) {
341
  finalArgs.push(positionalArgs[i]);
342
- } else if (Object.prototype.hasOwnProperty.call(keywordArgs, paramName)) {
343
  finalArgs.push(keywordArgs[paramName]);
344
  } else if (
345
- Object.prototype.hasOwnProperty.call(
346
- schema.parameters.properties[paramName],
347
- "default"
348
- )
349
  ) {
350
  finalArgs.push(schema.parameters.properties[paramName].default);
351
  } else if (!requiredParams.includes(paramName)) {
@@ -358,32 +274,26 @@ const App: React.FC = () => {
358
  const bodyMatch = functionCode.match(/function[^{]+{([\s\S]*)}/);
359
  if (!bodyMatch) {
360
  throw new Error(
361
- "Could not parse function body. Ensure it's a standard `function` declaration."
362
  );
363
  }
364
  const body = bodyMatch[1];
365
  const AsyncFunction = Object.getPrototypeOf(
366
- async function () {}
367
  ).constructor;
368
  const func = new AsyncFunction(...paramNames, body);
369
  const result = await func(...finalArgs);
370
- console.log('✅ Local tool result:', result);
 
371
  return JSON.stringify(result);
372
  };
373
 
374
  const executeToolCalls = async (
375
- toolCallContent: string
376
  ): Promise<RenderInfo[]> => {
377
- console.log('🔧 Executing tool calls from content:', toolCallContent);
378
-
379
  const toolCalls = extractPythonicCalls(toolCallContent);
380
-
381
- if (toolCalls.length === 0) {
382
- console.warn('⚠️ No valid tool calls found');
383
  return [{ call: "", error: "No valid tool calls found." }];
384
- }
385
-
386
- console.log('🔧 Found tool calls:', toolCalls);
387
 
388
  const results: RenderInfo[] = [];
389
  for (const call of toolCalls) {
@@ -404,16 +314,16 @@ const App: React.FC = () => {
404
  parsedResult = result;
405
  }
406
 
407
- let namedParams: Record<string, unknown> = Object.create(null);
408
  if (parsedCall && toolUsed) {
409
  const schema = generateSchemaFromCode(
410
- extractFunctionAndRenderer(toolUsed.code).functionCode
411
  );
412
  const paramNames = Object.keys(schema.parameters.properties);
413
  namedParams = mapArgsToNamedParams(
414
  paramNames,
415
  parsedCall.positionalArgs,
416
- parsedCall.keywordArgs
417
  );
418
  }
419
 
@@ -425,7 +335,6 @@ const App: React.FC = () => {
425
  });
426
  } catch (error) {
427
  const errorMessage = getErrorMessage(error);
428
- console.error('❌ Tool execution error:', errorMessage);
429
  results.push({ call, error: errorMessage });
430
  }
431
  }
@@ -433,15 +342,12 @@ const App: React.FC = () => {
433
  };
434
 
435
  const handleSendMessage = async (): Promise<void> => {
436
- if (!input.trim() || !isReady) {
437
- console.warn('⚠️ Cannot send message:', { inputEmpty: !input.trim(), isReady });
438
- return;
439
- }
440
 
441
- console.log('🔵 Starting message send...', { input, isReady });
442
 
443
  const userMessage: Message = { role: "user", content: input };
444
- const currentMessages: Message[] = [...messages, userMessage];
445
  setMessages(currentMessages);
446
  setInput("");
447
  setIsGenerating(true);
@@ -451,42 +357,24 @@ const App: React.FC = () => {
451
  .filter((tool) => tool.enabled)
452
  .map((tool) => generateSchemaFromCode(tool.code));
453
 
454
- console.log('🟡 Enabled tools:', {
455
- count: toolSchemas.length,
456
- tools: toolSchemas.map(s => s.name)
457
- });
458
-
459
- let loopCount = 0;
460
- const MAX_LOOPS = 10; // Prevent infinite loops
461
-
462
- while (loopCount < MAX_LOOPS) {
463
- loopCount++;
464
- console.log(`🔄 Generation loop iteration ${loopCount}`);
465
 
 
466
  const messagesForGeneration = [
467
  { role: "system" as const, content: systemPrompt },
468
  ...currentMessages,
469
  ];
470
 
471
- console.log('🟡 Calling generateResponse...', {
472
- messageCount: messagesForGeneration.length,
473
- toolSchemaCount: toolSchemas.length
474
- });
475
-
476
  setMessages([...currentMessages, { role: "assistant", content: "" }]);
477
 
478
  let accumulatedContent = "";
479
- let tokenCount = 0;
480
 
481
  const response = await generateResponse(
482
  messagesForGeneration,
483
  toolSchemas,
484
  (token: string) => {
485
- tokenCount++;
486
  accumulatedContent += token;
487
- if (tokenCount % 10 === 0) {
488
- console.log(`📝 Streaming... (${tokenCount} tokens)`);
489
- }
490
  setMessages((current) => {
491
  const updated = [...current];
492
  updated[updated.length - 1] = {
@@ -495,25 +383,17 @@ const App: React.FC = () => {
495
  };
496
  return updated;
497
  });
498
- }
499
  );
500
 
501
- console.log('🟢 Response received:', {
502
- length: response.length,
503
- preview: response.substring(0, 100),
504
- totalTokens: tokenCount
505
- });
506
 
507
  currentMessages.push({ role: "assistant", content: response });
508
  const toolCallContent = extractToolCallContent(response);
509
 
510
  if (toolCallContent) {
511
- console.log('🔧 Tool call detected in response');
512
  const toolResults = await executeToolCalls(toolCallContent);
513
- console.log('🔧 Tool execution complete:', {
514
- resultCount: toolResults.length,
515
- hasErrors: toolResults.some(r => r.error)
516
- });
517
 
518
  const toolMessage: ToolMessage = {
519
  role: "tool",
@@ -522,24 +402,15 @@ const App: React.FC = () => {
522
  };
523
  currentMessages.push(toolMessage);
524
  setMessages([...currentMessages]);
525
-
526
- console.log('🔄 Continuing generation loop with tool results');
527
  continue;
528
  } else {
529
- console.log('✅ No tool calls detected, finishing generation');
530
  setMessages(currentMessages);
531
  break;
532
  }
533
  }
534
-
535
- if (loopCount >= MAX_LOOPS) {
536
- console.warn('⚠️ Maximum generation loops reached');
537
- throw new Error('Maximum generation loops reached. The model may be stuck in a tool-calling loop.');
538
- }
539
-
540
- console.log('🏁 Generation complete successfully');
541
  } catch (error) {
542
- console.error('🔴 Error in handleSendMessage:', error);
543
  const errorMessage = getErrorMessage(error);
544
  setMessages([
545
  ...currentMessages,
@@ -576,7 +447,7 @@ const App: React.FC = () => {
576
  console.error("Failed to save system prompt:", error);
577
  }
578
  },
579
- []
580
  );
581
 
582
  const loadSelectedModel = useCallback(async (): Promise<void> => {
@@ -635,7 +506,7 @@ const App: React.FC = () => {
635
  console.error("Failed to save selected model ID:", error);
636
  }
637
  },
638
- []
639
  );
640
 
641
  useEffect(() => {
@@ -646,4 +517,333 @@ const App: React.FC = () => {
646
  const handleModelSelect = async (modelId: string) => {
647
  setSelectedModelId(modelId);
648
  setIsModelDropdownOpen(false);
649
- a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  RotateCcw,
14
  Settings,
15
  X,
 
 
16
  } from "lucide-react";
17
  import { useLLM } from "./hooks/useLLM";
 
18
 
19
  import type { Tool } from "./components/ToolItem";
20
 
 
38
  import ToolItem from "./components/ToolItem";
39
  import ResultBlock from "./components/ResultBlock";
40
  import ExamplePrompts from "./components/ExamplePrompts";
 
41
 
42
  import { LoadingScreen } from "./components/LoadingScreen";
43
 
44
  interface RenderInfo {
45
  call: string;
46
+ result?: any;
47
  renderer?: string;
48
+ input?: Record<string, any>;
49
  error?: string;
50
  }
51
 
 
78
 
79
  const App: React.FC = () => {
80
  const [systemPrompt, setSystemPrompt] = useState<string>(
81
+ DEFAULT_SYSTEM_PROMPT,
82
  );
83
  const [isSystemPromptModalOpen, setIsSystemPromptModalOpen] =
84
  useState<boolean>(false);
 
89
  const [isGenerating, setIsGenerating] = useState<boolean>(false);
90
  const isMobile = useMemo(isMobileOrTablet, []);
91
  const [selectedModelId, setSelectedModelId] = useState<string>(
92
+ isMobile ? "350M" : "1.2B",
93
  );
94
  const [isModelDropdownOpen, setIsModelDropdownOpen] =
95
  useState<boolean>(false);
 
 
96
  const chatContainerRef = useRef<HTMLDivElement>(null);
97
  const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
98
  const toolsContainerRef = useRef<HTMLDivElement>(null);
 
107
  clearPastKeyValues,
108
  } = useLLM(selectedModelId);
109
 
 
 
 
 
 
 
 
110
  const loadTools = useCallback(async (): Promise<void> => {
111
+ const db = await getDB();
112
+ const allTools: Tool[] = await db.getAll(STORE_NAME);
113
+ if (allTools.length === 0) {
114
+ const defaultTools: Tool[] = Object.entries(DEFAULT_TOOLS).map(
115
+ ([name, code], id) => ({
116
+ id,
117
+ name,
118
+ code,
119
+ enabled: true,
120
+ isCollapsed: false,
121
+ }),
122
+ );
123
+ const tx = db.transaction(STORE_NAME, "readwrite");
124
+ await Promise.all(defaultTools.map((tool) => tx.store.put(tool)));
125
+ await tx.done;
126
+ setTools(defaultTools);
127
+ } else {
128
+ setTools(allTools.map((t) => ({ ...t, isCollapsed: false })));
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  }
130
+ }, []);
131
 
132
  useEffect(() => {
133
  loadTools();
134
+ }, [loadTools]);
 
 
 
 
135
 
136
  useEffect(() => {
137
  if (chatContainerRef.current) {
 
159
  const clearChat = useCallback(() => {
160
  setMessages([]);
161
  clearPastKeyValues();
 
162
  }, [clearPastKeyValues]);
163
 
164
  const addTool = async (): Promise<void> => {
 
209
  const toggleToolCollapsed = (id: number): void => {
210
  setTools(
211
  tools.map((tool) =>
212
+ tool.id === id ? { ...tool, isCollapsed: !tool.isCollapsed } : tool,
213
+ ),
214
  );
215
  };
216
 
217
  const expandTool = (id: number): void => {
218
  setTools(
219
  tools.map((tool) =>
220
+ tool.id === id ? { ...tool, isCollapsed: false } : tool,
221
+ ),
222
  );
223
  };
224
 
 
241
  console.log('🔧 Executing tool call:', callString);
242
 
243
  const parsedCall = parsePythonicCalls(callString);
244
+ if (!parsedCall) throw new Error(`Invalid tool call format: ${callString}`);
 
 
 
 
245
 
246
  const { name, positionalArgs, keywordArgs } = parsedCall;
247
  const toolToUse = tools.find((t) => t.name === name && t.enabled);
248
+ if (!toolToUse) throw new Error(`Tool '${name}' not found or is disabled.`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  const { functionCode } = extractFunctionAndRenderer(toolToUse.code);
251
  const schema = generateSchemaFromCode(functionCode);
252
  const paramNames = Object.keys(schema.parameters.properties);
253
 
254
+ const finalArgs: any[] = [];
255
  const requiredParams = schema.parameters.required || [];
256
 
257
  for (let i = 0; i < paramNames.length; ++i) {
258
  const paramName = paramNames[i];
259
  if (i < positionalArgs.length) {
260
  finalArgs.push(positionalArgs[i]);
261
+ } else if (keywordArgs.hasOwnProperty(paramName)) {
262
  finalArgs.push(keywordArgs[paramName]);
263
  } else if (
264
+ schema.parameters.properties[paramName].hasOwnProperty("default")
 
 
 
265
  ) {
266
  finalArgs.push(schema.parameters.properties[paramName].default);
267
  } else if (!requiredParams.includes(paramName)) {
 
274
  const bodyMatch = functionCode.match(/function[^{]+{([\s\S]*)}/);
275
  if (!bodyMatch) {
276
  throw new Error(
277
+ "Could not parse function body. Ensure it's a standard `function` declaration.",
278
  );
279
  }
280
  const body = bodyMatch[1];
281
  const AsyncFunction = Object.getPrototypeOf(
282
+ async function () {},
283
  ).constructor;
284
  const func = new AsyncFunction(...paramNames, body);
285
  const result = await func(...finalArgs);
286
+
287
+ console.log('✅ Tool result:', result);
288
  return JSON.stringify(result);
289
  };
290
 
291
  const executeToolCalls = async (
292
+ toolCallContent: string,
293
  ): Promise<RenderInfo[]> => {
 
 
294
  const toolCalls = extractPythonicCalls(toolCallContent);
295
+ if (toolCalls.length === 0)
 
 
296
  return [{ call: "", error: "No valid tool calls found." }];
 
 
 
297
 
298
  const results: RenderInfo[] = [];
299
  for (const call of toolCalls) {
 
314
  parsedResult = result;
315
  }
316
 
317
+ let namedParams: Record<string, any> = Object.create(null);
318
  if (parsedCall && toolUsed) {
319
  const schema = generateSchemaFromCode(
320
+ extractFunctionAndRenderer(toolUsed.code).functionCode,
321
  );
322
  const paramNames = Object.keys(schema.parameters.properties);
323
  namedParams = mapArgsToNamedParams(
324
  paramNames,
325
  parsedCall.positionalArgs,
326
+ parsedCall.keywordArgs,
327
  );
328
  }
329
 
 
335
  });
336
  } catch (error) {
337
  const errorMessage = getErrorMessage(error);
 
338
  results.push({ call, error: errorMessage });
339
  }
340
  }
 
342
  };
343
 
344
  const handleSendMessage = async (): Promise<void> => {
345
+ if (!input.trim() || !isReady) return;
 
 
 
346
 
347
+ console.log('🔵 Message sent:', input);
348
 
349
  const userMessage: Message = { role: "user", content: input };
350
+ let currentMessages: Message[] = [...messages, userMessage];
351
  setMessages(currentMessages);
352
  setInput("");
353
  setIsGenerating(true);
 
357
  .filter((tool) => tool.enabled)
358
  .map((tool) => generateSchemaFromCode(tool.code));
359
 
360
+ console.log('🟡 Enabled tools:', toolSchemas.length);
 
 
 
 
 
 
 
 
 
 
361
 
362
+ while (true) {
363
  const messagesForGeneration = [
364
  { role: "system" as const, content: systemPrompt },
365
  ...currentMessages,
366
  ];
367
 
 
 
 
 
 
368
  setMessages([...currentMessages, { role: "assistant", content: "" }]);
369
 
370
  let accumulatedContent = "";
371
+ console.log('🟡 Generating response...');
372
 
373
  const response = await generateResponse(
374
  messagesForGeneration,
375
  toolSchemas,
376
  (token: string) => {
 
377
  accumulatedContent += token;
 
 
 
378
  setMessages((current) => {
379
  const updated = [...current];
380
  updated[updated.length - 1] = {
 
383
  };
384
  return updated;
385
  });
386
+ },
387
  );
388
 
389
+ console.log('🟢 Response:', response.substring(0, 100));
 
 
 
 
390
 
391
  currentMessages.push({ role: "assistant", content: response });
392
  const toolCallContent = extractToolCallContent(response);
393
 
394
  if (toolCallContent) {
395
+ console.log('🔧 Tool call detected');
396
  const toolResults = await executeToolCalls(toolCallContent);
 
 
 
 
397
 
398
  const toolMessage: ToolMessage = {
399
  role: "tool",
 
402
  };
403
  currentMessages.push(toolMessage);
404
  setMessages([...currentMessages]);
 
 
405
  continue;
406
  } else {
407
+ console.log('✅ Done');
408
  setMessages(currentMessages);
409
  break;
410
  }
411
  }
 
 
 
 
 
 
 
412
  } catch (error) {
413
+ console.error('🔴 Error:', error);
414
  const errorMessage = getErrorMessage(error);
415
  setMessages([
416
  ...currentMessages,
 
447
  console.error("Failed to save system prompt:", error);
448
  }
449
  },
450
+ [],
451
  );
452
 
453
  const loadSelectedModel = useCallback(async (): Promise<void> => {
 
506
  console.error("Failed to save selected model ID:", error);
507
  }
508
  },
509
+ [],
510
  );
511
 
512
  useEffect(() => {
 
517
  const handleModelSelect = async (modelId: string) => {
518
  setSelectedModelId(modelId);
519
  setIsModelDropdownOpen(false);
520
+ await saveSelectedModel(modelId);
521
+ };
522
+
523
+ const handleExampleClick = async (messageText: string): Promise<void> => {
524
+ if (!isReady || isGenerating) return;
525
+
526
+ setInput(messageText);
527
+
528
+ const userMessage: Message = { role: "user", content: messageText };
529
+ const currentMessages: Message[] = [...messages, userMessage];
530
+ setMessages(currentMessages);
531
+ setInput("");
532
+ setIsGenerating(true);
533
+
534
+ try {
535
+ const toolSchemas = tools
536
+ .filter((tool) => tool.enabled)
537
+ .map((tool) => generateSchemaFromCode(tool.code));
538
+
539
+ while (true) {
540
+ const messagesForGeneration = [
541
+ { role: "system" as const, content: systemPrompt },
542
+ ...currentMessages,
543
+ ];
544
+
545
+ setMessages([...currentMessages, { role: "assistant", content: "" }]);
546
+
547
+ let accumulatedContent = "";
548
+ const response = await generateResponse(
549
+ messagesForGeneration,
550
+ toolSchemas,
551
+ (token: string) => {
552
+ accumulatedContent += token;
553
+ setMessages((current) => {
554
+ const updated = [...current];
555
+ updated[updated.length - 1] = {
556
+ role: "assistant",
557
+ content: accumulatedContent,
558
+ };
559
+ return updated;
560
+ });
561
+ },
562
+ );
563
+
564
+ currentMessages.push({ role: "assistant", content: response });
565
+ const toolCallContent = extractToolCallContent(response);
566
+
567
+ if (toolCallContent) {
568
+ const toolResults = await executeToolCalls(toolCallContent);
569
+
570
+ const toolMessage: ToolMessage = {
571
+ role: "tool",
572
+ content: JSON.stringify(toolResults.map((r) => r.result ?? null)),
573
+ renderInfo: toolResults,
574
+ };
575
+ currentMessages.push(toolMessage);
576
+ setMessages([...currentMessages]);
577
+ continue;
578
+ } else {
579
+ setMessages(currentMessages);
580
+ break;
581
+ }
582
+ }
583
+ } catch (error) {
584
+ const errorMessage = getErrorMessage(error);
585
+ setMessages([
586
+ ...currentMessages,
587
+ {
588
+ role: "assistant",
589
+ content: `Error generating response: ${errorMessage}`,
590
+ },
591
+ ]);
592
+ } finally {
593
+ setIsGenerating(false);
594
+ setTimeout(() => inputRef.current?.focus(), 0);
595
+ }
596
+ };
597
+
598
+ return (
599
+ <div className="font-sans bg-gray-900">
600
+ {!isReady ? (
601
+ <LoadingScreen
602
+ isLoading={isLoading}
603
+ progress={progress}
604
+ error={error}
605
+ loadSelectedModel={loadSelectedModel}
606
+ selectedModelId={selectedModelId}
607
+ isModelDropdownOpen={isModelDropdownOpen}
608
+ setIsModelDropdownOpen={setIsModelDropdownOpen}
609
+ handleModelSelect={handleModelSelect}
610
+ />
611
+ ) : (
612
+ <div className="flex h-screen text-white">
613
+ <div className="w-1/2 flex flex-col p-4">
614
+ <div className="flex items-center justify-between mb-4">
615
+ <div className="flex items-center gap-3">
616
+ <h1 className="text-3xl font-bold text-gray-200">
617
+ LFM2 WebGPU
618
+ </h1>
619
+ </div>
620
+ <div className="flex items-center gap-3">
621
+ <div className="flex items-center text-green-400">
622
+ <Zap size={16} className="mr-2" />
623
+ Ready
624
+ </div>
625
+ <button
626
+ disabled={isGenerating}
627
+ onClick={clearChat}
628
+ className={`h-10 flex items-center px-3 py-2 rounded-lg font-bold transition-colors text-sm ${
629
+ isGenerating
630
+ ? "bg-gray-600 cursor-not-allowed opacity-50"
631
+ : "bg-gray-600 hover:bg-gray-700"
632
+ }`}
633
+ title="Clear chat"
634
+ >
635
+ <RotateCcw size={14} className="mr-2" /> Clear
636
+ </button>
637
+ <button
638
+ onClick={handleOpenSystemPromptModal}
639
+ className="h-10 flex items-center px-3 py-2 rounded-lg font-bold transition-colors bg-gray-600 hover:bg-gray-700 text-sm"
640
+ title="Edit system prompt"
641
+ >
642
+ <Settings size={16} />
643
+ </button>
644
+ </div>
645
+ </div>
646
+
647
+ <div
648
+ ref={chatContainerRef}
649
+ className="flex-grow bg-gray-800 rounded-lg p-4 overflow-y-auto mb-4 space-y-4"
650
+ >
651
+ {messages.length === 0 && isReady ? (
652
+ <ExamplePrompts onExampleClick={handleExampleClick} />
653
+ ) : (
654
+ messages.map((msg, index) => {
655
+ const key = `${msg.role}-${index}`;
656
+
657
+ if (msg.role === "user") {
658
+ return (
659
+ <div key={key} className="flex justify-end">
660
+ <div className="p-3 rounded-lg max-w-md bg-indigo-600">
661
+ <p className="text-sm whitespace-pre-wrap">
662
+ {msg.content}
663
+ </p>
664
+ </div>
665
+ </div>
666
+ );
667
+ } else if (msg.role === "assistant") {
668
+ const isToolCall = msg.content.includes(
669
+ "<|tool_call_start|>",
670
+ );
671
+
672
+ if (isToolCall) {
673
+ const nextMessage = messages[index + 1];
674
+ const isCompleted = nextMessage?.role === "tool";
675
+ const hasError =
676
+ isCompleted &&
677
+ (nextMessage as ToolMessage).renderInfo.some(
678
+ (info) => !!info.error,
679
+ );
680
+
681
+ return (
682
+ <div key={key} className="flex justify-start">
683
+ <div className="p-3 rounded-lg bg-gray-700">
684
+ <ToolCallIndicator
685
+ content={msg.content}
686
+ isRunning={!isCompleted}
687
+ hasError={hasError}
688
+ />
689
+ </div>
690
+ </div>
691
+ );
692
+ }
693
+
694
+ return (
695
+ <div key={key} className="flex justify-start">
696
+ <div className="p-3 rounded-lg max-w-md bg-gray-700">
697
+ <p className="text-sm whitespace-pre-wrap">
698
+ {msg.content}
699
+ </p>
700
+ </div>
701
+ </div>
702
+ );
703
+ } else if (msg.role === "tool") {
704
+ const visibleToolResults = msg.renderInfo.filter(
705
+ (info) =>
706
+ info.error || (info.result != null && info.renderer),
707
+ );
708
+
709
+ if (visibleToolResults.length === 0) return null;
710
+
711
+ return (
712
+ <div key={key} className="flex justify-start">
713
+ <div className="p-3 rounded-lg bg-gray-700 max-w-lg">
714
+ <div className="space-y-3">
715
+ {visibleToolResults.map((info, idx) => (
716
+ <div className="flex flex-col gap-2" key={idx}>
717
+ <div className="text-xs text-gray-400 font-mono">
718
+ {info.call}
719
+ </div>
720
+ {info.error ? (
721
+ <ResultBlock error={info.error} />
722
+ ) : (
723
+ <ToolResultRenderer
724
+ result={info.result}
725
+ rendererCode={info.renderer}
726
+ input={info.input}
727
+ />
728
+ )}
729
+ </div>
730
+ ))}
731
+ </div>
732
+ </div>
733
+ </div>
734
+ );
735
+ }
736
+ return null;
737
+ })
738
+ )}
739
+ </div>
740
+
741
+ <div className="flex">
742
+ <input
743
+ ref={inputRef}
744
+ type="text"
745
+ value={input}
746
+ onChange={(e) => setInput(e.target.value)}
747
+ onKeyDown={(e) =>
748
+ e.key === "Enter" &&
749
+ !isGenerating &&
750
+ isReady &&
751
+ handleSendMessage()
752
+ }
753
+ disabled={isGenerating || !isReady}
754
+ className="flex-grow bg-gray-700 rounded-l-lg p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50"
755
+ placeholder={
756
+ isReady
757
+ ? "Type your message here..."
758
+ : "Load model first to enable chat"
759
+ }
760
+ />
761
+ <button
762
+ onClick={handleSendMessage}
763
+ disabled={isGenerating || !isReady}
764
+ className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-bold p-3 rounded-r-lg transition-colors"
765
+ >
766
+ <Play size={20} />
767
+ </button>
768
+ </div>
769
+ </div>
770
+
771
+ <div className="w-1/2 flex flex-col p-4 border-l border-gray-700">
772
+ <div className="flex justify-between items-center mb-4">
773
+ <h2 className="text-2xl font-bold text-teal-400">Tools</h2>
774
+ <button
775
+ onClick={addTool}
776
+ className="flex items-center bg-teal-600 hover:bg-teal-700 text-white font-bold py-2 px-4 rounded-lg transition-colors"
777
+ >
778
+ <Plus size={16} className="mr-2" /> Add Tool
779
+ </button>
780
+ </div>
781
+ <div
782
+ ref={toolsContainerRef}
783
+ className="flex-grow bg-gray-800 rounded-lg p-4 overflow-y-auto space-y-3"
784
+ >
785
+ {tools.map((tool) => (
786
+ <ToolItem
787
+ key={tool.id}
788
+ tool={tool}
789
+ onToggleEnabled={() => toggleToolEnabled(tool.id)}
790
+ onToggleCollapsed={() => toggleToolCollapsed(tool.id)}
791
+ onExpand={() => expandTool(tool.id)}
792
+ onDelete={() => deleteTool(tool.id)}
793
+ onCodeChange={(newCode) =>
794
+ handleToolCodeChange(tool.id, newCode)
795
+ }
796
+ />
797
+ ))}
798
+ </div>
799
+ </div>
800
+ </div>
801
+ )}
802
+
803
+ {isSystemPromptModalOpen && (
804
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
805
+ <div className="bg-gray-800 rounded-lg p-6 w-3/4 max-w-4xl max-h-3/4 flex flex-col text-gray-100">
806
+ <div className="flex justify-between items-center mb-4">
807
+ <h2 className="text-xl font-bold text-indigo-400">
808
+ Edit System Prompt
809
+ </h2>
810
+ <button
811
+ onClick={handleCancelSystemPrompt}
812
+ className="text-gray-400 hover:text-white"
813
+ >
814
+ <X size={20} />
815
+ </button>
816
+ </div>
817
+ <div className="flex-grow mb-4">
818
+ <textarea
819
+ value={tempSystemPrompt}
820
+ onChange={(e) => setTempSystemPrompt(e.target.value)}
821
+ className="w-full h-full bg-gray-700 text-white p-4 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-indigo-500"
822
+ placeholder="Enter your system prompt here..."
823
+ style={{ minHeight: "300px" }}
824
+ />
825
+ </div>
826
+ <div className="flex justify-between">
827
+ <button
828
+ onClick={handleResetSystemPrompt}
829
+ className="px-4 py-2 bg-teal-600 hover:bg-teal-700 rounded-lg transition-colors"
830
+ >
831
+ Reset
832
+ </button>
833
+ <div className="flex gap-3">
834
+ <button
835
+ onClick={handleSaveSystemPrompt}
836
+ className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors"
837
+ >
838
+ Save
839
+ </button>
840
+ </div>
841
+ </div>
842
+ </div>
843
+ </div>
844
+ )}
845
+ </div>
846
+ );
847
+ };
848
+
849
+ export default App;