feat(FN-807): add model preset selection to QuickEntryBox and InlineCreateCard
- Extend ModelSelectionModal with preset picker and apply-to-task wiring - Wire preset state through QuickEntryBox and InlineCreateCard new-task flows - Add regression tests for preset selection in ModelSelectionModal, QuickEntryBox, InlineCreateCard, and ListView - Update dashboard README with preset-in-quick-add documentation - Clean up unused footer-safe-layout test and adjust styles
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import type { Task, TaskCreateInput } from "@fusion/core";
|
||||
import type { Task, TaskCreateInput, Settings } from "@fusion/core";
|
||||
import type { ModelInfo, RefinementType } from "../api";
|
||||
import { fetchModels, refineText, getRefineErrorMessage, updateGlobalSettings } from "../api";
|
||||
import { fetchModels, fetchSettings, refineText, getRefineErrorMessage, updateGlobalSettings } from "../api";
|
||||
import { Link, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
import { ModelSelectionModal } from "./ModelSelectionModal";
|
||||
@@ -101,6 +101,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const [loadedModels, setLoadedModels] = useState<ModelInfo[]>(availableModels ?? []);
|
||||
const [favoriteProviders, setFavoriteProviders] = useState<string[]>([]);
|
||||
const [favoriteModels, setFavoriteModels] = useState<string[]>([]);
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
const [selectedPresetId, setSelectedPresetId] = useState<string | undefined>(undefined);
|
||||
|
||||
// AI Refinement state
|
||||
const [isRefineMenuOpen, setIsRefineMenuOpen] = useState(false);
|
||||
@@ -114,46 +116,56 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
// If onCreate is not provided, the component is disabled
|
||||
const isDisabled = !onCreate;
|
||||
|
||||
// Fetch models if not provided by parent
|
||||
// Fetch models and settings if not provided by parent
|
||||
useEffect(() => {
|
||||
if (availableModels) {
|
||||
setLoadedModels(availableModels);
|
||||
setModelsLoading(false);
|
||||
setModelsError(null);
|
||||
return;
|
||||
} else {
|
||||
let cancelled = false;
|
||||
setModelsLoading(true);
|
||||
setModelsError(null);
|
||||
fetchModels()
|
||||
.then((response) => {
|
||||
if (!cancelled) {
|
||||
setLoadedModels(response.models);
|
||||
// Only set internal favorites when parent doesn't manage them
|
||||
if (!parentFavoriteProviders) {
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
}
|
||||
if (!parentFavoriteModels) {
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (!cancelled) {
|
||||
setModelsError(err?.message || "Failed to load models");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Also fetch settings for presets
|
||||
fetchSettings()
|
||||
.then((nextSettings) => {
|
||||
if (!cancelled) {
|
||||
setSettings(nextSettings);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore settings fetch failure
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setModelsLoading(true);
|
||||
setModelsError(null);
|
||||
fetchModels()
|
||||
.then((response) => {
|
||||
if (!cancelled) {
|
||||
setLoadedModels(response.models);
|
||||
// Only set internal favorites when parent doesn't manage them
|
||||
if (!parentFavoriteProviders) {
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
}
|
||||
if (!parentFavoriteModels) {
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (!cancelled) {
|
||||
setModelsError(err?.message || "Failed to load models");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [availableModels]);
|
||||
}, [availableModels, parentFavoriteProviders, parentFavoriteModels]);
|
||||
|
||||
const executorSelectionValue = getModelSelectionValue(executorProvider, executorModelId);
|
||||
const validatorSelectionValue = getModelSelectionValue(validatorProvider, validatorModelId);
|
||||
@@ -162,6 +174,13 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const hasValidatorOverride = Boolean(validatorProvider && validatorModelId);
|
||||
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride);
|
||||
|
||||
const availablePresets = settings?.modelPresets || [];
|
||||
const selectedPreset = availablePresets.find((p) => p.id === selectedPresetId);
|
||||
|
||||
const handlePresetChange = useCallback((presetId: string | undefined) => {
|
||||
setSelectedPresetId(presetId);
|
||||
}, []);
|
||||
|
||||
const getModelBadgeLabel = useCallback(
|
||||
(provider?: string, modelId?: string) => {
|
||||
if (!provider || !modelId) return "Using default";
|
||||
@@ -248,6 +267,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
setExecutorModelId(undefined);
|
||||
setValidatorProvider(undefined);
|
||||
setValidatorModelId(undefined);
|
||||
setSelectedPresetId(undefined);
|
||||
setShowDeps(false);
|
||||
setIsModelModalOpen(false);
|
||||
setIsRefineMenuOpen(false);
|
||||
@@ -274,6 +294,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
description: trimmed,
|
||||
column: "triage",
|
||||
dependencies: dependencies.length ? dependencies : undefined,
|
||||
modelPresetId: selectedPresetId,
|
||||
modelProvider: hasExecutorOverride ? executorProvider : undefined,
|
||||
modelId: hasExecutorOverride ? executorModelId : undefined,
|
||||
validatorModelProvider: hasValidatorOverride ? validatorProvider : undefined,
|
||||
@@ -723,9 +744,11 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
data-testid="quick-entry-models-button"
|
||||
>
|
||||
<Brain size={12} style={{ verticalAlign: "middle" }} />
|
||||
{selectedModelCount > 0
|
||||
? ` ${selectedModelCount} model${selectedModelCount === 1 ? "" : "s"}`
|
||||
: " Models"}
|
||||
{selectedPreset
|
||||
? ` ${selectedPreset.name}`
|
||||
: selectedModelCount > 0
|
||||
? ` ${selectedModelCount} model${selectedModelCount === 1 ? "" : "s"}`
|
||||
: " Models"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -766,6 +789,9 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
favoriteModels={effectiveFavoriteModels}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
presets={availablePresets}
|
||||
selectedPresetId={selectedPresetId}
|
||||
onPresetChange={handlePresetChange}
|
||||
/>,
|
||||
document.body,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user