@nocobase/portal-template-default 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.MD +90 -1
  2. package/components.json +3 -1
  3. package/package.json +21 -5
  4. package/registry/nocobase-acl/README.md +19 -0
  5. package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
  6. package/registry/nocobase-acl/components/index.ts +3 -0
  7. package/registry/nocobase-acl/components/role-options.ts +36 -0
  8. package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
  9. package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
  10. package/registry/nocobase-acl/demo/components.tsx +231 -0
  11. package/registry/nocobase-acl/demo/index.tsx +505 -0
  12. package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
  13. package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
  14. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  15. package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
  16. package/registry/nocobase-acl/extension.tsx +71 -0
  17. package/registry/nocobase-acl/index.ts +1 -0
  18. package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
  19. package/registry/nocobase-ai/README.md +70 -0
  20. package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
  21. package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  22. package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
  23. package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  24. package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  25. package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  26. package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  27. package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  28. package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
  29. package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  30. package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
  31. package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  32. package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
  33. package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  34. package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  35. package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  36. package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  37. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  38. package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  39. package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  40. package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  41. package/registry/nocobase-ai/components/index.ts +59 -0
  42. package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  43. package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  44. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  45. package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  46. package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  47. package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  48. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  49. package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  50. package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  51. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  52. package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  53. package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  54. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  55. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  56. package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  57. package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  58. package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  59. package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  60. package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  61. package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  62. package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  63. package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  64. package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  65. package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  66. package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  67. package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  68. package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
  69. package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
  70. package/registry/nocobase-ai/demo/floating.tsx +134 -0
  71. package/registry/nocobase-ai/demo/index.tsx +346 -0
  72. package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  73. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  74. package/registry/nocobase-ai/demo/page-context.tsx +884 -0
  75. package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  76. package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
  77. package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
  78. package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
  79. package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
  80. package/registry/nocobase-ai/extension.tsx +158 -0
  81. package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
  82. package/registry/nocobase-ai/index.ts +3 -0
  83. package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
  84. package/registry/nocobase-ai/providers/avatars.ts +817 -0
  85. package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
  86. package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
  87. package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
  88. package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
  89. package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
  90. package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
  91. package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
  92. package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
  93. package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  94. package/registry/nocobase-ai/providers/index.ts +61 -0
  95. package/registry/nocobase-ai/providers/model.ts +32 -0
  96. package/registry/nocobase-ai/providers/page-context.tsx +136 -0
  97. package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
  98. package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
  99. package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
  100. package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  101. package/registry/nocobase-ai/providers/types.ts +183 -0
  102. package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
  103. package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  104. package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  105. package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  106. package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  107. package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  108. package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  109. package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
  110. package/registry/nocobase-ai/services/index.ts +7 -0
  111. package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  112. package/registry/nocobase-ai/services/types.ts +82 -0
  113. package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
  114. package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
  115. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
  116. package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
  117. package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
  118. package/registry/nocobase-i18n/README.md +53 -0
  119. package/registry/nocobase-i18n/components/index.ts +1 -0
  120. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  121. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  122. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  123. package/registry/nocobase-i18n/extension.tsx +46 -0
  124. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  125. package/registry/nocobase-i18n/index.ts +6 -0
  126. package/registry/nocobase-i18n/locale-store.ts +134 -0
  127. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  128. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  129. package/registry/nocobase-i18n/provider.tsx +32 -0
  130. package/registry/nocobase-i18n/runtime.ts +118 -0
  131. package/registry/nocobase-i18n/server-resources.ts +184 -0
  132. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  133. package/registry.config.json +163 -0
  134. package/scripts/registry.mjs +164 -0
  135. package/src/App.css +95 -0
  136. package/src/App.tsx +87 -69
  137. package/src/app/extension.ts +11 -0
  138. package/src/app/extensions.tsx +49 -0
  139. package/src/components/access-control/access-denied.tsx +30 -0
  140. package/src/components/access-control/acl-bootstrap.tsx +9 -0
  141. package/src/components/access-control/acl-gate.tsx +36 -0
  142. package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
  143. package/src/components/access-control/resource-access-guard.tsx +21 -0
  144. package/src/components/access-control/use-acl-runtime.ts +28 -0
  145. package/src/components/app-shell/breadcrumb.tsx +39 -3
  146. package/src/components/app-shell/document-title-handler.tsx +11 -20
  147. package/src/components/app-shell/header.tsx +22 -4
  148. package/src/components/app-shell/loading-overlay.tsx +2 -4
  149. package/src/components/app-shell/loading-state.tsx +15 -0
  150. package/src/components/app-shell/sidebar.tsx +103 -45
  151. package/src/components/data-table/data-table-pagination.tsx +27 -7
  152. package/src/components/data-table/data-table-styles.ts +57 -0
  153. package/src/components/data-table/data-table.tsx +14 -59
  154. package/src/components/resources/resource-label.ts +101 -0
  155. package/src/components/resources/views/create-view.tsx +17 -14
  156. package/src/components/resources/views/edit-view.tsx +17 -15
  157. package/src/components/resources/views/list-view.tsx +20 -10
  158. package/src/components/resources/views/show-view.tsx +20 -18
  159. package/src/index.tsx +2 -3
  160. package/src/lib/i18n.ts +161 -0
  161. package/src/lib/nocobase/acl/action.ts +147 -0
  162. package/src/lib/nocobase/acl/data-source.ts +24 -0
  163. package/src/lib/nocobase/acl/index.ts +6 -0
  164. package/src/lib/nocobase/acl/menu.ts +41 -0
  165. package/src/lib/nocobase/acl/record-permissions.ts +111 -0
  166. package/src/lib/nocobase/acl/store.ts +182 -0
  167. package/src/lib/nocobase/acl/types.ts +96 -0
  168. package/src/lib/nocobase/client.ts +279 -0
  169. package/src/lib/nocobase/error.ts +32 -0
  170. package/src/locales/en-US.ts +82 -0
  171. package/src/locales/index.ts +13 -0
  172. package/src/locales/zh-CN.ts +79 -0
  173. package/src/pages/users/create.tsx +98 -0
  174. package/src/pages/users/edit.tsx +92 -0
  175. package/src/pages/users/form-context.ts +57 -0
  176. package/src/pages/users/form-fields.tsx +211 -0
  177. package/src/pages/users/list.tsx +205 -0
  178. package/src/pages/users/show.tsx +187 -0
  179. package/src/pages/users/types.ts +20 -0
  180. package/src/providers/access-control.ts +34 -0
  181. package/src/providers/auth.ts +74 -60
  182. package/src/providers/constants.ts +3 -37
  183. package/src/providers/data.ts +242 -85
  184. package/vite.config.ts +17 -0
  185. package/src/pages/blog-posts/create.tsx +0 -177
  186. package/src/pages/blog-posts/edit.tsx +0 -197
  187. package/src/pages/blog-posts/list.tsx +0 -131
  188. package/src/pages/blog-posts/show.tsx +0 -81
  189. package/src/pages/categories/create.tsx +0 -73
  190. package/src/pages/categories/edit.tsx +0 -73
  191. package/src/pages/categories/index.ts +0 -4
  192. package/src/pages/categories/list.tsx +0 -86
  193. package/src/pages/categories/show.tsx +0 -37
  194. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -0,0 +1,67 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { Bot, ChevronDown, LoaderCircle } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { useAI } from "../../providers";
5
+ import { AIEmployeeAvatar } from "../chat/ai-employee-avatar";
6
+ import { getNocoBaseToolCallMetadata } from "../chat/tool-call-card";
7
+ import type { AIToolRendererProps } from "./tool-renderer-provider";
8
+ import { asRecord, asString } from "./tool-renderer-utils";
9
+
10
+ export function SubAgentRenderer({ part }: AIToolRendererProps) {
11
+ const { employees } = useAI();
12
+ const input = asRecord(part.input);
13
+ const username = asString(input.username);
14
+ const employee = employees.find((item) => item.username === username);
15
+ const fallbackName = username
16
+ ? `${username.charAt(0).toUpperCase()}${username.slice(1)}`
17
+ : "AI employee";
18
+ const [expanded, setExpanded] = useState(false);
19
+ const question = asString(input.question);
20
+ const metadata = getNocoBaseToolCallMetadata(part);
21
+ const generating =
22
+ part.state !== "output-available" &&
23
+ part.state !== "output-error" &&
24
+ !["done", "confirmed"].includes(metadata?.invokeStatus ?? "");
25
+
26
+ return (
27
+ <button
28
+ type="button"
29
+ className="w-full rounded-lg bg-muted/50 p-2.5 text-left"
30
+ onClick={() => question && setExpanded((value) => !value)}
31
+ >
32
+ <div className="flex items-center gap-2.5">
33
+ {employee ? (
34
+ <AIEmployeeAvatar employee={employee} className="size-8" />
35
+ ) : (
36
+ <span className="flex size-8 items-center justify-center rounded-full border bg-background">
37
+ <Bot className="size-4" />
38
+ </span>
39
+ )}
40
+ <div className="min-w-0 flex-1">
41
+ <div className="truncate text-sm font-medium">
42
+ @{employee?.nickname || fallbackName}
43
+ </div>
44
+ <div className="truncate text-xs text-muted-foreground">
45
+ {employee?.position || "Working on a delegated task"}
46
+ </div>
47
+ </div>
48
+ {question ? (
49
+ <ChevronDown
50
+ className={cn(
51
+ "size-4 text-muted-foreground transition-transform",
52
+ expanded && "rotate-180"
53
+ )}
54
+ />
55
+ ) : null}
56
+ {generating ? (
57
+ <LoaderCircle className="size-4 shrink-0 animate-spin text-muted-foreground" />
58
+ ) : null}
59
+ </div>
60
+ {expanded ? (
61
+ <p className="mt-2 border-t pt-2 text-xs leading-5 text-muted-foreground">
62
+ {question}
63
+ </p>
64
+ ) : null}
65
+ </button>
66
+ );
67
+ }
@@ -0,0 +1,59 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { cn } from "@/lib/utils";
3
+ import { useState } from "react";
4
+ import { getNocoBaseToolCallMetadata } from "../chat/tool-call-card";
5
+ import type { AIToolRendererProps } from "./tool-renderer-provider";
6
+ import { asRecord, parseArray } from "./tool-renderer-utils";
7
+
8
+ export function SuggestionsRenderer({
9
+ part,
10
+ disabled,
11
+ onEdit,
12
+ }: AIToolRendererProps) {
13
+ const input = asRecord(part.input);
14
+ const metadata = getNocoBaseToolCallMetadata(part);
15
+ const options = parseArray(input.options).filter(
16
+ (option): option is string => typeof option === "string"
17
+ );
18
+ const [selected, setSelected] = useState<string>();
19
+ const persistedSelection = metadata?.selectedSuggestion;
20
+ const hasSelected =
21
+ selected !== undefined || persistedSelection !== undefined;
22
+ const canSelect =
23
+ metadata?.invokeStatus === undefined ||
24
+ metadata.invokeStatus === "interrupted";
25
+
26
+ if (!options.length) {
27
+ return (
28
+ <p className="text-xs text-muted-foreground">Generating suggestions…</p>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <div className="flex flex-wrap gap-2">
34
+ {options.map((option) => (
35
+ <Button
36
+ key={option}
37
+ variant="outline"
38
+ size="sm"
39
+ className={cn(
40
+ "h-auto min-h-8 whitespace-normal text-left",
41
+ (selected === option || persistedSelection === option) &&
42
+ "border-2 border-dashed bg-muted"
43
+ )}
44
+ disabled={disabled || hasSelected || !canSelect}
45
+ onClick={() => {
46
+ setSelected(option);
47
+ void Promise.resolve(onEdit({ ...input, option })).catch(() => {
48
+ setSelected((current) =>
49
+ current === option ? undefined : current
50
+ );
51
+ });
52
+ }}
53
+ >
54
+ {option}
55
+ </Button>
56
+ ))}
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useMemo,
5
+ type ComponentType,
6
+ type PropsWithChildren,
7
+ } from "react";
8
+ import type { ToolCallPart } from "../chat/tool-call-card";
9
+ import { builtInToolRenderers } from "./builtin-tool-renderers";
10
+ import { BusinessReportDialogProvider } from "./business-report-dialog";
11
+
12
+ export type AIToolRendererProps = {
13
+ part: ToolCallPart;
14
+ disabled: boolean;
15
+ onEdit: (input: unknown) => void | Promise<void>;
16
+ onApprove: () => void | Promise<void>;
17
+ onReject: (message?: string) => void | Promise<void>;
18
+ onRevise: () => void;
19
+ };
20
+
21
+ export type AIToolRenderer = ComponentType<AIToolRendererProps>;
22
+ export type AIToolRendererDefinition = {
23
+ component: AIToolRenderer;
24
+ handlesApproval?: boolean;
25
+ standalone?: boolean;
26
+ };
27
+ export type AIToolRendererEntry = AIToolRenderer | AIToolRendererDefinition;
28
+ export type AIToolRendererMap = Record<string, AIToolRendererEntry>;
29
+
30
+ const AIToolRendererContext =
31
+ createContext<AIToolRendererMap>(builtInToolRenderers);
32
+
33
+ export function AIToolRendererProvider({
34
+ renderers,
35
+ children,
36
+ }: PropsWithChildren<{ renderers?: AIToolRendererMap }>) {
37
+ const value = useMemo(
38
+ () => ({ ...builtInToolRenderers, ...renderers }),
39
+ [renderers]
40
+ );
41
+
42
+ return (
43
+ <BusinessReportDialogProvider>
44
+ <AIToolRendererContext.Provider value={value}>
45
+ {children}
46
+ </AIToolRendererContext.Provider>
47
+ </BusinessReportDialogProvider>
48
+ );
49
+ }
50
+
51
+ export function useAIToolRenderer(toolName: string) {
52
+ const entry = useContext(AIToolRendererContext)[toolName];
53
+ if (!entry) return undefined;
54
+ return typeof entry === "function" ? { component: entry } : entry;
55
+ }
@@ -0,0 +1,18 @@
1
+ export const asRecord = (value: unknown): Record<string, unknown> =>
2
+ value && typeof value === "object" && !Array.isArray(value)
3
+ ? (value as Record<string, unknown>)
4
+ : {};
5
+
6
+ export const asString = (value: unknown) =>
7
+ typeof value === "string" ? value : "";
8
+
9
+ export const parseArray = (value: unknown): unknown[] => {
10
+ if (Array.isArray(value)) return value;
11
+ if (typeof value !== "string") return [];
12
+ try {
13
+ const parsed = JSON.parse(value) as unknown;
14
+ return Array.isArray(parsed) ? parsed : [];
15
+ } catch {
16
+ return [];
17
+ }
18
+ };
@@ -0,0 +1,108 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import { CheckCircle2, GitBranch } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { MarkdownMessage } from "../chat/markdown-message";
5
+ import { getNocoBaseToolCallMetadata } from "../chat/tool-call-card";
6
+ import type { AIToolRendererProps } from "./tool-renderer-provider";
7
+ import { asRecord, asString } from "./tool-renderer-utils";
8
+
9
+ export function WorkflowRenderer({
10
+ part,
11
+ disabled,
12
+ onApprove,
13
+ onReject,
14
+ onRevise,
15
+ }: AIToolRendererProps) {
16
+ const input = asRecord(part.input);
17
+ const metadata = getNocoBaseToolCallMetadata(part);
18
+ const entries = Object.entries(asRecord(input.result));
19
+ const [action, setAction] = useState<"approve" | "reject" | "revise">();
20
+ const [decided, setDecided] = useState(false);
21
+ const canDecide =
22
+ metadata?.invokeStatus === undefined ||
23
+ metadata.invokeStatus === "interrupted";
24
+ const actionDisabled =
25
+ disabled || action !== undefined || decided || !canDecide;
26
+
27
+ const runAction = async (
28
+ nextAction: "approve" | "reject",
29
+ callback: () => void | Promise<void>
30
+ ) => {
31
+ setAction(nextAction);
32
+ try {
33
+ await callback();
34
+ setDecided(true);
35
+ } finally {
36
+ setAction(undefined);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div className="rounded-lg border bg-background p-3">
42
+ <div className="flex items-center gap-2 text-sm font-medium">
43
+ <GitBranch className="size-4" />
44
+ {asString(input.workflowTitle) || "Workflow task"}
45
+ </div>
46
+ <div className="mt-3 grid gap-2 sm:grid-cols-2">
47
+ {entries.map(([key, value]) => (
48
+ <div key={key} className="rounded-md bg-muted/40 px-2.5 py-2">
49
+ <div className="text-[11px] uppercase tracking-wide text-muted-foreground">
50
+ {key}
51
+ </div>
52
+ <div className="mt-0.5 overflow-x-auto text-xs font-medium">
53
+ {typeof value === "string" ||
54
+ typeof value === "number" ||
55
+ typeof value === "boolean" ? (
56
+ <MarkdownMessage>{String(value)}</MarkdownMessage>
57
+ ) : (
58
+ <pre className="whitespace-pre-wrap break-words font-mono text-[11px] leading-5">
59
+ {JSON.stringify(value, null, 2)}
60
+ </pre>
61
+ )}
62
+ </div>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ {!entries.length ? (
67
+ <div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
68
+ <CheckCircle2 className="size-3.5" /> Ready for review
69
+ </div>
70
+ ) : null}
71
+ <div className="mt-3 flex flex-wrap justify-end gap-2 border-t pt-3">
72
+ <Button
73
+ variant="ghost"
74
+ size="sm"
75
+ disabled={actionDisabled}
76
+ onClick={() =>
77
+ void runAction("reject", () =>
78
+ onReject(
79
+ "The user rejected this workflow node. Stop. Do not continue, do not reply about the task result, and do not call this tool again. Only state that you understand."
80
+ )
81
+ )
82
+ }
83
+ >
84
+ {action === "reject" ? "Rejecting…" : "Reject"}
85
+ </Button>
86
+ <Button
87
+ variant="outline"
88
+ size="sm"
89
+ disabled={actionDisabled}
90
+ onClick={() => {
91
+ setAction("revise");
92
+ onRevise();
93
+ setAction(undefined);
94
+ }}
95
+ >
96
+ Revise
97
+ </Button>
98
+ <Button
99
+ size="sm"
100
+ disabled={actionDisabled}
101
+ onClick={() => void runAction("approve", onApprove)}
102
+ >
103
+ {action === "approve" ? "Approving…" : "Approve"}
104
+ </Button>
105
+ </div>
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,73 @@
1
+ import nocobaseAIChatIcon from "../../assets/nocobase-ai-chat.svg";
2
+ import { cn } from "@/lib/utils";
3
+ import {
4
+ useAI,
5
+ useAIChatControllerState,
6
+ useGlobalAIChatController,
7
+ } from "../../providers";
8
+ import type { AIChatController } from "../../providers";
9
+
10
+ export type AIChatFloatingTriggerProps = {
11
+ aiEmployee?: string;
12
+ controller?: AIChatController;
13
+ unreadCount?: number;
14
+ position?: "fixed" | "absolute";
15
+ hideWhenOpen?: boolean;
16
+ className?: string;
17
+ };
18
+
19
+ export function AIChatFloatingTrigger({
20
+ aiEmployee,
21
+ controller: providedController,
22
+ unreadCount = 0,
23
+ position = "fixed",
24
+ hideWhenOpen = true,
25
+ className,
26
+ }: AIChatFloatingTriggerProps) {
27
+ const ai = useAI();
28
+ const globalController = useGlobalAIChatController();
29
+ const controller = providedController ?? globalController;
30
+ const { open } = useAIChatControllerState(controller);
31
+
32
+ if (hideWhenOpen && open) return null;
33
+
34
+ const openChat = () => {
35
+ const employee = aiEmployee ?? ai.employees[0]?.username;
36
+ if (employee) {
37
+ controller.triggerTask({ aiEmployee: employee, open: true });
38
+ return;
39
+ }
40
+ controller.open();
41
+ };
42
+
43
+ return (
44
+ <button
45
+ type="button"
46
+ aria-label="Open AI chat"
47
+ className={cn(
48
+ "group/ai-floating z-40 flex items-center rounded-l-full border bg-background py-2.5 pr-5 pl-3 shadow-lg transition-[transform,opacity,box-shadow] duration-300 hover:translate-x-0 hover:opacity-100 hover:shadow-xl focus-visible:translate-x-0 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
49
+ position === "fixed"
50
+ ? "fixed right-0 bottom-10"
51
+ : "absolute right-0 bottom-8",
52
+ "translate-x-2 opacity-80",
53
+ className
54
+ )}
55
+ onClick={openChat}
56
+ >
57
+ <span className="relative flex size-[42px] items-center justify-center">
58
+ <span className="flex size-full overflow-hidden rounded-lg">
59
+ <img
60
+ src={nocobaseAIChatIcon}
61
+ alt=""
62
+ className="size-full object-contain"
63
+ />
64
+ </span>
65
+ {unreadCount > 0 ? (
66
+ <span className="absolute -top-1.5 -right-1.5 z-10 flex min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] leading-4 font-semibold text-white ring-2 ring-background">
67
+ {unreadCount > 99 ? "99+" : unreadCount}
68
+ </span>
69
+ ) : null}
70
+ </span>
71
+ </button>
72
+ );
73
+ }
@@ -0,0 +1,149 @@
1
+ import {
2
+ HoverCard,
3
+ HoverCardContent,
4
+ HoverCardTrigger,
5
+ } from "@/components/ui/hover-card";
6
+ import { Button } from "@/components/ui/button";
7
+ import { cn } from "@/lib/utils";
8
+ import {
9
+ useAI,
10
+ useAIPageContextScope,
11
+ useGlobalAIChatController,
12
+ type AIChatController,
13
+ type AIEmployee,
14
+ type AIEmployeeTask,
15
+ type AIWorkContextItem,
16
+ } from "../../providers";
17
+ import { AIEmployeeAvatar } from "../chat/ai-employee-avatar";
18
+ import { Send, TextCursorInput } from "lucide-react";
19
+ import { useState } from "react";
20
+
21
+ export type AIEmployeeShortcutProps = {
22
+ aiEmployee: string | AIEmployee;
23
+ tasks?: AIEmployeeTask[];
24
+ context?: AIWorkContextItem[];
25
+ target?: AIChatController;
26
+ auto?: boolean;
27
+ size?: number;
28
+ label?: string;
29
+ showNotice?: boolean;
30
+ className?: string;
31
+ onTrigger?: (task?: AIEmployeeTask) => void;
32
+ };
33
+
34
+ export function AIEmployeeShortcut({
35
+ aiEmployee,
36
+ tasks = [],
37
+ context,
38
+ target,
39
+ auto,
40
+ size = 48,
41
+ label,
42
+ showNotice = false,
43
+ className,
44
+ onTrigger,
45
+ }: AIEmployeeShortcutProps) {
46
+ const ai = useAI();
47
+ const [focused, setFocused] = useState(false);
48
+ const globalController = useGlobalAIChatController();
49
+ const inheritedContext = useAIPageContextScope();
50
+ const controller = target ?? globalController;
51
+ const employee =
52
+ typeof aiEmployee === "string"
53
+ ? ai.employees.find((item) => item.username === aiEmployee)
54
+ : ai.employees.find((item) => item.username === aiEmployee.username) ??
55
+ aiEmployee;
56
+
57
+ if (!employee) return null;
58
+
59
+ const trigger = (task?: AIEmployeeTask) => {
60
+ onTrigger?.(task);
61
+ controller.triggerTask({
62
+ aiEmployee: employee,
63
+ task,
64
+ tasks: task ? undefined : tasks,
65
+ context: context?.length ? context : inheritedContext,
66
+ auto,
67
+ open: true,
68
+ });
69
+ };
70
+
71
+ const visibleTasks = tasks.filter((task) => task.title);
72
+
73
+ return (
74
+ <HoverCard>
75
+ <HoverCardTrigger
76
+ render={
77
+ <button
78
+ type="button"
79
+ className={cn(
80
+ "group/ai-shortcut relative inline-flex items-center gap-2 rounded-full outline-none transition-transform hover:scale-[1.04] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
81
+ label && "border bg-background pr-3 shadow-sm",
82
+ className
83
+ )}
84
+ onClick={() => trigger()}
85
+ onMouseEnter={() => setFocused(true)}
86
+ onMouseLeave={() => setFocused(false)}
87
+ />
88
+ }
89
+ >
90
+ <AIEmployeeAvatar
91
+ employee={employee}
92
+ flip={focused || showNotice}
93
+ className="transition-[filter] group-hover/ai-shortcut:brightness-105"
94
+ style={{ width: size, height: size }}
95
+ />
96
+ {label ? <span className="text-sm font-medium">{label}</span> : null}
97
+ {showNotice ? (
98
+ <span className="absolute top-0 right-0 size-2.5 rounded-full border-2 border-background bg-foreground" />
99
+ ) : null}
100
+ </HoverCardTrigger>
101
+ <HoverCardContent side="top" align="start" className="w-72 p-3">
102
+ <div className="flex items-center gap-3">
103
+ <AIEmployeeAvatar employee={employee} className="size-10" />
104
+ <div className="min-w-0">
105
+ <div className="truncate text-sm font-medium">
106
+ {employee.nickname}
107
+ </div>
108
+ {employee.position ? (
109
+ <div className="truncate text-xs text-muted-foreground">
110
+ {employee.position}
111
+ </div>
112
+ ) : null}
113
+ </div>
114
+ </div>
115
+ {employee.bio ?? employee.description ? (
116
+ <p className="mt-3 border-t pt-3 text-xs leading-5 text-muted-foreground">
117
+ {employee.bio ?? employee.description}
118
+ </p>
119
+ ) : null}
120
+ {visibleTasks.length ? (
121
+ <div className="mt-3 flex flex-wrap gap-1.5 border-t pt-3">
122
+ {visibleTasks.map((task, index) => (
123
+ <Button
124
+ key={`${task.title}-${index}`}
125
+ variant="secondary"
126
+ size="sm"
127
+ className="h-auto min-h-7 whitespace-normal py-1 text-left"
128
+ onClick={(event) => {
129
+ event.stopPropagation();
130
+ trigger(task);
131
+ }}
132
+ >
133
+ {task.autoSend ? (
134
+ <Send className="size-3.5" />
135
+ ) : (
136
+ <TextCursorInput className="size-3.5" />
137
+ )}
138
+ <span>{task.title}</span>
139
+ <span className="text-[10px] text-muted-foreground">
140
+ {task.autoSend ? "Auto send" : "Fill composer"}
141
+ </span>
142
+ </Button>
143
+ ))}
144
+ </div>
145
+ ) : null}
146
+ </HoverCardContent>
147
+ </HoverCard>
148
+ );
149
+ }
@@ -0,0 +1,49 @@
1
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { LoadingState } from "@/components/app-shell/loading-state";
4
+ import { useAI } from "../providers";
5
+ import { CircleAlert } from "lucide-react";
6
+ import type { ReactNode } from "react";
7
+
8
+ export function AIConfigurationGate({ children }: { children: ReactNode }) {
9
+ const {
10
+ configurationStatus,
11
+ configurationError,
12
+ modelConfigurationError,
13
+ hasEnabledModels,
14
+ } = useAI();
15
+
16
+ if (configurationStatus === "ready" && hasEnabledModels) return children;
17
+
18
+ const loading = configurationStatus === "loading";
19
+ const error = configurationError ?? modelConfigurationError;
20
+
21
+ if (loading) return <LoadingState className="min-h-80" />;
22
+
23
+ return (
24
+ <div className="space-y-6 pb-12">
25
+ <section className="border-b pb-8">
26
+ <div className="flex items-center gap-2">
27
+ <Badge variant="secondary">AI Components</Badge>
28
+ <Badge variant="outline">Live API</Badge>
29
+ </div>
30
+ <h1 className="mt-4 text-3xl font-semibold tracking-[-0.035em]">
31
+ NocoBase AI
32
+ </h1>
33
+ <p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
34
+ Conversations use the AI employees and enabled models configured in
35
+ the connected NocoBase application.
36
+ </p>
37
+ </section>
38
+
39
+ <Alert variant="destructive">
40
+ <CircleAlert />
41
+ <AlertTitle>NocoBase AI is not available</AlertTitle>
42
+ <AlertDescription>
43
+ {error?.message ??
44
+ "Check the NocoBase connection and the AI employees available to the current user."}
45
+ </AlertDescription>
46
+ </Alert>
47
+ </div>
48
+ );
49
+ }