Yassine Mhirsi commited on
Commit
c01020f
·
1 Parent(s): 9b2d092

inhance dropdown in ChatInput component

Browse files
Files changed (1) hide show
  1. src/app/components/chat/ChatInput.tsx +145 -135
src/app/components/chat/ChatInput.tsx CHANGED
@@ -159,24 +159,24 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
159
  const viewportWidth = window.innerWidth;
160
  const spaceBelow = viewportHeight - buttonRect.bottom;
161
  const spaceAbove = buttonRect.top;
162
- const dropdownHeight = 400; // Approximate max height
163
  const minSpace = 20; // Minimum space from viewport edge
164
 
165
  // Determine if dropdown should be above or below
166
  if (spaceBelow < dropdownHeight + minSpace && spaceAbove > spaceBelow) {
167
  setDropdownPosition('above');
168
  // Calculate max height based on available space above
169
- const maxHeight = Math.min(320, spaceAbove - minSpace - 60); // 60px for padding/margins
170
- setDropdownMaxHeight(Math.max(200, maxHeight));
171
  } else {
172
  setDropdownPosition('below');
173
  // Calculate max height based on available space below
174
- const maxHeight = Math.min(320, spaceBelow - minSpace - 60);
175
- setDropdownMaxHeight(Math.max(200, maxHeight));
176
  }
177
 
178
  // Adjust horizontal position if dropdown would overflow
179
- const dropdownWidth = 320; // w-80 = 320px
180
  const dropdownElement = dropdownRef.current?.querySelector('[data-dropdown-content]') as HTMLElement;
181
  if (dropdownElement) {
182
  if (buttonRect.left + dropdownWidth > viewportWidth - minSpace) {
@@ -204,6 +204,13 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
204
  }
205
  }, [showToolsDropdown]);
206
 
 
 
 
 
 
 
 
207
  const toggleToolsDropdown = () => {
208
  const nextState = !showToolsDropdown;
209
  setShowToolsDropdown(nextState);
@@ -279,147 +286,150 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
279
  )}
280
  </button>
281
 
282
- {showToolsDropdown && (
283
- <div
284
- ref={dropdownContentRef}
285
- data-dropdown-content
286
- className={`absolute left-0 w-80 rounded-2xl border border-zinc-200/70 dark:border-zinc-700/60 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xl shadow-[0_20px_45px_-20px_rgba(12,12,12,0.75)] z-50 overflow-hidden animate-fade-in flex flex-col ${
287
- dropdownPosition === 'above'
288
- ? 'bottom-[calc(100%+0.6rem)]'
289
- : 'top-[calc(100%+0.6rem)]'
290
- }`}
291
- style={{ maxHeight: `${dropdownMaxHeight}px` }}
292
- >
293
- {/* Header */}
294
- <div className="px-4 py-3 border-b border-zinc-200/50 dark:border-zinc-700/50 flex-shrink-0">
295
- <div className="flex items-center gap-2 mb-3">
296
- <Sparkles className="h-4 w-4 text-teal-500 dark:text-teal-400" />
297
- <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Available Tools</h3>
298
- <div className="ml-auto">
299
- <span className="text-xs text-zinc-500 dark:text-zinc-400 bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-full">
300
- {tools.filter(isMCPTool).length} tools
301
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  </div>
303
  </div>
304
 
305
- {/* Search input */}
306
- <div className="relative">
307
- <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-zinc-400 dark:text-zinc-500" />
308
- <input
309
- ref={searchInputRef}
310
- type="text"
311
- placeholder="Search tools..."
312
- value={searchQuery}
313
- onChange={(e) => {
314
- setSearchQuery(e.target.value);
315
- setFocusedIndex(-1);
316
- }}
317
- className="w-full pl-10 pr-4 py-2 text-sm bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-zinc-100 placeholder-zinc-500 dark:placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:border-teal-500/50 transition-all duration-200"
318
- />
319
- </div>
320
- </div>
321
-
322
- {/* Tools list */}
323
- <div
324
- className="overflow-y-auto scrollbar-hide flex-1"
325
- style={{ maxHeight: `${dropdownMaxHeight - 140}px` }}
326
- >
327
- {loading ? (
328
- <div className="flex items-center justify-center gap-3 px-4 py-8">
329
- <Loader2 className="h-5 w-5 animate-spin text-teal-500 dark:text-teal-400" />
330
- <span className="text-sm text-zinc-600 dark:text-zinc-300">Loading tools…</span>
331
- </div>
332
- ) : error ? (
333
- <div className="px-4 py-6">
334
- <div className="flex flex-col items-center gap-3 text-center">
335
- <div className="h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/20 flex items-center justify-center">
336
- <X className="h-6 w-6 text-red-500 dark:text-red-400" />
337
- </div>
338
- <div>
339
- <p className="text-sm font-medium text-red-600 dark:text-red-400">Failed to load tools</p>
340
- <p className="text-xs text-red-500 dark:text-red-500 mt-1">Please try again later</p>
341
- </div>
342
  </div>
343
- </div>
344
- ) : tools.filter(isMCPTool).length === 0 ? (
345
- <div className="px-4 py-6">
346
- <div className="flex flex-col items-center gap-3 text-center">
347
- <div className="h-12 w-12 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
348
- <Settings2 className="h-6 w-6 text-zinc-400 dark:text-zinc-500" />
 
 
 
 
349
  </div>
350
- <div>
351
- <p className="text-sm font-medium text-zinc-600 dark:text-zinc-300">No tools available</p>
352
- <p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Check back later for updates</p>
 
 
 
 
 
 
 
 
353
  </div>
354
  </div>
355
- </div>
356
- ) : (
357
- <div className="p-2">
358
- {tools
359
- .filter(isMCPTool)
360
- .filter(tool =>
361
- tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
362
- (tool.description && tool.description.toLowerCase().includes(searchQuery.toLowerCase()))
363
- )
364
- .map((tool, index) => (
365
- <button
366
- key={tool.name || index}
367
- type="button"
368
- className={`w-full rounded-xl px-4 py-3 text-left transition-all duration-200 group ${
369
- focusedIndex === index
370
- ? 'bg-teal-50 dark:bg-teal-900/20 border border-teal-300/80 dark:border-teal-400/60'
371
- : selectedTool === tool.name
372
- ? 'bg-teal-50/50 dark:bg-teal-900/10 border border-teal-200/60 dark:border-teal-400/40'
373
- : 'hover:bg-zinc-50 dark:hover:bg-zinc-800/50 border border-transparent'
374
- }`}
375
- onClick={() => {
376
- setSelectedTool((current) => (current === tool.name ? null : tool.name));
377
- setShowToolsDropdown(false);
378
- setFocusedIndex(-1);
379
- }}
380
- onMouseEnter={() => setFocusedIndex(index)}
381
- >
382
- <div className="flex items-start justify-between gap-3">
383
- <div className="flex-1 min-w-0">
384
- <div className="flex items-center gap-2 mb-1">
385
- <h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 truncate">
386
- {tool.name}
387
- </h4>
388
- {selectedTool === tool.name && (
389
- <Check className="h-3.5 w-3.5 text-teal-500 dark:text-teal-400 flex-shrink-0" />
 
 
 
 
 
390
  )}
391
  </div>
392
- {tool.description && (
393
- <p className="text-xs text-zinc-600 dark:text-zinc-400 line-clamp-2 leading-relaxed">
394
- {tool.description}
395
- </p>
396
- )}
397
  </div>
398
- </div>
399
- </button>
400
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  </div>
402
- )}
403
- </div>
404
-
405
- {/* Footer */}
406
- <div className="px-4 py-2 border-t border-zinc-200/50 dark:border-zinc-700/50 bg-zinc-50/50 dark:bg-zinc-800/30 flex-shrink-0">
407
- <div className="flex items-center justify-end">
408
- <button
409
- type="button"
410
- onClick={() => {
411
- setSelectedTool(null);
412
- setShowToolsDropdown(false);
413
- setFocusedIndex(-1);
414
- }}
415
- className="text-xs text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors duration-200"
416
- >
417
- Clear selection
418
- </button>
419
  </div>
420
  </div>
421
- </div>
422
- )}
423
  </div>
424
 
425
  <button
@@ -434,7 +444,7 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
434
  type="button"
435
  className="h-8 px-3 rounded-lg text-sm font-medium hover:opacity-90 transition-all duration-200 hover:scale-105 flex items-center justify-center bg-teal-900 dark:bg-[#032827] text-teal-300 dark:text-[#2DD4BF]"
436
  >
437
- Agent
438
  </button>
439
  </div>
440
 
 
159
  const viewportWidth = window.innerWidth;
160
  const spaceBelow = viewportHeight - buttonRect.bottom;
161
  const spaceAbove = buttonRect.top;
162
+ const dropdownHeight = 500; // Approximate max height (increased from 400)
163
  const minSpace = 20; // Minimum space from viewport edge
164
 
165
  // Determine if dropdown should be above or below
166
  if (spaceBelow < dropdownHeight + minSpace && spaceAbove > spaceBelow) {
167
  setDropdownPosition('above');
168
  // Calculate max height based on available space above
169
+ const maxHeight = Math.min(450, spaceAbove - minSpace - 60); // Increased from 320 to 450
170
+ setDropdownMaxHeight(Math.max(250, maxHeight)); // Increased minimum from 200 to 250
171
  } else {
172
  setDropdownPosition('below');
173
  // Calculate max height based on available space below
174
+ const maxHeight = Math.min(450, spaceBelow - minSpace - 60); // Increased from 320 to 450
175
+ setDropdownMaxHeight(Math.max(250, maxHeight)); // Increased minimum from 200 to 250
176
  }
177
 
178
  // Adjust horizontal position if dropdown would overflow
179
+ const dropdownWidth = 384; // w-96 = 384px (increased from w-80)
180
  const dropdownElement = dropdownRef.current?.querySelector('[data-dropdown-content]') as HTMLElement;
181
  if (dropdownElement) {
182
  if (buttonRect.left + dropdownWidth > viewportWidth - minSpace) {
 
204
  }
205
  }, [showToolsDropdown]);
206
 
207
+ // Focus search input when dropdown opens
208
+ useEffect(() => {
209
+ if (showToolsDropdown && searchInputRef.current) {
210
+ setTimeout(() => searchInputRef.current?.focus(), 100);
211
+ }
212
+ }, [showToolsDropdown]);
213
+
214
  const toggleToolsDropdown = () => {
215
  const nextState = !showToolsDropdown;
216
  setShowToolsDropdown(nextState);
 
286
  )}
287
  </button>
288
 
289
+ {showToolsDropdown && (
290
+ <div
291
+ ref={dropdownContentRef}
292
+ data-dropdown-content
293
+ className={`absolute left-0 w-96 rounded-2xl border border-zinc-200/70 dark:border-zinc-700/60 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xl shadow-[0_20px_45px_-20px_rgba(12,12,12,0.75)] z-50 overflow-hidden animate-fade-in flex flex-col ${
294
+ dropdownPosition === 'above'
295
+ ? 'bottom-[calc(100%+0.6rem)]'
296
+ : 'top-[calc(100%+0.6rem)]'
297
+ }`}
298
+ style={{ maxHeight: `${dropdownMaxHeight}px` }}
299
+ >
300
+ {/* Header */}
301
+ <div className="px-4 py-3 border-b border-zinc-200/50 dark:border-zinc-700/50 flex-shrink-0">
302
+ <div className="flex items-center gap-2 mb-3">
303
+ <Sparkles className="h-4 w-4 text-teal-500 dark:text-teal-400" />
304
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Available Tools</h3>
305
+ <div className="ml-auto">
306
+ <span className="text-xs text-zinc-500 dark:text-zinc-400 bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-full">
307
+ {tools.filter(isMCPTool).length} tools
308
+ </span>
309
+ </div>
310
+ </div>
311
+
312
+ {/* Search input */}
313
+ <div className="relative">
314
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-zinc-400 dark:text-zinc-500" />
315
+ <input
316
+ ref={searchInputRef}
317
+ type="text"
318
+ placeholder="Search tools..."
319
+ value={searchQuery}
320
+ onChange={(e) => {
321
+ setSearchQuery(e.target.value);
322
+ setFocusedIndex(-1);
323
+ }}
324
+ className="w-full pl-10 pr-4 py-2 text-sm bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg text-zinc-900 dark:text-zinc-100 placeholder-zinc-500 dark:placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:border-teal-500/50 transition-all duration-200"
325
+ />
326
  </div>
327
  </div>
328
 
329
+ {/* Tools list */}
330
+ <div
331
+ className="overflow-y-auto scrollbar-hide flex-1"
332
+ style={{ maxHeight: `${dropdownMaxHeight - 140}px` }}
333
+ >
334
+ {loading ? (
335
+ <div className="flex items-center justify-center gap-3 px-4 py-8">
336
+ <Loader2 className="h-5 w-5 animate-spin text-teal-500 dark:text-teal-400" />
337
+ <span className="text-sm text-zinc-600 dark:text-zinc-300">Loading tools…</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  </div>
339
+ ) : error ? (
340
+ <div className="px-4 py-6">
341
+ <div className="flex flex-col items-center gap-3 text-center">
342
+ <div className="h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/20 flex items-center justify-center">
343
+ <X className="h-6 w-6 text-red-500 dark:text-red-400" />
344
+ </div>
345
+ <div>
346
+ <p className="text-sm font-medium text-red-600 dark:text-red-400">Failed to load tools</p>
347
+ <p className="text-xs text-red-500 dark:text-red-500 mt-1">Please try again later</p>
348
+ </div>
349
  </div>
350
+ </div>
351
+ ) : tools.filter(isMCPTool).length === 0 ? (
352
+ <div className="px-4 py-6">
353
+ <div className="flex flex-col items-center gap-3 text-center">
354
+ <div className="h-12 w-12 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
355
+ <Settings2 className="h-6 w-6 text-zinc-400 dark:text-zinc-500" />
356
+ </div>
357
+ <div>
358
+ <p className="text-sm font-medium text-zinc-600 dark:text-zinc-300">No tools available</p>
359
+ <p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Check back later for updates</p>
360
+ </div>
361
  </div>
362
  </div>
363
+ ) : (
364
+ <div className="p-2">
365
+ {tools
366
+ .filter(isMCPTool)
367
+ .filter(tool =>
368
+ tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
369
+ (tool.description && tool.description.toLowerCase().includes(searchQuery.toLowerCase()))
370
+ )
371
+ .map((tool, index) => (
372
+ <button
373
+ key={tool.name || index}
374
+ type="button"
375
+ className={`w-full rounded-xl px-4 py-3 text-left transition-all duration-200 group ${
376
+ focusedIndex === index
377
+ ? 'bg-teal-50 dark:bg-teal-900/20 border border-teal-300/80 dark:border-teal-400/60'
378
+ : selectedTool === tool.name
379
+ ? 'bg-teal-50/50 dark:bg-teal-900/10 border border-teal-200/60 dark:border-teal-400/40'
380
+ : 'hover:bg-zinc-50 dark:hover:bg-zinc-800/50 border border-transparent'
381
+ }`}
382
+ onClick={() => {
383
+ setSelectedTool((current) => (current === tool.name ? null : tool.name));
384
+ setShowToolsDropdown(false);
385
+ setFocusedIndex(-1);
386
+ }}
387
+ onMouseEnter={() => setFocusedIndex(index)}
388
+ >
389
+ <div className="flex items-start justify-between gap-3">
390
+ <div className="flex-1 min-w-0">
391
+ <div className="flex items-center gap-2 mb-1">
392
+ <h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 truncate">
393
+ {tool.name}
394
+ </h4>
395
+ {selectedTool === tool.name && (
396
+ <Check className="h-3.5 w-3.5 text-teal-500 dark:text-teal-400 flex-shrink-0" />
397
+ )}
398
+ </div>
399
+ {tool.description && (
400
+ <p className="text-xs text-zinc-600 dark:text-zinc-400 line-clamp-2 leading-relaxed">
401
+ {tool.description}
402
+ </p>
403
  )}
404
  </div>
 
 
 
 
 
405
  </div>
406
+ </button>
407
+ ))}
408
+ </div>
409
+ )}
410
+ </div>
411
+
412
+ {/* Footer */}
413
+ <div className="px-4 py-2 border-t border-zinc-200/50 dark:border-zinc-700/50 bg-zinc-50/50 dark:bg-zinc-800/30 flex-shrink-0">
414
+ <div className="flex items-center justify-between">
415
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
416
+ Use ↑↓ to navigate, Enter to select
417
+ </p>
418
+ <button
419
+ type="button"
420
+ onClick={() => {
421
+ setSelectedTool(null);
422
+ setShowToolsDropdown(false);
423
+ setFocusedIndex(-1);
424
+ }}
425
+ className="text-xs text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors duration-200"
426
+ >
427
+ Clear selection
428
+ </button>
429
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  </div>
431
  </div>
432
+ )}
 
433
  </div>
434
 
435
  <button
 
444
  type="button"
445
  className="h-8 px-3 rounded-lg text-sm font-medium hover:opacity-90 transition-all duration-200 hover:scale-105 flex items-center justify-center bg-teal-900 dark:bg-[#032827] text-teal-300 dark:text-[#2DD4BF]"
446
  >
447
+ LLM
448
  </button>
449
  </div>
450