'use client' /** * AI Prompt Component * * Eingabezeile für Fragen an den lokalen Ollama-Server. * Unterstützt Streaming-Antworten und automatische Modell-Erkennung. */ import { useState, useEffect, useRef } from 'react' interface OllamaModel { name: string size: number digest: string } export default function AiPrompt() { const [prompt, setPrompt] = useState('') const [response, setResponse] = useState('') const [isLoading, setIsLoading] = useState(false) const [models, setModels] = useState([]) const [selectedModel, setSelectedModel] = useState('llama3.2:latest') const [showResponse, setShowResponse] = useState(false) const textareaRef = useRef(null) const abortControllerRef = useRef(null) // Lade verfügbare Modelle von Ollama useEffect(() => { const loadModels = async () => { try { const ollamaUrl = getOllamaBaseUrl() const res = await fetch(`${ollamaUrl}/api/tags`) if (res.ok) { const data = await res.json() if (data.models && data.models.length > 0) { setModels(data.models) setSelectedModel(data.models[0].name) } } } catch (error) { console.log('Ollama nicht erreichbar:', error) } } loadModels() }, []) const getOllamaBaseUrl = () => { if (typeof window !== 'undefined') { if (window.location.hostname === 'macmini') { return 'http://macmini:11434' } } return 'http://localhost:11434' } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() sendPrompt() } } const autoResize = () => { if (textareaRef.current) { textareaRef.current.style.height = 'auto' textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 120) + 'px' } } const sendPrompt = async () => { if (!prompt.trim() || isLoading) return // Vorherige Anfrage abbrechen if (abortControllerRef.current) { abortControllerRef.current.abort() } abortControllerRef.current = new AbortController() setIsLoading(true) setResponse('') setShowResponse(true) try { const ollamaUrl = getOllamaBaseUrl() const res = await fetch(`${ollamaUrl}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: selectedModel, prompt: prompt.trim(), stream: true, }), signal: abortControllerRef.current.signal, }) if (!res.ok) { throw new Error(`Ollama Fehler: ${res.status}`) } const reader = res.body?.getReader() const decoder = new TextDecoder() let fullResponse = '' if (reader) { while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value) const lines = chunk.split('\n').filter(l => l.trim()) for (const line of lines) { try { const data = JSON.parse(line) if (data.response) { fullResponse += data.response setResponse(fullResponse) } } catch { // Ignore JSON parse errors for partial chunks } } } } } catch (error) { if ((error as Error).name === 'AbortError') { setResponse('Anfrage abgebrochen.') } else { console.error('AI Prompt Fehler:', error) setResponse(`❌ Fehler: ${(error as Error).message}\n\nBitte prüfen Sie, ob Ollama läuft.`) } } finally { setIsLoading(false) abortControllerRef.current = null } } const formatResponse = (text: string) => { // Einfache Markdown-Formatierung return text .replace(/```(\w+)?\n([\s\S]*?)```/g, '
$2
') .replace(/`([^`]+)`/g, '$1') .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/\*([^*]+)\*/g, '$1') .replace(/\n/g, '
') } return (
{/* Header */}
🤖

KI-Assistent

Fragen Sie Ihren lokalen Ollama-Assistenten

{/* Input */}