Files
breakpilot-lehrer/website/components/admin/LLMModeSwitcher.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

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>
)
}