#!/bin/bash
# Pixie — one-file installer. Double-click this file (or right-click > Open
# the first time, since it's unsigned) and Pixie sets itself up and launches.
# Safe to re-run anytime: it refreshes the app files, skips steps that are
# already done, and starts Pixie.
set -e

APP_DIR="$HOME/Pixie"

if ! command -v node >/dev/null 2>&1; then
  echo "Node.js isn't installed yet."
  echo "Opening the Node.js download page - please install it, then run this file again."
  open https://nodejs.org 2>/dev/null || true
  read -p "Press Enter to close..." _
  exit 1
fi

if ! command -v ollama >/dev/null 2>&1; then
  echo "Ollama isn't installed yet."
  echo "Opening the Ollama download page - please install it, then run this file again."
  open https://ollama.ai 2>/dev/null || true
  read -p "Press Enter to close..." _
  exit 1
fi

echo "================================"
echo "   Setting up Pixie..."
echo "================================"
echo
mkdir -p "$APP_DIR/public"

cat > "$APP_DIR/package.json" <<'PIXIE_EOF_PKG'
{
  "name": "ai-buddy",
  "version": "1.0.0",
  "description": "A fully local AI chat assistant powered by Ollama",
  "type": "module",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "engines": {
    "node": ">=18"
  },
  "dependencies": {
    "express": "^4.19.2"
  }
}

PIXIE_EOF_PKG

cat > "$APP_DIR/server.js" <<'PIXIE_EOF_SERVER'
import express from 'express';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
const PORT = process.env.PORT || 3000;
const DATA_DIR = process.env.PIXIE_MEMORY_DIR || path.join(os.homedir(), 'Desktop', 'Pixie Memory');
const MEMORY_FILE = path.join(DATA_DIR, 'memory.json');
const PERSONA_PROMPTS = {
  kid:
    'Your name is Pixie Kid. If asked your name, say "Pixie". Talk like a fun, ' +
    'encouraging friend for a kid: simple words, short sentences, playful and ' +
    'silly, lots of enthusiasm and emojis. Avoid grown-up jargon, complex ' +
    'topics, or anything scary or inappropriate for a child.',
  pro:
    'Your name is Pixie Pro. If asked your name, say "Pixie". Respond ' +
    'professionally: clear, concise, precise language suited for a working ' +
    'adult. Minimal emojis, no baby talk, get straight to useful, accurate ' +
    'information.',
};
const DEFAULT_PERSONA = 'kid';

const AGENT_PROMPT =
  'You are an agentic assistant with real tools available: remember_fact, ' +
  'recall_memories, set_background_color, reset_background_color, and ' +
  'get_current_datetime. Call the appropriate tool whenever it would genuinely ' +
  'help — e.g. the user asks you to remember something, asks what you remember, ' +
  'asks you to change the background color, or asks for the current date/time — ' +
  'instead of guessing or making things up. After a tool result comes back, ' +
  'reply to the user in natural language.';

const TOOLS = [
  {
    type: 'function',
    function: {
      name: 'remember_fact',
      description: "Save a fact about the user to Pixie's long-term memory so it can be recalled in future conversations.",
      parameters: {
        type: 'object',
        properties: { text: { type: 'string', description: 'The fact to remember, written in plain language.' } },
        required: ['text'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'recall_memories',
      description: 'List everything Pixie currently remembers about the user.',
      parameters: { type: 'object', properties: {} },
    },
  },
  {
    type: 'function',
    function: {
      name: 'set_background_color',
      description: 'Change the chat UI background color.',
      parameters: {
        type: 'object',
        properties: {
          color: { type: 'string', description: 'A CSS color name (e.g. "skyblue") or hex code (e.g. "#ff00aa").' },
        },
        required: ['color'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'reset_background_color',
      description: "Reset the chat UI background back to its default.",
      parameters: { type: 'object', properties: {} },
    },
  },
  {
    type: 'function',
    function: {
      name: 'get_current_datetime',
      description: 'Get the current date and time.',
      parameters: { type: 'object', properties: {} },
    },
  },
];

const MAX_TOOL_ROUNDS = 4;

function runTool(name, args) {
  switch (name) {
    case 'remember_fact': {
      const facts = loadMemory();
      const fact = makeFact(args.text || '');
      facts.push(fact);
      saveMemory(facts);
      return { saved: fact };
    }
    case 'recall_memories':
      return { facts: loadMemory() };
    case 'set_background_color':
      return { color: args.color };
    case 'reset_background_color':
      return {};
    case 'get_current_datetime':
      return { now: new Date().toString() };
    default:
      return { error: `Unknown tool: ${name}` };
  }
}

const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

function loadMemory() {
  try {
    return JSON.parse(fs.readFileSync(MEMORY_FILE, 'utf8'));
  } catch {
    return [];
  }
}

function saveMemory(facts) {
  fs.mkdirSync(DATA_DIR, { recursive: true });
  fs.writeFileSync(MEMORY_FILE, JSON.stringify(facts, null, 2));
}

function makeFact(text) {
  return {
    id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
    text: text.trim(),
    createdAt: new Date().toISOString(),
  };
}

app.get('/api/memory', (req, res) => {
  res.json({ facts: loadMemory() });
});

app.post('/api/memory', (req, res) => {
  const { text } = req.body;
  if (!text || !text.trim()) {
    return res.status(400).json({ error: 'text is required' });
  }
  const facts = loadMemory();
  facts.push(makeFact(text));
  saveMemory(facts);
  res.json({ facts });
});

app.delete('/api/memory/:id', (req, res) => {
  const facts = loadMemory().filter((f) => f.id !== req.params.id);
  saveMemory(facts);
  res.json({ facts });
});

app.get('/api/models', async (req, res) => {
  try {
    const r = await fetch(`${OLLAMA_URL}/api/tags`);
    if (!r.ok) throw new Error(`Ollama responded with ${r.status}`);
    const data = await r.json();
    res.json(data);
  } catch (err) {
    res.status(502).json({ error: `Could not reach Ollama at ${OLLAMA_URL}. Is it running? (${err.message})` });
  }
});

async function callOllama(model, msgs) {
  return fetch(`${OLLAMA_URL}/api/chat`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ model, messages: msgs, tools: TOOLS, stream: true, keep_alive: '30m' }),
  });
}

async function pipeOllamaStream(ollamaRes, res) {
  const decoder = new TextDecoder();
  let buffer = '';
  let toolCalls = null;
  let content = '';
  const handleLine = (line) => {
    if (!line.trim()) return;
    const parsed = JSON.parse(line);
    if (parsed.message?.tool_calls?.length) toolCalls = parsed.message.tool_calls;
    if (parsed.message?.content) {
      content += parsed.message.content;
      res.write(JSON.stringify({ message: { content: parsed.message.content } }) + '\n');
    }
  };
  for await (const chunk of ollamaRes.body) {
    buffer += decoder.decode(chunk, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop();
    for (const line of lines) handleLine(line);
  }
  if (buffer.trim()) handleLine(buffer);
  return { toolCalls, content };
}

app.post('/api/chat', async (req, res) => {
  const { model, messages, persona } = req.body;
  if (!model || !Array.isArray(messages)) {
    return res.status(400).json({ error: 'Request must include "model" and "messages".' });
  }
  const personaPrompt = PERSONA_PROMPTS[persona] || PERSONA_PROMPTS[DEFAULT_PERSONA];

  const facts = loadMemory();
  const augmented = [...messages];
  if (facts.length > 0) {
    augmented.unshift({
      role: 'system',
      content: `Here are things to remember about the user from past conversations:\n${facts.map((f) => `- ${f.text}`).join('\n')}`,
    });
  }
  augmented.unshift({ role: 'system', content: `${personaPrompt}\n\n${AGENT_PROMPT}` });

  let ollamaRes;
  try {
    ollamaRes = await callOllama(model, augmented);
  } catch (err) {
    return res.status(502).json({ error: `Could not reach Ollama at ${OLLAMA_URL}. Is it running? (${err.message})` });
  }
  if (!ollamaRes.ok || !ollamaRes.body) {
    const text = await ollamaRes.text().catch(() => '');
    return res.status(502).json({ error: `Ollama error: ${text || ollamaRes.statusText}` });
  }

  res.setHeader('Content-Type', 'application/x-ndjson');

  try {
    for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
      if (round > 0) {
        ollamaRes = await callOllama(model, augmented);
        if (!ollamaRes.ok || !ollamaRes.body) {
          const text = await ollamaRes.text().catch(() => '');
          res.write(JSON.stringify({ error: `Ollama error: ${text || ollamaRes.statusText}` }) + '\n');
          break;
        }
      }

      const { toolCalls, content } = await pipeOllamaStream(ollamaRes, res);
      if (!toolCalls || toolCalls.length === 0) break;

      augmented.push({ role: 'assistant', content, tool_calls: toolCalls });
      for (const call of toolCalls) {
        const name = call.function?.name;
        let args = call.function?.arguments || {};
        if (typeof args === 'string') {
          try { args = JSON.parse(args); } catch { args = {}; }
        }
        const result = await runTool(name, args);
        res.write(JSON.stringify({ tool_call: { name, arguments: args, result } }) + '\n');
        augmented.push({ role: 'tool', content: JSON.stringify(result) });
      }
    }
  } catch (err) {
    res.write(JSON.stringify({ error: err.message }) + '\n');
  }
  res.end();
});

app.listen(PORT, () => {
  console.log(`Pixie is running at http://localhost:${PORT}`);
  console.log(`Proxying to Ollama at ${OLLAMA_URL}`);
  console.log(`Memories are stored at ${MEMORY_FILE}`);
});

PIXIE_EOF_SERVER

cat > "$APP_DIR/public/index.html" <<'PIXIE_EOF_INDEX'
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Pixie</title>
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>%F0%9F%A7%9A</text></svg>" />
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div class="app">
    <header class="topbar">
      <h1><span class="mascot">🧚</span> Pixie</h1>
      <div class="model-picker">
        <select id="persona-select">
          <option value="kid">🧒 Pixie Kid</option>
          <option value="pro">💼 Pixie Pro</option>
        </select>
        <select id="model-select"><option>Loading models…</option></select>
        <button id="memory-toggle" type="button" title="Things Pixie remembers">🧠 Memory</button>
        <button id="new-chat" title="New chat">🆕 New chat</button>
      </div>
    </header>

    <div id="memory-panel" class="memory-panel hidden">
      <div class="memory-panel-header">
        <span>✨ Things Pixie remembers</span>
        <button id="memory-close" type="button" title="Close">✕</button>
      </div>
      <ul id="memory-list" class="memory-list"></ul>
      <p class="memory-hint">Tip: say <em>"remember I like dinosaurs"</em> in the chat and Pixie will save it here — and in a "Pixie Memory" folder on your Desktop!</p>
    </div>

    <main id="messages" class="messages"></main>

    <form id="chat-form" class="composer">
      <textarea id="chat-input" placeholder="Ask Pixie anything! 🌟" rows="1"></textarea>
      <button type="submit" id="send-btn">Send ✨</button>
    </form>
  </div>

  <script src="app.js"></script>
</body>
</html>

PIXIE_EOF_INDEX

cat > "$APP_DIR/public/style.css" <<'PIXIE_EOF_STYLE'
:root {
  --bg: #fff2fb;
  --bg-gradient: linear-gradient(160deg, #fff2fb 0%, #f3e8ff 45%, #e8f4ff 100%);
  --panel: #ffffff;
  --border: #f0cdec;
  --text: #402a4d;
  --muted: #9b7cae;
  --accent: #ff6fb5;
  --accent-2: #a06bff;
  --user-bubble: linear-gradient(135deg, #ffb3da, #ff9fce);
  --assistant-bubble: #f3e8ff;
  --shadow: 0 4px 14px rgba(160, 107, 255, 0.18);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #241a33;
    --bg-gradient: linear-gradient(160deg, #241a33 0%, #2c1f42 50%, #1f2a45 100%);
    --panel: #2f2242;
    --border: #4a3564;
    --text: #f5eaff;
    --muted: #b79fd1;
    --accent: #ff7ec2;
    --accent-2: #b98bff;
    --user-bubble: linear-gradient(135deg, #ff6fb5, #c76bff);
    --assistant-bubble: #3a2a54;
    --shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
  }
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--bg-gradient);
  color: var(--text);
  font-family: "Comic Sans MS", "Chalkboard SE", "Segoe UI Rounded", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  max-width: 860px;
  margin: 0 auto;
  position: relative;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  border-bottom: 3px dashed var(--border);
  flex-wrap: wrap;
  gap: 10px;
}

.topbar h1 {
  font-size: 24px;
  margin: 0;
  color: var(--accent-2);
}

.mascot {
  display: inline-block;
  animation: bob 2.2s ease-in-out infinite;
}

@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50% { transform: translateY(-4px) rotate(4deg); }
}

.model-picker {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}

select, button, textarea {
  font-family: inherit;
  font-size: 14px;
}

select {
  background: var(--panel);
  color: var(--text);
  border: 2px solid var(--border);
  border-radius: 999px;
  padding: 8px 14px;
}

#new-chat, #memory-toggle {
  background: var(--panel);
  color: var(--accent-2);
  border: 2px solid var(--border);
  border-radius: 999px;
  padding: 8px 14px;
  cursor: pointer;
  font-weight: bold;
}
#new-chat:hover, #memory-toggle:hover {
  border-color: var(--accent);
  transform: translateY(-1px);
}

.memory-panel {
  position: absolute;
  top: 66px;
  right: 12px;
  width: min(320px, calc(100% - 24px));
  background: var(--panel);
  border: 3px solid var(--border);
  border-radius: 20px;
  box-shadow: var(--shadow);
  padding: 14px;
  z-index: 10;
}
.memory-panel.hidden { display: none; }

.memory-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-weight: bold;
  color: var(--accent-2);
  margin-bottom: 8px;
}

#memory-close {
  background: transparent;
  border: none;
  color: var(--muted);
  font-size: 16px;
  cursor: pointer;
}

.memory-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 240px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.memory-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  background: var(--assistant-bubble);
  border-radius: 12px;
  padding: 8px 10px;
  font-size: 13px;
}

.memory-list .memory-delete {
  background: transparent;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 14px;
  flex-shrink: 0;
}
.memory-list .memory-delete:hover { color: var(--accent); }

.memory-empty {
  color: var(--muted);
  font-size: 13px;
  text-align: center;
  padding: 8px 0;
}

.memory-hint {
  color: var(--muted);
  font-size: 12px;
  margin: 10px 0 0;
}

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.msg {
  max-width: 80%;
  padding: 12px 16px;
  border-radius: 20px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-wrap: break-word;
  box-shadow: var(--shadow);
}

.msg.user {
  align-self: flex-end;
  background: var(--user-bubble);
  color: #3a1030;
  border-bottom-right-radius: 6px;
}

.msg.assistant {
  align-self: flex-start;
  background: var(--assistant-bubble);
  border-bottom-left-radius: 6px;
}

.msg.assistant::before {
  content: "🧚 ";
}

.msg.system {
  align-self: center;
  color: var(--muted);
  font-size: 13px;
  background: transparent;
  box-shadow: none;
}

.msg.thinking {
  color: var(--muted);
  font-style: italic;
  animation: pulse 1.4s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 0.5; }
  50% { opacity: 1; }
}

.composer {
  display: flex;
  gap: 10px;
  padding: 16px 20px;
  border-top: 3px dashed var(--border);
}

#chat-input {
  flex: 1;
  resize: none;
  background: var(--panel);
  color: var(--text);
  border: 2px solid var(--border);
  border-radius: 18px;
  padding: 12px 16px;
  max-height: 160px;
}
#chat-input:focus { outline: none; border-color: var(--accent); }

#send-btn {
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: white;
  border: none;
  border-radius: 999px;
  padding: 0 22px;
  cursor: pointer;
  font-weight: bold;
}
#send-btn:disabled { opacity: 0.5; cursor: default; }
#send-btn:hover:not(:disabled) { filter: brightness(1.08); transform: translateY(-1px); }

PIXIE_EOF_STYLE

cat > "$APP_DIR/public/app.js" <<'PIXIE_EOF_APP'
const messagesEl = document.getElementById('messages');
const formEl = document.getElementById('chat-form');
const inputEl = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
const modelSelect = document.getElementById('model-select');
const personaSelect = document.getElementById('persona-select');
const newChatBtn = document.getElementById('new-chat');
const memoryToggleBtn = document.getElementById('memory-toggle');
const memoryCloseBtn = document.getElementById('memory-close');
const memoryPanel = document.getElementById('memory-panel');
const memoryListEl = document.getElementById('memory-list');

let history = [];

const NAMED_COLORS = new Set([
  'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige',
  'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown',
  'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral',
  'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan',
  'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki',
  'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred',
  'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray',
  'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink',
  'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick',
  'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite',
  'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey', 'honeydew',
  'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender',
  'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral',
  'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen',
  'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen',
  'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue',
  'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon',
  'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple',
  'mediumseagreen', 'mediumslateblue', 'mediumspringgreen',
  'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream',
  'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive',
  'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod',
  'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip',
  'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple',
  'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown',
  'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver',
  'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow',
  'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato',
  'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow',
  'yellowgreen', 'transparent',
]);
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;

function applyBackgroundColor(color) {
  const c = (color || '').trim().toLowerCase();
  if (!HEX_COLOR.test(c) && !NAMED_COLORS.has(c)) return;
  document.body.style.background = c;
  addMessage('system', `🎨 Background changed to ${c}.`);
}

function resetBackground() {
  document.body.style.background = '';
  addMessage('system', '🎨 Background reset to default.');
}

function handleToolCall({ name, result }) {
  switch (name) {
    case 'remember_fact':
      addMessage('system', `✨ Pixie will remember: "${result.saved.text}"`);
      if (!memoryPanel.classList.contains('hidden')) loadMemory();
      break;
    case 'recall_memories':
      addMessage('system', result.facts?.length
        ? `🧠 Pixie checked its memory (${result.facts.length} thing${result.facts.length === 1 ? '' : 's'} remembered).`
        : "🧠 Pixie checked its memory — it's empty so far.");
      break;
    case 'set_background_color':
      applyBackgroundColor(result.color);
      break;
    case 'reset_background_color':
      resetBackground();
      break;
    case 'get_current_datetime':
      addMessage('system', `🕒 Pixie checked the time: ${result.now}`);
      break;
    default:
      addMessage('system', `🔧 Pixie used a tool: ${name}`);
  }
}

function addMessage(role, content) {
  const el = document.createElement('div');
  el.className = `msg ${role}`;
  el.textContent = content;
  messagesEl.appendChild(el);
  messagesEl.scrollTop = messagesEl.scrollHeight;
  return el;
}

function autoGrow() {
  inputEl.style.height = 'auto';
  inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px';
}
inputEl.addEventListener('input', autoGrow);

async function loadModels() {
  try {
    const r = await fetch('/api/models');
    const data = await r.json();
    if (data.error) throw new Error(data.error);
    const models = data.models || [];
    if (models.length === 0) {
      modelSelect.innerHTML = '<option>No models found — run "ollama pull hermes3"</option>';
      return;
    }
    modelSelect.innerHTML = models
      .map((m) => `<option value="${m.name}">${m.name}</option>`)
      .join('');
    const preferred = models.find((m) => m.name.toLowerCase().startsWith('hermes'));
    if (preferred) modelSelect.value = preferred.name;
  } catch (err) {
    modelSelect.innerHTML = '<option>Pixie can\'t find Ollama</option>';
    addMessage('system', err.message);
  }
}

function renderMemory(facts) {
  if (!facts || facts.length === 0) {
    memoryListEl.innerHTML = '<li class="memory-empty">Pixie doesn\'t remember anything yet!</li>';
    return;
  }
  memoryListEl.innerHTML = '';
  for (const fact of facts) {
    const li = document.createElement('li');
    const span = document.createElement('span');
    span.textContent = fact.text;
    const del = document.createElement('button');
    del.className = 'memory-delete';
    del.type = 'button';
    del.title = 'Forget this';
    del.textContent = '✕';
    del.addEventListener('click', () => deleteMemory(fact.id));
    li.appendChild(span);
    li.appendChild(del);
    memoryListEl.appendChild(li);
  }
}

async function loadMemory() {
  try {
    const r = await fetch('/api/memory');
    const data = await r.json();
    renderMemory(data.facts || []);
  } catch {
    memoryListEl.innerHTML = '<li class="memory-empty">Couldn\'t load memory.</li>';
  }
}

async function deleteMemory(id) {
  const r = await fetch(`/api/memory/${id}`, { method: 'DELETE' });
  const data = await r.json();
  renderMemory(data.facts || []);
}

memoryToggleBtn.addEventListener('click', () => {
  memoryPanel.classList.toggle('hidden');
  if (!memoryPanel.classList.contains('hidden')) loadMemory();
});
memoryCloseBtn.addEventListener('click', () => memoryPanel.classList.add('hidden'));

personaSelect.addEventListener('change', () => {
  const label = personaSelect.value === 'pro' ? 'Pixie Pro' : 'Pixie Kid';
  addMessage('system', `Switched to ${label} mode.`);
});

newChatBtn.addEventListener('click', () => {
  history = [];
  messagesEl.innerHTML = '';
});

formEl.addEventListener('submit', async (e) => {
  e.preventDefault();
  const text = inputEl.value.trim();
  if (!text) return;

  const model = modelSelect.value;
  if (!model || modelSelect.options.length === 0) return;

  inputEl.value = '';
  autoGrow();
  addMessage('user', text);
  history.push({ role: 'user', content: text });

  sendBtn.disabled = true;
  const assistantEl = addMessage('assistant thinking', '(thinking what to say next)');
  let assistantText = '';
  let firstChunk = true;

  try {
    const res = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ model, messages: history, persona: personaSelect.value }),
    });

    if (!res.ok || !res.body) {
      const data = await res.json().catch(() => ({}));
      throw new Error(data.error || `Request failed (${res.status})`);
    }

    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 });

      const lines = buffer.split('\n');
      buffer = lines.pop();

      for (const line of lines) {
        if (!line.trim()) continue;
        const chunk = JSON.parse(line);

        if (chunk.tool_call) {
          handleToolCall(chunk.tool_call);
          continue;
        }

        if (chunk.error) {
          addMessage('system', `Error: ${chunk.error}`);
          continue;
        }

        if (chunk.message?.content) {
          if (firstChunk) {
            assistantEl.classList.remove('thinking');
            firstChunk = false;
          }
          assistantText += chunk.message.content;
          assistantEl.textContent = assistantText;
          messagesEl.scrollTop = messagesEl.scrollHeight;
        }
      }
    }

    history.push({ role: 'assistant', content: assistantText });
  } catch (err) {
    assistantEl.classList.remove('assistant', 'thinking');
    assistantEl.classList.add('system');
    assistantEl.textContent = `Error: ${err.message}`;
  } finally {
    sendBtn.disabled = false;
  }
});

formEl.addEventListener('keydown', (e) => {
  if (e.target === inputEl && e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    formEl.requestSubmit();
  }
});

loadModels();
loadMemory();

PIXIE_EOF_APP

echo "Installing dependencies (first run only, this can take a minute)..."
if [ ! -d "$APP_DIR/node_modules" ]; then
  (cd "$APP_DIR" && npm install)
fi

echo
echo "Checking for the hermes3 model (first-time download is a few GB, please be patient)..."
if ! ollama list | grep -qi "hermes3"; then
  echo "Downloading hermes3..."
  ollama pull hermes3
fi

echo
echo "All set! Launching Pixie..."
(cd "$APP_DIR" && (sleep 3 && open http://localhost:3000 2>/dev/null || true) & npm start)
