Repository navigation
ECHO-1043 feat: dembrane proposes key terms and settings while you create a project - #1368
Open
JorimTheuns wants to merge 9 commits into
Open
JorimTheuns wants to merge 9 commits into
JorimTheuns wants to merge 9 commits into
Conversation
…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
… 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
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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
JorimTheuns
force-pushed
the
echo-1042-project-templates
branch
from
November 10, 2026 17:35
48f4e10 to
7b93383
Compare
JorimTheuns
force-pushed
the
echo-1043-proposed-key-terms
branch
from
November 10, 2026 17:35
589d148 to
60cbb42
Compare
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/suggestionsRequest:
{ 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, 503project.setup_suggestions_unavailablewhen the model fails or answers nonsense, 429 per-person limit (20 per hour), 403/404 withoutproject:createin 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.packages/projects/src/setup/(routes, service, rules, settings, model, fetch-page, stoplists), mounted inapps/api. Uses@dembrane/llm'sCompleterwith a JSON schema ontext_fast(the group catalog translation uses for short structured answers), thinking off. Metered underwithPayer({ ref: "workspace" }), budget checked withrequireAiBudgetForWorkspace, limited with the sharedRateLimiter.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.ts): only a subset ofPresetFields(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. Neveranonymize_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.@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.fetchCheckedgained an optionalblockedpredicate (defaultisPrivateAddress) so tests can reach their own 127.0.0.1 server while everything else stays refused;DeliveryErroris now exported.projectsnow allowswebhooksin its package map.POST /v2/workspaces/:id/projectstakes optionaloverrides(validated againstPresetFields, strict; anything else is a 422validation.invalid_inputnamingoverrides.<field>), written over the preset in the creating transaction (and on a bare project too).PROJECT_SETUP_WEB_SEARCH(default false). Only when it is true and the provider grounds with search (vertex,LLM_SEARCH_GROUNDINGon), 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 witharia-pressed, reason on hover and focus and viaaria-describedby, a remove button each, Accept all, "x of 30" from 24),SuggestedSettings(checkboxes with the template value and the proposal marked), thinSessionBriefConnected(wizard) andSuggestKeyTermsConnected(portal editor),useSuggestionBlock. Results are announced in onearia-live="polite"region.data-tour="setup-suggestions".ProjectUpdateSuggestionCardintocomponents/common/settingChange.tsx, shared by both; the card is otherwise unchanged.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.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 --parallelwith 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.tsprints 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 (--realruns 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
isPrivateAddressinwebhooks/src/deliver.tsno 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 withindexOf/slice; an embedded dotted IPv4 tail is found after the last colon and must passisIP(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.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%or0characters return at once.::1fails in the agent's container on every branch (no IPv6 loopback); it is not touched.Generated by Claude Code