Skip to content

Commit 85f8df1

Browse files
Merge pull request #1841 from iamfaran/feat/chat-component
[In Progress] [Feat]: Chat Component
2 parents 6ee73d2 + be3b3ab commit 85f8df1

File tree

21 files changed

+2626
-9
lines changed

21 files changed

+2626
-9
lines changed

client/packages/lowcoder/package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
"main": "src/index.sdk.ts",
77
"types": "src/index.sdk.ts",
88
"dependencies": {
9+
"@ai-sdk/openai": "^1.3.22",
910
"@ant-design/icons": "^5.3.0",
11+
"@assistant-ui/react": "^0.10.24",
12+
"@assistant-ui/react-ai-sdk": "^0.10.14",
13+
"@assistant-ui/react-markdown": "^0.10.5",
14+
"@assistant-ui/styles": "^0.1.13",
1015
"@bany/curl-to-json": "^1.2.8",
1116
"@codemirror/autocomplete": "^6.11.1",
1217
"@codemirror/commands": "^6.3.2",
@@ -28,6 +33,8 @@
2833
"@jsonforms/core": "^3.5.1",
2934
"@lottiefiles/dotlottie-react": "^0.13.0",
3035
"@manaflair/redux-batch": "^1.0.0",
36+
"@radix-ui/react-slot": "^1.2.3",
37+
"@radix-ui/react-tooltip": "^1.2.7",
3138
"@rjsf/antd": "^5.24.9",
3239
"@rjsf/core": "^5.24.9",
3340
"@rjsf/utils": "^5.24.9",
@@ -37,11 +44,13 @@
3744
"@types/react-signature-canvas": "^1.0.2",
3845
"@types/react-test-renderer": "^18.0.0",
3946
"@types/react-virtualized": "^9.21.21",
47+
"ai": "^4.3.16",
4048
"alasql": "^4.6.6",
4149
"animate.css": "^4.1.1",
4250
"antd": "^5.25.2",
4351
"axios": "^1.7.7",
4452
"buffer": "^6.0.3",
53+
"class-variance-authority": "^0.7.1",
4554
"clsx": "^2.0.0",
4655
"cnchar": "^3.2.4",
4756
"coolshapes-react": "lowcoder-org/coolshapes-react",
@@ -61,6 +70,7 @@
6170
"loglevel": "^1.8.0",
6271
"lowcoder-core": "workspace:^",
6372
"lowcoder-design": "workspace:^",
73+
"lucide-react": "^0.525.0",
6474
"mime": "^3.0.0",
6575
"moment": "^2.29.4",
6676
"numbro": "^2.3.6",
@@ -98,7 +108,7 @@
98108
"regenerator-runtime": "^0.13.9",
99109
"rehype-raw": "^6.1.1",
100110
"rehype-sanitize": "^5.0.1",
101-
"remark-gfm": "^4.0.0",
111+
"remark-gfm": "^4.0.1",
102112
"resize-observer-polyfill": "^1.5.1",
103113
"simplebar-react": "^3.2.4",
104114
"sql-formatter": "^8.2.0",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
8+
// Build the component
9+
const ChatTmpComp = new UICompBuilder(
10+
chatChildrenMap,
11+
(props) => <ChatView {...props} chatQuery={props.chatQuery.value} />
12+
)
13+
.setPropertyViewFn((children) => <ChatPropertyView children={children} />)
14+
.build();
15+
16+
// Export the component
17+
export const ChatComp = withExposingConfigs(ChatTmpComp, [
18+
new NameConfig("text", "Chat component text"),
19+
]);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// client/packages/lowcoder/src/comps/comps/chatComp/chatCompTypes.ts
2+
import { StringControl, NumberControl } from "comps/controls/codeControl";
3+
import { withDefault } from "comps/generators";
4+
import { BoolControl } from "comps/controls/boolControl";
5+
import { dropdownControl } from "comps/controls/dropdownControl";
6+
import QuerySelectControl from "comps/controls/querySelectControl";
7+
8+
// Model type dropdown options
9+
const ModelTypeOptions = [
10+
{ label: "Direct LLM", value: "direct-llm" },
11+
{ label: "n8n Workflow", value: "n8n" },
12+
] as const;
13+
14+
export const chatChildrenMap = {
15+
text: withDefault(StringControl, "Chat Component Placeholder"),
16+
chatQuery: QuerySelectControl,
17+
modelType: dropdownControl(ModelTypeOptions, "direct-llm"),
18+
streaming: BoolControl.DEFAULT_TRUE,
19+
systemPrompt: withDefault(StringControl, "You are a helpful assistant."),
20+
agent: BoolControl,
21+
maxInteractions: withDefault(NumberControl, 10),
22+
};
23+
24+
export type ChatCompProps = {
25+
text: string;
26+
chatQuery: string;
27+
modelType: string;
28+
streaming: boolean;
29+
systemPrompt: string;
30+
agent: boolean;
31+
maxInteractions: number;
32+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx
2+
import React from "react";
3+
import { Section, sectionNames } from "lowcoder-design";
4+
5+
export const ChatPropertyView = React.memo((props: any) => {
6+
const { children } = props;
7+
8+
return (
9+
<Section name={sectionNames.basic}>
10+
{children.text.propertyView({ label: "Text" })}
11+
{children.chatQuery.propertyView({ label: "Chat Query" })}
12+
{children.modelType.propertyView({ label: "Model Type" })}
13+
{children.streaming.propertyView({ label: "Enable Streaming" })}
14+
{children.systemPrompt.propertyView({
15+
label: "System Prompt",
16+
placeholder: "Enter system prompt...",
17+
enableSpellCheck: false,
18+
})}
19+
{children.agent.propertyView({ label: "Enable Agent Mode" })}
20+
{children.maxInteractions.propertyView({
21+
label: "Max Interactions",
22+
placeholder: "10",
23+
})}
24+
</Section>
25+
);
26+
});
27+
28+
ChatPropertyView.displayName = 'ChatPropertyView';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// client/packages/lowcoder/src/comps/comps/chatComp/chatView.tsx
2+
import React from "react";
3+
import { ChatCompProps } from "./chatCompTypes";
4+
import { ChatApp } from "./components/ChatApp";
5+
6+
import "@assistant-ui/styles/index.css";
7+
import "@assistant-ui/styles/markdown.css";
8+
9+
export const ChatView = React.memo((props: ChatCompProps) => {
10+
return <ChatApp />;
11+
});
12+
13+
ChatView.displayName = 'ChatView';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ChatProvider } from "./context/ChatContext";
2+
import { ChatMain } from "./ChatMain";
3+
4+
export function ChatApp() {
5+
return (
6+
<ChatProvider>
7+
<ChatMain />
8+
</ChatProvider>
9+
);
10+
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import React, { useState } from "react";
2+
import {
3+
useExternalStoreRuntime,
4+
ThreadMessageLike,
5+
AppendMessage,
6+
AssistantRuntimeProvider,
7+
ExternalStoreThreadListAdapter,
8+
} from "@assistant-ui/react";
9+
import { Thread } from "./assistant-ui/thread";
10+
import { ThreadList } from "./assistant-ui/thread-list";
11+
import {
12+
useChatContext,
13+
MyMessage,
14+
ThreadData,
15+
RegularThreadData,
16+
ArchivedThreadData
17+
} from "./context/ChatContext";
18+
import styled from "styled-components";
19+
20+
const ChatContainer = styled.div`
21+
display: flex;
22+
height: 500px;
23+
24+
.aui-thread-list-root {
25+
width: 250px;
26+
background-color: #fff;
27+
padding: 10px;
28+
}
29+
30+
.aui-thread-root {
31+
flex: 1;
32+
background-color: #f9fafb;
33+
}
34+
35+
.aui-thread-list-item {
36+
cursor: pointer;
37+
transition: background-color 0.2s ease;
38+
39+
&[data-active="true"] {
40+
background-color: #dbeafe;
41+
border: 1px solid #bfdbfe;
42+
}
43+
}
44+
`;
45+
46+
const generateId = () => Math.random().toString(36).substr(2, 9);
47+
48+
const callYourAPI = async (text: string) => {
49+
// Simulate API delay
50+
await new Promise(resolve => setTimeout(resolve, 1500));
51+
52+
// Simple responses
53+
return {
54+
content: "This is a mock response from your backend. You typed: " + text
55+
};
56+
};
57+
58+
export function ChatMain() {
59+
const { state, actions } = useChatContext();
60+
const [isRunning, setIsRunning] = useState(false);
61+
62+
console.log("STATE", state);
63+
64+
// Get messages for current thread
65+
const currentMessages = actions.getCurrentMessages();
66+
67+
// Convert custom format to ThreadMessageLike
68+
const convertMessage = (message: MyMessage): ThreadMessageLike => ({
69+
role: message.role,
70+
content: [{ type: "text", text: message.text }],
71+
id: message.id,
72+
createdAt: new Date(message.timestamp),
73+
});
74+
75+
const onNew = async (message: AppendMessage) => {
76+
// Extract text from AppendMessage content array
77+
if (message.content.length !== 1 || message.content[0]?.type !== "text") {
78+
throw new Error("Only text content is supported");
79+
}
80+
81+
// Add user message in custom format
82+
const userMessage: MyMessage = {
83+
id: generateId(),
84+
role: "user",
85+
text: message.content[0].text,
86+
timestamp: Date.now(),
87+
};
88+
89+
// Update current thread with new user message
90+
await actions.addMessage(state.currentThreadId, userMessage);
91+
setIsRunning(true);
92+
93+
try {
94+
// Call mock API
95+
const response = await callYourAPI(userMessage.text);
96+
97+
const assistantMessage: MyMessage = {
98+
id: generateId(),
99+
role: "assistant",
100+
text: response.content,
101+
timestamp: Date.now(),
102+
};
103+
104+
// Update current thread with assistant response
105+
await actions.addMessage(state.currentThreadId, assistantMessage);
106+
} catch (error) {
107+
// Handle errors gracefully
108+
const errorMessage: MyMessage = {
109+
id: generateId(),
110+
role: "assistant",
111+
text: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`,
112+
timestamp: Date.now(),
113+
};
114+
115+
await actions.addMessage(state.currentThreadId, errorMessage);
116+
} finally {
117+
setIsRunning(false);
118+
}
119+
};
120+
121+
// Add onEdit functionality
122+
const onEdit = async (message: AppendMessage) => {
123+
// Extract text from AppendMessage content array
124+
if (message.content.length !== 1 || message.content[0]?.type !== "text") {
125+
throw new Error("Only text content is supported");
126+
}
127+
128+
// Find the index where to insert the edited message
129+
const index = currentMessages.findIndex((m) => m.id === message.parentId) + 1;
130+
131+
// Keep messages up to the parent
132+
const newMessages = [...currentMessages.slice(0, index)];
133+
134+
// Add the edited message in custom format
135+
const editedMessage: MyMessage = {
136+
id: generateId(),
137+
role: "user",
138+
text: message.content[0].text,
139+
timestamp: Date.now(),
140+
};
141+
newMessages.push(editedMessage);
142+
143+
// Update messages using the new context action
144+
await actions.updateMessages(state.currentThreadId, newMessages);
145+
setIsRunning(true);
146+
147+
try {
148+
// Generate new response
149+
const response = await callYourAPI(editedMessage.text);
150+
151+
const assistantMessage: MyMessage = {
152+
id: generateId(),
153+
role: "assistant",
154+
text: response.content,
155+
timestamp: Date.now(),
156+
};
157+
158+
newMessages.push(assistantMessage);
159+
await actions.updateMessages(state.currentThreadId, newMessages);
160+
} catch (error) {
161+
// Handle errors gracefully
162+
const errorMessage: MyMessage = {
163+
id: generateId(),
164+
role: "assistant",
165+
text: `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`,
166+
timestamp: Date.now(),
167+
};
168+
169+
newMessages.push(errorMessage);
170+
await actions.updateMessages(state.currentThreadId, newMessages);
171+
} finally {
172+
setIsRunning(false);
173+
}
174+
};
175+
176+
// Thread list adapter for managing multiple threads
177+
const threadListAdapter: ExternalStoreThreadListAdapter = {
178+
threadId: state.currentThreadId,
179+
threads: state.threadList.filter((t): t is RegularThreadData => t.status === "regular"),
180+
archivedThreads: state.threadList.filter((t): t is ArchivedThreadData => t.status === "archived"),
181+
182+
onSwitchToNewThread: async () => {
183+
const threadId = await actions.createThread("New Chat");
184+
actions.setCurrentThread(threadId);
185+
},
186+
187+
onSwitchToThread: (threadId) => {
188+
actions.setCurrentThread(threadId);
189+
},
190+
191+
onRename: async (threadId, newTitle) => {
192+
await actions.updateThread(threadId, { title: newTitle });
193+
},
194+
195+
onArchive: async (threadId) => {
196+
await actions.updateThread(threadId, { status: "archived" });
197+
},
198+
199+
onDelete: async (threadId) => {
200+
await actions.deleteThread(threadId);
201+
},
202+
};
203+
204+
const runtime = useExternalStoreRuntime({
205+
messages: currentMessages,
206+
setMessages: (messages) => {
207+
actions.updateMessages(state.currentThreadId, messages);
208+
},
209+
convertMessage,
210+
isRunning,
211+
onNew,
212+
onEdit,
213+
adapters: {
214+
threadList: threadListAdapter,
215+
},
216+
});
217+
218+
if (!state.isInitialized) {
219+
return <div>Loading...</div>;
220+
}
221+
222+
return (
223+
<AssistantRuntimeProvider runtime={runtime}>
224+
<ChatContainer>
225+
<ThreadList />
226+
<Thread />
227+
</ChatContainer>
228+
</AssistantRuntimeProvider>
229+
);
230+
}
231+

0 commit comments

Comments
 (0)