feat(KB-627): Steps 1-2 — Create ModelSelectionModal and integrate with QuickEntryBox

This commit is contained in:
gsxdsm
2026-03-31 17:51:20 -07:00
parent dbfb8bc1d9
commit be5a7d9b3f
5 changed files with 405 additions and 95 deletions

View File

@@ -5,6 +5,7 @@ import type { ModelInfo, RefinementType } from "../api";
import { fetchModels, refineText, getRefineErrorMessage } from "../api";
import { Link, Brain, Lightbulb, ListTree, Sparkles, Save } from "lucide-react";
import { CustomModelDropdown } from "./CustomModelDropdown";
import { ModelSelectionModal } from "./ModelSelectionModal";
const STORAGE_KEY = "kb-quick-entry-text";
@@ -60,7 +61,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
const [dependencies, setDependencies] = useState<string[]>([]);
const [showDeps, setShowDeps] = useState(false);
const [depSearch, setDepSearch] = useState("");
const [showModels, setShowModels] = useState(false);
const [isModelModalOpen, setIsModelModalOpen] = useState(false);
const [executorProvider, setExecutorProvider] = useState<string | undefined>(undefined);
const [executorModelId, setExecutorModelId] = useState<string | undefined>(undefined);
const [validatorProvider, setValidatorProvider] = useState<string | undefined>(undefined);
@@ -200,7 +201,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setValidatorProvider(undefined);
setValidatorModelId(undefined);
setShowDeps(false);
setShowModels(false);
setIsModelModalOpen(false);
setIsRefineMenuOpen(false);
setIsRefining(false);
setIsExpanded(false);
@@ -266,10 +267,14 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
handleSubmit();
} else if (e.key === "Escape") {
e.preventDefault();
// Close modal first if open
if (isModelModalOpen) {
setIsModelModalOpen(false);
return;
}
// Close dropdowns first if open
if (showDeps || showModels || isRefineMenuOpen) {
if (showDeps || isRefineMenuOpen) {
setShowDeps(false);
setShowModels(false);
setIsRefineMenuOpen(false);
return;
}
@@ -295,7 +300,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
textareaRef.current?.blur();
}
},
[handleSubmit, description, isExpanded, showDeps, showModels, isRefineMenuOpen, resetForm],
[handleSubmit, description, isExpanded, showDeps, isModelModalOpen, isRefineMenuOpen, resetForm],
);
const handleFocus = useCallback(() => {
@@ -316,7 +321,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
// Collapse after a short delay to allow click events on dropdowns
// Collapse regardless of content - only check if dropdowns are open
blurTimeoutRef.current = setTimeout(() => {
if (!showDeps && !showModels && !isRefineMenuOpen) {
if (!showDeps && !isModelModalOpen && !isRefineMenuOpen) {
setIsExpanded(false);
// Reset height when collapsing
if (textareaRef.current) {
@@ -325,7 +330,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
}
blurTimeoutRef.current = null;
}, 200);
}, [showDeps, showModels, isRefineMenuOpen]);
}, [showDeps, isModelModalOpen, isRefineMenuOpen]);
const toggleDep = useCallback((id: string) => {
setDependencies((prev) =>
@@ -336,17 +341,14 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
const toggleDepsDropdown = useCallback(() => {
setShowDeps((prev) => {
const next = !prev;
if (next) setShowModels(false);
if (next) setIsModelModalOpen(false);
return next;
});
}, []);
const toggleModelsDropdown = useCallback(() => {
setShowModels((prev) => {
const next = !prev;
if (next) setShowDeps(false);
return next;
});
const openModelModal = useCallback(() => {
setIsModelModalOpen(true);
setShowDeps(false);
}, []);
const handleExecutorChange = useCallback((value: string) => {
@@ -361,17 +363,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setValidatorModelId(next.modelId);
}, []);
const handleModelDropdownMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target;
if (
target instanceof HTMLElement &&
(target.closest("button") || target.closest("input"))
) {
return;
}
e.preventDefault();
}, []);
const handlePlanClick = useCallback(() => {
const trimmed = description.trim();
if (!trimmed) {
@@ -525,8 +516,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<button
type="button"
className="btn btn-sm quick-entry-model-trigger"
onClick={toggleModelsDropdown}
aria-expanded={showModels}
onClick={openModelModal}
aria-expanded={isModelModalOpen}
aria-haspopup="dialog"
data-testid="quick-entry-models-button"
>
@@ -535,69 +526,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
? ` ${selectedModelCount} model${selectedModelCount === 1 ? "" : "s"}`
: " Models"}
</button>
{showModels && (
<div
className="inline-create-model-dropdown"
onMouseDown={handleModelDropdownMouseDown}
>
{modelsLoading ? (
<div className="inline-create-model-empty">Loading models</div>
) : modelsError ? (
<div className="inline-create-model-empty">
<span>Failed to load models.</span>
<button
type="button"
className="btn btn-sm"
onClick={() => void loadModels()}
>
Retry
</button>
</div>
) : loadedModels.length === 0 ? (
<div className="inline-create-model-empty">
No models available. Configure authentication in Settings to enable model selection.
</div>
) : (
<>
<div className="inline-create-model-row">
<label htmlFor="quick-entry-executor-model" className="inline-create-model-label">
Executor Model
</label>
<span className={`model-badge ${hasExecutorOverride ? "model-badge-custom" : "model-badge-default"}`}>
{getModelBadgeLabel(executorProvider, executorModelId)}
</span>
<CustomModelDropdown
id="quick-entry-executor-model"
label="Executor Model"
value={executorSelectionValue}
onChange={handleExecutorChange}
models={loadedModels}
disabled={isSubmitting}
placeholder="Select executor model…"
/>
</div>
<div className="inline-create-model-row">
<label htmlFor="quick-entry-validator-model" className="inline-create-model-label">
Validator Model
</label>
<span className={`model-badge ${hasValidatorOverride ? "model-badge-custom" : "model-badge-default"}`}>
{getModelBadgeLabel(validatorProvider, validatorModelId)}
</span>
<CustomModelDropdown
id="quick-entry-validator-model"
label="Validator Model"
value={validatorSelectionValue}
onChange={handleValidatorChange}
models={loadedModels}
disabled={isSubmitting}
placeholder="Select validator model…"
/>
</div>
</>
)}
</div>
)}
</div>
{!isSubmitting && (
@@ -698,6 +626,19 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
</div>
</div>
)}
<ModelSelectionModal
isOpen={isModelModalOpen}
onClose={() => setIsModelModalOpen(false)}
models={loadedModels}
executorValue={executorSelectionValue}
validatorValue={validatorSelectionValue}
onExecutorChange={handleExecutorChange}
onValidatorChange={handleValidatorChange}
modelsLoading={modelsLoading}
modelsError={modelsError}
onRetry={loadModels}
/>
</div>
);
}