@@ -273,11 +290,6 @@ import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments } fr
-
-
-
-
-
);
};
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..d24e0ce84 100644
--- a/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts
+++ b/client/packages/lowcoder/src/comps/comps/chatComp/types/chatTypes.ts
@@ -63,28 +63,38 @@ export interface ChatMessage {
export interface QueryHandlerConfig {
chatQuery: string;
dispatch: any;
- streaming?: boolean;
- 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/comps/hooks/chatControllerV2Comp.tsx b/client/packages/lowcoder/src/comps/hooks/chatControllerV2Comp.tsx
new file mode 100644
index 000000000..af90fbc9a
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/hooks/chatControllerV2Comp.tsx
@@ -0,0 +1,354 @@
+import React, { useEffect, useRef } from "react";
+import { Section, sectionNames } from "lowcoder-design";
+import {
+ simpleMultiComp,
+ stateComp,
+ withDefault,
+ withPropertyViewFn,
+ withViewFn,
+} from "../generators";
+import { NameConfig, withExposingConfigs } from "../generators/withExposing";
+import { withMethodExposing } from "../generators/withMethodExposing";
+import { stringExposingStateControl } from "comps/controls/codeStateControl";
+import { StringControl } from "comps/controls/codeControl";
+import { eventHandlerControl } from "comps/controls/eventHandlerControl";
+import { JSONObject } from "../../util/jsonTypes";
+import { useChatStore, UseChatStoreReturn } from "../comps/chatBoxComponentv2/useChatStore";
+
+// ─── Event definitions ──────────────────────────────────────────────────────
+
+const ChatControllerEvents = [
+ { label: "Message Sent", value: "messageSent", description: "Triggered when the current user sends a message" },
+ { label: "Message Received", value: "messageReceived", description: "Triggered when a message is received from another user" },
+ { label: "Room Joined", value: "roomJoined", description: "Triggered when the user joins a room" },
+ { label: "Room Left", value: "roomLeft", description: "Triggered when the user leaves a room" },
+ { label: "Connected", value: "connected", description: "Triggered when the chat store is ready" },
+ { label: "Error", value: "error", description: "Triggered when an error occurs" },
+] as const;
+
+// ─── Children map ───────────────────────────────────────────────────────────
+
+const childrenMap = {
+ // Configuration (shown in property panel, readable & writable)
+ applicationId: stringExposingStateControl("applicationId", "lowcoder_app"),
+ userId: stringExposingStateControl("userId", "user_1"),
+ userName: stringExposingStateControl("userName", "User"),
+ defaultRoom: withDefault(StringControl, "general"),
+ wsUrl: withDefault(StringControl, "ws://localhost:3005"),
+
+ // Events
+ onEvent: eventHandlerControl(ChatControllerEvents),
+
+ // Reactive state (synced from useChatStore, exposed to users)
+ ready: stateComp(false),
+ error: stateComp(null),
+ connectionStatus: stateComp("Connecting..."),
+ currentRoom: stateComp(null),
+ messages: stateComp([]),
+ userRooms: stateComp([]),
+ currentRoomMembers: stateComp([]),
+ typingUsers: stateComp([]),
+
+ // Internal: holds useChatStore actions so withMethodExposing can call them
+ _chatActions: stateComp({}),
+};
+
+// ─── View function (headless — returns null) ────────────────────────────────
+
+const ChatControllerV2Base = withViewFn(
+ simpleMultiComp(childrenMap),
+ (comp) => {
+ const userId = comp.children.userId.getView().value;
+ const userName = comp.children.userName.getView().value;
+ const applicationId = comp.children.applicationId.getView().value;
+ const defaultRoom = comp.children.defaultRoom.getView();
+ const wsUrl = comp.children.wsUrl.getView();
+
+ const chat = useChatStore({
+ applicationId: applicationId || "lowcoder_app",
+ defaultRoom: defaultRoom || "general",
+ userId: userId || "user_1",
+ userName: userName || "User",
+ wsUrl: wsUrl || "ws://localhost:3005",
+ });
+
+ const prevRef = useRef<{
+ ready: boolean;
+ msgCount: number;
+ roomId: string | null;
+ }>({ ready: false, msgCount: 0, roomId: null });
+
+ const triggerEvent = comp.children.onEvent.getView();
+
+ // ── Sync ready ─────────────────────────────────────────────────────
+ useEffect(() => {
+ comp.children.ready.dispatchChangeValueAction(chat.ready);
+ if (chat.ready && !prevRef.current.ready) {
+ triggerEvent("connected");
+ }
+ prevRef.current.ready = chat.ready;
+ }, [chat.ready]);
+
+ // ── Sync error ─────────────────────────────────────────────────────
+ useEffect(() => {
+ comp.children.error.dispatchChangeValueAction(chat.error);
+ if (chat.error) {
+ triggerEvent("error");
+ }
+ }, [chat.error]);
+
+ // ── Sync connection status ─────────────────────────────────────────
+ useEffect(() => {
+ comp.children.connectionStatus.dispatchChangeValueAction(chat.connectionLabel);
+ }, [chat.connectionLabel]);
+
+ // ── Sync currentRoom ───────────────────────────────────────────────
+ useEffect(() => {
+ comp.children.currentRoom.dispatchChangeValueAction(
+ chat.currentRoom as unknown as JSONObject | null,
+ );
+ const newRoomId = chat.currentRoom?.id ?? null;
+ if (newRoomId && newRoomId !== prevRef.current.roomId) {
+ triggerEvent("roomJoined");
+ }
+ prevRef.current.roomId = newRoomId;
+ }, [chat.currentRoom]);
+
+ // ── Sync messages ──────────────────────────────────────────────────
+ useEffect(() => {
+ comp.children.messages.dispatchChangeValueAction(
+ chat.messages as unknown as JSONObject[],
+ );
+ const newCount = chat.messages.length;
+ if (newCount > prevRef.current.msgCount && prevRef.current.msgCount > 0) {
+ const lastMsg = chat.messages[newCount - 1];
+ if (lastMsg?.authorId === userId) {
+ triggerEvent("messageSent");
+ } else {
+ triggerEvent("messageReceived");
+ }
+ }
+ prevRef.current.msgCount = newCount;
+ }, [chat.messages, userId]);
+
+ // ── Sync userRooms ─────────────────────────────────────────────────
+ useEffect(() => {
+ comp.children.userRooms.dispatchChangeValueAction(
+ chat.userRooms as unknown as JSONObject[],
+ );
+ }, [chat.userRooms]);
+
+ // ── Sync currentRoomMembers ────────────────────────────────────────
+ useEffect(() => {
+ comp.children.currentRoomMembers.dispatchChangeValueAction(
+ chat.currentRoomMembers as unknown as JSONObject[],
+ );
+ }, [chat.currentRoomMembers]);
+
+ // ── Sync typingUsers ───────────────────────────────────────────────
+ useEffect(() => {
+ comp.children.typingUsers.dispatchChangeValueAction(
+ chat.typingUsers as unknown as JSONObject[],
+ );
+ }, [chat.typingUsers]);
+
+ // ── Store actions for method access ────────────────────────────────
+ useEffect(() => {
+ comp.children._chatActions.dispatchChangeValueAction(
+ chat as unknown as JSONObject,
+ );
+ }, [chat.ready, chat.currentRoom]);
+
+ return null;
+ },
+);
+
+// ─── Property panel ─────────────────────────────────────────────────────────
+
+const ChatControllerV2WithProps = withPropertyViewFn(ChatControllerV2Base, (comp) => (
+ <>
+
+ {comp.children.applicationId.propertyView({
+ label: "Application ID",
+ tooltip: "Scopes chat rooms to this application",
+ })}
+ {comp.children.userId.propertyView({
+ label: "User ID",
+ tooltip: "Current user's unique identifier",
+ })}
+ {comp.children.userName.propertyView({
+ label: "User Name",
+ tooltip: "Current user's display name",
+ })}
+ {comp.children.defaultRoom.propertyView({
+ label: "Default Room",
+ tooltip: "Room to auto-join on initialization",
+ })}
+ {comp.children.wsUrl.propertyView({
+ label: "WebSocket URL",
+ tooltip: "Yjs WebSocket server URL for real-time sync",
+ })}
+
+
+ {comp.children.onEvent.getPropertyView()}
+
+ >
+));
+
+// ─── Expose state properties ────────────────────────────────────────────────
+
+let ChatControllerV2Comp = withExposingConfigs(ChatControllerV2WithProps, [
+ new NameConfig("ready", "Whether the chat store is initialized and ready"),
+ new NameConfig("error", "Error message if initialization failed"),
+ new NameConfig("connectionStatus", "Current connection status label"),
+ new NameConfig("currentRoom", "Currently active chat room object"),
+ new NameConfig("messages", "Messages in the current room"),
+ new NameConfig("userRooms", "Rooms the current user has joined"),
+ new NameConfig("currentRoomMembers", "Members of the current room"),
+ new NameConfig("typingUsers", "Users currently typing in the current room"),
+ new NameConfig("userId", "Current user ID"),
+ new NameConfig("userName", "Current user name"),
+ new NameConfig("applicationId", "Application scope ID"),
+]);
+
+// ─── Expose methods ─────────────────────────────────────────────────────────
+
+ChatControllerV2Comp = withMethodExposing(ChatControllerV2Comp, [
+ {
+ method: {
+ name: "sendMessage",
+ description: "Send a message to the current room",
+ params: [{ name: "text", type: "string" }],
+ },
+ execute: async (comp, values) => {
+ const actions = comp.children._chatActions.getView() as unknown as UseChatStoreReturn;
+ if (actions?.sendMessage) {
+ return await actions.sendMessage(values?.[0] as string);
+ }
+ return false;
+ },
+ },
+ {
+ method: {
+ name: "switchRoom",
+ description: "Switch to a different room by its ID",
+ params: [{ name: "roomId", type: "string" }],
+ },
+ execute: async (comp, values) => {
+ const actions = comp.children._chatActions.getView() as unknown as UseChatStoreReturn;
+ if (actions?.switchRoom) {
+ await actions.switchRoom(values?.[0] as string);
+ }
+ },
+ },
+ {
+ method: {
+ name: "createRoom",
+ description: "Create a new chat room",
+ params: [
+ { name: "name", type: "string" },
+ { name: "type", type: "string" },
+ { name: "description", type: "string" },
+ ],
+ },
+ execute: async (comp, values) => {
+ const actions = comp.children._chatActions.getView() as unknown as UseChatStoreReturn;
+ if (actions?.createRoom) {
+ return await actions.createRoom(
+ values?.[0] as string,
+ (values?.[1] as "public" | "private") || "public",
+ values?.[2] as string | undefined,
+ );
+ }
+ return null;
+ },
+ },
+ {
+ method: {
+ name: "joinRoom",
+ description: "Join a room by its ID",
+ params: [{ name: "roomId", type: "string" }],
+ },
+ execute: async (comp, values) => {
+ const actions = comp.children._chatActions.getView() as unknown as UseChatStoreReturn;
+ if (actions?.joinRoom) {
+ return await actions.joinRoom(values?.[0] as string);
+ }
+ return false;
+ },
+ },
+ {
+ method: {
+ name: "leaveRoom",
+ description: "Leave a room by its ID",
+ params: [{ name: "roomId", type: "string" }],
+ },
+ execute: async (comp, values) => {
+ const actions = comp.children._chatActions.getView() as unknown as UseChatStoreReturn;
+ if (actions?.leaveRoom) {
+ const ok = await actions.leaveRoom(values?.[0] as string);
+ if (ok) {
+ comp.children.onEvent.getView()("roomLeft");
+ }
+ return ok;
+ }
+ return false;
+ },
+ },
+ {
+ method: {
+ name: "searchRooms",
+ description: "Search for public rooms by query string",
+ params: [{ name: "query", type: "string" }],
+ },
+ execute: async (comp, values) => {
+ const actions = comp.children._chatActions.getView() as unknown as UseChatStoreReturn;
+ if (actions?.searchRooms) {
+ return await actions.searchRooms(values?.[0] as string);
+ }
+ return [];
+ },
+ },
+ {
+ method: {
+ name: "startTyping",
+ description: "Signal that the current user started typing",
+ params: [],
+ },
+ execute: (comp) => {
+ const actions = comp.children._chatActions.getView() as unknown as UseChatStoreReturn;
+ if (actions?.startTyping) {
+ actions.startTyping();
+ }
+ },
+ },
+ {
+ method: {
+ name: "stopTyping",
+ description: "Signal that the current user stopped typing",
+ params: [],
+ },
+ execute: (comp) => {
+ const actions = comp.children._chatActions.getView() as unknown as UseChatStoreReturn;
+ if (actions?.stopTyping) {
+ actions.stopTyping();
+ }
+ },
+ },
+ {
+ method: {
+ name: "setUser",
+ description: "Update the current chat user credentials",
+ params: [
+ { name: "userId", type: "string" },
+ { name: "userName", type: "string" },
+ ],
+ },
+ execute: (comp, values) => {
+ if (values?.[0]) comp.children.userId.getView().onChange(values[0] as string);
+ if (values?.[1]) comp.children.userName.getView().onChange(values[1] as string);
+ },
+ },
+]);
+
+export { ChatControllerV2Comp };
diff --git a/client/packages/lowcoder/src/comps/hooks/hookComp.tsx b/client/packages/lowcoder/src/comps/hooks/hookComp.tsx
index fa4294709..6e6e5f19a 100644
--- a/client/packages/lowcoder/src/comps/hooks/hookComp.tsx
+++ b/client/packages/lowcoder/src/comps/hooks/hookComp.tsx
@@ -38,6 +38,7 @@ import UrlParamsHookComp from "./UrlParamsHookComp";
import { UtilsComp } from "./utilsComp";
import { ScreenInfoHookComp } from "./screenInfoComp";
import { ChatControllerComp } from "../comps/chatBoxComponent/chatControllerComp";
+import { ChatControllerV2Comp } from "./chatControllerV2Comp";
window._ = _;
window.dayjs = dayjs;
@@ -120,6 +121,7 @@ const HookMap: HookCompMapRawType = {
drawer: DrawerComp,
theme: ThemeComp,
chatController: ChatControllerComp,
+ chatControllerV2: ChatControllerV2Comp,
};
export const HookTmpComp = withTypeAndChildren(HookMap, "title", {
diff --git a/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx b/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx
index 22e79e6d1..ee63a7f6a 100644
--- a/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx
+++ b/client/packages/lowcoder/src/comps/hooks/hookCompTypes.tsx
@@ -19,7 +19,8 @@ const AllHookComp = [
"urlParams",
"theme",
"meeting",
- "chatController"
+ "chatController",
+ "chatControllerV2"
] as const;
export type HookCompType = (typeof AllHookComp)[number];
@@ -54,6 +55,10 @@ const HookCompConfig: Record<
category: "ui",
singleton: false,
},
+ chatControllerV2: {
+ category: "ui",
+ singleton: false,
+ },
lodashJsLib: {
category: "hide",
},
diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx
index be34b1670..ebbb019ad 100644
--- a/client/packages/lowcoder/src/comps/index.tsx
+++ b/client/packages/lowcoder/src/comps/index.tsx
@@ -196,6 +196,8 @@ import { ContainerComp as FloatTextContainerComp } from "./comps/containerComp/t
import { ChatComp } from "./comps/chatComp";
import { ChatBoxComp } from "./comps/chatBoxComponent";
import { ChatControllerComp } from "./comps/chatBoxComponent/chatControllerComp";
+import { ChatControllerV2Comp } from "./hooks/chatControllerV2Comp";
+import { ChatBoxV2Comp } from "./comps/chatBoxComponentv2";
type Registry = {
[key in UICompType]?: UICompManifest;
@@ -973,6 +975,30 @@ export var uiCompMap: Registry = {
isContainer: true,
},
+ chatControllerV2: {
+ name: "Chat Controller V2",
+ enName: "Chat Controller V2",
+ description: "Headless chat controller — exposes state, methods & events so you can build custom chat UIs with built-in components",
+ categories: ["collaboration"],
+ icon: CommentCompIcon,
+ keywords: "chatbox,chat,controller,headless,rooms,messaging,v2",
+ comp: ChatControllerV2Comp,
+ },
+
+ chatBoxV: {
+ name: "Chat Box V2",
+ enName: "Chat Box V2",
+ description: "Chat Box with rooms, messaging, and local persistence",
+ categories: ["collaboration"],
+ icon: CommentCompIcon,
+ keywords: "chatbox,chat,conversation,rooms,messaging,v2",
+ comp: ChatBoxV2Comp,
+ layoutInfo: {
+ w: 12,
+ h: 24,
+ },
+ },
+
// Forms
form: {
diff --git a/client/packages/lowcoder/src/comps/uiCompRegistry.ts b/client/packages/lowcoder/src/comps/uiCompRegistry.ts
index 1de611df8..35f80bbb8 100644
--- a/client/packages/lowcoder/src/comps/uiCompRegistry.ts
+++ b/client/packages/lowcoder/src/comps/uiCompRegistry.ts
@@ -145,6 +145,8 @@ export type UICompType =
| "chat" //Added By Kamal Qureshi
| "chatBox" //Added By Kamal Qureshi
| "chatController"
+ | "chatControllerV2"
+ | "chatBoxV"
| "autocomplete" //Added By Mousheng
| "colorPicker" //Added By Mousheng
| "floatingButton" //Added By Mousheng
diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts
index 56fa433e2..38b43aab0 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.",
@@ -1421,18 +1443,11 @@ export const en = {
"chat": {
// Property View Labels & Tooltips
- "handlerType": "Handler Type",
- "handlerTypeTooltip": "How messages are processed",
"chatQuery": "Chat Query",
"chatQueryPlaceholder": "Select a query to handle messages",
- "modelHost": "N8N Webhook URL",
- "modelHostPlaceholder": "http://localhost:5678/webhook/...",
- "modelHostTooltip": "N8N webhook endpoint for processing messages",
"systemPrompt": "System Prompt",
"systemPromptPlaceholder": "You are a helpful assistant...",
"systemPromptTooltip": "Initial instructions for the AI",
- "streaming": "Enable Streaming",
- "streamingTooltip": "Stream responses in real-time (when supported)",
"databaseName": "Database Name",
"databaseNameTooltip": "Auto-generated database name for this chat component (read-only)",
@@ -1453,11 +1468,6 @@ export const en = {
// Error Messages
"errorUnknown": "Sorry, I encountered an error. Please try again.",
-
- // Handler Types
- "handlerTypeQuery": "Query",
- "handlerTypeN8N": "N8N Workflow",
-
// Section Names
"messageHandler": "Message Handler",
"uiConfiguration": "UI Configuration",
@@ -1477,10 +1487,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": {
diff --git a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx
index a558d8b8d..97a29ad78 100644
--- a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx
+++ b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx
@@ -309,4 +309,6 @@ export const CompStateIcon: {
chat: ,
chatBox: ,
chatController: ,
+ chatControllerV2: ,
+ chatBoxV: ,
} as const;