A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
244 lines
8.2 KiB
TypeScript
244 lines
8.2 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* LLM Mode Switcher Component
|
|
*
|
|
* UI-Komponente zum Umschalten zwischen LLM-Modi:
|
|
* - Hybrid: Lokal zuerst, Cloud als Fallback
|
|
* - Nur Lokal: Maximaler Datenschutz (nur Ollama)
|
|
* - Nur Cloud: Beste Qualitaet (Claude/OpenAI)
|
|
* - Auto: Automatische Auswahl nach Komplexitaet
|
|
*
|
|
* Kann als standalone oder in andere Komponenten integriert verwendet werden.
|
|
*/
|
|
|
|
import { useLLMMode, LLMMode, LLM_MODE_CONFIGS } from '@/lib/llm-mode-context'
|
|
|
|
interface LLMModeSwitcherProps {
|
|
// Kompakte Ansicht (nur Icons) oder volle Ansicht (mit Beschreibungen)
|
|
compact?: boolean
|
|
// Callback wenn Modus geaendert wird
|
|
onModeChange?: (mode: LLMMode) => void
|
|
// Zeigt Provider-Details an
|
|
showProviderDetails?: boolean
|
|
// Custom className fuer den Container
|
|
className?: string
|
|
}
|
|
|
|
// Icons als SVG (Tailwind-freundlich)
|
|
const ModeIcon = ({ mode, className = 'w-5 h-5' }: { mode: LLMMode; className?: string }) => {
|
|
switch (mode) {
|
|
case 'hybrid':
|
|
return (
|
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
)
|
|
case 'local-only':
|
|
return (
|
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
)
|
|
case 'cloud-only':
|
|
return (
|
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
|
</svg>
|
|
)
|
|
case 'auto':
|
|
return (
|
|
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
)
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Mode colors for styling
|
|
const modeColors: Record<LLMMode, { bg: string; border: string; text: string; activeBg: string }> = {
|
|
hybrid: {
|
|
bg: 'bg-blue-50',
|
|
border: 'border-blue-300',
|
|
text: 'text-blue-700',
|
|
activeBg: 'bg-blue-100',
|
|
},
|
|
'local-only': {
|
|
bg: 'bg-green-50',
|
|
border: 'border-green-300',
|
|
text: 'text-green-700',
|
|
activeBg: 'bg-green-100',
|
|
},
|
|
'cloud-only': {
|
|
bg: 'bg-purple-50',
|
|
border: 'border-purple-300',
|
|
text: 'text-purple-700',
|
|
activeBg: 'bg-purple-100',
|
|
},
|
|
auto: {
|
|
bg: 'bg-amber-50',
|
|
border: 'border-amber-300',
|
|
text: 'text-amber-700',
|
|
activeBg: 'bg-amber-100',
|
|
},
|
|
}
|
|
|
|
export default function LLMModeSwitcher({
|
|
compact = false,
|
|
onModeChange,
|
|
showProviderDetails = false,
|
|
className = '',
|
|
}: LLMModeSwitcherProps) {
|
|
const { mode, setMode, config, enableOllama, enableClaude, enableOpenAI } = useLLMMode()
|
|
|
|
const handleModeChange = (newMode: LLMMode) => {
|
|
setMode(newMode)
|
|
onModeChange?.(newMode)
|
|
}
|
|
|
|
const modes = Object.keys(LLM_MODE_CONFIGS) as LLMMode[]
|
|
|
|
if (compact) {
|
|
// Compact view: horizontal button group
|
|
return (
|
|
<div className={`flex items-center gap-1 ${className}`}>
|
|
{modes.map((m) => {
|
|
const isActive = m === mode
|
|
const colors = modeColors[m]
|
|
return (
|
|
<button
|
|
key={m}
|
|
onClick={() => handleModeChange(m)}
|
|
className={`p-2 rounded-lg border transition-all ${
|
|
isActive
|
|
? `${colors.activeBg} ${colors.border} ${colors.text} border-2`
|
|
: 'bg-white border-slate-200 text-slate-500 hover:bg-slate-50'
|
|
}`}
|
|
title={`${LLM_MODE_CONFIGS[m].label}: ${LLM_MODE_CONFIGS[m].description}`}
|
|
>
|
|
<ModeIcon mode={m} className="w-5 h-5" />
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Full view: cards with descriptions
|
|
return (
|
|
<div className={className}>
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-slate-700">LLM-Modus</h3>
|
|
<span className={`text-xs px-2 py-1 rounded-full ${modeColors[mode].bg} ${modeColors[mode].text}`}>
|
|
{config.label}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{modes.map((m) => {
|
|
const modeConfig = LLM_MODE_CONFIGS[m]
|
|
const isActive = m === mode
|
|
const colors = modeColors[m]
|
|
|
|
return (
|
|
<button
|
|
key={m}
|
|
onClick={() => handleModeChange(m)}
|
|
className={`p-3 rounded-lg border-2 transition-all text-left ${
|
|
isActive
|
|
? `${colors.activeBg} ${colors.border} ${colors.text}`
|
|
: 'bg-white border-slate-200 text-slate-600 hover:border-slate-300 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<ModeIcon mode={m} className={`w-4 h-4 ${isActive ? colors.text : 'text-slate-400'}`} />
|
|
<span className={`text-sm font-medium ${isActive ? colors.text : 'text-slate-700'}`}>
|
|
{modeConfig.label}
|
|
</span>
|
|
</div>
|
|
<p className={`text-xs ${isActive ? colors.text : 'text-slate-500'}`}>
|
|
{modeConfig.description}
|
|
</p>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Provider Details */}
|
|
{showProviderDetails && (
|
|
<div className="mt-4 p-3 bg-slate-50 rounded-lg">
|
|
<p className="text-xs font-medium text-slate-600 mb-2">Aktive Provider:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{enableOllama && (
|
|
<span className="text-xs px-2 py-1 rounded bg-green-100 text-green-700">
|
|
Ollama (Lokal)
|
|
</span>
|
|
)}
|
|
{enableClaude && (
|
|
<span className="text-xs px-2 py-1 rounded bg-orange-100 text-orange-700">
|
|
Claude
|
|
</span>
|
|
)}
|
|
{enableOpenAI && (
|
|
<span className="text-xs px-2 py-1 rounded bg-emerald-100 text-emerald-700">
|
|
OpenAI
|
|
</span>
|
|
)}
|
|
{!enableOllama && !enableClaude && !enableOpenAI && (
|
|
<span className="text-xs px-2 py-1 rounded bg-red-100 text-red-700">
|
|
Keine Provider aktiv
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Export compact variant for use in headers/toolbars
|
|
export function LLMModeSwitcherCompact(props: Omit<LLMModeSwitcherProps, 'compact'>) {
|
|
return <LLMModeSwitcher {...props} compact />
|
|
}
|
|
|
|
// Export a dropdown variant for tight spaces
|
|
export function LLMModeSwitcherDropdown({
|
|
className = '',
|
|
onModeChange,
|
|
}: Pick<LLMModeSwitcherProps, 'className' | 'onModeChange'>) {
|
|
const { mode, setMode, config } = useLLMMode()
|
|
const colors = modeColors[mode]
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
const newMode = e.target.value as LLMMode
|
|
setMode(newMode)
|
|
onModeChange?.(newMode)
|
|
}
|
|
|
|
return (
|
|
<div className={`relative ${className}`}>
|
|
<select
|
|
value={mode}
|
|
onChange={handleChange}
|
|
className={`appearance-none pl-8 pr-8 py-2 rounded-lg border text-sm font-medium cursor-pointer ${colors.bg} ${colors.border} ${colors.text}`}
|
|
>
|
|
{(Object.keys(LLM_MODE_CONFIGS) as LLMMode[]).map((m) => (
|
|
<option key={m} value={m}>
|
|
{LLM_MODE_CONFIGS[m].label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<div className="absolute left-2 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
<ModeIcon mode={mode} className={`w-4 h-4 ${colors.text}`} />
|
|
</div>
|
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
<svg className={`w-4 h-4 ${colors.text}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|