@lobehub/chat 1.69.5 → 1.70.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/self-hosting/advanced/auth/clerk.zh-CN.mdx +1 -1
  4. package/docs/self-hosting/server-database/vercel.zh-CN.mdx +1 -1
  5. package/locales/ar/chat.json +7 -1
  6. package/locales/ar/components.json +2 -0
  7. package/locales/ar/models.json +3 -0
  8. package/locales/bg-BG/chat.json +7 -1
  9. package/locales/bg-BG/components.json +2 -0
  10. package/locales/bg-BG/models.json +3 -0
  11. package/locales/de-DE/chat.json +7 -1
  12. package/locales/de-DE/components.json +2 -0
  13. package/locales/de-DE/models.json +3 -0
  14. package/locales/en-US/chat.json +7 -1
  15. package/locales/en-US/components.json +2 -0
  16. package/locales/en-US/models.json +3 -0
  17. package/locales/es-ES/chat.json +7 -1
  18. package/locales/es-ES/components.json +2 -0
  19. package/locales/es-ES/models.json +3 -0
  20. package/locales/fa-IR/chat.json +7 -1
  21. package/locales/fa-IR/components.json +2 -0
  22. package/locales/fa-IR/models.json +3 -0
  23. package/locales/fr-FR/chat.json +7 -1
  24. package/locales/fr-FR/components.json +2 -0
  25. package/locales/fr-FR/models.json +3 -0
  26. package/locales/it-IT/chat.json +7 -1
  27. package/locales/it-IT/components.json +2 -0
  28. package/locales/it-IT/models.json +3 -0
  29. package/locales/ja-JP/chat.json +7 -1
  30. package/locales/ja-JP/components.json +2 -0
  31. package/locales/ja-JP/models.json +3 -0
  32. package/locales/ko-KR/chat.json +7 -1
  33. package/locales/ko-KR/components.json +2 -0
  34. package/locales/ko-KR/models.json +3 -0
  35. package/locales/nl-NL/chat.json +7 -1
  36. package/locales/nl-NL/components.json +2 -0
  37. package/locales/nl-NL/models.json +3 -0
  38. package/locales/pl-PL/chat.json +7 -1
  39. package/locales/pl-PL/components.json +2 -0
  40. package/locales/pl-PL/models.json +3 -0
  41. package/locales/pt-BR/chat.json +7 -1
  42. package/locales/pt-BR/components.json +2 -0
  43. package/locales/pt-BR/models.json +3 -0
  44. package/locales/ru-RU/chat.json +7 -1
  45. package/locales/ru-RU/components.json +2 -0
  46. package/locales/ru-RU/models.json +3 -0
  47. package/locales/tr-TR/chat.json +7 -1
  48. package/locales/tr-TR/components.json +2 -0
  49. package/locales/tr-TR/models.json +3 -0
  50. package/locales/vi-VN/chat.json +7 -1
  51. package/locales/vi-VN/components.json +2 -0
  52. package/locales/vi-VN/models.json +3 -0
  53. package/locales/zh-CN/chat.json +7 -1
  54. package/locales/zh-CN/components.json +3 -1
  55. package/locales/zh-CN/models.json +3 -0
  56. package/locales/zh-TW/chat.json +7 -1
  57. package/locales/zh-TW/components.json +2 -0
  58. package/locales/zh-TW/models.json +3 -0
  59. package/package.json +3 -3
  60. package/packages/web-crawler/package.json +1 -1
  61. package/packages/web-crawler/src/crawImpl/__tests__/browserless.test.ts +1 -1
  62. package/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
  63. package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
  64. package/packages/web-crawler/src/crawler.ts +11 -1
  65. package/packages/web-crawler/src/utils/__snapshots__/htmlToMarkdown.test.ts.snap +2 -382
  66. package/packages/web-crawler/src/utils/htmlToMarkdown.ts +32 -2
  67. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +4 -1
  68. package/src/app/(backend)/webapi/chat/[provider]/route.ts +5 -1
  69. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx +5 -1
  70. package/src/{features/Conversation/Messages/Assistant/Tool/Inspector/Loader.tsx → components/CircleLoader/index.tsx} +4 -3
  71. package/src/config/modelProviders/openai.ts +3 -0
  72. package/src/config/tools.ts +2 -0
  73. package/src/const/settings/agent.ts +6 -0
  74. package/src/database/client/db.ts +3 -3
  75. package/src/features/ChatInput/ActionBar/Search/FCSearchModel.tsx +56 -0
  76. package/src/features/ChatInput/ActionBar/Search/FunctionCallingModelSelect/index.tsx +85 -0
  77. package/src/features/ChatInput/ActionBar/Search/SwitchPanel.tsx +9 -23
  78. package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +15 -23
  79. package/src/features/Conversation/Extras/Usage/UsageDetail/pricing.ts +26 -0
  80. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +4 -4
  81. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +15 -10
  82. package/src/features/Conversation/Messages/Assistant/IntentUnderstanding.tsx +25 -0
  83. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/ToolTitle.tsx +1 -2
  84. package/src/features/Conversation/Messages/Assistant/index.tsx +18 -9
  85. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/index.tsx +51 -52
  86. package/src/features/ModelSwitchPanel/index.tsx +34 -8
  87. package/src/hooks/useAgentEnableSearch.ts +1 -9
  88. package/src/libs/agent-runtime/anthropic/index.ts +5 -2
  89. package/src/locales/default/chat.ts +7 -3
  90. package/src/locales/default/components.ts +3 -1
  91. package/src/server/routers/tools/search.ts +8 -1
  92. package/src/services/__tests__/chat.test.ts +124 -0
  93. package/src/services/chat.ts +82 -49
  94. package/src/services/session/type.ts +1 -1
  95. package/src/store/agent/slices/chat/selectors/__snapshots__/agent.test.ts.snap +4 -0
  96. package/src/store/agent/slices/chat/selectors/chatConfig.ts +5 -1
  97. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +12 -4
  98. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +113 -13
  99. package/src/store/chat/slices/aiChat/initialState.ts +2 -0
  100. package/src/store/chat/slices/aiChat/selectors.ts +8 -1
  101. package/src/store/chat/slices/message/action.ts +9 -1
  102. package/src/store/chat/slices/plugin/action.ts +6 -4
  103. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +4 -0
  104. package/src/types/agent/chatConfig.ts +6 -0
  105. package/src/types/openai/chat.ts +0 -1
  106. package/src/utils/fetch/__tests__/fetchSSE.test.ts +3 -2
  107. package/src/utils/fetch/fetchSSE.ts +1 -1
@@ -0,0 +1,85 @@
1
+ import { Select, SelectProps } from 'antd';
2
+ import { createStyles } from 'antd-style';
3
+ import { memo, useMemo } from 'react';
4
+
5
+ import { ModelItemRender, ProviderItemRender } from '@/components/ModelSelect';
6
+ import { useEnabledChatModels } from '@/hooks/useEnabledChatModels';
7
+ import { WorkingModel } from '@/types/agent';
8
+ import { EnabledProviderWithModels } from '@/types/aiProvider';
9
+
10
+ const useStyles = createStyles(({ css, prefixCls }) => ({
11
+ select: css`
12
+ &.${prefixCls}-select-dropdown .${prefixCls}-select-item-option-grouped {
13
+ padding-inline-start: 12px;
14
+ }
15
+ `,
16
+ }));
17
+
18
+ interface ModelOption {
19
+ label: any;
20
+ provider: string;
21
+ value: string;
22
+ }
23
+
24
+ interface ModelSelectProps {
25
+ onChange?: (props: WorkingModel) => void;
26
+ showAbility?: boolean;
27
+ value?: WorkingModel;
28
+ }
29
+
30
+ const ModelSelect = memo<ModelSelectProps>(({ value, onChange }) => {
31
+ const enabledList = useEnabledChatModels();
32
+
33
+ const { styles } = useStyles();
34
+
35
+ const options = useMemo<SelectProps['options']>(() => {
36
+ const getChatModels = (provider: EnabledProviderWithModels) =>
37
+ provider.children
38
+ .filter((model) => !!model.abilities.functionCall)
39
+ .map((model) => ({
40
+ label: <ModelItemRender {...model} {...model.abilities} showInfoTag={false} />,
41
+ provider: provider.id,
42
+ value: `${provider.id}/${model.id}`,
43
+ }));
44
+
45
+ if (enabledList.length === 1) {
46
+ const provider = enabledList[0];
47
+
48
+ return getChatModels(provider);
49
+ }
50
+
51
+ return enabledList
52
+ .filter((p) => !!getChatModels(p).length)
53
+ .map((provider) => {
54
+ const options = getChatModels(provider);
55
+
56
+ return {
57
+ label: (
58
+ <ProviderItemRender
59
+ logo={provider.logo}
60
+ name={provider.name}
61
+ provider={provider.id}
62
+ source={provider.source}
63
+ />
64
+ ),
65
+ options,
66
+ };
67
+ });
68
+ }, [enabledList]);
69
+
70
+ return (
71
+ <Select
72
+ onChange={(value, option) => {
73
+ const model = value.split('/').slice(1).join('/');
74
+ onChange?.({ model, provider: (option as unknown as ModelOption).provider });
75
+ }}
76
+ options={options}
77
+ popupClassName={styles.select}
78
+ popupMatchSelectWidth={false}
79
+ value={`${value?.provider}/${value?.model}`}
80
+ variant={'filled'}
81
+ />
82
+ );
83
+ });
84
+
85
+ export default ModelSelect;
@@ -12,6 +12,7 @@ import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/slices/c
12
12
  import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
13
13
  import { SearchMode } from '@/types/search';
14
14
 
15
+ import FCSearchModel from './FCSearchModel';
15
16
  import ModelBuiltinSearch from './ModelBuiltinSearch';
16
17
 
17
18
  const { Text } = Typography;
@@ -38,10 +39,6 @@ const useStyles = createStyles(({ css, token }) => ({
38
39
  font-size: 12px;
39
40
  color: ${token.colorTextSecondary};
40
41
  `,
41
- disable: css`
42
- cursor: not-allowed;
43
- opacity: 0.45;
44
- `,
45
42
  iconWrapper: css`
46
43
  display: flex;
47
44
  flex-shrink: 0;
@@ -80,8 +77,7 @@ const useStyles = createStyles(({ css, token }) => ({
80
77
  `,
81
78
  }));
82
79
 
83
- const Item = memo<NetworkOption>(({ value, description, icon, label, disable }) => {
84
- const { t } = useTranslation('chat');
80
+ const Item = memo<NetworkOption>(({ value, description, icon, label }) => {
85
81
  const { styles } = useStyles();
86
82
  const [mode, updateAgentChatConfig] = useAgentStore((s) => [
87
83
  agentChatConfigSelectors.agentSearchMode(s),
@@ -96,12 +92,12 @@ const Item = memo<NetworkOption>(({ value, description, icon, label, disable })
96
92
  key={value}
97
93
  onClick={() => updateAgentChatConfig({ searchMode: value })}
98
94
  >
99
- <Flexbox className={disable ? styles.disable : ''} gap={8} horizontal>
95
+ <Flexbox gap={8} horizontal>
100
96
  <div className={styles.iconWrapper}>{icon}</div>
101
97
  <div className={styles.content}>
102
98
  <div className={styles.title}>{label}</div>
103
99
  <Text className={styles.description} type="secondary">
104
- {disable ? t('search.mode.disable') : description}
100
+ {description}
105
101
  </Text>
106
102
  </div>
107
103
  </Flexbox>
@@ -132,34 +128,24 @@ const AINetworkSettings = memo<AINetworkSettingsProps>(() => {
132
128
  label: t('search.mode.off.title'),
133
129
  value: 'off',
134
130
  },
135
- // 等应用层联网功能做好以后再开启
136
- // {
137
- // description: t('search.mode.on.desc'),
138
- // icon: <WifiOutlined />,
139
- // label: t('search.mode.on.title'),
140
- // value: 'on',
141
- // },
142
131
  {
143
132
  description: t('search.mode.auto.desc'),
144
- disable: !supportFC,
145
133
  icon: <Icon icon={SparklesIcon} />,
146
134
  label: t('search.mode.auto.title'),
147
135
  value: 'auto',
148
136
  },
149
137
  ];
150
138
 
139
+ const showDivider = isModelHasBuiltinSearchConfig || !supportFC;
140
+
151
141
  return (
152
142
  <Flexbox gap={8}>
153
143
  {options.map((option) => (
154
144
  <Item {...option} key={option.value} />
155
145
  ))}
156
-
157
- {isModelHasBuiltinSearchConfig && (
158
- <>
159
- <Divider style={{ margin: 0, paddingInline: 12 }} />
160
- <ModelBuiltinSearch />
161
- </>
162
- )}
146
+ {showDivider && <Divider style={{ margin: 0, paddingInline: 12 }} />}
147
+ {isModelHasBuiltinSearchConfig && <ModelBuiltinSearch />}
148
+ {!supportFC && <FCSearchModel />}
163
149
  </Flexbox>
164
150
  );
165
151
  });
@@ -7,11 +7,10 @@ import { memo } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
10
+ import { getPrice } from '@/features/Conversation/Extras/Usage/UsageDetail/pricing';
10
11
  import { useGlobalStore } from '@/store/global';
11
12
  import { systemStatusSelectors } from '@/store/global/selectors';
12
13
  import { LobeDefaultAiModelListItem } from '@/types/aiModel';
13
- import { ModelPriceCurrency } from '@/types/llm';
14
- import { formatPriceByCurrency } from '@/utils/format';
15
14
 
16
15
  export const useStyles = createStyles(({ css, token }) => {
17
16
  return {
@@ -40,19 +39,8 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
40
39
  const isShowCredit = useGlobalStore(systemStatusSelectors.isShowCredit) && !!pricing;
41
40
  const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
42
41
 
43
- const inputPrice = formatPriceByCurrency(pricing?.input, pricing?.currency as ModelPriceCurrency);
44
- const cachedInputPrice = formatPriceByCurrency(
45
- pricing?.cachedInput,
46
- pricing?.currency as ModelPriceCurrency,
47
- );
48
- const writeCacheInputPrice = formatPriceByCurrency(
49
- pricing?.writeCacheInput,
50
- pricing?.currency as ModelPriceCurrency,
51
- );
52
- const outputPrice = formatPriceByCurrency(
53
- pricing?.output,
54
- pricing?.currency as ModelPriceCurrency,
55
- );
42
+ const formatPrice = getPrice(pricing || {});
43
+
56
44
  return (
57
45
  <Flexbox gap={8}>
58
46
  <Flexbox
@@ -103,37 +91,41 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
103
91
  {pricing?.cachedInput && (
104
92
  <Tooltip
105
93
  title={t('messages.modelCard.pricing.inputCachedTokens', {
106
- amount: cachedInputPrice,
94
+ amount: formatPrice.cachedInput,
107
95
  })}
108
96
  >
109
97
  <Flexbox gap={2} horizontal>
110
98
  <Icon icon={CircleFadingArrowUp} />
111
- {cachedInputPrice}
99
+ {formatPrice.cachedInput}
112
100
  </Flexbox>
113
101
  </Tooltip>
114
102
  )}
115
103
  {pricing?.writeCacheInput && (
116
104
  <Tooltip
117
105
  title={t('messages.modelCard.pricing.writeCacheInputTokens', {
118
- amount: writeCacheInputPrice,
106
+ amount: formatPrice.writeCacheInput,
119
107
  })}
120
108
  >
121
109
  <Flexbox gap={2} horizontal>
122
110
  <Icon icon={BookUp2Icon} />
123
- {writeCacheInputPrice}
111
+ {formatPrice.writeCacheInput}
124
112
  </Flexbox>
125
113
  </Tooltip>
126
114
  )}
127
- <Tooltip title={t('messages.modelCard.pricing.inputTokens', { amount: inputPrice })}>
115
+ <Tooltip
116
+ title={t('messages.modelCard.pricing.inputTokens', { amount: formatPrice.input })}
117
+ >
128
118
  <Flexbox gap={2} horizontal>
129
119
  <Icon icon={ArrowUpFromDot} />
130
- {inputPrice}
120
+ {formatPrice.input}
131
121
  </Flexbox>
132
122
  </Tooltip>
133
- <Tooltip title={t('messages.modelCard.pricing.outputTokens', { amount: outputPrice })}>
123
+ <Tooltip
124
+ title={t('messages.modelCard.pricing.outputTokens', { amount: formatPrice.output })}
125
+ >
134
126
  <Flexbox gap={2} horizontal>
135
127
  <Icon icon={ArrowDownToDot} />
136
- {outputPrice}
128
+ {formatPrice.output}
137
129
  </Flexbox>
138
130
  </Tooltip>
139
131
  </Flexbox>
@@ -0,0 +1,26 @@
1
+ import { ChatModelPricing } from '@/types/aiModel';
2
+ import { ModelPriceCurrency } from '@/types/llm';
3
+ import { formatPriceByCurrency } from '@/utils/format';
4
+
5
+ export const getPrice = (pricing: ChatModelPricing) => {
6
+ const inputPrice = formatPriceByCurrency(pricing?.input, pricing?.currency as ModelPriceCurrency);
7
+ const cachedInputPrice = formatPriceByCurrency(
8
+ pricing?.cachedInput,
9
+ pricing?.currency as ModelPriceCurrency,
10
+ );
11
+ const writeCacheInputPrice = formatPriceByCurrency(
12
+ pricing?.writeCacheInput,
13
+ pricing?.currency as ModelPriceCurrency,
14
+ );
15
+ const outputPrice = formatPriceByCurrency(
16
+ pricing?.output,
17
+ pricing?.currency as ModelPriceCurrency,
18
+ );
19
+
20
+ return {
21
+ cachedInput: Number(cachedInputPrice),
22
+ input: Number(inputPrice),
23
+ output: Number(outputPrice),
24
+ writeCacheInput: Number(writeCacheInputPrice),
25
+ };
26
+ };
@@ -70,7 +70,7 @@ describe('getDetailsToken', () => {
70
70
  const result = getDetailsToken(usage, mockModelCard);
71
71
 
72
72
  expect(result.inputCached).toEqual({
73
- credit: 0, // 50 * 0.005 = 0.25, rounded to 0
73
+ credit: 1,
74
74
  token: 50,
75
75
  });
76
76
 
@@ -165,12 +165,12 @@ describe('getDetailsToken', () => {
165
165
  const result = getDetailsToken(usage, mockModelCard);
166
166
 
167
167
  // uncachedInput: (200 - 50) * 0.01 = 1.5 -> 2
168
- // cachedInput: 50 * 0.005 = 0.25 -> 0
168
+ // cachedInput: 50 * 0.005 = 0.25 -> 1
169
169
  // totalOutput: 300 * 0.02 = 6
170
- // totalCredit = 2 + 0 + 6 = 8
170
+ // totalCredit = 2 + 1 + 6 = 9
171
171
 
172
172
  expect(result.totalTokens).toEqual({
173
- credit: 8,
173
+ credit: 9,
174
174
  token: 500,
175
175
  });
176
176
  });
@@ -1,6 +1,8 @@
1
- import { LobeDefaultAiModelListItem } from '@/types/aiModel';
1
+ import { ChatModelPricing, LobeDefaultAiModelListItem } from '@/types/aiModel';
2
2
  import { ModelTokensUsage } from '@/types/message';
3
3
 
4
+ import { getPrice } from './pricing';
5
+
4
6
  const calcCredit = (token: number, pricing?: number) => {
5
7
  if (!pricing) return '-';
6
8
 
@@ -29,23 +31,26 @@ export const getDetailsToken = (
29
31
  ? usage?.inputCacheMissTokens
30
32
  : totalInputTokens - (inputCacheTokens || 0);
31
33
 
34
+ // Pricing
35
+ const formatPrice = getPrice(modelCard?.pricing as ChatModelPricing);
36
+
32
37
  const inputCacheMissCredit = (
33
- !!inputCacheMissTokens ? calcCredit(inputCacheMissTokens, modelCard?.pricing?.input) : 0
38
+ !!inputCacheMissTokens ? calcCredit(inputCacheMissTokens, formatPrice.input) : 0
34
39
  ) as number;
35
40
 
36
41
  const inputCachedCredit = (
37
- !!inputCacheTokens ? calcCredit(inputCacheTokens, modelCard?.pricing?.cachedInput) : 0
42
+ !!inputCacheTokens ? calcCredit(inputCacheTokens, formatPrice.cachedInput) : 0
38
43
  ) as number;
39
44
 
40
45
  const inputWriteCachedCredit = !!inputWriteCacheTokens
41
- ? (calcCredit(inputWriteCacheTokens, modelCard?.pricing?.writeCacheInput) as number)
46
+ ? (calcCredit(inputWriteCacheTokens, formatPrice.writeCacheInput) as number)
42
47
  : 0;
43
48
 
44
49
  const totalOutputCredit = (
45
- !!totalOutputTokens ? calcCredit(totalOutputTokens, modelCard?.pricing?.output) : 0
50
+ !!totalOutputTokens ? calcCredit(totalOutputTokens, formatPrice.output) : 0
46
51
  ) as number;
47
52
  const totalInputCredit = (
48
- !!totalInputTokens ? calcCredit(totalInputTokens, modelCard?.pricing?.output) : 0
53
+ !!totalInputTokens ? calcCredit(totalInputTokens, formatPrice.output) : 0
49
54
  ) as number;
50
55
 
51
56
  const totalCredit =
@@ -69,13 +74,13 @@ export const getDetailsToken = (
69
74
  : undefined,
70
75
  inputCitation: !!usage.inputCitationTokens
71
76
  ? {
72
- credit: calcCredit(usage.inputCitationTokens, modelCard?.pricing?.input),
77
+ credit: calcCredit(usage.inputCitationTokens, formatPrice.input),
73
78
  token: usage.inputCitationTokens,
74
79
  }
75
80
  : undefined,
76
81
  inputText: !!inputTextTokens
77
82
  ? {
78
- credit: calcCredit(inputTextTokens, modelCard?.pricing?.input),
83
+ credit: calcCredit(inputTextTokens, formatPrice.input),
79
84
  token: inputTextTokens,
80
85
  }
81
86
  : undefined,
@@ -89,13 +94,13 @@ export const getDetailsToken = (
89
94
  : undefined,
90
95
  outputReasoning: !!outputReasoningTokens
91
96
  ? {
92
- credit: calcCredit(outputReasoningTokens, modelCard?.pricing?.output),
97
+ credit: calcCredit(outputReasoningTokens, formatPrice.output),
93
98
  token: outputReasoningTokens,
94
99
  }
95
100
  : undefined,
96
101
  outputText: !!outputTextTokens
97
102
  ? {
98
- credit: calcCredit(outputTextTokens, modelCard?.pricing?.output),
103
+ credit: calcCredit(outputTextTokens, formatPrice.output),
99
104
  token: outputTextTokens,
100
105
  }
101
106
  : undefined,
@@ -0,0 +1,25 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import CircleLoader from '@/components/CircleLoader';
6
+ import { shinyTextStylish } from '@/styles/loading';
7
+
8
+ const useStyles = createStyles(({ token }) => ({
9
+ shinyText: shinyTextStylish(token),
10
+ }));
11
+
12
+ const IntentUnderstanding = () => {
13
+ const { styles } = useStyles();
14
+ const { t } = useTranslation('chat');
15
+
16
+ return (
17
+ <Flexbox align={'center'} gap={8} horizontal>
18
+ <CircleLoader />
19
+ <Flexbox className={styles.shinyText} horizontal>
20
+ {t('intentUnderstanding.title')}
21
+ </Flexbox>
22
+ </Flexbox>
23
+ );
24
+ };
25
+ export default IntentUnderstanding;
@@ -6,6 +6,7 @@ import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
+ import Loader from '@/components/CircleLoader';
9
10
  import PluginAvatar from '@/features/PluginAvatar';
10
11
  import { useChatStore } from '@/store/chat';
11
12
  import { chatSelectors } from '@/store/chat/selectors';
@@ -14,8 +15,6 @@ import { toolSelectors } from '@/store/tool/selectors';
14
15
  import { shinyTextStylish } from '@/styles/loading';
15
16
  import { WebBrowsingManifest } from '@/tools/web-browsing';
16
17
 
17
- import Loader from './Loader';
18
-
19
18
  export const useStyles = createStyles(({ css, token }) => ({
20
19
  apiName: css`
21
20
  overflow: hidden;
@@ -8,6 +8,7 @@ import { ChatMessage } from '@/types/message';
8
8
 
9
9
  import { DefaultMessage } from '../Default';
10
10
  import FileChunks from './FileChunks';
11
+ import IntentUnderstanding from './IntentUnderstanding';
11
12
  import Reasoning from './Reasoning';
12
13
  import SearchGrounding from './SearchGrounding';
13
14
  import Tool from './Tool';
@@ -24,6 +25,8 @@ export const AssistantMessage = memo<
24
25
 
25
26
  const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
26
27
 
28
+ const isIntentUnderstanding = useChatStore(aiChatSelectors.isIntentUnderstanding(id));
29
+
27
30
  const showSearch = !!search && !!search.citations?.length;
28
31
 
29
32
  // remove \n to avoid empty content
@@ -32,6 +35,8 @@ export const AssistantMessage = memo<
32
35
  (!!props.reasoning && props.reasoning.content?.trim() !== '') ||
33
36
  (!props.reasoning && isReasoning);
34
37
 
38
+ const showFileChunks = !!chunksList && chunksList.length > 0;
39
+
35
40
  return editing ? (
36
41
  <DefaultMessage
37
42
  content={content}
@@ -44,16 +49,20 @@ export const AssistantMessage = memo<
44
49
  {showSearch && (
45
50
  <SearchGrounding citations={search?.citations} searchQueries={search?.searchQueries} />
46
51
  )}
47
- {!!chunksList && chunksList.length > 0 && <FileChunks data={chunksList} />}
52
+ {showFileChunks && <FileChunks data={chunksList} />}
48
53
  {showReasoning && <Reasoning {...props.reasoning} id={id} />}
49
- {content && (
50
- <DefaultMessage
51
- addIdOnDOM={false}
52
- content={content}
53
- id={id}
54
- isToolCallGenerating={isToolCallGenerating}
55
- {...props}
56
- />
54
+ {isIntentUnderstanding ? (
55
+ <IntentUnderstanding />
56
+ ) : (
57
+ content && (
58
+ <DefaultMessage
59
+ addIdOnDOM={false}
60
+ content={content}
61
+ id={id}
62
+ isToolCallGenerating={isToolCallGenerating}
63
+ {...props}
64
+ />
65
+ )
57
66
  )}
58
67
  {tools && (
59
68
  <Flexbox gap={8}>
@@ -64,16 +64,14 @@ const Render = memo<ArtifactProps>(({ identifier, title, type, language, childre
64
64
 
65
65
  const inThread = useContext(InPortalThreadContext);
66
66
  const { message } = App.useApp();
67
- const [isGenerating, isArtifactTagClosed, currentArtifactMessageId, openArtifact, closeArtifact] =
68
- useChatStore((s) => {
69
- return [
70
- chatSelectors.isMessageGenerating(id)(s),
71
- chatPortalSelectors.isArtifactTagClosed(id)(s),
72
- chatPortalSelectors.artifactMessageId(s),
73
- s.openArtifact,
74
- s.closeArtifact,
75
- ];
76
- });
67
+ const [isGenerating, isArtifactTagClosed, openArtifact, closeArtifact] = useChatStore((s) => {
68
+ return [
69
+ chatSelectors.isMessageGenerating(id)(s),
70
+ chatPortalSelectors.isArtifactTagClosed(id)(s),
71
+ s.openArtifact,
72
+ s.closeArtifact,
73
+ ];
74
+ });
77
75
 
78
76
  const openArtifactUI = () => {
79
77
  openArtifact({ id, identifier, language, title, type });
@@ -86,52 +84,53 @@ const Render = memo<ArtifactProps>(({ identifier, title, type, language, childre
86
84
  }, [isGenerating, hasChildren, str, identifier, title, type, id, language]);
87
85
 
88
86
  return (
89
- <p>
90
- <Flexbox
91
- className={styles.container}
92
- gap={16}
93
- onClick={() => {
94
- if (currentArtifactMessageId === id) {
95
- closeArtifact();
96
- } else {
97
- if (inThread) {
98
- message.info(t('artifact.inThread'));
99
- return;
100
- }
101
- openArtifactUI();
87
+ <Flexbox
88
+ className={styles.container}
89
+ gap={16}
90
+ onClick={() => {
91
+ const currentArtifactMessageId = chatPortalSelectors.artifactMessageId(
92
+ useChatStore.getState(),
93
+ );
94
+ if (currentArtifactMessageId === id) {
95
+ closeArtifact();
96
+ } else {
97
+ if (inThread) {
98
+ message.info(t('artifact.inThread'));
99
+ return;
102
100
  }
103
- }}
104
- width={'100%'}
105
- >
106
- <Flexbox align={'center'} flex={1} horizontal>
107
- <Center className={styles.avatar} height={64} horizontal width={64}>
108
- <ArtifactIcon type={type} />
109
- </Center>
110
- <Flexbox gap={4} paddingBlock={8} paddingInline={12}>
111
- {!title && isGenerating ? (
112
- <Flexbox className={cx(dotLoading)} horizontal>
113
- {t('artifact.generating')}
101
+ openArtifactUI();
102
+ }
103
+ }}
104
+ width={'100%'}
105
+ >
106
+ <Flexbox align={'center'} flex={1} horizontal>
107
+ <Center className={styles.avatar} height={64} horizontal width={64}>
108
+ <ArtifactIcon type={type} />
109
+ </Center>
110
+ <Flexbox gap={4} paddingBlock={8} paddingInline={12}>
111
+ {!title && isGenerating ? (
112
+ <Flexbox className={cx(dotLoading)} horizontal>
113
+ {t('artifact.generating')}
114
+ </Flexbox>
115
+ ) : (
116
+ <Flexbox className={cx(styles.title)}>{title || t('artifact.unknownTitle')}</Flexbox>
117
+ )}
118
+ {hasChildren && (
119
+ <Flexbox className={styles.desc} horizontal>
120
+ {identifier} ·{' '}
121
+ <Flexbox gap={2} horizontal>
122
+ {!isArtifactTagClosed && (
123
+ <div>
124
+ <Icon icon={Loader2} spin />
125
+ </div>
126
+ )}
127
+ {str?.length}
114
128
  </Flexbox>
115
- ) : (
116
- <Flexbox className={cx(styles.title)}>{title || t('artifact.unknownTitle')}</Flexbox>
117
- )}
118
- {hasChildren && (
119
- <Flexbox className={styles.desc} horizontal>
120
- {identifier} ·{' '}
121
- <Flexbox gap={2} horizontal>
122
- {!isArtifactTagClosed && (
123
- <div>
124
- <Icon icon={Loader2} spin />
125
- </div>
126
- )}
127
- {str?.length}
128
- </Flexbox>
129
- </Flexbox>
130
- )}
131
- </Flexbox>
129
+ </Flexbox>
130
+ )}
132
131
  </Flexbox>
133
132
  </Flexbox>
134
- </p>
133
+ </Flexbox>
135
134
  );
136
135
  });
137
136
 
@@ -1,8 +1,9 @@
1
- import { Icon } from '@lobehub/ui';
1
+ import { ActionIcon, Icon } from '@lobehub/ui';
2
2
  import { Dropdown } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
4
  import type { ItemType } from 'antd/es/menu/interface';
5
- import { LucideArrowRight } from 'lucide-react';
5
+ import { LucideArrowRight, LucideBolt } from 'lucide-react';
6
+ import Link from 'next/link';
6
7
  import { useRouter } from 'next/navigation';
7
8
  import { PropsWithChildren, memo, useMemo } from 'react';
8
9
  import { useTranslation } from 'react-i18next';
@@ -86,17 +87,42 @@ const ModelSwitchPanel = memo<PropsWithChildren>(({ children }) => {
86
87
  return items;
87
88
  };
88
89
 
90
+ if (enabledList.length === 0)
91
+ return [
92
+ {
93
+ key: `no-provider`,
94
+ label: (
95
+ <Flexbox gap={8} horizontal style={{ color: theme.colorTextTertiary }}>
96
+ {t('ModelSwitchPanel.emptyProvider')}
97
+ <Icon icon={LucideArrowRight} />
98
+ </Flexbox>
99
+ ),
100
+ onClick: () => {
101
+ router.push(isDeprecatedEdition ? '/settings/llm' : `/settings/provider`);
102
+ },
103
+ },
104
+ ];
105
+
89
106
  // otherwise show with provider group
90
107
  return enabledList.map((provider) => ({
91
108
  children: getModelItems(provider),
92
109
  key: provider.id,
93
110
  label: (
94
- <ProviderItemRender
95
- logo={provider.logo}
96
- name={provider.name}
97
- provider={provider.id}
98
- source={provider.source}
99
- />
111
+ <Flexbox horizontal justify="space-between">
112
+ <ProviderItemRender
113
+ logo={provider.logo}
114
+ name={provider.name}
115
+ provider={provider.id}
116
+ source={provider.source}
117
+ />
118
+ <Link href={isDeprecatedEdition ? '/settings/llm' : `/settings/provider/${provider.id}`}>
119
+ <ActionIcon
120
+ icon={LucideBolt}
121
+ size={'small'}
122
+ title={t('ModelSwitchPanel.goToSettings')}
123
+ />
124
+ </Link>
125
+ </Flexbox>
100
126
  ),
101
127
  type: 'group',
102
128
  }));