refactor: update LLM role management logic and enhance UI feedback

This commit is contained in:
Anish Sarkar 2026-04-02 09:58:14 +05:30
parent 407175ffae
commit d2cf3fb3b7
2 changed files with 52 additions and 122 deletions

View file

@ -109,10 +109,11 @@ export const updateLLMPreferencesMutationAtom = atomWithMutation((get) => {
mutationFn: async (request: UpdateLLMPreferencesRequest) => {
return newLLMConfigApiService.updateLLMPreferences(request);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: cacheKeys.newLLMConfigs.preferences(Number(searchSpaceId)),
});
onSuccess: (_data, request: UpdateLLMPreferencesRequest) => {
queryClient.setQueryData(
cacheKeys.newLLMConfigs.preferences(Number(searchSpaceId)),
(old: Record<string, unknown> | undefined) => ({ ...old, ...request.data })
);
},
onError: (error: Error) => {
toast.error(error.message || "Failed to update LLM preferences");

View file

@ -4,16 +4,14 @@ import { useAtomValue } from "jotai";
import {
AlertCircle,
Bot,
CheckCircle,
CircleCheck,
CircleDashed,
FileText,
ImageIcon,
RefreshCw,
RotateCcw,
Save,
Shuffle,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import {
globalImageGenConfigsAtom,
@ -40,6 +38,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { getProviderIcon } from "@/lib/provider-icons";
import { cn } from "@/lib/utils";
@ -48,8 +47,8 @@ const ROLE_DESCRIPTIONS = {
icon: Bot,
title: "Agent LLM",
description: "Primary LLM for chat interactions and agent operations",
color: "text-blue-600 dark:text-blue-400",
bgColor: "bg-blue-500/10",
color: "text-muted-foreground",
bgColor: "bg-muted",
prefKey: "agent_llm_id" as const,
configType: "llm" as const,
},
@ -57,8 +56,8 @@ const ROLE_DESCRIPTIONS = {
icon: FileText,
title: "Document Summary LLM",
description: "Handles document summarization and research synthesis",
color: "text-purple-600 dark:text-purple-400",
bgColor: "bg-purple-500/10",
color: "text-muted-foreground",
bgColor: "bg-muted",
prefKey: "document_summary_llm_id" as const,
configType: "llm" as const,
},
@ -66,8 +65,8 @@ const ROLE_DESCRIPTIONS = {
icon: ImageIcon,
title: "Image Generation Model",
description: "Model used for AI image generation (DALL-E, GPT Image, etc.)",
color: "text-teal-600 dark:text-teal-400",
bgColor: "bg-teal-500/10",
color: "text-muted-foreground",
bgColor: "bg-muted",
prefKey: "image_generation_config_id" as const,
configType: "image" as const,
},
@ -118,88 +117,41 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
image_generation_config_id: preferences.image_generation_config_id ?? "",
}));
const [hasChanges, setHasChanges] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [savingRole, setSavingRole] = useState<string | null>(null);
const savingRef = useRef(false);
useEffect(() => {
const newAssignments = {
agent_llm_id: preferences.agent_llm_id ?? "",
document_summary_llm_id: preferences.document_summary_llm_id ?? "",
image_generation_config_id: preferences.image_generation_config_id ?? "",
};
setAssignments(newAssignments);
setHasChanges(false);
if (!savingRef.current) {
setAssignments({
agent_llm_id: preferences.agent_llm_id ?? "",
document_summary_llm_id: preferences.document_summary_llm_id ?? "",
image_generation_config_id: preferences.image_generation_config_id ?? "",
});
}
}, [
preferences?.agent_llm_id,
preferences?.document_summary_llm_id,
preferences?.image_generation_config_id,
]);
const handleRoleAssignment = (prefKey: string, configId: string) => {
const newAssignments = {
...assignments,
[prefKey]: configId === "unassigned" ? "" : parseInt(configId),
};
const handleRoleAssignment = useCallback(async (prefKey: string, configId: string) => {
const value = configId === "unassigned" ? "" : parseInt(configId);
setAssignments(newAssignments);
setAssignments((prev) => ({ ...prev, [prefKey]: value }));
setSavingRole(prefKey);
savingRef.current = true;
const currentPrefs = {
agent_llm_id: preferences.agent_llm_id ?? "",
document_summary_llm_id: preferences.document_summary_llm_id ?? "",
image_generation_config_id: preferences.image_generation_config_id ?? "",
};
const hasChangesNow = Object.keys(newAssignments).some(
(key) =>
newAssignments[key as keyof typeof newAssignments] !==
currentPrefs[key as keyof typeof currentPrefs]
);
setHasChanges(hasChangesNow);
};
const handleSave = async () => {
setIsSaving(true);
const toNumericOrUndefined = (val: string | number) =>
typeof val === "string" ? (val ? parseInt(val) : undefined) : val;
const numericAssignments = {
agent_llm_id: toNumericOrUndefined(assignments.agent_llm_id),
document_summary_llm_id: toNumericOrUndefined(assignments.document_summary_llm_id),
image_generation_config_id: toNumericOrUndefined(assignments.image_generation_config_id),
};
await updatePreferences({
search_space_id: searchSpaceId,
data: numericAssignments,
});
setHasChanges(false);
toast.success("Role assignments saved successfully!");
setIsSaving(false);
};
const handleReset = () => {
setAssignments({
agent_llm_id: preferences.agent_llm_id ?? "",
document_summary_llm_id: preferences.document_summary_llm_id ?? "",
image_generation_config_id: preferences.image_generation_config_id ?? "",
});
setHasChanges(false);
};
const isAssignmentComplete =
assignments.agent_llm_id !== "" &&
assignments.agent_llm_id !== null &&
assignments.agent_llm_id !== undefined &&
assignments.document_summary_llm_id !== "" &&
assignments.document_summary_llm_id !== null &&
assignments.document_summary_llm_id !== undefined &&
assignments.image_generation_config_id !== "" &&
assignments.image_generation_config_id !== null &&
assignments.image_generation_config_id !== undefined;
try {
await updatePreferences({
search_space_id: searchSpaceId,
data: { [prefKey]: value || undefined },
});
toast.success("Role assignment updated");
} finally {
setSavingRole(null);
savingRef.current = false;
}
}, [updatePreferences, searchSpaceId]);
// Combine global and custom LLM configs
const allLLMConfigs = [
@ -213,6 +165,11 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
...(userImageConfigs ?? []).filter((config) => config.id && config.id.toString().trim() !== ""),
];
const isAssignmentComplete =
allLLMConfigs.some((c) => c.id === assignments.agent_llm_id) &&
allLLMConfigs.some((c) => c.id === assignments.document_summary_llm_id) &&
allImageConfigs.some((c) => c.id === assignments.image_generation_config_id);
const isLoading =
configsLoading ||
preferencesLoading ||
@ -244,9 +201,9 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{isAssignmentComplete && !isLoading && !hasError && (
<Badge
variant="outline"
className="text-xs gap-1.5 border-emerald-500/30 text-emerald-700 dark:text-emerald-300 bg-emerald-500/5"
className="text-xs gap-1.5 text-muted-foreground"
>
<CheckCircle className="h-3 w-3" />
<CircleCheck className="h-3 w-3" />
All roles assigned
</Badge>
)}
@ -332,10 +289,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
const roleAllConfigs = isImageRole ? allImageConfigs : allLLMConfigs;
const assignedConfig = roleAllConfigs.find((config) => config.id === currentAssignment);
const isAssigned =
currentAssignment !== "" &&
currentAssignment !== null &&
currentAssignment !== undefined;
const isAssigned = !!assignedConfig;
const isAutoMode =
assignedConfig && "is_auto_mode" in assignedConfig && assignedConfig.is_auto_mode;
@ -361,8 +315,10 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
</p>
</div>
</div>
{isAssigned ? (
<CheckCircle className="w-4 h-4 text-emerald-500 shrink-0 mt-0.5" />
{savingRole === role.prefKey ? (
<Spinner size="sm" className="shrink-0 mt-0.5 text-muted-foreground" />
) : isAssigned ? (
<CircleCheck className="w-4 h-4 text-muted-foreground/40 shrink-0 mt-0.5" />
) : (
<CircleDashed className="w-4 h-4 text-muted-foreground/40 shrink-0 mt-0.5" />
)}
@ -374,7 +330,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
Configuration
</Label>
<Select
value={currentAssignment?.toString() || "unassigned"}
value={isAssigned ? currentAssignment.toString() : "unassigned"}
onValueChange={(value) => handleRoleAssignment(role.prefKey, value)}
>
<SelectTrigger className="w-full h-9 md:h-10 text-xs md:text-sm">
@ -534,33 +490,6 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
</div>
)}
{/* Save / Reset Bar */}
{hasChanges && (
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-muted/50 p-3 md:p-4">
<p className="text-xs md:text-sm text-muted-foreground">You have unsaved changes</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleReset}
disabled={isSaving}
className="h-8 text-xs gap-1.5"
>
<RotateCcw className="w-3 h-3" />
Reset
</Button>
<Button
size="sm"
onClick={handleSave}
disabled={isSaving}
className="h-8 text-xs gap-1.5"
>
<Save className="w-3 h-3" />
{isSaving ? "Saving…" : "Save Changes"}
</Button>
</div>
</div>
)}
</div>
);
}