152 lines
6.6 KiB
TypeScript
152 lines
6.6 KiB
TypeScript
import type { SessionMetadata } from "@jaeswift/jae-web-ui";
|
|
import { html, LitElement } from "lit";
|
|
import { customElement, property, state } from "lit/decorators.js";
|
|
|
|
@customElement("jae-session-sidebar")
|
|
export class JaeSessionSidebar extends LitElement {
|
|
@property({ type: Boolean }) collapsed = false;
|
|
@property({ type: String }) currentSessionId: string | undefined = undefined;
|
|
@property({ attribute: false }) onLoadSession?: (id: string) => void;
|
|
@property({ attribute: false }) onNewSession?: () => void;
|
|
|
|
@state() private _sessions: SessionMetadata[] = [];
|
|
@state() private _pinnedIds: Set<string> = new Set();
|
|
@state() private _confirmDelete: string | null = null;
|
|
|
|
protected override createRenderRoot() {
|
|
return this;
|
|
}
|
|
|
|
override connectedCallback() {
|
|
super.connectedCallback();
|
|
const raw = localStorage.getItem("jae-pinned-sessions");
|
|
if (raw) {
|
|
try {
|
|
this._pinnedIds = new Set(JSON.parse(raw));
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
setSessions(sessions: SessionMetadata[]) {
|
|
this._sessions = [...sessions];
|
|
this.requestUpdate();
|
|
}
|
|
|
|
private _togglePin(e: Event, id: string) {
|
|
e.stopPropagation();
|
|
const s = new Set(this._pinnedIds);
|
|
s.has(id) ? s.delete(id) : s.add(id);
|
|
this._pinnedIds = s;
|
|
localStorage.setItem("jae-pinned-sessions", JSON.stringify([...s]));
|
|
this.requestUpdate();
|
|
}
|
|
|
|
private _deleteSession(e: Event, id: string) {
|
|
e.stopPropagation();
|
|
if (this._confirmDelete === id) {
|
|
this._confirmDelete = null;
|
|
this.dispatchEvent(new CustomEvent("delete-session", { detail: id, bubbles: true, composed: true }));
|
|
} else {
|
|
this._confirmDelete = id;
|
|
this.requestUpdate();
|
|
setTimeout(() => {
|
|
this._confirmDelete = null;
|
|
this.requestUpdate();
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
private _fmt(iso: string) {
|
|
const ms = Date.now() - new Date(iso).getTime();
|
|
if (ms < 60000) return "just now";
|
|
if (ms < 3600000) return Math.floor(ms / 60000) + "m ago";
|
|
if (ms < 86400000) return Math.floor(ms / 3600000) + "h ago";
|
|
if (ms < 604800000) return Math.floor(ms / 86400000) + "d ago";
|
|
return new Date(iso).toLocaleDateString();
|
|
}
|
|
|
|
override render() {
|
|
if (this.collapsed) return html``;
|
|
const pinned = this._sessions
|
|
.filter((s) => this._pinnedIds.has(s.id))
|
|
.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
|
|
const rest = this._sessions
|
|
.filter((s) => !this._pinnedIds.has(s.id))
|
|
.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
|
|
const sorted = [...pinned, ...rest];
|
|
|
|
return html`
|
|
<div class="flex flex-col h-full border-r border-border bg-background shrink-0" style="width:100%">
|
|
<div class="flex items-center justify-between px-3 py-2 border-b border-border shrink-0">
|
|
<span class="text-[11px] font-semibold text-muted-foreground uppercase tracking-widest">Chats</span>
|
|
<button @click=${() => this.onNewSession?.()}
|
|
class="w-6 h-6 flex items-center justify-center rounded hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors"
|
|
title="New chat">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" stroke-width="2.5" 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>
|
|
</button>
|
|
</div>
|
|
<div class="flex-1 overflow-y-auto py-1">
|
|
${
|
|
sorted.length === 0
|
|
? html`
|
|
<div class="px-4 py-10 text-center">
|
|
<div class="text-3xl mb-2">💬</div>
|
|
<div class="text-xs text-muted-foreground">No chats yet</div>
|
|
</div>
|
|
`
|
|
: sorted.map(
|
|
(s) => html`
|
|
<div class="group relative flex items-center gap-1 px-2 py-1.5 mx-1 my-0.5 rounded-lg cursor-pointer transition-colors select-none
|
|
${s.id === this.currentSessionId ? "bg-secondary" : "hover:bg-secondary/50"}"
|
|
@click=${() => this.onLoadSession?.(s.id)}>
|
|
${
|
|
this._pinnedIds.has(s.id)
|
|
? html`
|
|
<div class="absolute left-0.5 top-1/2 -translate-y-1/2 w-1 h-4 rounded-full bg-primary/60"></div>
|
|
`
|
|
: html``
|
|
}
|
|
<div class="flex-1 min-w-0 pl-1">
|
|
<div class="text-xs font-medium truncate" style="color: inherit">${s.title || "Untitled"}</div>
|
|
<div class="text-[10px] text-muted-foreground leading-tight">${this._fmt(s.lastModified)}</div>
|
|
</div>
|
|
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
|
<button @click=${(e: Event) => this._togglePin(e, s.id)}
|
|
class="w-5 h-5 flex items-center justify-center rounded transition-colors
|
|
${this._pinnedIds.has(s.id) ? "text-primary" : "text-muted-foreground hover:text-foreground"}"
|
|
title="${this._pinnedIds.has(s.id) ? "Unpin" : "Pin"}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24"
|
|
fill="${this._pinnedIds.has(s.id) ? "currentColor" : "none"}"
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="12" y1="17" x2="12" y2="22"/>
|
|
<path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/>
|
|
</svg>
|
|
</button>
|
|
<button @click=${(e: Event) => this._deleteSession(e, s.id)}
|
|
class="w-5 h-5 flex items-center justify-center rounded transition-colors
|
|
${this._confirmDelete === s.id ? "text-destructive bg-destructive/10" : "text-muted-foreground hover:text-destructive"}"
|
|
title="${this._confirmDelete === s.id ? "Click again to confirm" : "Delete chat"}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24"
|
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="3 6 5 6 21 6"/>
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
)
|
|
}
|
|
</div>
|
|
<div class="px-3 py-1.5 border-t border-border shrink-0">
|
|
<div class="text-[10px] text-muted-foreground text-center">
|
|
${sorted.length} chat${sorted.length !== 1 ? "s" : ""}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|