mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 08:50:36 +00:00
Persist discovery answers to CSV for reprocessing and recovery
Save each discovery conversation's prompts and answers to durable CSV files (per-conversation + append-only master log) on both save and completion, so answers survive an extraction error, can be re-fed to the AI, and can be reviewed/resumed by the user. - app/services/answer_store.py: canonical prompt list + atomic CSV writes, master append, and read-back helpers (DB stays system of record; CSV failures are logged, never fatal). - discovery router: write CSV on /respond and /complete; new endpoints GET /answers, GET /answers.csv, POST /reprocess (shared extraction helper; locked profiles return 409). - discovery.html: prefill/resume from saved answers after an error and a "Re-run analysis" button wired to /reprocess. - scripts/reprocess_csv.py: offline CLI to re-run extraction from a CSV (print or --write-db). - QUESTIONS_DIR / QUESTIONS_MASTER_CSV config, .gitignore, README, tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dg6XWUwprmP5QCL18HxssY
This commit is contained in:
+102
-5
@@ -133,11 +133,62 @@
|
||||
}
|
||||
});
|
||||
|
||||
const STORAGE_KEY = "discovery_conversation_id";
|
||||
|
||||
async function startConversation() {
|
||||
const res = await authedFetch("/discovery/start", { method: "POST" });
|
||||
if (!res.ok) throw new Error("Could not start conversation");
|
||||
const data = await res.json();
|
||||
conversationId = data.conversation_id;
|
||||
// Remember the in-progress conversation so a reload after an error can
|
||||
// resume from the original answers rather than losing them.
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, conversationId);
|
||||
} catch (e) {
|
||||
/* storage unavailable — non-fatal */
|
||||
}
|
||||
}
|
||||
|
||||
async function resumeOrStart() {
|
||||
let savedId = null;
|
||||
try {
|
||||
savedId = localStorage.getItem(STORAGE_KEY);
|
||||
} catch (e) {
|
||||
/* storage unavailable */
|
||||
}
|
||||
if (savedId) {
|
||||
try {
|
||||
const res = await authedFetch(`/discovery/${savedId}/answers`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const hasText = (data.answers || []).some((a) => a.answer.trim());
|
||||
if (!data.completed_at && hasText) {
|
||||
// Resume: restore the saved answers into the flow.
|
||||
conversationId = savedId;
|
||||
const byKey = {};
|
||||
data.answers.forEach((a) => {
|
||||
byKey[a.prompt_key] = a.answer;
|
||||
});
|
||||
PROMPTS.forEach((p, i) => {
|
||||
// CSV/key uses the short key; PROMPTS uses prompt_* column names.
|
||||
const shortKey = p.key.replace(/^prompt_/, "");
|
||||
answers[i] = byKey[shortKey] || "";
|
||||
});
|
||||
render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
/* fall through to a fresh start */
|
||||
}
|
||||
}
|
||||
// No resumable conversation — start a fresh one.
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
await startConversation();
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
@@ -173,24 +224,70 @@
|
||||
throw new Error(detail.detail || "Extraction failed");
|
||||
}
|
||||
|
||||
// Success: the conversation is complete, drop the resume marker.
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
window.location.href = "/static/profile.html";
|
||||
} catch (err) {
|
||||
el.loading.classList.remove("active");
|
||||
el.flow.style.display = "block";
|
||||
el.flow.innerHTML =
|
||||
`<div class="error-box"><strong>Something went wrong.</strong><br/>` +
|
||||
`${err.message}<br/><br/>Your answers are still here — ` +
|
||||
`please try submitting again.</div>` +
|
||||
`${err.message}<br/><br/>Your answers are saved — you can ` +
|
||||
`re-run the analysis or reload to keep editing.</div>` +
|
||||
`<div class="nav">` +
|
||||
`<button class="btn-ghost" onclick="location.reload()">Reload</button>` +
|
||||
`<button class="btn-primary" id="rerunBtn">Re-run analysis</button>` +
|
||||
`</div>`;
|
||||
const rerun = document.getElementById("rerunBtn");
|
||||
if (rerun) rerun.addEventListener("click", reprocess);
|
||||
}
|
||||
}
|
||||
|
||||
async function reprocess() {
|
||||
if (!conversationId) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
el.flow.style.display = "none";
|
||||
el.loading.classList.add("active");
|
||||
try {
|
||||
const res = await authedFetch(
|
||||
`/discovery/${conversationId}/reprocess`,
|
||||
{ method: "POST" }
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await res
|
||||
.json()
|
||||
.catch(() => ({ detail: "Re-run failed" }));
|
||||
throw new Error(detail.detail || "Re-run failed");
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
window.location.href = "/static/profile.html";
|
||||
} catch (err) {
|
||||
el.loading.classList.remove("active");
|
||||
el.flow.style.display = "block";
|
||||
el.flow.innerHTML =
|
||||
`<div class="error-box"><strong>Re-run failed.</strong><br/>` +
|
||||
`${err.message}<br/><br/>Your answers are still saved.</div>` +
|
||||
`<div class="nav"><span></span>` +
|
||||
`<button class="btn-primary" onclick="location.reload()">Reload</button></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Kick off a conversation as soon as the page loads so the id is ready.
|
||||
startConversation().catch(() => {
|
||||
// Resume an in-progress conversation if one exists, otherwise start a
|
||||
// fresh one so the id is ready by submit time.
|
||||
render();
|
||||
resumeOrStart().catch(() => {
|
||||
/* will retry on submit */
|
||||
});
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user