@nocobase/portal-template-default 1.0.3 → 1.0.5

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.
Files changed (170) hide show
  1. package/README.MD +10 -5
  2. package/package.json +17 -17
  3. package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
  4. package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
  5. package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
  6. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
  7. package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
  8. package/registry/nocobase-i18n/README.md +23 -32
  9. package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
  10. package/registry/nocobase-i18n/demo/index.tsx +2 -2
  11. package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
  12. package/registry/nocobase-i18n/extension.tsx +3 -4
  13. package/registry/nocobase-i18n/index.ts +4 -3
  14. package/registry/nocobase-i18n/locales/en-US.ts +2 -52
  15. package/registry/nocobase-i18n/locales/index.ts +8 -0
  16. package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
  17. package/registry/nocobase-i18n/provider.tsx +18 -6
  18. package/registry/nocobase-i18n/server-resources.ts +13 -31
  19. package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
  20. package/registry/nocobase-route-surfaces/README.md +21 -0
  21. package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
  22. package/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx +2 -1
  23. package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
  24. package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
  25. package/registry/nocobase-route-surfaces/extension.tsx +119 -0
  26. package/registry/nocobase-route-surfaces/index.ts +7 -0
  27. package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
  28. package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
  29. package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
  30. package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
  31. package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
  32. package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
  33. package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
  34. package/registry.config.json +30 -3
  35. package/scripts/registry.mjs +0 -2
  36. package/src/App.tsx +65 -35
  37. package/src/app/extension.ts +1 -2
  38. package/src/app/extensions.tsx +0 -14
  39. package/src/components/app-shell/breadcrumb.tsx +67 -2
  40. package/src/components/app-shell/sidebar.tsx +2 -2
  41. package/src/components/resources/views/list-view.tsx +3 -2
  42. package/src/components/ui/sheet.tsx +22 -21
  43. package/src/lib/nocobase/client.ts +6 -1
  44. package/src/locales/en-US.ts +81 -0
  45. package/src/locales/zh-CN.ts +77 -0
  46. package/src/pages/users/create.tsx +76 -53
  47. package/src/pages/users/edit.tsx +87 -56
  48. package/src/pages/users/index.ts +3 -0
  49. package/src/pages/users/layout.tsx +15 -0
  50. package/src/pages/users/list.tsx +19 -21
  51. package/src/pages/users/role-badges.tsx +43 -0
  52. package/src/pages/users/role-detail.tsx +272 -0
  53. package/src/pages/users/role-utils.ts +6 -0
  54. package/src/pages/users/routes.ts +24 -0
  55. package/src/pages/users/show.tsx +195 -104
  56. package/src/pages/users/types.ts +13 -0
  57. package/src/providers/i18n/index.ts +3 -0
  58. package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
  59. package/src/providers/system-settings/context.ts +26 -0
  60. package/src/providers/system-settings/index.ts +2 -0
  61. package/src/providers/system-settings/provider.tsx +94 -0
  62. package/src/extensions/nocobase-acl/README.md +0 -19
  63. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +0 -106
  64. package/src/extensions/nocobase-acl/components/index.ts +0 -3
  65. package/src/extensions/nocobase-acl/components/role-options.ts +0 -36
  66. package/src/extensions/nocobase-acl/components/role-switcher.tsx +0 -135
  67. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +0 -186
  68. package/src/extensions/nocobase-acl/demo/components.tsx +0 -231
  69. package/src/extensions/nocobase-acl/demo/index.tsx +0 -505
  70. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +0 -134
  71. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -198
  72. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +0 -37
  73. package/src/extensions/nocobase-acl/extension.tsx +0 -71
  74. package/src/extensions/nocobase-acl/index.ts +0 -1
  75. package/src/extensions/nocobase-ai/README.md +0 -70
  76. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +0 -20
  77. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +0 -50
  78. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +0 -32
  79. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -31
  80. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +0 -92
  81. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +0 -90
  82. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +0 -429
  83. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +0 -51
  84. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +0 -88
  85. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -77
  86. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +0 -252
  87. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +0 -173
  88. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +0 -118
  89. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +0 -343
  90. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +0 -165
  91. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +0 -30
  92. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +0 -29
  93. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -142
  94. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +0 -316
  95. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -99
  96. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +0 -114
  97. package/src/extensions/nocobase-ai/components/index.ts +0 -59
  98. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +0 -44
  99. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -469
  100. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
  101. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +0 -18
  102. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +0 -18
  103. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -54
  104. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
  105. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -54
  106. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -31
  107. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +0 -364
  108. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +0 -122
  109. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +0 -282
  110. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +0 -99
  111. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +0 -59
  112. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -18
  113. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -20
  114. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -34
  115. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -24
  116. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +0 -138
  117. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -67
  118. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -59
  119. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -55
  120. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -18
  121. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +0 -108
  122. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +0 -73
  123. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -149
  124. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +0 -49
  125. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +0 -210
  126. package/src/extensions/nocobase-ai/demo/floating.tsx +0 -134
  127. package/src/extensions/nocobase-ai/demo/index.tsx +0 -346
  128. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +0 -151
  129. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -355
  130. package/src/extensions/nocobase-ai/demo/page-context.tsx +0 -884
  131. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +0 -197
  132. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +0 -41
  133. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +0 -452
  134. package/src/extensions/nocobase-ai/demo/shortcut.tsx +0 -1207
  135. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +0 -530
  136. package/src/extensions/nocobase-ai/extension.tsx +0 -158
  137. package/src/extensions/nocobase-ai/global-ai-chat.tsx +0 -213
  138. package/src/extensions/nocobase-ai/index.ts +0 -3
  139. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +0 -323
  140. package/src/extensions/nocobase-ai/providers/avatars.ts +0 -817
  141. package/src/extensions/nocobase-ai/providers/chat-context.tsx +0 -109
  142. package/src/extensions/nocobase-ai/providers/chat-controller.ts +0 -84
  143. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +0 -198
  144. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +0 -871
  145. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +0 -147
  146. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +0 -105
  147. package/src/extensions/nocobase-ai/providers/chat-transport.ts +0 -184
  148. package/src/extensions/nocobase-ai/providers/form-registry.tsx +0 -271
  149. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +0 -231
  150. package/src/extensions/nocobase-ai/providers/index.ts +0 -61
  151. package/src/extensions/nocobase-ai/providers/model.ts +0 -32
  152. package/src/extensions/nocobase-ai/providers/page-context.tsx +0 -136
  153. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +0 -60
  154. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +0 -82
  155. package/src/extensions/nocobase-ai/providers/stream-parser.ts +0 -61
  156. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +0 -316
  157. package/src/extensions/nocobase-ai/providers/types.ts +0 -183
  158. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +0 -463
  159. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -94
  160. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +0 -206
  161. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +0 -554
  162. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +0 -210
  163. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +0 -93
  164. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +0 -108
  165. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +0 -178
  166. package/src/extensions/nocobase-ai/services/index.ts +0 -7
  167. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +0 -454
  168. package/src/extensions/nocobase-ai/services/types.ts +0 -82
  169. /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
  170. /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
@@ -1,884 +0,0 @@
1
- import {
2
- AIChatWindow,
3
- AIEmployeeShortcut,
4
- ChatInline,
5
- useAIForm,
6
- useAIPageElement,
7
- useAIPageElementHandle,
8
- } from "../components";
9
- import { applyReactHookFormValues } from "../adapters/react-hook-form";
10
- import { Badge } from "@/components/ui/badge";
11
- import { Button } from "@/components/ui/button";
12
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
13
- import { Input } from "@/components/ui/input";
14
- import { Label } from "@/components/ui/label";
15
- import {
16
- Select,
17
- SelectContent,
18
- SelectItem,
19
- SelectTrigger,
20
- SelectValue,
21
- } from "@/components/ui/select";
22
- import { Textarea } from "@/components/ui/textarea";
23
- import {
24
- AIChatProvider,
25
- AIPageContextScope,
26
- useAI,
27
- useAIChatController,
28
- type AIEmployeeTask,
29
- type AIEmployeeTasks,
30
- type AIWorkContextItem,
31
- } from "../providers";
32
- import { useMemo, useState, type ReactNode } from "react";
33
- import { useForm } from "react-hook-form";
34
- import { AIConfigurationGate } from "./configuration-gate";
35
- import { PageContextPromptGenerator } from "./page-context-prompt-generator";
36
- import { PageElementShowcase } from "./page-element-showcase";
37
-
38
- const isBusinessEmployee = (employee: { username: string }) =>
39
- !["nathan", "dara"].includes(employee.username.toLowerCase());
40
-
41
- export function PageContextPage() {
42
- return (
43
- <AIConfigurationGate>
44
- <PageContextPageContent />
45
- </AIConfigurationGate>
46
- );
47
- }
48
-
49
- function PageContextPageContent() {
50
- return (
51
- <div className="space-y-12 pb-12">
52
- <section className="flex flex-wrap items-start justify-between gap-5 border-b pb-8">
53
- <div>
54
- <div className="flex items-center gap-2">
55
- <Badge variant="secondary">AI Components</Badge>
56
- <Badge variant="outline">Conversation context</Badge>
57
- </div>
58
- <h1 className="mt-4 text-3xl font-semibold tracking-[-0.035em]">
59
- Page context and frontend tools
60
- </h1>
61
- <p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
62
- Connect AI employees to live React page state. Start with manual
63
- context selection, configure task context, then expose fixed or
64
- custom frontend capabilities that can safely update the page.
65
- </p>
66
- </div>
67
- </section>
68
-
69
- <ContextSection
70
- eyebrow="Manual context"
71
- title="Pick page context while composing a message"
72
- description="The user can pick any registered page element from the composer. Its current content is added to this message without changing task configuration."
73
- >
74
- <PageElementShowcase />
75
- </ContextSection>
76
-
77
- <ContextSection
78
- eyebrow="Task context · Shortcut"
79
- title="Reference page context from a Shortcut task"
80
- description="The Shortcut task stores a page-element reference in message.workContext and reads its latest content when the user starts the task."
81
- >
82
- <ShortcutTaskContextShowcase />
83
- </ContextSection>
84
-
85
- <ContextSection
86
- eyebrow="Task context · Conversation preset"
87
- title="Reference page context from a conversation preset task"
88
- description="The AIChatProvider employeeTasks configuration uses the same message.workContext reference, but exposes the task in the conversation empty state instead of through a Shortcut."
89
- >
90
- <PresetTaskContextShowcase />
91
- </ContextSection>
92
-
93
- <ContextSection
94
- eyebrow="Task context · Scope inheritance"
95
- title="Inherit the surrounding page context"
96
- description="A Shortcut or AIChatProvider inside AIPageContextScope inherits that context. A task-level message.workContext still takes precedence when configured."
97
- >
98
- <InheritedContextShowcase />
99
- </ContextSection>
100
-
101
- <ContextSection
102
- eyebrow="Built-in frontend tool"
103
- title="Fill a registered React form with Form filler"
104
- description="Form filler is registered once by AIProvider. The form exposes its identifier, field schema, live values, and setter through useAIForm; it is not part of the custom frontend Tool catalog."
105
- >
106
- <FormFillerShowcase />
107
- </ContextSection>
108
-
109
- <ContextSection
110
- eyebrow="Custom frontend tool"
111
- title="Expose a page-specific action to the AI employee"
112
- description="A registered page element can advertise custom Tools through the NocoBase loadFrontendTool and executeFrontendTool protocol. The example uses ASK permission and updates only the local quote preview."
113
- >
114
- <CustomFrontendToolShowcase />
115
- </ContextSection>
116
-
117
- <ContextSection
118
- eyebrow="Prompt generator"
119
- title="Generate a complete page context scene"
120
- description="Describe the business scene, select the AI employee and task, then generate the full page surface, context binding, conversation layout, and optional frontend capability together."
121
- >
122
- <PageContextPromptGenerator />
123
- </ContextSection>
124
- </div>
125
- );
126
- }
127
-
128
- type LeadIntakeValues = {
129
- company: string;
130
- contactName: string;
131
- email: string;
132
- priority: "low" | "normal" | "high";
133
- };
134
-
135
- function FormFillerShowcase() {
136
- const { employees } = useAI();
137
- const employee =
138
- employees.find((item) => item.username === "form_assistant") ??
139
- employees.find((item) => item.username.toLowerCase() === "dex") ??
140
- employees.filter(isBusinessEmployee)[0]!;
141
- const controller = useAIChatController();
142
- const [sourceText, setSourceText] = useState(
143
- "Acme Logistics is evaluating the enterprise plan. The main contact is Jordan Lee at jordan.lee@acme.test. This is a high-priority opportunity."
144
- );
145
- const form = useForm<LeadIntakeValues>({
146
- defaultValues: {
147
- company: "",
148
- contactName: "",
149
- email: "",
150
- priority: "normal",
151
- },
152
- });
153
- const fields = useMemo(
154
- () => [
155
- { name: "company", title: "Company", type: "string", required: true },
156
- {
157
- name: "contactName",
158
- title: "Contact name",
159
- type: "string",
160
- required: true,
161
- },
162
- { name: "email", title: "Email", type: "email" },
163
- {
164
- name: "priority",
165
- title: "Priority",
166
- type: "string",
167
- enum: ["low", "normal", "high"],
168
- },
169
- ],
170
- []
171
- );
172
- const formRef = useAIForm({
173
- id: "lead-intake-form",
174
- title: "Lead intake form",
175
- fields,
176
- getValues: form.getValues,
177
- setValues: (values) => applyReactHookFormValues(form, values),
178
- });
179
- const contextReference = useMemo<AIWorkContextItem>(
180
- () => ({
181
- type: "page-element",
182
- id: "lead-intake-form",
183
- title: "Lead intake form",
184
- kind: "form",
185
- }),
186
- []
187
- );
188
- const task = useMemo<AIEmployeeTask>(
189
- () => ({
190
- title: "Fill lead form",
191
- message: {
192
- user: `Extract the lead details from the following text and fill the current form. Do not submit it.\n\n${sourceText}`,
193
- },
194
- autoSend: true,
195
- }),
196
- [sourceText]
197
- );
198
-
199
- return (
200
- <AIPageContextScope context={contextReference}>
201
- <AIChatProvider
202
- id="form-filler-context-demo"
203
- controller={controller}
204
- defaultEmployee={employee.username}
205
- >
206
- <Card className="gap-0 overflow-hidden py-0">
207
- <div className="grid min-h-[610px] xl:grid-cols-[minmax(0,1fr)_410px]">
208
- <div className="space-y-5 bg-muted/15 p-4 sm:p-5">
209
- <Card>
210
- <CardHeader>
211
- <div className="flex items-start justify-between gap-4">
212
- <div>
213
- <CardTitle>Source content</CardTitle>
214
- <p className="mt-1 text-xs leading-5 text-muted-foreground">
215
- Edit this text, then ask the AI employee to fill the
216
- registered form below.
217
- </p>
218
- </div>
219
- <AIEmployeeShortcut
220
- aiEmployee={employee.username}
221
- target={controller}
222
- tasks={[task]}
223
- label={`Ask ${employee.nickname}`}
224
- size={34}
225
- />
226
- </div>
227
- </CardHeader>
228
- <CardContent>
229
- <Textarea
230
- value={sourceText}
231
- onChange={(event) => setSourceText(event.target.value)}
232
- className="min-h-28"
233
- />
234
- </CardContent>
235
- </Card>
236
-
237
- <form ref={formRef} className="rounded-xl border bg-card">
238
- <div className="flex flex-wrap items-start justify-between gap-3 border-b p-5">
239
- <div>
240
- <h3 className="font-semibold">Lead intake form</h3>
241
- <p className="mt-1 text-xs text-muted-foreground">
242
- Form filler changes visible values only. It never submits
243
- the form.
244
- </p>
245
- </div>
246
- <div className="flex items-center gap-2">
247
- <Badge variant="secondary">useAIForm</Badge>
248
- <Button
249
- type="button"
250
- variant="outline"
251
- size="sm"
252
- onClick={() =>
253
- form.reset({
254
- company: "",
255
- contactName: "",
256
- email: "",
257
- priority: "normal",
258
- })
259
- }
260
- >
261
- Clear
262
- </Button>
263
- </div>
264
- </div>
265
- <div className="grid gap-4 p-5 sm:grid-cols-2">
266
- <label className="space-y-2">
267
- <Label htmlFor="form-filler-company">Company</Label>
268
- <Input
269
- id="form-filler-company"
270
- {...form.register("company")}
271
- />
272
- </label>
273
- <label className="space-y-2">
274
- <Label htmlFor="form-filler-contact">Contact name</Label>
275
- <Input
276
- id="form-filler-contact"
277
- {...form.register("contactName")}
278
- />
279
- </label>
280
- <label className="space-y-2">
281
- <Label htmlFor="form-filler-email">Email</Label>
282
- <Input
283
- id="form-filler-email"
284
- type="email"
285
- {...form.register("email")}
286
- />
287
- </label>
288
- <label className="space-y-2">
289
- <Label>Priority</Label>
290
- <Select
291
- value={form.watch("priority")}
292
- onValueChange={(value) =>
293
- value &&
294
- form.setValue(
295
- "priority",
296
- value as LeadIntakeValues["priority"],
297
- {
298
- shouldDirty: true,
299
- shouldTouch: true,
300
- }
301
- )
302
- }
303
- >
304
- <SelectTrigger className="w-full">
305
- <SelectValue />
306
- </SelectTrigger>
307
- <SelectContent>
308
- <SelectItem value="low">Low</SelectItem>
309
- <SelectItem value="normal">Normal</SelectItem>
310
- <SelectItem value="high">High</SelectItem>
311
- </SelectContent>
312
- </Select>
313
- </label>
314
- </div>
315
- </form>
316
- </div>
317
-
318
- <div className="min-h-0 border-t bg-card xl:border-l xl:border-t-0">
319
- <ChatInline className="h-[610px] rounded-none border-0">
320
- <AIChatWindow
321
- showConversationToggle={false}
322
- disclaimer={false}
323
- />
324
- </ChatInline>
325
- </div>
326
- </div>
327
- </Card>
328
- </AIChatProvider>
329
- </AIPageContextScope>
330
- );
331
- }
332
-
333
- function CustomFrontendToolShowcase() {
334
- const { employees } = useAI();
335
- const employee = employees.filter(isBusinessEmployee)[0]!;
336
- const controller = useAIChatController();
337
- const [discountPercent, setDiscountPercent] = useState(0);
338
- const [approvalNote, setApprovalNote] = useState("Not reviewed");
339
- const baseAmount = 24_000;
340
- const total = baseAmount * (1 - discountPercent / 100);
341
- const tools = useMemo(
342
- () => [
343
- {
344
- name: "update_quote_discount",
345
- title: "Update quote discount",
346
- description:
347
- "Update the discount percentage and review note shown on the current quote. This changes the page preview only and does not save the quote.",
348
- permission: "ASK" as const,
349
- inputSchema: {
350
- type: "object",
351
- properties: {
352
- discountPercent: {
353
- type: "number",
354
- minimum: 0,
355
- maximum: 30,
356
- description: "Discount percentage applied to the quote.",
357
- },
358
- note: {
359
- type: "string",
360
- description: "Short explanation shown in the review note.",
361
- },
362
- },
363
- required: ["discountPercent", "note"],
364
- additionalProperties: false,
365
- },
366
- execute: (args: unknown) => {
367
- const values =
368
- args && typeof args === "object" && !Array.isArray(args)
369
- ? (args as { discountPercent?: unknown; note?: unknown })
370
- : {};
371
- if (
372
- typeof values.discountPercent !== "number" ||
373
- values.discountPercent < 0 ||
374
- values.discountPercent > 30 ||
375
- typeof values.note !== "string"
376
- ) {
377
- return {
378
- status: "error",
379
- content: "Provide a discount from 0 to 30 and a review note.",
380
- };
381
- }
382
- setDiscountPercent(values.discountPercent);
383
- setApprovalNote(values.note);
384
- return {
385
- status: "success",
386
- content: `Updated the quote preview to a ${values.discountPercent}% discount.`,
387
- };
388
- },
389
- },
390
- ],
391
- []
392
- );
393
- const quoteContext = useAIPageElementHandle({
394
- id: "quote-review-card",
395
- title: "Quote review",
396
- kind: "record-detail",
397
- tools,
398
- getContext: () => ({
399
- resource: "quotes",
400
- record: {
401
- quoteNumber: "QT-2026-1048",
402
- customer: "Northwind Studio",
403
- baseAmount,
404
- discountPercent,
405
- total,
406
- approvalNote,
407
- },
408
- }),
409
- });
410
- const task = useMemo<AIEmployeeTask>(
411
- () => ({
412
- title: "Apply review discount",
413
- message: {
414
- user: "Update the current quote preview to use a 12% discount and set the review note to 'Approved for the renewal discussion'.",
415
- },
416
- autoSend: true,
417
- }),
418
- []
419
- );
420
-
421
- return (
422
- <AIPageContextScope context={quoteContext.context}>
423
- <AIChatProvider
424
- id="custom-frontend-tool-demo"
425
- controller={controller}
426
- defaultEmployee={employee.username}
427
- >
428
- <Card className="gap-0 overflow-hidden py-0">
429
- <div className="grid min-h-[560px] xl:grid-cols-[minmax(0,1fr)_410px]">
430
- <div className="bg-muted/15 p-4 sm:p-5">
431
- <Card ref={quoteContext.ref}>
432
- <CardHeader>
433
- <div className="flex flex-wrap items-start justify-between gap-4">
434
- <div>
435
- <CardTitle>Quote review</CardTitle>
436
- <p className="mt-1 text-xs text-muted-foreground">
437
- QT-2026-1048 · Northwind Studio
438
- </p>
439
- </div>
440
- <div className="flex items-center gap-2">
441
- <Badge variant="outline">Custom Tool</Badge>
442
- <Badge variant="secondary">ASK</Badge>
443
- </div>
444
- </div>
445
- </CardHeader>
446
- <CardContent className="space-y-5">
447
- <div className="grid gap-3 sm:grid-cols-3">
448
- {[
449
- ["Base amount", `$${baseAmount.toLocaleString()}`],
450
- ["Discount", `${discountPercent}%`],
451
- ["Preview total", `$${total.toLocaleString()}`],
452
- ].map(([label, value]) => (
453
- <div
454
- key={label}
455
- className="rounded-lg border bg-background p-3"
456
- >
457
- <div className="text-xs text-muted-foreground">
458
- {label}
459
- </div>
460
- <div className="mt-1 font-medium">{value}</div>
461
- </div>
462
- ))}
463
- </div>
464
- <div className="rounded-lg border bg-background p-3">
465
- <div className="text-xs text-muted-foreground">
466
- Review note
467
- </div>
468
- <div className="mt-1 text-sm">{approvalNote}</div>
469
- </div>
470
- <div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
471
- <p className="max-w-lg text-xs leading-5 text-muted-foreground">
472
- The page element registers update_quote_discount. Its full
473
- input schema is loaded only when the AI chooses this Tool.
474
- Approval is controlled by the ASK permission.
475
- </p>
476
- <AIEmployeeShortcut
477
- aiEmployee={employee.username}
478
- target={controller}
479
- tasks={[task]}
480
- label={`Ask ${employee.nickname}`}
481
- size={34}
482
- />
483
- </div>
484
- </CardContent>
485
- </Card>
486
- </div>
487
-
488
- <div className="min-h-0 border-t bg-card xl:border-l xl:border-t-0">
489
- <ChatInline className="h-[560px] rounded-none border-0">
490
- <AIChatWindow
491
- showConversationToggle={false}
492
- disclaimer={false}
493
- />
494
- </ChatInline>
495
- </div>
496
- </div>
497
- </Card>
498
- </AIChatProvider>
499
- </AIPageContextScope>
500
- );
501
- }
502
-
503
- function ShortcutTaskContextShowcase() {
504
- const { employees } = useAI();
505
- const employee = employees.filter(isBusinessEmployee)[0]!;
506
- const controller = useAIChatController();
507
- const [summary, setSummary] = useState(
508
- "Payment callback reached the order service twelve minutes late."
509
- );
510
- const [severity, setSeverity] = useState("high");
511
- const contextReference = useMemo<AIWorkContextItem>(
512
- () => ({
513
- type: "page-element",
514
- id: "selected-support-case",
515
- title: "Selected support case",
516
- }),
517
- []
518
- );
519
- const contextRef = useAIPageElement({
520
- id: "selected-support-case",
521
- title: "Selected support case",
522
- kind: "editable-record",
523
- getContext: () => ({
524
- resource: "supportTickets",
525
- values: { summary, severity },
526
- }),
527
- });
528
-
529
- const shortcutTask = useMemo<AIEmployeeTask>(
530
- () => ({
531
- title: "Analyze selected case",
532
- message: {
533
- user: "Analyze the selected support case.",
534
- workContext: [contextReference],
535
- },
536
- autoSend: false,
537
- }),
538
- [contextReference]
539
- );
540
- return (
541
- <AIChatProvider
542
- id="shortcut-task-context-demo"
543
- controller={controller}
544
- defaultEmployee={employee.username}
545
- >
546
- <Card className="gap-0 overflow-hidden py-0">
547
- <div className="grid min-h-[560px] xl:grid-cols-[minmax(0,1fr)_410px]">
548
- <div className="bg-muted/15 p-4 sm:p-5">
549
- <Card ref={contextRef}>
550
- <CardHeader>
551
- <div className="flex items-start justify-between gap-3">
552
- <div>
553
- <CardTitle>Selected support case</CardTitle>
554
- <p className="mt-1 text-xs text-muted-foreground">
555
- Change a value before running either task.
556
- </p>
557
- </div>
558
- <Badge variant="secondary">Task context</Badge>
559
- </div>
560
- </CardHeader>
561
- <CardContent className="space-y-4">
562
- <label className="block space-y-2">
563
- <Label htmlFor="selected-context-summary">Summary</Label>
564
- <Input
565
- id="selected-context-summary"
566
- value={summary}
567
- onChange={(event) => setSummary(event.target.value)}
568
- />
569
- </label>
570
- <label className="block space-y-2">
571
- <Label>Severity</Label>
572
- <Select
573
- value={severity}
574
- onValueChange={(value) => value && setSeverity(value)}
575
- >
576
- <SelectTrigger className="w-full">
577
- <SelectValue />
578
- </SelectTrigger>
579
- <SelectContent>
580
- <SelectItem value="low">Low</SelectItem>
581
- <SelectItem value="medium">Medium</SelectItem>
582
- <SelectItem value="high">High</SelectItem>
583
- </SelectContent>
584
- </Select>
585
- </label>
586
- </CardContent>
587
- </Card>
588
- <Card className="mt-5 border-dashed bg-background/80">
589
- <CardHeader>
590
- <div className="flex items-start justify-between gap-4">
591
- <div>
592
- <CardTitle>Task shortcut area</CardTitle>
593
- <p className="mt-1 text-xs leading-5 text-muted-foreground">
594
- This button is outside the selected page element. Its task
595
- explicitly references “Selected support case”.
596
- </p>
597
- </div>
598
- <Badge variant="outline">Explicit reference</Badge>
599
- </div>
600
- </CardHeader>
601
- <CardContent>
602
- <AIEmployeeShortcut
603
- aiEmployee={employee.username}
604
- target={controller}
605
- tasks={[shortcutTask]}
606
- label="Analyze selected case"
607
- size={34}
608
- />
609
- </CardContent>
610
- </Card>
611
- </div>
612
-
613
- <div className="min-h-0 border-t bg-card xl:border-l xl:border-t-0">
614
- <ChatInline className="h-[560px] rounded-none border-0">
615
- <AIChatWindow showConversationToggle={false} disclaimer={false} />
616
- </ChatInline>
617
- </div>
618
- </div>
619
- </Card>
620
- </AIChatProvider>
621
- );
622
- }
623
-
624
- function PresetTaskContextShowcase() {
625
- const { employees } = useAI();
626
- const employee = employees.filter(isBusinessEmployee)[0]!;
627
- const [opportunityName, setOpportunityName] = useState(
628
- "Enterprise workspace expansion"
629
- );
630
- const [forecast, setForecast] = useState("likely");
631
- const contextReference = useMemo<AIWorkContextItem>(
632
- () => ({
633
- type: "page-element",
634
- id: "selected-opportunity",
635
- title: "Selected opportunity",
636
- }),
637
- []
638
- );
639
- const contextRef = useAIPageElement({
640
- id: "selected-opportunity",
641
- title: "Selected opportunity",
642
- kind: "record-detail",
643
- getContext: () => ({
644
- resource: "opportunities",
645
- record: { name: opportunityName, forecast },
646
- }),
647
- });
648
- const presetTask = useMemo<AIEmployeeTask>(
649
- () => ({
650
- title: "Prepare opportunity brief",
651
- message: {
652
- user: "Prepare a brief for the selected opportunity.",
653
- workContext: [contextReference],
654
- },
655
- autoSend: false,
656
- }),
657
- [contextReference]
658
- );
659
- const employeeTasks = useMemo<AIEmployeeTasks>(
660
- () => ({ [employee.username]: [presetTask] }),
661
- [employee.username, presetTask]
662
- );
663
-
664
- return (
665
- <AIChatProvider
666
- id="preset-task-context-demo"
667
- defaultEmployee={employee.username}
668
- employeeTasks={employeeTasks}
669
- >
670
- <Card className="gap-0 overflow-hidden py-0">
671
- <div className="grid min-h-[560px] xl:grid-cols-[minmax(0,1fr)_410px]">
672
- <div className="bg-muted/15 p-4 sm:p-5">
673
- <Card ref={contextRef}>
674
- <CardHeader>
675
- <div className="flex items-start justify-between gap-3">
676
- <div>
677
- <CardTitle>Selected opportunity</CardTitle>
678
- <p className="mt-1 text-xs text-muted-foreground">
679
- Change a value, then select the preset task in the chat.
680
- </p>
681
- </div>
682
- <Badge variant="secondary">Preset task context</Badge>
683
- </div>
684
- </CardHeader>
685
- <CardContent className="space-y-4">
686
- <label className="block space-y-2">
687
- <Label htmlFor="preset-context-opportunity">
688
- Opportunity
689
- </Label>
690
- <Input
691
- id="preset-context-opportunity"
692
- value={opportunityName}
693
- onChange={(event) => setOpportunityName(event.target.value)}
694
- />
695
- </label>
696
- <label className="block space-y-2">
697
- <Label>Forecast</Label>
698
- <Select
699
- value={forecast}
700
- onValueChange={(value) => value && setForecast(value)}
701
- >
702
- <SelectTrigger className="w-full">
703
- <SelectValue />
704
- </SelectTrigger>
705
- <SelectContent>
706
- <SelectItem value="pipeline">Pipeline</SelectItem>
707
- <SelectItem value="likely">Likely</SelectItem>
708
- <SelectItem value="committed">Committed</SelectItem>
709
- </SelectContent>
710
- </Select>
711
- </label>
712
- <div className="rounded-lg border bg-background p-3 text-xs leading-5 text-muted-foreground">
713
- No Shortcut is used here. “Prepare opportunity brief” comes
714
- from AIChatProvider.employeeTasks and appears in the
715
- conversation empty state.
716
- </div>
717
- </CardContent>
718
- </Card>
719
- </div>
720
-
721
- <div className="min-h-0 border-t bg-card xl:border-l xl:border-t-0">
722
- <ChatInline className="h-[560px] rounded-none border-0">
723
- <AIChatWindow showConversationToggle={false} disclaimer={false} />
724
- </ChatInline>
725
- </div>
726
- </div>
727
- </Card>
728
- </AIChatProvider>
729
- );
730
- }
731
-
732
- function InheritedContextShowcase() {
733
- const { employees } = useAI();
734
- const employee = employees.filter(isBusinessEmployee)[0]!;
735
- const controller = useAIChatController();
736
- const [accountName, setAccountName] = useState("Northwind Finance");
737
- const [stage, setStage] = useState("negotiation");
738
- const contextReference = useMemo<AIWorkContextItem>(
739
- () => ({
740
- type: "page-element",
741
- id: "inherited-renewal-context",
742
- title: "Current account renewal",
743
- }),
744
- []
745
- );
746
- const contextRef = useAIPageElement({
747
- id: "inherited-renewal-context",
748
- title: "Current account renewal",
749
- kind: "record-detail",
750
- getContext: () => ({
751
- resource: "accounts",
752
- record: { accountName, stage },
753
- }),
754
- });
755
- const shortcutTask = useMemo<AIEmployeeTask>(
756
- () => ({
757
- title: "Review current renewal",
758
- message: { user: "Review the current account renewal." },
759
- autoSend: false,
760
- }),
761
- []
762
- );
763
- const presetTask = useMemo<AIEmployeeTask>(
764
- () => ({
765
- title: "Recommend next renewal action",
766
- message: { user: "Recommend the next renewal action." },
767
- autoSend: false,
768
- }),
769
- []
770
- );
771
- const employeeTasks = useMemo<AIEmployeeTasks>(
772
- () => ({ [employee.username]: [presetTask] }),
773
- [employee.username, presetTask]
774
- );
775
-
776
- return (
777
- <AIPageContextScope context={contextReference}>
778
- <AIChatProvider
779
- id="inherited-context-demo"
780
- controller={controller}
781
- defaultEmployee={employee.username}
782
- employeeTasks={employeeTasks}
783
- >
784
- <Card className="gap-0 overflow-hidden py-0">
785
- <div className="grid min-h-[560px] xl:grid-cols-[minmax(0,1fr)_410px]">
786
- <div className="bg-muted/15 p-4 sm:p-5">
787
- <Card ref={contextRef}>
788
- <CardHeader>
789
- <div className="flex items-start justify-between gap-3">
790
- <div>
791
- <CardTitle>Current account renewal</CardTitle>
792
- <p className="mt-1 text-xs text-muted-foreground">
793
- Shortcut and conversation are inside this context scope.
794
- </p>
795
- </div>
796
- <Badge variant="outline">Inherited</Badge>
797
- </div>
798
- </CardHeader>
799
- <CardContent className="space-y-4">
800
- <label className="block space-y-2">
801
- <Label htmlFor="inherited-account-name">Account</Label>
802
- <Input
803
- id="inherited-account-name"
804
- value={accountName}
805
- onChange={(event) => setAccountName(event.target.value)}
806
- />
807
- </label>
808
- <label className="block space-y-2">
809
- <Label>Renewal stage</Label>
810
- <Select
811
- value={stage}
812
- onValueChange={(value) => value && setStage(value)}
813
- >
814
- <SelectTrigger className="w-full">
815
- <SelectValue />
816
- </SelectTrigger>
817
- <SelectContent>
818
- <SelectItem value="discovery">Discovery</SelectItem>
819
- <SelectItem value="negotiation">Negotiation</SelectItem>
820
- <SelectItem value="committed">Committed</SelectItem>
821
- </SelectContent>
822
- </Select>
823
- </label>
824
- <div className="space-y-2 border-t pt-4">
825
- <div className="text-xs text-muted-foreground">
826
- Shortcut task without message.workContext
827
- </div>
828
- <AIEmployeeShortcut
829
- aiEmployee={employee.username}
830
- target={controller}
831
- tasks={[shortcutTask]}
832
- label="Review current renewal"
833
- size={34}
834
- />
835
- </div>
836
- <div className="rounded-lg border bg-background p-3 text-xs leading-5 text-muted-foreground">
837
- The preset task “Recommend next renewal action” also has no
838
- task context, so it inherits this scope.
839
- </div>
840
- </CardContent>
841
- </Card>
842
- </div>
843
-
844
- <div className="min-h-0 border-t bg-card xl:border-l xl:border-t-0">
845
- <ChatInline className="h-[560px] rounded-none border-0">
846
- <AIChatWindow
847
- showConversationToggle={false}
848
- disclaimer={false}
849
- />
850
- </ChatInline>
851
- </div>
852
- </div>
853
- </Card>
854
- </AIChatProvider>
855
- </AIPageContextScope>
856
- );
857
- }
858
-
859
- function ContextSection({
860
- eyebrow,
861
- title,
862
- description,
863
- children,
864
- }: {
865
- eyebrow: string;
866
- title: string;
867
- description: string;
868
- children: ReactNode;
869
- }) {
870
- return (
871
- <section className="space-y-5">
872
- <div>
873
- <p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
874
- {eyebrow}
875
- </p>
876
- <h2 className="mt-2 text-xl font-semibold tracking-tight">{title}</h2>
877
- <p className="mt-1.5 max-w-3xl text-sm leading-6 text-muted-foreground">
878
- {description}
879
- </p>
880
- </div>
881
- {children}
882
- </section>
883
- );
884
- }