diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
index 57ac9040a..b923e3aab 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatComp.tsx
@@ -4,12 +4,15 @@ 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";
@@ -17,6 +20,18 @@ 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";
@@ -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[]),
};
// ============================================================================
@@ -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(() => {
@@ -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 (
-
+
+
+
+
+
);
}
)
.setPropertyViewFn((children) => )
.build();
+// Override autoHeight to support AUTO/FIXED height mode
+const ChatCompWithAutoHeight = class extends ChatTmpComp {
+ override autoHeight(): boolean {
+ return this.children.autoHeight.getView();
+ }
+};
+
// ============================================================================
// EXPORT WITH EXPOSED VARIABLES
// ============================================================================
-export const ChatComp = withExposingConfigs(ChatTmpComp, [
+export const ChatComp = withExposingConfigs(ChatCompWithAutoHeight, [
new NameConfig("currentMessage", "Current user message"),
- new NameConfig("conversationHistory", "Full conversation history as JSON array (includes system prompt for API calls)"),
+ // conversationHistory is now a proper array (not JSON string) - supports setConversationHistory(), clearConversationHistory(), resetConversationHistory()
+ new NameConfig("conversationHistory", "Full conversation history array with system prompt (use directly in API calls, no JSON.parse needed)"),
new NameConfig("databaseName", "Database name for SQL queries (ChatDB_)"),
]);
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx
index 0e2fd0290..1e396ebcb 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/chatPropertyView.tsx
@@ -2,8 +2,8 @@
import React, { useMemo } from "react";
import { Section, sectionNames, DocLink } from "lowcoder-design";
-import { placeholderPropertyView } from "../../utils/propertyUtils";
import { trans } from "i18n";
+import { hiddenPropertyView } from "comps/utils/propertyUtils";
// ============================================================================
// CLEAN PROPERTY VIEW - FOCUSED ON ESSENTIAL CONFIGURATION
@@ -55,7 +55,7 @@ export const ChatPropertyView = React.memo((props: any) => {
tooltip: trans("chat.systemPromptTooltip"),
})}
- {children.streaming.propertyView({
+ {children.streaming.propertyView({
label: trans("chat.streaming"),
tooltip: trans("chat.streamingTooltip"),
})}
@@ -63,11 +63,20 @@ export const ChatPropertyView = React.memo((props: any) => {
{/* UI Configuration */}
- {children.placeholder.propertyView({
- label: trans("chat.placeholderLabel"),
- placeholder: trans("chat.defaultPlaceholder"),
- tooltip: trans("chat.placeholderTooltip"),
- })}
+ {children.placeholder.propertyView({
+ label: trans("chat.placeholderLabel"),
+ placeholder: trans("chat.defaultPlaceholder"),
+ tooltip: trans("chat.placeholderTooltip"),
+ })}
+
+
+ {/* Layout Section - Height Mode & Sidebar Width */}
+
+ {children.autoHeight.getPropertyView()}
+ {children.leftPanelWidth.propertyView({
+ label: trans("chat.leftPanelWidth"),
+ tooltip: trans("chat.leftPanelWidthTooltip"),
+ })}
{/* Database Section */}
@@ -84,6 +93,39 @@ export const ChatPropertyView = React.memo((props: any) => {
{children.onEvent.getPropertyView()}
+ {/* STYLE SECTIONS */}
+
+ {children.style.getPropertyView()}
+
+
+
+ {children.sidebarStyle.getPropertyView()}
+
+
+
+ {children.messagesStyle.getPropertyView()}
+
+
+
+ {children.inputStyle.getPropertyView()}
+
+
+
+ {children.sendButtonStyle.getPropertyView()}
+
+
+
+ {children.newThreadButtonStyle.getPropertyView()}
+
+
+
+ {children.threadItemStyle.getPropertyView()}
+
+
+
+ {children.animationStyle.getPropertyView()}
+
+
>
), [children]);
});
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx
new file mode 100644
index 000000000..af8028b4d
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx
@@ -0,0 +1,253 @@
+// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatContainer.tsx
+
+import React, { useState, useEffect, useRef } 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, ChatCoreProps } from "../types/chatTypes";
+import { trans } from "i18n";
+import { universalAttachmentAdapter } from "../utils/attachmentAdapter";
+import { StyledChatContainer } from "./ChatContainerStyles";
+
+// ============================================================================
+// CHAT CONTAINER - USES CONTEXT FROM CHATPROVIDER
+// ============================================================================
+
+const generateId = () => Math.random().toString(36).substr(2, 9);
+
+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) {
+ onConversationUpdateRef.current?.(currentMessages);
+ }
+ }, [currentMessages, isRunning]);
+
+ useEffect(() => {
+ onEventRef.current?.("componentLoad");
+ }, []);
+
+ 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 - SIMPLIFIED (PROVIDERS MOVED UP ONE LEVEL)
+// ============================================================================
+
+export const ChatContainer = ChatContainerView;
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/ChatCore.tsx b/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx
deleted file mode 100644
index ad0d33e2c..000000000
--- a/client/packages/lowcoder/src/comps/comps/chatComp/components/ChatCore.tsx
+++ /dev/null
@@ -1,34 +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,
- onMessageUpdate,
- onConversationUpdate,
- onEvent
-}: ChatCoreProps) {
- return (
-
-
-
-
-
- );
-}
\ No newline at end of file
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..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,7 +1,7 @@
// client/packages/lowcoder/src/comps/comps/chatComp/components/ChatPanel.tsx
-import { useMemo } from "react";
-import { ChatCore } from "./ChatCore";
+import { useMemo, useEffect } from "react";
+import { ChatPanelContainer } from "./ChatPanelContainer";
import { createChatStorage } from "../utils/storageFactory";
import { N8NHandler } from "../handlers/messageHandlers";
import { ChatPanelProps } from "../types/chatTypes";
@@ -11,7 +11,7 @@ import "@assistant-ui/styles/index.css";
import "@assistant-ui/styles/markdown.css";
// ============================================================================
-// CHAT PANEL - CLEAN BOTTOM PANEL COMPONENT
+// CHAT PANEL - SIMPLIFIED BOTTOM PANEL (NO STYLING CONTROLS)
// ============================================================================
export function ChatPanel({
@@ -21,24 +21,29 @@ export function ChatPanel({
streaming = true,
onMessageUpdate
}: ChatPanelProps) {
- // Create storage instance
- const storage = useMemo(() =>
- createChatStorage(tableName),
+ const storage = useMemo(() =>
+ createChatStorage(tableName),
[tableName]
);
-
- // Create N8N message handler
- 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 (
- `
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 +53,7 @@ const ChatContainer = styled.div`
.aui-thread-root {
flex: 1;
background-color: #f9fafb;
+ height: auto;
}
.aui-thread-list-item {
@@ -58,54 +68,27 @@ const ChatContainer = styled.div`
`;
// ============================================================================
-// CHAT CORE MAIN - CLEAN PROPS, FOCUSED RESPONSIBILITY
+// CHAT PANEL CONTAINER - DIRECT RENDERING
// ============================================================================
-interface ChatCoreMainProps {
+const generateId = () => Math.random().toString(36).substr(2, 9);
+
+export interface ChatPanelContainerProps {
+ storage: any;
messageHandler: MessageHandler;
placeholder?: string;
onMessageUpdate?: (message: string) => void;
- onConversationUpdate?: (conversationHistory: ChatMessage[]) => void;
- // STANDARD LOWCODER EVENT PATTERN - SINGLE CALLBACK (OPTIONAL)
- onEvent?: (eventName: string) => void;
}
-const generateId = () => Math.random().toString(36).substr(2, 9);
-
-export function ChatCoreMain({
- messageHandler,
- placeholder,
- onMessageUpdate,
- onConversationUpdate,
- onEvent
-}: ChatCoreMainProps) {
+function ChatPanelView({ messageHandler, placeholder, onMessageUpdate }: Omit) {
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) {
@@ -123,22 +106,17 @@ export function ChatCoreMain({
};
};
- // 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) {
+ if (!text && !completeAttachments.length) {
throw new Error("Cannot send an empty message");
}
@@ -154,105 +132,79 @@ export function ChatCoreMain({
setIsRunning(true);
try {
- const response = await messageHandler.sendMessage(userMessage); // Send full message object with attachments
-
+ 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 - 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) {
+ if (!text && !completeAttachments.length) {
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 = {
+ newMessages.push({
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 response = await messageHandler.sendMessage(newMessages[newMessages.length - 1]);
+ onMessageUpdate?.(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 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"),
@@ -261,7 +213,6 @@ export function ChatCoreMain({
onSwitchToNewThread: async () => {
const threadId = await actions.createThread(trans("chat.newChatTitle"));
actions.setCurrentThread(threadId);
- onEvent?.("threadCreated");
},
onSwitchToThread: (threadId) => {
@@ -270,25 +221,20 @@ export function ChatCoreMain({
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,
@@ -305,11 +251,28 @@ export function ChatCoreMain({
return (
-
+
-
+
);
}
+// ============================================================================
+// EXPORT - WITH PROVIDERS
+// ============================================================================
+
+export function ChatPanelContainer({ storage, messageHandler, placeholder, onMessageUpdate }: ChatPanelContainerProps) {
+ return (
+
+
+
+
+
+ );
+}
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]);
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 5e757f231..b465eda8b 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts
@@ -67,24 +67,36 @@ export interface ChatMessage {
systemPrompt?: string;
}
- // ============================================================================
- // COMPONENT PROPS (what each component actually needs)
- // ============================================================================
-
- export interface ChatCoreProps {
- storage: ChatStorage;
- messageHandler: MessageHandler;
- placeholder?: string;
- onMessageUpdate?: (message: string) => void;
- onConversationUpdate?: (conversationHistory: ChatMessage[]) => void;
- // STANDARD LOWCODER EVENT PATTERN - SINGLE CALLBACK
- onEvent?: (eventName: string) => void;
- }
-
- export interface ChatPanelProps {
- tableName: string;
- modelHost: string;
- systemPrompt?: string;
- streaming?: boolean;
- onMessageUpdate?: (message: string) => void;
- }
+// ============================================================================
+// COMPONENT PROPS (what each component actually needs)
+// ============================================================================
+
+// Main Chat Component Props (with full styling support)
+export interface ChatCoreProps {
+ messageHandler: MessageHandler;
+ placeholder?: string;
+ autoHeight?: boolean;
+ sidebarWidth?: string;
+ onMessageUpdate?: (message: string) => void;
+ onConversationUpdate?: (conversationHistory: ChatMessage[]) => void;
+ // STANDARD LOWCODER EVENT PATTERN - SINGLE CALLBACK
+ onEvent?: (eventName: string) => void;
+ // Style controls (only for main component)
+ style?: any;
+ sidebarStyle?: any;
+ messagesStyle?: any;
+ inputStyle?: any;
+ sendButtonStyle?: any;
+ newThreadButtonStyle?: any;
+ threadItemStyle?: any;
+ animationStyle?: any;
+}
+
+// Bottom Panel Props (simplified, no styling controls)
+export interface ChatPanelProps {
+ tableName: string;
+ modelHost: string;
+ systemPrompt?: string;
+ streaming?: boolean;
+ onMessageUpdate?: (message: string) => void;
+}
diff --git a/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts b/client/packages/lowcoder/src/comps/comps/chatComp/utils/attachmentAdapter.ts
index a0f7c78e0..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,25 +5,21 @@ 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
+
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) {
+ 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).`
+ );
}
return {
@@ -33,33 +29,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) {
+ const errorMessage = `Failed to process attachment "${attachment.name}": ${err instanceof Error ? err.message : "unknown error"}`;
+ messageInstance.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+
return {
...attachment,
content,
status: {
- type: "complete"
- }
+ type: "complete",
+ },
};
},
diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
index 176afbbfc..e09e2b1fc 100644
--- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
+++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
@@ -2372,6 +2372,156 @@ export const RichTextEditorStyle = [
BORDER_WIDTH,
] as const;
+// Chat Component Styles
+export const ChatStyle = [
+ getBackground(),
+ MARGIN,
+ PADDING,
+ BORDER,
+ BORDER_STYLE,
+ RADIUS,
+ BORDER_WIDTH,
+] as const;
+
+export const ChatSidebarStyle = [
+ {
+ name: "sidebarBackground",
+ label: trans("style.sidebarBackground"),
+ depTheme: "primarySurface",
+ depType: DEP_TYPE.SELF,
+ transformer: toSelf,
+ },
+ {
+ name: "threadText",
+ label: trans("style.threadText"),
+ depName: "sidebarBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+] as const;
+
+export const ChatMessagesStyle = [
+ {
+ name: "messagesBackground",
+ label: trans("style.messagesBackground"),
+ color: "#f9fafb",
+ },
+ {
+ name: "userMessageBackground",
+ label: trans("style.userMessageBackground"),
+ depTheme: "primary",
+ depType: DEP_TYPE.SELF,
+ transformer: toSelf,
+ },
+ {
+ name: "userMessageText",
+ label: trans("style.userMessageText"),
+ depName: "userMessageBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+ {
+ name: "assistantMessageBackground",
+ label: trans("style.assistantMessageBackground"),
+ color: "#ffffff",
+ },
+ {
+ name: "assistantMessageText",
+ label: trans("style.assistantMessageText"),
+ depName: "assistantMessageBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+] as const;
+
+export const ChatInputStyle = [
+ {
+ name: "inputBackground",
+ label: trans("style.inputBackground"),
+ color: "#ffffff",
+ },
+ {
+ name: "inputText",
+ label: trans("style.inputText"),
+ depName: "inputBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+ {
+ name: "inputBorder",
+ label: trans("style.inputBorder"),
+ depName: "inputBackground",
+ transformer: backgroundToBorder,
+ },
+] as const;
+
+export const ChatSendButtonStyle = [
+ {
+ name: "sendButtonBackground",
+ label: trans("style.sendButtonBackground"),
+ depTheme: "primary",
+ depType: DEP_TYPE.SELF,
+ transformer: toSelf,
+ },
+ {
+ name: "sendButtonIcon",
+ label: trans("style.sendButtonIcon"),
+ depName: "sendButtonBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+] as const;
+
+export const ChatNewThreadButtonStyle = [
+ {
+ name: "newThreadBackground",
+ label: trans("style.newThreadBackground"),
+ depTheme: "primary",
+ depType: DEP_TYPE.SELF,
+ transformer: toSelf,
+ },
+ {
+ name: "newThreadText",
+ label: trans("style.newThreadText"),
+ depName: "newThreadBackground",
+ depType: DEP_TYPE.CONTRAST_TEXT,
+ transformer: contrastText,
+ },
+] as const;
+
+export const ChatThreadItemStyle = [
+ {
+ name: "threadItemBackground",
+ label: trans("style.threadItemBackground"),
+ color: "transparent",
+ },
+ {
+ name: "threadItemText",
+ label: trans("style.threadItemText"),
+ color: "inherit",
+ },
+ {
+ name: "threadItemBorder",
+ label: trans("style.threadItemBorder"),
+ color: "transparent",
+ },
+ {
+ name: "activeThreadBackground",
+ label: trans("style.activeThreadBackground"),
+ color: "#dbeafe",
+ },
+ {
+ name: "activeThreadText",
+ label: trans("style.activeThreadText"),
+ color: "inherit",
+ },
+ {
+ name: "activeThreadBorder",
+ label: trans("style.activeThreadBorder"),
+ color: "#bfdbfe",
+ },
+] as const;
+
export type QRCodeStyleType = StyleConfigType;
export type TimeLineStyleType = StyleConfigType;
export type AvatarStyleType = StyleConfigType;
@@ -2490,6 +2640,14 @@ export type NavLayoutItemActiveStyleType = StyleConfigType<
typeof NavLayoutItemActiveStyle
>;
+export type ChatStyleType = StyleConfigType;
+export type ChatSidebarStyleType = StyleConfigType;
+export type ChatMessagesStyleType = StyleConfigType;
+export type ChatInputStyleType = StyleConfigType;
+export type ChatSendButtonStyleType = StyleConfigType;
+export type ChatNewThreadButtonStyleType = StyleConfigType;
+export type ChatThreadItemStyleType = StyleConfigType;
+
export function widthCalculator(margin: string) {
const marginArr = margin?.trim().replace(/\s+/g, " ").split(" ") || "";
if (marginArr.length === 1) {
diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts
index 56fa433e2..9c3accda6 100644
--- a/client/packages/lowcoder/src/i18n/locales/en.ts
+++ b/client/packages/lowcoder/src/i18n/locales/en.ts
@@ -600,6 +600,28 @@ export const en = {
"detailSize": "Detail Size",
"hideColumn": "Hide Column",
+ // Chat Component Styles
+ "sidebarBackground": "Sidebar Background",
+ "threadText": "Thread Text Color",
+ "messagesBackground": "Messages Background",
+ "userMessageBackground": "User Message Background",
+ "userMessageText": "User Message Text",
+ "assistantMessageBackground": "Assistant Message Background",
+ "assistantMessageText": "Assistant Message Text",
+ "inputBackground": "Input Background",
+ "inputText": "Input Text Color",
+ "inputBorder": "Input Border",
+ "sendButtonBackground": "Send Button Background",
+ "sendButtonIcon": "Send Button Icon Color",
+ "newThreadBackground": "New Thread Button Background",
+ "newThreadText": "New Thread Button Text",
+ "threadItemBackground": "Thread Item Background",
+ "threadItemText": "Thread Item Text",
+ "threadItemBorder": "Thread Item Border",
+ "activeThreadBackground": "Active Thread Background",
+ "activeThreadText": "Active Thread Text",
+ "activeThreadBorder": "Active Thread Border",
+
"radiusTip": "Specifies the radius of the element's corners. Example: 5px, 50%, or 1em.",
"gapTip": "Specifies the gap between rows and columns in a grid or flex container. Example: 10px, 1rem, or 5%.",
"cardRadiusTip": "Defines the corner radius for card components. Example: 10px, 15px.",
@@ -1477,10 +1499,22 @@ export const en = {
"threadDeleted": "Thread Deleted",
"threadDeletedDesc": "Triggered when a thread is deleted - Delete thread from backend",
+ // Layout
+ "leftPanelWidth": "Sidebar Width",
+ "leftPanelWidthTooltip": "Width of the thread list sidebar (e.g., 250px, 30%)",
+
// Exposed Variables (for documentation)
"currentMessage": "Current user message",
"conversationHistory": "Full conversation history as JSON array",
- "databaseNameExposed": "Database name for SQL queries (ChatDB_)"
+ "databaseNameExposed": "Database name for SQL queries (ChatDB_)",
+
+ // Style Section Names
+ "sidebarStyle": "Sidebar Style",
+ "messagesStyle": "Messages Style",
+ "inputStyle": "Input Field Style",
+ "sendButtonStyle": "Send Button Style",
+ "newThreadButtonStyle": "New Thread Button Style",
+ "threadItemStyle": "Thread Item Style"
},
"chatBox": {