fix: new chat history glitches

This commit is contained in:
phact 2025-08-29 16:50:20 -04:00
parent 0fb9246e88
commit de10c05eef
9 changed files with 312 additions and 69 deletions

View file

@ -47,15 +47,21 @@ interface ChatConversation {
export function Navigation() { export function Navigation() {
const pathname = usePathname() const pathname = usePathname()
const { endpoint, refreshTrigger, loadConversation, currentConversationId, setCurrentConversationId, conversationDocs, addConversationDoc } = useChat() const { endpoint, refreshTrigger, loadConversation, currentConversationId, setCurrentConversationId, startNewConversation, conversationDocs, addConversationDoc, refreshConversations, placeholderConversation, setPlaceholderConversation } = useChat()
const [conversations, setConversations] = useState<ChatConversation[]>([]) const [conversations, setConversations] = useState<ChatConversation[]>([])
const [loadingConversations, setLoadingConversations] = useState(false) const [loadingConversations, setLoadingConversations] = useState(false)
const [previousConversationCount, setPreviousConversationCount] = useState(0)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const handleNewConversation = () => { const handleNewConversation = () => {
setCurrentConversationId(null) // Ensure current conversation appears in sidebar before starting a new one
// The chat page will handle resetting messages when it detects a new conversation request refreshConversations()
window.dispatchEvent(new CustomEvent('newConversation')) // Use context helper to fully reset conversation state
startNewConversation()
// Notify chat view even if state was already 'new'
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('newConversation'))
}
} }
const handleFileUpload = async (file: File) => { const handleFileUpload = async (file: File) => {
@ -173,8 +179,44 @@ export function Navigation() {
}) })
setConversations(conversations) setConversations(conversations)
// If no conversations exist and no placeholder is shown, create a default placeholder
if (conversations.length === 0 && !placeholderConversation) {
const defaultPlaceholder: ChatConversation = {
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(),
total_messages: 1
}
setPlaceholderConversation(defaultPlaceholder)
}
} else { } else {
setConversations([]) setConversations([])
// Also create placeholder when request fails and no conversations exist
if (!placeholderConversation) {
const defaultPlaceholder: ChatConversation = {
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(),
total_messages: 1
}
setPlaceholderConversation(defaultPlaceholder)
}
} }
// Conversation documents are now managed in chat context // Conversation documents are now managed in chat context
@ -194,6 +236,24 @@ export function Navigation() {
} }
}, [isOnChatPage, endpoint, refreshTrigger, fetchConversations]) }, [isOnChatPage, endpoint, refreshTrigger, fetchConversations])
// Clear placeholder when conversation count increases (new conversation was created)
useEffect(() => {
const currentCount = conversations.length
// If we had a placeholder and the conversation count increased, clear the placeholder and highlight the new conversation
if (placeholderConversation && currentCount > previousConversationCount && conversations.length > 0) {
setPlaceholderConversation(null)
// Highlight the most recent conversation (first in sorted array) without loading its messages
const newestConversation = conversations[0]
if (newestConversation) {
setCurrentConversationId(newestConversation.response_id)
}
}
// Update the previous count
setPreviousConversationCount(currentCount)
}, [conversations.length, placeholderConversation, setPlaceholderConversation, previousConversationCount, conversations, setCurrentConversationId])
return ( return (
<div className="space-y-4 py-4 flex flex-col h-full bg-background"> <div className="space-y-4 py-4 flex flex-col h-full bg-background">
<div className="px-3 py-2 flex-shrink-0"> <div className="px-3 py-2 flex-shrink-0">
@ -244,32 +304,57 @@ export function Navigation() {
<div className="flex-shrink-0 overflow-y-auto scrollbar-hide space-y-1 max-h-full"> <div className="flex-shrink-0 overflow-y-auto scrollbar-hide space-y-1 max-h-full">
{loadingConversations ? ( {loadingConversations ? (
<div className="text-sm text-muted-foreground p-2">Loading...</div> <div className="text-sm text-muted-foreground p-2">Loading...</div>
) : conversations.length === 0 ? (
<div className="text-sm text-muted-foreground p-2">No conversations yet</div>
) : ( ) : (
conversations.map((conversation) => ( <>
<div {/* Show placeholder conversation if it exists */}
key={conversation.response_id} {placeholderConversation && (
className={`p-2 rounded-lg hover:bg-accent cursor-pointer group ${ <div
currentConversationId === conversation.response_id ? 'bg-accent' : '' className="p-2 rounded-lg bg-accent/50 border border-dashed border-accent cursor-pointer group"
}`} onClick={() => {
onClick={() => { // Don't load placeholder as a real conversation, just focus the input
loadConversation(conversation) if (typeof window !== 'undefined') {
}} window.dispatchEvent(new CustomEvent('focusInput'))
> }
<div className="text-sm font-medium text-foreground mb-1 truncate"> }}
{conversation.title} >
</div> <div className="text-sm font-medium text-foreground mb-1 truncate">
<div className="text-xs text-muted-foreground"> {placeholderConversation.title}
{conversation.total_messages} messages
</div>
{conversation.last_activity && (
<div className="text-xs text-muted-foreground">
{new Date(conversation.last_activity).toLocaleDateString()}
</div> </div>
)} <div className="text-xs text-muted-foreground">
</div> Start typing to begin...
)) </div>
</div>
)}
{/* Show regular conversations */}
{conversations.length === 0 && !placeholderConversation ? (
<div className="text-sm text-muted-foreground p-2">No conversations yet</div>
) : (
conversations.map((conversation) => (
<div
key={conversation.response_id}
className={`p-2 rounded-lg hover:bg-accent cursor-pointer group ${
currentConversationId === conversation.response_id ? 'bg-accent' : ''
}`}
onClick={() => {
loadConversation(conversation)
}}
>
<div className="text-sm font-medium text-foreground mb-1 truncate">
{conversation.title}
</div>
<div className="text-xs text-muted-foreground">
{conversation.total_messages} messages
</div>
{conversation.last_activity && (
<div className="text-xs text-muted-foreground">
{new Date(conversation.last_activity).toLocaleDateString()}
</div>
)}
</div>
))
)}
</>
)} )}
</div> </div>
@ -316,4 +401,4 @@ export function Navigation() {
)} )}
</div> </div>
) )
} }

View file

@ -70,7 +70,7 @@ interface RequestBody {
function ChatPage() { function ChatPage() {
const isDebugMode = process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_OPENRAG_DEBUG === 'true' const isDebugMode = process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_OPENRAG_DEBUG === 'true'
const { user } = useAuth() const { user } = useAuth()
const { endpoint, setEndpoint, currentConversationId, conversationData, setCurrentConversationId, addConversationDoc, forkFromResponse } = useChat() const { endpoint, setEndpoint, currentConversationId, conversationData, setCurrentConversationId, addConversationDoc, forkFromResponse, refreshConversations, previousResponseIds, setPreviousResponseIds, setPlaceholderConversation } = useChat()
const [messages, setMessages] = useState<Message[]>([ const [messages, setMessages] = useState<Message[]>([
{ {
role: "assistant", role: "assistant",
@ -87,10 +87,7 @@ function ChatPage() {
timestamp: Date timestamp: Date
} | null>(null) } | null>(null)
const [expandedFunctionCalls, setExpandedFunctionCalls] = useState<Set<string>>(new Set()) const [expandedFunctionCalls, setExpandedFunctionCalls] = useState<Set<string>>(new Set())
const [previousResponseIds, setPreviousResponseIds] = useState<{ // previousResponseIds now comes from useChat context
chat: string | null
langflow: string | null
}>({ chat: null, langflow: null })
const [isUploading, setIsUploading] = useState(false) const [isUploading, setIsUploading] = useState(false)
const [isDragOver, setIsDragOver] = useState(false) const [isDragOver, setIsDragOver] = useState(false)
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false) const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false)
@ -107,6 +104,8 @@ function ChatPage() {
const inputRef = useRef<HTMLTextAreaElement>(null) const inputRef = useRef<HTMLTextAreaElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null) const dropdownRef = useRef<HTMLDivElement>(null)
const streamAbortRef = useRef<AbortController | null>(null)
const streamIdRef = useRef(0)
const { addTask, isMenuOpen } = useTask() const { addTask, isMenuOpen } = useTask()
const { selectedFilter, parsedFilterData, isPanelOpen, setSelectedFilter } = useKnowledgeFilter() const { selectedFilter, parsedFilterData, isPanelOpen, setSelectedFilter } = useKnowledgeFilter()
@ -209,6 +208,8 @@ function ChatPage() {
[endpoint]: result.response_id [endpoint]: result.response_id
})) }))
} }
// 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}`)
@ -351,6 +352,40 @@ function ChatPage() {
inputRef.current?.focus() inputRef.current?.focus()
}, []) }, [])
// Explicitly handle external new conversation trigger
useEffect(() => {
const handleNewConversation = () => {
// Abort any in-flight streaming so it doesn't bleed into new chat
if (streamAbortRef.current) {
streamAbortRef.current.abort()
}
// Reset chat UI even if context state was already 'new'
setMessages([
{
role: "assistant",
content: "How can I assist?",
timestamp: new Date(),
},
])
setInput("")
setStreamingMessage(null)
setExpandedFunctionCalls(new Set())
setIsFilterHighlighted(false)
setLoading(false)
}
const handleFocusInput = () => {
inputRef.current?.focus()
}
window.addEventListener('newConversation', handleNewConversation)
window.addEventListener('focusInput', handleFocusInput)
return () => {
window.removeEventListener('newConversation', handleNewConversation)
window.removeEventListener('focusInput', handleFocusInput)
}
}, [])
// Load conversation when conversationData changes // Load conversation when conversationData changes
useEffect(() => { useEffect(() => {
const now = Date.now() const now = Date.now()
@ -499,6 +534,13 @@ function ChatPage() {
const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow" const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow"
try { try {
// Abort any existing stream before starting a new one
if (streamAbortRef.current) {
streamAbortRef.current.abort()
}
const controller = new AbortController()
streamAbortRef.current = controller
const thisStreamId = ++streamIdRef.current
const requestBody: RequestBody = { const requestBody: RequestBody = {
prompt: userMessage.content, prompt: userMessage.content,
stream: true, stream: true,
@ -536,6 +578,7 @@ function ChatPage() {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(requestBody), body: JSON.stringify(requestBody),
signal: controller.signal,
}) })
if (!response.ok) { if (!response.ok) {
@ -554,18 +597,19 @@ function ChatPage() {
let newResponseId: string | null = null let newResponseId: string | null = null
// Initialize streaming message // Initialize streaming message
setStreamingMessage({ if (!controller.signal.aborted && thisStreamId === streamIdRef.current) {
content: "", setStreamingMessage({
functionCalls: [], content: "",
timestamp: new Date() functionCalls: [],
}) timestamp: new Date()
})
}
try { try {
while (true) { while (true) {
const { done, value } = await reader.read() const { done, value } = await reader.read()
if (controller.signal.aborted || thisStreamId !== streamIdRef.current) break
if (done) break if (done) break
buffer += decoder.decode(value, { stream: true }) buffer += decoder.decode(value, { stream: true })
// Process complete lines (JSON objects) // Process complete lines (JSON objects)
@ -908,11 +952,13 @@ function ChatPage() {
} }
// Update streaming message // Update streaming message
setStreamingMessage({ if (!controller.signal.aborted && thisStreamId === streamIdRef.current) {
content: currentContent, setStreamingMessage({
functionCalls: [...currentFunctionCalls], content: currentContent,
timestamp: new Date() functionCalls: [...currentFunctionCalls],
}) timestamp: new Date()
})
}
} catch (parseError) { } catch (parseError) {
console.warn("Failed to parse chunk:", line, parseError) console.warn("Failed to parse chunk:", line, parseError)
@ -932,18 +978,29 @@ function ChatPage() {
timestamp: new Date() timestamp: new Date()
} }
setMessages(prev => [...prev, finalMessage]) if (!controller.signal.aborted && thisStreamId === streamIdRef.current) {
setStreamingMessage(null) setMessages(prev => [...prev, finalMessage])
setStreamingMessage(null)
}
// Store the response ID for the next request for this endpoint // Store the response ID for the next request for this endpoint
if (newResponseId) { if (newResponseId && !controller.signal.aborted && thisStreamId === streamIdRef.current) {
setPreviousResponseIds(prev => ({ setPreviousResponseIds(prev => ({
...prev, ...prev,
[endpoint]: newResponseId [endpoint]: newResponseId
})) }))
} }
// Trigger sidebar refresh to include this conversation (with small delay to ensure backend has processed)
setTimeout(() => {
try { refreshConversations() } catch {}
}, 100)
} catch (error) { } catch (error) {
// If stream was aborted (e.g., starting new conversation), do not append errors or final messages
if (streamAbortRef.current?.signal.aborted) {
return
}
console.error("SSE Stream error:", error) console.error("SSE Stream error:", error)
setStreamingMessage(null) setStreamingMessage(null)
@ -1034,6 +1091,10 @@ function ChatPage() {
[endpoint]: result.response_id [endpoint]: result.response_id
})) }))
} }
// 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 = {
@ -1583,8 +1644,9 @@ 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
const words = newValue.split(' ') const words = newValue.split(' ')

View file

@ -8,17 +8,17 @@ import { useAuth } from "@/contexts/auth-context"
import { Lock, LogIn, Loader2 } from "lucide-react" import { Lock, LogIn, Loader2 } from "lucide-react"
function LoginPageContent() { function LoginPageContent() {
const { isLoading, isAuthenticated, login } = useAuth() const { isLoading, isAuthenticated, isNoAuthMode, login } = useAuth()
const router = useRouter() const router = useRouter()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const redirect = searchParams.get('redirect') || '/chat' const redirect = searchParams.get('redirect') || '/chat'
// Redirect if already authenticated // Redirect if already authenticated or in no-auth mode
useEffect(() => { useEffect(() => {
if (!isLoading && isAuthenticated) { if (!isLoading && (isAuthenticated || isNoAuthMode)) {
router.push(redirect) router.push(redirect)
} }
}, [isLoading, isAuthenticated, router, redirect]) }, [isLoading, isAuthenticated, isNoAuthMode, router, redirect])
if (isLoading) { if (isLoading) {
return ( return (
@ -31,7 +31,7 @@ function LoginPageContent() {
) )
} }
if (isAuthenticated) { if (isAuthenticated || isNoAuthMode) {
return null // Will redirect in useEffect return null // Will redirect in useEffect
} }

View file

@ -12,11 +12,14 @@ import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel"
// import { DiscordLink } from "@/components/discord-link" // import { DiscordLink } from "@/components/discord-link"
import { useTask } from "@/contexts/task-context" import { useTask } from "@/contexts/task-context"
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context" import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"
import { useAuth } from "@/contexts/auth-context"
import { Loader2 } from "lucide-react"
export function LayoutWrapper({ children }: { children: React.ReactNode }) { export function LayoutWrapper({ children }: { children: React.ReactNode }) {
const pathname = usePathname() const pathname = usePathname()
const { tasks, isMenuOpen, toggleMenu } = useTask() const { tasks, isMenuOpen, toggleMenu } = useTask()
const { selectedFilter, setSelectedFilter, isPanelOpen } = useKnowledgeFilter() const { selectedFilter, setSelectedFilter, isPanelOpen } = useKnowledgeFilter()
const { isLoading } = useAuth()
// List of paths that should not show navigation // List of paths that should not show navigation
const authPaths = ['/login', '/auth/callback'] const authPaths = ['/login', '/auth/callback']
@ -27,6 +30,18 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
task.status === 'pending' || task.status === 'running' || task.status === 'processing' task.status === 'pending' || task.status === 'running' || task.status === 'processing'
) )
// Show loading state when backend isn't ready
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<Loader2 className="h-8 w-8 animate-spin" />
<p className="text-muted-foreground">Starting OpenRAG...</p>
</div>
</div>
)
}
if (isAuthPage) { if (isAuthPage) {
// For auth pages, render without navigation // For auth pages, render without navigation
return ( return (

View file

@ -10,19 +10,24 @@ interface ProtectedRouteProps {
} }
export function ProtectedRoute({ children }: ProtectedRouteProps) { export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isLoading, isAuthenticated } = useAuth() const { isLoading, isAuthenticated, isNoAuthMode } = useAuth()
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
console.log("ProtectedRoute - isLoading:", isLoading, "isAuthenticated:", isAuthenticated, "pathname:", pathname) console.log("ProtectedRoute - isLoading:", isLoading, "isAuthenticated:", isAuthenticated, "isNoAuthMode:", isNoAuthMode, "pathname:", pathname)
useEffect(() => { useEffect(() => {
// In no-auth mode, allow access without authentication
if (isNoAuthMode) {
return
}
if (!isLoading && !isAuthenticated) { if (!isLoading && !isAuthenticated) {
// Redirect to login with current path as redirect parameter // Redirect to login with current path as redirect parameter
const redirectUrl = `/login?redirect=${encodeURIComponent(pathname)}` const redirectUrl = `/login?redirect=${encodeURIComponent(pathname)}`
router.push(redirectUrl) router.push(redirectUrl)
} }
}, [isLoading, isAuthenticated, router, pathname]) }, [isLoading, isAuthenticated, isNoAuthMode, router, pathname])
// Show loading state while checking authentication // Show loading state while checking authentication
if (isLoading) { if (isLoading) {
@ -36,6 +41,11 @@ export function ProtectedRoute({ children }: ProtectedRouteProps) {
) )
} }
// In no-auth mode, always render content
if (isNoAuthMode) {
return <>{children}</>
}
// Don't render anything if not authenticated (will redirect) // Don't render anything if not authenticated (will redirect)
if (!isAuthenticated) { if (!isAuthenticated) {
return null return null

View file

@ -15,7 +15,7 @@ import { LogIn, LogOut, User, Moon, Sun, ChevronsUpDown } from "lucide-react"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
export function UserNav() { export function UserNav() {
const { user, isLoading, isAuthenticated, login, logout } = useAuth() const { user, isLoading, isAuthenticated, isNoAuthMode, login, logout } = useAuth()
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
if (isLoading) { if (isLoading) {
@ -24,6 +24,20 @@ export function UserNav() {
) )
} }
// In no-auth mode, show a simple theme switcher instead of auth UI
if (isNoAuthMode) {
return (
<Button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
)
}
if (!isAuthenticated) { if (!isAuthenticated) {
return ( return (
<Button <Button

View file

@ -15,6 +15,7 @@ interface AuthContextType {
user: User | null user: User | null
isLoading: boolean isLoading: boolean
isAuthenticated: boolean isAuthenticated: boolean
isNoAuthMode: boolean
login: () => void login: () => void
logout: () => Promise<void> logout: () => Promise<void>
refreshAuth: () => Promise<void> refreshAuth: () => Promise<void>
@ -37,26 +38,49 @@ interface AuthProviderProps {
export function AuthProvider({ children }: AuthProviderProps) { export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null) const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isNoAuthMode, setIsNoAuthMode] = useState(false)
const checkAuth = async () => { const checkAuth = async () => {
try { try {
const response = await fetch('/api/auth/me') const response = await fetch('/api/auth/me')
// If we can't reach the backend, keep loading
if (!response.ok && (response.status === 0 || response.status >= 500)) {
console.log('Backend not ready, retrying in 2 seconds...')
setTimeout(checkAuth, 2000)
return
}
const data = await response.json() const data = await response.json()
if (data.authenticated && data.user) { // Check if we're in no-auth mode
if (data.no_auth_mode) {
setIsNoAuthMode(true)
setUser(null)
} else if (data.authenticated && data.user) {
setIsNoAuthMode(false)
setUser(data.user) setUser(data.user)
} else { } else {
setIsNoAuthMode(false)
setUser(null) setUser(null)
} }
setIsLoading(false)
} catch (error) { } catch (error) {
console.error('Auth check failed:', error) console.error('Auth check failed:', error)
setUser(null) // Network error - backend not ready, keep loading and retry
} finally { console.log('Backend not ready, retrying in 2 seconds...')
setIsLoading(false) setTimeout(checkAuth, 2000)
} }
} }
const login = () => { const login = () => {
// Don't allow login in no-auth mode
if (isNoAuthMode) {
console.log('Login attempted in no-auth mode - ignored')
return
}
// Use the correct auth callback URL, not connectors callback // Use the correct auth callback URL, not connectors callback
const redirectUri = `${window.location.origin}/auth/callback` const redirectUri = `${window.location.origin}/auth/callback`
@ -111,6 +135,12 @@ export function AuthProvider({ children }: AuthProviderProps) {
} }
const logout = async () => { const logout = async () => {
// Don't allow logout in no-auth mode
if (isNoAuthMode) {
console.log('Logout attempted in no-auth mode - ignored')
return
}
try { try {
await fetch('/api/auth/logout', { await fetch('/api/auth/logout', {
method: 'POST', method: 'POST',
@ -133,6 +163,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
user, user,
isLoading, isLoading,
isAuthenticated: !!user, isAuthenticated: !!user,
isNoAuthMode,
login, login,
logout, logout,
refreshAuth, refreshAuth,

View file

@ -42,6 +42,8 @@ interface ChatContextType {
conversationDocs: ConversationDocument[] conversationDocs: ConversationDocument[]
addConversationDoc: (filename: string) => void addConversationDoc: (filename: string) => void
clearConversationDocs: () => void clearConversationDocs: () => void
placeholderConversation: ConversationData | null
setPlaceholderConversation: (conversation: ConversationData | null) => void
} }
const ChatContext = createContext<ChatContextType | undefined>(undefined) const ChatContext = createContext<ChatContextType | undefined>(undefined)
@ -60,6 +62,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
const [refreshTrigger, setRefreshTrigger] = useState(0) const [refreshTrigger, setRefreshTrigger] = useState(0)
const [conversationData, setConversationData] = useState<ConversationData | null>(null) const [conversationData, setConversationData] = useState<ConversationData | null>(null)
const [conversationDocs, setConversationDocs] = useState<ConversationDocument[]>([]) const [conversationDocs, setConversationDocs] = useState<ConversationDocument[]>([])
const [placeholderConversation, setPlaceholderConversation] = useState<ConversationData | null>(null)
const refreshConversations = () => { const refreshConversations = () => {
setRefreshTrigger(prev => prev + 1) setRefreshTrigger(prev => prev + 1)
@ -71,13 +74,32 @@ export function ChatProvider({ children }: ChatProviderProps) {
// Store the full conversation data for the chat page to use // Store the full conversation data for the chat page to use
// We'll pass it through a ref or state that the chat page can access // We'll pass it through a ref or state that the chat page can access
setConversationData(conversation) setConversationData(conversation)
// Clear placeholder when loading a real conversation
setPlaceholderConversation(null)
} }
const startNewConversation = () => { 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) setCurrentConversationId(null)
setPreviousResponseIds({ chat: null, langflow: null }) setPreviousResponseIds({ chat: null, langflow: null })
setConversationData(null) setConversationData(null)
setConversationDocs([]) setConversationDocs([])
setPlaceholderConversation(placeholderConversation)
// Force a refresh to ensure sidebar shows correct state
setRefreshTrigger(prev => prev + 1)
} }
const addConversationDoc = (filename: string) => { const addConversationDoc = (filename: string) => {
@ -97,6 +119,8 @@ export function ChatProvider({ children }: ChatProviderProps) {
...prev, ...prev,
[endpoint]: responseId [endpoint]: responseId
})) }))
// Clear placeholder when forking
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
} }
@ -116,6 +140,8 @@ export function ChatProvider({ children }: ChatProviderProps) {
conversationDocs, conversationDocs,
addConversationDoc, addConversationDoc,
clearConversationDocs, clearConversationDocs,
placeholderConversation,
setPlaceholderConversation,
} }
return ( return (

View file

@ -37,10 +37,10 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
const [isPolling, setIsPolling] = useState(false) const [isPolling, setIsPolling] = useState(false)
const [isFetching, setIsFetching] = useState(false) const [isFetching, setIsFetching] = useState(false)
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isMenuOpen, setIsMenuOpen] = useState(false)
const { isAuthenticated } = useAuth() const { isAuthenticated, isNoAuthMode } = useAuth()
const fetchTasks = useCallback(async () => { const fetchTasks = useCallback(async () => {
if (!isAuthenticated) return if (!isAuthenticated && !isNoAuthMode) return
setIsFetching(true) setIsFetching(true)
try { try {
@ -81,7 +81,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
} finally { } finally {
setIsFetching(false) setIsFetching(false)
} }
}, [isAuthenticated]) // Removed 'tasks' from dependencies to prevent infinite loop! }, [isAuthenticated, isNoAuthMode]) // Removed 'tasks' from dependencies to prevent infinite loop!
const addTask = useCallback((taskId: string) => { const addTask = useCallback((taskId: string) => {
// Immediately start aggressive polling for the new task // Immediately start aggressive polling for the new task
@ -163,7 +163,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
// Periodic polling for task updates // Periodic polling for task updates
useEffect(() => { useEffect(() => {
if (!isAuthenticated) return if (!isAuthenticated && !isNoAuthMode) return
setIsPolling(true) setIsPolling(true)
@ -177,7 +177,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
clearInterval(interval) clearInterval(interval)
setIsPolling(false) setIsPolling(false)
} }
}, [isAuthenticated, fetchTasks]) }, [isAuthenticated, isNoAuthMode, fetchTasks])
const value: TaskContextType = { const value: TaskContextType = {
tasks, tasks,