improve chat ux

This commit is contained in:
phact 2025-08-20 06:15:50 -04:00
parent 9bad6a80f3
commit fb61f80239
3 changed files with 184 additions and 125 deletions

View file

@ -2,7 +2,7 @@
import Link from "next/link" import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { Search, Database, MessageCircle } from "lucide-react" import { Library, Database, MessageSquare, Settings2 } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export function Navigation() { export function Navigation() {
@ -10,22 +10,22 @@ export function Navigation() {
const routes = [ const routes = [
{ {
label: "Knowledge Sources", label: "Chat",
icon: Database, icon: MessageSquare,
href: "/knowledge-sources", href: "/chat",
active: pathname === "/" || pathname === "/knowledge-sources", active: pathname === "/" || pathname === "/chat",
}, },
{ {
label: "Search", label: "Knowledge",
icon: Search, icon: Library,
href: "/search", href: "/search",
active: pathname === "/search", active: pathname === "/search",
}, },
{ {
label: "Chat", label: "Settings",
icon: MessageCircle, icon: Settings2,
href: "/chat", href: "/knowledge-sources",
active: pathname === "/chat", active: pathname === "/knowledge-sources",
}, },
] ]
@ -33,22 +33,26 @@ export function Navigation() {
<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-1"> <div className="px-3 py-2 flex-1">
<div className="space-y-1"> <div className="space-y-1">
{routes.map((route) => ( {routes.map((route, index) => (
<Link <div key={route.href}>
key={route.href} <Link
href={route.href} href={route.href}
className={cn( className={cn(
"text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:bg-accent hover:text-accent-foreground rounded-lg transition-all", "text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:bg-accent hover:text-accent-foreground rounded-lg transition-all",
route.active route.active
? "bg-accent text-accent-foreground shadow-sm" ? "bg-accent text-accent-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground", : "text-foreground hover:text-accent-foreground",
)}
>
<div className="flex items-center flex-1">
<route.icon className={cn("h-4 w-4 mr-3 shrink-0", route.active ? "text-accent-foreground" : "text-muted-foreground group-hover:text-foreground")} />
{route.label}
</div>
</Link>
{route.label === "Settings" && (
<div className="mx-3 my-2 border-t border-border/40" />
)} )}
> </div>
<div className="flex items-center flex-1">
<route.icon className={cn("h-4 w-4 mr-3 shrink-0", route.active ? "text-accent-foreground" : "text-muted-foreground group-hover:text-foreground")} />
{route.label}
</div>
</Link>
))} ))}
</div> </div>
</div> </div>

View file

@ -4,10 +4,12 @@ import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { MessageCircle, Send, Loader2, User, Bot, Zap, Settings, ChevronDown, ChevronRight, Upload } from "lucide-react" import { MessageCircle, Send, Loader2, User, Bot, Zap, Settings, ChevronDown, ChevronRight, Upload, AtSign, Plus } from "lucide-react"
import { ProtectedRoute } from "@/components/protected-route" import { ProtectedRoute } from "@/components/protected-route"
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 { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
interface Message { interface Message {
role: "user" | "assistant" role: "user" | "assistant"
@ -56,7 +58,15 @@ interface RequestBody {
} }
function ChatPage() { function ChatPage() {
const [messages, setMessages] = useState<Message[]>([]) const isDebugMode = process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_OPENRAG_DEBUG === 'true'
const { user } = useAuth()
const [messages, setMessages] = useState<Message[]>([
{
role: "assistant",
content: "How can I assist?",
timestamp: new Date()
}
])
const [input, setInput] = useState("") const [input, setInput] = useState("")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [endpoint, setEndpoint] = useState<EndpointType>("langflow") const [endpoint, setEndpoint] = useState<EndpointType>("langflow")
@ -1013,84 +1023,81 @@ function ChatPage() {
) )
} }
return ( const suggestionChips = [
<div className="space-y-8"> "Show me this quarter's top 10 deals",
<div> "Summarize recent client interactions",
<h1 className="text-3xl font-bold tracking-tight">Chat Assistant</h1> "Search OpenSearch for mentions of our competitors"
<p className="text-muted-foreground mt-2">Ask questions about your documents and get AI-powered answers</p> ]
</div>
<Card className="h-[600px] flex flex-col max-w-full overflow-hidden"> const handleSuggestionClick = (suggestion: string) => {
<CardHeader className="flex-shrink-0"> setInput(suggestion)
<div className="flex items-center justify-between"> inputRef.current?.focus()
<div className="flex items-center gap-2"> }
<MessageCircle className="h-5 w-5" />
<CardTitle>Chat</CardTitle> return (
{selectedFilter && ( <div className="fixed inset-0 md:left-72 md:right-6 top-[53px] flex flex-col">
<span className="text-sm font-normal text-blue-400 bg-blue-400/10 px-2 py-1 rounded"> {/* Debug header - only show in debug mode */}
Context: {selectedFilter.name} {isDebugMode && (
</span> <div className="flex items-center justify-between mb-6 px-6 pt-6">
)} <div className="flex items-center gap-2">
</div>
<div className="flex items-center gap-4">
{/* Async Mode Toggle */}
<div className="flex items-center gap-2 bg-muted/50 rounded-lg p-1">
<Button
variant={!asyncMode ? "default" : "ghost"}
size="sm"
onClick={() => setAsyncMode(false)}
className="h-7 text-xs"
>
Streaming Off
</Button>
<Button
variant={asyncMode ? "default" : "ghost"}
size="sm"
onClick={() => setAsyncMode(true)}
className="h-7 text-xs"
>
<Zap className="h-3 w-3 mr-1" />
Streaming On
</Button>
</div>
{/* Endpoint Toggle */}
<div className="flex items-center gap-2 bg-muted/50 rounded-lg p-1">
<Button
variant={endpoint === "chat" ? "default" : "ghost"}
size="sm"
onClick={() => handleEndpointChange("chat")}
className="h-7 text-xs"
>
Chat
</Button>
<Button
variant={endpoint === "langflow" ? "default" : "ghost"}
size="sm"
onClick={() => handleEndpointChange("langflow")}
className="h-7 text-xs"
>
Langflow
</Button>
</div>
</div>
</div>
<CardDescription>
Chat with AI about your indexed documents using {endpoint === "chat" ? "Chat" : "Langflow"} endpoint
{asyncMode ? " with real-time streaming" : ""}
{selectedFilter && ( {selectedFilter && (
<span className="block text-blue-400 text-xs mt-1"> <span className="text-sm font-normal text-blue-400 bg-blue-400/10 px-2 py-1 rounded">
Using knowledge filter: {selectedFilter.name} Context: {selectedFilter.name}
</span> </span>
)} )}
</CardDescription> </div>
</CardHeader> <div className="flex items-center gap-4">
<CardContent className="flex-1 flex flex-col gap-4 min-h-0"> {/* Async Mode Toggle */}
<div className="flex items-center gap-2 bg-muted/50 rounded-lg p-1">
<Button
variant={!asyncMode ? "default" : "ghost"}
size="sm"
onClick={() => setAsyncMode(false)}
className="h-7 text-xs"
>
Streaming Off
</Button>
<Button
variant={asyncMode ? "default" : "ghost"}
size="sm"
onClick={() => setAsyncMode(true)}
className="h-7 text-xs"
>
<Zap className="h-3 w-3 mr-1" />
Streaming On
</Button>
</div>
{/* Endpoint Toggle */}
<div className="flex items-center gap-2 bg-muted/50 rounded-lg p-1">
<Button
variant={endpoint === "chat" ? "default" : "ghost"}
size="sm"
onClick={() => handleEndpointChange("chat")}
className="h-7 text-xs"
>
Chat
</Button>
<Button
variant={endpoint === "langflow" ? "default" : "ghost"}
size="sm"
onClick={() => handleEndpointChange("langflow")}
className="h-7 text-xs"
>
Langflow
</Button>
</div>
</div>
</div>
)}
<div className="flex-1 flex flex-col min-h-0 px-6">
<div className="flex-1 flex flex-col gap-4 min-h-0 overflow-hidden">
{/* Messages Area */} {/* Messages Area */}
<div <div
className={`flex-1 overflow-y-auto overflow-x-hidden space-y-6 p-4 rounded-lg min-h-0 transition-all relative ${ className={`flex-1 overflow-y-auto overflow-x-hidden space-y-6 min-h-0 transition-all relative ${
isDragOver isDragOver
? 'bg-primary/10 border-2 border-dashed border-primary' ? 'bg-primary/10 border-2 border-dashed border-primary rounded-lg p-4'
: 'bg-muted/20' : ''
}`} }`}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragOver={handleDragOver} onDragOver={handleDragOver}
@ -1112,14 +1119,7 @@ function ChatPage() {
<p>Processing your document...</p> <p>Processing your document...</p>
<p className="text-sm mt-2">This may take a few moments</p> <p className="text-sm mt-2">This may take a few moments</p>
</> </>
) : ( ) : null}
<>
<MessageCircle className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Start a conversation by asking a question!</p>
<p className="text-sm mt-2">I can help you find information in your documents.</p>
<p className="text-xs mt-3 opacity-75">💡 Tip: Drag & drop a document here to add context</p>
</>
)}
</div> </div>
</div> </div>
) : ( ) : (
@ -1129,10 +1129,13 @@ function ChatPage() {
{message.role === "user" && ( {message.role === "user" && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center"> <Avatar className="w-8 h-8">
<User className="h-4 w-4 text-primary" /> <AvatarImage src={user?.picture} alt={user?.name} />
</div> <AvatarFallback className="text-sm bg-primary/20 text-primary">
<span className="font-medium text-foreground">User</span> {user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-4 w-4" />}
</AvatarFallback>
</Avatar>
<span className="font-medium text-foreground">{user?.name || "User"}</span>
</div> </div>
<div className="pl-10 max-w-full"> <div className="pl-10 max-w-full">
<p className="text-foreground whitespace-pre-wrap break-words overflow-wrap-anywhere">{message.content}</p> <p className="text-foreground whitespace-pre-wrap break-words overflow-wrap-anywhere">{message.content}</p>
@ -1147,7 +1150,6 @@ function ChatPage() {
<Bot className="h-4 w-4 text-accent-foreground" /> <Bot className="h-4 w-4 text-accent-foreground" />
</div> </div>
<span className="font-medium text-foreground">AI</span> <span className="font-medium text-foreground">AI</span>
<span className="text-sm text-muted-foreground">gpt-4.1</span>
</div> </div>
<div className="pl-10 max-w-full"> <div className="pl-10 max-w-full">
<div className="rounded-lg bg-card border border-border/40 p-4 max-w-full overflow-hidden"> <div className="rounded-lg bg-card border border-border/40 p-4 max-w-full overflow-hidden">
@ -1175,7 +1177,6 @@ function ChatPage() {
<Bot className="h-4 w-4 text-accent-foreground" /> <Bot className="h-4 w-4 text-accent-foreground" />
</div> </div>
<span className="font-medium text-foreground">AI</span> <span className="font-medium text-foreground">AI</span>
<span className="text-sm text-muted-foreground">gpt-4.1</span>
</div> </div>
<div className="pl-10 max-w-full"> <div className="pl-10 max-w-full">
<div className="rounded-lg bg-card border border-border/40 p-4 max-w-full overflow-hidden"> <div className="rounded-lg bg-card border border-border/40 p-4 max-w-full overflow-hidden">
@ -1203,7 +1204,6 @@ function ChatPage() {
<Bot className="h-4 w-4 text-accent-foreground" /> <Bot className="h-4 w-4 text-accent-foreground" />
</div> </div>
<span className="font-medium text-foreground">AI</span> <span className="font-medium text-foreground">AI</span>
<span className="text-sm text-muted-foreground">gpt-4.1</span>
</div> </div>
<div className="pl-10 max-w-full"> <div className="pl-10 max-w-full">
<div className="rounded-lg bg-card border border-border/40 p-4 max-w-full overflow-hidden"> <div className="rounded-lg bg-card border border-border/40 p-4 max-w-full overflow-hidden">
@ -1229,27 +1229,82 @@ function ChatPage() {
</div> </div>
)} )}
</div> </div>
</div>
{/* Input Area */} </div>
<form onSubmit={handleSubmit} className="flex gap-2 flex-shrink-0 w-full">
<Input {/* Suggestion chips - always show unless streaming */}
{!streamingMessage && (
<div className="flex-shrink-0 p-6 pb-4 flex justify-center">
<div className="w-full max-w-[75%] relative">
<div className="flex gap-2 justify-start overflow-hidden">
{suggestionChips.map((suggestion, index) => (
<button
key={index}
onClick={() => handleSuggestionClick(suggestion)}
className="px-4 py-2 bg-muted/30 hover:bg-muted/50 rounded-lg text-sm text-muted-foreground hover:text-foreground transition-colors whitespace-nowrap"
>
{suggestion}
</button>
))}
</div>
{/* Fade out gradient on the right */}
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-background to-transparent pointer-events-none"></div>
</div>
</div>
)}
{/* Input Area - Fixed at bottom */}
<div className="flex-shrink-0 p-6 pb-8 flex justify-center">
<div className="w-full max-w-[75%]">
<form onSubmit={handleSubmit} className="relative">
<textarea
ref={inputRef} ref={inputRef}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
placeholder="Ask a question about your documents..." onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
if (input.trim() && !loading) {
handleSubmit(e as any)
}
}
}}
placeholder="Type to ask a question..."
disabled={loading} disabled={loading}
className="flex-1 min-w-0" className="w-full bg-muted/20 rounded-lg border border-border/50 px-4 py-4 min-h-[100px] focus-visible:ring-1 focus-visible:ring-ring resize-none outline-none"
rows={1}
/> />
<Button type="submit" disabled={!input.trim() || loading}> <Button
type="button"
variant="ghost"
size="sm"
className="absolute bottom-3 left-3 h-8 w-8 p-0 rounded-full hover:bg-muted/50"
>
<AtSign className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute bottom-3 left-12 h-8 w-8 p-0 rounded-full hover:bg-muted/50"
>
<Plus className="h-4 w-4" />
</Button>
<Button
type="submit"
disabled={!input.trim() || loading}
className="absolute bottom-3 right-3 rounded-lg h-10 px-4"
>
{loading ? ( {loading ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : ( ) : (
<Send className="h-4 w-4" /> "Send"
)} )}
</Button> </Button>
</form> </form>
</CardContent> </div>
</Card> </div>
</div> </div>
) )
} }

View file

@ -8,8 +8,8 @@ function HomePage() {
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => {
// Redirect to knowledge sources page - the new home page // Redirect to chat page - the new home page
router.replace("/knowledge-sources") router.replace("/chat")
}, [router]) }, [router])
return null return null