Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /