MemPrepMate / src /lib /components /SessionList.svelte
Christian Kniep
new webapp
1fff71f
<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>