gbclient/app/chat/page.tsx
Rodrigo Rodriguez (Pragmatismo) 473fae930a
Some checks failed
GBCI / build (push) Failing after 2m23s
feat: add new UI components including Drawer, InputOTP, Pagination, Sidebar, Sonner, and ToggleGroup
- Implemented Drawer component for modal-like functionality.
- Added InputOTP component for handling one-time password inputs.
- Created Pagination component for navigating through paginated content.
- Developed Sidebar component with collapsible and mobile-friendly features.
- Integrated Sonner for toast notifications with theme support.
- Introduced ToggleGroup for grouping toggle buttons with context support.
- Added useIsMobile hook to determine mobile view based on screen width.
2025-06-21 19:45:21 -03:00

349 lines
No EOL
14 KiB
TypeScript

"use client";
import React, { useState, useRef, useEffect } from 'react';
import {
Send, Plus, Menu, Search, Archive, Trash2, Edit3,
MessageSquare, User, Bot, Copy, ThumbsUp, ThumbsDown,
RotateCcw, Share, MoreHorizontal, Image, Video, FileText,
Code, Table, Music, Mic, Settings, Zap, Brain, Sparkles,
BarChart2, Clock, Globe, Lock, Unlock, Book, Feather,
Camera, Film, Headphones, Download, Upload, Link, Mail,
AlertCircle, CheckCircle, HelpCircle, Info, Star, Heart,
Award, Gift, Coffee, ShoppingCart, CreditCard, Key,
Map, Navigation, Phone, Tag, Watch, Wifi,
Cloud, Sun, Moon, Umbrella, Droplet, Wind
} from 'lucide-react';
const ChatPage = () => {
const [messages, setMessages] = useState([
{
id: 1,
type: 'assistant',
content: "Hello! I'm General Bots, a large language model by Pragmatismo. How can I help you today?",
timestamp: new Date().toISOString()
}
]);
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [conversations, setConversations] = useState([
{ id: 1, title: 'Current Chat', timestamp: new Date(), active: true },
{ id: 2, title: 'Previous Conversation', timestamp: new Date(Date.now() - 86400000), active: false },
{ id: 3, title: 'Code Review Discussion', timestamp: new Date(Date.now() - 172800000), active: false },
{ id: 4, title: 'Project Planning', timestamp: new Date(Date.now() - 259200000), active: false },
]);
const [activeMode, setActiveMode] = useState('assistant');
const messagesEndRef = useRef(null);
const textareaRef = useRef(null);
// Mode buttons
const modeButtons = [
{ id: 'deep-think', icon: <Brain size={16} />, label: 'Deep Think' },
{ id: 'web', icon: <Globe size={16} />, label: 'Web' },
{ id: 'image', icon: <Image size={16} />, label: 'Image' },
{ id: 'video', icon: <Video size={16} />, label: 'Video' },
];
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSubmit = async () => {
if (!input.trim()) return;
const userMessage = {
id: Date.now(),
type: 'user',
content: input.trim(),
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsTyping(true);
// Simulate assistant response
setTimeout(() => {
const assistantMessage = {
id: Date.now() + 1,
type: 'assistant',
content: `I understand you're asking about "${input.trim()}". This is a simulated response to demonstrate the chat interface. The actual implementation would connect to your chat provider and send real responses.`,
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, assistantMessage]);
setIsTyping(false);
}, 1500);
};
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
const now = new Date();
const diffInHours = (now - date) / (1000 * 60 * 60);
if (diffInHours < 24) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else if (diffInHours < 48) {
return 'Yesterday';
} else {
return date.toLocaleDateString();
}
};
const newChat = () => {
const newConv = {
id: Date.now(),
title: 'New Chat',
timestamp: new Date(),
active: true
};
setConversations(prev => [newConv, ...prev.map(c => ({ ...c, active: false }))]);
setMessages([{
id: Date.now(),
type: 'assistant',
content: "Hello! I'm General Bots, a large language model by Pragmatismo. How can I help you today?",
timestamp: new Date().toISOString()
}]);
};
const MessageActions = ({ message }) => (
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<button className="p-1.5 hover:bg-secondary rounded-md transition-colors">
<Copy className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button className="p-1.5 hover:bg-secondary rounded-md transition-colors">
<ThumbsUp className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button className="p-1.5 hover:bg-secondary rounded-md transition-colors">
<ThumbsDown className="w-3.5 h-3.5 text-muted-foreground" />
</button>
<button className="p-1.5 hover:bg-secondary rounded-md transition-colors">
<Share className="w-3.5 h-3.5 text-muted-foreground" />
</button>
</div>
);
// Auto-resize textarea
useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
}
}, [input]);
return (
<div className="flex min-h-[calc(100vh-43px)] bg-background text-foreground">
{/* Sidebar */}
<div
className={`${
sidebarOpen ? 'w-80' : 'w-0'
} transition-all duration-300 ease-in-out bg-card border-r border-border flex flex-col overflow-hidden`}
>
{sidebarOpen && (
<>
{/* Sidebar Header */}
<div className="p-4 border-b border-border flex-shrink-0">
<button
onClick={newChat}
className="flex items-center gap-3 w-full p-3 rounded-xl border-2 border-dashed border-muted hover:border-accent hover:bg-accent/10 transition-all duration-200 group"
>
<Plus className="w-5 h-5 text-muted-foreground group-hover:text-accent" />
<span className="font-medium text-foreground group-hover:text-accent">
New Chat
</span>
</button>
</div>
{/* Conversations List */}
<div className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto px-3 py-2 space-y-1">
{conversations.map(conv => (
<div
key={conv.id}
className={`group relative flex items-center gap-3 p-3 rounded-xl cursor-pointer transition-all duration-200 ${
conv.active
? 'bg-primary/10 border border-primary'
: 'hover:bg-secondary'
}`}
onClick={() => {
setConversations(prev => prev.map(c => ({ ...c, active: c.id === conv.id })));
}}
>
<MessageSquare className={`w-4 h-4 flex-shrink-0 ${
conv.active ? 'text-primary' : 'text-muted-foreground'
}`} />
<div className="flex-1 min-w-0">
<div className={`font-medium truncate ${
conv.active ? 'text-primary' : 'text-foreground'
}`}>
{conv.title}
</div>
<div className="text-xs text-muted-foreground truncate">
{formatTimestamp(conv.timestamp)}
</div>
</div>
{conv.active && (
<div className="w-2 h-2 rounded-full bg-primary flex-shrink-0"></div>
)}
</div>
))}
</div>
</div>
{/* Sidebar Footer */}
<div className="p-4 border-t border-border flex-shrink-0">
<div className="flex items-center gap-3 p-3 rounded-xl hover:bg-secondary cursor-pointer transition-colors duration-200">
<User className="w-5 h-5 text-muted-foreground" />
<User className="w-5 h-5 text-muted-foreground" />
<User className="w-5 h-5 text-muted-foreground" />
<User className="w-5 h-5 text-muted-foreground" />
<User className="w-5 h-5 text-muted-foreground" />
</div>
</div>
</>
)}
</div>
{/* Main Chat Area */}
<div className="flex-1 flex flex-col min-w-0 bg-background">
{/* Header */}
<div className="flex-shrink-0 p-4 border-b border-border bg-card">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="p-2 hover:bg-secondary rounded-lg transition-colors duration-200"
>
<Menu className="w-5 h-5" />
</button>
<h1 className="text-xl font-semibold text-foreground">
{conversations.find(c => c.active)?.title || 'New Chat'}
</h1>
</div>
<button className="p-2 hover:bg-secondary rounded-lg transition-colors duration-200">
<Search className="w-5 h-5" />
</button>
</div>
</div>
{/* Messages Container */}
<div className="flex-1 overflow-hidden flex flex-col">
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-6">
{messages.map(message => (
<div
key={message.id}
className={`group flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-[85%] md:max-w-[75%] ${
message.type === 'user'
? 'bg-primary text-primary-foreground'
: 'bg-secondary text-secondary-foreground'
} rounded-2xl px-4 py-3 shadow-sm`}>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 mt-0.5">
{message.type === 'user' ? (
<User className="w-4 h-4" />
) : (
<Bot className="w-4 h-4" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="whitespace-pre-wrap break-words leading-relaxed">
{message.content}
</div>
<div className={`mt-3 flex items-center justify-between text-xs ${
message.type === 'user'
? 'text-primary-foreground/80'
: 'text-muted-foreground'
}`}>
<span>{formatTimestamp(message.timestamp)}</span>
{message.type === 'assistant' && <MessageActions message={message} />}
</div>
</div>
</div>
</div>
</div>
))}
{isTyping && (
<div className="flex justify-start">
<div className="max-w-[85%] md:max-w-[75%] bg-secondary rounded-2xl px-4 py-3 shadow-sm">
<div className="flex items-center gap-3">
<Bot className="w-4 h-4 text-muted-foreground" />
<div className="flex gap-1">
<div className="w-2 h-2 rounded-full bg-muted-foreground animate-bounce"></div>
<div className="w-2 h-2 rounded-full bg-muted-foreground animate-bounce" style={{ animationDelay: '0.2s' }}></div>
<div className="w-2 h-2 rounded-full bg-muted-foreground animate-bounce" style={{ animationDelay: '0.4s' }}></div>
</div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
{/* Mode Carousel */}
<div className="flex-shrink-0 border-t border-border bg-card">
<div className="overflow-x-auto px-4 py-3">
<div className="flex gap-2 min-w-max">
{modeButtons.map(button => (
<button
key={button.id}
onClick={() => setActiveMode(button.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 whitespace-nowrap ${
activeMode === button.id
? 'bg-primary/10 text-primary border border-primary'
: 'bg-secondary text-secondary-foreground hover:bg-secondary/80'
}`}
>
{button.icon}
<span>{button.label}</span>
</button>
))}
</div>
</div>
</div>
{/* Input Area */}
<div className="flex-shrink-0 p-4 border-t border-border bg-card">
<div className="relative max-w-4xl mx-auto">
<textarea
ref={textareaRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your message..."
className="w-full p-4 pr-14 rounded-2xl border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent resize-none transition-all duration-200"
rows={1}
style={{ minHeight: '56px', maxHeight: '120px' }}
/>
<button
onClick={handleSubmit}
disabled={!input.trim()}
className={`absolute right-6 bottom-3 p-2.5 rounded-xl transition-all duration-200 ${
input.trim()
? 'bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transform hover:scale-105'
: 'bg-muted text-muted-foreground cursor-not-allowed'
}`}
>
<Send className="w-5 h-5 " />
</button>
</div>
</div>
</div>
</div>
);
};
export default ChatPage;