diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
index 57ac9040a..20cfcb00f 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
@@ -4,11 +4,13 @@ import { UICompBuilder } from "comps/generators";
import { NameConfig, withExposingConfigs } from "comps/generators/withExposing";
import { StringControl } from "comps/controls/codeControl";
import { arrayObjectExposingStateControl, stringExposingStateControl } from "comps/controls/codeStateControl";
+import { JSONObject } from "util/jsonTypes";
import { withDefault } from "comps/generators";
import { BoolControl } from "comps/controls/boolControl";
import { dropdownControl } from "comps/controls/dropdownControl";
import QuerySelectControl from "comps/controls/querySelectControl";
import { eventHandlerControl, EventConfigType } from "comps/controls/eventHandlerControl";
+import { AutoHeightControl } from "comps/controls/autoHeightControl";
import { ChatCore } from "./components/ChatCore";
import { ChatPropertyView } from "./chatPropertyView";
import { createChatStorage } from "./utils/storageFactory";
@@ -17,6 +19,17 @@ import { useMemo, useRef, useEffect } from "react";
import { changeChildAction } from "lowcoder-core";
import { ChatMessage } from "./types/chatTypes";
import { trans } from "i18n";
+import { styleControl } from "comps/controls/styleControl";
+import {
+ ChatStyle,
+ ChatSidebarStyle,
+ ChatMessagesStyle,
+ ChatInputStyle,
+ ChatSendButtonStyle,
+ ChatNewThreadButtonStyle,
+ ChatThreadItemStyle,
+} from "comps/controls/styleControlConstants";
+import { AnimationStyle } from "comps/controls/styleControlConstants";
import "@assistant-ui/styles/index.css";
import "@assistant-ui/styles/markdown.css";
@@ -147,15 +160,31 @@ export const chatChildrenMap = {
// UI Configuration
placeholder: withDefault(StringControl, trans("chat.defaultPlaceholder")),
+ // Layout Configuration
+ autoHeight: AutoHeightControl,
+ leftPanelWidth: withDefault(StringControl, "250px"),
+
// Database Information (read-only)
databaseName: withDefault(StringControl, ""),
// Event Handlers
onEvent: ChatEventHandlerControl,
+ // Style Controls
+ style: styleControl(ChatStyle),
+ sidebarStyle: styleControl(ChatSidebarStyle),
+ messagesStyle: styleControl(ChatMessagesStyle),
+ inputStyle: styleControl(ChatInputStyle),
+ sendButtonStyle: styleControl(ChatSendButtonStyle),
+ newThreadButtonStyle: styleControl(ChatNewThreadButtonStyle),
+ threadItemStyle: styleControl(ChatThreadItemStyle),
+ animationStyle: styleControl(AnimationStyle),
+
// Exposed Variables (not shown in Property View)
currentMessage: stringExposingStateControl("currentMessage", ""),
- conversationHistory: stringExposingStateControl("conversationHistory", "[]"),
+ // Use arrayObjectExposingStateControl for proper Lowcoder pattern
+ // This exposes: conversationHistory.value, setConversationHistory(), clearConversationHistory(), resetConversationHistory()
+ conversationHistory: arrayObjectExposingStateControl("conversationHistory", [] as JSONObject[]),
};
// ============================================================================
@@ -221,30 +250,32 @@ const ChatTmpComp = new UICompBuilder(
]);
// Handle message updates for exposed variable
+ // Using Lowcoder pattern: props.currentMessage.onChange() instead of dispatch(changeChildAction(...))
const handleMessageUpdate = (message: string) => {
- dispatch(changeChildAction("currentMessage", message, false));
+ props.currentMessage.onChange(message);
// Trigger messageSent event
props.onEvent("messageSent");
};
// Handle conversation history updates for exposed variable
- // Handle conversation history updates for exposed variable
-const handleConversationUpdate = (conversationHistory: any[]) => {
- // Use utility function to create complete history with system prompt
- const historyWithSystemPrompt = addSystemPromptToHistory(
- conversationHistory,
- props.systemPrompt
- );
-
- // Expose the complete history (with system prompt) for use in queries
- dispatch(changeChildAction("conversationHistory", JSON.stringify(historyWithSystemPrompt), false));
-
- // Trigger messageReceived event when bot responds
- const lastMessage = conversationHistory[conversationHistory.length - 1];
- if (lastMessage && lastMessage.role === 'assistant') {
- props.onEvent("messageReceived");
- }
-};
+ // Using Lowcoder pattern: props.conversationHistory.onChange() instead of dispatch(changeChildAction(...))
+ const handleConversationUpdate = (messages: ChatMessage[]) => {
+ // Use utility function to create complete history with system prompt
+ const historyWithSystemPrompt = addSystemPromptToHistory(
+ messages,
+ props.systemPrompt
+ );
+
+ // Update using proper Lowcoder pattern - calling onChange on the control
+ // This properly updates the exposed variable and triggers reactivity
+ props.conversationHistory.onChange(historyWithSystemPrompt as JSONObject[]);
+
+ // Trigger messageReceived event when bot responds
+ const lastMessage = messages[messages.length - 1];
+ if (lastMessage && lastMessage.role === 'assistant') {
+ props.onEvent("messageReceived");
+ }
+ };
// Cleanup on unmount
useEffect(() => {
@@ -261,9 +292,19 @@ const handleConversationUpdate = (conversationHistory: any[]) => {
storage={storage}
messageHandler={messageHandler}
placeholder={props.placeholder}
+ autoHeight={props.autoHeight}
+ sidebarWidth={props.leftPanelWidth}
onMessageUpdate={handleMessageUpdate}
onConversationUpdate={handleConversationUpdate}
onEvent={props.onEvent}
+ style={props.style}
+ sidebarStyle={props.sidebarStyle}
+ messagesStyle={props.messagesStyle}
+ inputStyle={props.inputStyle}
+ sendButtonStyle={props.sendButtonStyle}
+ newThreadButtonStyle={props.newThreadButtonStyle}
+ threadItemStyle={props.threadItemStyle}
+ animationStyle={props.animationStyle}
/>
);
}
@@ -271,12 +312,20 @@ const handleConversationUpdate = (conversationHistory: any[]) => {
.setPropertyViewFn((children) => )
.build();
+// Override autoHeight to support AUTO/FIXED height mode
+const ChatCompWithAutoHeight = class extends ChatTmpComp {
+ override autoHeight(): boolean {
+ return this.children.autoHeight.getView();
+ }
+};
+
// ============================================================================
// EXPORT WITH EXPOSED VARIABLES
// ============================================================================
-export const ChatComp = withExposingConfigs(ChatTmpComp, [
+export const ChatComp = withExposingConfigs(ChatCompWithAutoHeight, [
new NameConfig("currentMessage", "Current user message"),
- new NameConfig("conversationHistory", "Full conversation history as JSON array (includes system prompt for API calls)"),
+ // conversationHistory is now a proper array (not JSON string) - supports setConversationHistory(), clearConversationHistory(), resetConversationHistory()
+ new NameConfig("conversationHistory", "Full conversation history array with system prompt (use directly in API calls, no JSON.parse needed)"),
new NameConfig("databaseName", "Database name for SQL queries (ChatDB_)"),
]);
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx
index 0e2fd0290..1e396ebcb 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx
@@ -2,8 +2,8 @@
import React, { useMemo } from "react";
import { Section, sectionNames, DocLink } from "lowcoder-design";
-import { placeholderPropertyView } from "../../utils/propertyUtils";
import { trans } from "i18n";
+import { hiddenPropertyView } from "comps/utils/propertyUtils";
// ============================================================================
// CLEAN PROPERTY VIEW - FOCUSED ON ESSENTIAL CONFIGURATION
@@ -55,7 +55,7 @@ export const ChatPropertyView = React.memo((props: any) => {
tooltip: trans("chat.systemPromptTooltip"),
})}
- {children.streaming.propertyView({
+ {children.streaming.propertyView({
label: trans("chat.streaming"),
tooltip: trans("chat.streamingTooltip"),
})}
@@ -63,11 +63,20 @@ export const ChatPropertyView = React.memo((props: any) => {
{/* UI Configuration */}
- {children.placeholder.propertyView({
- label: trans("chat.placeholderLabel"),
- placeholder: trans("chat.defaultPlaceholder"),
- tooltip: trans("chat.placeholderTooltip"),
- })}
+ {children.placeholder.propertyView({
+ label: trans("chat.placeholderLabel"),
+ placeholder: trans("chat.defaultPlaceholder"),
+ tooltip: trans("chat.placeholderTooltip"),
+ })}
+
+
+ {/* Layout Section - Height Mode & Sidebar Width */}
+
+ {children.autoHeight.getPropertyView()}
+ {children.leftPanelWidth.propertyView({
+ label: trans("chat.leftPanelWidth"),
+ tooltip: trans("chat.leftPanelWidthTooltip"),
+ })}
{/* Database Section */}
@@ -84,6 +93,39 @@ export const ChatPropertyView = React.memo((props: any) => {
{children.onEvent.getPropertyView()}
+ {/* STYLE SECTIONS */}
+
+ {children.style.getPropertyView()}
+
+
+
+ {children.sidebarStyle.getPropertyView()}
+
+
+
+ {children.messagesStyle.getPropertyView()}
+
+
+
+ {children.inputStyle.getPropertyView()}
+
+
+
+ {children.sendButtonStyle.getPropertyView()}
+
+
+
+ {children.newThreadButtonStyle.getPropertyView()}
+
+
+
+ {children.threadItemStyle.getPropertyView()}
+
+
+
+ {children.animationStyle.getPropertyView()}
+
+
>
), [children]);
});
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx
index ad0d33e2c..0a50ccae9 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx
@@ -14,19 +14,39 @@ export function ChatCore({
storage,
messageHandler,
placeholder,
+ autoHeight,
+ sidebarWidth,
onMessageUpdate,
onConversationUpdate,
- onEvent
+ onEvent,
+ style,
+ sidebarStyle,
+ messagesStyle,
+ inputStyle,
+ sendButtonStyle,
+ newThreadButtonStyle,
+ threadItemStyle,
+ animationStyle
}: ChatCoreProps) {
return (
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx
index d5b0ce187..ec0f1b534 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx
@@ -27,47 +27,131 @@ import { universalAttachmentAdapter } from "../utils/attachmentAdapter";
// STYLED COMPONENTS (same as your current ChatMain)
// ============================================================================
-const ChatContainer = styled.div`
+const ChatContainer = styled.div<{
+ $autoHeight?: boolean;
+ $sidebarWidth?: string;
+ $style?: any;
+ $sidebarStyle?: any;
+ $messagesStyle?: any;
+ $inputStyle?: any;
+ $sendButtonStyle?: any;
+ $newThreadButtonStyle?: any;
+ $threadItemStyle?: any;
+ $animationStyle?: any;
+}>`
display: flex;
- height: 500px;
+ height: ${(props) => (props.$autoHeight ? "auto" : "100%")};
+ min-height: ${(props) => (props.$autoHeight ? "300px" : "unset")};
+
+ /* Main container styles */
+ background: ${(props) => props.$style?.background || "transparent"};
+ margin: ${(props) => props.$style?.margin || "0"};
+ padding: ${(props) => props.$style?.padding || "0"};
+ border: ${(props) => props.$style?.borderWidth || "0"} ${(props) => props.$style?.borderStyle || "solid"} ${(props) => props.$style?.border || "transparent"};
+ border-radius: ${(props) => props.$style?.radius || "0"};
+
+ /* Animation styles */
+ animation: ${(props) => props.$animationStyle?.animation || "none"};
+ animation-duration: ${(props) => props.$animationStyle?.animationDuration || "0s"};
+ animation-delay: ${(props) => props.$animationStyle?.animationDelay || "0s"};
+ animation-iteration-count: ${(props) => props.$animationStyle?.animationIterationCount || "1"};
p {
margin: 0;
}
+ /* Sidebar Styles */
.aui-thread-list-root {
- width: 250px;
- background-color: #fff;
+ width: ${(props) => props.$sidebarWidth || "250px"};
+ background-color: ${(props) => props.$sidebarStyle?.sidebarBackground || "#fff"};
padding: 10px;
}
+ .aui-thread-list-item-title {
+ color: ${(props) => props.$sidebarStyle?.threadText || "inherit"};
+ }
+
+ /* Messages Window Styles */
.aui-thread-root {
flex: 1;
- background-color: #f9fafb;
+ background-color: ${(props) => props.$messagesStyle?.messagesBackground || "#f9fafb"};
+ height: auto;
+ }
+
+ /* User Message Styles */
+ .aui-user-message-content {
+ background-color: ${(props) => props.$messagesStyle?.userMessageBackground || "#3b82f6"};
+ color: ${(props) => props.$messagesStyle?.userMessageText || "#ffffff"};
+ }
+
+ /* Assistant Message Styles */
+ .aui-assistant-message-content {
+ background-color: ${(props) => props.$messagesStyle?.assistantMessageBackground || "#ffffff"};
+ color: ${(props) => props.$messagesStyle?.assistantMessageText || "inherit"};
+ }
+
+ /* Input Field Styles */
+ .aui-composer-input {
+ background-color: ${(props) => props.$inputStyle?.inputBackground || "#ffffff"};
+ color: ${(props) => props.$inputStyle?.inputText || "inherit"};
+ border-color: ${(props) => props.$inputStyle?.inputBorder || "#d1d5db"};
+ }
+
+ /* Send Button Styles */
+ .aui-composer-send {
+ background-color: ${(props) => props.$sendButtonStyle?.sendButtonBackground || "#3b82f6"} !important;
+
+ svg {
+ color: ${(props) => props.$sendButtonStyle?.sendButtonIcon || "#ffffff"};
+ }
+ }
+
+ /* New Thread Button Styles */
+ .aui-thread-list-root > button {
+ background-color: ${(props) => props.$newThreadButtonStyle?.newThreadBackground || "#3b82f6"} !important;
+ color: ${(props) => props.$newThreadButtonStyle?.newThreadText || "#ffffff"} !important;
+ border-color: ${(props) => props.$newThreadButtonStyle?.newThreadBackground || "#3b82f6"} !important;
}
+ /* Thread item styling */
.aui-thread-list-item {
cursor: pointer;
transition: background-color 0.2s ease;
+ background-color: ${(props) => props.$threadItemStyle?.threadItemBackground || "transparent"};
+ color: ${(props) => props.$threadItemStyle?.threadItemText || "inherit"};
+ border: 1px solid ${(props) => props.$threadItemStyle?.threadItemBorder || "transparent"};
&[data-active="true"] {
- background-color: #dbeafe;
- border: 1px solid #bfdbfe;
+ background-color: ${(props) => props.$threadItemStyle?.activeThreadBackground || "#dbeafe"};
+ color: ${(props) => props.$threadItemStyle?.activeThreadText || "inherit"};
+ border: 1px solid ${(props) => props.$threadItemStyle?.activeThreadBorder || "#bfdbfe"};
}
}
`;
// ============================================================================
-// CHAT CORE MAIN - CLEAN PROPS, FOCUSED RESPONSIBILITY
+// CHAT CORE MAIN - FOR MAIN COMPONENT WITH FULL STYLING SUPPORT
+// (Bottom panel uses ChatPanelCore instead - see ChatPanelCore.tsx)
// ============================================================================
interface ChatCoreMainProps {
messageHandler: MessageHandler;
placeholder?: string;
+ autoHeight?: boolean;
+ sidebarWidth?: string;
onMessageUpdate?: (message: string) => void;
onConversationUpdate?: (conversationHistory: ChatMessage[]) => void;
// STANDARD LOWCODER EVENT PATTERN - SINGLE CALLBACK (OPTIONAL)
onEvent?: (eventName: string) => void;
+ // Style controls
+ style?: any;
+ sidebarStyle?: any;
+ messagesStyle?: any;
+ inputStyle?: any;
+ sendButtonStyle?: any;
+ newThreadButtonStyle?: any;
+ threadItemStyle?: any;
+ animationStyle?: any;
}
const generateId = () => Math.random().toString(36).substr(2, 9);
@@ -75,9 +159,19 @@ const generateId = () => Math.random().toString(36).substr(2, 9);
export function ChatCoreMain({
messageHandler,
placeholder,
+ autoHeight,
+ sidebarWidth,
onMessageUpdate,
onConversationUpdate,
- onEvent
+ onEvent,
+ style,
+ sidebarStyle,
+ messagesStyle,
+ inputStyle,
+ sendButtonStyle,
+ newThreadButtonStyle,
+ threadItemStyle,
+ animationStyle
}: ChatCoreMainProps) {
const { state, actions } = useChatContext();
const [isRunning, setIsRunning] = useState(false);
@@ -305,7 +399,18 @@ export function ChatCoreMain({
return (
-
+
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx
index 1c9af4f55..a0586b67a 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx
@@ -1,17 +1,19 @@
// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx
import { useMemo } from "react";
-import { ChatCore } from "./ChatCore";
+import { ChatProvider } from "./context/ChatContext";
+import { ChatPanelCore } from "./ChatPanelCore";
import { createChatStorage } from "../utils/storageFactory";
import { N8NHandler } from "../handlers/messageHandlers";
import { ChatPanelProps } from "../types/chatTypes";
import { trans } from "i18n";
+import { TooltipProvider } from "@radix-ui/react-tooltip";
import "@assistant-ui/styles/index.css";
import "@assistant-ui/styles/markdown.css";
// ============================================================================
-// CHAT PANEL - CLEAN BOTTOM PANEL COMPONENT
+// CHAT PANEL - SIMPLIFIED BOTTOM PANEL COMPONENT (NO STYLING CONTROLS)
// ============================================================================
export function ChatPanel({
@@ -38,10 +40,13 @@ export function ChatPanel({
);
return (
-
+
+
+
+
+
);
}
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelCore.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelCore.tsx
new file mode 100644
index 000000000..f0978e56b
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelCore.tsx
@@ -0,0 +1,308 @@
+// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelCore.tsx
+
+import React, { useState, useEffect } from "react";
+import {
+ useExternalStoreRuntime,
+ ThreadMessageLike,
+ AppendMessage,
+ AssistantRuntimeProvider,
+ ExternalStoreThreadListAdapter,
+ CompleteAttachment,
+ TextContentPart,
+ ThreadUserContentPart
+} from "@assistant-ui/react";
+import { Thread } from "./assistant-ui/thread";
+import { ThreadList } from "./assistant-ui/thread-list";
+import {
+ useChatContext,
+ RegularThreadData,
+ ArchivedThreadData
+} from "./context/ChatContext";
+import { MessageHandler, ChatMessage } from "../types/chatTypes";
+import styled from "styled-components";
+import { trans } from "i18n";
+import { universalAttachmentAdapter } from "../utils/attachmentAdapter";
+
+// ============================================================================
+// SIMPLE STYLED COMPONENTS - FIXED STYLING FOR BOTTOM PANEL
+// ============================================================================
+
+const ChatContainer = styled.div<{
+ $autoHeight?: boolean;
+ $sidebarWidth?: string;
+}>`
+ display: flex;
+ height: ${(props) => (props.$autoHeight ? "auto" : "100%")};
+ min-height: ${(props) => (props.$autoHeight ? "300px" : "unset")};
+
+ p {
+ margin: 0;
+ }
+
+ .aui-thread-list-root {
+ width: ${(props) => props.$sidebarWidth || "250px"};
+ background-color: #fff;
+ padding: 10px;
+ }
+
+ .aui-thread-root {
+ flex: 1;
+ background-color: #f9fafb;
+ height: auto;
+ }
+
+ .aui-thread-list-item {
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &[data-active="true"] {
+ background-color: #dbeafe;
+ border: 1px solid #bfdbfe;
+ }
+ }
+`;
+
+// ============================================================================
+// CHAT PANEL CORE - SIMPLIFIED FOR BOTTOM PANEL (NO STYLING PROPS)
+// ============================================================================
+
+interface ChatPanelCoreProps {
+ messageHandler: MessageHandler;
+ placeholder?: string;
+ autoHeight?: boolean;
+ sidebarWidth?: string;
+ onMessageUpdate?: (message: string) => void;
+ onConversationUpdate?: (conversationHistory: ChatMessage[]) => void;
+ onEvent?: (eventName: string) => void;
+}
+
+const generateId = () => Math.random().toString(36).substr(2, 9);
+
+export function ChatPanelCore({
+ messageHandler,
+ placeholder,
+ autoHeight,
+ sidebarWidth,
+ onMessageUpdate,
+ onConversationUpdate,
+ onEvent
+}: ChatPanelCoreProps) {
+ const { state, actions } = useChatContext();
+ const [isRunning, setIsRunning] = useState(false);
+
+ // Get messages for current thread
+ const currentMessages = actions.getCurrentMessages();
+
+ // Notify parent component of conversation changes
+ useEffect(() => {
+ if (currentMessages.length > 0 && !isRunning) {
+ onConversationUpdate?.(currentMessages);
+ }
+ }, [currentMessages, isRunning]);
+
+ // Trigger component load event on mount
+ useEffect(() => {
+ onEvent?.("componentLoad");
+ }, [onEvent]);
+
+ // Convert custom format to ThreadMessageLike
+ const convertMessage = (message: ChatMessage): ThreadMessageLike => {
+ const content: ThreadUserContentPart[] = [{ type: "text", text: message.text }];
+
+ if (message.attachments && message.attachments.length > 0) {
+ for (const attachment of message.attachments) {
+ if (attachment.content) {
+ content.push(...attachment.content);
+ }
+ }
+ }
+
+ return {
+ role: message.role,
+ content,
+ id: message.id,
+ createdAt: new Date(message.timestamp),
+ ...(message.attachments && message.attachments.length > 0 && { attachments: message.attachments }),
+ };
+ };
+
+ // Handle new message
+ const onNew = async (message: AppendMessage) => {
+ const textPart = (message.content as ThreadUserContentPart[]).find(
+ (part): part is TextContentPart => part.type === "text"
+ );
+
+ const text = textPart?.text?.trim() ?? "";
+
+ const completeAttachments = (message.attachments ?? []).filter(
+ (att): att is CompleteAttachment => att.status.type === "complete"
+ );
+
+ const hasText = text.length > 0;
+ const hasAttachments = completeAttachments.length > 0;
+
+ if (!hasText && !hasAttachments) {
+ throw new Error("Cannot send an empty message");
+ }
+
+ const userMessage: ChatMessage = {
+ id: generateId(),
+ role: "user",
+ text,
+ timestamp: Date.now(),
+ attachments: completeAttachments,
+ };
+
+ await actions.addMessage(state.currentThreadId, userMessage);
+ setIsRunning(true);
+
+ try {
+ const response = await messageHandler.sendMessage(userMessage);
+
+ onMessageUpdate?.(userMessage.text);
+
+ const assistantMessage: ChatMessage = {
+ id: generateId(),
+ role: "assistant",
+ text: response.content,
+ timestamp: Date.now(),
+ };
+
+ await actions.addMessage(state.currentThreadId, assistantMessage);
+ } catch (error) {
+ const errorMessage: ChatMessage = {
+ id: generateId(),
+ role: "assistant",
+ text: trans("chat.errorUnknown"),
+ timestamp: Date.now(),
+ };
+
+ await actions.addMessage(state.currentThreadId, errorMessage);
+ } finally {
+ setIsRunning(false);
+ }
+ };
+
+ // Handle edit message
+ const onEdit = async (message: AppendMessage) => {
+ const textPart = (message.content as ThreadUserContentPart[]).find(
+ (part): part is TextContentPart => part.type === "text"
+ );
+
+ const text = textPart?.text?.trim() ?? "";
+
+ const completeAttachments = (message.attachments ?? []).filter(
+ (att): att is CompleteAttachment => att.status.type === "complete"
+ );
+
+ const hasText = text.length > 0;
+ const hasAttachments = completeAttachments.length > 0;
+
+ if (!hasText && !hasAttachments) {
+ throw new Error("Cannot send an empty message");
+ }
+
+ const index = currentMessages.findIndex((m) => m.id === message.parentId) + 1;
+ const newMessages = [...currentMessages.slice(0, index)];
+
+ const editedMessage: ChatMessage = {
+ id: generateId(),
+ role: "user",
+ text,
+ timestamp: Date.now(),
+ attachments: completeAttachments,
+ };
+
+ newMessages.push(editedMessage);
+ await actions.updateMessages(state.currentThreadId, newMessages);
+ setIsRunning(true);
+
+ try {
+ const response = await messageHandler.sendMessage(editedMessage);
+
+ onMessageUpdate?.(editedMessage.text);
+
+ const assistantMessage: ChatMessage = {
+ id: generateId(),
+ role: "assistant",
+ text: response.content,
+ timestamp: Date.now(),
+ };
+
+ newMessages.push(assistantMessage);
+ await actions.updateMessages(state.currentThreadId, newMessages);
+ } catch (error) {
+ const errorMessage: ChatMessage = {
+ id: generateId(),
+ role: "assistant",
+ text: trans("chat.errorUnknown"),
+ timestamp: Date.now(),
+ };
+
+ newMessages.push(errorMessage);
+ await actions.updateMessages(state.currentThreadId, newMessages);
+ } finally {
+ setIsRunning(false);
+ }
+ };
+
+ // Thread list adapter
+ const threadListAdapter: ExternalStoreThreadListAdapter = {
+ threadId: state.currentThreadId,
+ threads: state.threadList.filter((t): t is RegularThreadData => t.status === "regular"),
+ archivedThreads: state.threadList.filter((t): t is ArchivedThreadData => t.status === "archived"),
+
+ onSwitchToNewThread: async () => {
+ const threadId = await actions.createThread(trans("chat.newChatTitle"));
+ actions.setCurrentThread(threadId);
+ onEvent?.("threadCreated");
+ },
+
+ onSwitchToThread: (threadId) => {
+ actions.setCurrentThread(threadId);
+ },
+
+ onRename: async (threadId, newTitle) => {
+ await actions.updateThread(threadId, { title: newTitle });
+ onEvent?.("threadUpdated");
+ },
+
+ onArchive: async (threadId) => {
+ await actions.updateThread(threadId, { status: "archived" });
+ onEvent?.("threadUpdated");
+ },
+
+ onDelete: async (threadId) => {
+ await actions.deleteThread(threadId);
+ onEvent?.("threadDeleted");
+ },
+ };
+
+ const runtime = useExternalStoreRuntime({
+ messages: currentMessages,
+ setMessages: (messages) => {
+ actions.updateMessages(state.currentThreadId, messages);
+ },
+ convertMessage,
+ isRunning,
+ onNew,
+ onEdit,
+ adapters: {
+ threadList: threadListAdapter,
+ attachments: universalAttachmentAdapter,
+ },
+ });
+
+ if (!state.isInitialized) {
+ return Loading...
;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts
index 5e757f231..919094ba7 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts
@@ -67,24 +67,37 @@ export interface ChatMessage {
systemPrompt?: string;
}
- // ============================================================================
- // COMPONENT PROPS (what each component actually needs)
- // ============================================================================
-
- export interface ChatCoreProps {
- storage: ChatStorage;
- messageHandler: MessageHandler;
- placeholder?: string;
- onMessageUpdate?: (message: string) => void;
- onConversationUpdate?: (conversationHistory: ChatMessage[]) => void;
- // STANDARD LOWCODER EVENT PATTERN - SINGLE CALLBACK
- onEvent?: (eventName: string) => void;
- }
-
- export interface ChatPanelProps {
- tableName: string;
- modelHost: string;
- systemPrompt?: string;
- streaming?: boolean;
- onMessageUpdate?: (message: string) => void;
- }
+// ============================================================================
+// COMPONENT PROPS (what each component actually needs)
+// ============================================================================
+
+// Main Chat Component Props (with full styling support)
+export interface ChatCoreProps {
+ storage: ChatStorage;
+ messageHandler: MessageHandler;
+ placeholder?: string;
+ autoHeight?: boolean;
+ sidebarWidth?: string;
+ onMessageUpdate?: (message: string) => void;
+ onConversationUpdate?: (conversationHistory: ChatMessage[]) => void;
+ // STANDARD LOWCODER EVENT PATTERN - SINGLE CALLBACK
+ onEvent?: (eventName: string) => void;
+ // Style controls (only for main component)
+ style?: any;
+ sidebarStyle?: any;
+ messagesStyle?: any;
+ inputStyle?: any;
+ sendButtonStyle?: any;
+ newThreadButtonStyle?: any;
+ threadItemStyle?: any;
+ animationStyle?: any;
+}
+
+// Bottom Panel Props (simplified, no styling controls)
+export interface ChatPanelProps {
+ tableName: string;
+ modelHost: string;
+ systemPrompt?: string;
+ streaming?: boolean;
+ onMessageUpdate?: (message: string) => void;
+}
diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
index 176afbbfc..e09e2b1fc 100644
--- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
+++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
@@ -2372,6 +2372,156 @@ export const RichTextEditorStyle = [
BORDER_WIDTH,
] as const;
+// Chat Component Styles
+export const ChatStyle = [
+ getBackground(),
+ MARGIN,
+ PADDING,
+ BORDER,
+ BORDER_STYLE,
+ RADIUS,
+ BORDER_WIDTH,
+] as const;
+
+export const ChatSidebarStyle = [
+ {
+ name: "sidebarBackground",
+ label: trans("style.sidebarBackground"),
+ depTheme: "primarySurface",
+ depType: DEP_TYPE.SELF,
+ transformer: toSelf,
+ },
+ {
+ name: "threadText",
+ label: trans("style.threadText"),
+ depName: "sidebarBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+] as const;
+
+export const ChatMessagesStyle = [
+ {
+ name: "messagesBackground",
+ label: trans("style.messagesBackground"),
+ color: "#f9fafb",
+ },
+ {
+ name: "userMessageBackground",
+ label: trans("style.userMessageBackground"),
+ depTheme: "primary",
+ depType: DEP_TYPE.SELF,
+ transformer: toSelf,
+ },
+ {
+ name: "userMessageText",
+ label: trans("style.userMessageText"),
+ depName: "userMessageBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+ {
+ name: "assistantMessageBackground",
+ label: trans("style.assistantMessageBackground"),
+ color: "#ffffff",
+ },
+ {
+ name: "assistantMessageText",
+ label: trans("style.assistantMessageText"),
+ depName: "assistantMessageBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+] as const;
+
+export const ChatInputStyle = [
+ {
+ name: "inputBackground",
+ label: trans("style.inputBackground"),
+ color: "#ffffff",
+ },
+ {
+ name: "inputText",
+ label: trans("style.inputText"),
+ depName: "inputBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+ {
+ name: "inputBorder",
+ label: trans("style.inputBorder"),
+ depName: "inputBackground",
+ transformer: backgroundToBorder,
+ },
+] as const;
+
+export const ChatSendButtonStyle = [
+ {
+ name: "sendButtonBackground",
+ label: trans("style.sendButtonBackground"),
+ depTheme: "primary",
+ depType: DEP_TYPE.SELF,
+ transformer: toSelf,
+ },
+ {
+ name: "sendButtonIcon",
+ label: trans("style.sendButtonIcon"),
+ depName: "sendButtonBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+] as const;
+
+export const ChatNewThreadButtonStyle = [
+ {
+ name: "newThreadBackground",
+ label: trans("style.newThreadBackground"),
+ depTheme: "primary",
+ depType: DEP_TYPE.SELF,
+ transformer: toSelf,
+ },
+ {
+ name: "newThreadText",
+ label: trans("style.newThreadText"),
+ depName: "newThreadBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+] as const;
+
+export const ChatThreadItemStyle = [
+ {
+ name: "threadItemBackground",
+ label: trans("style.threadItemBackground"),
+ color: "transparent",
+ },
+ {
+ name: "threadItemText",
+ label: trans("style.threadItemText"),
+ color: "inherit",
+ },
+ {
+ name: "threadItemBorder",
+ label: trans("style.threadItemBorder"),
+ color: "transparent",
+ },
+ {
+ name: "activeThreadBackground",
+ label: trans("style.activeThreadBackground"),
+ color: "#dbeafe",
+ },
+ {
+ name: "activeThreadText",
+ label: trans("style.activeThreadText"),
+ color: "inherit",
+ },
+ {
+ name: "activeThreadBorder",
+ label: trans("style.activeThreadBorder"),
+ color: "#bfdbfe",
+ },
+] as const;
+
export type QRCodeStyleType = StyleConfigType;
export type TimeLineStyleType = StyleConfigType;
export type AvatarStyleType = StyleConfigType;
@@ -2490,6 +2640,14 @@ export type NavLayoutItemActiveStyleType = StyleConfigType<
typeof NavLayoutItemActiveStyle
>;
+export type ChatStyleType = StyleConfigType;
+export type ChatSidebarStyleType = StyleConfigType;
+export type ChatMessagesStyleType = StyleConfigType;
+export type ChatInputStyleType = StyleConfigType;
+export type ChatSendButtonStyleType = StyleConfigType;
+export type ChatNewThreadButtonStyleType = StyleConfigType;
+export type ChatThreadItemStyleType = StyleConfigType;
+
export function widthCalculator(margin: string) {
const marginArr = margin?.trim().replace(/\s+/g, " ").split(" ") || "";
if (marginArr.length === 1) {
diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts
index 56fa433e2..9c3accda6 100644
--- a/client/packages/lowcoder/src/i18n/locales/en.ts
+++ b/client/packages/lowcoder/src/i18n/locales/en.ts
@@ -600,6 +600,28 @@ export const en = {
"detailSize": "Detail Size",
"hideColumn": "Hide Column",
+ // Chat Component Styles
+ "sidebarBackground": "Sidebar Background",
+ "threadText": "Thread Text Color",
+ "messagesBackground": "Messages Background",
+ "userMessageBackground": "User Message Background",
+ "userMessageText": "User Message Text",
+ "assistantMessageBackground": "Assistant Message Background",
+ "assistantMessageText": "Assistant Message Text",
+ "inputBackground": "Input Background",
+ "inputText": "Input Text Color",
+ "inputBorder": "Input Border",
+ "sendButtonBackground": "Send Button Background",
+ "sendButtonIcon": "Send Button Icon Color",
+ "newThreadBackground": "New Thread Button Background",
+ "newThreadText": "New Thread Button Text",
+ "threadItemBackground": "Thread Item Background",
+ "threadItemText": "Thread Item Text",
+ "threadItemBorder": "Thread Item Border",
+ "activeThreadBackground": "Active Thread Background",
+ "activeThreadText": "Active Thread Text",
+ "activeThreadBorder": "Active Thread Border",
+
"radiusTip": "Specifies the radius of the element's corners. Example: 5px, 50%, or 1em.",
"gapTip": "Specifies the gap between rows and columns in a grid or flex container. Example: 10px, 1rem, or 5%.",
"cardRadiusTip": "Defines the corner radius for card components. Example: 10px, 15px.",
@@ -1477,10 +1499,22 @@ export const en = {
"threadDeleted": "Thread Deleted",
"threadDeletedDesc": "Triggered when a thread is deleted - Delete thread from backend",
+ // Layout
+ "leftPanelWidth": "Sidebar Width",
+ "leftPanelWidthTooltip": "Width of the thread list sidebar (e.g., 250px, 30%)",
+
// Exposed Variables (for documentation)
"currentMessage": "Current user message",
"conversationHistory": "Full conversation history as JSON array",
- "databaseNameExposed": "Database name for SQL queries (ChatDB_)"
+ "databaseNameExposed": "Database name for SQL queries (ChatDB_)",
+
+ // Style Section Names
+ "sidebarStyle": "Sidebar Style",
+ "messagesStyle": "Messages Style",
+ "inputStyle": "Input Field Style",
+ "sendButtonStyle": "Send Button Style",
+ "newThreadButtonStyle": "New Thread Button Style",
+ "threadItemStyle": "Thread Item Style"
},
"chatBox": {