shreyask commited on
Commit
4513a96
·
verified ·
1 Parent(s): 8c21d03

feat: implement debounced auto-save for conversations and enhance ResultBlock component with truncation support

Browse files
src/App.tsx CHANGED
@@ -10,7 +10,6 @@ import {
10
  Play,
11
  Plus,
12
  Zap,
13
- RotateCcw,
14
  Settings,
15
  X,
16
  PanelRightClose,
@@ -170,6 +169,7 @@ const App: React.FC = () => {
170
  const [isConversationsPanelVisible, setIsConversationsPanelVisible] = useState<boolean>(false);
171
  const chatContainerRef = useRef<HTMLDivElement>(null);
172
  const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
 
173
  const toolsContainerRef = useRef<HTMLDivElement>(null);
174
  const inputRef = useRef<HTMLInputElement>(null);
175
  const {
@@ -278,32 +278,48 @@ const App: React.FC = () => {
278
  }, []);
279
 
280
  // Auto-save current conversation when messages change
 
281
  useEffect(() => {
282
  if (messages.length === 0) return;
283
 
284
- const saveCurrentConversation = async () => {
285
- const title = generateConversationTitle(messages);
286
- const conversation: Conversation = {
287
- ...(currentConversationId ? { id: currentConversationId } : {}),
288
- title,
289
- messages,
290
- createdAt: currentConversationId ? conversations.find(c => c.id === currentConversationId)?.createdAt || Date.now() : Date.now(),
291
- updatedAt: Date.now(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  };
293
 
294
- const id = await saveConversation(conversation);
295
- if (!currentConversationId) {
296
- setCurrentConversationId(id);
297
- }
298
 
299
- // Reload conversations list
300
- const updatedConversations = await loadAllConversations();
301
- setConversations(updatedConversations);
 
 
302
  };
303
-
304
- saveCurrentConversation().catch((error) => {
305
- console.error("Failed to save conversation:", error);
306
- });
307
  }, [messages, currentConversationId, conversations]);
308
 
309
  const updateToolInDB = async (tool: Tool): Promise<void> => {
@@ -595,22 +611,50 @@ const App: React.FC = () => {
595
  setMessages([...currentMessages, { role: "assistant", content: "" }]);
596
 
597
  let accumulatedContent = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  const response = await generateResponse(
599
  messagesForGeneration,
600
  toolSchemas,
601
  (token: string) => {
602
  accumulatedContent += token;
603
- setMessages((current) => {
604
- const updated = [...current];
605
- updated[updated.length - 1] = {
606
- role: "assistant",
607
- content: accumulatedContent,
608
- };
609
- return updated;
610
- });
 
 
 
 
 
 
611
  }
612
  );
613
 
 
 
 
 
 
614
  currentMessages.push({ role: "assistant", content: response });
615
  const toolCallContent = extractToolCallContent(response);
616
 
@@ -760,22 +804,50 @@ const App: React.FC = () => {
760
  setMessages([...currentMessages, { role: "assistant", content: "" }]);
761
 
762
  let accumulatedContent = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  const response = await generateResponse(
764
  messagesForGeneration,
765
  toolSchemas,
766
  (token: string) => {
767
  accumulatedContent += token;
768
- setMessages((current) => {
769
- const updated = [...current];
770
- updated[updated.length - 1] = {
771
- role: "assistant",
772
- content: accumulatedContent,
773
- };
774
- return updated;
775
- });
 
 
 
 
 
 
776
  }
777
  );
778
 
 
 
 
 
 
779
  currentMessages.push({ role: "assistant", content: response });
780
  const toolCallContent = extractToolCallContent(response);
781
 
 
10
  Play,
11
  Plus,
12
  Zap,
 
13
  Settings,
14
  X,
15
  PanelRightClose,
 
169
  const [isConversationsPanelVisible, setIsConversationsPanelVisible] = useState<boolean>(false);
170
  const chatContainerRef = useRef<HTMLDivElement>(null);
171
  const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
172
+ const conversationSaveTimer = useRef<NodeJS.Timeout | null>(null);
173
  const toolsContainerRef = useRef<HTMLDivElement>(null);
174
  const inputRef = useRef<HTMLInputElement>(null);
175
  const {
 
278
  }, []);
279
 
280
  // Auto-save current conversation when messages change
281
+ // Debounced conversation auto-save to prevent excessive IndexedDB writes
282
  useEffect(() => {
283
  if (messages.length === 0) return;
284
 
285
+ // Clear existing timer
286
+ if (conversationSaveTimer.current) {
287
+ clearTimeout(conversationSaveTimer.current);
288
+ }
289
+
290
+ // Set new timer to save after 1 second of inactivity
291
+ conversationSaveTimer.current = setTimeout(() => {
292
+ const saveCurrentConversation = async () => {
293
+ const title = generateConversationTitle(messages);
294
+ const conversation: Conversation = {
295
+ ...(currentConversationId ? { id: currentConversationId } : {}),
296
+ title,
297
+ messages,
298
+ createdAt: currentConversationId ? conversations.find(c => c.id === currentConversationId)?.createdAt || Date.now() : Date.now(),
299
+ updatedAt: Date.now(),
300
+ };
301
+
302
+ const id = await saveConversation(conversation);
303
+ if (!currentConversationId) {
304
+ setCurrentConversationId(id);
305
+ }
306
+
307
+ // Reload conversations list
308
+ const updatedConversations = await loadAllConversations();
309
+ setConversations(updatedConversations);
310
  };
311
 
312
+ saveCurrentConversation().catch((error) => {
313
+ console.error("Failed to save conversation:", error);
314
+ });
315
+ }, 1000);
316
 
317
+ // Cleanup on unmount
318
+ return () => {
319
+ if (conversationSaveTimer.current) {
320
+ clearTimeout(conversationSaveTimer.current);
321
+ }
322
  };
 
 
 
 
323
  }, [messages, currentConversationId, conversations]);
324
 
325
  const updateToolInDB = async (tool: Tool): Promise<void> => {
 
611
  setMessages([...currentMessages, { role: "assistant", content: "" }]);
612
 
613
  let accumulatedContent = "";
614
+ let lastUpdateTime = 0;
615
+ let pendingUpdate = false;
616
+ const THROTTLE_MS = 50; // Update UI at most every 50ms
617
+
618
+ const updateUI = () => {
619
+ setMessages((current) => {
620
+ const updated = [...current];
621
+ updated[updated.length - 1] = {
622
+ role: "assistant",
623
+ content: accumulatedContent,
624
+ };
625
+ return updated;
626
+ });
627
+ lastUpdateTime = Date.now();
628
+ pendingUpdate = false;
629
+ };
630
+
631
  const response = await generateResponse(
632
  messagesForGeneration,
633
  toolSchemas,
634
  (token: string) => {
635
  accumulatedContent += token;
636
+ const now = Date.now();
637
+
638
+ // Throttle updates: only update if enough time has passed
639
+ if (now - lastUpdateTime >= THROTTLE_MS) {
640
+ updateUI();
641
+ } else if (!pendingUpdate) {
642
+ // Schedule an update if one isn't already pending
643
+ pendingUpdate = true;
644
+ setTimeout(() => {
645
+ if (pendingUpdate) {
646
+ updateUI();
647
+ }
648
+ }, THROTTLE_MS - (now - lastUpdateTime));
649
+ }
650
  }
651
  );
652
 
653
+ // Ensure final update is applied
654
+ if (accumulatedContent !== "") {
655
+ updateUI();
656
+ }
657
+
658
  currentMessages.push({ role: "assistant", content: response });
659
  const toolCallContent = extractToolCallContent(response);
660
 
 
804
  setMessages([...currentMessages, { role: "assistant", content: "" }]);
805
 
806
  let accumulatedContent = "";
807
+ let lastUpdateTime = 0;
808
+ let pendingUpdate = false;
809
+ const THROTTLE_MS = 50; // Update UI at most every 50ms
810
+
811
+ const updateUI = () => {
812
+ setMessages((current) => {
813
+ const updated = [...current];
814
+ updated[updated.length - 1] = {
815
+ role: "assistant",
816
+ content: accumulatedContent,
817
+ };
818
+ return updated;
819
+ });
820
+ lastUpdateTime = Date.now();
821
+ pendingUpdate = false;
822
+ };
823
+
824
  const response = await generateResponse(
825
  messagesForGeneration,
826
  toolSchemas,
827
  (token: string) => {
828
  accumulatedContent += token;
829
+ const now = Date.now();
830
+
831
+ // Throttle updates: only update if enough time has passed
832
+ if (now - lastUpdateTime >= THROTTLE_MS) {
833
+ updateUI();
834
+ } else if (!pendingUpdate) {
835
+ // Schedule an update if one isn't already pending
836
+ pendingUpdate = true;
837
+ setTimeout(() => {
838
+ if (pendingUpdate) {
839
+ updateUI();
840
+ }
841
+ }, THROTTLE_MS - (now - lastUpdateTime));
842
+ }
843
  }
844
  );
845
 
846
+ // Ensure final update is applied
847
+ if (accumulatedContent !== "") {
848
+ updateUI();
849
+ }
850
+
851
  currentMessages.push({ role: "assistant", content: response });
852
  const toolCallContent = extractToolCallContent(response);
853
 
src/components/ResultBlock.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import type React from "react";
2
 
3
  interface ResultBlockProps {
4
  error?: string;
@@ -8,21 +8,68 @@ interface ResultBlockProps {
8
  const ResultBlock: React.FC<ResultBlockProps> = ({
9
  error,
10
  result,
11
- }) => (
12
- <div
13
- className={
14
- error
15
- ? "bg-red-900 border border-red-600 rounded p-3"
16
- : "bg-gray-700 border border-gray-600 rounded p-3"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
18
- >
19
- {error ? <p className="text-red-300 text-sm">Error: {error}</p> : null}
20
- <pre className="text-sm text-gray-300 whitespace-pre-wrap overflow-auto mt-2">
21
- {result !== undefined && result !== null
22
- ? (typeof result === "object" ? JSON.stringify(result, null, 2) : String(result))
23
- : "No result"}
24
- </pre>
25
- </div>
26
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  export default ResultBlock;
 
1
+ import React, { useState, useMemo } from "react";
2
 
3
  interface ResultBlockProps {
4
  error?: string;
 
8
  const ResultBlock: React.FC<ResultBlockProps> = ({
9
  error,
10
  result,
11
+ }) => {
12
+ const [isExpanded, setIsExpanded] = useState(false);
13
+ const MAX_LENGTH = 10000; // Characters to show before truncating
14
+
15
+ const formattedResult = useMemo(() => {
16
+ if (result === undefined || result === null) {
17
+ return "No result";
18
+ }
19
+
20
+ if (typeof result !== "object") {
21
+ return String(result);
22
+ }
23
+
24
+ try {
25
+ const fullString = JSON.stringify(result, null, 2);
26
+
27
+ // If the result is very large, provide truncation
28
+ if (fullString.length > MAX_LENGTH && !isExpanded) {
29
+ return fullString.substring(0, MAX_LENGTH);
30
+ }
31
+
32
+ return fullString;
33
+ } catch (err) {
34
+ // Handle circular references or other JSON.stringify errors
35
+ return String(result);
36
  }
37
+ }, [result, isExpanded]);
38
+
39
+ const isTruncated = useMemo(() => {
40
+ if (typeof result !== "object" || result === null) return false;
41
+ try {
42
+ return JSON.stringify(result, null, 2).length > MAX_LENGTH;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }, [result]);
47
+
48
+ return (
49
+ <div
50
+ className={
51
+ error
52
+ ? "bg-red-900 border border-red-600 rounded p-3"
53
+ : "bg-gray-700 border border-gray-600 rounded p-3"
54
+ }
55
+ >
56
+ {error ? <p className="text-red-300 text-sm">Error: {error}</p> : null}
57
+ <pre className="text-sm text-gray-300 whitespace-pre-wrap overflow-auto mt-2 max-h-96">
58
+ {formattedResult}
59
+ {isTruncated && !isExpanded && (
60
+ <span className="text-yellow-400">... (truncated)</span>
61
+ )}
62
+ </pre>
63
+ {isTruncated && (
64
+ <button
65
+ onClick={() => setIsExpanded(!isExpanded)}
66
+ className="mt-2 text-xs text-blue-400 hover:text-blue-300 underline"
67
+ >
68
+ {isExpanded ? "Show less" : "Show full result"}
69
+ </button>
70
+ )}
71
+ </div>
72
+ );
73
+ };
74
 
75
  export default ResultBlock;
src/services/mcpClient.ts CHANGED
@@ -157,11 +157,7 @@ export class MCPClientService {
157
  version: MCP_CLIENT_CONFIG.VERSION,
158
  },
159
  {
160
- capabilities: {
161
- tools: {},
162
- resources: {},
163
- prompts: {},
164
- },
165
  }
166
  );
167
 
@@ -378,9 +374,7 @@ export class MCPClientService {
378
  version: MCP_CLIENT_CONFIG.VERSION,
379
  },
380
  {
381
- capabilities: {
382
- tools: {},
383
- },
384
  }
385
  );
386
 
 
157
  version: MCP_CLIENT_CONFIG.VERSION,
158
  },
159
  {
160
+ capabilities: {},
 
 
 
 
161
  }
162
  );
163
 
 
374
  version: MCP_CLIENT_CONFIG.VERSION,
375
  },
376
  {
377
+ capabilities: {},
 
 
378
  }
379
  );
380
 
src/services/oauth.ts CHANGED
@@ -5,12 +5,22 @@ import {
5
  exchangeAuthorization,
6
  registerClient,
7
  } from "@modelcontextprotocol/sdk/client/auth.js";
 
8
  import { secureStorage } from "../utils/storage";
9
  import { MCP_CLIENT_CONFIG, STORAGE_KEYS, DEFAULTS } from "../config/constants";
 
 
 
 
 
 
 
 
 
10
  // Utility to fetch .well-known/modelcontextprotocol for OAuth endpoints
11
  export async function discoverOAuthEndpoints(serverUrl: string) {
12
  // ...existing code...
13
- let resourceMetadata, authMetadata, authorizationServerUrl;
14
  try {
15
  resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl);
16
  if (resourceMetadata?.authorization_servers?.length) {
 
5
  exchangeAuthorization,
6
  registerClient,
7
  } from "@modelcontextprotocol/sdk/client/auth.js";
8
+ import type { AuthorizationServerMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
9
  import { secureStorage } from "../utils/storage";
10
  import { MCP_CLIENT_CONFIG, STORAGE_KEYS, DEFAULTS } from "../config/constants";
11
+
12
+ // Extended type to include custom properties for OAuth flow
13
+ type ExtendedAuthMetadata = AuthorizationServerMetadata & {
14
+ client_id?: string;
15
+ client_secret?: string;
16
+ redirect_uri?: string;
17
+ scopes?: string[];
18
+ };
19
+
20
  // Utility to fetch .well-known/modelcontextprotocol for OAuth endpoints
21
  export async function discoverOAuthEndpoints(serverUrl: string) {
22
  // ...existing code...
23
+ let resourceMetadata, authMetadata: ExtendedAuthMetadata | undefined, authorizationServerUrl;
24
  try {
25
  resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl);
26
  if (resourceMetadata?.authorization_servers?.length) {