trusty-memory 0.1.45

Machine-wide, blazingly fast AI memory service
<script>
  import { api } from '../api.js';

  let { palaceId } = $props();

  let messages = $state([]);
  let input = $state('');
  let sending = $state(false);
  let error = $state(null);

  async function send() {
    if (!input.trim() || sending) return;
    const userMsg = { role: 'user', content: input.trim() };
    messages = [...messages, userMsg];
    const history = messages.slice(0, -1);
    input = '';
    sending = true;
    error = null;

    try {
      const res = await api.chat({
        palace_id: palaceId,
        message: userMsg.content,
        history
      });
      if (!res.ok) {
        const txt = await res.text();
        throw new Error(`${res.status}: ${txt}`);
      }
      const assistantMsg = { role: 'assistant', content: '' };
      messages = [...messages, assistantMsg];
      const idx = messages.length - 1;

      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buffer += decoder.decode(value, { stream: true });
        // Parse SSE frames: "data: ...\n\n"
        const frames = buffer.split('\n\n');
        buffer = frames.pop() || '';
        for (const frame of frames) {
          for (const line of frame.split('\n')) {
            if (!line.startsWith('data:')) continue;
            const data = line.slice(5).trim();
            if (data === '[DONE]' || data === '') continue;
            try {
              const parsed = JSON.parse(data);
              if (parsed.delta) {
                messages[idx].content += parsed.delta;
                messages = [...messages];
              } else if (parsed.error) {
                error = parsed.error;
              }
            } catch {
              // Treat non-JSON data as raw text.
              messages[idx].content += data;
              messages = [...messages];
            }
          }
        }
      }
    } catch (e) {
      error = e.message;
    } finally {
      sending = false;
    }
  }
</script>

<div class="chat">
  <div class="messages">
    {#if messages.length === 0}
      <div class="empty">
        Ask a question. Top recall hits from this palace will be added as context.
      </div>
    {/if}
    {#each messages as m}
      <div class="msg msg-{m.role}">
        <div class="msg-role">{m.role}</div>
        <div class="msg-body">{m.content || (sending && m.role === 'assistant' ? '…' : '')}</div>
      </div>
    {/each}
    {#if error}
      <div class="badge badge-danger">{error}</div>
    {/if}
  </div>
  <div class="composer">
    <textarea
      class="textarea"
      placeholder="Send a message…"
      bind:value={input}
      onkeydown={(e) => {
        if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) send();
      }}
    ></textarea>
    <button class="btn btn-primary" disabled={sending || !input.trim()} onclick={send}>
      {sending ? 'Sending…' : 'Send (⌘↵)'}
    </button>
  </div>
</div>

<style>
  .chat {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 260px);
    min-height: 400px;
  }
  .messages {
    flex: 1;
    overflow-y: auto;
    padding: var(--trusty-space-4);
    background: var(--trusty-card-bg);
    border: 1px solid var(--trusty-border);
    border-radius: var(--trusty-radius);
  }
  .msg {
    margin-bottom: var(--trusty-space-4);
  }
  .msg-role {
    font-size: var(--trusty-fs-xs);
    font-weight: 600;
    color: var(--trusty-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin-bottom: var(--trusty-space-1);
  }
  .msg-user .msg-role {
    color: var(--trusty-accent);
  }
  .msg-body {
    white-space: pre-wrap;
    line-height: 1.5;
  }
  .composer {
    margin-top: var(--trusty-space-3);
    display: flex;
    gap: var(--trusty-space-3);
  }
  .composer .textarea {
    flex: 1;
    min-height: 60px;
  }
  .composer .btn {
    align-self: stretch;
  }
</style>