@lobehub/chat 1.107.5 → 1.107.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.107.6](https://github.com/lobehub/lobe-chat/compare/v1.107.5...v1.107.6)
6
+
7
+ <sup>Released on **2025-08-05**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Break line for Gemini Artifacts.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Break line for Gemini Artifacts, closes [#8627](https://github.com/lobehub/lobe-chat/issues/8627) ([65609dd](https://github.com/lobehub/lobe-chat/commit/65609dd))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 1.107.5](https://github.com/lobehub/lobe-chat/compare/v1.107.4...v1.107.5)
6
31
 
7
32
  <sup>Released on **2025-08-04**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Break line for Gemini Artifacts."
6
+ ]
7
+ },
8
+ "date": "2025-08-05",
9
+ "version": "1.107.6"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.107.5",
3
+ "version": "1.107.6",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -3,7 +3,7 @@ import { Tabs } from 'antd';
3
3
  import { memo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
- import PluginResult from './PluginResultJSON';
6
+ import PluginResult from './PluginResult';
7
7
  import PluginState from './PluginState';
8
8
 
9
9
  interface DebugProps {
@@ -12,17 +12,22 @@ export interface FunctionMessageProps {
12
12
  const PluginResult = memo<FunctionMessageProps>(({ toolCallId, variant }) => {
13
13
  const toolMessage = useChatStore(chatSelectors.getMessageByToolCallId(toolCallId));
14
14
 
15
- const data = useMemo(() => {
15
+ const { data, language } = useMemo(() => {
16
16
  try {
17
- return JSON.stringify(JSON.parse(toolMessage?.content || ''), null, 2);
17
+ const parsed = JSON.parse(toolMessage?.content || '');
18
+ // Special case: if the parsed result is a string, it means the original content was a stringified string
19
+ if (typeof parsed === 'string') {
20
+ return { data: parsed, language: 'plaintext' }; // Return the parsed string directly, do not re-serialize
21
+ }
22
+ return { data: JSON.stringify(parsed, null, 2), language: 'json' };
18
23
  } catch {
19
- return toolMessage?.content || '';
24
+ return { data: toolMessage?.content || '', language: 'plaintext' };
20
25
  }
21
26
  }, [toolMessage?.content]);
22
27
 
23
28
  return (
24
29
  <Highlighter
25
- language={'json'}
30
+ language={language}
26
31
  style={{ maxHeight: 200, overflow: 'scroll', width: '100%' }}
27
32
  variant={variant}
28
33
  >
@@ -6,7 +6,7 @@ import { memo, useCallback, useEffect, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
- import PluginResult from '@/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResultJSON';
9
+ import PluginResult from '@/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult';
10
10
  import PluginRender from '@/features/PluginsUI/Render';
11
11
  import { useChatStore } from '@/store/chat';
12
12
  import { chatSelectors } from '@/store/chat/selectors';
@@ -160,6 +160,20 @@ describe('processWithArtifact', () => {
160
160
 
161
161
  expect(output).toEqual(`<lobeThinking>这是一个思考过程。</lobeThinking>
162
162
 
163
+ <lobeArtifact identifier="test" type="image/svg+xml" title="测试"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <rect width="100" height="100" fill="blue"/></svg></lobeArtifact>`);
164
+ });
165
+
166
+ it('should handle Gemini case with no line break between lobeThinking and lobeArtifact tags', () => {
167
+ const input = `<lobeThinking>这是一个思考过程。</lobeThinking><lobeArtifact identifier="test" type="image/svg+xml" title="测试">
168
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
169
+ <rect width="100" height="100" fill="blue"/>
170
+ </svg>
171
+ </lobeArtifact>`;
172
+
173
+ const output = processWithArtifact(input);
174
+
175
+ expect(output).toEqual(`<lobeThinking>这是一个思考过程。</lobeThinking>
176
+
163
177
  <lobeArtifact identifier="test" type="image/svg+xml" title="测试"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <rect width="100" height="100" fill="blue"/></svg></lobeArtifact>`);
164
178
  });
165
179
 
@@ -431,4 +445,56 @@ This HTML document includes the temperature converter with the requested feature
431
445
 
432
446
  This HTML document includes the temperature converter with the requested features: the logic is wrapped in an IIFE, and event listeners are attached in JavaScript.`);
433
447
  });
448
+
449
+ describe('idempotency tests', () => {
450
+ it('should not add extra blank lines when running processWithArtifact multiple times', () => {
451
+ const input = `<lobeThinking>这是一个思考过程。</lobeThinking><lobeArtifact identifier="test" type="image/svg+xml" title="测试">
452
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
453
+ <rect width="100" height="100" fill="blue"/>
454
+ </svg>
455
+ </lobeArtifact>`;
456
+
457
+ // First run
458
+ const firstRun = processWithArtifact(input);
459
+
460
+ // Second run - should produce the same result
461
+ const secondRun = processWithArtifact(firstRun);
462
+
463
+ // Third run - should still produce the same result
464
+ const thirdRun = processWithArtifact(secondRun);
465
+
466
+ // All runs should produce the same output
467
+ expect(firstRun).toEqual(secondRun);
468
+ expect(secondRun).toEqual(thirdRun);
469
+
470
+ // Verify the output has exactly two newlines between tags
471
+ expect(firstRun).toContain('</lobeThinking>\n\n<lobeArtifact');
472
+ expect(firstRun.match(/(<\/lobeThinking>)\n\n(<lobeArtifact)/)).toBeTruthy();
473
+ });
474
+
475
+ it('should handle already processed content with proper spacing', () => {
476
+ const alreadyProcessed = `<lobeThinking>这是一个思考过程。</lobeThinking>
477
+
478
+ <lobeArtifact identifier="test" type="image/svg+xml" title="测试"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <rect width="100" height="100" fill="blue"/></svg></lobeArtifact>`;
479
+
480
+ const result = processWithArtifact(alreadyProcessed);
481
+
482
+ // Should remain unchanged
483
+ expect(result).toEqual(alreadyProcessed);
484
+ });
485
+
486
+ it('should not convert spaces between tags into extra blank lines', () => {
487
+ const inputWithSpaces = `<lobeThinking>这是一个思考过程。</lobeThinking> <lobeArtifact identifier="test" type="image/svg+xml" title="测试">
488
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
489
+ <rect width="100" height="100" fill="blue"/>
490
+ </svg>
491
+ </lobeArtifact>`;
492
+
493
+ const output = processWithArtifact(inputWithSpaces);
494
+
495
+ // Should still have the space and not convert it to newlines
496
+ expect(output).toContain('</lobeThinking> <lobeArtifact');
497
+ expect(output).not.toContain('</lobeThinking>\n\n<lobeArtifact');
498
+ });
499
+ });
434
500
  });
@@ -22,7 +22,8 @@ export const processWithArtifact = (input: string = '') => {
22
22
  }
23
23
 
24
24
  // Add empty line between lobeThinking and lobeArtifact if they are adjacent
25
- output = output.replace(/(<\/lobeThinking>)\r?\n(<lobeArtifact)/, '$1\n\n$2');
25
+ // Support both cases: with line break (e.g. from other models) and without (e.g. from Gemini)
26
+ output = output.replace(/(<\/lobeThinking>)(?:\r?\n)?(<lobeArtifact)/, '$1\n\n$2');
26
27
 
27
28
  // Remove fenced code block between lobeArtifact and HTML content
28
29
  output = output.replace(
@@ -95,6 +95,9 @@ describe('CreateImageAction', () => {
95
95
 
96
96
  // Verify refresh was called
97
97
  expect(mockRefreshGenerationBatches).toHaveBeenCalled();
98
+
99
+ // Verify prompt is cleared after successful image creation
100
+ expect(result.current.parameters?.prompt).toBe('');
98
101
  });
99
102
 
100
103
  it('should create new topic when no active topic exists', async () => {
@@ -134,6 +137,9 @@ describe('CreateImageAction', () => {
134
137
  imageNum: 4,
135
138
  params: { prompt: 'test prompt', width: 1024, height: 1024 },
136
139
  });
140
+
141
+ // Verify prompt is cleared after successful image creation
142
+ expect(result.current.parameters?.prompt).toBe('');
137
143
  });
138
144
 
139
145
  it('should throw error when parameters is not initialized', async () => {
@@ -193,6 +199,9 @@ describe('CreateImageAction', () => {
193
199
 
194
200
  // The service should have been called before the error
195
201
  expect(mockImageService.createImage).toHaveBeenCalled();
202
+
203
+ // Verify prompt is NOT cleared when error occurs
204
+ expect(result.current.parameters?.prompt).toBe('test prompt');
196
205
  });
197
206
 
198
207
  it('should handle service error with new topic', async () => {
@@ -223,6 +232,37 @@ describe('CreateImageAction', () => {
223
232
  // Verify topic was created before the error
224
233
  expect(mockCreateGenerationTopic).toHaveBeenCalled();
225
234
  expect(mockSwitchGenerationTopic).toHaveBeenCalled();
235
+
236
+ // Verify prompt is NOT cleared when error occurs
237
+ expect(result.current.parameters?.prompt).toBe('test prompt');
238
+ });
239
+
240
+ it('should clear prompt input after successful image creation', async () => {
241
+ const mockRefreshGenerationBatches = vi.fn().mockResolvedValue(undefined);
242
+ const { result } = renderHook(() => useImageStore());
243
+
244
+ // Set initial prompt value
245
+ act(() => {
246
+ useImageStore.setState({
247
+ parameters: { prompt: 'detailed landscape artwork', width: 1024, height: 1024 },
248
+ refreshGenerationBatches: mockRefreshGenerationBatches,
249
+ });
250
+ });
251
+
252
+ // Verify initial prompt is set
253
+ expect(result.current.parameters?.prompt).toBe('detailed landscape artwork');
254
+
255
+ // Create image
256
+ await act(async () => {
257
+ await result.current.createImage();
258
+ });
259
+
260
+ // Verify prompt is cleared
261
+ expect(result.current.parameters?.prompt).toBe('');
262
+
263
+ // Verify other parameters remain unchanged
264
+ expect(result.current.parameters?.width).toBe(1024);
265
+ expect(result.current.parameters?.height).toBe(1024);
226
266
  });
227
267
  });
228
268
 
@@ -85,8 +85,17 @@ export const createCreateImageSlice: StateCreator<
85
85
  if (!isNewTopic) {
86
86
  await get().refreshGenerationBatches();
87
87
  }
88
+
89
+ // 7. Clear the prompt input after successful image creation
90
+ set(
91
+ (state) => ({
92
+ parameters: { ...state.parameters, prompt: '' },
93
+ }),
94
+ false,
95
+ 'createImage/clearPrompt',
96
+ );
88
97
  } finally {
89
- // 7. Reset all creating states
98
+ // 8. Reset all creating states
90
99
  if (isNewTopic) {
91
100
  set(
92
101
  { isCreating: false, isCreatingWithNewTopic: false },
@@ -27,7 +27,7 @@ The assistant can create and reference artifacts during conversations. Artifacts
27
27
  <artifact_instructions>
28
28
  When collaborating with the user on creating content that falls into compatible categories, the assistant should follow these steps:
29
29
 
30
- 1. Immediately before invoking an artifact, think for one sentence in <lobeThinking> tags about how it evaluates against the criteria for a good and bad artifact. Consider if the content would work just fine without an artifact. If it's artifact-worthy, in another sentence determine if it's a new artifact or an update to an existing one (most common). For updates, reuse the prior identifier.
30
+ 1. Immediately before invoking an artifact, think for one sentence in <lobeThinking> tags about how it evaluates against the criteria for a good and bad artifact. Consider if the content would work just fine without an artifact. If it's artifact-worthy, in another sentence determine if it's a new artifact or an update to an existing one (most common). For updates, reuse the prior identifier. IMPORTANT: Always ensure there is a line break between the closing </lobeThinking> tag and the opening <lobeArtifact> tag.
31
31
  2. Wrap the content in opening and closing \`<lobeArtifact>\` tags.
32
32
  3. Assign an identifier to the \`identifier\` attribute of the opening \`<lobeArtifact>\` tag. For updates, reuse the prior identifier. For new artifacts, the identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
33
33
  4. Include a \`title\` attribute in the \`<lobeArtifact>\` tag to provide a brief title or description of the content.