Compare commits

...

3 Commits

Author SHA1 Message Date
Weves
fe69948964 more refactor 2025-06-02 11:27:19 -07:00
Weves
f9faab6690 Use zustand 2025-05-25 19:03:17 -07:00
Weves
fd7c0a3b17 initial refactor
more

rebase

remove console.log
2025-05-24 16:19:28 -07:00
96 changed files with 5007 additions and 4088 deletions

View File

@@ -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();

View File

@@ -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()));

View File

@@ -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 = ({

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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();

View File

@@ -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[]>([]);

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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({

View File

@@ -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 (

File diff suppressed because it is too large Load Diff

View File

@@ -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";

View File

@@ -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

View File

@@ -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";

View File

@@ -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";

View File

@@ -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 = ({

View File

@@ -1,4 +1,4 @@
import { ChatSession } from "../interfaces";
import { ChatSession } from "@/app/chat/interfaces";
export interface Folder {
folder_id?: number;

View File

@@ -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();

View File

@@ -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>

View File

@@ -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";

View File

@@ -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>
</>

View File

@@ -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";

View File

@@ -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";

View File

@@ -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(() => {

View File

@@ -1,5 +1,5 @@
import { useRouter } from "next/router";
import { ChatSession } from "../interfaces";
import { ChatSession } from "@/app/chat/interfaces";
export const ChatGroup = ({
groupName,

View File

@@ -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";

View File

@@ -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;

View File

@@ -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,

View 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,
};
}

File diff suppressed because it is too large Load Diff

View 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,
};
}

View File

@@ -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;

View File

@@ -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[]>([]);

View File

@@ -1,4 +1,4 @@
import { SourceChip } from "../input/ChatInputBar";
import { SourceChip } from "../components/input/ChatInputBar";
import { useEffect } from "react";
import { useState } from "react";

View File

@@ -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;
};

View File

@@ -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";

View File

@@ -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 = [

View File

@@ -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";

View File

@@ -14,7 +14,7 @@ import {
cleanThinkingContent,
hasPartialThinkingTokens,
isThinkingComplete,
} from "../../utils/thinkingTokens";
} from "../../services/thinkingTokens";
import "./ThinkingBox.css";
interface ThinkingBoxProps {

View File

@@ -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>
);
}

View File

@@ -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";

View 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;
};

View 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;
}
}

View File

@@ -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[],

View 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
}

View File

@@ -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();
}
}

View File

@@ -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({

View 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;
});

View File

@@ -1,11 +0,0 @@
export type FeedbackType = "like" | "dislike";
export type ChatState =
| "input"
| "loading"
| "streaming"
| "toolBuilding"
| "uploading";
export interface RegenerationState {
regenerating: boolean;
finalMessageIndex: number;
}

View File

@@ -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,

View File

@@ -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>({

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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";

View File

@@ -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>(

View File

@@ -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");

View File

@@ -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";

View File

@@ -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";

View 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;
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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";

View File

@@ -1,4 +1,4 @@
import { PacketType } from "@/app/chat/lib";
import { PacketType } from "@/app/chat/services/lib";
type NonEmptyObject = { [k: string]: any };