mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-04-07 16:02:45 +00:00
Compare commits
3 Commits
cli/v0.2.1
...
refactor-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe69948964 | ||
|
|
f9faab6690 | ||
|
|
fd7c0a3b17 |
@@ -56,8 +56,8 @@ import {
|
||||
SwapIcon,
|
||||
TrashIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { buildImgUrl } from "@/app/chat/files/images/utils";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { buildImgUrl } from "@/app/chat/components/files/images/utils";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import { debounce } from "lodash";
|
||||
import { LLMProviderView } from "../configuration/llm/interfaces";
|
||||
import StarterMessagesList from "./StarterMessageList";
|
||||
@@ -72,7 +72,7 @@ import {
|
||||
SearchMultiSelectDropdown,
|
||||
Option as DropdownOption,
|
||||
} from "@/components/Dropdown";
|
||||
import { SourceChip } from "@/app/chat/input/ChatInputBar";
|
||||
import { SourceChip } from "@/app/chat/components/input/ChatInputBar";
|
||||
import {
|
||||
TagIcon,
|
||||
UserIcon,
|
||||
@@ -89,7 +89,7 @@ import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
import { FilePickerModal } from "@/app/chat/my-documents/components/FilePicker";
|
||||
import { useDocumentsContext } from "@/app/chat/my-documents/DocumentsContext";
|
||||
|
||||
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
|
||||
import { SEARCH_TOOL_ID } from "@/app/chat/components/tools/constants";
|
||||
import TextView from "@/components/chat/TextView";
|
||||
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
|
||||
import { MAX_CHARACTERS_PERSONA_DESCRIPTION } from "@/lib/constants";
|
||||
@@ -138,7 +138,8 @@ export function AssistantEditor({
|
||||
shouldAddAssistantToUserPreferences?: boolean;
|
||||
admin?: boolean;
|
||||
}) {
|
||||
const { refreshAssistants, isImageGenerationAvailable } = useAssistants();
|
||||
const { refreshAssistants, isImageGenerationAvailable } =
|
||||
useAssistantsContext();
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { FiEdit2 } from "react-icons/fi";
|
||||
import { TrashIcon } from "@/components/icons/icons";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
|
||||
function PersonaTypeDisplay({ persona }: { persona: Persona }) {
|
||||
@@ -48,7 +48,7 @@ export function PersonasTable() {
|
||||
allAssistants: assistants,
|
||||
refreshAssistants,
|
||||
editablePersonas,
|
||||
} = useAssistants();
|
||||
} = useAssistantsContext();
|
||||
|
||||
const editablePersonaIds = useMemo(() => {
|
||||
return new Set(editablePersonas.map((p) => p.id.toString()));
|
||||
|
||||
@@ -18,7 +18,7 @@ import CardSection from "@/components/admin/CardSection";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
|
||||
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
|
||||
import { SEARCH_TOOL_ID } from "@/app/chat/components/tools/constants";
|
||||
import { SlackChannelConfigFormFields } from "./SlackChannelConfigFormFields";
|
||||
|
||||
export const SlackChannelConfigCreationForm = ({
|
||||
|
||||
@@ -9,11 +9,11 @@ import { useRouter } from "next/navigation";
|
||||
import FixedLogo from "../../components/logo/FixedLogo";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import { HistorySidebar } from "../chat/sessionSidebar/HistorySidebar";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { HistorySidebar } from "@/app/chat/components/sessionSidebar/HistorySidebar";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import AssistantModal from "./mine/AssistantModal";
|
||||
import { useSidebarShortcut } from "@/lib/browserUtilities";
|
||||
import { UserSettingsModal } from "../chat/modal/UserSettingsModal";
|
||||
import { UserSettingsModal } from "@/app/chat/components/modal/UserSettingsModal";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
|
||||
const sidebarElementRef = useRef<HTMLDivElement>(null);
|
||||
const { folders, openedFolders, chatSessions } = useChatContext();
|
||||
const { assistants } = useAssistants();
|
||||
const { assistants } = useAssistantsContext();
|
||||
const explicitlyUntoggle = () => {
|
||||
setShowDocSidebar(false);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FiImage, FiSearch } from "react-icons/fi";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { SEARCH_TOOL_ID } from "../chat/tools/constants";
|
||||
import { SEARCH_TOOL_ID } from "../chat/components/tools/constants";
|
||||
|
||||
export function AssistantTools({
|
||||
assistant,
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import { checkUserOwnsAssistant } from "@/lib/assistants/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -60,7 +60,7 @@ const AssistantCard: React.FC<{
|
||||
}> = ({ persona, pinned, closeModal }) => {
|
||||
const { user, toggleAssistantPinnedStatus } = useUser();
|
||||
const router = useRouter();
|
||||
const { refreshAssistants, pinnedAssistants } = useAssistants();
|
||||
const { refreshAssistants, pinnedAssistants } = useAssistantsContext();
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const isOwnedByUser = checkUserOwnsAssistant(user, persona);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import AssistantCard from "./AssistantCard";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { FilterIcon, XIcon } from "lucide-react";
|
||||
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
|
||||
@@ -64,7 +64,7 @@ interface AssistantModalProps {
|
||||
}
|
||||
|
||||
export function AssistantModal({ hideModal }: AssistantModalProps) {
|
||||
const { assistants, pinnedAssistants } = useAssistants();
|
||||
const { assistants, pinnedAssistants } = useAssistantsContext();
|
||||
const { assistantFilters, toggleAssistantFilter } = useAssistantFilter();
|
||||
const router = useRouter();
|
||||
const { user } = useUser();
|
||||
|
||||
@@ -15,7 +15,7 @@ import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { Bubble } from "@/components/Bubble";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
|
||||
interface AssistantSharingModalProps {
|
||||
assistant: Persona;
|
||||
@@ -32,7 +32,7 @@ export function AssistantSharingModal({
|
||||
show,
|
||||
onClose,
|
||||
}: AssistantSharingModalProps) {
|
||||
const { refreshAssistants } = useAssistants();
|
||||
const { refreshAssistants } = useAssistantsContext();
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [selectedUsers, setSelectedUsers] = useState<MinimalUserSnapshot[]>([]);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { Bubble } from "@/components/Bubble";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
|
||||
interface AssistantSharingPopoverProps {
|
||||
assistant: Persona;
|
||||
@@ -29,7 +29,7 @@ export function AssistantSharingPopover({
|
||||
allUsers,
|
||||
onClose,
|
||||
}: AssistantSharingPopoverProps) {
|
||||
const { refreshAssistants } = useAssistants();
|
||||
const { refreshAssistants } = useAssistantsContext();
|
||||
const { popup, setPopup } = usePopup();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [selectedUsers, setSelectedUsers] = useState<MinimalUserSnapshot[]>([]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,148 +0,0 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { FiCheck, FiChevronDown, FiPlusSquare, FiEdit2 } from "react-icons/fi";
|
||||
import { CustomDropdown, DefaultDropdownElement } from "@/components/Dropdown";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { checkUserIdOwnsAssistant } from "@/lib/assistants/checkOwnership";
|
||||
|
||||
function PersonaItem({
|
||||
id,
|
||||
name,
|
||||
onSelect,
|
||||
isSelected,
|
||||
isOwner,
|
||||
}: {
|
||||
id: number;
|
||||
name: string;
|
||||
onSelect: (personaId: number) => void;
|
||||
isSelected: boolean;
|
||||
isOwner: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
<div
|
||||
key={id}
|
||||
className={`
|
||||
flex
|
||||
flex-grow
|
||||
px-3
|
||||
text-sm
|
||||
py-2
|
||||
my-0.5
|
||||
rounded
|
||||
mx-1
|
||||
select-none
|
||||
cursor-pointer
|
||||
text-text-darker
|
||||
bg-background
|
||||
hover:bg-accent-background
|
||||
${
|
||||
isSelected
|
||||
? "bg-accent-background-hovered text-selected-emphasis"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
onSelect(id);
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
{isSelected && (
|
||||
<div className="ml-auto mr-1 my-auto">
|
||||
<FiCheck />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isOwner && (
|
||||
<Link href={`/assistants/edit/${id}`} className="mx-2 my-auto">
|
||||
<FiEdit2
|
||||
className="hover:bg-accent-background-hovered p-0.5 my-auto"
|
||||
size={20}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomDropdown
|
||||
dropdown={
|
||||
<div
|
||||
className={`
|
||||
border
|
||||
border-border
|
||||
bg-background
|
||||
rounded-lg
|
||||
shadow-lg
|
||||
flex
|
||||
flex-col
|
||||
w-64
|
||||
max-h-96
|
||||
overflow-y-auto
|
||||
p-1
|
||||
overscroll-contain`}
|
||||
>
|
||||
{personas.map((persona) => {
|
||||
const isSelected = persona.id === selectedPersonaId;
|
||||
const isOwner = checkUserIdOwnsAssistant(userId, persona);
|
||||
return (
|
||||
<PersonaItem
|
||||
key={persona.id}
|
||||
id={persona.id}
|
||||
name={persona.name}
|
||||
onSelect={(clickedPersonaId) => {
|
||||
const clickedPersona = personas.find(
|
||||
(persona) => persona.id === clickedPersonaId
|
||||
);
|
||||
if (clickedPersona) {
|
||||
onPersonaChange(clickedPersona);
|
||||
}
|
||||
}}
|
||||
isSelected={isSelected}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="border-t border-border pt-2">
|
||||
<DefaultDropdownElement
|
||||
name={
|
||||
<div className="flex items-center">
|
||||
<FiPlusSquare className="mr-2" />
|
||||
New Assistant
|
||||
</div>
|
||||
}
|
||||
onSelect={() => router.push("/assistants/new")}
|
||||
isSelected={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="select-none text-xl text-strong font-bold flex px-2 rounded cursor-pointer hover:bg-accent-background">
|
||||
<div className="mt-auto">
|
||||
{currentlySelectedPersona?.name || "Default"}
|
||||
</div>
|
||||
<FiChevronDown className="my-auto ml-1" />
|
||||
</div>
|
||||
</CustomDropdown>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import { ChatPage } from "./ChatPage";
|
||||
import { ChatPage } from "./components/ChatPage";
|
||||
import FunctionalWrapper from "../../components/chat/FunctionalWrapper";
|
||||
|
||||
export default function WrappedChat({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { Persona } from "../../admin/assistants/interfaces";
|
||||
|
||||
export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) {
|
||||
return (
|
||||
1811
web/src/app/chat/components/ChatPage.tsx
Normal file
1811
web/src/app/chat/components/ChatPage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,16 @@ import {
|
||||
TooltipContent,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { FiChevronRight } from "react-icons/fi";
|
||||
import { StatusIndicator, ToggleState } from "./message/SubQuestionsDisplay";
|
||||
import { SubQuestionDetail } from "./interfaces";
|
||||
import {
|
||||
StatusIndicator,
|
||||
ToggleState,
|
||||
} from "../../message/SubQuestionsDisplay";
|
||||
import { SubQuestionDetail } from "../../interfaces";
|
||||
import {
|
||||
PHASES_ORDER,
|
||||
StreamingPhase,
|
||||
StreamingPhaseText,
|
||||
} from "./message/StreamingMessages";
|
||||
} from "../../message/StreamingMessages";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import next from "next";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MinimalOnyxDocument, OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
|
||||
import { removeDuplicateDocs } from "@/lib/documentUtils";
|
||||
import { ChatFileType, Message } from "../interfaces";
|
||||
import { ChatFileType, Message } from "@/app/chat/interfaces";
|
||||
import {
|
||||
Dispatch,
|
||||
ForwardedRef,
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { XIcon } from "@/components/icons/icons";
|
||||
import { FileSourceCardInResults } from "../message/SourcesDisplay";
|
||||
import { useDocumentsContext } from "../my-documents/DocumentsContext";
|
||||
import { FileSourceCardInResults } from "@/app/chat/message/SourcesDisplay";
|
||||
import { useDocumentsContext } from "@/app/chat/my-documents/DocumentsContext";
|
||||
interface DocumentResultsProps {
|
||||
agenticMessage: boolean;
|
||||
humanMessage: Message | null;
|
||||
@@ -52,19 +52,6 @@ export const DocumentResults = forwardRef<HTMLDivElement, DocumentResultsProps>(
|
||||
},
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
const [delayedSelectedDocumentCount, setDelayedSelectedDocumentCount] =
|
||||
useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
setDelayedSelectedDocumentCount(selectedDocuments?.length || 0);
|
||||
},
|
||||
selectedDocuments?.length == 0 ? 1000 : 0
|
||||
);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [selectedDocuments]);
|
||||
const { files: allUserFiles } = useDocumentsContext();
|
||||
|
||||
const humanFileDescriptors = humanMessage?.files.filter(
|
||||
@@ -81,7 +68,6 @@ export const DocumentResults = forwardRef<HTMLDivElement, DocumentResultsProps>(
|
||||
|
||||
const tokenLimitReached = selectedDocumentTokens > maxTokens - 75;
|
||||
|
||||
const hasSelectedDocuments = selectedDocumentIds.length > 0;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FileDescriptor } from "../interfaces";
|
||||
import { FileDescriptor } from "@/app/chat/interfaces";
|
||||
|
||||
import { FiX, FiLoader, FiFileText } from "react-icons/fi";
|
||||
import { InputBarPreviewImage } from "./images/InputBarPreviewImage";
|
||||
@@ -7,7 +7,7 @@ import React, {
|
||||
forwardRef,
|
||||
} from "react";
|
||||
import { Folder } from "./interfaces";
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { FiTrash2, FiCheck, FiX } from "react-icons/fi";
|
||||
import { Caret } from "@/components/icons/icons";
|
||||
import { deleteFolder } from "./FolderManagement";
|
||||
@@ -23,7 +23,7 @@ import { useRouter } from "next/navigation";
|
||||
import { CHAT_SESSION_ID_KEY } from "@/lib/drag/constants";
|
||||
import Cookies from "js-cookie";
|
||||
import { Popover } from "@/components/popover/Popover";
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
|
||||
const FolderItem = ({
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
|
||||
export interface Folder {
|
||||
folder_id?: number;
|
||||
@@ -8,7 +8,7 @@ import { InputPrompt } from "@/app/chat/interfaces";
|
||||
|
||||
import { FilterManager, getDisplayNameForModel, LlmManager } from "@/lib/hooks";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import { ChatFileType, FileDescriptor } from "../interfaces";
|
||||
import { ChatFileType, FileDescriptor } from "../../interfaces";
|
||||
import {
|
||||
DocumentIcon2,
|
||||
FileIcon,
|
||||
@@ -24,22 +24,21 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Hoverable } from "@/components/Hoverable";
|
||||
import { ChatState } from "../types";
|
||||
import { ChatState } from "@/app/chat/interfaces";
|
||||
import UnconfiguredProviderText from "@/components/chat/UnconfiguredProviderText";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import { CalendarIcon, TagIcon, XIcon, FolderIcon } from "lucide-react";
|
||||
import { FilterPopup } from "@/components/search/filtering/FilterPopup";
|
||||
import { DocumentSet, Tag } from "@/lib/types";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { getFormattedDateRangeString } from "@/lib/dateUtils";
|
||||
import { truncateString } from "@/lib/utils";
|
||||
import { buildImgUrl } from "../files/images/utils";
|
||||
import { buildImgUrl } from "@/app/chat/components/files/images/utils";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { AgenticToggle } from "./AgenticToggle";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { useDocumentsContext } from "@/app/chat/my-documents/DocumentsContext";
|
||||
import { getProviderIcon } from "@/app/admin/configuration/llm/utils";
|
||||
import { useDocumentsContext } from "../my-documents/DocumentsContext";
|
||||
import { UploadIntent } from "../ChatPage";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
export const SourceChip2 = ({
|
||||
@@ -275,7 +274,7 @@ export function ChatInputBar({
|
||||
}
|
||||
};
|
||||
|
||||
const { finalAssistants: assistantOptions } = useAssistants();
|
||||
const { finalAssistants: assistantOptions } = useAssistantsContext();
|
||||
|
||||
const { llmProviders, inputPrompts } = useChatContext();
|
||||
|
||||
@@ -29,13 +29,13 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
|
||||
onClick,
|
||||
minimize,
|
||||
}) => {
|
||||
const componentRef = useRef<HTMLButtonElement>(null);
|
||||
const componentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
<div
|
||||
ref={componentRef}
|
||||
className={`
|
||||
relative
|
||||
@@ -76,7 +76,7 @@ export const ChatInputOption: React.FC<ChatInputOptionProps> = ({
|
||||
<ChevronDownIcon className="flex-none ml-1" size={size - 4} />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -2,11 +2,11 @@ import React, { useEffect } from "react";
|
||||
import { FiPlusCircle } from "react-icons/fi";
|
||||
import { ChatInputOption } from "./ChatInputOption";
|
||||
import { FilterManager } from "@/lib/hooks";
|
||||
import { ChatFileType, FileDescriptor } from "../interfaces";
|
||||
import { ChatFileType, FileDescriptor } from "@/app/chat/interfaces";
|
||||
import {
|
||||
InputBarPreview,
|
||||
InputBarPreviewImageProvider,
|
||||
} from "../files/InputBarPreview";
|
||||
} from "@/app/chat/components/files/InputBarPreview";
|
||||
import { SendIcon } from "@/components/icons/icons";
|
||||
import { HorizontalSourceSelector } from "@/components/search/filtering/HorizontalSourceSelector";
|
||||
import { Tag } from "@/lib/types";
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FeedbackType } from "../types";
|
||||
import { FeedbackType } from "@/app/chat/interfaces";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { FilledLikeIcon } from "@/components/icons/icons";
|
||||
import { handleChatFeedback } from "../../services/lib";
|
||||
|
||||
const predefinedPositiveFeedbackOptions = process.env
|
||||
.NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS
|
||||
@@ -21,30 +22,60 @@ const predefinedNegativeFeedbackOptions = process.env
|
||||
|
||||
interface FeedbackModalProps {
|
||||
feedbackType: FeedbackType;
|
||||
messageId: number;
|
||||
onClose: () => void;
|
||||
onSubmit: (feedbackDetails: {
|
||||
message: string;
|
||||
predefinedFeedback?: string;
|
||||
}) => void;
|
||||
setPopup: (popup: { message: string; type: "success" | "error" }) => void;
|
||||
}
|
||||
|
||||
export const FeedbackModal = ({
|
||||
feedbackType,
|
||||
messageId,
|
||||
onClose,
|
||||
onSubmit,
|
||||
setPopup,
|
||||
}: FeedbackModalProps) => {
|
||||
const [message, setMessage] = useState("");
|
||||
const [predefinedFeedback, setPredefinedFeedback] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handlePredefinedFeedback = (feedback: string) => {
|
||||
setPredefinedFeedback(feedback);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit({ message, predefinedFeedback });
|
||||
onClose();
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await handleChatFeedback(
|
||||
messageId,
|
||||
feedbackType,
|
||||
message,
|
||||
predefinedFeedback
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: "Thanks for your feedback!",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
const responseJson = await response.json();
|
||||
const errorMsg = responseJson.detail || responseJson.message;
|
||||
setPopup({
|
||||
message: `Failed to submit feedback - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setPopup({
|
||||
message: "Failed to submit feedback - network error",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const predefinedFeedbackOptions =
|
||||
@@ -76,8 +107,19 @@ export const FeedbackModal = ({
|
||||
{predefinedFeedbackOptions.map((feedback, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`bg-background-dark hover:bg-accent-background-hovered text-default py-2 px-4 rounded m-1
|
||||
${predefinedFeedback === feedback && "ring-2 ring-accent/20"}`}
|
||||
disabled={isSubmitting}
|
||||
className={`
|
||||
bg-background-dark
|
||||
hover:bg-accent-background-hovered
|
||||
text-default
|
||||
py-2
|
||||
px-4
|
||||
rounded
|
||||
m-1
|
||||
disabled:opacity-50
|
||||
disabled:cursor-not-allowed
|
||||
${predefinedFeedback === feedback && "ring-2 ring-accent/20"}
|
||||
`}
|
||||
onClick={() => handlePredefinedFeedback(feedback)}
|
||||
>
|
||||
{feedback}
|
||||
@@ -87,14 +129,27 @@ export const FeedbackModal = ({
|
||||
|
||||
<textarea
|
||||
autoFocus
|
||||
disabled={isSubmitting}
|
||||
className={`
|
||||
w-full flex-grow
|
||||
border border-border-strong rounded
|
||||
outline-none placeholder-subtle
|
||||
pl-4 pr-4 py-4 bg-background
|
||||
overflow-hidden h-28
|
||||
whitespace-normal resize-none
|
||||
break-all overscroll-contain
|
||||
w-full
|
||||
flex-grow
|
||||
border
|
||||
border-border-strong
|
||||
rounded
|
||||
outline-none
|
||||
placeholder-subtle
|
||||
pl-4
|
||||
pr-4
|
||||
py-4
|
||||
bg-background
|
||||
overflow-hidden
|
||||
h-28
|
||||
whitespace-normal
|
||||
resize-none
|
||||
break-all
|
||||
overscroll-contain
|
||||
disabled:opacity-50
|
||||
disabled:cursor-not-allowed
|
||||
`}
|
||||
role="textarea"
|
||||
aria-multiline
|
||||
@@ -109,10 +164,22 @@ export const FeedbackModal = ({
|
||||
|
||||
<div className="flex mt-2">
|
||||
<button
|
||||
className="bg-agent text-white py-2 px-4 rounded hover:bg-agent/50 focus:outline-none mx-auto"
|
||||
disabled={isSubmitting}
|
||||
className={`
|
||||
bg-agent
|
||||
text-white
|
||||
py-2
|
||||
px-4
|
||||
rounded
|
||||
hover:bg-agent/50
|
||||
focus:outline-none
|
||||
mx-auto
|
||||
disabled:opacity-50
|
||||
disabled:cursor-not-allowed
|
||||
`}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit feedback
|
||||
{isSubmitting ? "Submitting..." : "Submit feedback"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@@ -5,10 +5,10 @@ import { Callout } from "@/components/ui/callout";
|
||||
|
||||
import Text from "@/components/ui/text";
|
||||
|
||||
import { ChatSessionSharedStatus } from "../interfaces";
|
||||
import { ChatSessionSharedStatus } from "@/app/chat/interfaces";
|
||||
import { FiCopy } from "react-icons/fi";
|
||||
import { CopyButton } from "@/components/CopyButton";
|
||||
import { SEARCH_PARAM_NAMES } from "../searchParams";
|
||||
import { SEARCH_PARAM_NAMES } from "@/app/chat/services/searchParams";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { structureValue } from "@/lib/llm/utils";
|
||||
import { LlmDescriptor } from "@/lib/hooks";
|
||||
@@ -25,7 +25,7 @@ import { useTheme } from "next-themes";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FiTrash2 } from "react-icons/fi";
|
||||
import { deleteAllChatSessions } from "../lib";
|
||||
import { deleteAllChatSessions } from "@/app/chat/services/lib";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
|
||||
type SettingsSection = "settings" | "password";
|
||||
@@ -19,7 +19,7 @@ import { getFinalLLM } from "@/lib/llm/utils";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
|
||||
import { DraggableAssistantCard } from "@/components/assistants/AssistantCards";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
|
||||
export function AssistantsTab({
|
||||
@@ -33,7 +33,7 @@ export function AssistantsTab({
|
||||
}) {
|
||||
const { refreshUser } = useUser();
|
||||
const [_, llmName] = getFinalLLM(llmProviders, null, null);
|
||||
const { finalAssistants, refreshAssistants } = useAssistants();
|
||||
const { finalAssistants, refreshAssistants } = useAssistantsContext();
|
||||
const [assistants, setAssistants] = useState(finalAssistants);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
|
||||
export const ChatGroup = ({
|
||||
groupName,
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { useState, useEffect, useContext, useRef, useCallback } from "react";
|
||||
import {
|
||||
deleteChatSession,
|
||||
getChatRetentionInfo,
|
||||
renameChatSession,
|
||||
} from "../lib";
|
||||
} from "@/app/chat/services/lib";
|
||||
import { BasicSelectable } from "@/components/BasicClickable";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from "react-icons/fi";
|
||||
import { DefaultDropdownElement } from "@/components/Dropdown";
|
||||
import { Popover } from "@/components/popover/Popover";
|
||||
import { ShareChatSessionModal } from "../modal/ShareChatSessionModal";
|
||||
import { ShareChatSessionModal } from "@/app/chat/components/modal/ShareChatSessionModal";
|
||||
import { CHAT_SESSION_ID_KEY, FOLDER_ID_KEY } from "@/lib/drag/constants";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { DragHandle } from "@/components/table/DragHandle";
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { Folder } from "../folders/interfaces";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Folder } from "@/app/chat/components/folders/interfaces";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
|
||||
import {
|
||||
@@ -29,9 +29,9 @@ import { pageType } from "./types";
|
||||
import LogoWithText from "@/components/header/LogoWithText";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { DragEndEvent } from "@dnd-kit/core";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { buildChatUrl } from "../lib";
|
||||
import { buildChatUrl } from "@/app/chat/services/lib";
|
||||
import { reorderPinnedAssistants } from "@/lib/assistants/updateAssistantPreferences";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { DragHandle } from "@/components/table/DragHandle";
|
||||
@@ -193,7 +193,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
const router = useRouter();
|
||||
const { user, toggleAssistantPinnedStatus } = useUser();
|
||||
const { refreshAssistants, pinnedAssistants, setPinnedAssistants } =
|
||||
useAssistants();
|
||||
useAssistantsContext();
|
||||
|
||||
const currentChatId = currentChatSession?.id;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import {
|
||||
createFolder,
|
||||
updateFolderName,
|
||||
@@ -14,7 +14,7 @@ import { FolderDropdown } from "../folders/FolderDropdown";
|
||||
import { ChatSessionDisplay } from "./ChatSessionDisplay";
|
||||
import { useState, useCallback, useRef, useContext, useEffect } from "react";
|
||||
import { Caret } from "@/components/icons/icons";
|
||||
import { groupSessionsByDateRange } from "../lib";
|
||||
import { groupSessionsByDateRange } from "@/app/chat/services/lib";
|
||||
import React from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
99
web/src/app/chat/hooks/useAssistantController.ts
Normal file
99
web/src/app/chat/hooks/useAssistantController.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { useMemo, useState } from "react";
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { SEARCH_PARAM_NAMES } from "../services/searchParams";
|
||||
|
||||
export function useAssistantController({
|
||||
selectedChatSession,
|
||||
}: {
|
||||
selectedChatSession: ChatSession | null | undefined;
|
||||
}) {
|
||||
const searchParams = useSearchParams();
|
||||
const { assistants: availableAssistants, pinnedAssistants } =
|
||||
useAssistantsContext();
|
||||
|
||||
const defaultAssistantIdRaw = searchParams?.get(
|
||||
SEARCH_PARAM_NAMES.PERSONA_ID
|
||||
);
|
||||
const defaultAssistantId = defaultAssistantIdRaw
|
||||
? parseInt(defaultAssistantIdRaw)
|
||||
: undefined;
|
||||
|
||||
const existingChatSessionAssistantId = selectedChatSession?.persona_id;
|
||||
const [selectedAssistant, setSelectedAssistant] = useState<
|
||||
Persona | undefined
|
||||
>(
|
||||
// NOTE: look through available assistants here, so that even if the user
|
||||
// has hidden this assistant it still shows the correct assistant when
|
||||
// going back to an old chat session
|
||||
existingChatSessionAssistantId !== undefined
|
||||
? availableAssistants.find(
|
||||
(assistant) => assistant.id === existingChatSessionAssistantId
|
||||
)
|
||||
: defaultAssistantId !== undefined
|
||||
? availableAssistants.find(
|
||||
(assistant) => assistant.id === defaultAssistantId
|
||||
)
|
||||
: undefined
|
||||
);
|
||||
|
||||
// when the user tags an assistant, we store it here
|
||||
const [alternativeAssistant, setAlternativeAssistant] =
|
||||
useState<Persona | null>(null);
|
||||
|
||||
// Current assistant is decided based on this ordering
|
||||
// 1. Alternative assistant (assistant selected explicitly by user)
|
||||
// 2. Selected assistant (assistnat default in this chat session)
|
||||
// 3. First pinned assistants (ordered list of pinned assistants)
|
||||
// 4. Available assistants (ordered list of available assistants)
|
||||
// Relevant test: `live_assistant.spec.ts`
|
||||
const liveAssistant: Persona | undefined = useMemo(
|
||||
() =>
|
||||
alternativeAssistant ||
|
||||
selectedAssistant ||
|
||||
pinnedAssistants[0] ||
|
||||
availableAssistants[0],
|
||||
[
|
||||
alternativeAssistant,
|
||||
selectedAssistant,
|
||||
pinnedAssistants,
|
||||
availableAssistants,
|
||||
]
|
||||
);
|
||||
|
||||
const setSelectedAssistantFromId = (
|
||||
assistantId: number | null | undefined
|
||||
) => {
|
||||
// NOTE: also intentionally look through available assistants here, so that
|
||||
// even if the user has hidden an assistant they can still go back to it
|
||||
// for old chats
|
||||
let newAssistant =
|
||||
assistantId !== null
|
||||
? availableAssistants.find((assistant) => assistant.id === assistantId)
|
||||
: undefined;
|
||||
|
||||
// if no assistant was passed in / found, use the default assistant
|
||||
if (!newAssistant && defaultAssistantId) {
|
||||
newAssistant = availableAssistants.find(
|
||||
(assistant) => assistant.id === defaultAssistantId
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedAssistant(newAssistant);
|
||||
};
|
||||
|
||||
return {
|
||||
// main assistant selection
|
||||
selectedAssistant,
|
||||
setSelectedAssistantFromId,
|
||||
|
||||
// takes priority over selectedAssistant
|
||||
alternativeAssistant,
|
||||
setAlternativeAssistant,
|
||||
|
||||
// final computed assistant
|
||||
liveAssistant,
|
||||
};
|
||||
}
|
||||
1236
web/src/app/chat/hooks/useChatController.ts
Normal file
1236
web/src/app/chat/hooks/useChatController.ts
Normal file
File diff suppressed because it is too large
Load Diff
302
web/src/app/chat/hooks/useChatSessionController.ts
Normal file
302
web/src/app/chat/hooks/useChatSessionController.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ReadonlyURLSearchParams, useRouter } from "next/navigation";
|
||||
import {
|
||||
nameChatSession,
|
||||
processRawChatHistory,
|
||||
patchMessageToBeLatest,
|
||||
} from "../services/lib";
|
||||
import {
|
||||
getLatestMessageChain,
|
||||
setMessageAsLatest,
|
||||
} from "../services/messageTree";
|
||||
import { BackendChatSession, ChatSessionSharedStatus } from "../interfaces";
|
||||
import {
|
||||
SEARCH_PARAM_NAMES,
|
||||
shouldSubmitOnLoad,
|
||||
} from "../services/searchParams";
|
||||
import { FilterManager } from "@/lib/hooks";
|
||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { FileDescriptor } from "../interfaces";
|
||||
import { FileResponse, FolderResponse } from "../my-documents/DocumentsContext";
|
||||
import { useChatSessionStore } from "../stores/useChatSessionStore";
|
||||
|
||||
interface UseChatSessionControllerProps {
|
||||
existingChatSessionId: string | null;
|
||||
searchParams: ReadonlyURLSearchParams;
|
||||
filterManager: FilterManager;
|
||||
firstMessage?: string;
|
||||
|
||||
// UI state setters
|
||||
setSelectedAssistantFromId: (assistantId: number | null) => void;
|
||||
setSelectedDocuments: (documents: OnyxDocument[]) => void;
|
||||
setCurrentMessageFiles: (
|
||||
files: FileDescriptor[] | ((prev: FileDescriptor[]) => FileDescriptor[])
|
||||
) => void;
|
||||
|
||||
// Refs
|
||||
chatSessionIdRef: React.MutableRefObject<string | null>;
|
||||
loadedIdSessionRef: React.MutableRefObject<string | null>;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
scrollInitialized: React.MutableRefObject<boolean>;
|
||||
isInitialLoad: React.MutableRefObject<boolean>;
|
||||
submitOnLoadPerformed: React.MutableRefObject<boolean>;
|
||||
|
||||
// State
|
||||
hasPerformedInitialScroll: boolean;
|
||||
|
||||
// Actions
|
||||
clientScrollToBottom: (fast?: boolean) => void;
|
||||
clearSelectedItems: () => void;
|
||||
refreshChatSessions: () => void;
|
||||
onSubmit: (params: {
|
||||
message: string;
|
||||
selectedFiles: FileResponse[];
|
||||
selectedFolders: FolderResponse[];
|
||||
currentMessageFiles: FileDescriptor[];
|
||||
useAgentSearch: boolean;
|
||||
isSeededChat?: boolean;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useChatSessionController({
|
||||
existingChatSessionId,
|
||||
searchParams,
|
||||
filterManager,
|
||||
firstMessage,
|
||||
setSelectedAssistantFromId,
|
||||
setSelectedDocuments,
|
||||
setCurrentMessageFiles,
|
||||
chatSessionIdRef,
|
||||
loadedIdSessionRef,
|
||||
textAreaRef,
|
||||
scrollInitialized,
|
||||
isInitialLoad,
|
||||
submitOnLoadPerformed,
|
||||
hasPerformedInitialScroll,
|
||||
clientScrollToBottom,
|
||||
clearSelectedItems,
|
||||
refreshChatSessions,
|
||||
onSubmit,
|
||||
}: UseChatSessionControllerProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// Store actions
|
||||
const updateSessionAndMessageTree = useChatSessionStore(
|
||||
(state) => state.updateSessionAndMessageTree
|
||||
);
|
||||
const updateSessionMessageTree = useChatSessionStore(
|
||||
(state) => state.updateSessionMessageTree
|
||||
);
|
||||
const setIsFetchingChatMessages = useChatSessionStore(
|
||||
(state) => state.setIsFetchingChatMessages
|
||||
);
|
||||
const setCurrentSession = useChatSessionStore(
|
||||
(state) => state.setCurrentSession
|
||||
);
|
||||
const updateHasPerformedInitialScroll = useChatSessionStore(
|
||||
(state) => state.updateHasPerformedInitialScroll
|
||||
);
|
||||
const updateCurrentChatSessionSharedStatus = useChatSessionStore(
|
||||
(state) => state.updateCurrentChatSessionSharedStatus
|
||||
);
|
||||
const updateCurrentSelectedMessageForDocDisplay = useChatSessionStore(
|
||||
(state) => state.updateCurrentSelectedMessageForDocDisplay
|
||||
);
|
||||
const currentChatState = useChatSessionStore(
|
||||
(state) =>
|
||||
state.sessions.get(state.currentSessionId || "")?.chatState || "input"
|
||||
);
|
||||
|
||||
// Fetch chat messages for the chat session
|
||||
useEffect(() => {
|
||||
const priorChatSessionId = chatSessionIdRef.current;
|
||||
const loadedSessionId = loadedIdSessionRef.current;
|
||||
chatSessionIdRef.current = existingChatSessionId;
|
||||
loadedIdSessionRef.current = existingChatSessionId;
|
||||
|
||||
textAreaRef.current?.focus();
|
||||
|
||||
// Only clear things if we're going from one chat session to another
|
||||
const isChatSessionSwitch = existingChatSessionId !== priorChatSessionId;
|
||||
if (isChatSessionSwitch) {
|
||||
// De-select documents
|
||||
// Reset all filters
|
||||
filterManager.setSelectedDocumentSets([]);
|
||||
filterManager.setSelectedSources([]);
|
||||
filterManager.setSelectedTags([]);
|
||||
filterManager.setTimeRange(null);
|
||||
|
||||
// Remove uploaded files
|
||||
setCurrentMessageFiles([]);
|
||||
|
||||
// If switching from one chat to another, then need to scroll again
|
||||
// If we're creating a brand new chat, then don't need to scroll
|
||||
if (priorChatSessionId !== null) {
|
||||
setSelectedDocuments([]);
|
||||
clearSelectedItems();
|
||||
if (existingChatSessionId) {
|
||||
updateHasPerformedInitialScroll(existingChatSessionId, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initialSessionFetch() {
|
||||
if (existingChatSessionId === null) {
|
||||
// Clear the current session in the store to show intro messages
|
||||
setCurrentSession(null);
|
||||
|
||||
// Reset the selected assistant back to default
|
||||
setSelectedAssistantFromId(null);
|
||||
updateCurrentChatSessionSharedStatus(ChatSessionSharedStatus.Private);
|
||||
|
||||
// If we're supposed to submit on initial load, then do that here
|
||||
if (
|
||||
shouldSubmitOnLoad(searchParams) &&
|
||||
!submitOnLoadPerformed.current
|
||||
) {
|
||||
submitOnLoadPerformed.current = true;
|
||||
await onSubmit({
|
||||
message: firstMessage || "",
|
||||
selectedFiles: [],
|
||||
selectedFolders: [],
|
||||
currentMessageFiles: [],
|
||||
useAgentSearch: false,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/api/chat/get-chat-session/${existingChatSessionId}`
|
||||
);
|
||||
|
||||
const session = await response.json();
|
||||
const chatSession = session as BackendChatSession;
|
||||
setSelectedAssistantFromId(chatSession.persona_id);
|
||||
|
||||
// Always set the current session when we have an existing chat session ID
|
||||
setCurrentSession(chatSession.chat_session_id);
|
||||
|
||||
const newMessageMap = processRawChatHistory(chatSession.messages);
|
||||
const newMessageHistory = getLatestMessageChain(newMessageMap);
|
||||
|
||||
// Update message history except for edge where where
|
||||
// last message is an error and we're on a new chat.
|
||||
// This corresponds to a "renaming" of chat, which occurs after first message
|
||||
// stream
|
||||
if (
|
||||
(newMessageHistory[newMessageHistory.length - 1]?.type !== "error" ||
|
||||
loadedSessionId != null) &&
|
||||
!(
|
||||
currentChatState == "toolBuilding" ||
|
||||
currentChatState == "streaming" ||
|
||||
currentChatState == "loading"
|
||||
)
|
||||
) {
|
||||
const latestMessageId =
|
||||
newMessageHistory[newMessageHistory.length - 1]?.messageId;
|
||||
|
||||
updateCurrentSelectedMessageForDocDisplay(
|
||||
latestMessageId !== undefined && latestMessageId !== null
|
||||
? latestMessageId
|
||||
: null
|
||||
);
|
||||
|
||||
updateSessionAndMessageTree(chatSession.chat_session_id, newMessageMap);
|
||||
chatSessionIdRef.current = chatSession.chat_session_id;
|
||||
}
|
||||
|
||||
// Go to bottom. If initial load, then do a scroll,
|
||||
// otherwise just appear at the bottom
|
||||
scrollInitialized.current = false;
|
||||
|
||||
if (!hasPerformedInitialScroll) {
|
||||
if (isInitialLoad.current) {
|
||||
if (chatSession.chat_session_id) {
|
||||
updateHasPerformedInitialScroll(chatSession.chat_session_id, true);
|
||||
}
|
||||
isInitialLoad.current = false;
|
||||
}
|
||||
clientScrollToBottom();
|
||||
|
||||
setTimeout(() => {
|
||||
if (chatSession.chat_session_id) {
|
||||
updateHasPerformedInitialScroll(chatSession.chat_session_id, true);
|
||||
}
|
||||
}, 100);
|
||||
} else if (isChatSessionSwitch) {
|
||||
if (chatSession.chat_session_id) {
|
||||
updateHasPerformedInitialScroll(chatSession.chat_session_id, true);
|
||||
}
|
||||
clientScrollToBottom(true);
|
||||
}
|
||||
|
||||
setIsFetchingChatMessages(chatSession.chat_session_id, false);
|
||||
|
||||
// If this is a seeded chat, then kick off the AI message generation
|
||||
if (
|
||||
newMessageHistory.length === 1 &&
|
||||
!submitOnLoadPerformed.current &&
|
||||
searchParams?.get(SEARCH_PARAM_NAMES.SEEDED) === "true"
|
||||
) {
|
||||
submitOnLoadPerformed.current = true;
|
||||
|
||||
const seededMessage = newMessageHistory[0]?.message;
|
||||
if (!seededMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
await onSubmit({
|
||||
message: seededMessage,
|
||||
isSeededChat: true,
|
||||
selectedFiles: [],
|
||||
selectedFolders: [],
|
||||
currentMessageFiles: [],
|
||||
useAgentSearch: false,
|
||||
});
|
||||
// Force re-name if the chat session doesn't have one
|
||||
if (!chatSession.description) {
|
||||
await nameChatSession(existingChatSessionId);
|
||||
refreshChatSessions();
|
||||
}
|
||||
} else if (newMessageHistory.length === 2 && !chatSession.description) {
|
||||
await nameChatSession(existingChatSessionId);
|
||||
refreshChatSessions();
|
||||
}
|
||||
}
|
||||
|
||||
initialSessionFetch();
|
||||
}, [
|
||||
existingChatSessionId,
|
||||
searchParams?.get(SEARCH_PARAM_NAMES.PERSONA_ID),
|
||||
// Note: We're intentionally not including all dependencies to avoid infinite loops
|
||||
// This effect should only run when existingChatSessionId or persona ID changes
|
||||
]);
|
||||
|
||||
const onMessageSelection = (messageId: number) => {
|
||||
updateCurrentSelectedMessageForDocDisplay(messageId);
|
||||
const currentMessageTree = useChatSessionStore
|
||||
.getState()
|
||||
.sessions.get(
|
||||
useChatSessionStore.getState().currentSessionId || ""
|
||||
)?.messageTree;
|
||||
|
||||
if (currentMessageTree) {
|
||||
const newMessageTree = setMessageAsLatest(currentMessageTree, messageId);
|
||||
const currentSessionId = useChatSessionStore.getState().currentSessionId;
|
||||
if (currentSessionId) {
|
||||
updateSessionMessageTree(currentSessionId, newMessageTree);
|
||||
}
|
||||
}
|
||||
|
||||
// Makes actual API call to set message as latest in the DB so we can
|
||||
// edit this message and so it sticks around on page reload
|
||||
patchMessageToBeLatest(messageId);
|
||||
};
|
||||
|
||||
return {
|
||||
onMessageSelection,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { useState } from "react";
|
||||
import { FileResponse } from "./my-documents/DocumentsContext";
|
||||
import { FileResponse } from "../my-documents/DocumentsContext";
|
||||
|
||||
interface DocumentInfo {
|
||||
num_chunks: number;
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { SourceChip } from "../input/ChatInputBar";
|
||||
import { SourceChip } from "../components/input/ChatInputBar";
|
||||
|
||||
export default function InputPrompts() {
|
||||
const [inputPrompts, setInputPrompts] = useState<InputPrompt[]>([]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SourceChip } from "../input/ChatInputBar";
|
||||
import { SourceChip } from "../components/input/ChatInputBar";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -3,13 +3,20 @@ import {
|
||||
Filters,
|
||||
SearchOnyxDocument,
|
||||
StreamStopReason,
|
||||
SubQuestionPiece,
|
||||
SubQueryPiece,
|
||||
AgentAnswerPiece,
|
||||
SubQuestionSearchDoc,
|
||||
StreamStopInfo,
|
||||
} from "@/lib/search/interfaces";
|
||||
|
||||
export type FeedbackType = "like" | "dislike";
|
||||
export type ChatState =
|
||||
| "input"
|
||||
| "loading"
|
||||
| "streaming"
|
||||
| "toolBuilding"
|
||||
| "uploading";
|
||||
export interface RegenerationState {
|
||||
regenerating: boolean;
|
||||
finalMessageIndex: number;
|
||||
}
|
||||
|
||||
export enum RetrievalType {
|
||||
None = "none",
|
||||
Search = "search",
|
||||
@@ -250,139 +257,3 @@ export interface SubQueryDetail {
|
||||
query_id: number;
|
||||
doc_ids?: number[] | null;
|
||||
}
|
||||
|
||||
export const constructSubQuestions = (
|
||||
subQuestions: SubQuestionDetail[],
|
||||
newDetail:
|
||||
| SubQuestionPiece
|
||||
| SubQueryPiece
|
||||
| AgentAnswerPiece
|
||||
| SubQuestionSearchDoc
|
||||
| DocumentsResponse
|
||||
| StreamStopInfo
|
||||
): SubQuestionDetail[] => {
|
||||
if (!newDetail) {
|
||||
return subQuestions;
|
||||
}
|
||||
if (newDetail.level_question_num == 0) {
|
||||
return subQuestions;
|
||||
}
|
||||
|
||||
const updatedSubQuestions = [...subQuestions];
|
||||
|
||||
if ("stop_reason" in newDetail) {
|
||||
const { level, level_question_num } = newDetail;
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
if (subQuestion) {
|
||||
if (newDetail.stream_type == "sub_answer") {
|
||||
subQuestion.answer_streaming = false;
|
||||
} else {
|
||||
subQuestion.is_complete = true;
|
||||
subQuestion.is_stopped = true;
|
||||
}
|
||||
}
|
||||
} else if ("top_documents" in newDetail) {
|
||||
const { level, level_question_num, top_documents } = newDetail;
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
if (!subQuestion) {
|
||||
subQuestion = {
|
||||
level: level ?? 0,
|
||||
level_question_num: level_question_num ?? 0,
|
||||
question: "",
|
||||
answer: "",
|
||||
sub_queries: [],
|
||||
context_docs: { top_documents },
|
||||
is_complete: false,
|
||||
};
|
||||
} else {
|
||||
subQuestion.context_docs = { top_documents };
|
||||
}
|
||||
} else if ("answer_piece" in newDetail) {
|
||||
// Handle AgentAnswerPiece
|
||||
const { level, level_question_num, answer_piece } = newDetail;
|
||||
// Find or create the relevant SubQuestionDetail
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
|
||||
if (!subQuestion) {
|
||||
subQuestion = {
|
||||
level,
|
||||
level_question_num,
|
||||
question: "",
|
||||
answer: "",
|
||||
sub_queries: [],
|
||||
context_docs: undefined,
|
||||
is_complete: false,
|
||||
};
|
||||
updatedSubQuestions.push(subQuestion);
|
||||
}
|
||||
|
||||
// Append to the answer
|
||||
subQuestion.answer += answer_piece;
|
||||
} else if ("sub_question" in newDetail) {
|
||||
// Handle SubQuestionPiece
|
||||
const { level, level_question_num, sub_question } = newDetail;
|
||||
|
||||
// Find or create the relevant SubQuestionDetail
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
|
||||
if (!subQuestion) {
|
||||
subQuestion = {
|
||||
level,
|
||||
level_question_num,
|
||||
question: "",
|
||||
answer: "",
|
||||
sub_queries: [],
|
||||
context_docs: undefined,
|
||||
is_complete: false,
|
||||
};
|
||||
updatedSubQuestions.push(subQuestion);
|
||||
}
|
||||
|
||||
// Append to the question
|
||||
subQuestion.question += sub_question;
|
||||
} else if ("sub_query" in newDetail) {
|
||||
// Handle SubQueryPiece
|
||||
const { level, level_question_num, query_id, sub_query } = newDetail;
|
||||
|
||||
// Find the relevant SubQuestionDetail
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
|
||||
if (!subQuestion) {
|
||||
// If we receive a sub_query before its parent question, create a placeholder
|
||||
subQuestion = {
|
||||
level,
|
||||
level_question_num: level_question_num,
|
||||
question: "",
|
||||
answer: "",
|
||||
sub_queries: [],
|
||||
context_docs: undefined,
|
||||
};
|
||||
updatedSubQuestions.push(subQuestion);
|
||||
}
|
||||
|
||||
// Find or create the relevant SubQueryDetail
|
||||
let subQuery = subQuestion.sub_queries?.find(
|
||||
(sq) => sq.query_id === query_id
|
||||
);
|
||||
|
||||
if (!subQuery) {
|
||||
subQuery = { query: "", query_id };
|
||||
subQuestion.sub_queries = [...(subQuestion.sub_queries || []), subQuery];
|
||||
}
|
||||
|
||||
// Append to the query
|
||||
subQuery.query += sub_query;
|
||||
}
|
||||
|
||||
return updatedSubQuestions;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FiChevronRight, FiChevronLeft } from "react-icons/fi";
|
||||
import { FeedbackType } from "../types";
|
||||
import { FeedbackType } from "@/app/chat/interfaces";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
FileDescriptor,
|
||||
SubQuestionDetail,
|
||||
ToolCallMetadata,
|
||||
} from "../interfaces";
|
||||
import { SEARCH_TOOL_NAME } from "../tools/constants";
|
||||
} from "@/app/chat/interfaces";
|
||||
import { SEARCH_TOOL_NAME } from "@/app/chat/components/tools/constants";
|
||||
import { Hoverable, HoverableIcon } from "@/components/Hoverable";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
import rehypePrism from "rehype-prism-plus";
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { useMouseTracking } from "./hooks";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import RegenerateOption from "../RegenerateOption";
|
||||
import RegenerateOption from "../components/RegenerateOption";
|
||||
import { LlmDescriptor } from "@/lib/hooks";
|
||||
import { ContinueGenerating } from "./ContinueMessage";
|
||||
import { MemoizedAnchor, MemoizedParagraph } from "./MemoizedTextComponents";
|
||||
@@ -50,13 +50,13 @@ import {
|
||||
extractThinkingContent,
|
||||
isThinkingComplete,
|
||||
removeThinkingTokens,
|
||||
} from "../utils/thinkingTokens";
|
||||
} from "../services/thinkingTokens";
|
||||
|
||||
import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import "katex/dist/katex.min.css";
|
||||
import SubQuestionsDisplay from "./SubQuestionsDisplay";
|
||||
import { StatusRefinement } from "../Refinement";
|
||||
import { StatusRefinement } from "../components/agentSearch/Refinement";
|
||||
import { copyAll, handleCopy } from "./copyingUtils";
|
||||
import { ErrorBanner } from "./Resubmit";
|
||||
import { transformLinkUri } from "@/lib/utils";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
FiTool,
|
||||
FiGlobe,
|
||||
} from "react-icons/fi";
|
||||
import { FeedbackType } from "../types";
|
||||
import { FeedbackType } from "@/app/chat/interfaces";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
@@ -26,16 +26,20 @@ import { SearchSummary, UserKnowledgeFiles } from "./SearchSummary";
|
||||
import { SkippedSearch } from "./SkippedSearch";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CopyButton } from "@/components/CopyButton";
|
||||
import { ChatFileType, FileDescriptor, ToolCallMetadata } from "../interfaces";
|
||||
import {
|
||||
ChatFileType,
|
||||
FileDescriptor,
|
||||
ToolCallMetadata,
|
||||
} from "@/app/chat/interfaces";
|
||||
import {
|
||||
IMAGE_GENERATION_TOOL_NAME,
|
||||
SEARCH_TOOL_NAME,
|
||||
INTERNET_SEARCH_TOOL_NAME,
|
||||
} from "../tools/constants";
|
||||
import { ToolRunDisplay } from "../tools/ToolRunningAnimation";
|
||||
} from "@/app/chat/components/tools/constants";
|
||||
import { ToolRunDisplay } from "../components/tools/ToolRunningAnimation";
|
||||
import { Hoverable, HoverableIcon } from "@/components/Hoverable";
|
||||
import { DocumentPreview } from "../files/documents/DocumentPreview";
|
||||
import { InMessageImage } from "../files/images/InMessageImage";
|
||||
import { DocumentPreview } from "../components/files/documents/DocumentPreview";
|
||||
import { InMessageImage } from "../components/files/images/InMessageImage";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
import rehypePrism from "rehype-prism-plus";
|
||||
import "prismjs/themes/prism-tomorrow.css";
|
||||
@@ -55,8 +59,8 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useMouseTracking } from "./hooks";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import GeneratingImageDisplay from "../tools/GeneratingImageDisplay";
|
||||
import RegenerateOption from "../RegenerateOption";
|
||||
import GeneratingImageDisplay from "../components/tools/GeneratingImageDisplay";
|
||||
import RegenerateOption from "../components/RegenerateOption";
|
||||
import { LlmDescriptor } from "@/lib/hooks";
|
||||
import { ContinueGenerating } from "./ContinueMessage";
|
||||
import { MemoizedAnchor, MemoizedParagraph } from "./MemoizedTextComponents";
|
||||
@@ -80,7 +84,7 @@ import {
|
||||
extractThinkingContent,
|
||||
isThinkingComplete,
|
||||
removeThinkingTokens,
|
||||
} from "../utils/thinkingTokens";
|
||||
} from "../services/thinkingTokens";
|
||||
import { FileResponse } from "../my-documents/DocumentsContext";
|
||||
|
||||
const TOOLS_WITH_CUSTOM_HANDLING = [
|
||||
|
||||
@@ -8,7 +8,7 @@ import React, {
|
||||
import { FiSearch } from "react-icons/fi";
|
||||
import { OnyxDocument } from "@/lib/search/interfaces";
|
||||
import { BaseQuestionIdentifier, SubQuestionDetail } from "../interfaces";
|
||||
import { SourceChip2 } from "../input/ChatInputBar";
|
||||
import { SourceChip2 } from "../components/input/ChatInputBar";
|
||||
import { ResultIcon } from "@/components/chat/sources/SourceCard";
|
||||
import { openDocument } from "@/lib/search/utils";
|
||||
import { SourcesDisplay } from "./SourcesDisplay";
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
cleanThinkingContent,
|
||||
hasPartialThinkingTokens,
|
||||
isThinkingComplete,
|
||||
} from "../../utils/thinkingTokens";
|
||||
} from "../../services/thinkingTokens";
|
||||
import "./ThinkingBox.css";
|
||||
|
||||
interface ThinkingBoxProps {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { BasicClickable } from "@/components/BasicClickable";
|
||||
import { ControlledPopup, DefaultDropdownElement } from "@/components/Dropdown";
|
||||
import { useState } from "react";
|
||||
import { FiCpu, FiSearch } from "react-icons/fi";
|
||||
|
||||
export const QA = "Question Answering";
|
||||
export const SEARCH = "Search Only";
|
||||
|
||||
function SearchTypeSelectorContent({
|
||||
selectedSearchType,
|
||||
setSelectedSearchType,
|
||||
}: {
|
||||
selectedSearchType: string;
|
||||
setSelectedSearchType: React.Dispatch<React.SetStateAction<string>>;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-56">
|
||||
<DefaultDropdownElement
|
||||
key={QA}
|
||||
name={QA}
|
||||
icon={FiCpu}
|
||||
onSelect={() => setSelectedSearchType(QA)}
|
||||
isSelected={selectedSearchType === QA}
|
||||
/>
|
||||
<DefaultDropdownElement
|
||||
key={SEARCH}
|
||||
name={SEARCH}
|
||||
icon={FiSearch}
|
||||
onSelect={() => setSelectedSearchType(SEARCH)}
|
||||
isSelected={selectedSearchType === SEARCH}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchTypeSelector({
|
||||
selectedSearchType,
|
||||
setSelectedSearchType,
|
||||
}: {
|
||||
selectedSearchType: string;
|
||||
setSelectedSearchType: React.Dispatch<React.SetStateAction<string>>;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ControlledPopup
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
popupContent={
|
||||
<SearchTypeSelectorContent
|
||||
selectedSearchType={selectedSearchType}
|
||||
setSelectedSearchType={setSelectedSearchType}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<BasicClickable onClick={() => setIsOpen(!isOpen)}>
|
||||
<div className="flex text-xs">
|
||||
{selectedSearchType === QA ? (
|
||||
<>
|
||||
<FiCpu className="my-auto mr-1" /> QA
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FiSearch className="my-auto mr-1" /> Search
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</BasicClickable>
|
||||
</ControlledPopup>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SimplifiedChatInputBar } from "../input/SimplifiedChatInputBar";
|
||||
import { SimplifiedChatInputBar } from "../components/input/SimplifiedChatInputBar";
|
||||
import { Menu } from "lucide-react";
|
||||
import { Shortcut } from "./interfaces";
|
||||
import {
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { useNightTime } from "@/lib/dateUtils";
|
||||
import { useFilters } from "@/lib/hooks";
|
||||
import { uploadFilesForChat } from "../lib";
|
||||
import { uploadFilesForChat } from "../services/lib";
|
||||
import { ChatFileType, FileDescriptor } from "../interfaces";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
import Dropzone from "react-dropzone";
|
||||
|
||||
146
web/src/app/chat/services/constructSubQuestions.ts
Normal file
146
web/src/app/chat/services/constructSubQuestions.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
AgentAnswerPiece,
|
||||
SubQuestionPiece,
|
||||
SubQuestionSearchDoc,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { StreamStopInfo } from "@/lib/search/interfaces";
|
||||
import { SubQueryPiece } from "@/lib/search/interfaces";
|
||||
import { SubQuestionDetail } from "../interfaces";
|
||||
|
||||
import { DocumentsResponse } from "../interfaces";
|
||||
|
||||
export const constructSubQuestions = (
|
||||
subQuestions: SubQuestionDetail[],
|
||||
newDetail:
|
||||
| SubQuestionPiece
|
||||
| SubQueryPiece
|
||||
| AgentAnswerPiece
|
||||
| SubQuestionSearchDoc
|
||||
| DocumentsResponse
|
||||
| StreamStopInfo
|
||||
): SubQuestionDetail[] => {
|
||||
if (!newDetail) {
|
||||
return subQuestions;
|
||||
}
|
||||
if (newDetail.level_question_num == 0) {
|
||||
return subQuestions;
|
||||
}
|
||||
|
||||
const updatedSubQuestions = [...subQuestions];
|
||||
|
||||
if ("stop_reason" in newDetail) {
|
||||
const { level, level_question_num } = newDetail;
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
if (subQuestion) {
|
||||
if (newDetail.stream_type == "sub_answer") {
|
||||
subQuestion.answer_streaming = false;
|
||||
} else {
|
||||
subQuestion.is_complete = true;
|
||||
subQuestion.is_stopped = true;
|
||||
}
|
||||
}
|
||||
} else if ("top_documents" in newDetail) {
|
||||
const { level, level_question_num, top_documents } = newDetail;
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
if (!subQuestion) {
|
||||
subQuestion = {
|
||||
level: level ?? 0,
|
||||
level_question_num: level_question_num ?? 0,
|
||||
question: "",
|
||||
answer: "",
|
||||
sub_queries: [],
|
||||
context_docs: { top_documents },
|
||||
is_complete: false,
|
||||
};
|
||||
} else {
|
||||
subQuestion.context_docs = { top_documents };
|
||||
}
|
||||
} else if ("answer_piece" in newDetail) {
|
||||
// Handle AgentAnswerPiece
|
||||
const { level, level_question_num, answer_piece } = newDetail;
|
||||
// Find or create the relevant SubQuestionDetail
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
|
||||
if (!subQuestion) {
|
||||
subQuestion = {
|
||||
level,
|
||||
level_question_num,
|
||||
question: "",
|
||||
answer: "",
|
||||
sub_queries: [],
|
||||
context_docs: undefined,
|
||||
is_complete: false,
|
||||
};
|
||||
updatedSubQuestions.push(subQuestion);
|
||||
}
|
||||
|
||||
// Append to the answer
|
||||
subQuestion.answer += answer_piece;
|
||||
} else if ("sub_question" in newDetail) {
|
||||
// Handle SubQuestionPiece
|
||||
const { level, level_question_num, sub_question } = newDetail;
|
||||
|
||||
// Find or create the relevant SubQuestionDetail
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
|
||||
if (!subQuestion) {
|
||||
subQuestion = {
|
||||
level,
|
||||
level_question_num,
|
||||
question: "",
|
||||
answer: "",
|
||||
sub_queries: [],
|
||||
context_docs: undefined,
|
||||
is_complete: false,
|
||||
};
|
||||
updatedSubQuestions.push(subQuestion);
|
||||
}
|
||||
|
||||
// Append to the question
|
||||
subQuestion.question += sub_question;
|
||||
} else if ("sub_query" in newDetail) {
|
||||
// Handle SubQueryPiece
|
||||
const { level, level_question_num, query_id, sub_query } = newDetail;
|
||||
|
||||
// Find the relevant SubQuestionDetail
|
||||
let subQuestion = updatedSubQuestions.find(
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
|
||||
if (!subQuestion) {
|
||||
// If we receive a sub_query before its parent question, create a placeholder
|
||||
subQuestion = {
|
||||
level,
|
||||
level_question_num: level_question_num,
|
||||
question: "",
|
||||
answer: "",
|
||||
sub_queries: [],
|
||||
context_docs: undefined,
|
||||
};
|
||||
updatedSubQuestions.push(subQuestion);
|
||||
}
|
||||
|
||||
// Find or create the relevant SubQueryDetail
|
||||
let subQuery = subQuestion.sub_queries?.find(
|
||||
(sq) => sq.query_id === query_id
|
||||
);
|
||||
|
||||
if (!subQuery) {
|
||||
subQuery = { query: "", query_id };
|
||||
subQuestion.sub_queries = [...(subQuestion.sub_queries || []), subQuery];
|
||||
}
|
||||
|
||||
// Append to the query
|
||||
subQuery.query += sub_query;
|
||||
}
|
||||
|
||||
return updatedSubQuestions;
|
||||
};
|
||||
45
web/src/app/chat/services/currentMessageFIFO.ts
Normal file
45
web/src/app/chat/services/currentMessageFIFO.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { PacketType, sendMessage, SendMessageParams } from "./lib";
|
||||
|
||||
export class CurrentMessageFIFO {
|
||||
private stack: PacketType[] = [];
|
||||
isComplete: boolean = false;
|
||||
error: string | null = null;
|
||||
|
||||
push(packetBunch: PacketType) {
|
||||
this.stack.push(packetBunch);
|
||||
}
|
||||
|
||||
nextPacket(): PacketType | undefined {
|
||||
return this.stack.shift();
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.stack.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCurrentMessageFIFO(
|
||||
stack: CurrentMessageFIFO,
|
||||
params: SendMessageParams
|
||||
) {
|
||||
try {
|
||||
for await (const packet of sendMessage(params)) {
|
||||
if (params.signal?.aborted) {
|
||||
throw new Error("AbortError");
|
||||
}
|
||||
stack.push(packet);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === "AbortError") {
|
||||
console.debug("Stream aborted");
|
||||
} else {
|
||||
stack.error = error.message;
|
||||
}
|
||||
} else {
|
||||
stack.error = String(error);
|
||||
}
|
||||
} finally {
|
||||
stack.isComplete = true;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
RefinedAnswerImprovement,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { handleSSEStream } from "@/lib/search/streamingUtils";
|
||||
import { ChatState, FeedbackType } from "./types";
|
||||
import { ChatState, FeedbackType } from "@/app/chat/interfaces";
|
||||
import { MutableRefObject, RefObject, useEffect, useRef } from "react";
|
||||
import {
|
||||
BackendMessage,
|
||||
@@ -27,14 +27,14 @@ import {
|
||||
ToolCallMetadata,
|
||||
AgenticMessageResponseIDInfo,
|
||||
UserKnowledgeFilePacket,
|
||||
} from "./interfaces";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
} from "../interfaces";
|
||||
import { Persona } from "../../admin/assistants/interfaces";
|
||||
import { ReadonlyURLSearchParams } from "next/navigation";
|
||||
import { SEARCH_PARAM_NAMES } from "./searchParams";
|
||||
import { Settings } from "../admin/settings/interfaces";
|
||||
import { INTERNET_SEARCH_TOOL_ID } from "./tools/constants";
|
||||
import { SEARCH_TOOL_ID } from "./tools/constants";
|
||||
import { IIMAGE_GENERATION_TOOL_ID } from "./tools/constants";
|
||||
import { Settings } from "../../admin/settings/interfaces";
|
||||
import { INTERNET_SEARCH_TOOL_ID } from "../components/tools/constants";
|
||||
import { SEARCH_TOOL_ID } from "../components/tools/constants";
|
||||
import { IIMAGE_GENERATION_TOOL_ID } from "../components/tools/constants";
|
||||
|
||||
interface ChatRetentionInfo {
|
||||
chatRetentionDays: number;
|
||||
@@ -179,7 +179,7 @@ export interface SendMessageParams {
|
||||
signal?: AbortSignal;
|
||||
userFileIds?: number[];
|
||||
userFolderIds?: number[];
|
||||
useLanggraph?: boolean;
|
||||
useAgentSearch?: boolean;
|
||||
}
|
||||
|
||||
export async function* sendMessage({
|
||||
@@ -201,7 +201,7 @@ export async function* sendMessage({
|
||||
useExistingUserMessage,
|
||||
alternateAssistantId,
|
||||
signal,
|
||||
useLanggraph,
|
||||
useAgentSearch,
|
||||
}: SendMessageParams): AsyncGenerator<PacketType, void, unknown> {
|
||||
const documentsAreSelected =
|
||||
selectedDocumentIds && selectedDocumentIds.length > 0;
|
||||
@@ -241,7 +241,7 @@ export async function* sendMessage({
|
||||
}
|
||||
: null,
|
||||
use_existing_user_message: useExistingUserMessage,
|
||||
use_agentic_search: useLanggraph ?? false,
|
||||
use_agentic_search: useAgentSearch ?? false,
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/chat/send-message`, {
|
||||
@@ -274,7 +274,7 @@ export async function nameChatSession(chatSessionId: string) {
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function setMessageAsLatest(messageId: number) {
|
||||
export async function patchMessageToBeLatest(messageId: number) {
|
||||
const response = await fetch("/api/chat/set-message-as-latest", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
@@ -459,18 +459,6 @@ export function groupSessionsByDateRange(chatSessions: ChatSession[]) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
export function getLastSuccessfulMessageId(messageHistory: Message[]) {
|
||||
const lastSuccessfulMessage = messageHistory
|
||||
.slice()
|
||||
.reverse()
|
||||
.find(
|
||||
(message) =>
|
||||
(message.type === "assistant" || message.type === "system") &&
|
||||
message.messageId !== -1 &&
|
||||
message.messageId !== null
|
||||
);
|
||||
return lastSuccessfulMessage ? lastSuccessfulMessage?.messageId : null;
|
||||
}
|
||||
export function processRawChatHistory(
|
||||
rawMessages: BackendMessage[]
|
||||
): Map<number, Message> {
|
||||
@@ -549,86 +537,6 @@ export function processRawChatHistory(
|
||||
return messages;
|
||||
}
|
||||
|
||||
export function buildLatestMessageChain(
|
||||
messageMap: Map<number, Message>,
|
||||
additionalMessagesOnMainline: Message[] = []
|
||||
): Message[] {
|
||||
const rootMessage = Array.from(messageMap.values()).find(
|
||||
(message) => message.parentMessageId === null
|
||||
);
|
||||
|
||||
let finalMessageList: Message[] = [];
|
||||
if (rootMessage) {
|
||||
let currMessage: Message | null = rootMessage;
|
||||
while (currMessage) {
|
||||
finalMessageList.push(currMessage);
|
||||
const childMessageNumber = currMessage.latestChildMessageId;
|
||||
if (childMessageNumber && messageMap.has(childMessageNumber)) {
|
||||
currMessage = messageMap.get(childMessageNumber) as Message;
|
||||
} else {
|
||||
currMessage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// remove system message
|
||||
if (
|
||||
finalMessageList.length > 0 &&
|
||||
finalMessageList[0] &&
|
||||
finalMessageList[0].type === "system"
|
||||
) {
|
||||
finalMessageList = finalMessageList.slice(1);
|
||||
}
|
||||
return finalMessageList.concat(additionalMessagesOnMainline);
|
||||
}
|
||||
|
||||
export function updateParentChildren(
|
||||
message: Message,
|
||||
completeMessageMap: Map<number, Message>,
|
||||
setAsLatestChild: boolean = false
|
||||
) {
|
||||
// NOTE: updates the `completeMessageMap` in place
|
||||
const parentMessage = message.parentMessageId
|
||||
? completeMessageMap.get(message.parentMessageId)
|
||||
: null;
|
||||
if (parentMessage) {
|
||||
if (setAsLatestChild) {
|
||||
parentMessage.latestChildMessageId = message.messageId;
|
||||
}
|
||||
|
||||
const parentChildMessages = parentMessage.childrenMessageIds || [];
|
||||
if (!parentChildMessages.includes(message.messageId)) {
|
||||
parentChildMessages.push(message.messageId);
|
||||
}
|
||||
parentMessage.childrenMessageIds = parentChildMessages;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeMessage(
|
||||
messageId: number,
|
||||
completeMessageMap: Map<number, Message>
|
||||
) {
|
||||
const messageToRemove = completeMessageMap.get(messageId);
|
||||
if (!messageToRemove) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentMessage = messageToRemove.parentMessageId
|
||||
? completeMessageMap.get(messageToRemove.parentMessageId)
|
||||
: null;
|
||||
if (parentMessage) {
|
||||
if (parentMessage.latestChildMessageId === messageId) {
|
||||
parentMessage.latestChildMessageId = null;
|
||||
}
|
||||
const currChildMessage = parentMessage.childrenMessageIds || [];
|
||||
const newChildMessage = currChildMessage.filter((id) => id !== messageId);
|
||||
parentMessage.childrenMessageIds = newChildMessage;
|
||||
}
|
||||
|
||||
completeMessageMap.delete(messageId);
|
||||
}
|
||||
|
||||
export function checkAnyAssistantHasSearch(
|
||||
messageHistory: Message[],
|
||||
availableAssistants: Persona[],
|
||||
391
web/src/app/chat/services/messageTree.ts
Normal file
391
web/src/app/chat/services/messageTree.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
import { Message } from "../interfaces";
|
||||
|
||||
export const SYSTEM_MESSAGE_ID = -3;
|
||||
|
||||
export type MessageTreeState = Map<number, Message>;
|
||||
|
||||
export function createInitialMessageTreeState(
|
||||
initialMessages?: Map<number, Message> | Message[]
|
||||
): MessageTreeState {
|
||||
if (!initialMessages) {
|
||||
return new Map();
|
||||
}
|
||||
if (initialMessages instanceof Map) {
|
||||
return new Map(initialMessages); // Shallow copy
|
||||
}
|
||||
return new Map(initialMessages.map((msg) => [msg.messageId, msg]));
|
||||
}
|
||||
|
||||
export function getMessage(
|
||||
messages: MessageTreeState,
|
||||
messageId: number
|
||||
): Message | undefined {
|
||||
return messages.get(messageId);
|
||||
}
|
||||
|
||||
function updateParentInMap(
|
||||
map: Map<number, Message>,
|
||||
parentId: number,
|
||||
childId: number,
|
||||
makeLatest: boolean
|
||||
): void {
|
||||
const parent = map.get(parentId);
|
||||
if (parent) {
|
||||
const parentChildren = parent.childrenMessageIds || [];
|
||||
const childrenSet = new Set(parentChildren);
|
||||
let updatedChildren = parentChildren;
|
||||
|
||||
if (!childrenSet.has(childId)) {
|
||||
updatedChildren = [...parentChildren, childId];
|
||||
}
|
||||
|
||||
const updatedParent = {
|
||||
...parent,
|
||||
childrenMessageIds: updatedChildren,
|
||||
// Update latestChild only if explicitly requested or if it's the only child,
|
||||
// or if the child was newly added
|
||||
latestChildMessageId:
|
||||
makeLatest || updatedChildren.length === 1 || !childrenSet.has(childId)
|
||||
? childId
|
||||
: parent.latestChildMessageId,
|
||||
};
|
||||
if (makeLatest && parent.latestChildMessageId !== childId) {
|
||||
updatedParent.latestChildMessageId = childId;
|
||||
}
|
||||
|
||||
map.set(parentId, updatedParent);
|
||||
} else {
|
||||
console.warn(
|
||||
`Parent message with ID ${parentId} not found when updating for child ${childId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function upsertMessages(
|
||||
currentMessages: MessageTreeState,
|
||||
messagesToAdd: Message[],
|
||||
makeLatestChildMessage: boolean = false
|
||||
): MessageTreeState {
|
||||
let newMessages = new Map(currentMessages);
|
||||
let messagesToAddClones = messagesToAdd.map((msg) => ({ ...msg })); // Clone all incoming messages
|
||||
|
||||
if (newMessages.size === 0 && messagesToAddClones.length > 0) {
|
||||
const firstMessage = messagesToAddClones[0];
|
||||
if (!firstMessage) {
|
||||
throw new Error("No first message found in the message tree.");
|
||||
}
|
||||
const systemMessageId =
|
||||
firstMessage.parentMessageId !== null
|
||||
? firstMessage.parentMessageId
|
||||
: SYSTEM_MESSAGE_ID;
|
||||
const firstMessageId = firstMessage.messageId;
|
||||
|
||||
// Check if system message needs to be added or already exists (e.g., from parentMessageId)
|
||||
if (!newMessages.has(systemMessageId)) {
|
||||
const dummySystemMessage: Message = {
|
||||
messageId: systemMessageId,
|
||||
message: "",
|
||||
type: "system",
|
||||
files: [],
|
||||
toolCall: null,
|
||||
parentMessageId: null,
|
||||
childrenMessageIds: [firstMessageId],
|
||||
latestChildMessageId: firstMessageId,
|
||||
};
|
||||
newMessages.set(dummySystemMessage.messageId, dummySystemMessage);
|
||||
}
|
||||
// Ensure the first message points to the system message if its parent was null
|
||||
if (!firstMessage) {
|
||||
console.error("No first message found in the message tree.");
|
||||
return newMessages;
|
||||
}
|
||||
if (firstMessage.parentMessageId === null) {
|
||||
firstMessage.parentMessageId = systemMessageId;
|
||||
}
|
||||
}
|
||||
|
||||
messagesToAddClones.forEach((message) => {
|
||||
// Add/update the message itself
|
||||
newMessages.set(message.messageId, message);
|
||||
|
||||
// Update parent's children if the message has a parent
|
||||
if (message.parentMessageId !== null) {
|
||||
// When adding multiple messages, only make the *first* one added potentially the latest,
|
||||
// unless `makeLatestChildMessage` is true for all.
|
||||
// Let's stick to the original logic: update parent, potentially making this message latest
|
||||
// based on makeLatestChildMessage flag OR if it's a new child being added.
|
||||
updateParentInMap(
|
||||
newMessages,
|
||||
message.parentMessageId,
|
||||
message.messageId,
|
||||
makeLatestChildMessage
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Explicitly set the last message of the batch as the latest if requested,
|
||||
// overriding previous updates within the loop if necessary.
|
||||
if (makeLatestChildMessage && messagesToAddClones.length > 0) {
|
||||
const lastMessage = messagesToAddClones[messagesToAddClones.length - 1];
|
||||
if (!lastMessage) {
|
||||
console.error("No last message found in the message tree.");
|
||||
return newMessages;
|
||||
}
|
||||
if (lastMessage.parentMessageId !== null) {
|
||||
const parent = newMessages.get(lastMessage.parentMessageId);
|
||||
if (parent && parent.latestChildMessageId !== lastMessage.messageId) {
|
||||
const updatedParent = {
|
||||
...parent,
|
||||
latestChildMessageId: lastMessage.messageId,
|
||||
};
|
||||
newMessages.set(parent.messageId, updatedParent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
export function removeMessage(
|
||||
currentMessages: MessageTreeState,
|
||||
messageIdToRemove: number
|
||||
): MessageTreeState {
|
||||
if (!currentMessages.has(messageIdToRemove)) {
|
||||
return currentMessages; // Return original if message doesn't exist
|
||||
}
|
||||
|
||||
const newMessages = new Map(currentMessages);
|
||||
const messageToRemove = newMessages.get(messageIdToRemove)!;
|
||||
|
||||
// Collect all descendant IDs to remove
|
||||
const idsToRemove = new Set<number>();
|
||||
const queue: number[] = [messageIdToRemove];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift()!;
|
||||
if (!newMessages.has(currentId) || idsToRemove.has(currentId)) continue;
|
||||
idsToRemove.add(currentId);
|
||||
|
||||
const currentMsg = newMessages.get(currentId);
|
||||
if (currentMsg?.childrenMessageIds) {
|
||||
currentMsg.childrenMessageIds.forEach((childId) => queue.push(childId));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all descendants
|
||||
idsToRemove.forEach((id) => newMessages.delete(id));
|
||||
|
||||
// Update the parent
|
||||
if (messageToRemove.parentMessageId !== null) {
|
||||
const parent = newMessages.get(messageToRemove.parentMessageId);
|
||||
if (parent) {
|
||||
const updatedChildren = (parent.childrenMessageIds || []).filter(
|
||||
(id) => id !== messageIdToRemove
|
||||
);
|
||||
const updatedParent = {
|
||||
...parent,
|
||||
childrenMessageIds: updatedChildren,
|
||||
// If the removed message was the latest, find the new latest (last in the updated children list)
|
||||
latestChildMessageId:
|
||||
parent.latestChildMessageId === messageIdToRemove
|
||||
? updatedChildren.length > 0
|
||||
? updatedChildren[updatedChildren.length - 1]
|
||||
: null
|
||||
: parent.latestChildMessageId,
|
||||
};
|
||||
newMessages.set(parent.messageId, updatedParent);
|
||||
}
|
||||
}
|
||||
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
export function setMessageAsLatest(
|
||||
currentMessages: MessageTreeState,
|
||||
messageId: number
|
||||
): MessageTreeState {
|
||||
const message = currentMessages.get(messageId);
|
||||
if (!message || message.parentMessageId === null) {
|
||||
return currentMessages; // Cannot set root or non-existent message as latest
|
||||
}
|
||||
|
||||
const parent = currentMessages.get(message.parentMessageId);
|
||||
if (!parent || !(parent.childrenMessageIds || []).includes(messageId)) {
|
||||
console.warn(
|
||||
`Cannot set message ${messageId} as latest, parent ${message.parentMessageId} or child link missing.`
|
||||
);
|
||||
return currentMessages; // Parent doesn't exist or doesn't list this message as a child
|
||||
}
|
||||
|
||||
if (parent.latestChildMessageId === messageId) {
|
||||
return currentMessages; // Already the latest
|
||||
}
|
||||
|
||||
const newMessages = new Map(currentMessages);
|
||||
const updatedParent = {
|
||||
...parent,
|
||||
latestChildMessageId: messageId,
|
||||
};
|
||||
newMessages.set(parent.messageId, updatedParent);
|
||||
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
export function getLatestMessageChain(messages: MessageTreeState): Message[] {
|
||||
const chain: Message[] = [];
|
||||
if (messages.size === 0) {
|
||||
return chain;
|
||||
}
|
||||
|
||||
// Find the root message
|
||||
let root: Message | undefined;
|
||||
if (messages.has(SYSTEM_MESSAGE_ID)) {
|
||||
root = messages.get(SYSTEM_MESSAGE_ID);
|
||||
} else {
|
||||
// Use Array.from to fix linter error
|
||||
const potentialRoots = Array.from(messages.values()).filter(
|
||||
(message) =>
|
||||
message.parentMessageId === null ||
|
||||
!messages.has(message.parentMessageId!)
|
||||
);
|
||||
if (potentialRoots.length > 0) {
|
||||
// Prefer non-system message if multiple roots found somehow
|
||||
root =
|
||||
potentialRoots.find((m) => m.type !== "system") || potentialRoots[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!root) {
|
||||
console.error("Could not determine the root message.");
|
||||
// Fallback: return flat list sorted by ID perhaps? Or empty?
|
||||
return Array.from(messages.values()).sort(
|
||||
(a, b) => a.messageId - b.messageId
|
||||
);
|
||||
}
|
||||
|
||||
let currentMessage: Message | undefined = root;
|
||||
// The root itself (like SYSTEM_MESSAGE) might not be part of the visible chain
|
||||
if (root.messageId !== SYSTEM_MESSAGE_ID && root.type !== "system") {
|
||||
// Need to clone message for safety? If MessageTreeState guarantees immutability maybe not.
|
||||
// Let's assume Message objects within the map are treated as immutable.
|
||||
chain.push(root);
|
||||
}
|
||||
|
||||
while (
|
||||
currentMessage?.latestChildMessageId !== null &&
|
||||
currentMessage?.latestChildMessageId !== undefined
|
||||
) {
|
||||
const nextMessageId = currentMessage.latestChildMessageId;
|
||||
const nextMessage = messages.get(nextMessageId);
|
||||
if (nextMessage) {
|
||||
chain.push(nextMessage);
|
||||
currentMessage = nextMessage;
|
||||
} else {
|
||||
console.warn(`Chain broken: Message ${nextMessageId} not found.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
export function getHumanAndAIMessageFromMessageNumber(
|
||||
messages: MessageTreeState,
|
||||
messageNumber: number
|
||||
): { humanMessage: Message | null; aiMessage: Message | null } {
|
||||
const latestChain = getLatestMessageChain(messages);
|
||||
const messageIndex = latestChain.findIndex(
|
||||
(msg) => msg.messageId === messageNumber
|
||||
);
|
||||
|
||||
if (messageIndex === -1) {
|
||||
// Maybe the message exists but isn't in the latest chain? Search the whole map.
|
||||
const message = messages.get(messageNumber);
|
||||
if (!message) return { humanMessage: null, aiMessage: null };
|
||||
|
||||
if (message.type === "user") {
|
||||
// Find its latest child that is an assistant
|
||||
const potentialAiMessage =
|
||||
message.latestChildMessageId !== null &&
|
||||
message.latestChildMessageId !== undefined
|
||||
? messages.get(message.latestChildMessageId)
|
||||
: undefined;
|
||||
const aiMessage =
|
||||
potentialAiMessage?.type === "assistant" ? potentialAiMessage : null;
|
||||
return { humanMessage: message, aiMessage };
|
||||
} else if (message.type === "assistant" || message.type === "error") {
|
||||
const humanMessage =
|
||||
message.parentMessageId !== null
|
||||
? messages.get(message.parentMessageId)
|
||||
: null;
|
||||
return {
|
||||
humanMessage: humanMessage?.type === "user" ? humanMessage : null,
|
||||
aiMessage: message,
|
||||
};
|
||||
}
|
||||
return { humanMessage: null, aiMessage: null };
|
||||
}
|
||||
|
||||
// Message is in the latest chain
|
||||
const message = latestChain[messageIndex];
|
||||
if (!message) {
|
||||
console.error(`Message ${messageNumber} not found in the latest chain.`);
|
||||
return { humanMessage: null, aiMessage: null };
|
||||
}
|
||||
|
||||
if (message.type === "user") {
|
||||
const potentialAiMessage = latestChain[messageIndex + 1];
|
||||
const aiMessage =
|
||||
potentialAiMessage?.type === "assistant" &&
|
||||
potentialAiMessage.parentMessageId === message.messageId
|
||||
? potentialAiMessage
|
||||
: null;
|
||||
return { humanMessage: message, aiMessage };
|
||||
} else if (message.type === "assistant" || message.type === "error") {
|
||||
const potentialHumanMessage = latestChain[messageIndex - 1];
|
||||
const humanMessage =
|
||||
potentialHumanMessage?.type === "user" &&
|
||||
message.parentMessageId === potentialHumanMessage.messageId
|
||||
? potentialHumanMessage
|
||||
: null;
|
||||
return { humanMessage, aiMessage: message };
|
||||
}
|
||||
|
||||
return { humanMessage: null, aiMessage: null };
|
||||
}
|
||||
|
||||
export function getLastSuccessfulMessageId(
|
||||
messages: MessageTreeState,
|
||||
chain?: Message[]
|
||||
): number | null {
|
||||
const messageChain = chain || getLatestMessageChain(messages);
|
||||
for (let i = messageChain.length - 1; i >= 0; i--) {
|
||||
const message = messageChain[i];
|
||||
if (!message) {
|
||||
console.error(`Message ${i} not found in the message chain.`);
|
||||
continue;
|
||||
}
|
||||
if (message.type !== "error") {
|
||||
return message.messageId;
|
||||
}
|
||||
}
|
||||
|
||||
// If the chain starts with an error or is empty, check for system message
|
||||
const systemMessage = messages.get(SYSTEM_MESSAGE_ID);
|
||||
if (systemMessage) {
|
||||
// Check if the system message itself is considered "successful" (it usually is)
|
||||
// Or if it has a successful child
|
||||
const childId = systemMessage.latestChildMessageId;
|
||||
if (childId !== null && childId !== undefined) {
|
||||
const firstRealMessage = messages.get(childId);
|
||||
if (firstRealMessage && firstRealMessage.type !== "error") {
|
||||
return firstRealMessage.messageId;
|
||||
}
|
||||
}
|
||||
// If no successful child, return the system message ID itself as the root?
|
||||
// This matches the class behavior implicitly returning the root ID if nothing else works.
|
||||
return systemMessage.messageId;
|
||||
}
|
||||
|
||||
return null; // No successful message found
|
||||
}
|
||||
@@ -5,108 +5,122 @@
|
||||
/**
|
||||
* Check if a message contains complete thinking tokens
|
||||
*/
|
||||
export function hasCompletedThinkingTokens(content: string | JSX.Element): boolean {
|
||||
if (typeof content !== 'string') return false;
|
||||
|
||||
return /<think>[\s\S]*?<\/think>/.test(content) ||
|
||||
/<thinking>[\s\S]*?<\/thinking>/.test(content);
|
||||
export function hasCompletedThinkingTokens(
|
||||
content: string | JSX.Element
|
||||
): boolean {
|
||||
if (typeof content !== "string") return false;
|
||||
|
||||
return (
|
||||
/<think>[\s\S]*?<\/think>/.test(content) ||
|
||||
/<thinking>[\s\S]*?<\/thinking>/.test(content)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message contains partial thinking tokens (streaming)
|
||||
*/
|
||||
export function hasPartialThinkingTokens(content: string | JSX.Element): boolean {
|
||||
if (typeof content !== 'string') return false;
|
||||
|
||||
export function hasPartialThinkingTokens(
|
||||
content: string | JSX.Element
|
||||
): boolean {
|
||||
if (typeof content !== "string") return false;
|
||||
|
||||
// Count opening and closing tags
|
||||
const thinkOpenCount = (content.match(/<think>/g) || []).length;
|
||||
const thinkCloseCount = (content.match(/<\/think>/g) || []).length;
|
||||
const thinkingOpenCount = (content.match(/<thinking>/g) || []).length;
|
||||
const thinkingCloseCount = (content.match(/<\/thinking>/g) || []).length;
|
||||
|
||||
|
||||
// Return true if we have any unmatched tags
|
||||
return thinkOpenCount > thinkCloseCount || thinkingOpenCount > thinkingCloseCount;
|
||||
return (
|
||||
thinkOpenCount > thinkCloseCount || thinkingOpenCount > thinkingCloseCount
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract thinking content from a message
|
||||
*/
|
||||
export function extractThinkingContent(content: string | JSX.Element): string {
|
||||
if (typeof content !== 'string') return '';
|
||||
|
||||
if (typeof content !== "string") return "";
|
||||
|
||||
// For complete thinking tags, extract all sections
|
||||
const completeThinkRegex = /<think>[\s\S]*?<\/think>/g;
|
||||
const completeThinkingRegex = /<thinking>[\s\S]*?<\/thinking>/g;
|
||||
|
||||
|
||||
const thinkMatches = Array.from(content.matchAll(completeThinkRegex));
|
||||
const thinkingMatches = Array.from(content.matchAll(completeThinkingRegex));
|
||||
|
||||
|
||||
if (thinkMatches.length > 0 || thinkingMatches.length > 0) {
|
||||
// Combine all matches and sort by their position in the original string
|
||||
const allMatches = [...thinkMatches, ...thinkingMatches]
|
||||
.sort((a, b) => (a.index || 0) - (b.index || 0));
|
||||
return allMatches.map(match => match[0]).join('\n');
|
||||
const allMatches = [...thinkMatches, ...thinkingMatches].sort(
|
||||
(a, b) => (a.index || 0) - (b.index || 0)
|
||||
);
|
||||
return allMatches.map((match) => match[0]).join("\n");
|
||||
}
|
||||
|
||||
|
||||
// For partial thinking tokens (streaming)
|
||||
if (hasPartialThinkingTokens(content)) {
|
||||
// Find the last opening tag position
|
||||
const lastThinkPos = content.lastIndexOf('<think>');
|
||||
const lastThinkingPos = content.lastIndexOf('<thinking>');
|
||||
|
||||
const lastThinkPos = content.lastIndexOf("<think>");
|
||||
const lastThinkingPos = content.lastIndexOf("<thinking>");
|
||||
|
||||
// Use the position of whichever tag appears last
|
||||
const startPos = Math.max(lastThinkPos, lastThinkingPos);
|
||||
|
||||
|
||||
if (startPos >= 0) {
|
||||
// Extract everything from the last opening tag to the end
|
||||
return content.substring(startPos);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if thinking tokens are complete
|
||||
*/
|
||||
export function isThinkingComplete(content: string | JSX.Element): boolean {
|
||||
if (typeof content !== 'string') return false;
|
||||
|
||||
if (typeof content !== "string") return false;
|
||||
|
||||
// Count opening and closing tags
|
||||
const thinkOpenCount = (content.match(/<think>/g) || []).length;
|
||||
const thinkCloseCount = (content.match(/<\/think>/g) || []).length;
|
||||
const thinkingOpenCount = (content.match(/<thinking>/g) || []).length;
|
||||
const thinkingCloseCount = (content.match(/<\/thinking>/g) || []).length;
|
||||
|
||||
|
||||
// All tags must be matched
|
||||
return thinkOpenCount === thinkCloseCount && thinkingOpenCount === thinkingCloseCount;
|
||||
return (
|
||||
thinkOpenCount === thinkCloseCount &&
|
||||
thinkingOpenCount === thinkingCloseCount
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove thinking tokens from content
|
||||
*/
|
||||
export function removeThinkingTokens(content: string | JSX.Element): string | JSX.Element {
|
||||
if (typeof content !== 'string') return content;
|
||||
|
||||
export function removeThinkingTokens(
|
||||
content: string | JSX.Element
|
||||
): string | JSX.Element {
|
||||
if (typeof content !== "string") return content;
|
||||
|
||||
// First, remove complete thinking blocks
|
||||
let result = content.replace(/<think>[\s\S]*?<\/think>/g, '');
|
||||
result = result.replace(/<thinking>[\s\S]*?<\/thinking>/g, '');
|
||||
|
||||
let result = content.replace(/<think>[\s\S]*?<\/think>/g, "");
|
||||
result = result.replace(/<thinking>[\s\S]*?<\/thinking>/g, "");
|
||||
|
||||
// Handle case where there's an incomplete thinking token at the end
|
||||
if (hasPartialThinkingTokens(result)) {
|
||||
// Find the last opening tag position
|
||||
const lastThinkPos = result.lastIndexOf('<think>');
|
||||
const lastThinkingPos = result.lastIndexOf('<thinking>');
|
||||
|
||||
const lastThinkPos = result.lastIndexOf("<think>");
|
||||
const lastThinkingPos = result.lastIndexOf("<thinking>");
|
||||
|
||||
// Use the position of whichever tag appears last
|
||||
const startPos = Math.max(lastThinkPos, lastThinkingPos);
|
||||
|
||||
|
||||
if (startPos >= 0) {
|
||||
// Only keep content before the last opening tag
|
||||
result = result.substring(0, startPos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
@@ -114,9 +128,9 @@ export function removeThinkingTokens(content: string | JSX.Element): string | JS
|
||||
// * Clean the extracted thinking content (remove tags)
|
||||
// */
|
||||
export function cleanThinkingContent(thinkingContent: string): string {
|
||||
if (!thinkingContent) return '';
|
||||
|
||||
if (!thinkingContent) return "";
|
||||
|
||||
return thinkingContent
|
||||
.replace(/<think>|<\/think>|<thinking>|<\/thinking>/g, '')
|
||||
.replace(/<think>|<\/think>|<thinking>|<\/thinking>/g, "")
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
buildLatestMessageChain,
|
||||
getCitedDocumentsFromMessage,
|
||||
processRawChatHistory,
|
||||
} from "../../lib";
|
||||
} from "../../services/lib";
|
||||
import { AIMessage, HumanMessage } from "../../message/Messages";
|
||||
import { AgenticMessage } from "../../message/AgenticMessage";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
@@ -15,14 +15,12 @@ import { useContext, useEffect, useState } from "react";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
|
||||
import TextView from "@/components/chat/TextView";
|
||||
import { DocumentResults } from "../../documentSidebar/DocumentResults";
|
||||
import { DocumentResults } from "../../components/documentSidebar/DocumentResults";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import FunctionalHeader from "@/components/chat/Header";
|
||||
import FixedLogo from "@/components/logo/FixedLogo";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
function BackToOnyxButton({
|
||||
|
||||
672
web/src/app/chat/stores/useChatSessionStore.ts
Normal file
672
web/src/app/chat/stores/useChatSessionStore.ts
Normal file
@@ -0,0 +1,672 @@
|
||||
import { create } from "zustand";
|
||||
import {
|
||||
ChatState,
|
||||
RegenerationState,
|
||||
Message,
|
||||
ChatSessionSharedStatus,
|
||||
BackendChatSession,
|
||||
} from "../interfaces";
|
||||
import {
|
||||
getLatestMessageChain,
|
||||
MessageTreeState,
|
||||
} from "../services/messageTree";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface ChatSessionData {
|
||||
sessionId: string;
|
||||
messageTree: MessageTreeState;
|
||||
chatState: ChatState;
|
||||
regenerationState: RegenerationState | null;
|
||||
canContinue: boolean;
|
||||
submittedMessage: string;
|
||||
maxTokens: number;
|
||||
chatSessionSharedStatus: ChatSessionSharedStatus;
|
||||
selectedMessageForDocDisplay: number | null;
|
||||
abortController: AbortController;
|
||||
hasPerformedInitialScroll: boolean;
|
||||
documentSidebarVisible: boolean;
|
||||
hasSentLocalUserMessage: boolean;
|
||||
|
||||
// Session-specific state (previously global)
|
||||
isFetchingChatMessages: boolean;
|
||||
agenticGenerating: boolean;
|
||||
uncaughtError: string | null;
|
||||
loadingError: string | null;
|
||||
isReady: boolean;
|
||||
|
||||
// Session metadata
|
||||
lastAccessed: Date;
|
||||
isLoaded: boolean;
|
||||
description?: string;
|
||||
personaId?: number;
|
||||
}
|
||||
|
||||
interface ChatSessionStore {
|
||||
// Session management
|
||||
currentSessionId: string | null;
|
||||
sessions: Map<string, ChatSessionData>;
|
||||
|
||||
// Actions - Session Management
|
||||
setCurrentSession: (sessionId: string | null) => void;
|
||||
createSession: (
|
||||
sessionId: string,
|
||||
initialData?: Partial<ChatSessionData>
|
||||
) => void;
|
||||
updateSessionData: (
|
||||
sessionId: string,
|
||||
updates: Partial<ChatSessionData>
|
||||
) => void;
|
||||
updateSessionMessageTree: (
|
||||
sessionId: string,
|
||||
messageTree: MessageTreeState
|
||||
) => void;
|
||||
updateSessionAndMessageTree: (
|
||||
sessionId: string,
|
||||
messageTree: MessageTreeState
|
||||
) => void;
|
||||
|
||||
// Actions - Message Management
|
||||
updateChatState: (sessionId: string, chatState: ChatState) => void;
|
||||
updateRegenerationState: (
|
||||
sessionId: string,
|
||||
state: RegenerationState | null
|
||||
) => void;
|
||||
updateCanContinue: (sessionId: string, canContinue: boolean) => void;
|
||||
updateSubmittedMessage: (sessionId: string, message: string) => void;
|
||||
updateSelectedMessageForDocDisplay: (
|
||||
sessionId: string,
|
||||
selectedMessageForDocDisplay: number | null
|
||||
) => void;
|
||||
updateHasPerformedInitialScroll: (
|
||||
sessionId: string,
|
||||
hasPerformedInitialScroll: boolean
|
||||
) => void;
|
||||
updateDocumentSidebarVisible: (
|
||||
sessionId: string,
|
||||
documentSidebarVisible: boolean
|
||||
) => void;
|
||||
updateCurrentDocumentSidebarVisible: (
|
||||
documentSidebarVisible: boolean
|
||||
) => void;
|
||||
updateHasSentLocalUserMessage: (
|
||||
sessionId: string,
|
||||
hasSentLocalUserMessage: boolean
|
||||
) => void;
|
||||
updateCurrentHasSentLocalUserMessage: (
|
||||
hasSentLocalUserMessage: boolean
|
||||
) => void;
|
||||
|
||||
// Convenience functions that automatically use current session ID
|
||||
updateCurrentSelectedMessageForDocDisplay: (
|
||||
selectedMessageForDocDisplay: number | null
|
||||
) => void;
|
||||
updateCurrentChatSessionSharedStatus: (
|
||||
chatSessionSharedStatus: ChatSessionSharedStatus
|
||||
) => void;
|
||||
updateCurrentChatState: (chatState: ChatState) => void;
|
||||
updateCurrentRegenerationState: (
|
||||
regenerationState: RegenerationState | null
|
||||
) => void;
|
||||
updateCurrentCanContinue: (canContinue: boolean) => void;
|
||||
updateCurrentSubmittedMessage: (submittedMessage: string) => void;
|
||||
|
||||
// Actions - Session-specific State (previously global)
|
||||
setIsFetchingChatMessages: (sessionId: string, fetching: boolean) => void;
|
||||
setAgenticGenerating: (sessionId: string, generating: boolean) => void;
|
||||
setUncaughtError: (sessionId: string, error: string | null) => void;
|
||||
setLoadingError: (sessionId: string, error: string | null) => void;
|
||||
setIsReady: (sessionId: string, ready: boolean) => void;
|
||||
|
||||
// Actions - Abort Controllers
|
||||
setAbortController: (sessionId: string, controller: AbortController) => void;
|
||||
abortSession: (sessionId: string) => void;
|
||||
abortAllSessions: () => void;
|
||||
|
||||
// Utilities
|
||||
initializeSession: (
|
||||
sessionId: string,
|
||||
backendSession?: BackendChatSession
|
||||
) => void;
|
||||
cleanupOldSessions: (maxSessions?: number) => void;
|
||||
}
|
||||
|
||||
const createInitialSessionData = (
|
||||
sessionId: string,
|
||||
initialData?: Partial<ChatSessionData>
|
||||
): ChatSessionData => ({
|
||||
sessionId,
|
||||
messageTree: new Map<number, Message>(),
|
||||
chatState: "input" as ChatState,
|
||||
regenerationState: null,
|
||||
canContinue: false,
|
||||
submittedMessage: "",
|
||||
maxTokens: 128_000,
|
||||
chatSessionSharedStatus: ChatSessionSharedStatus.Private,
|
||||
selectedMessageForDocDisplay: null,
|
||||
abortController: new AbortController(),
|
||||
hasPerformedInitialScroll: true,
|
||||
documentSidebarVisible: false,
|
||||
hasSentLocalUserMessage: false,
|
||||
|
||||
// Session-specific state defaults
|
||||
isFetchingChatMessages: false,
|
||||
agenticGenerating: false,
|
||||
uncaughtError: null,
|
||||
loadingError: null,
|
||||
isReady: true,
|
||||
|
||||
lastAccessed: new Date(),
|
||||
isLoaded: false,
|
||||
...initialData,
|
||||
});
|
||||
|
||||
export const useChatSessionStore = create<ChatSessionStore>()((set, get) => ({
|
||||
// Initial state
|
||||
currentSessionId: null,
|
||||
sessions: new Map<string, ChatSessionData>(),
|
||||
|
||||
// Session Management Actions
|
||||
setCurrentSession: (sessionId: string | null) => {
|
||||
set((state) => {
|
||||
if (sessionId && !state.sessions.has(sessionId)) {
|
||||
// Create new session if it doesn't exist
|
||||
const newSession = createInitialSessionData(sessionId);
|
||||
const newSessions = new Map(state.sessions);
|
||||
newSessions.set(sessionId, newSession);
|
||||
|
||||
return {
|
||||
currentSessionId: sessionId,
|
||||
sessions: newSessions,
|
||||
};
|
||||
}
|
||||
|
||||
// Update last accessed for the new current session
|
||||
if (sessionId && state.sessions.has(sessionId)) {
|
||||
const session = state.sessions.get(sessionId)!;
|
||||
const updatedSession = { ...session, lastAccessed: new Date() };
|
||||
const newSessions = new Map(state.sessions);
|
||||
newSessions.set(sessionId, updatedSession);
|
||||
|
||||
return {
|
||||
currentSessionId: sessionId,
|
||||
sessions: newSessions,
|
||||
};
|
||||
}
|
||||
|
||||
return { currentSessionId: sessionId };
|
||||
});
|
||||
},
|
||||
|
||||
createSession: (
|
||||
sessionId: string,
|
||||
initialData?: Partial<ChatSessionData>
|
||||
) => {
|
||||
set((state) => {
|
||||
const newSession = createInitialSessionData(sessionId, initialData);
|
||||
const newSessions = new Map(state.sessions);
|
||||
newSessions.set(sessionId, newSession);
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
updateSessionData: (sessionId: string, updates: Partial<ChatSessionData>) => {
|
||||
set((state) => {
|
||||
const session = state.sessions.get(sessionId);
|
||||
const updatedSession = {
|
||||
...(session || createInitialSessionData(sessionId)),
|
||||
...updates,
|
||||
lastAccessed: new Date(),
|
||||
};
|
||||
const newSessions = new Map(state.sessions);
|
||||
newSessions.set(sessionId, updatedSession);
|
||||
|
||||
return { sessions: newSessions };
|
||||
});
|
||||
},
|
||||
|
||||
updateSessionMessageTree: (
|
||||
sessionId: string,
|
||||
messageTree: MessageTreeState
|
||||
) => {
|
||||
console.log("updateSessionMessageTree", sessionId, messageTree);
|
||||
get().updateSessionData(sessionId, { messageTree });
|
||||
},
|
||||
|
||||
updateSessionAndMessageTree: (
|
||||
sessionId: string,
|
||||
messageTree: MessageTreeState
|
||||
) => {
|
||||
set((state) => {
|
||||
// Ensure session exists
|
||||
const existingSession = state.sessions.get(sessionId);
|
||||
const session = existingSession || createInitialSessionData(sessionId);
|
||||
|
||||
// Update session with new message tree
|
||||
const updatedSession = {
|
||||
...session,
|
||||
messageTree,
|
||||
lastAccessed: new Date(),
|
||||
};
|
||||
|
||||
const newSessions = new Map(state.sessions);
|
||||
newSessions.set(sessionId, updatedSession);
|
||||
|
||||
// Return both updates in a single state change
|
||||
return {
|
||||
currentSessionId: sessionId,
|
||||
sessions: newSessions,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Message Management Actions
|
||||
updateChatState: (sessionId: string, chatState: ChatState) => {
|
||||
get().updateSessionData(sessionId, { chatState });
|
||||
},
|
||||
|
||||
updateRegenerationState: (
|
||||
sessionId: string,
|
||||
regenerationState: RegenerationState | null
|
||||
) => {
|
||||
get().updateSessionData(sessionId, { regenerationState });
|
||||
},
|
||||
|
||||
updateCanContinue: (sessionId: string, canContinue: boolean) => {
|
||||
get().updateSessionData(sessionId, { canContinue });
|
||||
},
|
||||
|
||||
updateSubmittedMessage: (sessionId: string, submittedMessage: string) => {
|
||||
get().updateSessionData(sessionId, { submittedMessage });
|
||||
},
|
||||
|
||||
updateSelectedMessageForDocDisplay: (
|
||||
sessionId: string,
|
||||
selectedMessageForDocDisplay: number | null
|
||||
) => {
|
||||
get().updateSessionData(sessionId, { selectedMessageForDocDisplay });
|
||||
},
|
||||
|
||||
updateHasPerformedInitialScroll: (
|
||||
sessionId: string,
|
||||
hasPerformedInitialScroll: boolean
|
||||
) => {
|
||||
get().updateSessionData(sessionId, { hasPerformedInitialScroll });
|
||||
},
|
||||
|
||||
updateDocumentSidebarVisible: (
|
||||
sessionId: string,
|
||||
documentSidebarVisible: boolean
|
||||
) => {
|
||||
get().updateSessionData(sessionId, { documentSidebarVisible });
|
||||
},
|
||||
|
||||
updateCurrentDocumentSidebarVisible: (documentSidebarVisible: boolean) => {
|
||||
const { currentSessionId } = get();
|
||||
if (currentSessionId) {
|
||||
get().updateDocumentSidebarVisible(
|
||||
currentSessionId,
|
||||
documentSidebarVisible
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
updateHasSentLocalUserMessage: (
|
||||
sessionId: string,
|
||||
hasSentLocalUserMessage: boolean
|
||||
) => {
|
||||
get().updateSessionData(sessionId, { hasSentLocalUserMessage });
|
||||
},
|
||||
|
||||
updateCurrentHasSentLocalUserMessage: (hasSentLocalUserMessage: boolean) => {
|
||||
const { currentSessionId } = get();
|
||||
if (currentSessionId) {
|
||||
get().updateHasSentLocalUserMessage(
|
||||
currentSessionId,
|
||||
hasSentLocalUserMessage
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Convenience functions that automatically use current session ID
|
||||
updateCurrentSelectedMessageForDocDisplay: (
|
||||
selectedMessageForDocDisplay: number | null
|
||||
) => {
|
||||
const { currentSessionId } = get();
|
||||
if (currentSessionId) {
|
||||
get().updateSelectedMessageForDocDisplay(
|
||||
currentSessionId,
|
||||
selectedMessageForDocDisplay
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentChatSessionSharedStatus: (
|
||||
chatSessionSharedStatus: ChatSessionSharedStatus
|
||||
) => {
|
||||
const { currentSessionId } = get();
|
||||
if (currentSessionId) {
|
||||
get().updateSessionData(currentSessionId, { chatSessionSharedStatus });
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentChatState: (chatState: ChatState) => {
|
||||
const { currentSessionId } = get();
|
||||
if (currentSessionId) {
|
||||
get().updateChatState(currentSessionId, chatState);
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentRegenerationState: (
|
||||
regenerationState: RegenerationState | null
|
||||
) => {
|
||||
const { currentSessionId } = get();
|
||||
if (currentSessionId) {
|
||||
get().updateRegenerationState(currentSessionId, regenerationState);
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentCanContinue: (canContinue: boolean) => {
|
||||
const { currentSessionId } = get();
|
||||
if (currentSessionId) {
|
||||
get().updateCanContinue(currentSessionId, canContinue);
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentSubmittedMessage: (submittedMessage: string) => {
|
||||
const { currentSessionId } = get();
|
||||
if (currentSessionId) {
|
||||
get().updateSubmittedMessage(currentSessionId, submittedMessage);
|
||||
}
|
||||
},
|
||||
|
||||
// Session-specific State Actions (previously global)
|
||||
setIsFetchingChatMessages: (
|
||||
sessionId: string,
|
||||
isFetchingChatMessages: boolean
|
||||
) => {
|
||||
get().updateSessionData(sessionId, { isFetchingChatMessages });
|
||||
},
|
||||
|
||||
setAgenticGenerating: (sessionId: string, agenticGenerating: boolean) => {
|
||||
get().updateSessionData(sessionId, { agenticGenerating });
|
||||
},
|
||||
|
||||
setUncaughtError: (sessionId: string, uncaughtError: string | null) => {
|
||||
get().updateSessionData(sessionId, { uncaughtError });
|
||||
},
|
||||
|
||||
setLoadingError: (sessionId: string, loadingError: string | null) => {
|
||||
get().updateSessionData(sessionId, { loadingError });
|
||||
},
|
||||
|
||||
setIsReady: (sessionId: string, isReady: boolean) => {
|
||||
get().updateSessionData(sessionId, { isReady });
|
||||
},
|
||||
|
||||
// Abort Controller Actions
|
||||
setAbortController: (sessionId: string, controller: AbortController) => {
|
||||
get().updateSessionData(sessionId, { abortController: controller });
|
||||
},
|
||||
|
||||
abortSession: (sessionId: string) => {
|
||||
const session = get().sessions.get(sessionId);
|
||||
if (session?.abortController) {
|
||||
session.abortController.abort();
|
||||
get().updateSessionData(sessionId, {
|
||||
abortController: new AbortController(),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
abortAllSessions: () => {
|
||||
const { sessions } = get();
|
||||
sessions.forEach((session, sessionId) => {
|
||||
if (session.abortController) {
|
||||
session.abortController.abort();
|
||||
get().updateSessionData(sessionId, {
|
||||
abortController: new AbortController(),
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Utilities
|
||||
initializeSession: (
|
||||
sessionId: string,
|
||||
backendSession?: BackendChatSession
|
||||
) => {
|
||||
const initialData: Partial<ChatSessionData> = {
|
||||
isLoaded: true,
|
||||
description: backendSession?.description,
|
||||
personaId: backendSession?.persona_id,
|
||||
};
|
||||
|
||||
const existingSession = get().sessions.get(sessionId);
|
||||
if (existingSession) {
|
||||
get().updateSessionData(sessionId, initialData);
|
||||
} else {
|
||||
get().createSession(sessionId, initialData);
|
||||
}
|
||||
},
|
||||
|
||||
cleanupOldSessions: (maxSessions: number = 10) => {
|
||||
set((state) => {
|
||||
const sortedSessions = Array.from(state.sessions.entries()).sort(
|
||||
([, a], [, b]) => b.lastAccessed.getTime() - a.lastAccessed.getTime()
|
||||
);
|
||||
|
||||
if (sortedSessions.length <= maxSessions) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const sessionsToKeep = sortedSessions.slice(0, maxSessions);
|
||||
const sessionsToRemove = sortedSessions.slice(maxSessions);
|
||||
|
||||
// Abort controllers for sessions being removed
|
||||
sessionsToRemove.forEach(([, session]) => {
|
||||
if (session.abortController) {
|
||||
session.abortController.abort();
|
||||
}
|
||||
});
|
||||
|
||||
const newSessions = new Map(sessionsToKeep);
|
||||
|
||||
return {
|
||||
sessions: newSessions,
|
||||
};
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
// Custom hooks for accessing store data
|
||||
export const useCurrentSession = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
return currentSessionId ? sessions.get(currentSessionId) || null : null;
|
||||
});
|
||||
|
||||
export const useSession = (sessionId: string) =>
|
||||
useChatSessionStore((state) => state.sessions.get(sessionId) || null);
|
||||
|
||||
export const useCurrentMessageTree = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.messageTree;
|
||||
});
|
||||
|
||||
export const useCurrentMessageHistory = () => {
|
||||
const messageTree = useCurrentMessageTree();
|
||||
return useMemo(() => {
|
||||
if (!messageTree) {
|
||||
return [];
|
||||
}
|
||||
return getLatestMessageChain(messageTree);
|
||||
}, [messageTree]);
|
||||
};
|
||||
|
||||
export const useCurrentChatState = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.chatState || "input";
|
||||
});
|
||||
|
||||
export const useCurrentRegenerationState = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.regenerationState || null;
|
||||
});
|
||||
|
||||
export const useCanContinue = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.canContinue || false;
|
||||
});
|
||||
|
||||
export const useSubmittedMessage = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.submittedMessage || "";
|
||||
});
|
||||
|
||||
export const useRegenerationState = (sessionId: string) =>
|
||||
useChatSessionStore((state) => {
|
||||
const session = state.sessions.get(sessionId);
|
||||
return session?.regenerationState || null;
|
||||
});
|
||||
|
||||
export const useAbortController = (sessionId: string) =>
|
||||
useChatSessionStore((state) => {
|
||||
const session = state.sessions.get(sessionId);
|
||||
return session?.abortController || null;
|
||||
});
|
||||
|
||||
export const useAbortControllers = () => {
|
||||
const sessions = useChatSessionStore((state) => state.sessions);
|
||||
return useMemo(() => {
|
||||
const controllers = new Map<string, AbortController>();
|
||||
sessions.forEach((session: ChatSessionData) => {
|
||||
if (session.abortController) {
|
||||
controllers.set(session.sessionId, session.abortController);
|
||||
}
|
||||
});
|
||||
return controllers;
|
||||
}, [sessions]);
|
||||
};
|
||||
|
||||
// Session-specific state hooks (previously global)
|
||||
export const useAgenticGenerating = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.agenticGenerating || false;
|
||||
});
|
||||
|
||||
export const useIsFetching = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.isFetchingChatMessages || false;
|
||||
});
|
||||
|
||||
export const useUncaughtError = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.uncaughtError || null;
|
||||
});
|
||||
|
||||
export const useLoadingError = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.loadingError || null;
|
||||
});
|
||||
|
||||
export const useIsReady = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.isReady ?? true;
|
||||
});
|
||||
|
||||
export const useMaxTokens = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.maxTokens || 128_000;
|
||||
});
|
||||
|
||||
export const useHasPerformedInitialScroll = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.hasPerformedInitialScroll || true;
|
||||
});
|
||||
|
||||
export const useDocumentSidebarVisible = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.documentSidebarVisible || false;
|
||||
});
|
||||
|
||||
export const useSelectedMessageForDocDisplay = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.selectedMessageForDocDisplay || null;
|
||||
});
|
||||
|
||||
export const useChatSessionSharedStatus = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return (
|
||||
currentSession?.chatSessionSharedStatus || ChatSessionSharedStatus.Private
|
||||
);
|
||||
});
|
||||
|
||||
export const useHasSentLocalUserMessage = () =>
|
||||
useChatSessionStore((state) => {
|
||||
const { currentSessionId, sessions } = state;
|
||||
const currentSession = currentSessionId
|
||||
? sessions.get(currentSessionId)
|
||||
: null;
|
||||
return currentSession?.hasSentLocalUserMessage || false;
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
export type FeedbackType = "like" | "dislike";
|
||||
export type ChatState =
|
||||
| "input"
|
||||
| "loading"
|
||||
| "streaming"
|
||||
| "toolBuilding"
|
||||
| "uploading";
|
||||
export interface RegenerationState {
|
||||
regenerating: boolean;
|
||||
finalMessageIndex: number;
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
usePersonaMessages,
|
||||
usePersonaUniqueUsers,
|
||||
} from "../lib";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { DateRangePickerValue } from "@/components/dateRangeSelectors/AdminDateRangeSelector";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import Text from "@/components/ui/text";
|
||||
import Title from "@/components/ui/title";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { DateRangePickerValue } from "@/components/dateRangeSelectors/AdminDateRangeSelector";
|
||||
|
||||
export function PersonaMessagesChart({
|
||||
timeRange,
|
||||
@@ -30,7 +30,7 @@ export function PersonaMessagesChart({
|
||||
>(undefined);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||
const { allAssistants: personaList } = useAssistants();
|
||||
const { allAssistants: personaList } = useAssistantsContext();
|
||||
|
||||
const {
|
||||
data: personaMessagesData,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
AdminDateRangeSelector,
|
||||
DateRange,
|
||||
} from "@/components/dateRangeSelectors/AdminDateRangeSelector";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useAssistantsContext } from "@/components/context/AssistantsContext";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { AreaChartDisplay } from "@/components/ui/areaChart";
|
||||
@@ -26,7 +26,7 @@ type AssistantStatsResponse = {
|
||||
export function AssistantStats({ assistantId }: { assistantId: number }) {
|
||||
const [assistantStats, setAssistantStats] =
|
||||
useState<AssistantStatsResponse | null>(null);
|
||||
const { assistants } = useAssistants();
|
||||
const { assistants } = useAssistantsContext();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dateRange, setDateRange] = useState<DateRange>({
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Popover } from "./popover/Popover";
|
||||
import { LOGOUT_DISABLED } from "@/lib/constants";
|
||||
import { SettingsContext } from "./settings/SettingsProvider";
|
||||
import { BellIcon, LightSettingsIcon, UserIcon } from "./icons/icons";
|
||||
import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||
import { pageType } from "@/app/chat/components/sessionSidebar/types";
|
||||
import { NavigationItem, Notification } from "@/app/admin/settings/interfaces";
|
||||
import DynamicFaIcon, { preloadIcons } from "./icons/DynamicFaIcon";
|
||||
import { useUser } from "./user/UserProvider";
|
||||
|
||||
@@ -30,7 +30,7 @@ import { usePathname } from "next/navigation";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { useContext, useState } from "react";
|
||||
import { MdOutlineCreditCard } from "react-icons/md";
|
||||
import { UserSettingsModal } from "@/app/chat/modal/UserSettingsModal";
|
||||
import { UserSettingsModal } from "@/app/chat/components/modal/UserSettingsModal";
|
||||
import { usePopup } from "./connectors/Popup";
|
||||
import { useChatContext } from "../context/ChatContext";
|
||||
import {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { FiImage, FiSearch } from "react-icons/fi";
|
||||
import { MdDragIndicator } from "react-icons/md";
|
||||
|
||||
import { Badge } from "../ui/badge";
|
||||
import { IIMAGE_GENERATION_TOOL_ID } from "@/app/chat/tools/constants";
|
||||
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
|
||||
import { IIMAGE_GENERATION_TOOL_ID } from "@/app/chat/components/tools/constants";
|
||||
import { SEARCH_TOOL_ID } from "@/app/chat/components/tools/constants";
|
||||
|
||||
export const AssistantCard = ({
|
||||
assistant,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import crypto from "crypto";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { buildImgUrl } from "@/app/chat/files/images/utils";
|
||||
import { buildImgUrl } from "@/app/chat/components/files/images/utils";
|
||||
import {
|
||||
ArtAsistantIcon,
|
||||
GeneralAssistantIcon,
|
||||
SearchAssistantIcon,
|
||||
} from "../icons/icons";
|
||||
} from "@/components/icons/icons";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
||||
@@ -4,9 +4,9 @@ import { FiShare2 } from "react-icons/fi";
|
||||
import { SetStateAction, useContext, useEffect } from "react";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import Link from "next/link";
|
||||
import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||
import { pageType } from "@/app/chat/components/sessionSidebar/types";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChatBanner } from "@/app/chat/ChatBanner";
|
||||
import { ChatBanner } from "@/app/chat/components/ChatBanner";
|
||||
import LogoWithText from "../header/LogoWithText";
|
||||
import { NewChatIcon } from "../icons/icons";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
NotificationType,
|
||||
} from "@/app/admin/settings/interfaces";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { useAssistants } from "../context/AssistantsContext";
|
||||
import { useAssistantsContext } from "../context/AssistantsContext";
|
||||
import { useUser } from "../user/UserProvider";
|
||||
import { XIcon } from "../icons/icons";
|
||||
import { Spinner } from "@phosphor-icons/react";
|
||||
@@ -22,7 +22,7 @@ export const Notifications = ({
|
||||
}) => {
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const router = useRouter();
|
||||
const { refreshAssistants } = useAssistants();
|
||||
const { refreshAssistants } = useAssistantsContext();
|
||||
|
||||
const { refreshUser } = useUser();
|
||||
const [personas, setPersonas] = useState<Record<number, Persona> | undefined>(
|
||||
|
||||
@@ -201,7 +201,7 @@ export const AssistantsProvider: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
export const useAssistants = (): AssistantsContextProps => {
|
||||
export const useAssistantsContext = (): AssistantsContextProps => {
|
||||
const context = useContext(AssistantsContext);
|
||||
if (!context) {
|
||||
throw new Error("useAssistants must be used within an AssistantsProvider");
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { createContext, useContext, useState } from "react";
|
||||
import { CCPairBasicInfo, DocumentSet, Tag, ValidSources } from "@/lib/types";
|
||||
import { ChatSession, InputPrompt } from "@/app/chat/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { Folder } from "@/app/chat/components/folders/interfaces";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { pageType } from "@/app/chat/sessionSidebar/types";
|
||||
import { pageType } from "@/app/chat/components/sessionSidebar/types";
|
||||
import { Logo } from "../logo/Logo";
|
||||
import Link from "next/link";
|
||||
import { LogoComponent } from "@/components/logo/FixedLogo";
|
||||
|
||||
23
web/src/hooks/useScreenSize.ts
Normal file
23
web/src/hooks/useScreenSize.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function useScreenSize() {
|
||||
const [screenSize, setScreenSize] = useState({
|
||||
width: typeof window !== "undefined" ? window.innerWidth : 0,
|
||||
height: typeof window !== "undefined" ? window.innerHeight : 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setScreenSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
return screenSize;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { FullEmbeddingModelResponse } from "@/components/embedding/interfaces";
|
||||
import { Settings } from "@/app/admin/settings/interfaces";
|
||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { Folder } from "@/app/chat/components/folders/interfaces";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import {
|
||||
SIDEBAR_TOGGLED_COOKIE_NAME,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { Folder } from "@/app/chat/components/folders/interfaces";
|
||||
import { personaComparator } from "@/app/admin/assistants/lib";
|
||||
import { cookies } from "next/headers";
|
||||
import {
|
||||
|
||||
@@ -28,8 +28,8 @@ import { isAnthropic } from "@/app/admin/configuration/llm/utils";
|
||||
import { getSourceMetadata } from "./sources";
|
||||
import { AuthType, NEXT_PUBLIC_CLOUD_ENABLED } from "./constants";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
|
||||
import { updateTemperatureOverrideForChatSession } from "@/app/chat/lib";
|
||||
import { SEARCH_TOOL_ID } from "@/app/chat/components/tools/constants";
|
||||
import { updateTemperatureOverrideForChatSession } from "@/app/chat/services/lib";
|
||||
|
||||
const CREDENTIAL_URL = "/api/manage/admin/credential";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PacketType } from "@/app/chat/lib";
|
||||
import { PacketType } from "@/app/chat/services/lib";
|
||||
|
||||
type NonEmptyObject = { [k: string]: any };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user