2025-10-26 00:02:19 -03:00
<!-- Riot.js component for the chat page (converted from app/chat/page.tsx) -->
< template >
< div class = "flex min-h-[calc(100vh-43px)] bg-background text-foreground" >
<!-- Sidebar -->
< div class = "{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 class = "p-4 border-b border-border flex-shrink-0" >
< button @ click = {newChat} class = "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 class = "w-5 h-5 text-muted-foreground group-hover:text-accent" / >
< span class = "font-medium text-foreground group-hover:text-accent" > New Chat< / span >
< / button >
< / div >
<!-- Conversations List -->
< div class = "flex-1 overflow-hidden" >
< div class = "h-full overflow-y-auto px-3 py-2 space-y-1" >
{conversations.map(conv => (
< div
key={conv.id}
class="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'}"
@click={() => setActiveConversation(conv.id)}
>
< MessageSquare class = "w-4 h-4 flex-shrink-0 {conv.active ? 'text-primary' : 'text-muted-foreground'}" / >
< div class = "flex-1 min-w-0" >
< div class = "font-medium truncate {conv.active ? 'text-primary' : 'text-foreground'}" > {conv.title}< / div >
< div class = "text-xs text-muted-foreground truncate" > {formatTimestamp(conv.timestamp)}< / div >
< / div >
{conv.active & & < div class = "w-2 h-2 rounded-full bg-primary flex-shrink-0" > < / div > }
< / div >
))}
< / div >
< / div >
<!-- Sidebar Footer -->
< div class = "p-4 border-t border-border flex-shrink-0" >
< div class = "flex items-center gap-3 p-3 rounded-xl hover:bg-secondary cursor-pointer transition-colors duration-200" >
< User class = "w-5 h-5 text-muted-foreground" / >
< User class = "w-5 h-5 text-muted-foreground" / >
< User class = "w-5 h-5 text-muted-foreground" / >
< User class = "w-5 h-5 text-muted-foreground" / >
< User class = "w-5 h-5 text-muted-foreground" / >
< / div >
< / div >
< />
)}
< / div >
<!-- Main Chat Area -->
< div class = "flex-1 flex flex-col min-w-0 bg-background" >
<!-- Header -->
< div class = "flex-shrink-0 p-4 border-b border-border bg-card" >
< div class = "flex items-center justify-between" >
< div class = "flex items-center gap-4" >
< button @ click = {() = > sidebarOpen = !sidebarOpen} class="p-2 hover:bg-secondary rounded-lg transition-colors duration-200">
< Menu class = "w-5 h-5" / >
< / button >
< h1 class = "text-xl font-semibold text-foreground" > {activeConversation?.title || 'New Chat'}< / h1 >
< / div >
< button class = "p-2 hover:bg-secondary rounded-lg transition-colors duration-200" >
< Search class = "w-5 h-5" / >
< / button >
< / div >
< / div >
<!-- Messages Container -->
< div class = "flex-1 overflow-hidden flex flex-col" >
< div class = "flex-1 overflow-y-auto px-4 py-6 space-y-6" >
{messages.map(message => (
< div key = {message.id} class = "group flex {message.type === 'user' ? 'justify-end' : 'justify-start'}" >
< div class = "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 class = "flex items-start gap-3" >
< div class = "flex-shrink-0 mt-0.5" >
{message.type === 'user' ? < User class = "w-4 h-4" / > : < Bot class = "w-4 h-4" / > }
< / div >
< div class = "flex-1 min-w-0" >
< div class = "whitespace-pre-wrap break-words leading-relaxed" > {message.content}< / div >
< div class = "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' & & (
< div class = "flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200" >
< button class = "p-1.5 hover:bg-secondary rounded-md transition-colors" >
< Copy class = "w-3.5 h-3.5 text-muted-foreground" / >
< / button >
< button class = "p-1.5 hover:bg-secondary rounded-md transition-colors" >
< ThumbsUp class = "w-3.5 h-3.5 text-muted-foreground" / >
< / button >
< button class = "p-1.5 hover:bg-secondary rounded-md transition-colors" >
< ThumbsDown class = "w-3.5 h-3.5 text-muted-foreground" / >
< / button >
< button class = "p-1.5 hover:bg-secondary rounded-md transition-colors" >
< Share class = "w-3.5 h-3.5 text-muted-foreground" / >
< / button >
< / div >
)}
< / div >
< / div >
< / div >
< / div >
< / div >
))}
{isTyping & & (
< div class = "flex justify-start" >
< div class = "max-w-[85%] md:max-w-[75%] bg-secondary rounded-2xl px-4 py-3 shadow-sm" >
< div class = "flex items-center gap-3" >
< Bot class = "w-4 h-4 text-muted-foreground" / >
< div class = "flex gap-1" >
< div class = "w-2 h-2 rounded-full bg-muted-foreground animate-bounce" > < / div >
< div class = "w-2 h-2 rounded-full bg-muted-foreground animate-bounce" style = "animation-delay:0.2s" > < / div >
< div class = "w-2 h-2 rounded-full bg-muted-foreground animate-bounce" style = "animation-delay:0.4s" > < / div >
< / div >
< / div >
< / div >
< / div >
)}
< div ref = {messagesEndRef} > < / div >
< / div >
< / div >
<!-- Mode Carousel -->
< div class = "flex-shrink-0 border-t border-border bg-card" >
< div class = "overflow-x-auto px-4 py-3" >
< div class = "flex gap-2 min-w-max" >
{modeButtons.map(button => (
< button
key={button.id}
@click={() => activeMode = button.id}
class="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 class = "flex-shrink-0 p-4 border-t border-border bg-card" >
< div class = "relative max-w-4xl mx-auto" >
< textarea
ref={textareaRef}
value={input}
@input={e => input = e.target.value}
@keydown={handleKeyDown}
placeholder="Type your message..."
class="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="min-height:56px;max-height:120px"
>< / textarea >
< button
@click={handleSubmit}
disabled={!input.trim()}
class="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 class = "w-5 h-5" / >
< / button >
< / div >
< / div >
< / div >
< / div >
< / template >
2025-10-26 08:07:14 -03:00
< script >
2025-10-26 00:02:19 -03:00
import { useState, useRef } from 'riot';
import {
Send, Plus, Menu, Search,
MessageSquare, User, Bot, Copy, ThumbsUp, ThumbsDown,
Share, Image, Video,
Brain, Globe
} from 'lucide-react';
import './style.css';
export default {
// Reactive state
messages: [],
input: '',
isTyping: false,
sidebarOpen: true,
conversations: [],
activeMode: 'assistant',
modeButtons: [],
activeConversation: null,
textareaRef: null,
messagesEndRef: null,
// Lifecycle
async mounted() {
// Initialize state (mirroring the original React defaults)
this.messages = [
{
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()
}
];
this.input = '';
this.isTyping = false;
this.sidebarOpen = true;
this.conversations = [
{ 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 },
];
this.activeMode = 'assistant';
this.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' },
];
this.setActiveConversation(1);
this.scrollToBottom();
},
// Helpers
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();
}
},
scrollToBottom() {
this.messagesEndRef?.scrollIntoView({ behavior: 'smooth' });
},
// Event handlers
async handleSubmit() {
if (!this.input.trim()) return;
const userMessage = {
id: Date.now(),
type: 'user',
content: this.input.trim(),
timestamp: new Date().toISOString()
};
this.messages = [...this.messages, userMessage];
this.input = '';
this.isTyping = true;
this.update();
// Simulate assistant response
setTimeout(() => {
const assistantMessage = {
id: Date.now() + 1,
type: 'assistant',
content: `I understand you're asking about "${userMessage.content}". 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()
};
this.messages = [...this.messages, assistantMessage];
this.isTyping = false;
this.update();
this.scrollToBottom();
}, 1500);
},
handleKeyDown(e) {
if (e.key === 'Enter' & & !e.shiftKey) {
e.preventDefault();
this.handleSubmit();
}
},
newChat() {
const newConv = {
id: Date.now(),
title: 'New Chat',
timestamp: new Date(),
active: true
};
this.conversations = [newConv, ...this.conversations.map(c => ({ ...c, active: false }))];
this.messages = [
{
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()
}
];
this.setActiveConversation(newConv.id);
},
setActiveConversation(id) {
this.conversations = this.conversations.map(c => ({ ...c, active: c.id === id }));
this.activeConversation = this.conversations.find(c => c.id === id);
}
};
< / script >