diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx
index aeb543af4..a8b4e570d 100644
--- a/web/src/app/chat/ChatPage.tsx
+++ b/web/src/app/chat/ChatPage.tsx
@@ -43,7 +43,7 @@ import { useContext, useEffect, useRef, useState } from "react";
import { usePopup } from "@/components/admin/connectors/Popup";
import { SEARCH_PARAM_NAMES, shouldSubmitOnLoad } from "./searchParams";
import { useDocumentSelection } from "./useDocumentSelection";
-import { useFilters } from "@/lib/hooks";
+import { useFilters, useLlmOverride } from "@/lib/hooks";
import { computeAvailableFilters } from "@/lib/filters";
import { FeedbackType } from "./types";
import ResizableSection from "@/components/resizable/ResizableSection";
@@ -62,13 +62,16 @@ import { SelectedDocuments } from "./modifiers/SelectedDocuments";
import { ChatFilters } from "./modifiers/ChatFilters";
import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces";
import { buildFilters } from "@/lib/search/utils";
-import { Tabs } from "./sessionSidebar/constants";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import Dropzone from "react-dropzone";
import { LLMProviderDescriptor } from "../admin/models/llm/interfaces";
import { checkLLMSupportsImageInput, getFinalLLM } from "@/lib/llm/utils";
import { InputBarPreviewImage } from "./images/InputBarPreviewImage";
import { Folder } from "./folders/interfaces";
+import { ChatInputBar } from "./input/ChatInputBar";
+import { ConfigurationModal } from "./modal/configuration/ConfigurationModal";
+import { useChatContext } from "@/components/context/ChatContext";
+import { UserDropdown } from "@/components/UserDropdown";
const MAX_INPUT_HEIGHT = 200;
const TEMP_USER_MESSAGE_ID = -1;
@@ -76,32 +79,26 @@ const TEMP_ASSISTANT_MESSAGE_ID = -2;
const SYSTEM_MESSAGE_ID = -3;
export function ChatPage({
- user,
- chatSessions,
- availableSources,
- availableDocumentSets,
- availablePersonas,
- availableTags,
- llmProviders,
- defaultSelectedPersonaId,
documentSidebarInitialWidth,
- defaultSidebarTab,
- folders,
- openedFolders,
+ defaultSelectedPersonaId,
}: {
- user: User | null;
- chatSessions: ChatSession[];
- availableSources: ValidSources[];
- availableDocumentSets: DocumentSet[];
- availablePersonas: Persona[];
- availableTags: Tag[];
- llmProviders: LLMProviderDescriptor[];
- defaultSelectedPersonaId?: number; // what persona to default to
documentSidebarInitialWidth?: number;
- defaultSidebarTab?: Tabs;
- folders: Folder[];
- openedFolders: { [key: number]: boolean };
+ defaultSelectedPersonaId?: number;
}) {
+ const [configModalActiveTab, setConfigModalActiveTab] = useState<
+ string | null
+ >(null);
+ let {
+ user,
+ chatSessions,
+ availableSources,
+ availableDocumentSets,
+ availablePersonas,
+ llmProviders,
+ folders,
+ openedFolders,
+ } = useChatContext();
+
const router = useRouter();
const searchParams = useSearchParams();
const existingChatIdRaw = searchParams.get("chatId");
@@ -145,6 +142,13 @@ export function ChatPage({
filterManager.setSelectedSources([]);
filterManager.setSelectedTags([]);
filterManager.setTimeRange(null);
+ // reset LLM overrides
+ llmOverrideManager.setLlmOverride({
+ name: "",
+ provider: "",
+ modelName: "",
+ });
+ llmOverrideManager.setTemperature(null);
// remove uploaded files
setCurrentMessageFileIds([]);
@@ -386,6 +390,8 @@ export function ChatPage({
availableDocumentSets,
});
+ const llmOverrideManager = useLlmOverride();
+
// state for cancelling streaming
const [isCancelled, setIsCancelled] = useState(false);
const isCancelledRef = useRef(isCancelled);
@@ -592,9 +598,13 @@ export function ChatPage({
.map((document) => document.db_doc_id as number),
queryOverride,
forceSearch,
+ modelProvider: llmOverrideManager.llmOverride.name || undefined,
modelVersion:
- searchParams.get(SEARCH_PARAM_NAMES.MODEL_VERSION) || undefined,
+ llmOverrideManager.llmOverride.modelName ||
+ searchParams.get(SEARCH_PARAM_NAMES.MODEL_VERSION) ||
+ undefined,
temperature:
+ llmOverrideManager.temperature ||
parseFloat(searchParams.get(SEARCH_PARAM_NAMES.TEMPERATURE) || "") ||
undefined,
systemPromptOverride:
@@ -767,6 +777,32 @@ export function ChatPage({
}
};
+ const handleImageUpload = (acceptedFiles: File[]) => {
+ const llmAcceptsImages = checkLLMSupportsImageInput(
+ ...getFinalLLM(llmProviders, livePersona)
+ );
+ if (!llmAcceptsImages) {
+ setPopup({
+ type: "error",
+ message:
+ "The current Assistant does not support image input. Please select an assistant with Vision support.",
+ });
+ return;
+ }
+
+ uploadFilesForChat(acceptedFiles).then(([fileIds, error]) => {
+ if (error) {
+ setPopup({
+ type: "error",
+ message: error,
+ });
+ } else {
+ const newFileIds = [...currentMessageFileIds, ...fileIds];
+ setCurrentMessageFileIds(newFileIds);
+ }
+ });
+ };
+
// handle redirect if chat page is disabled
// NOTE: this must be done here, in a client component since
// settings are passed in via Context and therefore aren't
@@ -779,9 +815,9 @@ export function ChatPage({
const retrievalDisabled = !personaIncludesRetrieval(livePersona);
return (
<>
-
*/}
@@ -789,10 +825,6 @@ export function ChatPage({
@@ -830,35 +862,18 @@ export function ChatPage({
/>
)}
- {documentSidebarInitialWidth !== undefined ? (
- {
- const llmAcceptsImages = checkLLMSupportsImageInput(
- ...getFinalLLM(llmProviders, livePersona)
- );
- if (!llmAcceptsImages) {
- setPopup({
- type: "error",
- message:
- "The current Assistant does not support image input. Please select an assistant with Vision support.",
- });
- return;
- }
+ setConfigModalActiveTab(null)}
+ filterManager={filterManager}
+ selectedAssistant={livePersona}
+ setSelectedAssistant={onPersonaChange}
+ llmOverrideManager={llmOverrideManager}
+ />
- uploadFilesForChat(acceptedFiles).then(([fileIds, error]) => {
- if (error) {
- setPopup({
- type: "error",
- message: error,
- });
- } else {
- const newFileIds = [...currentMessageFileIds, ...fileIds];
- setCurrentMessageFileIds(newFileIds);
- }
- });
- }}
- noClick
- >
+ {documentSidebarInitialWidth !== undefined ? (
+
{({ getRootProps }) => (
<>
{/*
*/}
{livePersona && (
-
+
- {chatSessionId !== null && (
-
setSharingModalVisible(true)}
- className="ml-auto mr-6 my-auto border-border border p-2 rounded cursor-pointer hover:bg-hover-light"
- >
-
+
+ {chatSessionId !== null && (
+
setSharingModalVisible(true)}
+ className={`
+ my-auto
+ p-2
+ rounded
+ cursor-pointer
+ hover:bg-hover-light
+ `}
+ >
+
+
+ )}
+
+
+
- )}
+
)}
@@ -1166,143 +1194,23 @@ export function ChatPage({
-
-
- {!retrievalDisabled && (
-
-
- {selectedDocuments.length > 0 ? (
-
- ) : (
-
- )}
-
-
- )}
-
-
-
-
- {currentMessageFileIds.length > 0 && (
-
- {currentMessageFileIds.map((fileId) => (
-
- {
- setCurrentMessageFileIds(
- currentMessageFileIds.filter(
- (id) => id !== fileId
- )
- );
- }}
- />
-
- ))}
-
- )}
-
-
-
{
- if (!isStreaming) {
- if (message) {
- onSubmit();
- }
- } else {
- setIsCancelled(true);
- }
- }}
- >
- {isStreaming ? (
-
- ) : (
-
- )}
-
-
-
-
+
diff --git a/web/src/app/chat/ChatPersonaSelector.tsx b/web/src/app/chat/ChatPersonaSelector.tsx
index e63179500..fbddafc1f 100644
--- a/web/src/app/chat/ChatPersonaSelector.tsx
+++ b/web/src/app/chat/ChatPersonaSelector.tsx
@@ -1,44 +1,58 @@
import { Persona } from "@/app/admin/assistants/interfaces";
-import { FiCheck, FiChevronDown } from "react-icons/fi";
-import { CustomDropdown } from "@/components/Dropdown";
+import { FiCheck, FiChevronDown, FiPlusSquare, FiEdit } from "react-icons/fi";
+import { CustomDropdown, DefaultDropdownElement } from "@/components/Dropdown";
+import { useRouter } from "next/navigation";
+import { Divider } from "@tremor/react";
+import Link from "next/link";
function PersonaItem({
id,
name,
onSelect,
isSelected,
+ isOwner,
}: {
id: number;
name: string;
onSelect: (personaId: number) => void;
isSelected: boolean;
+ isOwner: boolean;
}) {
return (
-
{
- onSelect(id);
- }}
- >
- {name}
- {isSelected && (
-
-
-
+
+
{
+ onSelect(id);
+ }}
+ >
+ {name}
+ {isSelected && (
+
+
+
+ )}
+
+ {isOwner && (
+
+
+
)}
);
@@ -48,11 +62,15 @@ export function ChatPersonaSelector({
personas,
selectedPersonaId,
onPersonaChange,
+ userId,
}: {
personas: Persona[];
selectedPersonaId: number | null;
onPersonaChange: (persona: Persona | null) => void;
+ userId: string | undefined;
}) {
+ const router = useRouter();
+
const currentlySelectedPersona = personas.find(
(persona) => persona.id === selectedPersonaId
);
@@ -66,16 +84,18 @@ export function ChatPersonaSelector({
border-border
bg-background
rounded-lg
+ shadow-lg
flex
flex-col
w-64
max-h-96
overflow-y-auto
- flex
+ p-1
overscroll-contain`}
>
- {personas.map((persona, ind) => {
+ {personas.map((persona) => {
const isSelected = persona.id === selectedPersonaId;
+ const isOwner = persona.owner?.id === userId;
return (
);
})}
+
+
+
+
+ New Assistant
+
+ }
+ onSelect={() => router.push("/assistants/new")}
+ isSelected={false}
+ />
+
}
>
-
+
{currentlySelectedPersona?.name || "Default"}
diff --git a/web/src/app/chat/documentSidebar/DocumentSidebar.tsx b/web/src/app/chat/documentSidebar/DocumentSidebar.tsx
index 34660c4a3..a7f4170b5 100644
--- a/web/src/app/chat/documentSidebar/DocumentSidebar.tsx
+++ b/web/src/app/chat/documentSidebar/DocumentSidebar.tsx
@@ -67,7 +67,6 @@ export function DocumentSidebar({
flex-col
w-full
h-screen
- ${HEADER_PADDING}
`}
id="document-sidebar"
>
diff --git a/web/src/app/chat/folders/FolderList.tsx b/web/src/app/chat/folders/FolderList.tsx
index 1e7bfa6e3..469638efb 100644
--- a/web/src/app/chat/folders/FolderList.tsx
+++ b/web/src/app/chat/folders/FolderList.tsx
@@ -230,7 +230,7 @@ export const FolderList = ({
}
return (
-
+
{folders.map((folder) => (
void;
+ onSubmit: () => void;
+ isStreaming: boolean;
+ setIsCancelled: (value: boolean) => void;
+ retrievalDisabled: boolean;
+ filterManager: FilterManager;
+ llmOverrideManager: LlmOverrideManager;
+ selectedAssistant: Persona;
+ fileIds: string[];
+ setFileIds: (fileIds: string[]) => void;
+ handleFileUpload: (files: File[]) => void;
+ setConfigModalActiveTab: (tab: string) => void;
+}) {
+ const textareaRef = useRef(null);
+
+ const { llmProviders } = useChatContext();
+ const [_, llmName] = getFinalLLM(llmProviders, selectedAssistant);
+
+ return (
+
+
+
+
+
+
+
+ {fileIds.length > 0 && (
+
+ {fileIds.map((fileId) => (
+
+ {
+ setFileIds(fileIds.filter((id) => id !== fileId));
+ }}
+ />
+
+ ))}
+
+ )}
+
+
+
+ {/*
+ Press "/" for shortcuts and useful prompts
+
*/}
+
+ );
+}
diff --git a/web/src/app/chat/input/ChatInputOption.tsx b/web/src/app/chat/input/ChatInputOption.tsx
new file mode 100644
index 000000000..050c5c430
--- /dev/null
+++ b/web/src/app/chat/input/ChatInputOption.tsx
@@ -0,0 +1,96 @@
+import React, { useState } from "react";
+import { IconType } from "react-icons";
+import { DefaultDropdownElement } from "../../../components/Dropdown";
+import { Popover } from "../../../components/popover/Popover";
+
+interface ChatInputOptionProps {
+ name: string;
+ icon: IconType;
+ onClick: () => void;
+ size?: number;
+ options?: { name: string; value: number; onClick?: () => void }[];
+}
+
+const ChatInputOption = ({
+ name,
+ icon: Icon,
+ onClick,
+ size = 16,
+ options,
+}: ChatInputOptionProps) => {
+ const [isDropupVisible, setDropupVisible] = useState(false);
+
+ const handleClick = () => {
+ setDropupVisible(!isDropupVisible);
+ // onClick();
+ };
+
+ const dropdownContent = options ? (
+
+ {options.map((option) => (
+ {
+ if (option.onClick) {
+ option.onClick();
+ setDropupVisible(false);
+ }
+ }}
+ isSelected={false}
+ />
+ ))}
+
+ ) : null;
+
+ const option = (
+
+ );
+
+ if (!dropdownContent) {
+ return {option}
;
+ }
+
+ return (
+
+ );
+};
+
+export default ChatInputOption;
diff --git a/web/src/app/chat/input/SelectedFilterDisplay.tsx b/web/src/app/chat/input/SelectedFilterDisplay.tsx
new file mode 100644
index 000000000..56b92d999
--- /dev/null
+++ b/web/src/app/chat/input/SelectedFilterDisplay.tsx
@@ -0,0 +1,152 @@
+import { SourceIcon } from "@/components/SourceIcon";
+import React from "react";
+import { FiBookmark, FiTag, FiX } from "react-icons/fi";
+import { FilterManager } from "@/lib/hooks";
+import { DateRangePickerValue } from "@tremor/react";
+
+const displayTimeRange = (timeRange: DateRangePickerValue) => {
+ if (timeRange.selectValue) {
+ return timeRange.selectValue;
+ }
+
+ if (timeRange.from && timeRange.to) {
+ return `${timeRange.from.toLocaleDateString()} to ${timeRange.to.toLocaleDateString()}`;
+ } else if (timeRange.from) {
+ return `From ${timeRange.from.toLocaleDateString()}`;
+ } else if (timeRange.to) {
+ return `Until ${timeRange.to.toLocaleDateString()}`;
+ } else {
+ return "No date range selected";
+ }
+};
+
+const SelectedFilter = ({
+ onClick,
+ children,
+}: {
+ onClick: () => void;
+ children: JSX.Element | string;
+}) => (
+
+ {children}
+
+
+);
+
+export function SelectedFilterDisplay({
+ filterManager,
+}: {
+ filterManager: FilterManager;
+}) {
+ const {
+ timeRange,
+ setTimeRange,
+ selectedSources,
+ setSelectedSources,
+ selectedDocumentSets,
+ setSelectedDocumentSets,
+ selectedTags,
+ setSelectedTags,
+ } = filterManager;
+
+ const anyFilters =
+ timeRange !== null ||
+ selectedSources.length > 0 ||
+ selectedDocumentSets.length > 0 ||
+ selectedTags.length > 0;
+
+ if (!anyFilters) {
+ return null;
+ }
+
+ return (
+
+
+ {timeRange &&
+ (timeRange.selectValue || timeRange.from || timeRange.to) && (
+
setTimeRange(null)}>
+ {displayTimeRange(timeRange)}
+
+ )}
+ {selectedSources.map((source) => (
+
+ setSelectedSources((prevSources) =>
+ prevSources.filter(
+ (s) => s.internalName !== source.internalName
+ )
+ )
+ }
+ >
+ <>
+
+ {source.displayName}
+ >
+
+ ))}
+ {selectedDocumentSets.length > 0 &&
+ selectedDocumentSets.map((documentSetName) => (
+
+ setSelectedDocumentSets((prevSets) =>
+ prevSets.filter((s) => s !== documentSetName)
+ )
+ }
+ >
+ <>
+
+
+
+ {documentSetName}
+ >
+
+ ))}
+ {selectedTags.length > 0 &&
+ selectedTags.map((tag) => (
+
+ setSelectedTags((prevTags) =>
+ prevTags.filter(
+ (t) =>
+ t.tag_key !== tag.tag_key || t.tag_value !== tag.tag_value
+ )
+ )
+ }
+ >
+ <>
+
+
+
+
+ {tag.tag_key}
+ =
+ {tag.tag_value}
+
+ >
+
+ ))}
+
+
+ );
+}
diff --git a/web/src/app/chat/lib.tsx b/web/src/app/chat/lib.tsx
index 9428bc45a..baabf6717 100644
--- a/web/src/app/chat/lib.tsx
+++ b/web/src/app/chat/lib.tsx
@@ -57,6 +57,7 @@ export async function* sendMessage({
selectedDocumentIds,
queryOverride,
forceSearch,
+ modelProvider,
modelVersion,
temperature,
systemPromptOverride,
@@ -72,6 +73,7 @@ export async function* sendMessage({
queryOverride?: string;
forceSearch?: boolean;
// LLM overrides
+ modelProvider?: string;
modelVersion?: string;
temperature?: number;
// prompt overrides
@@ -117,6 +119,7 @@ export async function* sendMessage({
temperature || modelVersion
? {
temperature,
+ model_provider: modelProvider,
model_version: modelVersion,
}
: null,
diff --git a/web/src/app/chat/modal/ModalWrapper.tsx b/web/src/app/chat/modal/ModalWrapper.tsx
index ad0e667ec..9b0f56433 100644
--- a/web/src/app/chat/modal/ModalWrapper.tsx
+++ b/web/src/app/chat/modal/ModalWrapper.tsx
@@ -13,7 +13,8 @@ export const ModalWrapper = ({
onClose && onClose()}
className={
- "fixed z-50 inset-0 overflow-y-auto bg-black bg-opacity-30 flex justify-center items-center " +
+ "fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm " +
+ "flex items-center justify-center z-50 " +
(bgClassName || "")
}
>
diff --git a/web/src/app/chat/modal/configuration/AssistantsTab.tsx b/web/src/app/chat/modal/configuration/AssistantsTab.tsx
new file mode 100644
index 000000000..8bbda0c8e
--- /dev/null
+++ b/web/src/app/chat/modal/configuration/AssistantsTab.tsx
@@ -0,0 +1,90 @@
+import { Persona } from "@/app/admin/assistants/interfaces";
+import { Bubble } from "@/components/Bubble";
+import { useChatContext } from "@/components/context/ChatContext";
+import { getFinalLLM } from "@/lib/llm/utils";
+import React from "react";
+import { FiBookmark, FiImage, FiSearch } from "react-icons/fi";
+
+export function AssistantsTab({
+ selectedAssistant,
+ onSelect,
+}: {
+ selectedAssistant: Persona;
+ onSelect: (assistant: Persona) => void;
+}) {
+ const { availablePersonas, llmProviders } = useChatContext();
+ const [_, llmName] = getFinalLLM(llmProviders, null);
+
+ return (
+
+
Choose Assistant
+
+ {availablePersonas.map((assistant) => (
+
onSelect(assistant)}
+ >
+
{assistant.name}
+
+ {assistant.description}
+
+
+ {assistant.document_sets.length > 0 && (
+
+
Document Sets:
+ {assistant.document_sets.map((set) => (
+
+
+
+ {set.name}
+
+
+ ))}
+
+ )}
+ {assistant.tools.length > 0 && (
+
+
Tools:
+ {assistant.tools.map((tool) => {
+ let toolName = tool.name;
+ let toolIcon = null;
+
+ if (tool.name === "SearchTool") {
+ toolName = "Search";
+ toolIcon =
;
+ } else if (tool.name === "ImageGenerationTool") {
+ toolName = "Image Generation";
+ toolIcon =
;
+ }
+
+ return (
+
+
+ {toolIcon}
+ {toolName}
+
+
+ );
+ })}
+
+ )}
+
+ Default Model: {" "}
+ {assistant.llm_model_version_override || llmName}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/web/src/app/chat/modal/configuration/ConfigurationModal.tsx b/web/src/app/chat/modal/configuration/ConfigurationModal.tsx
new file mode 100644
index 000000000..ee75d88aa
--- /dev/null
+++ b/web/src/app/chat/modal/configuration/ConfigurationModal.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Modal } from "../../../../components/Modal";
+import { FilterManager, LlmOverrideManager } from "@/lib/hooks";
+import { FiltersTab } from "./FiltersTab";
+import { FiCpu, FiFilter, FiX } from "react-icons/fi";
+import { IconType } from "react-icons";
+import { FaBrain } from "react-icons/fa";
+import { AssistantsTab } from "./AssistantsTab";
+import { Persona } from "@/app/admin/assistants/interfaces";
+import { LlmTab } from "./LlmTab";
+
+const TabButton = ({
+ label,
+ icon: Icon,
+ isActive,
+ onClick,
+}: {
+ label: string;
+ icon: IconType;
+ isActive: boolean;
+ onClick: () => void;
+}) => (
+
+
+ {label}
+
+);
+
+export function ConfigurationModal({
+ activeTab,
+ setActiveTab,
+ onClose,
+ selectedAssistant,
+ setSelectedAssistant,
+ filterManager,
+ llmOverrideManager,
+}: {
+ activeTab: string | null;
+ setActiveTab: (tab: string | null) => void;
+ onClose: () => void;
+ selectedAssistant: Persona;
+ setSelectedAssistant: (assistant: Persona) => void;
+ filterManager: FilterManager;
+ llmOverrideManager: LlmOverrideManager;
+}) {
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Escape") {
+ onClose();
+ }
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [onClose]);
+
+ if (!activeTab) return null;
+
+ return (
+
+
+
+
+
+ setActiveTab("assistants")}
+ />
+ setActiveTab("llms")}
+ />
+ setActiveTab("filters")}
+ />
+
+
+
+
+
+
+
+
+
+ {activeTab === "filters" && (
+
+ )}
+
+ {activeTab === "llms" && (
+
+ )}
+
+ {activeTab === "assistants" && (
+
+
{
+ setSelectedAssistant(assistant);
+ onClose();
+ }}
+ />
+
+ )}
+
+
+
+
+ );
+}
diff --git a/web/src/app/chat/modal/configuration/FiltersTab.tsx b/web/src/app/chat/modal/configuration/FiltersTab.tsx
new file mode 100644
index 000000000..581c15cce
--- /dev/null
+++ b/web/src/app/chat/modal/configuration/FiltersTab.tsx
@@ -0,0 +1,260 @@
+import { useChatContext } from "@/components/context/ChatContext";
+import { FilterManager } from "@/lib/hooks";
+import { listSourceMetadata } from "@/lib/sources";
+import { useRef, useState } from "react";
+import {
+ DateRangePicker,
+ DateRangePickerItem,
+ Divider,
+ Text,
+} from "@tremor/react";
+import { getXDaysAgo } from "@/lib/dateUtils";
+import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
+import { Bubble } from "@/components/Bubble";
+import { FiX } from "react-icons/fi";
+
+export function FiltersTab({
+ filterManager,
+}: {
+ filterManager: FilterManager;
+}): JSX.Element {
+ const [filterValue, setFilterValue] = useState
("");
+ const inputRef = useRef(null);
+
+ const { availableSources, availableDocumentSets, availableTags } =
+ useChatContext();
+
+ const allSources = listSourceMetadata();
+ const availableSourceMetadata = allSources.filter((source) =>
+ availableSources.includes(source.internalName)
+ );
+
+ return (
+
+
+
+
+
Time Range
+
+ Choose the time range we should search over. If only one date is
+ selected, will only search after the specified date.
+
+
+
+ filterManager.setTimeRange({
+ from: value.from,
+ to: value.to,
+ selectValue: value.selectValue,
+ })
+ }
+ selectPlaceholder="Select range"
+ enableSelect
+ >
+
+ Last 30 Days
+
+
+ Last 7 Days
+
+
+ Today
+
+
+
+
+
+
+
+
+
Knowledge Sets
+
+ Choose which knowledge sets we should search over. If multiple are
+ selected, we will search through all of them.
+
+
+ {availableDocumentSets.length > 0 ? (
+ availableDocumentSets.map((set) => {
+ const isSelected =
+ filterManager.selectedDocumentSets.includes(set.name);
+ return (
+
+ filterManager.setSelectedDocumentSets((prev) =>
+ isSelected
+ ? prev.filter((s) => s !== set.name)
+ : [...prev, set.name]
+ )
+ }
+ />
+ );
+ })
+ ) : (
+ No knowledge sets available
+ )}
+
+
+
+
+
+
+
Sources
+
+ Choose which sources we should search over. If multiple sources
+ are selected, we will search through all of them.
+
+
+
+
+
+
+
+
Tags
+
+ {filterManager.selectedTags.length > 0 ? (
+ filterManager.selectedTags.map((tag) => (
+
+ filterManager.setSelectedTags((prev) =>
+ prev.filter(
+ (t) =>
+ t.tag_key !== tag.tag_key ||
+ t.tag_value !== tag.tag_value
+ )
+ )
+ }
+ >
+
+
+ {tag.tag_key}={tag.tag_value}
+
{" "}
+
+
+
+ ))
+ ) : (
+ No selected tags
+ )}
+
+
+
+
+
+ setFilterValue(event.target.value)}
+ />
+
+
+
+ {availableTags.length > 0 ? (
+ availableTags
+ .filter(
+ (tag) =>
+ !filterManager.selectedTags.some(
+ (selectedTag) =>
+ selectedTag.tag_key === tag.tag_key &&
+ selectedTag.tag_value === tag.tag_value
+ ) &&
+ (tag.tag_key.includes(filterValue) ||
+ tag.tag_value.includes(filterValue))
+ )
+ .slice(0, 12)
+ .map((tag) => (
+
+ filterManager.setSelectedTags((prev) =>
+ filterManager.selectedTags.includes(tag)
+ ? prev.filter(
+ (t) =>
+ t.tag_key !== tag.tag_key ||
+ t.tag_value !== tag.tag_value
+ )
+ : [...prev, tag]
+ )
+ }
+ >
+ <>
+ {tag.tag_key}={tag.tag_value}
+ >
+
+ ))
+ ) : (
+
+ No matching tags found
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/app/chat/modal/configuration/LlmTab.tsx b/web/src/app/chat/modal/configuration/LlmTab.tsx
new file mode 100644
index 000000000..74ece8af5
--- /dev/null
+++ b/web/src/app/chat/modal/configuration/LlmTab.tsx
@@ -0,0 +1,133 @@
+import { useChatContext } from "@/components/context/ChatContext";
+import { LlmOverride, LlmOverrideManager } from "@/lib/hooks";
+import React, { useCallback, useRef, useState } from "react";
+import { debounce } from "lodash";
+import { DefaultDropdown } from "@/components/Dropdown";
+import { Text } from "@tremor/react";
+import { Persona } from "@/app/admin/assistants/interfaces";
+import { getFinalLLM } from "@/lib/llm/utils";
+
+export function LlmTab({
+ llmOverrideManager,
+ currentAssistant,
+}: {
+ llmOverrideManager: LlmOverrideManager;
+ currentAssistant: Persona;
+}) {
+ const { llmProviders } = useChatContext();
+ const { llmOverride, setLlmOverride, temperature, setTemperature } =
+ llmOverrideManager;
+
+ const [localTemperature, setLocalTemperature] = useState(
+ temperature || 0
+ );
+
+ const debouncedSetTemperature = useCallback(
+ debounce((value) => {
+ setTemperature(value);
+ }, 300),
+ []
+ );
+
+ const handleTemperatureChange = (value: number) => {
+ setLocalTemperature(value);
+ debouncedSetTemperature(value);
+ };
+
+ const [_, defaultLlmName] = getFinalLLM(llmProviders, currentAssistant);
+
+ const llmOptions: { name: string; value: string }[] = [];
+ const structureValue = (
+ name: string,
+ provider: string,
+ modelName: string
+ ) => {
+ return `${name}__${provider}__${modelName}`;
+ };
+ const destructureValue = (value: string): LlmOverride => {
+ const [displayName, provider, modelName] = value.split("__");
+ return {
+ name: displayName,
+ provider,
+ modelName,
+ };
+ };
+ llmProviders.forEach((llmProvider) => {
+ llmProvider.model_names.forEach((modelName) => {
+ llmOptions.push({
+ name: modelName,
+ value: structureValue(
+ llmProvider.name,
+ llmProvider.provider,
+ modelName
+ ),
+ });
+ });
+ });
+
+ return (
+
+
Choose Model
+
+ Override the default model for the{" "}
+ {currentAssistant.name} assistant. The
+ override will apply only for this chat session.
+
+
+ Default Model: {defaultLlmName} .
+
+
+
+ setLlmOverride(destructureValue(value as string))
+ }
+ />
+
+
+
Temperature
+
+
+ Adjust the temperature of the LLM. Higher temperature will make the LLM
+ generate more creative and diverse responses, while lower temperature
+ will make the LLM generate more conservative and focused responses.
+
+
+
+
handleTemperatureChange(parseFloat(e.target.value))}
+ className="
+ w-full
+ p-2
+ border
+ border-border
+ rounded-md
+ "
+ min="0"
+ max="2"
+ step="0.01"
+ value={localTemperature}
+ />
+
+ {localTemperature}
+
+
+
+ );
+}
diff --git a/web/src/app/chat/page.tsx b/web/src/app/chat/page.tsx
index e913a0801..f9623cb9c 100644
--- a/web/src/app/chat/page.tsx
+++ b/web/src/app/chat/page.tsx
@@ -28,10 +28,10 @@ import { ChatPage } from "./ChatPage";
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
import { Settings } from "../admin/settings/interfaces";
-import { SIDEBAR_TAB_COOKIE, Tabs } from "./sessionSidebar/constants";
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
import { LLMProviderDescriptor } from "../admin/models/llm/interfaces";
import { Folder } from "./folders/interfaces";
+import { ChatProvider } from "@/components/context/ChatContext";
export default async function Page({
searchParams,
@@ -151,10 +151,6 @@ export default async function Page({
? parseInt(documentSidebarCookieInitialWidth.value)
: undefined;
- const defaultSidebarTab = cookies().get(SIDEBAR_TAB_COOKIE)?.value as
- | Tabs
- | undefined;
-
const hasAnyConnectors = ccPairs.length > 0;
const shouldShowWelcomeModal =
!hasCompletedWelcomeFlowSS() &&
@@ -197,20 +193,24 @@ export default async function Page({
)}
-
+
+
+
>
);
}
diff --git a/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx b/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx
index 26bbd3e3f..99084baa6 100644
--- a/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx
+++ b/web/src/app/chat/sessionSidebar/ChatSessionDisplay.tsx
@@ -108,9 +108,6 @@ export function ChatSessionDisplay({
<>
-
-
-
{isRenamingChat ? (
diff --git a/web/src/app/chat/sessionSidebar/ChatSidebar.tsx b/web/src/app/chat/sessionSidebar/ChatSidebar.tsx
index 8340caa41..b84bd1270 100644
--- a/web/src/app/chat/sessionSidebar/ChatSidebar.tsx
+++ b/web/src/app/chat/sessionSidebar/ChatSidebar.tsx
@@ -1,108 +1,35 @@
"use client";
-import {
- FiFolderPlus,
- FiLogOut,
- FiMessageSquare,
- FiMoreHorizontal,
- FiPlusSquare,
- FiSearch,
- FiTool,
-} from "react-icons/fi";
-import { useEffect, useRef, useState } from "react";
+import { FiEdit, FiFolderPlus, FiPlusSquare } from "react-icons/fi";
+import { useContext, useEffect, useRef, useState } from "react";
import Link from "next/link";
+import Image from "next/image";
import { useRouter } from "next/navigation";
-import { User } from "@/lib/types";
-import { logout } from "@/lib/user";
import { BasicClickable, BasicSelectable } from "@/components/BasicClickable";
import { ChatSession } from "../interfaces";
-import {
- HEADER_PADDING,
- NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
-} from "@/lib/constants";
+import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
import { ChatTab } from "./ChatTab";
-import { AssistantsTab } from "./AssistantsTab";
-import { Persona } from "@/app/admin/assistants/interfaces";
-import Cookies from "js-cookie";
-import { SIDEBAR_TAB_COOKIE, Tabs } from "./constants";
import { Folder } from "../folders/interfaces";
import { createFolder } from "../folders/FolderManagement";
import { usePopup } from "@/components/admin/connectors/Popup";
+import { SettingsContext } from "@/components/settings/SettingsProvider";
+
+import React from "react";
export const ChatSidebar = ({
existingChats,
currentChatSession,
- personas,
- onPersonaChange,
- user,
- defaultTab,
folders,
openedFolders,
}: {
existingChats: ChatSession[];
currentChatSession: ChatSession | null | undefined;
- personas: Persona[];
- onPersonaChange: (persona: Persona | null) => void;
- user: User | null;
- defaultTab?: Tabs;
folders: Folder[];
openedFolders: { [key: number]: boolean };
}) => {
const router = useRouter();
const { popup, setPopup } = usePopup();
- const [openTab, _setOpenTab] = useState(defaultTab || Tabs.CHATS);
- const setOpenTab = (tab: Tabs) => {
- Cookies.set(SIDEBAR_TAB_COOKIE, tab);
- _setOpenTab(tab);
- };
-
- function TabOption({ tab }: { tab: Tabs }) {
- return (
- {
- setOpenTab(tab);
- }}
- >
- {tab}
-
- );
- }
-
- const [userInfoVisible, setUserInfoVisible] = useState(false);
- const userInfoRef = useRef(null);
-
- const handleLogout = () => {
- logout().then((isSuccess) => {
- if (!isSuccess) {
- alert("Failed to logout");
- }
- router.push("/auth/login");
- });
- };
-
- // hides logout popup on any click outside
- const handleClickOutside = (event: MouseEvent) => {
- if (
- userInfoRef.current &&
- !userInfoRef.current.contains(event.target as Node)
- ) {
- setUserInfoVisible(false);
- }
- };
-
- useEffect(() => {
- document.addEventListener("mousedown", handleClickOutside);
-
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- };
- }, []);
-
const currentChatId = currentChatSession?.id;
// prevent the NextJS Router cache from causing the chat sidebar to not
@@ -111,6 +38,12 @@ export const ChatSidebar = ({
router.refresh();
}, [currentChatId]);
+ const combinedSettings = useContext(SettingsContext);
+ if (!combinedSettings) {
+ return null;
+ }
+ const settings = combinedSettings.settings;
+
return (
<>
{popup}
@@ -119,7 +52,6 @@ export const ChatSidebar = ({
flex-none
w-64
3xl:w-72
- ${HEADER_PADDING}
border-r
border-border
flex
@@ -128,145 +60,74 @@ export const ChatSidebar = ({
transition-transform`}
id="chat-sidebar"
>
-
-
- {openTab == Tabs.CHATS && (
- <>
-
-
-
-
- New Chat
-
-
-
-
-
-
- createFolder("New Folder")
- .then((folderId) => {
- console.log(`Folder created with ID: ${folderId}`);
- router.refresh();
- })
- .catch((error) => {
- console.error("Failed to create folder:", error);
- setPopup({
- message: `Failed to create folder: ${error.message}`,
- type: "error",
- });
- })
- }
- >
-
-
-
-
+
-
- >
- )}
-
- {openTab == Tabs.ASSISTANTS && (
- <>
-
-
-
- New Assistant
-
-
-
-
- >
- )}
-
-
-
- {userInfoVisible && (
-
-
-
- Danswer Search
-
-
-
- Danswer Chat
-
- {(!user || user.role === "admin") && (
-
-
- Admin Panel
-
- )}
- {user && (
-
-
- Log out
-
- )}
+
+
+
+
+ New Chat
- )}
-
- setUserInfoVisible(!userInfoVisible)}
- className="flex h-8"
- >
-
- {user && user.email ? user.email[0].toUpperCase() : "A"}
-
-
- {user ? user.email : "Anonymous Possum"}
-
-
+
+
+
+
+
+ createFolder("New Folder")
+ .then((folderId) => {
+ console.log(`Folder created with ID: ${folderId}`);
+ router.refresh();
+ })
+ .catch((error) => {
+ console.error("Failed to create folder:", error);
+ setPopup({
+ message: `Failed to create folder: ${error.message}`,
+ type: "error",
+ });
+ })
+ }
+ >
+
+
-
+
+
+
+
+
>
);
diff --git a/web/src/app/chat/sessionSidebar/ChatTab.tsx b/web/src/app/chat/sessionSidebar/ChatTab.tsx
index 92cba3ea3..d47881d93 100644
--- a/web/src/app/chat/sessionSidebar/ChatTab.tsx
+++ b/web/src/app/chat/sessionSidebar/ChatTab.tsx
@@ -50,14 +50,19 @@ export function ChatTab({
};
return (
-
-
-
-
+
+ {folders.length > 0 && (
+
+ )}
{
@@ -66,7 +71,7 @@ export function ChatTab({
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDropToRemoveFromFolder}
- className={`transition duration-300 ease-in-out mr-3 ${
+ className={`pt-1 transition duration-300 ease-in-out mr-3 ${
isDragOver ? "bg-hover" : ""
} rounded-md`}
>
@@ -75,7 +80,7 @@ export function ChatTab({
if (chatSessions.length > 0) {
return (
-
+
{dateRange}
{chatSessions
diff --git a/web/src/app/chat/sessionSidebar/constants.ts b/web/src/app/chat/sessionSidebar/constants.ts
deleted file mode 100644
index 1bb1fba80..000000000
--- a/web/src/app/chat/sessionSidebar/constants.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export const SIDEBAR_TAB_COOKIE = "chatSidebarTab";
-
-export enum Tabs {
- CHATS = "Chats",
- ASSISTANTS = "Assistants",
-}
diff --git a/web/src/components/Bubble.tsx b/web/src/components/Bubble.tsx
index 4cd1170ea..9bc28d684 100644
--- a/web/src/components/Bubble.tsx
+++ b/web/src/components/Bubble.tsx
@@ -1,11 +1,15 @@
+import { CustomCheckbox } from "./CustomCheckbox";
+
export function Bubble({
isSelected,
onClick,
children,
+ showCheckbox = false,
}: {
isSelected: boolean;
onClick?: () => void;
children: string | JSX.Element;
+ showCheckbox?: boolean;
}) {
return (
{children}
+ {showCheckbox && (
+
+ null} />
+
+ )}
);
}
diff --git a/web/src/components/Dropdown.tsx b/web/src/components/Dropdown.tsx
index b88fe8693..29c50c263 100644
--- a/web/src/components/Dropdown.tsx
+++ b/web/src/components/Dropdown.tsx
@@ -369,6 +369,7 @@ export function DefaultDropdown({
side={side}
sideOffset={5}
matchWidth
+ triggerMaxWidth
/>
);
diff --git a/web/src/components/Modal.tsx b/web/src/components/Modal.tsx
index 645c418a9..9f9893afd 100644
--- a/web/src/components/Modal.tsx
+++ b/web/src/components/Modal.tsx
@@ -9,6 +9,7 @@ interface ModalProps {
width?: string;
titleSize?: string;
hideDividerForTitle?: boolean;
+ noPadding?: boolean;
}
export function Modal({
@@ -19,12 +20,13 @@ export function Modal({
width,
titleSize,
hideDividerForTitle,
+ noPadding,
}: ModalProps) {
return (
event.stopPropagation()}
diff --git a/web/src/components/UserDropdown.tsx b/web/src/components/UserDropdown.tsx
new file mode 100644
index 000000000..79a0628a7
--- /dev/null
+++ b/web/src/components/UserDropdown.tsx
@@ -0,0 +1,130 @@
+import { useState, useRef, useEffect } from "react";
+import {
+ FiSearch,
+ FiMessageSquare,
+ FiTool,
+ FiLogOut,
+ FiMoreHorizontal,
+} from "react-icons/fi";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { User } from "@/lib/types";
+import { logout } from "@/lib/user";
+import { BasicSelectable } from "@/components/BasicClickable";
+import { DefaultDropdown } from "./Dropdown";
+import { Popover } from "./popover/Popover";
+
+export function UserDropdown({
+ user,
+ hideChatAndSearch,
+}: {
+ user: User | null;
+ hideChatAndSearch?: boolean;
+}) {
+ const [userInfoVisible, setUserInfoVisible] = useState(false);
+ const userInfoRef = useRef
(null);
+ const router = useRouter();
+
+ const handleLogout = () => {
+ logout().then((isSuccess) => {
+ if (!isSuccess) {
+ alert("Failed to logout");
+ }
+ router.push("/auth/login");
+ });
+ };
+
+ const showAdminPanel = !user || user.role === "admin";
+
+ return (
+
+
+ setUserInfoVisible(!userInfoVisible)}
+ className="flex cursor-pointer"
+ >
+
+ {user && user.email ? user.email[0].toUpperCase() : "A"}
+
+
+
+ }
+ popover={
+
+ {!hideChatAndSearch && (
+ <>
+
+
+ Danswer Search
+
+
+
+ Danswer Chat
+
+ >
+ )}
+ {showAdminPanel && (
+ <>
+ {!hideChatAndSearch && (
+
+ )}
+
+
+ Admin Panel
+
+ >
+ )}
+ {user && (
+ <>
+ {(!hideChatAndSearch || showAdminPanel) && (
+
+ )}
+
+
+ Log out
+
+ >
+ )}
+
+ }
+ side="bottom"
+ align="end"
+ sideOffset={5}
+ alignOffset={-10}
+ />
+
+ );
+}
diff --git a/web/src/components/context/ChatContext.tsx b/web/src/components/context/ChatContext.tsx
new file mode 100644
index 000000000..4fb716d83
--- /dev/null
+++ b/web/src/components/context/ChatContext.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import React, { createContext, useContext } from "react";
+import {
+ CCPairBasicInfo,
+ DocumentSet,
+ Tag,
+ User,
+ ValidSources,
+} from "@/lib/types";
+import { ChatSession } from "@/app/chat/interfaces";
+import { Persona } from "@/app/admin/assistants/interfaces";
+import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces";
+import { Folder } from "@/app/chat/folders/interfaces";
+
+interface ChatContextProps {
+ user: User | null;
+ chatSessions: ChatSession[];
+ availableSources: ValidSources[];
+ availableDocumentSets: DocumentSet[];
+ availablePersonas: Persona[];
+ availableTags: Tag[];
+ llmProviders: LLMProviderDescriptor[];
+ folders: Folder[];
+ openedFolders: Record;
+}
+
+const ChatContext = createContext(undefined);
+
+export const ChatProvider: React.FC<{
+ value: ChatContextProps;
+ children: React.ReactNode;
+}> = ({ value, children }) => {
+ return {children} ;
+};
+
+export const useChatContext = (): ChatContextProps => {
+ const context = useContext(ChatContext);
+ if (!context) {
+ throw new Error("useChatContext must be used within a ChatProvider");
+ }
+ return context;
+};
diff --git a/web/src/components/header/Header.tsx b/web/src/components/header/Header.tsx
index 5cc8c220b..497bd8a8f 100644
--- a/web/src/components/header/Header.tsx
+++ b/web/src/components/header/Header.tsx
@@ -10,6 +10,7 @@ import { CustomDropdown, DefaultDropdownElement } from "../Dropdown";
import { FiMessageSquare, FiSearch } from "react-icons/fi";
import { HeaderWrapper } from "./HeaderWrapper";
import { SettingsContext } from "../settings/SettingsProvider";
+import { UserDropdown } from "../UserDropdown";
interface HeaderProps {
user: User | null;
@@ -106,35 +107,7 @@ export function Header({ user }: HeaderProps) {
-
- {/* Show connector option if (1) auth is disabled or (2) user is an admin */}
- {(!user || user.role === "admin") && (
-
-
-
- )}
- {user && (
-
- )}
-
- }
- >
-
-
- {user && user.email ? user.email[0].toUpperCase() : "A"}
-
-
-
+
diff --git a/web/src/components/popover/Popover.tsx b/web/src/components/popover/Popover.tsx
index 22ae839c9..dd89f2f9c 100644
--- a/web/src/components/popover/Popover.tsx
+++ b/web/src/components/popover/Popover.tsx
@@ -12,8 +12,10 @@ export function Popover({
side,
align,
sideOffset,
+ alignOffset,
matchWidth,
requiresContentPadding,
+ triggerMaxWidth,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -22,8 +24,10 @@ export function Popover({
side?: "top" | "right" | "bottom" | "left";
align?: "start" | "center" | "end";
sideOffset?: number;
+ alignOffset?: number;
matchWidth?: boolean;
requiresContentPadding?: boolean;
+ triggerMaxWidth?: boolean;
}) {
/*
This Popover is needed when we want to put a popup / dropdown in a component
@@ -36,7 +40,7 @@ export function Popover({
return (
-
+
{/* NOTE: this weird `-mb-1.5` is needed to offset the Anchor, otherwise
the content will shift up by 1.5px when the Popover is open. */}
{open ? (
@@ -57,6 +61,7 @@ export function Popover({
side={side}
align={align}
sideOffset={sideOffset}
+ alignOffset={alignOffset}
>
{popover}
diff --git a/web/src/lib/hooks.ts b/web/src/lib/hooks.ts
index 27556b067..dc9ca7787 100644
--- a/web/src/lib/hooks.ts
+++ b/web/src/lib/hooks.ts
@@ -79,7 +79,20 @@ export const useTimeRange = (initialValue?: DateRangePickerValue) => {
return useState(null);
};
-export function useFilters() {
+export interface FilterManager {
+ timeRange: DateRangePickerValue | null;
+ setTimeRange: React.Dispatch<
+ React.SetStateAction
+ >;
+ selectedSources: SourceMetadata[];
+ setSelectedSources: React.Dispatch>;
+ selectedDocumentSets: string[];
+ setSelectedDocumentSets: React.Dispatch>;
+ selectedTags: Tag[];
+ setSelectedTags: React.Dispatch>;
+}
+
+export function useFilters(): FilterManager {
const [timeRange, setTimeRange] = useTimeRange();
const [selectedSources, setSelectedSources] = useState([]);
const [selectedDocumentSets, setSelectedDocumentSets] = useState(
@@ -109,6 +122,35 @@ export const useUsers = () => {
};
};
+export interface LlmOverride {
+ name: string;
+ provider: string;
+ modelName: string;
+}
+
+export interface LlmOverrideManager {
+ llmOverride: LlmOverride;
+ setLlmOverride: React.Dispatch>;
+ temperature: number | null;
+ setTemperature: React.Dispatch>;
+}
+
+export function useLlmOverride(): LlmOverrideManager {
+ const [llmOverride, setLlmOverride] = useState({
+ name: "",
+ provider: "",
+ modelName: "",
+ });
+ const [temperature, setTemperature] = useState(null);
+
+ return {
+ llmOverride,
+ setLlmOverride,
+ temperature,
+ setTemperature,
+ };
+}
+
/*
EE Only APIs
*/