Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0a18e0
fix conversation history
iamfaran Feb 10, 2026
74f1787
fix height messages window + add height mode (auto/fixed) for chat co…
iamfaran Feb 10, 2026
b4620e5
add style customization
iamfaran Feb 11, 2026
5875702
seperate chat panel component
iamfaran Feb 12, 2026
11bb844
complete chat styles
iamfaran Feb 12, 2026
a12a9b9
refactor ai chat & chat panel component
iamfaran Feb 13, 2026
e3fe298
fix re rendering issue
iamfaran Feb 16, 2026
c970bc2
fix button forward ref + ChatProvider level
iamfaran Feb 16, 2026
64dddc0
fix styling warninings
iamfaran Feb 17, 2026
5ef4403
refactor styles and add storage cleaner for bottom chat panel
iamfaran Feb 17, 2026
a46c1f7
fix attachment file adaptor for chat component
iamfaran Feb 18, 2026
4d22430
add messageInstance for throwing errors in attachments for AI chat co…
iamfaran Feb 18, 2026
7fd74ca
remove unnecessary settings from the AI chat component
iamfaran Feb 19, 2026
0c4e885
fix image attachments preview and remove attachments from the bottom …
iamfaran Feb 20, 2026
57fd468
add chatCompv2 + new chatData store
iamfaran Feb 25, 2026
18abefb
setup basic data structure for chatv2
iamfaran Feb 26, 2026
d8a8423
add yjs support
iamfaran Feb 27, 2026
362e362
fix linter errors
iamfaran Mar 3, 2026
6a1911b
add typing indicators
iamfaran Mar 3, 2026
bf08ee3
refactor chatbox styles, modes and fix registry
iamfaran Mar 4, 2026
6a0ff47
refactor chatbox to multiple files
iamfaran Mar 5, 2026
0a4fe35
remove duplication of modes
iamfaran Mar 5, 2026
7d40041
add testing chat controller
iamfaran Mar 6, 2026
58598dc
add typing state via awareness protocol
iamfaran Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useContext } from "react";
import { Section, sectionNames } from "lowcoder-design";
import { UICompBuilder, withDefault } from "../../generators";
import { NameConfig, NameConfigHidden, withExposingConfigs } from "../../generators/withExposing";
import { withMethodExposing } from "../../generators/withMethodExposing";
import { stringExposingStateControl } from "comps/controls/codeStateControl";
import { BoolControl } from "comps/controls/boolControl";
import { StringControl } from "comps/controls/codeControl";
import { AutoHeightControl } from "comps/controls/autoHeightControl";
import { eventHandlerControl } from "comps/controls/eventHandlerControl";
import { styleControl } from "comps/controls/styleControl";
import { AnimationStyle, TextStyle } from "comps/controls/styleControlConstants";
import { hiddenPropertyView } from "comps/utils/propertyUtils";
import { EditorContext } from "comps/editorState";
import { trans } from "i18n";

import { ChatBoxView } from "./components/ChatBoxView";

// ─── Event definitions ──────────────────────────────────────────────────────

const ChatEvents = [
{ label: trans("chatBox.messageSent"), value: "messageSent", description: trans("chatBox.messageSentDesc") },
{ label: trans("chatBox.messageReceived"), value: "messageReceived", description: trans("chatBox.messageReceivedDesc") },
{ label: trans("chatBox.roomJoined"), value: "roomJoined", description: trans("chatBox.roomJoinedDesc") },
{ label: trans("chatBox.roomLeft"), value: "roomLeft", description: trans("chatBox.roomLeftDesc") },
] as const;

// ─── Children map (component properties) ────────────────────────────────────

const childrenMap = {
chatName: stringExposingStateControl("chatName", "Chat Room"),
userId: stringExposingStateControl("userId", "user_1"),
userName: stringExposingStateControl("userName", "User"),
applicationId: stringExposingStateControl("applicationId", "lowcoder_app"),
wsUrl: withDefault(StringControl, "ws://localhost:3005"),

allowRoomCreation: withDefault(BoolControl, true),
allowRoomSearch: withDefault(BoolControl, true),
showRoomPanel: withDefault(BoolControl, true),
roomPanelWidth: withDefault(StringControl, "220px"),

autoHeight: AutoHeightControl,
onEvent: eventHandlerControl(ChatEvents),
style: styleControl(TextStyle, "style"),
animationStyle: styleControl(AnimationStyle, "animationStyle"),
};

// ─── Property panel ─────────────────────────────────────────────────────────

const ChatBoxPropertyView = React.memo((props: { children: any }) => {
const { children } = props;
const editorMode = useContext(EditorContext).editorModeStatus;

return (
<>
<Section name={sectionNames.basic}>
{children.chatName.propertyView({ label: "Chat Name", tooltip: "Display name for the chat header" })}
{children.userId.propertyView({ label: "User ID", tooltip: "Current user's unique identifier" })}
{children.userName.propertyView({ label: "User Name", tooltip: "Current user's display name" })}
{children.applicationId.propertyView({ label: "Application ID", tooltip: "Scopes rooms to this application" })}
{children.wsUrl.propertyView({
label: "WebSocket URL",
tooltip: "Yjs WebSocket server URL for real-time sync (e.g. ws://localhost:3005)",
})}
</Section>

<Section name="Room Settings">
{children.allowRoomCreation.propertyView({ label: "Allow Room Creation" })}
{children.allowRoomSearch.propertyView({ label: "Allow Room Search" })}
{children.showRoomPanel.propertyView({ label: "Show Room Panel" })}
{children.roomPanelWidth.propertyView({ label: "Panel Width", tooltip: "e.g. 220px or 25%" })}
</Section>

{["logic", "both"].includes(editorMode) && (
<Section name={sectionNames.interaction}>
{hiddenPropertyView(children)}
{children.onEvent.getPropertyView()}
</Section>
)}

{["layout", "both"].includes(editorMode) && (
<>
<Section name={sectionNames.layout}>
{children.autoHeight.getPropertyView()}
</Section>
<Section name={sectionNames.style}>
{children.style.getPropertyView()}
</Section>
<Section name={sectionNames.animationStyle} hasTooltip={true}>
{children.animationStyle.getPropertyView()}
</Section>
</>
)}
</>
);
});

ChatBoxPropertyView.displayName = "ChatBoxV2PropertyView";

// ─── Build component ────────────────────────────────────────────────────────

let ChatBoxV2Tmp = (function () {
return new UICompBuilder(childrenMap, (props) => <ChatBoxView {...props} />)
.setPropertyViewFn((children) => <ChatBoxPropertyView children={children} />)
.build();
})();

ChatBoxV2Tmp = class extends ChatBoxV2Tmp {
override autoHeight(): boolean {
return this.children.autoHeight.getView();
}
};

// ─── Methods ────────────────────────────────────────────────────────────────

ChatBoxV2Tmp = withMethodExposing(ChatBoxV2Tmp, [
{
method: {
name: "setUser",
description: "Update the current chat user",
params: [
{ name: "userId", type: "string" },
{ name: "userName", type: "string" },
],
},
execute: (comp: any, values: any[]) => {
if (values[0]) comp.children.userId.getView().onChange(values[0]);
if (values[1]) comp.children.userName.getView().onChange(values[1]);
},
},
]);

// ─── Exposing configs ───────────────────────────────────────────────────────

export const ChatBoxV2Comp = withExposingConfigs(ChatBoxV2Tmp, [
new NameConfig("chatName", "Chat display name"),
new NameConfig("userId", "Current user ID"),
new NameConfig("userName", "Current user name"),
new NameConfig("applicationId", "Application scope"),
NameConfigHidden,
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React, { useCallback, useState } from "react";
import { UserOutlined } from "@ant-design/icons";
import { useChatStore } from "../useChatStore";
import { Wrapper, ChatPanelContainer, ChatHeaderBar, ConnectionBanner, ConnectionDot } from "../styles";
import { RoomPanel } from "./RoomPanel";
import { MessageList } from "./MessageList";
import { InputBar } from "./InputBar";
import { CreateRoomModal } from "./CreateRoomModal";

type ChatBoxEventName = "messageSent" | "messageReceived" | "roomJoined" | "roomLeft";

export interface ChatBoxViewProps {
chatName: { value: string };
userId: { value: string };
userName: { value: string };
applicationId: { value: string };
wsUrl: string;
allowRoomCreation: boolean;
allowRoomSearch: boolean;
showRoomPanel: boolean;
roomPanelWidth: string;
style: any;
animationStyle: any;
onEvent: (event: ChatBoxEventName) => any;
[key: string]: any;
}

function connectionStatus(ready: boolean, label: string): "online" | "offline" | "connecting" {
if (!ready) return "connecting";
if (label.includes("Online") || label === "Local") return "online";
return "offline";
}

export const ChatBoxView = React.memo((props: ChatBoxViewProps) => {
const {
chatName,
userId,
userName,
applicationId,
wsUrl,
allowRoomCreation,
allowRoomSearch,
showRoomPanel,
roomPanelWidth,
style,
animationStyle,
onEvent,
} = props;

const chat = useChatStore({
applicationId: applicationId.value || "lowcoder_app",
userId: userId.value || "user_1",
userName: userName.value || "User",
wsUrl: wsUrl || "ws://localhost:3005",
});

const [createModalOpen, setCreateModalOpen] = useState(false);

const handleLeaveRoom = useCallback(
async (roomId: string) => {
const ok = await chat.leaveRoom(roomId);
if (ok) onEvent("roomLeft");
},
[chat.leaveRoom, onEvent],
);

const handleJoinRoom = useCallback(
async (roomId: string) => {
const ok = await chat.joinRoom(roomId);
if (ok) onEvent("roomJoined");
},
[chat.joinRoom, onEvent],
);

const status = connectionStatus(chat.ready, chat.connectionLabel);

return (
<Wrapper $style={style} $anim={animationStyle}>
{showRoomPanel && (
<RoomPanel
width={roomPanelWidth}
rooms={chat.userRooms}
currentRoomId={chat.currentRoom?.id}
ready={chat.ready}
allowRoomCreation={allowRoomCreation}
allowRoomSearch={allowRoomSearch}
onSwitchRoom={chat.switchRoom}
onJoinRoom={handleJoinRoom}
onLeaveRoom={handleLeaveRoom}
onSearchRooms={chat.searchRooms}
onCreateModalOpen={() => setCreateModalOpen(true)}
/>
)}

<ChatPanelContainer>
<ChatHeaderBar>
<div>
<div style={{ fontWeight: 600, fontSize: 16 }}>{chatName.value}</div>
<div style={{ fontSize: 13, color: "#888" }}>
{chat.currentRoom?.name || "No room selected"}
{chat.currentRoomMembers.length > 0 && (
<span style={{ marginLeft: 8 }}>
<UserOutlined style={{ fontSize: 11, marginRight: 2 }} />
{chat.currentRoomMembers.length}
</span>
)}
</div>
</div>
<ConnectionBanner $status={status}>
<ConnectionDot $status={status} />
{chat.ready ? chat.connectionLabel : chat.error || "Connecting..."}
</ConnectionBanner>
</ChatHeaderBar>

<MessageList
messages={chat.messages}
typingUsers={chat.typingUsers}
currentUserId={userId.value}
ready={chat.ready}
/>

<InputBar
ready={chat.ready}
currentRoom={chat.currentRoom}
onSend={chat.sendMessage}
onStartTyping={chat.startTyping}
onStopTyping={chat.stopTyping}
onMessageSentEvent={() => onEvent("messageSent")}
/>
</ChatPanelContainer>

<CreateRoomModal
open={createModalOpen}
onClose={() => setCreateModalOpen(false)}
onCreateRoom={chat.createRoom}
onRoomCreatedEvent={() => onEvent("roomJoined")}
/>
</Wrapper>
);
});

ChatBoxView.displayName = "ChatBoxV2View";
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useCallback } from "react";
import { Modal, Form, Input, Radio, Button, Space } from "antd";
import { PlusOutlined, GlobalOutlined, LockOutlined } from "@ant-design/icons";
import type { ChatRoom } from "../store";

export interface CreateRoomModalProps {
open: boolean;
onClose: () => void;
onCreateRoom: (name: string, type: "public" | "private", description?: string) => Promise<ChatRoom | null>;
onRoomCreatedEvent: () => void;
}

export const CreateRoomModal = React.memo((props: CreateRoomModalProps) => {
const { open, onClose, onCreateRoom, onRoomCreatedEvent } = props;
const [form] = Form.useForm();

const handleFinish = useCallback(
async (values: { roomName: string; roomType: "public" | "private"; description?: string }) => {
const room = await onCreateRoom(values.roomName.trim(), values.roomType, values.description);
if (room) {
form.resetFields();
onClose();
onRoomCreatedEvent();
}
},
[onCreateRoom, form, onClose, onRoomCreatedEvent],
);

const handleCancel = useCallback(() => {
onClose();
form.resetFields();
}, [onClose, form]);

return (
<Modal
title="Create Room"
open={open}
onCancel={handleCancel}
footer={null}
width={420}
centered
destroyOnHidden
>
<Form
form={form}
layout="vertical"
onFinish={handleFinish}
initialValues={{ roomType: "public" }}
>
<Form.Item
name="roomName"
label="Room Name"
rules={[
{ required: true, message: "Room name is required" },
{ min: 2, message: "At least 2 characters" },
{ max: 50, message: "At most 50 characters" },
]}
>
<Input placeholder="e.g. Design Team" />
</Form.Item>
<Form.Item name="description" label="Description">
<Input.TextArea placeholder="What is this room about?" rows={2} />
</Form.Item>
<Form.Item name="roomType" label="Visibility">
<Radio.Group>
<Radio value="public">
<GlobalOutlined style={{ color: "#52c41a", marginRight: 4 }} /> Public
</Radio>
<Radio value="private">
<LockOutlined style={{ color: "#fa8c16", marginRight: 4 }} /> Private
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Space style={{ width: "100%", justifyContent: "flex-end" }}>
<Button onClick={handleCancel}>Cancel</Button>
<Button type="primary" htmlType="submit" icon={<PlusOutlined />}>
Create
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
);
});

CreateRoomModal.displayName = "CreateRoomModal";
Loading
Loading