<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import { extractHtml } from './lib/extract'

interface Msg {
  role: 'user' | 'assistant'
  content: string
}

const EXAMPLES = [
  '一个会转的彩色立方体，鼠标拖动可以转',
  '满屏的星空粒子，慢慢往前飞',
  '能用鼠标在画布上画发光线条',
  '一片随风起伏的彩虹色波浪',
  '点一下屏幕就放一个会弹跳的小球',
  '一个跟着鼠标转的霓虹甜甜圈',
]

const EXAMPLES_EN = [
  'A colorful 3D cube you can drag to spin',
  'A field of stars flying past, like hyperspace',
  'Draw glowing neon lines with the mouse',
  'Bouncing balls that spawn where you tap',
  'A rainbow torus that follows the cursor',
  'Ripples spreading out wherever you click',
]

const messages = ref<Msg[]>([])
const input = ref('')
const busy = ref(false)
const error = ref<string | null>(null)

// 生成计时器（让等待期一眼看出在工作，尤其 gemma 首字延迟那十几秒）
const genSeconds = ref(0)
let genTimer: number | undefined
function startTimer() {
  genSeconds.value = 0
  const t0 = Date.now()
  genTimer = window.setInterval(() => {
    genSeconds.value = Math.floor((Date.now() - t0) / 1000)
  }, 250)
}
function stopTimer() {
  if (genTimer !== undefined) {
    clearInterval(genTimer)
    genTimer = undefined
  }
}

// 流式累积的原始文本（代码 tab 实时显示），以及结束后提取的可运行 HTML
const code = ref('')
const runningHtml = ref('')
const tab = ref<'player' | 'code'>('player')
const iframeKey = ref(0)

const threadEl = ref<HTMLElement | null>(null)
const codeEl = ref<HTMLElement | null>(null)
const iframeEl = ref<HTMLIFrameElement | null>(null)

const canSend = computed(() => !busy.value && input.value.trim().length > 0)
const hasProgram = computed(() => runningHtml.value.length > 0)
const lineCount = computed(() => (code.value ? code.value.split('\n').length : 0))
// 还没吐出第一个字符（gemma 首字延迟 / thinking 阶段）—— 最容易被误以为坏了
const thinking = computed(() => busy.value && code.value.length === 0)
const genStatus = computed(() =>
  thinking.value ? `正在思考…（${genSeconds.value}s）` : `正在写代码…（${genSeconds.value}s · ${lineCount.value} 行）`,
)

watch([messages, busy], async () => {
  await nextTick()
  threadEl.value?.scrollTo({ top: threadEl.value.scrollHeight, behavior: 'smooth' })
}, { deep: true })

// 流式时让代码视图自动滚到底，像在“看它写代码”
watch(code, async () => {
  if (tab.value !== 'code') return
  await nextTick()
  codeEl.value?.scrollTo({ top: codeEl.value.scrollHeight })
})

function useExample(ex: string) {
  if (busy.value) return
  input.value = ex
  send()
}

async function send() {
  if (!canSend.value) return
  const prompt = input.value.trim()
  input.value = ''
  error.value = null
  messages.value.push({ role: 'user', content: prompt })
  busy.value = true
  startTimer()
  code.value = ''
  tab.value = 'code' // 切到代码 tab 看它一行行生成

  const payload: { prompt: string; current_code?: string } = { prompt }
  if (runningHtml.value) payload.current_code = runningHtml.value

  try {
    const res = await fetch('/api/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    })
    if (!res.ok || !res.body) {
      throw new Error((await res.text().catch(() => '')) || `HTTP ${res.status}`)
    }
    await consumeSse(res.body)
  } catch (e) {
    const msg = (e as Error).message
    error.value = msg
    messages.value.push({ role: 'assistant', content: `出错了：${msg}` })
  } finally {
    busy.value = false
    stopTimer()
  }
}

/** 读 fetch 流，解析 SSE 帧（event/data），驱动 code / runningHtml。 */
async function consumeSse(body: ReadableStream<Uint8Array>) {
  const reader = body.getReader()
  const decoder = new TextDecoder()
  let buf = ''
  for (;;) {
    const { value, done } = await reader.read()
    if (done) break
    buf += decoder.decode(value, { stream: true })
    let sep: number
    while ((sep = buf.indexOf('\n\n')) !== -1) {
      const frame = buf.slice(0, sep)
      buf = buf.slice(sep + 2)
      handleFrame(frame)
    }
  }
}

function handleFrame(frame: string) {
  let event = 'message'
  const dataLines: string[] = []
  for (const raw of frame.split('\n')) {
    const line = raw.replace(/\r$/, '')
    if (line.startsWith('event:')) event = line.slice(6).trim()
    else if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^ /, ''))
  }
  const data = dataLines.join('\n')
  if (event === 'error') {
    let msg = data
    try { msg = JSON.parse(data).message ?? data } catch { /* keep raw */ }
    error.value = msg
    messages.value.push({ role: 'assistant', content: `出错了：${msg}` })
    return
  }
  if (event === 'done') {
    finishGeneration()
    return
  }
  // 普通文本片段
  try {
    const t = JSON.parse(data).t
    if (typeof t === 'string') code.value += t
  } catch { /* 忽略坏帧 */ }
}

function finishGeneration() {
  const html = extractHtml(code.value)
  if (!html) {
    error.value = '生成的内容里没找到可运行的 HTML，再换个说法试试？'
    messages.value.push({ role: 'assistant', content: error.value })
    return
  }
  runningHtml.value = html
  iframeKey.value++
  tab.value = 'player'
  messages.value.push({ role: 'assistant', content: '✓ 生成好了，已经在运行 — 想改就接着说。' })
}

function rerun() {
  iframeKey.value++
}

function goFullscreen() {
  iframeEl.value?.requestFullscreen?.()
}

async function copyCode() {
  try {
    await navigator.clipboard.writeText(code.value)
  } catch { /* 不支持就算了 */ }
}

function downloadHtml() {
  const blob = new Blob([runningHtml.value || code.value], { type: 'text/html' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'webgl-program.html'
  a.click()
  URL.revokeObjectURL(url)
}

function onKeydown(e: KeyboardEvent) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault()
    send()
  }
}
</script>

<template>
  <div class="layout">
    <!-- 左侧：聊天 说需求 -->
    <aside class="sidebar">
      <header class="brand">
        <span class="logo" />
        <div>
          <strong>WebGL Studio</strong>
          <small>说一句，AI 现场生成</small>
        </div>
      </header>

      <div class="thread" ref="threadEl">
        <div v-if="messages.length === 0" class="welcome">
          <p>想做个什么小程序？用大白话说出来就行 👇</p>
          <div class="chips">
            <button v-for="ex in EXAMPLES" :key="ex" class="chip" :disabled="busy" @click="useExample(ex)">
              {{ ex }}
            </button>
          </div>
          <p class="grouplabel">English demos</p>
          <div class="chips">
            <button v-for="ex in EXAMPLES_EN" :key="ex" class="chip en" :disabled="busy" @click="useExample(ex)">
              {{ ex }}
            </button>
          </div>
        </div>

        <div v-for="(m, i) in messages" :key="i" :class="['bubble', m.role]">
          {{ m.content }}
        </div>

        <div v-if="busy" class="bubble assistant typing">
          <span /><span /><span /> {{ genStatus }}
        </div>
      </div>

      <footer class="composer">
        <textarea
          v-model="input"
          :disabled="busy"
          @keydown="onKeydown"
          rows="2"
          :placeholder="hasProgram ? '接着说想怎么改…（Enter 发送）' : '描述你想要的程序…（Enter 发送）'"
        />
        <button class="send" :disabled="!canSend" @click="send">{{ hasProgram ? '改' : '生成' }}</button>
      </footer>
    </aside>

    <!-- 右侧：主区 tabs -->
    <main class="main">
      <nav class="tabs">
        <button :class="{ active: tab === 'player' }" @click="tab = 'player'">▶ 运行</button>
        <button :class="{ active: tab === 'code' }" @click="tab = 'code'">
          &lt;/&gt; 代码<span v-if="lineCount" class="count">{{ lineCount }} 行</span>
        </button>
        <div class="spacer" />
        <template v-if="tab === 'player' && hasProgram">
          <button class="ghost" @click="rerun" title="重新运行">↻ 重跑</button>
          <button class="ghost" @click="goFullscreen" title="全屏">⛶ 全屏</button>
        </template>
        <template v-else-if="tab === 'code' && code">
          <button class="ghost" @click="copyCode">⧉ 复制</button>
          <button class="ghost" @click="downloadHtml">↓ 下载</button>
        </template>
      </nav>

      <section class="stage">
        <!-- Player -->
        <div v-show="tab === 'player'" class="player">
          <iframe
            v-if="hasProgram"
            :key="iframeKey"
            ref="iframeEl"
            :srcdoc="runningHtml"
            sandbox="allow-scripts allow-pointer-lock"
            allow="fullscreen; accelerometer; gyroscope; xr-spatial-tracking"
            title="生成的 WebGL 程序"
          />
          <div v-else class="placeholder">
            <template v-if="busy">
              <div class="spinner" />
              <p class="status">{{ genStatus }}</p>
              <p class="sub">写完会自动在这里跑起来 · 切到 <b>&lt;/&gt; 代码</b> 看 AI 实时写 →</p>
            </template>
            <template v-else>
              <div class="big">▶</div>
              <p>左边说个需求，生成好的程序会在这里运行。</p>
            </template>
          </div>
        </div>

        <!-- Code -->
        <div v-show="tab === 'code'" class="codewrap" ref="codeEl">
          <pre v-if="code" class="code"><code>{{ code }}</code><span v-if="busy" class="caret" /></pre>
          <div v-else class="placeholder small">
            <template v-if="busy">
              <div class="spinner" />
              <p class="status">{{ genStatus }}</p>
            </template>
            <p v-else>这里会实时显示 AI 写的代码。</p>
          </div>
        </div>
      </section>

      <div v-if="error" class="errorbar">{{ error }}</div>
    </main>
  </div>
</template>

<style scoped>
.layout {
  display: grid;
  grid-template-columns: 360px 1fr;
  height: 100vh;
  height: 100dvh;
}

/* ---- sidebar ---- */
.sidebar {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--bg-soft);
  border-right: 1px solid var(--border);
}
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.logo {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  box-shadow: 0 0 14px rgba(124, 58, 237, 0.5);
}
.brand strong { display: block; font-size: 1rem; }
.brand small { color: var(--fg-dim); font-size: 0.78rem; }

.thread {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.welcome { color: var(--fg-dim); font-size: 0.9rem; }
.welcome p { margin-bottom: 12px; }
.grouplabel {
  margin-top: 16px;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-dim);
  opacity: 0.75;
}
.chips { display: flex; flex-direction: column; gap: 8px; }
.chip.en { font-style: italic; }
.chip {
  text-align: left;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  color: var(--fg);
  border-radius: 10px;
  padding: 9px 12px;
  font-size: 0.86rem;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, transform 0.1s;
}
.chip:hover:not(:disabled) { background: rgba(124, 58, 237, 0.16); border-color: var(--accent); transform: translateX(2px); }
.chip:disabled { opacity: 0.5; cursor: default; }

.bubble {
  max-width: 92%;
  padding: 9px 12px;
  border-radius: 12px;
  line-height: 1.45;
  font-size: 0.9rem;
  white-space: pre-wrap;
  word-break: break-word;
}
.bubble.user { align-self: flex-end; background: linear-gradient(135deg, var(--accent), #4f46e5); color: #fff; }
.bubble.assistant { align-self: flex-start; background: rgba(255, 255, 255, 0.07); border: 1px solid var(--border); }
.bubble.typing { display: inline-flex; align-items: center; gap: 5px; color: var(--fg-dim); }
.bubble.typing span {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent-2); animation: bounce 1.2s infinite;
}
.bubble.typing span:nth-child(2) { animation-delay: 0.15s; }
.bubble.typing span:nth-child(3) { animation-delay: 0.3s; }
@keyframes bounce { 0%,60%,100% { transform: translateY(0); opacity: 0.5; } 30% { transform: translateY(-5px); opacity: 1; } }

.composer {
  display: flex;
  gap: 8px;
  padding: 12px;
  border-top: 1px solid var(--border);
  align-items: flex-end;
}
.composer textarea {
  flex: 1;
  resize: none;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 9px 11px;
  color: inherit;
  font: inherit;
  font-size: 0.9rem;
  line-height: 1.4;
}
.composer textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.send {
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: #fff; border: none; border-radius: 10px;
  padding: 11px 18px; font-weight: 700; cursor: pointer; white-space: nowrap;
}
.send:disabled { background: rgba(255, 255, 255, 0.1); color: var(--fg-dim); cursor: not-allowed; }

/* ---- main ---- */
.main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.tabs {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}
.tabs > button {
  background: transparent;
  border: 1px solid transparent;
  color: var(--fg-dim);
  padding: 7px 14px;
  border-radius: 8px;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
}
.tabs > button.active { background: rgba(255, 255, 255, 0.07); color: var(--fg); border-color: var(--border); }
.tabs .count {
  margin-left: 6px;
  font-size: 0.72rem;
  color: var(--fg-dim);
  font-weight: 500;
}
.spacer { flex: 1; }
.ghost {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--fg-dim);
  padding: 6px 11px;
  border-radius: 8px;
  font-size: 0.82rem;
  cursor: pointer;
}
.ghost:hover { color: var(--fg); border-color: var(--accent); }

.stage { flex: 1; min-height: 0; position: relative; }
.player, .codewrap { position: absolute; inset: 0; }
.player iframe { width: 100%; height: 100%; border: 0; display: block; background: #000; }

.codewrap { overflow: auto; background: #0a0c12; }
.code {
  margin: 0;
  padding: 16px 18px;
  font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Menlo, Consolas, monospace;
  font-size: 0.82rem;
  line-height: 1.55;
  color: #c8d3e8;
  white-space: pre-wrap;
  word-break: break-word;
  tab-size: 2;
}
.caret {
  display: inline-block;
  width: 8px; height: 1.05em;
  vertical-align: text-bottom;
  background: var(--accent-2);
  animation: blink 1s steps(2) infinite;
}
@keyframes blink { 50% { opacity: 0; } }

.placeholder {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; color: var(--fg-dim); text-align: center; padding: 24px;
}
.placeholder .big { font-size: 3.2rem; opacity: 0.35; }
.placeholder.small { position: static; height: 100%; }
.placeholder .status { color: var(--fg); font-size: 1rem; font-weight: 600; }
.placeholder .sub { font-size: 0.82rem; opacity: 0.8; }
.placeholder .sub b { color: var(--fg); }
.spinner {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-right-color: var(--accent-2);
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.errorbar {
  padding: 9px 14px;
  background: rgba(251, 113, 133, 0.14);
  border-top: 1px solid var(--rose);
  color: var(--rose);
  font-size: 0.85rem;
}

@media (max-width: 860px) {
  .layout { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
  .sidebar { order: 2; border-right: none; border-top: 1px solid var(--border); max-height: 46vh; }
  .main { order: 1; }
}
</style>
