@messenger-box/tailwind-ui-inbox 10.0.3-alpha.122 → 10.0.3-alpha.124

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 (86) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/components/AIAgent/AIAgent.d.ts +2 -0
  3. package/lib/components/AIAgent/AIAgent.d.ts.map +1 -1
  4. package/lib/components/AIAgent/AIAgent.js +42 -26
  5. package/lib/components/AIAgent/AIAgent.js.map +1 -1
  6. package/lib/components/InboxMessage/InputComponent.d.ts +4 -1
  7. package/lib/components/InboxMessage/InputComponent.d.ts.map +1 -1
  8. package/lib/components/InboxMessage/InputComponent.js +20 -304
  9. package/lib/components/InboxMessage/InputComponent.js.map +1 -1
  10. package/lib/components/InboxMessage/UploadImageButton.js +2 -6
  11. package/lib/components/InboxMessage/UploadImageButton.js.map +1 -1
  12. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.d.ts +1 -0
  13. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.d.ts.map +1 -1
  14. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.js +15 -5
  15. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.js.map +1 -1
  16. package/lib/components/ModelConfigPanel.d.ts +10 -0
  17. package/lib/components/ModelConfigPanel.d.ts.map +1 -1
  18. package/lib/components/ModelConfigPanel.js +551 -2
  19. package/lib/components/ModelConfigPanel.js.map +1 -1
  20. package/lib/components/filler-components/RightSiderBar.d.ts +1 -0
  21. package/lib/components/filler-components/RightSiderBar.d.ts.map +1 -1
  22. package/lib/components/filler-components/RightSiderBar.js +174 -140
  23. package/lib/components/filler-components/RightSiderBar.js.map +1 -1
  24. package/lib/components/slot-fill/right-sidebar-filler.d.ts.map +1 -1
  25. package/lib/components/slot-fill/right-sidebar-filler.js.map +1 -1
  26. package/lib/config/env-config.d.ts +2 -0
  27. package/lib/config/env-config.d.ts.map +1 -1
  28. package/lib/config/env-config.js +7 -1
  29. package/lib/config/env-config.js.map +1 -1
  30. package/lib/container/AiLandingInput.d.ts.map +1 -1
  31. package/lib/container/AiLandingInput.js +11 -9
  32. package/lib/container/AiLandingInput.js.map +1 -1
  33. package/lib/container/Inbox.js +1 -1
  34. package/lib/container/Inbox.js.map +1 -1
  35. package/lib/container/InboxAiMessagesLoader.d.ts +1 -0
  36. package/lib/container/InboxAiMessagesLoader.d.ts.map +1 -1
  37. package/lib/container/InboxAiMessagesLoader.js +4 -1
  38. package/lib/container/InboxAiMessagesLoader.js.map +1 -1
  39. package/lib/container/InboxContainer.d.ts +1 -0
  40. package/lib/container/InboxContainer.d.ts.map +1 -1
  41. package/lib/container/InboxContainer.js +1 -6
  42. package/lib/container/InboxContainer.js.map +1 -1
  43. package/lib/container/InboxWithAiLoader.d.ts +2 -0
  44. package/lib/container/InboxWithAiLoader.d.ts.map +1 -1
  45. package/lib/container/InboxWithAiLoader.js +8 -3
  46. package/lib/container/InboxWithAiLoader.js.map +1 -1
  47. package/lib/container/ServiceInbox.js +1 -1
  48. package/lib/container/ServiceInbox.js.map +1 -1
  49. package/lib/container/ThreadMessages.js +1 -1
  50. package/lib/container/ThreadMessages.js.map +1 -1
  51. package/lib/container/ThreadMessagesInbox.js +1 -1
  52. package/lib/container/ThreadMessagesInbox.js.map +1 -1
  53. package/lib/container/Threads.js +1 -1
  54. package/lib/container/Threads.js.map +1 -1
  55. package/lib/module.js +1 -1
  56. package/lib/module.js.map +1 -1
  57. package/lib/templates/InboxWithAi.d.ts +1 -0
  58. package/lib/templates/InboxWithAi.d.ts.map +1 -1
  59. package/lib/templates/InboxWithAi.js +9 -6
  60. package/lib/templates/InboxWithAi.js.map +1 -1
  61. package/lib/templates/InboxWithAi.tsx +7 -3
  62. package/lib/xstate/index.d.ts +3 -0
  63. package/lib/xstate/index.d.ts.map +1 -0
  64. package/lib/xstate/rightSidebar.machine.d.ts +4 -0
  65. package/lib/xstate/rightSidebar.machine.d.ts.map +1 -0
  66. package/lib/xstate/rightSidebar.machine.js +174 -0
  67. package/lib/xstate/rightSidebar.machine.js.map +1 -0
  68. package/lib/xstate/rightSidebar.types.d.ts +52 -0
  69. package/lib/xstate/rightSidebar.types.d.ts.map +1 -0
  70. package/package.json +4 -4
  71. package/src/components/AIAgent/AIAgent.tsx +35 -21
  72. package/src/components/InboxMessage/InputComponent.tsx +23 -375
  73. package/src/components/InboxMessage/UploadImageButton.tsx +4 -4
  74. package/src/components/InboxMessage/message-widgets/ModernMessageGroup.tsx +17 -0
  75. package/src/components/ModelConfigPanel.tsx +666 -0
  76. package/src/components/filler-components/RightSiderBar.tsx +189 -150
  77. package/src/components/slot-fill/right-sidebar-filler.tsx +1 -0
  78. package/src/config/env-config.ts +3 -1
  79. package/src/container/AiLandingInput.tsx +11 -111
  80. package/src/container/InboxAiMessagesLoader.tsx +13 -2
  81. package/src/container/InboxContainer.tsx +2 -8
  82. package/src/container/InboxWithAiLoader.tsx +8 -2
  83. package/src/templates/InboxWithAi.tsx +7 -3
  84. package/src/xstate/index.ts +2 -0
  85. package/src/xstate/rightSidebar.machine.ts +139 -0
  86. package/src/xstate/rightSidebar.types.ts +55 -0
@@ -1,4 +1,7 @@
1
1
  import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import { UploadImageButton } from './InboxMessage/UploadImageButton';
4
+ import { ModelConfig as PersistentModelConfig } from '../hooks/usePersistentModelConfig';
2
5
  import { ModelConfig } from '../hooks/usePersistentModelConfig';
3
6
 
4
7
  interface ModelConfigPanelProps {
@@ -341,3 +344,666 @@ export const ModelConfigPanel: React.FC<ModelConfigPanelProps> = ({
341
344
  ModelConfigPanel.displayName = 'ModelConfigPanel';
342
345
 
343
346
  export default ModelConfigPanel;
347
+
348
+ // Toolbar extracted for reuse from InputComponent.tsx (lines 209-394)
349
+ export interface ModelToolbarProps {
350
+ modelConfig?: PersistentModelConfig;
351
+ onModelConfigChange?: (config: PersistentModelConfig) => void;
352
+ sending: boolean;
353
+ canSend: boolean;
354
+ onSend: () => void;
355
+ onUploadImageChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
356
+ }
357
+
358
+ const FloatingDropdown: React.FC<{
359
+ anchorRef: React.RefObject<HTMLElement>;
360
+ open: boolean;
361
+ onClose?: () => void;
362
+ minWidth?: number;
363
+ children: React.ReactNode;
364
+ }> = ({ anchorRef, open, onClose, minWidth = 224, children }) => {
365
+ const [style, setStyle] = useState<{ top: number; left: number; width: number } | null>(null);
366
+
367
+ useEffect(() => {
368
+ if (!open) return;
369
+ const update = () => {
370
+ const el = anchorRef.current as HTMLElement | null;
371
+ if (!el) return;
372
+ const rect = el.getBoundingClientRect();
373
+ const width = Math.max(minWidth, rect.width);
374
+
375
+ // Heuristic expected dropdown height (px). Matches header + max-h-60 (15rem = 240px) + paddings.
376
+ const expectedHeight = 320;
377
+ const viewportHeight = window.innerHeight;
378
+ const spaceBelow = viewportHeight - rect.bottom;
379
+ const spaceAbove = rect.top;
380
+
381
+ const openAbove = spaceBelow < expectedHeight && spaceAbove > spaceBelow;
382
+ const top = openAbove ? Math.max(8, rect.top - expectedHeight - 8) : rect.bottom + 8;
383
+ const left = Math.min(Math.max(8, rect.left), window.innerWidth - width - 8);
384
+ setStyle({ top, left, width });
385
+ };
386
+ update();
387
+ window.addEventListener('scroll', update, true);
388
+ window.addEventListener('resize', update);
389
+ return () => {
390
+ window.removeEventListener('scroll', update, true);
391
+ window.removeEventListener('resize', update);
392
+ };
393
+ }, [open, anchorRef, minWidth]);
394
+
395
+ useEffect(() => {
396
+ if (!open) return;
397
+ const onKey = (e: KeyboardEvent) => {
398
+ if (e.key === 'Escape') onClose?.();
399
+ };
400
+ document.addEventListener('keydown', onKey);
401
+ return () => document.removeEventListener('keydown', onKey);
402
+ }, [open, onClose]);
403
+
404
+ if (!open || !style) return null;
405
+ return ReactDOM.createPortal(
406
+ <div
407
+ style={{ position: 'fixed', top: style.top, left: style.left, width: style.width, zIndex: 9999 }}
408
+ className="bg-white border border-gray-200 rounded-lg shadow-lg"
409
+ >
410
+ {children}
411
+ </div>,
412
+ document.body,
413
+ );
414
+ };
415
+
416
+ export const ModelToolbar: React.FC<ModelToolbarProps> = ({
417
+ modelConfig,
418
+ onModelConfigChange,
419
+ sending,
420
+ canSend,
421
+ onSend,
422
+ onUploadImageChange,
423
+ }) => {
424
+ const [showModelDropdown, setShowModelDropdown] = useState(false);
425
+ const [showToolbarModelDropdown, setShowToolbarModelDropdown] = useState(false);
426
+ const [showTemplateDropdown, setShowTemplateDropdown] = useState(false);
427
+ const [showSettingsModal, setShowSettingsModal] = useState(false);
428
+ const [settingsActiveTab, setSettingsActiveTab] = useState<'model' | 'other_settings'>('model');
429
+ const [templateSearch, setTemplateSearch] = useState('');
430
+ const [modelSearch, setModelSearch] = useState('');
431
+ const [toolbarModelSearch, setToolbarModelSearch] = useState('');
432
+ const modelDropdownRef = useRef<HTMLDivElement>(null);
433
+ const toolbarModelButtonRef = useRef<HTMLButtonElement>(null);
434
+ const templateDropdownRef = useRef<HTMLDivElement>(null);
435
+
436
+ useEffect(() => {
437
+ const handleClickOutside = (event: MouseEvent) => {
438
+ if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) {
439
+ setShowModelDropdown(false);
440
+ }
441
+ const target = event.target as HTMLElement;
442
+ if (showToolbarModelDropdown) {
443
+ const isClickInsideButton = toolbarModelButtonRef.current?.contains(target);
444
+ const isClickInsideDropdown = target.closest('[data-model-dropdown]');
445
+ if (!isClickInsideButton && !isClickInsideDropdown) {
446
+ setShowToolbarModelDropdown(false);
447
+ setToolbarModelSearch('');
448
+ }
449
+ }
450
+ if (templateDropdownRef.current && !templateDropdownRef.current.contains(event.target as Node)) {
451
+ setShowTemplateDropdown(false);
452
+ }
453
+ };
454
+ document.addEventListener('mousedown', handleClickOutside);
455
+ return () => document.removeEventListener('mousedown', handleClickOutside);
456
+ }, [showToolbarModelDropdown]);
457
+
458
+ const allModelOptions = useMemo(() => {
459
+ return getAllModels();
460
+ }, []);
461
+
462
+ const templateOptionsList = useMemo(() => {
463
+ return templateOptions.map((t) => ({ value: t.value, label: t.label, icon: t.icon }));
464
+ }, []);
465
+
466
+ const filteredTemplates = useMemo(() => {
467
+ if (!templateSearch) return templateOptionsList;
468
+ const q = templateSearch.toLowerCase();
469
+ return templateOptionsList.filter(
470
+ (o) => o.label.toLowerCase().includes(q) || String(o.value).toLowerCase().includes(q),
471
+ );
472
+ }, [templateOptionsList, templateSearch]);
473
+
474
+ const filteredModels = useMemo(() => {
475
+ if (!modelSearch) return allModelOptions;
476
+ const q = modelSearch.toLowerCase();
477
+ return allModelOptions.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
478
+ }, [allModelOptions, modelSearch]);
479
+
480
+ const filteredToolbarModels = useMemo(() => {
481
+ if (!toolbarModelSearch) return allModelOptions;
482
+ const q = toolbarModelSearch.toLowerCase();
483
+ return allModelOptions.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
484
+ }, [allModelOptions, toolbarModelSearch]);
485
+
486
+ const handleModelSelect = useCallback(
487
+ (modelValue: string) => {
488
+ if (onModelConfigChange && modelConfig) {
489
+ onModelConfigChange({ ...modelConfig, model: modelValue });
490
+ }
491
+ setShowModelDropdown(false);
492
+ },
493
+ [modelConfig, onModelConfigChange],
494
+ );
495
+
496
+ const handleToolbarModelSelect = useCallback(
497
+ (modelValue: string) => {
498
+ if (onModelConfigChange && modelConfig) {
499
+ onModelConfigChange({ ...modelConfig, model: modelValue });
500
+ }
501
+ setShowToolbarModelDropdown(false);
502
+ setToolbarModelSearch('');
503
+ },
504
+ [modelConfig, onModelConfigChange],
505
+ );
506
+
507
+ const handleTemplateSelect = useCallback(
508
+ (templateValue: PersistentModelConfig['template']) => {
509
+ if (onModelConfigChange && modelConfig) {
510
+ onModelConfigChange({ ...modelConfig, template: templateValue });
511
+ }
512
+ setShowTemplateDropdown(false);
513
+ },
514
+ [modelConfig, onModelConfigChange],
515
+ );
516
+
517
+ return (
518
+ <>
519
+ <div className="flex items-center gap-2 overflow-x-auto overflow-y-visible whitespace-nowrap py-1 px-2 sm:px-0">
520
+ {/* Template selection moved to Project Settings modal */}
521
+
522
+ {/* 2. Model Selection Dropdown */}
523
+ {/* Model selection moved to Project Settings modal */}
524
+
525
+ <div className="flex-1"></div>
526
+
527
+ {/* Settings Button */}
528
+ <button
529
+ onClick={() => setShowSettingsModal(true)}
530
+ className="flex items-center justify-center w-7 h-7 sm:w-7 sm:h-7 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors shrink-0"
531
+ >
532
+ <svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
533
+ <path
534
+ strokeLinecap="round"
535
+ strokeLinejoin="round"
536
+ strokeWidth={2}
537
+ d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
538
+ />
539
+ <path
540
+ strokeLinecap="round"
541
+ strokeLinejoin="round"
542
+ strokeWidth={2}
543
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
544
+ />
545
+ </svg>
546
+ </button>
547
+
548
+ {/* Model Selection Button */}
549
+ <div className="relative shrink-0">
550
+ <button
551
+ ref={toolbarModelButtonRef}
552
+ onClick={() => setShowToolbarModelDropdown(!showToolbarModelDropdown)}
553
+ className="flex items-center justify-center w-7 h-7 sm:w-7 sm:h-7 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors shrink-0"
554
+ title={
555
+ modelConfig?.model
556
+ ? allModelOptions.find((o) => o.value === modelConfig.model)?.label || 'Choose a model'
557
+ : 'Choose a model'
558
+ }
559
+ >
560
+ <svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
561
+ {/* Outer square */}
562
+ <rect x="4" y="4" width="16" height="16" rx="1" strokeWidth="2" />
563
+ {/* Inner square */}
564
+ <rect x="8" y="8" width="8" height="8" rx="0.5" strokeWidth="2" />
565
+ {/* Top pins */}
566
+ <line x1="10" y1="4" x2="10" y2="2" strokeWidth="2" />
567
+ <line x1="14" y1="4" x2="14" y2="2" strokeWidth="2" />
568
+ {/* Bottom pins */}
569
+ <line x1="10" y1="20" x2="10" y2="22" strokeWidth="2" />
570
+ <line x1="14" y1="20" x2="14" y2="22" strokeWidth="2" />
571
+ {/* Left pins */}
572
+ <line x1="4" y1="10" x2="2" y2="10" strokeWidth="2" />
573
+ <line x1="4" y1="14" x2="2" y2="14" strokeWidth="2" />
574
+ {/* Right pins */}
575
+ <line x1="20" y1="10" x2="22" y2="10" strokeWidth="2" />
576
+ <line x1="20" y1="14" x2="22" y2="14" strokeWidth="2" />
577
+ </svg>
578
+ </button>
579
+
580
+ <FloatingDropdown
581
+ anchorRef={toolbarModelButtonRef as unknown as React.RefObject<HTMLElement>}
582
+ open={showToolbarModelDropdown}
583
+ onClose={() => {
584
+ setShowToolbarModelDropdown(false);
585
+ setToolbarModelSearch('');
586
+ }}
587
+ minWidth={280}
588
+ >
589
+ <div data-model-dropdown className="bg-white">
590
+ <div className="p-2 border-b border-gray-100">
591
+ <input
592
+ autoFocus
593
+ value={toolbarModelSearch}
594
+ onChange={(e) => setToolbarModelSearch(e.target.value)}
595
+ placeholder="Search model..."
596
+ className="w-full px-2 py-1.5 text-xs border border-gray-200 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
597
+ />
598
+ </div>
599
+ <div className="py-1 max-h-60 overflow-y-auto">
600
+ {filteredToolbarModels.map((option) => (
601
+ <button
602
+ key={option.value}
603
+ onClick={() => handleToolbarModelSelect(option.value)}
604
+ className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 transition-colors flex items-center justify-between ${
605
+ modelConfig?.model === option.value ? 'bg-blue-50' : ''
606
+ }`}
607
+ >
608
+ <span className="text-gray-900">{option.label}</span>
609
+ {modelConfig?.model === option.value && (
610
+ <svg
611
+ className="w-4 h-4 text-blue-600"
612
+ fill="none"
613
+ stroke="currentColor"
614
+ viewBox="0 0 24 24"
615
+ >
616
+ <path
617
+ strokeLinecap="round"
618
+ strokeLinejoin="round"
619
+ strokeWidth={2}
620
+ d="M5 13l4 4L19 7"
621
+ />
622
+ </svg>
623
+ )}
624
+ </button>
625
+ ))}
626
+ </div>
627
+ </div>
628
+ </FloatingDropdown>
629
+ </div>
630
+
631
+ <UploadImageButton onChange={onUploadImageChange}>
632
+ <div className="flex items-center justify-center w-7 h-7 sm:w-7 sm:h-7 rounded-lg border border-gray-300 bg-white shrink-0 ml-1">
633
+ <svg className="w-3 h-3 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
634
+ <path
635
+ strokeLinecap="round"
636
+ strokeLinejoin="round"
637
+ strokeWidth={2}
638
+ d="M21.44 11.05l-9.19 9.19a6 6 0 11-8.49-8.49l9.19-9.19a4 4 0 115.66 5.66l-9.19 9.19a2 2 0 11-2.83-2.83l8.49-8.49"
639
+ />
640
+ </svg>
641
+ </div>
642
+ </UploadImageButton>
643
+
644
+ {/* Send Button */}
645
+ <button
646
+ className={`flex items-center justify-center w-7 h-7 sm:w-7 sm:h-7 rounded-lg border transition-colors shrink-0 ${
647
+ canSend && !sending
648
+ ? 'border-blue-500 bg-blue-500 hover:bg-blue-600 text-white'
649
+ : 'border-gray-300 bg-gray-100 text-gray-400 cursor-not-allowed'
650
+ }`}
651
+ onClick={onSend}
652
+ disabled={!canSend || sending}
653
+ type="button"
654
+ >
655
+ {sending ? (
656
+ <svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
657
+ <circle
658
+ className="opacity-25"
659
+ cx="12"
660
+ cy="12"
661
+ r="10"
662
+ stroke="currentColor"
663
+ strokeWidth="4"
664
+ ></circle>
665
+ <path
666
+ className="opacity-75"
667
+ fill="currentColor"
668
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
669
+ ></path>
670
+ </svg>
671
+ ) : (
672
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
673
+ <path
674
+ strokeLinecap="round"
675
+ strokeLinejoin="round"
676
+ strokeWidth={2}
677
+ d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
678
+ />
679
+ </svg>
680
+ )}
681
+ </button>
682
+ </div>
683
+
684
+ {/* Settings Modal - Only API Key and Model + IDs */}
685
+ {showSettingsModal && (
686
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
687
+ <div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
688
+ <div className="p-6 mb-4">
689
+ <div className="flex items-center justify-between mb-4">
690
+ <h3 className="text-lg font-semibold text-gray-900">Project Settings</h3>
691
+ <button
692
+ onClick={() => setShowSettingsModal(false)}
693
+ className="text-gray-400 hover:text-gray-600 transition-colors"
694
+ >
695
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
696
+ <path
697
+ strokeLinecap="round"
698
+ strokeLinejoin="round"
699
+ strokeWidth={2}
700
+ d="M6 18L18 6M6 6l12 12"
701
+ />
702
+ </svg>
703
+ </button>
704
+ </div>
705
+
706
+ {/* Tabs */}
707
+ <div className="mb-4">
708
+ <div className="flex border-b border-gray-200">
709
+ <button
710
+ className={`px-3 py-2 text-sm -mb-px border-b-2 transition-colors ${
711
+ settingsActiveTab === 'model'
712
+ ? 'border-blue-500 text-blue-600'
713
+ : 'border-transparent text-gray-600 hover:text-gray-800'
714
+ }`}
715
+ onClick={() => setSettingsActiveTab('model')}
716
+ type="button"
717
+ >
718
+ Configuration
719
+ </button>
720
+ <button
721
+ className={`ml-2 px-3 py-2 text-sm -mb-px border-b-2 transition-colors ${
722
+ settingsActiveTab === 'other_settings'
723
+ ? 'border-blue-500 text-blue-600'
724
+ : 'border-transparent text-gray-600 hover:text-gray-800'
725
+ }`}
726
+ onClick={() => setSettingsActiveTab('other_settings')}
727
+ type="button"
728
+ >
729
+ Other Settings
730
+ </button>
731
+ </div>
732
+ </div>
733
+
734
+ {modelConfig && onModelConfigChange && (
735
+ <div className="space-y-4">
736
+ {settingsActiveTab === 'model' ? (
737
+ <div className="space-y-4">
738
+ <div className="space-y-2">
739
+ <label className="block text-sm font-medium text-gray-700">Model</label>
740
+ <div className="relative" ref={modelDropdownRef}>
741
+ <button
742
+ type="button"
743
+ onClick={() => setShowModelDropdown(!showModelDropdown)}
744
+ className="w-full flex items-center gap-2 px-3 py-2 text-sm rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors overflow-hidden"
745
+ >
746
+ {(() => {
747
+ if (modelConfig && modelConfig.model) {
748
+ const currentModel = allModelOptions.find(
749
+ (option) => option.value === modelConfig.model,
750
+ );
751
+ return currentModel ? (
752
+ <span className="text-gray-700 truncate">
753
+ {currentModel.label}
754
+ </span>
755
+ ) : (
756
+ <span className="text-gray-500 truncate">
757
+ Model
758
+ </span>
759
+ );
760
+ }
761
+ return (
762
+ <span className="text-gray-500 truncate">Model</span>
763
+ );
764
+ })()}
765
+ <svg
766
+ className="w-4 h-4 text-gray-500 ml-auto"
767
+ fill="none"
768
+ stroke="currentColor"
769
+ viewBox="0 0 24 24"
770
+ >
771
+ <path
772
+ strokeLinecap="round"
773
+ strokeLinejoin="round"
774
+ strokeWidth={2}
775
+ d="M19 9l-7 7-7-7"
776
+ />
777
+ </svg>
778
+ </button>
779
+
780
+ <FloatingDropdown
781
+ anchorRef={
782
+ modelDropdownRef as unknown as React.RefObject<HTMLElement>
783
+ }
784
+ open={showModelDropdown}
785
+ onClose={() => setShowModelDropdown(false)}
786
+ minWidth={320}
787
+ >
788
+ <div className="p-2 border-b border-gray-100">
789
+ <input
790
+ autoFocus
791
+ value={modelSearch}
792
+ onChange={(e) => setModelSearch(e.target.value)}
793
+ placeholder="Search model..."
794
+ className="w-full px-2 py-1 text-xs border border-gray-200 rounded"
795
+ />
796
+ </div>
797
+ <div className="py-1 max-h-60 overflow-y-auto">
798
+ {filteredModels.map((option) => (
799
+ <button
800
+ key={option.value}
801
+ onClick={() => handleModelSelect(option.value)}
802
+ className={`w-full px-3 py-2 text-left text-xs hover:bg-gray-50 transition-colors ${
803
+ modelConfig?.model === option.value
804
+ ? 'bg-blue-50'
805
+ : ''
806
+ }`}
807
+ >
808
+ <div className="font-medium text-gray-900 truncate">
809
+ {option.label}
810
+ </div>
811
+ </button>
812
+ ))}
813
+ </div>
814
+ </FloatingDropdown>
815
+ </div>
816
+ </div>
817
+
818
+ <div className="space-y-2">
819
+ <label className="block text-sm font-medium text-gray-700">
820
+ Template
821
+ </label>
822
+ <div className="relative" ref={templateDropdownRef}>
823
+ <button
824
+ type="button"
825
+ onClick={() => setShowTemplateDropdown(!showTemplateDropdown)}
826
+ className="w-full flex items-center gap-2 px-3 py-2 text-sm rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors overflow-hidden"
827
+ >
828
+ {(() => {
829
+ if (modelConfig && modelConfig.template) {
830
+ const currentTemplate = templateOptionsList.find(
831
+ (option) => option.value === modelConfig.template,
832
+ );
833
+ return currentTemplate ? (
834
+ <>
835
+ <span className="text-base">
836
+ {currentTemplate.icon}
837
+ </span>
838
+ <span className="text-gray-700 truncate">
839
+ {currentTemplate.label}
840
+ </span>
841
+ </>
842
+ ) : (
843
+ <>
844
+ <span className="text-base">📝</span>
845
+ <span className="text-gray-500 truncate">
846
+ Template
847
+ </span>
848
+ </>
849
+ );
850
+ }
851
+ return (
852
+ <>
853
+ <span className="text-base">📝</span>
854
+ <span className="text-gray-500 truncate">
855
+ Template
856
+ </span>
857
+ </>
858
+ );
859
+ })()}
860
+ <svg
861
+ className="w-4 h-4 text-gray-500 ml-auto"
862
+ fill="none"
863
+ stroke="currentColor"
864
+ viewBox="0 0 24 24"
865
+ >
866
+ <path
867
+ strokeLinecap="round"
868
+ strokeLinejoin="round"
869
+ strokeWidth={2}
870
+ d="M19 9l-7 7-7-7"
871
+ />
872
+ </svg>
873
+ </button>
874
+
875
+ <FloatingDropdown
876
+ anchorRef={
877
+ templateDropdownRef as unknown as React.RefObject<HTMLElement>
878
+ }
879
+ open={showTemplateDropdown}
880
+ onClose={() => setShowTemplateDropdown(false)}
881
+ minWidth={320}
882
+ >
883
+ <div className="p-2 border-b border-gray-100">
884
+ <input
885
+ autoFocus
886
+ value={templateSearch}
887
+ onChange={(e) => setTemplateSearch(e.target.value)}
888
+ placeholder="Search template..."
889
+ className="w-full px-2 py-1 text-xs border border-gray-200 rounded"
890
+ />
891
+ </div>
892
+ <div className="py-1 max-h-60 overflow-y-auto">
893
+ {filteredTemplates.map((option) => (
894
+ <button
895
+ key={option.value}
896
+ onClick={() => handleTemplateSelect(option.value)}
897
+ className={`w-full px-3 py-2 text-left text-xs hover:bg-gray-50 transition-colors flex items-center gap-2 ${
898
+ modelConfig?.template === option.value
899
+ ? 'bg-blue-50'
900
+ : ''
901
+ }`}
902
+ >
903
+ <span className="text-sm">{option.icon}</span>
904
+ <span className="font-medium text-gray-900 truncate">
905
+ {option.label}
906
+ </span>
907
+ </button>
908
+ ))}
909
+ </div>
910
+ </FloatingDropdown>
911
+ </div>
912
+ </div>
913
+ </div>
914
+ ) : (
915
+ <>
916
+ <div>
917
+ <label className="block text-sm font-medium text-gray-700 mb-2">
918
+ API Key <span className="text-red-500">*</span>
919
+ </label>
920
+ <input
921
+ type="password"
922
+ value={modelConfig.apiKey}
923
+ onChange={(e) =>
924
+ onModelConfigChange({ ...modelConfig, apiKey: e.target.value })
925
+ }
926
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
927
+ placeholder="Enter your API key"
928
+ />
929
+ </div>
930
+
931
+ <div>
932
+ <label className="block text-sm font-medium text-gray-700 mb-2">
933
+ Extension ID <span className="text-red-500">*</span>
934
+ </label>
935
+ <input
936
+ type="text"
937
+ value={modelConfig.extensionId}
938
+ onChange={(e) =>
939
+ onModelConfigChange({
940
+ ...modelConfig,
941
+ extensionId: e.target.value,
942
+ })
943
+ }
944
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
945
+ placeholder="Enter your extension ID"
946
+ />
947
+ </div>
948
+
949
+ <div>
950
+ <label className="block text-sm font-medium text-gray-700 mb-2">
951
+ Form ID <span className="text-red-500">*</span>
952
+ </label>
953
+ <input
954
+ type="text"
955
+ value={modelConfig.formId || ''}
956
+ onChange={(e) =>
957
+ onModelConfigChange({ ...modelConfig, formId: e.target.value })
958
+ }
959
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
960
+ placeholder="Enter form ID"
961
+ />
962
+ </div>
963
+
964
+ <div>
965
+ <label className="block text-sm font-medium text-gray-700 mb-2">
966
+ Function ID <span className="text-red-500">*</span>
967
+ </label>
968
+ <input
969
+ type="text"
970
+ value={modelConfig.functionId || ''}
971
+ onChange={(e) =>
972
+ onModelConfigChange({
973
+ ...modelConfig,
974
+ functionId: e.target.value,
975
+ })
976
+ }
977
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
978
+ placeholder="Enter function ID"
979
+ />
980
+ </div>
981
+
982
+ <div>
983
+ <label className="block text-sm font-medium text-gray-700 mb-2">
984
+ Step Name <span className="text-gray-500 text-xs">(optional)</span>
985
+ </label>
986
+ <input
987
+ type="text"
988
+ value={modelConfig.stepName || ''}
989
+ onChange={(e) =>
990
+ onModelConfigChange({
991
+ ...modelConfig,
992
+ stepName: e.target.value,
993
+ })
994
+ }
995
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
996
+ placeholder="Enter step name (optional)"
997
+ />
998
+ </div>
999
+ </>
1000
+ )}
1001
+ </div>
1002
+ )}
1003
+ </div>
1004
+ </div>
1005
+ </div>
1006
+ )}
1007
+ </>
1008
+ );
1009
+ };