mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-11 08:30:51 +02:00
348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
import React, { useState, useRef, useCallback, useEffect } from "react";
|
|
import { useAssistants } from "@/components/context/AssistantsContext";
|
|
import { useChatContext } from "@/components/context/ChatContext";
|
|
import { useUser } from "@/components/user/UserProvider";
|
|
import { Persona } from "@/app/admin/assistants/interfaces";
|
|
import { FiChevronDown } from "react-icons/fi";
|
|
import { destructureValue, getFinalLLM } from "@/lib/llm/utils";
|
|
import { updateModelOverrideForChatSession } from "@/app/chat/lib";
|
|
import { debounce } from "lodash";
|
|
import { LlmList } from "@/components/llm/LLMList";
|
|
import { checkPersonaRequiresImageGeneration } from "@/app/admin/assistants/lib";
|
|
import {
|
|
DndContext,
|
|
closestCenter,
|
|
KeyboardSensor,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
DragEndEvent,
|
|
} from "@dnd-kit/core";
|
|
import {
|
|
arrayMove,
|
|
SortableContext,
|
|
sortableKeyboardCoordinates,
|
|
verticalListSortingStrategy,
|
|
} from "@dnd-kit/sortable";
|
|
import { DraggableAssistantCard } from "@/components/assistants/AssistantCards";
|
|
import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
|
|
|
|
import Text from "@/components/ui/text";
|
|
import { getDisplayNameForModel, LlmOverrideManager } from "@/lib/hooks";
|
|
import { Tab } from "@headlessui/react";
|
|
import { AssistantIcon } from "../assistants/AssistantIcon";
|
|
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
import { restrictToParentElement } from "@dnd-kit/modifiers";
|
|
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "../ui/drawer";
|
|
import { truncateString } from "@/lib/utils";
|
|
|
|
const AssistantSelector = ({
|
|
liveAssistant,
|
|
onAssistantChange,
|
|
chatSessionId,
|
|
llmOverrideManager,
|
|
isMobile,
|
|
}: {
|
|
liveAssistant: Persona;
|
|
onAssistantChange: (assistant: Persona) => void;
|
|
chatSessionId?: string;
|
|
llmOverrideManager?: LlmOverrideManager;
|
|
isMobile: boolean;
|
|
}) => {
|
|
const { finalAssistants } = useAssistants();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
const { llmProviders } = useChatContext();
|
|
const { user } = useUser();
|
|
const [assistants, setAssistants] = useState<Persona[]>(finalAssistants);
|
|
const [isTemperatureExpanded, setIsTemperatureExpanded] = useState(false);
|
|
const [localTemperature, setLocalTemperature] = useState<number>(
|
|
llmOverrideManager?.temperature || 0
|
|
);
|
|
|
|
// Initialize selectedTab from localStorage
|
|
const [selectedTab, setSelectedTab] = useState<number>(() => {
|
|
const storedTab = localStorage.getItem("assistantSelectorSelectedTab");
|
|
return storedTab !== null ? Number(storedTab) : 0;
|
|
});
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 8,
|
|
},
|
|
}),
|
|
useSensor(KeyboardSensor, {
|
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
})
|
|
);
|
|
|
|
const handleDragEnd = async (event: DragEndEvent) => {
|
|
const { active, over } = event;
|
|
if (over && active.id !== over.id) {
|
|
const oldIndex = assistants.findIndex(
|
|
(item) => item.id.toString() === active.id
|
|
);
|
|
const newIndex = assistants.findIndex(
|
|
(item) => item.id.toString() === over.id
|
|
);
|
|
const updatedAssistants = arrayMove(assistants, oldIndex, newIndex);
|
|
setAssistants(updatedAssistants);
|
|
await updateUserAssistantList(updatedAssistants.map((a) => a.id));
|
|
}
|
|
};
|
|
|
|
const debouncedSetTemperature = useCallback(
|
|
(value: number) => {
|
|
const debouncedFunction = debounce((value: number) => {
|
|
llmOverrideManager?.setTemperature(value);
|
|
}, 300);
|
|
return debouncedFunction(value);
|
|
},
|
|
[llmOverrideManager]
|
|
);
|
|
|
|
const handleTemperatureChange = (value: number) => {
|
|
setLocalTemperature(value);
|
|
debouncedSetTemperature(value);
|
|
};
|
|
|
|
// Handle tab change and update localStorage
|
|
const handleTabChange = (index: number) => {
|
|
setSelectedTab(index);
|
|
localStorage.setItem("assistantSelectorSelectedTab", index.toString());
|
|
};
|
|
|
|
// Get the user's default model
|
|
const userDefaultModel = user?.preferences.default_model;
|
|
|
|
const [_, currentLlm] = getFinalLLM(
|
|
llmProviders,
|
|
liveAssistant,
|
|
llmOverrideManager?.llmOverride ?? null
|
|
);
|
|
|
|
const requiresImageGeneration =
|
|
checkPersonaRequiresImageGeneration(liveAssistant);
|
|
|
|
const content = (
|
|
<>
|
|
<Tab.Group selectedIndex={selectedTab} onChange={handleTabChange}>
|
|
<Tab.List className="flex p-1 space-x-1 bg-gray-100 rounded-t-md">
|
|
<Tab
|
|
className={({ selected }) =>
|
|
`w-full py-2.5 text-sm leading-5 font-medium rounded-md
|
|
${
|
|
selected
|
|
? "bg-white text-gray-700 shadow"
|
|
: "text-gray-500 hover:bg-white/[0.12] hover:text-gray-700"
|
|
}`
|
|
}
|
|
>
|
|
Assistant
|
|
</Tab>
|
|
<Tab
|
|
className={({ selected }) =>
|
|
`w-full py-2.5 text-sm leading-5 font-medium rounded-md
|
|
${
|
|
selected
|
|
? "bg-white text-gray-700 shadow"
|
|
: "text-gray-500 hover:bg-white/[0.12] hover:text-gray-700"
|
|
}`
|
|
}
|
|
>
|
|
Model
|
|
</Tab>
|
|
</Tab.List>
|
|
<Tab.Panels>
|
|
<Tab.Panel className="p-3">
|
|
<div className="mb-4">
|
|
<h3 className="text-center text-lg font-semibold text-gray-800">
|
|
Choose an Assistant
|
|
</h3>
|
|
</div>
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={handleDragEnd}
|
|
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
|
|
>
|
|
<SortableContext
|
|
items={assistants.map((a) => a.id.toString())}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
{assistants.map((assistant) => (
|
|
<DraggableAssistantCard
|
|
key={assistant.id.toString()}
|
|
assistant={assistant}
|
|
isSelected={liveAssistant.id === assistant.id}
|
|
onSelect={(assistant) => {
|
|
onAssistantChange(assistant);
|
|
setIsOpen(false);
|
|
}}
|
|
llmName={
|
|
assistant.llm_model_version_override ??
|
|
userDefaultModel ??
|
|
currentLlm
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
</Tab.Panel>
|
|
<Tab.Panel className="p-3">
|
|
<div className="mb-4">
|
|
<h3 className="text-center text-lg font-semibold text-gray-800 ">
|
|
Choose a Model
|
|
</h3>
|
|
</div>
|
|
<LlmList
|
|
currentAssistant={liveAssistant}
|
|
requiresImageGeneration={requiresImageGeneration}
|
|
llmProviders={llmProviders}
|
|
currentLlm={currentLlm}
|
|
userDefault={userDefaultModel}
|
|
includeUserDefault={true}
|
|
onSelect={(value: string | null) => {
|
|
if (value == null) return;
|
|
const { modelName, name, provider } = destructureValue(value);
|
|
llmOverrideManager?.setLlmOverride({
|
|
name,
|
|
provider,
|
|
modelName,
|
|
});
|
|
if (chatSessionId) {
|
|
updateModelOverrideForChatSession(chatSessionId, value);
|
|
}
|
|
setIsOpen(false);
|
|
}}
|
|
/>
|
|
<div className="mt-4">
|
|
<button
|
|
className="flex items-center text-sm font-medium transition-colors duration-200"
|
|
onClick={() => setIsTemperatureExpanded(!isTemperatureExpanded)}
|
|
>
|
|
<span className="mr-2 text-xs text-primary">
|
|
{isTemperatureExpanded ? "▼" : "►"}
|
|
</span>
|
|
<span>Temperature</span>
|
|
</button>
|
|
|
|
{isTemperatureExpanded && (
|
|
<>
|
|
<Text className="mt-2 mb-8">
|
|
Adjust the temperature of the LLM. Higher temperatures will
|
|
make the LLM generate more creative and diverse responses,
|
|
while lower temperature will make the LLM generate more
|
|
conservative and focused responses.
|
|
</Text>
|
|
|
|
<div className="relative w-full">
|
|
<input
|
|
type="range"
|
|
onChange={(e) =>
|
|
handleTemperatureChange(parseFloat(e.target.value))
|
|
}
|
|
className="w-full p-2 border border-border rounded-md"
|
|
min="0"
|
|
max="2"
|
|
step="0.01"
|
|
value={localTemperature}
|
|
/>
|
|
<div
|
|
className="absolute text-sm"
|
|
style={{
|
|
left: `${(localTemperature || 0) * 50}%`,
|
|
transform: `translateX(-${Math.min(
|
|
Math.max((localTemperature || 0) * 50, 10),
|
|
90
|
|
)}%)`,
|
|
top: "-1.5rem",
|
|
}}
|
|
>
|
|
{localTemperature}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Tab.Panel>
|
|
</Tab.Panels>
|
|
</Tab.Group>
|
|
</>
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!isMobile) {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node)
|
|
) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
};
|
|
}
|
|
}, [isMobile]);
|
|
|
|
return (
|
|
<div className="pointer-events-auto relative" ref={dropdownRef}>
|
|
<div className="flex justify-center">
|
|
<div
|
|
onClick={() => {
|
|
setIsOpen(!isOpen);
|
|
// Get selectedTab from localStorage when opening
|
|
const storedTab = localStorage.getItem(
|
|
"assistantSelectorSelectedTab"
|
|
);
|
|
setSelectedTab(storedTab !== null ? Number(storedTab) : 0);
|
|
}}
|
|
className="flex items-center gap-x-2 justify-between px-6 py-3 text-sm font-medium text-white bg-black rounded-full shadow-lg hover:shadow-xl transition-all duration-300 cursor-pointer"
|
|
>
|
|
<div className="flex gap-x-2 items-center">
|
|
<AssistantIcon assistant={liveAssistant} size="xs" />
|
|
<span className="font-bold">{liveAssistant.name}</span>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<span className="mr-2 text-xs">
|
|
{truncateString(getDisplayNameForModel(currentLlm), 30)}
|
|
</span>
|
|
<FiChevronDown
|
|
className={`w-5 h-5 text-white transition-transform duration-300 transform ${
|
|
isOpen ? "rotate-180" : ""
|
|
}`}
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isMobile ? (
|
|
<Drawer open={isOpen} onOpenChange={setIsOpen}>
|
|
<DrawerContent>
|
|
<DrawerHeader>
|
|
<DrawerTitle>Assistant Selector</DrawerTitle>
|
|
</DrawerHeader>
|
|
{content}
|
|
</DrawerContent>
|
|
</Drawer>
|
|
) : (
|
|
isOpen && (
|
|
<div className="absolute z-10 w-96 mt-2 origin-top-center left-1/2 transform -translate-x-1/2 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
{content}
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AssistantSelector;
|