Initial commit: Hermes Chat App with Svelte 5 + shadcn-svelte
This commit is contained in:
102
src/lib/components/chat/Sidebar.svelte
Normal file
102
src/lib/components/chat/Sidebar.svelte
Normal file
@@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import { sessions, activeSessionId, createSession } from '$lib/stores/chat';
|
||||
import { isStreaming } from '$lib/stores/chat';
|
||||
import { get } from 'svelte/store';
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import { getHermesStatus } from '$lib/hermes';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { onClose }: { onClose?: () => void } = $props();
|
||||
|
||||
let hermesAvailable = $state(false);
|
||||
let hermesModel = $state('');
|
||||
let sessionList = $state<any[]>([]);
|
||||
let activeId = $state<string | null>(null);
|
||||
|
||||
sessions.subscribe((v) => (sessionList = v));
|
||||
activeSessionId.subscribe((v) => (activeId = v));
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const status = await getHermesStatus();
|
||||
hermesAvailable = status.available;
|
||||
hermesModel = status.model || status.provider || 'connected';
|
||||
} catch {
|
||||
hermesAvailable = false;
|
||||
}
|
||||
});
|
||||
|
||||
function formatDate(ts: number) {
|
||||
const d = new Date(ts);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - d.getTime();
|
||||
if (diff < 86400000) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
if (diff < 604800000) return d.toLocaleDateString([], { weekday: 'short' });
|
||||
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside class="flex h-full w-72 flex-col border-r bg-muted/30">
|
||||
<!-- New chat button -->
|
||||
<div class="p-3">
|
||||
<button
|
||||
class="flex w-full items-center gap-2 rounded-lg border border-dashed border-border px-3 py-2.5 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors disabled:opacity-50"
|
||||
onclick={() => { createSession(); onClose?.(); }}
|
||||
disabled={get(isStreaming)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
New Chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<!-- Sessions list -->
|
||||
<div class="flex-1 overflow-y-auto p-2 space-y-1">
|
||||
{#if sessionList.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-12 text-center px-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground/40 mb-3">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<p class="text-sm text-muted-foreground">No conversations yet</p>
|
||||
<p class="text-xs text-muted-foreground/60 mt-1">Start a new chat above</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each sessionList as session (session.id)}
|
||||
<button
|
||||
class="w-full text-left rounded-lg px-3 py-2.5 transition-colors group
|
||||
{session.id === activeId
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent/50 text-muted-foreground hover:text-foreground'}"
|
||||
onclick={() => {
|
||||
activeSessionId.set(session.id);
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span class="text-sm font-medium truncate flex-1">{session.title}</span>
|
||||
<span class="text-[10px] text-muted-foreground/60 whitespace-nowrap mt-0.5">
|
||||
{formatDate(session.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
{#if session.preview}
|
||||
<p class="text-xs text-muted-foreground/70 mt-1 truncate">{session.preview}</p>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Footer status -->
|
||||
<div class="border-t p-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2 w-2 rounded-full {hermesAvailable ? 'bg-green-500' : 'bg-red-500'}" />
|
||||
<span class="text-xs text-muted-foreground truncate">
|
||||
{hermesAvailable ? (hermesModel || 'Hermes Agent') : 'Hermes disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
Reference in New Issue
Block a user