@lobehub/chat 1.62.5 → 1.62.6

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 (45) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/plugin.json +4 -0
  4. package/locales/bg-BG/plugin.json +4 -0
  5. package/locales/de-DE/plugin.json +4 -0
  6. package/locales/en-US/plugin.json +4 -0
  7. package/locales/es-ES/plugin.json +4 -0
  8. package/locales/fa-IR/plugin.json +4 -0
  9. package/locales/fr-FR/plugin.json +4 -0
  10. package/locales/it-IT/plugin.json +4 -0
  11. package/locales/ja-JP/plugin.json +4 -0
  12. package/locales/ko-KR/plugin.json +4 -0
  13. package/locales/nl-NL/plugin.json +4 -0
  14. package/locales/pl-PL/plugin.json +4 -0
  15. package/locales/pt-BR/plugin.json +4 -0
  16. package/locales/ru-RU/plugin.json +4 -0
  17. package/locales/tr-TR/plugin.json +4 -0
  18. package/locales/vi-VN/plugin.json +4 -0
  19. package/locales/zh-CN/plugin.json +5 -1
  20. package/locales/zh-TW/plugin.json +4 -0
  21. package/package.json +1 -1
  22. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +1 -1
  23. package/src/database/server/models/aiProvider.ts +22 -9
  24. package/src/database/server/models/topic.ts +2 -0
  25. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/Debug.tsx +43 -0
  26. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/Loader.tsx +58 -0
  27. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/index.tsx +151 -0
  28. package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments.tsx +165 -0
  29. package/src/features/Conversation/Messages/Assistant/{ToolCallItem/Tool.tsx → Tool/Render/CustomRender.tsx} +34 -35
  30. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +39 -0
  31. package/src/features/Conversation/Messages/Assistant/Tool/index.tsx +70 -0
  32. package/src/features/Conversation/Messages/Assistant/index.tsx +19 -27
  33. package/src/features/InitClientDB/PGliteIcon.tsx +2 -2
  34. package/src/features/PluginsUI/Render/index.tsx +2 -11
  35. package/src/locales/default/plugin.ts +4 -0
  36. package/src/styles/loading.ts +27 -0
  37. package/src/tools/dalle/Render/GalleyGrid.tsx +60 -0
  38. package/src/tools/dalle/Render/index.tsx +1 -1
  39. package/src/features/Conversation/Messages/Assistant/ToolCallItem/Inspector/index.tsx +0 -166
  40. package/src/features/Conversation/Messages/Assistant/ToolCallItem/Inspector/style.ts +0 -35
  41. package/src/features/Conversation/Messages/Assistant/ToolCallItem/index.tsx +0 -89
  42. package/src/features/Conversation/Messages/Assistant/ToolCallItem/style.ts +0 -35
  43. package/src/features/Conversation/Messages/components/Arguments.tsx +0 -22
  44. /package/src/features/Conversation/Messages/Assistant/{ToolCallItem → Tool}/Inspector/PluginResultJSON.tsx +0 -0
  45. /package/src/features/Conversation/Messages/Assistant/{ToolCallItem → Tool}/Inspector/Settings.tsx +0 -0
@@ -0,0 +1,165 @@
1
+ import { Highlighter, copyToClipboard } from '@lobehub/ui';
2
+ import { App } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { parse } from 'partial-json';
5
+ import { memo, useMemo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import { useIsMobile } from '@/hooks/useIsMobile';
9
+ import { useYamlArguments } from '@/hooks/useYamlArguments';
10
+ import { shinyTextStylish } from '@/styles/loading';
11
+
12
+ const useStyles = createStyles(({ css, token }) => ({
13
+ arrayRow: css`
14
+ &:not(:first-child) {
15
+ border-block-start: 1px dotted ${token.colorBorderSecondary};
16
+ }
17
+ `,
18
+ colon: css`
19
+ color: ${token.colorTextTertiary};
20
+ `,
21
+ container: css`
22
+ padding-block: 4px;
23
+ padding-inline: 12px;
24
+ border-radius: ${token.borderRadiusLG}px;
25
+
26
+ font-family: ${token.fontFamilyCode};
27
+ font-size: 13px;
28
+ line-height: 1.5;
29
+
30
+ background: ${token.colorFillQuaternary};
31
+ `,
32
+ copyable: css`
33
+ cursor: pointer;
34
+ width: 100%;
35
+ margin-block: 2px;
36
+ padding: 4px;
37
+
38
+ &:hover {
39
+ border-radius: 6px;
40
+ background: ${token.colorFillTertiary};
41
+ }
42
+ `,
43
+ key: css`
44
+ color: ${token.colorTextTertiary};
45
+ `,
46
+ row: css`
47
+ display: flex;
48
+ align-items: baseline;
49
+
50
+ &:not(:first-child) {
51
+ border-block-start: 1px dotted ${token.colorBorderSecondary};
52
+ }
53
+ `,
54
+ shineText: shinyTextStylish(token),
55
+ value: css`
56
+ color: ${token.colorTextSecondary};
57
+ `,
58
+ }));
59
+
60
+ interface ObjectDisplayProps {
61
+ data: Record<string, any>;
62
+ shine?: boolean;
63
+ }
64
+
65
+ const ObjectDisplay = memo(({ data, shine }: ObjectDisplayProps) => {
66
+ const { styles, cx } = useStyles();
67
+ const { t } = useTranslation('common');
68
+
69
+ const { message } = App.useApp();
70
+ const isMobile = useIsMobile();
71
+
72
+ const formatValue = (value: any): string | string[] => {
73
+ if (Array.isArray(value)) {
74
+ return value.map((v) => (typeof v === 'object' ? JSON.stringify(v) : v));
75
+ }
76
+
77
+ if (typeof value === 'object' && value !== null) {
78
+ return Object.entries(value)
79
+ .map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`)
80
+ .join(', ');
81
+ }
82
+ return String(value);
83
+ };
84
+
85
+ const hasMinWidth = Object.keys(data).length > 1;
86
+ if (Object.keys(data).length === 0) return null;
87
+
88
+ return (
89
+ <div className={styles.container}>
90
+ {Object.entries(data).map(([key, value]) => {
91
+ const formatedValue = formatValue(value);
92
+ return (
93
+ <div className={styles.row} key={key}>
94
+ <span
95
+ className={styles.key}
96
+ style={{ minWidth: hasMinWidth ? (isMobile ? 60 : 80) : undefined }}
97
+ >
98
+ {key}
99
+ </span>
100
+ <span className={styles.colon}>:</span>
101
+ <div className={cx(shine ? styles.shineText : styles.value)} style={{ width: '100%' }}>
102
+ {typeof formatedValue === 'string' ? (
103
+ <div
104
+ className={styles.copyable}
105
+ onClick={async () => {
106
+ await copyToClipboard(formatedValue);
107
+ message.success(t('copySuccess'));
108
+ }}
109
+ >
110
+ {formatedValue}
111
+ </div>
112
+ ) : (
113
+ formatedValue.map((v, i) => (
114
+ <div
115
+ className={styles.arrayRow}
116
+ key={i}
117
+ onClick={async () => {
118
+ await copyToClipboard(v);
119
+ message.success(t('copySuccess'));
120
+ }}
121
+ >
122
+ <div className={styles.copyable}>{v}</div>
123
+ </div>
124
+ ))
125
+ )}
126
+ </div>
127
+ </div>
128
+ );
129
+ })}
130
+ </div>
131
+ );
132
+ });
133
+
134
+ export interface ArgumentsProps {
135
+ arguments?: string;
136
+ shine?: boolean;
137
+ }
138
+
139
+ const Arguments = memo<ArgumentsProps>(({ arguments: args = '', shine }) => {
140
+ const requestArgs = useMemo(() => {
141
+ try {
142
+ const obj = parse(args);
143
+
144
+ if (Object.keys(obj).length === 0) return {};
145
+
146
+ return obj;
147
+ } catch {
148
+ return args;
149
+ }
150
+ }, [args]);
151
+
152
+ const yaml = useYamlArguments(args);
153
+
154
+ return typeof requestArgs === 'string' ? (
155
+ !!yaml && (
156
+ <Highlighter language={'yaml'} showLanguage={false}>
157
+ {yaml}
158
+ </Highlighter>
159
+ )
160
+ ) : (
161
+ <ObjectDisplay data={requestArgs} shine={shine} />
162
+ );
163
+ });
164
+
165
+ export default Arguments;
@@ -2,7 +2,7 @@ import { Icon } from '@lobehub/ui';
2
2
  import { ConfigProvider, Empty } from 'antd';
3
3
  import { useTheme } from 'antd-style';
4
4
  import { LucideSquareArrowLeft, LucideSquareArrowRight } from 'lucide-react';
5
- import { memo, useContext, useState } from 'react';
5
+ import { memo, useContext, useEffect } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Center, Flexbox } from 'react-layout-kit';
8
8
 
@@ -11,14 +11,15 @@ import { useChatStore } from '@/store/chat';
11
11
  import { chatPortalSelectors, chatSelectors } from '@/store/chat/selectors';
12
12
  import { ChatMessage } from '@/types/message';
13
13
 
14
- import Arguments from '../../components/Arguments';
15
- import Inspector from './Inspector';
14
+ import Arguments from './Arguments';
16
15
 
17
- const Tool = memo<
16
+ const CustomRender = memo<
18
17
  ChatMessage & {
19
- showPortal?: boolean;
18
+ requestArgs?: string;
19
+ setShowPluginRender: (show: boolean) => void;
20
+ showPluginRender: boolean;
20
21
  }
21
- >(({ id, content, pluginState, plugin, showPortal }) => {
22
+ >(({ id, content, pluginState, plugin, requestArgs, showPluginRender, setShowPluginRender }) => {
22
23
  const [loading, isMessageToolUIOpen] = useChatStore((s) => [
23
24
  chatSelectors.isPluginApiInvoking(id)(s),
24
25
  chatPortalSelectors.isPluginUIOpen(id)(s),
@@ -27,36 +28,34 @@ const Tool = memo<
27
28
  const { t } = useTranslation('plugin');
28
29
 
29
30
  const theme = useTheme();
30
- const [showRender, setShow] = useState(plugin?.type !== 'default');
31
+ useEffect(() => {
32
+ if (!plugin?.type) return;
33
+
34
+ setShowPluginRender(plugin?.type !== 'default');
35
+ }, [plugin?.type]);
36
+
37
+ if (isMessageToolUIOpen)
38
+ return (
39
+ <Center paddingBlock={8} style={{ background: theme.colorFillQuaternary, borderRadius: 4 }}>
40
+ <Empty
41
+ description={t('showInPortal')}
42
+ image={
43
+ <Icon
44
+ color={theme.colorTextQuaternary}
45
+ icon={direction === 'rtl' ? LucideSquareArrowLeft : LucideSquareArrowRight}
46
+ size={'large'}
47
+ />
48
+ }
49
+ styles={{
50
+ image: { height: 24 },
51
+ }}
52
+ />
53
+ </Center>
54
+ );
31
55
 
32
56
  return (
33
57
  <Flexbox gap={12} id={id} width={'100%'}>
34
- <Inspector
35
- arguments={plugin?.arguments}
36
- content={content}
37
- id={id}
38
- identifier={plugin?.identifier}
39
- loading={loading}
40
- payload={plugin}
41
- setShow={setShow}
42
- showPortal={showPortal}
43
- showRender={showRender}
44
- />
45
- {isMessageToolUIOpen ? (
46
- <Center paddingBlock={8} style={{ background: theme.colorFillQuaternary, borderRadius: 4 }}>
47
- <Empty
48
- description={t('showInPortal')}
49
- image={
50
- <Icon
51
- color={theme.colorTextQuaternary}
52
- icon={direction === 'rtl' ? LucideSquareArrowLeft : LucideSquareArrowRight}
53
- size={'large'}
54
- />
55
- }
56
- imageStyle={{ height: 24 }}
57
- />
58
- </Center>
59
- ) : showRender || loading ? (
58
+ {showPluginRender ? (
60
59
  <PluginRender
61
60
  arguments={plugin?.arguments}
62
61
  content={content}
@@ -68,10 +67,10 @@ const Tool = memo<
68
67
  type={plugin?.type}
69
68
  />
70
69
  ) : (
71
- <Arguments arguments={plugin?.arguments} />
70
+ <Arguments arguments={requestArgs} />
72
71
  )}
73
72
  </Flexbox>
74
73
  );
75
74
  });
76
75
 
77
- export default Tool;
76
+ export default CustomRender;
@@ -0,0 +1,39 @@
1
+ import { Suspense, memo } from 'react';
2
+
3
+ import { useChatStore } from '@/store/chat';
4
+ import { chatSelectors } from '@/store/chat/selectors';
5
+
6
+ import Arguments from './Arguments';
7
+ import CustomRender from './CustomRender';
8
+
9
+ interface RenderProps {
10
+ messageId: string;
11
+ requestArgs?: string;
12
+ setShowPluginRender: (show: boolean) => void;
13
+ showPluginRender: boolean;
14
+ toolCallId: string;
15
+ toolIndex: number;
16
+ }
17
+ const Render = memo<RenderProps>(
18
+ ({ toolCallId, toolIndex, messageId, requestArgs, showPluginRender, setShowPluginRender }) => {
19
+ const loading = useChatStore(chatSelectors.isToolCallStreaming(messageId, toolIndex));
20
+ const toolMessage = useChatStore(chatSelectors.getMessageByToolCallId(toolCallId));
21
+
22
+ // 如果处于 loading 或者找不到 toolMessage 则展示 Arguments
23
+ if (loading || !toolMessage) return <Arguments arguments={requestArgs} />;
24
+
25
+ if (!!toolMessage)
26
+ return (
27
+ <Suspense fallback={<Arguments arguments={requestArgs} shine />}>
28
+ <CustomRender
29
+ {...toolMessage}
30
+ requestArgs={requestArgs}
31
+ setShowPluginRender={setShowPluginRender}
32
+ showPluginRender={showPluginRender}
33
+ />
34
+ </Suspense>
35
+ );
36
+ },
37
+ );
38
+
39
+ export default Render;
@@ -0,0 +1,70 @@
1
+ import { AnimatePresence, motion } from 'framer-motion';
2
+ import { CSSProperties, memo, useState } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import Inspectors from './Inspector';
6
+ import Render from './Render';
7
+
8
+ export interface InspectorProps {
9
+ apiName: string;
10
+ arguments?: string;
11
+ id: string;
12
+ identifier: string;
13
+ index: number;
14
+ messageId: string;
15
+ payload: object;
16
+ style?: CSSProperties;
17
+ }
18
+
19
+ const Tool = memo<InspectorProps>(
20
+ ({ arguments: requestArgs, apiName, messageId, id, index, identifier, style, payload }) => {
21
+ const [showRender, setShowRender] = useState(true);
22
+ const [showPluginRender, setShowPluginRender] = useState(false);
23
+
24
+ return (
25
+ <Flexbox gap={8} style={style}>
26
+ <Inspectors
27
+ apiName={apiName}
28
+ arguments={requestArgs}
29
+ id={id}
30
+ identifier={identifier}
31
+ index={index}
32
+ messageId={messageId}
33
+ payload={payload}
34
+ setShowPluginRender={setShowPluginRender}
35
+ setShowRender={setShowRender}
36
+ showPluginRender={showPluginRender}
37
+ showRender={showRender}
38
+ />
39
+ <AnimatePresence initial={false}>
40
+ {showRender && (
41
+ <motion.div
42
+ animate="open"
43
+ exit="collapsed"
44
+ initial="collapsed"
45
+ transition={{
46
+ duration: 0.1,
47
+ ease: [0.4, 0, 0.2, 1], // 使用 ease-out 缓动函数
48
+ }}
49
+ variants={{
50
+ collapsed: { height: 0, opacity: 0, width: 0 },
51
+ open: { height: 'auto', opacity: 1, width: 'auto' },
52
+ }}
53
+ >
54
+ <Render
55
+ messageId={messageId}
56
+ requestArgs={requestArgs}
57
+ setShowPluginRender={setShowPluginRender}
58
+ showPluginRender={showPluginRender}
59
+ toolCallId={id}
60
+ toolIndex={index}
61
+ />
62
+ </motion.div>
63
+ )}
64
+ </AnimatePresence>
65
+ </Flexbox>
66
+ );
67
+ },
68
+ );
69
+
70
+ export default Tool;
@@ -1,18 +1,15 @@
1
- import { Skeleton } from 'antd';
2
- import { ReactNode, Suspense, memo, useContext } from 'react';
1
+ import { ReactNode, memo } from 'react';
3
2
  import { Flexbox } from 'react-layout-kit';
4
3
 
5
4
  import { LOADING_FLAT } from '@/const/message';
6
5
  import { useChatStore } from '@/store/chat';
7
- import { chatSelectors } from '@/store/chat/selectors';
8
- import { aiChatSelectors } from '@/store/chat/slices/aiChat/selectors';
6
+ import { aiChatSelectors, chatSelectors } from '@/store/chat/selectors';
9
7
  import { ChatMessage } from '@/types/message';
10
8
 
11
- import { InPortalThreadContext } from '../../components/ChatItem/InPortalThreadContext';
12
9
  import { DefaultMessage } from '../Default';
13
10
  import FileChunks from './FileChunks';
14
- import Thinking from './Reasoning';
15
- import ToolCall from './ToolCallItem';
11
+ import Reasoning from './Reasoning';
12
+ import Tool from './Tool';
16
13
 
17
14
  export const AssistantMessage = memo<
18
15
  ChatMessage & {
@@ -22,7 +19,6 @@ export const AssistantMessage = memo<
22
19
  const editing = useChatStore(chatSelectors.isMessageEditing(id));
23
20
  const generating = useChatStore(chatSelectors.isMessageGenerating(id));
24
21
 
25
- const inThread = useContext(InPortalThreadContext);
26
22
  const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
27
23
 
28
24
  const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
@@ -43,7 +39,7 @@ export const AssistantMessage = memo<
43
39
  ) : (
44
40
  <Flexbox gap={8} id={id}>
45
41
  {!!chunksList && chunksList.length > 0 && <FileChunks data={chunksList} />}
46
- {showReasoning && <Thinking {...props.reasoning} id={id} />}
42
+ {showReasoning && <Reasoning {...props.reasoning} id={id} />}
47
43
  {content && (
48
44
  <DefaultMessage
49
45
  addIdOnDOM={false}
@@ -54,24 +50,20 @@ export const AssistantMessage = memo<
54
50
  />
55
51
  )}
56
52
  {tools && (
57
- <Suspense
58
- fallback={<Skeleton.Button active style={{ height: 46, minWidth: 200, width: '100%' }} />}
59
- >
60
- <Flexbox gap={8}>
61
- {tools.map((toolCall, index) => (
62
- <ToolCall
63
- apiName={toolCall.apiName}
64
- arguments={toolCall.arguments}
65
- id={toolCall.id}
66
- identifier={toolCall.identifier}
67
- index={index}
68
- key={toolCall.id}
69
- messageId={id}
70
- showPortal={!inThread}
71
- />
72
- ))}
73
- </Flexbox>
74
- </Suspense>
53
+ <Flexbox gap={8}>
54
+ {tools.map((toolCall, index) => (
55
+ <Tool
56
+ apiName={toolCall.apiName}
57
+ arguments={toolCall.arguments}
58
+ id={toolCall.id}
59
+ identifier={toolCall.identifier}
60
+ index={index}
61
+ key={toolCall.id}
62
+ messageId={id}
63
+ payload={toolCall}
64
+ />
65
+ ))}
66
+ </Flexbox>
75
67
  )}
76
68
  </Flexbox>
77
69
  );
@@ -17,9 +17,9 @@ const PGliteIcon: IconType = forwardRef(({ size = '1em', style }, ref) => {
17
17
  >
18
18
  <title>PGlite</title>
19
19
  <path
20
- clip-rule="evenodd"
20
+ clipRule="evenodd"
21
21
  d="M941.581 335.737v460.806c0 15.926-12.913 28.836-28.832 28.818l-115.283-.137c-15.243-.018-27.706-11.88-28.703-26.877.011-.569.018-1.138.018-1.711l-.004-172.904c0-47.745-38.736-86.451-86.454-86.451-46.245 0-84.052-36.359-86.342-82.068V191.496l201.708.149c79.484.058 143.892 64.553 143.892 144.092zm-576-144.281v201.818c0 47.746 38.682 86.456 86.4 86.456h86.4v-5.796c0 66.816 54.13 120.98 120.902 120.98 28.617 0 51.815 23.213 51.815 51.848v149.644c0 .688.011 1.372.025 2.057-.943 15.065-13.453 26.992-28.746 26.992l-144.982-.007.986-201.586c.079-15.915-12.755-28.88-28.66-28.959-15.904-.079-28.861 12.763-28.94 28.678l-.986 201.741v.118l-172.174-.01V623.722c0-15.915-12.895-28.819-28.8-28.819-15.906 0-28.8 12.904-28.8 28.819v201.704l-143.642-.007c-15.905-.004-28.798-12.904-28.798-28.819V335.547c0-79.58 64.471-144.093 144.001-144.092l143.999.001zm446.544 173.693c0-23.874-19.343-43.228-43.2-43.228-23.861 0-43.2 19.354-43.2 43.228 0 23.875 19.339 43.226 43.2 43.226 23.857 0 43.2-19.351 43.2-43.226z"
22
- fill-rule="evenodd"
22
+ fillRule="evenodd"
23
23
  />
24
24
  </svg>
25
25
  );
@@ -1,21 +1,12 @@
1
1
  import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
2
- import { Skeleton } from 'antd';
3
- import dynamic from 'next/dynamic';
4
2
  import { memo } from 'react';
5
3
 
6
4
  import { LobeToolRenderType } from '@/types/tool';
7
5
 
6
+ import BuiltinType from './BuiltinType';
8
7
  import DefaultType from './DefaultType';
9
8
  import Markdown from './MarkdownType';
10
-
11
- const loading = () => (
12
- <Skeleton.Node active style={{ width: '100%' }}>
13
- {' '}
14
- </Skeleton.Node>
15
- );
16
-
17
- const Standalone = dynamic(() => import('./StandaloneType'), { loading });
18
- const BuiltinType = dynamic(() => import('./BuiltinType'), { loading });
9
+ import Standalone from './StandaloneType';
19
10
 
20
11
  export interface PluginRenderProps {
21
12
  arguments?: string;
@@ -119,6 +119,10 @@ export default {
119
119
  reinstallError: '插件 {{name}} 刷新失败',
120
120
  urlError: '该链接没有返回 JSON 格式的内容, 请确保是有效的链接',
121
121
  },
122
+ inspector: {
123
+ args: '查看参数列表',
124
+ pluginRender: '查看插件界面',
125
+ },
122
126
  list: {
123
127
  item: {
124
128
  'deprecated.title': '已删除',
@@ -1,4 +1,6 @@
1
1
  import { css } from 'antd-style';
2
+ import type { FullToken } from 'antd-style/lib/types';
3
+ import { rgba } from 'polished';
2
4
 
3
5
  export const dotLoading = css`
4
6
  &::after {
@@ -26,3 +28,28 @@ export const dotLoading = css`
26
28
  }
27
29
  }
28
30
  `;
31
+
32
+ export const shinyTextStylish = (token: FullToken) => css`
33
+ color: ${rgba(token.colorText, 0.45)};
34
+
35
+ background: linear-gradient(
36
+ 120deg,
37
+ ${rgba(token.colorTextBase, 0)} 40%,
38
+ ${token.colorTextSecondary} 50%,
39
+ ${rgba(token.colorTextBase, 0)} 60%
40
+ );
41
+ background-clip: text;
42
+ background-size: 200% 100%;
43
+
44
+ animation: shine 1.5s linear infinite;
45
+
46
+ @keyframes shine {
47
+ 0% {
48
+ background-position: 100%;
49
+ }
50
+
51
+ 100% {
52
+ background-position: -100%;
53
+ }
54
+ }
55
+ `;
@@ -0,0 +1,60 @@
1
+ import { useResponsive } from 'antd-style';
2
+ import { ReactNode, memo, useMemo } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import Grid from '@/components/GalleyGrid/Grid';
6
+
7
+ const MAX_SIZE_DESKTOP = 640;
8
+ const MAX_SIZE_MOBILE = 280;
9
+
10
+ interface GalleyGridProps<T = any> {
11
+ items: T[];
12
+ renderItem: (props: T) => ReactNode;
13
+ }
14
+
15
+ const GalleyGrid = memo<GalleyGridProps>(({ items, renderItem: Render }) => {
16
+ const { mobile } = useResponsive();
17
+
18
+ const { firstRow, lastRow } = useMemo(() => {
19
+ if (items.length === 4) {
20
+ return {
21
+ firstRow: items.slice(0, 2),
22
+ lastRow: items.slice(2, 4),
23
+ };
24
+ }
25
+
26
+ const firstCol = items.length % 3 === 0 ? 3 : items.length % 3;
27
+
28
+ return {
29
+ firstRow: items.slice(0, firstCol),
30
+ lastRow: items.slice(firstCol, items.length),
31
+ };
32
+ }, [items]);
33
+
34
+ const { gap, max } = useMemo(
35
+ () => ({
36
+ gap: mobile ? 4 : 6,
37
+ max: (mobile ? MAX_SIZE_MOBILE : MAX_SIZE_DESKTOP) * firstRow.length,
38
+ }),
39
+ [mobile],
40
+ );
41
+
42
+ return (
43
+ <Flexbox gap={gap}>
44
+ <Grid col={firstRow.length} gap={gap} max={max}>
45
+ {firstRow.map((i, index) => (
46
+ <Render {...i} index={index} key={index} />
47
+ ))}
48
+ </Grid>
49
+ {lastRow.length > 0 && (
50
+ <Grid col={lastRow.length > 2 ? 3 : lastRow.length} gap={gap} max={max}>
51
+ {lastRow.map((i, index) => (
52
+ <Render {...i} index={index} key={index} />
53
+ ))}
54
+ </Grid>
55
+ )}
56
+ </Flexbox>
57
+ );
58
+ });
59
+
60
+ export default GalleyGrid;
@@ -3,11 +3,11 @@ import { Download } from 'lucide-react';
3
3
  import { memo, useRef } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
- import GalleyGrid from '@/components/GalleyGrid';
7
6
  import { fileService } from '@/services/file';
8
7
  import { BuiltinRenderProps } from '@/types/tool';
9
8
  import { DallEImageItem } from '@/types/tool/dalle';
10
9
 
10
+ import GalleyGrid from './GalleyGrid';
11
11
  import ImageItem from './Item';
12
12
 
13
13
  const DallE = memo<BuiltinRenderProps<DallEImageItem[]>>(({ content, messageId }) => {