321 lines
11 KiB
JavaScript
321 lines
11 KiB
JavaScript
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 = '', 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, research }),
|
|
})
|
|
return jsonOrThrow(res)
|
|
}
|
|
|
|
// Live-Kanban-Board der Blocks-Erzeugung (Spalten + Karten + Agenten + Dead-Letter).
|
|
export async function fetchBlocksBoard(topic) {
|
|
const res = await fetch(`${BASE}/blocks/board?topic=${encodeURIComponent(topic)}`)
|
|
return jsonOrThrow(res)
|
|
}
|
|
|
|
// Karten ab Spalte zurücksetzen (keine Generierung).
|
|
export async function resetBlocksStage(topic, board, stage) {
|
|
const res = await fetch(`${BASE}/blocks/reset-stage`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ topic, board, stage }),
|
|
})
|
|
return jsonOrThrow(res)
|
|
}
|
|
|
|
// Einen weiteren Research-Agenten anhängen (Attach-or-Start).
|
|
export async function addBlocksResearch(topic, provider = 'claude') {
|
|
const res = await fetch(`${BASE}/blocks/research?topic=${encodeURIComponent(topic)}&provider=${encodeURIComponent(provider)}`, { method: 'POST' })
|
|
return jsonOrThrow(res)
|
|
}
|
|
|
|
export async function requeueBlocksDead(topic) {
|
|
const res = await fetch(`${BASE}/blocks/requeue-dead?topic=${encodeURIComponent(topic)}`, { method: 'POST' })
|
|
return jsonOrThrow(res)
|
|
}
|
|
|
|
// Live-Board der Guide-Erzeugung.
|
|
export async function fetchGuideBoard(topic, format = 'Guide') {
|
|
const res = await fetch(`${BASE}/guides/board?topic=${encodeURIComponent(topic)}&format=${encodeURIComponent(format)}`)
|
|
return jsonOrThrow(res)
|
|
}
|
|
|
|
export async function resetGuideBoard(topic, format, abStage) {
|
|
const res = await fetch(`${BASE}/guides/board/reset`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ topic, format, ab_stage: abStage }),
|
|
})
|
|
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)
|
|
}
|
|
|
|
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()
|
|
}
|