Skip to content

ECHO-1043 feat: dembrane proposes key terms and settings while you create a project - #1368

Open
JorimTheuns wants to merge 9 commits into
echo-1042-project-templatesfrom
echo-1043-proposed-key-terms
Open

JorimTheuns wants to merge 9 commits into
echo-1042-project-templatesfrom
echo-1043-proposed-key-terms

Conversation

@JorimTheuns

@JorimTheuns JorimTheuns commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1365

What this does

In the create flow's name step, after the template is chosen, the one text field is the project context: its help text now asks what the session is about, who takes part and where, and invites pasting an agenda or invitation. "Suggest key terms" sits right under it (enabled once the context or a link has content), with an optional link behind a collapsed "Add a link". What the host types is saved as the project context, whole (the column has no length limit, so nothing is cut), and its first 20,000 characters are what suggestions read; a longer paste gets a plain note saying so. Proposed key terms appear as chips with a reason each; settings worth changing on top of the template appear as a short list with checkboxes. Accepted terms go into the key terms the wizard already submits (visible and editable under "More options"); ticked settings are sent with the create as overrides. The block never blocks: Create stays enabled while it runs, and a failure is one quiet line. The same action sits next to Key terms in the portal editor (Advanced settings), reading the project's saved name, context and existing terms; key terms only there. With no context yet it waits, and its hint links to the context field. Both blocks load lazily, outside the portal's first-load chunk.

Why: hosts do not set key terms on their own (1 new host changed them in the portal editor in 90 days), and the team types them in during onboarding calls from the agenda or invitation the client sends.

Endpoint

POST /api/v2/workspaces/:workspace_id/project-setup/suggestions

Request: { template_key, name, description? (the project context, max 20k), source_text? (max 20k, unused by the UI now), source_url?, language, existing_key_terms?: string[], include_settings?: boolean = true }

Response: { key_terms: [{ term, reason, source: "description"|"text"|"url" }], settings: [{ field, current, proposed, reason }], notes: ("url_not_read"|"at_cap")[], max_key_terms: 30 }

Errors: 402 billing.tier_limit (limit: ai_budget) when the free AI budget is spent, 503 project.setup_suggestions_unavailable when the model fails or answers nonsense, 429 per-person limit (20 per hour), 403/404 without project:create in the workspace, 422 for an unknown template or oversized input. The UI turns every one of them into the same quiet "not available right now" line and tracks the cause.

  • Lives in packages/projects/src/setup/ (routes, service, rules, settings, model, fetch-page, stoplists), mounted in apps/api. Uses @dembrane/llm's Completer with a JSON schema on text_fast (the group catalog translation uses for short structured answers), thinking off. Metered under withPayer({ ref: "workspace" }), budget checked with requireAiBudgetForWorkspace, limited with the shared RateLimiter.
  • Key term rules, in code after the model answers (rules.ts): at most 30 including existing ones; at most 6 words and 60 characters; must occur literally (case-insensitive, whitespace-normalised, whole words) in the name/description, the pasted text or the fetched page; the source's capitalisation is kept; deduped case-insensitively against existing terms; a stoplist per portal language (en, nl, de, fr, es, it, uk, cs) plus: a single word needs a capital or an acronym in the source, and a capital that only starts a sentence or an agenda line does not count; three or more words all in capitals (a heading or a shouted sentence) are dropped. The source is worked out by us, never taken from the model.
  • Settings (settings.ts): only a subset of PresetFields (ask for a name, tutorial, Verify, Verify on finish, Explore/replies, automatic titles), dropped when equal to the template's value, Verify-on-finish only with Verify, at most 4. Never anonymize_transcripts, legal basis, privacy URL, visibility, the event call to action, is_conversation_allowed, or asking participants for email. None of the proposable fields is tier-gated today.
  • Linked page: fetched by our server only, through @dembrane/webhooks' fetchChecked (it resolves once and connects to the checked address). http(s) only, no credentials in the URL, every hop refused when any address is private, loopback, link-local, CGNAT or metadata, at most 3 redirects each re-checked, 5 s for the whole read, 1 MB cap, text/html and text/plain only, scripts and markup stripped, 20k characters. A failure is a soft note; only the reason code is logged, never the URL or content. fetchChecked gained an optional blocked predicate (default isPrivateAddress) so tests can reach their own 127.0.0.1 server while everything else stays refused; DeliveryError is now exported. projects now allows webhooks in its package map.
  • Untrusted input: description, pasted text and page are fenced as data, with an instruction that they are never instructions. The schema and the literal-occurrence check are the defence; a test feeds an injection and a model that obeys it.
  • Create with overrides: POST /v2/workspaces/:id/projects takes optional overrides (validated against PresetFields, strict; anything else is a 422 validation.invalid_input naming overrides.<field>), written over the preset in the creating transaction (and on a bare project too).
  • Web search: PROJECT_SETUP_WEB_SEARCH (default false). Only when it is true and the provider grounds with search (vertex, LLM_SEARCH_GROUNDING on), one grounded look-up runs first. Turning it on sends the project name and the first 500 characters of the host's description to Google Search via Vertex grounding; pasted text and the linked page are never sent there. Its answer is background for the structured call only: terms must still occur in what the host gave or the page. A failed look-up is ignored.

Frontend

  • src/components/project/setup-suggestions/: SessionBrief (the action, "Add a link", status and results; no text field of its own), SuggestedTerms (chips: toggle buttons with aria-pressed, reason on hover and focus and via aria-describedby, a remove button each, Accept all, "x of 30" from 24), SuggestedSettings (checkboxes with the template value and the proposal marked), thin SessionBriefConnected (wizard) and SuggestKeyTermsConnected (portal editor), useSuggestionBlock. Results are announced in one aria-live="polite" region. data-tour="setup-suggestions".
  • The field label and the old/new value marks moved out of ProjectUpdateSuggestionCard into components/common/settingChange.tsx, shared by both; the card is otherwise unchanged.
  • Analytics: project_setup_ai_suggested {template_key, has_text (the project context had content), has_url, terms_proposed, settings_proposed, surface}, project_setup_ai_accepted {terms_accepted, settings_accepted, surface} (wizard: once at create, what the host kept; portal editor: on each accept), project_setup_ai_failed {cause, surface}. Never the text, link or terms.
  • All eight locales written by hand in each catalog's terminology (Dutch: kernbegrippen, informal je; German follows the catalog's Sie), mirrored in machine-translations.json.

Verification

Platform: biome ci (1 pre-existing warning in pricing), typecheck, config check, packages check, drizzle "No schema changes", error-code and accounts-contract sync checks, bun test --parallel with a local Postgres 16: 2438 pass, 163 skip (parity and live-model suites gated on env), 1 fail that also fails on the base branch (webhooks.test.ts "an address that does not answer gives way to the next one": this container has no IPv6, EAFNOSUPPORT on [::1]).

Frontend: check-path-case, pnpm lint, pnpm exec tsc, extract and compile, strict translations (no locale missing a string), sync checks, pnpm exec vitest run, check:contrast (88 of 88), check:grammar (no new findings against the base; the base itself fails on files this PR does not touch), and the portal bundle budget (scripts/check-portal-bundle.mjs): 1092944 bytes gzipped against a budget of 1098464.

Rendered the step in a Playwright harness (e2e/setup-suggestions-harness.spec.ts, reusing the templates harness page, starting from the conversation template whose settings the fixture proposes to change) at 1280 and 390 px, English and Dutch, in empty, idle, loading, results (with an accepted chip, a ticked setting and the focus tooltip), accept-all with the unread-link note, empty and error; Create stays enabled while loading, no sideways scroll.

Quality check: bun packages/projects/test/setup-quality/run.ts prints the proposals for six fixtures (three Dutch municipal sessions, one healthcare, one English workshop, one pasted agenda). This container has no model credentials, so it ran with a deliberately naive fake model only; real-model quality is unverified and must be judged on the preview (--real runs it against Vertex text_fast where credentials exist).

For the docs (ECHO-1045)

When you create a project, the project context is where you describe your session: what it is about, who takes part and where, or paste the agenda or invitation. Under it, "Suggest key terms" (optionally with a link to a page about the session) makes dembrane suggest key terms (the names, places, organisations and abbreviations transcription should spell right), each with a short reason, and sometimes a setting worth changing on top of the template you picked. Every suggested term comes from what you wrote or the page you linked; nothing is added until you accept it, a project holds at most 30 key terms, and you can always edit them later under Key terms. dembrane reads the link once, from its own server. The same "Suggest key terms" button sits next to Key terms in the portal editor for existing projects and reads the project context you saved.

https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL

Fix round

  • CodeQL (two high alerts on Preview only, do not merge: easier onboarding, all work combined #1364): isPrivateAddress in webhooks/src/deliver.ts no longer runs regular expressions on host-supplied text. Anything that is not an IP literal (or is over 100 characters) is refused before parsing; the zone id is cut with indexOf/slice; an embedded dotted IPv4 tail is found after the last colon and must pass isIP(tail) === 4; each group is checked by hand. The same addresses are judged private. This hardens code that also serves webhooks on main; it belongs here because the setup suggestions' page fetch now reaches it with host-supplied URLs.
  • New webhooks/test/private-address.test.ts: mapped, compatible, NAT64, 6to4, zone ids, private ranges and public addresses as before; non-IP input blocked; 50,000 % or 0 characters return at once.
  • The one IPv6 delivery test that needs ::1 fails in the agent's container on every branch (no IPv6 loopback); it is not touched.

Generated by Claude Code

JorimTheuns and others added 3 commits November 10, 2026 14:47
…and overrides on create

POST /v2/workspaces/:id/project-setup/suggestions proposes key terms and a few
settings before the project exists, from the name, context, pasted text and one
linked page. One text_fast call with a JSON schema; every term is checked in code
afterwards (at most 30 with the host's own, 6 words, 60 characters, found
literally in what the host gave or the page we read, no common words, deduped,
the source's capitals). Settings only from the template registry's preset
fields, on top of the chosen template. The page is read on our server through
webhooks' guarded fetch (public addresses only, every redirect checked, 5 s,
1 MB, html or plain text). Same permission as creating a project, rate limited
per person, metered against the workspace's AI budget. Web search stays off
behind PROJECT_SETUP_WEB_SEARCH.

The workspace project create takes optional overrides (preset fields only),
written over the preset in the same transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
…reate flow, and suggest key terms in the portal editor

The name step gets an optional block: one text box, an optional link and
"Suggest key terms". Proposed terms come back as chips with a reason each (accept,
remove, accept all, "x of 30" near the cap) and any settings worth changing as
checkboxes over the template. Accepted terms land in the key terms the wizard
already submits; ticked settings ride on the create as overrides. Create is never
held up. The portal editor's key terms get the same block, key terms only. The
field labels and value marks of the assistant's proposal card move into a shared
piece both use. Analytics carry counts and flags only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
Written by hand in each catalog's terms (kernbegrippen in Dutch, informal je;
German keeps the catalog's Sie), and listed in machine-translations.json for
review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
@JorimTheuns JorimTheuns added the journey label Nov 10, 2026 — with Claude
@linear

linear Bot commented Nov 10, 2026

Copy link
Copy Markdown

ECHO-1043

JorimTheuns and others added 5 commits November 10, 2026 15:03
… they show

The create route and the portal editor sit in the first chunk the participant
portal shares, which put the portal's first load 10 bytes over its budget. The
blocks now load with React.lazy from both screens: first load 1098474 to 1092793
bytes gzipped (budget 1098464).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
…20,000 characters

The create flow now sends the project context (where hosts paste an agenda) as the
description, so it takes the same 20,000 characters as pasted text. A test pins that
a proposal equal to what the live event template already sets is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
…est key terms"

The separate session text box is gone. The project context's help text and
placeholder now ask what the session is, who is there and where, and invite an
agenda or invitation; the action sits right under it, with the link behind "Add a
link". What the host types is saved as the context, whole, and its first 20,000
characters are what suggestions read (the host is told when it is longer). The
portal editor uses the saved context, and when there is none its hint links to the
context field. The harness starts from the conversation template its proposals
change. has_text now means the context had content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
…essions

isPrivateAddress now refuses anything that is not an IP literal (or is over 100
characters) before parsing it, strips a zone id with indexOf and slice, finds an
embedded dotted IPv4 tail after the last colon, and checks each group by hand. The
same addresses are judged private; host-supplied text can no longer make it slow.
This module also serves webhooks on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant