- Terracotta (light) and Carbon (dark) themes with toggle - Inknut Antiqua headings, DM Sans body - Dot grid background on EntityGraph and GraphBackground - Theme-adaptive graph colors on EntityGraph - Art Nouveau chat styling (borders, underlines, blockquote quotes) - Bullet point styles in chat prose - Task goal in header, rename Overview→Tasks, New Task labels - Logo uses var(--primary) for theme awareness
81 lines
3.1 KiB
Svelte
81 lines
3.1 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte'
|
|
import { sessions, currentSession, loadSessions, loadSessionMessages, newChat, deleteSession } from '$lib/stores/chat'
|
|
import { relativeTime } from '$lib/utils'
|
|
import { Button } from '$lib/components/ui/button'
|
|
import { ScrollArea } from '$lib/components/ui/scroll-area'
|
|
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
import Trash2Icon from '@lucide/svelte/icons/trash-2'
|
|
|
|
onMount(() => {
|
|
loadSessions()
|
|
})
|
|
|
|
$effect(() => {
|
|
void $currentSession
|
|
loadSessions()
|
|
})
|
|
|
|
let confirmDelete = $state<string | null>(null)
|
|
|
|
function handleDelete(e: MouseEvent, id: string) {
|
|
e.stopPropagation()
|
|
if (confirmDelete === id) {
|
|
deleteSession(id)
|
|
confirmDelete = null
|
|
} else {
|
|
confirmDelete = id
|
|
// Hide confirmation after 3s
|
|
setTimeout(() => { if (confirmDelete === id) confirmDelete = null }, 3000)
|
|
}
|
|
}
|
|
|
|
function handleClick(sessionId: string) {
|
|
confirmDelete = null
|
|
loadSessionMessages(sessionId)
|
|
}
|
|
</script>
|
|
|
|
<aside class="flex h-full w-56 shrink-0 flex-col gap-2 overflow-y-auto border-r bg-card/50 p-2">
|
|
<Button variant="outline" size="sm" class="justify-start gap-2" onclick={() => newChat()}>
|
|
<PlusIcon class="size-3.5" />
|
|
New Task
|
|
</Button>
|
|
<ScrollArea class="min-h-0 flex-1">
|
|
<div class="flex flex-col gap-1 pr-2">
|
|
{#each $sessions as session (session.id)}
|
|
<div class="group relative">
|
|
<button
|
|
type="button"
|
|
class="flex w-full flex-col items-start gap-0.5 rounded-md border py-1.5 pl-2 pr-7 text-left text-xs transition-colors hover:bg-muted/60 {$currentSession === session.id ? 'border-primary bg-muted/50' : 'border-transparent'}"
|
|
onclick={() => handleClick(session.id)}
|
|
>
|
|
<span class="min-w-0 max-w-full truncate font-medium">{session.title || 'Untitled'}</span>
|
|
<span class="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
{relativeTime(session.last_active_at)}
|
|
{#if session.pending_approvals}
|
|
<span class="rounded bg-warning/20 px-1 text-[10px] font-medium text-warning">{session.pending_approvals}</span>
|
|
{/if}
|
|
</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="absolute right-1 top-1.5 shrink-0 rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 hover:bg-destructive/20 hover:text-destructive"
|
|
onclick={(e) => handleDelete(e, session.id)}
|
|
aria-label={confirmDelete === session.id ? 'Click again to confirm delete' : 'Delete session'}
|
|
title={confirmDelete === session.id ? 'Click again to confirm delete' : 'Delete session'}
|
|
>
|
|
{#if confirmDelete === session.id}
|
|
<span class="text-[10px] font-semibold text-destructive">Sure?</span>
|
|
{:else}
|
|
<Trash2Icon class="size-3" />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
{:else}
|
|
<p class="px-2 py-4 text-center text-xs text-muted-foreground">No sessions yet.</p>
|
|
{/each}
|
|
</div>
|
|
</ScrollArea>
|
|
</aside>
|