Skip to content

Commit d1d5070

Browse files
authored
Merge pull request #33 from Solomonsolomonsolomon/main
Feat: refactored chat-history ,integrated conversations and updated client
2 parents 7d2398a + c0dbbf1 commit d1d5070

32 files changed

+965
-232
lines changed

apps/client/app/components/layout/Header.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const Header = () => {
77
const { address, connected } = useWallet();
88

99
return (
10-
<div className="w-[75dvw] grid grid-cols-1 md:flex justify-between">
11-
<span className="flex items-center">
10+
<div className="w-[60dvw] grid grid-cols-1 md:flex justify-end">
11+
{/* <span className="flex items-center">
1212
<Image src="/coinSageLogo.png" width={50} height={50} alt="atomasage logo" priority />
1313
<p
1414
style={{
@@ -18,7 +18,7 @@ const Header = () => {
1818
>
1919
AtomaSage
2020
</p>
21-
</span>
21+
</span> */}
2222

2323
<div className="flex items-center gap-4">
2424
{connected && (

apps/client/app/components/layout/Sidebar.tsx

Lines changed: 117 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,135 @@
11
'use client';
2-
32
import { useState, useEffect } from 'react';
4-
import { FiMenu } from 'react-icons/fi';
5-
import { AiOutlineHome, AiOutlineUser, AiOutlineSetting } from 'react-icons/ai';
3+
import { useWallet } from '@suiet/wallet-kit';
4+
import { useRouter, useParams } from 'next/navigation';
5+
import api from '@/app/lib/api';
6+
import { ConversationItem } from '../sections/ConversationItem';
7+
import { KebabMenu } from '../sections/ConversationMenu';
8+
import { NewChatButton } from '../sections/NewChatButton';
9+
import { SearchBar } from '../sections/ConversationSearchBar';
10+
import { MobileSidebar } from '../sections/MobileSidebar';
11+
12+
interface Conversation {
13+
title: string;
14+
id: string;
15+
}
616

717
const Sidebar: React.FC<{ children: React.ReactNode }> = ({ children }) => {
18+
const router = useRouter();
819
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
20+
const [conversations, setConversations] = useState<Conversation[]>([]);
21+
const { connected, address } = useWallet();
22+
const { conversationId } = useParams();
23+
const [kebabMenu, setKebabMenu] = useState({
24+
show: false,
25+
x: 0,
26+
y: 0,
27+
id: ''
28+
});
929

10-
// Close menu when clicking outside
1130
useEffect(() => {
12-
const handleOutsideClick = (event: MouseEvent) => {
13-
if (
14-
isMobileMenuOpen &&
15-
!(event.target as HTMLElement).closest('#mobile-menu') &&
16-
!(event.target as HTMLElement).closest('#menu-button')
17-
) {
18-
setIsMobileMenuOpen(false);
19-
}
20-
};
31+
if (address) {
32+
const getConvoIds = async () => {
33+
try {
34+
const res = await api.get(`/conversations/user/${address}/id`);
35+
setConversations(res.data);
36+
} catch (error) {
37+
console.error(error);
38+
}
39+
};
40+
getConvoIds();
41+
}
42+
}, [connected, address]);
2143

22-
document.addEventListener('click', handleOutsideClick);
23-
return () => document.removeEventListener('click', handleOutsideClick);
24-
}, [isMobileMenuOpen]);
44+
const addChat = () => {
45+
if (!address) {
46+
alert('Connect wallet to add a new chat');
47+
} else {
48+
(async () => {
49+
try {
50+
const res = await api.post('/conversations/new', { walletAddress: address });
51+
const { _id } = res.data;
52+
setConversations((prev) => [...prev, { title: res.data?.title, id: _id }]);
53+
router.push(`/conversations/${_id}`);
54+
} catch (error) {
55+
alert('Failed to create new chat');
56+
}
57+
})();
58+
}
59+
};
60+
61+
const handleKebabClick = (event: React.MouseEvent, id: string) => {
62+
event.stopPropagation();
63+
setKebabMenu({
64+
show: true,
65+
x: event.clientX,
66+
y: event.clientY,
67+
id: id
68+
});
69+
};
70+
71+
const handleKebabClose = () => {
72+
setKebabMenu({ ...kebabMenu, show: false });
73+
};
74+
const handleDelete = async () => {
75+
try {
76+
const response = await api.delete(`/conversations/${kebabMenu.id}/remove`);
77+
if (response.status === 204) {
78+
setConversations((prevConversations) =>
79+
prevConversations.filter((convo) => convo.id !== kebabMenu.id)
80+
);
81+
handleKebabClose();
82+
// If the deleted conversation was the current one, redirect to the homepage
83+
if (kebabMenu.id === conversationId) {
84+
router.push('/');
85+
}
86+
} else {
87+
console.error('Unexpected status code:', response.status);
88+
alert('Failed to delete conversation. Server returned an unexpected response.');
89+
}
90+
} catch (error) {
91+
// Handle error (e.g., network issues, server problems)
92+
console.error('Error deleting conversation:', error);
93+
alert('Failed to delete conversation. Please check your connection and try again.');
94+
}
95+
};
2596

2697
return (
2798
<div className="flex h-screen">
28-
{/* Desktop Sidebar */}
29-
<div className="hidden sm:flex flex-col bg-purple-950 text-white w-16 hover:w-64 group transition-all duration-300">
30-
{/* Logo */}
31-
<div className="p-4 flex justify-center">
32-
<FiMenu size={24} />
99+
<div className="hidden sm:flex flex-col bg-white border-r w-72 p-4 space-y-4">
100+
<div className="flex items-center space-x-2 mb-2">
101+
<div className="w-8 h-8 rounded-full bg-purple-600"></div>
102+
<span className="text-lg font-semibold">AtomaSage</span>
33103
</div>
34-
35-
{/* Sidebar Items */}
36-
<div className="flex-1 space-y-4 mt-4">
37-
<div className="flex items-center p-4 hover:bg-gray-700 cursor-pointer">
38-
<AiOutlineHome size={24} />
39-
<span className="ml-4 opacity-0 group-hover:opacity-100 transition-opacity">Home</span>
40-
</div>
41-
<div className="flex items-center p-4 hover:bg-gray-700 cursor-pointer">
42-
<AiOutlineUser size={24} />
43-
<span className="ml-4 opacity-0 group-hover:opacity-100 transition-opacity">
44-
Profile
45-
</span>
46-
</div>
47-
<div className="flex items-center p-4 hover:bg-gray-700 cursor-pointer">
48-
<AiOutlineSetting size={24} />
49-
<span className="ml-4 opacity-0 group-hover:opacity-100 transition-opacity">
50-
Settings
51-
</span>
52-
</div>
104+
<NewChatButton addChat={addChat} />
105+
<SearchBar />
106+
<div className="overflow-scroll">
107+
{conversations.map((conversation) => (
108+
<ConversationItem
109+
key={conversation.id}
110+
conversation={conversation}
111+
conversationId={conversationId}
112+
handleKebabClick={handleKebabClick}
113+
/>
114+
))}
53115
</div>
54-
</div>
55-
56-
{/* Mobile Hamburger Menu */}
57-
<div className="sm:hidden">
58-
<button
59-
id="menu-button"
60-
className="fixed top-4 right-4 z-50 p-2 bg-gray-900 text-white rounded"
61-
onClick={() => setIsMobileMenuOpen(true)}
62-
>
63-
<FiMenu size={24} />
64-
</button>
65-
66-
{isMobileMenuOpen && (
67-
<div
68-
id="mobile-menu"
69-
className="fixed inset-0 bg-gray-800/90 text-white flex flex-col z-40 p-4"
70-
>
71-
<button className="self-end p-2" onClick={() => setIsMobileMenuOpen(false)}>
72-
73-
</button>
74-
<div className="flex-1 space-y-4 mt-4">
75-
<div className="p-4 hover:bg-gray-700 cursor-pointer">Home</div>
76-
<div className="p-4 hover:bg-gray-700 cursor-pointer">Profile</div>
77-
<div className="p-4 hover:bg-gray-700 cursor-pointer">Settings</div>
78-
</div>
79-
</div>
116+
{kebabMenu.show && (
117+
<KebabMenu
118+
x={kebabMenu.x}
119+
y={kebabMenu.y}
120+
onClose={handleKebabClose}
121+
onDelete={handleDelete}
122+
/>
80123
)}
81124
</div>
82-
83-
{/* Main Content */}
84-
<main className="flex-1">{children}</main>
125+
<MobileSidebar
126+
isOpen={isMobileMenuOpen}
127+
setIsOpen={setIsMobileMenuOpen}
128+
addChat={addChat}
129+
conversations={conversations}
130+
handleKebabClick={handleKebabClick}
131+
/>
132+
<main className="flex-1 bg-gray-50">{children}</main>
85133
</div>
86134
);
87135
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client';
2+
import { MoreVertical } from 'lucide-react';
3+
import { useRouter } from 'next/navigation';
4+
5+
interface ConversationItemProps {
6+
conversation: {
7+
title: string;
8+
id: string;
9+
};
10+
conversationId: string;
11+
handleKebabClick: (event: React.MouseEvent, id: string) => void;
12+
}
13+
14+
export const ConversationItem: React.FC<ConversationItemProps> = ({
15+
conversation,
16+
conversationId,
17+
handleKebabClick
18+
}) => {
19+
const router = useRouter();
20+
return (
21+
<div
22+
onClick={() => router.push(`/conversations/${conversation.id}`)}
23+
className={`${
24+
conversation.id === conversationId ? 'bg-purple-600' : ''
25+
} p-2 border rounded-lg mt-1 flex justify-center items-center relative`}
26+
>
27+
<p className={`${conversation.id === conversationId ? 'text-white' : ''}`}>
28+
conversation {conversation.id}
29+
</p>
30+
<MoreVertical
31+
size={30}
32+
className="z-10 cursor-pointer"
33+
onClick={(e) => handleKebabClick(e, conversation.id)}
34+
/>
35+
</div>
36+
);
37+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client';
2+
3+
interface KebabMenuProps {
4+
x: number;
5+
y: number;
6+
onClose: () => void;
7+
onDelete: () => void;
8+
}
9+
10+
export const KebabMenu: React.FC<KebabMenuProps> = ({ x, y, onClose, onDelete }) => {
11+
return (
12+
<div
13+
style={{ left: x, top: y }}
14+
className="absolute z-50 bg-white border rounded shadow-lg p-2"
15+
>
16+
<button className="block w-full text-left py-1 hover:bg-gray-100" onClick={onDelete}>
17+
Delete
18+
</button>
19+
<button className="block w-full text-left py-1 hover:bg-gray-100" onClick={onClose}>
20+
Cancel
21+
</button>
22+
</div>
23+
);
24+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use client';
2+
import { AiOutlineSearch } from 'react-icons/ai';
3+
4+
export const SearchBar = () => {
5+
return (
6+
<div className="relative">
7+
<AiOutlineSearch
8+
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
9+
size={20}
10+
/>
11+
<input
12+
type="text"
13+
placeholder="Search"
14+
className="w-full bg-gray-100 p-2 pl-10 rounded-lg focus:outline-none border-none"
15+
/>
16+
</div>
17+
);
18+
};

apps/client/app/components/sections/Messages.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React from 'react';
1+
import React, { useEffect, useRef } from 'react';
2+
import JSONFormatter from '@/app/utils/JSONFormatter';
23

34
interface Message {
4-
text: string;
5-
sender: 'user' | 'llm';
5+
message: string;
6+
sender: 'user' | 'ai';
67
isHTML?: boolean;
78
}
89

@@ -11,25 +12,46 @@ interface MessagesProps {
1112
}
1213

1314
const Messages: React.FC<MessagesProps> = ({ messages }) => {
15+
const messagesEndRef = useRef<HTMLDivElement | null>(null);
16+
useEffect(() => {
17+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
18+
}, [messages]);
19+
20+
const processedMessages = messages.map((message) => {
21+
try {
22+
const parsed = JSON.parse(message.message);
23+
if (parsed && typeof parsed === 'object') {
24+
return { ...message, isHTML: true };
25+
}
26+
} catch (error) {
27+
// Not a JSON string, keep as is
28+
}
29+
return { ...message, isHTML: false };
30+
});
31+
1432
return (
15-
<div>
16-
{' '}
17-
{messages.map((message, index) => (
33+
<div className="h-full overflow-y-auto relative">
34+
{processedMessages.map((message, index) => (
1835
<div
1936
key={index}
20-
className={`relative mb-3 p-3 rounded-md w-fit md:max-w-[40%] break-words opacity-100 ${
37+
className={`relative mb-3 p-3 rounded-md w-fit md:max-w-[40%] break-words opacity-100 ${
2138
message.sender === 'user'
2239
? 'bg-blue-500 text-white self-end ml-auto text-right'
2340
: 'bg-gray-300 text-black self-start mr-auto text-left'
2441
}`}
2542
>
2643
{message.isHTML ? (
27-
<div dangerouslySetInnerHTML={{ __html: message.text }} />
44+
<div
45+
dangerouslySetInnerHTML={{
46+
__html: JSONFormatter.format(JSON.parse(message.message))
47+
}}
48+
/>
2849
) : (
29-
<div>{message.text}</div>
50+
<div>{message.message}</div>
3051
)}
3152
</div>
3253
))}
54+
<div ref={messagesEndRef} />
3355
</div>
3456
);
3557
};

0 commit comments

Comments
 (0)