Skip to content

Commit 9596b65

Browse files
committed
feat(condo): DOMA-12967 fix comments
1 parent 2735a1f commit 9596b65

10 files changed

Lines changed: 457 additions & 147 deletions

File tree

apps/condo/domains/ai/components/AIChat/AIChat.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const AIChat: React.FC<AIChatProps> = ({
8787

8888
const [inputValue, setInputValue] = useState('')
8989
const [messages, setMessages] = useState<Message[]>([])
90+
// User message id of the turn currently pinned/clamped in the scroller
9091
const [activeTurnUserMessageId, setActiveTurnUserMessageId] = useState<string | null>(null)
9192

9293
const messagesContainerRef = useRef<HTMLDivElement>(null)
@@ -208,6 +209,7 @@ export const AIChat: React.FC<AIChatProps> = ({
208209
}))
209210
setMessages(historyWithDates)
210211
setActiveTurnUserMessageId(null)
212+
// Wait for history messages to commit to the DOM before scrolling to bottom
211213
requestAnimationFrame(() => {
212214
const container = messagesContainerRef.current
213215
if (container) {
@@ -218,7 +220,7 @@ export const AIChat: React.FC<AIChatProps> = ({
218220
const lastMessage = historyWithDates[historyWithDates.length - 1]
219221

220222
if (lastMessage?.status === 'sending' && lastMessage?.executionAIFlowTaskId) {
221-
void (async () => {
223+
const resumeLastMessage = async () => {
222224
try {
223225
const result = await resume(lastMessage.executionAIFlowTaskId)
224226
finalizeAssistantMessage(lastMessage, result)
@@ -230,7 +232,9 @@ export const AIChat: React.FC<AIChatProps> = ({
230232
status: 'sent',
231233
})
232234
}
233-
})()
235+
}
236+
237+
resumeLastMessage()
234238
}
235239
}, [aiSessionId, resume, finalizeAssistantMessage, changeMessage, errorMessage])
236240

@@ -389,7 +393,7 @@ export const AIChat: React.FC<AIChatProps> = ({
389393
if (userIndex < 0) return
390394

391395
const turnAssistant = messages[userIndex + 1]
392-
if (!turnAssistant || turnAssistant.role !== 'assistant') return
396+
if (turnAssistant?.role !== 'assistant') return
393397

394398
pendingScrollToMessageIdRef.current = null
395399
shouldScrollActiveTurnRef.current = true
@@ -447,6 +451,7 @@ export const AIChat: React.FC<AIChatProps> = ({
447451

448452
const syncInputHeight = () => {
449453
chatContainer.style.setProperty('--ai-chat-input-height', `${inputContainer.offsetHeight}px`)
454+
// Wait for padding from --ai-chat-input-height to apply before measuring scrollport
450455
requestAnimationFrame(() => {
451456
if (messagesContainerRef.current) {
452457
syncScrollportHeight(messagesContainerRef.current)
@@ -613,7 +618,7 @@ export const AIChat: React.FC<AIChatProps> = ({
613618
label: btn.buttonName,
614619
tooltip: btn.buttonDescription || undefined,
615620
disabled: !canExecuteAIFlow,
616-
onClick: () => void handleScenarioButtonClick(btn.buttonId, btn.buttonName),
621+
onClick: () => handleScenarioButtonClick(btn.buttonId, btn.buttonName),
617622
}))}
618623
/>
619624
)}

apps/condo/domains/ai/components/AIChat/AIChatMessage.module.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
.user-message-actions {
2929
opacity: 0;
30-
transition: opacity 0.15s ease;
30+
transition: opacity var(--condo-global-transition-duration-default) ease;
3131
}
3232

3333
.user-message-bubble {
@@ -39,7 +39,7 @@
3939
padding: var(--condo-global-spacing-4) var(--condo-global-spacing-8);
4040
background: var(--condo-global-color-white);
4141
border-radius: var(--condo-global-border-radius-medium);
42-
box-shadow: 0 1px 2px rgb(0 0 0 / 3%);
42+
box-shadow: var(--condo-global-box-shadow-default);
4343
}
4444

4545
.user-message-attachments {
@@ -79,5 +79,5 @@
7979
}
8080

8181
.assistant-markdown * {
82-
margin-bottom: 8px !important;
82+
margin-bottom: var(--condo-global-spacing-8) !important;
8383
}

apps/condo/domains/ai/components/AIChat/AIChatMessage.tsx

Lines changed: 129 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,20 @@ import type { Message } from './AIChat'
1616

1717
const COPY_RESET_TIMEOUT_MS = 2000
1818
const EXPORT_MENU_FORMATS: ExportAIMessageFormat[] = ['docx', 'pdf', 'txt']
19+
const SUGGESTION_ANIMATION_DELAY_MS = 70
1920

2021
export type AIChatMessageProps = {
2122
message: Message
2223
onSuggestionClick?: (suggestion: string) => void
2324
canExecuteAIFlow?: boolean
2425
}
2526

26-
export const AIChatMessage: React.FC<AIChatMessageProps> = ({
27-
message,
28-
onSuggestionClick,
29-
canExecuteAIFlow = true,
30-
}) => {
27+
const useCopyButton = (message: Message) => {
3128
const intl = useIntl()
32-
const assistantMarkdownRef = useRef<HTMLDivElement>(null)
3329
const [copied, setCopied] = useState(false)
34-
const [exportLoadingByFormat, setExportLoadingByFormat] = useState<Record<ExportAIMessageFormat, boolean>>({
35-
txt: false,
36-
pdf: false,
37-
docx: false,
38-
})
3930

4031
const copyLabel = intl.formatMessage({ id: 'Copy' })
4132
const copiedLabel = intl.formatMessage({ id: 'Copied' })
42-
const downloadLabel = intl.formatMessage({ id: 'Download' })
43-
const exportMenuItems = EXPORT_MENU_FORMATS.map((format) => ({
44-
key: format,
45-
label: `${downloadLabel} ${format.toUpperCase()}`,
46-
}))
4733

4834
const handleCopy = useCallback(async () => {
4935
if (copied) return
@@ -62,6 +48,86 @@ export const AIChatMessage: React.FC<AIChatMessageProps> = ({
6248
}
6349
}, [copied, message.content.text, message.role])
6450

51+
const copyButton = (
52+
<Tooltip title={copied ? copiedLabel : copyLabel}>
53+
<Button
54+
type='secondary'
55+
compact
56+
minimal
57+
size='medium'
58+
icon={copied ? <Check size='small' /> : <Copy size='small' />}
59+
onClick={handleCopy}
60+
disabled={copied}
61+
aria-label={copied ? copiedLabel : copyLabel}
62+
/>
63+
</Tooltip>
64+
)
65+
66+
return copyButton
67+
}
68+
69+
const AIChatUserMessage: React.FC<{ message: Message }> = ({ message }) => {
70+
const copyButton = useCopyButton(message)
71+
72+
return (
73+
<div
74+
data-message-id={message.id}
75+
className={`${styles.messageWrapper} ${styles.userMessage}`}
76+
>
77+
<div className={styles.userMessageContainer}>
78+
<div className={styles.userMessageRow}>
79+
{message.copyable === true && message.content.text?.trim() && (
80+
<div className={styles.userMessageActions}>{copyButton}</div>
81+
)}
82+
{(message.content.text?.trim() || message.content.attachments?.length) ? (
83+
<div className={styles.userMessageBubble}>
84+
{message.content.text?.trim() ? (
85+
<Typography.Text>{message.content.text}</Typography.Text>
86+
) : null}
87+
{message.content.attachments?.length ? (
88+
<div className={styles.userMessageAttachments}>
89+
{message.content.attachments.map((attachment, index) => (
90+
<AIChatDocument
91+
key={`${attachment.name}-${index}`}
92+
name={attachment.name}
93+
/>
94+
))}
95+
</div>
96+
) : null}
97+
</div>
98+
) : null}
99+
</div>
100+
</div>
101+
</div>
102+
)
103+
}
104+
105+
type AIChatAssistantMessageProps = {
106+
message: Message
107+
onSuggestionClick?: (suggestion: string) => void
108+
canExecuteAIFlow: boolean
109+
}
110+
111+
const AIChatAssistantMessage: React.FC<AIChatAssistantMessageProps> = ({
112+
message,
113+
onSuggestionClick,
114+
canExecuteAIFlow,
115+
}) => {
116+
const intl = useIntl()
117+
const assistantMarkdownRef = useRef<HTMLDivElement>(null)
118+
const copyButton = useCopyButton(message)
119+
const [exportLoadingByFormat, setExportLoadingByFormat] = useState<Record<ExportAIMessageFormat, boolean>>({
120+
txt: false,
121+
pdf: false,
122+
docx: false,
123+
})
124+
125+
const downloadLabel = intl.formatMessage({ id: 'Download' })
126+
const exportMenuItems = EXPORT_MENU_FORMATS.map((format) => ({
127+
key: format,
128+
label: `${downloadLabel} ${format.toUpperCase()}`,
129+
}))
130+
65131
const handleExport = useCallback(async (format: ExportAIMessageFormat) => {
66132
if (exportLoadingByFormat[format]) return
67133

@@ -89,21 +155,6 @@ export const AIChatMessage: React.FC<AIChatMessageProps> = ({
89155

90156
const isExporting = exportLoadingByFormat.txt || exportLoadingByFormat.pdf || exportLoadingByFormat.docx
91157

92-
const copyButton = (
93-
<Tooltip title={copied ? copiedLabel : copyLabel}>
94-
<Button
95-
type='secondary'
96-
compact
97-
minimal
98-
size='medium'
99-
icon={copied ? <Check size='small' /> : <Copy size='small' />}
100-
onClick={handleCopy}
101-
disabled={copied}
102-
aria-label={copied ? copiedLabel : copyLabel}
103-
/>
104-
</Tooltip>
105-
)
106-
107158
const downloadButton = (
108159
<Dropdown
109160
trigger={['click']}
@@ -131,64 +182,55 @@ export const AIChatMessage: React.FC<AIChatMessageProps> = ({
131182
return (
132183
<div
133184
data-message-id={message.id}
134-
className={`${styles.messageWrapper} ${message.role === 'user' ? styles.userMessage : styles.assistantMessage}`}
185+
className={`${styles.messageWrapper} ${styles.assistantMessage}`}
135186
>
136-
{message.role === 'user' ? (
137-
<div className={styles.userMessageContainer}>
138-
<div className={styles.userMessageRow}>
139-
{message.copyable === true && message.content.text?.trim() && (
140-
<div className={styles.userMessageActions}>{copyButton}</div>
141-
)}
142-
{(message.content.text?.trim() || message.content.attachments?.length) ? (
143-
<div className={styles.userMessageBubble}>
144-
{message.content.text?.trim() ? (
145-
<Typography.Text>{message.content.text}</Typography.Text>
146-
) : null}
147-
{message.content.attachments?.length ? (
148-
<div className={styles.userMessageAttachments}>
149-
{message.content.attachments.map((attachment, index) => (
150-
<AIChatDocument
151-
key={`${attachment.name}-${index}`}
152-
name={attachment.name}
153-
/>
154-
))}
155-
</div>
156-
) : null}
157-
</div>
158-
) : null}
187+
<div className={styles.assistantMessageContainer}>
188+
{message.status === 'sending' && !message.content.text?.trim() ? (
189+
<AIChatThinkingStatus />
190+
) : (
191+
<div
192+
ref={assistantMarkdownRef}
193+
className={styles.assistantMarkdown}
194+
>
195+
<Markdown type='inline'>{message.content.text}</Markdown>
159196
</div>
160-
</div>
161-
) : (
162-
<div className={styles.assistantMessageContainer}>
163-
{message.status === 'sending' && !message.content.text?.trim() ? (
164-
<AIChatThinkingStatus />
165-
) : (
166-
<div
167-
ref={assistantMarkdownRef}
168-
className={styles.assistantMarkdown}
169-
>
170-
<Markdown type='inline'>{message.content.text}</Markdown>
171-
</div>
172-
)}
173-
{message.copyable === true && message.status !== 'sending' && (
174-
<div className={styles.assistantMessageActions}>
175-
{copyButton}
176-
{downloadButton}
177-
</div>
178-
)}
179-
{message.content.suggestions?.length > 0 && (
180-
<AIChatSuggestions
181-
items={message.content.suggestions.map((suggestion, index) => ({
182-
key: `${message.id}-${suggestion}`,
183-
label: suggestion,
184-
disabled: !canExecuteAIFlow,
185-
animationDelayMs: index * 70,
186-
onClick: () => onSuggestionClick?.(suggestion),
187-
}))}
188-
/>
189-
)}
190-
</div>
191-
)}
197+
)}
198+
{message.copyable === true && message.status !== 'sending' && (
199+
<div className={styles.assistantMessageActions}>
200+
{copyButton}
201+
{downloadButton}
202+
</div>
203+
)}
204+
{message.content.suggestions?.length > 0 && (
205+
<AIChatSuggestions
206+
items={message.content.suggestions.map((suggestion, index) => ({
207+
key: `${message.id}-${suggestion}`,
208+
label: suggestion,
209+
disabled: !canExecuteAIFlow,
210+
animationDelayMs: index * SUGGESTION_ANIMATION_DELAY_MS,
211+
onClick: () => onSuggestionClick?.(suggestion),
212+
}))}
213+
/>
214+
)}
215+
</div>
192216
</div>
193217
)
194218
}
219+
220+
export const AIChatMessage: React.FC<AIChatMessageProps> = ({
221+
message,
222+
onSuggestionClick,
223+
canExecuteAIFlow = true,
224+
}) => {
225+
if (message.role === 'user') {
226+
return <AIChatUserMessage message={message} />
227+
}
228+
229+
return (
230+
<AIChatAssistantMessage
231+
message={message}
232+
onSuggestionClick={onSuggestionClick}
233+
canExecuteAIFlow={canExecuteAIFlow}
234+
/>
235+
)
236+
}

apps/condo/domains/ai/components/AIChat/AIChatSuggestions.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
.suggestion-item {
99
display: inline-flex;
10-
animation: suggestion-fade-in 0.28s ease-in-out both;
10+
animation: suggestion-fade-in var(--condo-global-transition-duration-default) ease-in-out both;
1111
}
1212

1313
.tooltip-wrap {

apps/condo/domains/ai/components/AIInputNotification.module.css

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@
77
.panel {
88
position: absolute;
99
right: 0;
10-
bottom: calc(100% + 8px);
10+
bottom: calc(100% + var(--condo-global-spacing-8));
1111
left: 0;
1212
z-index: 5;
1313
width: 100%;
14+
max-height: min(60vh, calc(100dvh - 96px));
1415
overflow: hidden;
16+
display: flex;
17+
flex-direction: column;
1518
background-color: var(--condo-global-color-white);
1619
border: 1px solid var(--condo-global-color-gray-3);
1720
border-radius: var(--condo-global-border-radius-large);
18-
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
21+
box-shadow: var(--condo-global-box-shadow-default);
1922
}
2023

2124
.notification {
25+
min-height: 0;
26+
overflow: hidden;
2227
width: 100%;
2328
padding: var(--condo-global-spacing-12);
2429
background-color: var(--condo-global-color-white);
@@ -49,6 +54,9 @@
4954

5055
/* Match textarea line breaks after "Apply" (plain text with \n, not Markdown). */
5156
.result-text {
57+
display: block;
58+
max-height: min(45vh, calc(100dvh - 180px));
59+
overflow-y: auto;
5260
white-space: pre-wrap;
5361
overflow-wrap: break-word;
5462
}

0 commit comments

Comments
 (0)