Files
paliad/frontend/src/client/paliadin-starters.ts
m ba2408eb51 feat(paliadin/inline-widget): t-paliad-161 Slice C — floating button + slide-out drawer
The inline Paliadin chat surface — reachable from every authenticated
page, replacing the standalone /paliadin route as the primary entry
point. The standalone page survives as the dedicated full-screen mode
(the drawer's "↗ fullscreen" action links to it).

Components:

- frontend/src/components/PaliadinWidget.tsx — emits the floating
  trigger button (bottom-right, lime , owner-revealed by JS), a
  scrim, and the right-edge slide-out drawer with header (reset /
  fullscreen / close), context chip, message stream, empty-state
  starter list, and textarea+send form. Loads /assets/paliadin-widget.js.

- frontend/src/client/paliadin-widget.ts — runtime. /api/me probe
  reveals the trigger when caller matches PaliadinOwnerEmail (with
  optional is_paliadin_owner flag fast-path); Cmd+J / Ctrl+J shortcut
  toggles open/close (Cmd+K stays reserved for global search per
  client/search.ts). Uses computePaliadinContext() (Slice B) per send
  so route + entity + selection flow into every turn. SSE consumer
  writes assistant bubbles; localStorage persists per-session history.

- frontend/src/client/paliadin-starters.ts — per-route starter prompt
  registry. 14 routes covered (dashboard, projects.*, deadlines.*,
  appointments.*, agenda, events, inbox, tools.*, glossary, courts) +
  a _default fallback. Bilingual (DE/EN); prompts ending in `: ` seed
  the textarea for the user to finish; fully-formed prompts auto-send.

- 39 authenticated TSX pages get a `<PaliadinWidget />` element after
  `<Footer />` via a mechanical pass. paliadin.tsx (the standalone)
  is intentionally excluded — its dedicated UI is the widget's
  fullscreen escape hatch, not a place to overlay another widget.

- frontend/build.ts registers the new bundle.
- frontend/src/styles/global.css gains ~280 lines of widget CSS
  (trigger / scrim / drawer / header / context-chip / messages /
   bubbles / starters / form / send-btn) using only existing tokens.
   Mobile (≤640px): drawer goes full-screen; trigger lifts above
   bottom-nav slots.
- 11 new i18n keys × 2 langs = 22 entries under paliadin.widget.*.

Visibility predicate (paliadin-context.shouldSendContext) hides the
widget on /paliadin, /login, /onboarding. Owner-only gate stays on
PaliadinOwnerEmail.

Build clean: i18n 1955 → 1966 keys, IIFE-wrapped 218KB bundle, go test
green.

Refs: docs/design-paliadin-inline-2026-05-08.md §3, §5.
2026-05-08 19:54:18 +02:00

224 lines
6.3 KiB
TypeScript

// paliadin-starters.ts — per-route starter-prompt registry for the
// Paliadin inline widget (t-paliad-161).
//
// The drawer's empty state renders the matching starter list. Click →
// the prompt populates the textarea; if the prompt ends with `: ` it
// stays in the textarea so the user finishes the sentence.
//
// Static registry by design. LLM-generated starters were considered and
// rejected (latency, determinism, translatability — see design doc §5.2).
export interface Starter {
label_de: string;
label_en: string;
prompt_de: string;
prompt_en: string;
}
export const paliadinStarters: Record<string, Starter[]> = {
"dashboard": [
{
label_de: "Heute",
label_en: "Today",
prompt_de: "Was steht heute an?",
prompt_en: "What's on my plate today?",
},
{
label_de: "Diese Woche",
label_en: "This week",
prompt_de: "Welche Fristen sind diese Woche?",
prompt_en: "Which deadlines are this week?",
},
{
label_de: "Nächste Schritte",
label_en: "Next steps",
prompt_de: "Was sollte ich als nächstes erledigen?",
prompt_en: "What should I tackle next?",
},
],
"projects.detail": [
{
label_de: "Status der Akte",
label_en: "Project status",
prompt_de: "Was ist der aktuelle Status dieser Akte?",
prompt_en: "What's the status of this project?",
},
{
label_de: "Diese Woche",
label_en: "This week",
prompt_de: "Was steht für diese Akte diese Woche an?",
prompt_en: "What's on for this project this week?",
},
{
label_de: "Frist anlegen",
label_en: "Add a deadline",
prompt_de: "Lege eine Frist für diese Akte an: ",
prompt_en: "Add a deadline for this project: ",
},
],
"projects.list": [
{
label_de: "Aktive Akten",
label_en: "Active projects",
prompt_de: "Welche Akten sind aktuell aktiv?",
prompt_en: "Which projects are currently active?",
},
{
label_de: "UPC-Akten",
label_en: "UPC projects",
prompt_de: "Zeige mir alle UPC-Akten.",
prompt_en: "Show me all UPC projects.",
},
],
"deadlines.detail": [
{
label_de: "Erkläre die Frist",
label_en: "Explain this deadline",
prompt_de: "Erkläre mir die Frist auf dieser Seite.",
prompt_en: "Explain this deadline.",
},
{
label_de: "Rechtsgrundlage",
label_en: "Legal basis",
prompt_de: "Welche Norm ist hier einschlägig?",
prompt_en: "What's the relevant rule?",
},
{
label_de: "Folgefristen",
label_en: "Follow-on deadlines",
prompt_de: "Welche Fristen ergeben sich aus dieser?",
prompt_en: "What follow-on deadlines flow from this?",
},
],
"deadlines.list": [
{
label_de: "Überfällige",
label_en: "Overdue",
prompt_de: "Welche Fristen sind überfällig?",
prompt_en: "Which deadlines are overdue?",
},
{
label_de: "Diese Woche",
label_en: "This week",
prompt_de: "Was steht diese Woche an?",
prompt_en: "What's due this week?",
},
{
label_de: "Frist anlegen",
label_en: "Add a deadline",
prompt_de: "Lege eine Frist an: ",
prompt_en: "Add a deadline: ",
},
],
"appointments.list": [
{
label_de: "Heute",
label_en: "Today",
prompt_de: "Welche Termine habe ich heute?",
prompt_en: "What appointments do I have today?",
},
{
label_de: "Termin anlegen",
label_en: "Add an appointment",
prompt_de: "Lege einen Termin an: ",
prompt_en: "Add an appointment: ",
},
],
"appointments.detail": [
{
label_de: "Erkläre den Termin",
label_en: "Explain this appointment",
prompt_de: "Was ist auf diesem Termin zu klären?",
prompt_en: "What needs to be addressed at this appointment?",
},
],
"agenda": [
{
label_de: "Diese Woche",
label_en: "This week",
prompt_de: "Welche Termine und Fristen habe ich diese Woche?",
prompt_en: "What appointments and deadlines do I have this week?",
},
{
label_de: "Konflikte prüfen",
label_en: "Check conflicts",
prompt_de: "Gibt es Terminkonflikte in dieser Ansicht?",
prompt_en: "Are there scheduling conflicts in this view?",
},
],
"events": [
{
label_de: "Diese Woche",
label_en: "This week",
prompt_de: "Was steht diese Woche an?",
prompt_en: "What's on for this week?",
},
{
label_de: "Überfällige",
label_en: "Overdue",
prompt_de: "Was ist überfällig?",
prompt_en: "What's overdue?",
},
],
"inbox": [
{
label_de: "Was wartet",
label_en: "What's waiting",
prompt_de: "Was wartet auf meine Genehmigung?",
prompt_en: "What's waiting for my approval?",
},
],
"tools.fristenrechner": [
{
label_de: "Erkläre den Rechner",
label_en: "Explain the calculator",
prompt_de: "Wie funktioniert der Fristenrechner?",
prompt_en: "How does the deadline calculator work?",
},
{
label_de: "Verfahrensablauf",
label_en: "Proceeding flow",
prompt_de: "Welche Folgefristen kommen typischerweise nach einer Klage?",
prompt_en: "What deadlines typically follow a complaint?",
},
],
"tools.kostenrechner": [
{
label_de: "Erkläre die Berechnung",
label_en: "Explain the calculation",
prompt_de: "Wie wird der Streitwert berechnet?",
prompt_en: "How is the matter value calculated?",
},
],
"glossary": [
{
label_de: "Begriff erklären",
label_en: "Explain a term",
prompt_de: "Erkläre mir den Begriff: ",
prompt_en: "Explain the term: ",
},
],
"courts": [
{
label_de: "UPC-Divisionen",
label_en: "UPC divisions",
prompt_de: "Zeige mir alle UPC Local Divisions.",
prompt_en: "Show me all UPC Local Divisions.",
},
],
// Fallback for unmapped routes — the textarea stays empty, the user
// types from scratch.
"_default": [
{
label_de: "Was kann ich für dich tun?",
label_en: "What can I help with?",
prompt_de: "",
prompt_en: "",
},
],
};
export function startersFor(routeName: string): Starter[] {
return paliadinStarters[routeName] || paliadinStarters["_default"];
}