VibeGame / src /lib /components /chat /ChatPanel.svelte
dylanebert's picture
improved prompting/UX
db9635c
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { fade } from "svelte/transition";
import gsap from "gsap";
import MessageList from "./MessageList.svelte";
import MessageInput from "./MessageInput.svelte";
import ExampleRow from "./ExampleRow.svelte";
import { isConnected, isProcessing, messages, error } from "../../stores/chat-store";
import { chatController } from "../../controllers/chat-controller";
import { authStore } from "../../services/auth";
import { agentService } from "../../services/agent";
let inputValue = "";
let messageListRef: any;
let autoScroll = true;
let headerRef: HTMLDivElement;
onMount(() => {
if ($authStore.isAuthenticated && !$authStore.loading) {
agentService.connect();
}
if (headerRef) {
gsap.fromTo(headerRef,
{ opacity: 0, y: -10 },
{ opacity: 1, y: 0, duration: 0.4, ease: "power2.out" }
);
}
});
$: if ($authStore.isAuthenticated && !$authStore.loading) {
if (!$isConnected) {
agentService.connect();
} else {
agentService.reauthenticate();
}
}
onDestroy(() => {
agentService.disconnect();
});
function handleSend(event: CustomEvent<string>) {
chatController.sendMessage(event.detail);
}
function handleStop() {
chatController.stopConversation();
}
function handleClear() {
chatController.clearConversation();
}
function handleExampleMessage(message: string) {
if ($authStore.isAuthenticated && $isConnected && !$isProcessing) {
chatController.sendMessage(message);
}
}
function handleScroll(event: CustomEvent<{ nearBottom: boolean }>) {
autoScroll = event.detail.nearBottom;
}
function scrollToBottom() {
messageListRef?.scrollToBottom();
}
</script>
<div class="chat-panel">
<div class="chat-header" bind:this={headerRef}>
<h2>Chat</h2>
{#if $messages.length > 0 && $authStore.isAuthenticated && !$isProcessing}
<button
class="clear-button"
on:click={handleClear}
title="Clear conversation"
transition:fade={{ duration: 200 }}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14zM10 11v6M14 11v6" />
</svg>
<span>Clear</span>
</button>
{/if}
</div>
{#if !autoScroll}
<button
class="scroll-to-bottom"
on:click={scrollToBottom}
title="Scroll to bottom"
transition:fade={{ duration: 200 }}
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 14L5 9L6.41 7.59L10 11.17L13.59 7.59L15 9L10 14Z" />
</svg>
</button>
{/if}
<div class="messages-container">
{#if !$authStore.isAuthenticated && !$authStore.loading}
<div class="auth-prompt">
<button on:click={() => authStore.login()} class="auth-prompt-btn">
Sign in with 🤗 Hugging Face
</button>
</div>
{:else}
<MessageList
bind:this={messageListRef}
messages={$messages}
error={$error}
{autoScroll}
isConnected={$isConnected}
onSendMessage={handleExampleMessage}
on:scroll={handleScroll}
/>
{/if}
</div>
<ExampleRow
onSendMessage={handleExampleMessage}
visible={$messages.length === 0 && $authStore.isAuthenticated && $isConnected}
/>
<MessageInput
bind:value={inputValue}
placeholder={!$authStore.isAuthenticated
? "Sign in to chat..."
: $isConnected
? $isProcessing
? "Processing..."
: "Type a message..."
: "Connecting..."}
disabled={!$authStore.isAuthenticated || !$isConnected || $isProcessing}
processing={$isProcessing}
on:send={handleSend}
on:stop={handleStop}
/>
</div>
<style>
.chat-panel {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background: rgba(12, 11, 10, 0.85);
border-top: 1px solid rgba(139, 115, 85, 0.12);
position: relative;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background: rgba(18, 17, 16, 0.6);
border-bottom: 1px solid rgba(139, 115, 85, 0.15);
flex-shrink: 0;
}
.chat-header h2 {
margin: 0;
font-size: 11px;
font-weight: 500;
color: rgba(251, 248, 244, 0.7);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.clear-button {
padding: 4px 10px;
background: rgba(139, 115, 85, 0.08);
color: rgba(251, 248, 244, 0.55);
border: 1px solid rgba(139, 115, 85, 0.15);
border-radius: 4px;
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.3px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
gap: 0.25rem;
}
.clear-button:hover {
background: rgba(139, 115, 85, 0.12);
color: rgba(251, 248, 244, 0.75);
border-color: rgba(139, 115, 85, 0.2);
}
.clear-button:active {
transform: scale(0.95);
}
.clear-button svg {
width: 14px;
height: 14px;
}
.scroll-to-bottom {
position: absolute;
bottom: 4.5rem;
right: 1rem;
width: 32px;
height: 32px;
border-radius: 4px;
background: rgba(124, 152, 133, 0.08);
border: 1px solid rgba(124, 152, 133, 0.15);
color: rgba(124, 152, 133, 0.9);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.scroll-to-bottom:hover {
background: rgba(124, 152, 133, 0.15);
border-color: rgba(124, 152, 133, 0.3);
transform: translateY(-1px);
}
.scroll-to-bottom:active {
transform: translateY(0) scale(0.95);
}
.auth-prompt {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
height: 100%;
text-align: center;
}
.auth-prompt-btn {
padding: 0.6rem 1.8rem;
background: rgba(255, 210, 30, 0.08);
color: rgba(255, 210, 30, 0.9);
border: 1px solid rgba(255, 210, 30, 0.2);
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
}
.auth-prompt-btn:hover {
transform: translateY(-2px);
background: rgba(255, 210, 30, 0.12);
color: rgba(255, 210, 30, 1);
border-color: rgba(255, 210, 30, 0.3);
}
.auth-prompt-btn:active {
transform: translateY(0);
}
.messages-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
</style>