From e0a18e0b15667ae56bfe6f999b48331848ef7062 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 10 Feb 2026 19:18:55 +0500 Subject: [PATCH 01/12] fix conversation history --- .../src/comps/comps/chatComp/chatComp.tsx | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index 57ac9040a..ca10208c3 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx @@ -4,6 +4,7 @@ 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"; @@ -155,7 +156,9 @@ export const chatChildrenMap = { // 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 +224,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(() => { @@ -277,6 +282,7 @@ const handleConversationUpdate = (conversationHistory: any[]) => { export const ChatComp = withExposingConfigs(ChatTmpComp, [ 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 From 74f17878c58a5ec6fe31caf79937b7778b0337a4 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 10 Feb 2026 22:57:45 +0500 Subject: [PATCH 02/12] fix height messages window + add height mode (auto/fixed) for chat component --- .../src/comps/comps/chatComp/chatComp.tsx | 16 +++++++++++++- .../comps/comps/chatComp/chatPropertyView.tsx | 22 +++++++++++++------ .../comps/chatComp/components/ChatCore.tsx | 10 ++++++--- .../chatComp/components/ChatCoreMain.tsx | 17 ++++++++++---- .../comps/comps/chatComp/types/chatTypes.ts | 2 ++ .../packages/lowcoder/src/i18n/locales/en.ts | 4 ++++ 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index ca10208c3..58510de2f 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx @@ -10,6 +10,7 @@ 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"; @@ -148,6 +149,10 @@ 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, ""), @@ -266,6 +271,8 @@ const ChatTmpComp = new UICompBuilder( storage={storage} messageHandler={messageHandler} placeholder={props.placeholder} + autoHeight={props.autoHeight} + sidebarWidth={props.leftPanelWidth} onMessageUpdate={handleMessageUpdate} onConversationUpdate={handleConversationUpdate} onEvent={props.onEvent} @@ -276,11 +283,18 @@ const ChatTmpComp = new UICompBuilder( .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"), // 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)"), diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx index 0e2fd0290..d1589500f 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx @@ -2,7 +2,6 @@ import React, { useMemo } from "react"; import { Section, sectionNames, DocLink } from "lowcoder-design"; -import { placeholderPropertyView } from "../../utils/propertyUtils"; import { trans } from "i18n"; // ============================================================================ @@ -55,7 +54,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 +62,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 */} 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..5059dc3db 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx @@ -14,6 +14,8 @@ export function ChatCore({ storage, messageHandler, placeholder, + autoHeight, + sidebarWidth, onMessageUpdate, onConversationUpdate, onEvent @@ -23,9 +25,11 @@ export function ChatCore({ 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..c4bd77dbc 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx @@ -27,16 +27,20 @@ import { universalAttachmentAdapter } from "../utils/attachmentAdapter"; // STYLED COMPONENTS (same as your current ChatMain) // ============================================================================ -const ChatContainer = styled.div` +const ChatContainer = styled.div<{ + $autoHeight?: boolean; + $sidebarWidth?: string; +}>` display: flex; - height: 500px; + height: ${(props) => (props.$autoHeight ? "auto" : "100%")}; + min-height: ${(props) => (props.$autoHeight ? "300px" : "unset")}; p { margin: 0; } .aui-thread-list-root { - width: 250px; + width: ${(props) => props.$sidebarWidth || "250px"}; background-color: #fff; padding: 10px; } @@ -44,6 +48,7 @@ const ChatContainer = styled.div` .aui-thread-root { flex: 1; background-color: #f9fafb; + height: auto; } .aui-thread-list-item { @@ -64,6 +69,8 @@ const ChatContainer = styled.div` 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) @@ -75,6 +82,8 @@ const generateId = () => Math.random().toString(36).substr(2, 9); export function ChatCoreMain({ messageHandler, placeholder, + autoHeight, + sidebarWidth, onMessageUpdate, onConversationUpdate, onEvent @@ -305,7 +314,7 @@ export function ChatCoreMain({ 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..23bf16df5 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -75,6 +75,8 @@ export interface ChatMessage { storage: ChatStorage; messageHandler: MessageHandler; placeholder?: string; + autoHeight?: boolean; + sidebarWidth?: string; onMessageUpdate?: (message: string) => void; onConversationUpdate?: (conversationHistory: ChatMessage[]) => void; // STANDARD LOWCODER EVENT PATTERN - SINGLE CALLBACK diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 56fa433e2..ce330e7b9 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -1477,6 +1477,10 @@ 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", From b4620e5a7a31238b2167914e0584b93a85c65f3f Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 11 Feb 2026 22:46:12 +0500 Subject: [PATCH 03/12] add style customization --- .../src/comps/comps/chatComp/chatComp.tsx | 26 ++++ .../comps/comps/chatComp/chatPropertyView.tsx | 30 +++++ .../comps/chatComp/components/ChatCore.tsx | 16 ++- .../chatComp/components/ChatCoreMain.tsx | 95 +++++++++++++- .../comps/comps/chatComp/types/chatTypes.ts | 30 +++-- .../comps/controls/styleControlConstants.tsx | 124 ++++++++++++++++++ .../packages/lowcoder/src/i18n/locales/en.ts | 25 +++- 7 files changed, 329 insertions(+), 17 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index 58510de2f..e011582c3 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx @@ -19,6 +19,16 @@ 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, +} from "comps/controls/styleControlConstants"; +import { AnimationStyle } from "comps/controls/styleControlConstants"; import "@assistant-ui/styles/index.css"; import "@assistant-ui/styles/markdown.css"; @@ -159,6 +169,15 @@ export const chatChildrenMap = { // Event Handlers onEvent: ChatEventHandlerControl, + // Style Controls + style: styleControl(ChatStyle), + sidebarStyle: styleControl(ChatSidebarStyle), + messagesStyle: styleControl(ChatMessagesStyle), + inputStyle: styleControl(ChatInputStyle), + sendButtonStyle: styleControl(ChatSendButtonStyle), + newThreadButtonStyle: styleControl(ChatNewThreadButtonStyle), + animationStyle: styleControl(AnimationStyle), + // Exposed Variables (not shown in Property View) currentMessage: stringExposingStateControl("currentMessage", ""), // Use arrayObjectExposingStateControl for proper Lowcoder pattern @@ -276,6 +295,13 @@ const ChatTmpComp = new UICompBuilder( 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} + animationStyle={props.animationStyle} /> ); } diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx index d1589500f..1cda06b3d 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx @@ -3,6 +3,7 @@ import React, { useMemo } from "react"; import { Section, sectionNames, DocLink } from "lowcoder-design"; import { trans } from "i18n"; +import { hiddenPropertyView } from "comps/utils/propertyUtils"; // ============================================================================ // CLEAN PROPERTY VIEW - FOCUSED ON ESSENTIAL CONFIGURATION @@ -92,6 +93,35 @@ 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.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 5059dc3db..ff8e7b009 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx @@ -18,7 +18,14 @@ export function ChatCore({ sidebarWidth, onMessageUpdate, onConversationUpdate, - onEvent + onEvent, + style, + sidebarStyle, + messagesStyle, + inputStyle, + sendButtonStyle, + newThreadButtonStyle, + animationStyle }: ChatCoreProps) { return ( @@ -31,6 +38,13 @@ export function ChatCore({ onMessageUpdate={onMessageUpdate} onConversationUpdate={onConversationUpdate} onEvent={onEvent} + style={style} + sidebarStyle={sidebarStyle} + messagesStyle={messagesStyle} + inputStyle={inputStyle} + sendButtonStyle={sendButtonStyle} + newThreadButtonStyle={newThreadButtonStyle} + animationStyle={animationStyle} /> 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 c4bd77dbc..ab6ff50a5 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx @@ -30,27 +30,89 @@ import { universalAttachmentAdapter } from "../utils/attachmentAdapter"; const ChatContainer = styled.div<{ $autoHeight?: boolean; $sidebarWidth?: string; + $style?: any; + $sidebarStyle?: any; + $messagesStyle?: any; + $inputStyle?: any; + $sendButtonStyle?: any; + $newThreadButtonStyle?: any; + $animationStyle?: any; }>` display: flex; 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: ${(props) => props.$sidebarWidth || "250px"}; - background-color: #fff; + 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[type="button"]:first-child { + 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; @@ -75,6 +137,14 @@ interface ChatCoreMainProps { 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; + animationStyle?: any; } const generateId = () => Math.random().toString(36).substr(2, 9); @@ -86,7 +156,14 @@ export function ChatCoreMain({ sidebarWidth, onMessageUpdate, onConversationUpdate, - onEvent + onEvent, + style, + sidebarStyle, + messagesStyle, + inputStyle, + sendButtonStyle, + newThreadButtonStyle, + animationStyle }: ChatCoreMainProps) { const { state, actions } = useChatContext(); const [isRunning, setIsRunning] = useState(false); @@ -314,7 +391,17 @@ export function ChatCoreMain({ 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 23bf16df5..472d13c59 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -71,17 +71,25 @@ export interface ChatMessage { // COMPONENT PROPS (what each component actually needs) // ============================================================================ - 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; - } +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 + style?: any; + sidebarStyle?: any; + messagesStyle?: any; + inputStyle?: any; + sendButtonStyle?: any; + newThreadButtonStyle?: any; + animationStyle?: any; +} export interface ChatPanelProps { tableName: string; diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 176afbbfc..09aa476a0 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -2372,6 +2372,123 @@ 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 type QRCodeStyleType = StyleConfigType; export type TimeLineStyleType = StyleConfigType; export type AvatarStyleType = StyleConfigType; @@ -2490,6 +2607,13 @@ 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 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 ce330e7b9..9a1ecbae2 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -600,6 +600,22 @@ 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", + "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.", @@ -1484,7 +1500,14 @@ export const en = { // 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" }, "chatBox": { From 5875702f611c2d444747cff3a6b9ad1da00f9cb7 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 12 Feb 2026 19:39:18 +0500 Subject: [PATCH 04/12] seperate chat panel component --- .../chatComp/components/ChatCoreMain.tsx | 3 +- .../comps/chatComp/components/ChatPanel.tsx | 19 +- .../chatComp/components/ChatPanelCore.tsx | 308 ++++++++++++++++++ .../comps/comps/chatComp/types/chatTypes.ts | 28 +- 4 files changed, 337 insertions(+), 21 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelCore.tsx 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 ab6ff50a5..db510780a 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx @@ -125,7 +125,8 @@ const ChatContainer = styled.div<{ `; // ============================================================================ -// 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 { 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 472d13c59..0d2bd7f9c 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -67,10 +67,11 @@ export interface ChatMessage { systemPrompt?: string; } - // ============================================================================ - // COMPONENT PROPS (what each component actually needs) - // ============================================================================ - +// ============================================================================ +// COMPONENT PROPS (what each component actually needs) +// ============================================================================ + +// Main Chat Component Props (with full styling support) export interface ChatCoreProps { storage: ChatStorage; messageHandler: MessageHandler; @@ -81,7 +82,7 @@ export interface ChatCoreProps { onConversationUpdate?: (conversationHistory: ChatMessage[]) => void; // STANDARD LOWCODER EVENT PATTERN - SINGLE CALLBACK onEvent?: (eventName: string) => void; - // Style controls + // Style controls (only for main component) style?: any; sidebarStyle?: any; messagesStyle?: any; @@ -90,11 +91,12 @@ export interface ChatCoreProps { newThreadButtonStyle?: any; animationStyle?: any; } - - export interface ChatPanelProps { - tableName: string; - modelHost: string; - systemPrompt?: string; - streaming?: boolean; - onMessageUpdate?: (message: string) => void; - } + +// Bottom Panel Props (simplified, no styling controls) +export interface ChatPanelProps { + tableName: string; + modelHost: string; + systemPrompt?: string; + streaming?: boolean; + onMessageUpdate?: (message: string) => void; +} From 11bb84456ca89cf8c15e6f478a5acb4aa5139295 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Thu, 12 Feb 2026 23:53:32 +0500 Subject: [PATCH 05/12] complete chat styles --- .../src/comps/comps/chatComp/chatComp.tsx | 3 ++ .../comps/comps/chatComp/chatPropertyView.tsx | 4 +++ .../comps/chatComp/components/ChatCore.tsx | 2 ++ .../chatComp/components/ChatCoreMain.tsx | 14 ++++++-- .../comps/comps/chatComp/types/chatTypes.ts | 1 + .../comps/controls/styleControlConstants.tsx | 34 +++++++++++++++++++ .../packages/lowcoder/src/i18n/locales/en.ts | 9 ++++- 7 files changed, 63 insertions(+), 4 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index e011582c3..20cfcb00f 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx @@ -27,6 +27,7 @@ import { ChatInputStyle, ChatSendButtonStyle, ChatNewThreadButtonStyle, + ChatThreadItemStyle, } from "comps/controls/styleControlConstants"; import { AnimationStyle } from "comps/controls/styleControlConstants"; @@ -176,6 +177,7 @@ export const chatChildrenMap = { inputStyle: styleControl(ChatInputStyle), sendButtonStyle: styleControl(ChatSendButtonStyle), newThreadButtonStyle: styleControl(ChatNewThreadButtonStyle), + threadItemStyle: styleControl(ChatThreadItemStyle), animationStyle: styleControl(AnimationStyle), // Exposed Variables (not shown in Property View) @@ -301,6 +303,7 @@ const ChatTmpComp = new UICompBuilder( inputStyle={props.inputStyle} sendButtonStyle={props.sendButtonStyle} newThreadButtonStyle={props.newThreadButtonStyle} + threadItemStyle={props.threadItemStyle} animationStyle={props.animationStyle} /> ); diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx index 1cda06b3d..1e396ebcb 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx @@ -118,6 +118,10 @@ export const ChatPropertyView = React.memo((props: any) => { {children.newThreadButtonStyle.getPropertyView()} +
+ {children.threadItemStyle.getPropertyView()} +
+
{children.animationStyle.getPropertyView()}
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 ff8e7b009..0a50ccae9 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx @@ -25,6 +25,7 @@ export function ChatCore({ inputStyle, sendButtonStyle, newThreadButtonStyle, + threadItemStyle, animationStyle }: ChatCoreProps) { return ( @@ -44,6 +45,7 @@ export function ChatCore({ inputStyle={inputStyle} sendButtonStyle={sendButtonStyle} newThreadButtonStyle={newThreadButtonStyle} + threadItemStyle={threadItemStyle} animationStyle={animationStyle} /> 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 db510780a..ec0f1b534 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx @@ -36,6 +36,7 @@ const ChatContainer = styled.div<{ $inputStyle?: any; $sendButtonStyle?: any; $newThreadButtonStyle?: any; + $threadItemStyle?: any; $animationStyle?: any; }>` display: flex; @@ -106,7 +107,7 @@ const ChatContainer = styled.div<{ } /* New Thread Button Styles */ - .aui-thread-list-root button[type="button"]:first-child { + .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; @@ -116,10 +117,14 @@ const ChatContainer = styled.div<{ .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"}; } } `; @@ -145,6 +150,7 @@ interface ChatCoreMainProps { inputStyle?: any; sendButtonStyle?: any; newThreadButtonStyle?: any; + threadItemStyle?: any; animationStyle?: any; } @@ -164,6 +170,7 @@ export function ChatCoreMain({ inputStyle, sendButtonStyle, newThreadButtonStyle, + threadItemStyle, animationStyle }: ChatCoreMainProps) { const { state, actions } = useChatContext(); @@ -401,6 +408,7 @@ export function ChatCoreMain({ $inputStyle={inputStyle} $sendButtonStyle={sendButtonStyle} $newThreadButtonStyle={newThreadButtonStyle} + $threadItemStyle={threadItemStyle} $animationStyle={animationStyle} > 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 0d2bd7f9c..919094ba7 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -89,6 +89,7 @@ export interface ChatCoreProps { inputStyle?: any; sendButtonStyle?: any; newThreadButtonStyle?: any; + threadItemStyle?: any; animationStyle?: any; } diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 09aa476a0..e09e2b1fc 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -2489,6 +2489,39 @@ export const ChatNewThreadButtonStyle = [ }, ] 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; @@ -2613,6 +2646,7 @@ 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(" ") || ""; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 9a1ecbae2..9c3accda6 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -615,6 +615,12 @@ export const en = { "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%.", @@ -1507,7 +1513,8 @@ export const en = { "messagesStyle": "Messages Style", "inputStyle": "Input Field Style", "sendButtonStyle": "Send Button Style", - "newThreadButtonStyle": "New Thread Button Style" + "newThreadButtonStyle": "New Thread Button Style", + "threadItemStyle": "Thread Item Style" }, "chatBox": { From a12a9b9eb7f3b0efc758ca3ccc866cf30bc86577 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Sat, 14 Feb 2026 01:43:08 +0500 Subject: [PATCH 06/12] refactor ai chat & chat panel component --- .../src/comps/comps/chatComp/chatComp.tsx | 39 +- .../chatComp/components/ChatContainer.tsx | 340 ++++++++++++++ .../comps/chatComp/components/ChatCore.tsx | 54 --- .../chatComp/components/ChatCoreMain.tsx | 420 ------------------ .../comps/chatComp/components/ChatPanel.tsx | 21 +- ...atPanelCore.tsx => ChatPanelContainer.tsx} | 138 +++--- 6 files changed, 424 insertions(+), 588 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx delete mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx delete mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx rename client/packages/lowcoder/src/comps/comps/chatComp/components/{ChatPanelCore.tsx => ChatPanelContainer.tsx} (71%) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index 20cfcb00f..dc1cfa1fc 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx @@ -11,7 +11,7 @@ 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 { ChatContainer } from "./components/ChatContainer"; import { ChatPropertyView } from "./chatPropertyView"; import { createChatStorage } from "./utils/storageFactory"; import { QueryHandler, createMessageHandler } from "./handlers/messageHandlers"; @@ -170,15 +170,17 @@ export const chatChildrenMap = { // Event Handlers onEvent: ChatEventHandlerControl, - // Style Controls - style: styleControl(ChatStyle), - sidebarStyle: styleControl(ChatSidebarStyle), - messagesStyle: styleControl(ChatMessagesStyle), - inputStyle: styleControl(ChatInputStyle), + // Style Controls - Consolidated to reduce prop count + style: styleControl(ChatStyle), // Main container + sidebarStyle: styleControl(ChatSidebarStyle), // Sidebar (includes threads & new button) + messagesStyle: styleControl(ChatMessagesStyle), // Messages area + inputStyle: styleControl(ChatInputStyle), // Input + send button area + animationStyle: styleControl(AnimationStyle), // Animations + + // Legacy style props (kept for backward compatibility, consolidated internally) sendButtonStyle: styleControl(ChatSendButtonStyle), newThreadButtonStyle: styleControl(ChatNewThreadButtonStyle), threadItemStyle: styleControl(ChatThreadItemStyle), - animationStyle: styleControl(AnimationStyle), // Exposed Variables (not shown in Property View) currentMessage: stringExposingStateControl("currentMessage", ""), @@ -287,8 +289,20 @@ const ChatTmpComp = new UICompBuilder( }; }, []); + // Group all styles into single object for cleaner prop passing + const styles = { + style: props.style, + sidebarStyle: props.sidebarStyle, + messagesStyle: props.messagesStyle, + inputStyle: props.inputStyle, + sendButtonStyle: props.sendButtonStyle, + newThreadButtonStyle: props.newThreadButtonStyle, + threadItemStyle: props.threadItemStyle, + animationStyle: props.animationStyle, + }; + return ( - ); } diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx new file mode 100644 index 000000000..dc4c35b2d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx @@ -0,0 +1,340 @@ +// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.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 { + ChatProvider, + useChatContext, + RegularThreadData, + ArchivedThreadData +} from "./context/ChatContext"; +import { MessageHandler, ChatMessage, ChatCoreProps } from "../types/chatTypes"; +import styled from "styled-components"; +import { trans } from "i18n"; +import { universalAttachmentAdapter } from "../utils/attachmentAdapter"; +import { TooltipProvider } from "@radix-ui/react-tooltip"; + +// ============================================================================ +// STYLED CONTAINER - FOLLOWING LOWCODER PATTERNS +// Styles passed as objects, applied directly like Button100 and InputStyle +// ============================================================================ + +const StyledChatContainer = styled.div` + display: flex; + 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: ${(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: ${(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: ${(props) => props.threadItemStyle?.activeThreadBackground || "#dbeafe"}; + color: ${(props) => props.threadItemStyle?.activeThreadText || "inherit"}; + border: 1px solid ${(props) => props.threadItemStyle?.activeThreadBorder || "#bfdbfe"}; + } + } +`; + +// ============================================================================ +// CHAT CONTAINER - DIRECT RENDERING LIKE ButtonView +// ============================================================================ + +const generateId = () => Math.random().toString(36).substr(2, 9); + +function ChatContainerView(props: ChatCoreProps) { + const { state, actions } = useChatContext(); + const [isRunning, setIsRunning] = useState(false); + + const currentMessages = actions.getCurrentMessages(); + + useEffect(() => { + if (currentMessages.length > 0 && !isRunning) { + props.onConversationUpdate?.(currentMessages); + } + }, [currentMessages, isRunning, props.onConversationUpdate]); + + useEffect(() => { + props.onEvent?.("componentLoad"); + }, [props.onEvent]); + + 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 }), + }; + }; + + 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" + ); + + if (!text && !completeAttachments.length) { + 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 props.messageHandler.sendMessage(userMessage); + props.onMessageUpdate?.(userMessage.text); + + const assistantMessage: ChatMessage = { + id: generateId(), + role: "assistant", + text: response.content, + timestamp: Date.now(), + }; + + await actions.addMessage(state.currentThreadId, assistantMessage); + } catch (error) { + await actions.addMessage(state.currentThreadId, { + id: generateId(), + role: "assistant", + text: trans("chat.errorUnknown"), + timestamp: Date.now(), + }); + } finally { + setIsRunning(false); + } + }; + + 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" + ); + + if (!text && !completeAttachments.length) { + 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 props.messageHandler.sendMessage(editedMessage); + props.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) { + newMessages.push({ + id: generateId(), + role: "assistant", + text: trans("chat.errorUnknown"), + timestamp: Date.now(), + }); + await actions.updateMessages(state.currentThreadId, newMessages); + } finally { + setIsRunning(false); + } + }; + + 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); + props.onEvent?.("threadCreated"); + }, + + onSwitchToThread: (threadId) => { + actions.setCurrentThread(threadId); + }, + + onRename: async (threadId, newTitle) => { + await actions.updateThread(threadId, { title: newTitle }); + props.onEvent?.("threadUpdated"); + }, + + onArchive: async (threadId) => { + await actions.updateThread(threadId, { status: "archived" }); + props.onEvent?.("threadUpdated"); + }, + + onDelete: async (threadId) => { + await actions.deleteThread(threadId); + props.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 ( + + + + + + + ); +} + +// ============================================================================ +// EXPORT - WITH PROVIDERS (LIKE BUTTON/INPUT PATTERN) +// ============================================================================ + +export function ChatContainer(props: ChatCoreProps) { + return ( + + + + + + ); +} diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx deleted file mode 100644 index 0a50ccae9..000000000 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx - -import React from "react"; -import { ChatProvider } from "./context/ChatContext"; -import { ChatCoreMain } from "./ChatCoreMain"; -import { ChatCoreProps } from "../types/chatTypes"; -import { TooltipProvider } from "@radix-ui/react-tooltip"; - -// ============================================================================ -// CHAT CORE - THE SHARED FOUNDATION -// ============================================================================ - -export function ChatCore({ - storage, - messageHandler, - placeholder, - autoHeight, - sidebarWidth, - onMessageUpdate, - onConversationUpdate, - onEvent, - style, - sidebarStyle, - messagesStyle, - inputStyle, - sendButtonStyle, - newThreadButtonStyle, - threadItemStyle, - animationStyle -}: ChatCoreProps) { - return ( - - - - - - ); -} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx deleted file mode 100644 index ec0f1b534..000000000 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.tsx +++ /dev/null @@ -1,420 +0,0 @@ -// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCoreMain.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"; - -// ============================================================================ -// STYLED COMPONENTS (same as your current ChatMain) -// ============================================================================ - -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: ${(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: ${(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: ${(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: ${(props) => props.$threadItemStyle?.activeThreadBackground || "#dbeafe"}; - color: ${(props) => props.$threadItemStyle?.activeThreadText || "inherit"}; - border: 1px solid ${(props) => props.$threadItemStyle?.activeThreadBorder || "#bfdbfe"}; - } - } -`; - -// ============================================================================ -// 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); - -export function ChatCoreMain({ - messageHandler, - placeholder, - autoHeight, - sidebarWidth, - onMessageUpdate, - onConversationUpdate, - onEvent, - style, - sidebarStyle, - messagesStyle, - inputStyle, - sendButtonStyle, - newThreadButtonStyle, - threadItemStyle, - animationStyle -}: ChatCoreMainProps) { - const { state, actions } = useChatContext(); - const [isRunning, setIsRunning] = useState(false); - - console.log("RENDERING CHAT CORE MAIN"); - - // Get messages for current thread - const currentMessages = actions.getCurrentMessages(); - - // Notify parent component of conversation changes - OPTIMIZED TIMING - useEffect(() => { - // Only update conversationHistory when we have complete conversations - // Skip empty states and intermediate processing states - if (currentMessages.length > 0 && !isRunning) { - onConversationUpdate?.(currentMessages); - } - }, [currentMessages, isRunning]); - - // Trigger component load event on mount - useEffect(() => { - onEvent?.("componentLoad"); - }, [onEvent]); - - // Convert custom format to ThreadMessageLike (same as your current implementation) - const convertMessage = (message: ChatMessage): ThreadMessageLike => { - const content: ThreadUserContentPart[] = [{ type: "text", text: message.text }]; - - // Add attachment content if attachments exist - 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 - MUCH CLEANER with messageHandler - 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); // Send full message object with attachments - - 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 - CLEANER with messageHandler - const onEdit = async (message: AppendMessage) => { - // Extract the first text content part (if any) - const textPart = (message.content as ThreadUserContentPart[]).find( - (part): part is TextContentPart => part.type === "text" - ); - - const text = textPart?.text?.trim() ?? ""; - - // Filter only complete attachments - 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"); - } - - // Find the index of the message being edited - const index = currentMessages.findIndex((m) => m.id === message.parentId) + 1; - - // Build a new messages array: messages up to and including the one being edited - const newMessages = [...currentMessages.slice(0, index)]; - - // Build the edited user message - const editedMessage: ChatMessage = { - id: generateId(), - role: "user", - text, - timestamp: Date.now(), - attachments: completeAttachments, - }; - - newMessages.push(editedMessage); - - // Update state with edited context - await actions.updateMessages(state.currentThreadId, newMessages); - setIsRunning(true); - - try { - const response = await messageHandler.sendMessage(editedMessage); // Send full message object with attachments - - 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 for managing multiple threads (same as your current implementation) - 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/components/ChatPanel.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx index a0586b67a..78f65eb54 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx @@ -1,19 +1,17 @@ // client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx import { useMemo } from "react"; -import { ChatProvider } from "./context/ChatContext"; -import { ChatPanelCore } from "./ChatPanelCore"; +import { ChatPanelContainer } from "./ChatPanelContainer"; 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 - SIMPLIFIED BOTTOM PANEL COMPONENT (NO STYLING CONTROLS) +// CHAT PANEL - SIMPLIFIED BOTTOM PANEL (NO STYLING CONTROLS) // ============================================================================ export function ChatPanel({ @@ -23,13 +21,11 @@ export function ChatPanel({ streaming = true, onMessageUpdate }: ChatPanelProps) { - // Create storage instance const storage = useMemo(() => createChatStorage(tableName), [tableName] ); - // Create N8N message handler const messageHandler = useMemo(() => new N8NHandler({ modelHost, @@ -40,13 +36,10 @@ 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/ChatPanelContainer.tsx similarity index 71% rename from client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelCore.tsx rename to client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx index f0978e56b..612afc32e 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelCore.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx @@ -1,4 +1,4 @@ -// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelCore.tsx +// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanelContainer.tsx import React, { useState, useEffect } from "react"; import { @@ -14,6 +14,7 @@ import { import { Thread } from "./assistant-ui/thread"; import { ThreadList } from "./assistant-ui/thread-list"; import { + ChatProvider, useChatContext, RegularThreadData, ArchivedThreadData @@ -22,25 +23,29 @@ import { MessageHandler, ChatMessage } from "../types/chatTypes"; import styled from "styled-components"; import { trans } from "i18n"; import { universalAttachmentAdapter } from "../utils/attachmentAdapter"; +import { TooltipProvider } from "@radix-ui/react-tooltip"; + +import "@assistant-ui/styles/index.css"; +import "@assistant-ui/styles/markdown.css"; // ============================================================================ -// SIMPLE STYLED COMPONENTS - FIXED STYLING FOR BOTTOM PANEL +// STYLED CONTAINER - SIMPLE FIXED STYLING FOR BOTTOM PANEL // ============================================================================ -const ChatContainer = styled.div<{ - $autoHeight?: boolean; - $sidebarWidth?: string; +const StyledChatContainer = styled.div<{ + autoHeight?: boolean; + sidebarWidth?: string; }>` display: flex; - height: ${(props) => (props.$autoHeight ? "auto" : "100%")}; - min-height: ${(props) => (props.$autoHeight ? "300px" : "unset")}; + 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"}; + width: ${(props) => props.sidebarWidth || "250px"}; background-color: #fff; padding: 10px; } @@ -63,49 +68,24 @@ const ChatContainer = styled.div<{ `; // ============================================================================ -// CHAT PANEL CORE - SIMPLIFIED FOR BOTTOM PANEL (NO STYLING PROPS) +// CHAT PANEL CONTAINER - DIRECT RENDERING // ============================================================================ -interface ChatPanelCoreProps { +const generateId = () => Math.random().toString(36).substr(2, 9); + +export interface ChatPanelContainerProps { + storage: any; 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) { +function ChatPanelView({ messageHandler, placeholder, onMessageUpdate }: Omit) { 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 }]; @@ -126,22 +106,17 @@ export function ChatPanelCore({ }; }; - // 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) { + if (!text && !completeAttachments.length) { throw new Error("Cannot send an empty message"); } @@ -158,95 +133,78 @@ export function ChatPanelCore({ try { const response = await messageHandler.sendMessage(userMessage); - onMessageUpdate?.(userMessage.text); - const assistantMessage: ChatMessage = { + await actions.addMessage(state.currentThreadId, { id: generateId(), role: "assistant", text: response.content, timestamp: Date.now(), - }; - - await actions.addMessage(state.currentThreadId, assistantMessage); + }); } catch (error) { - const errorMessage: ChatMessage = { + await actions.addMessage(state.currentThreadId, { 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) { + if (!text && !completeAttachments.length) { 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 = { + newMessages.push({ 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); + const response = await messageHandler.sendMessage(newMessages[newMessages.length - 1]); + onMessageUpdate?.(text); - onMessageUpdate?.(editedMessage.text); - - const assistantMessage: ChatMessage = { + newMessages.push({ id: generateId(), role: "assistant", text: response.content, timestamp: Date.now(), - }; - - newMessages.push(assistantMessage); + }); await actions.updateMessages(state.currentThreadId, newMessages); } catch (error) { - const errorMessage: ChatMessage = { + newMessages.push({ 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"), @@ -255,7 +213,6 @@ export function ChatPanelCore({ onSwitchToNewThread: async () => { const threadId = await actions.createThread(trans("chat.newChatTitle")); actions.setCurrentThread(threadId); - onEvent?.("threadCreated"); }, onSwitchToThread: (threadId) => { @@ -264,25 +221,20 @@ export function ChatPanelCore({ 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); - }, + setMessages: (messages) => actions.updateMessages(state.currentThreadId, messages), convertMessage, isRunning, onNew, @@ -299,10 +251,28 @@ export function ChatPanelCore({ return ( - + - + ); } + +// ============================================================================ +// EXPORT - WITH PROVIDERS +// ============================================================================ + +export function ChatPanelContainer({ storage, messageHandler, placeholder, onMessageUpdate }: ChatPanelContainerProps) { + return ( + + + + + + ); +} From e3fe29839df3905fc586b60ab9224c6d7942e9c6 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 16 Feb 2026 17:34:49 +0500 Subject: [PATCH 07/12] fix re rendering issue --- .../chatComp/components/ChatContainer.tsx | 20 +++++++++++++------ .../components/context/ChatContext.tsx | 2 ++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx index dc4c35b2d..5e884bb12 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx @@ -1,6 +1,6 @@ // client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useExternalStoreRuntime, ThreadMessageLike, @@ -83,7 +83,7 @@ const StyledChatContainer = styled.div` } /* Input Field Styles */ - .aui-composer-input { + form.aui-composer-root { background-color: ${(props) => props.inputStyle?.inputBackground || "#ffffff"}; color: ${(props) => props.inputStyle?.inputText || "inherit"}; border-color: ${(props) => props.inputStyle?.inputBorder || "#d1d5db"}; @@ -131,17 +131,25 @@ function ChatContainerView(props: ChatCoreProps) { const { state, actions } = useChatContext(); const [isRunning, setIsRunning] = useState(false); + // Store callback props in refs so useEffects don't re-fire + // when Lowcoder's builder creates new function references on each render + const onConversationUpdateRef = useRef(props.onConversationUpdate); + onConversationUpdateRef.current = props.onConversationUpdate; + + const onEventRef = useRef(props.onEvent); + onEventRef.current = props.onEvent; + const currentMessages = actions.getCurrentMessages(); useEffect(() => { if (currentMessages.length > 0 && !isRunning) { - props.onConversationUpdate?.(currentMessages); + onConversationUpdateRef.current?.(currentMessages); } - }, [currentMessages, isRunning, props.onConversationUpdate]); + }, [currentMessages, isRunning]); useEffect(() => { - props.onEvent?.("componentLoad"); - }, [props.onEvent]); + onEventRef.current?.("componentLoad"); + }, []); const convertMessage = (message: ChatMessage): ThreadMessageLike => { const content: ThreadUserContentPart[] = [{ type: "text", text: message.text }]; diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/context/ChatContext.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/context/ChatContext.tsx index 1a31222a9..e733727f3 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/context/ChatContext.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/context/ChatContext.tsx @@ -360,7 +360,9 @@ export function ChatProvider({ children, storage }: { // Auto-initialize on mount useEffect(() => { + console.log("useEffect Inside ChatProvider", state.isInitialized, state.isLoading); if (!state.isInitialized && !state.isLoading) { + console.log("Initializing chat data..."); initialize(); } }, [state.isInitialized, state.isLoading]); From c970bc2a84556de9275002ea2532d93cf9ba1d42 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Mon, 16 Feb 2026 23:30:47 +0500 Subject: [PATCH 08/12] fix button forward ref + ChatProvider level --- .../src/comps/comps/chatComp/chatComp.tsx | 27 +++++++++++-------- .../chatComp/components/ChatContainer.tsx | 16 +++-------- .../comps/chatComp/components/ui/button.tsx | 22 +++++++-------- .../comps/comps/chatComp/types/chatTypes.ts | 1 - 4 files changed, 30 insertions(+), 36 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx index dc1cfa1fc..b923e3aab 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx @@ -12,6 +12,7 @@ import QuerySelectControl from "comps/controls/querySelectControl"; import { eventHandlerControl, EventConfigType } from "comps/controls/eventHandlerControl"; import { AutoHeightControl } from "comps/controls/autoHeightControl"; import { ChatContainer } from "./components/ChatContainer"; +import { ChatProvider } from "./components/context/ChatContext"; import { ChatPropertyView } from "./chatPropertyView"; import { createChatStorage } from "./utils/storageFactory"; import { QueryHandler, createMessageHandler } from "./handlers/messageHandlers"; @@ -19,6 +20,7 @@ import { useMemo, useRef, useEffect } from "react"; import { changeChildAction } from "lowcoder-core"; import { ChatMessage } from "./types/chatTypes"; import { trans } from "i18n"; +import { TooltipProvider } from "@radix-ui/react-tooltip"; import { styleControl } from "comps/controls/styleControl"; import { ChatStyle, @@ -302,17 +304,20 @@ const ChatTmpComp = new UICompBuilder( }; return ( - + + + + + ); } ) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx index 5e884bb12..f6577ff1f 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx @@ -14,7 +14,6 @@ import { import { Thread } from "./assistant-ui/thread"; import { ThreadList } from "./assistant-ui/thread-list"; import { - ChatProvider, useChatContext, RegularThreadData, ArchivedThreadData @@ -23,7 +22,6 @@ import { MessageHandler, ChatMessage, ChatCoreProps } from "../types/chatTypes"; import styled from "styled-components"; import { trans } from "i18n"; import { universalAttachmentAdapter } from "../utils/attachmentAdapter"; -import { TooltipProvider } from "@radix-ui/react-tooltip"; // ============================================================================ // STYLED CONTAINER - FOLLOWING LOWCODER PATTERNS @@ -122,7 +120,7 @@ const StyledChatContainer = styled.div` `; // ============================================================================ -// CHAT CONTAINER - DIRECT RENDERING LIKE ButtonView +// CHAT CONTAINER - USES CONTEXT FROM CHATPROVIDER // ============================================================================ const generateId = () => Math.random().toString(36).substr(2, 9); @@ -334,15 +332,7 @@ function ChatContainerView(props: ChatCoreProps) { } // ============================================================================ -// EXPORT - WITH PROVIDERS (LIKE BUTTON/INPUT PATTERN) +// EXPORT - SIMPLIFIED (PROVIDERS MOVED UP ONE LEVEL) // ============================================================================ -export function ChatContainer(props: ChatCoreProps) { - return ( - - - - - - ); -} +export const ChatContainer = ChatContainerView; diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ui/button.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ui/button.tsx index 4406b74e6..945783c69 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ui/button.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ui/button.tsx @@ -21,25 +21,25 @@ const buttonVariants = cva("aui-button", { }, }); -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean; - }) { +const Button = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + } +>(({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; return ( ); -} +}); + +Button.displayName = "Button"; export { Button, buttonVariants }; \ No newline at end of file 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 919094ba7..b465eda8b 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts @@ -73,7 +73,6 @@ export interface ChatMessage { // Main Chat Component Props (with full styling support) export interface ChatCoreProps { - storage: ChatStorage; messageHandler: MessageHandler; placeholder?: string; autoHeight?: boolean; From 64dddc06da6eab64a9dd9d615c4b08f69236dabf Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 17 Feb 2026 19:47:39 +0500 Subject: [PATCH 09/12] fix styling warninings --- .../chatComp/components/ChatContainer.tsx | 86 ++++++++++++------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx index f6577ff1f..6d6e8e04a 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx @@ -26,12 +26,27 @@ import { universalAttachmentAdapter } from "../utils/attachmentAdapter"; // ============================================================================ // STYLED CONTAINER - FOLLOWING LOWCODER PATTERNS // Styles passed as objects, applied directly like Button100 and InputStyle +// Using $ prefix for transient props (props that shouldn't be passed to DOM) // ============================================================================ -const StyledChatContainer = styled.div` +// Transient props interface - these props are for styling only and won't be passed to DOM +interface StyledChatContainerProps { + $autoHeight?: boolean; + $sidebarWidth?: string; + $sidebarStyle?: any; + $messagesStyle?: any; + $inputStyle?: any; + $sendButtonStyle?: any; + $newThreadButtonStyle?: any; + $threadItemStyle?: any; + $animationStyle?: any; + style?: any; // This is a valid DOM attribute, no $ needed +} + +const StyledChatContainer = styled.div` display: flex; - height: ${(props) => (props.autoHeight ? "auto" : "100%")}; - min-height: ${(props) => (props.autoHeight ? "300px" : "unset")}; + height: ${(props) => (props.$autoHeight ? "auto" : "100%")}; + min-height: ${(props) => (props.$autoHeight ? "300px" : "unset")}; /* Main container styles */ background: ${(props) => props.style?.background || "transparent"}; @@ -41,10 +56,10 @@ const StyledChatContainer = styled.div` 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"}; + 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; @@ -52,69 +67,69 @@ const StyledChatContainer = styled.div` /* Sidebar Styles */ .aui-thread-list-root { - width: ${(props) => props.sidebarWidth || "250px"}; - background-color: ${(props) => props.sidebarStyle?.sidebarBackground || "#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"}; + color: ${(props) => props.$sidebarStyle?.threadText || "inherit"}; } /* Messages Window Styles */ .aui-thread-root { flex: 1; - background-color: ${(props) => props.messagesStyle?.messagesBackground || "#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"}; + 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"}; + background-color: ${(props) => props.$messagesStyle?.assistantMessageBackground || "#ffffff"}; + color: ${(props) => props.$messagesStyle?.assistantMessageText || "inherit"}; } /* Input Field Styles */ form.aui-composer-root { - background-color: ${(props) => props.inputStyle?.inputBackground || "#ffffff"}; - color: ${(props) => props.inputStyle?.inputText || "inherit"}; - border-color: ${(props) => props.inputStyle?.inputBorder || "#d1d5db"}; + 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; + background-color: ${(props) => props.$sendButtonStyle?.sendButtonBackground || "#3b82f6"} !important; svg { - color: ${(props) => props.sendButtonStyle?.sendButtonIcon || "#ffffff"}; + 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; + 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"}; + 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: ${(props) => props.threadItemStyle?.activeThreadBackground || "#dbeafe"}; - color: ${(props) => props.threadItemStyle?.activeThreadText || "inherit"}; - border: 1px solid ${(props) => props.threadItemStyle?.activeThreadBorder || "#bfdbfe"}; + background-color: ${(props) => props.$threadItemStyle?.activeThreadBackground || "#dbeafe"}; + color: ${(props) => props.$threadItemStyle?.activeThreadText || "inherit"}; + border: 1px solid ${(props) => props.$threadItemStyle?.activeThreadBorder || "#bfdbfe"}; } } `; @@ -323,7 +338,18 @@ function ChatContainerView(props: ChatCoreProps) { return ( - + From 5ef44036cb4619608ecdb88d8e14cea93c1f7bac Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Tue, 17 Feb 2026 21:49:08 +0500 Subject: [PATCH 10/12] refactor styles and add storage cleaner for bottom chat panel --- .../chatComp/components/ChatContainer.tsx | 113 +----------------- .../components/ChatContainerStyles.ts | 108 +++++++++++++++++ .../comps/chatComp/components/ChatPanel.tsx | 19 ++- 3 files changed, 122 insertions(+), 118 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx index 6d6e8e04a..af8028b4d 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx @@ -19,120 +19,9 @@ import { ArchivedThreadData } from "./context/ChatContext"; import { MessageHandler, ChatMessage, ChatCoreProps } from "../types/chatTypes"; -import styled from "styled-components"; import { trans } from "i18n"; import { universalAttachmentAdapter } from "../utils/attachmentAdapter"; - -// ============================================================================ -// STYLED CONTAINER - FOLLOWING LOWCODER PATTERNS -// Styles passed as objects, applied directly like Button100 and InputStyle -// Using $ prefix for transient props (props that shouldn't be passed to DOM) -// ============================================================================ - -// Transient props interface - these props are for styling only and won't be passed to DOM -interface StyledChatContainerProps { - $autoHeight?: boolean; - $sidebarWidth?: string; - $sidebarStyle?: any; - $messagesStyle?: any; - $inputStyle?: any; - $sendButtonStyle?: any; - $newThreadButtonStyle?: any; - $threadItemStyle?: any; - $animationStyle?: any; - style?: any; // This is a valid DOM attribute, no $ needed -} - -const StyledChatContainer = styled.div` - display: flex; - 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: ${(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: ${(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 */ - form.aui-composer-root { - 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: ${(props) => props.$threadItemStyle?.activeThreadBackground || "#dbeafe"}; - color: ${(props) => props.$threadItemStyle?.activeThreadText || "inherit"}; - border: 1px solid ${(props) => props.$threadItemStyle?.activeThreadBorder || "#bfdbfe"}; - } - } -`; +import { StyledChatContainer } from "./ChatContainerStyles"; // ============================================================================ // CHAT CONTAINER - USES CONTEXT FROM CHATPROVIDER diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts new file mode 100644 index 000000000..1f2d4580d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainerStyles.ts @@ -0,0 +1,108 @@ +// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.styles.ts + +import styled from "styled-components"; + + +export interface StyledChatContainerProps { + $autoHeight?: boolean; + $sidebarWidth?: string; + $sidebarStyle?: any; + $messagesStyle?: any; + $inputStyle?: any; + $sendButtonStyle?: any; + $newThreadButtonStyle?: any; + $threadItemStyle?: any; + $animationStyle?: any; + style?: any; +} + +export const StyledChatContainer = styled.div` + display: flex; + 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: ${(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: ${(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 */ + form.aui-composer-root { + 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: ${(props) => props.$threadItemStyle?.activeThreadBackground || "#dbeafe"}; + color: ${(props) => props.$threadItemStyle?.activeThreadText || "inherit"}; + border: 1px solid ${(props) => props.$threadItemStyle?.activeThreadBorder || "#bfdbfe"}; + } + } +`; 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 78f65eb54..f4823011e 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx +++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx @@ -1,6 +1,6 @@ // client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx -import { useMemo } from "react"; +import { useMemo, useEffect } from "react"; import { ChatPanelContainer } from "./ChatPanelContainer"; import { createChatStorage } from "../utils/storageFactory"; import { N8NHandler } from "../handlers/messageHandlers"; @@ -21,20 +21,27 @@ export function ChatPanel({ streaming = true, onMessageUpdate }: ChatPanelProps) { - const storage = useMemo(() => - createChatStorage(tableName), + const storage = useMemo(() => + createChatStorage(tableName), [tableName] ); - - const messageHandler = useMemo(() => + + const messageHandler = useMemo(() => new N8NHandler({ modelHost, systemPrompt, streaming - }), + }), [modelHost, systemPrompt, streaming] ); + // Cleanup on unmount - delete chat data from storage + useEffect(() => { + return () => { + storage.cleanup(); + }; + }, [storage]); + return ( Date: Wed, 18 Feb 2026 18:08:17 +0500 Subject: [PATCH 11/12] fix attachment file adaptor for chat component --- .../comps/chatComp/utils/attachmentAdapter.ts | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts index a0f7c78e0..a70afc734 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts @@ -6,24 +6,16 @@ import type { ThreadUserContentPart } from "@assistant-ui/react"; + const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB + export const universalAttachmentAdapter: AttachmentAdapter = { accept: "*/*", async add({ file }): Promise { - const MAX_SIZE = 10 * 1024 * 1024; - - if (file.size > MAX_SIZE) { - return { - id: crypto.randomUUID(), - type: getAttachmentType(file.type), - name: file.name, - file, - contentType: file.type, - status: { - type: "incomplete", - reason: "error" - } - }; + if (file.size > MAX_FILE_SIZE) { + throw new Error( + `File "${file.name}" exceeds the 10 MB size limit (${(file.size / 1024 / 1024).toFixed(1)} MB).` + ); } return { @@ -33,33 +25,40 @@ import type { file, contentType: file.type, status: { - type: "running", - reason: "uploading", - progress: 0 - } + type: "requires-action", + reason: "composer-send", + }, }; }, async send(attachment: PendingAttachment): Promise { - const isImage = attachment.contentType.startsWith("image/"); - - const content: ThreadUserContentPart[] = isImage - ? [{ - type: "image", - image: await fileToBase64(attachment.file) - }] - : [{ - type: "file", - data: URL.createObjectURL(attachment.file), - mimeType: attachment.file.type - }]; - + const isImage = attachment.contentType?.startsWith("image/"); + + let content: ThreadUserContentPart[]; + + try { + content = isImage + ? [{ + type: "image", + image: await fileToBase64(attachment.file), + }] + : [{ + type: "file", + data: URL.createObjectURL(attachment.file), + mimeType: attachment.file.type, + }]; + } catch (err) { + throw new Error( + `Failed to process attachment "${attachment.name}": ${err instanceof Error ? err.message : "unknown error"}` + ); + } + return { ...attachment, content, status: { - type: "complete" - } + type: "complete", + }, }; }, From 4d22430b46140e69c51d808b27e3418d5a853708 Mon Sep 17 00:00:00 2001 From: Faran Javed Date: Wed, 18 Feb 2026 18:23:20 +0500 Subject: [PATCH 12/12] add messageInstance for throwing errors in attachments for AI chat component --- .../comps/comps/chatComp/utils/attachmentAdapter.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts index a70afc734..9ff22d436 100644 --- a/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts +++ b/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts @@ -5,6 +5,7 @@ import type { Attachment, ThreadUserContentPart } from "@assistant-ui/react"; +import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB @@ -13,6 +14,9 @@ import type { async add({ file }): Promise { if (file.size > MAX_FILE_SIZE) { + messageInstance.error( + `File "${file.name}" exceeds the 10 MB size limit (${(file.size / 1024 / 1024).toFixed(1)} MB).` + ); throw new Error( `File "${file.name}" exceeds the 10 MB size limit (${(file.size / 1024 / 1024).toFixed(1)} MB).` ); @@ -48,9 +52,9 @@ import type { mimeType: attachment.file.type, }]; } catch (err) { - throw new Error( - `Failed to process attachment "${attachment.name}": ${err instanceof Error ? err.message : "unknown error"}` - ); + const errorMessage = `Failed to process attachment "${attachment.name}": ${err instanceof Error ? err.message : "unknown error"}`; + messageInstance.error(errorMessage); + throw new Error(errorMessage); } return {