'use client' import { useState, useEffect, useCallback, useMemo } from 'react' import { Search, BookOpen, AlertTriangle, Shield, Scale, Handshake, Briefcase, MessageCircle, Building2, Database, ChevronRight, ArrowLeft, ExternalLink, Tag, Clock, Globe, Gavel, } from 'lucide-react' import type { WikiCategory, WikiArticle, WikiSearchResult } from '@/lib/sdk/types' // ============================================================================= // SIMPLE MARKDOWN RENDERER // ============================================================================= function renderMarkdown(md: string): string { let html = md // Escape HTML .replace(/&/g, '&') .replace(//g, '>') // Code blocks (``` ... ```) html = html.replace( /^```[\w]*\n([\s\S]*?)^```$/gm, (_match, code: string) => `
${code.trimEnd()}
` ) // Tables (must be before other block elements) html = html.replace( /^(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)*)/gm, (_match, header: string, _sep: string, body: string) => { const ths = header.split('|').filter((c: string) => c.trim()).map((c: string) => `${c.trim()}`).join('') const rows = body.trim().split('\n').map((row: string) => { const tds = row.split('|').filter((c: string) => c.trim()).map((c: string) => `${c.trim()}`).join('') return `${tds}` }).join('') return `${ths}${rows}
` } ) // Headers html = html.replace(/^### (.+)$/gm, '

$1

') html = html.replace(/^## (.+)$/gm, '

$1

') // Bold html = html.replace(/\*\*(.+?)\*\*/g, '$1') // Unordered lists html = html.replace(/^- (.+)$/gm, '
  • $1
  • ') html = html.replace(/((?:]*>.*<\/li>\n?)+)/g, '') // Paragraphs (lines that aren't already HTML) html = html.replace(/^(?!<[hultdp]|$)(.+)$/gm, '

    $1

    ') return html } // ============================================================================= // ICON MAP // ============================================================================= const ICON_MAP: Record> = { Database, Shield, AlertTriangle, Scale, Handshake, Briefcase, MessageCircle, Building2, Clock, Globe, Gavel, } function CategoryIcon({ icon, className }: { icon: string; className?: string }) { const Icon = ICON_MAP[icon] || BookOpen return } // ============================================================================= // RELEVANCE BADGE // ============================================================================= function RelevanceBadge({ relevance }: { relevance: string }) { const config = { critical: { bg: 'bg-red-100 text-red-800', label: 'Kritisch' }, important: { bg: 'bg-amber-100 text-amber-800', label: 'Wichtig' }, info: { bg: 'bg-blue-100 text-blue-800', label: 'Info' }, }[relevance] || { bg: 'bg-gray-100 text-gray-600', label: relevance } return ( {config.label} ) } // ============================================================================= // WIKI PAGE // ============================================================================= export default function WikiPage() { const [categories, setCategories] = useState([]) const [articles, setArticles] = useState([]) const [selectedCategory, setSelectedCategory] = useState(null) const [selectedArticle, setSelectedArticle] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [searchResults, setSearchResults] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Load categories on mount useEffect(() => { async function load() { try { const res = await fetch('/api/sdk/v1/wiki?endpoint=categories') if (!res.ok) throw new Error('Failed to load categories') const data = await res.json() const cats: WikiCategory[] = (data.categories || []).map((c: Record) => ({ id: c.id, name: c.name, description: c.description || '', icon: c.icon || '', sortOrder: c.sort_order ?? 0, articleCount: c.article_count ?? 0, })) setCategories(cats) // Load all articles const artRes = await fetch('/api/sdk/v1/wiki?endpoint=articles') if (artRes.ok) { const artData = await artRes.json() setArticles((artData.articles || []).map((a: Record) => ({ id: a.id, categoryId: a.category_id, categoryName: a.category_name, title: a.title, summary: a.summary, content: a.content, legalRefs: a.legal_refs || [], tags: a.tags || [], relevance: a.relevance || 'info', sourceUrls: a.source_urls || [], version: a.version || 1, updatedAt: a.updated_at || '', }))) } } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Laden') } finally { setLoading(false) } } load() }, []) // Search handler const handleSearch = useCallback(async (query: string) => { setSearchQuery(query) if (query.length < 2) { setSearchResults(null) return } try { const res = await fetch(`/api/sdk/v1/wiki?endpoint=search&q=${encodeURIComponent(query)}`) if (res.ok) { const data = await res.json() setSearchResults((data.results || []).map((r: Record) => ({ id: r.id, title: r.title, summary: r.summary, categoryName: r.category_name, relevance: r.relevance || 'info', highlight: r.highlight || '', }))) } } catch { // silently fail search } }, []) // Debounced search useEffect(() => { const timer = setTimeout(() => { if (searchQuery.length >= 2) handleSearch(searchQuery) }, 300) return () => clearTimeout(timer) }, [searchQuery, handleSearch]) // Filtered articles for selected category const filteredArticles = useMemo(() => { if (!selectedCategory) return articles return articles.filter(a => a.categoryId === selectedCategory) }, [articles, selectedCategory]) // Select article from search result const selectFromSearch = (id: string) => { const article = articles.find(a => a.id === id) if (article) { setSelectedArticle(article) setSelectedCategory(article.categoryId) setSearchResults(null) setSearchQuery('') } } if (loading) { return (
    ) } if (error) { return (
    {error}
    ) } return (
    {/* Header */}

    Compliance Wiki

    Interne Wissensbasis — {articles.length} Artikel in {categories.length} Kategorien

    {/* Search */}
    setSearchQuery(e.target.value)} className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent" /> {/* Search results dropdown */} {searchResults && searchResults.length > 0 && (
    {searchResults.map(r => ( ))}
    )} {searchResults && searchResults.length === 0 && searchQuery.length >= 2 && (
    Keine Ergebnisse fuer "{searchQuery}"
    )}
    {/* Content: Two columns */}
    {/* Left: Categories */}
    {categories.map(cat => ( ))}
    {/* Right: Article list or detail */}
    {selectedArticle ? ( /* Article detail view */
    {selectedArticle.categoryName} · v{selectedArticle.version}

    {selectedArticle.title}

    {selectedArticle.summary}

    {/* Content (rendered markdown) */}
    {/* Legal References */} {selectedArticle.legalRefs.length > 0 && (

    Rechtsreferenzen

    {selectedArticle.legalRefs.map(ref => ( {ref} ))}
    )} {/* Tags */} {selectedArticle.tags.length > 0 && (

    Tags

    {selectedArticle.tags.map(tag => ( {tag} ))}
    )} {/* Source URLs */} {selectedArticle.sourceUrls.length > 0 && (

    Quellen

    {selectedArticle.sourceUrls.map(url => (
    {url.startsWith('http') ? ( {url} ) : ( {url} )}
    ))}
    )}
    ) : ( /* Article list view */

    {selectedCategory ? categories.find(c => c.id === selectedCategory)?.name || 'Artikel' : 'Alle Artikel' } ({filteredArticles.length})

    {selectedCategory && (

    {categories.find(c => c.id === selectedCategory)?.description}

    )}
    {filteredArticles.map(article => ( ))} {filteredArticles.length === 0 && (
    Keine Artikel in dieser Kategorie.
    )}
    )}
    ) }