ModelPicker.tsx

  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;