@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.
- package/README.MD +90 -1
- package/components.json +3 -1
- package/package.json +21 -5
- package/registry/nocobase-acl/README.md +19 -0
- package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
- package/registry/nocobase-acl/components/index.ts +3 -0
- package/registry/nocobase-acl/components/role-options.ts +36 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
- package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
- package/registry/nocobase-acl/demo/components.tsx +231 -0
- package/registry/nocobase-acl/demo/index.tsx +505 -0
- package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
- package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
- package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
- package/registry/nocobase-acl/extension.tsx +71 -0
- package/registry/nocobase-acl/index.ts +1 -0
- package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
- package/registry/nocobase-ai/README.md +70 -0
- package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
- package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
- package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
- package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
- package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
- package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
- package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
- package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
- package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
- package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
- package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
- package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
- package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
- package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
- package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
- package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
- package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
- package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
- package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
- package/registry/nocobase-ai/components/index.ts +59 -0
- package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
- package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
- package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
- package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
- package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
- package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
- package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
- package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
- package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
- package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
- package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
- package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
- package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
- package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
- package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
- package/registry/nocobase-ai/demo/floating.tsx +134 -0
- package/registry/nocobase-ai/demo/index.tsx +346 -0
- package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
- package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
- package/registry/nocobase-ai/demo/page-context.tsx +884 -0
- package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
- package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
- package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
- package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
- package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
- package/registry/nocobase-ai/extension.tsx +158 -0
- package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
- package/registry/nocobase-ai/index.ts +3 -0
- package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
- package/registry/nocobase-ai/providers/avatars.ts +817 -0
- package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
- package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
- package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
- package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
- package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
- package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
- package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
- package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
- package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
- package/registry/nocobase-ai/providers/index.ts +61 -0
- package/registry/nocobase-ai/providers/model.ts +32 -0
- package/registry/nocobase-ai/providers/page-context.tsx +136 -0
- package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
- package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
- package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
- package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
- package/registry/nocobase-ai/providers/types.ts +183 -0
- package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
- package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
- package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
- package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
- package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
- package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
- package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
- package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
- package/registry/nocobase-ai/services/index.ts +7 -0
- package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
- package/registry/nocobase-ai/services/types.ts +82 -0
- package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
- package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
- package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
- package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
- package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
- package/registry/nocobase-i18n/README.md +53 -0
- package/registry/nocobase-i18n/components/index.ts +1 -0
- package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
- package/registry/nocobase-i18n/demo/index.tsx +209 -0
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
- package/registry/nocobase-i18n/extension.tsx +46 -0
- package/registry/nocobase-i18n/i18n-provider.ts +9 -0
- package/registry/nocobase-i18n/index.ts +6 -0
- package/registry/nocobase-i18n/locale-store.ts +134 -0
- package/registry/nocobase-i18n/locales/en-US.ts +89 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
- package/registry/nocobase-i18n/provider.tsx +32 -0
- package/registry/nocobase-i18n/runtime.ts +118 -0
- package/registry/nocobase-i18n/server-resources.ts +184 -0
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
- package/registry.config.json +163 -0
- package/scripts/registry.mjs +164 -0
- package/src/App.css +95 -0
- package/src/App.tsx +87 -69
- package/src/app/extension.ts +11 -0
- package/src/app/extensions.tsx +49 -0
- package/src/components/access-control/access-denied.tsx +30 -0
- package/src/components/access-control/acl-bootstrap.tsx +9 -0
- package/src/components/access-control/acl-gate.tsx +36 -0
- package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
- package/src/components/access-control/resource-access-guard.tsx +21 -0
- package/src/components/access-control/use-acl-runtime.ts +28 -0
- package/src/components/app-shell/breadcrumb.tsx +39 -3
- package/src/components/app-shell/document-title-handler.tsx +11 -20
- package/src/components/app-shell/header.tsx +22 -4
- package/src/components/app-shell/loading-overlay.tsx +2 -4
- package/src/components/app-shell/loading-state.tsx +15 -0
- package/src/components/app-shell/sidebar.tsx +103 -45
- package/src/components/data-table/data-table-pagination.tsx +27 -7
- package/src/components/data-table/data-table-styles.ts +57 -0
- package/src/components/data-table/data-table.tsx +14 -59
- package/src/components/resources/resource-label.ts +101 -0
- package/src/components/resources/views/create-view.tsx +17 -14
- package/src/components/resources/views/edit-view.tsx +17 -15
- package/src/components/resources/views/list-view.tsx +20 -10
- package/src/components/resources/views/show-view.tsx +20 -18
- package/src/index.tsx +2 -3
- package/src/lib/i18n.ts +161 -0
- package/src/lib/nocobase/acl/action.ts +147 -0
- package/src/lib/nocobase/acl/data-source.ts +24 -0
- package/src/lib/nocobase/acl/index.ts +6 -0
- package/src/lib/nocobase/acl/menu.ts +41 -0
- package/src/lib/nocobase/acl/record-permissions.ts +111 -0
- package/src/lib/nocobase/acl/store.ts +182 -0
- package/src/lib/nocobase/acl/types.ts +96 -0
- package/src/lib/nocobase/client.ts +279 -0
- package/src/lib/nocobase/error.ts +32 -0
- package/src/locales/en-US.ts +82 -0
- package/src/locales/index.ts +13 -0
- package/src/locales/zh-CN.ts +79 -0
- package/src/pages/users/create.tsx +98 -0
- package/src/pages/users/edit.tsx +92 -0
- package/src/pages/users/form-context.ts +57 -0
- package/src/pages/users/form-fields.tsx +211 -0
- package/src/pages/users/list.tsx +205 -0
- package/src/pages/users/show.tsx +187 -0
- package/src/pages/users/types.ts +20 -0
- package/src/providers/access-control.ts +34 -0
- package/src/providers/auth.ts +74 -60
- package/src/providers/constants.ts +3 -37
- package/src/providers/data.ts +242 -85
- package/vite.config.ts +17 -0
- package/src/pages/blog-posts/create.tsx +0 -177
- package/src/pages/blog-posts/edit.tsx +0 -197
- package/src/pages/blog-posts/list.tsx +0 -131
- package/src/pages/blog-posts/show.tsx +0 -81
- package/src/pages/categories/create.tsx +0 -73
- package/src/pages/categories/edit.tsx +0 -73
- package/src/pages/categories/index.ts +0 -4
- package/src/pages/categories/list.tsx +0 -86
- package/src/pages/categories/show.tsx +0 -37
- /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;
|