@lobehub/lobehub 2.0.0-next.338 → 2.0.0-next.339

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 (257) hide show
  1. package/.gitattributes +35 -0
  2. package/CHANGELOG.md +44 -0
  3. package/changelog/v1.json +15 -0
  4. package/locales/ar/plugin.json +12 -2
  5. package/locales/ar/providers.json +1 -0
  6. package/locales/ar/setting.json +77 -1
  7. package/locales/bg-BG/models.json +5 -10
  8. package/locales/bg-BG/plugin.json +12 -2
  9. package/locales/bg-BG/providers.json +1 -0
  10. package/locales/bg-BG/setting.json +78 -2
  11. package/locales/de-DE/models.json +51 -9
  12. package/locales/de-DE/plugin.json +12 -2
  13. package/locales/de-DE/providers.json +1 -0
  14. package/locales/de-DE/setting.json +78 -2
  15. package/locales/en-US/models.json +11 -10
  16. package/locales/en-US/plugin.json +14 -4
  17. package/locales/en-US/providers.json +1 -0
  18. package/locales/en-US/setting.json +78 -2
  19. package/locales/es-ES/plugin.json +12 -2
  20. package/locales/es-ES/providers.json +1 -0
  21. package/locales/es-ES/setting.json +78 -2
  22. package/locales/fa-IR/plugin.json +12 -2
  23. package/locales/fa-IR/providers.json +1 -0
  24. package/locales/fa-IR/setting.json +78 -2
  25. package/locales/fr-FR/plugin.json +12 -2
  26. package/locales/fr-FR/providers.json +1 -0
  27. package/locales/fr-FR/setting.json +78 -2
  28. package/locales/it-IT/plugin.json +12 -2
  29. package/locales/it-IT/providers.json +1 -0
  30. package/locales/it-IT/setting.json +78 -2
  31. package/locales/ja-JP/plugin.json +12 -2
  32. package/locales/ja-JP/providers.json +1 -0
  33. package/locales/ja-JP/setting.json +78 -2
  34. package/locales/ko-KR/plugin.json +12 -2
  35. package/locales/ko-KR/providers.json +1 -0
  36. package/locales/ko-KR/setting.json +78 -2
  37. package/locales/nl-NL/models.json +4 -9
  38. package/locales/nl-NL/plugin.json +12 -2
  39. package/locales/nl-NL/providers.json +1 -0
  40. package/locales/nl-NL/setting.json +78 -2
  41. package/locales/pl-PL/plugin.json +12 -2
  42. package/locales/pl-PL/providers.json +1 -0
  43. package/locales/pl-PL/setting.json +78 -2
  44. package/locales/pt-BR/plugin.json +12 -2
  45. package/locales/pt-BR/providers.json +1 -0
  46. package/locales/pt-BR/setting.json +78 -2
  47. package/locales/ru-RU/plugin.json +12 -2
  48. package/locales/ru-RU/providers.json +1 -0
  49. package/locales/ru-RU/setting.json +78 -2
  50. package/locales/tr-TR/plugin.json +12 -2
  51. package/locales/tr-TR/providers.json +1 -0
  52. package/locales/tr-TR/setting.json +78 -2
  53. package/locales/vi-VN/plugin.json +12 -2
  54. package/locales/vi-VN/providers.json +1 -0
  55. package/locales/vi-VN/setting.json +77 -1
  56. package/locales/zh-CN/plugin.json +12 -2
  57. package/locales/zh-CN/providers.json +1 -0
  58. package/locales/zh-CN/setting.json +78 -2
  59. package/locales/zh-TW/plugin.json +12 -2
  60. package/locales/zh-TW/providers.json +1 -0
  61. package/locales/zh-TW/setting.json +78 -2
  62. package/package.json +1 -1
  63. package/packages/agent-runtime/src/groupOrchestration/GroupOrchestrationSupervisor.ts +2 -0
  64. package/packages/agent-runtime/src/groupOrchestration/__tests__/GroupOrchestrationSupervisor.test.ts +3 -1
  65. package/packages/agent-runtime/src/groupOrchestration/types.ts +5 -0
  66. package/packages/const/src/index.ts +1 -0
  67. package/packages/const/src/klavis.ts +144 -0
  68. package/packages/const/src/lobehubSkill.ts +34 -0
  69. package/packages/const/src/recommendedSkill.ts +17 -0
  70. package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +38 -0
  71. package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +20 -1
  72. package/packages/model-runtime/src/core/contextBuilders/google.test.ts +42 -0
  73. package/packages/model-runtime/src/core/contextBuilders/google.ts +17 -0
  74. package/scripts/electronWorkflow/modifiers/dynamicToStatic.mts +273 -0
  75. package/scripts/electronWorkflow/modifiers/index.mts +10 -0
  76. package/scripts/electronWorkflow/modifiers/nextConfig.mts +1 -0
  77. package/scripts/electronWorkflow/modifiers/nextDynamicToStatic.mts +233 -0
  78. package/scripts/electronWorkflow/modifiers/removeSuspense.mts +124 -0
  79. package/scripts/electronWorkflow/modifiers/routes.mts +14 -2
  80. package/scripts/electronWorkflow/modifiers/settingsContentToStatic.mts +148 -0
  81. package/scripts/electronWorkflow/modifiers/wrapChildrenWithClientOnly.mts +73 -0
  82. package/src/app/[variants]/(main)/home/features/InputArea/SkillInstallBanner.tsx +131 -0
  83. package/src/app/[variants]/(main)/home/features/InputArea/index.tsx +34 -27
  84. package/src/app/[variants]/(main)/settings/features/SettingHeader.tsx +8 -4
  85. package/src/app/[variants]/(main)/settings/features/SettingsContent.tsx +3 -0
  86. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +6 -0
  87. package/src/{features/PluginStore/InstalledList/List/Item/Action.tsx → app/[variants]/(main)/settings/skill/features/Actions.tsx} +45 -40
  88. package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +353 -0
  89. package/src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx +344 -0
  90. package/src/app/[variants]/(main)/settings/skill/features/McpSkillItem.tsx +116 -0
  91. package/src/app/[variants]/(main)/settings/skill/features/SkillList.tsx +244 -0
  92. package/src/app/[variants]/(main)/settings/skill/index.tsx +35 -0
  93. package/src/components/Plugins/PluginTag.tsx +23 -35
  94. package/src/components/client/ClientOnly.tsx +6 -2
  95. package/src/features/AgentSetting/AgentPlugin/index.tsx +2 -2
  96. package/src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx +8 -32
  97. package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +8 -30
  98. package/src/features/ChatInput/ActionBar/Tools/PopoverContent.tsx +48 -59
  99. package/src/features/ChatInput/ActionBar/Tools/index.tsx +5 -23
  100. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +158 -56
  101. package/src/features/IntegrationDetailModal/index.tsx +293 -0
  102. package/src/features/{PluginStore/McpList/Detail → MCP/MCPDetail}/index.tsx +15 -6
  103. package/src/features/MCP/MCPSettings/McpSettingsModal.tsx +58 -0
  104. package/src/features/{PluginStore/McpList/Detail/Settings → MCP/MCPSettings}/index.tsx +39 -27
  105. package/src/features/PluginDetailModal/index.tsx +2 -2
  106. package/src/features/PluginDevModal/index.tsx +16 -40
  107. package/src/features/ProfileEditor/AgentTool.tsx +2 -2
  108. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/index.tsx +1 -1
  109. package/src/features/{PluginStore/AddPluginButton.tsx → SkillStore/AddSkillButton.tsx} +3 -3
  110. package/src/features/SkillStore/CommunityList/Item.tsx +158 -0
  111. package/src/features/SkillStore/CommunityList/index.tsx +101 -0
  112. package/src/features/SkillStore/Content.tsx +59 -0
  113. package/src/features/{PluginStore/PluginEmpty.tsx → SkillStore/Empty.tsx} +8 -8
  114. package/src/features/SkillStore/LobeHubList/Item.tsx +118 -0
  115. package/src/features/SkillStore/LobeHubList/index.tsx +187 -0
  116. package/src/features/SkillStore/LobeHubList/useSkillConnect.ts +239 -0
  117. package/src/features/SkillStore/Search/index.tsx +43 -0
  118. package/src/features/{PluginStore → SkillStore}/index.tsx +14 -10
  119. package/src/features/SkillStore/style.ts +27 -0
  120. package/src/locales/default/plugin.ts +15 -4
  121. package/src/locales/default/setting.ts +185 -2
  122. package/src/services/chat/mecha/agentConfigResolver.test.ts +197 -0
  123. package/src/services/chat/mecha/agentConfigResolver.ts +44 -17
  124. package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +40 -37
  125. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +78 -0
  126. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +50 -16
  127. package/src/store/global/initialState.ts +1 -0
  128. package/src/store/tool/slices/lobehubSkillStore/action.test.ts +914 -0
  129. package/src/store/tool/slices/lobehubSkillStore/selectors.test.ts +548 -0
  130. package/.cursor/skills/vercel-react-best-practices/AGENTS.md +0 -2410
  131. package/.cursor/skills/vercel-react-best-practices/SKILL.md +0 -125
  132. package/.cursor/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  133. package/.cursor/skills/vercel-react-best-practices/rules/advanced-use-latest.md +0 -49
  134. package/.cursor/skills/vercel-react-best-practices/rules/async-api-routes.md +0 -38
  135. package/.cursor/skills/vercel-react-best-practices/rules/async-defer-await.md +0 -80
  136. package/.cursor/skills/vercel-react-best-practices/rules/async-dependencies.md +0 -36
  137. package/.cursor/skills/vercel-react-best-practices/rules/async-parallel.md +0 -28
  138. package/.cursor/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -99
  139. package/.cursor/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -59
  140. package/.cursor/skills/vercel-react-best-practices/rules/bundle-conditional.md +0 -31
  141. package/.cursor/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -49
  142. package/.cursor/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  143. package/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md +0 -50
  144. package/.cursor/skills/vercel-react-best-practices/rules/client-event-listeners.md +0 -74
  145. package/.cursor/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -71
  146. package/.cursor/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -48
  147. package/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md +0 -56
  148. package/.cursor/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -57
  149. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-function-results.md +0 -80
  150. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-property-access.md +0 -28
  151. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-storage.md +0 -70
  152. package/.cursor/skills/vercel-react-best-practices/rules/js-combine-iterations.md +0 -32
  153. package/.cursor/skills/vercel-react-best-practices/rules/js-early-exit.md +0 -50
  154. package/.cursor/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -45
  155. package/.cursor/skills/vercel-react-best-practices/rules/js-index-maps.md +0 -37
  156. package/.cursor/skills/vercel-react-best-practices/rules/js-length-check-first.md +0 -49
  157. package/.cursor/skills/vercel-react-best-practices/rules/js-min-max-loop.md +0 -82
  158. package/.cursor/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -24
  159. package/.cursor/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -57
  160. package/.cursor/skills/vercel-react-best-practices/rules/rendering-activity.md +0 -26
  161. package/.cursor/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  162. package/.cursor/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -40
  163. package/.cursor/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -38
  164. package/.cursor/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  165. package/.cursor/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  166. package/.cursor/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -28
  167. package/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -39
  168. package/.cursor/skills/vercel-react-best-practices/rules/rerender-dependencies.md +0 -45
  169. package/.cursor/skills/vercel-react-best-practices/rules/rerender-derived-state.md +0 -29
  170. package/.cursor/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -74
  171. package/.cursor/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  172. package/.cursor/skills/vercel-react-best-practices/rules/rerender-memo.md +0 -44
  173. package/.cursor/skills/vercel-react-best-practices/rules/rerender-transitions.md +0 -40
  174. package/.cursor/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -73
  175. package/.cursor/skills/vercel-react-best-practices/rules/server-cache-lru.md +0 -41
  176. package/.cursor/skills/vercel-react-best-practices/rules/server-cache-react.md +0 -76
  177. package/.cursor/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -83
  178. package/.cursor/skills/vercel-react-best-practices/rules/server-serialization.md +0 -38
  179. package/src/features/PluginStore/Content.tsx +0 -54
  180. package/src/features/PluginStore/InstalledList/Detail/CustomPluginEmptyState.tsx +0 -79
  181. package/src/features/PluginStore/InstalledList/Detail/index.tsx +0 -21
  182. package/src/features/PluginStore/InstalledList/List/Item/index.tsx +0 -61
  183. package/src/features/PluginStore/InstalledList/List/index.tsx +0 -72
  184. package/src/features/PluginStore/InstalledList/index.tsx +0 -90
  185. package/src/features/PluginStore/McpList/List/Action.tsx +0 -119
  186. package/src/features/PluginStore/McpList/List/Item.tsx +0 -83
  187. package/src/features/PluginStore/McpList/List/index.tsx +0 -93
  188. package/src/features/PluginStore/McpList/index.tsx +0 -58
  189. package/src/features/PluginStore/PluginList/Detail/DetailProvider.tsx +0 -19
  190. package/src/features/PluginStore/PluginList/Detail/EmptyState.tsx +0 -56
  191. package/src/features/PluginStore/PluginList/Detail/Header.tsx +0 -130
  192. package/src/features/PluginStore/PluginList/Detail/InstallDetail/Nav.tsx +0 -73
  193. package/src/features/PluginStore/PluginList/Detail/InstallDetail/Settings.tsx +0 -19
  194. package/src/features/PluginStore/PluginList/Detail/InstallDetail/Tools.tsx +0 -111
  195. package/src/features/PluginStore/PluginList/Detail/InstallDetail/index.tsx +0 -24
  196. package/src/features/PluginStore/PluginList/Detail/Loading.tsx +0 -42
  197. package/src/features/PluginStore/PluginList/Detail/TagList.tsx +0 -35
  198. package/src/features/PluginStore/PluginList/Detail/index.tsx +0 -39
  199. package/src/features/PluginStore/PluginList/Detail/useCategory.tsx +0 -76
  200. package/src/features/PluginStore/PluginList/List/Action.tsx +0 -78
  201. package/src/features/PluginStore/PluginList/List/Item.tsx +0 -92
  202. package/src/features/PluginStore/PluginList/List/index.tsx +0 -94
  203. package/src/features/PluginStore/PluginList/index.tsx +0 -46
  204. package/src/features/PluginStore/Search/index.tsx +0 -40
  205. /package/{.codex/skills → .agents}/vercel-react-best-practices/AGENTS.md +0 -0
  206. /package/{.codex/skills → .agents}/vercel-react-best-practices/SKILL.md +0 -0
  207. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -0
  208. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/advanced-use-latest.md +0 -0
  209. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-api-routes.md +0 -0
  210. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-defer-await.md +0 -0
  211. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-dependencies.md +0 -0
  212. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-parallel.md +0 -0
  213. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -0
  214. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -0
  215. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-conditional.md +0 -0
  216. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -0
  217. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -0
  218. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-preload.md +0 -0
  219. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/client-event-listeners.md +0 -0
  220. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -0
  221. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -0
  222. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/client-swr-dedup.md +0 -0
  223. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -0
  224. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-cache-function-results.md +0 -0
  225. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-cache-property-access.md +0 -0
  226. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-cache-storage.md +0 -0
  227. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-combine-iterations.md +0 -0
  228. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-early-exit.md +0 -0
  229. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -0
  230. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-index-maps.md +0 -0
  231. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-length-check-first.md +0 -0
  232. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-min-max-loop.md +0 -0
  233. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -0
  234. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -0
  235. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-activity.md +0 -0
  236. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -0
  237. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -0
  238. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -0
  239. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -0
  240. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -0
  241. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -0
  242. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -0
  243. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-dependencies.md +0 -0
  244. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-derived-state.md +0 -0
  245. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -0
  246. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -0
  247. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-memo.md +0 -0
  248. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-transitions.md +0 -0
  249. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -0
  250. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-cache-lru.md +0 -0
  251. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-cache-react.md +0 -0
  252. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -0
  253. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-serialization.md +0 -0
  254. /package/src/{features/PluginStore/InstalledList → app/[variants]/(main)/settings/skill/features}/EditCustomPlugin.tsx +0 -0
  255. /package/src/features/{PluginStore/McpList/Detail → MCP/MCPDetail}/Loading.tsx +0 -0
  256. /package/src/features/{PluginStore → SkillStore}/Loading.tsx +0 -0
  257. /package/src/features/{PluginStore → SkillStore}/VirtuosoLoading.tsx +0 -0
@@ -0,0 +1,118 @@
1
+ 'use client';
2
+
3
+ import { ActionIcon, Block, DropdownMenu, Flexbox, Icon, Image } from '@lobehub/ui';
4
+ import { App } from 'antd';
5
+ import { cssVar } from 'antd-style';
6
+ import type { Klavis } from 'klavis';
7
+ import { Loader2, MoreVerticalIcon, Plus, Unplug } from 'lucide-react';
8
+ import React, { memo } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+
11
+ import { useItemStyles } from '../style';
12
+ import { useSkillConnect } from './useSkillConnect';
13
+
14
+ interface ItemProps {
15
+ description?: string;
16
+ icon: string | React.ComponentType;
17
+ identifier: string;
18
+ isConnected: boolean;
19
+ label: string;
20
+ onOpenDetail?: () => void;
21
+ serverName?: Klavis.McpServerName;
22
+ type: 'klavis' | 'lobehub';
23
+ }
24
+
25
+ const Item = memo<ItemProps>(
26
+ ({ description, icon, identifier, label, onOpenDetail, serverName, type }) => {
27
+ const { t } = useTranslation('setting');
28
+ const { styles } = useItemStyles();
29
+ const { modal } = App.useApp();
30
+
31
+ const { handleConnect, handleDisconnect, isConnected, isConnecting } = useSkillConnect({
32
+ identifier,
33
+ serverName,
34
+ type,
35
+ });
36
+
37
+ // Get localized description
38
+ const i18nPrefix = type === 'klavis' ? 'tools.klavis.servers' : 'tools.lobehubSkill.providers';
39
+ // @ts-ignore
40
+ const localizedDescription = t(`${i18nPrefix}.${identifier}.description`, {
41
+ defaultValue: description,
42
+ });
43
+
44
+ const confirmDisconnect = () => {
45
+ modal.confirm({
46
+ cancelText: t('cancel', { ns: 'common' }),
47
+ centered: true,
48
+ content: t('tools.lobehubSkill.disconnectConfirm.desc', { name: label }),
49
+ okButtonProps: { danger: true },
50
+ okText: t('tools.lobehubSkill.disconnect'),
51
+ onOk: handleDisconnect,
52
+ title: t('tools.lobehubSkill.disconnectConfirm.title', { name: label }),
53
+ });
54
+ };
55
+
56
+ const renderIcon = () => {
57
+ if (typeof icon === 'string') {
58
+ return <Image alt={label} height={40} src={icon} width={40} />;
59
+ }
60
+ return <Icon fill={cssVar.colorText} icon={icon as any} size={40} />;
61
+ };
62
+
63
+ const renderAction = () => {
64
+ if (isConnecting) {
65
+ return <ActionIcon icon={Loader2} loading />;
66
+ }
67
+
68
+ if (isConnected) {
69
+ return (
70
+ <DropdownMenu
71
+ items={[
72
+ {
73
+ icon: <Icon icon={Unplug} />,
74
+ key: 'disconnect',
75
+ label: t('tools.lobehubSkill.disconnect'),
76
+ onClick: confirmDisconnect,
77
+ },
78
+ ]}
79
+ placement="bottomRight"
80
+ >
81
+ <ActionIcon icon={MoreVerticalIcon} />
82
+ </DropdownMenu>
83
+ );
84
+ }
85
+
86
+ return (
87
+ <ActionIcon icon={Plus} onClick={handleConnect} title={t('tools.lobehubSkill.connect')} />
88
+ );
89
+ };
90
+
91
+ return (
92
+ <Block
93
+ align={'center'}
94
+ className={styles.container}
95
+ gap={12}
96
+ horizontal
97
+ onClick={onOpenDetail}
98
+ paddingBlock={12}
99
+ paddingInline={12}
100
+ style={{ cursor: 'pointer' }}
101
+ variant={'filled'}
102
+ >
103
+ {renderIcon()}
104
+ <Flexbox flex={1} gap={4} style={{ minWidth: 0, overflow: 'hidden' }}>
105
+ <span className={styles.title}>{label}</span>
106
+ {localizedDescription && (
107
+ <span className={styles.description}>{localizedDescription}</span>
108
+ )}
109
+ </Flexbox>
110
+ <div onClick={(e) => e.stopPropagation()}>{renderAction()}</div>
111
+ </Block>
112
+ );
113
+ },
114
+ );
115
+
116
+ Item.displayName = 'LobeHubListItem';
117
+
118
+ export default Item;
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+
3
+ import { KLAVIS_SERVER_TYPES, LOBEHUB_SKILL_PROVIDERS } from '@lobechat/const';
4
+ import { createStyles } from 'antd-style';
5
+ import isEqual from 'fast-deep-equal';
6
+ import type { Klavis } from 'klavis';
7
+ import { memo, useMemo, useState } from 'react';
8
+
9
+ import IntegrationDetailModal from '@/features/IntegrationDetailModal';
10
+ import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
11
+ import { useToolStore } from '@/store/tool';
12
+ import { klavisStoreSelectors, lobehubSkillStoreSelectors } from '@/store/tool/selectors';
13
+ import { KlavisServerStatus } from '@/store/tool/slices/klavisStore';
14
+ import { LobehubSkillStatus } from '@/store/tool/slices/lobehubSkillStore/types';
15
+
16
+ import Empty from '../Empty';
17
+ import Item from './Item';
18
+ import { useSkillConnect } from './useSkillConnect';
19
+
20
+ const useStyles = createStyles(({ css }) => ({
21
+ grid: css`
22
+ display: grid;
23
+ grid-template-columns: repeat(2, 1fr);
24
+ gap: 12px;
25
+
26
+ padding-block-end: 16px;
27
+ padding-inline: 16px;
28
+
29
+ @media (max-width: 768px) {
30
+ grid-template-columns: 1fr;
31
+ }
32
+ `,
33
+ }));
34
+
35
+ interface LobeHubListProps {
36
+ keywords: string;
37
+ }
38
+
39
+ interface DetailState {
40
+ identifier: string;
41
+ serverName?: Klavis.McpServerName;
42
+ type: 'klavis' | 'lobehub';
43
+ }
44
+
45
+ interface DetailModalWithConnectProps {
46
+ detailState: DetailState;
47
+ onClose: () => void;
48
+ }
49
+
50
+ const DetailModalWithConnect = memo<DetailModalWithConnectProps>(({ detailState, onClose }) => {
51
+ const { handleConnect, isConnecting } = useSkillConnect({
52
+ identifier: detailState.identifier,
53
+ serverName: detailState.serverName,
54
+ type: detailState.type,
55
+ });
56
+
57
+ return (
58
+ <IntegrationDetailModal
59
+ identifier={detailState.identifier}
60
+ isConnecting={isConnecting}
61
+ onClose={onClose}
62
+ onConnect={handleConnect}
63
+ open
64
+ type={detailState.type}
65
+ />
66
+ );
67
+ });
68
+
69
+ DetailModalWithConnect.displayName = 'DetailModalWithConnect';
70
+
71
+ export const LobeHubList = memo<LobeHubListProps>(({ keywords }) => {
72
+ const { styles } = useStyles();
73
+ const [detailState, setDetailState] = useState<DetailState | null>(null);
74
+
75
+ const isLobehubSkillEnabled = useServerConfigStore(serverConfigSelectors.enableLobehubSkill);
76
+ const isKlavisEnabled = useServerConfigStore(serverConfigSelectors.enableKlavis);
77
+ const allLobehubSkillServers = useToolStore(lobehubSkillStoreSelectors.getServers, isEqual);
78
+ const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
79
+
80
+ const [useFetchLobehubSkillConnections, useFetchUserKlavisServers] = useToolStore((s) => [
81
+ s.useFetchLobehubSkillConnections,
82
+ s.useFetchUserKlavisServers,
83
+ ]);
84
+
85
+ useFetchLobehubSkillConnections(isLobehubSkillEnabled);
86
+ useFetchUserKlavisServers(isKlavisEnabled);
87
+
88
+ const getLobehubSkillServerByProvider = (providerId: string) => {
89
+ return allLobehubSkillServers.find((server) => server.identifier === providerId);
90
+ };
91
+
92
+ const getKlavisServerByIdentifier = (identifier: string) => {
93
+ return allKlavisServers.find((server) => server.identifier === identifier);
94
+ };
95
+
96
+ const filteredItems = useMemo(() => {
97
+ const items: Array<
98
+ | { provider: (typeof LOBEHUB_SKILL_PROVIDERS)[number]; type: 'lobehub' }
99
+ | { serverType: (typeof KLAVIS_SERVER_TYPES)[number]; type: 'klavis' }
100
+ > = [];
101
+
102
+ // Add LobeHub skills
103
+ if (isLobehubSkillEnabled) {
104
+ for (const provider of LOBEHUB_SKILL_PROVIDERS) {
105
+ items.push({ provider, type: 'lobehub' });
106
+ }
107
+ }
108
+
109
+ // Add Klavis skills
110
+ if (isKlavisEnabled) {
111
+ for (const serverType of KLAVIS_SERVER_TYPES) {
112
+ items.push({ serverType, type: 'klavis' });
113
+ }
114
+ }
115
+
116
+ // Filter by keywords
117
+ const lowerKeywords = keywords.toLowerCase().trim();
118
+ if (!lowerKeywords) return items;
119
+
120
+ return items.filter((item) => {
121
+ const label = item.type === 'lobehub' ? item.provider.label : item.serverType.label;
122
+ return label.toLowerCase().includes(lowerKeywords);
123
+ });
124
+ }, [keywords, isLobehubSkillEnabled, isKlavisEnabled]);
125
+
126
+ const hasSearchKeywords = Boolean(keywords && keywords.trim());
127
+
128
+ if (filteredItems.length === 0) return <Empty search={hasSearchKeywords} />;
129
+
130
+ return (
131
+ <>
132
+ <div className={styles.grid}>
133
+ {filteredItems.map((item) => {
134
+ if (item.type === 'lobehub') {
135
+ const server = getLobehubSkillServerByProvider(item.provider.id);
136
+ const isConnected = server?.status === LobehubSkillStatus.CONNECTED;
137
+ return (
138
+ <Item
139
+ description={item.provider.description}
140
+ icon={item.provider.icon}
141
+ identifier={item.provider.id}
142
+ isConnected={isConnected}
143
+ key={item.provider.id}
144
+ label={item.provider.label}
145
+ onOpenDetail={() =>
146
+ setDetailState({ identifier: item.provider.id, type: 'lobehub' })
147
+ }
148
+ type="lobehub"
149
+ />
150
+ );
151
+ }
152
+ const server = getKlavisServerByIdentifier(item.serverType.identifier);
153
+ const isConnected = server?.status === KlavisServerStatus.CONNECTED;
154
+ return (
155
+ <Item
156
+ description={item.serverType.description}
157
+ icon={item.serverType.icon}
158
+ identifier={item.serverType.identifier}
159
+ isConnected={isConnected}
160
+ key={item.serverType.identifier}
161
+ label={item.serverType.label}
162
+ onOpenDetail={() =>
163
+ setDetailState({
164
+ identifier: item.serverType.identifier,
165
+ serverName: item.serverType.serverName,
166
+ type: 'klavis',
167
+ })
168
+ }
169
+ serverName={item.serverType.serverName}
170
+ type="klavis"
171
+ />
172
+ );
173
+ })}
174
+ </div>
175
+ {detailState && (
176
+ <DetailModalWithConnect
177
+ detailState={detailState}
178
+ onClose={() => setDetailState(null)}
179
+ />
180
+ )}
181
+ </>
182
+ );
183
+ });
184
+
185
+ LobeHubList.displayName = 'LobeHubList';
186
+
187
+ export default LobeHubList;
@@ -0,0 +1,239 @@
1
+ 'use client';
2
+
3
+ import { getLobehubSkillProviderById } from '@lobechat/const';
4
+ import type { Klavis } from 'klavis';
5
+ import { useCallback, useEffect, useRef, useState } from 'react';
6
+
7
+ import { useToolStore } from '@/store/tool';
8
+ import { klavisStoreSelectors, lobehubSkillStoreSelectors } from '@/store/tool/selectors';
9
+ import { KlavisServerStatus } from '@/store/tool/slices/klavisStore';
10
+ import { LobehubSkillStatus } from '@/store/tool/slices/lobehubSkillStore/types';
11
+ import { useUserStore } from '@/store/user';
12
+ import { userProfileSelectors } from '@/store/user/selectors';
13
+
14
+ const POLL_INTERVAL_MS = 1000;
15
+ const POLL_TIMEOUT_MS = 15_000;
16
+
17
+ interface UseSkillConnectOptions {
18
+ identifier: string;
19
+ serverName?: Klavis.McpServerName;
20
+ type: 'klavis' | 'lobehub';
21
+ }
22
+
23
+ export const useSkillConnect = ({ identifier, serverName, type }: UseSkillConnectOptions) => {
24
+ const [isConnecting, setIsConnecting] = useState(false);
25
+ const [isWaitingAuth, setIsWaitingAuth] = useState(false);
26
+
27
+ const oauthWindowRef = useRef<Window | null>(null);
28
+ const windowCheckIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
29
+ const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
30
+ const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
31
+
32
+ // LobeHub skill hooks
33
+ const checkLobehubStatus = useToolStore((s) => s.checkLobehubSkillStatus);
34
+ const revokeLobehubConnect = useToolStore((s) => s.revokeLobehubSkill);
35
+ const getAuthorizeUrl = useToolStore((s) => s.getLobehubSkillAuthorizeUrl);
36
+ const lobehubServer = useToolStore(lobehubSkillStoreSelectors.getServerByIdentifier(identifier));
37
+
38
+ // Klavis hooks
39
+ const userId = useUserStore(userProfileSelectors.userId);
40
+ const createKlavisServer = useToolStore((s) => s.createKlavisServer);
41
+ const refreshKlavisServerTools = useToolStore((s) => s.refreshKlavisServerTools);
42
+ const removeKlavisServer = useToolStore((s) => s.removeKlavisServer);
43
+ const klavisServer = useToolStore(klavisStoreSelectors.getServerByIdentifier(identifier));
44
+
45
+ const cleanup = useCallback(() => {
46
+ if (windowCheckIntervalRef.current) {
47
+ clearInterval(windowCheckIntervalRef.current);
48
+ windowCheckIntervalRef.current = null;
49
+ }
50
+ if (pollIntervalRef.current) {
51
+ clearInterval(pollIntervalRef.current);
52
+ pollIntervalRef.current = null;
53
+ }
54
+ if (pollTimeoutRef.current) {
55
+ clearTimeout(pollTimeoutRef.current);
56
+ pollTimeoutRef.current = null;
57
+ }
58
+ oauthWindowRef.current = null;
59
+ setIsWaitingAuth(false);
60
+ }, []);
61
+
62
+ useEffect(() => {
63
+ return () => {
64
+ cleanup();
65
+ };
66
+ }, [cleanup]);
67
+
68
+ useEffect(() => {
69
+ const connected =
70
+ type === 'lobehub'
71
+ ? lobehubServer?.status === LobehubSkillStatus.CONNECTED
72
+ : klavisServer?.status === KlavisServerStatus.CONNECTED;
73
+
74
+ if (connected && isWaitingAuth) {
75
+ cleanup();
76
+ }
77
+ }, [type, lobehubServer?.status, klavisServer?.status, isWaitingAuth, cleanup]);
78
+
79
+ // Listen for OAuth success message from popup window (for LobeHub skills)
80
+ useEffect(() => {
81
+ if (type !== 'lobehub') return;
82
+
83
+ const handleMessage = async (event: MessageEvent) => {
84
+ if (event.origin !== window.location.origin) return;
85
+
86
+ if (
87
+ event.data?.type === 'LOBEHUB_SKILL_AUTH_SUCCESS' &&
88
+ event.data?.provider === identifier
89
+ ) {
90
+ cleanup();
91
+ await checkLobehubStatus(identifier);
92
+ }
93
+ };
94
+
95
+ window.addEventListener('message', handleMessage);
96
+ return () => window.removeEventListener('message', handleMessage);
97
+ }, [type, identifier, cleanup, checkLobehubStatus]);
98
+
99
+ const startFallbackPolling = useCallback(
100
+ (serverIdOrName: string) => {
101
+ if (pollIntervalRef.current) return;
102
+
103
+ pollIntervalRef.current = setInterval(async () => {
104
+ try {
105
+ if (type === 'lobehub') {
106
+ await checkLobehubStatus(serverIdOrName);
107
+ } else {
108
+ await refreshKlavisServerTools(serverIdOrName);
109
+ }
110
+ } catch (error) {
111
+ console.error('[SkillStore] Failed to check status:', error);
112
+ }
113
+ }, POLL_INTERVAL_MS);
114
+
115
+ pollTimeoutRef.current = setTimeout(() => {
116
+ if (pollIntervalRef.current) {
117
+ clearInterval(pollIntervalRef.current);
118
+ pollIntervalRef.current = null;
119
+ }
120
+ setIsWaitingAuth(false);
121
+ }, POLL_TIMEOUT_MS);
122
+ },
123
+ [type, checkLobehubStatus, refreshKlavisServerTools],
124
+ );
125
+
126
+ const startWindowMonitor = useCallback(
127
+ (oauthWindow: Window, serverIdOrName: string) => {
128
+ windowCheckIntervalRef.current = setInterval(async () => {
129
+ try {
130
+ if (oauthWindow.closed) {
131
+ if (windowCheckIntervalRef.current) {
132
+ clearInterval(windowCheckIntervalRef.current);
133
+ windowCheckIntervalRef.current = null;
134
+ }
135
+ oauthWindowRef.current = null;
136
+ // Check status and then reset waiting state
137
+ if (type === 'lobehub') {
138
+ await checkLobehubStatus(serverIdOrName);
139
+ } else {
140
+ await refreshKlavisServerTools(serverIdOrName);
141
+ }
142
+ setIsWaitingAuth(false);
143
+ }
144
+ } catch {
145
+ if (windowCheckIntervalRef.current) {
146
+ clearInterval(windowCheckIntervalRef.current);
147
+ windowCheckIntervalRef.current = null;
148
+ }
149
+ startFallbackPolling(serverIdOrName);
150
+ }
151
+ }, 500);
152
+ },
153
+ [type, checkLobehubStatus, refreshKlavisServerTools, startFallbackPolling],
154
+ );
155
+
156
+ const openOAuthWindow = useCallback(
157
+ (oauthUrl: string, serverIdOrName: string) => {
158
+ cleanup();
159
+ setIsWaitingAuth(true);
160
+
161
+ const oauthWindow = window.open(oauthUrl, '_blank', 'width=600,height=700');
162
+ if (oauthWindow) {
163
+ oauthWindowRef.current = oauthWindow;
164
+ startWindowMonitor(oauthWindow, serverIdOrName);
165
+ } else {
166
+ startFallbackPolling(serverIdOrName);
167
+ }
168
+ },
169
+ [cleanup, startWindowMonitor, startFallbackPolling],
170
+ );
171
+
172
+ // Handle connect for LobeHub
173
+ const handleLobehubConnect = useCallback(async () => {
174
+ if (lobehubServer?.isConnected) return;
175
+
176
+ setIsConnecting(true);
177
+ try {
178
+ const provider = getLobehubSkillProviderById(identifier);
179
+ if (!provider) return;
180
+
181
+ const redirectUri = `${window.location.origin}/oauth/callback/success?provider=${encodeURIComponent(identifier)}`;
182
+ const { authorizeUrl } = await getAuthorizeUrl(identifier, { redirectUri });
183
+ openOAuthWindow(authorizeUrl, identifier);
184
+ } catch (error) {
185
+ console.error('[SkillStore] Failed to get authorize URL:', error);
186
+ } finally {
187
+ setIsConnecting(false);
188
+ }
189
+ }, [identifier, lobehubServer?.isConnected, getAuthorizeUrl, openOAuthWindow]);
190
+
191
+ // Handle connect for Klavis
192
+ const handleKlavisConnect = useCallback(async () => {
193
+ if (!userId || !serverName) return;
194
+ if (klavisServer) return;
195
+
196
+ setIsConnecting(true);
197
+ try {
198
+ const newServer = await createKlavisServer({
199
+ identifier,
200
+ serverName,
201
+ userId,
202
+ });
203
+
204
+ if (newServer) {
205
+ if (newServer.isAuthenticated) {
206
+ await refreshKlavisServerTools(newServer.identifier);
207
+ } else if (newServer.oauthUrl) {
208
+ openOAuthWindow(newServer.oauthUrl, newServer.identifier);
209
+ }
210
+ }
211
+ } catch (error) {
212
+ console.error('[SkillStore] Failed to connect server:', error);
213
+ } finally {
214
+ setIsConnecting(false);
215
+ }
216
+ }, [userId, serverName, klavisServer, identifier, createKlavisServer, refreshKlavisServerTools, openOAuthWindow]);
217
+
218
+ const handleConnect = type === 'lobehub' ? handleLobehubConnect : handleKlavisConnect;
219
+
220
+ const handleDisconnect = useCallback(async () => {
221
+ if (type === 'lobehub' && lobehubServer) {
222
+ await revokeLobehubConnect(lobehubServer.identifier);
223
+ } else if (type === 'klavis' && klavisServer) {
224
+ await removeKlavisServer(klavisServer.identifier);
225
+ }
226
+ }, [type, lobehubServer, klavisServer, revokeLobehubConnect, removeKlavisServer]);
227
+
228
+ const isConnected =
229
+ type === 'lobehub'
230
+ ? lobehubServer?.status === LobehubSkillStatus.CONNECTED
231
+ : klavisServer?.status === KlavisServerStatus.CONNECTED;
232
+
233
+ return {
234
+ handleConnect,
235
+ handleDisconnect,
236
+ isConnected,
237
+ isConnecting: isConnecting || isWaitingAuth,
238
+ };
239
+ };
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import { Flexbox, SearchBar } from '@lobehub/ui';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { useToolStore } from '@/store/tool';
8
+
9
+ import { SkillStoreTab } from '../Content';
10
+
11
+ interface SearchProps {
12
+ activeTab: SkillStoreTab;
13
+ onLobeHubSearch: (keywords: string) => void;
14
+ }
15
+
16
+ export const Search = memo<SearchProps>(({ activeTab, onLobeHubSearch }) => {
17
+ const { t } = useTranslation('setting');
18
+ const mcpKeywords = useToolStore((s) => s.mcpSearchKeywords);
19
+
20
+ const keywords = activeTab === SkillStoreTab.Community ? mcpKeywords : '';
21
+
22
+ return (
23
+ <Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
24
+ <Flexbox flex={1}>
25
+ <SearchBar
26
+ allowClear
27
+ defaultValue={keywords}
28
+ onSearch={(keywords: string) => {
29
+ if (activeTab === SkillStoreTab.Community) {
30
+ useToolStore.setState({ mcpSearchKeywords: keywords, searchLoading: true });
31
+ } else {
32
+ onLobeHubSearch(keywords);
33
+ }
34
+ }}
35
+ placeholder={t('skillStore.search')}
36
+ variant={'borderless'}
37
+ />
38
+ </Flexbox>
39
+ </Flexbox>
40
+ );
41
+ });
42
+
43
+ export default Search;
@@ -1,33 +1,37 @@
1
+ 'use client';
2
+
1
3
  import { Modal } from '@lobehub/ui';
2
4
  import { memo } from 'react';
3
5
  import { useTranslation } from 'react-i18next';
4
6
 
5
7
  import Content from './Content';
6
8
 
7
- interface PluginStoreProps {
8
- open?: boolean;
9
+ interface SkillStoreProps {
10
+ open: boolean;
9
11
  setOpen: (open: boolean) => void;
10
12
  }
11
- export const PluginStore = memo<PluginStoreProps>(({ setOpen, open }) => {
12
- const { t } = useTranslation('plugin');
13
+
14
+ export const SkillStore = memo<SkillStoreProps>(({ open, setOpen }) => {
15
+ const { t } = useTranslation('setting');
13
16
 
14
17
  return (
15
18
  <Modal
16
19
  allowFullscreen
20
+ destroyOnClose={false}
17
21
  footer={null}
18
- onCancel={() => {
19
- setOpen(false);
20
- }}
22
+ onCancel={() => setOpen(false)}
21
23
  open={open}
22
24
  styles={{
23
25
  body: { overflow: 'hidden', padding: 0 },
24
26
  }}
25
- title={t('store.title')}
26
- width={'min(90%, 1280px)'}
27
+ title={t('skillStore.title')}
28
+ width={'min(80%, 800px)'}
27
29
  >
28
30
  <Content />
29
31
  </Modal>
30
32
  );
31
33
  });
32
34
 
33
- export default PluginStore;
35
+ SkillStore.displayName = 'SkillStore';
36
+
37
+ export default SkillStore;
@@ -0,0 +1,27 @@
1
+ import { createStyles } from 'antd-style';
2
+
3
+ export const useItemStyles = createStyles(({ css, token }) => ({
4
+ container: css`
5
+ position: relative;
6
+ overflow: hidden;
7
+ flex: 1;
8
+ min-width: 0;
9
+ `,
10
+ description: css`
11
+ overflow: hidden;
12
+
13
+ font-size: 12px;
14
+ color: ${token.colorTextSecondary};
15
+ text-overflow: ellipsis;
16
+ white-space: nowrap;
17
+ `,
18
+ title: css`
19
+ overflow: hidden;
20
+
21
+ font-size: 14px;
22
+ font-weight: 500;
23
+ color: ${token.colorText};
24
+ text-overflow: ellipsis;
25
+ white-space: nowrap;
26
+ `,
27
+ }));