1import React, { useState, useRef, useEffect } from "react";
2import { Model } from "../types";
3
4interface ModelPickerProps {
5 models: Model[];
6 selectedModel: string;
7 onSelectModel: (modelId: string) => void;
8 onManageModels: () => void;
9 disabled?: boolean;
10}
11
12function ModelPicker({
13 models,
14 selectedModel,
15 onSelectModel,
16 onManageModels,
17 disabled = false,
18}: ModelPickerProps) {
19 const [isOpen, setIsOpen] = useState(false);
20 const [openUpward, setOpenUpward] = useState(false);
21 const containerRef = useRef<HTMLDivElement>(null);
22 const dropdownRef = useRef<HTMLDivElement>(null);
23
24 // Close dropdown when clicking outside
25 useEffect(() => {
26 function handleClickOutside(event: MouseEvent) {
27 if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
28 setIsOpen(false);
29 }
30 }
31
32 if (isOpen) {
33 document.addEventListener("mousedown", handleClickOutside);
34 return () => document.removeEventListener("mousedown", handleClickOutside);
35 }
36 }, [isOpen]);
37
38 // Close on escape
39 useEffect(() => {
40 function handleKeyDown(event: KeyboardEvent) {
41 if (event.key === "Escape") {
42 setIsOpen(false);
43 }
44 }
45
46 if (isOpen) {
47 document.addEventListener("keydown", handleKeyDown);
48 return () => document.removeEventListener("keydown", handleKeyDown);
49 }
50 }, [isOpen]);
51
52 // Determine if dropdown should open upward
53 useEffect(() => {
54 if (isOpen && containerRef.current) {
55 const rect = containerRef.current.getBoundingClientRect();
56 const spaceBelow = window.innerHeight - rect.bottom;
57 const dropdownHeight = 320; // approximate max height
58 setOpenUpward(spaceBelow < dropdownHeight && rect.top > spaceBelow);
59 }
60 }, [isOpen]);
61
62 const selectedModelObj = models.find((m) => m.id === selectedModel);
63 const displayName = selectedModelObj?.display_name || selectedModel;
64 const displayWithSource =
65 selectedModelObj?.source && selectedModelObj.source !== "custom"
66 ? `${displayName} (${selectedModelObj.source})`
67 : displayName;
68
69 const handleSelect = (modelId: string) => {
70 onSelectModel(modelId);
71 setIsOpen(false);
72 };
73
74 const handleManageModels = () => {
75 setIsOpen(false);
76 onManageModels();
77 };
78
79 return (
80 <div className="model-picker" ref={containerRef}>
81 <button
82 className="model-picker-trigger"
83 onClick={() => !disabled && setIsOpen(!isOpen)}
84 disabled={disabled}
85 type="button"
86 >
87 <span className="model-picker-value">{displayWithSource}</span>
88 <svg
89 className={`model-picker-chevron ${isOpen ? "open" : ""}`}
90 width="12"
91 height="12"
92 viewBox="0 0 24 24"
93 fill="none"
94 stroke="currentColor"
95 strokeWidth="2"
96 >
97 <path d="M6 9l6 6 6-6" />
98 </svg>
99 </button>
100
101 {isOpen && (
102 <div
103 className={`model-picker-dropdown ${openUpward ? "open-upward" : ""}`}
104 ref={dropdownRef}
105 >
106 <div className="model-picker-options">
107 {models.map((model) => (
108 <button
109 key={model.id}
110 className={`model-picker-option ${model.id === selectedModel ? "selected" : ""} ${!model.ready ? "disabled" : ""}`}
111 onClick={() => model.ready && handleSelect(model.id)}
112 disabled={!model.ready}
113 type="button"
114 >
115 <div className="model-picker-option-content">
116 <span className="model-picker-option-name">{model.display_name || model.id}</span>
117 {model.source && (
118 <span className="model-picker-option-source">{model.source}</span>
119 )}
120 </div>
121 {!model.ready && <span className="model-picker-option-badge">not ready</span>}
122 {model.id === selectedModel && (
123 <svg
124 className="model-picker-option-check"
125 width="14"
126 height="14"
127 viewBox="0 0 24 24"
128 fill="none"
129 stroke="currentColor"
130 strokeWidth="2"
131 >
132 <path d="M20 6L9 17l-5-5" />
133 </svg>
134 )}
135 </button>
136 ))}
137 </div>
138 <div className="model-picker-divider" />
139 <button
140 className="model-picker-option model-picker-manage"
141 onClick={handleManageModels}
142 type="button"
143 >
144 <svg
145 width="14"
146 height="14"
147 viewBox="0 0 24 24"
148 fill="none"
149 stroke="currentColor"
150 strokeWidth="2"
151 >
152 <path d="M12 4v16m-8-8h16" />
153 </svg>
154 <span>Add / Remove Models...</span>
155 </button>
156 </div>
157 )}
158 </div>
159 );
160}
161
162export default ModelPicker;