@lobehub/lobehub 2.0.0-next.48 → 2.0.0-next.49

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 (90) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/changelog/v1.json +12 -0
  5. package/locales/ar/chat.json +1 -0
  6. package/locales/ar/topic.json +1 -0
  7. package/locales/bg-BG/chat.json +1 -0
  8. package/locales/bg-BG/topic.json +1 -0
  9. package/locales/de-DE/chat.json +1 -0
  10. package/locales/de-DE/topic.json +1 -0
  11. package/locales/en-US/chat.json +1 -0
  12. package/locales/en-US/topic.json +1 -0
  13. package/locales/es-ES/chat.json +1 -0
  14. package/locales/es-ES/topic.json +1 -0
  15. package/locales/fa-IR/chat.json +1 -0
  16. package/locales/fa-IR/topic.json +1 -0
  17. package/locales/fr-FR/chat.json +1 -0
  18. package/locales/fr-FR/topic.json +1 -0
  19. package/locales/it-IT/chat.json +1 -0
  20. package/locales/it-IT/topic.json +1 -0
  21. package/locales/ja-JP/chat.json +1 -0
  22. package/locales/ja-JP/topic.json +1 -0
  23. package/locales/ko-KR/chat.json +1 -0
  24. package/locales/ko-KR/topic.json +1 -0
  25. package/locales/nl-NL/chat.json +1 -0
  26. package/locales/nl-NL/topic.json +1 -0
  27. package/locales/pl-PL/chat.json +1 -0
  28. package/locales/pl-PL/topic.json +1 -0
  29. package/locales/pt-BR/chat.json +1 -0
  30. package/locales/pt-BR/topic.json +1 -0
  31. package/locales/ru-RU/chat.json +1 -0
  32. package/locales/ru-RU/topic.json +1 -0
  33. package/locales/tr-TR/chat.json +1 -0
  34. package/locales/tr-TR/topic.json +1 -0
  35. package/locales/vi-VN/chat.json +1 -0
  36. package/locales/vi-VN/topic.json +1 -0
  37. package/locales/zh-CN/chat.json +1 -0
  38. package/locales/zh-CN/discover.json +1 -1
  39. package/locales/zh-CN/topic.json +1 -0
  40. package/locales/zh-TW/chat.json +1 -0
  41. package/locales/zh-TW/topic.json +1 -0
  42. package/package.json +9 -3
  43. package/packages/agent-runtime/src/core/InterventionChecker.ts +5 -16
  44. package/packages/agent-runtime/src/core/__tests__/InterventionChecker.test.ts +27 -80
  45. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +32 -13
  46. package/packages/agent-runtime/src/core/runtime.ts +7 -3
  47. package/packages/agent-runtime/src/types/event.ts +2 -1
  48. package/packages/agent-runtime/src/types/generalAgent.ts +1 -0
  49. package/packages/agent-runtime/src/types/instruction.ts +3 -2
  50. package/packages/agent-runtime/src/types/state.ts +3 -1
  51. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +4 -1
  52. package/packages/database/src/models/message.ts +3 -0
  53. package/packages/obervability-otel/src/node.ts +15 -1
  54. package/packages/types/src/message/common/base.ts +2 -2
  55. package/packages/types/src/message/common/tools.ts +16 -10
  56. package/packages/types/src/message/ui/chat.ts +7 -1
  57. package/packages/types/src/tool/intervention.ts +2 -3
  58. package/packages/types/src/user/settings/tool.ts +15 -28
  59. package/renovate.json +28 -11
  60. package/src/app/[variants]/(main)/chat/components/topic/features/Topic/TopicListContent/TopicItem/TopicContent.tsx +1 -1
  61. package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/Actions.tsx +1 -1
  62. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +20 -15
  63. package/src/features/Conversation/Messages/Group/GroupContext.tsx +15 -0
  64. package/src/features/Conversation/Messages/Group/Tool/Inspector/BuiltinPluginTitle.tsx +2 -4
  65. package/src/features/Conversation/Messages/Group/Tool/Inspector/ToolTitle.tsx +3 -5
  66. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +19 -7
  67. package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/index.tsx +14 -12
  68. package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/ApprovalActions.tsx +143 -0
  69. package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/KeyValueEditor.tsx +213 -0
  70. package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/ModeSelector.tsx +134 -0
  71. package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/index.tsx +99 -0
  72. package/src/features/Conversation/Messages/Group/Tool/Render/RejectedResponse.tsx +45 -0
  73. package/src/features/Conversation/Messages/Group/Tool/Render/index.tsx +23 -1
  74. package/src/features/Conversation/Messages/Group/Tool/index.tsx +42 -18
  75. package/src/features/Conversation/Messages/Group/Tools.tsx +3 -1
  76. package/src/locales/default/chat.ts +22 -0
  77. package/src/locales/default/common.ts +1 -0
  78. package/src/locales/default/topic.ts +1 -0
  79. package/src/server/routers/lambda/message.ts +4 -1
  80. package/src/server/services/message/index.ts +13 -0
  81. package/src/services/message/index.ts +17 -2
  82. package/src/store/chat/agents/GeneralChatAgent.ts +141 -24
  83. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +605 -0
  84. package/src/store/chat/agents/createAgentExecutors.ts +144 -26
  85. package/src/store/chat/agents/createToolEngine.ts +22 -0
  86. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +106 -0
  87. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +54 -26
  88. package/src/store/chat/slices/message/reducer.ts +2 -1
  89. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +26 -1
  90. package/src/store/user/slices/settings/action.ts +15 -0
package/renovate.json CHANGED
@@ -17,22 +17,39 @@
17
17
  "ignoreDeps": [],
18
18
  "labels": ["dependencies"],
19
19
  "packageRules": [
20
- // 1) Pinned deps: isolate (OK to use separate* here because there's no matchUpdateTypes)
21
20
  {
22
- "description": "Isolate PRs for pinned deps (exact x.y.z)",
21
+ "description": "Un-group tilde ranges",
23
22
  "matchManagers": ["npm", "pnpm", "yarn", "bun"],
24
- "matchCurrentValue": "^\\d+\\.\\d+\\.\\d+([+-][0-9A-Za-z.-]+)?$",
23
+ "matchCurrentValue": "^~.*$",
25
24
  "groupName": null,
26
- "separateMinorPatch": true,
27
- "separateMajorMinor": true
25
+ "groupSlug": null,
26
+ "separateMajorMinor": true,
27
+ "separateMinorPatch": true
28
28
  },
29
- // 2) Non-pinned deps: Patch versions, grouped together
30
29
  {
31
- "description": "Group patch versions together for non-pinned deps",
30
+ "description": "Un-group pinned deps (exact x.y.z)",
32
31
  "matchManagers": ["npm", "pnpm", "yarn", "bun"],
33
- "matchCurrentValue": "/(^[~^]|[<>=| -])/", // anything that looks like a range
34
- "groupName": "patch dependencies",
35
- "matchUpdateTypes": ["patch"]
32
+ "matchCurrentValue": "/^\\d+\\.\\d+\\.\\d+([+-][0-9A-Za-z.-]+)?$/",
33
+ "groupName": null,
34
+ "groupSlug": null,
35
+ "separateMajorMinor": true,
36
+ "separateMinorPatch": true
37
+ },
38
+ {
39
+ "description": "Un-group experimental caret ranges (^0.x)",
40
+ "matchManagers": ["npm", "pnpm", "yarn", "bun"],
41
+ "matchCurrentValue": "^\\^0\\.\\d+(\\.\\d+)?([+-][0-9A-Za-z.-]+)?$",
42
+ "groupName": null,
43
+ "separateMajorMinor": true,
44
+ "separateMinorPatch": true
45
+ },
46
+ {
47
+ "description": "Group ^>=1 ranges for patch/minor",
48
+ "matchManagers": ["npm", "pnpm", "yarn", "bun"],
49
+ "matchCurrentValue": "/^\\^[1-9]+[0-9]*(\\.\\d+){0,2}([+-][0-9A-Za-z.-]+)?$/",
50
+ "matchUpdateTypes": ["minor", "patch"],
51
+ "groupName": "all non-major dependencies",
52
+ "groupSlug": "all-non-major-dependencies"
36
53
  }
37
54
  ],
38
55
  "postUpdateOptions": ["yarnDedupeHighest"],
@@ -41,7 +58,7 @@
41
58
  "rangeStrategy": "bump",
42
59
  "rebaseWhen": "conflicted",
43
60
  "schedule": "on sunday before 6:00am",
61
+ "separateMinorPatch": false,
44
62
  "separateMajorMinor": true,
45
- "separateMultipleMajor": true,
46
63
  "timezone": "UTC"
47
64
  }
@@ -100,7 +100,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
100
100
  {
101
101
  icon: <Icon icon={ExternalLink} />,
102
102
  key: 'openInNewWindow',
103
- label: '单独打开页面',
103
+ label: t('actions.openInNewWindow'),
104
104
  onClick: () => {
105
105
  openTopicInNewWindow(activeId, id);
106
106
  },
@@ -97,7 +97,7 @@ const Actions = memo<ActionProps>(({ group, id, openCreateGroupModal, parentType
97
97
  {
98
98
  icon: <Icon icon={ExternalLink} />,
99
99
  key: 'openInNewWindow',
100
- label: '单独打开页面',
100
+ label: t('openInNewWindow'),
101
101
  onClick: ({ domEvent }: { domEvent: Event }) => {
102
102
  domEvent.stopPropagation();
103
103
  openSessionInNewWindow(id);
@@ -1,9 +1,10 @@
1
1
  import { AssistantContentBlock } from '@lobechat/types';
2
2
  import { createStyles } from 'antd-style';
3
3
  import isEqual from 'fast-deep-equal';
4
- import { memo } from 'react';
4
+ import { memo, useMemo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
+ import { GroupMessageContext } from './GroupContext';
7
8
  import GroupItem from './GroupItem';
8
9
 
9
10
  const useStyles = createStyles(({ css }) => {
@@ -28,21 +29,25 @@ const GroupChildren = memo<GroupChildrenProps>(
28
29
  ({ blocks, contentId, disableEditing, messageIndex, id }) => {
29
30
  const { styles } = useStyles();
30
31
 
32
+ const contextValue = useMemo(() => ({ assistantGroupId: id }), [id]);
33
+
31
34
  return (
32
- <Flexbox className={styles.container} gap={8}>
33
- {blocks.map((item, index) => {
34
- return (
35
- <GroupItem
36
- {...item}
37
- contentId={contentId}
38
- disableEditing={disableEditing}
39
- index={index}
40
- key={`${id}_${index}`}
41
- messageIndex={messageIndex}
42
- />
43
- );
44
- })}
45
- </Flexbox>
35
+ <GroupMessageContext value={contextValue}>
36
+ <Flexbox className={styles.container} gap={8}>
37
+ {blocks.map((item, index) => {
38
+ return (
39
+ <GroupItem
40
+ {...item}
41
+ contentId={contentId}
42
+ disableEditing={disableEditing}
43
+ index={index}
44
+ key={`${id}_${index}`}
45
+ messageIndex={messageIndex}
46
+ />
47
+ );
48
+ })}
49
+ </Flexbox>
50
+ </GroupMessageContext>
46
51
  );
47
52
  },
48
53
  isEqual,
@@ -0,0 +1,15 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ interface GroupMessageContextValue {
4
+ assistantGroupId: string;
5
+ }
6
+
7
+ export const GroupMessageContext = createContext<GroupMessageContextValue | null>(null);
8
+
9
+ export const useGroupMessage = () => {
10
+ const context = useContext(GroupMessageContext);
11
+ if (!context) {
12
+ throw new Error('useGroupMessage must be used within GroupMessageContext');
13
+ }
14
+ return context;
15
+ };
@@ -23,20 +23,18 @@ export const useStyles = createStyles(({ css, token }) => ({
23
23
 
24
24
  interface BuiltinPluginTitleProps {
25
25
  apiName: string;
26
- hasResult?: boolean;
27
26
  icon?: ReactNode;
28
27
  identifier: string;
29
28
  index: number;
29
+ isLoading?: boolean;
30
30
  messageId: string;
31
31
  title: string;
32
32
  toolCallId: string;
33
33
  }
34
34
 
35
- const BuiltinPluginTitle = memo<BuiltinPluginTitleProps>(({ apiName, title, hasResult }) => {
35
+ const BuiltinPluginTitle = memo<BuiltinPluginTitleProps>(({ apiName, title, isLoading }) => {
36
36
  const { styles } = useStyles();
37
37
 
38
- const isLoading = !hasResult;
39
-
40
38
  return (
41
39
  <Flexbox align={'center'} className={isLoading ? styles.shinyText : ''} gap={4} horizontal>
42
40
  <div>{title}</div>
@@ -31,20 +31,18 @@ export const useStyles = createStyles(({ css, token }) => ({
31
31
 
32
32
  interface ToolTitleProps {
33
33
  apiName: string;
34
- hasResult?: boolean;
35
34
  identifier: string;
36
35
  index: number;
36
+ isLoading?: boolean;
37
37
  messageId: string;
38
38
  toolCallId: string;
39
39
  }
40
40
 
41
41
  const ToolTitle = memo<ToolTitleProps>(
42
- ({ identifier, apiName, hasResult, index, toolCallId, messageId }) => {
42
+ ({ identifier, apiName, isLoading, index, toolCallId, messageId }) => {
43
43
  const { t } = useTranslation('plugin');
44
44
  const { styles } = useStyles();
45
45
 
46
- const isLoading = !hasResult;
47
-
48
46
  const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);
49
47
 
50
48
  const plugins = useMemo(
@@ -69,9 +67,9 @@ const ToolTitle = memo<ToolTitleProps>(
69
67
  return (
70
68
  <BuiltinPluginTitle
71
69
  {...builtinPluginTitle}
72
- hasResult={hasResult}
73
70
  identifier={identifier}
74
71
  index={index}
72
+ isLoading={isLoading}
75
73
  messageId={messageId}
76
74
  toolCallId={toolCallId}
77
75
  />
@@ -1,12 +1,13 @@
1
- import { ActionIcon } from '@lobehub/ui';
1
+ import { ActionIcon, Icon, Tooltip } from '@lobehub/ui';
2
2
  import { createStyles } from 'antd-style';
3
- import { Check, LayoutPanelTop, LogsIcon, LucideBug, LucideBugOff, X } from 'lucide-react';
3
+ import { Ban, Check, LayoutPanelTop, LogsIcon, LucideBug, LucideBugOff, X } from 'lucide-react';
4
4
  import { CSSProperties, memo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
 
8
8
  import { LOADING_FLAT } from '@/const/message';
9
9
  import { shinyTextStylish } from '@/styles/loading';
10
+ import { ToolIntervention } from '@/types/message';
10
11
 
11
12
  import Debug from './Debug';
12
13
  import Settings from './Settings';
@@ -66,6 +67,7 @@ interface InspectorProps {
66
67
  id: string;
67
68
  identifier: string;
68
69
  index: number;
70
+ intervention?: ToolIntervention;
69
71
  messageId: string;
70
72
  result?: { content: string | null; error?: any; state?: any };
71
73
  setShowPluginRender: (show: boolean) => void;
@@ -91,6 +93,7 @@ const Inspectors = memo<InspectorProps>(
91
93
  showPluginRender,
92
94
  setShowPluginRender,
93
95
  type,
96
+ intervention,
94
97
  }) => {
95
98
  const { t } = useTranslation('plugin');
96
99
  const { styles, theme } = useStyles();
@@ -102,6 +105,11 @@ const Inspectors = memo<InspectorProps>(
102
105
 
103
106
  const hasResult = hasSuccessResult || hasError;
104
107
 
108
+ const isPending = intervention?.status === 'pending';
109
+ const isReject = intervention?.status === 'rejected';
110
+ const isTitleLoading = !hasResult && !isPending;
111
+
112
+ const showCustomPluginRender = showRender && !isPending && !isReject;
105
113
  return (
106
114
  <Flexbox className={styles.container} gap={4}>
107
115
  <Flexbox align={'center'} distribution={'space-between'} gap={8} horizontal>
@@ -117,16 +125,16 @@ const Inspectors = memo<InspectorProps>(
117
125
  >
118
126
  <ToolTitle
119
127
  apiName={apiName}
120
- hasResult={hasResult}
121
128
  identifier={identifier}
122
129
  index={index}
130
+ isLoading={isTitleLoading}
123
131
  messageId={messageId}
124
132
  toolCallId={id}
125
133
  />
126
134
  </Flexbox>
127
135
  <Flexbox align={'center'} gap={8} horizontal>
128
136
  <Flexbox className={styles.actions} horizontal>
129
- {showRender && (
137
+ {showCustomPluginRender && (
130
138
  <ActionIcon
131
139
  icon={showPluginRender ? LogsIcon : LayoutPanelTop}
132
140
  onClick={() => {
@@ -148,10 +156,14 @@ const Inspectors = memo<InspectorProps>(
148
156
  </Flexbox>
149
157
  {hasResult && (
150
158
  <Flexbox align={'center'} gap={4} horizontal style={{ fontSize: 12 }}>
151
- {hasError ? (
152
- <X color={theme.colorError} size={14} />
159
+ {isReject ? (
160
+ <Tooltip title={t('tool.intervention.toolRejected', { ns: 'chat' })}>
161
+ <Icon color={theme.colorTextTertiary} icon={Ban} />
162
+ </Tooltip>
163
+ ) : hasError ? (
164
+ <Icon color={theme.colorError} icon={X} />
153
165
  ) : (
154
- <Check color={theme.colorSuccess} size={14} />
166
+ <Icon color={theme.colorSuccess} icon={Check} />
155
167
  )}
156
168
  </Flexbox>
157
169
  )}
@@ -115,18 +115,20 @@ const Arguments = memo<ArgumentsProps>(({ arguments: args = '', shine, actions }
115
115
  {actions}
116
116
  </Flexbox>
117
117
  )}
118
- {Object.entries(displayArgs).map(([key, value]) => {
119
- return (
120
- <ObjectEntity
121
- editable={false}
122
- hasMinWidth={hasMinWidth}
123
- key={key}
124
- objectKey={key}
125
- shine={shine}
126
- value={value}
127
- />
128
- );
129
- })}
118
+ <Flexbox>
119
+ {Object.entries(displayArgs).map(([key, value]) => {
120
+ return (
121
+ <ObjectEntity
122
+ editable={false}
123
+ hasMinWidth={hasMinWidth}
124
+ key={key}
125
+ objectKey={key}
126
+ shine={shine}
127
+ value={value}
128
+ />
129
+ );
130
+ })}
131
+ </Flexbox>
130
132
  </div>
131
133
  );
132
134
  });
@@ -0,0 +1,143 @@
1
+ import { Button, Dropdown } from '@lobehub/ui';
2
+ import { Input, Popover, Space } from 'antd';
3
+ import { ChevronDown } from 'lucide-react';
4
+ import { memo, useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import { useGroupMessage } from '@/features/Conversation/Messages/Group/GroupContext';
9
+ import { useChatStore } from '@/store/chat';
10
+ import { useUserStore } from '@/store/user';
11
+
12
+ import { ApprovalMode } from './index';
13
+
14
+ interface ApprovalActionsProps {
15
+ apiName: string;
16
+ approvalMode: ApprovalMode;
17
+ identifier: string;
18
+ messageId: string;
19
+ toolCallId: string;
20
+ }
21
+
22
+ const ApprovalActions = memo<ApprovalActionsProps>(
23
+ ({ approvalMode, messageId, identifier, apiName }) => {
24
+ const { t } = useTranslation(['chat', 'common']);
25
+ const [rejectReason, setRejectReason] = useState('');
26
+ const [rejectPopoverOpen, setRejectPopoverOpen] = useState(false);
27
+ const [rejectLoading, setRejectLoading] = useState(false);
28
+ const [approveLoading, setApproveLoading] = useState(false);
29
+
30
+ const { assistantGroupId } = useGroupMessage();
31
+ const [approveToolIntervention, rejectToolIntervention] = useChatStore((s) => [
32
+ s.approveToolCalling,
33
+ s.rejectToolCalling,
34
+ ]);
35
+ const addToolToAllowList = useUserStore((s) => s.addToolToAllowList);
36
+
37
+ const handleApprove = async (remember?: boolean) => {
38
+ setApproveLoading(true);
39
+ try {
40
+ // 1. Update intervention status
41
+ await approveToolIntervention(messageId, assistantGroupId);
42
+
43
+ // 2. If remembered, add to allowList
44
+ if (remember) {
45
+ const toolKey = `${identifier}/${apiName}`;
46
+ await addToolToAllowList(toolKey);
47
+ }
48
+ } finally {
49
+ setApproveLoading(false);
50
+ }
51
+ };
52
+
53
+ const handleReject = async (reason?: string) => {
54
+ setRejectLoading(true);
55
+ await rejectToolIntervention(messageId, reason);
56
+ setRejectLoading(false);
57
+ setRejectPopoverOpen(false);
58
+ setRejectReason('');
59
+ };
60
+
61
+ return (
62
+ <Flexbox gap={8} horizontal>
63
+ <Popover
64
+ arrow={false}
65
+ content={
66
+ <Flexbox gap={12} style={{ width: 400 }}>
67
+ <Flexbox align={'center'} horizontal justify={'space-between'}>
68
+ <div>{t('tool.intervention.rejectTitle')}</div>
69
+
70
+ <Button
71
+ loading={rejectLoading}
72
+ onClick={() => handleReject(rejectReason)}
73
+ size="small"
74
+ type="primary"
75
+ >
76
+ {t('confirm', { ns: 'common' })}
77
+ </Button>
78
+ </Flexbox>
79
+ <Input.TextArea
80
+ autoFocus
81
+ onChange={(e) => setRejectReason(e.target.value)}
82
+ placeholder={t('tool.intervention.rejectReasonPlaceholder')}
83
+ rows={3}
84
+ value={rejectReason}
85
+ variant={'filled'}
86
+ />
87
+ </Flexbox>
88
+ }
89
+ onOpenChange={(open) => {
90
+ if (rejectLoading) return;
91
+
92
+ setRejectPopoverOpen(open);
93
+ }}
94
+ open={rejectPopoverOpen}
95
+ placement="bottomRight"
96
+ trigger="click"
97
+ >
98
+ <Button size="small" type="default">
99
+ {t('tool.intervention.reject')}
100
+ </Button>
101
+ </Popover>
102
+
103
+ {approvalMode === 'allow-list' ? (
104
+ <Space.Compact>
105
+ <Button
106
+ loading={approveLoading}
107
+ onClick={() => handleApprove(true)}
108
+ size="small"
109
+ type="primary"
110
+ >
111
+ {t('tool.intervention.approveAndRemember')}
112
+ </Button>
113
+ <Dropdown
114
+ menu={{
115
+ items: [
116
+ {
117
+ disabled: approveLoading,
118
+ key: 'once',
119
+ label: t('tool.intervention.approveOnce'),
120
+ onClick: () => handleApprove(false),
121
+ },
122
+ ],
123
+ }}
124
+ >
125
+ <Button disabled={approveLoading} icon={ChevronDown} size="small" type="primary" />
126
+ </Dropdown>
127
+ </Space.Compact>
128
+ ) : (
129
+ <Button
130
+ loading={approveLoading}
131
+ onClick={() => handleApprove()}
132
+ size="small"
133
+ type="primary"
134
+ >
135
+ {t('tool.intervention.approve')}
136
+ </Button>
137
+ )}
138
+ </Flexbox>
139
+ );
140
+ },
141
+ );
142
+
143
+ export default ApprovalActions;
@@ -0,0 +1,213 @@
1
+ import { ActionIcon, Button, Icon, Input } from '@lobehub/ui';
2
+ import { App, Form, FormInstance } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { LucidePlus, LucideTrash } from 'lucide-react';
5
+ import { memo, useEffect, useRef, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ const useStyles = createStyles(({ css, token }) => ({
10
+ form: css`
11
+ position: relative;
12
+
13
+ width: 100%;
14
+ min-width: 600px;
15
+ padding: 8px;
16
+ border-radius: ${token.borderRadiusLG}px;
17
+ `,
18
+ formItem: css`
19
+ margin-block-end: 4px !important;
20
+ `,
21
+ input: css`
22
+ font-family: ${token.fontFamilyCode};
23
+ font-size: 12px;
24
+ `,
25
+ row: css`
26
+ position: relative;
27
+ `,
28
+ title: css`
29
+ margin-block-end: 4px;
30
+ color: ${token.colorTextTertiary};
31
+ `,
32
+ }));
33
+
34
+ interface KeyValueItem {
35
+ id: string;
36
+ key?: string;
37
+ value?: string;
38
+ }
39
+
40
+ interface KeyValueEditorProps {
41
+ initialValue?: Record<string, any>;
42
+ onCancel?: () => void;
43
+ onFinish?: (value: Record<string, any>) => Promise<void>;
44
+ }
45
+
46
+ const recordToFormList = (record: Record<string, any>): KeyValueItem[] =>
47
+ Object.entries(record)
48
+ .map(([key, val], index) => ({
49
+ id: `${key}-${index}`,
50
+ key,
51
+ value: typeof val === 'string' ? val : JSON.stringify(val),
52
+ }))
53
+ .filter((item) => item.key);
54
+
55
+ const formListToRecord = (list: KeyValueItem[]): Record<string, any> => {
56
+ const record: Record<string, any> = {};
57
+ list.forEach((item) => {
58
+ if (item.key) {
59
+ try {
60
+ record[item.key] = JSON.parse(item.value || '""');
61
+ } catch {
62
+ record[item.key] = item.value || '';
63
+ }
64
+ }
65
+ });
66
+ return record;
67
+ };
68
+
69
+ const KeyValueEditor = memo<KeyValueEditorProps>(({ initialValue = {}, onFinish, onCancel }) => {
70
+ const { styles } = useStyles();
71
+ const { t } = useTranslation(['tool', 'common']);
72
+ const [form] = Form.useForm();
73
+ const { message } = App.useApp();
74
+ const formRef = useRef<FormInstance>(null);
75
+
76
+ useEffect(() => {
77
+ form.setFieldsValue({ items: recordToFormList(initialValue) });
78
+ }, [initialValue, form]);
79
+
80
+ const [updating, setUpdating] = useState(false);
81
+ const handleFinish = async () => {
82
+ setUpdating(true);
83
+ try {
84
+ await form.validateFields();
85
+ const values = form.getFieldsValue();
86
+ const record = formListToRecord(values.items || []);
87
+ await onFinish?.(record);
88
+ } catch (errorInfo) {
89
+ console.error('Validation Failed:', errorInfo);
90
+ message.error(t('updateArgs.formValidationFailed') || 'Please check the form for errors.');
91
+ }
92
+ setUpdating(false);
93
+ };
94
+
95
+ const handleCancel = () => {
96
+ onCancel?.();
97
+ };
98
+
99
+ const validateKeys = (_: any, item: KeyValueItem, items: KeyValueItem[]) => {
100
+ if (!item?.key) {
101
+ return Promise.resolve();
102
+ }
103
+ const keys = items.map((i) => i?.key).filter(Boolean);
104
+ if (keys.filter((k) => k === item.key).length > 1) {
105
+ return Promise.reject(new Error(t('updateArgs.duplicateKeyError')));
106
+ }
107
+
108
+ return Promise.resolve();
109
+ };
110
+
111
+ return (
112
+ <Form
113
+ autoComplete="off"
114
+ className={styles.form}
115
+ form={form}
116
+ initialValues={{ items: recordToFormList(initialValue) }}
117
+ ref={formRef}
118
+ >
119
+ <Flexbox className={styles.title} gap={8} horizontal>
120
+ <Flexbox flex={1}>key</Flexbox>
121
+ <Flexbox flex={4}>value</Flexbox>
122
+ </Flexbox>
123
+ <Form.List name="items">
124
+ {(fields, { add, remove }) => (
125
+ <Flexbox width={'100%'}>
126
+ {fields.map(({ key, name, ...restField }, index) => (
127
+ <Flexbox
128
+ align="center"
129
+ className={styles.row}
130
+ gap={8}
131
+ horizontal
132
+ key={key}
133
+ width={'100%'}
134
+ >
135
+ <Form.Item
136
+ {...restField}
137
+ className={styles.formItem}
138
+ name={[name, 'key']}
139
+ rules={[
140
+ { message: t('updateArgs.keyRequired'), required: true },
141
+ {
142
+ validator: (rule) =>
143
+ validateKeys(
144
+ rule,
145
+ form.getFieldValue(['items', index]),
146
+ form.getFieldValue('items'),
147
+ ),
148
+ },
149
+ ]}
150
+ style={{ flex: 1 }}
151
+ validateTrigger={['onChange', 'onBlur']}
152
+ >
153
+ <Input
154
+ allowClear
155
+ className={styles.input}
156
+ placeholder={t('updateArgs.form.key')}
157
+ variant={'filled'}
158
+ />
159
+ </Form.Item>
160
+ <Form.Item
161
+ {...restField}
162
+ className={styles.formItem}
163
+ name={[name, 'value']}
164
+ style={{ flex: 4 }}
165
+ >
166
+ <Input
167
+ allowClear
168
+ className={styles.input}
169
+ placeholder={t('updateArgs.form.value')}
170
+ variant={'filled'}
171
+ />
172
+ </Form.Item>
173
+ <ActionIcon
174
+ icon={LucideTrash}
175
+ onClick={() => remove(name)}
176
+ size={'small'}
177
+ style={{
178
+ marginBottom: 6,
179
+ }}
180
+ title={t('delete', { ns: 'common' })}
181
+ />
182
+ </Flexbox>
183
+ ))}
184
+ <Form.Item style={{ marginBottom: 0, marginTop: 8 }}>
185
+ <Flexbox gap={8} horizontal justify={'space-between'}>
186
+ <Button
187
+ color={'default'}
188
+ icon={<Icon icon={LucidePlus} />}
189
+ onClick={() => add({ id: `new-${Date.now()}`, key: '', value: '' })}
190
+ size={'small'}
191
+ variant="filled"
192
+ >
193
+ {t('updateArgs.form.add')}
194
+ </Button>
195
+
196
+ <Flexbox gap={8} horizontal>
197
+ <Button onClick={handleCancel} size={'small'}>
198
+ {t('cancel', { ns: 'common' })}
199
+ </Button>
200
+ <Button loading={updating} onClick={handleFinish} size={'small'} type={'primary'}>
201
+ {t('save', { ns: 'common' })}
202
+ </Button>
203
+ </Flexbox>
204
+ </Flexbox>
205
+ </Form.Item>
206
+ </Flexbox>
207
+ )}
208
+ </Form.List>
209
+ </Form>
210
+ );
211
+ });
212
+
213
+ export default KeyValueEditor;