From 3669fd8d0a1f8668834989a34b27adcf72f5b316 Mon Sep 17 00:00:00 2001 From: team3 Date: Tue, 30 Jun 2026 00:36:41 +0200 Subject: [PATCH] update --- backend/blocks.py | 24 +++++++++---- backend/models.py | 1 + backend/routes.py | 2 +- frontend/src/App.vue | 8 ++--- frontend/src/api.js | 4 +-- frontend/src/components/BlocksOverview.vue | 39 ++++++++++++++-------- 6 files changed, 52 insertions(+), 26 deletions(-) diff --git a/backend/blocks.py b/backend/blocks.py index 9af8b25..0418831 100644 --- a/backend/blocks.py +++ b/backend/blocks.py @@ -3079,7 +3079,7 @@ async def _reset_db_from_phase(topic: str, label: str) -> None: await db.delete_pipeline_state(topic, ["Source prep"]) -async def generate_blocks(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None, ab_step: int | None = None) -> None: +async def generate_blocks(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None, ab_step: int | None = None, to_step: int | None = None) -> None: if topic in _blocks_progress: return _blocks_progress[topic] = "Waiting…" @@ -3102,6 +3102,10 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE def aborted() -> None: _blocks_errors[topic] = "Cancelled — progress is preserved" + _step_list = _blocks_steps(topic) + def _past_limit(step: str) -> bool: # optional end limit: stop before any step past to_step + return to_step is not None and step in _step_list and _step_list.index(step) > to_step + ctx = GenContext(topic=topic, provider=provider, is_cancelled=is_cancelled) try: @@ -3143,14 +3147,19 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE await db.delete_sub_artefakte(topic) # Inventory (DB): research loop → consolidation → clarification. + if _past_limit("Research"): return if not await _stage(_research_batch(ctx, set_p, files, q, folder, instructions)): return + if _past_limit("Consolidation"): return if not await _stage(_consolidate(ctx, set_p, files)): return + if _past_limit("Clarification"): return if not await _stage(_clarify_inventory(ctx, set_p, files)): return + if _past_limit("Dedup"): return if not await _stage(_dedup_inventory(ctx, set_p, files)): return + if _past_limit("Blocks-Filter"): return if not await _stage(_filter_inventory(ctx, set_p, files)): return consensus_rows = await db.list_blocks(topic, status="consensus") @@ -3161,7 +3170,7 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE # Projects only: subject-field supplement — script/project is an excerpt, # a web agent adds canonically missing blocks, marked with [Supplement]. - if q["type"] == "projekt": + if q["type"] == "projekt" and not _past_limit("Supplement"): set_p("Supplementing subject field…", step=_step_idx(topic, "Supplement")) supp_path = files["ergaenzung"] supplements = _supplement_schema(_json_file(supp_path)) @@ -3196,6 +3205,7 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE # Make titles unique and write the unsorted inventory entries = _unique_title(entries) atomic_write_text(final_path, "\n".join(f"{i}. {t}" for i, t in entries.items()) + "\n") + if _past_limit("Subblocks find"): return # end limit inside the inventory → stop with blocks.md written # Block B + C: subblocks per block + levels → sidecar subblocks.json. # Non-destructive: blocks.md already exists; if the sidecar is missing, only @@ -3210,6 +3220,7 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE if raw is None: return # error is set atomic_write_json(files["sub_roh"], raw, indent=1) + if _past_limit("Facts find"): return # end limit after subblocks # Facts per sub (BEFORE the level): extract + verify source facts → facts.json. # Extract-once grounding — level/relevance/questions/guide feed on it. if not _facts_complete(files): @@ -3229,6 +3240,7 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE raw = {bt: subs for bt, subs in raw.items() if subs} # drop empty blocks (_sub_roh_schema requires ≥1) atomic_write_json(files["sub_roh"], raw, indent=1) atomic_write_json(files["facts"], facts_map, indent=1) + if _past_limit("Levels find"): return # end limit after facts (sidecar not yet valid → no DB mirror) sidecar = await _levels_block(ctx, set_p, files, raw, instructions) if is_cancelled(): aborted() @@ -3249,7 +3261,7 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE # Own phase after the levels; drives the ProGuide format (all blocks # with ≥1 relevant subblock) and filters peripheral subs out of the guides. sidecar = _json_file(files["sidecar"]) - if _sidecar_schema(sidecar) is not None and not _relevance_complete(sidecar): + if not _past_limit("Relevance find") and _sidecar_schema(sidecar) is not None and not _relevance_complete(sidecar): relevance_by_id = await _relevance_block(ctx, set_p, files, sidecar, instructions) if is_cancelled(): aborted() @@ -3265,7 +3277,7 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE # Block D.5: outline (blocks artifact) — chapter structure over ALL blocks, # only read by the guide. Format-agnostic; the guide filters per format. - if not _outline_complete(files): + if not _past_limit("Outline") and not _outline_complete(files): await _outline_block(ctx, set_p, files, entries, instructions) if is_cancelled(): aborted() @@ -3275,7 +3287,7 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE # At exam time each agent draws a pattern without replacement and formulates # a question from it — distinct seeding prevents the duplicate questions of live generation. sidecar = _json_file(files["sidecar"]) - if _sidecar_schema(sidecar) is not None and _relevance_complete(sidecar) and not _question_pattern_complete(topic): + if not _past_limit("Questions find") and _sidecar_schema(sidecar) is not None and _relevance_complete(sidecar) and not _question_pattern_complete(topic): pattern = await _question_pattern_block(ctx, set_p, files, sidecar, instructions) if is_cancelled(): aborted() @@ -3287,7 +3299,7 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE # Block F: learning artefacts (flashcards/examples) from the facts — bonus, # presented by the frontend. Does not abort the run (artefacts are optional). sidecar = _json_file(files["sidecar"]) - if _sidecar_schema(sidecar) is not None and not _artefacts_complete(files): + if not _past_limit("Flashcards") and _sidecar_schema(sidecar) is not None and not _artefacts_complete(files): artefacts = await _artefacts_block(ctx, set_p, files, sidecar, instructions) if is_cancelled(): aborted() diff --git a/backend/models.py b/backend/models.py index 1244cfa..182638e 100644 --- a/backend/models.py +++ b/backend/models.py @@ -32,6 +32,7 @@ class BlocksCreateRequest(BaseModel): source_location: str = Field(default="", max_length=2000) ab_phase: int | None = Field(default=None, ge=1, le=9) # re-run from a coarse phase (position in _phasen(topic), 1-based; up to 9: …outline/questions/artifacts); None = resume/continue without deleting ab_step: int | None = Field(default=None, ge=0) # re-run from a fine sub-step (0-based index into _blocks_steps); takes precedence over ab_phase + to_step: int | None = Field(default=None, ge=0) # stop AFTER this fine sub-step (0-based index into _blocks_steps); None = run to the end class BlocksResetStepRequest(BaseModel): diff --git a/backend/routes.py b/backend/routes.py index 325ed9f..7404be1 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -164,7 +164,7 @@ async def create_blocks(req: BlocksCreateRequest): raise HTTPException(400, "Link must start with http:// or https://.") qp.parent.mkdir(parents=True, exist_ok=True) atomic_write_json(qp, {"type": type, "location": location, "spec": req.instructions.strip()}) - asyncio.create_task(generate_blocks(topic, req.instructions.strip(), req.provider, ab_phase=req.ab_phase, ab_step=req.ab_step)) + asyncio.create_task(generate_blocks(topic, req.instructions.strip(), req.provider, ab_phase=req.ab_phase, ab_step=req.ab_step, to_step=req.to_step)) return {"ok": True} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5449bb4..6811578 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -224,12 +224,12 @@ async function handleResetFromStep(step) { await loadBlocks() } -async function handleBlocksClick({ instructions, abPhase = null, abStep = null }) { +async function handleBlocksClick({ instructions, abPhase = null, abStep = null, toStep = null }) { if (!selectedTopic.value) return uiError.value = null try { - // Source is already fixed here; abPhase/abStep control the re-run scope. - await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase, abStep) + // Source is already fixed here; abPhase/abStep set the start, toStep an optional end limit. + await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase, abStep, toStep) } catch (e) { uiError.value = e.message return @@ -422,7 +422,7 @@ onMounted(async () => { :ready="blocks.ready" :partial="blocks.partial" @close="mainView = 'detail'" - @restartFrom="(i) => handleBlocksClick({ instructions: '', abStep: i })" + @restartFrom="(r) => handleBlocksClick({ instructions: '', abStep: r.from, toStep: r.to })" @resetFrom="handleResetFromStep" @restartAll="() => handleBlocksClick({ abPhase: blocks.ready ? 1 : null })" @removeAll="handleResetBlocks" diff --git a/frontend/src/api.js b/frontend/src/api.js index 943a1df..85ff9e4 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -47,11 +47,11 @@ export async function fetchBlocksStatus(topic) { return res.json() } -export async function createBlocks(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', abPhase = null, abStep = null) { +export async function createBlocks(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', abPhase = null, abStep = null, toStep = null) { 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 }), + body: JSON.stringify({ topic, instructions, provider, source_type: sourceType, source_location: sourceOrt, ab_phase: abPhase, ab_step: abStep, to_step: toStep }), }) return jsonOrThrow(res) } diff --git a/frontend/src/components/BlocksOverview.vue b/frontend/src/components/BlocksOverview.vue index 277ae40..2fc824f 100644 --- a/frontend/src/components/BlocksOverview.vue +++ b/frontend/src/components/BlocksOverview.vue @@ -23,22 +23,32 @@ const phaseGroups = computed(() => { return out }) -const selected = ref(null) // marked start point (step index) +const startSel = ref(null) // marked start point (step index) — only ≤ current stand +const endSel = ref(null) // optional end point (step index, > start) — generation stops there const confirm = ref(null) // which destructive action currently shows "Sure?" -const selectedLabel = computed(() => props.steps[selected.value]?.label || '') +const startLabel = computed(() => props.steps[startSel.value]?.label || '') +const endLabel = computed(() => props.steps[endSel.value]?.label || '') + +// Start is only valid up to the current stand: done/active steps, never a pending one (never ran). +function startDisabled(idx) { return startSel.value === null && props.steps[idx]?.state === 'pending' } +function inRange(idx) { return startSel.value !== null && endSel.value !== null && idx > startSel.value && idx < endSel.value } function stepClick(idx) { - if (props.generating) return - selected.value = selected.value === idx ? null : idx + if (props.generating || startDisabled(idx)) return confirm.value = null + if (startSel.value === null) { startSel.value = idx; endSel.value = null } // 1st click → start + else if (idx === startSel.value) { startSel.value = null; endSel.value = null } // re-click start → clear + else if (idx > startSel.value) { endSel.value = endSel.value === idx ? null : idx } // later step → toggle end + else if (props.steps[idx]?.state !== 'pending') { startSel.value = idx; endSel.value = null } // earlier → new start } +function clearSel() { startSel.value = null; endSel.value = null; confirm.value = null } // 2-click confirmation for destructive actions: first click "arms", second runs it. function arm(action, fn) { if (confirm.value === action) { confirm.value = null; fn() } else confirm.value = action } -function regenerateFromHere() { const i = selected.value; selected.value = null; confirm.value = null; emit('restartFrom', i) } -function deleteFromHere() { const i = selected.value; selected.value = null; confirm.value = null; emit('resetFrom', i) } +function regenerateFromHere() { const from = startSel.value, to = endSel.value; clearSel(); emit('restartFrom', { from, to }) } +function deleteFromHere() { const from = startSel.value; clearSel(); emit('resetFrom', from) } const items = ref([]) const loading = ref(true) @@ -119,19 +129,19 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subblocks?.l v-for="s in g.steps" :key="s.idx" class="bk-step" - :class="[s.state, { sel: selected === s.idx }]" - :disabled="generating" - :title="`Choose start point «${s.label}»`" + :class="[s.state, { sel: startSel === s.idx, end: endSel === s.idx, 'in-range': inRange(s.idx) }]" + :disabled="generating || startDisabled(s.idx)" + :title="startDisabled(s.idx) ? `«${s.label}» — not reached yet` : (startSel !== null && s.idx > startSel ? `End at «${s.label}»` : `Start at «${s.label}»`)" @click="stepClick(s.idx)" >{{ s.label }} -
- From «{{ selectedLabel }}»: +
+ From «{{ startLabel }}» to «{{ endLabel }}»: - +
@@ -251,7 +261,10 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subblocks?.l .bk-step.done { border-color: var(--success-border); color: var(--success); } .bk-step.active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); font-weight: 600; } .bk-step.pending { color: var(--text-faint); } -.bk-step.sel { border-color: var(--accent); color: var(--on-accent); background: var(--accent); font-weight: 700; box-shadow: 0 0 0 2px var(--accent-soft); } +.bk-step.sel, +.bk-step.end { border-color: var(--accent); color: var(--on-accent); background: var(--accent); font-weight: 700; box-shadow: 0 0 0 2px var(--accent-soft); } +.bk-step.in-range { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); } +.bk-step:disabled:not(.done):not(.active) { opacity: 0.45; } /* Header: progress left, global buttons right */ .bk-steps-top { display: flex; align-items: center; gap: 1rem; min-height: 1.9rem; margin-bottom: 0.7rem; }