🚀 (frontend): Implement support for process.env.PORT to run app on a configurable port 🔧 (frontend): Change port variable case from lowercase 'port' to uppercase 'PORT' for better semantics 📝 (frontend): Add comments to clarify the purpose of loading conversation data only when user explicitly selects a conversation 📝 (frontend): Add comments to explain the logic for loading conversation data based on certain conditions 📝 (frontend): Add comments to describe the purpose of handling new conversation creation and resetting messages 📝 (frontend): Add comments to explain the logic for loading conversation data when conversationData changes 📝 (frontend): Add comments to clarify the purpose of loading conversations from the backend 📝 (frontend): Add comments to describe the logic for silent refresh to update data without loading states 📝 (frontend): Add comments to explain the purpose of starting a new conversation and creating a placeholder conversation 📝 (frontend): Add comments to clarify the logic for forking from a response and starting a new conversation 📝 (frontend): Add comments to describe the purpose of adding a conversation document and clearing conversation documents 📝 (frontend): Add comments to explain the logic for using a timeout to debounce multiple rapid refresh calls 📝 (frontend): Add comments to clarify the purpose of cleaning up timeout on unmount 📝 (frontend): Add comments to describe the logic for handling new conversation creation and resetting state 📝 (frontend): Add comments to explain the logic for forking from a response and starting a new conversation 📝 (frontend): Add comments to clarify the purpose of using useMemo for optimizing performance in ChatProvider 📝 (frontend): Add comments to describe the logic for using useMemo in the ChatProvider component 📝 (frontend): Add comments to explain the purpose of the useChat custom hook 📝 (frontend): Add comments to clarify the error message when useChat is not used within a ChatProvider 📝 (services): Update ChatService to fetch Langflow history with flow_id parameter for better control
244 lines
7.2 KiB
TypeScript
244 lines
7.2 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
ReactNode,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
|
|
export type EndpointType = "chat" | "langflow";
|
|
|
|
interface ConversationDocument {
|
|
filename: string;
|
|
uploadTime: Date;
|
|
}
|
|
|
|
interface ConversationMessage {
|
|
role: string;
|
|
content: string;
|
|
timestamp?: string;
|
|
response_id?: string;
|
|
}
|
|
|
|
interface ConversationData {
|
|
messages: ConversationMessage[];
|
|
endpoint: EndpointType;
|
|
response_id: string;
|
|
title: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface ChatContextType {
|
|
endpoint: EndpointType;
|
|
setEndpoint: (endpoint: EndpointType) => void;
|
|
currentConversationId: string | null;
|
|
setCurrentConversationId: (id: string | null) => void;
|
|
previousResponseIds: {
|
|
chat: string | null;
|
|
langflow: string | null;
|
|
};
|
|
setPreviousResponseIds: (
|
|
ids:
|
|
| { chat: string | null; langflow: string | null }
|
|
| ((prev: { chat: string | null; langflow: string | null }) => {
|
|
chat: string | null;
|
|
langflow: string | null;
|
|
})
|
|
) => void;
|
|
refreshConversations: (force?: boolean) => void;
|
|
refreshConversationsSilent: () => Promise<void>;
|
|
refreshTrigger: number;
|
|
refreshTriggerSilent: number;
|
|
loadConversation: (conversation: ConversationData) => void;
|
|
startNewConversation: () => void;
|
|
conversationData: ConversationData | null;
|
|
forkFromResponse: (responseId: string) => void;
|
|
conversationDocs: ConversationDocument[];
|
|
addConversationDoc: (filename: string) => void;
|
|
clearConversationDocs: () => void;
|
|
placeholderConversation: ConversationData | null;
|
|
setPlaceholderConversation: (conversation: ConversationData | null) => void;
|
|
}
|
|
|
|
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
|
|
|
interface ChatProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function ChatProvider({ children }: ChatProviderProps) {
|
|
const [endpoint, setEndpoint] = useState<EndpointType>("langflow");
|
|
const [currentConversationId, setCurrentConversationId] = useState<
|
|
string | null
|
|
>(null);
|
|
const [previousResponseIds, setPreviousResponseIds] = useState<{
|
|
chat: string | null;
|
|
langflow: string | null;
|
|
}>({ chat: null, langflow: null });
|
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
const [refreshTriggerSilent, setRefreshTriggerSilent] = useState(0);
|
|
const [conversationData, setConversationData] =
|
|
useState<ConversationData | null>(null);
|
|
const [conversationDocs, setConversationDocs] = useState<
|
|
ConversationDocument[]
|
|
>([]);
|
|
const [placeholderConversation, setPlaceholderConversation] =
|
|
useState<ConversationData | null>(null);
|
|
|
|
// Debounce refresh requests to prevent excessive reloads
|
|
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const refreshConversations = useCallback((force = false) => {
|
|
if (force) {
|
|
// Immediate refresh for important updates like new conversations
|
|
setRefreshTrigger((prev) => prev + 1);
|
|
return;
|
|
}
|
|
|
|
// Clear any existing timeout
|
|
if (refreshTimeoutRef.current) {
|
|
clearTimeout(refreshTimeoutRef.current);
|
|
}
|
|
|
|
// Set a new timeout to debounce multiple rapid refresh calls
|
|
refreshTimeoutRef.current = setTimeout(() => {
|
|
setRefreshTrigger((prev) => prev + 1);
|
|
}, 250); // 250ms debounce
|
|
}, []);
|
|
|
|
// Cleanup timeout on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (refreshTimeoutRef.current) {
|
|
clearTimeout(refreshTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// Silent refresh - updates data without loading states
|
|
const refreshConversationsSilent = useCallback(async () => {
|
|
// Trigger silent refresh that updates conversation data without showing loading states
|
|
setRefreshTriggerSilent((prev) => prev + 1);
|
|
}, []);
|
|
|
|
const loadConversation = useCallback((conversation: ConversationData) => {
|
|
setCurrentConversationId(conversation.response_id);
|
|
setEndpoint(conversation.endpoint);
|
|
// Store the full conversation data for the chat page to use
|
|
setConversationData(conversation);
|
|
// Clear placeholder when loading a real conversation
|
|
setPlaceholderConversation(null);
|
|
}, []);
|
|
|
|
const startNewConversation = useCallback(() => {
|
|
// Clear current conversation data and reset state
|
|
setCurrentConversationId(null);
|
|
setPreviousResponseIds({ chat: null, langflow: null });
|
|
setConversationData(null);
|
|
setConversationDocs([]);
|
|
|
|
// Create a temporary placeholder conversation to show in sidebar
|
|
const placeholderConversation: ConversationData = {
|
|
response_id: "new-conversation-" + Date.now(),
|
|
title: "New conversation",
|
|
endpoint: endpoint,
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
content: "How can I assist?",
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
],
|
|
created_at: new Date().toISOString(),
|
|
last_activity: new Date().toISOString(),
|
|
};
|
|
|
|
setPlaceholderConversation(placeholderConversation);
|
|
// Force immediate refresh to ensure sidebar shows correct state
|
|
refreshConversations(true);
|
|
}, [endpoint, refreshConversations]);
|
|
|
|
const addConversationDoc = useCallback((filename: string) => {
|
|
setConversationDocs((prev) => [
|
|
...prev,
|
|
{ filename, uploadTime: new Date() },
|
|
]);
|
|
}, []);
|
|
|
|
const clearConversationDocs = useCallback(() => {
|
|
setConversationDocs([]);
|
|
}, []);
|
|
|
|
const forkFromResponse = useCallback(
|
|
(responseId: string) => {
|
|
// Start a new conversation with the messages up to the fork point
|
|
setCurrentConversationId(null); // Clear current conversation to indicate new conversation
|
|
setConversationData(null); // Clear conversation data to prevent reloading
|
|
// Set the response ID that we're forking from as the previous response ID
|
|
setPreviousResponseIds((prev) => ({
|
|
...prev,
|
|
[endpoint]: responseId,
|
|
}));
|
|
// Clear placeholder when forking
|
|
setPlaceholderConversation(null);
|
|
// The messages are already set by the chat page component before calling this
|
|
},
|
|
[endpoint]
|
|
);
|
|
|
|
const value = useMemo<ChatContextType>(
|
|
() => ({
|
|
endpoint,
|
|
setEndpoint,
|
|
currentConversationId,
|
|
setCurrentConversationId,
|
|
previousResponseIds,
|
|
setPreviousResponseIds,
|
|
refreshConversations,
|
|
refreshConversationsSilent,
|
|
refreshTrigger,
|
|
refreshTriggerSilent,
|
|
loadConversation,
|
|
startNewConversation,
|
|
conversationData,
|
|
forkFromResponse,
|
|
conversationDocs,
|
|
addConversationDoc,
|
|
clearConversationDocs,
|
|
placeholderConversation,
|
|
setPlaceholderConversation,
|
|
}),
|
|
[
|
|
endpoint,
|
|
currentConversationId,
|
|
previousResponseIds,
|
|
refreshConversations,
|
|
refreshConversationsSilent,
|
|
refreshTrigger,
|
|
refreshTriggerSilent,
|
|
loadConversation,
|
|
startNewConversation,
|
|
conversationData,
|
|
forkFromResponse,
|
|
conversationDocs,
|
|
addConversationDoc,
|
|
clearConversationDocs,
|
|
placeholderConversation,
|
|
]
|
|
);
|
|
|
|
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
|
|
}
|
|
|
|
export function useChat(): ChatContextType {
|
|
const context = useContext(ChatContext);
|
|
if (context === undefined) {
|
|
throw new Error("useChat must be used within a ChatProvider");
|
|
}
|
|
return context;
|
|
}
|