@admin-layout/tailwind-chat-pro 12.2.4-alpha.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.d.ts.map +1 -0
- package/lib/components/messages-container-ui/DefaultInputToolBar.d.ts +21 -0
- package/lib/components/messages-container-ui/DefaultInputToolBar.d.ts.map +1 -0
- package/lib/components/messages-container-ui/DefaultInputToolBar.js +46 -0
- package/lib/components/messages-container-ui/DefaultInputToolBar.js.map +1 -0
- package/lib/components/messages-container-ui/DefaultMessageGroup.d.ts +16 -0
- package/lib/components/messages-container-ui/DefaultMessageGroup.d.ts.map +1 -0
- package/lib/components/messages-container-ui/DefaultMessageGroup.js +64 -0
- package/lib/components/messages-container-ui/DefaultMessageGroup.js.map +1 -0
- package/lib/components/messages-container-ui/MessagesContainerUI.d.ts +98 -0
- package/lib/components/messages-container-ui/MessagesContainerUI.d.ts.map +1 -0
- package/lib/components/messages-container-ui/MessagesContainerUI.js +88 -0
- package/lib/components/messages-container-ui/MessagesContainerUI.js.map +1 -0
- package/lib/components/messages-container-ui/PlanModeView.d.ts +107 -0
- package/lib/components/messages-container-ui/PlanModeView.d.ts.map +1 -0
- package/lib/components/messages-container-ui/PlanModeView.js +294 -0
- package/lib/components/messages-container-ui/PlanModeView.js.map +1 -0
- package/lib/components/messages-container-ui/index.d.ts +9 -0
- package/lib/components/messages-container-ui/index.d.ts.map +1 -0
- package/lib/components/messages-container-ui/types.d.ts +63 -0
- package/lib/components/messages-container-ui/types.d.ts.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -0
- package/lib/routes.json +1 -0
- package/package.json +42 -0
- package/rollup.config.mjs +34 -0
- package/src/components/index.ts +1 -0
- package/src/components/messages-container-ui/DefaultInputToolBar.tsx +63 -0
- package/src/components/messages-container-ui/DefaultMessageGroup.tsx +75 -0
- package/src/components/messages-container-ui/MessagesContainerUI.tsx +178 -0
- package/src/components/messages-container-ui/PlanModeView.tsx +457 -0
- package/src/components/messages-container-ui/index.ts +8 -0
- package/src/components/messages-container-ui/types.ts +68 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { PlanModeView } from './PlanModeView';
|
|
4
|
+
import type { PlanMessage, MessageGroupComponentType } from './types';
|
|
5
|
+
import type { PlanModeViewInputToolbarProps } from './PlanModeView';
|
|
6
|
+
|
|
7
|
+
export interface MessagesContainerUIProps {
|
|
8
|
+
messages: PlanMessage[];
|
|
9
|
+
streamingContent?: string;
|
|
10
|
+
currentUser: { id: string; name?: string; avatar?: string; [key: string]: any };
|
|
11
|
+
onSend: (text: string) => void;
|
|
12
|
+
/** Optional classes applied to the container root (useful for width/padding tweaks). */
|
|
13
|
+
className?: string;
|
|
14
|
+
/** Optional ref for the scroll container inside PlanModeView. */
|
|
15
|
+
scrollContainerRef?: React.Ref<HTMLDivElement>;
|
|
16
|
+
/** Optional className applied to PlanModeView root wrapper. */
|
|
17
|
+
planModeViewClassName?: string;
|
|
18
|
+
/** Optional className applied to PlanModeView scroll container element. */
|
|
19
|
+
planModeScrollContainerClassName?: string;
|
|
20
|
+
/** When true, hide scrollbar visuals but keep it scrollable. */
|
|
21
|
+
hideScrollbar?: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
isLoading?: boolean;
|
|
24
|
+
onStop?: () => void;
|
|
25
|
+
showDate?: boolean;
|
|
26
|
+
showAvatar?: boolean;
|
|
27
|
+
showMessageBubbleBorder?: boolean;
|
|
28
|
+
/** When false (default), do NOT create an internal scroll container (avoids nested scroll). */
|
|
29
|
+
useInternalScroll?: boolean;
|
|
30
|
+
/** When true, the built-in input toolbar area is not rendered. */
|
|
31
|
+
hideInputToolbar?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Where to place the input toolbar.
|
|
34
|
+
* - `bottom` (default): render toolbar in the bottom area.
|
|
35
|
+
* - `inlineWhenEmpty`: when there are no messages and no streaming/loading, render the toolbar inline
|
|
36
|
+
* inside the empty state (matches landing UI), otherwise use bottom placement.
|
|
37
|
+
*/
|
|
38
|
+
inputPlacement?: 'bottom' | 'inlineWhenEmpty';
|
|
39
|
+
/**
|
|
40
|
+
* Optional: render the input composer; receives value, onChange, onSend, disabled.
|
|
41
|
+
* Defaults to `@admin-layout/tailwind-ui`'s `InputToolBar`, so the base works out of the box.
|
|
42
|
+
*/
|
|
43
|
+
renderInputToolbar?: (props: PlanModeViewInputToolbarProps) => React.ReactNode;
|
|
44
|
+
/** Placeholder for the default input composer (ignored when `renderInputToolbar` is provided). */
|
|
45
|
+
inputPlaceholder?: string;
|
|
46
|
+
/** Optional: passed to the injected message-group renderer */
|
|
47
|
+
onOpen?: (element?: any) => void;
|
|
48
|
+
onMessageClick?: (msg: any) => void;
|
|
49
|
+
isDesktopView?: boolean;
|
|
50
|
+
isSmallScreen?: boolean;
|
|
51
|
+
sandboxErrors?: any[];
|
|
52
|
+
currentFiles?: Record<string, string>;
|
|
53
|
+
onFixError?: (errorMessage: string) => Promise<void>;
|
|
54
|
+
/** Optional: error message to show above messages; use with onRetry for retry button */
|
|
55
|
+
error?: string | null;
|
|
56
|
+
/** Optional: called when user clicks retry (e.g. after error) */
|
|
57
|
+
onRetry?: () => void;
|
|
58
|
+
/**
|
|
59
|
+
* Optional: custom content when there are no messages and no streaming/loading.
|
|
60
|
+
* Receives input toolbar props so you can render the composer inline (e.g. landing UI).
|
|
61
|
+
*/
|
|
62
|
+
renderEmptyState?: (ctx: { inputToolbarProps: PlanModeViewInputToolbarProps }) => React.ReactNode;
|
|
63
|
+
/**
|
|
64
|
+
* Optional: when provided, scroll to bottom whenever these deps change (e.g. [streamingContent, messages.length]).
|
|
65
|
+
* Use from the parent to keep the view at bottom on each stream chunk.
|
|
66
|
+
*/
|
|
67
|
+
scrollToBottomDeps?: React.DependencyList;
|
|
68
|
+
/** Optional: className applied to the content wrapper inside PlanModeView. */
|
|
69
|
+
contentClassName?: string;
|
|
70
|
+
/** Optional: className applied to the post container inside PlanModeView. */
|
|
71
|
+
postContainerClassName?: string;
|
|
72
|
+
/** When true, code blocks show copy and download actions (default: true). */
|
|
73
|
+
showCopyDownloadOnCodeBlock?: boolean;
|
|
74
|
+
|
|
75
|
+
// ----- Customization (the "base, customizable" surface) -----
|
|
76
|
+
/**
|
|
77
|
+
* The message-group renderer. Defaults to a plain-text bubble renderer so the base
|
|
78
|
+
* works standalone. Inject a rich renderer (e.g. messenger-box's
|
|
79
|
+
* `ModernMessageGroupComponent`) for markdown / code / widget-card rendering.
|
|
80
|
+
*/
|
|
81
|
+
MessageGroupComponent?: MessageGroupComponentType;
|
|
82
|
+
/** Optional: how to render the in-flight streaming assistant content (default: plain text). */
|
|
83
|
+
renderStreamingContent?: (content: string) => React.ReactNode;
|
|
84
|
+
/** Optional: override how a PlanMessage is mapped to the post shape handed to the renderer. */
|
|
85
|
+
mapMessageToPost?: (msg: PlanMessage, currentUser: MessagesContainerUIProps['currentUser']) => any;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* MessagesContainerUI — a customizable base chat container for plan mode
|
|
90
|
+
* (chat / search / research). Owns the layout, scrolling, streaming area,
|
|
91
|
+
* empty / error / loading states and the input-toolbar slot. The actual message
|
|
92
|
+
* rendering is delegated to an injectable `MessageGroupComponent` (defaults to a
|
|
93
|
+
* plain-text bubble renderer), so consumers plug in their own rich renderer.
|
|
94
|
+
*/
|
|
95
|
+
export function MessagesContainerUI({
|
|
96
|
+
messages,
|
|
97
|
+
streamingContent,
|
|
98
|
+
currentUser,
|
|
99
|
+
onSend,
|
|
100
|
+
className,
|
|
101
|
+
scrollContainerRef,
|
|
102
|
+
planModeViewClassName,
|
|
103
|
+
planModeScrollContainerClassName,
|
|
104
|
+
hideScrollbar = false,
|
|
105
|
+
disabled = false,
|
|
106
|
+
isLoading = false,
|
|
107
|
+
onStop,
|
|
108
|
+
showDate = false,
|
|
109
|
+
showAvatar = false,
|
|
110
|
+
showMessageBubbleBorder = false,
|
|
111
|
+
useInternalScroll = false,
|
|
112
|
+
hideInputToolbar = false,
|
|
113
|
+
inputPlacement = 'bottom',
|
|
114
|
+
renderInputToolbar,
|
|
115
|
+
inputPlaceholder,
|
|
116
|
+
onOpen,
|
|
117
|
+
onMessageClick,
|
|
118
|
+
isDesktopView = false,
|
|
119
|
+
isSmallScreen = false,
|
|
120
|
+
sandboxErrors,
|
|
121
|
+
currentFiles,
|
|
122
|
+
onFixError,
|
|
123
|
+
error = null,
|
|
124
|
+
onRetry,
|
|
125
|
+
renderEmptyState,
|
|
126
|
+
scrollToBottomDeps,
|
|
127
|
+
contentClassName,
|
|
128
|
+
postContainerClassName,
|
|
129
|
+
showCopyDownloadOnCodeBlock = true,
|
|
130
|
+
MessageGroupComponent,
|
|
131
|
+
renderStreamingContent,
|
|
132
|
+
mapMessageToPost,
|
|
133
|
+
}: MessagesContainerUIProps) {
|
|
134
|
+
return (
|
|
135
|
+
<div className={clsx('flex flex-col flex-1 min-h-0', className)}>
|
|
136
|
+
<PlanModeView
|
|
137
|
+
messages={messages}
|
|
138
|
+
streamingContent={streamingContent}
|
|
139
|
+
currentUser={currentUser}
|
|
140
|
+
onSend={onSend}
|
|
141
|
+
disabled={disabled}
|
|
142
|
+
isLoading={isLoading}
|
|
143
|
+
onStop={onStop}
|
|
144
|
+
showDate={showDate}
|
|
145
|
+
showAvatar={showAvatar}
|
|
146
|
+
showMessageBubbleBorder={showMessageBubbleBorder}
|
|
147
|
+
useInternalScroll={useInternalScroll}
|
|
148
|
+
scrollContainerRef={scrollContainerRef}
|
|
149
|
+
className={planModeViewClassName}
|
|
150
|
+
scrollContainerClassName={planModeScrollContainerClassName}
|
|
151
|
+
hideScrollbar={hideScrollbar}
|
|
152
|
+
hideInputToolbar={hideInputToolbar}
|
|
153
|
+
inputPlacement={inputPlacement}
|
|
154
|
+
renderInputToolbar={renderInputToolbar}
|
|
155
|
+
inputPlaceholder={inputPlaceholder}
|
|
156
|
+
onOpen={onOpen}
|
|
157
|
+
onMessageClick={onMessageClick}
|
|
158
|
+
isDesktopView={isDesktopView}
|
|
159
|
+
isSmallScreen={isSmallScreen}
|
|
160
|
+
sandboxErrors={sandboxErrors}
|
|
161
|
+
currentFiles={currentFiles}
|
|
162
|
+
onFixError={onFixError}
|
|
163
|
+
error={error}
|
|
164
|
+
onRetry={onRetry}
|
|
165
|
+
renderEmptyState={renderEmptyState}
|
|
166
|
+
scrollToBottomDeps={scrollToBottomDeps}
|
|
167
|
+
contentClassName={contentClassName}
|
|
168
|
+
postContainerClassName={postContainerClassName}
|
|
169
|
+
showCopyDownloadOnCodeBlock={showCopyDownloadOnCodeBlock}
|
|
170
|
+
MessageGroupComponent={MessageGroupComponent}
|
|
171
|
+
renderStreamingContent={renderStreamingContent}
|
|
172
|
+
mapMessageToPost={mapMessageToPost}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export default MessagesContainerUI;
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { AiAgentMessageRole, PostTypeEnum } from 'common';
|
|
4
|
+
import type { MessageGroupComponentType, PlanMessage } from './types';
|
|
5
|
+
import { DefaultMessageGroup } from './DefaultMessageGroup';
|
|
6
|
+
import { DefaultInputToolBar } from './DefaultInputToolBar';
|
|
7
|
+
|
|
8
|
+
const ASSISTANT_BUBBLE_BG = 'bg-white';
|
|
9
|
+
const BORDER_COLOR = 'border-gray-200';
|
|
10
|
+
|
|
11
|
+
const HIDE_SCROLLBAR_CSS = `
|
|
12
|
+
/* Hide scrollbar while keeping scrollability */
|
|
13
|
+
.ym-hide-scrollbar {
|
|
14
|
+
-ms-overflow-style: none; /* IE/Edge */
|
|
15
|
+
scrollbar-width: none; /* Firefox */
|
|
16
|
+
}
|
|
17
|
+
.ym-hide-scrollbar::-webkit-scrollbar {
|
|
18
|
+
display: none; /* Chrome/Safari */
|
|
19
|
+
width: 0;
|
|
20
|
+
height: 0;
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const DEFAULT_ASSISTANT_AUTHOR = {
|
|
25
|
+
id: 'ai-assistant',
|
|
26
|
+
givenName: 'AI',
|
|
27
|
+
familyName: 'Assistant',
|
|
28
|
+
fullName: 'AI Assistant',
|
|
29
|
+
username: 'ai-assistant',
|
|
30
|
+
email: 'ai@assistant.com',
|
|
31
|
+
picture: null,
|
|
32
|
+
alias: [],
|
|
33
|
+
tokens: [],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface PlanModeViewInputToolbarProps {
|
|
37
|
+
value: string;
|
|
38
|
+
onChange: (text: string) => void;
|
|
39
|
+
onSend: () => void;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PlanModeViewProps {
|
|
44
|
+
messages: PlanMessage[];
|
|
45
|
+
streamingContent?: string;
|
|
46
|
+
currentUser: { id: string; name?: string; avatar?: string; [key: string]: any };
|
|
47
|
+
onSend: (text: string) => void;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
isLoading?: boolean;
|
|
50
|
+
onStop?: () => void;
|
|
51
|
+
showDate?: boolean;
|
|
52
|
+
showAvatar?: boolean;
|
|
53
|
+
showMessageBubbleBorder?: boolean;
|
|
54
|
+
/** When false (default), do NOT create an internal scroll container (avoids nested scroll). */
|
|
55
|
+
useInternalScroll?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Optional ref for the scroll container element (defaults to the internal
|
|
58
|
+
* message list wrapper).
|
|
59
|
+
*
|
|
60
|
+
* If provided, it will be attached to the internal scroll container div
|
|
61
|
+
* (so parent code can read/scroll it).
|
|
62
|
+
*/
|
|
63
|
+
scrollContainerRef?: React.Ref<HTMLDivElement>;
|
|
64
|
+
/** Optional className applied to the scroll container element. */
|
|
65
|
+
scrollContainerClassName?: string;
|
|
66
|
+
/** When true, hide the scrollbar visuals but keep it scrollable. */
|
|
67
|
+
hideScrollbar?: boolean;
|
|
68
|
+
contentClassName?: string;
|
|
69
|
+
postContainerClassName?: string;
|
|
70
|
+
|
|
71
|
+
/** Optional className applied to the root wrapper element. */
|
|
72
|
+
className?: string;
|
|
73
|
+
/** When true, the built-in input toolbar area is not rendered. */
|
|
74
|
+
hideInputToolbar?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Where to place the input toolbar.
|
|
77
|
+
* - `bottom` (default): render toolbar in the bottom area.
|
|
78
|
+
* - `inlineWhenEmpty`: when there are no messages and no streaming/loading, render the toolbar inline
|
|
79
|
+
* inside the empty state (matches landing UI), otherwise use bottom placement.
|
|
80
|
+
*/
|
|
81
|
+
inputPlacement?: 'bottom' | 'inlineWhenEmpty';
|
|
82
|
+
/**
|
|
83
|
+
* Optional: render the input toolbar; receives value, onChange, onSend, disabled.
|
|
84
|
+
* Defaults to {@link DefaultInputToolBar} (which wraps `@admin-layout/tailwind-ui`'s
|
|
85
|
+
* `InputToolBar`), so the base ships a working composer out of the box.
|
|
86
|
+
*/
|
|
87
|
+
renderInputToolbar?: (props: PlanModeViewInputToolbarProps) => React.ReactNode;
|
|
88
|
+
/** Placeholder for the default input toolbar (ignored when `renderInputToolbar` is provided). */
|
|
89
|
+
inputPlaceholder?: string;
|
|
90
|
+
/** Optional: passed to the injected message-group renderer */
|
|
91
|
+
onOpen?: (element?: any) => void;
|
|
92
|
+
onMessageClick?: (msg: any) => void;
|
|
93
|
+
isDesktopView?: boolean;
|
|
94
|
+
isSmallScreen?: boolean;
|
|
95
|
+
sandboxErrors?: any[];
|
|
96
|
+
currentFiles?: Record<string, string>;
|
|
97
|
+
onFixError?: (errorMessage: string) => Promise<void>;
|
|
98
|
+
/** Optional: error message to show above messages; use with onRetry for retry button */
|
|
99
|
+
error?: string | null;
|
|
100
|
+
/** Optional: called when user clicks retry (e.g. after error) */
|
|
101
|
+
onRetry?: () => void;
|
|
102
|
+
/**
|
|
103
|
+
* Optional: custom content when there are no messages and no streaming/loading.
|
|
104
|
+
* Receives input toolbar props so you can render the composer inline (e.g. landing UI).
|
|
105
|
+
*/
|
|
106
|
+
renderEmptyState?: (ctx: { inputToolbarProps: PlanModeViewInputToolbarProps }) => React.ReactNode;
|
|
107
|
+
/**
|
|
108
|
+
* Optional: when provided, scroll to bottom whenever these deps change (e.g. [streamingContent, messages.length]).
|
|
109
|
+
* Lets the parent force scroll on each stream chunk when the internal ResizeObserver/layout effect miss.
|
|
110
|
+
*/
|
|
111
|
+
scrollToBottomDeps?: React.DependencyList;
|
|
112
|
+
/** When true, fenced / raw code blocks show copy and download actions (default: true). */
|
|
113
|
+
showCopyDownloadOnCodeBlock?: boolean;
|
|
114
|
+
|
|
115
|
+
// ----- Customization (the "base, customizable" surface) -----
|
|
116
|
+
/**
|
|
117
|
+
* The message-group renderer. Each rendered group receives a single mapped post.
|
|
118
|
+
* Defaults to {@link DefaultMessageGroup} (plain-text bubbles) so the base works
|
|
119
|
+
* standalone. Inject a rich renderer (e.g. messenger-box's
|
|
120
|
+
* `ModernMessageGroupComponent`) to get markdown / code / widget-card rendering.
|
|
121
|
+
*/
|
|
122
|
+
MessageGroupComponent?: MessageGroupComponentType;
|
|
123
|
+
/**
|
|
124
|
+
* Optional: how to render the in-flight streaming assistant content. Defaults to a
|
|
125
|
+
* plain-text bubble. Inject a richer renderer (markdown / code) if desired.
|
|
126
|
+
*/
|
|
127
|
+
renderStreamingContent?: (content: string) => React.ReactNode;
|
|
128
|
+
/**
|
|
129
|
+
* Optional: override how a PlanMessage is mapped to the post shape handed to
|
|
130
|
+
* `MessageGroupComponent`. Defaults to {@link defaultPlanMessageToPost}.
|
|
131
|
+
*/
|
|
132
|
+
mapMessageToPost?: (msg: PlanMessage, currentUser: PlanModeViewProps['currentUser']) => any;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Default mapping of a PlanMessage to the post-like shape expected by message-group renderers. */
|
|
136
|
+
export function defaultPlanMessageToPost(msg: PlanMessage, currentUser: PlanModeViewProps['currentUser']): any {
|
|
137
|
+
const isUser = msg.role === 'user';
|
|
138
|
+
const author =
|
|
139
|
+
msg.author ??
|
|
140
|
+
(isUser
|
|
141
|
+
? {
|
|
142
|
+
id: currentUser.id,
|
|
143
|
+
givenName: currentUser.givenName ?? currentUser.name ?? 'User',
|
|
144
|
+
familyName: currentUser.familyName ?? '',
|
|
145
|
+
fullName: currentUser.fullName ?? currentUser.name ?? 'User',
|
|
146
|
+
username: currentUser.username ?? currentUser.id,
|
|
147
|
+
email: currentUser.email ?? '',
|
|
148
|
+
picture: currentUser.picture ?? currentUser.avatar ?? null,
|
|
149
|
+
alias: currentUser.alias ?? [],
|
|
150
|
+
tokens: currentUser.tokens ?? [],
|
|
151
|
+
}
|
|
152
|
+
: DEFAULT_ASSISTANT_AUTHOR);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
id: msg.id,
|
|
156
|
+
message: msg.content,
|
|
157
|
+
author,
|
|
158
|
+
createdAt: msg.createdAt ?? new Date().toISOString(),
|
|
159
|
+
type: msg.type ?? PostTypeEnum.Aiassistant,
|
|
160
|
+
isDelivered: false,
|
|
161
|
+
isRead: false,
|
|
162
|
+
parentId: null,
|
|
163
|
+
fromServer: null,
|
|
164
|
+
updatedAt: msg.createdAt ?? new Date().toISOString(),
|
|
165
|
+
propsConfiguration: {
|
|
166
|
+
...(msg.propsConfiguration ?? { contents: {} }),
|
|
167
|
+
contents: {
|
|
168
|
+
...((msg.propsConfiguration as any)?.contents ?? {}),
|
|
169
|
+
role:
|
|
170
|
+
(msg.propsConfiguration as any)?.contents?.role ??
|
|
171
|
+
(isUser ? AiAgentMessageRole.User : AiAgentMessageRole.Assistant),
|
|
172
|
+
// Plan/build mode: consumers (e.g. ModernMessageBubble) read propsConfiguration.contents.mode
|
|
173
|
+
...((msg.props as { mode?: string } | undefined)?.mode != null
|
|
174
|
+
? { mode: (msg.props as { mode?: string }).mode }
|
|
175
|
+
: {}),
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
props: msg.props ?? {},
|
|
179
|
+
files: msg.files ?? [],
|
|
180
|
+
replies: null,
|
|
181
|
+
channel: null,
|
|
182
|
+
isPinned: false,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function PlanModeView({
|
|
187
|
+
messages,
|
|
188
|
+
streamingContent,
|
|
189
|
+
currentUser,
|
|
190
|
+
onSend,
|
|
191
|
+
disabled = false,
|
|
192
|
+
isLoading = false,
|
|
193
|
+
onStop,
|
|
194
|
+
showDate = false,
|
|
195
|
+
showAvatar = false,
|
|
196
|
+
showMessageBubbleBorder = false,
|
|
197
|
+
useInternalScroll = false,
|
|
198
|
+
scrollContainerRef,
|
|
199
|
+
scrollContainerClassName,
|
|
200
|
+
hideScrollbar = false,
|
|
201
|
+
contentClassName,
|
|
202
|
+
postContainerClassName,
|
|
203
|
+
className,
|
|
204
|
+
hideInputToolbar = false,
|
|
205
|
+
inputPlacement = 'bottom',
|
|
206
|
+
renderInputToolbar,
|
|
207
|
+
inputPlaceholder,
|
|
208
|
+
onOpen = () => {},
|
|
209
|
+
onMessageClick = () => {},
|
|
210
|
+
isDesktopView = false,
|
|
211
|
+
isSmallScreen = false,
|
|
212
|
+
sandboxErrors = [],
|
|
213
|
+
currentFiles = {},
|
|
214
|
+
onFixError,
|
|
215
|
+
error = null,
|
|
216
|
+
onRetry,
|
|
217
|
+
renderEmptyState,
|
|
218
|
+
scrollToBottomDeps,
|
|
219
|
+
showCopyDownloadOnCodeBlock = true,
|
|
220
|
+
MessageGroupComponent = DefaultMessageGroup,
|
|
221
|
+
renderStreamingContent,
|
|
222
|
+
mapMessageToPost = defaultPlanMessageToPost,
|
|
223
|
+
}: PlanModeViewProps) {
|
|
224
|
+
const [inputValue, setInputValue] = useState('');
|
|
225
|
+
const internalListRef = useRef<HTMLDivElement>(null);
|
|
226
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
227
|
+
const shouldAutoScrollRef = useRef(true);
|
|
228
|
+
|
|
229
|
+
const setScrollContainerRef = useCallback(
|
|
230
|
+
(node: HTMLDivElement | null) => {
|
|
231
|
+
internalListRef.current = node;
|
|
232
|
+
|
|
233
|
+
if (!scrollContainerRef) return;
|
|
234
|
+
if (typeof scrollContainerRef === 'function') {
|
|
235
|
+
scrollContainerRef(node);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// RefObject / MutableRefObject
|
|
240
|
+
(scrollContainerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
241
|
+
},
|
|
242
|
+
[scrollContainerRef],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const scrollToBottom = useCallback(() => {
|
|
246
|
+
const el = internalListRef.current;
|
|
247
|
+
if (el && shouldAutoScrollRef.current) {
|
|
248
|
+
el.scrollTop = el.scrollHeight;
|
|
249
|
+
}
|
|
250
|
+
}, []);
|
|
251
|
+
|
|
252
|
+
const handleSendFromInput = useCallback(() => {
|
|
253
|
+
const t = inputValue.trim();
|
|
254
|
+
if (!t) return;
|
|
255
|
+
onSend(t);
|
|
256
|
+
setInputValue('');
|
|
257
|
+
}, [inputValue, onSend]);
|
|
258
|
+
|
|
259
|
+
const inputToolbarProps: PlanModeViewInputToolbarProps = {
|
|
260
|
+
value: inputValue,
|
|
261
|
+
onChange: setInputValue,
|
|
262
|
+
onSend: handleSendFromInput,
|
|
263
|
+
disabled: disabled || isLoading,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Default the input composer to @admin-layout/tailwind-ui's InputToolBar (the shared
|
|
267
|
+
// "ai-tool-box"); consumers can override with `renderInputToolbar` for a richer composer.
|
|
268
|
+
const renderToolbar =
|
|
269
|
+
renderInputToolbar ??
|
|
270
|
+
((p: PlanModeViewInputToolbarProps) => (
|
|
271
|
+
<DefaultInputToolBar {...p} isLoading={isLoading} onStop={onStop} placeholder={inputPlaceholder} />
|
|
272
|
+
));
|
|
273
|
+
|
|
274
|
+
const currentUserForGroup = useMemo(
|
|
275
|
+
() => ({
|
|
276
|
+
id: currentUser.id,
|
|
277
|
+
givenName: currentUser.givenName ?? currentUser.name ?? 'You',
|
|
278
|
+
familyName: currentUser.familyName ?? '',
|
|
279
|
+
fullName: currentUser.fullName ?? currentUser.name ?? 'You',
|
|
280
|
+
username: currentUser.username ?? currentUser.id,
|
|
281
|
+
email: currentUser.email ?? '',
|
|
282
|
+
picture: currentUser.picture ?? currentUser.avatar ?? null,
|
|
283
|
+
alias: currentUser.alias ?? [],
|
|
284
|
+
tokens: currentUser.tokens ?? [],
|
|
285
|
+
}),
|
|
286
|
+
[currentUser],
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const posts = useMemo(
|
|
290
|
+
() => messages.map((m) => mapMessageToPost(m, currentUser)),
|
|
291
|
+
[messages, currentUser, mapMessageToPost],
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const isEmpty = posts.length === 0 && !streamingContent?.trim() && !isLoading;
|
|
295
|
+
const shouldInlineToolbar = inputPlacement === 'inlineWhenEmpty' && isEmpty;
|
|
296
|
+
const shouldRenderBottomToolbar = !hideInputToolbar && !shouldInlineToolbar;
|
|
297
|
+
|
|
298
|
+
// Auto-scroll to bottom while user stays near bottom.
|
|
299
|
+
useLayoutEffect(() => {
|
|
300
|
+
if (!useInternalScroll) return;
|
|
301
|
+
if (!shouldAutoScrollRef.current) return;
|
|
302
|
+
scrollToBottom();
|
|
303
|
+
const id = requestAnimationFrame(() => {
|
|
304
|
+
scrollToBottom();
|
|
305
|
+
requestAnimationFrame(scrollToBottom);
|
|
306
|
+
});
|
|
307
|
+
return () => cancelAnimationFrame(id);
|
|
308
|
+
}, [useInternalScroll, scrollToBottom, posts.length, streamingContent, isLoading]);
|
|
309
|
+
|
|
310
|
+
// When internal scroll is (re)enabled, force follow-to-bottom.
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (!useInternalScroll) return;
|
|
313
|
+
shouldAutoScrollRef.current = true;
|
|
314
|
+
scrollToBottom();
|
|
315
|
+
}, [useInternalScroll, scrollToBottom]);
|
|
316
|
+
|
|
317
|
+
// When inner content grows (stream chunks, markdown expand), scroll to bottom if user was near bottom.
|
|
318
|
+
// Observing the content wrapper ensures we react to content height changes; the scroll container size often does not change.
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (!useInternalScroll) return;
|
|
321
|
+
const el = contentRef.current;
|
|
322
|
+
if (!el) return;
|
|
323
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
324
|
+
|
|
325
|
+
const ro = new ResizeObserver(() => {
|
|
326
|
+
if (!shouldAutoScrollRef.current) return;
|
|
327
|
+
scrollToBottom();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
ro.observe(el);
|
|
331
|
+
return () => ro.disconnect();
|
|
332
|
+
}, [useInternalScroll, scrollToBottom]);
|
|
333
|
+
|
|
334
|
+
// Parent-controlled scroll: when scrollToBottomDeps change (e.g. each stream chunk), force scroll to bottom.
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (!useInternalScroll || !scrollToBottomDeps?.length) return;
|
|
337
|
+
scrollToBottom();
|
|
338
|
+
const id = requestAnimationFrame(() => {
|
|
339
|
+
scrollToBottom();
|
|
340
|
+
});
|
|
341
|
+
return () => cancelAnimationFrame(id);
|
|
342
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: parent passes deps
|
|
343
|
+
}, scrollToBottomDeps ?? []);
|
|
344
|
+
|
|
345
|
+
const handleScroll = useCallback(() => {
|
|
346
|
+
const el = internalListRef.current;
|
|
347
|
+
if (!el) return;
|
|
348
|
+
const thresholdPx = 160;
|
|
349
|
+
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
350
|
+
shouldAutoScrollRef.current = distanceFromBottom <= thresholdPx;
|
|
351
|
+
}, []);
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div className={clsx('flex flex-col flex-1 min-h-0 overflow-hidden', className)}>
|
|
355
|
+
{useInternalScroll && hideScrollbar && <style>{HIDE_SCROLLBAR_CSS}</style>}
|
|
356
|
+
<div
|
|
357
|
+
ref={setScrollContainerRef}
|
|
358
|
+
onScroll={useInternalScroll ? handleScroll : undefined}
|
|
359
|
+
className={
|
|
360
|
+
useInternalScroll
|
|
361
|
+
? clsx(
|
|
362
|
+
'flex-1 min-h-0 overflow-y-auto',
|
|
363
|
+
hideScrollbar && 'ym-hide-scrollbar',
|
|
364
|
+
scrollContainerClassName,
|
|
365
|
+
)
|
|
366
|
+
: (scrollContainerClassName ?? '')
|
|
367
|
+
}
|
|
368
|
+
>
|
|
369
|
+
<div ref={contentRef} className={clsx('w-full pb-4 pt-2 space-y-3 px-4', contentClassName ?? '')}>
|
|
370
|
+
{error && (
|
|
371
|
+
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 flex items-center justify-between gap-2">
|
|
372
|
+
<span className="text-sm text-red-800">{error}</span>
|
|
373
|
+
{onRetry && (
|
|
374
|
+
<button
|
|
375
|
+
type="button"
|
|
376
|
+
onClick={onRetry}
|
|
377
|
+
className="shrink-0 px-3 py-1.5 text-sm font-medium text-red-700 hover:bg-red-100 rounded-md transition-colors"
|
|
378
|
+
>
|
|
379
|
+
Retry
|
|
380
|
+
</button>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
{isEmpty && (
|
|
385
|
+
<div className="px-4 py-8 text-center">
|
|
386
|
+
{renderEmptyState ? (
|
|
387
|
+
renderEmptyState({ inputToolbarProps })
|
|
388
|
+
) : (
|
|
389
|
+
<>
|
|
390
|
+
<div className="text-gray-500 text-lg font-medium mb-2">
|
|
391
|
+
Describe your idea and I'll help you create it step by step
|
|
392
|
+
</div>
|
|
393
|
+
<div className="text-gray-400 text-sm">
|
|
394
|
+
Start a new conversation by typing your message below
|
|
395
|
+
</div>
|
|
396
|
+
</>
|
|
397
|
+
)}
|
|
398
|
+
{shouldInlineToolbar && (
|
|
399
|
+
<div className="mt-6 text-left">{renderToolbar(inputToolbarProps)}</div>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{posts.map((post) => (
|
|
405
|
+
<div key={post.id} className={clsx('mb-3', postContainerClassName)}>
|
|
406
|
+
<MessageGroupComponent
|
|
407
|
+
messages={[post]}
|
|
408
|
+
currentUser={currentUserForGroup}
|
|
409
|
+
onOpen={onOpen}
|
|
410
|
+
onMessageClick={onMessageClick}
|
|
411
|
+
isDesktopView={isDesktopView}
|
|
412
|
+
isSmallScreen={isSmallScreen}
|
|
413
|
+
showAuthorName={showAvatar}
|
|
414
|
+
showAvatar={showAvatar}
|
|
415
|
+
showTimestamp={showDate}
|
|
416
|
+
sandboxErrors={sandboxErrors}
|
|
417
|
+
currentFiles={currentFiles}
|
|
418
|
+
onFixError={onFixError}
|
|
419
|
+
showCopyDownloadOnCodeBlock={showCopyDownloadOnCodeBlock}
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
422
|
+
))}
|
|
423
|
+
|
|
424
|
+
{streamingContent?.trim() && (
|
|
425
|
+
<div className="flex justify-start">
|
|
426
|
+
<div
|
|
427
|
+
className={clsx(
|
|
428
|
+
'max-w-[85%] w-full rounded-2xl px-4 py-3 text-gray-900',
|
|
429
|
+
ASSISTANT_BUBBLE_BG,
|
|
430
|
+
showMessageBubbleBorder && `border ${BORDER_COLOR}`,
|
|
431
|
+
)}
|
|
432
|
+
>
|
|
433
|
+
<div className="min-w-0 text-[15px] leading-relaxed break-words whitespace-pre-wrap">
|
|
434
|
+
{renderStreamingContent
|
|
435
|
+
? renderStreamingContent(streamingContent)
|
|
436
|
+
: streamingContent}
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
{isLoading && (
|
|
443
|
+
<div className="flex justify-start">
|
|
444
|
+
<div className="rounded-2xl px-4 py-3 bg-gray-100 border border-gray-200 flex items-center gap-2">
|
|
445
|
+
<div className="w-4 h-4 border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin" />
|
|
446
|
+
<span className="text-sm text-gray-600">Thinking...</span>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
{shouldRenderBottomToolbar && (
|
|
453
|
+
<div className="border-t border-gray-200 bg-white p-3">{renderToolbar(inputToolbarProps)}</div>
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { MessagesContainerUI } from './MessagesContainerUI';
|
|
2
|
+
export type { MessagesContainerUIProps } from './MessagesContainerUI';
|
|
3
|
+
export { PlanModeView, defaultPlanMessageToPost } from './PlanModeView';
|
|
4
|
+
export type { PlanModeViewProps, PlanModeViewInputToolbarProps } from './PlanModeView';
|
|
5
|
+
export { DefaultMessageGroup } from './DefaultMessageGroup';
|
|
6
|
+
export { DefaultInputToolBar } from './DefaultInputToolBar';
|
|
7
|
+
export type { DefaultInputToolBarProps } from './DefaultInputToolBar';
|
|
8
|
+
export type { PlanMessage, CurrentUser, MessageGroupRenderProps, MessageGroupComponentType } from './types';
|