Initial commit: IT Nexus Web-App

This commit is contained in:
2026-06-01 20:49:07 +02:00
commit 8023765e6c
387 changed files with 106900 additions and 0 deletions

View File

@@ -0,0 +1,592 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import aiService from '../services/aiService';
import ticketService from '../services/ticketService';
import { toast } from 'react-toastify';
import { Marked } from 'marked';
const marked = new Marked({ breaks: true, gfm: true });
const renderMd = (text) => ({ __html: marked.parse(text || '') });
const CATEGORIES = ['Allgemein', 'Software', 'Hardware', 'Netzwerk', 'SelectLine', 'Sonstiges'];
const CAT_COLOR = { Software: '#6366f1', Hardware: '#f59e0b', SelectLine: '#10b981', Netzwerk: '#3b82f6', Allgemein: '#64748b', Sonstiges: '#8b5cf6' };
const catColor = (c) => CAT_COLOR[c] || '#64748b';
const EMPTY_FORM = { problem: '', solution: '', category: 'Allgemein', tags: '' };
export default function KnowledgeBasePage() {
const { isAdmin, isSuperAdmin } = useAuth();
const canEdit = isAdmin() || isSuperAdmin();
// Tab state
const [tab, setTab] = useState('artikel');
// KB Articles state
const [articles, setArticles] = useState([]);
const [loadingArt, setLoadingArt] = useState(true);
const [searchArt, setSearchArt] = useState('');
const [catFilter, setCatFilter] = useState('Alle');
const [expanded, setExpanded] = useState(null);
const [showModal, setShowModal] = useState(false);
const [editingId, setEditingId] = useState(null);
const [form, setForm] = useState(EMPTY_FORM);
const [saving, setSaving] = useState(false);
const [showImport, setShowImport] = useState(false);
const [importTab, setImportTab] = useState('text'); // 'text' | 'file' | 'url'
const [importText, setImportText] = useState('');
const [importFile, setImportFile] = useState(null);
const [importUrl, setImportUrl] = useState('');
const [importCrawl, setImportCrawl] = useState(false);
const [importCrawlMax, setImportCrawlMax] = useState(20);
const [importing, setImporting] = useState(false);
// Closed tickets state
const [tickets, setTickets] = useState([]);
const [loadingTickets, setLoadingTickets] = useState(false);
const [searchTickets, setSearchTickets] = useState('');
const [ticketCat, setTicketCat] = useState('Alle');
const [ticketsLoaded, setTicketsLoaded] = useState(false);
const loadArticles = useCallback(async () => {
setLoadingArt(true);
try {
const data = await aiService.getKnowledgeBase();
setArticles(data);
} catch { toast.error('Fehler beim Laden der Wissensdatenbank'); }
finally { setLoadingArt(false); }
}, []);
const loadTickets = useCallback(async () => {
if (ticketsLoaded) return;
setLoadingTickets(true);
try {
const data = await ticketService.getAll({ status: 'geschlossen' });
setTickets(Array.isArray(data) ? data : []);
setTicketsLoaded(true);
} catch { setTickets([]); }
finally { setLoadingTickets(false); }
}, [ticketsLoaded]);
useEffect(() => { loadArticles(); }, [loadArticles]);
useEffect(() => { if (tab === 'tickets') loadTickets(); }, [tab, loadTickets]);
// ── Article handlers ─────────────────────────────────────────────────────
const openCreate = () => {
setEditingId(null);
setForm(EMPTY_FORM);
setShowModal(true);
};
const openEdit = (art, e) => {
e.stopPropagation();
setEditingId(art.id);
setForm({ problem: art.problem, solution: art.solution, category: art.category || 'Allgemein', tags: art.tags || '' });
setShowModal(true);
};
const handleSave = async (e) => {
e.preventDefault();
setSaving(true);
try {
if (editingId) {
await aiService.updateKnowledgeEntry(editingId, form);
toast.success('Artikel aktualisiert');
} else {
await aiService.addKnowledgeEntry(form);
toast.success('Artikel erstellt');
}
setShowModal(false);
loadArticles();
} catch { toast.error('Fehler beim Speichern'); }
finally { setSaving(false); }
};
const handleImport = async (e) => {
e.preventDefault();
setImporting(true);
try {
let result;
if (importTab === 'file' && importFile) {
result = await aiService.importFileToKb(importFile);
} else if (importTab === 'url') {
result = importCrawl
? await aiService.importCrawlToKb(importUrl, importCrawlMax)
: await aiService.importUrlToKb(importUrl);
} else {
result = await aiService.importTextToKb(importText);
}
const pagesInfo = result.pages ? ` (${result.pages} Seiten)` : '';
toast.success(`${result.created} Artikel erstellt${pagesInfo}`);
setShowImport(false);
setImportText('');
setImportFile(null);
setImportUrl('');
setImportCrawl(false);
setImportCrawlMax(20);
setImportTab('text');
loadArticles();
} catch (err) { toast.error(err?.response?.data?.message || err?.message || 'Fehler beim Importieren'); }
finally { setImporting(false); }
};
const handleDelete = async (id, e) => {
e.stopPropagation();
if (!window.confirm('Artikel wirklich löschen?')) return;
try {
await aiService.deleteKnowledgeEntry(id);
toast.success('Artikel gelöscht');
setExpanded(null);
loadArticles();
} catch { toast.error('Fehler beim Löschen'); }
};
const handleImageUpload = async (artId, e) => {
const file = e.target.files?.[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) { toast.error('Max. 5 MB'); return; }
try {
await aiService.uploadKbImage(artId, file);
toast.success('Bild hochgeladen');
loadArticles();
} catch { toast.error('Fehler beim Hochladen'); }
e.target.value = '';
};
const handleImageDelete = async (artId, filename, e) => {
e.stopPropagation();
if (!window.confirm('Bild löschen?')) return;
try {
await aiService.deleteKbImage(artId, filename);
loadArticles();
} catch { toast.error('Fehler beim Löschen'); }
};
// ── Filtered data ─────────────────────────────────────────────────────────
const filteredArticles = articles.filter(a => {
const q = searchArt.toLowerCase();
const matchQ = !q || a.problem?.toLowerCase().includes(q) || a.solution?.toLowerCase().includes(q) || a.tags?.toLowerCase().includes(q);
const matchC = catFilter === 'Alle' || a.category === catFilter;
return matchQ && matchC;
});
const ticketCats = ['Alle', ...Array.from(new Set(tickets.map(t => t.category).filter(Boolean)))];
const filteredTickets = tickets.filter(t => {
const q = searchTickets.toLowerCase();
const matchQ = !q || t.title?.toLowerCase().includes(q) || t.ticket_number?.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q);
const matchC = ticketCat === 'Alle' || t.category === ticketCat;
return matchQ && matchC;
});
const formatDate = (d) => d ? new Date(d).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '—';
const getDuration = (a, b) => {
if (!a || !b) return null;
const h = Math.floor((new Date(b) - new Date(a)) / 3600000);
return h < 24 ? `${h}h` : `${Math.floor(h / 24)}d`;
};
// ── Render ────────────────────────────────────────────────────────────────
return (
<div className="main-content">
<div style={{ maxWidth: 1100, margin: '0 auto' }}>
{/* Header */}
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
<div>
<h1 style={{ margin: 0, fontSize: '1.5rem', fontWeight: 700 }}>📚 Wissensdatenbank</h1>
<p style={{ margin: '4px 0 0', color: 'var(--text-muted)', fontSize: '0.875rem' }}>
{tab === 'artikel'
? `${articles.length} Artikel · durchsuchbare Lösungen`
: `${tickets.length} geschlossene Tickets`}
</p>
</div>
{tab === 'artikel' && canEdit && (
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn btn-secondary" onClick={() => setShowImport(true)}>📥 Text importieren</button>
<button className="btn btn-primary" onClick={openCreate}>+ Neuer Artikel</button>
</div>
)}
</div>
{/* Tabs */}
<div style={{ display: 'flex', gap: 4, marginBottom: 20, borderBottom: '1px solid var(--border-color)', paddingBottom: 0 }}>
{[
{ key: 'artikel', label: '📖 Artikel', count: articles.length },
{ key: 'tickets', label: '🎫 Geschlossene Tickets', count: ticketsLoaded ? tickets.length : null },
].map(t => (
<button key={t.key} onClick={() => setTab(t.key)} style={{
padding: '8px 18px', border: 'none', background: 'transparent', cursor: 'pointer',
fontSize: '0.875rem', fontWeight: tab === t.key ? 700 : 400,
color: tab === t.key ? 'var(--primary)' : 'var(--text-muted)',
borderBottom: tab === t.key ? '2px solid var(--primary)' : '2px solid transparent',
marginBottom: -1,
}}>
{t.label}{t.count !== null ? <span style={{ marginLeft: 6, fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '1px 6px', borderRadius: 10 }}>{t.count}</span> : null}
</button>
))}
</div>
{/* ── ARTIKEL TAB ── */}
{tab === 'artikel' && (
<>
{/* Filter bar */}
<div style={{ display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap', alignItems: 'center' }}>
<input className="form-input" style={{ flex: 1, minWidth: 200 }}
placeholder="Suchen in Artikeln…"
value={searchArt} onChange={e => setSearchArt(e.target.value)} />
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{['Alle', ...CATEGORIES].map(c => (
<button key={c} onClick={() => setCatFilter(c)} style={{
padding: '5px 12px', borderRadius: 20,
border: `1px solid ${catFilter === c ? catColor(c) : 'var(--border-color)'}`,
background: catFilter === c ? catColor(c) : 'transparent',
color: catFilter === c ? '#fff' : 'var(--text-secondary)',
cursor: 'pointer', fontSize: '0.78rem', fontWeight: catFilter === c ? 600 : 400,
}}>{c}</button>
))}
</div>
</div>
{loadingArt ? (
<div style={{ textAlign: 'center', padding: '3rem', color: 'var(--text-muted)' }}>Lade</div>
) : filteredArticles.length === 0 ? (
<div className="card" style={{ textAlign: 'center', padding: '3rem', color: 'var(--text-muted)' }}>
<div style={{ fontSize: '2.5rem', marginBottom: 8 }}>📭</div>
<div>{articles.length === 0 ? 'Noch keine Artikel. Erstelle den ersten!' : 'Keine Treffer.'}</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{filteredArticles.map(art => (
<div key={art.id} className="card" style={{ padding: 0, overflow: 'hidden', cursor: 'pointer', borderColor: expanded === art.id ? 'var(--primary)' : undefined }}
onClick={() => setExpanded(expanded === art.id ? null : art.id)}>
{/* Header row */}
<div style={{ padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: catColor(art.category), flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: '0.9rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{art.problem}
</div>
<div style={{ fontSize: '0.72rem', color: 'var(--text-muted)', marginTop: 2, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<span style={{ padding: '1px 7px', borderRadius: 10, background: catColor(art.category) + '22', color: catColor(art.category), fontWeight: 600 }}>{art.category}</span>
{art.tags && art.tags.split(',').map(tag => (
<span key={tag.trim()} style={{ padding: '1px 7px', borderRadius: 10, background: 'var(--bg-tertiary)', color: 'var(--text-muted)' }}>#{tag.trim()}</span>
))}
{art.auto_generated ? <span style={{ color: '#8b5cf6', fontWeight: 600 }}>🤖 KI</span> : null}
{art.source_ticket_id ? <span style={{ color: 'var(--text-muted)' }}>Ticket #{art.source_ticket_id}</span> : null}
<span>📅 {formatDate(art.created_at)}</span>
{art.created_by_username && <span>von {art.created_by_username}</span>}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
{canEdit && (
<>
<button className="btn btn-secondary btn-small" onClick={(e) => openEdit(art, e)}
style={{ fontSize: '11px', padding: '3px 8px' }}></button>
<button className="btn btn-danger btn-small" onClick={(e) => handleDelete(art.id, e)}
style={{ fontSize: '11px', padding: '3px 8px' }}>🗑</button>
</>
)}
<span style={{ color: 'var(--text-muted)', fontSize: '0.75rem' }}>{expanded === art.id ? '▲' : '▼'}</span>
</div>
</div>
{/* Expanded solution */}
{expanded === art.id && (
<div style={{ padding: '0 16px 14px 16px', borderTop: '1px solid var(--border-color)' }}>
<div style={{ fontSize: '0.78rem', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', margin: '12px 0 6px' }}>
Lösung
</div>
<div className="md-content" style={{ fontSize: '0.875rem', lineHeight: 1.6, color: 'var(--text-primary)' }}
dangerouslySetInnerHTML={renderMd(art.solution)} />
{/* Images */}
{(() => {
const imgs = (() => { try { return JSON.parse(art.images || '[]'); } catch { return []; } })();
return (imgs.length > 0 || canEdit) && (
<div style={{ marginTop: 14 }}>
<div style={{ fontSize: '0.78rem', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 8 }}>
Bilder
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'flex-start' }}>
{imgs.map(img => (
<div key={img} style={{ position: 'relative', display: 'inline-block' }}>
<img src={`/uploads/kb/${img}`} alt={img}
style={{ maxWidth: 220, maxHeight: 160, borderRadius: 6, border: '1px solid var(--border-color)', cursor: 'pointer', display: 'block' }}
onClick={e => { e.stopPropagation(); window.open(`/uploads/kb/${img}`, '_blank'); }} />
{canEdit && (
<button onClick={e => handleImageDelete(art.id, img, e)}
style={{ position: 'absolute', top: 4, right: 4, background: 'rgba(0,0,0,0.6)', color: '#fff', border: 'none', borderRadius: 4, padding: '2px 6px', cursor: 'pointer', fontSize: '11px' }}>
×
</button>
)}
</div>
))}
{canEdit && (
<label onClick={e => e.stopPropagation()} style={{ width: 80, height: 80, border: '2px dashed var(--border-color)', borderRadius: 6, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', fontSize: '0.72rem', color: 'var(--text-muted)', gap: 4 }}>
<span style={{ fontSize: '1.2rem' }}>+</span>
<span>Bild</span>
<input type="file" accept="image/*" style={{ display: 'none' }}
onChange={e => handleImageUpload(art.id, e)} />
</label>
)}
</div>
</div>
);
})()}
</div>
)}
</div>
))}
</div>
)}
</>
)}
{/* ── TICKETS TAB ── */}
{tab === 'tickets' && (
<>
<div style={{ display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap', alignItems: 'center' }}>
<input className="form-input" style={{ flex: 1, minWidth: 200 }}
placeholder="Suchen nach Titel, Ticketnummer…"
value={searchTickets} onChange={e => setSearchTickets(e.target.value)} />
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{ticketCats.map(c => (
<button key={c} onClick={() => setTicketCat(c)} style={{
padding: '5px 12px', borderRadius: 20,
border: `1px solid ${ticketCat === c ? catColor(c) : 'var(--border-color)'}`,
background: ticketCat === c ? catColor(c) : 'transparent',
color: ticketCat === c ? '#fff' : 'var(--text-secondary)',
cursor: 'pointer', fontSize: '0.78rem', fontWeight: ticketCat === c ? 600 : 400,
}}>{c}</button>
))}
</div>
</div>
{loadingTickets ? (
<div style={{ textAlign: 'center', padding: '3rem', color: 'var(--text-muted)' }}>Lade</div>
) : filteredTickets.length === 0 ? (
<div className="card" style={{ textAlign: 'center', padding: '3rem', color: 'var(--text-muted)' }}>
<div style={{ fontSize: '2.5rem', marginBottom: 8 }}>📭</div>
<div>Keine geschlossenen Tickets gefunden</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{filteredTickets.map(t => (
<Link key={t.id} to={`/tickets/${t.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<div className="card" style={{ padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 14, transition: 'border-color 0.15s' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--primary)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border-color)'}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: catColor(t.category), flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<span style={{ fontFamily: 'monospace', fontSize: '0.72rem', color: 'var(--text-muted)' }}>{t.ticket_number}</span>
<span style={{ fontWeight: 600, fontSize: '0.875rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.title}</span>
</div>
<div style={{ fontSize: '0.72rem', color: 'var(--text-muted)', marginTop: 2 }}>
{t.requester_name && <span>👤 {t.requester_name}</span>}
{t.assigned_to_username && <span style={{ marginLeft: 10 }}>🛠 {t.assigned_to_first_name || ''} {t.assigned_to_last_name || t.assigned_to_username}</span>}
</div>
</div>
<span style={{ padding: '2px 8px', borderRadius: 10, background: catColor(t.category) + '22', color: catColor(t.category), fontSize: '0.72rem', fontWeight: 600, flexShrink: 0 }}>
{t.category}
</span>
{t.satisfaction_rating === 'gut' && (
<span title="Feedback: Positiv" style={{ fontSize: '1rem', flexShrink: 0 }}>👍</span>
)}
{t.satisfaction_rating === 'schlecht' && (
<span title="Feedback: Negativ" style={{ fontSize: '1rem', flexShrink: 0 }}>👎</span>
)}
<div style={{ textAlign: 'right', flexShrink: 0, fontSize: '0.72rem', color: 'var(--text-muted)' }}>
<div> {formatDate(t.closed_at || t.updated_at)}</div>
{getDuration(t.created_at, t.closed_at || t.updated_at) && (
<div style={{ marginTop: 2 }}> {getDuration(t.created_at, t.closed_at || t.updated_at)}</div>
)}
</div>
</div>
</Link>
))}
</div>
)}
</>
)}
</div>
{/* ── Import Modal ── */}
{showImport && (
<div className="modal-overlay" onClick={() => setShowImport(false)}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">📥 In Wissensdatenbank importieren</h2>
<button className="modal-close" onClick={() => { setShowImport(false); setImportTab('text'); setImportFile(null); setImportUrl(''); }}>×</button>
</div>
{/* Sub-tabs */}
<div style={{ display: 'flex', borderBottom: '1px solid var(--border-color)', padding: '0 24px' }}>
{[{ key: 'text', label: '📝 Text einfügen' }, { key: 'file', label: '📎 Datei hochladen' }, { key: 'url', label: '🌐 URL importieren' }].map(t => (
<button key={t.key} type="button" onClick={() => setImportTab(t.key)} style={{
padding: '10px 14px', border: 'none', background: 'transparent', cursor: 'pointer',
fontSize: '0.83rem', fontWeight: importTab === t.key ? 700 : 400,
color: importTab === t.key ? 'var(--primary)' : 'var(--text-muted)',
borderBottom: importTab === t.key ? '2px solid var(--primary)' : '2px solid transparent',
marginBottom: -1,
}}>{t.label}</button>
))}
</div>
<form onSubmit={handleImport}>
<div style={{ padding: '16px 24px 0' }}>
{importTab === 'text' ? (
<>
<p style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: 12, marginTop: 0 }}>
Füge beliebigen Text ein (Handbuch, Anleitung, E-Mail-Inhalt usw.).<br />
Die KI extrahiert automatisch Problem/Lösungs-Paare.
</p>
<div className="form-group">
<textarea className="form-textarea" rows="12" required={importTab === 'text'}
placeholder="Text hier einfügen…"
value={importText}
onChange={e => setImportText(e.target.value)} />
</div>
</>
) : importTab === 'file' ? (
<>
<p style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: 16, marginTop: 0 }}>
Lade eine Datei hoch. Unterstützte Formate: <strong>PDF, DOCX, DOC, EML, TXT</strong>.<br />
Der Inhalt wird von der KI gelesen und in Artikel umgewandelt.
</p>
<label style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
border: '2px dashed var(--border-color)', borderRadius: 8, padding: '2rem', cursor: 'pointer',
background: 'var(--bg-secondary)', gap: 8, marginBottom: 16,
borderColor: importFile ? 'var(--primary)' : 'var(--border-color)',
}}>
<span style={{ fontSize: '2rem' }}>{importFile ? '✅' : '📂'}</span>
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: 'var(--text-primary)' }}>
{importFile ? importFile.name : 'Datei auswählen'}
</span>
{importFile && (
<span style={{ fontSize: '0.72rem', color: 'var(--text-muted)' }}>
{(importFile.size / 1024).toFixed(0)} KB
</span>
)}
<input type="file" accept=".pdf,.docx,.doc,.eml,.txt" style={{ display: 'none' }}
onChange={e => setImportFile(e.target.files?.[0] || null)} />
</label>
{importFile && (
<button type="button" className="btn btn-secondary btn-small"
onClick={() => setImportFile(null)}
style={{ marginBottom: 8 }}>
Datei entfernen
</button>
)}
</>
) : importTab === 'url' ? (
<>
<p style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: 16, marginTop: 0 }}>
Gib die URL einer Hilfe- oder Dokumentationsseite ein.<br />
Die KI liest den Inhalt und erstellt daraus Wissensdatenbank-Artikel.
</p>
<div className="form-group">
<label className="form-label">URL *</label>
<input className="form-input" type="url" required={importTab === 'url'}
placeholder="https://hilfe.selectline.de/…"
value={importUrl}
onChange={e => setImportUrl(e.target.value)} />
</div>
{/* Crawler option */}
<div style={{ marginTop: 14, padding: '12px 14px', background: 'var(--bg-secondary)', borderRadius: 8, border: `1px solid ${importCrawl ? 'var(--primary)' : 'var(--border-color)'}` }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', userSelect: 'none' }}>
<input type="checkbox" checked={importCrawl} onChange={e => setImportCrawl(e.target.checked)}
style={{ width: 16, height: 16, cursor: 'pointer' }} />
<div>
<div style={{ fontWeight: 600, fontSize: '0.875rem' }}>🕷 Unterseiten automatisch crawlen</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: 2 }}>
Folgt allen Links auf derselben Domain &amp; demselben Pfad
</div>
</div>
</label>
{importCrawl && (
<div style={{ marginTop: 12 }}>
<label style={{ fontSize: '0.8rem', fontWeight: 600, display: 'block', marginBottom: 6 }}>
Max. Seiten: <span style={{ color: 'var(--primary)' }}>{importCrawlMax}</span>
</label>
<input type="range" min={2} max={100} step={1}
value={importCrawlMax} onChange={e => setImportCrawlMax(parseInt(e.target.value))}
style={{ width: '100%', accentColor: 'var(--primary)' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.7rem', color: 'var(--text-muted)' }}>
<span>2</span><span>100</span>
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: 6 }}>
Mehr Seiten = längere Wartezeit. Bei 20+ Seiten kann es 12 Minuten dauern.
</div>
</div>
)}
</div>
</>
) : null}
</div>
<div className="card-footer" style={{ marginTop: 8 }}>
<button type="button" className="btn btn-secondary" onClick={() => { setShowImport(false); setImportTab('text'); setImportFile(null); setImportUrl(''); }}>Abbrechen</button>
<button type="submit" className="btn btn-primary"
disabled={importing || (importTab === 'text' ? importText.trim().length < 20 : importTab === 'file' ? !importFile : importUrl.trim().length < 10)}>
{importing
? (importTab === 'url' && importCrawl ? `🕷️ Crawle Seiten…` : '🤖 KI analysiert…')
: '🤖 Artikel generieren'}
</button>
</div>
</form>
</div>
</div>
)}
{/* ── Create / Edit Modal ── */}
{showModal && (
<div className="modal-overlay" onClick={() => setShowModal(false)}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">{editingId ? 'Artikel bearbeiten' : 'Neuer Artikel'}</h2>
<button className="modal-close" onClick={() => setShowModal(false)}>×</button>
</div>
<form onSubmit={handleSave}>
<div style={{ padding: '0 24px' }}>
<div className="form-group">
<label className="form-label">Problem / Titel *</label>
<input className="form-input" required placeholder="Kurze Problembeschreibung…"
value={form.problem} onChange={e => setForm({ ...form, problem: e.target.value })} />
</div>
<div className="form-group">
<label className="form-label">Lösung *</label>
<textarea className="form-textarea" rows="6" required placeholder="Schritt-für-Schritt-Lösung…"
value={form.solution} onChange={e => setForm({ ...form, solution: e.target.value })} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">Kategorie</label>
<select className="form-select" value={form.category} onChange={e => setForm({ ...form, category: e.target.value })}>
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div className="form-group" style={{ margin: 0 }}>
<label className="form-label">Tags (kommagetrennt)</label>
<input className="form-input" placeholder="z.B. vpn, outlook, drucker"
value={form.tags} onChange={e => setForm({ ...form, tags: e.target.value })} />
</div>
</div>
</div>
<div className="card-footer" style={{ marginTop: 16 }}>
<button type="button" className="btn btn-secondary" onClick={() => setShowModal(false)}>Abbrechen</button>
<button type="submit" className="btn btn-primary" disabled={saving}>
{saving ? 'Speichern…' : editingId ? 'Aktualisieren' : 'Erstellen'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}