Added LLM selection config

This commit is contained in:
Utkarsh-Patel-13 2025-07-22 17:20:14 -07:00
parent d8797b4f71
commit 1e441e07a3

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { ChatInput } from "@llamaindex/chat-ui"; import { ChatInput } from "@llamaindex/chat-ui";
import { FolderOpen, Check } from "lucide-react"; import { FolderOpen, Check, Zap, Brain } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@ -18,6 +18,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Suspense, useState, useCallback } from "react"; import { Suspense, useState, useCallback } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useDocuments, Document } from "@/hooks/use-documents"; import { useDocuments, Document } from "@/hooks/use-documents";
@ -28,6 +29,7 @@ import {
ConnectorButton as ConnectorButtonComponent, ConnectorButton as ConnectorButtonComponent,
} from "@/components/chat/ConnectorComponents"; } from "@/components/chat/ConnectorComponents";
import { ResearchMode } from "@/components/chat"; import { ResearchMode } from "@/components/chat";
import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs";
import React from "react"; import React from "react";
const DocumentSelector = React.memo( const DocumentSelector = React.memo(
@ -65,9 +67,12 @@ const DocumentSelector = React.memo(
const handleDone = useCallback(() => { const handleDone = useCallback(() => {
setIsOpen(false); setIsOpen(false);
}, [selectedDocuments]); }, []);
const selectedCount = selectedDocuments.length; const selectedCount = React.useMemo(
() => selectedDocuments.length,
[selectedDocuments.length]
);
return ( return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}> <Dialog open={isOpen} onOpenChange={handleOpenChange}>
@ -120,6 +125,8 @@ const DocumentSelector = React.memo(
} }
); );
DocumentSelector.displayName = "DocumentSelector";
const ConnectorSelector = React.memo( const ConnectorSelector = React.memo(
({ ({
onSelectionChange, onSelectionChange,
@ -240,24 +247,37 @@ const ConnectorSelector = React.memo(
} }
); );
const SearchModeSelector = ({ ConnectorSelector.displayName = "ConnectorSelector";
const SearchModeSelector = React.memo(
({
searchMode, searchMode,
onSearchModeChange, onSearchModeChange,
}: { }: {
searchMode?: "DOCUMENTS" | "CHUNKS"; searchMode?: "DOCUMENTS" | "CHUNKS";
onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void;
}) => { }) => {
const handleDocumentsClick = React.useCallback(() => {
onSearchModeChange?.("DOCUMENTS");
}, [onSearchModeChange]);
const handleChunksClick = React.useCallback(() => {
onSearchModeChange?.("CHUNKS");
}, [onSearchModeChange]);
return ( return (
<div className="flex items-center gap-1 sm:gap-2"> <div className="flex items-center gap-1 sm:gap-2">
<span className="text-xs text-muted-foreground hidden sm:block"> <span className="text-xs text-muted-foreground hidden sm:block">
Scope: Scope:
</span> </span>
<div className="flex rounded-md border"> <div className="flex rounded-md border border-border overflow-hidden">
<Button <Button
variant={searchMode === "DOCUMENTS" ? "default" : "ghost"} variant={
searchMode === "DOCUMENTS" ? "default" : "ghost"
}
size="sm" size="sm"
className="rounded-r-none border-r h-8 px-2 sm:px-3 text-xs" className="rounded-none border-r h-8 px-2 sm:px-3 text-xs transition-all duration-200 hover:bg-muted/80"
onClick={() => onSearchModeChange?.("DOCUMENTS")} onClick={handleDocumentsClick}
> >
<span className="hidden sm:inline">Documents</span> <span className="hidden sm:inline">Documents</span>
<span className="sm:hidden">Docs</span> <span className="sm:hidden">Docs</span>
@ -265,58 +285,266 @@ const SearchModeSelector = ({
<Button <Button
variant={searchMode === "CHUNKS" ? "default" : "ghost"} variant={searchMode === "CHUNKS" ? "default" : "ghost"}
size="sm" size="sm"
className="rounded-l-none h-8 px-2 sm:px-3 text-xs" className="rounded-none h-8 px-2 sm:px-3 text-xs transition-all duration-200 hover:bg-muted/80"
onClick={() => onSearchModeChange?.("CHUNKS")} onClick={handleChunksClick}
> >
Chunks Chunks
</Button> </Button>
</div> </div>
</div> </div>
); );
}; }
);
const ResearchModeSelector = ({ SearchModeSelector.displayName = "SearchModeSelector";
const ResearchModeSelector = React.memo(
({
researchMode, researchMode,
onResearchModeChange, onResearchModeChange,
}: { }: {
researchMode?: ResearchMode; researchMode?: ResearchMode;
onResearchModeChange?: (mode: ResearchMode) => void; onResearchModeChange?: (mode: ResearchMode) => void;
}) => { }) => {
const handleValueChange = React.useCallback(
(value: string) => {
onResearchModeChange?.(value as ResearchMode);
},
[onResearchModeChange]
);
// Memoize mode options to prevent recreation
const modeOptions = React.useMemo(
() => [
{ value: "QNA", label: "Q&A", shortLabel: "Q&A" },
{
value: "REPORT_GENERAL",
label: "General Report",
shortLabel: "General",
},
{
value: "REPORT_DEEP",
label: "Deep Report",
shortLabel: "Deep",
},
{
value: "REPORT_DEEPER",
label: "Deeper Report",
shortLabel: "Deeper",
},
],
[]
);
return ( return (
<div className="flex items-center gap-1 sm:gap-2"> <div className="flex items-center gap-1 sm:gap-2">
<span className="text-xs text-muted-foreground hidden sm:block"> <span className="text-xs text-muted-foreground hidden sm:block">
Mode: Mode:
</span> </span>
<Select <Select value={researchMode} onValueChange={handleValueChange}>
value={researchMode} <SelectTrigger className="w-auto min-w-[80px] sm:min-w-[120px] h-8 text-xs border-border bg-background hover:bg-muted/50 transition-colors duration-200 focus:ring-2 focus:ring-primary/20">
onValueChange={(value) => <SelectValue placeholder="Mode" className="text-xs" />
onResearchModeChange?.(value as ResearchMode)
}
>
<SelectTrigger className="w-auto min-w-[80px] sm:min-w-[120px] h-8 text-xs">
<SelectValue placeholder="Mode" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent align="end" className="min-w-[140px]">
<SelectItem value="QNA">Q&A</SelectItem> <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b bg-muted/30">
<SelectItem value="REPORT_GENERAL"> Research Mode
<span className="hidden sm:inline">General Report</span> </div>
<span className="sm:hidden">General</span> {modeOptions.map((option) => (
</SelectItem> <SelectItem
<SelectItem value="REPORT_DEEP"> key={option.value}
<span className="hidden sm:inline">Deep Report</span> value={option.value}
<span className="sm:hidden">Deep</span> className="px-3 py-2 cursor-pointer hover:bg-accent/50 focus:bg-accent"
</SelectItem> >
<SelectItem value="REPORT_DEEPER"> <span className="hidden sm:inline">
<span className="hidden sm:inline">Deeper Report</span> {option.label}
<span className="sm:hidden">Deeper</span> </span>
<span className="sm:hidden">
{option.shortLabel}
</span>
</SelectItem> </SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
); );
}; }
);
const CustomChatInputOptions = ({ ResearchModeSelector.displayName = "ResearchModeSelector";
const LLMSelector = React.memo(() => {
const { llmConfigs, loading: llmLoading, error } = useLLMConfigs();
const {
preferences,
updatePreferences,
loading: preferencesLoading,
} = useLLMPreferences();
const isLoading = llmLoading || preferencesLoading;
// Memoize the selected config to avoid repeated lookups
const selectedConfig = React.useMemo(() => {
if (!preferences.fast_llm_id || !llmConfigs.length) return null;
return (
llmConfigs.find(
(config) => config.id === preferences.fast_llm_id
) || null
);
}, [preferences.fast_llm_id, llmConfigs]);
// Memoize the display value for the trigger
const displayValue = React.useMemo(() => {
if (!selectedConfig) return null;
return (
<div className="flex items-center gap-1">
<span className="font-medium text-xs">
{selectedConfig.provider}
</span>
<span className="text-muted-foreground"></span>
<span className="hidden sm:inline text-muted-foreground text-xs truncate max-w-[60px]">
{selectedConfig.name}
</span>
</div>
);
}, [selectedConfig]);
const handleValueChange = React.useCallback(
(value: string) => {
const llmId = value ? parseInt(value, 10) : undefined;
updatePreferences({ fast_llm_id: llmId });
},
[updatePreferences]
);
// Loading skeleton
if (isLoading) {
return (
<div className="h-8 min-w-[100px] sm:min-w-[120px]">
<div className="h-8 rounded-md bg-muted animate-pulse flex items-center px-3">
<div className="w-3 h-3 rounded bg-muted-foreground/20 mr-2" />
<div className="h-3 w-16 rounded bg-muted-foreground/20" />
</div>
</div>
);
}
// Error state
if (error) {
return (
<div className="h-8 min-w-[100px] sm:min-w-[120px]">
<Button
variant="outline"
size="sm"
className="h-8 px-3 text-xs text-destructive border-destructive/50 hover:bg-destructive/10"
disabled
>
<span className="text-xs">Error</span>
</Button>
</div>
);
}
return (
<div className="h-8 min-w-0">
<Select
value={preferences.fast_llm_id?.toString() || ""}
onValueChange={handleValueChange}
disabled={isLoading}
>
<SelectTrigger className="h-8 w-auto min-w-[100px] sm:min-w-[120px] px-3 text-xs border-border bg-background hover:bg-muted/50 transition-colors duration-200 focus:ring-2 focus:ring-primary/20">
<div className="flex items-center gap-2 min-w-0">
<Zap className="h-3 w-3 text-primary flex-shrink-0" />
<SelectValue placeholder="Fast LLM" className="text-xs">
{displayValue || (
<span className="text-muted-foreground">
Select LLM
</span>
)}
</SelectValue>
</div>
</SelectTrigger>
<SelectContent align="end" className="w-[300px] max-h-[400px]">
<div className="px-3 py-2 text-xs font-medium text-muted-foreground border-b bg-muted/30">
<div className="flex items-center gap-2">
<Zap className="h-3 w-3" />
Fast LLM Selection
</div>
</div>
{llmConfigs.length === 0 ? (
<div className="px-4 py-6 text-center">
<div className="mx-auto w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-3">
<Brain className="h-5 w-5 text-muted-foreground" />
</div>
<h4 className="text-sm font-medium mb-1">
No LLM configurations
</h4>
<p className="text-xs text-muted-foreground mb-3">
Configure AI models to get started
</p>
<Button
variant="outline"
size="sm"
className="text-xs"
onClick={() =>
window.open("/settings", "_blank")
}
>
Open Settings
</Button>
</div>
) : (
<div className="py-1">
{llmConfigs.map((config) => (
<SelectItem
key={config.id}
value={config.id.toString()}
className="px-3 py-2 cursor-pointer hover:bg-accent/50 focus:bg-accent"
>
<div className="flex items-center justify-between w-full min-w-0">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary/10 flex-shrink-0">
<Brain className="h-4 w-4 text-primary" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-sm truncate">
{config.name}
</span>
<Badge
variant="outline"
className="text-xs px-1.5 py-0.5 flex-shrink-0"
>
{config.provider}
</Badge>
</div>
<p className="text-xs text-muted-foreground font-mono truncate">
{config.model_name}
</p>
</div>
</div>
{preferences.fast_llm_id ===
config.id && (
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-primary ml-2 flex-shrink-0">
<Check className="h-3 w-3 text-primary-foreground" />
</div>
)}
</div>
</SelectItem>
))}
</div>
)}
</SelectContent>
</Select>
</div>
);
});
LLMSelector.displayName = "LLMSelector";
const CustomChatInputOptions = React.memo(
({
onDocumentSelectionChange, onDocumentSelectionChange,
selectedDocuments, selectedDocuments,
onConnectorSelectionChange, onConnectorSelectionChange,
@ -325,7 +553,7 @@ const CustomChatInputOptions = ({
onSearchModeChange, onSearchModeChange,
researchMode, researchMode,
onResearchModeChange, onResearchModeChange,
}: { }: {
onDocumentSelectionChange?: (documents: Document[]) => void; onDocumentSelectionChange?: (documents: Document[]) => void;
selectedDocuments?: Document[]; selectedDocuments?: Document[];
onConnectorSelectionChange?: (connectorTypes: string[]) => void; onConnectorSelectionChange?: (connectorTypes: string[]) => void;
@ -334,16 +562,24 @@ const CustomChatInputOptions = ({
onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void;
researchMode?: ResearchMode; researchMode?: ResearchMode;
onResearchModeChange?: (mode: ResearchMode) => void; onResearchModeChange?: (mode: ResearchMode) => void;
}) => { }) => {
// Memoize the loading fallback to prevent recreation
const loadingFallback = React.useMemo(
() => (
<div className="h-8 min-w-[100px] animate-pulse bg-muted rounded-md" />
),
[]
);
return ( return (
<div className="flex flex-wrap gap-2 sm:gap-3 items-center justify-start"> <div className="flex flex-wrap gap-2 sm:gap-3 items-center justify-start">
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={loadingFallback}>
<DocumentSelector <DocumentSelector
onSelectionChange={onDocumentSelectionChange} onSelectionChange={onDocumentSelectionChange}
selectedDocuments={selectedDocuments} selectedDocuments={selectedDocuments}
/> />
</Suspense> </Suspense>
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={loadingFallback}>
<ConnectorSelector <ConnectorSelector
onSelectionChange={onConnectorSelectionChange} onSelectionChange={onConnectorSelectionChange}
selectedConnectors={selectedConnectors} selectedConnectors={selectedConnectors}
@ -357,11 +593,16 @@ const CustomChatInputOptions = ({
researchMode={researchMode} researchMode={researchMode}
onResearchModeChange={onResearchModeChange} onResearchModeChange={onResearchModeChange}
/> />
<LLMSelector />
</div> </div>
); );
}; }
);
export const CustomChatInput = ({ CustomChatInputOptions.displayName = "CustomChatInputOptions";
export const CustomChatInput = React.memo(
({
onDocumentSelectionChange, onDocumentSelectionChange,
selectedDocuments, selectedDocuments,
onConnectorSelectionChange, onConnectorSelectionChange,
@ -370,7 +611,7 @@ export const CustomChatInput = ({
onSearchModeChange, onSearchModeChange,
researchMode, researchMode,
onResearchModeChange, onResearchModeChange,
}: { }: {
onDocumentSelectionChange?: (documents: Document[]) => void; onDocumentSelectionChange?: (documents: Document[]) => void;
selectedDocuments?: Document[]; selectedDocuments?: Document[];
onConnectorSelectionChange?: (connectorTypes: string[]) => void; onConnectorSelectionChange?: (connectorTypes: string[]) => void;
@ -379,7 +620,7 @@ export const CustomChatInput = ({
onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void; onSearchModeChange?: (mode: "DOCUMENTS" | "CHUNKS") => void;
researchMode?: ResearchMode; researchMode?: ResearchMode;
onResearchModeChange?: (mode: ResearchMode) => void; onResearchModeChange?: (mode: ResearchMode) => void;
}) => { }) => {
return ( return (
<ChatInput> <ChatInput>
<ChatInput.Form className="flex gap-2"> <ChatInput.Form className="flex gap-2">
@ -398,4 +639,7 @@ export const CustomChatInput = ({
/> />
</ChatInput> </ChatInput>
); );
}; }
);
CustomChatInput.displayName = "CustomChatInput";