feat: unified_search + ai_proactive plugins with Ollama Cloud DeepSeek V4
- unified_search: Hybride Suche (PostgreSQL FTS + pgvector + RRF Fusion) - 5 Search Providers (Contact, Company, Mail, File, Event) - KI Query Understanding (Fuzzy, Facetten via LiteLLM) - DMS Text-Extraction (PDF, DOCX, XLSX, PPTX) - Embedding Pipeline (ollama/nomic-embed-text, 768 Dim) - Background Jobs für Indexierung - Plugin-basierte Provider Registry - ai_proactive: Proaktiver KI-Agent - Context-Tracking (Frontend → Backend → Event Bus) - Proactive Engine mit LLM Suggestion-Generierung - SSE Real-time Push an Frontend - 6 AI Tools für Tool Registry - Rate-Limiting + User Settings - Deep Analysis Background Jobs - Frontend Integration: - useAIContext Hook, SuggestionSidebar, SuggestionBadge - ProactiveAISettings Page, Search API Client - Globale Suche auf neue API umgestellt - Tests: test_unified_search.py + test_ai_proactive.py (alle bestanden) - Config: Ollama Cloud DeepSeek V4 als Default, konfigurierbar - Dependencies: PyMuPDF, python-docx, python-pptx, pgvector - Bugfixes: notification type_key length, migration IF NOT EXISTS
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { useState } from 'react';
|
||||
import type { Suggestion } from './api';
|
||||
|
||||
const typeConfig = {
|
||||
info: { icon: '💡', color: 'blue', bg: 'bg-blue-50', border: 'border-blue-200', text: 'text-blue-700', bar: 'bg-blue-500' },
|
||||
warning: { icon: '⚠️', color: 'orange', bg: 'bg-orange-50', border: 'border-orange-200', text: 'text-orange-700', bar: 'bg-orange-500' },
|
||||
action: { icon: '✅', color: 'green', bg: 'bg-green-50', border: 'border-green-200', text: 'text-green-700', bar: 'bg-green-500' },
|
||||
insight: { icon: '🔍', color: 'purple', bg: 'bg-purple-50', border: 'border-purple-200', text: 'text-purple-700', bar: 'bg-purple-500' },
|
||||
};
|
||||
|
||||
interface SuggestionCardProps {
|
||||
suggestion: Suggestion;
|
||||
onDismiss: (id: string) => void;
|
||||
onAct: (id: string, actionIndex: number) => void;
|
||||
}
|
||||
|
||||
export function SuggestionCard({ suggestion, onDismiss, onAct }: SuggestionCardProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const config = typeConfig[suggestion.suggestion_type] || typeConfig.info;
|
||||
const confidencePercent = Math.round(suggestion.confidence * 100);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`animate-slide-in ${config.bg} ${config.border} border rounded-lg p-4 mb-3 shadow-sm hover:shadow-md transition-shadow`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{config.icon}</span>
|
||||
<h3 className={`font-semibold text-sm ${config.text}`}>{suggestion.title}</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDismiss(suggestion.id)}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title="Ignorieren"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<p className="text-sm text-gray-600 mb-3 line-clamp-3">{suggestion.content}</p>
|
||||
|
||||
{/* Confidence Bar */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex-1 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${config.bar} rounded-full transition-all duration-500`}
|
||||
style={{ width: `${confidencePercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 font-mono">{confidencePercent}%</span>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{suggestion.actions && suggestion.actions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{suggestion.actions.slice(0, expanded ? undefined : 2).map((action, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => onAct(suggestion.id, idx)}
|
||||
disabled={suggestion.is_acted_upon}
|
||||
className={`w-full text-left px-3 py-2 rounded-md text-xs font-medium transition-all ${
|
||||
suggestion.is_acted_upon
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-white hover:bg-gray-50 text-gray-700 border border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span className="font-mono text-[10px] uppercase mr-1 px-1 py-0.5 bg-gray-100 rounded">
|
||||
{action.method}
|
||||
</span>
|
||||
{action.description}
|
||||
</button>
|
||||
))}
|
||||
{suggestion.actions.length > 2 && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{expanded ? 'Weniger' : `${suggestion.actions.length - 2} weitere Aktionen`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Acted upon badge */}
|
||||
{suggestion.is_acted_upon && (
|
||||
<div className="mt-2 text-xs text-green-600 font-medium flex items-center gap-1">
|
||||
✓ Ausgeführt
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user