Skip to content

Commit 9fa0607

Browse files
Phase 4 - Enhance AI Chat functionality and styling
- Added `react-markdown` and `rehype-sanitize` for improved message formatting. - Updated `ChatMessage` component to render messages with Markdown support. - Introduced `TypingIndicator` component to show when AI is typing. - Modified `ChatContext` to manage typing state. - Enhanced `AIChat` and `ChatHeader` components for better user experience. - Added new styles for typing indicator and message animations. - Updated `ChatInput` to auto-resize based on content. - Improved overall chat UI with new animations and styles.
1 parent 451c78c commit 9fa0607

File tree

15 files changed

+1469
-44
lines changed

15 files changed

+1469
-44
lines changed

frontend/package-lock.json

Lines changed: 1202 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,10 @@
6666
"react-dom": "18.3.1",
6767
"react-error-boundary": "5.0.0",
6868
"react-i18next": "15.4.0",
69+
"react-markdown": "^10.1.0",
6970
"react-router": "5.3.4",
7071
"react-router-dom": "5.3.4",
72+
"rehype-sanitize": "^6.0.0",
7173
"uuid": "11.0.4",
7274
"yup": "1.6.1"
7375
},

frontend/src/common/models/chat.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ export interface ChatMessage {
1010
text: string;
1111
sender: 'user' | 'ai';
1212
timestamp: Date;
13+
isRead?: boolean;
1314
}
1415

1516
/**
1617
* API message format for Bedrock
1718
*/
1819
export interface BedrockMessage {
19-
role: 'user' | 'assistant';
20+
role: 'user' | 'assistant' | 'system';
2021
content: string;
2122
}
2223

@@ -27,20 +28,19 @@ export interface ChatCompletionRequest {
2728
messages: BedrockMessage[];
2829
temperature?: number;
2930
maxTokens?: number;
30-
model?: string;
31+
stream?: boolean;
3132
}
3233

3334
/**
3435
* Response from the chat completion API
3536
*/
3637
export interface ChatCompletionResponse {
3738
message: BedrockMessage;
38-
usage: {
39+
usage?: {
3940
promptTokens: number;
4041
completionTokens: number;
4142
totalTokens: number;
4243
};
43-
model: string;
4444
}
4545

4646
/**

frontend/src/pages/Chat/components/AIChat/AIChat.scss

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@
9191
font-size: 0.875rem;
9292
}
9393
}
94+
95+
&__typing-container {
96+
display: flex;
97+
justify-content: flex-start;
98+
margin: 0.5rem 1rem;
99+
animation: fadeIn 0.3s ease-in-out;
100+
}
94101
}
95102

96103
@keyframes bounce {
@@ -100,4 +107,13 @@
100107
40% {
101108
transform: scale(1);
102109
}
110+
}
111+
112+
@keyframes fadeIn {
113+
0% {
114+
opacity: 0;
115+
}
116+
100% {
117+
opacity: 1;
118+
}
103119
}

frontend/src/pages/Chat/components/AIChat/AIChat.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IonContent } from '@ionic/react';
33
import ChatHeader from '../ChatHeader/ChatHeader';
44
import ChatInput from '../ChatInput/ChatInput';
55
import ChatMessage from '../ChatMessage/ChatMessage';
6+
import TypingIndicator from '../ChatMessage/TypingIndicator';
67
import { useChatContext } from '../../hooks/useChatContext';
78
import { ChatSessionStatus } from 'common/models/chat';
89
import './AIChat.scss';
@@ -22,7 +23,7 @@ const AIChat: React.FC<AIChatProps> = ({
2223
onToggleExpand
2324
}) => {
2425
const { state, sendMessage } = useChatContext();
25-
const { messages, status, error } = state;
26+
const { messages, status, error, isTyping } = state;
2627
const contentRef = useRef<HTMLIonContentElement>(null);
2728
const isLoading = status === ChatSessionStatus.LOADING;
2829

@@ -32,12 +33,12 @@ const AIChat: React.FC<AIChatProps> = ({
3233
await sendMessage(text);
3334
};
3435

35-
// Scroll to bottom when messages change
36+
// Scroll to bottom when messages change or when typing
3637
useEffect(() => {
3738
if (contentRef.current) {
3839
contentRef.current.scrollToBottom(300);
3940
}
40-
}, [messages]);
41+
}, [messages, isTyping]);
4142

4243
return (
4344
<div className={`ai-chat ${isExpanded ? 'ai-chat--expanded' : ''}`}>
@@ -62,13 +63,9 @@ const AIChat: React.FC<AIChatProps> = ({
6263
))
6364
)}
6465

65-
{isLoading && (
66-
<div className="ai-chat__loading">
67-
<div className="ai-chat__loading-dots">
68-
<span></span>
69-
<span></span>
70-
<span></span>
71-
</div>
66+
{isTyping && (
67+
<div className="ai-chat__typing-container">
68+
<TypingIndicator />
7269
</div>
7370
)}
7471

frontend/src/pages/Chat/components/ChatHeader/ChatHeader.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
background-color: rgba(0, 0, 0, 0.1);
4141
}
4242

43+
&--clear {
44+
color: #ff6b6b;
45+
}
46+
4347
&--expand {
4448
color: #4765ff;
4549
}

frontend/src/pages/Chat/components/ChatHeader/ChatHeader.tsx

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React from 'react';
2-
import { IonHeader, IonIcon, IonToolbar } from '@ionic/react';
3-
import { close, expandOutline, contractOutline } from 'ionicons/icons';
1+
import React, { useState } from 'react';
2+
import { IonHeader, IonIcon, IonToolbar, IonAlert } from '@ionic/react';
3+
import { close, expandOutline, contractOutline, trashOutline } from 'ionicons/icons';
4+
import { useChatContext } from '../../hooks/useChatContext';
45
import './ChatHeader.scss';
56

67
interface ChatHeaderProps {
@@ -10,23 +11,46 @@ interface ChatHeaderProps {
1011
}
1112

1213
/**
13-
* ChatHeader component displays the title, close button, and expand toggle
14+
* ChatHeader component displays the title, clear chat button, expand toggle, and close button
1415
*/
1516
const ChatHeader: React.FC<ChatHeaderProps> = ({
1617
isExpanded,
1718
onClose,
1819
onToggleExpand
1920
}) => {
21+
const { clearMessages } = useChatContext();
22+
const [showClearConfirm, setShowClearConfirm] = useState(false);
23+
24+
const handleClearClick = () => {
25+
setShowClearConfirm(true);
26+
};
27+
28+
const handleClearConfirm = () => {
29+
clearMessages();
30+
setShowClearConfirm(false);
31+
};
32+
2033
return (
2134
<IonHeader className="chat-header">
2235
<IonToolbar className="chat-header__toolbar">
2336
<div className="chat-header__title">
2437
AI Assistant
2538
</div>
2639
<div className="chat-header__actions">
40+
<button
41+
className="chat-header__button chat-header__button--clear"
42+
onClick={handleClearClick}
43+
aria-label="Clear chat"
44+
>
45+
<IonIcon
46+
icon={trashOutline}
47+
className="chat-header__icon"
48+
/>
49+
</button>
2750
<button
2851
className="chat-header__button chat-header__button--expand"
2952
onClick={onToggleExpand}
53+
aria-label={isExpanded ? "Collapse" : "Expand"}
3054
>
3155
<IonIcon
3256
icon={isExpanded ? contractOutline : expandOutline}
@@ -36,6 +60,7 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
3660
<button
3761
className="chat-header__button chat-header__button--close"
3862
onClick={onClose}
63+
aria-label="Close"
3964
>
4065
<IonIcon
4166
icon={close}
@@ -44,6 +69,24 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
4469
</button>
4570
</div>
4671
</IonToolbar>
72+
73+
<IonAlert
74+
isOpen={showClearConfirm}
75+
onDidDismiss={() => setShowClearConfirm(false)}
76+
header="Clear Chat History"
77+
message="Are you sure you want to clear all messages? This action cannot be undone."
78+
buttons={[
79+
{
80+
text: 'Cancel',
81+
role: 'cancel',
82+
},
83+
{
84+
text: 'Clear',
85+
role: 'destructive',
86+
handler: handleClearConfirm
87+
}
88+
]}
89+
/>
4790
</IonHeader>
4891
);
4992
};

frontend/src/pages/Chat/components/ChatInput/ChatInput.scss

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.chat-input {
22
display: flex;
3-
align-items: center;
3+
align-items: flex-end;
44
padding: 0.75rem 1rem;
55
background-color: white;
66
border-top: 1px solid #efefef;
@@ -13,6 +13,11 @@
1313
font-size: 0.9375rem;
1414
outline: none;
1515
transition: border-color 0.2s ease, box-shadow 0.2s ease;
16+
resize: none;
17+
max-height: 6.25rem;
18+
overflow-y: auto;
19+
line-height: 1.4;
20+
font-family: inherit;
1621

1722
&:focus {
1823
border-color: #4765ff;
@@ -42,6 +47,7 @@
4247
color: white;
4348
cursor: pointer;
4449
transition: background-color 0.2s ease, transform 0.2s ease;
50+
flex-shrink: 0;
4551

4652
&:hover {
4753
background-color: #3b52cc;

frontend/src/pages/Chat/components/ChatInput/ChatInput.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, KeyboardEvent } from 'react';
1+
import React, { useState, KeyboardEvent, useRef, useEffect } from 'react';
22
import { IonIcon } from '@ionic/react';
33
import { send } from 'ionicons/icons';
44
import './ChatInput.scss';
@@ -16,6 +16,15 @@ const ChatInput: React.FC<ChatInputProps> = ({
1616
disabled = false
1717
}) => {
1818
const [inputValue, setInputValue] = useState('');
19+
const textareaRef = useRef<HTMLTextAreaElement>(null);
20+
21+
// Auto-resize the textarea based on content
22+
useEffect(() => {
23+
if (textareaRef.current) {
24+
textareaRef.current.style.height = 'auto';
25+
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
26+
}
27+
}, [inputValue]);
1928

2029
const handleSend = () => {
2130
if (inputValue.trim() && !disabled) {
@@ -24,7 +33,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
2433
}
2534
};
2635

27-
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
36+
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
2837
if (e.key === 'Enter' && !e.shiftKey) {
2938
e.preventDefault();
3039
handleSend();
@@ -33,19 +42,22 @@ const ChatInput: React.FC<ChatInputProps> = ({
3342

3443
return (
3544
<div className="chat-input">
36-
<input
37-
type="text"
45+
<textarea
46+
ref={textareaRef}
3847
className="chat-input__field"
3948
placeholder="Type your question..."
4049
value={inputValue}
4150
onChange={(e) => setInputValue(e.target.value)}
4251
onKeyDown={handleKeyDown}
4352
disabled={disabled}
53+
rows={1}
54+
maxLength={1000}
4455
/>
4556
<button
4657
className={`chat-input__button ${disabled || !inputValue.trim() ? 'chat-input__button--disabled' : ''}`}
4758
onClick={handleSend}
4859
disabled={disabled || !inputValue.trim()}
60+
aria-label="Send message"
4961
>
5062
<IonIcon icon={send} className="chat-input__send-icon" />
5163
</button>

frontend/src/pages/Chat/components/ChatMessage/ChatMessage.scss

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
display: flex;
33
flex-direction: column;
44
max-width: 100%;
5+
margin: 0.5rem 0;
6+
animation: fadeIn 0.3s ease-in-out;
7+
cursor: pointer;
58

69
&__content {
710
display: flex;
@@ -58,18 +61,70 @@
5861
padding: 0.75rem 1rem;
5962
max-width: 80%;
6063
word-wrap: break-word;
64+
transition: all 0.2s ease;
65+
66+
&:hover {
67+
filter: brightness(0.98);
68+
}
6169
}
6270

6371
&__text {
6472
font-size: 0.9375rem;
6573
line-height: 1.4;
6674
white-space: pre-wrap;
75+
76+
// Markdown styles
77+
a {
78+
color: inherit;
79+
text-decoration: underline;
80+
}
81+
82+
code {
83+
background-color: rgba(0, 0, 0, 0.1);
84+
padding: 0.125rem 0.25rem;
85+
border-radius: 0.25rem;
86+
font-family: monospace;
87+
font-size: 0.875rem;
88+
}
89+
90+
ul, ol {
91+
padding-left: 1.5rem;
92+
margin: 0.5rem 0;
93+
}
94+
95+
strong {
96+
font-weight: 600;
97+
}
98+
99+
em {
100+
font-style: italic;
101+
}
67102
}
68103

69104
&__time {
70105
font-size: 0.75rem;
71106
color: #999;
72107
margin-top: 0.25rem;
73108
text-align: right;
109+
opacity: 0;
110+
height: 0;
111+
overflow: hidden;
112+
transition: all 0.2s ease;
113+
114+
&--visible {
115+
opacity: 1;
116+
height: auto;
117+
}
118+
}
119+
}
120+
121+
@keyframes fadeIn {
122+
0% {
123+
opacity: 0;
124+
transform: translateY(0.5rem);
125+
}
126+
100% {
127+
opacity: 1;
128+
transform: translateY(0);
74129
}
75130
}

0 commit comments

Comments
 (0)