File size: 6,391 Bytes
ca51841
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { store } from '../store.js';
import { icon } from '../icons.js';

function relativeTime(isoString) {
  const now = Date.now();
  const then = new Date(isoString).getTime();
  const diff = Math.floor((now - then) / 1000);
  if (diff < 60) return 'just now';
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
  if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
  return new Date(isoString).toLocaleDateString();
}

export class Sidebar {
  constructor() {
    this.el = null;
    this._mobileOpen = false;
  }

  render() {
    const wrapper = document.createElement('div');
    wrapper.className = 'relative';
    wrapper.innerHTML = this._template();
    this.el = wrapper.firstElementChild;
    this._bindEvents();
    return this.el;
  }

  _template() {
    const convs = store.getConversations();
    const currentId = store.getCurrentConversationId();

    const items = convs.map(conv => {
      const active = conv.id === currentId;
      return `
        <div class="group relative flex items-center gap-2 rounded-xl cursor-pointer border px-3 py-2.5 transition-all ${active ? 'border-[var(--c-side-act-bd)] bg-[var(--c-side-act)]' : 'border-transparent hover:border-[var(--c-side-bd)] hover:bg-[var(--c-side-el)]'}" data-conv-id="${conv.id}" role="option" aria-selected="${active}">
          <div class="flex-1 min-w-0">
            <div class="text-[13px] truncate ${active ? 'font-medium text-[var(--c-side-tx)]' : 'text-[var(--c-side-tx2)]'}">${escapeHtml(conv.title || 'New Chat')}</div>
            <div class="mt-0.5 truncate text-[11px] text-[var(--c-side-tx3)]">${relativeTime(conv.updatedAt || conv.createdAt)}</div>
          </div>
          <button class="delete-conv flex-shrink-0 p-1 rounded-md text-[var(--c-side-tx3)] opacity-0 transition-all group-hover:opacity-100 hover:bg-[var(--c-side-el-h)] hover:text-[var(--c-side-tx)]" data-conv-id="${conv.id}" aria-label="Delete conversation">
            ${icon('trash')}
          </button>
        </div>
      `;
    }).join('');

    return `
      <aside id="sidebar" class="sidebar-transition flex h-full w-56 flex-col border-r border-[var(--c-side-bd)] bg-[var(--c-side)] flex-shrink-0">
        <div class="px-3 pt-3 pb-2">
          <button id="new-chat-btn" class="flex w-full items-center justify-center gap-1.5 rounded-xl border border-[var(--c-side-bd)] bg-[var(--c-side-el)] px-3 py-2.5 text-[12px] text-[var(--c-side-tx2)] transition-all hover:bg-[var(--c-side-el-h)] hover:text-[var(--c-side-tx)] hover:border-[var(--c-side-act-bd)]" aria-label="New chat">
            ${icon('plus')} <span>New Chat</span>
          </button>
        </div>
        <div id="conv-list" class="flex-1 overflow-y-auto pb-2 px-2 space-y-px" role="listbox" aria-label="Conversations">
          ${items || '<div class="px-4 py-10 text-center text-[11px] leading-relaxed text-[var(--c-side-tx3)]">No conversations yet.<br>Start a new chat!</div>'}
        </div>
        <div class="border-t border-[var(--c-side-bd)] px-3 py-3">
          <div class="flex items-center gap-2 rounded-xl px-1.5 py-1">
            <div class="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg border border-[var(--c-side-bd)] bg-[var(--c-side-el)]">
              <img src="/logo.svg" alt="" class="h-4 w-4 object-contain" aria-hidden="true" />
            </div>
            <div class="min-w-0">
              <div class="truncate text-[10px] font-semibold uppercase tracking-[0.16em] text-[var(--c-side-tx2)]">AXERA</div>
              <div class="truncate text-[10px] text-[var(--c-side-tx3)]">Lite WebUI</div>
            </div>
          </div>
        </div>
      </aside>
    `;
  }

  _bindEvents() {
    this.el.querySelector('#new-chat-btn').addEventListener('click', () => {
      document.dispatchEvent(new CustomEvent('sidebar:newchat'));
    });

    this.el.querySelector('#conv-list').addEventListener('click', (e) => {
      const delBtn = e.target.closest('.delete-conv');
      if (delBtn) {
        e.stopPropagation();
        const convId = delBtn.dataset.convId;
        if (confirm('Delete this conversation?')) {
          store.deleteConversation(convId);
          document.dispatchEvent(new CustomEvent('sidebar:deleted', { detail: { convId } }));
          this.update();
        }
        return;
      }
      const item = e.target.closest('[data-conv-id]');
      if (item && !item.classList.contains('delete-conv')) {
        const convId = item.dataset.convId;
        document.dispatchEvent(new CustomEvent('sidebar:select', { detail: { convId } }));
      }
    });
  }

  update() {
    const list = this.el.querySelector('#conv-list');
    const convs = store.getConversations();
    const currentId = store.getCurrentConversationId();

    if (convs.length === 0) {
      list.innerHTML = '<div class="px-4 py-10 text-center text-[11px] leading-relaxed text-[var(--c-side-tx3)]">No conversations yet.<br>Start a new chat!</div>';
      return;
    }

    const items = convs.map(conv => {
      const active = conv.id === currentId;
      return `
        <div class="group relative flex items-center gap-2 rounded-xl cursor-pointer border px-3 py-2.5 transition-all ${active ? 'border-[var(--c-side-act-bd)] bg-[var(--c-side-act)]' : 'border-transparent hover:border-[var(--c-side-bd)] hover:bg-[var(--c-side-el)]'}" data-conv-id="${conv.id}" role="option" aria-selected="${active}">
          <div class="flex-1 min-w-0">
            <div class="text-[13px] truncate ${active ? 'font-medium text-[var(--c-side-tx)]' : 'text-[var(--c-side-tx2)]'}">${escapeHtml(conv.title || 'New Chat')}</div>
            <div class="mt-0.5 truncate text-[11px] text-[var(--c-side-tx3)]">${relativeTime(conv.updatedAt || conv.createdAt)}</div>
          </div>
          <button class="delete-conv flex-shrink-0 p-1 rounded-md text-[var(--c-side-tx3)] opacity-0 transition-all group-hover:opacity-100 hover:bg-[var(--c-side-el-h)] hover:text-[var(--c-side-tx)]" data-conv-id="${conv.id}" aria-label="Delete conversation">
            ${icon('trash')}
          </button>
        </div>
      `;
    }).join('');

    list.innerHTML = items;
  }
}

function escapeHtml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}