@nocobase/portal-template-default 1.0.0 → 1.0.2

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 (194) hide show
  1. package/README.MD +90 -1
  2. package/components.json +3 -1
  3. package/package.json +21 -5
  4. package/registry/nocobase-acl/README.md +19 -0
  5. package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
  6. package/registry/nocobase-acl/components/index.ts +3 -0
  7. package/registry/nocobase-acl/components/role-options.ts +36 -0
  8. package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
  9. package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
  10. package/registry/nocobase-acl/demo/components.tsx +231 -0
  11. package/registry/nocobase-acl/demo/index.tsx +505 -0
  12. package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
  13. package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
  14. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  15. package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
  16. package/registry/nocobase-acl/extension.tsx +71 -0
  17. package/registry/nocobase-acl/index.ts +1 -0
  18. package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
  19. package/registry/nocobase-ai/README.md +70 -0
  20. package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
  21. package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  22. package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
  23. package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  24. package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  25. package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  26. package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  27. package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  28. package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
  29. package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  30. package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
  31. package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  32. package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
  33. package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  34. package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  35. package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  36. package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  37. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  38. package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  39. package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  40. package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  41. package/registry/nocobase-ai/components/index.ts +59 -0
  42. package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  43. package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  44. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  45. package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  46. package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  47. package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  48. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  49. package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  50. package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  51. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  52. package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  53. package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  54. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  55. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  56. package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  57. package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  58. package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  59. package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  60. package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  61. package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  62. package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  63. package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  64. package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  65. package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  66. package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  67. package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  68. package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
  69. package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
  70. package/registry/nocobase-ai/demo/floating.tsx +134 -0
  71. package/registry/nocobase-ai/demo/index.tsx +346 -0
  72. package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  73. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  74. package/registry/nocobase-ai/demo/page-context.tsx +884 -0
  75. package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  76. package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
  77. package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
  78. package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
  79. package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
  80. package/registry/nocobase-ai/extension.tsx +158 -0
  81. package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
  82. package/registry/nocobase-ai/index.ts +3 -0
  83. package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
  84. package/registry/nocobase-ai/providers/avatars.ts +817 -0
  85. package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
  86. package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
  87. package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
  88. package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
  89. package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
  90. package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
  91. package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
  92. package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
  93. package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  94. package/registry/nocobase-ai/providers/index.ts +61 -0
  95. package/registry/nocobase-ai/providers/model.ts +32 -0
  96. package/registry/nocobase-ai/providers/page-context.tsx +136 -0
  97. package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
  98. package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
  99. package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
  100. package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  101. package/registry/nocobase-ai/providers/types.ts +183 -0
  102. package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
  103. package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  104. package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  105. package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  106. package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  107. package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  108. package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  109. package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
  110. package/registry/nocobase-ai/services/index.ts +7 -0
  111. package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  112. package/registry/nocobase-ai/services/types.ts +82 -0
  113. package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
  114. package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
  115. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
  116. package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
  117. package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
  118. package/registry/nocobase-i18n/README.md +53 -0
  119. package/registry/nocobase-i18n/components/index.ts +1 -0
  120. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  121. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  122. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  123. package/registry/nocobase-i18n/extension.tsx +46 -0
  124. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  125. package/registry/nocobase-i18n/index.ts +6 -0
  126. package/registry/nocobase-i18n/locale-store.ts +134 -0
  127. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  128. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  129. package/registry/nocobase-i18n/provider.tsx +32 -0
  130. package/registry/nocobase-i18n/runtime.ts +118 -0
  131. package/registry/nocobase-i18n/server-resources.ts +184 -0
  132. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  133. package/registry.config.json +163 -0
  134. package/scripts/registry.mjs +164 -0
  135. package/src/App.css +95 -0
  136. package/src/App.tsx +87 -69
  137. package/src/app/extension.ts +11 -0
  138. package/src/app/extensions.tsx +49 -0
  139. package/src/components/access-control/access-denied.tsx +30 -0
  140. package/src/components/access-control/acl-bootstrap.tsx +9 -0
  141. package/src/components/access-control/acl-gate.tsx +36 -0
  142. package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
  143. package/src/components/access-control/resource-access-guard.tsx +21 -0
  144. package/src/components/access-control/use-acl-runtime.ts +28 -0
  145. package/src/components/app-shell/breadcrumb.tsx +39 -3
  146. package/src/components/app-shell/document-title-handler.tsx +11 -20
  147. package/src/components/app-shell/header.tsx +22 -4
  148. package/src/components/app-shell/loading-overlay.tsx +2 -4
  149. package/src/components/app-shell/loading-state.tsx +15 -0
  150. package/src/components/app-shell/sidebar.tsx +103 -45
  151. package/src/components/data-table/data-table-pagination.tsx +27 -7
  152. package/src/components/data-table/data-table-styles.ts +57 -0
  153. package/src/components/data-table/data-table.tsx +14 -59
  154. package/src/components/resources/resource-label.ts +101 -0
  155. package/src/components/resources/views/create-view.tsx +17 -14
  156. package/src/components/resources/views/edit-view.tsx +17 -15
  157. package/src/components/resources/views/list-view.tsx +20 -10
  158. package/src/components/resources/views/show-view.tsx +20 -18
  159. package/src/index.tsx +2 -3
  160. package/src/lib/i18n.ts +161 -0
  161. package/src/lib/nocobase/acl/action.ts +147 -0
  162. package/src/lib/nocobase/acl/data-source.ts +24 -0
  163. package/src/lib/nocobase/acl/index.ts +6 -0
  164. package/src/lib/nocobase/acl/menu.ts +41 -0
  165. package/src/lib/nocobase/acl/record-permissions.ts +111 -0
  166. package/src/lib/nocobase/acl/store.ts +182 -0
  167. package/src/lib/nocobase/acl/types.ts +96 -0
  168. package/src/lib/nocobase/client.ts +279 -0
  169. package/src/lib/nocobase/error.ts +32 -0
  170. package/src/locales/en-US.ts +82 -0
  171. package/src/locales/index.ts +13 -0
  172. package/src/locales/zh-CN.ts +79 -0
  173. package/src/pages/users/create.tsx +98 -0
  174. package/src/pages/users/edit.tsx +92 -0
  175. package/src/pages/users/form-context.ts +57 -0
  176. package/src/pages/users/form-fields.tsx +211 -0
  177. package/src/pages/users/list.tsx +205 -0
  178. package/src/pages/users/show.tsx +187 -0
  179. package/src/pages/users/types.ts +20 -0
  180. package/src/providers/access-control.ts +34 -0
  181. package/src/providers/auth.ts +74 -60
  182. package/src/providers/constants.ts +3 -37
  183. package/src/providers/data.ts +242 -85
  184. package/vite.config.ts +17 -0
  185. package/src/pages/blog-posts/create.tsx +0 -177
  186. package/src/pages/blog-posts/edit.tsx +0 -197
  187. package/src/pages/blog-posts/list.tsx +0 -131
  188. package/src/pages/blog-posts/show.tsx +0 -81
  189. package/src/pages/categories/create.tsx +0 -73
  190. package/src/pages/categories/edit.tsx +0 -73
  191. package/src/pages/categories/index.ts +0 -4
  192. package/src/pages/categories/list.tsx +0 -86
  193. package/src/pages/categories/show.tsx +0 -37
  194. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,197 @@
1
+ import {
2
+ AIChatWindow,
3
+ ChatInline,
4
+ useAIPageElement,
5
+ useAIPageElementPicker,
6
+ type AIChatComposerAction,
7
+ } from "../components";
8
+ import { Badge } from "@/components/ui/badge";
9
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
10
+ import { Input } from "@/components/ui/input";
11
+ import { Label } from "@/components/ui/label";
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from "@/components/ui/select";
19
+ import { AIChatProvider, useAIChatBase } from "../providers";
20
+ import { Globe2, MousePointer2 } from "lucide-react";
21
+ import { useMemo, useState } from "react";
22
+
23
+ export function PageElementShowcase() {
24
+ return (
25
+ <AIChatProvider id="page-element-demo">
26
+ <PageElementShowcaseContent />
27
+ </AIChatProvider>
28
+ );
29
+ }
30
+
31
+ function PageElementShowcaseContent() {
32
+ const [customerName, setCustomerName] = useState("Northwind Studio");
33
+ const [contactEmail, setContactEmail] = useState("ops@northwind.test");
34
+ const [priority, setPriority] = useState("high");
35
+ const [webSearch, setWebSearch] = useState(false);
36
+ const { id: chatId, addWorkContext, focusComposer } = useAIChatBase();
37
+ const { registeredCount, startPicking } = useAIPageElementPicker();
38
+
39
+ const composerActions = useMemo<AIChatComposerAction[]>(
40
+ () => [
41
+ {
42
+ key: "pick-page-element",
43
+ label: "Pick page element",
44
+ icon: <MousePointer2 />,
45
+ disabled: registeredCount === 0,
46
+ onClick: () =>
47
+ startPicking({
48
+ chatId,
49
+ onSelect: (item) => {
50
+ addWorkContext(item);
51
+ focusComposer();
52
+ },
53
+ }),
54
+ },
55
+ {
56
+ key: "web-search",
57
+ label: "Web search",
58
+ icon: <Globe2 />,
59
+ active: webSearch,
60
+ onClick: () => setWebSearch((active) => !active),
61
+ },
62
+ ],
63
+ [
64
+ addWorkContext,
65
+ chatId,
66
+ focusComposer,
67
+ registeredCount,
68
+ startPicking,
69
+ webSearch,
70
+ ]
71
+ );
72
+
73
+ const formRef = useAIPageElement({
74
+ id: "customer-intake-form",
75
+ title: "Customer intake form",
76
+ kind: "form",
77
+ getContext: () => ({
78
+ form: "customer-intake",
79
+ values: { customerName, contactEmail, priority },
80
+ }),
81
+ });
82
+ const detailRef = useAIPageElement({
83
+ id: "customer-health-summary",
84
+ title: "Customer health summary",
85
+ kind: "record-detail",
86
+ getContext: () => ({
87
+ resource: "customers",
88
+ record: {
89
+ name: customerName,
90
+ plan: "Enterprise",
91
+ healthScore: 86,
92
+ openRequests: 3,
93
+ renewalDate: "2026-09-30",
94
+ },
95
+ }),
96
+ });
97
+
98
+ return (
99
+ <Card className="gap-0 overflow-hidden py-0">
100
+ <div className="grid min-h-[560px] lg:grid-cols-[minmax(0,1fr)_390px]">
101
+ <div className="space-y-4 bg-muted/15 p-4 sm:p-5">
102
+ <div className="flex flex-wrap items-center justify-between gap-3">
103
+ <div>
104
+ <div className="text-sm font-medium">Customer workspace</div>
105
+ <div className="text-xs text-muted-foreground">
106
+ The form and detail card are registered page elements.
107
+ </div>
108
+ </div>
109
+ <Badge variant="outline">2 selectable elements</Badge>
110
+ </div>
111
+
112
+ <Card ref={formRef} className="transition-shadow">
113
+ <CardHeader>
114
+ <CardTitle>Customer intake form</CardTitle>
115
+ <p className="text-xs leading-5 text-muted-foreground">
116
+ Update a value, then pick this form to capture its current
117
+ state.
118
+ </p>
119
+ </CardHeader>
120
+ <CardContent className="grid gap-4 sm:grid-cols-2">
121
+ <div className="space-y-2 sm:col-span-2">
122
+ <Label htmlFor="page-context-customer-name">
123
+ Customer name
124
+ </Label>
125
+ <Input
126
+ id="page-context-customer-name"
127
+ value={customerName}
128
+ onChange={(event) => setCustomerName(event.target.value)}
129
+ />
130
+ </div>
131
+ <div className="space-y-2">
132
+ <Label htmlFor="page-context-contact-email">
133
+ Contact email
134
+ </Label>
135
+ <Input
136
+ id="page-context-contact-email"
137
+ value={contactEmail}
138
+ onChange={(event) => setContactEmail(event.target.value)}
139
+ />
140
+ </div>
141
+ <div className="space-y-2">
142
+ <Label>Priority</Label>
143
+ <Select
144
+ value={priority}
145
+ onValueChange={(value) => value && setPriority(value)}
146
+ >
147
+ <SelectTrigger className="w-full">
148
+ <SelectValue />
149
+ </SelectTrigger>
150
+ <SelectContent>
151
+ <SelectItem value="low">Low</SelectItem>
152
+ <SelectItem value="normal">Normal</SelectItem>
153
+ <SelectItem value="high">High</SelectItem>
154
+ </SelectContent>
155
+ </Select>
156
+ </div>
157
+ </CardContent>
158
+ </Card>
159
+
160
+ <Card ref={detailRef} className="transition-shadow">
161
+ <CardHeader>
162
+ <CardTitle>Customer health summary</CardTitle>
163
+ </CardHeader>
164
+ <CardContent className="grid grid-cols-2 gap-3 sm:grid-cols-4">
165
+ {[
166
+ ["Plan", "Enterprise"],
167
+ ["Health score", "86 / 100"],
168
+ ["Open requests", "3"],
169
+ ["Renewal", "Sep 30, 2026"],
170
+ ].map(([label, value]) => (
171
+ <div
172
+ key={label}
173
+ className="rounded-lg border bg-background p-3"
174
+ >
175
+ <div className="text-xs text-muted-foreground">{label}</div>
176
+ <div className="mt-1 text-sm font-medium">{value}</div>
177
+ </div>
178
+ ))}
179
+ </CardContent>
180
+ </Card>
181
+ </div>
182
+
183
+ <div className="min-h-0 border-t bg-card lg:border-l lg:border-t-0">
184
+ <ChatInline className="h-[560px] rounded-none border-0">
185
+ <AIChatWindow
186
+ composerActions={composerActions}
187
+ showConversationToggle={false}
188
+ enableAttachments
189
+ attachmentActionIndex={1}
190
+ disclaimer={false}
191
+ />
192
+ </ChatInline>
193
+ </div>
194
+ </div>
195
+ </Card>
196
+ );
197
+ }
@@ -0,0 +1,41 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { Card } from "@/components/ui/card";
3
+ import { Check, Copy } from "lucide-react";
4
+ import { useState } from "react";
5
+
6
+ export function PromptCard({
7
+ title,
8
+ description,
9
+ prompt,
10
+ }: {
11
+ title: string;
12
+ description: string;
13
+ prompt: string;
14
+ }) {
15
+ const [copied, setCopied] = useState(false);
16
+
17
+ return (
18
+ <Card className="gap-0 overflow-hidden py-0">
19
+ <div className="flex items-center justify-between gap-4 border-b px-4 py-3">
20
+ <div>
21
+ <div className="text-sm font-medium">{title}</div>
22
+ <div className="text-xs text-muted-foreground">{description}</div>
23
+ </div>
24
+ <Button
25
+ size="sm"
26
+ onClick={async () => {
27
+ await navigator.clipboard.writeText(prompt);
28
+ setCopied(true);
29
+ window.setTimeout(() => setCopied(false), 1500);
30
+ }}
31
+ >
32
+ {copied ? <Check /> : <Copy />}
33
+ {copied ? "Copied" : "Copy prompt"}
34
+ </Button>
35
+ </div>
36
+ <pre className="max-h-[620px] overflow-auto whitespace-pre-wrap bg-muted/25 p-5 font-mono text-xs leading-5 text-muted-foreground">
37
+ {prompt}
38
+ </pre>
39
+ </Card>
40
+ );
41
+ }
@@ -0,0 +1,452 @@
1
+ import {
2
+ AIChatCompact,
3
+ AIChatWindow,
4
+ ChatInline,
5
+ ChatPage,
6
+ useAIPageElementPicker,
7
+ type AIChatComposerAction,
8
+ } from "../components";
9
+ import { Button } from "@/components/ui/button";
10
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
11
+ import { Input } from "@/components/ui/input";
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from "@/components/ui/select";
19
+ import { Switch } from "@/components/ui/switch";
20
+ import { AIChatProvider, useAIChatBase } from "../providers";
21
+ import { Check, Copy, Globe2, MousePointer2 } from "lucide-react";
22
+ import { useMemo, useState } from "react";
23
+ import type { ChatContainer } from "./container-showcase";
24
+
25
+ type PromptCapabilities = {
26
+ conversationList: boolean;
27
+ employeeSelector: boolean;
28
+ modelSelector: boolean;
29
+ userPrompt: boolean;
30
+ pageElement: boolean;
31
+ upload: boolean;
32
+ webSearch: boolean;
33
+ panelExpansion: boolean;
34
+ };
35
+
36
+ type MessagePresentation = "transcript" | "compact-history-dialog";
37
+
38
+ const placements: Array<{
39
+ value: ChatContainer;
40
+ label: string;
41
+ description: string;
42
+ }> = [
43
+ {
44
+ value: "embedded",
45
+ label: "Inside the page",
46
+ description: "Embed chat in the selected content region.",
47
+ },
48
+ {
49
+ value: "page",
50
+ label: "Dedicated page",
51
+ description: "Create a full route for the AI conversation.",
52
+ },
53
+ {
54
+ value: "side-panel",
55
+ label: "Right side panel",
56
+ description: "Push the page narrower while chat is open.",
57
+ },
58
+ {
59
+ value: "dialog",
60
+ label: "Dialog",
61
+ description: "Open chat from a button or page action.",
62
+ },
63
+ {
64
+ value: "mobile",
65
+ label: "Mobile region",
66
+ description: "Optimize the embedded container for a narrow viewport.",
67
+ },
68
+ ];
69
+
70
+ const DEFAULT_CAPABILITIES: PromptCapabilities = {
71
+ conversationList: true,
72
+ employeeSelector: true,
73
+ modelSelector: true,
74
+ userPrompt: true,
75
+ pageElement: true,
76
+ upload: true,
77
+ webSearch: true,
78
+ panelExpansion: true,
79
+ };
80
+
81
+ export function PromptGenerator() {
82
+ return (
83
+ <AIChatProvider id="prompt-generator-preview">
84
+ <PromptGeneratorContent />
85
+ </AIChatProvider>
86
+ );
87
+ }
88
+
89
+ function PromptGeneratorContent() {
90
+ const { id: chatId, addWorkContext, focusComposer } = useAIChatBase();
91
+ const { registeredCount, startPicking } = useAIPageElementPicker();
92
+ const [target, setTarget] = useState("the current page's main content area");
93
+ const [placement, setPlacement] = useState<ChatContainer>("embedded");
94
+ const [panelWidth, setPanelWidth] = useState("450");
95
+ const [messagePresentation, setMessagePresentation] =
96
+ useState<MessagePresentation>("transcript");
97
+ const [capabilities, setCapabilities] = useState(DEFAULT_CAPABILITIES);
98
+ const [copied, setCopied] = useState(false);
99
+
100
+ const previewActions = useMemo<AIChatComposerAction[]>(() => {
101
+ const actions: AIChatComposerAction[] = [];
102
+ if (capabilities.pageElement) {
103
+ actions.push({
104
+ key: "pick-page-element",
105
+ label: "Pick page element",
106
+ icon: <MousePointer2 />,
107
+ disabled: registeredCount === 0,
108
+ onClick: () =>
109
+ startPicking({
110
+ chatId,
111
+ onSelect: (item) => {
112
+ addWorkContext(item);
113
+ focusComposer();
114
+ },
115
+ }),
116
+ });
117
+ }
118
+ if (capabilities.webSearch) {
119
+ actions.push({
120
+ key: "web-search",
121
+ label: "Web search",
122
+ icon: <Globe2 />,
123
+ });
124
+ }
125
+ return actions;
126
+ }, [
127
+ addWorkContext,
128
+ capabilities.pageElement,
129
+ capabilities.webSearch,
130
+ chatId,
131
+ focusComposer,
132
+ registeredCount,
133
+ startPicking,
134
+ ]);
135
+
136
+ const updateCapability = (key: keyof PromptCapabilities, value: boolean) => {
137
+ setCapabilities((current) => ({ ...current, [key]: value }));
138
+ };
139
+
140
+ const prompt = useMemo(() => {
141
+ const placementInstruction = {
142
+ embedded:
143
+ "Render ChatInline inside the target content region and keep its width fluid within the parent container.",
144
+ page: "Create a dedicated route and render ChatPage with AIChatWindow filling the available page content.",
145
+ "side-panel": `Use ChatSidePanelLayout on the right with a ${panelWidth}px panel. Opening chat must push the page narrower instead of covering it on desktop; mobile may use an overlay.`,
146
+ dialog:
147
+ "Open ChatDialog from a page action. Keep conversation state in AIProvider/AIChatProvider so closing the dialog does not discard it.",
148
+ mobile:
149
+ "Render ChatInline in the target region with a mobile-first width around 390px and preserve the same responsive AIChatWindow API.",
150
+ }[placement];
151
+
152
+ return `Add the standard NocoBase AI conversation to ${
153
+ target || "the target page"
154
+ }.
155
+
156
+ Placement:
157
+ - ${placementInstruction}
158
+ - Keep AIChatWindow position-independent. The surface wrapper owns placement and dimensions.
159
+
160
+ Message presentation:
161
+ - ${
162
+ messagePresentation === "transcript"
163
+ ? "Render the full AIChatMessageList inline as part of AIChatWindow."
164
+ : "Use AIChatCompact for the working surface and open AIChatHistoryDialog when the user requests the full transcript."
165
+ }
166
+
167
+ Conversation capabilities:
168
+ - Conversation list: ${capabilities.conversationList}
169
+ - AI employee selector: ${capabilities.employeeSelector}
170
+ - Model selector: ${capabilities.modelSelector}
171
+ - Personalized prompt editor: ${capabilities.userPrompt}
172
+ - Pick page element: ${capabilities.pageElement}
173
+ - Upload action: ${capabilities.upload}
174
+ - Web search action: ${capabilities.webSearch}
175
+ - Side panel can expand to a 95% dialog and collapse back: ${
176
+ capabilities.panelExpansion
177
+ }
178
+ - Tool approval follows NocoBase permissions. Always show Allow / Deny when the backend marks a tool as ASK.
179
+
180
+ Implementation requirements:
181
+ - Use AIProvider at application level and AIChatProvider with a stable id for this chat instance.
182
+ - Wrap the application content with AIPageElementProvider when page-element picking is enabled.
183
+ - Register selectable React components with useAIPageElement and return serializable business context from getContext.
184
+ - Reuse AIChatWindow and the existing providers; do not duplicate message, streaming, reasoning, or tool-call state.
185
+ - When the right panel is expandable, keep one AIChatProvider mounted while switching between ChatSidePanel and ChatDialog. Put ChatSurfaceActions in the window header.
186
+ - Keep the generic Tool Call shell responsible for status, ASK approval, errors, and disclosure. Register business-specific bodies through AIToolRendererProvider.
187
+ - Include the built-in specialized Tool Card renderers. This is part of the standard AI chat capability, not an optional page setting.
188
+ - Register browser-side tool implementations through AIProvider.toolInvokers. After all interrupted calls are approved, the provider executes these handlers and sends their results when resuming the NocoBase conversation.
189
+ - Pass application actions through composerActions.
190
+ - AIChatProvider handles NocoBase ASK decisions and resumes tool execution. Use onToolCallDecision only for application-specific side effects or telemetry.
191
+ - Use shadcn/Base UI components only; do not introduce Ant Design, Radix, or Zustand.
192
+ - Match the existing NocoBase ChatBox behavior and black/white/gray visual system.`;
193
+ }, [capabilities, messagePresentation, panelWidth, placement, target]);
194
+
195
+ const previewChat =
196
+ messagePresentation === "transcript" ? (
197
+ <AIChatWindow
198
+ composerActions={previewActions}
199
+ showConversationToggle={capabilities.conversationList}
200
+ showEmployeeSelector={capabilities.employeeSelector}
201
+ showModelSelector={capabilities.modelSelector}
202
+ showUserPrompt={capabilities.userPrompt}
203
+ enableAttachments={capabilities.upload}
204
+ attachmentActionIndex={capabilities.pageElement ? 1 : 0}
205
+ disclaimer={false}
206
+ onToolCallDecision={async () => undefined}
207
+ />
208
+ ) : (
209
+ <AIChatCompact
210
+ className="h-full rounded-none border-0"
211
+ composerActions={previewActions}
212
+ showEmployeeSelector={capabilities.employeeSelector}
213
+ showModelSelector={capabilities.modelSelector}
214
+ showUserPrompt={capabilities.userPrompt}
215
+ enableAttachments={capabilities.upload}
216
+ disclaimer={false}
217
+ onToolCallDecision={async () => undefined}
218
+ />
219
+ );
220
+
221
+ const previewSurface = (() => {
222
+ if (placement === "page") {
223
+ return (
224
+ <ChatPage className="h-[430px] min-h-0 w-full max-w-none">
225
+ {previewChat}
226
+ </ChatPage>
227
+ );
228
+ }
229
+ if (placement === "side-panel") {
230
+ return (
231
+ <div className="flex h-[430px] w-full overflow-hidden rounded-xl border bg-card shadow-sm">
232
+ <div className="hidden min-w-0 flex-1 space-y-4 bg-muted/20 p-5 sm:block">
233
+ <div className="h-7 w-40 rounded-md bg-muted" />
234
+ <div className="grid grid-cols-2 gap-3">
235
+ <div className="h-24 rounded-lg border bg-background" />
236
+ <div className="h-24 rounded-lg border bg-background" />
237
+ </div>
238
+ <div className="h-48 rounded-lg border bg-background" />
239
+ </div>
240
+ <aside
241
+ className="h-full max-w-full shrink-0 border-l bg-card"
242
+ style={{ width: `${panelWidth}px` }}
243
+ >
244
+ {previewChat}
245
+ </aside>
246
+ </div>
247
+ );
248
+ }
249
+ if (placement === "dialog") {
250
+ return (
251
+ <div className="relative h-[430px] w-full overflow-hidden rounded-xl border bg-muted/30 p-5">
252
+ <div className="space-y-3 opacity-45">
253
+ <div className="h-7 w-44 rounded bg-muted-foreground/20" />
254
+ <div className="h-24 rounded-lg bg-background" />
255
+ <div className="h-48 rounded-lg bg-background" />
256
+ </div>
257
+ <div className="absolute inset-4 overflow-hidden rounded-xl border bg-card shadow-2xl sm:inset-8">
258
+ {previewChat}
259
+ </div>
260
+ </div>
261
+ );
262
+ }
263
+ if (placement === "mobile") {
264
+ return (
265
+ <div className="h-[430px] w-full max-w-[390px] overflow-hidden rounded-[2rem] border-4 border-foreground/15 bg-card shadow-lg">
266
+ {previewChat}
267
+ </div>
268
+ );
269
+ }
270
+ return (
271
+ <ChatInline className="h-[430px] w-full max-w-2xl">
272
+ {previewChat}
273
+ </ChatInline>
274
+ );
275
+ })();
276
+
277
+ return (
278
+ <div className="grid items-start gap-5 xl:grid-cols-[380px_minmax(0,1fr)]">
279
+ <Card className="gap-0 py-0">
280
+ <CardHeader className="border-b py-4">
281
+ <CardTitle className="text-base">Describe the integration</CardTitle>
282
+ <p className="text-xs leading-5 text-muted-foreground">
283
+ Choose where chat belongs and which capabilities the target page
284
+ needs.
285
+ </p>
286
+ </CardHeader>
287
+ <CardContent className="space-y-5 p-4">
288
+ <div className="space-y-2">
289
+ <label className="text-xs font-medium" htmlFor="ai-chat-target">
290
+ Target page or region
291
+ </label>
292
+ <Input
293
+ id="ai-chat-target"
294
+ value={target}
295
+ onChange={(event) => setTarget(event.target.value)}
296
+ placeholder="e.g. the ticket detail page"
297
+ />
298
+ </div>
299
+
300
+ <div className="space-y-2">
301
+ <div className="text-xs font-medium">Placement</div>
302
+ <div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-1">
303
+ {placements.map((option) => (
304
+ <button
305
+ key={option.value}
306
+ type="button"
307
+ data-active={placement === option.value}
308
+ className="rounded-lg border px-3 py-2.5 text-left transition-colors hover:bg-muted/40 data-active:border-foreground/35 data-active:bg-muted/60"
309
+ onClick={() => setPlacement(option.value)}
310
+ >
311
+ <span className="block text-sm font-medium">
312
+ {option.label}
313
+ </span>
314
+ <span className="mt-0.5 block text-xs leading-5 text-muted-foreground">
315
+ {option.description}
316
+ </span>
317
+ </button>
318
+ ))}
319
+ </div>
320
+ </div>
321
+
322
+ {placement === "side-panel" ? (
323
+ <div className="space-y-2">
324
+ <div className="text-xs font-medium">Side panel width</div>
325
+ <Select
326
+ value={panelWidth}
327
+ onValueChange={(value) => value && setPanelWidth(value)}
328
+ >
329
+ <SelectTrigger className="w-full">
330
+ <SelectValue>{panelWidth}px</SelectValue>
331
+ </SelectTrigger>
332
+ <SelectContent>
333
+ <SelectItem value="400">400px</SelectItem>
334
+ <SelectItem value="450">450px · NocoBase default</SelectItem>
335
+ <SelectItem value="520">520px</SelectItem>
336
+ </SelectContent>
337
+ </Select>
338
+ </div>
339
+ ) : null}
340
+
341
+ <div className="space-y-2">
342
+ <div className="text-xs font-medium">Message presentation</div>
343
+ <Select
344
+ value={messagePresentation}
345
+ onValueChange={(value) =>
346
+ value && setMessagePresentation(value as MessagePresentation)
347
+ }
348
+ >
349
+ <SelectTrigger className="w-full">
350
+ <SelectValue>
351
+ {messagePresentation === "transcript"
352
+ ? "Full transcript"
353
+ : "Compact + history dialog"}
354
+ </SelectValue>
355
+ </SelectTrigger>
356
+ <SelectContent>
357
+ <SelectItem value="transcript">Full transcript</SelectItem>
358
+ <SelectItem value="compact-history-dialog">
359
+ Compact + history dialog
360
+ </SelectItem>
361
+ </SelectContent>
362
+ </Select>
363
+ </div>
364
+
365
+ <div className="space-y-1 border-t pt-4">
366
+ <div className="mb-2 text-xs font-medium">Capabilities</div>
367
+ {(Object.keys(capabilities) as Array<keyof PromptCapabilities>).map(
368
+ (key) => (
369
+ <label
370
+ key={key}
371
+ className="flex items-center justify-between gap-4 py-1.5 text-sm"
372
+ >
373
+ <span className="min-w-0">
374
+ <span className="block">{capabilityLabel(key)}</span>
375
+ </span>
376
+ <Switch
377
+ size="sm"
378
+ checked={capabilities[key]}
379
+ onCheckedChange={(checked) =>
380
+ updateCapability(key, checked)
381
+ }
382
+ />
383
+ </label>
384
+ )
385
+ )}
386
+ </div>
387
+ </CardContent>
388
+ </Card>
389
+
390
+ <div className="space-y-5">
391
+ <Card className="gap-0 overflow-hidden py-0">
392
+ <div className="flex items-center justify-between border-b px-4 py-3">
393
+ <div>
394
+ <div className="text-sm font-medium">Capabilities preview</div>
395
+ <div className="text-xs text-muted-foreground">
396
+ The selected controls are rendered on the real AIChatWindow.
397
+ </div>
398
+ </div>
399
+ <code className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
400
+ {placementLabel(placement)}
401
+ </code>
402
+ </div>
403
+ <div className="flex justify-center bg-muted/20 p-3 sm:p-4">
404
+ {previewSurface}
405
+ </div>
406
+ </Card>
407
+
408
+ <Card className="gap-0 overflow-hidden py-0">
409
+ <div className="flex items-center justify-between border-b px-4 py-3">
410
+ <div>
411
+ <div className="text-sm font-medium">
412
+ Generated implementation prompt
413
+ </div>
414
+ <div className="text-xs text-muted-foreground">
415
+ Updates from the selected page, placement, and capabilities.
416
+ </div>
417
+ </div>
418
+ <Button
419
+ size="sm"
420
+ onClick={async () => {
421
+ await navigator.clipboard.writeText(prompt);
422
+ setCopied(true);
423
+ window.setTimeout(() => setCopied(false), 1500);
424
+ }}
425
+ >
426
+ {copied ? <Check /> : <Copy />}
427
+ {copied ? "Copied" : "Copy prompt"}
428
+ </Button>
429
+ </div>
430
+ <pre className="max-h-[760px] min-h-[520px] overflow-auto whitespace-pre-wrap bg-muted/25 p-5 font-mono text-xs leading-5 text-muted-foreground">
431
+ {prompt}
432
+ </pre>
433
+ </Card>
434
+ </div>
435
+ </div>
436
+ );
437
+ }
438
+
439
+ const capabilityLabel = (key: keyof PromptCapabilities) =>
440
+ ({
441
+ conversationList: "Conversation list",
442
+ employeeSelector: "AI employee selector",
443
+ modelSelector: "Model selector",
444
+ userPrompt: "Personalized prompt editor",
445
+ pageElement: "Pick page element",
446
+ upload: "Upload files",
447
+ webSearch: "Web search",
448
+ panelExpansion: "Panel expand / collapse",
449
+ }[key]);
450
+
451
+ const placementLabel = (placement: ChatContainer) =>
452
+ placements.find((option) => option.value === placement)?.label ?? placement;