Skip to content

Commit ab37868

Browse files
committed
fix message styles, autoscroll and pluv server
1 parent d5b6b62 commit ab37868

9 files changed

Lines changed: 162 additions & 65 deletions

File tree

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/ChatBoxContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface ChatBoxContextValue {
2929
typingUsers: any[];
3030
onlineUsers: OnlineUser[];
3131
pendingInvites: PendingRoomInvite[];
32+
isAiThinking: boolean;
3233

3334
// Exposed state
3435
chatTitle: ExposedState;

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/chatBoxComp.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const childrenMap = {
9898
currentUserId: withDefault(StringControl, "user_1"),
9999
currentUserName: withDefault(StringControl, "User"),
100100
typingUsers: jsonArrayControl([]),
101+
isAiThinking: withDefault(BoolControl, false),
101102
lastSentMessageText: stringExposingStateControl("lastSentMessageText", ""),
102103
messageText: stringExposingStateControl("messageText", ""),
103104

@@ -187,6 +188,11 @@ const ChatBoxPropertyView = React.memo((props: { children: any }) => {
187188
tooltip:
188189
"Array of users currently typing. Bind to {{ chatController1.typingUsers }}",
189190
})}
191+
{children.isAiThinking.propertyView({
192+
label: "AI Is Thinking",
193+
tooltip:
194+
"Show the AI thinking animation to all users in this room. Bind to {{ chatController1.aiThinkingRooms[chatBox1.currentRoomId] }}",
195+
})}
190196
{children.onlineUsers.propertyView({
191197
label: "Online Users",
192198
tooltip:
@@ -232,6 +238,7 @@ let ChatBoxV2Tmp = (function () {
232238
const rooms = (Array.isArray(props.rooms) ? props.rooms : []) as unknown as ChatRoom[];
233239
const typingUsers = Array.isArray(props.typingUsers) ? props.typingUsers : [];
234240
const onlineUsers = Array.isArray(props.onlineUsers) ? props.onlineUsers : [];
241+
const isAiThinking = Boolean(props.isAiThinking);
235242
const pendingInvites = (Array.isArray(props.pendingInvites)
236243
? props.pendingInvites
237244
: []) as unknown as PendingRoomInvite[];
@@ -246,6 +253,7 @@ let ChatBoxV2Tmp = (function () {
246253
currentUserName: props.currentUserName,
247254
typingUsers,
248255
onlineUsers: onlineUsers as any,
256+
isAiThinking,
249257
pendingInvites,
250258

251259
chatTitle: props.chatTitle,
@@ -325,6 +333,7 @@ export const ChatBoxV2Comp = withExposingConfigs(ChatBoxV2Tmp, [
325333
"Text of the last message sent by the user — use in your save query",
326334
),
327335
new NameConfig("messageText", "Current text in the message input"),
336+
new NameConfig("currentRoomId", "Currently active room ID — for AI thinking or room-scoped queries"),
328337
new NameConfig(
329338
"pendingRoomId",
330339
"Room ID the user wants to switch to, join, or leave — read in roomSwitch/roomJoin/roomLeave events",

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/components/ChatBoxView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const ChatBoxView = React.memo(() => {
7272
messages={ctx.messages}
7373
typingUsers={ctx.typingUsers}
7474
currentUserId={ctx.currentUserId}
75+
isAiThinking={ctx.isAiThinking}
7576
/>
7677

7778
<InputBar

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/components/MessageList.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CopyOutlined, CheckOutlined, RobotOutlined } from "@ant-design/icons";
66
import { LLM_BOT_AUTHOR_ID } from "../store";
77
import {
88
MessagesArea,
9+
MessageWrapper,
910
Bubble,
1011
BubbleMeta,
1112
BubbleTime,
@@ -17,6 +18,7 @@ import {
1718
AiBadge,
1819
AiBubble,
1920
AiCopyButton,
21+
LlmLoadingBubble,
2022
} from "../styles";
2123

2224
// ── AI message bubble with copy button ───────────────────────────────────────
@@ -94,18 +96,24 @@ export interface MessageListProps {
9496
messages: any[];
9597
typingUsers: any[];
9698
currentUserId: string;
99+
isAiThinking?: boolean;
97100
}
98101

99102
export const MessageList = React.memo((props: MessageListProps) => {
100-
const { messages, typingUsers, currentUserId } = props;
101-
const bottomRef = useRef<HTMLDivElement>(null);
103+
const { messages, typingUsers, currentUserId, isAiThinking = false } = props;
104+
const containerRef = useRef<HTMLDivElement>(null);
102105

103106
useEffect(() => {
104-
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
105-
}, [messages.length]);
107+
if (containerRef.current) {
108+
containerRef.current.scrollTo({
109+
top: containerRef.current.scrollHeight,
110+
behavior: "smooth",
111+
});
112+
}
113+
}, [messages.length, isAiThinking]);
106114

107115
return (
108-
<MessagesArea>
116+
<MessagesArea ref={containerRef}>
109117
{messages.length === 0 ? (
110118
<EmptyChat>
111119
<div style={{ fontSize: 24 }}>💬</div>
@@ -151,7 +159,7 @@ export const MessageList = React.memo((props: MessageListProps) => {
151159
}
152160

153161
return (
154-
<div key={id}>
162+
<MessageWrapper key={id} $own={isOwn}>
155163
<BubbleMeta $own={isOwn}>{authorName}</BubbleMeta>
156164
<Bubble $own={isOwn}>{text}</Bubble>
157165
{timestamp > 0 && (
@@ -162,11 +170,26 @@ export const MessageList = React.memo((props: MessageListProps) => {
162170
})}
163171
</BubbleTime>
164172
)}
165-
</div>
173+
</MessageWrapper>
166174
);
167175
})
168176
)}
169177

178+
{/* AI thinking animation — shown to all users when the LLM is generating */}
179+
{isAiThinking && (
180+
<AiBubbleWrapper>
181+
<AiBadge>
182+
<RobotOutlined style={{ fontSize: 9 }} />
183+
AI is thinking…
184+
</AiBadge>
185+
<LlmLoadingBubble>
186+
<span />
187+
<span />
188+
<span />
189+
</LlmLoadingBubble>
190+
</AiBubbleWrapper>
191+
)}
192+
170193
{typingUsers.length > 0 && (
171194
<TypingIndicatorWrapper>
172195
<TypingDots>
@@ -182,7 +205,6 @@ export const MessageList = React.memo((props: MessageListProps) => {
182205
</TypingIndicatorWrapper>
183206
)}
184207

185-
<div ref={bottomRef} />
186208
</MessagesArea>
187209
);
188210
});

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/store/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type {
2+
AiThinkingState,
23
ChatMessage,
34
ChatRoom,
45
PendingRoomInvite,
@@ -19,5 +20,4 @@ export {
1920
useRoom,
2021
useConnection,
2122
useDoc,
22-
pluvConfig,
2323
} from "./pluvClient";

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/store/pluvClient.ts

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,41 @@ import { yjs } from "@pluv/crdt-yjs";
33
import { createBundle } from "@pluv/react";
44
import { z } from "zod";
55

6-
/**
7-
* Module-level config updated by ChatControllerV2 before connecting.
8-
* Allows dynamic auth without recreating the client.
9-
*/
10-
export const pluvConfig = {
11-
userId: "",
12-
userName: "",
13-
authUrl: "/api/auth/pluv",
14-
publicKey: "",
15-
};
6+
// Resolve the pluv.io publishable key from the environment.
7+
// This is set at build time via VITE_PLUV_PUBLIC_KEY, or injected at runtime
8+
// via globalThis.__PLUV_PUBLIC_KEY__ (e.g. from a server-rendered template).
9+
const PLUV_PUBLIC_KEY: string =
10+
(typeof import.meta !== "undefined" && (import.meta as any).env?.VITE_PLUV_PUBLIC_KEY) ||
11+
(typeof globalThis !== "undefined" && (globalThis as any).__PLUV_PUBLIC_KEY__) ||
12+
"";
1613

17-
function resolvePluvPublicKey(): string {
18-
return (
19-
pluvConfig.publicKey ||
20-
(typeof globalThis !== "undefined"
21-
? (globalThis as any).__PLUV_PUBLIC_KEY__
22-
: "") ||
23-
(typeof import.meta !== "undefined"
24-
? (import.meta as any).env?.VITE_PLUV_PUBLIC_KEY
25-
: "") ||
26-
""
27-
);
28-
}
14+
// Auth server URL. Defaults to a relative path so the Vite dev proxy and
15+
// production reverse-proxy both work without extra configuration.
16+
const PLUV_AUTH_URL: string =
17+
(typeof import.meta !== "undefined" && (import.meta as any).env?.VITE_PLUV_AUTH_URL) ||
18+
"/api/auth/pluv";
2919

20+
// `metadata` is PLUV's built-in mechanism for passing per-connection data
21+
// (like the current user) into the authEndpoint at the moment a room is
22+
// entered. It is provided as a prop on <PluvRoomProvider metadata={...} />,
23+
// so there is no need for any global mutable config object.
3024
const client = createClient({
31-
authEndpoint: (({ room }: { room: string }) => {
25+
metadata: z.object({
26+
userId: z.string(),
27+
userName: z.string(),
28+
}),
29+
publicKey: PLUV_PUBLIC_KEY,
30+
authEndpoint: ({ room, metadata }: { room: string; metadata: { userId: string; userName: string } }) => {
3231
const params = new URLSearchParams({
3332
room,
34-
userId: pluvConfig.userId,
35-
userName: pluvConfig.userName,
33+
userId: metadata.userId,
34+
userName: metadata.userName,
3635
});
37-
return `${pluvConfig.authUrl}?${params}`;
38-
}) as any,
39-
publicKey: resolvePluvPublicKey as any,
36+
return `${PLUV_AUTH_URL}?${params}`;
37+
},
4038
initialStorage: yjs.doc((t: any) => ({
4139
messageActivity: t.map("messageActivity", []),
40+
aiActivity: t.map("aiActivity", []),
4241
})),
4342
presence: z.object({
4443
userId: z.string(),

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/store/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ export interface MessageBroadcast {
5050
counter: number;
5151
}
5252

53+
export interface AiThinkingState {
54+
roomId: string;
55+
isThinking: boolean;
56+
timestamp: number;
57+
}
58+
5359
export const LLM_BOT_AUTHOR_ID = "__llm_bot__";
5460

5561
export function uid(): string {

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/styles.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,16 @@ export const MessagesArea = styled.div`
9191
gap: 8px;
9292
`;
9393

94-
export const Bubble = styled.div<{ $own: boolean }>`
94+
export const MessageWrapper = styled.div<{ $own: boolean }>`
95+
display: flex;
96+
flex-direction: column;
97+
align-self: ${(p) => (p.$own ? "flex-end" : "flex-start")};
9598
max-width: 70%;
99+
`;
100+
101+
export const Bubble = styled.div<{ $own: boolean }>`
96102
padding: 10px 14px;
97-
border-radius: 16px;
98-
align-self: ${(p) => (p.$own ? "flex-end" : "flex-start")};
103+
border-radius: ${(p) => (p.$own ? "16px 16px 4px 16px" : "16px 16px 16px 4px")};
99104
background: ${(p) => (p.$own ? "#1890ff" : "#f0f0f0")};
100105
color: ${(p) => (p.$own ? "#fff" : "#333")};
101106
font-size: 14px;

0 commit comments

Comments
 (0)