feat(history): Implement conversation history management with artifact restoration and Markdown rendering.

This commit is contained in:
Yunxiao Xu
2026-02-13 01:59:37 -08:00
parent 339f69a2a3
commit dc6e73ec79
18 changed files with 2288 additions and 62 deletions

View File

@@ -6,7 +6,8 @@ import { RegisterForm } from "./components/auth/RegisterForm"
import { AuthCallback } from "./components/auth/AuthCallback"
import { ChatInterface } from "./components/chat/ChatInterface"
import { AuthService, type UserResponse } from "./services/auth"
import { ChatService } from "./services/chat"
import { ChatService, type MessageResponse } from "./services/chat"
import { type Conversation } from "./components/layout/HistorySidebar"
import { registerUnauthorizedCallback } from "./services/api"
function App() {
@@ -15,12 +16,17 @@ function App() {
const [authMode, setAuthMode] = useState<"login" | "register">("login")
const [isLoading, setIsLoading] = useState(true)
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(null)
const [conversations, setConversations] = useState<Conversation[]>([])
const [threadMessages, setThreadMessages] = useState<Record<string, MessageResponse[]>>({})
useEffect(() => {
// Register callback to handle session expiration from anywhere in the app
registerUnauthorizedCallback(() => {
setIsAuthenticated(false)
setUser(null)
setConversations([])
setSelectedThreadId(null)
setThreadMessages({})
})
const initAuth = async () => {
@@ -28,8 +34,9 @@ function App() {
const userData = await AuthService.getMe()
setUser(userData)
setIsAuthenticated(true)
// Load history after successful auth
loadHistory()
} catch (err: unknown) {
// Not logged in or session expired - this is expected if no cookie
console.log("No active session found", err)
setIsAuthenticated(false)
} finally {
@@ -40,11 +47,21 @@ function App() {
initAuth()
}, [])
const loadHistory = async () => {
try {
const history = await ChatService.listConversations()
setConversations(history)
} catch (err) {
console.error("Failed to load conversation history:", err)
}
}
const handleAuthSuccess = async () => {
try {
const userData = await AuthService.getMe()
setUser(userData)
setIsAuthenticated(true)
loadHistory()
} catch (err: unknown) {
console.error("Failed to fetch user profile after login:", err)
}
@@ -59,19 +76,65 @@ function App() {
setIsAuthenticated(false)
setUser(null)
setSelectedThreadId(null)
setConversations([])
setThreadMessages({})
}
}
const handleCreateTempChat = async () => {
const handleSelectConversation = async (id: string) => {
setSelectedThreadId(id)
// Always fetch messages to avoid stale cache issues when switching back
// or if the session was updated from elsewhere
try {
const conv = await ChatService.createConversation("Temporary Chat")
setSelectedThreadId(conv.id)
const msgs = await ChatService.getMessages(id)
setThreadMessages(prev => ({ ...prev, [id]: msgs }))
} catch (err) {
console.error("Failed to fetch messages:", err)
}
}
const handleCreateConversation = async () => {
try {
const newConv = await ChatService.createConversation()
setConversations(prev => [newConv, ...prev])
setSelectedThreadId(newConv.id)
setThreadMessages(prev => ({ ...prev, [newConv.id]: [] }))
} catch (err) {
console.error("Failed to create conversation:", err)
alert("Failed to start chat session. Please try again.")
}
}
const handleRenameConversation = async (id: string, name: string) => {
try {
const updated = await ChatService.renameConversation(id, name)
setConversations(prev => prev.map(c => c.id === id ? updated : c))
} catch (err) {
console.error("Failed to rename conversation:", err)
}
}
const handleDeleteConversation = async (id: string) => {
try {
await ChatService.deleteConversation(id)
setConversations(prev => prev.filter(c => c.id !== id))
// Also clear from cache
setThreadMessages(prev => {
const next = { ...prev }
delete next[id]
return next
})
if (selectedThreadId === id) {
setSelectedThreadId(null)
}
} catch (err) {
console.error("Failed to delete conversation:", err)
}
}
const handleMessagesFinal = (id: string, messages: MessageResponse[]) => {
setThreadMessages(prev => ({ ...prev, [id]: messages }))
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
@@ -101,7 +164,14 @@ function App() {
)}
</div>
) : (
<MainLayout>
<MainLayout
conversations={conversations}
selectedId={selectedThreadId}
onSelect={handleSelectConversation}
onCreate={handleCreateConversation}
onRename={handleRenameConversation}
onDelete={handleDeleteConversation}
>
<div className="flex flex-col h-full gap-4">
<div className="flex justify-between items-center shrink-0">
<div>
@@ -119,7 +189,12 @@ function App() {
<div className="flex-1 min-h-0">
{selectedThreadId ? (
<ChatInterface threadId={selectedThreadId} />
<ChatInterface
key={selectedThreadId} // Force remount on thread change
threadId={selectedThreadId}
initialMessages={threadMessages[selectedThreadId] || []}
onMessagesFinal={(msgs) => handleMessagesFinal(selectedThreadId, msgs)}
/>
) : (
<div className="flex flex-col items-center justify-center h-full text-center space-y-4 bg-muted/30 rounded-xl border border-dashed p-12">
<div className="p-4 bg-background rounded-full shadow-sm">
@@ -144,10 +219,10 @@ function App() {
Create a new conversation in the sidebar to start asking questions.
</p>
<button
onClick={handleCreateTempChat}
onClick={handleCreateConversation}
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90"
>
Start Temporary Chat
Start New Chat
</button>
</div>
</div>