|
|
@@ -0,0 +1,141 @@
|
|
|
+<script lang="ts">
|
|
|
+ import {
|
|
|
+ BotIcon,
|
|
|
+ MessageSquareOffIcon,
|
|
|
+ MessagesSquareIcon,
|
|
|
+ SendIcon,
|
|
|
+ UserIcon,
|
|
|
+ } from '@lucide/svelte';
|
|
|
+ import { tick } from 'svelte';
|
|
|
+ import { fly } from 'svelte/transition';
|
|
|
+
|
|
|
+ import Button from '$lib/components/controls/button/button.svelte';
|
|
|
+ import Textarea from '$lib/components/controls/textarea/textarea.svelte';
|
|
|
+
|
|
|
+ type Message = {
|
|
|
+ role: 'user' | 'assistant';
|
|
|
+ content: string;
|
|
|
+ };
|
|
|
+
|
|
|
+ let messages = $state<Message[]>([]);
|
|
|
+ let input = $state('');
|
|
|
+ let viewport = $state<HTMLDivElement | null>(null);
|
|
|
+
|
|
|
+ const scrollToBottom = async () => {
|
|
|
+ await tick();
|
|
|
+ if (viewport) viewport.scrollTop = viewport.scrollHeight;
|
|
|
+ };
|
|
|
+
|
|
|
+ const send = () => {
|
|
|
+ const content = input.trim();
|
|
|
+ if (!content) return;
|
|
|
+ messages.push({ role: 'user', content });
|
|
|
+ input = '';
|
|
|
+ scrollToBottom();
|
|
|
+ setTimeout(() => {
|
|
|
+ messages.push({
|
|
|
+ role: 'assistant',
|
|
|
+ content: 'This is a placeholder reply — the assistant is not wired to a backend yet.',
|
|
|
+ });
|
|
|
+ scrollToBottom();
|
|
|
+ }, 300);
|
|
|
+ };
|
|
|
+
|
|
|
+ const clear = () => {
|
|
|
+ messages = [];
|
|
|
+ };
|
|
|
+
|
|
|
+ const onKeydown = (e: KeyboardEvent) => {
|
|
|
+ if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
+ e.preventDefault();
|
|
|
+ send();
|
|
|
+ }
|
|
|
+ };
|
|
|
+</script>
|
|
|
+
|
|
|
+<aside
|
|
|
+ class="flex h-svh w-full flex-col overflow-hidden border-l border-white/10 bg-background/20 backdrop-blur-lg"
|
|
|
+>
|
|
|
+ <!-- Header -->
|
|
|
+ <div class="flex h-12 shrink-0 items-center gap-2 border-b border-white/10 bg-white/5 px-4">
|
|
|
+ <MessagesSquareIcon class="shrink-0 text-primary" size={20} strokeWidth={3} />
|
|
|
+ <span class="flex-1 truncate text-sm font-medium text-white/90">Assistant</span>
|
|
|
+ <Button
|
|
|
+ disabled={messages.length === 0}
|
|
|
+ size="icon-sm"
|
|
|
+ title="Clear"
|
|
|
+ variant="ghost"
|
|
|
+ onclick={clear}
|
|
|
+ >
|
|
|
+ <MessageSquareOffIcon class="text-white/50" size={14} />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Messages -->
|
|
|
+ <div bind:this={viewport} class="flex-1 overflow-y-auto p-4">
|
|
|
+ {#if messages.length === 0}
|
|
|
+ <div class="flex h-full flex-col items-center justify-center gap-2 text-center">
|
|
|
+ <MessagesSquareIcon class="text-white/20" size={28} />
|
|
|
+ <p class="truncate text-xs text-white/40">Ask the assistant anything.</p>
|
|
|
+ </div>
|
|
|
+ {:else}
|
|
|
+ <ul class="flex flex-col gap-3">
|
|
|
+ {#each messages as message, i (i)}
|
|
|
+ <li
|
|
|
+ class="flex gap-2"
|
|
|
+ class:flex-row-reverse={message.role === 'user'}
|
|
|
+ in:fly={{ y: 8, duration: 150 }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class={[
|
|
|
+ 'flex size-6 shrink-0 items-center justify-center rounded-full',
|
|
|
+ message.role === 'user'
|
|
|
+ ? 'text-primary-300 bg-primary/20'
|
|
|
+ : 'bg-white/10 text-white/70',
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ {#if message.role === 'user'}
|
|
|
+ <UserIcon size={12} />
|
|
|
+ {:else}
|
|
|
+ <BotIcon size={12} />
|
|
|
+ {/if}
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class={[
|
|
|
+ 'max-w-[80%] rounded-lg px-3 py-2 text-sm whitespace-pre-wrap',
|
|
|
+ message.role === 'user'
|
|
|
+ ? 'bg-primary/20 text-white/90'
|
|
|
+ : 'bg-white/5 text-white/80',
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ {message.content}
|
|
|
+ </div>
|
|
|
+ </li>
|
|
|
+ {/each}
|
|
|
+ </ul>
|
|
|
+ {/if}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Composer -->
|
|
|
+ <div class="shrink-0 border-t border-white/10 bg-white/5 p-3">
|
|
|
+ <div class="relative">
|
|
|
+ <Textarea
|
|
|
+ class="max-h-40 min-h-16 resize-none border-white/10 bg-white/5 pr-10 text-sm text-white/90 placeholder:text-white/30"
|
|
|
+ rows={2}
|
|
|
+ bind:value={input}
|
|
|
+ onkeydown={onKeydown}
|
|
|
+ />
|
|
|
+ <Button
|
|
|
+ class="absolute right-1 bottom-1"
|
|
|
+ disabled={input.trim().length === 0}
|
|
|
+ size="icon-sm"
|
|
|
+ title="Send"
|
|
|
+ variant="ghost"
|
|
|
+ onclick={send}
|
|
|
+ >
|
|
|
+ <SendIcon class="text-primary" size={14} />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ <p class="mt-1 truncate text-[10px] text-white/30">Enter to send, Shift+Enter for newline.</p>
|
|
|
+ </div>
|
|
|
+</aside>
|