@lobehub/chat 1.18.2 → 1.19.0

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.

Potentially problematic release.


This version of @lobehub/chat might be problematic. Click here for more details.

Files changed (152) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/Dockerfile +2 -0
  3. package/Dockerfile.database +2 -0
  4. package/locales/ar/chat.json +6 -0
  5. package/locales/ar/error.json +1 -0
  6. package/locales/ar/modelProvider.json +7 -0
  7. package/locales/ar/portal.json +16 -0
  8. package/locales/bg-BG/chat.json +6 -0
  9. package/locales/bg-BG/error.json +1 -0
  10. package/locales/bg-BG/modelProvider.json +7 -0
  11. package/locales/bg-BG/portal.json +16 -0
  12. package/locales/de-DE/chat.json +6 -0
  13. package/locales/de-DE/error.json +1 -0
  14. package/locales/de-DE/modelProvider.json +7 -0
  15. package/locales/de-DE/portal.json +16 -0
  16. package/locales/en-US/chat.json +6 -0
  17. package/locales/en-US/error.json +1 -0
  18. package/locales/en-US/modelProvider.json +7 -0
  19. package/locales/en-US/portal.json +16 -0
  20. package/locales/es-ES/chat.json +6 -0
  21. package/locales/es-ES/error.json +1 -0
  22. package/locales/es-ES/modelProvider.json +7 -0
  23. package/locales/es-ES/portal.json +16 -0
  24. package/locales/fr-FR/chat.json +6 -0
  25. package/locales/fr-FR/error.json +1 -0
  26. package/locales/fr-FR/modelProvider.json +7 -0
  27. package/locales/fr-FR/portal.json +16 -0
  28. package/locales/it-IT/chat.json +6 -0
  29. package/locales/it-IT/error.json +1 -0
  30. package/locales/it-IT/modelProvider.json +7 -0
  31. package/locales/it-IT/portal.json +16 -0
  32. package/locales/ja-JP/chat.json +6 -0
  33. package/locales/ja-JP/error.json +1 -0
  34. package/locales/ja-JP/modelProvider.json +7 -0
  35. package/locales/ja-JP/portal.json +16 -0
  36. package/locales/ko-KR/chat.json +6 -0
  37. package/locales/ko-KR/error.json +1 -0
  38. package/locales/ko-KR/modelProvider.json +7 -0
  39. package/locales/ko-KR/portal.json +16 -0
  40. package/locales/nl-NL/chat.json +6 -0
  41. package/locales/nl-NL/error.json +1 -0
  42. package/locales/nl-NL/modelProvider.json +7 -0
  43. package/locales/nl-NL/portal.json +16 -0
  44. package/locales/pl-PL/chat.json +6 -0
  45. package/locales/pl-PL/error.json +1 -0
  46. package/locales/pl-PL/modelProvider.json +7 -0
  47. package/locales/pl-PL/portal.json +16 -0
  48. package/locales/pt-BR/chat.json +6 -0
  49. package/locales/pt-BR/error.json +1 -0
  50. package/locales/pt-BR/modelProvider.json +7 -0
  51. package/locales/pt-BR/portal.json +16 -0
  52. package/locales/ru-RU/chat.json +6 -0
  53. package/locales/ru-RU/error.json +1 -0
  54. package/locales/ru-RU/modelProvider.json +7 -0
  55. package/locales/ru-RU/portal.json +16 -0
  56. package/locales/tr-TR/chat.json +6 -0
  57. package/locales/tr-TR/error.json +1 -0
  58. package/locales/tr-TR/modelProvider.json +7 -0
  59. package/locales/tr-TR/portal.json +16 -0
  60. package/locales/vi-VN/chat.json +6 -0
  61. package/locales/vi-VN/error.json +1 -0
  62. package/locales/vi-VN/modelProvider.json +7 -0
  63. package/locales/vi-VN/portal.json +16 -0
  64. package/locales/zh-CN/chat.json +6 -0
  65. package/locales/zh-CN/error.json +2 -1
  66. package/locales/zh-CN/modelProvider.json +7 -0
  67. package/locales/zh-CN/portal.json +17 -1
  68. package/locales/zh-TW/chat.json +6 -0
  69. package/locales/zh-TW/error.json +1 -0
  70. package/locales/zh-TW/modelProvider.json +7 -0
  71. package/locales/zh-TW/portal.json +16 -0
  72. package/package.json +3 -1
  73. package/src/app/(main)/chat/(workspace)/@portal/Artifacts/Body/Renderer/HTML.tsx +25 -0
  74. package/src/app/(main)/chat/(workspace)/@portal/Artifacts/Body/Renderer/React.tsx +30 -0
  75. package/src/app/(main)/chat/(workspace)/@portal/Artifacts/Body/Renderer/SVG.tsx +114 -0
  76. package/src/app/(main)/chat/(workspace)/@portal/Artifacts/Body/Renderer/index.tsx +25 -0
  77. package/src/app/(main)/chat/(workspace)/@portal/Artifacts/Body/index.tsx +79 -0
  78. package/src/app/(main)/chat/(workspace)/@portal/Artifacts/Header.tsx +69 -0
  79. package/src/app/(main)/chat/(workspace)/@portal/Artifacts/index.ts +10 -0
  80. package/src/app/(main)/chat/(workspace)/@portal/Artifacts/useEnable.ts +4 -0
  81. package/src/app/(main)/chat/(workspace)/@portal/FilePreview/index.ts +2 -1
  82. package/src/app/(main)/chat/(workspace)/@portal/Home/Body/{Artifacts → Plugins}/index.tsx +1 -1
  83. package/src/app/(main)/chat/(workspace)/@portal/Home/Body/index.tsx +2 -2
  84. package/src/app/(main)/chat/(workspace)/@portal/MessageDetail/index.ts +2 -1
  85. package/src/app/(main)/chat/(workspace)/@portal/Plugins/Body/ToolRender.tsx +1 -1
  86. package/src/app/(main)/chat/(workspace)/@portal/Plugins/Body/index.tsx +1 -1
  87. package/src/app/(main)/chat/(workspace)/@portal/Plugins/Footer.tsx +1 -1
  88. package/src/app/(main)/chat/(workspace)/@portal/Plugins/index.ts +2 -1
  89. package/src/app/(main)/chat/(workspace)/@portal/Plugins/useEnable.ts +1 -1
  90. package/src/app/(main)/chat/(workspace)/@portal/_layout/Desktop.tsx +2 -4
  91. package/src/app/(main)/chat/(workspace)/@portal/features/Body.tsx +27 -0
  92. package/src/app/(main)/chat/(workspace)/@portal/router.tsx +3 -1
  93. package/src/app/(main)/chat/(workspace)/@portal/type.ts +7 -0
  94. package/src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx +3 -2
  95. package/src/app/(main)/settings/llm/ProviderList/Github/index.tsx +53 -0
  96. package/src/app/(main)/settings/llm/ProviderList/providers.tsx +6 -1
  97. package/src/app/api/chat/agentRuntime.ts +14 -0
  98. package/src/components/SidebarHeader/index.tsx +1 -1
  99. package/src/config/llm.ts +14 -0
  100. package/src/config/modelProviders/ai21.ts +37 -0
  101. package/src/config/modelProviders/anthropic.ts +4 -0
  102. package/src/config/modelProviders/github.ts +209 -0
  103. package/src/config/modelProviders/index.ts +8 -0
  104. package/src/const/layoutTokens.ts +1 -1
  105. package/src/const/plugin.test.ts +80 -0
  106. package/src/const/plugin.ts +12 -0
  107. package/src/const/settings/llm.ts +10 -0
  108. package/src/features/Conversation/Error/APIKeyForm/index.tsx +4 -0
  109. package/src/features/Conversation/Messages/Tool/Inspector/index.tsx +1 -1
  110. package/src/features/Conversation/Messages/Tool/index.tsx +1 -1
  111. package/src/features/Conversation/components/ChatItem/index.tsx +24 -2
  112. package/src/features/Conversation/components/ChatItem/utils.test.ts +150 -0
  113. package/src/features/Conversation/components/ChatItem/utils.ts +28 -0
  114. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/Icon.tsx +96 -0
  115. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/index.tsx +129 -0
  116. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/index.ts +10 -0
  117. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/rehypePlugin.ts +74 -0
  118. package/src/features/Conversation/components/MarkdownElements/LobeThinking/Render.tsx +86 -0
  119. package/src/features/Conversation/components/MarkdownElements/LobeThinking/index.ts +12 -0
  120. package/src/features/Conversation/components/MarkdownElements/LobeThinking/rehypePlugin.test.ts +124 -0
  121. package/src/features/Conversation/components/MarkdownElements/LobeThinking/rehypePlugin.ts +51 -0
  122. package/src/features/Conversation/components/MarkdownElements/index.ts +4 -0
  123. package/src/features/Conversation/components/MarkdownElements/type.ts +7 -0
  124. package/src/libs/agent-runtime/AgentRuntime.ts +14 -0
  125. package/src/libs/agent-runtime/ai21/index.test.ts +255 -0
  126. package/src/libs/agent-runtime/ai21/index.ts +18 -0
  127. package/src/libs/agent-runtime/error.ts +2 -0
  128. package/src/libs/agent-runtime/github/index.test.ts +246 -0
  129. package/src/libs/agent-runtime/github/index.ts +15 -0
  130. package/src/libs/agent-runtime/openrouter/__snapshots__/index.test.ts.snap +2215 -28
  131. package/src/libs/agent-runtime/openrouter/fixtures/models.json +3345 -37
  132. package/src/libs/agent-runtime/openrouter/index.ts +2 -1
  133. package/src/libs/agent-runtime/types/type.ts +2 -0
  134. package/src/locales/default/chat.ts +7 -0
  135. package/src/locales/default/error.ts +3 -0
  136. package/src/locales/default/modelProvider.ts +7 -0
  137. package/src/locales/default/portal.ts +17 -1
  138. package/src/server/globalConfig/index.ts +14 -0
  139. package/src/store/chat/slices/message/selectors.ts +30 -28
  140. package/src/store/chat/slices/portal/action.ts +15 -2
  141. package/src/store/chat/slices/portal/initialState.ts +11 -0
  142. package/src/store/chat/slices/portal/selectors.test.ts +29 -7
  143. package/src/store/chat/slices/portal/selectors.ts +56 -12
  144. package/src/styles/loading.ts +28 -0
  145. package/src/tools/artifacts/index.ts +13 -0
  146. package/src/tools/artifacts/systemRole.ts +338 -0
  147. package/src/tools/index.ts +6 -0
  148. package/src/types/user/settings/keyVaults.ts +2 -0
  149. package/src/utils/clipboard.ts +53 -0
  150. /package/src/app/(main)/chat/(workspace)/@portal/Home/Body/{Artifacts → Plugins}/ArtifactList/Item/index.tsx +0 -0
  151. /package/src/app/(main)/chat/(workspace)/@portal/Home/Body/{Artifacts → Plugins}/ArtifactList/Item/style.ts +0 -0
  152. /package/src/app/(main)/chat/(workspace)/@portal/Home/Body/{Artifacts → Plugins}/ArtifactList/index.tsx +0 -0
@@ -0,0 +1,150 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { processWithArtifact } from './utils';
4
+
5
+ describe('processWithArtifact', () => {
6
+ it('should removeLineBreaks with closed tag', () => {
7
+ const input = `好的
8
+
9
+ <lobeArtifact identifier="sleep-interpretation-card" type="image/svg+xml" title="睡觉的新解释">
10
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 600">
11
+ <defs>
12
+ <style>
13
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&amp;display=swap');
14
+ </style>
15
+ </defs>
16
+ <!-- 背景 -->
17
+ <rect width="400" height="600" fill="#F0EAD6"/>
18
+ <!-- 总结 -->
19
+ <text x="200" y="500" font-family="'Noto Serif SC', serif" font-size="20" text-anchor="middle" fill="#8B4513">睡觉:生产力的假死,创造力的重生。</text>
20
+ </svg>
21
+ </lobeArtifact>`;
22
+
23
+ const output = processWithArtifact(input);
24
+
25
+ expect(output).toEqual(`好的
26
+
27
+ <lobeArtifact identifier="sleep-interpretation-card" type="image/svg+xml" title="睡觉的新解释"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 600"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&amp;display=swap');</style></defs><!-- 背景 --><rect width="400" height="600" fill="#F0EAD6"/><!-- 总结 --><text x="200" y="500" font-family="'Noto Serif SC', serif" font-size="20" text-anchor="middle" fill="#8B4513">睡觉:生产力的假死,创造力的重生。</text></svg></lobeArtifact>`);
28
+ });
29
+
30
+ it('should removeLineBreaks with open tag', () => {
31
+ const input = `好的
32
+
33
+ <lobeArtifact identifier="ai-interpretation-card" type="image/svg+xml" title="人工智能新解卡片">
34
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 600">
35
+ <defs>
36
+ <style>
37
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&amp;display=swap');
38
+ </style>
39
+ </defs>
40
+ `;
41
+
42
+ const output = processWithArtifact(input);
43
+
44
+ expect(output).toEqual(`好的
45
+
46
+ <lobeArtifact identifier="ai-interpretation-card" type="image/svg+xml" title="人工智能新解卡片"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 600"> <defs> <style> @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&amp;display=swap'); </style> </defs>`);
47
+ });
48
+ it('should not throw error with empty', () => {
49
+ const input = '';
50
+
51
+ const output = processWithArtifact(input);
52
+
53
+ expect(output).toEqual('');
54
+ });
55
+
56
+ describe('close the <lobeArtifact tag', () => {
57
+ it('close tag for <lobeArtifact', () => {
58
+ const input = '<lobeArtifact';
59
+
60
+ const output = processWithArtifact(input);
61
+
62
+ expect(output).toEqual('<lobeArtifact>');
63
+ });
64
+
65
+ it('close tag for <lobeArtifact identifier="something"', () => {
66
+ const input = '<lobeArtifact identifier="something"';
67
+
68
+ const output = processWithArtifact(input);
69
+
70
+ expect(output).toEqual('<lobeArtifact>');
71
+ });
72
+
73
+ it('close tag for <lobeArtifact identifier="ai-interpretation" type="image/svg+xml" titl', () => {
74
+ const input = '<lobeArtifact identifier="ai-interpretation" type="image/svg+xml" titl';
75
+
76
+ const output = processWithArtifact(input);
77
+
78
+ expect(output).toEqual('<lobeArtifact>');
79
+ });
80
+
81
+ it('only change the <lobeArtifact> part', () => {
82
+ const input = `好的,让我来用新的视角解释"人工智能"这个词汇。
83
+
84
+ <lobeThinking>这个词汇涉及了当代科技和社会热点,需要用批判性和幽默感来解读其本质。我会用隐喻和讽刺来表达,同时保持简洁有力。</lobeThinking>
85
+
86
+ <lobeArtifact identifier="ai-new-interpretation" type="image/svg+xml" t`;
87
+
88
+ const output = processWithArtifact(input);
89
+
90
+ expect(output).toEqual(`好的,让我来用新的视角解释"人工智能"这个词汇。
91
+
92
+ <lobeThinking>这个词汇涉及了当代科技和社会热点,需要用批判性和幽默感来解读其本质。我会用隐喻和讽刺来表达,同时保持简洁有力。</lobeThinking>
93
+
94
+ <lobeArtifact>`);
95
+ });
96
+
97
+ it('not change for <lobeArtifact />', () => {
98
+ const input = '<lobeArtifact/>';
99
+
100
+ const output = processWithArtifact(input);
101
+
102
+ expect(output).toEqual(input);
103
+ });
104
+ });
105
+
106
+ it('should removeLinkBreaks for lobeThinking', () => {
107
+ const input = `好的,让我以一个特别的视角来解释"人工智能"这个词汇。
108
+
109
+ <lobeThinking>
110
+ 这个词汇涉及了当代科技和社会热点,需要用批判性、幽默而深刻的视角来解读。我会运用隐喻和讽刺,抓住其本质,并以精练的方式表达出来。这符合一个好的artifact的标准,因为它是一个独立的、可能被用户修改或重用的内容。我将创建一个新的SVG artifact来呈现这个解释。
111
+ </lobeThinking>
112
+
113
+ <lobeArtifact identifier="ai-new-interpretation" type="image/svg+xml" title="人工智能的新解释">
114
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 600">
115
+ <defs>
116
+ <style>
117
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&amp;display=swap');
118
+ .background { fill: #f0f0f0; }
119
+ .title { font-family: 'Noto Serif SC', serif; font-size: 28px; font-weight: 700; fill: #333; }
120
+ .content { font-family: 'Noto Serif SC', serif; font-size: 18px; fill: #555; }
121
+ .divider { stroke: #999; stroke-width: 1; }
122
+ .decoration { fill: none; stroke: #999; stroke-width: 1; }
123
+ </style>
124
+ </defs>
125
+ </svg>
126
+ </lobeArtifact>
127
+
128
+ 我为"人工智能"这个词创建了一个新的解释,并将其呈现在一个SVG卡片中。这个解释采用了批判性和幽默的视角,试图揭示这个概念背后的一些潜在问题。`;
129
+
130
+ const output = processWithArtifact(input);
131
+
132
+ expect(output).toEqual(`好的,让我以一个特别的视角来解释"人工智能"这个词汇。
133
+
134
+ <lobeThinking>这个词汇涉及了当代科技和社会热点,需要用批判性、幽默而深刻的视角来解读。我会运用隐喻和讽刺,抓住其本质,并以精练的方式表达出来。这符合一个好的artifact的标准,因为它是一个独立的、可能被用户修改或重用的内容。我将创建一个新的SVG artifact来呈现这个解释。</lobeThinking>
135
+
136
+ <lobeArtifact identifier="ai-new-interpretation" type="image/svg+xml" title="人工智能的新解释"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 600"> <defs> <style> @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&amp;display=swap'); .background { fill: #f0f0f0; } .title { font-family: 'Noto Serif SC', serif; font-size: 28px; font-weight: 700; fill: #333; } .content { font-family: 'Noto Serif SC', serif; font-size: 18px; fill: #555; } .divider { stroke: #999; stroke-width: 1; } .decoration { fill: none; stroke: #999; stroke-width: 1; } </style> </defs></svg></lobeArtifact>
137
+
138
+ 我为"人工智能"这个词创建了一个新的解释,并将其呈现在一个SVG卡片中。这个解释采用了批判性和幽默的视角,试图揭示这个概念背后的一些潜在问题。`);
139
+ });
140
+
141
+ it('should removeLinkBreaks for lobeThinking', () => {
142
+ const input = `<lobeThinking>
143
+ 这个词汇涉及了
144
+ `;
145
+
146
+ const output = processWithArtifact(input);
147
+
148
+ expect(output).toEqual(`<lobeThinking>这个词汇涉及了`);
149
+ });
150
+ });
@@ -0,0 +1,28 @@
1
+ import { ARTIFACT_TAG_REGEX, ARTIFACT_THINKING_TAG_REGEX } from '@/const/plugin';
2
+
3
+ /**
4
+ * Replace all line breaks in the matched `lobeArtifact` tag with an empty string
5
+ */
6
+ export const processWithArtifact = (input: string = '') => {
7
+ let output = input;
8
+ const thinkMatch = ARTIFACT_THINKING_TAG_REGEX.exec(input);
9
+
10
+ // If the input contains the `lobeThinking` tag, replace all line breaks with an empty string
11
+ if (thinkMatch)
12
+ output = input.replace(ARTIFACT_THINKING_TAG_REGEX, (match) =>
13
+ match.replaceAll(/\r?\n|\r/g, ''),
14
+ );
15
+
16
+ const match = ARTIFACT_TAG_REGEX.exec(input);
17
+ // If the input contains the `lobeArtifact` tag, replace all line breaks with an empty string
18
+ if (match)
19
+ return output.replace(ARTIFACT_TAG_REGEX, (match) => match.replaceAll(/\r?\n|\r/g, ''));
20
+
21
+ // if not match, check if it's start with <lobeArtifact but not closed
22
+ const regex = /<lobeArtifact\b(?:(?!\/?>)[\S\s])*$/;
23
+ if (regex.test(output)) {
24
+ return output.replace(regex, '<lobeArtifact>');
25
+ }
26
+
27
+ return output;
28
+ };
@@ -0,0 +1,96 @@
1
+ import { SiReact } from '@icons-pack/react-simple-icons';
2
+ import { Icon } from '@lobehub/ui';
3
+ import { createStyles } from 'antd-style';
4
+ import { CodeXml, GlobeIcon, ImageIcon, Loader2, OrigamiIcon } from 'lucide-react';
5
+ import { memo } from 'react';
6
+
7
+ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
8
+ container: css`
9
+ cursor: pointer;
10
+
11
+ margin-block-start: 12px;
12
+
13
+ color: ${token.colorText};
14
+
15
+ border: 1px solid ${token.colorBorder};
16
+ border-radius: 8px;
17
+
18
+ &:hover {
19
+ background: ${isDarkMode ? '' : token.colorFillSecondary};
20
+ }
21
+ `,
22
+
23
+ desc: css`
24
+ color: ${token.colorTextTertiary};
25
+ `,
26
+ title: css`
27
+ overflow: hidden;
28
+ display: -webkit-box;
29
+ -webkit-box-orient: vertical;
30
+ -webkit-line-clamp: 1;
31
+
32
+ font-size: 16px;
33
+ text-overflow: ellipsis;
34
+ `,
35
+ }));
36
+
37
+ interface ArtifactProps {
38
+ type: string;
39
+ }
40
+
41
+ const SIZE = 28;
42
+ const ArtifactIcon = memo<ArtifactProps>(({ type }) => {
43
+ const { theme } = useStyles();
44
+
45
+ if (!type)
46
+ return (
47
+ <Icon
48
+ icon={Loader2}
49
+ size={{ fontSize: SIZE }}
50
+ spin
51
+ style={{ color: theme.colorTextSecondary }}
52
+ />
53
+ );
54
+
55
+ switch (type) {
56
+ case 'application/lobe.artifacts.code': {
57
+ return (
58
+ <Icon
59
+ icon={CodeXml}
60
+ size={{ fontSize: SIZE }}
61
+ style={{ color: theme.colorTextSecondary }}
62
+ />
63
+ );
64
+ }
65
+
66
+ case 'application/lobe.artifacts.react': {
67
+ return <SiReact size={SIZE} style={{ color: theme.colorTextSecondary }} />;
68
+ }
69
+
70
+ case 'image/svg+xml': {
71
+ return (
72
+ <Icon
73
+ icon={ImageIcon}
74
+ size={{ fontSize: SIZE }}
75
+ style={{ color: theme.colorTextSecondary }}
76
+ />
77
+ );
78
+ }
79
+ case 'text/html': {
80
+ return (
81
+ <Icon
82
+ icon={GlobeIcon}
83
+ size={{ fontSize: SIZE }}
84
+ style={{ color: theme.colorTextSecondary }}
85
+ />
86
+ );
87
+ }
88
+ default: {
89
+ return (
90
+ <Icon color={theme.purple} icon={OrigamiIcon} size={{ fontSize: SIZE, strokeWidth: 1.2 }} />
91
+ );
92
+ }
93
+ }
94
+ });
95
+
96
+ export default ArtifactIcon;
@@ -0,0 +1,129 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { Loader2 } from 'lucide-react';
4
+ import { memo, useEffect } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Center, Flexbox } from 'react-layout-kit';
7
+
8
+ import { useChatStore } from '@/store/chat';
9
+ import { chatPortalSelectors, chatSelectors } from '@/store/chat/selectors';
10
+ import { dotLoading } from '@/styles/loading';
11
+
12
+ import { MarkdownElementProps } from '../../type';
13
+ import ArtifactIcon from './Icon';
14
+
15
+ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
16
+ avatar: css`
17
+ background: ${token.colorFillQuaternary};
18
+ border-inline-end: 1px solid ${token.colorSplit};
19
+ `,
20
+ container: css`
21
+ cursor: pointer;
22
+
23
+ margin-block-start: 12px;
24
+
25
+ color: ${token.colorText};
26
+
27
+ border: 1px solid ${token.colorBorder};
28
+ border-radius: 8px;
29
+ box-shadow: ${isDarkMode ? token.boxShadowSecondary : token.boxShadowTertiary};
30
+
31
+ &:hover {
32
+ background: ${token.colorFillQuaternary};
33
+ }
34
+ `,
35
+ desc: css`
36
+ font-size: 12px;
37
+ color: ${token.colorTextTertiary};
38
+ `,
39
+ title: css`
40
+ overflow: hidden;
41
+ display: -webkit-box;
42
+ -webkit-box-orient: vertical;
43
+ -webkit-line-clamp: 1;
44
+
45
+ text-overflow: ellipsis;
46
+ `,
47
+ }));
48
+
49
+ interface ArtifactProps extends MarkdownElementProps {
50
+ identifier: string;
51
+ title: string;
52
+ type: string;
53
+ }
54
+
55
+ const Render = memo<ArtifactProps>(({ identifier, title, type, children, id }) => {
56
+ const { t } = useTranslation('chat');
57
+ const { styles, cx } = useStyles();
58
+
59
+ const hasChildren = !!children;
60
+ const str = ((children as string) || '').toString?.();
61
+
62
+ const [isGenerating, isArtifactTagClosed, currentArtifactMessageId, openArtifact, closeArtifact] =
63
+ useChatStore((s) => {
64
+ return [
65
+ chatSelectors.isMessageGenerating(id)(s),
66
+ chatPortalSelectors.isArtifactTagClosed(id)(s),
67
+ chatPortalSelectors.artifactMessageId(s),
68
+ s.openArtifact,
69
+ s.closeArtifact,
70
+ ];
71
+ });
72
+
73
+ const openArtifactUI = () => {
74
+ openArtifact({ id, identifier, title, type });
75
+ };
76
+
77
+ useEffect(() => {
78
+ if (!hasChildren || !isGenerating) return;
79
+
80
+ openArtifactUI();
81
+ }, [isGenerating, hasChildren, str, identifier, title, type, id]);
82
+
83
+ return (
84
+ <p>
85
+ <Flexbox
86
+ className={styles.container}
87
+ gap={16}
88
+ onClick={() => {
89
+ if (currentArtifactMessageId === id) {
90
+ closeArtifact();
91
+ } else {
92
+ openArtifactUI();
93
+ }
94
+ }}
95
+ width={'100%'}
96
+ >
97
+ <Flexbox align={'center'} flex={1} horizontal>
98
+ <Center className={styles.avatar} height={64} horizontal width={64}>
99
+ <ArtifactIcon type={type} />
100
+ </Center>
101
+ <Flexbox gap={4} paddingBlock={8} paddingInline={12}>
102
+ {!title && isGenerating ? (
103
+ <Flexbox className={cx(dotLoading)} horizontal>
104
+ {t('artifact.generating')}
105
+ </Flexbox>
106
+ ) : (
107
+ <Flexbox className={cx(styles.title)}>{title || t('artifact.unknownTitle')}</Flexbox>
108
+ )}
109
+ {hasChildren && (
110
+ <Flexbox className={styles.desc} horizontal>
111
+ {identifier} ·{' '}
112
+ <Flexbox gap={2} horizontal>
113
+ {!isArtifactTagClosed && (
114
+ <div>
115
+ <Icon icon={Loader2} spin />
116
+ </div>
117
+ )}
118
+ {str?.length}
119
+ </Flexbox>
120
+ </Flexbox>
121
+ )}
122
+ </Flexbox>
123
+ </Flexbox>
124
+ </Flexbox>
125
+ </p>
126
+ );
127
+ });
128
+
129
+ export default Render;
@@ -0,0 +1,10 @@
1
+ import Component from './Render';
2
+ import rehypePlugin from './rehypePlugin';
3
+
4
+ const AntArtifactElement = {
5
+ Component,
6
+ rehypePlugin,
7
+ tag: 'lobeArtifact',
8
+ };
9
+
10
+ export default AntArtifactElement;
@@ -0,0 +1,74 @@
1
+ import { SKIP, visit } from 'unist-util-visit';
2
+
3
+ import { ARTIFACT_TAG } from '@/const/plugin';
4
+
5
+ function rehypeAntArtifact() {
6
+ return (tree: any) => {
7
+ visit(tree, (node, index, parent) => {
8
+ if (node.type === 'element' && node.tagName === 'p' && node.children.length > 0) {
9
+ const firstChild = node.children[0];
10
+ if (firstChild.type === 'raw' && firstChild.value.startsWith(`<${ARTIFACT_TAG}`)) {
11
+ // 提取 lobeArtifact 的属性
12
+ const attributes: Record<string, string> = {};
13
+ const attributeRegex = /(\w+)="([^"]*)"/g;
14
+ let match;
15
+ while ((match = attributeRegex.exec(firstChild.value)) !== null) {
16
+ attributes[match[1]] = match[2];
17
+ }
18
+
19
+ // 创建新的 lobeArtifact 节点
20
+ const newNode = {
21
+ children: [
22
+ {
23
+ type: 'text',
24
+ value: node.children
25
+ .slice(1, -1)
26
+ .map((child: any) => {
27
+ if (child.type === 'raw') {
28
+ return child.value;
29
+ } else if (child.type === 'text') {
30
+ return child.value;
31
+ } else if (child.type === 'element' && child.tagName === 'a') {
32
+ return child.children[0].value;
33
+ }
34
+ return '';
35
+ })
36
+ .join('')
37
+ .trim(),
38
+ },
39
+ ],
40
+ properties: attributes,
41
+ tagName: ARTIFACT_TAG,
42
+ type: 'element',
43
+ };
44
+
45
+ // 替换原来的 p 节点
46
+ parent.children.splice(index, 1, newNode);
47
+ return [SKIP, index];
48
+ }
49
+ }
50
+ // 如果字符串是 <lobeArtifact identifier="ai-new-interpretation" type="image/svg+xml" title="人工智能新解释">
51
+ // 得到的节点就是:
52
+ // {
53
+ // type: 'raw',
54
+ // value:
55
+ // '<lobeArtifact identifier="ai-new-interpretation" type="image/svg+xml" title="人工智能新解释">',
56
+ // }
57
+ else if (node.type === 'raw' && node.value.startsWith(`<${ARTIFACT_TAG}`)) {
58
+ // 创建新的 lobeArtifact 节点
59
+ const newNode = {
60
+ children: [],
61
+ properties: {},
62
+ tagName: ARTIFACT_TAG,
63
+ type: 'element',
64
+ };
65
+
66
+ // 替换原来的 p 节点
67
+ parent.children.splice(index, 1, newNode);
68
+ return [SKIP, index];
69
+ }
70
+ });
71
+ };
72
+ }
73
+
74
+ export default rehypeAntArtifact;
@@ -0,0 +1,86 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { BringToFrontIcon, ChevronDown, ChevronRight, Loader2Icon } 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 { ARTIFACT_THINKING_TAG } from '@/const/plugin';
9
+ import { useChatStore } from '@/store/chat';
10
+ import { chatSelectors } from '@/store/chat/selectors';
11
+ import { dotLoading } from '@/styles/loading';
12
+
13
+ import { MarkdownElementProps } from '../type';
14
+
15
+ /**
16
+ * Replace all line breaks in the matched `lobeArtifact` tag with an empty string
17
+ */
18
+ export const isLobeThinkingClosed = (input: string = '') => {
19
+ const openTag = `<${ARTIFACT_THINKING_TAG}>`;
20
+ const closeTag = `</${ARTIFACT_THINKING_TAG}>`;
21
+
22
+ return input.includes(openTag) && input.includes(closeTag);
23
+ };
24
+
25
+ const useStyles = createStyles(({ css, token }) => ({
26
+ container: css`
27
+ cursor: pointer;
28
+
29
+ padding-block: 8px;
30
+ padding-inline: 12px;
31
+ padding-inline-end: 12px;
32
+
33
+ color: ${token.colorText};
34
+
35
+ background: ${token.colorFillQuaternary};
36
+ border-radius: 8px;
37
+ `,
38
+ title: css`
39
+ overflow: hidden;
40
+ display: -webkit-box;
41
+ -webkit-box-orient: vertical;
42
+ -webkit-line-clamp: 1;
43
+
44
+ font-size: 12px;
45
+ text-overflow: ellipsis;
46
+ `,
47
+ }));
48
+
49
+ const Render = memo<MarkdownElementProps>(({ children, id }) => {
50
+ const { t } = useTranslation('chat');
51
+ const { styles, cx } = useStyles();
52
+
53
+ const [isGenerating] = useChatStore((s) => {
54
+ const message = chatSelectors.getMessageById(id)(s);
55
+ return [!isLobeThinkingClosed(message?.content)];
56
+ });
57
+
58
+ const [showDetail, setShowDetail] = useState(false);
59
+
60
+ const expand = showDetail || isGenerating;
61
+ return (
62
+ <Flexbox
63
+ className={styles.container}
64
+ gap={16}
65
+ onClick={() => {
66
+ setShowDetail(!showDetail);
67
+ }}
68
+ width={'100%'}
69
+ >
70
+ <Flexbox distribution={'space-between'} flex={1} horizontal>
71
+ <Flexbox gap={8} horizontal>
72
+ <Icon icon={isGenerating ? Loader2Icon : BringToFrontIcon} spin={isGenerating} />
73
+ {isGenerating ? (
74
+ <span className={cx(dotLoading)}>{t('artifact.thinking')}</span>
75
+ ) : (
76
+ t('artifact.thought')
77
+ )}
78
+ </Flexbox>
79
+ <Icon icon={expand ? ChevronDown : ChevronRight} />
80
+ </Flexbox>
81
+ {expand && children}
82
+ </Flexbox>
83
+ );
84
+ });
85
+
86
+ export default Render;
@@ -0,0 +1,12 @@
1
+ import { ARTIFACT_THINKING_TAG } from '@/const/plugin';
2
+
3
+ import Component from './Render';
4
+ import rehypePlugin from './rehypePlugin';
5
+
6
+ const AntThinkingElement = {
7
+ Component,
8
+ rehypePlugin,
9
+ tag: ARTIFACT_THINKING_TAG,
10
+ };
11
+
12
+ export default AntThinkingElement;