Skip to content

Commit 5875702

Browse files
committed
seperate chat panel component
1 parent b4620e5 commit 5875702

4 files changed

Lines changed: 337 additions & 21 deletions

File tree

client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ const ChatContainer = styled.div<{
125125
`;
126126

127127
// ============================================================================
128-
// CHAT CORE MAIN - CLEAN PROPS, FOCUSED RESPONSIBILITY
128+
// CHAT CORE MAIN - FOR MAIN COMPONENT WITH FULL STYLING SUPPORT
129+
// (Bottom panel uses ChatPanelCore instead - see ChatPanelCore.tsx)
129130
// ============================================================================
130131

131132
interface ChatCoreMainProps {

client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx
22

33
import { useMemo } from "react";
4-
import { ChatCore } from "./ChatCore";
4+
import { ChatProvider } from "./context/ChatContext";
5+
import { ChatPanelCore } from "./ChatPanelCore";
56
import { createChatStorage } from "../utils/storageFactory";
67
import { N8NHandler } from "../handlers/messageHandlers";
78
import { ChatPanelProps } from "../types/chatTypes";
89
import { trans } from "i18n";
10+
import { TooltipProvider } from "@radix-ui/react-tooltip";
911

1012
import "@assistant-ui/styles/index.css";
1113
import "@assistant-ui/styles/markdown.css";
1214

1315
// ============================================================================
14-
// CHAT PANEL - CLEAN BOTTOM PANEL COMPONENT
16+
// CHAT PANEL - SIMPLIFIED BOTTOM PANEL COMPONENT (NO STYLING CONTROLS)
1517
// ============================================================================
1618

1719
export function ChatPanel({
@@ -38,10 +40,13 @@ export function ChatPanel({
3840
);
3941

4042
return (
41-
<ChatCore
42-
storage={storage}
43-
messageHandler={messageHandler}
44-
onMessageUpdate={onMessageUpdate}
45-
/>
43+
<TooltipProvider>
44+
<ChatProvider storage={storage}>
45+
<ChatPanelCore
46+
messageHandler={messageHandler}
47+
onMessageUpdate={onMessageUpdate}
48+
/>
49+
</ChatProvider>
50+
</TooltipProvider>
4651
);
4752
}
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelCore.tsx
2+
3+
import React, { useState, useEffect } from "react";
4+
import {
5+
useExternalStoreRuntime,
6+
ThreadMessageLike,
7+
AppendMessage,
8+
AssistantRuntimeProvider,
9+
ExternalStoreThreadListAdapter,
10+
CompleteAttachment,
11+
TextContentPart,
12+
ThreadUserContentPart
13+
} from "@assistant-ui/react";
14+
import { Thread } from "./assistant-ui/thread";
15+
import { ThreadList } from "./assistant-ui/thread-list";
16+
import {
17+
useChatContext,
18+
RegularThreadData,
19+
ArchivedThreadData
20+
} from "./context/ChatContext";
21+
import { MessageHandler, ChatMessage } from "../types/chatTypes";
22+
import styled from "styled-components";
23+
import { trans } from "i18n";
24+
import { universalAttachmentAdapter } from "../utils/attachmentAdapter";
25+
26+
// ============================================================================
27+
// SIMPLE STYLED COMPONENTS - FIXED STYLING FOR BOTTOM PANEL
28+
// ============================================================================
29+
30+
const ChatContainer = styled.div<{
31+
$autoHeight?: boolean;
32+
$sidebarWidth?: string;
33+
}>`
34+
display: flex;
35+
height: ${(props) => (props.$autoHeight ? "auto" : "100%")};
36+
min-height: ${(props) => (props.$autoHeight ? "300px" : "unset")};
37+
38+
p {
39+
margin: 0;
40+
}
41+
42+
.aui-thread-list-root {
43+
width: ${(props) => props.$sidebarWidth || "250px"};
44+
background-color: #fff;
45+
padding: 10px;
46+
}
47+
48+
.aui-thread-root {
49+
flex: 1;
50+
background-color: #f9fafb;
51+
height: auto;
52+
}
53+
54+
.aui-thread-list-item {
55+
cursor: pointer;
56+
transition: background-color 0.2s ease;
57+
58+
&[data-active="true"] {
59+
background-color: #dbeafe;
60+
border: 1px solid #bfdbfe;
61+
}
62+
}
63+
`;
64+
65+
// ============================================================================
66+
// CHAT PANEL CORE - SIMPLIFIED FOR BOTTOM PANEL (NO STYLING PROPS)
67+
// ============================================================================
68+
69+
interface ChatPanelCoreProps {
70+
messageHandler: MessageHandler;
71+
placeholder?: string;
72+
autoHeight?: boolean;
73+
sidebarWidth?: string;
74+
onMessageUpdate?: (message: string) => void;
75+
onConversationUpdate?: (conversationHistory: ChatMessage[]) => void;
76+
onEvent?: (eventName: string) => void;
77+
}
78+
79+
const generateId = () => Math.random().toString(36).substr(2, 9);
80+
81+
export function ChatPanelCore({
82+
messageHandler,
83+
placeholder,
84+
autoHeight,
85+
sidebarWidth,
86+
onMessageUpdate,
87+
onConversationUpdate,
88+
onEvent
89+
}: ChatPanelCoreProps) {
90+
const { state, actions } = useChatContext();
91+
const [isRunning, setIsRunning] = useState(false);
92+
93+
// Get messages for current thread
94+
const currentMessages = actions.getCurrentMessages();
95+
96+
// Notify parent component of conversation changes
97+
useEffect(() => {
98+
if (currentMessages.length > 0 && !isRunning) {
99+
onConversationUpdate?.(currentMessages);
100+
}
101+
}, [currentMessages, isRunning]);
102+
103+
// Trigger component load event on mount
104+
useEffect(() => {
105+
onEvent?.("componentLoad");
106+
}, [onEvent]);
107+
108+
// Convert custom format to ThreadMessageLike
109+
const convertMessage = (message: ChatMessage): ThreadMessageLike => {
110+
const content: ThreadUserContentPart[] = [{ type: "text", text: message.text }];
111+
112+
if (message.attachments && message.attachments.length > 0) {
113+
for (const attachment of message.attachments) {
114+
if (attachment.content) {
115+
content.push(...attachment.content);
116+
}
117+
}
118+
}
119+
120+
return {
121+
role: message.role,
122+
content,
123+
id: message.id,
124+
createdAt: new Date(message.timestamp),
125+
...(message.attachments && message.attachments.length > 0 && { attachments: message.attachments }),
126+
};
127+
};
128+
129+
// Handle new message
130+
const onNew = async (message: AppendMessage) => {
131+
const textPart = (message.content as ThreadUserContentPart[]).find(
132+
(part): part is TextContentPart => part.type === "text"
133+
);
134+
135+
const text = textPart?.text?.trim() ?? "";
136+
137+
const completeAttachments = (message.attachments ?? []).filter(
138+
(att): att is CompleteAttachment => att.status.type === "complete"
139+
);
140+
141+
const hasText = text.length > 0;
142+
const hasAttachments = completeAttachments.length > 0;
143+
144+
if (!hasText && !hasAttachments) {
145+
throw new Error("Cannot send an empty message");
146+
}
147+
148+
const userMessage: ChatMessage = {
149+
id: generateId(),
150+
role: "user",
151+
text,
152+
timestamp: Date.now(),
153+
attachments: completeAttachments,
154+
};
155+
156+
await actions.addMessage(state.currentThreadId, userMessage);
157+
setIsRunning(true);
158+
159+
try {
160+
const response = await messageHandler.sendMessage(userMessage);
161+
162+
onMessageUpdate?.(userMessage.text);
163+
164+
const assistantMessage: ChatMessage = {
165+
id: generateId(),
166+
role: "assistant",
167+
text: response.content,
168+
timestamp: Date.now(),
169+
};
170+
171+
await actions.addMessage(state.currentThreadId, assistantMessage);
172+
} catch (error) {
173+
const errorMessage: ChatMessage = {
174+
id: generateId(),
175+
role: "assistant",
176+
text: trans("chat.errorUnknown"),
177+
timestamp: Date.now(),
178+
};
179+
180+
await actions.addMessage(state.currentThreadId, errorMessage);
181+
} finally {
182+
setIsRunning(false);
183+
}
184+
};
185+
186+
// Handle edit message
187+
const onEdit = async (message: AppendMessage) => {
188+
const textPart = (message.content as ThreadUserContentPart[]).find(
189+
(part): part is TextContentPart => part.type === "text"
190+
);
191+
192+
const text = textPart?.text?.trim() ?? "";
193+
194+
const completeAttachments = (message.attachments ?? []).filter(
195+
(att): att is CompleteAttachment => att.status.type === "complete"
196+
);
197+
198+
const hasText = text.length > 0;
199+
const hasAttachments = completeAttachments.length > 0;
200+
201+
if (!hasText && !hasAttachments) {
202+
throw new Error("Cannot send an empty message");
203+
}
204+
205+
const index = currentMessages.findIndex((m) => m.id === message.parentId) + 1;
206+
const newMessages = [...currentMessages.slice(0, index)];
207+
208+
const editedMessage: ChatMessage = {
209+
id: generateId(),
210+
role: "user",
211+
text,
212+
timestamp: Date.now(),
213+
attachments: completeAttachments,
214+
};
215+
216+
newMessages.push(editedMessage);
217+
await actions.updateMessages(state.currentThreadId, newMessages);
218+
setIsRunning(true);
219+
220+
try {
221+
const response = await messageHandler.sendMessage(editedMessage);
222+
223+
onMessageUpdate?.(editedMessage.text);
224+
225+
const assistantMessage: ChatMessage = {
226+
id: generateId(),
227+
role: "assistant",
228+
text: response.content,
229+
timestamp: Date.now(),
230+
};
231+
232+
newMessages.push(assistantMessage);
233+
await actions.updateMessages(state.currentThreadId, newMessages);
234+
} catch (error) {
235+
const errorMessage: ChatMessage = {
236+
id: generateId(),
237+
role: "assistant",
238+
text: trans("chat.errorUnknown"),
239+
timestamp: Date.now(),
240+
};
241+
242+
newMessages.push(errorMessage);
243+
await actions.updateMessages(state.currentThreadId, newMessages);
244+
} finally {
245+
setIsRunning(false);
246+
}
247+
};
248+
249+
// Thread list adapter
250+
const threadListAdapter: ExternalStoreThreadListAdapter = {
251+
threadId: state.currentThreadId,
252+
threads: state.threadList.filter((t): t is RegularThreadData => t.status === "regular"),
253+
archivedThreads: state.threadList.filter((t): t is ArchivedThreadData => t.status === "archived"),
254+
255+
onSwitchToNewThread: async () => {
256+
const threadId = await actions.createThread(trans("chat.newChatTitle"));
257+
actions.setCurrentThread(threadId);
258+
onEvent?.("threadCreated");
259+
},
260+
261+
onSwitchToThread: (threadId) => {
262+
actions.setCurrentThread(threadId);
263+
},
264+
265+
onRename: async (threadId, newTitle) => {
266+
await actions.updateThread(threadId, { title: newTitle });
267+
onEvent?.("threadUpdated");
268+
},
269+
270+
onArchive: async (threadId) => {
271+
await actions.updateThread(threadId, { status: "archived" });
272+
onEvent?.("threadUpdated");
273+
},
274+
275+
onDelete: async (threadId) => {
276+
await actions.deleteThread(threadId);
277+
onEvent?.("threadDeleted");
278+
},
279+
};
280+
281+
const runtime = useExternalStoreRuntime({
282+
messages: currentMessages,
283+
setMessages: (messages) => {
284+
actions.updateMessages(state.currentThreadId, messages);
285+
},
286+
convertMessage,
287+
isRunning,
288+
onNew,
289+
onEdit,
290+
adapters: {
291+
threadList: threadListAdapter,
292+
attachments: universalAttachmentAdapter,
293+
},
294+
});
295+
296+
if (!state.isInitialized) {
297+
return <div>Loading...</div>;
298+
}
299+
300+
return (
301+
<AssistantRuntimeProvider runtime={runtime}>
302+
<ChatContainer $autoHeight={autoHeight} $sidebarWidth={sidebarWidth}>
303+
<ThreadList />
304+
<Thread placeholder={placeholder} />
305+
</ChatContainer>
306+
</AssistantRuntimeProvider>
307+
);
308+
}

0 commit comments

Comments
 (0)