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 { 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"
|
||||
|
||||
export function Navigation() {
|
||||
|
|
@ -27,6 +27,12 @@ export function Navigation() {
|
|||
href: "/chat",
|
||||
active: pathname === "/chat",
|
||||
},
|
||||
{
|
||||
label: "Contexts",
|
||||
icon: BookOpenCheck,
|
||||
href: "/contexts",
|
||||
active: pathname.startsWith("/contexts"),
|
||||
},
|
||||
{
|
||||
label: "Connectors",
|
||||
icon: PlugZap,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { useSearchParams } 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"
|
||||
|
|
@ -39,13 +40,23 @@ interface ToolCallResult {
|
|||
|
||||
type EndpointType = "chat" | "langflow"
|
||||
|
||||
interface SelectedFilters {
|
||||
data_sources: string[]
|
||||
document_types: string[]
|
||||
owners: string[]
|
||||
}
|
||||
|
||||
interface RequestBody {
|
||||
prompt: string
|
||||
stream?: boolean
|
||||
previous_response_id?: string
|
||||
filters?: SelectedFilters
|
||||
limit?: number
|
||||
scoreThreshold?: number
|
||||
}
|
||||
|
||||
function ChatPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [input, setInput] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
|
@ -68,6 +79,51 @@ function ChatPage() {
|
|||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
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 = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
|
|
@ -229,7 +285,10 @@ function ChatPage() {
|
|||
try {
|
||||
const requestBody: RequestBody = {
|
||||
prompt: userMessage.content,
|
||||
stream: true
|
||||
stream: true,
|
||||
filters: selectedFilters,
|
||||
limit: resultLimit,
|
||||
scoreThreshold: scoreThreshold
|
||||
}
|
||||
|
||||
// Add previous_response_id if we have one for this endpoint
|
||||
|
|
@ -685,7 +744,12 @@ function ChatPage() {
|
|||
try {
|
||||
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
|
||||
const currentResponseId = previousResponseIds[endpoint]
|
||||
|
|
@ -970,6 +1034,11 @@ function ChatPage() {
|
|||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
<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 className="flex items-center gap-4">
|
||||
{/* Async Mode Toggle */}
|
||||
|
|
@ -1016,6 +1085,11 @@ function ChatPage() {
|
|||
<CardDescription>
|
||||
Chat with AI about your indexed documents using {endpoint === "chat" ? "Chat" : "Langflow"} endpoint
|
||||
{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>
|
||||
</CardHeader>
|
||||
<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"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useSearchParams } 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 { Checkbox } from "@/components/ui/checkbox"
|
||||
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"
|
||||
|
||||
interface SearchResult {
|
||||
|
|
@ -61,6 +62,7 @@ interface SelectedFilters {
|
|||
}
|
||||
|
||||
function SearchPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const [query, setQuery] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [results, setResults] = useState<SearchResult[]>([])
|
||||
|
|
@ -79,7 +81,52 @@ function SearchPage() {
|
|||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||
const [resultLimit, setResultLimit] = useState(10)
|
||||
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) => {
|
||||
if (e) e.preventDefault()
|
||||
|
|
@ -229,6 +276,70 @@ function SearchPage() {
|
|||
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 = ({
|
||||
title,
|
||||
buckets,
|
||||
|
|
@ -306,9 +417,19 @@ function SearchPage() {
|
|||
<CardTitle className="flex items-center gap-2">
|
||||
<Search className="h-5 w-5" />
|
||||
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>
|
||||
<CardDescription>
|
||||
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>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
|
|
@ -370,12 +491,7 @@ function SearchPage() {
|
|||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Filter 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>
|
||||
)}
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -479,32 +595,14 @@ function SearchPage() {
|
|||
</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 && (
|
||||
<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">
|
||||
<Filter className="h-5 w-5" />
|
||||
Filters
|
||||
<Settings className="h-5 w-5" />
|
||||
Search Configuration
|
||||
</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 className="space-y-6">
|
||||
|
|
@ -530,6 +628,26 @@ function SearchPage() {
|
|||
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 */}
|
||||
<div className="space-y-4 pt-4 border-t border-border/50">
|
||||
<div className="space-y-2">
|
||||
|
|
@ -723,6 +841,35 @@ function SearchPage() {
|
|||
/>
|
||||
</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>
|
||||
)}
|
||||
|
|
@ -740,6 +887,76 @@ function SearchPage() {
|
|||
)}
|
||||
</CardContent>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue