Skip to content

Commit 7c291df

Browse files
Merge pull request #1850 from iamfaran/feat/chat-component
[Feat] AI Chat Component
2 parents 2014590 + 4f9fbba commit 7c291df

29 files changed

+3168
-1333
lines changed

client/packages/lowcoder/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"@jsonforms/core": "^3.5.1",
3434
"@lottiefiles/dotlottie-react": "^0.13.0",
3535
"@manaflair/redux-batch": "^1.0.0",
36+
"@radix-ui/react-avatar": "^1.1.10",
37+
"@radix-ui/react-dialog": "^1.1.14",
3638
"@radix-ui/react-slot": "^1.2.3",
3739
"@radix-ui/react-tooltip": "^1.2.7",
3840
"@rjsf/antd": "^5.24.9",

client/packages/lowcoder/src/components/ResCreatePanel.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { BottomResTypeEnum } from "types/bottomRes";
1313
import { LargeBottomResIconWrapper } from "util/bottomResUtils";
1414
import type { PageType } from "../constants/pageConstants";
1515
import type { SizeType } from "antd/es/config-provider/SizeContext";
16-
import { Datasource } from "constants/datasourceConstants";
16+
import { Datasource, QUICK_SSE_HTTP_API_ID } from "constants/datasourceConstants";
1717
import {
1818
QUICK_GRAPHQL_ID,
1919
QUICK_REST_API_ID,
@@ -172,13 +172,22 @@ const ResButton = (props: {
172172
compType: "streamApi",
173173
},
174174
},
175+
175176
alasql: {
176177
label: trans("query.quickAlasql"),
177178
type: BottomResTypeEnum.Query,
178179
extra: {
179180
compType: "alasql",
180181
},
181182
},
183+
sseHttpApi: {
184+
label: trans("query.quickSseHttpAPI"),
185+
type: BottomResTypeEnum.Query,
186+
extra: {
187+
compType: "sseHttpApi",
188+
dataSourceId: QUICK_SSE_HTTP_API_ID,
189+
},
190+
},
182191
graphql: {
183192
label: trans("query.quickGraphql"),
184193
type: BottomResTypeEnum.Query,
@@ -339,6 +348,7 @@ export function ResCreatePanel(props: ResCreateModalProps) {
339348
<DataSourceListWrapper $placement={placement}>
340349
<ResButton size={buttonSize} identifier={"restApi"} onSelect={onSelect} />
341350
<ResButton size={buttonSize} identifier={"streamApi"} onSelect={onSelect} />
351+
<ResButton size={buttonSize} identifier={"sseHttpApi"} onSelect={onSelect} />
342352
<ResButton size={buttonSize} identifier={"alasql"} onSelect={onSelect} />
343353
<ResButton size={buttonSize} identifier={"graphql"} onSelect={onSelect} />
344354
<DataSourceButton size={buttonSize} onClick={() => setCurlModalVisible(true)}>
Lines changed: 257 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,258 @@
1-
// client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
2-
import { UICompBuilder } from "comps/generators";
3-
import { NameConfig, withExposingConfigs } from "comps/generators/withExposing";
4-
import { chatChildrenMap } from "./chatCompTypes";
5-
import { ChatView } from "./chatView";
6-
import { ChatPropertyView } from "./chatPropertyView";
7-
import { useEffect, useState } from "react";
8-
import { changeChildAction } from "lowcoder-core";
9-
10-
// Build the component
11-
let ChatTmpComp = new UICompBuilder(
12-
chatChildrenMap,
13-
(props, dispatch) => {
14-
useEffect(() => {
15-
if (Boolean(props.tableName)) return;
16-
17-
// Generate a unique database name for this ChatApp instance
18-
const generateUniqueTableName = () => {
19-
const timestamp = Date.now();
20-
const randomId = Math.random().toString(36).substring(2, 15);
21-
return `TABLE_${timestamp}`;
22-
};
23-
24-
const tableName = generateUniqueTableName();
25-
dispatch(changeChildAction('tableName', tableName, true));
26-
}, [props.tableName]);
27-
28-
if (!props.tableName) {
29-
return null; // Don't render until we have a unique DB name
30-
}
31-
return <ChatView {...props} chatQuery={props.chatQuery.value} />;
32-
}
33-
)
34-
.setPropertyViewFn((children) => <ChatPropertyView children={children} />)
35-
.build();
36-
37-
ChatTmpComp = class extends ChatTmpComp {
38-
override autoHeight(): boolean {
39-
return this.children.autoHeight.getView();
40-
}
41-
};
42-
43-
// Export the component
44-
export const ChatComp = withExposingConfigs(ChatTmpComp, [
45-
new NameConfig("text", "Chat component text"),
1+
// client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
2+
3+
import { UICompBuilder } from "comps/generators";
4+
import { NameConfig, withExposingConfigs } from "comps/generators/withExposing";
5+
import { StringControl } from "comps/controls/codeControl";
6+
import { arrayObjectExposingStateControl, stringExposingStateControl } from "comps/controls/codeStateControl";
7+
import { withDefault } from "comps/generators";
8+
import { BoolControl } from "comps/controls/boolControl";
9+
import { dropdownControl } from "comps/controls/dropdownControl";
10+
import QuerySelectControl from "comps/controls/querySelectControl";
11+
import { eventHandlerControl, EventConfigType } from "comps/controls/eventHandlerControl";
12+
import { ChatCore } from "./components/ChatCore";
13+
import { ChatPropertyView } from "./chatPropertyView";
14+
import { createChatStorage } from "./utils/storageFactory";
15+
import { QueryHandler, createMessageHandler } from "./handlers/messageHandlers";
16+
import { useMemo, useRef, useEffect } from "react";
17+
import { changeChildAction } from "lowcoder-core";
18+
import { ChatMessage } from "./types/chatTypes";
19+
import { trans } from "i18n";
20+
21+
import "@assistant-ui/styles/index.css";
22+
import "@assistant-ui/styles/markdown.css";
23+
24+
// ============================================================================
25+
// CHAT-SPECIFIC EVENTS
26+
// ============================================================================
27+
28+
export const componentLoadEvent: EventConfigType = {
29+
label: trans("chat.componentLoad"),
30+
value: "componentLoad",
31+
description: trans("chat.componentLoadDesc"),
32+
};
33+
34+
export const messageSentEvent: EventConfigType = {
35+
label: trans("chat.messageSent"),
36+
value: "messageSent",
37+
description: trans("chat.messageSentDesc"),
38+
};
39+
40+
export const messageReceivedEvent: EventConfigType = {
41+
label: trans("chat.messageReceived"),
42+
value: "messageReceived",
43+
description: trans("chat.messageReceivedDesc"),
44+
};
45+
46+
export const threadCreatedEvent: EventConfigType = {
47+
label: trans("chat.threadCreated"),
48+
value: "threadCreated",
49+
description: trans("chat.threadCreatedDesc"),
50+
};
51+
52+
export const threadUpdatedEvent: EventConfigType = {
53+
label: trans("chat.threadUpdated"),
54+
value: "threadUpdated",
55+
description: trans("chat.threadUpdatedDesc"),
56+
};
57+
58+
export const threadDeletedEvent: EventConfigType = {
59+
label: trans("chat.threadDeleted"),
60+
value: "threadDeleted",
61+
description: trans("chat.threadDeletedDesc"),
62+
};
63+
64+
const ChatEventOptions = [
65+
componentLoadEvent,
66+
messageSentEvent,
67+
messageReceivedEvent,
68+
threadCreatedEvent,
69+
threadUpdatedEvent,
70+
threadDeletedEvent,
71+
] as const;
72+
73+
export const ChatEventHandlerControl = eventHandlerControl(ChatEventOptions);
74+
75+
// ============================================================================
76+
// SIMPLIFIED CHILDREN MAP - WITH EVENT HANDLERS
77+
// ============================================================================
78+
79+
80+
export function addSystemPromptToHistory(
81+
conversationHistory: ChatMessage[],
82+
systemPrompt: string
83+
): Array<{ role: string; content: string; timestamp: number }> {
84+
// Format conversation history for use in queries
85+
const formattedHistory = conversationHistory.map(msg => ({
86+
role: msg.role,
87+
content: msg.text,
88+
timestamp: msg.timestamp
89+
}));
90+
91+
// Create system message (always exists since we have default)
92+
const systemMessage = [{
93+
role: "system" as const,
94+
content: systemPrompt,
95+
timestamp: Date.now() - 1000000 // Ensure it's always first chronologically
96+
}];
97+
98+
// Return complete history with system prompt prepended
99+
return [...systemMessage, ...formattedHistory];
100+
}
101+
102+
103+
function generateUniqueTableName(): string {
104+
return `chat${Math.floor(1000 + Math.random() * 9000)}`;
105+
}
106+
107+
const ModelTypeOptions = [
108+
{ label: trans("chat.handlerTypeQuery"), value: "query" },
109+
{ label: trans("chat.handlerTypeN8N"), value: "n8n" },
110+
] as const;
111+
112+
export const chatChildrenMap = {
113+
// Storage
114+
// Storage (add the hidden property here)
115+
_internalDbName: withDefault(StringControl, ""),
116+
// Message Handler Configuration
117+
handlerType: dropdownControl(ModelTypeOptions, "query"),
118+
chatQuery: QuerySelectControl, // Only used for "query" type
119+
modelHost: withDefault(StringControl, ""), // Only used for "n8n" type
120+
systemPrompt: withDefault(StringControl, trans("chat.defaultSystemPrompt")),
121+
streaming: BoolControl.DEFAULT_TRUE,
122+
123+
// UI Configuration
124+
placeholder: withDefault(StringControl, trans("chat.defaultPlaceholder")),
125+
126+
// Database Information (read-only)
127+
databaseName: withDefault(StringControl, ""),
128+
129+
// Event Handlers
130+
onEvent: ChatEventHandlerControl,
131+
132+
// Exposed Variables (not shown in Property View)
133+
currentMessage: stringExposingStateControl("currentMessage", ""),
134+
conversationHistory: stringExposingStateControl("conversationHistory", "[]"),
135+
};
136+
137+
// ============================================================================
138+
// CLEAN CHATCOMP - USES NEW ARCHITECTURE
139+
// ============================================================================
140+
141+
const ChatTmpComp = new UICompBuilder(
142+
chatChildrenMap,
143+
(props, dispatch) => {
144+
145+
const uniqueTableName = useRef<string>();
146+
// Generate unique table name once (with persistence)
147+
if (!uniqueTableName.current) {
148+
// Use persisted name if exists, otherwise generate new one
149+
uniqueTableName.current = props._internalDbName || generateUniqueTableName();
150+
151+
// Save the name for future refreshes
152+
if (!props._internalDbName) {
153+
dispatch(changeChildAction("_internalDbName", uniqueTableName.current, false));
154+
}
155+
156+
// Update the database name in the props for display
157+
const dbName = `ChatDB_${uniqueTableName.current}`;
158+
dispatch(changeChildAction("databaseName", dbName, false));
159+
}
160+
// Create storage with unique table name
161+
const storage = useMemo(() =>
162+
createChatStorage(uniqueTableName.current!),
163+
[]
164+
);
165+
166+
// Create message handler based on type
167+
const messageHandler = useMemo(() => {
168+
const handlerType = props.handlerType;
169+
170+
if (handlerType === "query") {
171+
return new QueryHandler({
172+
chatQuery: props.chatQuery.value,
173+
dispatch,
174+
streaming: props.streaming,
175+
});
176+
} else if (handlerType === "n8n") {
177+
return createMessageHandler("n8n", {
178+
modelHost: props.modelHost,
179+
systemPrompt: props.systemPrompt,
180+
streaming: props.streaming
181+
});
182+
} else {
183+
// Fallback to mock handler
184+
return createMessageHandler("mock", {
185+
chatQuery: props.chatQuery.value,
186+
dispatch,
187+
streaming: props.streaming
188+
});
189+
}
190+
}, [
191+
props.handlerType,
192+
props.chatQuery,
193+
props.modelHost,
194+
props.systemPrompt,
195+
props.streaming,
196+
dispatch,
197+
]);
198+
199+
// Handle message updates for exposed variable
200+
const handleMessageUpdate = (message: string) => {
201+
dispatch(changeChildAction("currentMessage", message, false));
202+
// Trigger messageSent event
203+
props.onEvent("messageSent");
204+
};
205+
206+
// Handle conversation history updates for exposed variable
207+
// Handle conversation history updates for exposed variable
208+
const handleConversationUpdate = (conversationHistory: any[]) => {
209+
// Use utility function to create complete history with system prompt
210+
const historyWithSystemPrompt = addSystemPromptToHistory(
211+
conversationHistory,
212+
props.systemPrompt
213+
);
214+
215+
// Expose the complete history (with system prompt) for use in queries
216+
dispatch(changeChildAction("conversationHistory", JSON.stringify(historyWithSystemPrompt), false));
217+
218+
// Trigger messageReceived event when bot responds
219+
const lastMessage = conversationHistory[conversationHistory.length - 1];
220+
if (lastMessage && lastMessage.role === 'assistant') {
221+
props.onEvent("messageReceived");
222+
}
223+
};
224+
225+
// Cleanup on unmount
226+
useEffect(() => {
227+
return () => {
228+
const tableName = uniqueTableName.current;
229+
if (tableName) {
230+
storage.cleanup();
231+
}
232+
};
233+
}, []);
234+
235+
return (
236+
<ChatCore
237+
storage={storage}
238+
messageHandler={messageHandler}
239+
placeholder={props.placeholder}
240+
onMessageUpdate={handleMessageUpdate}
241+
onConversationUpdate={handleConversationUpdate}
242+
onEvent={props.onEvent}
243+
/>
244+
);
245+
}
246+
)
247+
.setPropertyViewFn((children) => <ChatPropertyView children={children} />)
248+
.build();
249+
250+
// ============================================================================
251+
// EXPORT WITH EXPOSED VARIABLES
252+
// ============================================================================
253+
254+
export const ChatComp = withExposingConfigs(ChatTmpComp, [
255+
new NameConfig("currentMessage", "Current user message"),
256+
new NameConfig("conversationHistory", "Full conversation history as JSON array (includes system prompt for API calls)"),
257+
new NameConfig("databaseName", "Database name for SQL queries (ChatDB_<componentName>)"),
46258
]);

0 commit comments

Comments
 (0)