update chat page to use a new filter for every conversation
This commit is contained in:
parent
230081e591
commit
15701f8b16
2 changed files with 1620 additions and 1558 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue