Skip to content

Commit 7668e16

Browse files
Enhance AIAssistantModal with avatar support and styling updates
- Added avatar rendering for user and assistant messages in AIAssistantModal. - Updated styles in AIAssistantModal.scss for improved layout and responsiveness. - Introduced nullish coalescing operator (`??`) usage guideline in general rules. - Added new images for AI assistant functionality. - Updated tests to verify expand/collapse behavior of the modal.
1 parent ded3b36 commit 7668e16

File tree

6 files changed

+124
-22
lines changed

6 files changed

+124
-22
lines changed

.cursor/rules/general.mdc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,5 @@ AWS architecture: [aws architecture.pdf](mdc:docs/assets/aws architecture.pdf)
123123
```
124124

125125
This rule provides clear guidelines on what units to use, how to convert between units, and why it's important for your project. You can add this to your general rules to ensure consistency across the codebase.
126+
127+
Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
70.2 KB
Loading
110 KB
Loading

frontend/src/common/components/AIAssistant/AIAssistantModal.scss

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@
77
align-items: flex-end;
88

99
&.expanded {
10-
--height: 90vh;
11-
--max-height: 90vh;
10+
--height: 85vh;
11+
--max-height: 85vh;
12+
13+
&::part(content) {
14+
margin: 2rem 1rem;
15+
}
1216
}
1317

1418
&::part(content) {
1519
border-radius: var(--border-radius);
1620
margin: 0 1rem 5rem 1rem;
21+
display: flex;
22+
flex-direction: column;
1723
}
1824

1925
.ai-assistant-header {
@@ -37,6 +43,8 @@
3743

3844
.ai-assistant-content {
3945
--padding: 0;
46+
flex: 1;
47+
overflow: hidden;
4048

4149
.chat-container {
4250
display: flex;
@@ -60,7 +68,8 @@
6068
.chat-message {
6169
display: flex;
6270
flex-direction: column;
63-
max-width: 80%;
71+
max-width: 90%;
72+
margin-bottom: 0.75rem;
6473

6574
.message-content {
6675
padding: 0.75rem 1rem;
@@ -92,13 +101,45 @@
92101
border-bottom-left-radius: 0.25rem;
93102
}
94103
}
104+
105+
// Avatar styling
106+
.message-avatar {
107+
width: 1.5rem;
108+
height: 1.5rem;
109+
border-radius: 50%;
110+
margin-bottom: 0.25rem;
111+
112+
&.user-avatar {
113+
align-self: flex-end;
114+
margin-right: 0.25rem;
115+
}
116+
117+
&.assistant-avatar {
118+
align-self: flex-start;
119+
margin-left: 0.25rem;
120+
background-color: var(--ion-color-primary);
121+
display: flex;
122+
align-items: center;
123+
justify-content: center;
124+
125+
svg, img {
126+
width: 1rem;
127+
height: 1rem;
128+
filter: brightness(0) invert(1);
129+
}
130+
}
131+
}
95132
}
96133
}
97134
}
98135

99136
.ai-assistant-footer {
100137
padding: 0.5rem 1rem 1rem;
101138
background: transparent;
139+
position: sticky;
140+
bottom: 0;
141+
width: 100%;
142+
z-index: 2;
102143

103144
.input-container {
104145
display: flex;

frontend/src/common/components/AIAssistant/AIAssistantModal.tsx

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
IonFooter,
1212
} from '@ionic/react';
1313
import { useState, useRef, useEffect } from 'react';
14-
import { closeOutline, expandOutline, paperPlaneOutline } from 'ionicons/icons';
14+
import { closeOutline, expandOutline, contractOutline, paperPlaneOutline, personCircleOutline } from 'ionicons/icons';
1515
import { useTranslation } from 'react-i18next';
1616
import iconOnly from '../../../assets/img/icon-only.png';
1717
import './AIAssistantModal.scss';
@@ -65,6 +65,13 @@ const AIAssistantModal: React.FC<AIAssistantModalProps> = ({
6565

6666
const handleExpand = () => {
6767
setIsExpanded(!isExpanded);
68+
69+
// Ensure scroll position is maintained after expanding/collapsing
70+
setTimeout(() => {
71+
if (chatContainerRef.current) {
72+
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
73+
}
74+
}, 100);
6875
};
6976

7077
const handleSendMessage = () => {
@@ -107,6 +114,40 @@ const AIAssistantModal: React.FC<AIAssistantModalProps> = ({
107114
setInputValue(e.detail.value || '');
108115
};
109116

117+
const renderMessageWithAvatar = (message: ChatMessage) => {
118+
const isUser = message.sender === 'user';
119+
120+
return (
121+
<div
122+
key={message.id}
123+
className={`chat-message ${isUser ? 'user-message' : 'assistant-message'}`}
124+
aria-label={`${isUser ? 'You' : 'AI Assistant'}: ${message.text}`}
125+
>
126+
{isUser && (
127+
<div className="message-avatar user-avatar">
128+
<IonIcon icon={personCircleOutline} aria-hidden="true" />
129+
</div>
130+
)}
131+
132+
{!isUser && (
133+
<div className="message-avatar assistant-avatar">
134+
<img src={iconOnly} alt="AI" aria-hidden="true" />
135+
</div>
136+
)}
137+
138+
<div className="message-content">
139+
<p>{message.text}</p>
140+
</div>
141+
<span className="sr-only">
142+
{new Intl.DateTimeFormat('en-US', {
143+
hour: 'numeric',
144+
minute: 'numeric'
145+
}).format(message.timestamp)}
146+
</span>
147+
</div>
148+
);
149+
};
150+
110151
return (
111152
<IonModal
112153
isOpen={isOpen}
@@ -128,7 +169,7 @@ const AIAssistantModal: React.FC<AIAssistantModalProps> = ({
128169
aria-label={isExpanded ? "Collapse chat" : "Expand chat"}
129170
data-testid={`${testid}-expand-button`}
130171
>
131-
<IonIcon icon={expandOutline} aria-hidden="true" />
172+
<IonIcon icon={isExpanded ? contractOutline : expandOutline} aria-hidden="true" />
132173
</IonButton>
133174
<IonButton
134175
onClick={handleClose}
@@ -154,23 +195,7 @@ const AIAssistantModal: React.FC<AIAssistantModalProps> = ({
154195
<p>{t('common.aiAssistant.emptyState', 'How can I help you today?')}</p>
155196
</div>
156197
) : (
157-
messages.map((message) => (
158-
<div
159-
key={message.id}
160-
className={`chat-message ${message.sender === 'user' ? 'user-message' : 'assistant-message'}`}
161-
aria-label={`${message.sender === 'user' ? 'You' : 'AI Assistant'}: ${message.text}`}
162-
>
163-
<div className="message-content">
164-
<p>{message.text}</p>
165-
</div>
166-
<span className="sr-only">
167-
{new Intl.DateTimeFormat('en-US', {
168-
hour: 'numeric',
169-
minute: 'numeric'
170-
}).format(message.timestamp)}
171-
</span>
172-
</div>
173-
))
198+
messages.map(renderMessageWithAvatar)
174199
)}
175200
</div>
176201
</IonContent>

frontend/src/common/components/AIAssistant/__tests__/AIAssistantModal.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ interface IonInputProps {
2121
'data-testid'?: string;
2222
}
2323

24+
// Mock icons
25+
vi.mock('ionicons/icons', () => ({
26+
closeOutline: 'mock-close-icon',
27+
expandOutline: 'mock-expand-icon',
28+
contractOutline: 'mock-contract-icon',
29+
paperPlaneOutline: 'mock-paper-plane-icon',
30+
personCircleOutline: 'mock-person-circle-icon'
31+
}));
32+
2433
// Mock the IonModal implementation
2534
vi.mock('@ionic/react', async () => {
2635
const actual = await vi.importActual('@ionic/react');
@@ -45,6 +54,11 @@ vi.mock('@ionic/react', async () => {
4554
onChange={(e) => onIonInput?.({ detail: { value: e.target.value } })}
4655
onKeyPress={onKeyPress}
4756
/>
57+
),
58+
IonIcon: ({ icon, 'aria-hidden': ariaHidden }: { icon: string; 'aria-hidden'?: boolean }) => (
59+
<span data-testid={`icon-${icon}`} aria-hidden={ariaHidden}>
60+
{icon}
61+
</span>
4862
)
4963
};
5064
});
@@ -101,4 +115,24 @@ describe('AIAssistantModal', () => {
101115

102116
expect(mockSetIsOpen).toHaveBeenCalledWith(false);
103117
});
118+
119+
it('toggles between expanded and collapsed state when expand button is clicked', () => {
120+
customRender(<AIAssistantModal {...defaultProps} />);
121+
122+
// Initially should show expand icon
123+
expect(screen.getByTestId('icon-mock-expand-icon')).toBeDefined();
124+
125+
// Click expand button
126+
const expandButton = screen.getByTestId('test-ai-assistant-expand-button');
127+
fireEvent.click(expandButton);
128+
129+
// Now should show contract icon
130+
expect(screen.getByTestId('icon-mock-contract-icon')).toBeDefined();
131+
132+
// Click again to collapse
133+
fireEvent.click(expandButton);
134+
135+
// Should show expand icon again
136+
expect(screen.getByTestId('icon-mock-expand-icon')).toBeDefined();
137+
});
104138
});

0 commit comments

Comments
 (0)