fix: new chat history glitches
This commit is contained in:
parent
0fb9246e88
commit
de10c05eef
9 changed files with 312 additions and 69 deletions
|
|
@ -47,15 +47,21 @@ interface ChatConversation {
|
|||
|
||||
export function Navigation() {
|
||||
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 [loadingConversations, setLoadingConversations] = useState(false)
|
||||
const [previousConversationCount, setPreviousConversationCount] = useState(0)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleNewConversation = () => {
|
||||
setCurrentConversationId(null)
|
||||
// The chat page will handle resetting messages when it detects a new conversation request
|
||||
window.dispatchEvent(new CustomEvent('newConversation'))
|
||||
// Ensure current conversation appears in sidebar before starting a new one
|
||||
refreshConversations()
|
||||
// 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) => {
|
||||
|
|
@ -173,8 +179,44 @@ export function Navigation() {
|
|||
})
|
||||
|
||||
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 {
|
||||
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
|
||||
|
|
@ -194,6 +236,24 @@ export function Navigation() {
|
|||
}
|
||||
}, [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 (
|
||||
<div className="space-y-4 py-4 flex flex-col h-full bg-background">
|
||||
<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">
|
||||
{loadingConversations ? (
|
||||
<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
|
||||
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()}
|
||||
<>
|
||||
{/* Show placeholder conversation if it exists */}
|
||||
{placeholderConversation && (
|
||||
<div
|
||||
className="p-2 rounded-lg bg-accent/50 border border-dashed border-accent cursor-pointer group"
|
||||
onClick={() => {
|
||||
// Don't load placeholder as a real conversation, just focus the input
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('focusInput'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="text-sm font-medium text-foreground mb-1 truncate">
|
||||
{placeholderConversation.title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
<div className="text-xs text-muted-foreground">
|
||||
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>
|
||||
|
||||
|
|
@ -316,4 +401,4 @@ export function Navigation() {
|
|||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ interface RequestBody {
|
|||
function ChatPage() {
|
||||
const isDebugMode = process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_OPENRAG_DEBUG === 'true'
|
||||
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[]>([
|
||||
{
|
||||
role: "assistant",
|
||||
|
|
@ -87,10 +87,7 @@ function ChatPage() {
|
|||
timestamp: Date
|
||||
} | null>(null)
|
||||
const [expandedFunctionCalls, setExpandedFunctionCalls] = useState<Set<string>>(new Set())
|
||||
const [previousResponseIds, setPreviousResponseIds] = useState<{
|
||||
chat: string | null
|
||||
langflow: string | null
|
||||
}>({ chat: null, langflow: null })
|
||||
// previousResponseIds now comes from useChat context
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [isDragOver, setIsDragOver] = useState(false)
|
||||
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false)
|
||||
|
|
@ -107,6 +104,8 @@ function ChatPage() {
|
|||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const streamAbortRef = useRef<AbortController | null>(null)
|
||||
const streamIdRef = useRef(0)
|
||||
const { addTask, isMenuOpen } = useTask()
|
||||
const { selectedFilter, parsedFilterData, isPanelOpen, setSelectedFilter } = useKnowledgeFilter()
|
||||
|
||||
|
|
@ -209,6 +208,8 @@ function ChatPage() {
|
|||
[endpoint]: result.response_id
|
||||
}))
|
||||
}
|
||||
// Sidebar should show this conversation after upload creates it
|
||||
try { refreshConversations() } catch {}
|
||||
|
||||
} else {
|
||||
throw new Error(`Upload failed: ${response.status}`)
|
||||
|
|
@ -351,6 +352,40 @@ function ChatPage() {
|
|||
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
|
||||
useEffect(() => {
|
||||
const now = Date.now()
|
||||
|
|
@ -499,6 +534,13 @@ function ChatPage() {
|
|||
const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow"
|
||||
|
||||
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 = {
|
||||
prompt: userMessage.content,
|
||||
stream: true,
|
||||
|
|
@ -536,6 +578,7 @@ function ChatPage() {
|
|||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -554,18 +597,19 @@ function ChatPage() {
|
|||
let newResponseId: string | null = null
|
||||
|
||||
// Initialize streaming message
|
||||
setStreamingMessage({
|
||||
content: "",
|
||||
functionCalls: [],
|
||||
timestamp: new Date()
|
||||
})
|
||||
if (!controller.signal.aborted && thisStreamId === streamIdRef.current) {
|
||||
setStreamingMessage({
|
||||
content: "",
|
||||
functionCalls: [],
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (controller.signal.aborted || thisStreamId !== streamIdRef.current) break
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
// Process complete lines (JSON objects)
|
||||
|
|
@ -908,11 +952,13 @@ function ChatPage() {
|
|||
}
|
||||
|
||||
// Update streaming message
|
||||
setStreamingMessage({
|
||||
content: currentContent,
|
||||
functionCalls: [...currentFunctionCalls],
|
||||
timestamp: new Date()
|
||||
})
|
||||
if (!controller.signal.aborted && thisStreamId === streamIdRef.current) {
|
||||
setStreamingMessage({
|
||||
content: currentContent,
|
||||
functionCalls: [...currentFunctionCalls],
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
|
||||
} catch (parseError) {
|
||||
console.warn("Failed to parse chunk:", line, parseError)
|
||||
|
|
@ -932,18 +978,29 @@ function ChatPage() {
|
|||
timestamp: new Date()
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, finalMessage])
|
||||
setStreamingMessage(null)
|
||||
if (!controller.signal.aborted && thisStreamId === streamIdRef.current) {
|
||||
setMessages(prev => [...prev, finalMessage])
|
||||
setStreamingMessage(null)
|
||||
}
|
||||
|
||||
// Store the response ID for the next request for this endpoint
|
||||
if (newResponseId) {
|
||||
if (newResponseId && !controller.signal.aborted && thisStreamId === streamIdRef.current) {
|
||||
setPreviousResponseIds(prev => ({
|
||||
...prev,
|
||||
[endpoint]: newResponseId
|
||||
}))
|
||||
}
|
||||
|
||||
// Trigger sidebar refresh to include this conversation (with small delay to ensure backend has processed)
|
||||
setTimeout(() => {
|
||||
try { refreshConversations() } catch {}
|
||||
}, 100)
|
||||
|
||||
} 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)
|
||||
setStreamingMessage(null)
|
||||
|
||||
|
|
@ -1034,6 +1091,10 @@ function ChatPage() {
|
|||
[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 {
|
||||
console.error("Chat failed:", result.error)
|
||||
const errorMessage: Message = {
|
||||
|
|
@ -1583,8 +1644,9 @@ function ChatPage() {
|
|||
|
||||
// Clear filter highlight when user starts typing
|
||||
if (isFilterHighlighted) {
|
||||
setIsFilterHighlighted(false)
|
||||
}
|
||||
setIsFilterHighlighted(false)
|
||||
try { refreshConversations() } catch {}
|
||||
}
|
||||
|
||||
// Find if there's an @ at the start of the last word
|
||||
const words = newValue.split(' ')
|
||||
|
|
|
|||
|
|
@ -8,17 +8,17 @@ import { useAuth } from "@/contexts/auth-context"
|
|||
import { Lock, LogIn, Loader2 } from "lucide-react"
|
||||
|
||||
function LoginPageContent() {
|
||||
const { isLoading, isAuthenticated, login } = useAuth()
|
||||
const { isLoading, isAuthenticated, isNoAuthMode, login } = useAuth()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const redirect = searchParams.get('redirect') || '/chat'
|
||||
|
||||
// Redirect if already authenticated
|
||||
// Redirect if already authenticated or in no-auth mode
|
||||
useEffect(() => {
|
||||
if (!isLoading && isAuthenticated) {
|
||||
if (!isLoading && (isAuthenticated || isNoAuthMode)) {
|
||||
router.push(redirect)
|
||||
}
|
||||
}, [isLoading, isAuthenticated, router, redirect])
|
||||
}, [isLoading, isAuthenticated, isNoAuthMode, router, redirect])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -31,7 +31,7 @@ function LoginPageContent() {
|
|||
)
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
if (isAuthenticated || isNoAuthMode) {
|
||||
return null // Will redirect in useEffect
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,14 @@ import { KnowledgeFilterPanel } from "@/components/knowledge-filter-panel"
|
|||
// import { DiscordLink } from "@/components/discord-link"
|
||||
import { useTask } from "@/contexts/task-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 }) {
|
||||
const pathname = usePathname()
|
||||
const { tasks, isMenuOpen, toggleMenu } = useTask()
|
||||
const { selectedFilter, setSelectedFilter, isPanelOpen } = useKnowledgeFilter()
|
||||
const { isLoading } = useAuth()
|
||||
|
||||
// List of paths that should not show navigation
|
||||
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'
|
||||
)
|
||||
|
||||
// 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) {
|
||||
// For auth pages, render without navigation
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -10,19 +10,24 @@ interface ProtectedRouteProps {
|
|||
}
|
||||
|
||||
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const { isLoading, isAuthenticated } = useAuth()
|
||||
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
console.log("ProtectedRoute - isLoading:", isLoading, "isAuthenticated:", isAuthenticated, "pathname:", pathname)
|
||||
console.log("ProtectedRoute - isLoading:", isLoading, "isAuthenticated:", isAuthenticated, "isNoAuthMode:", isNoAuthMode, "pathname:", pathname)
|
||||
|
||||
useEffect(() => {
|
||||
// In no-auth mode, allow access without authentication
|
||||
if (isNoAuthMode) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
// Redirect to login with current path as redirect parameter
|
||||
const redirectUrl = `/login?redirect=${encodeURIComponent(pathname)}`
|
||||
router.push(redirectUrl)
|
||||
}
|
||||
}, [isLoading, isAuthenticated, router, pathname])
|
||||
}, [isLoading, isAuthenticated, isNoAuthMode, router, pathname])
|
||||
|
||||
// Show loading state while checking authentication
|
||||
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)
|
||||
if (!isAuthenticated) {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { LogIn, LogOut, User, Moon, Sun, ChevronsUpDown } from "lucide-react"
|
|||
import { useTheme } from "next-themes"
|
||||
|
||||
export function UserNav() {
|
||||
const { user, isLoading, isAuthenticated, login, logout } = useAuth()
|
||||
const { user, isLoading, isAuthenticated, isNoAuthMode, login, logout } = useAuth()
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
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) {
|
||||
return (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface AuthContextType {
|
|||
user: User | null
|
||||
isLoading: boolean
|
||||
isAuthenticated: boolean
|
||||
isNoAuthMode: boolean
|
||||
login: () => void
|
||||
logout: () => Promise<void>
|
||||
refreshAuth: () => Promise<void>
|
||||
|
|
@ -37,26 +38,49 @@ interface AuthProviderProps {
|
|||
export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isNoAuthMode, setIsNoAuthMode] = useState(false)
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
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()
|
||||
|
||||
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)
|
||||
} else {
|
||||
setIsNoAuthMode(false)
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error)
|
||||
setUser(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
// Network error - backend not ready, keep loading and retry
|
||||
console.log('Backend not ready, retrying in 2 seconds...')
|
||||
setTimeout(checkAuth, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
const redirectUri = `${window.location.origin}/auth/callback`
|
||||
|
||||
|
|
@ -111,6 +135,12 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||
}
|
||||
|
||||
const logout = async () => {
|
||||
// Don't allow logout in no-auth mode
|
||||
if (isNoAuthMode) {
|
||||
console.log('Logout attempted in no-auth mode - ignored')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
|
|
@ -133,6 +163,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||
user,
|
||||
isLoading,
|
||||
isAuthenticated: !!user,
|
||||
isNoAuthMode,
|
||||
login,
|
||||
logout,
|
||||
refreshAuth,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ interface ChatContextType {
|
|||
conversationDocs: ConversationDocument[]
|
||||
addConversationDoc: (filename: string) => void
|
||||
clearConversationDocs: () => void
|
||||
placeholderConversation: ConversationData | null
|
||||
setPlaceholderConversation: (conversation: ConversationData | null) => void
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextType | undefined>(undefined)
|
||||
|
|
@ -60,6 +62,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
||||
const [conversationData, setConversationData] = useState<ConversationData | null>(null)
|
||||
const [conversationDocs, setConversationDocs] = useState<ConversationDocument[]>([])
|
||||
const [placeholderConversation, setPlaceholderConversation] = useState<ConversationData | null>(null)
|
||||
|
||||
const refreshConversations = () => {
|
||||
setRefreshTrigger(prev => prev + 1)
|
||||
|
|
@ -71,13 +74,32 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
// 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
|
||||
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)
|
||||
setPreviousResponseIds({ chat: null, langflow: null })
|
||||
setConversationData(null)
|
||||
setConversationDocs([])
|
||||
setPlaceholderConversation(placeholderConversation)
|
||||
// Force a refresh to ensure sidebar shows correct state
|
||||
setRefreshTrigger(prev => prev + 1)
|
||||
}
|
||||
|
||||
const addConversationDoc = (filename: string) => {
|
||||
|
|
@ -97,6 +119,8 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
...prev,
|
||||
[endpoint]: responseId
|
||||
}))
|
||||
// Clear placeholder when forking
|
||||
setPlaceholderConversation(null)
|
||||
// The messages are already set by the chat page component before calling this
|
||||
}
|
||||
|
||||
|
|
@ -116,6 +140,8 @@ export function ChatProvider({ children }: ChatProviderProps) {
|
|||
conversationDocs,
|
||||
addConversationDoc,
|
||||
clearConversationDocs,
|
||||
placeholderConversation,
|
||||
setPlaceholderConversation,
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
|||
const [isPolling, setIsPolling] = useState(false)
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const { isAuthenticated } = useAuth()
|
||||
const { isAuthenticated, isNoAuthMode } = useAuth()
|
||||
|
||||
const fetchTasks = useCallback(async () => {
|
||||
if (!isAuthenticated) return
|
||||
if (!isAuthenticated && !isNoAuthMode) return
|
||||
|
||||
setIsFetching(true)
|
||||
try {
|
||||
|
|
@ -81,7 +81,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
|||
} finally {
|
||||
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) => {
|
||||
// Immediately start aggressive polling for the new task
|
||||
|
|
@ -163,7 +163,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
|||
|
||||
// Periodic polling for task updates
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return
|
||||
if (!isAuthenticated && !isNoAuthMode) return
|
||||
|
||||
setIsPolling(true)
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
|
|||
clearInterval(interval)
|
||||
setIsPolling(false)
|
||||
}
|
||||
}, [isAuthenticated, fetchTasks])
|
||||
}, [isAuthenticated, isNoAuthMode, fetchTasks])
|
||||
|
||||
const value: TaskContextType = {
|
||||
tasks,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue