@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,884 @@
|
|
|
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
|
+
}
|