
Some checks failed
GBCI / build (push) Failing after 3m42s
- Implemented ChatProvider to manage chat context and user state. - Added API fetching utility for chat instance and activity handling. - Integrated chat service with methods for sending activities. - Updated Tailwind CSS configuration to include additional content paths and custom utilities. - Added client-side rendering directive to mode-toggle component. - Created README.md for settings documentation.
354 lines
No EOL
15 KiB
TypeScript
354 lines
No EOL
15 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: 'creative', icon: <Sparkles size={16} />, label: 'Creative' },
|
|
{ id: 'image', icon: <Image size={16} />, label: 'Image' },
|
|
{ id: 'video', icon: <Video size={16} />, label: 'Video' },
|
|
{ id: 'document', icon: <FileText size={16} />, label: 'Document' },
|
|
{ id: 'code', icon: <Code size={16} />, label: 'Code' },
|
|
{ id: 'data', icon: <Table size={16} />, label: 'Data' },
|
|
{ id: 'audio', icon: <Headphones size={16} />, label: 'Audio' },
|
|
{ id: 'analysis', icon: <BarChart2 size={16} />, label: 'Analysis' },
|
|
{ id: 'research', icon: <Book size={16} />, label: 'Research' },
|
|
{ id: 'writing', icon: <Feather size={16} />, label: 'Writing' },
|
|
];
|
|
|
|
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-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors">
|
|
<Copy className="w-3.5 h-3.5" />
|
|
</button>
|
|
<button className="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors">
|
|
<ThumbsUp className="w-3.5 h-3.5" />
|
|
</button>
|
|
<button className="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors">
|
|
<ThumbsDown className="w-3.5 h-3.5" />
|
|
</button>
|
|
<button className="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors">
|
|
<Share className="w-3.5 h-3.5" />
|
|
</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-95px)] bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white">
|
|
{/* Sidebar */}
|
|
<div
|
|
className={`${
|
|
sidebarOpen ? 'w-80' : 'w-0'
|
|
} transition-all duration-300 ease-in-out bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col overflow-hidden`}
|
|
>
|
|
{sidebarOpen && (
|
|
<>
|
|
{/* Sidebar Header */}
|
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
<button
|
|
onClick={newChat}
|
|
className="flex items-center gap-3 w-full p-3 rounded-xl border-2 border-dashed border-gray-300 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all duration-200 group"
|
|
>
|
|
<Plus className="w-5 h-5 text-gray-600 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400" />
|
|
<span className="font-medium text-gray-700 dark:text-gray-300 group-hover:text-blue-700 dark:group-hover:text-blue-300">
|
|
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-blue-100 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700'
|
|
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
|
|
}`}
|
|
onClick={() => {
|
|
setConversations(prev => prev.map(c => ({ ...c, active: c.id === conv.id })));
|
|
}}
|
|
>
|
|
<MessageSquare className={`w-4 h-4 flex-shrink-0 ${
|
|
conv.active ? 'text-blue-600 dark:text-blue-400' : 'text-gray-500 dark:text-gray-400'
|
|
}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className={`font-medium truncate ${
|
|
conv.active ? 'text-blue-900 dark:text-blue-100' : 'text-gray-900 dark:text-gray-100'
|
|
}`}>
|
|
{conv.title}
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
|
{formatTimestamp(conv.timestamp)}
|
|
</div>
|
|
</div>
|
|
{conv.active && (
|
|
<div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0"></div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar Footer */}
|
|
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
<div className="flex items-center gap-3 p-3 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-700/50 cursor-pointer transition-colors duration-200">
|
|
<User className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
|
<span className="font-medium text-gray-700 dark:text-gray-300">Account</span>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Main Chat Area */}
|
|
<div className="flex-1 flex flex-col min-w-0 bg-white dark:bg-gray-900">
|
|
{/* Header */}
|
|
<div className="flex-shrink-0 p-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors duration-200"
|
|
>
|
|
<Menu className="w-5 h-5" />
|
|
</button>
|
|
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
{conversations.find(c => c.active)?.title || 'New Chat'}
|
|
</h1>
|
|
</div>
|
|
<button className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors duration-200">
|
|
<Search className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Messages Container - This is the key fix */}
|
|
<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-blue-600 text-white'
|
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white'
|
|
} 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-blue-100'
|
|
: 'text-gray-500 dark:text-gray-400'
|
|
}`}>
|
|
<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-gray-100 dark:bg-gray-800 rounded-2xl px-4 py-3 shadow-sm">
|
|
<div className="flex items-center gap-3">
|
|
<Bot className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
|
<div className="flex gap-1">
|
|
<div className="w-2 h-2 rounded-full bg-gray-400 animate-bounce"></div>
|
|
<div className="w-2 h-2 rounded-full bg-gray-400 animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
|
<div className="w-2 h-2 rounded-full bg-gray-400 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-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
|
<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-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-700'
|
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
{button.icon}
|
|
<span>{button.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Input Area */}
|
|
<div className="flex-shrink-0 p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
|
<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-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 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-3 bottom-3 p-2.5 rounded-xl transition-all duration-200 ${
|
|
input.trim()
|
|
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-lg hover:shadow-xl transform hover:scale-105'
|
|
: 'bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400 cursor-not-allowed'
|
|
}`}
|
|
>
|
|
<Send className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ChatPage; |