Spaces:
Running
Running
| <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{ | |
| 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> |