📝 (frontend): Add new function 'refreshConversationsSilent' to update data without loading states
🚀 (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
This commit is contained in:
parent
0db67b8c6a
commit
6dcb65debd
4 changed files with 495 additions and 180 deletions
|
|
@ -91,8 +91,10 @@ function ChatPage() {
|
||||||
addConversationDoc,
|
addConversationDoc,
|
||||||
forkFromResponse,
|
forkFromResponse,
|
||||||
refreshConversations,
|
refreshConversations,
|
||||||
|
refreshConversationsSilent,
|
||||||
previousResponseIds,
|
previousResponseIds,
|
||||||
setPreviousResponseIds,
|
setPreviousResponseIds,
|
||||||
|
placeholderConversation,
|
||||||
} = useChat();
|
} = useChat();
|
||||||
const [messages, setMessages] = useState<Message[]>([
|
const [messages, setMessages] = useState<Message[]>([
|
||||||
{
|
{
|
||||||
|
|
@ -133,6 +135,7 @@ function ChatPage() {
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const streamAbortRef = useRef<AbortController | null>(null);
|
const streamAbortRef = useRef<AbortController | null>(null);
|
||||||
const streamIdRef = useRef(0);
|
const streamIdRef = useRef(0);
|
||||||
|
const lastLoadedConversationRef = useRef<string | null>(null);
|
||||||
const { addTask, isMenuOpen } = useTask();
|
const { addTask, isMenuOpen } = useTask();
|
||||||
const { selectedFilter, parsedFilterData, isPanelOpen, setSelectedFilter } =
|
const { selectedFilter, parsedFilterData, isPanelOpen, setSelectedFilter } =
|
||||||
useKnowledgeFilter();
|
useKnowledgeFilter();
|
||||||
|
|
@ -241,11 +244,16 @@ function ChatPage() {
|
||||||
...prev,
|
...prev,
|
||||||
[endpoint]: result.response_id,
|
[endpoint]: result.response_id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// If this is a new conversation (no currentConversationId), set it now
|
||||||
|
if (!currentConversationId) {
|
||||||
|
setCurrentConversationId(result.response_id);
|
||||||
|
refreshConversations(true);
|
||||||
|
} else {
|
||||||
|
// For existing conversations, do a silent refresh to keep backend in sync
|
||||||
|
refreshConversationsSilent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Sidebar should show this conversation after upload creates it
|
|
||||||
try {
|
|
||||||
refreshConversations();
|
|
||||||
} catch {}
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Upload failed: ${response.status}`);
|
throw new Error(`Upload failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
@ -406,6 +414,7 @@ function ChatPage() {
|
||||||
setExpandedFunctionCalls(new Set());
|
setExpandedFunctionCalls(new Set());
|
||||||
setIsFilterHighlighted(false);
|
setIsFilterHighlighted(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
lastLoadedConversationRef.current = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocusInput = () => {
|
const handleFocusInput = () => {
|
||||||
|
|
@ -420,25 +429,19 @@ function ChatPage() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Load conversation when conversationData changes
|
// Load conversation only when user explicitly selects a conversation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const now = Date.now();
|
// Only load conversation data when:
|
||||||
|
// 1. conversationData exists AND
|
||||||
// Don't reset messages if user is in the middle of an interaction (like forking)
|
// 2. It's different from the last loaded conversation AND
|
||||||
if (isUserInteracting || isForkingInProgress) {
|
// 3. User is not in the middle of an interaction
|
||||||
console.log(
|
if (
|
||||||
"Skipping conversation load due to user interaction or forking"
|
conversationData &&
|
||||||
);
|
conversationData.messages &&
|
||||||
return;
|
lastLoadedConversationRef.current !== conversationData.response_id &&
|
||||||
}
|
!isUserInteracting &&
|
||||||
|
!isForkingInProgress
|
||||||
// Don't reload if we just forked recently (within 1 second)
|
) {
|
||||||
if (now - lastForkTimestamp < 1000) {
|
|
||||||
console.log("Skipping conversation load - recent fork detected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversationData && conversationData.messages) {
|
|
||||||
console.log(
|
console.log(
|
||||||
"Loading conversation with",
|
"Loading conversation with",
|
||||||
conversationData.messages.length,
|
conversationData.messages.length,
|
||||||
|
|
@ -460,6 +463,7 @@ function ChatPage() {
|
||||||
);
|
);
|
||||||
|
|
||||||
setMessages(convertedMessages);
|
setMessages(convertedMessages);
|
||||||
|
lastLoadedConversationRef.current = conversationData.response_id;
|
||||||
|
|
||||||
// Set the previous response ID for this conversation
|
// Set the previous response ID for this conversation
|
||||||
setPreviousResponseIds((prev) => ({
|
setPreviousResponseIds((prev) => ({
|
||||||
|
|
@ -467,14 +471,16 @@ function ChatPage() {
|
||||||
[conversationData.endpoint]: conversationData.response_id,
|
[conversationData.endpoint]: conversationData.response_id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// Reset messages when starting a new conversation (but not during forking)
|
}, [
|
||||||
else if (
|
conversationData,
|
||||||
currentConversationId === null &&
|
isUserInteracting,
|
||||||
!isUserInteracting &&
|
isForkingInProgress,
|
||||||
!isForkingInProgress &&
|
]);
|
||||||
now - lastForkTimestamp > 1000
|
|
||||||
) {
|
// Handle new conversation creation - only reset messages when placeholderConversation is set
|
||||||
console.log("Resetting to default message for new conversation");
|
useEffect(() => {
|
||||||
|
if (placeholderConversation && currentConversationId === null) {
|
||||||
|
console.log("Starting new conversation");
|
||||||
setMessages([
|
setMessages([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
|
|
@ -482,15 +488,9 @@ function ChatPage() {
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
lastLoadedConversationRef.current = null;
|
||||||
}
|
}
|
||||||
}, [
|
}, [placeholderConversation, currentConversationId]);
|
||||||
conversationData,
|
|
||||||
currentConversationId,
|
|
||||||
isUserInteracting,
|
|
||||||
isForkingInProgress,
|
|
||||||
lastForkTimestamp,
|
|
||||||
setPreviousResponseIds,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Listen for file upload events from navigation
|
// Listen for file upload events from navigation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1280,14 +1280,16 @@ function ChatPage() {
|
||||||
...prev,
|
...prev,
|
||||||
[endpoint]: newResponseId,
|
[endpoint]: newResponseId,
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger sidebar refresh to include this conversation (with small delay to ensure backend has processed)
|
// If this is a new conversation (no currentConversationId), set it now
|
||||||
setTimeout(() => {
|
if (!currentConversationId) {
|
||||||
try {
|
setCurrentConversationId(newResponseId);
|
||||||
refreshConversations();
|
refreshConversations(true);
|
||||||
} catch {}
|
} else {
|
||||||
}, 100);
|
// For existing conversations, do a silent refresh to keep backend in sync
|
||||||
|
refreshConversationsSilent();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If stream was aborted (e.g., starting new conversation), do not append errors or final messages
|
// If stream was aborted (e.g., starting new conversation), do not append errors or final messages
|
||||||
if (streamAbortRef.current?.signal.aborted) {
|
if (streamAbortRef.current?.signal.aborted) {
|
||||||
|
|
@ -1390,13 +1392,16 @@ function ChatPage() {
|
||||||
...prev,
|
...prev,
|
||||||
[endpoint]: result.response_id,
|
[endpoint]: result.response_id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// If this is a new conversation (no currentConversationId), set it now
|
||||||
|
if (!currentConversationId) {
|
||||||
|
setCurrentConversationId(result.response_id);
|
||||||
|
refreshConversations(true);
|
||||||
|
} else {
|
||||||
|
// For existing conversations, do a silent refresh to keep backend in sync
|
||||||
|
refreshConversationsSilent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Trigger sidebar refresh to include/update this conversation (with small delay to ensure backend has processed)
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
refreshConversations();
|
|
||||||
} catch {}
|
|
||||||
}, 100);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Chat failed:", result.error);
|
console.error("Chat failed:", result.error);
|
||||||
const errorMessage: Message = {
|
const errorMessage: Message = {
|
||||||
|
|
@ -2013,9 +2018,6 @@ function ChatPage() {
|
||||||
// Clear filter highlight when user starts typing
|
// Clear filter highlight when user starts typing
|
||||||
if (isFilterHighlighted) {
|
if (isFilterHighlighted) {
|
||||||
setIsFilterHighlighted(false);
|
setIsFilterHighlighted(false);
|
||||||
try {
|
|
||||||
refreshConversations();
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find if there's an @ at the start of the last word
|
// Find if there's an @ at the start of the last word
|
||||||
|
|
|
||||||
230
frontend/src/components/navigation.tsx
Normal file
230
frontend/src/components/navigation.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react"
|
||||||
|
import { useRouter, usePathname } from "next/navigation"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Plus, MessageSquare, Database, Settings, GitBranch } from "lucide-react"
|
||||||
|
import { useChat } from "@/contexts/chat-context"
|
||||||
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
|
|
||||||
|
interface Conversation {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
endpoint: string
|
||||||
|
last_activity: string
|
||||||
|
created_at: string
|
||||||
|
response_id: string
|
||||||
|
messages?: Array<{
|
||||||
|
role: string
|
||||||
|
content: string
|
||||||
|
timestamp?: string
|
||||||
|
response_id?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Navigation() {
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const { user } = useAuth()
|
||||||
|
const {
|
||||||
|
refreshTrigger,
|
||||||
|
refreshTriggerSilent,
|
||||||
|
loadConversation,
|
||||||
|
startNewConversation,
|
||||||
|
currentConversationId,
|
||||||
|
placeholderConversation,
|
||||||
|
} = useChat()
|
||||||
|
|
||||||
|
const [conversations, setConversations] = useState<Conversation[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
// Load conversations from backend
|
||||||
|
const loadConversations = async () => {
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const response = await fetch("/api/conversations")
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
setConversations(data.conversations || [])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load conversations:", error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load conversations on mount and when refreshTrigger changes (with loading state)
|
||||||
|
useEffect(() => {
|
||||||
|
loadConversations()
|
||||||
|
}, [refreshTrigger, user])
|
||||||
|
|
||||||
|
// Silent refresh - update data without loading state
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSilent = async () => {
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Don't show loading state for silent refresh
|
||||||
|
const response = await fetch("/api/conversations")
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
setConversations(data.conversations || [])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Silent conversation refresh failed:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only do silent refresh if we have a silent trigger change (not initial load)
|
||||||
|
if (refreshTriggerSilent > 0) {
|
||||||
|
loadSilent()
|
||||||
|
}
|
||||||
|
}, [refreshTriggerSilent, user])
|
||||||
|
|
||||||
|
const handleNewConversation = () => {
|
||||||
|
startNewConversation()
|
||||||
|
// Dispatch custom event to notify chat page
|
||||||
|
window.dispatchEvent(new CustomEvent('newConversation'))
|
||||||
|
router.push('/chat')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConversationClick = async (conversation: Conversation) => {
|
||||||
|
try {
|
||||||
|
// Load full conversation data from backend
|
||||||
|
const response = await fetch(`/api/conversations/${conversation.response_id}`)
|
||||||
|
if (response.ok) {
|
||||||
|
const fullConversation = await response.json()
|
||||||
|
loadConversation(fullConversation)
|
||||||
|
router.push('/chat')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load conversation:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatRelativeTime = (timestamp: string) => {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
const now = new Date()
|
||||||
|
const diffMs = now.getTime() - date.getTime()
|
||||||
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
||||||
|
const diffDays = Math.floor(diffHours / 24)
|
||||||
|
|
||||||
|
if (diffDays > 0) {
|
||||||
|
return `${diffDays}d ago`
|
||||||
|
} else if (diffHours > 0) {
|
||||||
|
return `${diffHours}h ago`
|
||||||
|
} else {
|
||||||
|
return 'Just now'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="flex flex-col h-full w-72 bg-muted/30 border-r border-border">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b border-border">
|
||||||
|
<Button
|
||||||
|
onClick={handleNewConversation}
|
||||||
|
className="w-full justify-start gap-2"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
New Conversation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<div className="p-4 border-b border-border">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Button
|
||||||
|
variant={pathname === '/chat' ? 'secondary' : 'ghost'}
|
||||||
|
className="w-full justify-start gap-2"
|
||||||
|
onClick={() => router.push('/chat')}
|
||||||
|
>
|
||||||
|
<MessageSquare className="h-4 w-4" />
|
||||||
|
Chat
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={pathname === '/knowledge' ? 'secondary' : 'ghost'}
|
||||||
|
className="w-full justify-start gap-2"
|
||||||
|
onClick={() => router.push('/knowledge')}
|
||||||
|
>
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
Knowledge
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={pathname === '/settings' ? 'secondary' : 'ghost'}
|
||||||
|
className="w-full justify-start gap-2"
|
||||||
|
onClick={() => router.push('/settings')}
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Conversations List */}
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<div className="px-4 py-2">
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground mb-2">Conversations</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto px-2">
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-4 text-sm text-muted-foreground">
|
||||||
|
Loading conversations...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{/* Show placeholder conversation if exists */}
|
||||||
|
{placeholderConversation && (
|
||||||
|
<div className="p-2 rounded-md bg-primary/10 border border-primary/20">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<MessageSquare className="h-3 w-3 text-primary" />
|
||||||
|
<span className="text-primary font-medium">New conversation</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
Active
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{conversations.map((conversation) => (
|
||||||
|
<button
|
||||||
|
key={conversation.id}
|
||||||
|
onClick={() => handleConversationClick(conversation)}
|
||||||
|
className={`w-full text-left p-2 rounded-md transition-colors hover:bg-muted/50 ${
|
||||||
|
currentConversationId === conversation.response_id
|
||||||
|
? 'bg-muted border border-border'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<MessageSquare className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium truncate">
|
||||||
|
{conversation.title || 'Untitled'}
|
||||||
|
</span>
|
||||||
|
{conversation.endpoint === 'chat' && (
|
||||||
|
<GitBranch className="h-3 w-3 text-blue-400 ml-auto" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{formatRelativeTime(conversation.last_activity)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{conversations.length === 0 && !placeholderConversation && (
|
||||||
|
<div className="p-4 text-sm text-muted-foreground text-center">
|
||||||
|
No conversations yet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,161 +1,244 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, ReactNode } from 'react'
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} 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 ConversationData {
|
interface ConversationData {
|
||||||
messages: ConversationMessage[]
|
messages: ConversationMessage[];
|
||||||
endpoint: EndpointType
|
endpoint: EndpointType;
|
||||||
response_id: string
|
response_id: string;
|
||||||
title: string
|
title: string;
|
||||||
[key: string]: unknown
|
[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: (ids: { chat: string | null; langflow: string | null } | ((prev: { chat: string | null; langflow: string | null }) => { chat: string | null; langflow: string | null })) => void
|
setPreviousResponseIds: (
|
||||||
refreshConversations: () => void
|
ids:
|
||||||
refreshTrigger: number
|
| { chat: string | null; langflow: string | null }
|
||||||
loadConversation: (conversation: ConversationData) => void
|
| ((prev: { chat: string | null; langflow: string | null }) => {
|
||||||
startNewConversation: () => void
|
chat: string | null;
|
||||||
conversationData: ConversationData | null
|
langflow: string | null;
|
||||||
forkFromResponse: (responseId: string) => void
|
})
|
||||||
conversationDocs: ConversationDocument[]
|
) => void;
|
||||||
addConversationDoc: (filename: string) => void
|
refreshConversations: (force?: boolean) => void;
|
||||||
clearConversationDocs: () => void
|
refreshConversationsSilent: () => Promise<void>;
|
||||||
placeholderConversation: ConversationData | null
|
refreshTrigger: number;
|
||||||
setPlaceholderConversation: (conversation: ConversationData | null) => void
|
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)
|
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<string | null>(null)
|
const [currentConversationId, setCurrentConversationId] = useState<
|
||||||
|
string | 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 [conversationData, setConversationData] = useState<ConversationData | null>(null)
|
const [refreshTriggerSilent, setRefreshTriggerSilent] = useState(0);
|
||||||
const [conversationDocs, setConversationDocs] = useState<ConversationDocument[]>([])
|
const [conversationData, setConversationData] =
|
||||||
const [placeholderConversation, setPlaceholderConversation] = useState<ConversationData | null>(null)
|
useState<ConversationData | null>(null);
|
||||||
|
const [conversationDocs, setConversationDocs] = useState<
|
||||||
|
ConversationDocument[]
|
||||||
|
>([]);
|
||||||
|
const [placeholderConversation, setPlaceholderConversation] =
|
||||||
|
useState<ConversationData | null>(null);
|
||||||
|
|
||||||
const refreshConversations = () => {
|
// Debounce refresh requests to prevent excessive reloads
|
||||||
setRefreshTrigger(prev => prev + 1)
|
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
}
|
|
||||||
|
|
||||||
const loadConversation = (conversation: ConversationData) => {
|
const refreshConversations = useCallback((force = false) => {
|
||||||
setCurrentConversationId(conversation.response_id)
|
if (force) {
|
||||||
setEndpoint(conversation.endpoint)
|
// Immediate refresh for important updates like new conversations
|
||||||
// Store the full conversation data for the chat page to use
|
setRefreshTrigger((prev) => prev + 1);
|
||||||
// We'll pass it through a ref or state that the chat page can access
|
return;
|
||||||
setConversationData(conversation)
|
|
||||||
// Clear placeholder when loading a real conversation
|
|
||||||
setPlaceholderConversation(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const startNewConversation = () => {
|
|
||||||
// Create a temporary placeholder conversation
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentConversationId(null)
|
// Clear any existing timeout
|
||||||
setPreviousResponseIds({ chat: null, langflow: null })
|
if (refreshTimeoutRef.current) {
|
||||||
setConversationData(null)
|
clearTimeout(refreshTimeoutRef.current);
|
||||||
setConversationDocs([])
|
}
|
||||||
setPlaceholderConversation(placeholderConversation)
|
|
||||||
// Force a refresh to ensure sidebar shows correct state
|
|
||||||
setRefreshTrigger(prev => prev + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addConversationDoc = (filename: string) => {
|
// Set a new timeout to debounce multiple rapid refresh calls
|
||||||
setConversationDocs(prev => [...prev, { filename, uploadTime: new Date() }])
|
refreshTimeoutRef.current = setTimeout(() => {
|
||||||
}
|
setRefreshTrigger((prev) => prev + 1);
|
||||||
|
}, 250); // 250ms debounce
|
||||||
|
}, []);
|
||||||
|
|
||||||
const clearConversationDocs = () => {
|
// Cleanup timeout on unmount
|
||||||
setConversationDocs([])
|
useEffect(() => {
|
||||||
}
|
return () => {
|
||||||
|
if (refreshTimeoutRef.current) {
|
||||||
|
clearTimeout(refreshTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const forkFromResponse = (responseId: string) => {
|
// Silent refresh - updates data without loading states
|
||||||
// Start a new conversation with the messages up to the fork point
|
const refreshConversationsSilent = useCallback(async () => {
|
||||||
setCurrentConversationId(null) // Clear current conversation to indicate new conversation
|
// Trigger silent refresh that updates conversation data without showing loading states
|
||||||
setConversationData(null) // Clear conversation data to prevent reloading
|
setRefreshTriggerSilent((prev) => prev + 1);
|
||||||
// Set the response ID that we're forking from as the previous response ID
|
}, []);
|
||||||
setPreviousResponseIds(prev => ({
|
|
||||||
|
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,
|
...prev,
|
||||||
[endpoint]: responseId
|
{ filename, uploadTime: new Date() },
|
||||||
}))
|
]);
|
||||||
// Clear placeholder when forking
|
}, []);
|
||||||
setPlaceholderConversation(null)
|
|
||||||
// The messages are already set by the chat page component before calling this
|
|
||||||
}
|
|
||||||
|
|
||||||
const value: ChatContextType = {
|
const clearConversationDocs = useCallback(() => {
|
||||||
endpoint,
|
setConversationDocs([]);
|
||||||
setEndpoint,
|
}, []);
|
||||||
currentConversationId,
|
|
||||||
setCurrentConversationId,
|
|
||||||
previousResponseIds,
|
|
||||||
setPreviousResponseIds,
|
|
||||||
refreshConversations,
|
|
||||||
refreshTrigger,
|
|
||||||
loadConversation,
|
|
||||||
startNewConversation,
|
|
||||||
conversationData,
|
|
||||||
forkFromResponse,
|
|
||||||
conversationDocs,
|
|
||||||
addConversationDoc,
|
|
||||||
clearConversationDocs,
|
|
||||||
placeholderConversation,
|
|
||||||
setPlaceholderConversation,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const forkFromResponse = useCallback(
|
||||||
<ChatContext.Provider value={value}>
|
(responseId: string) => {
|
||||||
{children}
|
// Start a new conversation with the messages up to the fork point
|
||||||
</ChatContext.Provider>
|
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 {
|
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;
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +328,7 @@ class ChatService:
|
||||||
# 2. Get historical conversations from Langflow database
|
# 2. Get historical conversations from Langflow database
|
||||||
# (works with both Google-bound users and direct Langflow users)
|
# (works with both Google-bound users and direct Langflow users)
|
||||||
print(f"[DEBUG] Attempting to fetch Langflow history for user: {user_id}")
|
print(f"[DEBUG] Attempting to fetch Langflow history for user: {user_id}")
|
||||||
langflow_history = await langflow_history_service.get_user_conversation_history(user_id)
|
langflow_history = await langflow_history_service.get_user_conversation_history(user_id, flow_id=FLOW_ID)
|
||||||
|
|
||||||
if langflow_history.get("conversations"):
|
if langflow_history.get("conversations"):
|
||||||
for conversation in langflow_history["conversations"]:
|
for conversation in langflow_history["conversations"]:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue