Yassine Mhirsi commited on
Commit
5193b92
·
1 Parent(s): faa7b57

added mcp tools in chatbot settings icon

Browse files
.cursor/rules/rules.mdc CHANGED
@@ -204,6 +204,119 @@ import type { Debate } from '../types/debater.types.ts';
204
  - Keep functions pure and typed
205
  - Export for reuse
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  ## Conventions
208
 
209
  1. **Authentication**:
@@ -218,13 +331,58 @@ import type { Debate } from '../types/debater.types.ts';
218
  - Use `.tsx` for React components (e.g., `import Component from './Component.tsx'`)
219
  - Use `.ts` for TypeScript files (e.g., `import { func } from './utils/index.ts'`)
220
  - Use `/index.ts` for directory barrel exports (e.g., `import { something } from './utils/index.ts'`)
 
221
  - This is required for webpack module resolution in this project
222
- 2. **Styling**: Tailwind-first, avoid custom CSS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  3. **Error Handling**: Use ErrorBoundary for React errors, try/catch for API errors
224
  4. **Loading States**: Use `Loading` component from `components/common/Loading`
225
  5. **Environment Variables**: All must start with `REACT_APP_` prefix
226
  6. **Testing**: Use React Testing Library, co-locate tests
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  ## What NOT to Do
229
 
230
  - ❌ Don't use class components (except ErrorBoundary)
@@ -234,6 +392,10 @@ import type { Debate } from '../types/debater.types.ts';
234
  - ❌ Don't commit `.env*` files (except `.env.example`)
235
  - ❌ Don't skip TypeScript types
236
  - ❌ Don't call `isUserRegistered()` directly in components - use `useAuth()` hook instead
 
 
 
 
237
 
238
  ## When Adding New Features
239
 
 
204
  - Keep functions pure and typed
205
  - Export for reuse
206
 
207
+ ### Custom Hooks & Data Fetching
208
+ - Create custom hooks in `src/app/hooks/` for reusable data-fetching logic
209
+ - Use `useCallback` to memoize async functions and prevent unnecessary re-renders
210
+ - Implement intelligent caching with localStorage (use `cache.utils.ts`)
211
+ - Handle loading, error, and empty states explicitly
212
+ - Use defensive type checking for API responses:
213
+
214
+ ```typescript
215
+ // Example: Custom hook with caching and error handling
216
+ import { useState, useEffect, useCallback } from 'react';
217
+ import api from '../services/api-wrapper.ts';
218
+ import type { MyType } from '../types/index.ts';
219
+ import { cacheMyData, getCachedMyData } from '../utils/cache.utils.ts';
220
+
221
+ type MyDataState = {
222
+ data: MyType[];
223
+ loading: boolean;
224
+ error: Error | null;
225
+ };
226
+
227
+ const isValidData = (value: unknown): value is MyType => {
228
+ if (!value || typeof value !== 'object') return false;
229
+ const candidate = value as { id?: unknown; name?: unknown };
230
+ return typeof candidate.id === 'number' && typeof candidate.name === 'string';
231
+ };
232
+
233
+ const normalizeResponse = (payload: unknown): MyType[] | null => {
234
+ if (Array.isArray(payload)) return payload.filter(isValidData);
235
+ if (payload && typeof payload === 'object') {
236
+ const candidate = payload as { data?: unknown; items?: unknown };
237
+ if (Array.isArray(candidate.data)) return candidate.data.filter(isValidData);
238
+ if (Array.isArray(candidate.items)) return candidate.items.filter(isValidData);
239
+ }
240
+ return null;
241
+ };
242
+
243
+ export const useMyData = () => {
244
+ const [state, setState] = useState<MyDataState>({ data: [], loading: false, error: null });
245
+
246
+ const fetchData = useCallback(async () => {
247
+ try {
248
+ const cached = getCachedMyData();
249
+ if (cached?.length > 0) {
250
+ setState({ data: cached, loading: false, error: null });
251
+ } else {
252
+ setState(prev => ({ ...prev, loading: true, error: null }));
253
+ }
254
+
255
+ const payload = await api.get<unknown>('/api/endpoint');
256
+ const normalized = normalizeResponse(payload);
257
+
258
+ if (!normalized) throw new Error('Invalid response shape');
259
+
260
+ setState({ data: normalized, loading: false, error: null });
261
+ cacheMyData(normalized);
262
+ } catch (err) {
263
+ setState({
264
+ data: getCachedMyData() ?? [],
265
+ loading: false,
266
+ error: err instanceof Error ? err : new Error('Failed to fetch data'),
267
+ });
268
+ }
269
+ }, []);
270
+
271
+ useEffect(() => {
272
+ fetchData();
273
+ }, [fetchData]);
274
+
275
+ return { ...state, refetch: fetchData };
276
+ };
277
+ ```
278
+
279
+ ### UI Components & Interactions
280
+ - Use refs (`useRef`) for managing DOM elements that need direct access (e.g., dropdown containers)
281
+ - Implement click-outside detection for dropdowns/modals:
282
+ - Add `mousedown` listener (not `click`) to capture events before bubbling
283
+ - Always clean up event listeners in `useEffect` return
284
+ - Implement state visibility toggles that also trigger data refetch on open:
285
+ - Useful for keeping data fresh when user opens a dropdown/modal
286
+ - Add proper ARIA attributes for accessibility:
287
+ - `aria-expanded` on toggle buttons
288
+ - `aria-haspopup` on buttons that open menus
289
+ - Limit list item display (e.g., `slice(0, 6)`) to prevent UI overflow
290
+
291
+ ```typescript
292
+ // Dropdown component pattern
293
+ const dropdownRef = useRef<HTMLDivElement>(null);
294
+ const [showDropdown, setShowDropdown] = useState(false);
295
+
296
+ // Close on click outside
297
+ useEffect(() => {
298
+ const handleClickOutside = (event: MouseEvent) => {
299
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
300
+ setShowDropdown(false);
301
+ }
302
+ };
303
+
304
+ document.addEventListener('mousedown', handleClickOutside);
305
+ return () => {
306
+ document.removeEventListener('mousedown', handleClickOutside);
307
+ };
308
+ }, []);
309
+
310
+ // Toggle with refetch on open
311
+ const toggleDropdown = () => {
312
+ const nextState = !showDropdown;
313
+ setShowDropdown(nextState);
314
+ if (nextState) {
315
+ refetch(); // Refresh data when opening
316
+ }
317
+ };
318
+ ```
319
+
320
  ## Conventions
321
 
322
  1. **Authentication**:
 
331
  - Use `.tsx` for React components (e.g., `import Component from './Component.tsx'`)
332
  - Use `.ts` for TypeScript files (e.g., `import { func } from './utils/index.ts'`)
333
  - Use `/index.ts` for directory barrel exports (e.g., `import { something } from './utils/index.ts'`)
334
+ - Use `type` keyword for type imports (e.g., `import type { Type } from './types.ts'`)
335
  - This is required for webpack module resolution in this project
336
+ - **Import organization order**:
337
+ 1. React and React hooks (e.g., `import React, { useState } from 'react'`)
338
+ 2. Third-party libraries (e.g., `import { Icon } from 'lucide-react'`)
339
+ 3. Local imports (hooks, types, components, services, utils)
340
+ 4. Type imports last (e.g., `import type { MyType } from './types.ts'`)
341
+ 2. **Styling**:
342
+ - Tailwind-first, avoid custom CSS files
343
+ - For utility classes needed globally (e.g., `.scrollbar-hide`), add to `src/index.css` with appropriate vendor prefixes
344
+ - Use Tailwind modifiers for responsive, dark mode, and interactive states
345
+ - Example utilities to `src/index.css`:
346
+ ```css
347
+ .scrollbar-hide {
348
+ scrollbar-width: none;
349
+ }
350
+ .scrollbar-hide::-webkit-scrollbar {
351
+ display: none;
352
+ }
353
+ ```
354
  3. **Error Handling**: Use ErrorBoundary for React errors, try/catch for API errors
355
  4. **Loading States**: Use `Loading` component from `components/common/Loading`
356
  5. **Environment Variables**: All must start with `REACT_APP_` prefix
357
  6. **Testing**: Use React Testing Library, co-locate tests
358
 
359
+ ## Response Normalization & API Best Practices
360
+
361
+ - **Assume API responses may vary**: Backend APIs can return data in different shapes (direct array, `.data` property, `.items` property, etc.)
362
+ - **Always implement type guards**: Use predicate functions to validate each item before using
363
+ - **Normalize responses early**: Convert varied response shapes into a consistent internal format in hooks/services
364
+ - **Handle edge cases**: Plan for empty responses, null values, and unexpected data structures
365
+
366
+ ```typescript
367
+ // ✅ GOOD: Defensive response handling
368
+ const isValidTool = (value: unknown): value is MCPTool => {
369
+ if (!value || typeof value !== 'object') return false;
370
+ const candidate = value as { name?: unknown; description?: unknown };
371
+ return typeof candidate.name === 'string' &&
372
+ (candidate.description === undefined || typeof candidate.description === 'string');
373
+ };
374
+
375
+ const normalizeToolsResponse = (payload: unknown): MCPTool[] | null => {
376
+ if (Array.isArray(payload)) return payload.filter(isValidTool);
377
+ if (payload && typeof payload === 'object') {
378
+ const candidate = payload as { tools?: unknown; data?: unknown };
379
+ if (Array.isArray(candidate.tools)) return candidate.tools.filter(isValidTool);
380
+ if (Array.isArray(candidate.data)) return candidate.data.filter(isValidTool);
381
+ }
382
+ return null;
383
+ };
384
+ ```
385
+
386
  ## What NOT to Do
387
 
388
  - ❌ Don't use class components (except ErrorBoundary)
 
392
  - ❌ Don't commit `.env*` files (except `.env.example`)
393
  - ❌ Don't skip TypeScript types
394
  - ❌ Don't call `isUserRegistered()` directly in components - use `useAuth()` hook instead
395
+ - ❌ Don't assume API response shapes - always normalize and validate
396
+ - ❌ Don't forget to clean up event listeners in useEffect return statements
397
+ - ❌ Don't use inline anonymous functions in onClick/onChange - prefer named handlers
398
+ - ❌ Don't skip error states in loading components - implement proper error UI
399
 
400
  ## When Adding New Features
401
 
.gitignore CHANGED
@@ -89,3 +89,4 @@ tmp/
89
  temp/
90
  QWEN.md
91
  .qwen/rules.md
 
 
89
  temp/
90
  QWEN.md
91
  .qwen/rules.md
92
+ .windsurf/rules/rules.md
package-lock.json CHANGED
@@ -22,6 +22,8 @@
22
  "web-vitals": "^2.1.4"
23
  },
24
  "devDependencies": {
 
 
25
  "autoprefixer": "^10.4.22",
26
  "postcss": "^8.5.6",
27
  "tailwindcss": "^3.4.18"
@@ -3870,6 +3872,26 @@
3870
  "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
3871
  "license": "MIT"
3872
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3873
  "node_modules/@types/resolve": {
3874
  "version": "1.17.1",
3875
  "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -6482,6 +6504,13 @@
6482
  "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
6483
  "license": "MIT"
6484
  },
 
 
 
 
 
 
 
6485
  "node_modules/d3-array": {
6486
  "version": "3.2.4",
6487
  "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
 
22
  "web-vitals": "^2.1.4"
23
  },
24
  "devDependencies": {
25
+ "@types/react": "^19.2.7",
26
+ "@types/react-dom": "^19.2.3",
27
  "autoprefixer": "^10.4.22",
28
  "postcss": "^8.5.6",
29
  "tailwindcss": "^3.4.18"
 
3872
  "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
3873
  "license": "MIT"
3874
  },
3875
+ "node_modules/@types/react": {
3876
+ "version": "19.2.7",
3877
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
3878
+ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
3879
+ "devOptional": true,
3880
+ "license": "MIT",
3881
+ "dependencies": {
3882
+ "csstype": "^3.2.2"
3883
+ }
3884
+ },
3885
+ "node_modules/@types/react-dom": {
3886
+ "version": "19.2.3",
3887
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
3888
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
3889
+ "devOptional": true,
3890
+ "license": "MIT",
3891
+ "peerDependencies": {
3892
+ "@types/react": "^19.2.0"
3893
+ }
3894
+ },
3895
  "node_modules/@types/resolve": {
3896
  "version": "1.17.1",
3897
  "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
 
6504
  "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
6505
  "license": "MIT"
6506
  },
6507
+ "node_modules/csstype": {
6508
+ "version": "3.2.3",
6509
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
6510
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
6511
+ "devOptional": true,
6512
+ "license": "MIT"
6513
+ },
6514
  "node_modules/d3-array": {
6515
  "version": "3.2.4",
6516
  "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
package.json CHANGED
@@ -17,6 +17,8 @@
17
  "web-vitals": "^2.1.4"
18
  },
19
  "devDependencies": {
 
 
20
  "autoprefixer": "^10.4.22",
21
  "postcss": "^8.5.6",
22
  "tailwindcss": "^3.4.18"
 
17
  "web-vitals": "^2.1.4"
18
  },
19
  "devDependencies": {
20
+ "@types/react": "^19.2.7",
21
+ "@types/react-dom": "^19.2.3",
22
  "autoprefixer": "^10.4.22",
23
  "postcss": "^8.5.6",
24
  "tailwindcss": "^3.4.18"
src/app/components/chat/ChatInput.tsx CHANGED
@@ -1,14 +1,25 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { Plus, ArrowUp, Settings2, Mic, X, Check } from 'lucide-react';
 
 
3
 
4
  type ChatInputProps = {
5
  onSubmit?: (message: string) => void;
6
  placeholder?: string;
7
  };
8
 
 
 
 
 
 
9
  const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputProps) => {
10
  const [input, setInput] = useState('');
11
  const [isRecording, setIsRecording] = useState(false);
 
 
 
 
12
 
13
  const handleSubmit = (e: any) => {
14
  e.preventDefault();
@@ -25,7 +36,7 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
25
  setIsRecording(true);
26
  setTimeout(() => {
27
  setIsRecording(false);
28
- setInput('When speech to text feature ?');
29
  }, 5000);
30
  };
31
 
@@ -35,7 +46,7 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
35
 
36
  const handleConfirmRecording = () => {
37
  setIsRecording(false);
38
- setInput('When speech to text feature ?');
39
  };
40
 
41
  const WaveAnimation = () => {
@@ -74,11 +85,34 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
74
  );
75
  };
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  return (
78
  <div className="relative">
79
  <form onSubmit={handleSubmit} className="relative">
80
  <div
81
- className="border border-zinc-300 dark:border-zinc-700 rounded-2xl p-4 relative transition-all duration-500 ease-in-out overflow-hidden bg-zinc-100 dark:bg-[#141415]"
82
  >
83
  {isRecording ? (
84
  <div className="flex items-center justify-between h-12 animate-fade-in w-full">
@@ -124,12 +158,56 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
124
  <Plus className="h-5 w-5" />
125
  </button>
126
 
127
- <button
128
- type="button"
129
- className="h-8 w-8 p-0 text-zinc-800 dark:text-white hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-lg transition-all duration-200 hover:scale-110 flex items-center justify-center"
130
- >
131
- <Settings2 className="h-5 w-5" />
132
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  <button
135
  type="button"
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { Plus, ArrowUp, Settings2, Mic, X, Check, Loader2 } from 'lucide-react';
3
+ import { useMCPTools } from '../../hooks/useMCPTools.ts';
4
+ import type { MCPTool } from '../../types/index.ts';
5
 
6
  type ChatInputProps = {
7
  onSubmit?: (message: string) => void;
8
  placeholder?: string;
9
  };
10
 
11
+ // Type guard to ensure tool type safety (used for runtime validation if needed)
12
+ const isMCPTool = (value: any): value is MCPTool => {
13
+ return value && typeof value === 'object' && typeof value.name === 'string';
14
+ };
15
+
16
  const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputProps) => {
17
  const [input, setInput] = useState('');
18
  const [isRecording, setIsRecording] = useState(false);
19
+ const [showToolsDropdown, setShowToolsDropdown] = useState(false);
20
+ const [selectedTool, setSelectedTool] = useState<string | null>(null);
21
+ const dropdownRef = useRef<HTMLDivElement>(null);
22
+ const { tools, loading, error, refetch } = useMCPTools();
23
 
24
  const handleSubmit = (e: any) => {
25
  e.preventDefault();
 
36
  setIsRecording(true);
37
  setTimeout(() => {
38
  setIsRecording(false);
39
+ setInput('speech to text feature');
40
  }, 5000);
41
  };
42
 
 
46
 
47
  const handleConfirmRecording = () => {
48
  setIsRecording(false);
49
+ setInput('speech to text feature');
50
  };
51
 
52
  const WaveAnimation = () => {
 
85
  );
86
  };
87
 
88
+ // Close dropdown when clicking outside
89
+ useEffect(() => {
90
+ const handleClickOutside = (event: MouseEvent) => {
91
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
92
+ setShowToolsDropdown(false);
93
+ }
94
+ };
95
+
96
+ document.addEventListener('mousedown', handleClickOutside);
97
+ return () => {
98
+ document.removeEventListener('mousedown', handleClickOutside);
99
+ };
100
+ }, []);
101
+
102
+ const toggleToolsDropdown = () => {
103
+ const nextState = !showToolsDropdown;
104
+ setShowToolsDropdown(nextState);
105
+
106
+ if (nextState) {
107
+ refetch();
108
+ }
109
+ };
110
+
111
  return (
112
  <div className="relative">
113
  <form onSubmit={handleSubmit} className="relative">
114
  <div
115
+ className="border border-zinc-300 dark:border-zinc-700 rounded-2xl p-4 relative transition-all duration-500 ease-in-out overflow-visible bg-zinc-100 dark:bg-[#141415]"
116
  >
117
  {isRecording ? (
118
  <div className="flex items-center justify-between h-12 animate-fade-in w-full">
 
158
  <Plus className="h-5 w-5" />
159
  </button>
160
 
161
+ <div className="relative" ref={dropdownRef}>
162
+ <button
163
+ type="button"
164
+ onClick={toggleToolsDropdown}
165
+ className={`h-8 w-8 p-0 rounded-lg transition-all duration-200 hover:scale-110 flex items-center justify-center ${selectedTool ? 'bg-teal-500/15 text-teal-600 dark:bg-teal-500/20 dark:text-teal-300 hover:bg-teal-500/25 dark:hover:bg-teal-500/30' : 'text-zinc-800 dark:text-white hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-200 dark:hover:bg-zinc-700'}`}
166
+ aria-expanded={showToolsDropdown}
167
+ aria-haspopup="true"
168
+ >
169
+ <Settings2 className="h-5 w-5" />
170
+ </button>
171
+
172
+ {showToolsDropdown && (
173
+ <div className="absolute top-[calc(100%+0.6rem)] left-0 w-fit min-w-[9rem] max-w-[20rem] rounded-2xl border border-zinc-200/70 dark:border-zinc-700/60 bg-white/95 dark:bg-zinc-900/90 backdrop-blur-xl shadow-[0_20px_45px_-20px_rgba(12,12,12,0.75)] z-50 overflow-hidden">
174
+ <div className="px-3 py-3 max-h-64 overflow-y-auto scrollbar-hide">
175
+ {loading ? (
176
+ <div className="flex items-center justify-center gap-3 rounded-xl bg-zinc-100/80 dark:bg-zinc-800/70 px-4 py-3 text-sm text-zinc-500 dark:text-zinc-300">
177
+ <Loader2 className="h-4 w-4 animate-spin" />
178
+ Loading tools…
179
+ </div>
180
+ ) : error ? (
181
+ <div className="rounded-xl border border-red-400/50 bg-red-50/80 px-4 py-3 text-sm text-red-600 dark:border-red-500/40 dark:bg-red-900/20 dark:text-red-300">
182
+ Failed to load tools. Please try again.
183
+ </div>
184
+ ) : tools.length === 0 ? (
185
+ <div className="rounded-xl bg-zinc-100/70 px-4 py-3 text-sm text-zinc-500 dark:bg-zinc-800/70 dark:text-zinc-300">
186
+ No tools available yet.
187
+ </div>
188
+ ) : (
189
+ <ul className="space-y-2">
190
+ {tools.slice(0, 6).filter(isMCPTool).map((tool, index) => (
191
+ <li key={tool.name || index}>
192
+ <button
193
+ type="button"
194
+ className={`w-full rounded-xl border border-transparent bg-gradient-to-r from-zinc-100/70 via-white to-zinc-100/70 px-4 py-3 text-left text-sm font-semibold text-zinc-800 transition-all duration-200 hover:-translate-y-0.5 hover:border-teal-300/80 hover:bg-white/90 hover:shadow-lg dark:from-zinc-800/50 dark:via-zinc-900/60 dark:to-zinc-800/50 dark:text-zinc-50 dark:hover:border-teal-400/70 dark:hover:bg-zinc-700/70 flex items-center justify-between gap-3 whitespace-nowrap ${selectedTool === tool.name ? 'border-teal-300/70 dark:border-teal-400/70' : ''}`}
195
+ onClick={() => {
196
+ setSelectedTool((current) => (current === tool.name ? null : tool.name));
197
+ setShowToolsDropdown(false);
198
+ }}
199
+ >
200
+ <span className="truncate">{tool.name}</span>
201
+ {selectedTool === tool.name && <Check className="h-4 w-4 text-teal-400 dark:text-teal-300" />}
202
+ </button>
203
+ </li>
204
+ ))}
205
+ </ul>
206
+ )}
207
+ </div>
208
+ </div>
209
+ )}
210
+ </div>
211
 
212
  <button
213
  type="button"
src/app/constants/index.ts CHANGED
@@ -10,6 +10,7 @@ export const API_ENDPOINTS = {
10
  USER_UPDATE_NAME: '/api/v1/user/me/name',
11
  ANALYSIS: '/api/v1/analyse',
12
  ANALYSIS_CSV: '/api/v1/analyse/csv',
 
13
  } as const;
14
 
15
  // App configuration
 
10
  USER_UPDATE_NAME: '/api/v1/user/me/name',
11
  ANALYSIS: '/api/v1/analyse',
12
  ANALYSIS_CSV: '/api/v1/analyse/csv',
13
+ MCP_TOOLS: '/api/v1/mcp/tools',
14
  } as const;
15
 
16
  // App configuration
src/app/hooks/useMCPTools.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import api from '../services/api-wrapper.ts';
3
+ import type { MCPTool } from '../types/index.ts';
4
+ import { API_ENDPOINTS } from '../constants/index.ts';
5
+ import { cacheMcpTools, getCachedMcpTools } from '../utils/cache.utils.ts';
6
+
7
+ type MCPToolsState = {
8
+ tools: MCPTool[];
9
+ loading: boolean;
10
+ error: Error | null;
11
+ };
12
+
13
+ const INITIAL_STATE = {
14
+ tools: [] as MCPTool[],
15
+ loading: false,
16
+ error: null,
17
+ } as MCPToolsState;
18
+
19
+ const isMCPTool = (value: unknown): value is MCPTool => {
20
+ if (!value || typeof value !== 'object') {
21
+ return false;
22
+ }
23
+
24
+ const candidate = value as { name?: unknown; description?: unknown };
25
+ return typeof candidate.name === 'string' &&
26
+ (candidate.description === undefined || typeof candidate.description === 'string');
27
+ };
28
+
29
+ const normalizeToolsResponse = (payload: unknown): MCPTool[] | null => {
30
+ if (Array.isArray(payload)) {
31
+ return payload.filter(isMCPTool);
32
+ }
33
+
34
+ if (payload && typeof payload === 'object') {
35
+ const candidate = payload as { tools?: unknown; data?: unknown };
36
+ if (Array.isArray(candidate.tools)) {
37
+ return candidate.tools.filter(isMCPTool);
38
+ }
39
+
40
+ if (Array.isArray(candidate.data)) {
41
+ return candidate.data.filter(isMCPTool);
42
+ }
43
+ }
44
+
45
+ return null;
46
+ };
47
+
48
+ export const useMCPTools = () => {
49
+ const [state, setState] = useState(INITIAL_STATE);
50
+
51
+ const fetchTools = useCallback(async () => {
52
+ try {
53
+ const cachedTools = getCachedMcpTools();
54
+ if (cachedTools && cachedTools.length > 0) {
55
+ setState({ tools: cachedTools, loading: false, error: null });
56
+ } else {
57
+ setState((prev) => ({ ...prev, loading: true, error: null }));
58
+ }
59
+
60
+ const payload = await api.get<unknown>(API_ENDPOINTS.MCP_TOOLS);
61
+ const parsedTools = normalizeToolsResponse(payload);
62
+
63
+ if (!parsedTools) {
64
+ throw new Error('Invalid MCP tools response shape');
65
+ }
66
+
67
+ setState({ tools: parsedTools, loading: false, error: null });
68
+ cacheMcpTools(parsedTools);
69
+ } catch (err) {
70
+ setState({
71
+ tools: getCachedMcpTools() ?? [],
72
+ loading: false,
73
+ error: err instanceof Error ? err : new Error('Failed to fetch MCP tools'),
74
+ });
75
+ }
76
+ }, []);
77
+
78
+ useEffect(() => {
79
+ fetchTools();
80
+ }, [fetchTools]);
81
+
82
+ return { ...state, refetch: fetchTools };
83
+ };
src/app/types/index.ts CHANGED
@@ -6,4 +6,5 @@
6
  export * from './api.types.ts';
7
  export * from './analysis.types.ts';
8
  export * from './user.types.ts';
 
9
 
 
6
  export * from './api.types.ts';
7
  export * from './analysis.types.ts';
8
  export * from './user.types.ts';
9
+ export * from './mcp.types.ts';
10
 
src/app/types/mcp.types.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Types related to Model Context Protocol tools
3
+ */
4
+
5
+ export type MCPTool = {
6
+ name: string;
7
+ description?: string;
8
+ };
src/app/utils/cache.utils.ts CHANGED
@@ -3,15 +3,24 @@
3
  */
4
 
5
  import type { AnalysisResult } from '../types/analysis.types.ts';
 
6
 
7
  const CACHE_KEY = 'analysis_data_cache';
8
  const CACHE_TIMESTAMP_KEY = 'analysis_data_cache_timestamp';
9
 
 
 
 
10
  type CachedAnalysisData = {
11
  results: AnalysisResult[];
12
  timestamp: number;
13
  };
14
 
 
 
 
 
 
15
  /**
16
  * Cache analysis results to localStorage
17
  */
@@ -70,3 +79,41 @@ export function clearAnalysisCache(): void {
70
  }
71
  }
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  */
4
 
5
  import type { AnalysisResult } from '../types/analysis.types.ts';
6
+ import type { MCPTool } from '../types/mcp.types.ts';
7
 
8
  const CACHE_KEY = 'analysis_data_cache';
9
  const CACHE_TIMESTAMP_KEY = 'analysis_data_cache_timestamp';
10
 
11
+ const MCP_TOOLS_CACHE_KEY = 'mcp_tools_cache';
12
+ const MCP_TOOLS_CACHE_TIMESTAMP_KEY = 'mcp_tools_cache_timestamp';
13
+
14
  type CachedAnalysisData = {
15
  results: AnalysisResult[];
16
  timestamp: number;
17
  };
18
 
19
+ type CachedMCPToolsData = {
20
+ tools: MCPTool[];
21
+ timestamp: number;
22
+ };
23
+
24
  /**
25
  * Cache analysis results to localStorage
26
  */
 
79
  }
80
  }
81
 
82
+ export function cacheMcpTools(tools: MCPTool[]): void {
83
+ try {
84
+ const cacheData: CachedMCPToolsData = {
85
+ tools,
86
+ timestamp: Date.now(),
87
+ };
88
+ localStorage.setItem(MCP_TOOLS_CACHE_KEY, JSON.stringify(cacheData));
89
+ localStorage.setItem(
90
+ MCP_TOOLS_CACHE_TIMESTAMP_KEY,
91
+ String(cacheData.timestamp)
92
+ );
93
+ } catch (error) {
94
+ console.warn('Failed to cache MCP tools:', error);
95
+ }
96
+ }
97
+
98
+ export function getCachedMcpTools(): MCPTool[] | null {
99
+ try {
100
+ const cached = localStorage.getItem(MCP_TOOLS_CACHE_KEY);
101
+ if (!cached) return null;
102
+
103
+ const cacheData: CachedMCPToolsData = JSON.parse(cached);
104
+ return cacheData.tools;
105
+ } catch (error) {
106
+ console.warn('Failed to retrieve cached MCP tools:', error);
107
+ return null;
108
+ }
109
+ }
110
+
111
+ export function clearMcpToolsCache(): void {
112
+ try {
113
+ localStorage.removeItem(MCP_TOOLS_CACHE_KEY);
114
+ localStorage.removeItem(MCP_TOOLS_CACHE_TIMESTAMP_KEY);
115
+ } catch (error) {
116
+ console.warn('Failed to clear MCP tools cache:', error);
117
+ }
118
+ }
119
+
src/index.css CHANGED
@@ -17,3 +17,11 @@ body {
17
  margin: 0;
18
  min-height: 100vh;
19
  }
 
 
 
 
 
 
 
 
 
17
  margin: 0;
18
  min-height: 100vh;
19
  }
20
+
21
+ .scrollbar-hide {
22
+ scrollbar-width: none;
23
+ }
24
+
25
+ .scrollbar-hide::-webkit-scrollbar {
26
+ display: none;
27
+ }