botserver/web/app/chat/chat.page.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>