Skip to content

Commit

Permalink
feat: create and adviser chat app
Browse files Browse the repository at this point in the history
  • Loading branch information
alireza-akbarzadeh committed Feb 1, 2025
1 parent 737e0eb commit 1a7cdcc
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 21 deletions.
10 changes: 10 additions & 0 deletions app/plan-advisor/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ChatInterface } from '@/components/plan-advisor/chat-interface';
import { ChatLayout } from '@/components/plan-advisor/chat-layout';

export default function PlanAdvisorPage() {
return (
<div className="h-screen w-full overflow-hidden bg-black">
<ChatLayout />
</div>
);
}
32 changes: 32 additions & 0 deletions components/plan-advisor/chat-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import { Plus, Search, LightbulbIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';

export function ChatHeader() {
return (
<div className="flex items-center gap-2 border-b border-gray-800 p-4">
<Button
variant="outline"
className="flex gap-2 rounded-full bg-[#262626] text-gray-300 hover:bg-[#363636] hover:text-white"
>
<Plus className="size-4" />
<span>New chat</span>
</Button>
<Button
variant="outline"
className="flex gap-2 rounded-full bg-[#262626] text-gray-300 hover:bg-[#363636] hover:text-white"
>
<Search className="size-4" />
<span>Search</span>
</Button>
<Button
variant="outline"
className="flex gap-2 rounded-full bg-[#262626] text-gray-300 hover:bg-[#363636] hover:text-white"
>
<LightbulbIcon className="size-4" />
<span>Reason</span>
</Button>
</div>
);
}
138 changes: 138 additions & 0 deletions components/plan-advisor/chat-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
'use client';

import * as React from 'react';
import { Send, Image, FileText, Mic, MoreHorizontal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';

interface ChatInputProps {
input: string;
setInput: (value: string) => void;
onSubmit: (e: React.FormEvent) => void;
}

export function ChatInput({ input, setInput, onSubmit }: ChatInputProps) {
const textareaRef = React.useRef<HTMLTextAreaElement>(null);

const adjustTextareaHeight = () => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
}
};

React.useEffect(() => {
adjustTextareaHeight();
}, [input]);

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSubmit(e);
}
};

return (
<div className="border-t border-gray-800 p-4">
<form onSubmit={onSubmit} className="relative space-y-4">
<ChatInputActions />
<div className="relative">
<Textarea
ref={textareaRef}
value={input}
onChange={(e) => {
setInput(e.target.value);
adjustTextareaHeight();
}}
onKeyDown={handleKeyDown}
placeholder="Message ChatGPT..."
rows={1}
className="min-h-[52px] w-full resize-none rounded-2xl border-gray-700 bg-[#262626] pr-12 text-white placeholder:text-gray-400 focus:border-gray-600"
style={{
maxHeight: '200px',
overflow: 'auto',
}}
/>
<Button
type="submit"
size="icon"
className="absolute bottom-1.5 right-1.5 size-8 rounded-xl bg-orange-600 p-2 hover:bg-orange-700 disabled:opacity-50"
disabled={!input.trim()}
>
<Send className="size-4" />
</Button>
</div>
</form>
<div className="mt-2 text-center text-xs text-gray-500">
Press Enter to send, Shift + Enter for new line
</div>
</div>
);
}

function ChatInputActions() {
return (
<div className="flex items-center gap-2 px-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-300"
>
<Image className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Upload image</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-300"
>
<FileText className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Upload file</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-300"
>
<Mic className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Voice message</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
type="button"
variant="ghost"
size="icon"
className="ml-auto text-gray-400 hover:text-gray-300"
>
<MoreHorizontal className="size-4" />
</Button>
</div>
);
}
44 changes: 44 additions & 0 deletions components/plan-advisor/chat-interface.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import * as React from 'react';
import { ChatHeader } from './chat-header';
import { ChatMessages } from './chat-messages';
import { ChatInput } from './chat-input';
import { Message } from './types';

export function ChatInterface() {
const [messages, setMessages] = React.useState<Message[]>([
{
role: 'assistant',
content:
"Hello! I'm here to help you choose the perfect plan for your needs. What type of features are you looking for?",
},
]);
const [input, setInput] = React.useState('');

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;

const newMessages = [
...messages,
{ role: 'user', content: input },
{
role: 'assistant',
content:
'Thank you for sharing. Let me analyze your needs and suggest the most suitable plan...',
},
];

setMessages(newMessages as Message[]);
setInput('');
};

return (
<div className="flex h-full flex-col">
<ChatHeader />
<ChatMessages messages={messages} />
<ChatInput input={input} setInput={setInput} onSubmit={handleSubmit} />
</div>
);
}
36 changes: 36 additions & 0 deletions components/plan-advisor/chat-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';

import * as React from 'react';
import { Menu } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ChatSidebar } from './chat-sidebar';
import { ChatInterface } from './chat-interface';

export function ChatLayout() {
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);

return (
<div className="relative flex h-screen bg-black">
<ChatSidebar
isOpen={isSidebarOpen}
onClose={() => setIsSidebarOpen(false)}
/>

<main className="flex flex-1 flex-col bg-[#1A1A1A]">
<div className="flex h-14 items-center border-b border-gray-800 px-4 md:hidden">
<Button
variant="ghost"
size="sm"
className="text-white"
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
>
<Menu className="size-5" />
</Button>
</div>
<div className="flex-1 overflow-hidden">
<ChatInterface />
</div>
</main>
</div>
);
}
45 changes: 45 additions & 0 deletions components/plan-advisor/chat-messages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
import { Message } from './types';

interface ChatMessagesProps {
messages: Message[];
}

export function ChatMessages({ messages }: ChatMessagesProps) {
return (
<ScrollArea className="flex-1 p-4">
<div className="space-y-6">
{messages.map((message, index) => (
<div
key={index}
className={`flex items-start gap-3 ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
{message.role === 'assistant' && (
<div className="flex size-8 items-center justify-center rounded-full bg-gray-800 text-white">
AI
</div>
)}
<div
className={cn(
'max-w-[80%] rounded-lg px-4 py-2',
message.role === 'user'
? 'bg-orange-600 text-white'
: 'bg-gray-800 text-gray-100',
)}
>
{message.content}
</div>
{message.role === 'user' && (
<div className="flex size-8 items-center justify-center rounded-full bg-gray-700 text-white">
You
</div>
)}
</div>
))}
</div>
</ScrollArea>
);
}
54 changes: 54 additions & 0 deletions components/plan-advisor/chat-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client';

import * as React from 'react';
import { Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';

interface ChatSidebarProps {
isOpen: boolean;
onClose: () => void;
}

export function ChatSidebar({ isOpen, onClose }: ChatSidebarProps) {
const [conversations] = React.useState([
{ id: 1, title: 'Previous consultation 1', date: '2024-02-20' },
{ id: 2, title: 'Previous consultation 2', date: '2024-02-19' },
]);

return (
<div
className={cn(
'fixed inset-y-0 left-0 z-40 flex w-[260px] flex-col bg-black transition-transform duration-300 ease-in-out md:relative md:translate-x-0',
isOpen ? 'translate-x-0' : '-translate-x-full',
)}
>
<div className="flex h-14 items-center border-b border-gray-800 px-2">
<Button
variant="outline"
className="w-full justify-start space-x-2 border-gray-700 bg-transparent text-white hover:bg-gray-800"
>
<Plus className="size-4" />
<span>New Consultation</span>
</Button>
</div>
<ScrollArea className="flex-1 px-2 py-4">
<div className="space-y-1">
{conversations.map((chat) => (
<Button
key={chat.id}
variant="ghost"
className="w-full justify-start px-3 py-2 text-gray-300 hover:bg-gray-800"
>
<div className="flex flex-col items-start text-sm">
<span className="font-medium">{chat.title}</span>
<span className="text-xs text-gray-500">{chat.date}</span>
</div>
</Button>
))}
</div>
</ScrollArea>
</div>
);
}
4 changes: 4 additions & 0 deletions components/plan-advisor/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Message {
role: 'user' | 'assistant';
content: string;
}
Loading

0 comments on commit 1a7cdcc

Please sign in to comment.