contexts ui, search context, chat with contexts
This commit is contained in:
parent
2675c2ff72
commit
c6decdccea
4 changed files with 676 additions and 32 deletions
|
|
@ -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, Settings, MessageCircle, PlugZap } from "lucide-react"
|
import { Search, Settings, MessageCircle, PlugZap, BookOpenCheck } from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export function Navigation() {
|
export function Navigation() {
|
||||||
|
|
@ -27,6 +27,12 @@ export function Navigation() {
|
||||||
href: "/chat",
|
href: "/chat",
|
||||||
active: pathname === "/chat",
|
active: pathname === "/chat",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Contexts",
|
||||||
|
icon: BookOpenCheck,
|
||||||
|
href: "/contexts",
|
||||||
|
active: pathname.startsWith("/contexts"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Connectors",
|
label: "Connectors",
|
||||||
icon: PlugZap,
|
icon: PlugZap,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react"
|
import { useState, useRef, useEffect } from "react"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
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"
|
||||||
|
|
@ -39,13 +40,23 @@ interface ToolCallResult {
|
||||||
|
|
||||||
type EndpointType = "chat" | "langflow"
|
type EndpointType = "chat" | "langflow"
|
||||||
|
|
||||||
|
interface SelectedFilters {
|
||||||
|
data_sources: string[]
|
||||||
|
document_types: string[]
|
||||||
|
owners: string[]
|
||||||
|
}
|
||||||
|
|
||||||
interface RequestBody {
|
interface RequestBody {
|
||||||
prompt: string
|
prompt: string
|
||||||
stream?: boolean
|
stream?: boolean
|
||||||
previous_response_id?: string
|
previous_response_id?: string
|
||||||
|
filters?: SelectedFilters
|
||||||
|
limit?: number
|
||||||
|
scoreThreshold?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatPage() {
|
function ChatPage() {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
const [input, setInput] = useState("")
|
const [input, setInput] = useState("")
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
@ -68,6 +79,51 @@ function ChatPage() {
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const { addTask } = useTask()
|
const { addTask } = useTask()
|
||||||
|
|
||||||
|
// Context-related state
|
||||||
|
const [selectedFilters, setSelectedFilters] = useState<SelectedFilters>({
|
||||||
|
data_sources: [],
|
||||||
|
document_types: [],
|
||||||
|
owners: []
|
||||||
|
})
|
||||||
|
const [resultLimit, setResultLimit] = useState(10)
|
||||||
|
const [scoreThreshold, setScoreThreshold] = useState(0)
|
||||||
|
const [loadedContextName, setLoadedContextName] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Load context if contextId is provided in URL
|
||||||
|
useEffect(() => {
|
||||||
|
const contextId = searchParams.get('contextId')
|
||||||
|
if (contextId) {
|
||||||
|
loadContext(contextId)
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const loadContext = async (contextId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/contexts/${contextId}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
const context = result.context
|
||||||
|
const parsedQueryData = JSON.parse(context.query_data)
|
||||||
|
|
||||||
|
// Load the context data into state
|
||||||
|
setSelectedFilters(parsedQueryData.filters)
|
||||||
|
setResultLimit(parsedQueryData.limit)
|
||||||
|
setScoreThreshold(parsedQueryData.scoreThreshold)
|
||||||
|
setLoadedContextName(context.name)
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load context:", result.error)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading context:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +285,10 @@ function ChatPage() {
|
||||||
try {
|
try {
|
||||||
const requestBody: RequestBody = {
|
const requestBody: RequestBody = {
|
||||||
prompt: userMessage.content,
|
prompt: userMessage.content,
|
||||||
stream: true
|
stream: true,
|
||||||
|
filters: selectedFilters,
|
||||||
|
limit: resultLimit,
|
||||||
|
scoreThreshold: scoreThreshold
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add previous_response_id if we have one for this endpoint
|
// Add previous_response_id if we have one for this endpoint
|
||||||
|
|
@ -685,7 +744,12 @@ function ChatPage() {
|
||||||
try {
|
try {
|
||||||
const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow"
|
const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow"
|
||||||
|
|
||||||
const requestBody: RequestBody = { prompt: userMessage.content }
|
const requestBody: RequestBody = {
|
||||||
|
prompt: userMessage.content,
|
||||||
|
filters: selectedFilters,
|
||||||
|
limit: resultLimit,
|
||||||
|
scoreThreshold: scoreThreshold
|
||||||
|
}
|
||||||
|
|
||||||
// Add previous_response_id if we have one for this endpoint
|
// Add previous_response_id if we have one for this endpoint
|
||||||
const currentResponseId = previousResponseIds[endpoint]
|
const currentResponseId = previousResponseIds[endpoint]
|
||||||
|
|
@ -970,6 +1034,11 @@ function ChatPage() {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MessageCircle className="h-5 w-5" />
|
<MessageCircle className="h-5 w-5" />
|
||||||
<CardTitle>Chat</CardTitle>
|
<CardTitle>Chat</CardTitle>
|
||||||
|
{loadedContextName && (
|
||||||
|
<span className="text-sm font-normal text-blue-400 bg-blue-400/10 px-2 py-1 rounded">
|
||||||
|
Context: {loadedContextName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Async Mode Toggle */}
|
{/* Async Mode Toggle */}
|
||||||
|
|
@ -1016,6 +1085,11 @@ function ChatPage() {
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Chat with AI about your indexed documents using {endpoint === "chat" ? "Chat" : "Langflow"} endpoint
|
Chat with AI about your indexed documents using {endpoint === "chat" ? "Chat" : "Langflow"} endpoint
|
||||||
{asyncMode ? " with real-time streaming" : ""}
|
{asyncMode ? " with real-time streaming" : ""}
|
||||||
|
{loadedContextName && (
|
||||||
|
<span className="block text-blue-400 text-xs mt-1">
|
||||||
|
Using search context with configured filters and settings
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1 flex flex-col gap-4 min-h-0">
|
<CardContent className="flex-1 flex flex-col gap-4 min-h-0">
|
||||||
|
|
|
||||||
347
frontend/src/app/contexts/page.tsx
Normal file
347
frontend/src/app/contexts/page.tsx
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
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 { Search, Loader2, BookOpenCheck, Settings, Calendar, MessageCircle } from "lucide-react"
|
||||||
|
import { ProtectedRoute } from "@/components/protected-route"
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
query_data: string
|
||||||
|
owner: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParsedQueryData {
|
||||||
|
query: string
|
||||||
|
filters: {
|
||||||
|
data_sources: string[]
|
||||||
|
document_types: string[]
|
||||||
|
owners: string[]
|
||||||
|
}
|
||||||
|
limit: number
|
||||||
|
scoreThreshold: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextsPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [contexts, setContexts] = useState<Context[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
const [selectedContext, setSelectedContext] = useState<Context | null>(null)
|
||||||
|
const [parsedQueryData, setParsedQueryData] = useState<ParsedQueryData | null>(null)
|
||||||
|
|
||||||
|
const loadContexts = async (query = "") => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/contexts/search", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
limit: 50
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
setContexts(result.contexts)
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load contexts:", result.error)
|
||||||
|
setContexts([])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading contexts:", error)
|
||||||
|
setContexts([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadContexts()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSearch = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
await loadContexts(searchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleContextClick = (context: Context) => {
|
||||||
|
setSelectedContext(context)
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(context.query_data) as ParsedQueryData
|
||||||
|
setParsedQueryData(parsed)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing query data:", error)
|
||||||
|
setParsedQueryData(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearchWithContext = () => {
|
||||||
|
if (!selectedContext) return
|
||||||
|
|
||||||
|
router.push(`/?contextId=${selectedContext.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChatWithContext = () => {
|
||||||
|
if (!selectedContext) return
|
||||||
|
|
||||||
|
router.push(`/chat?contextId=${selectedContext.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight text-white">
|
||||||
|
Contexts
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-xl text-muted-foreground">
|
||||||
|
Manage your saved search contexts
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground max-w-2xl">
|
||||||
|
View and manage your saved search queries, filters, and configurations for quick access to your most important searches.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Interface */}
|
||||||
|
<Card className="w-full bg-card/50 backdrop-blur-sm border-border/50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<BookOpenCheck className="h-5 w-5" />
|
||||||
|
Search Contexts
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Search through your saved search contexts by name or description
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<form onSubmit={handleSearch} className="space-y-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search contexts..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="h-12 bg-background/50 border-border/50 focus:border-blue-400/50 focus:ring-blue-400/20 flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="h-12 px-6 transition-all duration-200"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||||
|
Searching...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Search className="mr-2 h-5 w-5" />
|
||||||
|
Search
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="flex gap-6">
|
||||||
|
{/* Context List */}
|
||||||
|
<div className="flex-1 space-y-4">
|
||||||
|
{contexts.length === 0 ? (
|
||||||
|
<Card className="bg-muted/20 border-dashed border-muted-foreground/30">
|
||||||
|
<CardContent className="pt-8 pb-8">
|
||||||
|
<div className="text-center space-y-3">
|
||||||
|
<div className="mx-auto w-16 h-16 bg-muted/30 rounded-full flex items-center justify-center">
|
||||||
|
<BookOpenCheck className="h-8 w-8 text-muted-foreground/50" />
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-medium text-muted-foreground">
|
||||||
|
No contexts found
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground/70 max-w-md mx-auto">
|
||||||
|
Create your first context by saving a search configuration from the search page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{contexts.map((context) => (
|
||||||
|
<Card
|
||||||
|
key={context.id}
|
||||||
|
className={`bg-card/50 backdrop-blur-sm border-border/50 hover:bg-card/70 transition-all duration-200 hover:shadow-lg hover:shadow-blue-500/10 cursor-pointer ${
|
||||||
|
selectedContext?.id === context.id ? 'ring-2 ring-blue-500/50 bg-card/70' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => handleContextClick(context)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<h3 className="font-semibold text-lg">{context.name}</h3>
|
||||||
|
{context.description && (
|
||||||
|
<p className="text-sm text-muted-foreground">{context.description}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
<span>Created {formatDate(context.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
{context.updated_at !== context.created_at && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
<span>Updated {formatDate(context.updated_at)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Context Detail Panel */}
|
||||||
|
{selectedContext && parsedQueryData && (
|
||||||
|
<div className="w-64 space-y-6 flex-shrink-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<Settings className="h-5 w-5" />
|
||||||
|
Context Details
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Query Information */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium">Query</Label>
|
||||||
|
<div className="p-3 bg-muted/50 rounded-md">
|
||||||
|
<p className="text-sm">{parsedQueryData.query}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
{(parsedQueryData.filters.data_sources.length > 0 ||
|
||||||
|
parsedQueryData.filters.document_types.length > 0 ||
|
||||||
|
parsedQueryData.filters.owners.length > 0) && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label className="text-sm font-medium">Filters</Label>
|
||||||
|
|
||||||
|
{parsedQueryData.filters.data_sources.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs text-muted-foreground">Data Sources</Label>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{parsedQueryData.filters.data_sources.map((source, index) => (
|
||||||
|
<div key={index} className="px-2 py-1 bg-muted/30 rounded text-xs">
|
||||||
|
{source}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{parsedQueryData.filters.document_types.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs text-muted-foreground">Document Types</Label>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{parsedQueryData.filters.document_types.map((type, index) => (
|
||||||
|
<div key={index} className="px-2 py-1 bg-muted/30 rounded text-xs">
|
||||||
|
{type}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{parsedQueryData.filters.owners.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs text-muted-foreground">Owners</Label>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{parsedQueryData.filters.owners.map((owner, index) => (
|
||||||
|
<div key={index} className="px-2 py-1 bg-muted/30 rounded text-xs">
|
||||||
|
{owner}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Search Settings */}
|
||||||
|
<div className="space-y-4 pt-4 border-t border-border/50">
|
||||||
|
<Label className="text-sm font-medium">Search Settings</Label>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label className="text-xs text-muted-foreground">Limit</Label>
|
||||||
|
<span className="text-sm font-mono">{parsedQueryData.limit}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Label className="text-xs text-muted-foreground">Score Threshold</Label>
|
||||||
|
<span className="text-sm font-mono">{parsedQueryData.scoreThreshold}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="space-y-2 pt-4 border-t border-border/50">
|
||||||
|
<Button
|
||||||
|
onClick={handleSearchWithContext}
|
||||||
|
className="w-full flex items-center gap-2"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
Search with Context
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleChatWithContext}
|
||||||
|
className="w-full flex items-center gap-2"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<MessageCircle className="h-4 w-4" />
|
||||||
|
Chat with Context
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProtectedContextsPage() {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ContextsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
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 { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||||
import { Search, Loader2, FileText, Zap, ChevronDown, ChevronUp, Filter, X } from "lucide-react"
|
import { Search, Loader2, FileText, Zap, ChevronDown, ChevronUp, Filter, X, Settings, Save } from "lucide-react"
|
||||||
import { ProtectedRoute } from "@/components/protected-route"
|
import { ProtectedRoute } from "@/components/protected-route"
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
|
|
@ -61,6 +62,7 @@ interface SelectedFilters {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchPage() {
|
function SearchPage() {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const [query, setQuery] = useState("")
|
const [query, setQuery] = useState("")
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [results, setResults] = useState<SearchResult[]>([])
|
const [results, setResults] = useState<SearchResult[]>([])
|
||||||
|
|
@ -79,7 +81,52 @@ function SearchPage() {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||||
const [resultLimit, setResultLimit] = useState(10)
|
const [resultLimit, setResultLimit] = useState(10)
|
||||||
const [scoreThreshold, setScoreThreshold] = useState(0)
|
const [scoreThreshold, setScoreThreshold] = useState(0)
|
||||||
|
const [showSaveModal, setShowSaveModal] = useState(false)
|
||||||
|
const [contextTitle, setContextTitle] = useState("")
|
||||||
|
const [contextDescription, setContextDescription] = useState("")
|
||||||
|
const [savingContext, setSavingContext] = useState(false)
|
||||||
|
const [loadedContextName, setLoadedContextName] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Load context if contextId is provided in URL
|
||||||
|
useEffect(() => {
|
||||||
|
const contextId = searchParams.get('contextId')
|
||||||
|
if (contextId) {
|
||||||
|
loadContext(contextId)
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const loadContext = async (contextId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/contexts/${contextId}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
const context = result.context
|
||||||
|
const parsedQueryData = JSON.parse(context.query_data)
|
||||||
|
|
||||||
|
// Load the context data into state
|
||||||
|
setQuery(parsedQueryData.query)
|
||||||
|
setSelectedFilters(parsedQueryData.filters)
|
||||||
|
setResultLimit(parsedQueryData.limit)
|
||||||
|
setScoreThreshold(parsedQueryData.scoreThreshold)
|
||||||
|
setLoadedContextName(context.name)
|
||||||
|
|
||||||
|
// Automatically perform the search
|
||||||
|
setTimeout(() => {
|
||||||
|
handleSearch()
|
||||||
|
}, 100)
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load context:", result.error)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading context:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearch = async (e?: React.FormEvent) => {
|
const handleSearch = async (e?: React.FormEvent) => {
|
||||||
if (e) e.preventDefault()
|
if (e) e.preventDefault()
|
||||||
|
|
@ -229,6 +276,70 @@ function SearchPage() {
|
||||||
selectedFilters.owners.length
|
selectedFilters.owners.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSaveContext = async () => {
|
||||||
|
const contextId = searchParams.get('contextId')
|
||||||
|
|
||||||
|
// If no contextId present and no title, we need the modal
|
||||||
|
if (!contextId && !contextTitle.trim()) return
|
||||||
|
|
||||||
|
setSavingContext(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contextData = {
|
||||||
|
query,
|
||||||
|
filters: selectedFilters,
|
||||||
|
limit: resultLimit,
|
||||||
|
scoreThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (contextId) {
|
||||||
|
// Update existing context (upsert)
|
||||||
|
response = await fetch(`/api/contexts/${contextId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
queryData: JSON.stringify(contextData)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Create new context
|
||||||
|
response = await fetch("/api/contexts", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: contextTitle,
|
||||||
|
description: contextDescription,
|
||||||
|
queryData: JSON.stringify(contextData)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
if (!contextId) {
|
||||||
|
// Reset modal state only if we were creating a new context
|
||||||
|
setShowSaveModal(false)
|
||||||
|
setContextTitle("")
|
||||||
|
setContextDescription("")
|
||||||
|
}
|
||||||
|
console.log(contextId ? "Context updated successfully:" : "Context saved successfully:", result)
|
||||||
|
} else {
|
||||||
|
console.error(contextId ? "Failed to update context:" : "Failed to save context:", result.error)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(contextId ? "Error updating context:" : "Error saving context:", error)
|
||||||
|
} finally {
|
||||||
|
setSavingContext(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const FacetSection = ({
|
const FacetSection = ({
|
||||||
title,
|
title,
|
||||||
buckets,
|
buckets,
|
||||||
|
|
@ -306,9 +417,19 @@ function SearchPage() {
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Search className="h-5 w-5" />
|
<Search className="h-5 w-5" />
|
||||||
Search Documents
|
Search Documents
|
||||||
|
{loadedContextName && (
|
||||||
|
<span className="text-sm font-normal text-blue-400 bg-blue-400/10 px-2 py-1 rounded">
|
||||||
|
Context: {loadedContextName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Enter your search query to find relevant documents using hybrid search (semantic + keyword)
|
Enter your search query to find relevant documents using hybrid search (semantic + keyword)
|
||||||
|
{loadedContextName && (
|
||||||
|
<span className="block text-blue-400 text-xs mt-1">
|
||||||
|
Search configuration loaded from saved context
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
|
|
@ -370,12 +491,7 @@ function SearchPage() {
|
||||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Filter className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
{getSelectedFilterCount() > 0 && (
|
|
||||||
<span className="bg-primary text-primary-foreground text-xs px-1.5 py-0.5 rounded">
|
|
||||||
{getSelectedFilterCount()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -479,32 +595,14 @@ function SearchPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Sidebar - Filters */}
|
{/* Right Sidebar - Settings */}
|
||||||
{((facets.data_sources?.length ?? 0) > 0 || (facets.document_types?.length ?? 0) > 0 || (facets.owners?.length ?? 0) > 0) && sidebarOpen && (
|
{((facets.data_sources?.length ?? 0) > 0 || (facets.document_types?.length ?? 0) > 0 || (facets.owners?.length ?? 0) > 0) && sidebarOpen && (
|
||||||
<div className="w-64 space-y-6 flex-shrink-0">
|
<div className="w-64 space-y-6 flex-shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||||
<Filter className="h-5 w-5" />
|
<Settings className="h-5 w-5" />
|
||||||
Filters
|
Search Configuration
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={selectAllFilters}
|
|
||||||
className="text-xs h-auto px-2 py-1"
|
|
||||||
>
|
|
||||||
All
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={clearAllFilters}
|
|
||||||
className="text-xs h-auto px-2 py-1"
|
|
||||||
>
|
|
||||||
None
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -530,6 +628,26 @@ function SearchPage() {
|
||||||
onToggle={() => toggleSection('owners')}
|
onToggle={() => toggleSection('owners')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* All/None buttons - moved below facets */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={selectAllFilters}
|
||||||
|
className="h-auto px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 border-border/50"
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearAllFilters}
|
||||||
|
className="h-auto px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 border-border/50"
|
||||||
|
>
|
||||||
|
None
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Result Limit Control */}
|
{/* Result Limit Control */}
|
||||||
<div className="space-y-4 pt-4 border-t border-border/50">
|
<div className="space-y-4 pt-4 border-t border-border/50">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -723,6 +841,35 @@ function SearchPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Save Context Button */}
|
||||||
|
<div className="pt-4 border-t border-border/50">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const contextId = searchParams.get('contextId')
|
||||||
|
if (contextId) {
|
||||||
|
handleSaveContext()
|
||||||
|
} else {
|
||||||
|
setShowSaveModal(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!searchPerformed || !query.trim() || savingContext}
|
||||||
|
className="w-full flex items-center gap-2"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{savingContext ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
{searchParams.get('contextId') ? 'Updating...' : 'Saving...'}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
{searchParams.get('contextId') ? 'Update Context' : 'Save Context'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -740,6 +887,76 @@ function SearchPage() {
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Save Context Modal */}
|
||||||
|
{showSaveModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-card border border-border rounded-lg p-6 w-full max-w-md mx-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Save Search Context</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="context-title" className="font-medium">
|
||||||
|
Title <span className="text-red-400">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="context-title"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter a title for this search context"
|
||||||
|
value={contextTitle}
|
||||||
|
onChange={(e) => setContextTitle(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="context-description" className="font-medium">
|
||||||
|
Description (optional)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="context-description"
|
||||||
|
type="text"
|
||||||
|
placeholder="Brief description of this search context"
|
||||||
|
value={contextDescription}
|
||||||
|
onChange={(e) => setContextDescription(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 mt-6">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShowSaveModal(false)
|
||||||
|
setContextTitle("")
|
||||||
|
setContextDescription("")
|
||||||
|
}}
|
||||||
|
disabled={savingContext}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveContext}
|
||||||
|
disabled={!contextTitle.trim() || savingContext}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{savingContext ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
Save Context
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue