update chat page to use a new filter for every conversation

This commit is contained in:
Cole Goldsmith 2025-11-19 16:23:32 -06:00
parent 230081e591
commit 15701f8b16
2 changed files with 1620 additions and 1558 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,255 +1,304 @@
"use client"; "use client";
import { import {
createContext, createContext,
ReactNode, type ReactNode,
useCallback, useCallback,
useContext, useContext,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
export type EndpointType = "chat" | "langflow"; export type EndpointType = "chat" | "langflow";
interface ConversationDocument { interface ConversationDocument {
filename: string; filename: string;
uploadTime: Date; uploadTime: Date;
} }
interface ConversationMessage { interface ConversationMessage {
role: string; role: string;
content: string; content: string;
timestamp?: string; timestamp?: string;
response_id?: string; response_id?: string;
}
interface KnowledgeFilter {
id: string;
name: string;
description: string;
query_data: string;
owner: string;
created_at: string;
updated_at: string;
} }
interface ConversationData { interface ConversationData {
messages: ConversationMessage[]; messages: ConversationMessage[];
endpoint: EndpointType; endpoint: EndpointType;
response_id: string; response_id: string;
title: string; title: string;
[key: string]: unknown; filter?: KnowledgeFilter | null;
[key: string]: unknown;
} }
interface ChatContextType { interface ChatContextType {
endpoint: EndpointType; endpoint: EndpointType;
setEndpoint: (endpoint: EndpointType) => void; setEndpoint: (endpoint: EndpointType) => void;
currentConversationId: string | null; currentConversationId: string | null;
setCurrentConversationId: (id: string | null) => void; setCurrentConversationId: (id: string | null) => void;
previousResponseIds: { previousResponseIds: {
chat: string | null; chat: string | null;
langflow: string | null; langflow: string | null;
}; };
setPreviousResponseIds: ( setPreviousResponseIds: (
ids: ids:
| { chat: string | null; langflow: string | null } | { chat: string | null; langflow: string | null }
| ((prev: { chat: string | null; langflow: string | null }) => { | ((prev: { chat: string | null; langflow: string | null }) => {
chat: string | null; chat: string | null;
langflow: string | null; langflow: string | null;
}), }),
) => void; ) => void;
refreshConversations: (force?: boolean) => void; refreshConversations: (force?: boolean) => void;
refreshConversationsSilent: () => Promise<void>; refreshConversationsSilent: () => Promise<void>;
refreshTrigger: number; refreshTrigger: number;
refreshTriggerSilent: number; refreshTriggerSilent: number;
loadConversation: (conversation: ConversationData) => void; loadConversation: (conversation: ConversationData) => void;
startNewConversation: () => void; startNewConversation: () => void;
conversationData: ConversationData | null; conversationData: ConversationData | null;
forkFromResponse: (responseId: string) => void; forkFromResponse: (responseId: string) => void;
conversationDocs: ConversationDocument[]; conversationDocs: ConversationDocument[];
addConversationDoc: (filename: string) => void; addConversationDoc: (filename: string) => void;
clearConversationDocs: () => void; clearConversationDocs: () => void;
placeholderConversation: ConversationData | null; placeholderConversation: ConversationData | null;
setPlaceholderConversation: (conversation: ConversationData | null) => void; setPlaceholderConversation: (conversation: ConversationData | null) => void;
conversationLoaded: boolean; conversationLoaded: boolean;
setConversationLoaded: (loaded: boolean) => void; setConversationLoaded: (loaded: boolean) => void;
conversationFilter: KnowledgeFilter | null;
setConversationFilter: (filter: KnowledgeFilter | null) => void;
} }
const ChatContext = createContext<ChatContextType | undefined>(undefined); const ChatContext = createContext<ChatContextType | undefined>(undefined);
interface ChatProviderProps { interface ChatProviderProps {
children: ReactNode; children: ReactNode;
} }
export function ChatProvider({ children }: ChatProviderProps) { export function ChatProvider({ children }: ChatProviderProps) {
const [endpoint, setEndpoint] = useState<EndpointType>("langflow"); const [endpoint, setEndpoint] = useState<EndpointType>("langflow");
const [currentConversationId, setCurrentConversationId] = useState< const [currentConversationId, setCurrentConversationId] = useState<
string | null string | null
>(null); >(null);
const [previousResponseIds, setPreviousResponseIds] = useState<{ const [previousResponseIds, setPreviousResponseIds] = useState<{
chat: string | null; chat: string | null;
langflow: string | null; langflow: string | null;
}>({ chat: null, langflow: null }); }>({ chat: null, langflow: null });
const [refreshTrigger, setRefreshTrigger] = useState(0); const [refreshTrigger, setRefreshTrigger] = useState(0);
const [refreshTriggerSilent, setRefreshTriggerSilent] = useState(0); const [refreshTriggerSilent, setRefreshTriggerSilent] = useState(0);
const [conversationData, setConversationData] = const [conversationData, setConversationData] =
useState<ConversationData | null>(null); useState<ConversationData | null>(null);
const [conversationDocs, setConversationDocs] = useState< const [conversationDocs, setConversationDocs] = useState<
ConversationDocument[] ConversationDocument[]
>([]); >([]);
const [placeholderConversation, setPlaceholderConversation] = const [placeholderConversation, setPlaceholderConversation] =
useState<ConversationData | null>(null); useState<ConversationData | null>(null);
const [conversationLoaded, setConversationLoaded] = useState(false); const [conversationLoaded, setConversationLoaded] = useState(false);
const [conversationFilter, setConversationFilterState] =
useState<KnowledgeFilter | null>(null);
// Debounce refresh requests to prevent excessive reloads // Debounce refresh requests to prevent excessive reloads
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null); const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const refreshConversations = useCallback((force = false) => { const refreshConversations = useCallback((force = false) => {
if (force) { if (force) {
// Immediate refresh for important updates like new conversations // Immediate refresh for important updates like new conversations
setRefreshTrigger((prev) => prev + 1); setRefreshTrigger((prev) => prev + 1);
return; return;
} }
// Clear any existing timeout // Clear any existing timeout
if (refreshTimeoutRef.current) { if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current); clearTimeout(refreshTimeoutRef.current);
} }
// Set a new timeout to debounce multiple rapid refresh calls // Set a new timeout to debounce multiple rapid refresh calls
refreshTimeoutRef.current = setTimeout(() => { refreshTimeoutRef.current = setTimeout(() => {
setRefreshTrigger((prev) => prev + 1); setRefreshTrigger((prev) => prev + 1);
}, 250); // 250ms debounce }, 250); // 250ms debounce
}, []); }, []);
// Cleanup timeout on unmount // Cleanup timeout on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
if (refreshTimeoutRef.current) { if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current); clearTimeout(refreshTimeoutRef.current);
} }
}; };
}, []); }, []);
// Silent refresh - updates data without loading states // Silent refresh - updates data without loading states
const refreshConversationsSilent = useCallback(async () => { const refreshConversationsSilent = useCallback(async () => {
// Trigger silent refresh that updates conversation data without showing loading states // Trigger silent refresh that updates conversation data without showing loading states
setRefreshTriggerSilent((prev) => prev + 1); setRefreshTriggerSilent((prev) => prev + 1);
}, []); }, []);
const loadConversation = useCallback((conversation: ConversationData) => { const loadConversation = useCallback(
setCurrentConversationId(conversation.response_id); (conversation: ConversationData) => {
setEndpoint(conversation.endpoint); setCurrentConversationId(conversation.response_id);
// Store the full conversation data for the chat page to use setEndpoint(conversation.endpoint);
setConversationData(conversation); // Store the full conversation data for the chat page to use
// Clear placeholder when loading a real conversation setConversationData(conversation);
setPlaceholderConversation(null); // Load the filter if one exists for this conversation
setConversationLoaded(true); // Only update the filter if this is a different conversation (to preserve user's filter selection)
// Clear conversation docs to prevent duplicates when switching conversations setConversationFilterState((currentFilter) => {
setConversationDocs([]); // If we're loading a different conversation, load its filter
}, []); // Otherwise keep the current filter (don't reset it when conversation refreshes)
const isDifferentConversation =
conversation.response_id !== conversationData?.response_id;
return isDifferentConversation
? conversation.filter || null
: currentFilter;
});
// Clear placeholder when loading a real conversation
setPlaceholderConversation(null);
setConversationLoaded(true);
// Clear conversation docs to prevent duplicates when switching conversations
setConversationDocs([]);
},
[conversationData?.response_id],
);
const startNewConversation = useCallback(() => { const startNewConversation = useCallback(() => {
// Clear current conversation data and reset state // Clear current conversation data and reset state
setCurrentConversationId(null); setCurrentConversationId(null);
setPreviousResponseIds({ chat: null, langflow: null }); setPreviousResponseIds({ chat: null, langflow: null });
setConversationData(null); setConversationData(null);
setConversationDocs([]); setConversationDocs([]);
setConversationLoaded(false); setConversationLoaded(false);
// Clear the filter when starting a new conversation
setConversationFilterState(null);
// Create a temporary placeholder conversation to show in sidebar // Create a temporary placeholder conversation to show in sidebar
const placeholderConversation: ConversationData = { const placeholderConversation: ConversationData = {
response_id: "new-conversation-" + Date.now(), response_id: "new-conversation-" + Date.now(),
title: "New conversation", title: "New conversation",
endpoint: endpoint, endpoint: endpoint,
messages: [ messages: [
{ {
role: "assistant", role: "assistant",
content: "How can I assist?", content: "How can I assist?",
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}, },
], ],
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
last_activity: new Date().toISOString(), last_activity: new Date().toISOString(),
}; };
setPlaceholderConversation(placeholderConversation); setPlaceholderConversation(placeholderConversation);
// Force immediate refresh to ensure sidebar shows correct state // Force immediate refresh to ensure sidebar shows correct state
refreshConversations(true); refreshConversations(true);
}, [endpoint, refreshConversations]); }, [endpoint, refreshConversations]);
const addConversationDoc = useCallback((filename: string) => { const addConversationDoc = useCallback((filename: string) => {
setConversationDocs((prev) => [ setConversationDocs((prev) => [
...prev, ...prev,
{ filename, uploadTime: new Date() }, { filename, uploadTime: new Date() },
]); ]);
}, []); }, []);
const clearConversationDocs = useCallback(() => { const clearConversationDocs = useCallback(() => {
setConversationDocs([]); setConversationDocs([]);
}, []); }, []);
const forkFromResponse = useCallback( const forkFromResponse = useCallback(
(responseId: string) => { (responseId: string) => {
// Start a new conversation with the messages up to the fork point // Start a new conversation with the messages up to the fork point
setCurrentConversationId(null); // Clear current conversation to indicate new conversation setCurrentConversationId(null); // Clear current conversation to indicate new conversation
setConversationData(null); // Clear conversation data to prevent reloading setConversationData(null); // Clear conversation data to prevent reloading
// Set the response ID that we're forking from as the previous response ID // Set the response ID that we're forking from as the previous response ID
setPreviousResponseIds((prev) => ({ setPreviousResponseIds((prev) => ({
...prev, ...prev,
[endpoint]: responseId, [endpoint]: responseId,
})); }));
// Clear placeholder when forking // Clear placeholder when forking
setPlaceholderConversation(null); setPlaceholderConversation(null);
// The messages are already set by the chat page component before calling this // The messages are already set by the chat page component before calling this
}, },
[endpoint], [endpoint],
); );
const value = useMemo<ChatContextType>( const setConversationFilter = useCallback(
() => ({ (filter: KnowledgeFilter | null) => {
endpoint, setConversationFilterState(filter);
setEndpoint, // Update the conversation data to include the filter
currentConversationId, setConversationData((prev) => {
setCurrentConversationId, if (!prev) return prev;
previousResponseIds, return {
setPreviousResponseIds, ...prev,
refreshConversations, filter,
refreshConversationsSilent, };
refreshTrigger, });
refreshTriggerSilent, },
loadConversation, [],
startNewConversation, );
conversationData,
forkFromResponse,
conversationDocs,
addConversationDoc,
clearConversationDocs,
placeholderConversation,
setPlaceholderConversation,
conversationLoaded,
setConversationLoaded,
}),
[
endpoint,
currentConversationId,
previousResponseIds,
refreshConversations,
refreshConversationsSilent,
refreshTrigger,
refreshTriggerSilent,
loadConversation,
startNewConversation,
conversationData,
forkFromResponse,
conversationDocs,
addConversationDoc,
clearConversationDocs,
placeholderConversation,
conversationLoaded,
setConversationLoaded,
],
);
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>; const value = useMemo<ChatContextType>(
() => ({
endpoint,
setEndpoint,
currentConversationId,
setCurrentConversationId,
previousResponseIds,
setPreviousResponseIds,
refreshConversations,
refreshConversationsSilent,
refreshTrigger,
refreshTriggerSilent,
loadConversation,
startNewConversation,
conversationData,
forkFromResponse,
conversationDocs,
addConversationDoc,
clearConversationDocs,
placeholderConversation,
setPlaceholderConversation,
conversationLoaded,
setConversationLoaded,
conversationFilter,
setConversationFilter,
}),
[
endpoint,
currentConversationId,
previousResponseIds,
refreshConversations,
refreshConversationsSilent,
refreshTrigger,
refreshTriggerSilent,
loadConversation,
startNewConversation,
conversationData,
forkFromResponse,
conversationDocs,
addConversationDoc,
clearConversationDocs,
placeholderConversation,
conversationLoaded,
conversationFilter,
setConversationFilter,
],
);
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
} }
export function useChat(): ChatContextType { export function useChat(): ChatContextType {
const context = useContext(ChatContext); const context = useContext(ChatContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useChat must be used within a ChatProvider"); throw new Error("useChat must be used within a ChatProvider");
} }
return context; return context;
} }