Spaces:
Sleeping
Sleeping
| <script lang="ts"> | |
| import { sessionStore, activeSessions, sessionCount } from '$lib/stores/session'; | |
| import { formatRelativeTime } from '$lib/utils/formatters'; | |
| import { validateSessionTitle } from '$lib/utils/validators'; | |
| import { SESSION_LIMIT, SESSION_LIMIT_WARNING } from '$lib/utils/constants'; | |
| import { goto } from '$app/navigation'; | |
| // Modal state | |
| let showCreateModal = false; | |
| let showDeleteModal = false; | |
| let deleteSessionId: string | null = null; | |
| let deleteSessionTitle = ''; | |
| // Form state | |
| let newSessionTitle = ''; | |
| let titleError = ''; | |
| let creating = false; | |
| function openCreateModal() { | |
| newSessionTitle = ''; | |
| titleError = ''; | |
| showCreateModal = true; | |
| } | |
| function closeCreateModal() { | |
| showCreateModal = false; | |
| newSessionTitle = ''; | |
| titleError = ''; | |
| } | |
| async function handleCreateSession() { | |
| // Validate title | |
| const validationError = validateSessionTitle(newSessionTitle); | |
| if (validationError) { | |
| titleError = validationError; | |
| return; | |
| } | |
| creating = true; | |
| const sessionId = await sessionStore.createSession(newSessionTitle); | |
| creating = false; | |
| if (sessionId) { | |
| closeCreateModal(); | |
| // Navigate to new session | |
| goto(`/session/${sessionId}`); | |
| } | |
| } | |
| function openDeleteModal(sessionId: string, title: string) { | |
| deleteSessionId = sessionId; | |
| deleteSessionTitle = title; | |
| showDeleteModal = true; | |
| } | |
| function closeDeleteModal() { | |
| showDeleteModal = false; | |
| deleteSessionId = null; | |
| deleteSessionTitle = ''; | |
| } | |
| async function handleDeleteSession() { | |
| if (!deleteSessionId) return; | |
| const success = await sessionStore.deleteSession(deleteSessionId); | |
| if (success) { | |
| closeDeleteModal(); | |
| } | |
| } | |
| function handleSessionClick(sessionId: string) { | |
| goto(`/session/${sessionId}`); | |
| } | |
| // Check if at limit | |
| $: atLimit = $sessionCount >= SESSION_LIMIT; | |
| $: nearLimit = $sessionCount >= SESSION_LIMIT_WARNING; | |
| </script> | |
| <div class="session-list h-100 d-flex flex-column"> | |
| <!-- Header --> | |
| <div class="session-list-header p-2 p-md-3 border-bottom bg-light"> | |
| <div class="d-flex justify-content-between align-items-center mb-2"> | |
| <h5 class="mb-0 fs-6 fs-md-5"> | |
| <i class="bi bi-chat-left-dots me-2"></i> | |
| <span class="d-none d-sm-inline">Sessions</span> | |
| </h5> | |
| <button | |
| class="btn btn-primary btn-sm" | |
| on:click={openCreateModal} | |
| disabled={atLimit} | |
| title={atLimit ? 'Session limit reached' : 'Create new session'} | |
| > | |
| <i class="bi bi-plus-lg"></i> | |
| <span class="d-none d-lg-inline ms-1">New</span> | |
| </button> | |
| </div> | |
| {#if nearLimit} | |
| <div class="alert alert-{atLimit ? 'danger' : 'warning'} alert-sm mb-0 py-1 px-2" role="alert"> | |
| <small> | |
| {#if atLimit} | |
| <i class="bi bi-exclamation-triangle-fill me-1"></i> | |
| Session limit reached ({$sessionCount}/{SESSION_LIMIT}) | |
| {:else} | |
| <i class="bi bi-info-circle-fill me-1"></i> | |
| {$sessionCount}/{SESSION_LIMIT} sessions | |
| {/if} | |
| </small> | |
| </div> | |
| {/if} | |
| </div> | |
| <!-- Session List --> | |
| <div class="session-list-body flex-grow-1 overflow-auto"> | |
| {#if $activeSessions.length === 0} | |
| <div class="text-center text-muted p-4"> | |
| <i class="bi bi-chat-dots" style="font-size: 3rem; opacity: 0.3;"></i> | |
| <p class="mt-2 mb-0">No sessions yet</p> | |
| <small>Create a session to get started</small> | |
| </div> | |
| {:else} | |
| <div class="list-group list-group-flush"> | |
| {#each $activeSessions as session (session.id)} | |
| <button | |
| type="button" | |
| class="list-group-item list-group-item-action px-2 px-md-3 py-2" | |
| class:active={session.is_active} | |
| on:click={() => handleSessionClick(session.id)} | |
| > | |
| <div class="d-flex w-100 justify-content-between align-items-start"> | |
| <div class="flex-grow-1 text-start me-2 min-w-0"> | |
| <div class="d-flex align-items-center mb-1"> | |
| <h6 class="mb-0 text-truncate session-title"> | |
| {session.title} | |
| </h6> | |
| {#if session.is_reference} | |
| <span class="badge bg-info ms-2 flex-shrink-0" title="Reference session"> | |
| <i class="bi bi-star-fill"></i> | |
| </span> | |
| {/if} | |
| </div> | |
| <small class="text-muted d-block text-truncate"> | |
| <span class="d-none d-md-inline">{formatRelativeTime(session.last_interaction)} · </span> | |
| {session.message_count} msg | |
| </small> | |
| </div> | |
| <button | |
| type="button" | |
| class="btn btn-sm btn-outline-danger flex-shrink-0" | |
| on:click|stopPropagation={() => openDeleteModal(session.id, session.title)} | |
| title="Delete session" | |
| > | |
| <i class="bi bi-trash"></i> | |
| </button> | |
| </div> | |
| </button> | |
| {/each} | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <!-- Create Session Modal --> | |
| {#if showCreateModal} | |
| <div class="modal show d-block" tabindex="-1" role="dialog"> | |
| <div class="modal-dialog modal-dialog-centered" role="document"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title">Create New Session</h5> | |
| <button | |
| type="button" | |
| class="btn-close" | |
| aria-label="Close" | |
| on:click={closeCreateModal} | |
| ></button> | |
| </div> | |
| <div class="modal-body"> | |
| <form on:submit|preventDefault={handleCreateSession}> | |
| <div class="mb-3"> | |
| <label for="sessionTitle" class="form-label">Session Title</label> | |
| <input | |
| type="text" | |
| class="form-control" | |
| class:is-invalid={titleError} | |
| id="sessionTitle" | |
| bind:value={newSessionTitle} | |
| placeholder="Enter session title..." | |
| maxlength="200" | |
| required | |
| /> | |
| {#if titleError} | |
| <div class="invalid-feedback">{titleError}</div> | |
| {/if} | |
| <small class="form-text text-muted">Max 200 characters</small> | |
| </div> | |
| </form> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" on:click={closeCreateModal}> | |
| Cancel | |
| </button> | |
| <button | |
| type="button" | |
| class="btn btn-primary" | |
| on:click={handleCreateSession} | |
| disabled={creating || !newSessionTitle.trim()} | |
| > | |
| {#if creating} | |
| <span class="spinner-border spinner-border-sm me-2" role="status"></span> | |
| {/if} | |
| Create | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal-backdrop show"></div> | |
| {/if} | |
| <!-- Delete Confirmation Modal --> | |
| {#if showDeleteModal} | |
| <div class="modal show d-block" tabindex="-1" role="dialog"> | |
| <div class="modal-dialog modal-dialog-centered" role="document"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title">Delete Session</h5> | |
| <button | |
| type="button" | |
| class="btn-close" | |
| aria-label="Close" | |
| on:click={closeDeleteModal} | |
| ></button> | |
| </div> | |
| <div class="modal-body"> | |
| <p>Are you sure you want to delete this session?</p> | |
| <p class="text-muted mb-0"> | |
| <strong>{deleteSessionTitle}</strong> | |
| </p> | |
| <p class="text-danger mt-2 mb-0"> | |
| <i class="bi bi-exclamation-triangle-fill me-2"></i> | |
| This action cannot be undone. | |
| </p> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" on:click={closeDeleteModal}> | |
| Cancel | |
| </button> | |
| <button type="button" class="btn btn-danger" on:click={handleDeleteSession}> | |
| <i class="bi bi-trash me-2"></i> | |
| Delete | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal-backdrop show"></div> | |
| {/if} | |
| <style> | |
| .session-list { | |
| background-color: #f8f9fa; | |
| } | |
| .session-list-body { | |
| position: relative; | |
| } | |
| .list-group-item { | |
| cursor: pointer; | |
| border-left: 3px solid transparent; | |
| transition: all 0.2s ease; | |
| } | |
| .list-group-item:hover { | |
| border-left-color: #0d6efd; | |
| } | |
| .list-group-item.active { | |
| border-left-color: #0d6efd; | |
| background-color: #e7f1ff; | |
| color: #000; | |
| } | |
| .session-title { | |
| max-width: 100%; | |
| font-size: 0.95rem; | |
| } | |
| .min-w-0 { | |
| min-width: 0; | |
| } | |
| .modal { | |
| background-color: rgba(0, 0, 0, 0.5); | |
| } | |
| .alert-sm { | |
| font-size: 0.875rem; | |
| } | |
| /* Mobile optimizations */ | |
| @media (max-width: 576px) { | |
| .session-title { | |
| font-size: 0.875rem; | |
| } | |
| .list-group-item { | |
| border-left-width: 2px; | |
| } | |
| .badge { | |
| font-size: 0.7rem; | |
| padding: 0.2rem 0.35rem; | |
| } | |
| } | |
| /* Larger screens */ | |
| @media (min-width: 1200px) { | |
| .session-title { | |
| font-size: 1rem; | |
| } | |
| } | |
| </style> | |