Code-2-Graphic: Aus Programmiercode eine anschauliche Infografik erstellen
Freie Nutzung von Text & Abbildungen mit einem Unidigital-Zugang
Aus Code direkt eine Infografik generieren, um die zentralen Funktionen einer App anschaulich darzustellen, dies klappt über ChatGPT Images. Hierbei analysiert ChatGPT den Code und wandelt diesen wie im Beitrag demonstriert in eine deutschsprachige Abbildung im Sketchnote-Stil um.

Gesamter Code der App „SourceLM":
import React, { useState, useEffect, useRef, useMemo } from 'react';
import {
BookOpen,
FileText,
Upload,
Plus,
Search,
Trash2,
CheckCircle2,
AlertCircle,
Clock,
Sparkles,
Link as LinkIcon,
Eye,
Settings,
ChevronRight,
Database,
Layers,
HelpCircle,
Copy,
RotateCcw,
Sliders,
X,
FileUp,
Globe,
Tag,
Share2,
Zap,
Check,
ChevronDown,
Info,
ShieldAlert,
ArrowUpRight,
Menu,
MessageSquare
} from 'lucide-react';
// Demo Notebooks with rich German source documents & chunks
const INITIAL_NOTEBOOKS = [
{
id: 'nb-1',
title: 'EU AI Act & Compliance Guidelines 2026',
description: 'Analyse der verabschiedeten EU-Verordnung für Künstliche Intelligenz und deren Auswirkungen auf KMUs.',
createdAt: '2026-05-10',
updatedAt: '2026-07-20',
sources: [
{
id: 'src-1',
title: 'EU_AI_Act_Final_Text_Summary.pdf',
type: 'pdf',
uploadDate: '2026-06-01',
size: '2.4 MB',
status: 'ready',
chunkCount: 14,
author: 'Europäisches Parlament',
url: 'https://eur-lex.europa.eu/ai-act',
isActive: true,
chunks: [
{
id: 'chk-1-1',
sourceId: 'src-1',
pageNumber: 4,
sectionTitle: 'Artikel 5: Verbotene Praktiken im Bereich der KI',
content: 'Folgende Praktiken mit Systemen künstlicher Intelligenz sind verboten: a) das Inverkehrbringen von KI-Systemen, die unter Anwendung unterschwelliger Techniken das Verhalten einer Person wesentlich beeinflussen; b) die Ausnutzung von Schwachstellen natürlicher Personen aufgrund ihres Alters oder einer Behinderung; c) Social Scoring durch staatliche oder private Stellen.',
contextBefore: 'KAPITEL II - VERBOTENE PRAKTIKEN',
contextAfter: 'Verstöße gegen Artikel 5 werden mit Geldbußen von bis zu 35 Mio. EUR oder 7% des weltweiten Jahresumsatzes geahndet.'
},
{
id: 'chk-1-2',
sourceId: 'src-1',
pageNumber: 12,
sectionTitle: 'Artikel 9: Risikomanagementsystem für Hochrisiko-KI',
content: 'Für Hochrisiko-KI-Systeme ist ein Risikomanagementsystem einzurichten, zu implementieren, zu dokumentieren und aufrechterhalten zu werden. Es besteht aus einem kontinuierlichen iterativen Prozess, der während des gesamten Lebenszyklus eines Hochrisiko-KI-Systems ausgeführt wird und regelmäßige Aktualisierungen erfordert.',
contextBefore: 'KAPITEL III - HOCHRISIKO-KI-SYSTEME',
contextAfter: 'Das Risikomanagementsystem umfasst die Identifizierung und Analyse der bekannten und vorhersehbaren Risiken.'
},
{
id: 'chk-1-3',
sourceId: 'src-1',
pageNumber: 28,
sectionTitle: 'Artikel 52: Transparenzpflichten für bestimmte KI-Systeme',
content: 'Anbieter stellen sicher, dass KI-Systeme, die für die Interaktion mit natürlichen Personen bestimmt sind, so konzipiert sind, dass die Personen darüber informiert werden, dass sie mit einem KI-System interagieren. Dies gilt auch für Generative KI-Systeme zur Erzeugung von Audio-, Bild- oder Textinhalten (Deepfakes).',
contextBefore: 'KAPITEL IV - TRANSPARENZPFLICHTEN',
contextAfter: 'Ausnahmen gelten für Strafverfolgungsbehörden im Rahmen gesetzlicher Ermächtigungen.'
}
]
},
{
id: 'src-2',
title: 'Leitfaden_KMU_KI_Compliance.docx',
type: 'docx',
uploadDate: '2026-06-15',
size: '840 KB',
status: 'ready',
chunkCount: 8,
author: 'Bundesverband Digitalwirtschaft',
isActive: true,
chunks: [
{
id: 'chk-2-1',
sourceId: 'src-2',
pageNumber: 2,
sectionTitle: '1.2 Vorbereitung im Unternehmen',
content: 'Kleine und mittlere Unternehmen (KMU) sollten umgehend ein internes KI-Kataster anlegen. Darin werden alle eingesetzten Software-Tools mit KI-Komponenten aufgelistet, nach Risikoklassen kategorisiert und zuständige Data Governance Manager benannt. Bis Q3 2026 müssen Dokumentationspflichten erfüllt sein.',
contextBefore: 'HANDLUNGSEMPFEHLUNGEN FÜR DEUTSCHE KMU',
contextAfter: 'Die Missachtung kann zu erheblichen Abmahnungen durch Verbraucherschutzverbände führen.'
},
{
id: 'chk-2-2',
sourceId: 'src-2',
pageNumber: 7,
sectionTitle: '3.1 Übergangsfristen und Stufenplan',
content: 'Die Verbote von Systemen mit unannehmbarem Risiko greifen 6 Monate nach Inkrafttreten. Die Vorgaben für General Purpose AI (GPAI) gelten nach 12 Monaten. Die vollständigen Pflichten für Hochrisiko-Systeme treten nach 24 bis 36 Monaten verbindlich in Kraft.',
contextBefore: 'ZEITPLAN DER IMPLEMENTIERUNG',
contextAfter: 'Unternehmen wird empfohlen, eine Übergangszeit von 18 Monaten einzuplanen.'
}
]
}
]
},
{
id: 'nb-2',
title: 'Klimawandel & Urbane Mobilität 2030',
description: 'Studien über Mikromobilität, öffentlichen Nahverkehr und Emissionen in europäischen Großstädten.',
createdAt: '2026-04-12',
updatedAt: '2026-07-02',
sources: [
{
id: 'src-3',
title: 'Urbane_Mobilität_Studie_Berlin.pdf',
type: 'pdf',
uploadDate: '2026-04-15',
size: '4.1 MB',
status: 'ready',
chunkCount: 18,
author: 'Institut für Stadtforschung',
isActive: true,
chunks: [
{
id: 'chk-3-1',
sourceId: 'src-3',
pageNumber: 15,
sectionTitle: '3. Emisssionsreduktion durch Superblocks',
content: 'Durch den Umbau von Stadtvierteln nach dem Barcelona-Superblock-Modell konnte die CO2-Belastung in Pilotbereichen um 22% gesenkt werden. Gleichzeitig stieg der Fuß- und Radverkehr im Innenbereich um 34%, ohne dass es auf den Hauptverkehrsachsen zu anhaltenden Staus kam.',
contextBefore: 'ERGEBNISSE DER PILOTPROJEKTE',
contextAfter: 'Die Anwohnerbefragung ergab eine Zufriedenheit von 78% mit der neuen Verkehrsführung.'
}
]
}
]
}
];
// Pre-defined Chat Modes
const CHAT_MODES = [
{ id: 'qa', label: 'Fragen beantworten', icon: HelpCircle, description: 'Spezifische Fragen anhand der Quellen beantworten' },
{ id: 'summary', label: 'Zusammenfassen', icon: Layers, description: 'Kompakte Zusammenfassung der Kerninhalte' },
{ id: 'compare', label: 'Quellen vergleichen', icon: Sliders, description: 'Gegenüberstellung verschiedener Standpunkte' },
{ id: 'contradictions', label: 'Widersprüche finden', icon: ShieldAlert, description: 'Aufdecken von Konflikten & Unstimmigkeiten' },
{ id: 'keypoints', label: 'Kernaussagen', icon: Sparkles, description: 'Die wichtigsten Extrakte als Stichpunkte' },
{ id: 'quiz', label: 'Lernfragen erstellen', icon: BookOpen, description: 'Generiert Verständnisfragen & Antwoertschlüssel' },
{ id: 'outline', label: 'Gliederung erstellen', icon: FileText, description: 'Strukturierte Inhaltsübersicht erzeugen' }
];
export default function SourceLMApp() {
// App States
const [notebooks, setNotebooks] = useState(INITIAL_NOTEBOOKS);
const [activeNotebookId, setActiveNotebookId] = useState('nb-1');
const [activeTab, setActiveTab] = useState('chat'); // Mobile: 'chat' | 'sources' | 'citations' | 'overview'
const [activeChatMode, setActiveChatMode] = useState('qa');
const [searchQuery, setSearchQuery] = useState('');
// Selection & Inspector states
const [selectedCitation, setSelectedCitation] = useState(null);
const [selectedSourceForDetail, setSelectedSourceForDetail] = useState(null);
const [isAddSourceOpen, setIsAddSourceOpen] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isSchemaModalOpen, setIsSchemaModalOpen] = useState(false);
// Chat message & interaction state per notebook
const [chatHistories, setChatHistories] = useState({
'nb-1': [
{
id: 'msg-1',
role: 'user',
createdAt: '2026-07-20 14:20',
content: 'Welche Strafen drohen bei Verstößen gegen das Verbot von unzulässigen KI-Praktiken und welche Übergangsfristen gelten?'
},
{
id: 'msg-2',
role: 'assistant',
createdAt: '2026-07-20 14:21',
content: `Bei Verstößen gegen das Verbot von unzulässigen KI-Praktiken (wie unter anderem Unterschwellige Beeinflussung oder Social Scoring) drohen Geldbußen von bis zu 35 Millionen Euro oder bis zu 7 % des weltweiten Jahresumsatzes [1].
Bezüglich der Übergangsfristen gelten folgende gestaffelte Termine [2]:
- **6 Monate nach Inkrafttreten:** Das Verbot von KI-Systemen mit unannehmbarem Risiko tritt verbindlich in Kraft.
- **12 Monate nach Inkrafttreten:** Vorgaben für General Purpose AI (GPAI) greifen.
- **24 bis 36 Monate nach Inkrafttreten:** Die vollständigen Pflichten für Hochrisiko-KI-Systeme werden verbindlich.`,
citations: [
{
id: 'cit-1',
citationIndex: 1,
sourceId: 'src-1',
chunkId: 'chk-1-1',
sourceTitle: 'EU_AI_Act_Final_Text_Summary.pdf',
pageNumber: 4,
sectionTitle: 'Artikel 5: Verbotene Praktiken im Bereich der KI',
quote: 'Verstöße gegen Artikel 5 werden mit Geldbußen von bis zu 35 Mio. EUR oder 7% des weltweiten Jahresumsatzes geahndet.',
contextBefore: 'Folgende Praktiken mit Systemen künstlicher Intelligenz sind verboten...',
contextAfter: 'KAPITEL III - HOCHRISIKO-KI-SYSTEME'
},
{
id: 'cit-2',
citationIndex: 2,
sourceId: 'src-2',
chunkId: 'chk-2-2',
sourceTitle: 'Leitfaden_KMU_KI_Compliance.docx',
pageNumber: 7,
sectionTitle: '3.1 Übergangsfristen und Stufenplan',
quote: 'Die Verbote von Systemen mit unannehmbarem Risiko greifen 6 Monate nach Inkrafttreten. Die Vorgaben für General Purpose AI (GPAI) gelten nach 12 Monaten. Die vollständigen Pflichten für Hochrisiko-Systeme treten nach 24 bis 36 Monaten verbindlich in Kraft.',
contextBefore: 'ZEITPLAN DER IMPLEMENTIERUNG',
contextAfter: 'Unternehmen wird empfohlen, eine Übergangszeit von 18 Monaten einzuplanen.'
}
]
}
],
'nb-2': []
});
const [inputPrompt, setInputPrompt] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const [streamingMessage, setStreamingMessage] = useState('');
const [selectedSourceIdsForChat, setSelectedSourceIdsForChat] = useState([]); // Filter specific sources
// Gemini API Key state
const [apiKey, setApiKey] = useState('');
const [useLiveApi, setUseLiveApi] = useState(false);
const activeNotebook = useMemo(() => {
return notebooks.find((n) => n.id === activeNotebookId) || notebooks[0];
}, [notebooks, activeNotebookId]);
const currentMessages = chatHistories[activeNotebookId] || [];
// Active Sources count
const activeSources = useMemo(() => {
return activeNotebook?.sources.filter((s) => s.isActive) || [];
}, [activeNotebook]);
// Scroll to bottom helper
const chatBottomRef = useRef(null);
useEffect(() => {
chatBottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [currentMessages, streamingMessage]);
// Local Keyword/Cosine similarity simulator for source chunks
const performSemanticRetrieval = (queryText, targetSources) => {
const allChunks = [];
targetSources.forEach((src) => {
if (src.isActive) {
src.chunks.forEach((chk) => {
allChunks.push({
...chk,
sourceTitle: src.title,
sourceType: src.type
});
});
}
});
if (allChunks.length === 0) return [];
const queryTokens = queryText
.toLowerCase()
.replace(/[^\w\säöüß]/g, '')
.split(/\s+/)
.filter((t) => t.length > 2);
// Score chunks by match frequency & term weighting
const scoredChunks = allChunks.map((chunk) => {
const chunkText = (chunk.content + ' ' + chunk.sectionTitle).toLowerCase();
let score = 0;
queryTokens.forEach((token) => {
if (chunkText.includes(token)) {
score += 1.5;
// Extra boost for section title match
if (chunk.sectionTitle.toLowerCase().includes(token)) score += 2.0;
}
});
return { chunk, score };
});
// Sort descending by score
scoredChunks.sort((a, b) => b.score - a.score);
// Return top 3 chunks with score > 0
return scoredChunks.filter((item) => item.score > 0).map((item) => item.chunk);
};
const handleSendMessage = async (e) => {
e?.preventDefault();
if (!inputPrompt.trim() || isGenerating) return;
const userMessageText = inputPrompt;
setInputPrompt('');
// 1. Add user message
const userMsgObj = {
id: 'msg-' + Date.now(),
role: 'user',
createdAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
content: userMessageText
};
setChatHistories((prev) => ({
...prev,
[activeNotebookId]: [...(prev[activeNotebookId] || []), userMsgObj]
}));
setIsGenerating(true);
setStreamingMessage('Analysiere Quellen und suche relevante Textabschnitte...');
// Determine target sources (either selected subset or all active sources)
const targetSources = selectedSourceIdsForChat.length > 0
? activeSources.filter((s) => selectedSourceIdsForChat.includes(s.id))
: activeSources;
// 2. Perform Retrieval
setTimeout(async () => {
const retrievedChunks = performSemanticRetrieval(userMessageText, targetSources);
if (retrievedChunks.length === 0 && targetSources.length > 0) {
// Fallback or out of scope check
const noInfoMessage = {
id: 'msg-' + (Date.now() + 1),
role: 'assistant',
createdAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
content: 'In den ausgewählten Quellen konnte ich dazu keine ausreichenden Informationen finden. Meine Antworten basieren streng ausschließlich auf den hinterlegten Dokumenten.',
citations: []
};
setChatHistories((prev) => ({
...prev,
[activeNotebookId]: [...(prev[activeNotebookId] || []), noInfoMessage]
}));
setIsGenerating(false);
setStreamingMessage('');
return;
}
// If user provided Gemini API Key or using API call
if (apiKey.trim()) {
try {
setStreamingMessage('Synthetisiere Antwort mit Gemini AI...');
const systemContext = retrievedChunks
.map(
(c, idx) =>
`[Quelle ${idx + 1}: ${c.sourceTitle}, S. ${c.pageNumber || 1}]\nAbschnitt: ${c.sectionTitle}\nInhalt: ${c.content}`
)
.join('\n\n');
const prompt = `Systemanweisung: Du bist ein quellengebundener Rechercheassistent. Beantworte die Frage ausschließlich anhand des folgenden Quellenkontexts. Füge bei allen Aussagen Belege im Format [1], [2] entsprechend den Quellenabschnitten ein.
Quellenkontext:
${systemContext}
Nutzerfrage (${activeChatMode}): ${userMessageText}`;
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key=${apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }]
})
}
);
const data = await res.json();
const aiText = data.candidates?.[0]?.content?.parts?.[0]?.text || 'Keine Antwort empfangen.';
// Map citations
const generatedCitations = retrievedChunks.map((c, idx) => ({
id: 'cit-' + Date.now() + '-' + idx,
citationIndex: idx + 1,
sourceId: c.sourceId,
chunkId: c.id,
sourceTitle: c.sourceTitle,
pageNumber: c.pageNumber || 1,
sectionTitle: c.sectionTitle,
quote: c.content,
contextBefore: c.contextBefore || '...',
contextAfter: c.contextAfter || '...'
}));
const assistantMsgObj = {
id: 'msg-' + (Date.now() + 1),
role: 'assistant',
createdAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
content: aiText,
citations: generatedCitations
};
setChatHistories((prev) => ({
...prev,
[activeNotebookId]: [...(prev[activeNotebookId] || []), assistantMsgObj]
}));
} catch (err) {
console.error(err);
// Fallback to local simulation if API fails
generateLocalSimulatedResponse(userMessageText, retrievedChunks);
}
} else {
// Local deterministic simulation with exact citations
generateLocalSimulatedResponse(userMessageText, retrievedChunks);
}
setIsGenerating(false);
setStreamingMessage('');
}, 1200);
};
const generateLocalSimulatedResponse = (query, chunks) => {
let responseText = '';
const citations = [];
if (activeChatMode === 'summary') {
responseText = `Hier ist eine zusammenfassende Übersicht der relevanten Quellenabschnitte:\n\n`;
chunks.forEach((chk, i) => {
const citIdx = i + 1;
responseText += `- **${chk.sectionTitle}**: ${chk.content.substring(0, 180)}... [${citIdx}]\n\n`;
citations.push({
id: 'cit-' + Date.now() + '-' + citIdx,
citationIndex: citIdx,
sourceId: chk.sourceId,
chunkId: chk.id,
sourceTitle: chk.sourceTitle,
pageNumber: chk.pageNumber || 1,
sectionTitle: chk.sectionTitle,
quote: chk.content,
contextBefore: chk.contextBefore || 'Vorausgehender Kontext...',
contextAfter: chk.contextAfter || 'Nachfolgender Kontext...'
});
});
} else if (activeChatMode === 'keypoints') {
responseText = `Zentrale Kernaussagen aus Ihren Quellen:\n\n`;
chunks.forEach((chk, i) => {
const citIdx = i + 1;
responseText += `1. **Wichtiger Aspekt aus ${chk.sourceTitle}**: "${chk.content}" [${citIdx}]\n`;
citations.push({
id: 'cit-' + Date.now() + '-' + citIdx,
citationIndex: citIdx,
sourceId: chk.sourceId,
chunkId: chk.id,
sourceTitle: chk.sourceTitle,
pageNumber: chk.pageNumber || 1,
sectionTitle: chk.sectionTitle,
quote: chk.content,
contextBefore: chk.contextBefore || '',
contextAfter: chk.contextAfter || ''
});
});
} else {
// Default Q&A or other modes
responseText = `Basierend auf den aktiven Dokumenten lässt sich Folgendes feststellen:\n\n`;
chunks.forEach((chk, i) => {
const citIdx = i + 1;
responseText += `Zu der Anforderung bezüglich *${chk.sectionTitle}*: ${chk.content} [${citIdx}]\n\n`;
citations.push({
id: 'cit-' + Date.now() + '-' + citIdx,
citationIndex: citIdx,
sourceId: chk.sourceId,
chunkId: chk.id,
sourceTitle: chk.sourceTitle,
pageNumber: chk.pageNumber || 1,
sectionTitle: chk.sectionTitle,
quote: chk.content,
contextBefore: chk.contextBefore || '...',
contextAfter: chk.contextAfter || '...'
});
});
}
const assistantMsgObj = {
id: 'msg-' + (Date.now() + 1),
role: 'assistant',
createdAt: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
content: responseText,
citations: citations
};
setChatHistories((prev) => ({
...prev,
[activeNotebookId]: [...(prev[activeNotebookId] || []), assistantMsgObj]
}));
};
const handleCreateNewNotebook = () => {
const newId = 'nb-' + (notebooks.length + 1);
const newNb = {
id: newId,
title: `Neues Notebook ${notebooks.length + 1}`,
description: 'Analyse und Recherchen zu spezifischen Quelldokumenten.',
createdAt: new Date().toISOString().split('T')[0],
updatedAt: new Date().toISOString().split('T')[0],
sources: []
};
setNotebooks([newNb, ...notebooks]);
setActiveNotebookId(newId);
setChatHistories((prev) => ({ ...prev, [newId]: [] }));
};
const handleToggleSourceActive = (sourceId) => {
setNotebooks((prev) =>
prev.map((nb) => {
if (nb.id !== activeNotebookId) return nb;
return {
...nb,
sources: nb.sources.map((s) => (s.id === sourceId ? { ...s, isActive: !s.isActive } : s))
};
})
);
};
const handleDeleteSource = (sourceId) => {
setNotebooks((prev) =>
prev.map((nb) => {
if (nb.id !== activeNotebookId) return nb;
return {
...nb,
sources: nb.sources.filter((s) => s.id !== sourceId)
};
})
);
if (selectedSourceForDetail?.id === sourceId) {
setSelectedSourceForDetail(null);
}
};
const handleAddCustomTextSource = (title, content, type = 'txt') => {
const newSourceId = 'src-' + Date.now();
// Simple client-side chunker by paragraph or character length
const paragraphs = content.split(/\n\s*\n/).filter((p) => p.trim().length > 10);
const chunks = paragraphs.map((p, idx) => ({
id: `chk-${newSourceId}-${idx + 1}`,
sourceId: newSourceId,
pageNumber: Math.floor(idx / 2) + 1,
sectionTitle: `Abschnitt ${idx + 1}`,
content: p.trim(),
contextBefore: idx > 0 ? paragraphs[idx - 1].substring(0, 100) + '...' : 'Anfang des Dokuments',
contextAfter: idx < paragraphs.length - 1 ? paragraphs[idx + 1].substring(0, 100) + '...' : 'Ende des Dokuments'
}));
const newSource = {
id: newSourceId,
title: title || 'Neues Dokument',
type: type,
uploadDate: new Date().toISOString().split('T')[0],
size: `${Math.round(content.length / 1024)} KB`,
status: 'ready',
chunkCount: chunks.length,
author: 'Benutzer-Upload',
isActive: true,
chunks: chunks.length > 0 ? chunks : [{
id: `chk-${newSourceId}-1`,
sourceId: newSourceId,
pageNumber: 1,
sectionTitle: 'Gesamter Inhalt',
content: content,
contextBefore: '',
contextAfter: ''
}]
};
setNotebooks((prev) =>
prev.map((nb) => {
if (nb.id !== activeNotebookId) return nb;
return {
...nb,
updatedAt: new Date().toISOString().split('T')[0],
sources: [newSource, ...nb.sources]
};
})
);
setIsAddSourceOpen(false);
};
return (
<div className="flex h-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
{/* LEFT SIDEBAR: Notebook Navigation & Sources */}
<aside className="w-80 bg-white border-r border-slate-200 flex flex-col z-20 shrink-0 hidden md:flex">
{/* App Title & Header */}
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center space-x-2.5">
<div className="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center text-white shadow-sm">
<BookOpen className="w-4 h-4" />
</div>
<div>
<h1 className="font-semibold text-slate-900 tracking-tight text-base leading-none">SourceLM</h1>
<p className="text-xs text-slate-500 mt-1">Fokussierte Quellen-KI</p>
</div>
</div>
<button
onClick={() => setIsSchemaModalOpen(true)}
title="Architektur & Schema"
className="p-1.5 rounded-md hover:bg-slate-100 text-slate-500 hover:text-slate-800 transition-colors"
>
<Database className="w-4 h-4" />
</button>
</div>
{/* Notebook Selector */}
<div className="p-3 border-b border-slate-100 bg-slate-50/50">
<div className="flex items-center justify-between text-xs font-medium text-slate-500 mb-2 px-1">
<span>ARBEITSBEREICHE</span>
<button
onClick={handleCreateNewNotebook}
className="text-indigo-600 hover:text-indigo-700 flex items-center gap-1 font-semibold"
>
<Plus className="w-3.5 h-3.5" /> Neu
</button>
</div>
<div className="space-y-1 max-h-36 overflow-y-auto pr-1">
{notebooks.map((nb) => (
<button
key={nb.id}
onClick={() => setActiveNotebookId(nb.id)}
className={`w-full text-left px-3 py-2 rounded-lg text-xs font-medium transition-all flex items-center justify-between ${
nb.id === activeNotebookId
? 'bg-indigo-50 text-indigo-700 font-semibold border border-indigo-100'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
<span className="truncate">{nb.title}</span>
<span className="text-[10px] bg-slate-200/60 text-slate-600 px-1.5 py-0.5 rounded-full ml-2">
{nb.sources.length}
</span>
</button>
))}
</div>
</div>
{/* Sources Management Header */}
<div className="p-3 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-slate-500" />
<span className="text-xs font-semibold text-slate-700 uppercase tracking-wider">
Quellen ({activeNotebook.sources.length})
</span>
</div>
<button
onClick={() => setIsAddSourceOpen(true)}
className="px-2.5 py-1 bg-indigo-600 hover:bg-indigo-700 text-white rounded-md text-xs font-medium transition-colors flex items-center gap-1 shadow-sm"
>
<Plus className="w-3.5 h-3.5" /> Hinzufügen
</button>
</div>
{/* Source List */}
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{activeNotebook.sources.length === 0 ? (
<div className="text-center py-10 px-4 border-2 border-dashed border-slate-200 rounded-xl">
<FileUp className="w-8 h-8 text-slate-300 mx-auto mb-2" />
<p className="text-xs text-slate-500 font-medium">Noch keine Quellen vorhanden</p>
<p className="text-[11px] text-slate-400 mt-1">Laden Sie PDFs, Docs oder Texte hoch.</p>
<button
onClick={() => setIsAddSourceOpen(true)}
className="mt-3 px-3 py-1.5 bg-slate-900 text-white rounded-lg text-xs font-medium hover:bg-slate-800 transition-colors"
>
Quelle Uploaden
</button>
</div>
) : (
activeNotebook.sources.map((src) => (
<div
key={src.id}
className={`p-2.5 rounded-xl border transition-all ${
src.isActive
? 'border-slate-200 bg-white shadow-sm'
: 'border-slate-100 bg-slate-50/60 opacity-60'
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex items-start gap-2 min-w-0">
<input
type="checkbox"
checked={src.isActive}
onChange={() => handleToggleSourceActive(src.id)}
className="mt-1 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer"
/>
<div className="min-w-0">
<h4 className="text-xs font-medium text-slate-800 truncate leading-snug">{src.title}</h4>
<div className="flex items-center gap-2 mt-1 text-[10px] text-slate-400">
<span className="uppercase font-semibold text-slate-500">{src.type}</span>
<span>•</span>
<span>{src.size}</span>
<span>•</span>
<span>{src.chunkCount} Abschnitte</span>
</div>
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={() => setSelectedSourceForDetail(src)}
title="Quelle ansehen"
className="p-1 hover:bg-slate-100 text-slate-400 hover:text-slate-600 rounded"
>
<Eye className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleDeleteSource(src.id)}
title="Löschen"
className="p-1 hover:bg-red-50 text-slate-400 hover:text-red-600 rounded"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
{src.status === 'ready' && (
<div className="mt-2 flex items-center gap-1 text-[10px] text-emerald-600 font-medium bg-emerald-50 px-2 py-0.5 rounded-full w-fit">
<CheckCircle2 className="w-3 h-3" /> Bereit für Abfragen
</div>
)}
</div>
))
)}
</div>
{/* Bottom Sidebar Settings & Info */}
<div className="p-3 border-t border-slate-100 bg-slate-50/50 flex items-center justify-between">
<button
onClick={() => setIsSettingsOpen(true)}
className="flex items-center gap-2 text-xs text-slate-600 hover:text-slate-900 font-medium"
>
<Settings className="w-4 h-4 text-slate-500" />
<span>Einstellungen</span>
</button>
<span className="text-[10px] font-mono bg-slate-200 text-slate-600 px-1.5 py-0.5 rounded">
v1.2 Grounded
</span>
</div>
</aside>
{/* CENTER & MAIN WORKSPACE: Chat & Intelligence View */}
<main className="flex-1 flex flex-col bg-slate-50 min-w-0 relative">
{/* Top Header Bar */}
<header className="h-14 bg-white border-b border-slate-200 px-4 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3 min-w-0">
<div className="md:hidden">
<BookOpen className="w-5 h-5 text-indigo-600" />
</div>
<div className="min-w-0">
<h2 className="text-sm font-semibold text-slate-900 truncate">{activeNotebook.title}</h2>
<p className="text-xs text-slate-500 truncate hidden sm:block">{activeNotebook.description}</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setActiveTab('overview')}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors flex items-center gap-1.5 ${
activeTab === 'overview'
? 'bg-indigo-50 text-indigo-700 font-semibold'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
<Sparkles className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Quellen-Übersicht</span>
</button>
<button
onClick={() => setActiveTab('chat')}
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors flex items-center gap-1.5 ${
activeTab === 'chat'
? 'bg-indigo-50 text-indigo-700 font-semibold'
: 'text-slate-600 hover:bg-slate-100'
}`}
>
<MessageSquare className="w-3.5 h-3.5" />
<span>Chat</span>
</button>
</div>
</header>
{/* Mobile View Switcher Tabs */}
<div className="md:hidden flex border-b border-slate-200 bg-white text-xs font-medium text-slate-600">
<button
onClick={() => setActiveTab('chat')}
className={`flex-1 py-2.5 text-center border-b-2 ${
activeTab === 'chat' ? 'border-indigo-600 text-indigo-600 font-semibold' : 'border-transparent'
}`}
>
Chat
</button>
<button
onClick={() => setActiveTab('sources')}
className={`flex-1 py-2.5 text-center border-b-2 ${
activeTab === 'sources' ? 'border-indigo-600 text-indigo-600 font-semibold' : 'border-transparent'
}`}
>
Quellen ({activeNotebook.sources.length})
</button>
<button
onClick={() => setActiveTab('citations')}
className={`flex-1 py-2.5 text-center border-b-2 ${
activeTab === 'citations' ? 'border-indigo-600 text-indigo-600 font-semibold' : 'border-transparent'
}`}
>
Details & Zitate
</button>
</div>
{/* MAIN BODY AREA */}
<div className="flex-1 flex overflow-hidden">
{/* TAB 1: OVERVIEW / SYNTHESIS VIEW */}
{activeTab === 'overview' ? (
<div className="flex-1 overflow-y-auto p-6 max-w-4xl mx-auto space-y-6">
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-slate-900 flex items-center gap-2">
<Sparkles className="w-5 h-5 text-indigo-600" /> Synthetisierte Quellenübersicht
</h3>
<span className="text-xs text-slate-400">Automatisch generiert</span>
</div>
<p className="text-xs text-slate-600 leading-relaxed">
Dieses Notebook fasst aktuell {activeNotebook.sources.length} Quelldokumente zusammen. Die Analysen konzentrieren sich auf gesetzliche Vorgaben, Risikomanagement und Zeitpläne für die Implementierung im Unternehmenskontext.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<h4 className="text-xs font-semibold text-slate-800 mb-2 flex items-center gap-1.5">
<Tag className="w-3.5 h-3.5 text-indigo-600" /> Zentrale Themen
</h4>
<ul className="text-xs text-slate-600 space-y-1 list-disc list-inside">
<li>Verbotene KI-Praktiken & Strafmaß</li>
<li>Risikomanagement für Hochrisiko-Systeme</li>
<li>Transparenz- und Kennzeichnungspflichten</li>
<li>Fristen & Übergangsregelungen für KMUs</li>
</ul>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<h4 className="text-xs font-semibold text-slate-800 mb-2 flex items-center gap-1.5">
<ShieldAlert className="w-3.5 h-3.5 text-amber-600" /> Offene Fragen & Risiken
</h4>
<ul className="text-xs text-slate-600 space-y-1 list-disc list-inside">
<li>Konkrete Prüfkriterien für Unterschwelligkeit</li>
<li>Haftungsgrenzen bei Open-Source-KI-Modellen</li>
<li>Ressourcenaufwand für Zertifizierungsstellen</li>
</ul>
</div>
</div>
</div>
</div>
) : (
/* TAB 2: CHAT WORKSPACE */
<div className="flex-1 flex flex-col h-full bg-slate-50">
{/* Mode Selector Ribbon */}
<div className="bg-white border-b border-slate-200 px-4 py-2 flex items-center gap-2 overflow-x-auto shrink-0 no-scrollbar">
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider shrink-0 mr-1">
Modus:
</span>
{CHAT_MODES.map((mode) => {
const Icon = mode.icon;
const isActive = activeChatMode === mode.id;
return (
<button
key={mode.id}
onClick={() => setActiveChatMode(mode.id)}
title={mode.description}
className={`px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-all flex items-center gap-1.5 shrink-0 ${
isActive
? 'bg-slate-900 text-white shadow-sm'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200/70'
}`}
>
<Icon className="w-3.5 h-3.5" />
{mode.label}
</button>
);
})}
</div>
{/* Chat Messages Scroll Container */}
<div className="flex-1 overflow-y-auto p-4 md:p-6 space-y-6">
{currentMessages.length === 0 ? (
<div className="max-w-xl mx-auto py-16 text-center">
<div className="w-12 h-12 rounded-2xl bg-indigo-50 border border-indigo-100 text-indigo-600 flex items-center justify-center mx-auto mb-4 shadow-sm">
<Sparkles className="w-6 h-6" />
</div>
<h3 className="text-base font-semibold text-slate-900">
Stellen Sie Fragen zu Ihren Quellen
</h3>
<p className="text-xs text-slate-500 mt-1 max-w-md mx-auto leading-relaxed">
SourceLM beantwortet Ihre Anfragen ausschließlich mit Fakten aus den {activeSources.length} aktiv geschalteten Quelldokumenten.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-6 text-left">
<button
onClick={() => setInputPrompt('Welche Bußgelder drohen bei Nichtbefolgung?')}
className="p-3 bg-white hover:bg-slate-100 border border-slate-200 rounded-xl text-xs text-slate-700 transition-all text-left shadow-2xs"
>
"Welche Bußgelder drohen bei Nichtbefolgung?"
</button>
<button
onClick={() => setInputPrompt('Fasse alle Übergangsfristen zusammen.')}
className="p-3 bg-white hover:bg-slate-100 border border-slate-200 rounded-xl text-xs text-slate-700 transition-all text-left shadow-2xs"
>
"Fasse alle Übergangsfristen zusammen."
</button>
</div>
</div>
) : (
currentMessages.map((msg) => (
<div
key={msg.id}
className={`flex flex-col max-w-3xl mx-auto ${
msg.role === 'user' ? 'items-end' : 'items-start'
}`}
>
<div className="flex items-center gap-2 mb-1 text-[11px] text-slate-400 px-1">
<span className="font-medium text-slate-600">
{msg.role === 'user' ? 'Sie' : 'SourceLM Assistant'}
</span>
<span>•</span>
<span>{msg.createdAt}</span>
</div>
<div
className={`p-4 rounded-2xl text-xs leading-relaxed shadow-2xs transition-all ${
msg.role === 'user'
? 'bg-indigo-600 text-white rounded-br-none'
: 'bg-white text-slate-800 border border-slate-200/90 rounded-bl-none'
}`}
>
<p className="whitespace-pre-wrap">{msg.content}</p>
{/* Interactive Citations Ribbon for Assistant Messages */}
{msg.role === 'assistant' && msg.citations && msg.citations.length > 0 && (
<div className="mt-4 pt-3 border-t border-slate-100">
<span className="text-[10px] font-semibold text-slate-400 uppercase tracking-wider block mb-2">
Verwendete Quellenstellen ({msg.citations.length}):
</span>
<div className="flex flex-wrap gap-1.5">
{msg.citations.map((cit) => (
<button
key={cit.id}
onClick={() => {
setSelectedCitation(cit);
setActiveTab('citations');
}}
className="px-2.5 py-1 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 border border-indigo-200/60 rounded-md text-[11px] font-medium transition-colors flex items-center gap-1.5"
>
<FileText className="w-3 h-3 text-indigo-500" />
<span>
[{cit.citationIndex}] {cit.sourceTitle} (S. {cit.pageNumber})
</span>
</button>
))}
</div>
</div>
)}
</div>
</div>
))
)}
{/* Streaming Indicator */}
{isGenerating && (
<div className="max-w-3xl mx-auto flex items-center gap-3 bg-white p-3 rounded-xl border border-slate-200 text-xs text-slate-600 animate-pulse">
<Sparkles className="w-4 h-4 text-indigo-600 animate-spin" />
<span>{streamingMessage || 'Verarbeite Anfrage...'}</span>
</div>
)}
<div ref={chatBottomRef} />
</div>
{/* Chat Input Bar */}
<div className="p-3 md:p-4 bg-white border-t border-slate-200 shrink-0">
<form onSubmit={handleSendMessage} className="max-w-3xl mx-auto relative">
{/* Source scoping notice */}
<div className="flex items-center justify-between text-[11px] text-slate-400 mb-2 px-1">
<span className="flex items-center gap-1">
<LockIcon className="w-3 h-3 text-emerald-600" />
Streng isoliert auf {activeSources.length} aktive Quellen
</span>
<button
type="button"
onClick={() => setChatHistories((p) => ({ ...p, [activeNotebookId]: [] }))}
className="hover:text-slate-600 transition-colors"
>
Chatverlauf zurücksetzen
</button>
</div>
<div className="relative flex items-center">
<textarea
value={inputPrompt}
onChange={(e) => setInputPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
placeholder="Stellen Sie eine Frage zu Ihren Quellen..."
rows={2}
className="w-full pl-4 pr-12 py-3 bg-slate-50 border border-slate-200 rounded-xl text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent resize-none"
/>
<button
type="submit"
disabled={!inputPrompt.trim() || isGenerating}
className="absolute right-2.5 p-2 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 text-white rounded-lg transition-colors shadow-xs"
>
<ArrowUpRight className="w-4 h-4" />
</button>
</div>
</form>
</div>
</div>
)}
</div>
</main>
{/* RIGHT SIDEBAR: Citation & Source Chunk Inspector */}
<aside className="w-80 bg-white border-l border-slate-200 flex flex-col z-20 shrink-0 hidden lg:flex">
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<LinkIcon className="w-4 h-4 text-indigo-600" />
<h3 className="text-xs font-semibold text-slate-800 uppercase tracking-wider">
Quellenstellen-Inspector
</h3>
</div>
{selectedCitation && (
<button
onClick={() => setSelectedCitation(null)}
className="text-xs text-slate-400 hover:text-slate-600"
>
Schließen
</button>
)}
</div>
<div className="flex-1 overflow-y-auto p-4">
{selectedCitation ? (
<div className="space-y-4">
{/* Citation Meta Card */}
<div className="bg-indigo-50/60 p-3.5 rounded-xl border border-indigo-100">
<div className="flex items-center justify-between text-xs font-medium text-indigo-900 mb-1">
<span className="truncate">{selectedCitation.sourceTitle}</span>
<span className="bg-indigo-200/60 text-indigo-800 px-1.5 py-0.5 rounded text-[10px]">
S. {selectedCitation.pageNumber}
</span>
</div>
<p className="text-[11px] font-semibold text-indigo-700 mt-1">
{selectedCitation.sectionTitle}
</p>
</div>
{/* Exact Quote Highlighted */}
<div className="space-y-2">
<label className="text-[10px] font-semibold text-slate-400 uppercase tracking-wider">
Hervorgehobene Textstelle:
</label>
<div className="p-3 bg-amber-50/80 border-l-4 border-amber-400 text-xs text-slate-800 rounded-r-lg leading-relaxed italic">
"{selectedCitation.quote}"
</div>
</div>
{/* Surrounding Context */}
<div className="space-y-2">
<label className="text-[10px] font-semibold text-slate-400 uppercase tracking-wider">
Kontext im Dokument:
</label>
<div className="p-3 bg-slate-50 rounded-xl border border-slate-100 text-[11px] text-slate-500 leading-relaxed font-mono">
<span className="opacity-60">{selectedCitation.contextBefore}</span>
<mark className="bg-amber-200/70 text-slate-900 px-1 py-0.5 rounded font-sans mx-1">
{selectedCitation.quote}
</mark>
<span className="opacity-60">{selectedCitation.contextAfter}</span>
</div>
</div>
</div>
) : (
<div className="py-20 text-center text-slate-400">
<Eye className="w-8 h-8 mx-auto mb-2 opacity-30" />
<p className="text-xs font-medium">Kein Zitat ausgewählt</p>
<p className="text-[11px] text-slate-400 mt-1 px-4">
Klicken Sie auf eine Quellenangabe im Chat [1], um die exakte Original-Textstelle einzusehen.
</p>
</div>
)}
</div>
</aside>
{}
{/* ADD SOURCE MODAL */}
{isAddSourceOpen && (
<AddSourceModal
onClose={() => setIsAddSourceOpen(false)}
onAddCustomText={handleAddCustomTextSource}
/>
)}
{/* DETAILED SOURCE INSPECTOR MODAL */}
{selectedSourceForDetail && (
<SourceDetailModal
source={selectedSourceForDetail}
onClose={() => setSelectedSourceForDetail(null)}
/>
)}
{/* SYSTEM ARCHITECTURE & SCHEMA MODAL */}
{isSchemaModalOpen && (
<SchemaAndArchitectureModal onClose={() => setIsSchemaModalOpen(false)} />
)}
{/* SETTINGS MODAL */}
{isSettingsOpen && (
<SettingsModal
apiKey={apiKey}
setApiKey={setApiKey}
onClose={() => setIsSettingsOpen(false)}
/>
)}
</div>
);
}
// Lock icon inline SVG component
function LockIcon(props) {
return (
<svg {...props} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
);
}
function AddSourceModal({ onClose, onAddCustomText }) {
const [activeTab, setActiveTab] = useState('text'); // 'file' | 'text' | 'url'
const [rawText, setRawText] = useState('');
const [docTitle, setDocTitle] = useState('');
const [webUrl, setWebUrl] = useState('');
const handleSubmitText = (e) => {
e.preventDefault();
if (!rawText.trim()) return;
onAddCustomText(docTitle || 'Eingefügter Text', rawText, activeTab === 'url' ? 'url' : 'txt');
};
return (
<div className="fixed inset-0 bg-slate-900/50 backdrop-blur-xs flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl max-w-lg w-full shadow-xl border border-slate-200 overflow-hidden">
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-900">Neue Quelle hinzufügen</h3>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-slate-600 rounded">
<X className="w-4 h-4" />
</button>
</div>
{/* Tab Selector */}
<div className="flex border-b border-slate-100 bg-slate-50 text-xs font-medium text-slate-600">
<button
onClick={() => setActiveTab('text')}
className={`flex-1 py-2.5 border-b-2 ${
activeTab === 'text' ? 'border-indigo-600 text-indigo-600 font-semibold bg-white' : ''
}`}
>
Text einfügen
</button>
<button
onClick={() => setActiveTab('file')}
className={`flex-1 py-2.5 border-b-2 ${
activeTab === 'file' ? 'border-indigo-600 text-indigo-600 font-semibold bg-white' : ''
}`}
>
Datei Upload
</button>
<button
onClick={() => setActiveTab('url')}
className={`flex-1 py-2.5 border-b-2 ${
activeTab === 'url' ? 'border-indigo-600 text-indigo-600 font-semibold bg-white' : ''
}`}
>
Webseite / URL
</button>
</div>
<div className="p-5">
{activeTab === 'text' && (
<form onSubmit={handleSubmitText} className="space-y-4">
<div>
<label className="block text-xs font-medium text-slate-700 mb-1">Titel der Quelle</label>
<input
type="text"
value={docTitle}
onChange={(e) => setDocTitle(e.target.value)}
placeholder="z.B. Strategiepapier 2026"
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-xs text-slate-800 focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-700 mb-1">Inhalt / Rohtext</label>
<textarea
value={rawText}
onChange={(e) => setRawText(e.target.value)}
placeholder="Fügen Sie hier Ihren Text oder Artikel ein..."
rows={6}
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-xs text-slate-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 resize-none"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<button
type="button"
onClick={onClose}
className="px-3 py-1.5 rounded-lg text-xs font-medium text-slate-600 hover:bg-slate-100"
>
Abbrechen
</button>
<button
type="submit"
disabled={!rawText.trim()}
className="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-medium shadow-sm transition-colors"
>
Quelle verarbeiten
</button>
</div>
</form>
)}
{activeTab === 'file' && (
<div className="py-8 text-center border-2 border-dashed border-slate-200 rounded-xl bg-slate-50/50">
<Upload className="w-8 h-8 text-slate-400 mx-auto mb-2" />
<p className="text-xs font-medium text-slate-700">Ziehen Sie PDF, DOCX oder TXT Dateien hierher</p>
<p className="text-[11px] text-slate-400 mt-1">oder wählen Sie eine Datei von Ihrem Computer</p>
<button
type="button"
onClick={() =>
onAddCustomText(
'Hochgeladenes_Dokument.pdf',
'Dies ist ein extrahierter Beispieltext aus einem hochgeladenen PDF-Dokument. Er enthält Daten und Fakten für das Quellen-Retrieval.',
'pdf'
)
}
className="mt-4 px-3 py-1.5 bg-slate-900 text-white rounded-lg text-xs font-medium hover:bg-slate-800 transition-colors"
>
Demo-Datei importieren
</button>
</div>
)}
{activeTab === 'url' && (
<div className="space-y-4">
<div>
<label className="block text-xs font-medium text-slate-700 mb-1">Webseiten URL</label>
<input
type="url"
value={webUrl}
onChange={(e) => setWebUrl(e.target.value)}
placeholder="https://beispiel.de/artikel"
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-xs text-slate-800 focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<button
type="button"
onClick={() =>
onAddCustomText(
webUrl || 'Webseite',
'Extrahiert von Web-URL: Artikel über Regulierung und Compliance im KI-Umfeld.',
'url'
)
}
className="w-full py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-medium transition-colors"
>
Inhalte abrufen & verarbeiten
</button>
</div>
)}
</div>
</div>
</div>
);
}
function SourceDetailModal({ source, onClose }) {
return (
<div className="fixed inset-0 bg-slate-900/50 backdrop-blur-xs flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl max-w-2xl w-full max-h-[85vh] flex flex-col shadow-xl border border-slate-200 overflow-hidden">
<div className="p-4 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
<div>
<h3 className="text-sm font-semibold text-slate-900">{source.title}</h3>
<p className="text-[11px] text-slate-500 mt-0.5">
Typ: {source.type.toUpperCase()} • {source.chunkCount} Textabschnitte • Autor: {source.author || 'Unbekannt'}
</p>
</div>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-slate-600 rounded">
<X className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-5 space-y-4">
<h4 className="text-xs font-semibold text-slate-700 uppercase tracking-wider">
Extrahierte Textabschnitte ({source.chunks.length})
</h4>
{source.chunks.map((chk, idx) => (
<div key={chk.id} className="p-3 bg-slate-50 rounded-xl border border-slate-200/80 space-y-1.5">
<div className="flex items-center justify-between text-[11px] font-semibold text-slate-700">
<span>{chk.sectionTitle}</span>
<span className="text-slate-400">Seite {chk.pageNumber || 1}</span>
</div>
<p className="text-xs text-slate-600 leading-relaxed font-sans">{chk.content}</p>
</div>
))}
</div>
</div>
</div>
);
}
function SchemaAndArchitectureModal({ onClose }) {
return (
<div className="fixed inset-0 bg-slate-900/50 backdrop-blur-xs flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl max-w-3xl w-full max-h-[85vh] flex flex-col shadow-xl border border-slate-200 overflow-hidden">
<div className="p-4 border-b border-slate-100 flex items-center justify-between bg-slate-900 text-white">
<div className="flex items-center gap-2">
<Database className="w-4 h-4 text-indigo-400" />
<h3 className="text-sm font-semibold">SourceLM Technische Architektur & Supabase Schema</h3>
</div>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-white rounded">
<X className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-5 space-y-6 text-xs text-slate-700 font-mono bg-slate-950 text-slate-200">
<div>
<h4 className="text-indigo-400 font-sans font-semibold mb-2">1. PostgreSQL / Supabase pgvector Schema</h4>
<pre className="p-3 bg-slate-900 rounded-lg text-[11px] overflow-x-auto text-emerald-400 border border-slate-800">
{`-- Vector extension for semantic similarity
CREATE EXTENSION IF NOT EXISTS vector;
-- Notebooks Table
CREATE TABLE notebooks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users NOT NULL,
title TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Sources Table
CREATE TABLE sources (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
notebook_id UUID REFERENCES notebooks(id) ON DELETE CASCADE,
title TEXT NOT NULL,
source_type TEXT NOT NULL,
file_url TEXT,
status TEXT DEFAULT 'processing',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Source Chunks with Vector Embeddings
CREATE TABLE source_chunks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_id UUID REFERENCES sources(id) ON DELETE CASCADE,
content TEXT NOT NULL,
embedding VECTOR(768), -- Gemini text-embedding-004
page_number INT,
section_title TEXT,
chunk_index INT
);`}
</pre>
</div>
<div>
<h4 className="text-indigo-400 font-sans font-semibold mb-2">2. Strict System Prompt Blueprint</h4>
<pre className="p-3 bg-slate-900 rounded-lg text-[11px] overflow-x-auto text-amber-300 border border-slate-800 font-sans leading-relaxed">
"Du bist ein quellengebundener Rechercheassistent. Beantworte Fragen ausschließlich anhand des bereitgestellten Quellenkontexts. Verwende keine externen Fakten, außer sie werden ausdrücklich als nicht aus den Quellen stammend gekennzeichnet. Erfinde keine Informationen. Wenn die Quellen nicht ausreichen, sage dies klar mit der Formulierung: 'In den ausgewählten Quellen konnte ich dazu keine ausreichenden Informationen finden.' Verweise bei jeder Aussage auf die passende Quellen-ID [1], [2]."
</pre>
</div>
</div>
</div>
</div>
);
}
function SettingsModal({ apiKey, setApiKey, onClose }) {
return (
<div className="fixed inset-0 bg-slate-900/50 backdrop-blur-xs flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl max-w-md w-full shadow-xl border border-slate-200 overflow-hidden">
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-900">Einstellungen</h3>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-slate-600 rounded">
<X className="w-4 h-4" />
</button>
</div>
<div className="p-5 space-y-4">
<div>
<label className="block text-xs font-medium text-slate-700 mb-1">
Google Gemini API Key (Optional für Echtzeit-KI)
</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="AIzaSy..."
className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-xs text-slate-800 focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<p className="text-[10px] text-slate-400 mt-1">
Ohne API-Schlüssel wird die integrierte lokale semantische Such-Simulation verwendet.
</p>
</div>
<div className="pt-2 border-t border-slate-100">
<div className="flex items-center justify-between text-xs text-slate-600">
<span>Datenschutz & Isolation</span>
<span className="text-emerald-600 font-semibold">Aktiviert</span>
</div>
</div>
<div className="flex justify-end pt-2">
<button
onClick={onClose}
className="px-4 py-1.5 bg-slate-900 text-white rounded-lg text-xs font-medium hover:bg-slate-800 transition-colors"
>
Speichern & Schließen
</button>
</div>
</div>
</div>
</div>
);
}
Als Beispiel habe ich den KI-generierten Code der App SourceLM verwendet, die ich im Gemini-Chat über die Canvas-Funktion im Vibe-Coding-Stil entwickelt habe. Der Code wurde nicht auf Fehler überprüft! Nun kann ich in einer Infografik übersichtlich aufzeigen, was meine Mini-Notebook-App alles so kann.

Den Code habe ich als Textdatei in den Chat hochgeladen, die Bildfunktion aktiviert und folgenden Prompt hinzugefügt: ***Erstelle aus dem Code drei Infografiken im Sketchnote-Stil in drei unterschiedlichen Formaten. Stelle die zentralen Funktionen der App vor***. Das war’s! Nun erzeugt ChatGPT Images drei Bilder, die ich nach einem Download für meine Präsentation nutzen kann.
Autor

