new

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlowMind — AI Workspace</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
  --bg: #08080d;
  --surface: #101018;
  --surface2: #16161f;
  --border: #1e1e2e;
  --accent: #6c5ce7;
  --accent2: #4fc3f7;
  --accentG: rgba(108,92,231,0.12);
  --text: #e4e2ee;
  --muted: #7e7c92;
  --dim: #4a4860;
  --red: #ff6b6b;
  --green: #00d2a0;
  --orange: #f5a623;
  --blue: #4285f4;
  --font: 'DM Sans', sans-serif;
  --mono: 'JetBrains Mono', monospace;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { background:var(--bg); color:var(--text); font-family:var(--font); overflow:hidden; width:100vw; height:100vh; }
::-webkit-scrollbar { width:4px; }
::-webkit-scrollbar-track { background:transparent; }
::-webkit-scrollbar-thumb { background:var(--border); border-radius:4px; }
@keyframes pulse { 0%,100%{opacity:.3;transform:scale(.8)} 50%{opacity:1;transform:scale(1.2)} }
@keyframes fadeIn { from{opacity:0;transform:translateY(12px)} to{opacity:1;transform:translateY(0)} }
@keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} }
@keyframes spin { to{transform:rotate(360deg)} }
button { font-family:var(--font); }
input, textarea { font-family:var(--mono); }

/* Top bar */
#topbar {
  position:fixed; top:0; left:0; right:0; height:48px; z-index:200;
  display:flex; align-items:center; justify-content:space-between; padding:0 16px;
  background:rgba(8,8,13,0.88); backdrop-filter:blur(20px); border-bottom:1px solid var(--border);
}
#topbar .logo { display:flex; align-items:center; gap:10px; }
#topbar .logo-icon { width:28px; height:28px; border-radius:8px; background:linear-gradient(135deg,var(--accent),var(--accent2)); display:flex; align-items:center; justify-content:center; font-size:14px; }
#topbar .logo-text { font-size:15px; font-weight:700; letter-spacing:-0.5px; }
#topbar .badge { font-size:10px; padding:2px 8px; background:var(--accentG); border-radius:4px; color:var(--muted); }
#topbar .right { display:flex; align-items:center; gap:10px; }
#topbar .stats { font-size:11px; color:var(--dim); font-family:var(--mono); }
#topbar .conn-msg { font-size:11px; color:var(--accent); padding:3px 10px; background:var(--accentG); border-radius:6px; animation:pulse 1.5s infinite; display:none; }
#topbar .btn-clear { padding:5px 12px; background:rgba(255,107,107,0.1); border:1px solid rgba(255,107,107,0.2); border-radius:6px; color:var(--red); font-size:11px; cursor:pointer; }

/* Canvas */
#canvas-wrap {
  position:fixed; top:48px; left:0; right:0; bottom:0; overflow:hidden; cursor:default;
  background-image:radial-gradient(circle at 1px 1px, rgba(74,72,96,0.08) 1px, transparent 0);
  background-size:20px 20px;
}
#canvas { position:absolute; top:0; left:0; width:20000px; height:20000px; transform-origin:0 0; }

/* Welcome */
#welcome {
  position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); z-index:50;
  display:flex; flex-direction:column; align-items:center; gap:14px; pointer-events:none;
  animation:fadeIn 0.6s ease-out;
}
#welcome .icon { width:60px; height:60px; border-radius:18px; background:linear-gradient(135deg,var(--accent),var(--accent2)); display:flex; align-items:center; justify-content:center; font-size:26px; animation:float 3s ease-in-out infinite; box-shadow:0 8px 32px rgba(108,92,231,0.3); }
#welcome h2 { font-size:22px; font-weight:700; letter-spacing:-0.5px; }
#welcome p { font-size:13px; color:var(--muted); text-align:center; max-width:420px; line-height:1.7; }
#welcome .hints { display:flex; gap:14px; color:var(--dim); font-size:11px; flex-wrap:wrap; justify-content:center; }

/* Toolbar */
#toolbar {
  position:fixed; bottom:20px; left:50%; transform:translateX(-50%); z-index:200;
  display:flex; gap:2px; padding:6px 12px; background:rgba(12,12,20,0.92);
  backdrop-filter:blur(20px); border:1px solid var(--border); border-radius:16px;
  box-shadow:0 8px 32px rgba(0,0,0,0.5);
}
.tool-btn {
  display:flex; flex-direction:column; align-items:center; gap:2px;
  padding:8px 11px; background:transparent; border:none; border-radius:10px;
  cursor:pointer; color:var(--muted); transition:all 0.15s; font-size:10px; font-weight:500;
}
.tool-btn:hover { background:var(--surface2); color:var(--text); }
.tool-btn .ico { font-size:18px; line-height:1; }
.zoom-btn { display:flex; align-items:center; justify-content:center; width:32px; height:32px; background:transparent; border:none; border-radius:8px; cursor:pointer; color:var(--muted); font-size:16px; }
.zoom-btn:hover { color:var(--text); }
#zoom-label { min-width:40px; text-align:center; color:var(--muted); font-size:11px; font-family:var(--mono); display:flex; align-items:center; justify-content:center; }

/* Nodes */
.node {
  position:absolute; border-radius:12px; display:flex; flex-direction:column;
  overflow:hidden; box-shadow:0 4px 16px rgba(0,0,0,0.3); transition:box-shadow 0.2s;
  min-width:240px; min-height:160px;
}
.node.selected { box-shadow:0 0 0 1.5px var(--node-border), 0 8px 32px rgba(0,0,0,0.4), 0 0 20px rgba(108,92,231,0.15); }
.node-header {
  display:flex; align-items:center; padding:8px 10px; gap:6px;
  border-bottom:1px solid var(--border); background:rgba(0,0,0,0.2);
  min-height:40px; flex-shrink:0; cursor:grab; user-select:none;
}
.node-header:active { cursor:grabbing; }
.node-header .grip { opacity:0.3; font-size:10px; letter-spacing:2px; }
.node-header .type-ico { font-size:14px; }
.node-header input {
  flex:1; background:transparent; border:none; color:var(--text);
  font-size:13px; font-weight:600; outline:none; min-width:0; font-family:var(--font);
}
.node-header .color-dot {
  width:14px; height:14px; border-radius:50%; border:none; cursor:pointer; flex-shrink:0;
}
.node-header .hdr-btn {
  width:24px; height:24px; border-radius:6px; border:1px solid var(--border);
  background:transparent; cursor:pointer; display:flex; align-items:center; justify-content:center;
  color:var(--muted); font-size:12px; flex-shrink:0;
}
.node-header .hdr-btn:hover { border-color:var(--accent); color:var(--text); }
.node-header .hdr-btn.active { background:var(--accent); border-color:var(--accent); color:#fff; }
.node-header .hdr-btn.del { border:none; }
.node-header .hdr-btn.del:hover { color:var(--red); }
.node-body { flex:1; overflow:hidden; display:flex; flex-direction:column; }
.node-body > * { overflow-y:auto; }
.resize-handle {
  position:absolute; right:0; bottom:0; width:16px; height:16px; cursor:nwse-resize;
}

/* Node content styles */
.note-area {
  width:100%; height:100%; background:transparent; border:none; color:var(--text);
  resize:none; font-family:var(--mono); font-size:13px; line-height:1.6;
  padding:8px 12px; outline:none; overflow-y:auto;
}
.url-input, .yt-input {
  width:100%; padding:8px 12px; background:rgba(0,0,0,0.3); border:1px solid var(--border);
  border-radius:8px; color:var(--text); font-size:13px; outline:none;
}
.url-input:focus, .yt-input:focus { border-color:var(--accent); }
.yt-thumb-wrap {
  position:relative; display:block; border-radius:8px; overflow:hidden; text-decoration:none;
}
.yt-thumb-wrap img { width:100%; display:block; }
.yt-play {
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:50px; height:50px; border-radius:50%; background:rgba(255,0,0,0.9);
  display:flex; align-items:center; justify-content:center; font-size:20px; color:#fff;
}
.yt-overlay {
  position:absolute; bottom:0; left:0; right:0; padding:6px 10px;
  background:linear-gradient(transparent,rgba(0,0,0,0.8));
  font-size:10px; color:rgba(255,255,255,0.8);
}

/* Media node */
.media-btns { display:flex; gap:4px; flex-shrink:0; }
.gemini-btn {
  flex:1; padding:7px 10px; border-radius:8px; border:none; cursor:pointer;
  font-size:11px; font-weight:600; display:flex; align-items:center; justify-content:center; gap:4px;
  transition:opacity 0.2s;
}
.gemini-btn:disabled { opacity:0.5; cursor:default; }
.gemini-btn.primary { background:linear-gradient(135deg,var(--accent),var(--accent2)); color:#fff; }
.gemini-btn.secondary { background:rgba(245,166,35,0.12); color:var(--orange); border:1px solid rgba(245,166,35,0.25); }
.analysis-result {
  flex:1; min-height:80px; overflow-y:auto; padding:8px 10px;
  background:rgba(0,0,0,0.25); border-radius:8px; border:1px solid var(--border);
  color:var(--text); font-size:12px; line-height:1.6; white-space:pre-wrap; word-break:break-word;
}

/* AI Chat */
.model-sel { display:flex; gap:4px; padding:6px 10px; border-bottom:1px solid var(--border); flex-shrink:0; }
.model-btn {
  flex:1; padding:4px 8px; border-radius:6px; border:1px solid var(--border);
  background:transparent; color:var(--muted); font-size:11px; font-weight:600; cursor:pointer;
  transition:all 0.15s;
}
.model-btn.active-claude { border-color:var(--accent); background:rgba(108,92,231,0.12); color:var(--accent); }
.model-btn.active-gemini { border-color:var(--blue); background:rgba(66,133,244,0.12); color:var(--blue); }
.chat-messages { flex:1; overflow-y:auto; padding:8px 12px; display:flex; flex-direction:column; gap:8px; }
.chat-msg {
  padding:8px 12px; border-radius:10px; font-size:13px; line-height:1.5;
  max-width:90%; white-space:pre-wrap; word-break:break-word;
}
.chat-msg.user { background:var(--accentG); align-self:flex-end; border:1px solid rgba(108,92,231,0.2); }
.chat-msg.ai { background:rgba(0,0,0,0.3); align-self:flex-start; border:1px solid var(--border); }
.chat-input-wrap { display:flex; gap:6px; padding:8px 12px; border-top:1px solid var(--border); flex-shrink:0; }
.chat-input {
  flex:1; padding:8px 12px; background:rgba(0,0,0,0.3); border:1px solid var(--border);
  border-radius:8px; color:var(--text); font-size:13px; outline:none;
}
.chat-input:focus { border-color:var(--accent); }
.chat-send {
  padding:8px 14px; background:var(--accent); border:none; border-radius:8px;
  cursor:pointer; color:#fff; font-size:14px;
}
.chat-send:disabled { background:var(--border); cursor:default; }

/* Drop zone */
.dropzone {
  flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px;
  border:2px dashed var(--border); border-radius:8px; background:rgba(0,0,0,0.1);
  cursor:pointer; padding:16px; min-height:100px; transition:all 0.2s;
}
.dropzone.over { border-color:var(--accent); background:var(--accentG); }
.dropzone .dz-ico { font-size:28px; }
.dropzone .dz-text { color:var(--muted); font-size:12px; text-align:center; }

/* Editor */
.editor-area {
  width:100%; height:100%; padding:12px 16px; color:var(--text); font-size:14px;
  line-height:1.7; outline:none; font-family:'DM Sans',Georgia,serif; overflow-y:auto;
}

/* SVG connections */
#conn-svg { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; overflow:visible; }
#conn-svg path { pointer-events:stroke; cursor:pointer; }

/* Dots loader */
.dots { display:flex; gap:4px; }
.dots span {
  width:6px; height:6px; border-radius:50%; background:var(--accent);
  animation:pulse 1s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay:0.15s; }
.dots span:nth-child(3) { animation-delay:0.3s; }

/* Mindmap */
.mm-branch { margin-left:16px; }
.mm-item { display:flex; align-items:center; gap:6px; padding:4px 8px; margin:2px 0; }
.mm-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
.mm-text { color:var(--text); font-size:13px; flex:1; }
.mm-add { width:18px; height:18px; border-radius:4px; border:1px solid var(--border); background:transparent; cursor:pointer; color:var(--muted); font-size:12px; display:flex; align-items:center; justify-content:center; opacity:0.6; flex-shrink:0; }
.mm-input-wrap { display:flex; gap:4px; padding:8px 12px; border-top:1px solid var(--border); flex-shrink:0; }
.mm-input { flex:1; padding:6px 10px; background:rgba(0,0,0,0.3); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:12px; outline:none; }
</style>
</head>
<body>

<!-- Top Bar -->
<div id="topbar">
  <div class="logo">
    <div class="logo-icon">⚡</div>
    <span class="logo-text">FlowMind</span>
    <span class="badge">AI Workspace</span>
    <span class="badge" style="color:#4285f4;background:rgba(66,133,244,0.1)">Gemini Video</span>
  </div>
  <div class="right">
    <span class="stats" id="stats">0 nodes · 0 links</span>
    <span class="conn-msg" id="conn-msg">Click node to connect</span>
    <button class="btn-clear" onclick="clearAll()">Clear All</button>
  </div>
</div>

<!-- Canvas -->
<div id="canvas-wrap">
  <div id="canvas">
    <svg id="conn-svg">
      <defs>
        <linearGradient id="cg" x1="0%" y1="0%" x2="100%" y2="0%">
          <stop offset="0%" stop-color="#6c5ce7" stop-opacity="0.5"/>
          <stop offset="50%" stop-color="#4fc3f7" stop-opacity="0.7"/>
          <stop offset="100%" stop-color="#6c5ce7" stop-opacity="0.5"/>
        </linearGradient>
      </defs>
    </svg>
  </div>
</div>

<!-- Welcome -->
<div id="welcome">
  <div class="icon">✦</div>
  <h2>FlowMind AI Workspace</h2>
  <p>Drop videos from TikTok/IG on the canvas. Gemini will <b>see and analyze</b> them.<br>Connect nodes to AI Chat (Claude or Gemini) for deep analysis.</p>
  <div class="hints">
    <span>Scroll = zoom</span><span>·</span>
    <span>Drag canvas = pan</span><span>·</span>
    <span>Drop files = auto-create</span>
  </div>
</div>

<!-- Toolbar -->
<div id="toolbar">
  <button class="tool-btn" onclick="addNode('note')"><span class="ico">📝</span>Note</button>
  <button class="tool-btn" onclick="addNode('youtube')"><span class="ico">▶️</span>YouTube</button>
  <button class="tool-btn" onclick="addNode('image')"><span class="ico">🖼️</span>Image</button>
  <button class="tool-btn" onclick="addNode('media')"><span class="ico">🎬</span>Media</button>
  <button class="tool-btn" onclick="addNode('url')"><span class="ico">🔗</span>URL</button>
  <button class="tool-btn" onclick="addNode('ai_chat')"><span class="ico">🤖</span>AI Chat</button>
  <button class="tool-btn" onclick="addNode('editor')"><span class="ico">✏️</span>Editor</button>
  <button class="tool-btn" onclick="addNode('mindmap')"><span class="ico">🧠</span>MindMap</button>
  <div style="width:1px;background:var(--border);margin:4px 6px"></div>
  <button class="zoom-btn" onclick="changeZoom(0.1)">+</button>
  <span id="zoom-label">100%</span>
  <button class="zoom-btn" onclick="changeZoom(-0.1)">−</button>
</div>

<script>
const GEMINI_KEY = "AIzaSyCA2C7MnzkdFbAH3ivC8fjVUgYdCLhZI-M";
const COLORS = [
  {bg:"#1a1535",border:"#6c5ce7"},{bg:"#0d2137",border:"#4fc3f7"},
  {bg:"#0d2a20",border:"#00d2a0"},{bg:"#2a1a0d",border:"#f5a623"},
  {bg:"#2a0d1a",border:"#f78fb3"},{bg:"#2a2a0d",border:"#ffd93d"}
];

let state = { nodes: [], connections: [], pan: {x:0,y:0}, zoom: 1, selected: null, connecting: null };
let _id = 0;
const uid = () => "n" + Date.now() + (++_id);
const wrap = document.getElementById("canvas-wrap");
const canvas = document.getElementById("canvas");
const svg = document.getElementById("conn-svg");

// ─── PAN & ZOOM ──────────────────────────────────────────────────────
let isPanning = false, panStart = {x:0,y:0,px:0,py:0};

wrap.addEventListener("mousedown", (e) => {
  if (e.target !== wrap && e.target !== canvas && e.target.tagName !== "svg") return;
  if (state.connecting) { state.connecting = null; updateUI(); return; }
  state.selected = null; updateUI();
  isPanning = true;
  panStart = { x:e.clientX, y:e.clientY, px:state.pan.x, py:state.pan.y };
  wrap.style.cursor = "grabbing";
});
window.addEventListener("mousemove", (e) => {
  if (!isPanning) return;
  state.pan.x = panStart.px + (e.clientX - panStart.x) / state.zoom;
  state.pan.y = panStart.py + (e.clientY - panStart.y) / state.zoom;
  applyTransform();
});
window.addEventListener("mouseup", () => { isPanning = false; wrap.style.cursor = "default"; });

wrap.addEventListener("wheel", (e) => {
  if (e.target.closest(".node-body")) return;
  e.preventDefault();
  state.zoom = Math.max(0.2, Math.min(2, state.zoom + (e.deltaY > 0 ? -0.05 : 0.05)));
  applyTransform();
  document.getElementById("zoom-label").textContent = Math.round(state.zoom*100)+"%";
}, {passive:false});

function changeZoom(d) {
  state.zoom = Math.max(0.2, Math.min(2, state.zoom + d));
  applyTransform();
  document.getElementById("zoom-label").textContent = Math.round(state.zoom*100)+"%";
}
function applyTransform() {
  canvas.style.transform = "scale("+state.zoom+") translate("+state.pan.x+"px,"+state.pan.y+"px)";
}

// ─── DRAG & DROP FILES ON CANVAS ─────────────────────────────────────
wrap.addEventListener("dragover", (e) => e.preventDefault());
wrap.addEventListener("drop", async (e) => {
  e.preventDefault();
  const file = e.dataTransfer?.files?.[0];
  if (!file) return;
  const rect = wrap.getBoundingClientRect();
  const x = (e.clientX - rect.left) / state.zoom - state.pan.x;
  const y = (e.clientY - rect.top) / state.zoom - state.pan.y;
  const data = await fileToBase64(file);
  if (file.type.startsWith("image/")) {
    addNode("image", x - 175, y - 160, {imageData:data, imageName:file.name, title:file.name});
  } else if (file.type.startsWith("video/") || file.type.startsWith("audio/")) {
    addNode("media", x - 200, y - 225, {mediaData:data, mediaName:file.name, mediaType:file.type, title:file.name});
  }
});

function fileToBase64(file) {
  return new Promise((r,j) => { const rd = new FileReader(); rd.onload = () => r(rd.result); rd.onerror = j; rd.readAsDataURL(file); });
}

// ─── NODE MANAGEMENT ─────────────────────────────────────────────────
function addNode(type, cx, cy, extra) {
  const sizes = {note:[300,250],youtube:[380,360],image:[350,320],media:[400,480],url:[320,200],ai_chat:[400,480],editor:[450,350],mindmap:[350,350]};
  const [w,h] = sizes[type] || [300,250];
  if (cx === undefined) {
    cx = (window.innerWidth/2) / state.zoom - state.pan.x - w/2 + (state.nodes.length%5)*40;
    cy = (window.innerHeight/2) / state.zoom - state.pan.y - h/2 + (state.nodes.length%5)*40;
  }
  const node = {
    id: uid(), type, x:cx, y:cy, w, h,
    title: "", content: "", url: "", imageUrl: "", imageData: null, imageName: "",
    mediaData: null, mediaName: "", mediaType: "", analysis: "",
    messages: [], aiModel: "claude",
    mindmapData: type === "mindmap" ? [{id:"root",text:"Central Idea",children:[]}] : null,
    colorIndex: Math.floor(Math.random()*COLORS.length),
    z: state.nodes.length + 1,
    ...extra
  };
  state.nodes.push(node);
  state.selected = node.id;
  document.getElementById("welcome").style.display = "none";
  renderNode(node);
  updateUI();
  saveState();
}

function deleteNode(id) {
  const el = document.getElementById(id);
  if (el) el.remove();
  state.nodes = state.nodes.filter(n => n.id !== id);
  state.connections = state.connections.filter(c => c.from !== id && c.to !== id);
  if (state.selected === id) state.selected = null;
  updateUI();
  saveState();
}

function getNode(id) { return state.nodes.find(n => n.id === id); }

// ─── RENDER NODE ─────────────────────────────────────────────────────
function renderNode(node) {
  const nc = COLORS[node.colorIndex || 0];
  const div = document.createElement("div");
  div.className = "node" + (state.selected === node.id ? " selected" : "");
  div.id = node.id;
  div.style.cssText = "left:"+node.x+"px;top:"+node.y+"px;width:"+node.w+"px;height:"+node.h+"px;background:"+nc.bg+";border:1.5px solid "+(state.selected===node.id?nc.border:"var(--border)")+";z-index:"+(node.z||1)+";--node-border:"+nc.border;

  const icons = {note:"📝",youtube:"▶️",image:"🖼️",url:"🔗",ai_chat:"🤖",editor:"✏️",mindmap:"🧠",media:"🎬"};
  const labels = {note:"Note",youtube:"YouTube",image:"Image",url:"URL",ai_chat:"AI Chat",editor:"Editor",mindmap:"Mind Map",media:"Media"};

  div.innerHTML = `
    <div class="node-header" data-drag="${node.id}">
      <span class="grip">⠿</span>
      <span class="type-ico">${icons[node.type]}</span>
      <input value="${node.title||""}" placeholder="${labels[node.type]}" oninput="getNode('${node.id}').title=this.value;saveState()">
      <button class="color-dot" style="background:${nc.border}" onclick="cycleColor('${node.id}')"></button>
      <button class="hdr-btn ${state.connecting===node.id?'active':''}" onclick="toggleConnect('${node.id}')" title="Connect">⟷</button>
      <button class="hdr-btn del" onclick="deleteNode('${node.id}')" title="Delete">✕</button>
    </div>
    <div class="node-body" id="body-${node.id}"></div>
    <div class="resize-handle" data-resize="${node.id}"></div>
  `;
  canvas.appendChild(div);
  renderNodeBody(node);
  setupDrag(div, node);
  setupResize(div, node);
}

function renderNodeBody(node) {
  const body = document.getElementById("body-" + node.id);
  if (!body) return;
  body.innerHTML = "";
  switch(node.type) {
    case "note": body.innerHTML = `<textarea class="note-area" oninput="getNode('${node.id}').content=this.value;saveState()" placeholder="Write your thoughts...">${node.content||""}</textarea>`; break;
    case "youtube": renderYouTube(body, node); break;
    case "image": renderImage(body, node); break;
    case "media": renderMedia(body, node); break;
    case "url": renderUrl(body, node); break;
    case "ai_chat": renderAIChat(body, node); break;
    case "editor": body.innerHTML = `<div class="editor-area" contenteditable="true" onblur="getNode('${node.id}').content=this.innerHTML;saveState()">${node.content||"<p>Start writing...</p>"}</div>`; break;
    case "mindmap": renderMindmap(body, node); break;
  }
}

// ─── YOUTUBE NODE ────────────────────────────────────────────────────
function renderYouTube(body, node) {
  const vid = extractYTId(node.url);
  let html = `<div style="padding:8px 12px;height:100%;display:flex;flex-direction:column;gap:8px;overflow-y:auto">
    <input class="yt-input" value="${node.url||""}" placeholder="Paste YouTube URL..." oninput="getNode('${node.id}').url=this.value;renderNodeBody(getNode('${node.id}'));saveState()">`;
  if (vid) {
    html += `<a class="yt-thumb-wrap" href="https://www.youtube.com/watch?v=${vid}" target="_blank">
      <img src="https://img.youtube.com/vi/${vid}/hqdefault.jpg" alt="">
      <div class="yt-play">▶</div>
      <div class="yt-overlay">Open in YouTube ↗</div>
    </a>
    <textarea class="note-area" style="min-height:40px;flex:1" oninput="getNode('${node.id}').content=this.value;saveState()" placeholder="Notes...">${node.content||""}</textarea>`;
  } else {
    html += `<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--dim);font-size:12px">Paste a YouTube link above</div>`;
  }
  html += `</div>`;
  body.innerHTML = html;
}

function extractYTId(url) {
  if (!url) return null;
  const m = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/);
  return m ? m[1] : null;
}

// ─── IMAGE NODE ──────────────────────────────────────────────────────
function renderImage(body, node) {
  const has = node.imageData || node.imageUrl;
  if (!has) {
    body.innerHTML = `<div style="padding:8px 12px;height:100%;display:flex;flex-direction:column;gap:8px">
      <input class="url-input" value="${node.imageUrl||""}" placeholder="Paste image URL or drop below..." oninput="getNode('${node.id}').imageUrl=this.value;renderNodeBody(getNode('${node.id}'));saveState()">
      <div class="dropzone" id="dz-${node.id}"><span class="dz-ico">📁</span><span class="dz-text">Drop image or click</span></div>
    </div>`;
    setupDropzone("dz-"+node.id, "image/*", (f) => { node.imageData = f.data; node.imageName = f.name; renderNodeBody(node); saveState(); });
  } else {
    body.innerHTML = `<div style="padding:8px 12px;height:100%;display:flex;flex-direction:column;gap:8px;overflow-y:auto">
      <div style="position:relative;flex:1;border-radius:8px;overflow:hidden">
        <img src="${node.imageData||node.imageUrl}" style="width:100%;height:100%;object-fit:contain">
        <button onclick="getNode('${node.id}').imageData=null;getNode('${node.id}').imageUrl='';renderNodeBody(getNode('${node.id}'));saveState()" style="position:absolute;top:4px;right:4px;width:24px;height:24px;border-radius:6px;background:rgba(0,0,0,0.7);border:none;cursor:pointer;color:#fff;font-size:12px">✕</button>
      </div>
      ${node.imageName ? '<div style="color:var(--muted);font-size:11px;text-align:center">'+node.imageName+'</div>' : ''}
    </div>`;
  }
}

// ─── MEDIA NODE WITH GEMINI ──────────────────────────────────────────
function renderMedia(body, node) {
  if (!node.mediaData) {
    body.innerHTML = `<div style="padding:8px 12px;height:100%;display:flex;flex-direction:column;gap:8px">
      <div class="dropzone" id="dz-${node.id}"><span class="dz-ico">🎬</span><span class="dz-text">Drop video/audio here<br><small style="color:var(--dim)">MP4, WebM, MP3...</small></span></div>
    </div>`;
    setupDropzone("dz-"+node.id, "video/*,audio/*", (f) => { node.mediaData = f.data; node.mediaName = f.name; node.mediaType = f.type; renderNodeBody(node); saveState(); });
  } else {
    let mediaEl = node.mediaType?.startsWith("video")
      ? `<video src="${node.mediaData}" controls style="width:100%;max-height:200px;border-radius:8px"></video>`
      : `<audio src="${node.mediaData}" controls style="width:100%;margin:16px 0"></audio>`;

    body.innerHTML = `<div style="padding:8px 12px;height:100%;display:flex;flex-direction:column;gap:8px;overflow-y:auto">
      <div style="position:relative;border-radius:8px;overflow:hidden;background:#000;flex-shrink:0">
        ${mediaEl}
        <button onclick="getNode('${node.id}').mediaData=null;getNode('${node.id}').analysis='';renderNodeBody(getNode('${node.id}'));saveState()" style="position:absolute;top:4px;right:4px;width:24px;height:24px;border-radius:6px;background:rgba(0,0,0,0.7);border:none;cursor:pointer;color:#fff;font-size:12px">✕</button>
      </div>
      <div style="color:var(--muted);font-size:11px;text-align:center">${node.mediaName}</div>
      <div class="media-btns">
        <button class="gemini-btn primary" id="ga-${node.id}" onclick="geminiAnalyze('${node.id}')">👁 Gemini Analyze</button>
        <button class="gemini-btn secondary" id="gad-${node.id}" onclick="geminiAnalyzeAd('${node.id}')">📊 Ad Analysis</button>
      </div>
      <div class="analysis-result" id="ar-${node.id}" style="${node.analysis?'':'display:none'}">${node.analysis||""}</div>
      ${!node.analysis ? `<textarea class="note-area" style="min-height:40px" oninput="getNode('${node.id}').content=this.value;saveState()" placeholder="Notes...">${node.content||""}</textarea>` : ''}
    </div>`;
  }
}

async function geminiAnalyze(nodeId, customPrompt) {
  const node = getNode(nodeId);
  if (!node || !node.mediaData) return;
  const btn = document.getElementById("ga-"+nodeId);
  const btn2 = document.getElementById("gad-"+nodeId);
  if (btn) { btn.disabled = true; btn.innerHTML = '<span class="dots"><span></span><span></span><span></span></span> Analyzing...'; }
  if (btn2) btn2.disabled = true;
  try {
    const raw = node.mediaData.split(",")[1];
    const res = await fetch("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key="+GEMINI_KEY, {
      method:"POST", headers:{"Content-Type":"application/json"},
      body: JSON.stringify({contents:[{parts:[
        {inline_data:{mime_type:node.mediaType,data:raw}},
        {text: customPrompt || "Analyze this video in detail. Identify: 1) Hook in first 3 seconds 2) Main message and structure 3) Call-to-action 4) Visual style (talking head, B-roll, text overlay, etc) 5) Editing patterns 6) Text on screen 7) Audio/music style 8) What makes it engaging. Respond in the video's language."}
      ]}]})
    });
    const data = await res.json();
    const text = data.candidates?.[0]?.content?.parts?.[0]?.text || data.error?.message || "Could not analyze.";
    node.analysis = text;
    node.content = text;
    saveState();
    const ar = document.getElementById("ar-"+nodeId);
    if (ar) { ar.style.display = "block"; ar.textContent = text; }
  } catch(e) {
    const ar = document.getElementById("ar-"+nodeId);
    if (ar) { ar.style.display = "block"; ar.textContent = "Error: "+e.message; }
  }
  if (btn) { btn.disabled = false; btn.innerHTML = "👁 Gemini Analyze"; }
  if (btn2) btn2.disabled = false;
}

function geminiAnalyzeAd(nodeId) {
  geminiAnalyze(nodeId, "Analyze this video as a marketing ad. Extract in detail: 1) Hook (first 3 seconds - what catches attention) 2) Pain point addressed 3) Value proposition 4) Call-to-action 5) Copywriting patterns used 6) Target audience 7) Emotional triggers 8) What to replicate for your own ads. Respond in the video's language.");
}

// ─── URL NODE ────────────────────────────────────────────────────────
function renderUrl(body, node) {
  body.innerHTML = `<div style="padding:8px 12px;height:100%;display:flex;flex-direction:column;gap:8px;overflow-y:auto">
    <input class="url-input" value="${node.url||""}" placeholder="Paste any URL..." oninput="getNode('${node.id}').url=this.value;saveState()">
    ${node.url ? `<a href="${node.url}" target="_blank" style="flex:1;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.2);border-radius:8px;padding:16px;color:var(--accent);word-break:break-all;font-size:13px;text-decoration:none">↗ ${node.url}</a>` : ''}
  </div>`;
}

// ─── AI CHAT NODE ────────────────────────────────────────────────────
function renderAIChat(body, node) {
  const model = node.aiModel || "claude";
  let msgsHtml = "";
  if (node.messages.length === 0) {
    msgsHtml = '<div style="color:var(--dim);font-size:12px;text-align:center;padding:20px">Connect nodes for context, then ask</div>';
  }
  node.messages.forEach((m,i) => {
    msgsHtml += `<div class="chat-msg ${m.role==='user'?'user':'ai'}">${escHtml(m.content)}</div>`;
  });

  body.innerHTML = `
    <div class="model-sel">
      <button class="model-btn ${model==='claude'?'active-claude':''}" onclick="getNode('${node.id}').aiModel='claude';renderNodeBody(getNode('${node.id}'))">Claude</button>
      <button class="model-btn ${model==='gemini'?'active-gemini':''}" onclick="getNode('${node.id}').aiModel='gemini';renderNodeBody(getNode('${node.id}'))">Gemini</button>
    </div>
    <div class="chat-messages" id="msgs-${node.id}">${msgsHtml}</div>
    <div class="chat-input-wrap">
      <input class="chat-input" id="ci-${node.id}" placeholder="Ask AI..." onkeydown="if(event.key==='Enter'&&!event.shiftKey)sendChat('${node.id}')">
      <button class="chat-send" id="cs-${node.id}" onclick="sendChat('${node.id}')">➤</button>
    </div>`;
  const mc = document.getElementById("msgs-"+node.id);
  if (mc) mc.scrollTop = mc.scrollHeight;
}

async function sendChat(nodeId) {
  const node = getNode(nodeId);
  const input = document.getElementById("ci-"+nodeId);
  const btn = document.getElementById("cs-"+nodeId);
  if (!node || !input || !input.value.trim()) return;

  const text = input.value.trim();
  node.messages.push({role:"user", content:text});
  input.value = "";
  btn.disabled = true;

  // Add user msg
  const mc = document.getElementById("msgs-"+nodeId);
  mc.innerHTML += `<div class="chat-msg user">${escHtml(text)}</div>`;
  mc.innerHTML += `<div class="chat-msg ai" id="loading-${nodeId}"><span class="dots"><span></span><span></span><span></span></span></div>`;
  mc.scrollTop = mc.scrollHeight;

  // Gather context
  const ctx = gatherContext(nodeId);
  const sys = ctx ? "AI assistant in workspace. Connected nodes:\n"+ctx+"\nUse this context. Be concise. Respond in user's language." : "AI assistant in workspace. Be concise. Respond in user's language.";

  try {
    let aiText = "";
    if (node.aiModel === "gemini") {
      const res = await fetch("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key="+GEMINI_KEY, {
        method:"POST", headers:{"Content-Type":"application/json"},
        body:JSON.stringify({contents:[{parts:[{text:sys+"\n\nUser: "+text}]}]})
      });
      const data = await res.json();
      aiText = data.candidates?.[0]?.content?.parts?.[0]?.text || data.error?.message || "Error";
    } else {
      const res = await fetch("https://api.anthropic.com/v1/messages", {
        method:"POST", headers:{"Content-Type":"application/json"},
        body:JSON.stringify({model:"claude-sonnet-4-20250514",max_tokens:1000,system:sys,messages:node.messages.map(m=>({role:m.role,content:m.content}))})
      });
      const data = await res.json();
      aiText = data.content?.map(c=>c.text||"").join("") || "Error";
    }
    node.messages.push({role:"assistant",content:aiText});
    saveState();
    const ld = document.getElementById("loading-"+nodeId);
    if (ld) { ld.className = "chat-msg ai"; ld.textContent = aiText; }
  } catch(e) {
    node.messages.push({role:"assistant",content:"Error: "+e.message});
    const ld = document.getElementById("loading-"+nodeId);
    if (ld) { ld.className = "chat-msg ai"; ld.textContent = "Error: "+e.message; }
  }
  btn.disabled = false;
  mc.scrollTop = mc.scrollHeight;
}

function gatherContext(nodeId) {
  const linked = state.connections.filter(c => c.from===nodeId||c.to===nodeId).map(c => c.from===nodeId?c.to:c.from);
  return state.nodes.filter(n => linked.includes(n.id)).map(n => {
    let s = "["+n.type.toUpperCase()+'] "' + (n.title||"Untitled") + '"';
    if (n.content) s += "\nContent: " + n.content;
    if (n.url) s += "\nURL: " + n.url;
    if (n.analysis) s += "\nVideo Analysis: " + n.analysis;
    return s;
  }).join("\n---\n");
}

// ─── MINDMAP NODE ────────────────────────────────────────────────────
function renderMindmap(body, node) {
  const items = node.mindmapData || [{id:"root",text:"Central Idea",children:[]}];
  function renderBranch(item, depth) {
    const c = COLORS[depth % COLORS.length].border;
    let html = `<div ${depth>0?'class="mm-branch"':''} ${depth>0?'style="border-left:2px solid '+c+'"':''}>
      <div class="mm-item"><span class="mm-dot" style="background:${c}"></span><span class="mm-text">${escHtml(item.text)}</span>
      <button class="mm-add" onclick="mmAdd('${node.id}','${item.id}')">+</button></div>`;
    if (item.children) item.children.forEach(ch => { html += renderBranch(ch, depth+1); });
    html += '</div>';
    return html;
  }
  let html = '<div style="flex:1;overflow-y:auto;padding:8px">';
  items.forEach(i => { html += renderBranch(i, 0); });
  html += `</div><div class="mm-input-wrap"><input class="mm-input" id="mmi-${node.id}" placeholder="New branch..." onkeydown="if(event.key==='Enter')mmAdd('${node.id}','${items[0]?.id}')"></div>`;
  body.innerHTML = html;
}

function mmAdd(nodeId, parentId) {
  const node = getNode(nodeId);
  const input = document.getElementById("mmi-"+nodeId);
  const text = input ? input.value.trim() : "";
  if (!text) return;
  function findAndAdd(items) {
    for (const item of items) {
      if (item.id === parentId) { item.children = item.children||[]; item.children.push({id:uid(),text,children:[]}); return true; }
      if (item.children && findAndAdd(item.children)) return true;
    }
    return false;
  }
  findAndAdd(node.mindmapData);
  if (input) input.value = "";
  renderNodeBody(node);
  saveState();
}

// ─── CONNECTIONS ─────────────────────────────────────────────────────
function toggleConnect(nodeId) {
  if (state.connecting && state.connecting !== nodeId) {
    // Complete connection
    const exists = state.connections.some(c => (c.from===state.connecting&&c.to===nodeId)||(c.from===nodeId&&c.to===state.connecting));
    if (!exists && state.connecting !== nodeId) {
      state.connections.push({id:uid(),from:state.connecting,to:nodeId});
    }
    state.connecting = null;
  } else if (state.connecting === nodeId) {
    state.connecting = null;
  } else {
    state.connecting = nodeId;
  }
  updateUI();
  saveState();
}

function renderConnections() {
  const paths = svg.querySelectorAll("g.conn");
  paths.forEach(p => p.remove());
  state.connections.forEach(conn => {
    const nf = getNode(conn.from), nt = getNode(conn.to);
    if (!nf || !nt) return;
    const fx = nf.x+nf.w/2, fy = nf.y+nf.h/2;
    const tx = nt.x+nt.w/2, ty = nt.y+nt.h/2;
    const dx = tx-fx;
    const g = document.createElementNS("http://www.w3.org/2000/svg","g");
    g.setAttribute("class","conn");
    g.innerHTML = `<path d="M${fx},${fy} C${fx+dx*0.4},${fy} ${tx-dx*0.4},${ty} ${tx},${ty}" stroke="url(#cg)" stroke-width="2" fill="none" stroke-dasharray="6 4" style="pointer-events:stroke;cursor:pointer"/>
      <circle cx="${fx}" cy="${fy}" r="4" fill="#6c5ce7" opacity="0.6"/>
      <circle cx="${tx}" cy="${ty}" r="4" fill="#6c5ce7" opacity="0.6"/>`;
    g.querySelector("path").addEventListener("click", () => {
      state.connections = state.connections.filter(c => c.id !== conn.id);
      updateUI(); saveState();
    });
    svg.appendChild(g);
  });
}

// ─── DRAG & RESIZE ───────────────────────────────────────────────────
function setupDrag(el, node) {
  const header = el.querySelector("[data-drag]");
  if (!header) return;
  let dragging = false, ox, oy;
  header.addEventListener("mousedown", (e) => {
    if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON") return;
    dragging = true;
    state.selected = node.id;
    const rect = wrap.getBoundingClientRect();
    ox = (e.clientX - rect.left) / state.zoom - state.pan.x - node.x;
    oy = (e.clientY - rect.top) / state.zoom - state.pan.y - node.y;
    updateUI();
  });
  window.addEventListener("mousemove", (e) => {
    if (!dragging) return;
    const rect = wrap.getBoundingClientRect();
    node.x = (e.clientX - rect.left) / state.zoom - state.pan.x - ox;
    node.y = (e.clientY - rect.top) / state.zoom - state.pan.y - oy;
    el.style.left = node.x + "px";
    el.style.top = node.y + "px";
    renderConnections();
  });
  window.addEventListener("mouseup", () => { if (dragging) { dragging = false; saveState(); } });
}

function setupResize(el, node) {
  const handle = el.querySelector("[data-resize]");
  if (!handle) return;
  let resizing = false, sx, sy, sw, sh;
  handle.addEventListener("mousedown", (e) => {
    e.stopPropagation();
    resizing = true; sx = e.clientX; sy = e.clientY; sw = node.w; sh = node.h;
  });
  window.addEventListener("mousemove", (e) => {
    if (!resizing) return;
    node.w = Math.max(240, sw + (e.clientX - sx) / state.zoom);
    node.h = Math.max(180, sh + (e.clientY - sy) / state.zoom);
    el.style.width = node.w + "px";
    el.style.height = node.h + "px";
    renderConnections();
  });
  window.addEventListener("mouseup", () => { if (resizing) { resizing = false; saveState(); } });
}

// ─── DROPZONE SETUP ──────────────────────────────────────────────────
function setupDropzone(id, accept, onFile) {
  const dz = document.getElementById(id);
  if (!dz) return;
  dz.addEventListener("dragover", (e) => { e.preventDefault(); e.stopPropagation(); dz.classList.add("over"); });
  dz.addEventListener("dragleave", (e) => { e.preventDefault(); e.stopPropagation(); dz.classList.remove("over"); });
  dz.addEventListener("drop", async (e) => {
    e.preventDefault(); e.stopPropagation(); dz.classList.remove("over");
    const f = e.dataTransfer?.files?.[0];
    if (f) { const data = await fileToBase64(f); onFile({data,name:f.name,type:f.type}); }
  });
  dz.addEventListener("click", () => {
    const inp = document.createElement("input");
    inp.type = "file"; inp.accept = accept;
    inp.onchange = async () => { if (inp.files[0]) { const data = await fileToBase64(inp.files[0]); onFile({data,name:inp.files[0].name,type:inp.files[0].type}); }};
    inp.click();
  });
}

// ─── UTILS ───────────────────────────────────────────────────────────
function cycleColor(nodeId) {
  const node = getNode(nodeId);
  node.colorIndex = ((node.colorIndex||0)+1) % COLORS.length;
  const el = document.getElementById(nodeId);
  const nc = COLORS[node.colorIndex];
  el.style.background = nc.bg;
  el.style.setProperty("--node-border", nc.border);
  el.querySelector(".color-dot").style.background = nc.border;
  if (state.selected === nodeId) el.style.borderColor = nc.border;
  saveState();
}

function escHtml(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }

function updateUI() {
  document.getElementById("stats").textContent = state.nodes.length + " nodes · " + state.connections.length + " links";
  const cm = document.getElementById("conn-msg");
  cm.style.display = state.connecting ? "inline" : "none";
  // Update selected state
  document.querySelectorAll(".node").forEach(el => {
    const n = getNode(el.id);
    if (!n) return;
    const nc = COLORS[n.colorIndex||0];
    if (state.selected === el.id) {
      el.classList.add("selected");
      el.style.borderColor = nc.border;
      el.style.zIndex = 100;
    } else {
      el.classList.remove("selected");
      el.style.borderColor = "var(--border)";
      el.style.zIndex = n.z || 1;
    }
    // Update connect button
    const cb = el.querySelector(".hdr-btn:not(.del)");
    if (cb) { cb.className = "hdr-btn" + (state.connecting===el.id?" active":""); }
  });
  renderConnections();
}

// ─── SAVE/LOAD ───────────────────────────────────────────────────────
let saveTimeout;
function saveState() {
  clearTimeout(saveTimeout);
  saveTimeout = setTimeout(() => {
    const data = {
      nodes: state.nodes.map(n => {
        const c = {...n};
        if (c.mediaData && c.mediaData.length > 500000) { c.mediaData = null; c._cleared = true; }
        if (c.imageData && c.imageData.length > 500000) { c.imageData = null; c._cleared = true; }
        return c;
      }),
      connections: state.connections,
      pan: state.pan,
      zoom: state.zoom
    };
    try { localStorage.setItem("flowmind-v1", JSON.stringify(data)); } catch(e) {}
  }, 600);
}

function loadState() {
  try {
    const raw = localStorage.getItem("flowmind-v1");
    if (!raw) return;
    const data = JSON.parse(raw);
    if (data.nodes && data.nodes.length > 0) {
      state.pan = data.pan || {x:0,y:0};
      state.zoom = data.zoom || 1;
      state.connections = data.connections || [];
      applyTransform();
      document.getElementById("zoom-label").textContent = Math.round(state.zoom*100)+"%";
      document.getElementById("welcome").style.display = "none";
      data.nodes.forEach(n => {
        state.nodes.push(n);
        renderNode(n);
      });
      updateUI();
    }
  } catch(e) {}
}

function clearAll() {
  state.nodes.forEach(n => { const el = document.getElementById(n.id); if (el) el.remove(); });
  state.nodes = []; state.connections = []; state.selected = null; state.connecting = null;
  state.pan = {x:0,y:0}; state.zoom = 1;
  applyTransform();
  document.getElementById("zoom-label").textContent = "100%";
  document.getElementById("welcome").style.display = "flex";
  updateUI();
  try { localStorage.removeItem("flowmind-v1"); } catch(e) {}
}

// ─── INIT ────────────────────────────────────────────────────────────
loadState();
</script>
</body>
</html>