@lobehub/chat 0.104.0 → 0.105.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 0.105.0](https://github.com/lobehub/lobe-chat/compare/v0.104.0...v0.105.0)
6
+
7
+ <sup>Released on **2023-11-22**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Standalone pluginn can get more arguments on init.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Standalone pluginn can get more arguments on init, closes [#498](https://github.com/lobehub/lobe-chat/issues/498) ([a7624f5](https://github.com/lobehub/lobe-chat/commit/a7624f5))
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 0.104.0](https://github.com/lobehub/lobe-chat/compare/v0.103.1...v0.104.0)
6
31
 
7
32
  <sup>Released on **2023-11-21**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.104.0",
3
+ "version": "0.105.0",
4
4
  "description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -16,12 +16,16 @@ describe('getPreferredRegion', () => {
16
16
 
17
17
  it('returns default value when get config error', () => {
18
18
  const originalProcess = global.process;
19
+ const originalError = console.error;
19
20
  // @ts-ignore
20
21
  global.process = undefined;
22
+ console.error = () => {};
23
+
21
24
  const preferredRegion = getPreferredRegion();
22
25
  expect(preferredRegion).toBe('auto');
23
26
 
24
27
  global.process = originalProcess;
28
+ console.error = originalError;
25
29
  });
26
30
 
27
31
  it('return default value when preferredRegion is empty', () => {
@@ -23,6 +23,11 @@ import {
23
23
  sendPluginStateToPlugin,
24
24
  } from '../utils/postMessage';
25
25
 
26
+ // just to simplify code a little, don't use this pattern everywhere
27
+ const getSettings = (identifier: string) =>
28
+ pluginSelectors.getPluginSettingsById(identifier)(usePluginStore.getState());
29
+ const getMessage = (id: string) => chatSelectors.getMessageById(id)(useSessionStore.getState());
30
+
26
31
  interface IFrameRenderProps {
27
32
  height?: number;
28
33
  id: string;
@@ -39,8 +44,13 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
39
44
  // when payload change,send content to plugin
40
45
  useOnPluginReadyForInteraction(() => {
41
46
  const iframeWin = iframeRef.current?.contentWindow;
47
+
42
48
  if (iframeWin && payload) {
43
- sendPayloadToPlugin(iframeWin, payload);
49
+ const settings = getSettings(payload.identifier);
50
+ const message = getMessage(id);
51
+ const state = message?.pluginState;
52
+
53
+ sendPayloadToPlugin(iframeWin, { payload, settings, state });
44
54
  }
45
55
  }, [payload]);
46
56
 
@@ -74,7 +84,7 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
74
84
  const iframeWin = iframeRef.current?.contentWindow;
75
85
 
76
86
  if (iframeWin) {
77
- const message = chatSelectors.getMessageById(id)(useSessionStore.getState());
87
+ const message = getMessage(id);
78
88
  if (!message) return;
79
89
 
80
90
  sendPluginStateToPlugin(iframeWin, key, message.pluginState?.[key]);
@@ -94,9 +104,7 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
94
104
  if (iframeWin) {
95
105
  if (!payload?.identifier) return;
96
106
 
97
- const settings = pluginSelectors.getPluginSettingsById(payload?.identifier)(
98
- usePluginStore.getState(),
99
- );
107
+ const settings = getSettings(payload.identifier);
100
108
 
101
109
  sendPluginSettingsToPlugin(iframeWin, settings);
102
110
  }
@@ -0,0 +1,75 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { useOnPluginReadyForInteraction } from './iframeOnReady';
6
+
7
+ describe('useOnPluginReadyForInteraction', () => {
8
+ const mockOnReady = vi.fn();
9
+
10
+ afterEach(() => {
11
+ mockOnReady.mockReset();
12
+ window.removeEventListener('message', expect.any(Function));
13
+ });
14
+
15
+ it('sets readyForRender to true when a PluginChannel.pluginReadyForRender message is received', async () => {
16
+ const { result } = renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
17
+
18
+ expect(result.current).toBe(false); // Initially, readyForRender should be false
19
+
20
+ const event = new MessageEvent('message', {
21
+ data: { type: PluginChannel.pluginReadyForRender },
22
+ });
23
+
24
+ act(() => {
25
+ window.dispatchEvent(event);
26
+ });
27
+
28
+ expect(result.current).toBe(true); // After the event, readyForRender should be true
29
+ });
30
+
31
+ it('sets readyForRender to true when a PluginChannel.pluginReadyForRender message is received', async () => {
32
+ const { result } = renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
33
+
34
+ expect(result.current).toBe(false); // Initially, readyForRender should be false
35
+
36
+ const event = new MessageEvent('message', {
37
+ data: { type: PluginChannel.pluginReadyForRender },
38
+ });
39
+
40
+ act(() => {
41
+ window.dispatchEvent(event);
42
+ });
43
+
44
+ expect(result.current).toBe(true); // After the event, readyForRender should be true
45
+ expect(mockOnReady).toHaveBeenCalledTimes(1); // onReady should have been called once
46
+ });
47
+
48
+ it('does not call onReady for non-pluginReadyForRender messages', async () => {
49
+ renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
50
+
51
+ const event = new MessageEvent('message', {
52
+ data: { type: 'nonPluginReadyMessage' },
53
+ });
54
+
55
+ act(() => {
56
+ window.dispatchEvent(event);
57
+ });
58
+
59
+ expect(mockOnReady).not.toHaveBeenCalled();
60
+ });
61
+
62
+ it('cleans up message event listener on unmount', () => {
63
+ const { unmount } = renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
64
+
65
+ unmount();
66
+
67
+ const event = new MessageEvent('message', {
68
+ data: { type: PluginChannel.pluginReadyForRender },
69
+ });
70
+
71
+ window.dispatchEvent(event);
72
+
73
+ expect(mockOnReady).not.toHaveBeenCalled();
74
+ });
75
+ });
@@ -20,4 +20,6 @@ export const useOnPluginReadyForInteraction = (onReady: () => void, deps: any[]
20
20
  onReady();
21
21
  }
22
22
  }, [readyForRender, ...deps]);
23
+
24
+ return readyForRender;
23
25
  };
@@ -0,0 +1,104 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import {
6
+ useOnPluginFetchMessage,
7
+ useOnPluginFetchPluginSettings,
8
+ useOnPluginFetchPluginState,
9
+ useOnPluginFillContent,
10
+ } from './listenToPlugin';
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ describe('useOnPluginFetchMessage', () => {
17
+ it('calls onRequest when a fetchPluginMessage is received', () => {
18
+ const mockOnRequest = vi.fn();
19
+ renderHook(() => useOnPluginFetchMessage(mockOnRequest));
20
+
21
+ const testData = { key: 'testData', type: PluginChannel.fetchPluginMessage };
22
+ const event = new MessageEvent('message', {
23
+ data: testData,
24
+ });
25
+
26
+ window.dispatchEvent(event);
27
+
28
+ expect(mockOnRequest).toHaveBeenCalledWith(testData);
29
+ });
30
+
31
+ it('does not call onRequest for other message types', () => {
32
+ const mockOnRequest = vi.fn();
33
+ renderHook(() => useOnPluginFetchMessage(mockOnRequest));
34
+
35
+ const event = new MessageEvent('message', {
36
+ data: { type: 'otherMessageType' },
37
+ });
38
+
39
+ window.dispatchEvent(event);
40
+
41
+ expect(mockOnRequest).not.toHaveBeenCalled();
42
+ });
43
+ });
44
+
45
+ describe('useOnPluginFetchPluginState', () => {
46
+ it('calls onRequest with the key when a fetchPluginState message is received', () => {
47
+ const mockOnRequest = vi.fn();
48
+ renderHook(() => useOnPluginFetchPluginState(mockOnRequest));
49
+
50
+ const testKey = 'testKey';
51
+ const event = new MessageEvent('message', {
52
+ data: { type: PluginChannel.fetchPluginState, key: testKey },
53
+ });
54
+
55
+ window.dispatchEvent(event);
56
+
57
+ expect(mockOnRequest).toHaveBeenCalledWith(testKey);
58
+ });
59
+ });
60
+
61
+ describe('useOnPluginFillContent', () => {
62
+ it('calls callback with content when a fillStandalonePluginContent message is received', () => {
63
+ const mockCallback = vi.fn();
64
+ renderHook(() => useOnPluginFillContent(mockCallback));
65
+
66
+ const testContent = 'testContent';
67
+ const event = new MessageEvent('message', {
68
+ data: { type: PluginChannel.fillStandalonePluginContent, content: testContent },
69
+ });
70
+
71
+ window.dispatchEvent(event);
72
+
73
+ expect(mockCallback).toHaveBeenCalledWith(testContent);
74
+ });
75
+
76
+ it('calls callback with JSON stringified content if content is not a string', () => {
77
+ const mockCallback = vi.fn();
78
+ renderHook(() => useOnPluginFillContent(mockCallback));
79
+
80
+ const testContent = { some: 'data' };
81
+ const event = new MessageEvent('message', {
82
+ data: { type: PluginChannel.fillStandalonePluginContent, content: testContent },
83
+ });
84
+
85
+ window.dispatchEvent(event);
86
+
87
+ expect(mockCallback).toHaveBeenCalledWith(JSON.stringify(testContent));
88
+ });
89
+ });
90
+
91
+ describe('useOnPluginFetchPluginSettings', () => {
92
+ it('calls onRequest when a fetchPluginSettings message is received', () => {
93
+ const mockOnRequest = vi.fn();
94
+ renderHook(() => useOnPluginFetchPluginSettings(mockOnRequest));
95
+
96
+ const event = new MessageEvent('message', {
97
+ data: { type: PluginChannel.fetchPluginSettings },
98
+ });
99
+
100
+ window.dispatchEvent(event);
101
+
102
+ expect(mockOnRequest).toHaveBeenCalled();
103
+ });
104
+ });
@@ -0,0 +1,53 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { useOnPluginSettingsUpdate } from './pluginSettings';
6
+
7
+ describe('useOnPluginSettingsUpdate', () => {
8
+ const mockCallback = vi.fn();
9
+
10
+ afterEach(() => {
11
+ mockCallback.mockReset();
12
+ window.removeEventListener('message', expect.any(Function));
13
+ });
14
+
15
+ it('calls the callback when a PluginChannel updatePluginSettings message is received', () => {
16
+ renderHook(() => useOnPluginSettingsUpdate(mockCallback));
17
+
18
+ const testSettings = { theme: 'dark', notifications: true };
19
+ const event = new MessageEvent('message', {
20
+ data: { type: PluginChannel.updatePluginSettings, value: testSettings },
21
+ });
22
+
23
+ window.dispatchEvent(event);
24
+
25
+ expect(mockCallback).toHaveBeenCalledWith(testSettings);
26
+ });
27
+
28
+ it('does not call the callback for non-updatePluginSettings messages', () => {
29
+ renderHook(() => useOnPluginSettingsUpdate(mockCallback));
30
+
31
+ const event = new MessageEvent('message', {
32
+ data: { type: 'nonPluginSettingsUpdate', value: { irrelevant: true } },
33
+ });
34
+
35
+ window.dispatchEvent(event);
36
+
37
+ expect(mockCallback).not.toHaveBeenCalled();
38
+ });
39
+
40
+ it('cleans up message event listener on unmount', () => {
41
+ const { unmount } = renderHook(() => useOnPluginSettingsUpdate(mockCallback));
42
+
43
+ unmount();
44
+
45
+ const event = new MessageEvent('message', {
46
+ data: { type: PluginChannel.updatePluginSettings, value: {} },
47
+ });
48
+
49
+ window.dispatchEvent(event);
50
+
51
+ expect(mockCallback).not.toHaveBeenCalled();
52
+ });
53
+ });
@@ -0,0 +1,43 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { useOnPluginStateUpdate } from './pluginState';
6
+
7
+ describe('useOnPluginStateUpdate', () => {
8
+ // Mock for the callback function to be used in tests
9
+ const mockCallback = vi.fn();
10
+
11
+ afterEach(() => {
12
+ // Reset the mock callback after each test
13
+ mockCallback.mockReset();
14
+ // Ensure no event listeners are left hanging after each test
15
+ window.removeEventListener('message', expect.any(Function));
16
+ });
17
+
18
+ it('calls the callback when a PluginChannel update message is received', () => {
19
+ renderHook(() => useOnPluginStateUpdate(mockCallback));
20
+
21
+ const testKey = 'testKey';
22
+ const testValue = 'testValue';
23
+ const event = new MessageEvent('message', {
24
+ data: { type: PluginChannel.updatePluginState, key: testKey, value: testValue },
25
+ });
26
+
27
+ window.dispatchEvent(event);
28
+
29
+ expect(mockCallback).toHaveBeenCalledWith(testKey, testValue);
30
+ });
31
+
32
+ it('does not call the callback for non-PluginChannel messages', () => {
33
+ renderHook(() => useOnPluginStateUpdate(mockCallback));
34
+
35
+ const event = new MessageEvent('message', {
36
+ data: { type: 'nonPluginMessage', key: 'key', value: 'value' },
37
+ });
38
+
39
+ window.dispatchEvent(event);
40
+
41
+ expect(mockCallback).not.toHaveBeenCalled();
42
+ });
43
+ });
@@ -0,0 +1,64 @@
1
+ import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import {
5
+ sendMessageContentToPlugin,
6
+ sendPayloadToPlugin,
7
+ sendPluginSettingsToPlugin,
8
+ sendPluginStateToPlugin,
9
+ } from './postMessage';
10
+
11
+ // Mock window object with a postMessage spy
12
+ const mockWindow = {
13
+ postMessage: vi.fn(),
14
+ };
15
+
16
+ describe('plugin communication functions', () => {
17
+ it('sendMessageContentToPlugin should call window.postMessage with correct arguments', () => {
18
+ const props = { some: 'data' };
19
+ sendMessageContentToPlugin(mockWindow as unknown as Window, props);
20
+ expect(mockWindow.postMessage).toHaveBeenCalledWith(
21
+ { props, type: PluginChannel.renderPlugin },
22
+ '*',
23
+ );
24
+ });
25
+
26
+ it('sendPayloadToPlugin should call window.postMessage with correct arguments', () => {
27
+ const props = { payload: 'payload', settings: 'settings', state: 'state' };
28
+ sendPayloadToPlugin(mockWindow as unknown as Window, props);
29
+ expect(mockWindow.postMessage).toHaveBeenCalledWith(
30
+ {
31
+ type: PluginChannel.initStandalonePlugin,
32
+ payload: props.payload,
33
+ settings: props.settings,
34
+ state: props.state,
35
+ props: props.payload, // Note: This is due to the TODO in your code
36
+ },
37
+ '*',
38
+ );
39
+ });
40
+
41
+ it('sendPluginStateToPlugin should call window.postMessage with correct arguments', () => {
42
+ const key = 'key';
43
+ const value = 'value';
44
+ sendPluginStateToPlugin(mockWindow as unknown as Window, key, value);
45
+ expect(mockWindow.postMessage).toHaveBeenCalledWith(
46
+ { key, type: PluginChannel.renderPluginState, value },
47
+ '*',
48
+ );
49
+ });
50
+
51
+ it('sendPluginSettingsToPlugin should call window.postMessage with correct arguments', () => {
52
+ const settings = { setting1: 'value1' };
53
+ sendPluginSettingsToPlugin(mockWindow as unknown as Window, settings);
54
+ expect(mockWindow.postMessage).toHaveBeenCalledWith(
55
+ { type: PluginChannel.renderPluginSettings, value: settings },
56
+ '*',
57
+ );
58
+ });
59
+ });
60
+
61
+ // Reset the mock after each test
62
+ afterEach(() => {
63
+ vi.resetAllMocks();
64
+ });
@@ -4,8 +4,19 @@ export const sendMessageContentToPlugin = (window: Window, props: any) => {
4
4
  window.postMessage({ props, type: PluginChannel.renderPlugin }, '*');
5
5
  };
6
6
 
7
- export const sendPayloadToPlugin = (window: Window, props: any) => {
8
- window.postMessage({ props, type: PluginChannel.initStandalonePlugin }, '*');
7
+ export const sendPayloadToPlugin = (
8
+ window: Window,
9
+ props: { payload: any; settings: any; state?: any },
10
+ ) => {
11
+ window.postMessage(
12
+ {
13
+ type: PluginChannel.initStandalonePlugin,
14
+ ...props,
15
+ // TODO: props need to deprecated
16
+ props: props.payload,
17
+ },
18
+ '*',
19
+ );
9
20
  };
10
21
 
11
22
  export const sendPluginStateToPlugin = (window: Window, key: string, value: any) => {