feat(ui): complete Plan 06-01 - fix model scan + move model selector to input
- Fix Rust model scan to only list top-level models (not subdirectories) - Add scan_model_dir_top_level() function in model_files.rs - Create ModelSelectorPill component for chat input area - Remove model-section from Sidebar, move to ChatView - Update ChatView props to receive model-related props - Update tests for new ChatView props
This commit is contained in:
1 parent
3364dba5aa
commit
cb722e79a6
11 files changed
+1695
-254
No files matched your search
@@ -0,0 +1,102 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { ChevronDown, Loader2 } from "lucide-react";
|
||||
import type { ModelInfo, ServerStatus } from "../lib/types";
|
||||
import type { RuntimeModelFile } from "../features/runtime/types";
|
||||
import { getDisplayName, isInChatSelector } from "../lib/modelAlias";
|
||||
|
||||
interface ModelSelectorPillProps {
|
||||
models: ModelInfo[];
|
||||
localModels: RuntimeModelFile[];
|
||||
selectedModel: string;
|
||||
onModelChange: (id: string) => void;
|
||||
onModelSwap?: (path: string) => void;
|
||||
isSwapping?: boolean;
|
||||
status: ServerStatus;
|
||||
}
|
||||
|
||||
export default function ModelSelectorPill({
|
||||
models,
|
||||
localModels,
|
||||
selectedModel,
|
||||
onModelChange,
|
||||
onModelSwap,
|
||||
isSwapping,
|
||||
status,
|
||||
}: ModelSelectorPillProps) {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dropdownOpen) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [dropdownOpen]);
|
||||
|
||||
const handleSelectModel = (modelId: string) => {
|
||||
setDropdownOpen(false);
|
||||
onModelChange(modelId);
|
||||
if (onModelSwap) {
|
||||
onModelSwap(modelId);
|
||||
}
|
||||
};
|
||||
|
||||
// Unified model list: merge API models + local scanned files
|
||||
const allAvailableModels = [
|
||||
...models.map((m) => ({ id: m.id, label: getDisplayName(m.id) })),
|
||||
...localModels.map((m) => ({ id: m.path, label: getDisplayName(m.path) })),
|
||||
];
|
||||
// Deduplicate by id
|
||||
const seen = new Set<string>();
|
||||
const dedupedModels = allAvailableModels.filter((m) => {
|
||||
if (seen.has(m.id)) return false;
|
||||
seen.add(m.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Chat selector: only Cooper + Iron (Steel is settings-only)
|
||||
const chatSelectorModels = dedupedModels.filter((m) => isInChatSelector(m.id));
|
||||
|
||||
const selectedLabel = getDisplayName(selectedModel) || "No model";
|
||||
|
||||
return (
|
||||
<div className="model-selector-pill-wrapper" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="model-selector-pill"
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
disabled={chatSelectorModels.length === 0 || isSwapping}
|
||||
title={chatSelectorModels.length === 0 ? "Start server to load models" : ""}
|
||||
aria-label="Choose active model"
|
||||
aria-expanded={dropdownOpen}
|
||||
>
|
||||
<span className={`model-status-dot status-${status}`} aria-hidden="true" />
|
||||
{isSwapping ? (
|
||||
<Loader2 size={12} strokeWidth={2} className="animate-spin" />
|
||||
) : (
|
||||
<span className="model-selector-label">{selectedLabel}</span>
|
||||
)}
|
||||
<ChevronDown size={12} strokeWidth={2} className="model-selector-chevron" />
|
||||
</button>
|
||||
{dropdownOpen && (
|
||||
<ul className="model-selector-menu" role="listbox">
|
||||
{chatSelectorModels.map((m) => (
|
||||
<li
|
||||
key={m.id}
|
||||
role="option"
|
||||
aria-selected={m.id === selectedModel}
|
||||
className={`model-selector-item${m.id === selectedModel ? " model-selector-item--selected" : ""}`}
|
||||
onClick={() => handleSelectModel(m.id)}
|
||||
>
|
||||
{m.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user