update
This commit is contained in:
@@ -3079,7 +3079,7 @@ async def _reset_db_from_phase(topic: str, label: str) -> None:
|
|||||||
await db.delete_pipeline_state(topic, ["Source prep"])
|
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:
|
if topic in _blocks_progress:
|
||||||
return
|
return
|
||||||
_blocks_progress[topic] = "Waiting…"
|
_blocks_progress[topic] = "Waiting…"
|
||||||
@@ -3102,6 +3102,10 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE
|
|||||||
def aborted() -> None:
|
def aborted() -> None:
|
||||||
_blocks_errors[topic] = "Cancelled — progress is preserved"
|
_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)
|
ctx = GenContext(topic=topic, provider=provider, is_cancelled=is_cancelled)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -3143,14 +3147,19 @@ async def generate_blocks(topic: str, instructions: str = "", provider: str = DE
|
|||||||
await db.delete_sub_artefakte(topic)
|
await db.delete_sub_artefakte(topic)
|
||||||
|
|
||||||
# Inventory (DB): research loop → consolidation → clarification.
|
# Inventory (DB): research loop → consolidation → clarification.
|
||||||
|
if _past_limit("Research"): return
|
||||||
if not await _stage(_research_batch(ctx, set_p, files, q, folder, instructions)):
|
if not await _stage(_research_batch(ctx, set_p, files, q, folder, instructions)):
|
||||||
return
|
return
|
||||||
|
if _past_limit("Consolidation"): return
|
||||||
if not await _stage(_consolidate(ctx, set_p, files)):
|
if not await _stage(_consolidate(ctx, set_p, files)):
|
||||||
return
|
return
|
||||||
|
if _past_limit("Clarification"): return
|
||||||
if not await _stage(_clarify_inventory(ctx, set_p, files)):
|
if not await _stage(_clarify_inventory(ctx, set_p, files)):
|
||||||
return
|
return
|
||||||
|
if _past_limit("Dedup"): return
|
||||||
if not await _stage(_dedup_inventory(ctx, set_p, files)):
|
if not await _stage(_dedup_inventory(ctx, set_p, files)):
|
||||||
return
|
return
|
||||||
|
if _past_limit("Blocks-Filter"): return
|
||||||
if not await _stage(_filter_inventory(ctx, set_p, files)):
|
if not await _stage(_filter_inventory(ctx, set_p, files)):
|
||||||
return
|
return
|
||||||
consensus_rows = await db.list_blocks(topic, status="consensus")
|
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,
|
# Projects only: subject-field supplement — script/project is an excerpt,
|
||||||
# a web agent adds canonically missing blocks, marked with [Supplement].
|
# 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"))
|
set_p("Supplementing subject field…", step=_step_idx(topic, "Supplement"))
|
||||||
supp_path = files["ergaenzung"]
|
supp_path = files["ergaenzung"]
|
||||||
supplements = _supplement_schema(_json_file(supp_path))
|
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
|
# Make titles unique and write the unsorted inventory
|
||||||
entries = _unique_title(entries)
|
entries = _unique_title(entries)
|
||||||
atomic_write_text(final_path, "\n".join(f"{i}. {t}" for i, t in entries.items()) + "\n")
|
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.
|
# Block B + C: subblocks per block + levels → sidecar subblocks.json.
|
||||||
# Non-destructive: blocks.md already exists; if the sidecar is missing, only
|
# 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:
|
if raw is None:
|
||||||
return # error is set
|
return # error is set
|
||||||
atomic_write_json(files["sub_roh"], raw, indent=1)
|
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.
|
# Facts per sub (BEFORE the level): extract + verify source facts → facts.json.
|
||||||
# Extract-once grounding — level/relevance/questions/guide feed on it.
|
# Extract-once grounding — level/relevance/questions/guide feed on it.
|
||||||
if not _facts_complete(files):
|
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)
|
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["sub_roh"], raw, indent=1)
|
||||||
atomic_write_json(files["facts"], facts_map, 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)
|
sidecar = await _levels_block(ctx, set_p, files, raw, instructions)
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
aborted()
|
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
|
# Own phase after the levels; drives the ProGuide format (all blocks
|
||||||
# with ≥1 relevant subblock) and filters peripheral subs out of the guides.
|
# with ≥1 relevant subblock) and filters peripheral subs out of the guides.
|
||||||
sidecar = _json_file(files["sidecar"])
|
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)
|
relevance_by_id = await _relevance_block(ctx, set_p, files, sidecar, instructions)
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
aborted()
|
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,
|
# Block D.5: outline (blocks artifact) — chapter structure over ALL blocks,
|
||||||
# only read by the guide. Format-agnostic; the guide filters per format.
|
# 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)
|
await _outline_block(ctx, set_p, files, entries, instructions)
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
aborted()
|
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
|
# 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.
|
# a question from it — distinct seeding prevents the duplicate questions of live generation.
|
||||||
sidecar = _json_file(files["sidecar"])
|
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)
|
pattern = await _question_pattern_block(ctx, set_p, files, sidecar, instructions)
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
aborted()
|
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,
|
# Block F: learning artefacts (flashcards/examples) from the facts — bonus,
|
||||||
# presented by the frontend. Does not abort the run (artefacts are optional).
|
# presented by the frontend. Does not abort the run (artefacts are optional).
|
||||||
sidecar = _json_file(files["sidecar"])
|
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)
|
artefacts = await _artefacts_block(ctx, set_p, files, sidecar, instructions)
|
||||||
if is_cancelled():
|
if is_cancelled():
|
||||||
aborted()
|
aborted()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class BlocksCreateRequest(BaseModel):
|
|||||||
source_location: str = Field(default="", max_length=2000)
|
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_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
|
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):
|
class BlocksResetStepRequest(BaseModel):
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ async def create_blocks(req: BlocksCreateRequest):
|
|||||||
raise HTTPException(400, "Link must start with http:// or https://.")
|
raise HTTPException(400, "Link must start with http:// or https://.")
|
||||||
qp.parent.mkdir(parents=True, exist_ok=True)
|
qp.parent.mkdir(parents=True, exist_ok=True)
|
||||||
atomic_write_json(qp, {"type": type, "location": location, "spec": req.instructions.strip()})
|
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}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -224,12 +224,12 @@ async function handleResetFromStep(step) {
|
|||||||
await loadBlocks()
|
await loadBlocks()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBlocksClick({ instructions, abPhase = null, abStep = null }) {
|
async function handleBlocksClick({ instructions, abPhase = null, abStep = null, toStep = null }) {
|
||||||
if (!selectedTopic.value) return
|
if (!selectedTopic.value) return
|
||||||
uiError.value = null
|
uiError.value = null
|
||||||
try {
|
try {
|
||||||
// Source is already fixed here; abPhase/abStep control the re-run scope.
|
// 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)
|
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase, abStep, toStep)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uiError.value = e.message
|
uiError.value = e.message
|
||||||
return
|
return
|
||||||
@@ -422,7 +422,7 @@ onMounted(async () => {
|
|||||||
:ready="blocks.ready"
|
:ready="blocks.ready"
|
||||||
:partial="blocks.partial"
|
:partial="blocks.partial"
|
||||||
@close="mainView = 'detail'"
|
@close="mainView = 'detail'"
|
||||||
@restartFrom="(i) => handleBlocksClick({ instructions: '', abStep: i })"
|
@restartFrom="(r) => handleBlocksClick({ instructions: '', abStep: r.from, toStep: r.to })"
|
||||||
@resetFrom="handleResetFromStep"
|
@resetFrom="handleResetFromStep"
|
||||||
@restartAll="() => handleBlocksClick({ abPhase: blocks.ready ? 1 : null })"
|
@restartAll="() => handleBlocksClick({ abPhase: blocks.ready ? 1 : null })"
|
||||||
@removeAll="handleResetBlocks"
|
@removeAll="handleResetBlocks"
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ export async function fetchBlocksStatus(topic) {
|
|||||||
return res.json()
|
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`, {
|
const res = await fetch(`${BASE}/blocks`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,22 +23,32 @@ const phaseGroups = computed(() => {
|
|||||||
return out
|
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 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) {
|
function stepClick(idx) {
|
||||||
if (props.generating) return
|
if (props.generating || startDisabled(idx)) return
|
||||||
selected.value = selected.value === idx ? null : idx
|
|
||||||
confirm.value = null
|
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.
|
// 2-click confirmation for destructive actions: first click "arms", second runs it.
|
||||||
function arm(action, fn) {
|
function arm(action, fn) {
|
||||||
if (confirm.value === action) { confirm.value = null; fn() }
|
if (confirm.value === action) { confirm.value = null; fn() }
|
||||||
else confirm.value = action
|
else confirm.value = action
|
||||||
}
|
}
|
||||||
function regenerateFromHere() { const i = selected.value; selected.value = null; confirm.value = null; emit('restartFrom', i) }
|
function regenerateFromHere() { const from = startSel.value, to = endSel.value; clearSel(); emit('restartFrom', { from, to }) }
|
||||||
function deleteFromHere() { const i = selected.value; selected.value = null; confirm.value = null; emit('resetFrom', i) }
|
function deleteFromHere() { const from = startSel.value; clearSel(); emit('resetFrom', from) }
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const loading = ref(true)
|
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"
|
v-for="s in g.steps"
|
||||||
:key="s.idx"
|
:key="s.idx"
|
||||||
class="bk-step"
|
class="bk-step"
|
||||||
:class="[s.state, { sel: selected === s.idx }]"
|
:class="[s.state, { sel: startSel === s.idx, end: endSel === s.idx, 'in-range': inRange(s.idx) }]"
|
||||||
:disabled="generating"
|
:disabled="generating || startDisabled(s.idx)"
|
||||||
:title="`Choose start point «${s.label}»`"
|
: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)"
|
@click="stepClick(s.idx)"
|
||||||
>{{ s.label }}</button>
|
>{{ s.label }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selected !== null && !generating" class="bk-step-actions">
|
<div v-if="startSel !== null && !generating" class="bk-step-actions">
|
||||||
<span class="bk-step-actions-label">From «{{ selectedLabel }}»:</span>
|
<span class="bk-step-actions-label">From «{{ startLabel }}»<span v-if="endSel !== null"> to «{{ endLabel }}»</span>:</span>
|
||||||
<button class="bk-act play" @click="regenerateFromHere">↻ regenerate</button>
|
<button class="bk-act play" @click="regenerateFromHere">↻ regenerate</button>
|
||||||
<button class="bk-act danger" :class="{ armed: confirm === 'reset' }" @click="arm('reset', deleteFromHere)">{{ confirm === 'reset' ? 'Sure?' : '✕ delete all' }}</button>
|
<button class="bk-act danger" :class="{ armed: confirm === 'reset' }" @click="arm('reset', deleteFromHere)">{{ confirm === 'reset' ? 'Sure?' : '✕ delete all' }}</button>
|
||||||
<button class="bk-act ghost" @click="selected = null; confirm = null">Cancel</button>
|
<button class="bk-act ghost" @click="clearSel">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -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.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.active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); font-weight: 600; }
|
||||||
.bk-step.pending { color: var(--text-faint); }
|
.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 */
|
/* Header: progress left, global buttons right */
|
||||||
.bk-steps-top { display: flex; align-items: center; gap: 1rem; min-height: 1.9rem; margin-bottom: 0.7rem; }
|
.bk-steps-top { display: flex; align-items: center; gap: 1rem; min-height: 1.9rem; margin-bottom: 0.7rem; }
|
||||||
|
|||||||
Reference in New Issue
Block a user