Spaces:
Running
Running
| <script lang="ts"> | |
| import { createEventDispatcher, afterUpdate } from "svelte"; | |
| import gsap from "gsap"; | |
| import type { ChatMessage } from "../../models/chat-data"; | |
| import Message from "./Message.svelte"; | |
| import ExampleMessages from "./ExampleMessages.svelte"; | |
| import { typingIndicator } from "../../stores/chat-store"; | |
| export let messages: ReadonlyArray<ChatMessage> = []; | |
| export let error: string | null = null; | |
| export let autoScroll: boolean = true; | |
| export let isConnected: boolean = false; | |
| export let onSendMessage: ((message: string) => void) | undefined = undefined; | |
| let typingIndicatorRef: HTMLDivElement; | |
| const dispatch = createEventDispatcher(); | |
| let messagesDiv: HTMLDivElement; | |
| function handleScroll() { | |
| const isNearBottom = | |
| messagesDiv.scrollHeight - messagesDiv.scrollTop - messagesDiv.clientHeight < 50; | |
| dispatch("scroll", { nearBottom: isNearBottom }); | |
| } | |
| export function scrollToBottom() { | |
| messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
| } | |
| afterUpdate(() => { | |
| if (messages.length > 0 && autoScroll) { | |
| scrollToBottom(); | |
| } | |
| }); | |
| $: if ($typingIndicator && typingIndicatorRef) { | |
| gsap.fromTo(typingIndicatorRef, | |
| { opacity: 0, y: 10, scale: 0.95 }, | |
| { opacity: 1, y: 0, scale: 1, duration: 0.3, ease: "power2.out" } | |
| ); | |
| const dots = typingIndicatorRef.querySelectorAll('.dot'); | |
| gsap.to(dots, { | |
| y: -3, | |
| duration: 0.6, | |
| stagger: 0.15, | |
| repeat: -1, | |
| yoyo: true, | |
| ease: "power1.inOut" | |
| }); | |
| } | |
| $: if (!$typingIndicator && typingIndicatorRef) { | |
| gsap.killTweensOf(typingIndicatorRef.querySelectorAll('.dot')); | |
| gsap.to(typingIndicatorRef, { | |
| opacity: 0, | |
| y: -5, | |
| scale: 0.95, | |
| duration: 0.2, | |
| ease: "power2.in" | |
| }); | |
| } | |
| </script> | |
| <div class="messages" bind:this={messagesDiv} on:scroll={handleScroll}> | |
| {#if messages.length === 0 && isConnected && onSendMessage} | |
| <div class="welcome-section"> | |
| <ExampleMessages /> | |
| </div> | |
| {/if} | |
| {#each messages as message (message.id)} | |
| <Message {message} /> | |
| {/each} | |
| {#if $typingIndicator} | |
| <div class="typing-indicator" bind:this={typingIndicatorRef}> | |
| <div class="typing-dots"> | |
| <span class="dot"></span> | |
| <span class="dot"></span> | |
| <span class="dot"></span> | |
| </div> | |
| <span class="typing-text">AI is thinking...</span> | |
| </div> | |
| {/if} | |
| {#if error} | |
| <div class="error-message">Error: {error}</div> | |
| {/if} | |
| </div> | |
| <style> | |
| .messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 0.5rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| min-height: 0; | |
| position: relative; | |
| } | |
| .welcome-section { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 100%; | |
| } | |
| .typing-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.5rem; | |
| margin: 0.25rem 0; | |
| border-left: 2px solid rgba(33, 150, 243, 0.3); | |
| background: linear-gradient(90deg, | |
| rgba(33, 150, 243, 0.05), | |
| transparent); | |
| border-radius: 0 4px 4px 0; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .typing-indicator::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, | |
| transparent, | |
| rgba(33, 150, 243, 0.1), | |
| transparent); | |
| animation: shimmer 2s ease-in-out infinite; | |
| } | |
| @keyframes shimmer { | |
| to { left: 100%; } | |
| } | |
| .typing-dots { | |
| display: flex; | |
| gap: 0.3rem; | |
| align-items: center; | |
| } | |
| .dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, | |
| rgba(33, 150, 243, 0.8), | |
| rgba(100, 181, 246, 0.6)); | |
| display: inline-block; | |
| position: relative; | |
| } | |
| .typing-text { | |
| font-size: 11px; | |
| color: rgba(251, 248, 244, 0.4); | |
| font-style: normal; | |
| letter-spacing: 0.3px; | |
| animation: fade-pulse 2s ease-in-out infinite; | |
| } | |
| @keyframes fade-pulse { | |
| 0%, 100% { opacity: 0.4; } | |
| 50% { opacity: 0.7; } | |
| } | |
| .error-message { | |
| background: rgba(244, 67, 54, 0.1); | |
| border-left: 2px solid #f44336; | |
| border-radius: 3px; | |
| padding: 0.4rem 0.6rem; | |
| color: #ff9999; | |
| font-size: 0.875rem; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| </style> | |