Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Frontend/src/components/chat/QuickRefineToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from "react";
import { Scissors, Briefcase, List, RotateCcw } from "lucide-react";

interface QuickRefineToolbarProps {
onRefine: (prompt: string) => void;
}

const QuickRefineToolbar: React.FC<QuickRefineToolbarProps> = ({ onRefine }) => {
const actions = [
{
id: "shorten",
label: "Shorten",
icon: <Scissors className="h-3 w-3" />,
prompt: "Summarize the previous response to be much shorter."
},
{
id: "pro",
label: "Professional",
icon: <Briefcase className="h-3 w-3" />,
prompt: "Rewrite the previous response in a professional, formal tone."
},
{
id: "list",
label: "Listify",
icon: <List className="h-3 w-3" />,
prompt: "Convert the previous response into a bulleted list."
},
{
id: "retry",
label: "Retry",
icon: <RotateCcw className="h-3 w-3" />,
prompt: "Regenerate the previous response."
},
];

return (
<div className="flex flex-wrap gap-2 mt-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-in-out justify-start">
{actions.map((action) => (
<button
key={action.id}
onClick={() => onRefine(action.prompt)}
className="flex items-center gap-1 px-2 py-1 text-[10px] font-medium bg-white hover:bg-purple-50 border border-gray-200 rounded-md transition-all shadow-sm text-gray-600 hover:text-purple-700 hover:border-purple-200"
>
{action.icon}
{action.label}
</button>
))}
</div>
);
};

export default QuickRefineToolbar;
60 changes: 33 additions & 27 deletions Frontend/src/components/chat/message-item.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,61 @@
import { type Message } from "@/redux/chatSlice";
import { CheckCheckIcon, CheckIcon } from "lucide-react";

import React from "react";
import QuickRefineToolbar from "./QuickRefineToolbar";

interface MessageItemProps {
message: Message;
onSendMessage?: (msg: string) => void;
}

export default function MessageItem({ message }: { message: Message }) {
export default function MessageItem({ message, onSendMessage }: MessageItemProps) {
return (
<>
{message.isSent ? (
<div className="flex justify-end">
<div className="bg-purple-700 text-white rounded-lg p-3 max-w-md">
<p>{message.message}</p>
/* User Messages */
<div className="flex justify-end mb-4">
<div className="bg-purple-700 text-white rounded-lg p-3 max-w-md shadow-sm">
<p className="text-sm">{message.message}</p>
<div className="flex justify-end items-center text-xs mt-1 text-gray-200">
<span className="mr-1">
{new Date(message.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
{message.status === "sent" && (
<span>
<CheckIcon className="h-3 w-3 inline" />
</span>
)}
{message.status === "sent" && <CheckIcon className="h-3 w-3 inline" />}
{message.status === "delivered" && (
<span>
<CheckCheckIcon className="h-3 w-3 inline text-gray-300/70" />
</span>
<CheckCheckIcon className="h-3 w-3 inline text-gray-300/70" />
)}
{message.status === "seen" && (
<span>
<CheckCheckIcon className="h-3 w-3 inline text-blue-300" />
</span>
<CheckCheckIcon className="h-3 w-3 inline text-blue-300" />
)}
</div>
</div>
</div>
) : (
<div className="flex justify-start">
<div className="bg-gray-100 rounded-lg p-3 max-w-md">
<p>{message.message}</p>
<div className="flex justify-end items-center text-xs mt-1 text-gray-500">
<span>
{new Date(message.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
/* AI Messages - Added 'group' for hover logic */
<div className="flex justify-start mb-4 group">
<div className="flex flex-col items-start max-w-md">
<div className="bg-gray-100 rounded-lg p-3 shadow-sm border border-gray-200">
<p className="text-sm text-gray-800">{message.message}</p>
<div className="flex justify-end items-center text-xs mt-1 text-gray-500">
<span>
{new Date(message.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
</div>

{/* Toolbar only appears for AI responses on hover */}
{onSendMessage && (
<QuickRefineToolbar onRefine={onSendMessage} />
)}
</div>
</div>
)}
</>
);
}
}
65 changes: 26 additions & 39 deletions Frontend/src/components/chat/messages-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ export default function MessagesList({ messages }: { messages: Message[] }) {
(state: RootState) => state.chat.selectedChatId
);

const { markMessageAsSeen, sendMessage } = useChat();

useEffect(() => {
setLastMarkedAsSeen(new Date().toISOString());
}, [selectedChatId]);

const { markMessageAsSeen } = useChat();

useEffect(() => {
const unseenMessages = messages.filter(
(message) =>
Expand All @@ -33,56 +33,43 @@ export default function MessagesList({ messages }: { messages: Message[] }) {
});
setLastMarkedAsSeen(new Date().toISOString());
}
}, [messages]);
}, [messages, lastMarkedAsSeen, markMessageAsSeen]);

return (
<>
<div className="flex flex-col w-full px-4">
{messages.length > 0 ? (
<>
{messages.reduce((acc: JSX.Element[], message, index, array) => {
// Add date separator for first message
// Date Separator Logic
const messageDate = parseISO(message.createdAt);
if (index === 0) {
const firstDate = parseISO(message.createdAt);
acc.push(
<div
key={`date-first-${message.id}`}
className="flex justify-center my-4"
>
<div key={`date-first-${message.id}`} className="flex justify-center my-4">
<div className="bg-gray-100 px-3 py-1 rounded-full text-sm text-gray-500">
{format(firstDate, "PPP")}
{format(messageDate, "PPP")}
</div>
</div>
);
}

// Add the message component
acc.push(<MessageItem key={message.id} message={message} />);
// Push Message Item
acc.push(
<MessageItem
key={message.id}
message={message}
onSendMessage={sendMessage}
/>
);

// Check if the next message is from a different date
// Date separator for subsequent messages
if (index < array.length - 1) {
const currentDate = parseISO(message.createdAt);
const nextDate = parseISO(array[index + 1].createdAt);

// Check if dates are different
if (
!isEqual(
new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
currentDate.getDate()
),
new Date(
nextDate.getFullYear(),
nextDate.getMonth(),
nextDate.getDate()
)
)
) {
if (!isEqual(
new Date(messageDate.setHours(0,0,0,0)),
new Date(nextDate.setHours(0,0,0,0))
)) {
acc.push(
<div
key={`date-${message.id}`}
className="flex justify-center my-4"
>
<div key={`date-${array[index+1].id}`} className="flex justify-center my-4">
<div className="bg-gray-100 px-3 py-1 rounded-full text-sm text-gray-500">
{format(nextDate, "PPP")}
</div>
Expand All @@ -95,10 +82,10 @@ export default function MessagesList({ messages }: { messages: Message[] }) {
}, [])}
</>
) : (
<div className="flex justify-center items-center h-full">
<p className="text-gray-500">No messages yet</p>
<div className="flex justify-center items-center h-64">
<p className="text-gray-400 italic">No messages yet. Start a conversation!</p>
</div>
)}
</>
</div>
);
}
}
11 changes: 7 additions & 4 deletions Frontend/src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React from "react";
import { cn } from "../../lib/utils";
import * as React from "react";
import { cn } from "@/lib/utils";

const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
Expand All @@ -18,4 +21,4 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
);
Input.displayName = "Input";

export { Input };
export { Input };
30 changes: 8 additions & 22 deletions Frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,37 +117,23 @@
body {
@apply bg-background text-foreground;
}
}

/* Custom Animations */
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}

@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}

@keyframes glow {
0%, 100% {
box-shadow: 0 0 20px rgba(147, 51, 234, 0.3);
}
50% {
box-shadow: 0 0 40px rgba(147, 51, 234, 0.6);
}
0%, 100% { box-shadow: 0 0 20px rgba(147, 51, 234, 0.3); }
50% { box-shadow: 0 0 40px rgba(147, 51, 234, 0.6); }
}

.animate-gradient {
Expand Down