@nocobase/portal-template-default 1.0.1 → 1.0.3
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 +22 -3
- package/package.json +4 -2
- package/registry/nocobase-acl/components/role-options.ts +7 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
- package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
- package/registry/nocobase-acl/demo/components.tsx +32 -11
- package/registry/nocobase-acl/demo/index.tsx +45 -14
- package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
- package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
- package/registry/nocobase-acl/extension.tsx +1 -1
- package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
- package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
- package/registry/nocobase-ai/extension.tsx +2 -7
- 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 +34 -14
- package/scripts/registry.mjs +5 -0
- package/src/App.tsx +27 -86
- package/src/app/extension.ts +3 -1
- package/src/app/extensions.tsx +21 -0
- package/src/components/access-control/acl-gate.tsx +4 -10
- 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 +18 -3
- 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 +75 -47
- 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/extensions/nocobase-acl/README.md +19 -0
- package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
- package/src/extensions/nocobase-acl/components/index.ts +3 -0
- package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
- package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
- package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
- package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
- package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
- package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
- package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
- package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
- package/src/extensions/nocobase-acl/extension.tsx +71 -0
- package/src/extensions/nocobase-acl/index.ts +1 -0
- package/src/extensions/nocobase-ai/README.md +70 -0
- package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
- package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
- package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
- package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
- package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
- package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
- package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
- package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
- package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
- package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
- package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
- package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
- package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
- package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
- package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
- package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
- package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
- package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
- package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
- package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
- package/src/extensions/nocobase-ai/components/index.ts +59 -0
- package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
- package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
- package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
- package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
- package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
- package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
- package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
- package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
- package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
- package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
- package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
- package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
- package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
- package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
- package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
- package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
- package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
- package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
- package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
- package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
- package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
- package/src/extensions/nocobase-ai/extension.tsx +158 -0
- package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
- package/src/extensions/nocobase-ai/index.ts +3 -0
- package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
- package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
- package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
- package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
- package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
- package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
- package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
- package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
- package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
- package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
- package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
- package/src/extensions/nocobase-ai/providers/index.ts +61 -0
- package/src/extensions/nocobase-ai/providers/model.ts +32 -0
- package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
- package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
- package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
- package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
- package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
- package/src/extensions/nocobase-ai/providers/types.ts +183 -0
- package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
- package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
- package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
- package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
- package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
- package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
- package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
- package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
- package/src/extensions/nocobase-ai/services/index.ts +7 -0
- package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
- package/src/extensions/nocobase-ai/services/types.ts +82 -0
- package/src/index.tsx +2 -3
- package/src/lib/i18n.ts +161 -0
- package/src/lib/nocobase/client.ts +19 -2
- 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/constants.ts +1 -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,1207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AIChatWindow,
|
|
3
|
+
AIEmployeeShortcut,
|
|
4
|
+
AIModelSelectOptions,
|
|
5
|
+
ChatInline,
|
|
6
|
+
useAIPageElement,
|
|
7
|
+
useAIPageElementPicker,
|
|
8
|
+
} from "../components";
|
|
9
|
+
import {
|
|
10
|
+
Accordion,
|
|
11
|
+
AccordionContent,
|
|
12
|
+
AccordionItem,
|
|
13
|
+
AccordionTrigger,
|
|
14
|
+
} from "@/components/ui/accordion";
|
|
15
|
+
import { Badge } from "@/components/ui/badge";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
18
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
19
|
+
import { Input } from "@/components/ui/input";
|
|
20
|
+
import {
|
|
21
|
+
Select,
|
|
22
|
+
SelectContent,
|
|
23
|
+
SelectItem,
|
|
24
|
+
SelectSeparator,
|
|
25
|
+
SelectTrigger,
|
|
26
|
+
SelectValue,
|
|
27
|
+
} from "@/components/ui/select";
|
|
28
|
+
import { Switch } from "@/components/ui/switch";
|
|
29
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
30
|
+
import {
|
|
31
|
+
AIChatProvider,
|
|
32
|
+
AIPageContextScope,
|
|
33
|
+
findAIModel,
|
|
34
|
+
useAI,
|
|
35
|
+
useAIChatController,
|
|
36
|
+
type AIChatController,
|
|
37
|
+
type AIEmployeeTask,
|
|
38
|
+
type AIEmployeeTasks,
|
|
39
|
+
type AIWorkContextItem,
|
|
40
|
+
} from "../providers";
|
|
41
|
+
import { MousePointer2, Plus, Trash2, X } from "lucide-react";
|
|
42
|
+
import { useMemo, useState } from "react";
|
|
43
|
+
import { PromptCard } from "./prompt-card";
|
|
44
|
+
import { AIConfigurationGate } from "./configuration-gate";
|
|
45
|
+
|
|
46
|
+
const analyzeTicketTask: AIEmployeeTask = {
|
|
47
|
+
title: "Analyze this ticket",
|
|
48
|
+
message: {
|
|
49
|
+
system:
|
|
50
|
+
"Review the current ticket, identify operational risk, and suggest the next action.",
|
|
51
|
+
user: "Analyze the current ticket and recommend the next action.",
|
|
52
|
+
},
|
|
53
|
+
autoSend: true,
|
|
54
|
+
model: { llmService: "openai", model: "gpt-5" },
|
|
55
|
+
webSearch: true,
|
|
56
|
+
skillSettings: {
|
|
57
|
+
skills: ["ticket-analysis"],
|
|
58
|
+
tools: ["inspect-record"],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const draftReplyTask: AIEmployeeTask = {
|
|
63
|
+
title: "Draft a customer reply",
|
|
64
|
+
message: {
|
|
65
|
+
user: "Draft a concise customer reply for the current ticket.",
|
|
66
|
+
},
|
|
67
|
+
autoSend: false,
|
|
68
|
+
skillSettings: {
|
|
69
|
+
skills: ["response-drafting"],
|
|
70
|
+
tools: ["inspect-record"],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const reviewOperationalRiskTask: AIEmployeeTask = {
|
|
75
|
+
title: "Review operational risk",
|
|
76
|
+
message: {
|
|
77
|
+
system:
|
|
78
|
+
"Review the current queue, identify operational risk, and prioritize the next actions.",
|
|
79
|
+
user: "Review the current ticket queue and identify operational risk.",
|
|
80
|
+
},
|
|
81
|
+
autoSend: false,
|
|
82
|
+
skillSettings: {
|
|
83
|
+
skills: ["ticket-analysis"],
|
|
84
|
+
tools: ["inspect-record"],
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const ticketDetail = {
|
|
89
|
+
id: "TK-1042",
|
|
90
|
+
title: "Payment callback delayed",
|
|
91
|
+
status: "Open",
|
|
92
|
+
requester: "Northwind Finance",
|
|
93
|
+
createdAt: "July 22, 2026 · 09:42",
|
|
94
|
+
description:
|
|
95
|
+
"Payment succeeded, but the callback reached the order service twelve minutes late. The customer needs an impact assessment and a response before the next settlement window.",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const tickets = [
|
|
99
|
+
ticketDetail,
|
|
100
|
+
{ id: "TK-1041", title: "Unable to update profile" },
|
|
101
|
+
{ id: "TK-1038", title: "Invoice export formatting" },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const isBusinessEmployee = (employee: { username: string }) =>
|
|
105
|
+
!["nathan", "dara"].includes(employee.username.toLowerCase());
|
|
106
|
+
|
|
107
|
+
export function ShortcutPage() {
|
|
108
|
+
return (
|
|
109
|
+
<AIConfigurationGate>
|
|
110
|
+
<ShortcutPageContent />
|
|
111
|
+
</AIConfigurationGate>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ShortcutPageContent() {
|
|
116
|
+
const { employees } = useAI();
|
|
117
|
+
const embeddedController = useAIChatController();
|
|
118
|
+
const businessEmployees = employees.filter(isBusinessEmployee);
|
|
119
|
+
const primaryEmployee =
|
|
120
|
+
businessEmployees.find(
|
|
121
|
+
(employee) => employee.username.toLowerCase() === "atlas"
|
|
122
|
+
) ?? businessEmployees[0]!;
|
|
123
|
+
const secondaryEmployee =
|
|
124
|
+
businessEmployees.find(
|
|
125
|
+
(employee) => employee.username !== primaryEmployee.username
|
|
126
|
+
) ?? primaryEmployee;
|
|
127
|
+
const embeddedEmployee =
|
|
128
|
+
businessEmployees.find(
|
|
129
|
+
(employee) => employee.username.toLowerCase() === "atlas"
|
|
130
|
+
) ??
|
|
131
|
+
businessEmployees.find(
|
|
132
|
+
(employee) => employee.username !== primaryEmployee.username
|
|
133
|
+
) ??
|
|
134
|
+
primaryEmployee;
|
|
135
|
+
const ticketDetailRef = useAIPageElement({
|
|
136
|
+
id: "support-ticket-detail",
|
|
137
|
+
title: `${ticketDetail.id} · ${ticketDetail.title}`,
|
|
138
|
+
kind: "record-detail",
|
|
139
|
+
getContext: () => ({
|
|
140
|
+
resource: "supportTickets",
|
|
141
|
+
record: ticketDetail,
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
const chatEmployeeTasks = useMemo<AIEmployeeTasks>(
|
|
145
|
+
() => ({
|
|
146
|
+
[primaryEmployee.username]: [analyzeTicketTask, draftReplyTask],
|
|
147
|
+
...(secondaryEmployee.username !== primaryEmployee.username
|
|
148
|
+
? {
|
|
149
|
+
[secondaryEmployee.username]: [reviewOperationalRiskTask],
|
|
150
|
+
}
|
|
151
|
+
: {}),
|
|
152
|
+
}),
|
|
153
|
+
[primaryEmployee.username, secondaryEmployee.username]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="space-y-12 pb-12">
|
|
158
|
+
<section className="flex flex-wrap items-start justify-between gap-5 border-b pb-8">
|
|
159
|
+
<div>
|
|
160
|
+
<div className="flex items-center gap-2">
|
|
161
|
+
<Badge variant="secondary">AI Components</Badge>
|
|
162
|
+
<Badge variant="outline">Employee capability</Badge>
|
|
163
|
+
</div>
|
|
164
|
+
<h1 className="mt-4 text-3xl font-semibold tracking-[-0.035em]">
|
|
165
|
+
Employee tasks
|
|
166
|
+
</h1>
|
|
167
|
+
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
168
|
+
Bind reusable tasks to AI employees. Tasks can appear directly
|
|
169
|
+
inside a chat when the employee is selected, or be triggered from
|
|
170
|
+
contextual buttons elsewhere in the application.
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
|
|
175
|
+
<section className="space-y-5">
|
|
176
|
+
<div>
|
|
177
|
+
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
178
|
+
Scenario 1 · Multiple tasks
|
|
179
|
+
</p>
|
|
180
|
+
<h2 className="mt-2 text-xl font-semibold tracking-tight">
|
|
181
|
+
Trigger employee tasks from a business record
|
|
182
|
+
</h2>
|
|
183
|
+
<p className="mt-1.5 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
184
|
+
The Shortcut component can still sit in a detail header and provide
|
|
185
|
+
the current record as work context. Opening it shows the configured
|
|
186
|
+
analysis and reply tasks below the employee greeting.
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
<AIPageContextScope
|
|
190
|
+
context={{
|
|
191
|
+
type: "page-element",
|
|
192
|
+
id: "support-ticket-detail",
|
|
193
|
+
title: `${ticketDetail.id} · ${ticketDetail.title}`,
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<Card ref={ticketDetailRef} className="gap-0 overflow-hidden py-0">
|
|
197
|
+
<div className="flex flex-wrap items-start justify-between gap-4 border-b p-5">
|
|
198
|
+
<div className="min-w-0">
|
|
199
|
+
<div className="flex items-center gap-2">
|
|
200
|
+
<span className="font-mono text-xs text-muted-foreground">
|
|
201
|
+
{ticketDetail.id}
|
|
202
|
+
</span>
|
|
203
|
+
<Badge variant="outline">{ticketDetail.status}</Badge>
|
|
204
|
+
</div>
|
|
205
|
+
<h3 className="mt-2 text-lg font-semibold tracking-tight">
|
|
206
|
+
{ticketDetail.title}
|
|
207
|
+
</h3>
|
|
208
|
+
</div>
|
|
209
|
+
<AIEmployeeShortcut
|
|
210
|
+
aiEmployee={primaryEmployee.username}
|
|
211
|
+
tasks={[analyzeTicketTask, draftReplyTask]}
|
|
212
|
+
label={`Ask ${primaryEmployee.nickname}`}
|
|
213
|
+
size={34}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="grid gap-5 p-5 sm:grid-cols-2">
|
|
217
|
+
<div>
|
|
218
|
+
<div className="text-xs text-muted-foreground">Requester</div>
|
|
219
|
+
<div className="mt-1 text-sm font-medium">
|
|
220
|
+
{ticketDetail.requester}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<div className="text-xs text-muted-foreground">Created</div>
|
|
225
|
+
<div className="mt-1 text-sm font-medium">
|
|
226
|
+
{ticketDetail.createdAt}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="border-t p-5">
|
|
231
|
+
<div className="text-xs text-muted-foreground">Description</div>
|
|
232
|
+
<p className="mt-2 max-w-3xl text-sm leading-6">
|
|
233
|
+
{ticketDetail.description}
|
|
234
|
+
</p>
|
|
235
|
+
</div>
|
|
236
|
+
</Card>
|
|
237
|
+
</AIPageContextScope>
|
|
238
|
+
</section>
|
|
239
|
+
|
|
240
|
+
<section className="space-y-5">
|
|
241
|
+
<div>
|
|
242
|
+
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
243
|
+
Scenario 2 · Explicit target
|
|
244
|
+
</p>
|
|
245
|
+
<h2 className="mt-2 text-xl font-semibold tracking-tight">
|
|
246
|
+
Send a shortcut task to a designated embedded chat
|
|
247
|
+
</h2>
|
|
248
|
+
<p className="mt-1.5 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
249
|
+
The shortcut receives the embedded chat Controller directly. Its
|
|
250
|
+
Provider explicitly configures {embeddedEmployee.nickname} and an
|
|
251
|
+
employee task set. It does not need a global target ID and cannot
|
|
252
|
+
accidentally trigger another conversation on the same page.
|
|
253
|
+
</p>
|
|
254
|
+
</div>
|
|
255
|
+
<AIChatProvider
|
|
256
|
+
id="shortcut-embedded-chat"
|
|
257
|
+
controller={embeddedController}
|
|
258
|
+
defaultEmployee={embeddedEmployee.username}
|
|
259
|
+
>
|
|
260
|
+
<div className="grid items-stretch gap-5 xl:grid-cols-[320px_minmax(0,1fr)]">
|
|
261
|
+
<Card>
|
|
262
|
+
<CardHeader>
|
|
263
|
+
<CardTitle className="text-base">
|
|
264
|
+
Risk review workspace
|
|
265
|
+
</CardTitle>
|
|
266
|
+
</CardHeader>
|
|
267
|
+
<CardContent className="space-y-5">
|
|
268
|
+
<p className="text-sm leading-6 text-muted-foreground">
|
|
269
|
+
The embedded chat starts without preset tasks. Clicking the
|
|
270
|
+
Shortcut injects “Review operational risk” into this specific
|
|
271
|
+
conversation, where the user can choose it before the request
|
|
272
|
+
is placed in the composer.
|
|
273
|
+
</p>
|
|
274
|
+
<AIEmployeeShortcut
|
|
275
|
+
aiEmployee={embeddedEmployee.username}
|
|
276
|
+
target={embeddedController}
|
|
277
|
+
tasks={[reviewOperationalRiskTask]}
|
|
278
|
+
auto={false}
|
|
279
|
+
context={[
|
|
280
|
+
{
|
|
281
|
+
type: "table",
|
|
282
|
+
title: "Open ticket queue",
|
|
283
|
+
content: tickets,
|
|
284
|
+
},
|
|
285
|
+
]}
|
|
286
|
+
label={`Ask ${embeddedEmployee.nickname}`}
|
|
287
|
+
size={36}
|
|
288
|
+
/>
|
|
289
|
+
</CardContent>
|
|
290
|
+
</Card>
|
|
291
|
+
<ChatInline className="h-[560px] min-h-0">
|
|
292
|
+
<AIChatWindow />
|
|
293
|
+
</ChatInline>
|
|
294
|
+
</div>
|
|
295
|
+
</AIChatProvider>
|
|
296
|
+
</section>
|
|
297
|
+
|
|
298
|
+
<section className="space-y-5">
|
|
299
|
+
<div>
|
|
300
|
+
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
301
|
+
Scenario 3 · Chat-bound tasks
|
|
302
|
+
</p>
|
|
303
|
+
<h2 className="mt-2 text-xl font-semibold tracking-tight">
|
|
304
|
+
Show tasks when the selected employee changes
|
|
305
|
+
</h2>
|
|
306
|
+
<p className="mt-1.5 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
307
|
+
This conversation window binds task lists directly to business AI
|
|
308
|
+
employees. Starting a new conversation or switching employees in the
|
|
309
|
+
composer immediately replaces the empty-state tasks. Employees
|
|
310
|
+
without a binding keep the normal greeting-only state.
|
|
311
|
+
</p>
|
|
312
|
+
</div>
|
|
313
|
+
<div className="grid items-stretch gap-5 xl:grid-cols-[320px_minmax(0,1fr)]">
|
|
314
|
+
<Card>
|
|
315
|
+
<CardHeader>
|
|
316
|
+
<CardTitle className="text-base">
|
|
317
|
+
Employee task bindings
|
|
318
|
+
</CardTitle>
|
|
319
|
+
</CardHeader>
|
|
320
|
+
<CardContent className="space-y-4 text-sm text-muted-foreground">
|
|
321
|
+
<div className="rounded-lg border bg-muted/25 p-3 font-mono text-xs leading-5">
|
|
322
|
+
<div>{primaryEmployee.username}</div>
|
|
323
|
+
<div className="pl-4">Analyze this ticket</div>
|
|
324
|
+
<div className="pl-4">Draft a customer reply</div>
|
|
325
|
+
{secondaryEmployee.username !== primaryEmployee.username ? (
|
|
326
|
+
<>
|
|
327
|
+
<div className="mt-2">{secondaryEmployee.username}</div>
|
|
328
|
+
<div className="pl-4">Review operational risk</div>
|
|
329
|
+
</>
|
|
330
|
+
) : null}
|
|
331
|
+
</div>
|
|
332
|
+
<p>
|
|
333
|
+
Switch employees from the bottom of the chat to preview each
|
|
334
|
+
configured task list.
|
|
335
|
+
</p>
|
|
336
|
+
<p>
|
|
337
|
+
The same <code>employeeTasks</code> parameter works in page,
|
|
338
|
+
embedded, side-panel, dialog, and mobile containers.
|
|
339
|
+
</p>
|
|
340
|
+
</CardContent>
|
|
341
|
+
</Card>
|
|
342
|
+
<AIChatProvider
|
|
343
|
+
id="employee-task-scenario-chat"
|
|
344
|
+
defaultEmployee={primaryEmployee.username}
|
|
345
|
+
employeeTasks={chatEmployeeTasks}
|
|
346
|
+
>
|
|
347
|
+
<ChatInline className="h-[620px] min-h-0">
|
|
348
|
+
<AIChatWindow />
|
|
349
|
+
</ChatInline>
|
|
350
|
+
</AIChatProvider>
|
|
351
|
+
</div>
|
|
352
|
+
</section>
|
|
353
|
+
|
|
354
|
+
<ShortcutPromptGenerator embeddedController={embeddedController} />
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
type CapabilityMode = "preset" | "custom";
|
|
360
|
+
type IntegrationMode = "shortcut" | "chat-tasks";
|
|
361
|
+
type ChatContainer = "page" | "embedded" | "side-panel" | "dialog";
|
|
362
|
+
|
|
363
|
+
type ConfigTask = {
|
|
364
|
+
id: string;
|
|
365
|
+
employee: string;
|
|
366
|
+
title: string;
|
|
367
|
+
background: string;
|
|
368
|
+
userMessage: string;
|
|
369
|
+
autoSend: boolean;
|
|
370
|
+
workContext: AIWorkContextItem[];
|
|
371
|
+
model: string;
|
|
372
|
+
webSearch: boolean;
|
|
373
|
+
skillsMode: CapabilityMode;
|
|
374
|
+
skills: string[];
|
|
375
|
+
toolsMode: CapabilityMode;
|
|
376
|
+
tools: string[];
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const availableSkills = [
|
|
380
|
+
{ value: "ticket-analysis", label: "Ticket analysis" },
|
|
381
|
+
{ value: "response-drafting", label: "Response drafting" },
|
|
382
|
+
{ value: "workflow-design", label: "Workflow design" },
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
const availableTools = [
|
|
386
|
+
{ value: "inspect-record", label: "Inspect record" },
|
|
387
|
+
{ value: "search-records", label: "Search records" },
|
|
388
|
+
{ value: "update-record", label: "Update record" },
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
const initialConfigTasks: ConfigTask[] = [
|
|
392
|
+
{
|
|
393
|
+
id: "analyze-ticket",
|
|
394
|
+
employee: "",
|
|
395
|
+
title: "Analyze this ticket",
|
|
396
|
+
background: "Identify operational risk and recommend the next action.",
|
|
397
|
+
userMessage: "Analyze the current ticket and recommend the next action.",
|
|
398
|
+
autoSend: true,
|
|
399
|
+
workContext: [
|
|
400
|
+
{
|
|
401
|
+
type: "page-element",
|
|
402
|
+
id: "support-ticket-detail",
|
|
403
|
+
title: `${ticketDetail.id} · ${ticketDetail.title}`,
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
model: "gpt-5",
|
|
407
|
+
webSearch: true,
|
|
408
|
+
skillsMode: "custom",
|
|
409
|
+
skills: ["ticket-analysis"],
|
|
410
|
+
toolsMode: "custom",
|
|
411
|
+
tools: ["inspect-record"],
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "draft-reply",
|
|
415
|
+
employee: "",
|
|
416
|
+
title: "Draft a customer reply",
|
|
417
|
+
background:
|
|
418
|
+
"Write a concise and helpful response using the ticket context.",
|
|
419
|
+
userMessage: "Draft a customer reply for the current ticket.",
|
|
420
|
+
autoSend: false,
|
|
421
|
+
workContext: [],
|
|
422
|
+
model: "default",
|
|
423
|
+
webSearch: false,
|
|
424
|
+
skillsMode: "custom",
|
|
425
|
+
skills: ["response-drafting"],
|
|
426
|
+
toolsMode: "preset",
|
|
427
|
+
tools: [],
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
function ShortcutPromptGenerator({
|
|
432
|
+
embeddedController,
|
|
433
|
+
}: {
|
|
434
|
+
embeddedController: AIChatController;
|
|
435
|
+
}) {
|
|
436
|
+
const { employees, models } = useAI();
|
|
437
|
+
const { registeredCount, startPicking } = useAIPageElementPicker();
|
|
438
|
+
const businessEmployees = employees.filter(isBusinessEmployee);
|
|
439
|
+
const defaultBusinessEmployee =
|
|
440
|
+
businessEmployees.find((item) => item.username.toLowerCase() === "atlas") ??
|
|
441
|
+
businessEmployees[0]!;
|
|
442
|
+
const [integrationMode, setIntegrationMode] =
|
|
443
|
+
useState<IntegrationMode>("shortcut");
|
|
444
|
+
const [location, setLocation] = useState("the ticket detail header actions");
|
|
445
|
+
const [employee, setEmployee] = useState(defaultBusinessEmployee.username);
|
|
446
|
+
const [taskEmployee, setTaskEmployee] = useState(
|
|
447
|
+
defaultBusinessEmployee.username
|
|
448
|
+
);
|
|
449
|
+
const [target, setTarget] = useState<"global-side-panel" | "embedded">(
|
|
450
|
+
"global-side-panel"
|
|
451
|
+
);
|
|
452
|
+
const [chatContainer, setChatContainer] = useState<ChatContainer>("embedded");
|
|
453
|
+
const [tasks, setTasks] = useState<ConfigTask[]>(() =>
|
|
454
|
+
initialConfigTasks.map((task) => ({
|
|
455
|
+
...task,
|
|
456
|
+
employee: defaultBusinessEmployee.username,
|
|
457
|
+
}))
|
|
458
|
+
);
|
|
459
|
+
const [selectedTaskId, setSelectedTaskId] = useState(
|
|
460
|
+
initialConfigTasks[0].id
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const visibleTasks =
|
|
464
|
+
integrationMode === "chat-tasks"
|
|
465
|
+
? tasks.filter((task) => task.employee === taskEmployee)
|
|
466
|
+
: tasks;
|
|
467
|
+
const selectedTask =
|
|
468
|
+
visibleTasks.find((task) => task.id === selectedTaskId) ?? visibleTasks[0];
|
|
469
|
+
|
|
470
|
+
const runtimeTasks = useMemo<AIEmployeeTask[]>(
|
|
471
|
+
() =>
|
|
472
|
+
tasks.map((task) => {
|
|
473
|
+
const selectedModel = findAIModel(models, task.model);
|
|
474
|
+
const customCapabilities =
|
|
475
|
+
task.skillsMode === "custom" || task.toolsMode === "custom";
|
|
476
|
+
return {
|
|
477
|
+
title: task.title,
|
|
478
|
+
message: {
|
|
479
|
+
system: task.background || undefined,
|
|
480
|
+
user: task.userMessage || undefined,
|
|
481
|
+
workContext: task.workContext.length ? task.workContext : undefined,
|
|
482
|
+
},
|
|
483
|
+
autoSend: task.autoSend,
|
|
484
|
+
model: selectedModel
|
|
485
|
+
? {
|
|
486
|
+
llmService: selectedModel.llmService,
|
|
487
|
+
model: selectedModel.value,
|
|
488
|
+
}
|
|
489
|
+
: undefined,
|
|
490
|
+
webSearch: task.webSearch,
|
|
491
|
+
skillSettings: customCapabilities
|
|
492
|
+
? {
|
|
493
|
+
skills: task.skillsMode === "custom" ? task.skills : undefined,
|
|
494
|
+
tools: task.toolsMode === "custom" ? task.tools : undefined,
|
|
495
|
+
}
|
|
496
|
+
: undefined,
|
|
497
|
+
};
|
|
498
|
+
}),
|
|
499
|
+
[models, tasks]
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const prompt = useMemo(() => {
|
|
503
|
+
const describeTask = (task: ConfigTask, index: number) => {
|
|
504
|
+
const model =
|
|
505
|
+
task.model === "default"
|
|
506
|
+
? "Use the AI employee default"
|
|
507
|
+
: models.find((item) => item.value === task.model)?.label ??
|
|
508
|
+
task.model;
|
|
509
|
+
return `${index + 1}. ${task.title}
|
|
510
|
+
- Background: ${task.background || "None"}
|
|
511
|
+
- Default user message: ${task.userMessage || "None"}
|
|
512
|
+
- Send automatically: ${task.autoSend}
|
|
513
|
+
- Work context: ${
|
|
514
|
+
task.workContext.map((item) => item.title ?? item.id).join(", ") ||
|
|
515
|
+
"Inherit the surrounding context"
|
|
516
|
+
}
|
|
517
|
+
- Model: ${model}
|
|
518
|
+
- Web search: ${task.webSearch}
|
|
519
|
+
- Skills: ${
|
|
520
|
+
task.skillsMode === "preset"
|
|
521
|
+
? "Use the AI employee defaults"
|
|
522
|
+
: task.skills.join(", ") || "Disabled"
|
|
523
|
+
}
|
|
524
|
+
- Tools: ${
|
|
525
|
+
task.toolsMode === "preset"
|
|
526
|
+
? "Use the AI employee defaults"
|
|
527
|
+
: task.tools.join(", ") || "Disabled"
|
|
528
|
+
}`;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const taskDescriptions =
|
|
532
|
+
integrationMode === "chat-tasks"
|
|
533
|
+
? Array.from(new Set(tasks.map((task) => task.employee)))
|
|
534
|
+
.map((employeeUsername) => {
|
|
535
|
+
const employeeTasks = tasks.filter(
|
|
536
|
+
(task) => task.employee === employeeUsername
|
|
537
|
+
);
|
|
538
|
+
return `AI employee: ${employeeUsername}\n${employeeTasks
|
|
539
|
+
.map(describeTask)
|
|
540
|
+
.join("\n")}`;
|
|
541
|
+
})
|
|
542
|
+
.join("\n\n")
|
|
543
|
+
: tasks.map(describeTask).join("\n");
|
|
544
|
+
|
|
545
|
+
if (integrationMode === "chat-tasks") {
|
|
546
|
+
const containerLabel = {
|
|
547
|
+
page: "a dedicated page",
|
|
548
|
+
embedded: "an embedded block in the page",
|
|
549
|
+
"side-panel": "a push side panel",
|
|
550
|
+
dialog: "a dialog",
|
|
551
|
+
}[chatContainer];
|
|
552
|
+
return `Add an AI conversation window to ${location} using ${containerLabel}.
|
|
553
|
+
|
|
554
|
+
Employee task bindings:
|
|
555
|
+
${taskDescriptions}
|
|
556
|
+
|
|
557
|
+
Implementation requirements:
|
|
558
|
+
- Render AIChatWindow in the selected container and wrap it with AIChatProvider.
|
|
559
|
+
- Configure AIChatProvider with employeeTasks grouped by AI employee username.
|
|
560
|
+
- When the user starts a new conversation or switches AI employees, show the matching tasks below that employee’s greeting.
|
|
561
|
+
- Employees without configured tasks must keep the greeting-only empty state.
|
|
562
|
+
- Selecting a task must respect autoSend: either send immediately or fill the composer for review.
|
|
563
|
+
- Resolve task message.workContext when the task starts so selected page elements use their latest values.
|
|
564
|
+
- When a task has no message.workContext, inherit the context surrounding the Shortcut or AIChatProvider.
|
|
565
|
+
- Preserve every task’s background prompt, model override, Web search setting, Skills, Tools, and work context.
|
|
566
|
+
- Keep the employeeTasks capability independent of the container so the same configuration works in page, embedded, side-panel, dialog, and mobile layouts.
|
|
567
|
+
- Use the existing AIProvider/AIChatProvider runtime with shadcn/Base UI components.`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return `Add an AI employee shortcut to ${location}.
|
|
571
|
+
|
|
572
|
+
Shortcut configuration:
|
|
573
|
+
- AI employee: ${employee}
|
|
574
|
+
- Target conversation: ${
|
|
575
|
+
target === "global-side-panel"
|
|
576
|
+
? "the default global push side panel"
|
|
577
|
+
: "the embedded chat rendered in this page section"
|
|
578
|
+
}
|
|
579
|
+
- Include the current record or form values as work context.
|
|
580
|
+
|
|
581
|
+
Tasks:
|
|
582
|
+
${taskDescriptions}
|
|
583
|
+
|
|
584
|
+
Implementation requirements:
|
|
585
|
+
- Use AIEmployeeShortcut and pass all configured tasks through its tasks prop.
|
|
586
|
+
- When multiple tasks are provided, open a new conversation and show the tasks below the AI employee greeting.
|
|
587
|
+
- Selecting a task must respect that task's autoSend setting: either send immediately or fill the composer for review.
|
|
588
|
+
- Resolve task message.workContext when the task starts so selected page elements use their latest values.
|
|
589
|
+
- When a task has no message.workContext, inherit the context surrounding the Shortcut or AIChatProvider.
|
|
590
|
+
- Preserve each task's background prompt, work context, model override, Web search setting, Skills, and Tools.
|
|
591
|
+
- ${
|
|
592
|
+
target === "global-side-panel"
|
|
593
|
+
? "Use the global AI chat controller; do not pass a target prop."
|
|
594
|
+
: "Create a dedicated controller with useAIChatController(), bind it to the embedded AIChatProvider, and pass it through the shortcut target prop."
|
|
595
|
+
}
|
|
596
|
+
- Use the existing AIProvider and AIChatProvider runtime with shadcn/Base UI components.`;
|
|
597
|
+
}, [
|
|
598
|
+
chatContainer,
|
|
599
|
+
employee,
|
|
600
|
+
integrationMode,
|
|
601
|
+
location,
|
|
602
|
+
models,
|
|
603
|
+
target,
|
|
604
|
+
tasks,
|
|
605
|
+
]);
|
|
606
|
+
|
|
607
|
+
const updateSelectedTask = (patch: Partial<ConfigTask>) => {
|
|
608
|
+
if (!selectedTask) return;
|
|
609
|
+
setTasks((current) =>
|
|
610
|
+
current.map((task) =>
|
|
611
|
+
task.id === selectedTask.id ? { ...task, ...patch } : task
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const addTask = () => {
|
|
617
|
+
const id = `task-${crypto.randomUUID()}`;
|
|
618
|
+
setTasks((current) => [
|
|
619
|
+
...current,
|
|
620
|
+
{
|
|
621
|
+
id,
|
|
622
|
+
employee: integrationMode === "chat-tasks" ? taskEmployee : employee,
|
|
623
|
+
title: `Task ${current.length + 1}`,
|
|
624
|
+
background: "",
|
|
625
|
+
userMessage: "",
|
|
626
|
+
autoSend: false,
|
|
627
|
+
workContext: [],
|
|
628
|
+
model: "default",
|
|
629
|
+
webSearch: false,
|
|
630
|
+
skillsMode: "preset",
|
|
631
|
+
skills: [],
|
|
632
|
+
toolsMode: "preset",
|
|
633
|
+
tools: [],
|
|
634
|
+
},
|
|
635
|
+
]);
|
|
636
|
+
setSelectedTaskId(id);
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const removeTask = (id: string) => {
|
|
640
|
+
if (tasks.length === 1) return;
|
|
641
|
+
const nextTasks = tasks.filter((task) => task.id !== id);
|
|
642
|
+
setTasks(nextTasks);
|
|
643
|
+
if (selectedTaskId === id) {
|
|
644
|
+
const nextSelectedTask =
|
|
645
|
+
integrationMode === "chat-tasks"
|
|
646
|
+
? nextTasks.find((task) => task.employee === taskEmployee)
|
|
647
|
+
: nextTasks[0];
|
|
648
|
+
setSelectedTaskId(nextSelectedTask?.id ?? "");
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const toggleCapability = (
|
|
653
|
+
type: "skills" | "tools",
|
|
654
|
+
value: string,
|
|
655
|
+
checked: boolean
|
|
656
|
+
) => {
|
|
657
|
+
if (!selectedTask) return;
|
|
658
|
+
const current = selectedTask[type];
|
|
659
|
+
updateSelectedTask({
|
|
660
|
+
[type]: checked
|
|
661
|
+
? [...current, value]
|
|
662
|
+
: current.filter((item) => item !== value),
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
return (
|
|
667
|
+
<section className="space-y-5">
|
|
668
|
+
<div>
|
|
669
|
+
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
670
|
+
Employee tasks prompt
|
|
671
|
+
</p>
|
|
672
|
+
<h2 className="mt-2 text-xl font-semibold tracking-tight">
|
|
673
|
+
Generate an integration prompt
|
|
674
|
+
</h2>
|
|
675
|
+
<p className="mt-1.5 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
676
|
+
Choose whether tasks are exposed through a contextual Shortcut or
|
|
677
|
+
directly by the selected employee in a chat. Both modes share the same
|
|
678
|
+
task, model, Web search, Skills, and Tools configuration.
|
|
679
|
+
</p>
|
|
680
|
+
</div>
|
|
681
|
+
<div className="grid items-start gap-5 xl:grid-cols-[520px_minmax(0,1fr)]">
|
|
682
|
+
<Card className="gap-0 py-0">
|
|
683
|
+
<CardHeader className="py-4">
|
|
684
|
+
<CardTitle className="text-base">
|
|
685
|
+
Employee task configuration
|
|
686
|
+
</CardTitle>
|
|
687
|
+
</CardHeader>
|
|
688
|
+
<CardContent className="space-y-4 pb-4">
|
|
689
|
+
<label className="block space-y-2 text-xs font-medium">
|
|
690
|
+
<span>Integration</span>
|
|
691
|
+
<Select
|
|
692
|
+
value={integrationMode}
|
|
693
|
+
onValueChange={(value) => {
|
|
694
|
+
if (!value) return;
|
|
695
|
+
const mode = value as IntegrationMode;
|
|
696
|
+
setIntegrationMode(mode);
|
|
697
|
+
setLocation(
|
|
698
|
+
mode === "shortcut"
|
|
699
|
+
? "the ticket detail header actions"
|
|
700
|
+
: "the ticket detail page"
|
|
701
|
+
);
|
|
702
|
+
const nextTask =
|
|
703
|
+
mode === "chat-tasks"
|
|
704
|
+
? tasks.find((task) => task.employee === taskEmployee)
|
|
705
|
+
: tasks[0];
|
|
706
|
+
setSelectedTaskId(nextTask?.id ?? "");
|
|
707
|
+
}}
|
|
708
|
+
>
|
|
709
|
+
<SelectTrigger className="w-full">
|
|
710
|
+
<SelectValue>
|
|
711
|
+
{integrationMode === "shortcut"
|
|
712
|
+
? "Contextual Shortcut"
|
|
713
|
+
: "Tasks inside a chat"}
|
|
714
|
+
</SelectValue>
|
|
715
|
+
</SelectTrigger>
|
|
716
|
+
<SelectContent>
|
|
717
|
+
<SelectItem value="shortcut">Contextual Shortcut</SelectItem>
|
|
718
|
+
<SelectItem value="chat-tasks">
|
|
719
|
+
Tasks inside a chat
|
|
720
|
+
</SelectItem>
|
|
721
|
+
</SelectContent>
|
|
722
|
+
</Select>
|
|
723
|
+
</label>
|
|
724
|
+
{integrationMode === "chat-tasks" ? (
|
|
725
|
+
<label className="block space-y-2 border-t pt-4 text-xs font-medium">
|
|
726
|
+
<span>AI employee</span>
|
|
727
|
+
<Select
|
|
728
|
+
value={taskEmployee}
|
|
729
|
+
onValueChange={(value) => {
|
|
730
|
+
if (!value) return;
|
|
731
|
+
setTaskEmployee(value);
|
|
732
|
+
setSelectedTaskId(
|
|
733
|
+
tasks.find((task) => task.employee === value)?.id ?? ""
|
|
734
|
+
);
|
|
735
|
+
}}
|
|
736
|
+
>
|
|
737
|
+
<SelectTrigger className="w-full">
|
|
738
|
+
<SelectValue>
|
|
739
|
+
{businessEmployees.find(
|
|
740
|
+
(item) => item.username === taskEmployee
|
|
741
|
+
)?.nickname ?? taskEmployee}
|
|
742
|
+
</SelectValue>
|
|
743
|
+
</SelectTrigger>
|
|
744
|
+
<SelectContent>
|
|
745
|
+
{businessEmployees.map((item) => (
|
|
746
|
+
<SelectItem key={item.username} value={item.username}>
|
|
747
|
+
{item.nickname}
|
|
748
|
+
</SelectItem>
|
|
749
|
+
))}
|
|
750
|
+
</SelectContent>
|
|
751
|
+
</Select>
|
|
752
|
+
<span className="block font-normal leading-5 text-muted-foreground">
|
|
753
|
+
Select an employee first, then configure the tasks shown in
|
|
754
|
+
that employee’s new-conversation state.
|
|
755
|
+
</span>
|
|
756
|
+
</label>
|
|
757
|
+
) : null}
|
|
758
|
+
|
|
759
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
760
|
+
{integrationMode === "shortcut" ? (
|
|
761
|
+
<label className="block space-y-2 text-xs font-medium">
|
|
762
|
+
<span>AI employee</span>
|
|
763
|
+
<Select
|
|
764
|
+
value={employee}
|
|
765
|
+
onValueChange={(value) => value && setEmployee(value)}
|
|
766
|
+
>
|
|
767
|
+
<SelectTrigger className="w-full">
|
|
768
|
+
<SelectValue>
|
|
769
|
+
{businessEmployees.find(
|
|
770
|
+
(item) => item.username === employee
|
|
771
|
+
)?.nickname ?? employee}
|
|
772
|
+
</SelectValue>
|
|
773
|
+
</SelectTrigger>
|
|
774
|
+
<SelectContent>
|
|
775
|
+
{businessEmployees.map((item) => (
|
|
776
|
+
<SelectItem key={item.username} value={item.username}>
|
|
777
|
+
{item.nickname}
|
|
778
|
+
</SelectItem>
|
|
779
|
+
))}
|
|
780
|
+
</SelectContent>
|
|
781
|
+
</Select>
|
|
782
|
+
</label>
|
|
783
|
+
) : null}
|
|
784
|
+
<label className="block space-y-2 text-xs font-medium">
|
|
785
|
+
<span>Placement</span>
|
|
786
|
+
<Input
|
|
787
|
+
value={location}
|
|
788
|
+
onChange={(event) => setLocation(event.target.value)}
|
|
789
|
+
/>
|
|
790
|
+
</label>
|
|
791
|
+
{integrationMode === "chat-tasks" ? (
|
|
792
|
+
<label className="block space-y-2 text-xs font-medium">
|
|
793
|
+
<span>Chat container</span>
|
|
794
|
+
<Select
|
|
795
|
+
value={chatContainer}
|
|
796
|
+
onValueChange={(value) =>
|
|
797
|
+
value && setChatContainer(value as ChatContainer)
|
|
798
|
+
}
|
|
799
|
+
>
|
|
800
|
+
<SelectTrigger className="w-full">
|
|
801
|
+
<SelectValue>
|
|
802
|
+
{
|
|
803
|
+
{
|
|
804
|
+
page: "Page",
|
|
805
|
+
embedded: "Embedded block",
|
|
806
|
+
"side-panel": "Side panel",
|
|
807
|
+
dialog: "Dialog",
|
|
808
|
+
}[chatContainer]
|
|
809
|
+
}
|
|
810
|
+
</SelectValue>
|
|
811
|
+
</SelectTrigger>
|
|
812
|
+
<SelectContent>
|
|
813
|
+
<SelectItem value="page">Page</SelectItem>
|
|
814
|
+
<SelectItem value="embedded">Embedded block</SelectItem>
|
|
815
|
+
<SelectItem value="side-panel">Side panel</SelectItem>
|
|
816
|
+
<SelectItem value="dialog">Dialog</SelectItem>
|
|
817
|
+
</SelectContent>
|
|
818
|
+
</Select>
|
|
819
|
+
</label>
|
|
820
|
+
) : null}
|
|
821
|
+
</div>
|
|
822
|
+
|
|
823
|
+
{integrationMode === "shortcut" ? (
|
|
824
|
+
<label className="block space-y-2 text-xs font-medium">
|
|
825
|
+
<span>Target conversation</span>
|
|
826
|
+
<Select
|
|
827
|
+
value={target}
|
|
828
|
+
onValueChange={(value) =>
|
|
829
|
+
value &&
|
|
830
|
+
setTarget(value as "global-side-panel" | "embedded")
|
|
831
|
+
}
|
|
832
|
+
>
|
|
833
|
+
<SelectTrigger className="w-full">
|
|
834
|
+
<SelectValue>
|
|
835
|
+
{target === "global-side-panel"
|
|
836
|
+
? "Global side panel"
|
|
837
|
+
: "Embedded chat"}
|
|
838
|
+
</SelectValue>
|
|
839
|
+
</SelectTrigger>
|
|
840
|
+
<SelectContent>
|
|
841
|
+
<SelectItem value="global-side-panel">
|
|
842
|
+
Global side panel
|
|
843
|
+
</SelectItem>
|
|
844
|
+
<SelectItem value="embedded">Embedded chat</SelectItem>
|
|
845
|
+
</SelectContent>
|
|
846
|
+
</Select>
|
|
847
|
+
</label>
|
|
848
|
+
) : null}
|
|
849
|
+
|
|
850
|
+
<div className="space-y-2 border-t pt-4">
|
|
851
|
+
<div className="flex items-center justify-between gap-3">
|
|
852
|
+
<div>
|
|
853
|
+
<div className="text-sm font-medium">Tasks</div>
|
|
854
|
+
<div className="text-xs text-muted-foreground">
|
|
855
|
+
{visibleTasks.length} configured task
|
|
856
|
+
{visibleTasks.length === 1 ? "" : "s"}
|
|
857
|
+
{integrationMode === "chat-tasks"
|
|
858
|
+
? " for this employee"
|
|
859
|
+
: ""}
|
|
860
|
+
</div>
|
|
861
|
+
</div>
|
|
862
|
+
<Button variant="outline" size="sm" onClick={addTask}>
|
|
863
|
+
<Plus /> Add task
|
|
864
|
+
</Button>
|
|
865
|
+
</div>
|
|
866
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
867
|
+
{visibleTasks.map((task, index) => (
|
|
868
|
+
<div
|
|
869
|
+
key={task.id}
|
|
870
|
+
className={
|
|
871
|
+
task.id === selectedTask?.id
|
|
872
|
+
? "flex items-center gap-2 rounded-lg border border-foreground/25 bg-muted/60 p-1"
|
|
873
|
+
: "flex items-center gap-2 rounded-lg border p-1"
|
|
874
|
+
}
|
|
875
|
+
>
|
|
876
|
+
<button
|
|
877
|
+
type="button"
|
|
878
|
+
className="min-w-0 flex-1 rounded-md px-2 py-1.5 text-left"
|
|
879
|
+
onClick={() => setSelectedTaskId(task.id)}
|
|
880
|
+
>
|
|
881
|
+
<span className="block truncate text-xs font-medium">
|
|
882
|
+
{index + 1}. {task.title}
|
|
883
|
+
</span>
|
|
884
|
+
<span className="block text-[10px] text-muted-foreground">
|
|
885
|
+
{task.autoSend ? "Auto send" : "Fill composer"}
|
|
886
|
+
{task.workContext.length ? " · Task context" : ""}
|
|
887
|
+
{task.webSearch ? " · Web search" : ""}
|
|
888
|
+
</span>
|
|
889
|
+
</button>
|
|
890
|
+
<Button
|
|
891
|
+
variant="ghost"
|
|
892
|
+
size="icon-xs"
|
|
893
|
+
disabled={tasks.length === 1}
|
|
894
|
+
aria-label={`Remove ${task.title}`}
|
|
895
|
+
onClick={() => removeTask(task.id)}
|
|
896
|
+
>
|
|
897
|
+
<Trash2 />
|
|
898
|
+
</Button>
|
|
899
|
+
</div>
|
|
900
|
+
))}
|
|
901
|
+
</div>
|
|
902
|
+
</div>
|
|
903
|
+
|
|
904
|
+
{selectedTask ? (
|
|
905
|
+
<div className="space-y-3 border-t pt-4">
|
|
906
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
907
|
+
<label className="block space-y-2 text-xs font-medium">
|
|
908
|
+
<span>Title</span>
|
|
909
|
+
<Input
|
|
910
|
+
value={selectedTask.title}
|
|
911
|
+
onChange={(event) =>
|
|
912
|
+
updateSelectedTask({ title: event.target.value })
|
|
913
|
+
}
|
|
914
|
+
/>
|
|
915
|
+
</label>
|
|
916
|
+
<label className="block space-y-2 text-xs font-medium">
|
|
917
|
+
<span>Model</span>
|
|
918
|
+
<Select
|
|
919
|
+
value={selectedTask.model}
|
|
920
|
+
onValueChange={(value) =>
|
|
921
|
+
value && updateSelectedTask({ model: value })
|
|
922
|
+
}
|
|
923
|
+
>
|
|
924
|
+
<SelectTrigger className="w-full">
|
|
925
|
+
<SelectValue className="min-w-0 overflow-hidden">
|
|
926
|
+
<span
|
|
927
|
+
className="block min-w-0 truncate"
|
|
928
|
+
title={
|
|
929
|
+
selectedTask.model === "default"
|
|
930
|
+
? "Use employee default"
|
|
931
|
+
: findAIModel(models, selectedTask.model)
|
|
932
|
+
?.label ?? selectedTask.model
|
|
933
|
+
}
|
|
934
|
+
>
|
|
935
|
+
{selectedTask.model === "default"
|
|
936
|
+
? "Use employee default"
|
|
937
|
+
: findAIModel(models, selectedTask.model)
|
|
938
|
+
?.label ?? selectedTask.model}
|
|
939
|
+
</span>
|
|
940
|
+
</SelectValue>
|
|
941
|
+
</SelectTrigger>
|
|
942
|
+
<SelectContent>
|
|
943
|
+
<SelectItem value="default">
|
|
944
|
+
Use employee default
|
|
945
|
+
</SelectItem>
|
|
946
|
+
<SelectSeparator />
|
|
947
|
+
<AIModelSelectOptions models={models} />
|
|
948
|
+
</SelectContent>
|
|
949
|
+
</Select>
|
|
950
|
+
</label>
|
|
951
|
+
</div>
|
|
952
|
+
<label className="block space-y-2 text-xs font-medium">
|
|
953
|
+
<span>Default user message</span>
|
|
954
|
+
<Textarea
|
|
955
|
+
className="min-h-16"
|
|
956
|
+
value={selectedTask.userMessage}
|
|
957
|
+
onChange={(event) =>
|
|
958
|
+
updateSelectedTask({ userMessage: event.target.value })
|
|
959
|
+
}
|
|
960
|
+
/>
|
|
961
|
+
</label>
|
|
962
|
+
<label className="flex items-center justify-between gap-4 rounded-lg border px-3 py-2.5 text-sm">
|
|
963
|
+
<span>
|
|
964
|
+
<span className="block font-medium">Auto send</span>
|
|
965
|
+
<span className="block text-xs text-muted-foreground">
|
|
966
|
+
Otherwise the message is placed in the composer.
|
|
967
|
+
</span>
|
|
968
|
+
</span>
|
|
969
|
+
<Switch
|
|
970
|
+
size="sm"
|
|
971
|
+
checked={selectedTask.autoSend}
|
|
972
|
+
onCheckedChange={(checked) =>
|
|
973
|
+
updateSelectedTask({ autoSend: checked })
|
|
974
|
+
}
|
|
975
|
+
/>
|
|
976
|
+
</label>
|
|
977
|
+
<div className="space-y-3 rounded-lg border px-3 py-2.5">
|
|
978
|
+
<div className="flex items-start justify-between gap-3">
|
|
979
|
+
<span>
|
|
980
|
+
<span className="block text-sm font-medium">
|
|
981
|
+
Work context
|
|
982
|
+
</span>
|
|
983
|
+
<span className="block text-xs text-muted-foreground">
|
|
984
|
+
Select a page context for this task. Without one, the
|
|
985
|
+
task inherits its surrounding context.
|
|
986
|
+
</span>
|
|
987
|
+
</span>
|
|
988
|
+
<Button
|
|
989
|
+
variant="outline"
|
|
990
|
+
size="sm"
|
|
991
|
+
disabled={registeredCount === 0}
|
|
992
|
+
onClick={() =>
|
|
993
|
+
startPicking({
|
|
994
|
+
onSelect: (item) =>
|
|
995
|
+
updateSelectedTask({
|
|
996
|
+
workContext: [
|
|
997
|
+
{
|
|
998
|
+
type: item.type,
|
|
999
|
+
id: item.id,
|
|
1000
|
+
title: item.title,
|
|
1001
|
+
kind: item.kind,
|
|
1002
|
+
},
|
|
1003
|
+
],
|
|
1004
|
+
}),
|
|
1005
|
+
})
|
|
1006
|
+
}
|
|
1007
|
+
>
|
|
1008
|
+
<MousePointer2 /> Pick context
|
|
1009
|
+
</Button>
|
|
1010
|
+
</div>
|
|
1011
|
+
{selectedTask.workContext.map((item, index) => (
|
|
1012
|
+
<div
|
|
1013
|
+
key={`${item.type}:${item.id ?? index}`}
|
|
1014
|
+
className="flex items-center gap-2 rounded-md bg-muted/50 px-2 py-1.5 text-xs"
|
|
1015
|
+
>
|
|
1016
|
+
<span className="min-w-0 flex-1 truncate">
|
|
1017
|
+
{item.title ?? item.id ?? item.type}
|
|
1018
|
+
</span>
|
|
1019
|
+
<Button
|
|
1020
|
+
variant="ghost"
|
|
1021
|
+
size="icon-xs"
|
|
1022
|
+
aria-label="Remove task context"
|
|
1023
|
+
onClick={() => updateSelectedTask({ workContext: [] })}
|
|
1024
|
+
>
|
|
1025
|
+
<X />
|
|
1026
|
+
</Button>
|
|
1027
|
+
</div>
|
|
1028
|
+
))}
|
|
1029
|
+
</div>
|
|
1030
|
+
|
|
1031
|
+
<Accordion>
|
|
1032
|
+
<AccordionItem
|
|
1033
|
+
value="advanced-task-settings"
|
|
1034
|
+
className="rounded-lg border px-3"
|
|
1035
|
+
>
|
|
1036
|
+
<AccordionTrigger className="no-underline hover:no-underline">
|
|
1037
|
+
<span>
|
|
1038
|
+
<span className="block">Advanced task settings</span>
|
|
1039
|
+
<span className="block text-xs font-normal text-muted-foreground">
|
|
1040
|
+
Background, Web search, Skills, and Tools
|
|
1041
|
+
</span>
|
|
1042
|
+
</span>
|
|
1043
|
+
</AccordionTrigger>
|
|
1044
|
+
<AccordionContent className="space-y-4 pt-1">
|
|
1045
|
+
<label className="block space-y-2 text-xs font-medium">
|
|
1046
|
+
<span>Background</span>
|
|
1047
|
+
<Textarea
|
|
1048
|
+
className="min-h-16"
|
|
1049
|
+
value={selectedTask.background}
|
|
1050
|
+
onChange={(event) =>
|
|
1051
|
+
updateSelectedTask({
|
|
1052
|
+
background: event.target.value,
|
|
1053
|
+
})
|
|
1054
|
+
}
|
|
1055
|
+
/>
|
|
1056
|
+
</label>
|
|
1057
|
+
<label className="flex items-center justify-between gap-4 rounded-lg border px-3 py-2.5 text-sm">
|
|
1058
|
+
<span>
|
|
1059
|
+
<span className="block font-medium">Web search</span>
|
|
1060
|
+
<span className="block text-xs text-muted-foreground">
|
|
1061
|
+
Subject to the selected LLM service capability.
|
|
1062
|
+
</span>
|
|
1063
|
+
</span>
|
|
1064
|
+
<Switch
|
|
1065
|
+
size="sm"
|
|
1066
|
+
checked={selectedTask.webSearch}
|
|
1067
|
+
onCheckedChange={(checked) =>
|
|
1068
|
+
updateSelectedTask({ webSearch: checked })
|
|
1069
|
+
}
|
|
1070
|
+
/>
|
|
1071
|
+
</label>
|
|
1072
|
+
<CapabilityEditor
|
|
1073
|
+
title="Skills"
|
|
1074
|
+
mode={selectedTask.skillsMode}
|
|
1075
|
+
values={selectedTask.skills}
|
|
1076
|
+
options={availableSkills}
|
|
1077
|
+
onModeChange={(skillsMode) =>
|
|
1078
|
+
updateSelectedTask({ skillsMode })
|
|
1079
|
+
}
|
|
1080
|
+
onToggle={(value, checked) =>
|
|
1081
|
+
toggleCapability("skills", value, checked)
|
|
1082
|
+
}
|
|
1083
|
+
/>
|
|
1084
|
+
<CapabilityEditor
|
|
1085
|
+
title="Tools"
|
|
1086
|
+
mode={selectedTask.toolsMode}
|
|
1087
|
+
values={selectedTask.tools}
|
|
1088
|
+
options={availableTools}
|
|
1089
|
+
onModeChange={(toolsMode) =>
|
|
1090
|
+
updateSelectedTask({ toolsMode })
|
|
1091
|
+
}
|
|
1092
|
+
onToggle={(value, checked) =>
|
|
1093
|
+
toggleCapability("tools", value, checked)
|
|
1094
|
+
}
|
|
1095
|
+
/>
|
|
1096
|
+
</AccordionContent>
|
|
1097
|
+
</AccordionItem>
|
|
1098
|
+
</Accordion>
|
|
1099
|
+
</div>
|
|
1100
|
+
) : null}
|
|
1101
|
+
|
|
1102
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
|
|
1103
|
+
<div>
|
|
1104
|
+
<div className="text-xs font-medium">
|
|
1105
|
+
{integrationMode === "shortcut"
|
|
1106
|
+
? "Live shortcut preview"
|
|
1107
|
+
: "Chat-bound task configuration"}
|
|
1108
|
+
</div>
|
|
1109
|
+
<div className="text-[11px] text-muted-foreground">
|
|
1110
|
+
{integrationMode === "shortcut"
|
|
1111
|
+
? "Multiple tasks appear in the chat empty state after opening."
|
|
1112
|
+
: "Use the third scenario above to preview employee switching."}
|
|
1113
|
+
</div>
|
|
1114
|
+
</div>
|
|
1115
|
+
{integrationMode === "shortcut" ? (
|
|
1116
|
+
<AIEmployeeShortcut
|
|
1117
|
+
aiEmployee={employee}
|
|
1118
|
+
tasks={runtimeTasks}
|
|
1119
|
+
target={
|
|
1120
|
+
target === "embedded" ? embeddedController : undefined
|
|
1121
|
+
}
|
|
1122
|
+
label={`Ask ${
|
|
1123
|
+
businessEmployees.find((item) => item.username === employee)
|
|
1124
|
+
?.nickname ?? employee
|
|
1125
|
+
}`}
|
|
1126
|
+
size={34}
|
|
1127
|
+
/>
|
|
1128
|
+
) : null}
|
|
1129
|
+
</div>
|
|
1130
|
+
</CardContent>
|
|
1131
|
+
</Card>
|
|
1132
|
+
<PromptCard
|
|
1133
|
+
title={
|
|
1134
|
+
integrationMode === "shortcut"
|
|
1135
|
+
? "Add an AI employee task shortcut"
|
|
1136
|
+
: "Add employee tasks to a chat"
|
|
1137
|
+
}
|
|
1138
|
+
description={
|
|
1139
|
+
integrationMode === "shortcut"
|
|
1140
|
+
? "The prompt includes every task, capability override, and target conversation."
|
|
1141
|
+
: "The prompt groups configured tasks by business AI employee and selected chat container."
|
|
1142
|
+
}
|
|
1143
|
+
prompt={prompt}
|
|
1144
|
+
/>
|
|
1145
|
+
</div>
|
|
1146
|
+
</section>
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function CapabilityEditor({
|
|
1151
|
+
title,
|
|
1152
|
+
mode,
|
|
1153
|
+
values,
|
|
1154
|
+
options,
|
|
1155
|
+
onModeChange,
|
|
1156
|
+
onToggle,
|
|
1157
|
+
}: {
|
|
1158
|
+
title: string;
|
|
1159
|
+
mode: CapabilityMode;
|
|
1160
|
+
values: string[];
|
|
1161
|
+
options: { value: string; label: string }[];
|
|
1162
|
+
onModeChange: (mode: CapabilityMode) => void;
|
|
1163
|
+
onToggle: (value: string, checked: boolean) => void;
|
|
1164
|
+
}) {
|
|
1165
|
+
return (
|
|
1166
|
+
<div className="space-y-3 rounded-lg border p-3">
|
|
1167
|
+
<div className="flex items-center justify-between gap-4">
|
|
1168
|
+
<div>
|
|
1169
|
+
<div className="text-sm font-medium">{title}</div>
|
|
1170
|
+
<div className="text-xs text-muted-foreground">
|
|
1171
|
+
Preset inherits the AI employee configuration.
|
|
1172
|
+
</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
<Select
|
|
1175
|
+
value={mode}
|
|
1176
|
+
onValueChange={(value) =>
|
|
1177
|
+
value && onModeChange(value as CapabilityMode)
|
|
1178
|
+
}
|
|
1179
|
+
>
|
|
1180
|
+
<SelectTrigger size="sm" className="w-28">
|
|
1181
|
+
<SelectValue>{mode === "preset" ? "Preset" : "Custom"}</SelectValue>
|
|
1182
|
+
</SelectTrigger>
|
|
1183
|
+
<SelectContent>
|
|
1184
|
+
<SelectItem value="preset">Preset</SelectItem>
|
|
1185
|
+
<SelectItem value="custom">Custom</SelectItem>
|
|
1186
|
+
</SelectContent>
|
|
1187
|
+
</Select>
|
|
1188
|
+
</div>
|
|
1189
|
+
{mode === "custom" ? (
|
|
1190
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
1191
|
+
{options.map((option) => (
|
|
1192
|
+
<label
|
|
1193
|
+
key={option.value}
|
|
1194
|
+
className="flex items-center gap-2 rounded-md bg-muted/40 px-2.5 py-2 text-xs"
|
|
1195
|
+
>
|
|
1196
|
+
<Checkbox
|
|
1197
|
+
checked={values.includes(option.value)}
|
|
1198
|
+
onCheckedChange={(checked) => onToggle(option.value, checked)}
|
|
1199
|
+
/>
|
|
1200
|
+
<span>{option.label}</span>
|
|
1201
|
+
</label>
|
|
1202
|
+
))}
|
|
1203
|
+
</div>
|
|
1204
|
+
) : null}
|
|
1205
|
+
</div>
|
|
1206
|
+
);
|
|
1207
|
+
}
|