298 lines
12 KiB
HTML
298 lines
12 KiB
HTML
<!-- 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>
|
|
|
|
<script >
|
|
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>
|