rename pages, improve knowledge and chat

This commit is contained in:
phact 2025-08-20 11:16:55 -04:00
parent 46077709bf
commit f2b407b4c8
4 changed files with 64 additions and 221 deletions

View file

@ -2,7 +2,7 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { Library, Database, MessageSquare, Settings2 } from "lucide-react"
import { Library, MessageSquare, Settings2 } from "lucide-react"
import { cn } from "@/lib/utils"
export function Navigation() {
@ -18,14 +18,14 @@ export function Navigation() {
{
label: "Knowledge",
icon: Library,
href: "/search",
active: pathname === "/search",
href: "/knowledge",
active: pathname === "/knowledge",
},
{
label: "Settings",
icon: Settings2,
href: "/knowledge-sources",
active: pathname === "/knowledge-sources",
href: "/settings",
active: pathname === "/settings",
},
]
@ -33,7 +33,7 @@ export function Navigation() {
<div className="space-y-4 py-4 flex flex-col h-full bg-background">
<div className="px-3 py-2 flex-1">
<div className="space-y-1">
{routes.map((route, index) => (
{routes.map((route) => (
<div key={route.href}>
<Link
href={route.href}

View file

@ -2,9 +2,7 @@
import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { MessageCircle, Send, Loader2, User, Bot, Zap, Settings, ChevronDown, ChevronRight, Upload, AtSign, Plus } from "lucide-react"
import { Loader2, User, Bot, Zap, Settings, ChevronDown, ChevronRight, Upload, AtSign, Plus } from "lucide-react"
import { ProtectedRoute } from "@/components/protected-route"
import { useTask } from "@/contexts/task-context"
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"
@ -85,7 +83,7 @@ function ChatPage() {
const [isDragOver, setIsDragOver] = useState(false)
const dragCounterRef = useRef(0)
const messagesEndRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
const { addTask } = useTask()
const { selectedFilter, parsedFilterData } = useKnowledgeFilter()
@ -1125,44 +1123,29 @@ function ChatPage() {
) : (
<>
{messages.map((message, index) => (
<div key={index} className="space-y-2">
<div key={index} className="space-y-6">
{message.role === "user" && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Avatar className="w-8 h-8">
<AvatarImage src={user?.picture} alt={user?.name} />
<AvatarFallback className="text-sm bg-primary/20 text-primary">
{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 className="pl-10 max-w-full">
<div className="flex gap-3">
<Avatar className="w-8 h-8 flex-shrink-0">
<AvatarImage src={user?.picture} alt={user?.name} />
<AvatarFallback className="text-sm bg-primary/20 text-primary">
{user?.name ? user.name.charAt(0).toUpperCase() : <User className="h-4 w-4" />}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="text-foreground whitespace-pre-wrap break-words overflow-wrap-anywhere">{message.content}</p>
</div>
</div>
)}
{message.role === "assistant" && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center">
<Bot className="h-4 w-4 text-accent-foreground" />
</div>
<span className="font-medium text-foreground">AI</span>
<div className="flex gap-3">
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0">
<Bot className="h-4 w-4 text-accent-foreground" />
</div>
<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="flex items-center gap-2 mb-2">
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
<span className="text-sm text-green-400 font-medium">Finished</span>
<span className="text-xs text-muted-foreground ml-auto">
{message.timestamp.toLocaleTimeString()}
</span>
</div>
{renderFunctionCalls(message.functionCalls || [], index)}
<p className="text-foreground whitespace-pre-wrap break-words overflow-wrap-anywhere">{message.content}</p>
</div>
<div className="flex-1">
{renderFunctionCalls(message.functionCalls || [], index)}
<p className="text-foreground whitespace-pre-wrap break-words overflow-wrap-anywhere">{message.content}</p>
</div>
</div>
)}
@ -1171,46 +1154,30 @@ function ChatPage() {
{/* Streaming Message Display */}
{streamingMessage && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center">
<Bot className="h-4 w-4 text-accent-foreground" />
</div>
<span className="font-medium text-foreground">AI</span>
<div className="flex gap-3">
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0">
<Bot className="h-4 w-4 text-accent-foreground" />
</div>
<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="flex items-center gap-2 mb-2">
<Loader2 className="w-4 h-4 animate-spin text-blue-400" />
<span className="text-sm text-blue-400 font-medium">Streaming...</span>
<span className="text-xs text-muted-foreground ml-auto">
{streamingMessage.timestamp.toLocaleTimeString()}
</span>
</div>
{renderFunctionCalls(streamingMessage.functionCalls, messages.length)}
<p className="text-foreground whitespace-pre-wrap break-words overflow-wrap-anywhere">
{streamingMessage.content}
<span className="inline-block w-2 h-4 bg-blue-400 ml-1 animate-pulse"></span>
</p>
</div>
<div className="flex-1">
{renderFunctionCalls(streamingMessage.functionCalls, messages.length)}
<p className="text-foreground whitespace-pre-wrap break-words overflow-wrap-anywhere">
{streamingMessage.content}
<span className="inline-block w-2 h-4 bg-blue-400 ml-1 animate-pulse"></span>
</p>
</div>
</div>
)}
{loading && !asyncMode && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center">
<Bot className="h-4 w-4 text-accent-foreground" />
</div>
<span className="font-medium text-foreground">AI</span>
{/* Loading animation - shows immediately after user submits */}
{loading && (
<div className="flex gap-3">
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0">
<Bot className="h-4 w-4 text-accent-foreground" />
</div>
<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="flex items-center gap-2 mb-2">
<Loader2 className="w-4 h-4 animate-spin text-white" />
<span className="text-sm text-white font-medium">Thinking...</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">Thinking...</span>
</div>
</div>
</div>
@ -1266,7 +1233,7 @@ function ChatPage() {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
if (input.trim() && !loading) {
handleSubmit(e as any)
handleSubmit(e as React.FormEvent<HTMLFormElement>)
}
}
}}

View file

@ -3,11 +3,9 @@
import { useState, useEffect, useCallback, useRef } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Badge } from "@/components/ui/badge"
import { Search, Loader2, FileText, Zap, RefreshCw } from "lucide-react"
import { Search, Loader2, FileText } from "lucide-react"
import { ProtectedRoute } from "@/components/protected-route"
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"
@ -30,7 +28,7 @@ interface SearchResponse {
function SearchPage() {
const { selectedFilter, parsedFilterData } = useKnowledgeFilter()
const { parsedFilterData } = useKnowledgeFilter()
const [query, setQuery] = useState("")
const [loading, setLoading] = useState(false)
const [results, setResults] = useState<SearchResult[]>([])
@ -161,12 +159,23 @@ function SearchPage() {
}, [parsedFilterData, searchPerformed, query, handleSearch])
// Fetch stats with current knowledge filter applied
const fetchStats = async () => {
const fetchStats = useCallback(async () => {
try {
setStatsLoading(true)
// Build search payload with current filter data
const searchPayload: any = {
interface SearchPayload {
query: string;
limit: number;
scoreThreshold: number;
filters?: {
data_sources?: string[];
document_types?: string[];
owners?: string[];
};
}
const searchPayload: SearchPayload = {
query: '*',
limit: 0,
scoreThreshold: parsedFilterData?.scoreThreshold || 0
@ -183,7 +192,7 @@ function SearchPage() {
!filters.owners.includes("*")
if (hasSpecificFilters) {
const processedFilters: any = {}
const processedFilters: SearchPayload['filters'] = {}
// Only add filter arrays that don't contain wildcards
if (!filters.data_sources.includes("*")) {
@ -227,12 +236,12 @@ function SearchPage() {
} finally {
setStatsLoading(false)
}
}
}, [parsedFilterData])
// Initial stats fetch and refresh when filter changes
useEffect(() => {
fetchStats()
}, [parsedFilterData])
}, [fetchStats])

View file

@ -62,11 +62,6 @@ function KnowledgeSourcesPage() {
const [syncResults, setSyncResults] = useState<{[key: string]: SyncResult | null}>({})
const [maxFiles, setMaxFiles] = useState<number>(10)
// Stats state (from wildcard search aggregations)
const [statsLoading, setStatsLoading] = useState<boolean>(false)
const [totalDocs, setTotalDocs] = useState<number>(0)
const [totalChunks, setTotalChunks] = useState<number>(0)
const [facetStats, setFacetStats] = useState<{ data_sources: FacetBucket[]; document_types: FacetBucket[]; owners: FacetBucket[] } | null>(null)
// File upload handlers
const handleDirectFileUpload = async (file: File) => {
@ -87,8 +82,6 @@ function KnowledgeSourcesPage() {
if (response.ok) {
setUploadStatus(`File processed successfully! ID: ${result.id}`)
// Refresh stats after successful file upload
fetchStats()
} else {
setUploadStatus(`Error: ${result.error || "Processing failed"}`)
}
@ -176,9 +169,6 @@ function KnowledgeSourcesPage() {
addTask(taskId)
setUploadStatus(`🔄 Processing started for ${totalFiles} files. Check the task notification panel for real-time progress. (Task ID: ${taskId})`)
setBucketUrl("s3://")
// Refresh stats after successful bucket upload
fetchStats()
} else {
setUploadStatus(`Error: ${result.error || "Bucket processing failed"}`)
}
@ -389,37 +379,6 @@ function KnowledgeSourcesPage() {
}
}, [searchParams, isAuthenticated, checkConnectorStatuses])
// Fetch global stats using match-all wildcard
const fetchStats = async () => {
try {
setStatsLoading(true)
const response = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: '*', limit: 0 })
})
const result = await response.json()
if (response.ok) {
const aggs = result.aggregations || {}
const toBuckets = (agg: { buckets?: Array<{ key: string | number; doc_count: number }> }): FacetBucket[] =>
(agg?.buckets || []).map(b => ({ key: String(b.key), count: b.doc_count }))
const dataSourceBuckets = toBuckets(aggs.data_sources)
setFacetStats({
data_sources: dataSourceBuckets.slice(0, 10),
document_types: toBuckets(aggs.document_types).slice(0, 10),
owners: toBuckets(aggs.owners).slice(0, 10)
})
// Frontend-only doc count: number of distinct filenames (data_sources buckets)
setTotalDocs(dataSourceBuckets.length)
// Chunk count from hits.total (match_all over chunks)
setTotalChunks(Number(result.total || 0))
}
} catch {
// non-fatal keep page functional without stats
} finally {
setStatsLoading(false)
}
}
// Check AWS availability
useEffect(() => {
@ -437,10 +396,6 @@ function KnowledgeSourcesPage() {
checkAws()
}, [])
// Initial stats fetch
useEffect(() => {
fetchStats()
}, [])
// Track previous tasks to detect new completions
const [prevTasks, setPrevTasks] = useState<typeof tasks>([])
@ -454,9 +409,9 @@ function KnowledgeSourcesPage() {
})
if (newlyCompletedTasks.length > 0) {
// Refresh stats when any task newly completes
// Task completed - could refresh data here if needed
const timeoutId = setTimeout(() => {
fetchStats()
// Stats refresh removed
}, 1000)
// Update previous tasks state
@ -471,95 +426,10 @@ function KnowledgeSourcesPage() {
return (
<div className="space-y-8">
{/* Hero Section */}
<div className="space-y-4">
<div className="mb-4">
<h1 className="text-3xl font-bold tracking-tight">
Knowledge Sources
</h1>
</div>
<p className="text-xl text-muted-foreground">
Add documents to your knowledge base
</p>
<p className="text-sm text-muted-foreground max-w-2xl">
Import files and folders directly, or connect external services like Google Drive to automatically sync and index your documents.
</p>
</div>
{/* Knowledge Overview Stats */}
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">Knowledge Overview</div>
<Button
variant="outline"
size="sm"
onClick={fetchStats}
disabled={statsLoading}
className="ml-auto"
>
{statsLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</Button>
</CardTitle>
<CardDescription>Snapshot of indexed content</CardDescription>
</CardHeader>
<CardContent>
{/* Documents row */}
<div className="grid gap-6 md:grid-cols-1">
<div>
<div className="text-sm text-muted-foreground mb-1">Total documents</div>
<div className="text-2xl font-semibold">{statsLoading ? '—' : totalDocs}</div>
</div>
</div>
{/* Separator */}
<div className="border-t border-border/50 my-6" />
{/* Chunks row */}
<div className="grid gap-6 md:grid-cols-4">
<div>
<div className="text-sm text-muted-foreground mb-1">Total chunks</div>
<div className="text-2xl font-semibold">{statsLoading ? '—' : totalChunks}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-2">Top types</div>
<div className="flex flex-wrap gap-2">
{(facetStats?.document_types || []).slice(0,5).map((b) => (
<Badge key={`type-${b.key}`} variant="secondary">{b.key} · {b.count}</Badge>
))}
</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-2">Top owners</div>
<div className="flex flex-wrap gap-2">
{(facetStats?.owners || []).slice(0,5).map((b) => (
<Badge key={`owner-${b.key}`} variant="secondary">{b.key || 'unknown'} · {b.count}</Badge>
))}
</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-2">Top files</div>
<div className="flex flex-wrap gap-2">
{(facetStats?.data_sources || []).slice(0,5).map((b) => (
<Badge key={`file-${b.key}`} variant="secondary" title={b.key}>{b.key} · {b.count}</Badge>
))}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Upload Section */}
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight mb-2">Direct Import</h2>
<p className="text-muted-foreground">
Add individual files or process entire folders from your local system
</p>
<h2 className="text-2xl font-semibold tracking-tight mb-2">Import</h2>
</div>
<div className={`grid gap-6 ${awsEnabled ? 'md:grid-cols-3' : 'md:grid-cols-2'}`}>
@ -686,10 +556,7 @@ function KnowledgeSourcesPage() {
{/* Connectors Section */}
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight mb-2">Connectors</h2>
<p className="text-muted-foreground">
Connect external services to automatically sync and index your documents
</p>
<h2 className="text-2xl font-semibold tracking-tight mb-2">Cloud Connectors</h2>
</div>
{/* Sync Settings */}