feat(ux): Implement advanced reasoning view with progress tracking and refined Markdown rendering.

This commit is contained in:
Yunxiao Xu
2026-02-13 03:13:58 -08:00
parent dc6e73ec79
commit e16b19ed66
6 changed files with 114 additions and 49 deletions

View File

@@ -1,6 +1,7 @@
import * as React from "react"
import { MessageList } from "./MessageList"
import { ChatInput } from "./ChatInput"
import { ExecutionStatus } from "./ExecutionStatus"
import { ChatService, type MessageResponse } from "@/services/chat"
import { AlertCircle } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
@@ -22,18 +23,17 @@ export function ChatInterface({
const [isStreaming, setIsStreaming] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
// Get steps from the currently active assistant message if streaming
const activeSteps = isStreaming && messages.length > 0
? messages[messages.length - 1].steps || []
: []
// Sync messages if threadId or initialMessages changes
React.useEffect(() => {
console.log("ChatInterface: Syncing messages", initialMessages)
setMessages(initialMessages)
setError(null)
}, [threadId, initialMessages])
// Log messages changes for debugging
React.useEffect(() => {
console.log("ChatInterface: Messages state updated", messages)
}, [messages])
const handleSendMessage = async (text: string) => {
setError(null)
setIsStreaming(true)
@@ -69,7 +69,7 @@ export function ChatInterface({
return (
<div className="flex flex-col h-full bg-background rounded-xl border shadow-lg overflow-hidden">
{error && (
<Alert variant="destructive" className="m-4">
<Alert variant="destructive" className="m-4 shrink-0">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
@@ -77,6 +77,12 @@ export function ChatInterface({
)}
<MessageList messages={messages} />
{isStreaming && activeSteps.length > 0 && (
<div className="px-4 pb-4 shrink-0">
<ExecutionStatus steps={activeSteps} />
</div>
)}
<ChatInput
onSendMessage={handleSendMessage}

View File

@@ -0,0 +1,64 @@
import { cn } from "@/lib/utils"
import { Loader2Icon, CheckCircle2Icon } from "lucide-react"
interface ExecutionStatusProps {
steps: string[]
isComplete?: boolean
className?: string
}
const PHASE_CONFIG = [
{ label: "Analyzing query...", match: "Query analysis complete." },
{ label: "Generating strategic plan...", match: "Strategic plan generated." },
{ label: "Writing analysis code...", match: "Analysis code generated." },
{ label: "Performing data analysis...", match: "Data analysis and visualization complete." }
]
export function ExecutionStatus({ steps, isComplete, className }: ExecutionStatusProps) {
if (steps.length === 0) return null
// Calculate progress based on phase completion
const completedCount = PHASE_CONFIG.filter(p => steps.includes(p.match)).length
const totalPhases = PHASE_CONFIG.length
const progress = Math.min((completedCount / totalPhases) * 100, 100)
return (
<div className={cn("space-y-3 p-3 bg-muted/30 rounded-lg border border-border/50", className)}>
<div className="flex items-center justify-between gap-4">
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all duration-500 ease-out"
style={{ width: `${isComplete ? 100 : progress}%` }}
/>
</div>
<span className="text-[10px] font-medium tabular-nums opacity-60">
{isComplete ? "100%" : `${Math.round(progress)}%`}
</span>
</div>
<div className="space-y-1.5">
{steps.map((step, index) => {
const isLast = index === steps.length - 1
const isCompletionStep = PHASE_CONFIG.some(p => p.match === step)
return (
<div
key={index}
className={cn(
"flex items-start gap-2 transition-all duration-300",
isLast && !isComplete ? "text-xs font-medium opacity-100" : "text-[10px] opacity-50"
)}
>
{isLast && !isComplete && !isCompletionStep ? (
<Loader2Icon className="h-3 w-3 mt-0.5 animate-spin text-primary" />
) : (
<CheckCircle2Icon className="h-3 w-3 mt-0.5 text-primary opacity-80" />
)}
<span className="leading-tight">{step}</span>
</div>
)
})}
</div>
</div>
)
}

View File

@@ -24,26 +24,6 @@ export function MessageBubble({ message }: MessageBubbleProps) {
: "bg-primary text-primary-foreground"
)}
>
{isAssistant && message.steps && message.steps.length > 0 && (
<div className="mb-3 space-y-1 border-b border-secondary-foreground/10 pb-2">
{message.steps.map((step, index) => {
const isLast = index === message.steps!.length - 1
return (
<div
key={index}
className={cn(
"flex items-center gap-2 transition-all duration-300",
isLast ? "text-xs font-medium opacity-80" : "text-[10px] opacity-40"
)}
>
<div className={cn("rounded-full bg-current", isLast ? "h-1 w-1" : "h-0.5 w-0.5")} />
{step}
</div>
)
})}
</div>
)}
<div className="break-words text-sm">
{isAssistant ? (
message.content ? (