@nocobase/portal-template-default 1.0.3 → 1.0.5
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 +10 -5
- package/package.json +17 -17
- package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
- package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
- package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
- package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
- package/registry/nocobase-i18n/README.md +23 -32
- package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
- package/registry/nocobase-i18n/demo/index.tsx +2 -2
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
- package/registry/nocobase-i18n/extension.tsx +3 -4
- package/registry/nocobase-i18n/index.ts +4 -3
- package/registry/nocobase-i18n/locales/en-US.ts +2 -52
- package/registry/nocobase-i18n/locales/index.ts +8 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
- package/registry/nocobase-i18n/provider.tsx +18 -6
- package/registry/nocobase-i18n/server-resources.ts +13 -31
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
- package/registry/nocobase-route-surfaces/README.md +21 -0
- package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
- package/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx +2 -1
- package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
- package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
- package/registry/nocobase-route-surfaces/extension.tsx +119 -0
- package/registry/nocobase-route-surfaces/index.ts +7 -0
- package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
- package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
- package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
- package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
- package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
- package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
- package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
- package/registry.config.json +30 -3
- package/scripts/registry.mjs +0 -2
- package/src/App.tsx +65 -35
- package/src/app/extension.ts +1 -2
- package/src/app/extensions.tsx +0 -14
- package/src/components/app-shell/breadcrumb.tsx +67 -2
- package/src/components/app-shell/sidebar.tsx +2 -2
- package/src/components/resources/views/list-view.tsx +3 -2
- package/src/components/ui/sheet.tsx +22 -21
- package/src/lib/nocobase/client.ts +6 -1
- package/src/locales/en-US.ts +81 -0
- package/src/locales/zh-CN.ts +77 -0
- package/src/pages/users/create.tsx +76 -53
- package/src/pages/users/edit.tsx +87 -56
- package/src/pages/users/index.ts +3 -0
- package/src/pages/users/layout.tsx +15 -0
- package/src/pages/users/list.tsx +19 -21
- package/src/pages/users/role-badges.tsx +43 -0
- package/src/pages/users/role-detail.tsx +272 -0
- package/src/pages/users/role-utils.ts +6 -0
- package/src/pages/users/routes.ts +24 -0
- package/src/pages/users/show.tsx +195 -104
- package/src/pages/users/types.ts +13 -0
- package/src/providers/i18n/index.ts +3 -0
- package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
- package/src/providers/system-settings/context.ts +26 -0
- package/src/providers/system-settings/index.ts +2 -0
- package/src/providers/system-settings/provider.tsx +94 -0
- package/src/extensions/nocobase-acl/README.md +0 -19
- package/src/extensions/nocobase-acl/components/acl-boundary.tsx +0 -106
- package/src/extensions/nocobase-acl/components/index.ts +0 -3
- package/src/extensions/nocobase-acl/components/role-options.ts +0 -36
- package/src/extensions/nocobase-acl/components/role-switcher.tsx +0 -135
- package/src/extensions/nocobase-acl/demo/boundary-api.tsx +0 -186
- package/src/extensions/nocobase-acl/demo/components.tsx +0 -231
- package/src/extensions/nocobase-acl/demo/index.tsx +0 -505
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +0 -134
- package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -198
- package/src/extensions/nocobase-acl/demo/scenario-section.tsx +0 -37
- package/src/extensions/nocobase-acl/extension.tsx +0 -71
- package/src/extensions/nocobase-acl/index.ts +0 -1
- package/src/extensions/nocobase-ai/README.md +0 -70
- package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +0 -20
- package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +0 -50
- package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +0 -32
- package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -31
- package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +0 -92
- package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +0 -90
- package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +0 -429
- package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +0 -51
- package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +0 -88
- package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -77
- package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +0 -252
- package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +0 -173
- package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +0 -118
- package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +0 -343
- package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +0 -165
- package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +0 -30
- package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +0 -29
- package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -142
- package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +0 -316
- package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -99
- package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +0 -114
- package/src/extensions/nocobase-ai/components/index.ts +0 -59
- package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +0 -44
- package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -469
- package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
- package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +0 -18
- package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +0 -18
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -54
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
- package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -54
- package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -31
- package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +0 -364
- package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +0 -122
- package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +0 -282
- package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +0 -99
- package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +0 -59
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -18
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -20
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -34
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -24
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +0 -138
- package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -67
- package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -59
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -55
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -18
- package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +0 -108
- package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +0 -73
- package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -149
- package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +0 -49
- package/src/extensions/nocobase-ai/demo/container-showcase.tsx +0 -210
- package/src/extensions/nocobase-ai/demo/floating.tsx +0 -134
- package/src/extensions/nocobase-ai/demo/index.tsx +0 -346
- package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +0 -151
- package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -355
- package/src/extensions/nocobase-ai/demo/page-context.tsx +0 -884
- package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +0 -197
- package/src/extensions/nocobase-ai/demo/prompt-card.tsx +0 -41
- package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +0 -452
- package/src/extensions/nocobase-ai/demo/shortcut.tsx +0 -1207
- package/src/extensions/nocobase-ai/demo/tool-cards.tsx +0 -530
- package/src/extensions/nocobase-ai/extension.tsx +0 -158
- package/src/extensions/nocobase-ai/global-ai-chat.tsx +0 -213
- package/src/extensions/nocobase-ai/index.ts +0 -3
- package/src/extensions/nocobase-ai/providers/ai-provider.tsx +0 -323
- package/src/extensions/nocobase-ai/providers/avatars.ts +0 -817
- package/src/extensions/nocobase-ai/providers/chat-context.tsx +0 -109
- package/src/extensions/nocobase-ai/providers/chat-controller.ts +0 -84
- package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +0 -198
- package/src/extensions/nocobase-ai/providers/chat-provider.tsx +0 -871
- package/src/extensions/nocobase-ai/providers/chat-reducer.ts +0 -147
- package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +0 -105
- package/src/extensions/nocobase-ai/providers/chat-transport.ts +0 -184
- package/src/extensions/nocobase-ai/providers/form-registry.tsx +0 -271
- package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +0 -231
- package/src/extensions/nocobase-ai/providers/index.ts +0 -61
- package/src/extensions/nocobase-ai/providers/model.ts +0 -32
- package/src/extensions/nocobase-ai/providers/page-context.tsx +0 -136
- package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +0 -60
- package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +0 -82
- package/src/extensions/nocobase-ai/providers/stream-parser.ts +0 -61
- package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +0 -316
- package/src/extensions/nocobase-ai/providers/types.ts +0 -183
- package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +0 -463
- package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -94
- package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +0 -206
- package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +0 -554
- package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +0 -210
- package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +0 -93
- package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +0 -108
- package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +0 -178
- package/src/extensions/nocobase-ai/services/index.ts +0 -7
- package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +0 -454
- package/src/extensions/nocobase-ai/services/types.ts +0 -82
- /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
- /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { AI_DRAFT_CONVERSATION_ID, type AIConversation } from "./types";
|
|
2
|
-
|
|
3
|
-
export type AIChatState = {
|
|
4
|
-
activeConversationId: string;
|
|
5
|
-
conversations: AIConversation[];
|
|
6
|
-
conversationListOpen: boolean;
|
|
7
|
-
drafts: Record<string, string>;
|
|
8
|
-
selectedEmployeeUsername: string;
|
|
9
|
-
selectedModel: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type AIChatAction =
|
|
13
|
-
| { type: "reset"; state: AIChatState }
|
|
14
|
-
| { type: "set-active-conversation"; conversationId: string }
|
|
15
|
-
| { type: "set-conversation-list-open"; open: boolean }
|
|
16
|
-
| { type: "set-draft"; conversationId: string; value: string }
|
|
17
|
-
| { type: "select-employee"; username: string }
|
|
18
|
-
| { type: "select-model"; model: string }
|
|
19
|
-
| { type: "start-new-conversation" }
|
|
20
|
-
| { type: "add-conversation"; conversation: AIConversation }
|
|
21
|
-
| { type: "set-conversations"; conversations: AIConversation[] }
|
|
22
|
-
| { type: "remove-conversation"; conversationId: string }
|
|
23
|
-
| { type: "mark-conversation-read"; conversationId: string }
|
|
24
|
-
| { type: "rename-conversation"; conversationId: string; title: string }
|
|
25
|
-
| { type: "replace-conversation-id"; from: string; to: string };
|
|
26
|
-
|
|
27
|
-
export function createAIChatState({
|
|
28
|
-
conversations,
|
|
29
|
-
employeeUsername,
|
|
30
|
-
model,
|
|
31
|
-
}: {
|
|
32
|
-
conversations: AIConversation[];
|
|
33
|
-
employeeUsername: string;
|
|
34
|
-
model: string;
|
|
35
|
-
}): AIChatState {
|
|
36
|
-
return {
|
|
37
|
-
activeConversationId: conversations[0]?.id ?? AI_DRAFT_CONVERSATION_ID,
|
|
38
|
-
conversations,
|
|
39
|
-
conversationListOpen: false,
|
|
40
|
-
drafts: {},
|
|
41
|
-
selectedEmployeeUsername: employeeUsername,
|
|
42
|
-
selectedModel: model,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function aiChatReducer(
|
|
47
|
-
state: AIChatState,
|
|
48
|
-
action: AIChatAction
|
|
49
|
-
): AIChatState {
|
|
50
|
-
switch (action.type) {
|
|
51
|
-
case "reset":
|
|
52
|
-
return action.state;
|
|
53
|
-
case "set-active-conversation":
|
|
54
|
-
return {
|
|
55
|
-
...state,
|
|
56
|
-
activeConversationId: action.conversationId,
|
|
57
|
-
conversationListOpen: false,
|
|
58
|
-
conversations: state.conversations.map((conversation) =>
|
|
59
|
-
conversation.id === action.conversationId
|
|
60
|
-
? { ...conversation, unread: false }
|
|
61
|
-
: conversation
|
|
62
|
-
),
|
|
63
|
-
};
|
|
64
|
-
case "set-conversation-list-open":
|
|
65
|
-
return { ...state, conversationListOpen: action.open };
|
|
66
|
-
case "set-draft":
|
|
67
|
-
return {
|
|
68
|
-
...state,
|
|
69
|
-
drafts: { ...state.drafts, [action.conversationId]: action.value },
|
|
70
|
-
};
|
|
71
|
-
case "select-employee":
|
|
72
|
-
return { ...state, selectedEmployeeUsername: action.username };
|
|
73
|
-
case "select-model":
|
|
74
|
-
return { ...state, selectedModel: action.model };
|
|
75
|
-
case "start-new-conversation":
|
|
76
|
-
return {
|
|
77
|
-
...state,
|
|
78
|
-
activeConversationId: AI_DRAFT_CONVERSATION_ID,
|
|
79
|
-
conversationListOpen: false,
|
|
80
|
-
drafts: { ...state.drafts, [AI_DRAFT_CONVERSATION_ID]: "" },
|
|
81
|
-
};
|
|
82
|
-
case "add-conversation":
|
|
83
|
-
return {
|
|
84
|
-
...state,
|
|
85
|
-
activeConversationId: action.conversation.id,
|
|
86
|
-
conversations: [
|
|
87
|
-
action.conversation,
|
|
88
|
-
...state.conversations.filter(
|
|
89
|
-
(conversation) => conversation.id !== action.conversation.id
|
|
90
|
-
),
|
|
91
|
-
],
|
|
92
|
-
drafts: {
|
|
93
|
-
...state.drafts,
|
|
94
|
-
[action.conversation.id]:
|
|
95
|
-
state.drafts[AI_DRAFT_CONVERSATION_ID] ?? "",
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
case "set-conversations":
|
|
99
|
-
return {
|
|
100
|
-
...state,
|
|
101
|
-
conversations: action.conversations,
|
|
102
|
-
};
|
|
103
|
-
case "mark-conversation-read":
|
|
104
|
-
return {
|
|
105
|
-
...state,
|
|
106
|
-
conversations: state.conversations.map((conversation) =>
|
|
107
|
-
conversation.id === action.conversationId
|
|
108
|
-
? { ...conversation, unread: false }
|
|
109
|
-
: conversation
|
|
110
|
-
),
|
|
111
|
-
};
|
|
112
|
-
case "remove-conversation": {
|
|
113
|
-
const conversations = state.conversations.filter(
|
|
114
|
-
(conversation) => conversation.id !== action.conversationId
|
|
115
|
-
);
|
|
116
|
-
return { ...state, conversations };
|
|
117
|
-
}
|
|
118
|
-
case "rename-conversation":
|
|
119
|
-
return {
|
|
120
|
-
...state,
|
|
121
|
-
conversations: state.conversations.map((conversation) =>
|
|
122
|
-
conversation.id === action.conversationId
|
|
123
|
-
? { ...conversation, title: action.title }
|
|
124
|
-
: conversation
|
|
125
|
-
),
|
|
126
|
-
};
|
|
127
|
-
case "replace-conversation-id":
|
|
128
|
-
return {
|
|
129
|
-
...state,
|
|
130
|
-
activeConversationId:
|
|
131
|
-
state.activeConversationId === action.from
|
|
132
|
-
? action.to
|
|
133
|
-
: state.activeConversationId,
|
|
134
|
-
conversations: state.conversations.map((conversation) =>
|
|
135
|
-
conversation.id === action.from
|
|
136
|
-
? { ...conversation, id: action.to }
|
|
137
|
-
: conversation
|
|
138
|
-
),
|
|
139
|
-
drafts: {
|
|
140
|
-
...state.drafts,
|
|
141
|
-
[action.to]: state.drafts[action.from] ?? "",
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
default:
|
|
145
|
-
return state;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { getAIWorkContextRequiredTools, mergeAIRequiredTools } from "./page-context";
|
|
2
|
-
import type {
|
|
3
|
-
AIChatTaskRuntime,
|
|
4
|
-
AIEmployee,
|
|
5
|
-
AIEmployeeTask,
|
|
6
|
-
AIEmployeeTasks,
|
|
7
|
-
AIEmployeeTaskTrigger,
|
|
8
|
-
AIModel,
|
|
9
|
-
AIWorkContextItem,
|
|
10
|
-
} from "./types";
|
|
11
|
-
|
|
12
|
-
const EMPTY_TASKS: AIEmployeeTask[] = [];
|
|
13
|
-
|
|
14
|
-
export type AIChatTaskSet = {
|
|
15
|
-
employeeUsername: string;
|
|
16
|
-
tasks: AIEmployeeTask[];
|
|
17
|
-
context?: AIWorkContextItem[];
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function getConfiguredAIChatTaskSet({
|
|
21
|
-
employeeUsername,
|
|
22
|
-
defaultEmployeeUsername,
|
|
23
|
-
defaultTasks,
|
|
24
|
-
employeeTasks,
|
|
25
|
-
inheritedContext,
|
|
26
|
-
}: {
|
|
27
|
-
employeeUsername: string;
|
|
28
|
-
defaultEmployeeUsername: string;
|
|
29
|
-
defaultTasks: AIEmployeeTask[];
|
|
30
|
-
employeeTasks: AIEmployeeTasks;
|
|
31
|
-
inheritedContext: AIWorkContextItem[];
|
|
32
|
-
}): AIChatTaskSet | undefined {
|
|
33
|
-
const tasks =
|
|
34
|
-
employeeTasks[employeeUsername] ??
|
|
35
|
-
(employeeUsername === defaultEmployeeUsername
|
|
36
|
-
? defaultTasks
|
|
37
|
-
: EMPTY_TASKS);
|
|
38
|
-
return tasks.length
|
|
39
|
-
? { employeeUsername, tasks, context: inheritedContext }
|
|
40
|
-
: undefined;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function findTriggeredAIEmployee(
|
|
44
|
-
employees: AIEmployee[],
|
|
45
|
-
requested: AIEmployeeTaskTrigger["aiEmployee"]
|
|
46
|
-
) {
|
|
47
|
-
return typeof requested === "string"
|
|
48
|
-
? employees.find((item) => item.username === requested)
|
|
49
|
-
: employees.find((item) => item.username === requested.username) ??
|
|
50
|
-
requested;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function getTriggeredAIEmployeeTask(options: AIEmployeeTaskTrigger) {
|
|
54
|
-
return (
|
|
55
|
-
options.task ??
|
|
56
|
-
(options.tasks?.length === 1 && options.auto !== false
|
|
57
|
-
? options.tasks[0]
|
|
58
|
-
: undefined)
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function getTriggeredAIWorkContext(
|
|
63
|
-
options: AIEmployeeTaskTrigger,
|
|
64
|
-
task: AIEmployeeTask | undefined,
|
|
65
|
-
inheritedContext: AIWorkContextItem[]
|
|
66
|
-
) {
|
|
67
|
-
const taskContext = task?.message?.workContext ?? [];
|
|
68
|
-
return taskContext.length
|
|
69
|
-
? taskContext
|
|
70
|
-
: options.context?.length
|
|
71
|
-
? options.context
|
|
72
|
-
: inheritedContext;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function createAIChatTaskRuntime(
|
|
76
|
-
task: AIEmployeeTask | undefined,
|
|
77
|
-
workContext: AIWorkContextItem[]
|
|
78
|
-
): AIChatTaskRuntime {
|
|
79
|
-
const requiredTools = getAIWorkContextRequiredTools(workContext);
|
|
80
|
-
return task
|
|
81
|
-
? {
|
|
82
|
-
systemMessage: task.message?.system,
|
|
83
|
-
workContext: [],
|
|
84
|
-
skillSettings: mergeAIRequiredTools(task.skillSettings, requiredTools),
|
|
85
|
-
webSearch: task.webSearch,
|
|
86
|
-
}
|
|
87
|
-
: {
|
|
88
|
-
workContext: [],
|
|
89
|
-
skillSettings: mergeAIRequiredTools(undefined, requiredTools),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function findAIChatTaskModel(
|
|
94
|
-
models: AIModel[],
|
|
95
|
-
task: AIEmployeeTask | undefined
|
|
96
|
-
) {
|
|
97
|
-
return task?.model
|
|
98
|
-
? models.find(
|
|
99
|
-
(item) =>
|
|
100
|
-
item.value === task.model?.model &&
|
|
101
|
-
(!task.model.llmService ||
|
|
102
|
-
item.llmService === task.model.llmService)
|
|
103
|
-
)
|
|
104
|
-
: undefined;
|
|
105
|
-
}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import type { ChatTransport } from "ai";
|
|
2
|
-
import type { AIService } from "../services";
|
|
3
|
-
import type { AIChatMessage, AIChatRequestContext } from "./types";
|
|
4
|
-
import { createNocoBaseUIMessageStream } from "./ui-message-stream";
|
|
5
|
-
|
|
6
|
-
const messageText = (message?: AIChatMessage) =>
|
|
7
|
-
message?.parts
|
|
8
|
-
.filter((part) => part.type === "text")
|
|
9
|
-
.map((part) => part.text)
|
|
10
|
-
.join("\n") ?? "";
|
|
11
|
-
|
|
12
|
-
export class NocoBaseChatTransport implements ChatTransport<AIChatMessage> {
|
|
13
|
-
private pendingResend?: { messageId: string };
|
|
14
|
-
private pendingConversationResume?: { message?: AIChatMessage };
|
|
15
|
-
private pendingToolResume?: {
|
|
16
|
-
messageId: string;
|
|
17
|
-
responseMessageId: string;
|
|
18
|
-
toolCallIds: string[];
|
|
19
|
-
toolCallResults: Array<{ id: string; result: unknown }>;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
constructor(
|
|
23
|
-
private readonly options: {
|
|
24
|
-
service: AIService;
|
|
25
|
-
getContext: () => AIChatRequestContext;
|
|
26
|
-
onSessionCreated?: (sessionId: string) => void;
|
|
27
|
-
}
|
|
28
|
-
) {}
|
|
29
|
-
|
|
30
|
-
prepareResend(messageId: string) {
|
|
31
|
-
this.pendingResend = { messageId };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
cancelResend(messageId: string) {
|
|
35
|
-
if (this.pendingResend?.messageId === messageId) {
|
|
36
|
-
this.pendingResend = undefined;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
prepareConversationResume(messages: AIChatMessage[]) {
|
|
41
|
-
this.pendingConversationResume = {
|
|
42
|
-
message: [...messages]
|
|
43
|
-
.reverse()
|
|
44
|
-
.find((message) => message.role === "assistant"),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
prepareToolResume(
|
|
49
|
-
messageId: string,
|
|
50
|
-
responseMessageId: string,
|
|
51
|
-
toolCallIds: string[],
|
|
52
|
-
toolCallResults: Array<{ id: string; result: unknown }>
|
|
53
|
-
) {
|
|
54
|
-
this.pendingToolResume = {
|
|
55
|
-
messageId,
|
|
56
|
-
responseMessageId,
|
|
57
|
-
toolCallIds,
|
|
58
|
-
toolCallResults,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
cancelToolResume(messageId: string) {
|
|
63
|
-
if (this.pendingToolResume?.messageId === messageId) {
|
|
64
|
-
this.pendingToolResume = undefined;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async sendMessages({
|
|
69
|
-
messages,
|
|
70
|
-
abortSignal,
|
|
71
|
-
}: Parameters<ChatTransport<AIChatMessage>["sendMessages"]>[0]) {
|
|
72
|
-
const context = this.options.getContext();
|
|
73
|
-
if (context.model.configured === false) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
"No enabled LLM model is configured in NocoBase. Configure and enable an LLM service before starting a conversation."
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
let sessionId = context.sessionId;
|
|
79
|
-
|
|
80
|
-
const pendingResend = this.pendingResend;
|
|
81
|
-
this.pendingResend = undefined;
|
|
82
|
-
if (pendingResend) {
|
|
83
|
-
if (!sessionId) {
|
|
84
|
-
throw new Error("A conversation is required to retry a message.");
|
|
85
|
-
}
|
|
86
|
-
const stream = await this.options.service.resendMessagesStream(
|
|
87
|
-
{
|
|
88
|
-
sessionId,
|
|
89
|
-
messageId: pendingResend.messageId,
|
|
90
|
-
model: {
|
|
91
|
-
llmService: context.model.llmService,
|
|
92
|
-
model: context.model.value,
|
|
93
|
-
},
|
|
94
|
-
webSearch: context.task?.webSearch,
|
|
95
|
-
},
|
|
96
|
-
abortSignal
|
|
97
|
-
);
|
|
98
|
-
return createNocoBaseUIMessageStream(stream);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!sessionId) {
|
|
102
|
-
sessionId = await this.options.service.createConversation({
|
|
103
|
-
employee: context.employee,
|
|
104
|
-
model: context.model,
|
|
105
|
-
systemMessage: context.task?.systemMessage,
|
|
106
|
-
skillSettings: context.task?.skillSettings,
|
|
107
|
-
});
|
|
108
|
-
this.options.onSessionCreated?.(sessionId);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const lastMessage = messages.at(-1);
|
|
112
|
-
const attachments = lastMessage?.metadata?.attachments?.filter(
|
|
113
|
-
(attachment) => attachment.status === "done"
|
|
114
|
-
);
|
|
115
|
-
const workContext = [
|
|
116
|
-
...(context.task?.workContext ?? []),
|
|
117
|
-
...(lastMessage?.metadata?.workContext ?? []),
|
|
118
|
-
];
|
|
119
|
-
const stream = await this.options.service.sendMessagesStream(
|
|
120
|
-
{
|
|
121
|
-
sessionId,
|
|
122
|
-
aiEmployee: context.employee.username,
|
|
123
|
-
model: {
|
|
124
|
-
llmService: context.model.llmService,
|
|
125
|
-
model: context.model.value,
|
|
126
|
-
},
|
|
127
|
-
systemMessage: context.task?.systemMessage,
|
|
128
|
-
skillSettings: context.task?.skillSettings,
|
|
129
|
-
webSearch: context.task?.webSearch,
|
|
130
|
-
editingMessageId: lastMessage?.metadata?.editingMessageId,
|
|
131
|
-
messages: [
|
|
132
|
-
{
|
|
133
|
-
key: lastMessage?.id ?? crypto.randomUUID(),
|
|
134
|
-
role: "user",
|
|
135
|
-
content: { type: "text", content: messageText(lastMessage) },
|
|
136
|
-
attachments: attachments?.length ? attachments : undefined,
|
|
137
|
-
workContext: workContext.length ? workContext : undefined,
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
},
|
|
141
|
-
abortSignal
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
return createNocoBaseUIMessageStream(stream);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async reconnectToStream() {
|
|
148
|
-
const pendingToolResume = this.pendingToolResume;
|
|
149
|
-
this.pendingToolResume = undefined;
|
|
150
|
-
const pendingConversationResume = this.pendingConversationResume;
|
|
151
|
-
this.pendingConversationResume = undefined;
|
|
152
|
-
const context = this.options.getContext();
|
|
153
|
-
if (!context.sessionId) {
|
|
154
|
-
throw new Error("A conversation is required to resume a stream.");
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!pendingToolResume) {
|
|
158
|
-
const stream = await this.options.service.resumeConversationStream(
|
|
159
|
-
context.sessionId
|
|
160
|
-
);
|
|
161
|
-
return createNocoBaseUIMessageStream(stream, null, {
|
|
162
|
-
seedMessage: pendingConversationResume?.message,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
const stream = await this.options.service.resumeToolCallStream({
|
|
166
|
-
sessionId: context.sessionId,
|
|
167
|
-
messageId: pendingToolResume.messageId,
|
|
168
|
-
toolCallIds: pendingToolResume.toolCallIds,
|
|
169
|
-
toolCallResults: pendingToolResume.toolCallResults,
|
|
170
|
-
model: {
|
|
171
|
-
llmService: context.model.llmService,
|
|
172
|
-
model: context.model.value,
|
|
173
|
-
},
|
|
174
|
-
webSearch: context.task?.webSearch,
|
|
175
|
-
});
|
|
176
|
-
return createNocoBaseUIMessageStream(
|
|
177
|
-
stream,
|
|
178
|
-
pendingToolResume.responseMessageId,
|
|
179
|
-
{
|
|
180
|
-
waitForNewMessage: true,
|
|
181
|
-
}
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
useContext,
|
|
4
|
-
useMemo,
|
|
5
|
-
type PropsWithChildren,
|
|
6
|
-
} from "react";
|
|
7
|
-
import type { AIToolInvoker } from "./types";
|
|
8
|
-
|
|
9
|
-
export type AIFormField = {
|
|
10
|
-
name: string;
|
|
11
|
-
title?: string;
|
|
12
|
-
type?: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
readonly?: boolean;
|
|
15
|
-
required?: boolean;
|
|
16
|
-
enum?: unknown;
|
|
17
|
-
[key: string]: unknown;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type AIFormTarget = {
|
|
21
|
-
id: string;
|
|
22
|
-
title: string;
|
|
23
|
-
fields: AIFormField[];
|
|
24
|
-
getValues: () => unknown | Promise<unknown>;
|
|
25
|
-
setValues: (values: Record<string, unknown>) => void | Promise<void>;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type AIFormFillSkippedField = {
|
|
29
|
-
name: string;
|
|
30
|
-
reason: "undeclared" | "readonly" | "invalid";
|
|
31
|
-
message: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type AIFormFillResult = {
|
|
35
|
-
status: "success" | "error";
|
|
36
|
-
content: string;
|
|
37
|
-
appliedFields: string[];
|
|
38
|
-
skippedFields: AIFormFillSkippedField[];
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export class AIFormRegistry {
|
|
42
|
-
private readonly targets = new Map<
|
|
43
|
-
string,
|
|
44
|
-
{ token: symbol; target: AIFormTarget }
|
|
45
|
-
>();
|
|
46
|
-
|
|
47
|
-
register(target: AIFormTarget) {
|
|
48
|
-
if (!target.id.trim()) throw new Error("AI Form id is required");
|
|
49
|
-
if (!target.title.trim()) throw new Error("AI Form title is required");
|
|
50
|
-
if (!Array.isArray(target.fields)) {
|
|
51
|
-
throw new Error("AI Form fields must be an array");
|
|
52
|
-
}
|
|
53
|
-
if (typeof target.getValues !== "function") {
|
|
54
|
-
throw new Error("AI Form getValues must be a function");
|
|
55
|
-
}
|
|
56
|
-
if (typeof target.setValues !== "function") {
|
|
57
|
-
throw new Error("AI Form setValues must be a function");
|
|
58
|
-
}
|
|
59
|
-
const fieldNames = new Set<string>();
|
|
60
|
-
for (const field of target.fields) {
|
|
61
|
-
if (!field.name?.trim()) throw new Error("AI Form field name is required");
|
|
62
|
-
if (fieldNames.has(field.name)) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`AI Form field "${field.name}" is declared more than once`
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
fieldNames.add(field.name);
|
|
68
|
-
}
|
|
69
|
-
if (this.targets.has(target.id)) {
|
|
70
|
-
throw new Error(`AI Form "${target.id}" is already registered`);
|
|
71
|
-
}
|
|
72
|
-
const token = Symbol(target.id);
|
|
73
|
-
this.targets.set(target.id, { token, target });
|
|
74
|
-
return () => {
|
|
75
|
-
if (this.targets.get(target.id)?.token === token) {
|
|
76
|
-
this.targets.delete(target.id);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
get(formId: string) {
|
|
82
|
-
return this.targets.get(formId)?.target;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const AIFormRegistryContext = createContext<AIFormRegistry | null>(null);
|
|
87
|
-
|
|
88
|
-
export function AIFormRegistryProvider({ children }: PropsWithChildren) {
|
|
89
|
-
const registry = useMemo(() => new AIFormRegistry(), []);
|
|
90
|
-
return (
|
|
91
|
-
<AIFormRegistryContext.Provider value={registry}>
|
|
92
|
-
{children}
|
|
93
|
-
</AIFormRegistryContext.Provider>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function useAIFormRegistry() {
|
|
98
|
-
const registry = useContext(AIFormRegistryContext);
|
|
99
|
-
if (!registry) {
|
|
100
|
-
throw new Error(
|
|
101
|
-
"useAIFormRegistry must be used inside AIFormRegistryProvider"
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
return registry;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const getErrorMessage = (error: unknown) =>
|
|
108
|
-
error instanceof Error ? error.message : String(error);
|
|
109
|
-
|
|
110
|
-
const getEnumValues = (definition: unknown) => {
|
|
111
|
-
if (!Array.isArray(definition)) return undefined;
|
|
112
|
-
return definition.map((item) => {
|
|
113
|
-
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
114
|
-
return (item as { value?: unknown }).value;
|
|
115
|
-
}
|
|
116
|
-
return item;
|
|
117
|
-
});
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const validateFieldValue = (field: AIFormField, value: unknown) => {
|
|
121
|
-
const enumValues = getEnumValues(field.enum);
|
|
122
|
-
if (enumValues?.length && !enumValues.some((item) => Object.is(item, value))) {
|
|
123
|
-
return "The value is not one of the declared options.";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
switch (field.type?.toLowerCase()) {
|
|
127
|
-
case "string":
|
|
128
|
-
case "text":
|
|
129
|
-
case "textarea":
|
|
130
|
-
case "email":
|
|
131
|
-
case "url":
|
|
132
|
-
case "date":
|
|
133
|
-
case "datetime":
|
|
134
|
-
return typeof value === "string" ? undefined : "Expected a string.";
|
|
135
|
-
case "number":
|
|
136
|
-
case "percent":
|
|
137
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
138
|
-
? undefined
|
|
139
|
-
: "Expected a finite number.";
|
|
140
|
-
case "integer":
|
|
141
|
-
return typeof value === "number" && Number.isInteger(value)
|
|
142
|
-
? undefined
|
|
143
|
-
: "Expected an integer.";
|
|
144
|
-
case "boolean":
|
|
145
|
-
case "checkbox":
|
|
146
|
-
return typeof value === "boolean" ? undefined : "Expected a boolean.";
|
|
147
|
-
case "array":
|
|
148
|
-
return Array.isArray(value) ? undefined : "Expected an array.";
|
|
149
|
-
case "object":
|
|
150
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
151
|
-
? undefined
|
|
152
|
-
: "Expected an object.";
|
|
153
|
-
default:
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
export function createFormFillerInvoker(
|
|
159
|
-
registry: AIFormRegistry
|
|
160
|
-
): AIToolInvoker {
|
|
161
|
-
return async (input, context) => {
|
|
162
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
163
|
-
return {
|
|
164
|
-
status: "error",
|
|
165
|
-
content: "Form filler requires a form identifier and field data.",
|
|
166
|
-
appliedFields: [],
|
|
167
|
-
skippedFields: [],
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const { form, data } = input as { form?: unknown; data?: unknown };
|
|
172
|
-
if (typeof form !== "string" || !form) {
|
|
173
|
-
return {
|
|
174
|
-
status: "error",
|
|
175
|
-
content: "The target form identifier is missing.",
|
|
176
|
-
appliedFields: [],
|
|
177
|
-
skippedFields: [],
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
if (context.allowedFormIds?.includes(form) !== true) {
|
|
181
|
-
return {
|
|
182
|
-
status: "error",
|
|
183
|
-
content: `The target form "${form}" is not available in this conversation context.`,
|
|
184
|
-
appliedFields: [],
|
|
185
|
-
skippedFields: [],
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
189
|
-
return {
|
|
190
|
-
status: "error",
|
|
191
|
-
content: "Form filler data must be an object.",
|
|
192
|
-
appliedFields: [],
|
|
193
|
-
skippedFields: [],
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const target = registry.get(form);
|
|
198
|
-
if (!target) {
|
|
199
|
-
return {
|
|
200
|
-
status: "error",
|
|
201
|
-
content: `The target form "${form}" is not available on this page.`,
|
|
202
|
-
appliedFields: [],
|
|
203
|
-
skippedFields: [],
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
const fields = new Map(target.fields.map((field) => [field.name, field]));
|
|
209
|
-
const accepted: Record<string, unknown> = {};
|
|
210
|
-
const skippedFields: AIFormFillSkippedField[] = [];
|
|
211
|
-
for (const [name, value] of Object.entries(
|
|
212
|
-
data as Record<string, unknown>
|
|
213
|
-
)) {
|
|
214
|
-
const field = fields.get(name);
|
|
215
|
-
if (!field) {
|
|
216
|
-
skippedFields.push({
|
|
217
|
-
name,
|
|
218
|
-
reason: "undeclared",
|
|
219
|
-
message: "This field is not declared by the target form.",
|
|
220
|
-
});
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (field.readonly) {
|
|
224
|
-
skippedFields.push({
|
|
225
|
-
name,
|
|
226
|
-
reason: "readonly",
|
|
227
|
-
message: "This field is read-only.",
|
|
228
|
-
});
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const validationError = validateFieldValue(field, value);
|
|
232
|
-
if (validationError) {
|
|
233
|
-
skippedFields.push({
|
|
234
|
-
name,
|
|
235
|
-
reason: "invalid",
|
|
236
|
-
message: validationError,
|
|
237
|
-
});
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
accepted[name] = value;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const appliedFields = Object.keys(accepted);
|
|
244
|
-
if (!appliedFields.length) {
|
|
245
|
-
return {
|
|
246
|
-
status: "error",
|
|
247
|
-
content: `No valid editable fields were provided for "${target.title}".`,
|
|
248
|
-
appliedFields,
|
|
249
|
-
skippedFields,
|
|
250
|
-
} satisfies AIFormFillResult;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
await target.setValues(accepted);
|
|
254
|
-
return {
|
|
255
|
-
status: "success",
|
|
256
|
-
content: skippedFields.length
|
|
257
|
-
? `Filled ${appliedFields.length} field(s) in "${target.title}" and skipped ${skippedFields.length}. Please review the values and submit the form manually.`
|
|
258
|
-
: `Filled "${target.title}". Please review the values and submit the form manually.`,
|
|
259
|
-
appliedFields,
|
|
260
|
-
skippedFields,
|
|
261
|
-
} satisfies AIFormFillResult;
|
|
262
|
-
} catch (error) {
|
|
263
|
-
return {
|
|
264
|
-
status: "error",
|
|
265
|
-
content: `Unable to fill "${target.title}": ${getErrorMessage(error)}`,
|
|
266
|
-
appliedFields: [],
|
|
267
|
-
skippedFields: [],
|
|
268
|
-
} satisfies AIFormFillResult;
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
}
|