/* Shared components — PromptOptimizerModal, TrendChart, ReportChat
Requires api.js to be loaded first. */
const { useState: useStateC, useEffect: useEffectC, useRef: useRefC } = React;
/* ============================================================
PROMPT OPTIMIZER MODAL
Props:
rawPrompt — string already built from form fields
onClose — () => void
onLaunched — (simId: string) => void called after simulation started
============================================================ */
function PromptOptimizerModal({ rawPrompt, onClose, onLaunched }) {
const [phase, setPhase] = useStateC('optimizing'); // optimizing | review | launching | error
const [result, setResult] = useStateC(null);
const [edited, setEdited] = useStateC('');
const [err, setErr] = useStateC('');
useEffectC(() => {
let live = true;
window.RICS_API.optimizePrompt(rawPrompt)
.then((r) => {
if (!live) return;
setResult(r);
setEdited(r.improved);
setPhase('review');
})
.catch((e) => {
if (!live) return;
setErr(e.message);
setPhase('error');
});
return () => { live = false; };
}, [rawPrompt]);
const handleConfirm = async () => {
setPhase('launching');
try {
// 3-Schritt-Lifecycle: create → prepare → start
const created = await window.RICS_API.createSimulation({ prompt: edited });
const simId = created.simulation_id || created.simulationId || created.id;
await window.RICS_API.prepareSimulation({ simulation_id: simId });
await window.RICS_API.startSimulation({ simulation_id: simId });
onLaunched(simId);
} catch (e) {
setErr(e.message);
setPhase('error');
}
};
return (
{ if (e.target === e.currentTarget && phase !== 'launching') onClose(); }}
>
{/* Header */}
Prompt-Optimierung · KI-gestützt
{phase === 'optimizing' ? 'Prompt wird strukturiert…' :
phase === 'launching' ? 'Simulation wird gestartet…' :
phase === 'error' ? 'Fehler aufgetreten' :
'Prompt prüfen und bestätigen'}
{phase !== 'launching' && (
)}
{/* Body */}
{/* Loading state */}
{(phase === 'optimizing' || phase === 'launching') && (
{phase === 'optimizing' ? 'GeoStrat-Prompt-Engine · aktiv' : 'Backend-Simulation wird initialisiert…'}
)}
{/* Error state */}
{phase === 'error' && (
)}
{/* Review state */}
{phase === 'review' && result && (
{/* Side-by-side comparison */}
Original
{result.original}
{/* Changes list */}
{result.changes && (
Verbesserungen
{result.changes.split('\n').filter(Boolean).map((line, i) => (
{line}
))}
)}
{/* Action bar */}
)}
);
}
/* ============================================================
TREND CHART
Props:
simId (string) — fetches own data if snapshotData not provided
snapshotData (object, optional) — pre-fetched snapshot; skips internal fetch
Renders two tabs: Szenarien-Wahrscheinlichkeiten | Portfolio-Gewichtungen
============================================================ */
function TrendChart({ simId, snapshotData }) {
const [fetched, setFetched] = useStateC(null);
const [tab, setTab] = useStateC('prob');
const [loading, setLoading] = useStateC(!snapshotData);
const [err, setErr] = useStateC('');
useEffectC(() => {
// If parent already fetched the snapshot, skip the request
if (snapshotData !== undefined) { setLoading(false); return; }
if (!simId) return;
setLoading(true);
window.RICS_API.getSnapshot(simId)
.then((d) => { setFetched(d); setLoading(false); })
.catch((e) => { setErr(e.message); setLoading(false); });
}, [simId, snapshotData]);
const data = snapshotData !== undefined ? snapshotData : fetched;
const chartData = tab === 'prob' ? data?.probability_history : data?.weight_history;
if (loading) {
return (
Verlaufsdaten werden geladen…
);
}
if (err || !chartData || Object.keys(chartData).length === 0) {
return (
Verlaufsdaten noch nicht verfügbar
Zeitreihen erscheinen nach dem nächsten automatischen Agent-Run (täglich 00:00 + 12:00 UTC).
);
}
return (
{/* Tab switcher */}
{[['prob', 'Szenarien · P(%)'], ['weight', 'Portfolio · Gewichtung']].map(([k, label]) => (
))}
);
}
function SvgLineChart({ series, valueKey }) {
const W = 800, H = 220, PAD = { top: 12, right: 16, bottom: 36, left: 40 };
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
// Collect all points
const allPoints = Object.values(series).flatMap((s) => s.points || []);
if (allPoints.length === 0) return null;
const times = allPoints.map((p) => new Date(p.timestamp).getTime());
const vals = allPoints.map((p) => p[valueKey] ?? 0);
const tMin = Math.min(...times), tMax = Math.max(...times);
const vMin = 0, vMax = Math.max(...vals, 1);
const tx = (t) => PAD.left + ((new Date(t).getTime() - tMin) / Math.max(tMax - tMin, 1)) * plotW;
const ty = (v) => PAD.top + (1 - (v - vMin) / (vMax - vMin)) * plotH;
const COLORS = ['#FFFFFF', '#C6C6C6', '#8A8887', '#5E5C5B', '#E5E2E1', '#9B9897', '#6E6C6B', '#3F3D3C'];
const seriesKeys = Object.keys(series);
// X-axis tick positions (up to 5 ticks)
const tickCount = Math.min(5, allPoints.length);
const xTicks = Array.from({ length: tickCount }, (_, i) => {
const t = tMin + (i / Math.max(tickCount - 1, 1)) * (tMax - tMin);
return { t, label: new Date(t).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) };
});
return (
{/* Legend */}
{seriesKeys.map((key, i) => (
{series[key].label || key}
))}
);
}
/* ============================================================
REPORT CHAT
Props: simId (string)
============================================================ */
function ReportChat({ simId }) {
const [messages, setMessages] = useStateC([]);
const [input, setInput] = useStateC('');
const [thinking, setThinking] = useStateC(false);
const bottomRef = useRefC(null);
const SUGGESTIONS = [
'Erkläre das dominante Szenario.',
'Welche Risiken bestehen?',
'Welche Portfolio-Tilts empfiehlst du?',
'Was sind die wichtigsten Trigger-Events?',
];
useEffectC(() => {
if (bottomRef.current) bottomRef.current.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const send = async (text) => {
if (!text.trim() || thinking) return;
const userMsg = { role: 'user', content: text };
setMessages((prev) => [...prev, userMsg]);
setInput('');
setThinking(true);
const history = messages.slice(-10); // last 10 turns
try {
const res = await window.RICS_API.chatWithReport(simId, text, history);
setMessages((prev) => [...prev, { role: 'assistant', content: res.response }]);
} catch (e) {
setMessages((prev) => [...prev, { role: 'assistant', content: `Fehler: ${e.message}` }]);
} finally {
setThinking(false);
}
};
return (
{/* Header */}
Report-Agent
Fragen zu diesem Report · Kontext vollständig bekannt
{/* Messages */}
{messages.length === 0 && (
Vorschläge
{SUGGESTIONS.map((s) => (
))}
)}
{messages.map((m, i) => (
))}
{thinking && (
)}
{/* Input */}
setInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(input); } }}
placeholder="Frage stellen…"
disabled={thinking}
style={{ flex: 1, fontSize: 13 }}
/>
);
}
Object.assign(window, { PromptOptimizerModal, TrendChart, ReportChat });