const BASE = '/api' // Backend-Fehler (400/409 mit detail) als Error werfen statt sie zu verschlucken async function jsonOrThrow(res) { if (!res.ok) { let detail = `Fehler (HTTP ${res.status})` try { const data = await res.json() if (data.detail) detail = typeof data.detail === 'string' ? data.detail : JSON.stringify(data.detail) } catch { /* kein JSON-Body */ } throw new Error(detail) } return res.json() } export async function fetchGuides() { const res = await fetch(`${BASE}/guides`) return res.json() } export async function fetchGuideSteps(topic) { const res = await fetch(`${BASE}/guides/steps?topic=${encodeURIComponent(topic)}`) return res.json() } export async function fetchGuideLocks(topic) { const res = await fetch(`${BASE}/guides/locks?topic=${encodeURIComponent(topic)}`) return res.json() } export async function createGuide(topic, format, instructions = '', provider = 'claude', abStep = null) { const res = await fetch(`${BASE}/guides`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic, format, instructions, provider, ab_step: abStep }), }) return jsonOrThrow(res) } export async function fetchActiveBlocks() { const res = await fetch(`${BASE}/blocks/active`) return res.json() } export async function fetchBlocksStatus(topic) { const res = await fetch(`${BASE}/blocks/status?topic=${encodeURIComponent(topic)}`) return res.json() } export async function createBlocks(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', abPhase = null, abStep = null, toStep = null, research = true) { const res = await fetch(`${BASE}/blocks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic, instructions, provider, source_type: sourceType, source_location: sourceOrt, ab_phase: abPhase, ab_step: abStep, to_step: toStep, research }), }) return jsonOrThrow(res) } export async function resetBlocksFromStep(topic, abStep) { const res = await fetch(`${BASE}/blocks/reset-step`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic, ab_step: abStep }), }) return jsonOrThrow(res) } export async function cancelBlocks(topic) { await fetch(`${BASE}/blocks/cancel?topic=${encodeURIComponent(topic)}`, { method: 'POST' }) } export async function deleteBlocks(topic) { await fetch(`${BASE}/blocks?topic=${encodeURIComponent(topic)}`, { method: 'DELETE' }) } // --- Block-Learning: Chat, Exam --- export async function fetchBlockLearnState(topic) { const res = await fetch(`${BASE}/blocks/learnstate?topic=${encodeURIComponent(topic)}`) return jsonOrThrow(res) } export async function chatBlock({ topic, block, section, section_compact = '', messages, provider }) { const res = await fetch(`${BASE}/blocks/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic, block, section, section_compact, messages, provider }), }) return jsonOrThrow(res) } export async function examBlock({ topic, block, section, section_compact = '', provider, action = 'question', question = '', last_rating = '', avoid = [], asked_again = false, reason = '', pattern = '', cap = 6, messages = [], thorough = false, selection = [], correct = [], solution = '', alternatives = [], input = '', schwer = false, }) { const res = await fetch(`${BASE}/blocks/exam`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic, block, section, section_compact, action, question, last_rating, avoid, asked_again, reason, pattern, cap, messages, provider, thorough, selection, correct, solution, alternatives, input, schwer }), }) return jsonOrThrow(res) } export async function fetchQuestionPattern(topic, block) { const res = await fetch(`${BASE}/blocks/question-pattern?topic=${encodeURIComponent(topic)}&block=${encodeURIComponent(block)}`) return jsonOrThrow(res) } export async function fetchTopicProgress(topic) { const res = await fetch(`${BASE}/topics/progress?topic=${encodeURIComponent(topic)}`) return res.json() } export async function fetchStats() { const res = await fetch(`${BASE}/stats`) return res.json() } export async function fetchProviders() { const res = await fetch(`${BASE}/providers`) return res.json() } export async function fetchFolders(kind) { const res = await fetch(`${BASE}/folders?kind=${encodeURIComponent(kind)}`) return jsonOrThrow(res) } export async function fetchSource(topic) { const res = await fetch(`${BASE}/blocks/source?topic=${encodeURIComponent(topic)}`) return jsonOrThrow(res) } export async function updateSource(topic, { type, ort = '', spec = '' }) { const res = await fetch(`${BASE}/blocks/source`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic, type, location: ort, spec }), }) return jsonOrThrow(res) } export async function fetchBlocksOverview(topic) { const res = await fetch(`${BASE}/blocks/overview?topic=${encodeURIComponent(topic)}`) return jsonOrThrow(res) } // Live card counts per kanban column ({} when the streaming inventory is not in use). export async function fetchKanban(topic) { const res = await fetch(`${BASE}/blocks/kanban?topic=${encodeURIComponent(topic)}`) return jsonOrThrow(res) } // Currently running agents for a topic + their runtime in seconds. export async function fetchAgents(topic) { const res = await fetch(`${BASE}/blocks/agents?topic=${encodeURIComponent(topic)}`) return jsonOrThrow(res) } // Attach one more research agent to the running kanban flow. export async function addResearch(topic) { const res = await fetch(`${BASE}/blocks/research?topic=${encodeURIComponent(topic)}`, { method: 'POST' }) return jsonOrThrow(res) } export async function cancelGuide(id) { await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' }) } export async function deleteGuide(id, slots = false) { await fetch(`${BASE}/guides/${id}${slots ? '?slots=1' : ''}`, { method: 'DELETE' }) } export async function fetchGuideContent(id, level = 4) { const res = await fetch(`${BASE}/guides/${id}/content?level=${level}`) if (!res.ok) throw new Error(`Content not available (${res.status})`) return res.json() } // Lern-Artefakte (Flashcards/Examples/Diagramme) je Thema, gruppiert nach Block-Norm. export async function fetchArtefakte(topic) { const res = await fetch(`${BASE}/blocks/artefakte?topic=${encodeURIComponent(topic)}`) if (!res.ok) return { artefakte: {} } return res.json() } // Einen Markdown-Block on-demand gegen die Guide-Rules prüfen (Fokus, Rechtsklick). export async function pruefeBlock(id, { block, spot, snippet, hint = '', provider }) { const res = await fetch(`${BASE}/guides/${id}/block/pruefen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ block, spot, snippet, hint, provider }), }) return jsonOrThrow(res) } // Geprüften Block persistent übernehmen (alt → new im jeweiligen Feld). export async function uebernehmeBlock(id, { block, spot, alt, revised, provider }) { const res = await fetch(`${BASE}/guides/${id}/block/uebernehmen`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ block, spot, alt, revised, provider }), }) return jsonOrThrow(res) } // Reset a block's learning progress to zero (score/streak/flags/open question). export async function resetBlockProgress(topic, block) { const res = await fetch(`${BASE}/blocks/progress?topic=${encodeURIComponent(topic)}&block=${encodeURIComponent(block)}`, { method: 'DELETE', }) return jsonOrThrow(res) } export async function fetchTopics() { const res = await fetch(`${BASE}/topics`) return res.json() } export async function createTopic(name) { await fetch(`${BASE}/topics`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }) } export async function deleteTopic(name) { await fetch(`${BASE}/topics?topic=${encodeURIComponent(name)}`, { method: 'DELETE' }) } export async function chatGuide(id, { section, outline, messages, provider = 'claude' }) { const res = await fetch(`${BASE}/guides/${id}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ section, outline, messages, provider }), }) return res.json() } export async function fetchElements(topic) { const res = await fetch(`${BASE}/elements?topic=${encodeURIComponent(topic)}`) return res.json() } export async function createElement(topic, hint = '', provider = 'claude') { const res = await fetch(`${BASE}/elements`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic, hint, provider }), }) return res.json() } export async function chatElement(id, messages, provider = 'claude') { const res = await fetch(`${BASE}/elements/${id}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages, provider }), }) return res.json() } export async function deleteElement(id) { await fetch(`${BASE}/elements/${id}`, { method: 'DELETE' }) } export async function updateElement(id, fields) { const res = await fetch(`${BASE}/elements/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(fields), }) return res.json() } export async function styleElement(id, provider = 'claude') { const res = await fetch(`${BASE}/elements/${id}/style`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider }), }) if (!res.ok) throw new Error(`Stil-Exam fehlgeschlagen (${res.status})`) return res.json() } export async function refineSuggestion(id, suggestion, instruction, provider = 'claude') { const res = await fetch(`${BASE}/elements/${id}/refine`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ suggestion, instruction, provider }), }) if (!res.ok) throw new Error(`Überarbeitung fehlgeschlagen (${res.status})`) return res.json() } export async function checkElement(id, provider = 'claude') { const res = await fetch(`${BASE}/elements/${id}/check`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider }), }) if (!res.ok) throw new Error(`Exam fehlgeschlagen (${res.status})`) return res.json() }