Skip to content
Open
121 changes: 91 additions & 30 deletions client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,34 @@ import { UICompBuilder } from "comps/generators";
import { NameConfig, withExposingConfigs } from "comps/generators/withExposing";
import { StringControl } from "comps/controls/codeControl";
import { arrayObjectExposingStateControl, stringExposingStateControl } from "comps/controls/codeStateControl";
import { JSONObject } from "util/jsonTypes";
import { withDefault } from "comps/generators";
import { BoolControl } from "comps/controls/boolControl";
import { dropdownControl } from "comps/controls/dropdownControl";
import QuerySelectControl from "comps/controls/querySelectControl";
import { eventHandlerControl, EventConfigType } from "comps/controls/eventHandlerControl";
import { ChatCore } from "./components/ChatCore";
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";
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,
ChatSidebarStyle,
ChatMessagesStyle,
ChatInputStyle,
ChatSendButtonStyle,
ChatNewThreadButtonStyle,
ChatThreadItemStyle,
} from "comps/controls/styleControlConstants";
import { AnimationStyle } from "comps/controls/styleControlConstants";

import "@assistant-ui/styles/index.css";
import "@assistant-ui/styles/markdown.css";
Expand Down Expand Up @@ -147,15 +162,33 @@ export const chatChildrenMap = {
// UI Configuration
placeholder: withDefault(StringControl, trans("chat.defaultPlaceholder")),

// Layout Configuration
autoHeight: AutoHeightControl,
leftPanelWidth: withDefault(StringControl, "250px"),

// Database Information (read-only)
databaseName: withDefault(StringControl, ""),

// Event Handlers
onEvent: ChatEventHandlerControl,

// Style Controls - 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),

// 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[]),
};

// ============================================================================
Expand Down Expand Up @@ -221,30 +254,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(() => {
Expand All @@ -256,27 +291,53 @@ const handleConversationUpdate = (conversationHistory: any[]) => {
};
}, []);

// 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 (
<ChatCore
storage={storage}
messageHandler={messageHandler}
placeholder={props.placeholder}
onMessageUpdate={handleMessageUpdate}
onConversationUpdate={handleConversationUpdate}
onEvent={props.onEvent}
/>
<TooltipProvider>
<ChatProvider storage={storage}>
<ChatContainer
messageHandler={messageHandler}
placeholder={props.placeholder}
autoHeight={props.autoHeight}
sidebarWidth={props.leftPanelWidth}
onMessageUpdate={handleMessageUpdate}
onConversationUpdate={handleConversationUpdate}
onEvent={props.onEvent}
{...styles}
/>
</ChatProvider>
</TooltipProvider>
);
}
)
.setPropertyViewFn((children) => <ChatPropertyView children={children} />)
.build();

// Override autoHeight to support AUTO/FIXED height mode
const ChatCompWithAutoHeight = class extends ChatTmpComp {
override autoHeight(): boolean {
return this.children.autoHeight.getView();
}
};

// ============================================================================
// EXPORT WITH EXPOSED VARIABLES
// ============================================================================

export const ChatComp = withExposingConfigs(ChatTmpComp, [
export const ChatComp = withExposingConfigs(ChatCompWithAutoHeight, [
new NameConfig("currentMessage", "Current user message"),
new NameConfig("conversationHistory", "Full conversation history as JSON array (includes system prompt for API calls)"),
// conversationHistory is now a proper array (not JSON string) - supports setConversationHistory(), clearConversationHistory(), resetConversationHistory()
new NameConfig("conversationHistory", "Full conversation history array with system prompt (use directly in API calls, no JSON.parse needed)"),
new NameConfig("databaseName", "Database name for SQL queries (ChatDB_<componentName>)"),
]);
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import React, { useMemo } from "react";
import { Section, sectionNames, DocLink } from "lowcoder-design";
import { placeholderPropertyView } from "../../utils/propertyUtils";
import { trans } from "i18n";
import { hiddenPropertyView } from "comps/utils/propertyUtils";

// ============================================================================
// CLEAN PROPERTY VIEW - FOCUSED ON ESSENTIAL CONFIGURATION
Expand Down Expand Up @@ -55,19 +55,28 @@ 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"),
})}
</Section>

{/* UI Configuration */}
<Section name={trans("chat.uiConfiguration")}>
{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"),
})}
</Section>

{/* Layout Section - Height Mode & Sidebar Width */}
<Section name={sectionNames.layout}>
{children.autoHeight.getPropertyView()}
{children.leftPanelWidth.propertyView({
label: trans("chat.leftPanelWidth"),
tooltip: trans("chat.leftPanelWidthTooltip"),
})}
</Section>

{/* Database Section */}
Expand All @@ -84,6 +93,39 @@ export const ChatPropertyView = React.memo((props: any) => {
{children.onEvent.getPropertyView()}
</Section>

{/* STYLE SECTIONS */}
<Section name={sectionNames.style}>
{children.style.getPropertyView()}
</Section>

<Section name={trans("chat.sidebarStyle")}>
{children.sidebarStyle.getPropertyView()}
</Section>

<Section name={trans("chat.messagesStyle")}>
{children.messagesStyle.getPropertyView()}
</Section>

<Section name={trans("chat.inputStyle")}>
{children.inputStyle.getPropertyView()}
</Section>

<Section name={trans("chat.sendButtonStyle")}>
{children.sendButtonStyle.getPropertyView()}
</Section>

<Section name={trans("chat.newThreadButtonStyle")}>
{children.newThreadButtonStyle.getPropertyView()}
</Section>

<Section name={trans("chat.threadItemStyle")}>
{children.threadItemStyle.getPropertyView()}
</Section>

<Section name={sectionNames.animationStyle} hasTooltip={true}>
{children.animationStyle.getPropertyView()}
</Section>

</>
), [children]);
});
Expand Down
Loading
Loading