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