Skip to content

Commit 2791166

Browse files
authored
✨ feat: support markdown type plugin (#865)
* ✨ feat: support markdown type plugin * 🚨 ci: fix ci * 🚨 ci: fix test * ✨ feat: support trigger AI message and create assistant message * ✅ test: add unit tests * 📸 test: update test * 📸 test: update test * 💄 style: improve loading style
1 parent baaf06a commit 2791166

File tree

14 files changed

+466
-47
lines changed

14 files changed

+466
-47
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
"@types/ua-parser-js": "^0.7",
152152
"@types/uuid": "^9",
153153
"@umijs/lint": "^4",
154-
"@vitest/coverage-v8": "0.34.6",
154+
"@vitest/coverage-v8": "^1",
155155
"commitlint": "^18",
156156
"consola": "^3",
157157
"dpdm": "^3",
@@ -175,7 +175,7 @@
175175
"typescript": "^5",
176176
"unified": "^11",
177177
"unist-util-visit": "^5",
178-
"vitest": "0.34.6",
178+
"vitest": "^1",
179179
"vitest-canvas-mock": "^0.3.3"
180180
},
181181
"publishConfig": {

src/database/schemas/message.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const PluginSchema = z.object({
1111
identifier: z.string(),
1212
arguments: z.string(),
1313
apiName: z.string(),
14-
type: z.enum(['default', 'standalone', 'builtin']).default('default'),
14+
type: z.enum(['default', 'markdown', 'standalone', 'builtin']).default('default'),
1515
});
1616

1717
export const DB_MessageSchema = z.object({

src/features/Conversation/ChatList/Plugins/Render/BuiltinType/index.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { memo } from 'react';
2-
import { Flexbox } from 'react-layout-kit';
32

43
import { BuiltinToolsRenders } from '@/tools/renders';
54

@@ -17,13 +16,7 @@ const BuiltinType = memo<BuiltinTypeProps>(({ content, id, identifier, loading }
1716
const { isJSON, data } = useParseContent(content);
1817

1918
if (!isJSON) {
20-
return (
21-
loading && (
22-
<Flexbox gap={8}>
23-
<Loading />
24-
</Flexbox>
25-
)
26-
);
19+
return loading && <Loading />;
2720
}
2821

2922
const Render = BuiltinToolsRenders[identifier || ''];

src/features/Conversation/ChatList/Plugins/Render/DefaultType/index.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Skeleton } from 'antd';
22
import dynamic from 'next/dynamic';
33
import { Suspense, memo } from 'react';
4-
import { Flexbox } from 'react-layout-kit';
54

65
import { useToolStore } from '@/store/tool';
76
import { pluginSelectors } from '@/store/tool/selectors';
@@ -24,13 +23,7 @@ const PluginDefaultType = memo<PluginDefaultTypeProps>(({ content, name, loading
2423
const { isJSON, data } = useParseContent(content);
2524

2625
if (!isJSON) {
27-
return (
28-
loading && (
29-
<Flexbox gap={8}>
30-
<Loading />
31-
</Flexbox>
32-
)
33-
);
26+
return loading && <Loading />;
3427
}
3528

3629
if (!manifest?.ui) return;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Markdown } from '@lobehub/ui';
2+
import { memo } from 'react';
3+
4+
import Loading from '../Loading';
5+
6+
export interface PluginMarkdownTypeProps {
7+
content: string;
8+
loading?: boolean;
9+
}
10+
11+
const PluginMarkdownType = memo<PluginMarkdownTypeProps>(({ content, loading }) => {
12+
if (loading) return <Loading />;
13+
14+
return <Markdown>{content}</Markdown>;
15+
});
16+
17+
export default PluginMarkdownType;

src/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { pluginSelectors } from '@/store/tool/selectors';
99

1010
import { useOnPluginReadyForInteraction } from '../utils/iframeOnReady';
1111
import {
12+
useOnPluginCreateAssistantMessage,
1213
useOnPluginFetchMessage,
1314
useOnPluginFetchPluginSettings,
1415
useOnPluginFetchPluginState,
1516
useOnPluginFillContent,
17+
useOnPluginTriggerAIMessage,
1618
} from '../utils/listenToPlugin';
1719
import { useOnPluginSettingsUpdate } from '../utils/pluginSettings';
1820
import { useOnPluginStateUpdate } from '../utils/pluginState';
@@ -118,6 +120,21 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
118120
updatePluginSettings(payload?.identifier, value);
119121
});
120122

123+
// when plugin want to trigger AI message
124+
const triggerAIMessage = useChatStore((s) => s.triggerAIMessage);
125+
useOnPluginTriggerAIMessage((messageId) => {
126+
// we need to know which message to trigger
127+
if (messageId !== id) return;
128+
129+
triggerAIMessage(id);
130+
});
131+
132+
// when plugin want to create an assistant message
133+
const createAssistantMessage = useChatStore((s) => s.createAssistantMessageByPlugin);
134+
useOnPluginCreateAssistantMessage((content) => {
135+
createAssistantMessage(content, id);
136+
});
137+
121138
return (
122139
<>
123140
{loading && <Skeleton active style={{ maxWidth: '100%', width }} />}

src/features/Conversation/ChatList/Plugins/Render/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { LobeToolRenderType } from '@/types/tool';
55

66
import BuiltinType from '././BuiltinType';
77
import DefaultType from './DefaultType';
8+
import Markdown from './MarkdownType';
89
import Standalone from './StandaloneType';
910

1011
export interface PluginRenderProps {
@@ -27,6 +28,10 @@ const PluginRender = memo<PluginRenderProps>(
2728
return <BuiltinType content={content} id={id} identifier={identifier} loading={loading} />;
2829
}
2930

31+
case 'markdown': {
32+
return <Markdown content={content} loading={loading} />;
33+
}
34+
3035
default: {
3136
return <DefaultType content={content} loading={loading} name={identifier} />;
3237
}

src/features/Conversation/ChatList/Plugins/Render/utils/listenToPlugin.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { renderHook } from '@testing-library/react';
33
import { afterEach, describe, expect, it, vi } from 'vitest';
44

55
import {
6+
useOnPluginCreateAssistantMessage,
67
useOnPluginFetchMessage,
78
useOnPluginFetchPluginSettings,
89
useOnPluginFetchPluginState,
910
useOnPluginFillContent,
11+
useOnPluginTriggerAIMessage,
1012
} from './listenToPlugin';
1113

1214
afterEach(() => {
@@ -102,3 +104,61 @@ describe('useOnPluginFetchPluginSettings', () => {
102104
expect(mockOnRequest).toHaveBeenCalled();
103105
});
104106
});
107+
108+
describe('useOnPluginTriggerAIMessage', () => {
109+
it('calls callback with id when a triggerAIMessage is received', () => {
110+
const mockCallback = vi.fn();
111+
renderHook(() => useOnPluginTriggerAIMessage(mockCallback));
112+
113+
const testId = 'testId';
114+
const event = new MessageEvent('message', {
115+
data: { type: PluginChannel.triggerAIMessage, id: testId },
116+
});
117+
118+
window.dispatchEvent(event);
119+
120+
expect(mockCallback).toHaveBeenCalledWith(testId);
121+
});
122+
123+
it('does not call callback for other message types', () => {
124+
const mockCallback = vi.fn();
125+
renderHook(() => useOnPluginTriggerAIMessage(mockCallback));
126+
127+
const event = new MessageEvent('message', {
128+
data: { type: 'otherMessageType', id: 'testId' },
129+
});
130+
131+
window.dispatchEvent(event);
132+
133+
expect(mockCallback).not.toHaveBeenCalled();
134+
});
135+
});
136+
137+
describe('useOnPluginCreateAssistantMessage', () => {
138+
it('calls callback with content when a createAssistantMessage is received', () => {
139+
const mockCallback = vi.fn();
140+
renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));
141+
142+
const testContent = 'testContent';
143+
const event = new MessageEvent('message', {
144+
data: { type: PluginChannel.createAssistantMessage, content: testContent },
145+
});
146+
147+
window.dispatchEvent(event);
148+
149+
expect(mockCallback).toHaveBeenCalledWith(testContent);
150+
});
151+
152+
it('does not call callback for other message types', () => {
153+
const mockCallback = vi.fn();
154+
renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));
155+
156+
const event = new MessageEvent('message', {
157+
data: { type: 'otherMessageType', content: 'testContent' },
158+
});
159+
160+
window.dispatchEvent(event);
161+
162+
expect(mockCallback).not.toHaveBeenCalled();
163+
});
164+
});

src/features/Conversation/ChatList/Plugins/Render/utils/listenToPlugin.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,33 @@ export const useOnPluginFetchPluginSettings = (onRequest: () => void) => {
6363
};
6464
}, []);
6565
};
66+
67+
export const useOnPluginTriggerAIMessage = (callback: (id: string) => void) => {
68+
useEffect(() => {
69+
const fn = (e: MessageEvent) => {
70+
if (e.data.type === PluginChannel.triggerAIMessage) {
71+
callback(e.data.id);
72+
}
73+
};
74+
75+
window.addEventListener('message', fn);
76+
return () => {
77+
window.removeEventListener('message', fn);
78+
};
79+
}, []);
80+
};
81+
82+
export const useOnPluginCreateAssistantMessage = (callback: (content: string) => void) => {
83+
useEffect(() => {
84+
const fn = (e: MessageEvent) => {
85+
if (e.data.type === PluginChannel.createAssistantMessage) {
86+
callback(e.data.content);
87+
}
88+
};
89+
90+
window.addEventListener('message', fn);
91+
return () => {
92+
window.removeEventListener('message', fn);
93+
};
94+
}, []);
95+
};

src/services/__tests__/__snapshots__/plugin.test.ts.snap

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ General guidelines:
3333
- Inform users if information is not from Wolfram endpoints.
3434
- Display image URLs with Markdown syntax: ![URL]
3535
- ALWAYS use this exponent notation: \`6*10^14\`, NEVER \`6e14\`.
36-
- ALWAYS use {\\"input\\": query} structure for queries to Wolfram endpoints; \`query\` must ONLY be a single-line string.
37-
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\\\\n[expression]\\\\n$$' for standalone cases and '\\\\( [expression] \\\\)' when inline.
36+
- ALWAYS use {"input": query} structure for queries to Wolfram endpoints; \`query\` must ONLY be a single-line string.
37+
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\\n[expression]\\n$$' for standalone cases and '\\( [expression] \\)' when inline.
3838
- Format inline Wolfram Language code with Markdown code formatting.
3939
- Never mention your knowledge cutoff date; Wolfram may return more recent data.
4040
getWolframAlphaResults guidelines:
4141
- Understands natural language queries about entities in chemistry, physics, geography, history, art, astronomy, and more.
4242
- Performs mathematical calculations, date and unit conversions, formula solving, etc.
43-
- Convert inputs to simplified keyword queries whenever possible (e.g. convert \\"how many people live in France\\" to \\"France population\\").
43+
- Convert inputs to simplified keyword queries whenever possible (e.g. convert "how many people live in France" to "France population").
4444
- Use ONLY single-letter variable names, with or without integer subscript (e.g., n, n1, n_1).
4545
- Use named physical constants (e.g., 'speed of light') without numerical substitution.
46-
- Include a space between compound units (e.g., \\"Ω m\\" for \\"ohm*meter\\").
46+
- Include a space between compound units (e.g., "Ω m" for "ohm*meter").
4747
- To solve for a variable in an equation with units, consider solving a corresponding equation without units; exclude counting units (e.g., books), include genuine units (e.g., kg).
4848
- If data for multiple properties is needed, make separate calls for each property.
4949
- If a Wolfram Alpha result is not relevant to the query:
@@ -55,22 +55,22 @@ getWolframCloudResults guidelines:
5555
- Accepts only syntactically correct Wolfram Language code.
5656
- Performs complex calculations, data analysis, plotting, data import, and information retrieval.
5757
- Before writing code that uses Entity, EntityProperty, EntityClass, etc. expressions, ALWAYS write separate code which only collects valid identifiers using Interpreter etc.; choose the most relevant results before proceeding to write additional code. Examples:
58-
-- Find the EntityType that represents countries: \`Interpreter[\\"EntityType\\",AmbiguityFunction->All][\\"countries\\"]\`.
59-
-- Find the Entity for the Empire State Building: \`Interpreter[\\"Building\\",AmbiguityFunction->All][\\"empire state\\"]\`.
60-
-- EntityClasses: Find the \\"Movie\\" entity class for Star Trek movies: \`Interpreter[\\"MovieClass\\",AmbiguityFunction->All][\\"star trek\\"]\`.
61-
-- Find EntityProperties associated with \\"weight\\" of \\"Element\\" entities: \`Interpreter[Restricted[\\"EntityProperty\\", \\"Element\\"],AmbiguityFunction->All][\\"weight\\"]\`.
62-
-- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation[\\"skyscrapers\\",_,Hold,AmbiguityFunction->All]\`.
63-
-- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity[\\"Element\\",\\"Gold\\"][\\"AtomicNumber\\"]\` to \`ElementData[\\"Gold\\",\\"AtomicNumber\\"]\`).
58+
-- Find the EntityType that represents countries: \`Interpreter["EntityType",AmbiguityFunction->All]["countries"]\`.
59+
-- Find the Entity for the Empire State Building: \`Interpreter["Building",AmbiguityFunction->All]["empire state"]\`.
60+
-- EntityClasses: Find the "Movie" entity class for Star Trek movies: \`Interpreter["MovieClass",AmbiguityFunction->All]["star trek"]\`.
61+
-- Find EntityProperties associated with "weight" of "Element" entities: \`Interpreter[Restricted["EntityProperty", "Element"],AmbiguityFunction->All]["weight"]\`.
62+
-- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation["skyscrapers",_,Hold,AmbiguityFunction->All]\`.
63+
-- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity["Element","Gold"]["AtomicNumber"]\` to \`ElementData["Gold","AtomicNumber"]\`).
6464
- When composing code:
6565
-- Use batching techniques to retrieve data for multiple entities in a single call, if applicable.
6666
-- Use Association to organize and manipulate data when appropriate.
6767
-- Optimize code for performance and minimize the number of calls to external sources (e.g., the Wolfram Knowledgebase)
6868
-- Use only camel case for variable names (e.g., variableName).
69-
-- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {\\"sin(x)\\", \\"cos(x)\\", \\"tan(x)\\"}\`).
69+
-- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {"sin(x)", "cos(x)", "tan(x)"}\`).
7070
-- Avoid use of QuantityMagnitude.
71-
-- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity[\\"WolframLanguageSymbol\\",symbol],{\\"PlaintextUsage\\",\\"Options\\"}]\` to validate or retrieve usage information for relevant symbols; \`symbol\` may be a list of symbols.
71+
-- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity["WolframLanguageSymbol",symbol],{"PlaintextUsage","Options"}]\` to validate or retrieve usage information for relevant symbols; \`symbol\` may be a list of symbols.
7272
-- Apply Evaluate to complex expressions like integrals before plotting (e.g., \`Plot[Evaluate[Integrate[...]]]\`).
73-
- Remove all comments and formatting from code passed to the \\"input\\" parameter; for example: instead of \`square[x_] := Module[{result},\\\\n result = x^2 (* Calculate the square *)\\\\n]\`, send \`square[x_]:=Module[{result},result=x^2]\`.
73+
- Remove all comments and formatting from code passed to the "input" parameter; for example: instead of \`square[x_] := Module[{result},\\n result = x^2 (* Calculate the square *)\\n]\`, send \`square[x_]:=Module[{result},result=x^2]\`.
7474
- In ALL responses that involve code, write ALL code in Wolfram Language; create Wolfram Language functions even if an implementation is already well known in another language.
7575
",
7676
"type": "default",

src/store/chat/slices/message/action.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { StateCreator } from 'zustand/vanilla';
88
import { GPT4_VISION_MODEL_DEFAULT_MAX_TOKENS } from '@/const/llm';
99
import { LOADING_FLAT, isFunctionMessageAtStart, testFunctionMessageAtEnd } from '@/const/message';
1010
import { CreateMessageParams } from '@/database/models/message';
11-
import { DB_Message } from '@/database/schemas/message';
1211
import { chatService } from '@/services/chat';
1312
import { messageService } from '@/services/message';
1413
import { topicService } from '@/services/topic';
@@ -238,7 +237,7 @@ export const chatMessage: StateCreator<
238237
const { model } = getAgentConfig();
239238

240239
// 1. Add an empty message to place the AI response
241-
const assistantMessage: DB_Message = {
240+
const assistantMessage: CreateMessageParams = {
242241
role: 'assistant',
243242
content: LOADING_FLAT,
244243
fromModel: model,
@@ -383,7 +382,7 @@ export const chatMessage: StateCreator<
383382
toggleChatLoading(false, undefined, n('generateMessage(end)') as string);
384383

385384
// also exist message like this:
386-
// 请稍等,我帮您查询一下。{"function_call": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
385+
// 请稍等,我帮您查询一下。{"tool_calls": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
387386
if (!isFunctionCall) {
388387
const { content, valid } = testFunctionMessageAtEnd(output);
389388

0 commit comments

Comments
 (0)