@tangle-network/agent-app 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DesignCanvasEditor-IB2FMBBD.js → DesignCanvasEditor-QQNAZRJQ.js} +3 -3
- package/dist/IntakeInterview-WHVKMNQW.js +8 -0
- package/dist/IntakeInterview-WHVKMNQW.js.map +1 -0
- package/dist/chunk-4K7BZYO7.js +124 -0
- package/dist/chunk-4K7BZYO7.js.map +1 -0
- package/dist/chunk-5I72FRZY.js +212 -0
- package/dist/chunk-5I72FRZY.js.map +1 -0
- package/dist/{chunk-S5CAJX6O.js → chunk-FJUOBNIY.js} +2 -2
- package/dist/{chunk-Y2SZVW35.js → chunk-LVD37K5V.js} +41 -41
- package/dist/chunk-LVD37K5V.js.map +1 -0
- package/dist/chunk-ND3XEGBE.js +119 -0
- package/dist/chunk-ND3XEGBE.js.map +1 -0
- package/dist/chunk-USYXHDKE.js +30 -0
- package/dist/chunk-USYXHDKE.js.map +1 -0
- package/dist/{chunk-N4ZFKQ5C.js → chunk-YYYXGLLQ.js} +2 -2
- package/dist/design-canvas-react/index.js +3 -3
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/index.js +1 -1
- package/dist/intakes/api.d.ts +59 -0
- package/dist/intakes/api.js +63 -0
- package/dist/intakes/api.js.map +1 -0
- package/dist/intakes/drizzle.d.ts +385 -0
- package/dist/intakes/drizzle.js +61 -0
- package/dist/intakes/drizzle.js.map +1 -0
- package/dist/intakes/index.d.ts +42 -0
- package/dist/intakes/index.js +31 -0
- package/dist/intakes/index.js.map +1 -0
- package/dist/intakes-react/index.d.ts +48 -0
- package/dist/intakes-react/index.js +8 -0
- package/dist/intakes-react/index.js.map +1 -0
- package/dist/intakes-react/lazy.d.ts +8 -0
- package/dist/intakes-react/lazy.js +9 -0
- package/dist/intakes-react/lazy.js.map +1 -0
- package/dist/model-Cuj7d-xT.d.ts +116 -0
- package/dist/theme/index.js +1 -1
- package/dist/theme/tokens.css +50 -48
- package/dist/web-react/index.d.ts +6 -1
- package/dist/web-react/index.js +8 -14
- package/dist/web-react/index.js.map +1 -1
- package/package.json +26 -1
- package/dist/chunk-Y2SZVW35.js.map +0 -1
- /package/dist/{DesignCanvasEditor-IB2FMBBD.js.map → DesignCanvasEditor-QQNAZRJQ.js.map} +0 -0
- /package/dist/{chunk-S5CAJX6O.js.map → chunk-FJUOBNIY.js.map} +0 -0
- /package/dist/{chunk-N4ZFKQ5C.js.map → chunk-YYYXGLLQ.js.map} +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// src/intakes/model.ts
|
|
2
|
+
var URL_PATTERN = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
|
|
3
|
+
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4
|
+
function getQuestion(graph, questionId) {
|
|
5
|
+
return graph.questions.find((q) => q.id === questionId) ?? null;
|
|
6
|
+
}
|
|
7
|
+
function hasAnswer(value) {
|
|
8
|
+
if (value == null) return false;
|
|
9
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
10
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
function validateAnswer(question, value) {
|
|
14
|
+
if (!hasAnswer(value)) {
|
|
15
|
+
return question.required ? { ok: false, reason: "required" } : { ok: true };
|
|
16
|
+
}
|
|
17
|
+
switch (question.type) {
|
|
18
|
+
case "text":
|
|
19
|
+
case "long-text": {
|
|
20
|
+
if (typeof value !== "string") return { ok: false, reason: "wrong-type" };
|
|
21
|
+
const len = value.trim().length;
|
|
22
|
+
if (question.min != null && len < question.min) return { ok: false, reason: "too-short" };
|
|
23
|
+
if (question.max != null && len > question.max) return { ok: false, reason: "too-long" };
|
|
24
|
+
return { ok: true };
|
|
25
|
+
}
|
|
26
|
+
case "url": {
|
|
27
|
+
if (typeof value !== "string") return { ok: false, reason: "wrong-type" };
|
|
28
|
+
return URL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: "invalid-url" };
|
|
29
|
+
}
|
|
30
|
+
case "email": {
|
|
31
|
+
if (typeof value !== "string") return { ok: false, reason: "wrong-type" };
|
|
32
|
+
return EMAIL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: "invalid-email" };
|
|
33
|
+
}
|
|
34
|
+
case "number": {
|
|
35
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return { ok: false, reason: "wrong-type" };
|
|
36
|
+
if (question.min != null && value < question.min) return { ok: false, reason: "too-small" };
|
|
37
|
+
if (question.max != null && value > question.max) return { ok: false, reason: "too-large" };
|
|
38
|
+
return { ok: true };
|
|
39
|
+
}
|
|
40
|
+
case "boolean": {
|
|
41
|
+
return typeof value === "boolean" ? { ok: true } : { ok: false, reason: "wrong-type" };
|
|
42
|
+
}
|
|
43
|
+
case "single-select": {
|
|
44
|
+
if (typeof value !== "string") return { ok: false, reason: "wrong-type" };
|
|
45
|
+
return optionExists(question, value) ? { ok: true } : { ok: false, reason: "not-an-option" };
|
|
46
|
+
}
|
|
47
|
+
case "multi-select": {
|
|
48
|
+
if (!Array.isArray(value)) return { ok: false, reason: "wrong-type" };
|
|
49
|
+
if (!value.every((v) => typeof v === "string" && optionExists(question, v))) {
|
|
50
|
+
return { ok: false, reason: "not-an-option" };
|
|
51
|
+
}
|
|
52
|
+
if (question.min != null && value.length < question.min) return { ok: false, reason: "too-short" };
|
|
53
|
+
if (question.max != null && value.length > question.max) return { ok: false, reason: "too-long" };
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
return { ok: false, reason: "wrong-type" };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function nextQuestion(graph, answers) {
|
|
61
|
+
if (graph.questions.length === 0) return null;
|
|
62
|
+
const visited = /* @__PURE__ */ new Set();
|
|
63
|
+
let current = graph.questions[0] ?? null;
|
|
64
|
+
while (current) {
|
|
65
|
+
if (visited.has(current.id)) return null;
|
|
66
|
+
visited.add(current.id);
|
|
67
|
+
const answer = answers[current.id];
|
|
68
|
+
const validity = validateAnswer(current, answer);
|
|
69
|
+
if (!validity.ok || current.required && !hasAnswer(answer)) {
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
current = advance(graph, current, answers);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function isComplete(graph, answers) {
|
|
77
|
+
return nextQuestion(graph, answers) === null;
|
|
78
|
+
}
|
|
79
|
+
function intakeProgress(graph, answers) {
|
|
80
|
+
const reachable = reachableQuestions(graph, answers);
|
|
81
|
+
const required = reachable.filter((q) => q.required);
|
|
82
|
+
const answered = required.filter((q) => validateAnswer(q, answers[q.id]).ok && hasAnswer(answers[q.id])).length;
|
|
83
|
+
return { answered, total: required.length };
|
|
84
|
+
}
|
|
85
|
+
function reachableQuestions(graph, answers) {
|
|
86
|
+
if (graph.questions.length === 0) return [];
|
|
87
|
+
const visited = /* @__PURE__ */ new Set();
|
|
88
|
+
const out = [];
|
|
89
|
+
let current = graph.questions[0] ?? null;
|
|
90
|
+
while (current) {
|
|
91
|
+
if (visited.has(current.id)) break;
|
|
92
|
+
visited.add(current.id);
|
|
93
|
+
out.push(current);
|
|
94
|
+
current = advance(graph, current, answers);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function advance(graph, current, answers) {
|
|
99
|
+
if (current.next) {
|
|
100
|
+
const nextId = current.next(answers);
|
|
101
|
+
return nextId == null ? null : getQuestion(graph, nextId);
|
|
102
|
+
}
|
|
103
|
+
const index = graph.questions.indexOf(current);
|
|
104
|
+
return graph.questions[index + 1] ?? null;
|
|
105
|
+
}
|
|
106
|
+
function optionExists(question, value) {
|
|
107
|
+
return (question.options ?? []).some((option) => option.value === value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
getQuestion,
|
|
112
|
+
hasAnswer,
|
|
113
|
+
validateAnswer,
|
|
114
|
+
nextQuestion,
|
|
115
|
+
isComplete,
|
|
116
|
+
intakeProgress,
|
|
117
|
+
reachableQuestions
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=chunk-ND3XEGBE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/intakes/model.ts"],"sourcesContent":["/**\n * Pure intake MODEL — the question-graph and the answer/completion algebra a\n * one-question-at-a-time interview runs on. Zero dependencies: no drizzle, no\n * env, no react, no I/O. The DB layer (`./intakes/drizzle`), the handlers\n * (`./intakes/api`) and the React surface (`./intakes-react`) all build on\n * these functions; this leaf imports nothing back, so a consumer can pull just\n * the intake math.\n *\n * An intake is an ordered list of questions, optionally branching: a question\n * can declare `next` as a function of the answers so far, so the next prompt\n * depends on what was said (e.g. \"do you have a website?\" → no → skip the URL\n * question). Traversal is pure and deterministic — `nextQuestion(graph, answers)`\n * folds the answers into the single question to ask next (or null = done), and\n * `isComplete` is true exactly when every REQUIRED reachable question has a\n * valid answer.\n *\n * Answers are a flat `Record<questionId, value>`. Each question declares an\n * answer `type` that `validateAnswer` checks against — the same validation the\n * UI runs before advancing and the store runs before persisting, so an invalid\n * answer can never enter the payload.\n */\n\n/** The kinds of answer a question accepts. */\nexport type IntakeAnswerType =\n | 'text'\n | 'long-text'\n | 'single-select'\n | 'multi-select'\n | 'boolean'\n | 'number'\n | 'url'\n | 'email'\n\n/** A selectable option for single/multi-select questions. */\nexport interface IntakeOption {\n value: string\n label: string\n}\n\n/** A flat map of answers keyed by question id. */\nexport type IntakeAnswers = Record<string, IntakeAnswerValue>\n\n/** Any value an answer can hold; the type is validated per-question. */\nexport type IntakeAnswerValue = string | string[] | number | boolean | null\n\n/**\n * One question in the graph. `next` (optional) makes the graph branch: given\n * the answers so far it returns the id of the question to ask next, or null to\n * end the interview early. With no `next`, traversal falls through to the next\n * question in declaration order.\n */\nexport interface IntakeQuestion {\n id: string\n /** The prompt the interviewer asks. */\n prompt: string\n type: IntakeAnswerType\n /** Required questions gate completion; optional ones may be skipped. */\n required?: boolean\n /** Help text shown under the prompt. */\n help?: string\n /** Options for single/multi-select questions. */\n options?: IntakeOption[]\n /** Min length (text) or min value (number); inclusive. */\n min?: number\n /** Max length (text) or max value (number); inclusive. */\n max?: number\n /**\n * Branch override: given the answers so far, the id of the next question\n * (or null to end early). Omit for linear flow (next in declaration order).\n */\n next?(answers: IntakeAnswers): string | null\n}\n\n/** The intake definition: an ordered, addressable set of questions. */\nexport interface IntakeGraph {\n /** Stable id for the intake definition (e.g. 'user-onboarding-v1'). */\n id: string\n /** Human title shown at the top of the interview. */\n title: string\n /** Optional one-line description. */\n description?: string\n /** Questions in declaration (default traversal) order. */\n questions: IntakeQuestion[]\n}\n\n/** The reason an answer failed validation. */\nexport type AnswerRejectionReason =\n | 'required'\n | 'wrong-type'\n | 'too-short'\n | 'too-long'\n | 'too-small'\n | 'too-large'\n | 'not-an-option'\n | 'invalid-url'\n | 'invalid-email'\n | 'unknown-question'\n\nexport interface AnswerValidationResult {\n ok: boolean\n reason?: AnswerRejectionReason\n}\n\nconst URL_PATTERN = /^https?:\\/\\/[^\\s/$.?#].[^\\s]*$/i\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** Look up a question by id, or null if the graph has none. */\nexport function getQuestion(graph: IntakeGraph, questionId: string): IntakeQuestion | null {\n return graph.questions.find((q) => q.id === questionId) ?? null\n}\n\n/** True when an answer value is present (not null/undefined/empty). */\nexport function hasAnswer(value: IntakeAnswerValue | undefined): boolean {\n if (value == null) return false\n if (typeof value === 'string') return value.trim().length > 0\n if (Array.isArray(value)) return value.length > 0\n return true\n}\n\n/**\n * Validate one answer against its question. Pure: no I/O, no graph mutation.\n * An empty value is rejected as `required` only when the question is required;\n * an empty value for an optional question is OK (the user skipped it).\n */\nexport function validateAnswer(\n question: IntakeQuestion,\n value: IntakeAnswerValue | undefined,\n): AnswerValidationResult {\n if (!hasAnswer(value)) {\n return question.required ? { ok: false, reason: 'required' } : { ok: true }\n }\n\n switch (question.type) {\n case 'text':\n case 'long-text': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n const len = value.trim().length\n if (question.min != null && len < question.min) return { ok: false, reason: 'too-short' }\n if (question.max != null && len > question.max) return { ok: false, reason: 'too-long' }\n return { ok: true }\n }\n case 'url': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return URL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: 'invalid-url' }\n }\n case 'email': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return EMAIL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: 'invalid-email' }\n }\n case 'number': {\n if (typeof value !== 'number' || !Number.isFinite(value)) return { ok: false, reason: 'wrong-type' }\n if (question.min != null && value < question.min) return { ok: false, reason: 'too-small' }\n if (question.max != null && value > question.max) return { ok: false, reason: 'too-large' }\n return { ok: true }\n }\n case 'boolean': {\n return typeof value === 'boolean' ? { ok: true } : { ok: false, reason: 'wrong-type' }\n }\n case 'single-select': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return optionExists(question, value) ? { ok: true } : { ok: false, reason: 'not-an-option' }\n }\n case 'multi-select': {\n if (!Array.isArray(value)) return { ok: false, reason: 'wrong-type' }\n if (!value.every((v) => typeof v === 'string' && optionExists(question, v))) {\n return { ok: false, reason: 'not-an-option' }\n }\n if (question.min != null && value.length < question.min) return { ok: false, reason: 'too-short' }\n if (question.max != null && value.length > question.max) return { ok: false, reason: 'too-long' }\n return { ok: true }\n }\n default:\n return { ok: false, reason: 'wrong-type' }\n }\n}\n\n/**\n * The single question to ask next given the answers so far, or null when the\n * interview is done. Walks the graph from the first question, following each\n * question's `next` branch (or declaration order) and stopping at the first\n * reachable question that has no valid answer yet. Deterministic and pure.\n *\n * A `next` that points at a missing id, or returns null, ends the walk — so a\n * malformed graph terminates rather than looping. A visited-set guards against\n * a cyclic `next`.\n */\nexport function nextQuestion(graph: IntakeGraph, answers: IntakeAnswers): IntakeQuestion | null {\n if (graph.questions.length === 0) return null\n const visited = new Set<string>()\n let current: IntakeQuestion | null = graph.questions[0] ?? null\n\n while (current) {\n if (visited.has(current.id)) return null\n visited.add(current.id)\n\n const answer = answers[current.id]\n const validity = validateAnswer(current, answer)\n if (!validity.ok || (current.required && !hasAnswer(answer))) {\n return current\n }\n\n current = advance(graph, current, answers)\n }\n\n return null\n}\n\n/**\n * True when every REQUIRED question reachable under the current answers has a\n * valid answer. Pure: it replays the same traversal `nextQuestion` uses and is\n * complete exactly when that traversal has no question left to ask.\n */\nexport function isComplete(graph: IntakeGraph, answers: IntakeAnswers): boolean {\n return nextQuestion(graph, answers) === null\n}\n\n/**\n * Progress as answered-vs-total over the REACHABLE required questions under the\n * current answers — what a progress bar renders. Optional questions don't count\n * toward the denominator (they never block completion).\n */\nexport function intakeProgress(graph: IntakeGraph, answers: IntakeAnswers): { answered: number; total: number } {\n const reachable = reachableQuestions(graph, answers)\n const required = reachable.filter((q) => q.required)\n const answered = required.filter((q) => validateAnswer(q, answers[q.id]).ok && hasAnswer(answers[q.id])).length\n return { answered, total: required.length }\n}\n\n/** The questions reachable under the current answers, in traversal order. */\nexport function reachableQuestions(graph: IntakeGraph, answers: IntakeAnswers): IntakeQuestion[] {\n if (graph.questions.length === 0) return []\n const visited = new Set<string>()\n const out: IntakeQuestion[] = []\n let current: IntakeQuestion | null = graph.questions[0] ?? null\n\n while (current) {\n if (visited.has(current.id)) break\n visited.add(current.id)\n out.push(current)\n current = advance(graph, current, answers)\n }\n return out\n}\n\nfunction advance(graph: IntakeGraph, current: IntakeQuestion, answers: IntakeAnswers): IntakeQuestion | null {\n if (current.next) {\n const nextId = current.next(answers)\n return nextId == null ? null : getQuestion(graph, nextId)\n }\n const index = graph.questions.indexOf(current)\n return graph.questions[index + 1] ?? null\n}\n\nfunction optionExists(question: IntakeQuestion, value: string): boolean {\n return (question.options ?? []).some((option) => option.value === value)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isComplete
|
|
3
|
+
} from "./chunk-ND3XEGBE.js";
|
|
4
|
+
|
|
5
|
+
// src/intakes/completion.ts
|
|
6
|
+
function emptyPayload(graph) {
|
|
7
|
+
return { graphId: graph.id, answers: {} };
|
|
8
|
+
}
|
|
9
|
+
function withAnswer(payload, questionId, value) {
|
|
10
|
+
return { ...payload, answers: { ...payload.answers, [questionId]: value } };
|
|
11
|
+
}
|
|
12
|
+
function payloadComplete(graph, payload) {
|
|
13
|
+
if (payload.graphId !== graph.id) return false;
|
|
14
|
+
return isComplete(graph, payload.answers);
|
|
15
|
+
}
|
|
16
|
+
function payloadIsStale(graph, payload) {
|
|
17
|
+
return payload.graphId !== graph.id;
|
|
18
|
+
}
|
|
19
|
+
function markComplete(payload, at = /* @__PURE__ */ new Date()) {
|
|
20
|
+
return { ...payload, completedAt: at.toISOString() };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
emptyPayload,
|
|
25
|
+
withAnswer,
|
|
26
|
+
payloadComplete,
|
|
27
|
+
payloadIsStale,
|
|
28
|
+
markComplete
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=chunk-USYXHDKE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/intakes/completion.ts"],"sourcesContent":["/**\n * Pure completion-state helpers for an intake payload — the small algebra over\n * the JSON blob the store persists. No I/O: the store reads/writes the row;\n * these functions only build and judge the payload shape, so the same logic\n * runs in the UI, the handlers, and the DB layer without divergence.\n *\n * A payload is `{ graphId, answers, completedAt? }`. It carries the answers\n * and the id of the graph they were collected against, so a later graph\n * revision can detect a stale payload (`graphId` mismatch) rather than\n * silently mixing answers from two question sets.\n */\n\nimport type { IntakeAnswers, IntakeAnswerValue, IntakeGraph } from './model'\nimport { isComplete } from './model'\n\n/** The persisted JSON for one intake (the `payload` column). */\nexport interface IntakePayload {\n /** The graph id the answers were collected against — guards stale schemas. */\n graphId: string\n answers: IntakeAnswers\n /** ISO-8601 instant the intake was completed, or undefined while in progress. */\n completedAt?: string\n}\n\n/** An empty payload for a fresh intake against `graph`. */\nexport function emptyPayload(graph: IntakeGraph): IntakePayload {\n return { graphId: graph.id, answers: {} }\n}\n\n/** A copy of `payload` with one answer set (does not mutate the input). */\nexport function withAnswer(\n payload: IntakePayload,\n questionId: string,\n value: IntakeAnswerValue,\n): IntakePayload {\n return { ...payload, answers: { ...payload.answers, [questionId]: value } }\n}\n\n/**\n * True when the payload's answers complete the graph AND the payload was\n * collected against THIS graph. A `graphId` mismatch is never \"complete\" —\n * the answers belong to a different question set and must be re-collected.\n */\nexport function payloadComplete(graph: IntakeGraph, payload: IntakePayload): boolean {\n if (payload.graphId !== graph.id) return false\n return isComplete(graph, payload.answers)\n}\n\n/** True when the payload was collected against a DIFFERENT graph revision. */\nexport function payloadIsStale(graph: IntakeGraph, payload: IntakePayload): boolean {\n return payload.graphId !== graph.id\n}\n\n/**\n * Stamp the payload complete at `at` (default now). Returns a copy; pure. The\n * caller has already checked `payloadComplete` — this only records the instant.\n */\nexport function markComplete(payload: IntakePayload, at: Date = new Date()): IntakePayload {\n return { ...payload, completedAt: at.toISOString() }\n}\n"],"mappings":";;;;;AAyBO,SAAS,aAAa,OAAmC;AAC9D,SAAO,EAAE,SAAS,MAAM,IAAI,SAAS,CAAC,EAAE;AAC1C;AAGO,SAAS,WACd,SACA,YACA,OACe;AACf,SAAO,EAAE,GAAG,SAAS,SAAS,EAAE,GAAG,QAAQ,SAAS,CAAC,UAAU,GAAG,MAAM,EAAE;AAC5E;AAOO,SAAS,gBAAgB,OAAoB,SAAiC;AACnF,MAAI,QAAQ,YAAY,MAAM,GAAI,QAAO;AACzC,SAAO,WAAW,OAAO,QAAQ,OAAO;AAC1C;AAGO,SAAS,eAAe,OAAoB,SAAiC;AAClF,SAAO,QAAQ,YAAY,MAAM;AACnC;AAMO,SAAS,aAAa,SAAwB,KAAW,oBAAI,KAAK,GAAkB;AACzF,SAAO,EAAE,GAAG,SAAS,aAAa,GAAG,YAAY,EAAE;AACrD;","names":[]}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "./chunk-XWJXZ6WE.js";
|
|
25
25
|
import {
|
|
26
26
|
lightTheme
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-LVD37K5V.js";
|
|
28
28
|
import {
|
|
29
29
|
SCENE_SCHEMA_VERSION,
|
|
30
30
|
boundsIntersect,
|
|
@@ -1772,4 +1772,4 @@ export {
|
|
|
1772
1772
|
Workspace,
|
|
1773
1773
|
DesignCanvasEditor
|
|
1774
1774
|
};
|
|
1775
|
-
//# sourceMappingURL=chunk-
|
|
1775
|
+
//# sourceMappingURL=chunk-YYYXGLLQ.js.map
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
downloadDataUrl,
|
|
12
12
|
exportDocumentJson,
|
|
13
13
|
exportPageDataUrl
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-YYYXGLLQ.js";
|
|
15
15
|
import {
|
|
16
16
|
BTN,
|
|
17
17
|
BTN_ACTIVE,
|
|
@@ -85,8 +85,8 @@ import {
|
|
|
85
85
|
import {
|
|
86
86
|
DesignCanvasChromeLazy,
|
|
87
87
|
DesignCanvasLazy
|
|
88
|
-
} from "../chunk-
|
|
89
|
-
import "../chunk-
|
|
88
|
+
} from "../chunk-FJUOBNIY.js";
|
|
89
|
+
import "../chunk-LVD37K5V.js";
|
|
90
90
|
import {
|
|
91
91
|
assertSceneMediaSrc,
|
|
92
92
|
bleedAwareExportBounds,
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { I as IntakeAnswerValue, a as IntakeQuestion, b as IntakeGraph } from '../model-Cuj7d-xT.js';
|
|
2
|
+
import { IntakeStore } from './drizzle.js';
|
|
3
|
+
import 'drizzle-orm/sqlite-core';
|
|
4
|
+
import './index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Framework-neutral intake API: the get-current / save-answer / complete logic,
|
|
8
|
+
* lifted out of any one app's route file. Each app mounts these in its own
|
|
9
|
+
* route with its own auth — the handlers take an already-resolved store (the
|
|
10
|
+
* caller built it from `createUserIntakeStore` / `createProjectIntakeStore`
|
|
11
|
+
* after running RBAC), the intake graph, and the parsed inputs. They return
|
|
12
|
+
* web-standard `Response`s (available in Workers, Node 18+, Deno, browsers), so
|
|
13
|
+
* "framework-neutral" is literal: no Remix/React-Router/Express import anywhere.
|
|
14
|
+
*
|
|
15
|
+
* The handlers own the graph→HTTP mapping the UI needs: `getCurrentIntake`
|
|
16
|
+
* returns the loaded state PLUS the next question to ask (the question-graph
|
|
17
|
+
* traversal from the `./intakes` leaf) and the progress counter, so the client
|
|
18
|
+
* renders one question at a time without re-deriving the graph. Store
|
|
19
|
+
* `IntakeError`s map to typed 4xx codes, never a generic 500 — an invalid
|
|
20
|
+
* answer is a 400, an incomplete-complete is a 409, never a silent success.
|
|
21
|
+
*
|
|
22
|
+
* Imports `drizzle-orm` transitively (through the store types), so this is a
|
|
23
|
+
* subpath, never re-exported from root.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** What the client renders: the saved state, the next prompt, and progress. */
|
|
27
|
+
interface CurrentIntakeView {
|
|
28
|
+
graphId: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
answers: Record<string, IntakeAnswerValue>;
|
|
32
|
+
/** The next question to ask, or null when the interview is done. */
|
|
33
|
+
nextQuestion: IntakeQuestion | null;
|
|
34
|
+
completed: boolean;
|
|
35
|
+
completedAt: string | null;
|
|
36
|
+
progress: {
|
|
37
|
+
answered: number;
|
|
38
|
+
total: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
interface IntakeApiOptions {
|
|
42
|
+
store: IntakeStore;
|
|
43
|
+
graph: IntakeGraph;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build the intake API bound to one store + graph. Returns three handlers; an
|
|
47
|
+
* app maps its route methods onto them (GET→getCurrentIntake, POST→saveAnswer,
|
|
48
|
+
* a complete action→completeIntake).
|
|
49
|
+
*/
|
|
50
|
+
declare function createIntakeApi(opts: IntakeApiOptions): {
|
|
51
|
+
getCurrentIntake: () => Promise<Response>;
|
|
52
|
+
saveAnswer: (input: {
|
|
53
|
+
questionId?: string;
|
|
54
|
+
value?: IntakeAnswerValue;
|
|
55
|
+
}) => Promise<Response>;
|
|
56
|
+
completeIntake: () => Promise<Response>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export { type CurrentIntakeView, type IntakeApiOptions, createIntakeApi };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IntakeError
|
|
3
|
+
} from "../chunk-4K7BZYO7.js";
|
|
4
|
+
import "../chunk-USYXHDKE.js";
|
|
5
|
+
import {
|
|
6
|
+
intakeProgress,
|
|
7
|
+
nextQuestion
|
|
8
|
+
} from "../chunk-ND3XEGBE.js";
|
|
9
|
+
|
|
10
|
+
// src/intakes/api.ts
|
|
11
|
+
function createIntakeApi(opts) {
|
|
12
|
+
const { store, graph } = opts;
|
|
13
|
+
function view(state) {
|
|
14
|
+
return {
|
|
15
|
+
graphId: graph.id,
|
|
16
|
+
title: graph.title,
|
|
17
|
+
...graph.description ? { description: graph.description } : {},
|
|
18
|
+
answers: state.payload.answers,
|
|
19
|
+
nextQuestion: nextQuestion(graph, state.payload.answers),
|
|
20
|
+
completed: state.completed,
|
|
21
|
+
completedAt: state.completedAt ? state.completedAt.toISOString() : null,
|
|
22
|
+
progress: intakeProgress(graph, state.payload.answers)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async function getCurrentIntake() {
|
|
26
|
+
const state = await store.get();
|
|
27
|
+
return Response.json(view(state));
|
|
28
|
+
}
|
|
29
|
+
async function saveAnswer(input) {
|
|
30
|
+
if (!input.questionId) return Response.json({ error: "Missing questionId" }, { status: 400 });
|
|
31
|
+
try {
|
|
32
|
+
const state = await store.save(input.questionId, input.value ?? null);
|
|
33
|
+
return Response.json(view(state));
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return intakeErrorResponse(err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function completeIntake() {
|
|
39
|
+
try {
|
|
40
|
+
const state = await store.complete();
|
|
41
|
+
return Response.json(view(state));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return intakeErrorResponse(err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { getCurrentIntake, saveAnswer, completeIntake };
|
|
47
|
+
}
|
|
48
|
+
var ERROR_STATUS = {
|
|
49
|
+
"invalid-answer": 400,
|
|
50
|
+
"unknown-question": 404,
|
|
51
|
+
incomplete: 409,
|
|
52
|
+
"stale-graph": 409
|
|
53
|
+
};
|
|
54
|
+
function intakeErrorResponse(err) {
|
|
55
|
+
if (err instanceof IntakeError) {
|
|
56
|
+
return Response.json({ error: err.message, code: err.code }, { status: ERROR_STATUS[err.code] ?? 400 });
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
createIntakeApi
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/intakes/api.ts"],"sourcesContent":["/**\n * Framework-neutral intake API: the get-current / save-answer / complete logic,\n * lifted out of any one app's route file. Each app mounts these in its own\n * route with its own auth — the handlers take an already-resolved store (the\n * caller built it from `createUserIntakeStore` / `createProjectIntakeStore`\n * after running RBAC), the intake graph, and the parsed inputs. They return\n * web-standard `Response`s (available in Workers, Node 18+, Deno, browsers), so\n * \"framework-neutral\" is literal: no Remix/React-Router/Express import anywhere.\n *\n * The handlers own the graph→HTTP mapping the UI needs: `getCurrentIntake`\n * returns the loaded state PLUS the next question to ask (the question-graph\n * traversal from the `./intakes` leaf) and the progress counter, so the client\n * renders one question at a time without re-deriving the graph. Store\n * `IntakeError`s map to typed 4xx codes, never a generic 500 — an invalid\n * answer is a 400, an incomplete-complete is a 409, never a silent success.\n *\n * Imports `drizzle-orm` transitively (through the store types), so this is a\n * subpath, never re-exported from root.\n */\n\nimport {\n type IntakeAnswerValue,\n type IntakeGraph,\n type IntakeQuestion,\n intakeProgress,\n nextQuestion,\n} from './model'\nimport { type IntakeStore, IntakeError } from './drizzle/store'\n\n/** What the client renders: the saved state, the next prompt, and progress. */\nexport interface CurrentIntakeView {\n graphId: string\n title: string\n description?: string\n answers: Record<string, IntakeAnswerValue>\n /** The next question to ask, or null when the interview is done. */\n nextQuestion: IntakeQuestion | null\n completed: boolean\n completedAt: string | null\n progress: { answered: number; total: number }\n}\n\nexport interface IntakeApiOptions {\n store: IntakeStore\n graph: IntakeGraph\n}\n\n/**\n * Build the intake API bound to one store + graph. Returns three handlers; an\n * app maps its route methods onto them (GET→getCurrentIntake, POST→saveAnswer,\n * a complete action→completeIntake).\n */\nexport function createIntakeApi(opts: IntakeApiOptions) {\n const { store, graph } = opts\n\n function view(state: Awaited<ReturnType<IntakeStore['get']>>): CurrentIntakeView {\n return {\n graphId: graph.id,\n title: graph.title,\n ...(graph.description ? { description: graph.description } : {}),\n answers: state.payload.answers,\n nextQuestion: nextQuestion(graph, state.payload.answers),\n completed: state.completed,\n completedAt: state.completedAt ? state.completedAt.toISOString() : null,\n progress: intakeProgress(graph, state.payload.answers),\n }\n }\n\n async function getCurrentIntake(): Promise<Response> {\n const state = await store.get()\n return Response.json(view(state))\n }\n\n async function saveAnswer(input: { questionId?: string; value?: IntakeAnswerValue }): Promise<Response> {\n if (!input.questionId) return Response.json({ error: 'Missing questionId' }, { status: 400 })\n try {\n const state = await store.save(input.questionId, input.value ?? null)\n return Response.json(view(state))\n } catch (err) {\n return intakeErrorResponse(err)\n }\n }\n\n async function completeIntake(): Promise<Response> {\n try {\n const state = await store.complete()\n return Response.json(view(state))\n } catch (err) {\n return intakeErrorResponse(err)\n }\n }\n\n return { getCurrentIntake, saveAnswer, completeIntake }\n}\n\nconst ERROR_STATUS: Record<string, number> = {\n 'invalid-answer': 400,\n 'unknown-question': 404,\n incomplete: 409,\n 'stale-graph': 409,\n}\n\nfunction intakeErrorResponse(err: unknown): Response {\n if (err instanceof IntakeError) {\n return Response.json({ error: err.message, code: err.code }, { status: ERROR_STATUS[err.code] ?? 400 })\n }\n throw err\n}\n"],"mappings":";;;;;;;;;;AAoDO,SAAS,gBAAgB,MAAwB;AACtD,QAAM,EAAE,OAAO,MAAM,IAAI;AAEzB,WAAS,KAAK,OAAmE;AAC/E,WAAO;AAAA,MACL,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,MACb,GAAI,MAAM,cAAc,EAAE,aAAa,MAAM,YAAY,IAAI,CAAC;AAAA,MAC9D,SAAS,MAAM,QAAQ;AAAA,MACvB,cAAc,aAAa,OAAO,MAAM,QAAQ,OAAO;AAAA,MACvD,WAAW,MAAM;AAAA,MACjB,aAAa,MAAM,cAAc,MAAM,YAAY,YAAY,IAAI;AAAA,MACnE,UAAU,eAAe,OAAO,MAAM,QAAQ,OAAO;AAAA,IACvD;AAAA,EACF;AAEA,iBAAe,mBAAsC;AACnD,UAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,WAAO,SAAS,KAAK,KAAK,KAAK,CAAC;AAAA,EAClC;AAEA,iBAAe,WAAW,OAA8E;AACtG,QAAI,CAAC,MAAM,WAAY,QAAO,SAAS,KAAK,EAAE,OAAO,qBAAqB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC5F,QAAI;AACF,YAAM,QAAQ,MAAM,MAAM,KAAK,MAAM,YAAY,MAAM,SAAS,IAAI;AACpE,aAAO,SAAS,KAAK,KAAK,KAAK,CAAC;AAAA,IAClC,SAAS,KAAK;AACZ,aAAO,oBAAoB,GAAG;AAAA,IAChC;AAAA,EACF;AAEA,iBAAe,iBAAoC;AACjD,QAAI;AACF,YAAM,QAAQ,MAAM,MAAM,SAAS;AACnC,aAAO,SAAS,KAAK,KAAK,KAAK,CAAC;AAAA,IAClC,SAAS,KAAK;AACZ,aAAO,oBAAoB,GAAG;AAAA,IAChC;AAAA,EACF;AAEA,SAAO,EAAE,kBAAkB,YAAY,eAAe;AACxD;AAEA,IAAM,eAAuC;AAAA,EAC3C,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EACpB,YAAY;AAAA,EACZ,eAAe;AACjB;AAEA,SAAS,oBAAoB,KAAwB;AACnD,MAAI,eAAe,aAAa;AAC9B,WAAO,SAAS,KAAK,EAAE,OAAO,IAAI,SAAS,MAAM,IAAI,KAAK,GAAG,EAAE,QAAQ,aAAa,IAAI,IAAI,KAAK,IAAI,CAAC;AAAA,EACxG;AACA,QAAM;AACR;","names":[]}
|