Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
256 lines
8.6 KiB
TypeScript
256 lines
8.6 KiB
TypeScript
/**
|
|
* RAGSearchPanel Component
|
|
*
|
|
* Enhanced RAG search panel for teachers to query their Erwartungshorizonte.
|
|
* Features:
|
|
* - search_info display (which RAG features were active)
|
|
* - Confidence indicator based on re-ranking scores
|
|
* - Toggle for "Enhanced Search" with advanced options
|
|
*/
|
|
|
|
import { useState, useCallback } from 'react'
|
|
import { ehApi, EHRAGResult } from '../services/api'
|
|
|
|
interface RAGSearchPanelProps {
|
|
onClose: () => void
|
|
defaultSubject?: string
|
|
passphrase: string
|
|
}
|
|
|
|
interface SearchOptions {
|
|
rerank: boolean
|
|
limit: number
|
|
}
|
|
|
|
// Confidence level based on score
|
|
type ConfidenceLevel = 'high' | 'medium' | 'low'
|
|
|
|
function getConfidenceLevel(score: number): ConfidenceLevel {
|
|
if (score >= 0.8) return 'high'
|
|
if (score >= 0.5) return 'medium'
|
|
return 'low'
|
|
}
|
|
|
|
function getConfidenceColor(level: ConfidenceLevel): string {
|
|
switch (level) {
|
|
case 'high': return 'var(--bp-success, #22c55e)'
|
|
case 'medium': return 'var(--bp-warning, #f59e0b)'
|
|
case 'low': return 'var(--bp-danger, #ef4444)'
|
|
}
|
|
}
|
|
|
|
function getConfidenceLabel(level: ConfidenceLevel): string {
|
|
switch (level) {
|
|
case 'high': return 'Hohe Relevanz'
|
|
case 'medium': return 'Mittlere Relevanz'
|
|
case 'low': return 'Geringe Relevanz'
|
|
}
|
|
}
|
|
|
|
export default function RAGSearchPanel({ onClose, defaultSubject, passphrase }: RAGSearchPanelProps) {
|
|
const [query, setQuery] = useState('')
|
|
const [searching, setSearching] = useState(false)
|
|
const [results, setResults] = useState<EHRAGResult | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// Enhanced search options
|
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false)
|
|
const [options, setOptions] = useState<SearchOptions>({
|
|
rerank: true, // Default: enabled for better results
|
|
limit: 5
|
|
})
|
|
|
|
const handleSearch = useCallback(async () => {
|
|
if (!query.trim() || !passphrase) return
|
|
|
|
setSearching(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const result = await ehApi.ragQuery({
|
|
query_text: query,
|
|
passphrase: passphrase,
|
|
subject: defaultSubject,
|
|
limit: options.limit,
|
|
rerank: options.rerank
|
|
})
|
|
setResults(result)
|
|
} catch (err) {
|
|
console.error('RAG search failed:', err)
|
|
setError(err instanceof Error ? err.message : 'Suche fehlgeschlagen')
|
|
} finally {
|
|
setSearching(false)
|
|
}
|
|
}, [query, passphrase, defaultSubject, options])
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault()
|
|
handleSearch()
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="rag-search-overlay">
|
|
<div className="rag-search-modal">
|
|
{/* Header */}
|
|
<div className="rag-search-header">
|
|
<h2>Erwartungshorizont durchsuchen</h2>
|
|
<button className="rag-search-close" onClick={onClose}>×</button>
|
|
</div>
|
|
|
|
{/* Search Input */}
|
|
<div className="rag-search-input-container">
|
|
<textarea
|
|
className="rag-search-input"
|
|
placeholder="Suchen Sie nach relevanten Abschnitten im Erwartungshorizont..."
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
rows={2}
|
|
/>
|
|
<button
|
|
className="rag-search-btn"
|
|
onClick={handleSearch}
|
|
disabled={searching || !query.trim()}
|
|
>
|
|
{searching ? 'Sucht...' : 'Suchen'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Advanced Options Toggle */}
|
|
<div className="rag-advanced-toggle">
|
|
<button
|
|
className="rag-toggle-btn"
|
|
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
|
>
|
|
{showAdvancedOptions ? '▼' : '▶'} Erweiterte Optionen
|
|
</button>
|
|
</div>
|
|
|
|
{/* Advanced Options Panel */}
|
|
{showAdvancedOptions && (
|
|
<div className="rag-advanced-options">
|
|
<div className="rag-option-row">
|
|
<label className="rag-option-label">
|
|
<input
|
|
type="checkbox"
|
|
checked={options.rerank}
|
|
onChange={(e) => setOptions(prev => ({ ...prev, rerank: e.target.checked }))}
|
|
/>
|
|
<span className="rag-option-text">
|
|
Re-Ranking aktivieren
|
|
<span className="rag-option-hint">
|
|
(Cross-Encoder fuer hoehere Genauigkeit)
|
|
</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div className="rag-option-row">
|
|
<label className="rag-option-label">
|
|
Anzahl Ergebnisse:
|
|
<select
|
|
value={options.limit}
|
|
onChange={(e) => setOptions(prev => ({ ...prev, limit: Number(e.target.value) }))}
|
|
className="rag-option-select"
|
|
>
|
|
<option value={3}>3</option>
|
|
<option value={5}>5</option>
|
|
<option value={10}>10</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error Display */}
|
|
{error && (
|
|
<div className="rag-error">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Results */}
|
|
{results && (
|
|
<div className="rag-results">
|
|
{/* Search Info Badge */}
|
|
{results.search_info && (
|
|
<div className="rag-search-info">
|
|
<span className="rag-info-badge">
|
|
{results.search_info.rerank_applied && (
|
|
<span className="rag-info-tag rag-info-rerank">Re-Ranked</span>
|
|
)}
|
|
{results.search_info.hybrid_search_applied && (
|
|
<span className="rag-info-tag rag-info-hybrid">Hybrid Search</span>
|
|
)}
|
|
{results.search_info.embedding_model && (
|
|
<span className="rag-info-tag rag-info-model">
|
|
{results.search_info.embedding_model.split('/').pop()}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<span className="rag-info-count">
|
|
{results.search_info.total_candidates} Kandidaten gefiltert
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Context Summary */}
|
|
{results.context && (
|
|
<div className="rag-context-summary">
|
|
<h4>Zusammengefasster Kontext</h4>
|
|
<p>{results.context}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Source Results */}
|
|
<div className="rag-sources">
|
|
<h4>Relevante Abschnitte ({results.sources.length})</h4>
|
|
{results.sources.map((source, index) => {
|
|
const confidence = getConfidenceLevel(source.score)
|
|
return (
|
|
<div key={`${source.eh_id}-${source.chunk_index}`} className="rag-source-item">
|
|
<div className="rag-source-header">
|
|
<span className="rag-source-rank">#{index + 1}</span>
|
|
<span className="rag-source-title">{source.eh_title}</span>
|
|
<span
|
|
className="rag-confidence-badge"
|
|
style={{
|
|
backgroundColor: getConfidenceColor(confidence),
|
|
color: 'white'
|
|
}}
|
|
title={`Score: ${(source.score * 100).toFixed(1)}%`}
|
|
>
|
|
{getConfidenceLabel(confidence)}
|
|
{source.reranked && <span className="rag-reranked-icon" title="Re-ranked">✓</span>}
|
|
</span>
|
|
</div>
|
|
<div className="rag-source-text">
|
|
{source.text}
|
|
</div>
|
|
<div className="rag-source-meta">
|
|
<span>Chunk #{source.chunk_index}</span>
|
|
<span>Score: {(source.score * 100).toFixed(1)}%</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty State */}
|
|
{!results && !error && !searching && (
|
|
<div className="rag-empty-state">
|
|
<div className="rag-empty-icon">🔍</div>
|
|
<p>Geben Sie eine Suchanfrage ein, um relevante Abschnitte aus Ihren Erwartungshorizonten zu finden.</p>
|
|
<p className="rag-empty-hint">
|
|
Tipp: Aktivieren Sie "Re-Ranking" fuer praezisere Ergebnisse.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|