{% extends "base.html" %}
{% block title %}VeoForge - Batch Image-to-Video{% endblock %}
{% block head %}
<style>
    [x-cloak] { display: none !important; }
    .vf-scrollbar::-webkit-scrollbar { height: 8px; width: 8px; }
    .vf-scrollbar::-webkit-scrollbar-track { background: transparent; }
    .vf-scrollbar::-webkit-scrollbar-thumb { background: rgba(148, 163, 184, 0.3); border-radius: 999px; }
    .vf-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(148, 163, 184, 0.5); }

    .status-dot {
        width: 10px;
        height: 10px;
        border-radius: 50%;
        display: inline-block;
        flex-shrink: 0;
    }
    .dot-idle { background-color: #6b7280; }
    .dot-active { background-color: #f59e0b; animation: pulse-amber 1.5s ease-in-out infinite; }
    .dot-done { background-color: #10b981; }
    .dot-error { background-color: #ef4444; }

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

    .dir-item:hover { background-color: rgba(59, 130, 246, 0.1); }
</style>
{% endblock %}

{% block body %}
<div
    id="app"
    class="flex h-screen bg-gray-950 text-white"
    x-data="veoforge()"
    x-init="init()"
>
    <!-- LOGIN OVERLAY -->
    <div x-show="!authenticated" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-gray-950/95">
        <div class="w-full max-w-sm rounded-2xl border border-gray-800 bg-gray-900 p-8">
            <h2 class="text-xl font-semibold text-center mb-6">VeoForge Login</h2>
            <div class="space-y-4">
                <div>
                    <label class="block text-xs text-gray-400 mb-1">Username</label>
                    <input type="text" x-model="loginUsername"
                           @keydown.enter="doLogin()"
                           class="w-full rounded-xl border border-gray-700 bg-gray-950 px-3 py-2 text-sm text-white focus:border-blue-500 focus:outline-none"
                           placeholder="Enter username" autofocus />
                </div>
                <div>
                    <label class="block text-xs text-gray-400 mb-1">Password</label>
                    <input type="password" x-model="loginPassword"
                           @keydown.enter="doLogin()"
                           class="w-full rounded-xl border border-gray-700 bg-gray-950 px-3 py-2 text-sm text-white focus:border-blue-500 focus:outline-none"
                           placeholder="Enter password" />
                </div>
                <p x-show="loginError" x-text="loginError" class="text-xs text-red-400"></p>
                <button @click="doLogin()" :disabled="loginLoading"
                        class="w-full rounded-xl bg-blue-600 py-2 text-sm font-semibold text-white hover:bg-blue-500 disabled:bg-gray-700 disabled:text-gray-400">
                    <span x-text="loginLoading ? 'Logging in...' : 'Login'"></span>
                </button>
            </div>
        </div>
    </div>

    <!-- LEFT PANEL -->
    <main class="flex min-w-0 flex-1 flex-col overflow-hidden">
        <!-- Header -->
        <section class="border-b border-gray-800 px-5 py-4">
            <div class="flex items-center justify-between">
                <div>
                    <h1 class="text-xl font-semibold tracking-tight">VeoForge</h1>
                    <p class="mt-1 text-sm text-gray-400">Batch Image-to-Video with Kunavo Veo 3</p>
                </div>
                <div class="flex items-center gap-3">
                    <span x-show="authenticated" class="text-xs text-gray-500" x-text="'Hi, ' + displayName"></span>
                    <button x-show="authenticated" @click="doLogout()" class="text-xs text-gray-500 hover:text-gray-300 underline">Logout</button>
                    <div class="rounded-full border border-gray-800 px-3 py-1 text-xs text-gray-400">
                        veo-3 / 9:16
                    </div>
                </div>
            </div>
        </section>

        <!-- Directory Input -->
        <section class="border-b border-gray-800 px-5 py-4">
            <label class="block text-xs font-medium uppercase tracking-widest text-gray-500 mb-2">Source Directory</label>
            <div class="flex gap-2">
                <input
                    type="text"
                    x-model="inputDir"
                    placeholder="/Users/andy/Downloads/products"
                    class="flex-1 rounded-xl border border-gray-800 bg-gray-900 px-4 py-2.5 text-sm text-white outline-none transition focus:border-blue-500 placeholder-gray-600"
                />
                <button
                    type="button"
                    class="rounded-xl border border-gray-700 bg-gray-900 px-4 py-2.5 text-sm font-medium text-gray-200 transition hover:border-gray-600 hover:bg-gray-800"
                    @click="openBrowser('inputDir')"
                >
                    Browse
                </button>
                <button
                    type="button"
                    class="rounded-xl border border-gray-700 bg-gray-900 px-4 py-2.5 text-sm font-medium text-gray-200 transition hover:border-gray-600 hover:bg-gray-800 disabled:opacity-50"
                    :disabled="isScanning || !inputDir.trim()"
                    @click="scanDir()"
                >
                    <span x-text="isScanning ? 'Scanning...' : 'Scan'"></span>
                </button>
                <button
                    type="button"
                    class="rounded-xl border border-gray-700 bg-gray-900 px-4 py-2.5 text-sm font-medium text-gray-200 transition hover:border-gray-600 hover:bg-gray-800 disabled:opacity-50"
                    :disabled="!inputDir.trim()"
                    @click="openDir(inputDir)"
                    title="在访达中打开该目录"
                >
                    打开
                </button>
            </div>
            <div x-show="scanError" x-cloak class="mt-2 text-xs text-rose-400" x-text="scanError"></div>
        </section>

        <!-- Image Thumbnails -->
        <section class="flex-1 overflow-y-auto border-b border-gray-800 px-5 py-4 vf-scrollbar">
            <template x-if="!imageGroups.length">
                <div class="rounded-2xl border border-dashed border-gray-800 bg-gray-900/50 p-8 text-center text-sm text-gray-500">
                    Enter a directory path and click Scan to load images.
                </div>
            </template>

            <template x-for="group in imageGroups" :key="group.folder">
                <div class="mb-4">
                    <div class="flex items-center gap-2 mb-2">
                        <span class="text-sm font-medium text-gray-300" x-text="group.folder === '.' ? 'Root' : group.folder"></span>
                        <span class="rounded-full bg-gray-800 px-2 py-0.5 text-[11px] text-gray-400" x-text="group.count + ' images'"></span>
                    </div>
                    <div class="flex flex-wrap gap-2">
                        <template x-for="img in group.images" :key="img.path">
                            <div class="w-16 h-16 rounded-lg overflow-hidden border border-gray-800 bg-gray-900 hover:border-gray-600 transition cursor-default" :title="img.name">
                                <img
                                    :src="'/api/thumbnail?path=' + encodeURIComponent(img.path)"
                                    :alt="img.name"
                                    class="w-full h-full object-cover"
                                    loading="lazy"
                                />
                            </div>
                        </template>
                    </div>
                </div>
            </template>
        </section>

        <!-- Controls -->
        <section class="border-t border-gray-800 bg-gray-950/95 px-5 py-4">
            <div class="rounded-2xl border border-gray-800 bg-gray-900/80 p-4">
                <!-- Prompt -->
                <label class="block text-xs font-medium uppercase tracking-widest text-gray-500 mb-2">Prompt</label>
                <textarea
                    x-model="prompt"
                    rows="4"
                    class="w-full rounded-xl border border-gray-800 bg-gray-950 px-4 py-3 text-sm leading-6 text-white outline-none transition focus:border-blue-500 resize-y placeholder-gray-600"
                    placeholder="Describe the video style you want..."
                ></textarea>

                <!-- Parameters -->
                <div class="mt-3 flex flex-wrap items-end gap-4">
                    <label class="flex flex-col gap-1 text-xs text-gray-400">
                        Videos per image
                        <input
                            type="number"
                            min="1"
                            step="1"
                            x-model.number="videosPerImage"
                            class="w-24 rounded-xl border border-gray-800 bg-gray-950 px-3 py-2 text-sm text-white"
                        />
                    </label>

                </div>

                <!-- Action Buttons -->
                <div class="mt-4 flex items-center gap-3">
                    <button
                        type="button"
                        class="inline-flex h-10 items-center justify-center rounded-xl bg-blue-600 px-5 text-sm font-semibold text-white transition hover:bg-blue-500 disabled:cursor-not-allowed disabled:bg-gray-700 disabled:text-gray-400"
                        :disabled="isSubmitting || !imageGroups.length"
                        @click="startRun()"
                    >
                        <span x-text="isSubmitting ? 'Starting...' : 'Start Generation'"></span>
                    </button>

                    <button
                        type="button"
                        class="inline-flex h-10 items-center justify-center rounded-xl border border-amber-600/50 bg-amber-600/10 px-4 text-sm font-medium text-amber-300 transition hover:bg-amber-600/20 disabled:opacity-40 disabled:cursor-not-allowed"
                        :disabled="!canRetry"
                        @click="retryFailed()"
                    >
                        Retry Failed
                    </button>

                    <button
                        type="button"
                        class="inline-flex h-10 items-center justify-center rounded-xl border border-rose-600/50 bg-rose-600/10 px-4 text-sm font-medium text-rose-300 transition hover:bg-rose-600/20 disabled:opacity-40 disabled:cursor-not-allowed"
                        :disabled="!engineRunning"
                        @click="stopRun()"
                    >
                        Stop
                    </button>

                    <div class="ml-auto text-xs text-gray-500" x-show="totalImages > 0">
                        <span x-text="totalImages"></span> images x <span x-text="videosPerImage"></span> = <span x-text="totalImages * videosPerImage"></span> tasks
                    </div>
                </div>

                <!-- Status Message -->
                <div x-show="statusMsg" x-cloak class="mt-3 rounded-xl border border-gray-800 bg-gray-950 px-3 py-2 text-xs text-gray-400" x-text="statusMsg"></div>
            </div>
        </section>
    </main>

    <!-- Directory Browser Modal -->
    <template x-if="browserOpen">
        <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60" @click.self="browserOpen = false" @keydown.escape.window="browserOpen = false">
            <div class="w-full max-w-lg rounded-2xl border border-gray-700 bg-gray-900 shadow-2xl flex flex-col" style="max-height: 70vh;">
                <!-- Header -->
                <div class="flex items-center justify-between border-b border-gray-800 px-5 py-3">
                    <h3 class="text-sm font-semibold text-gray-200">Select Directory</h3>
                    <button @click="browserOpen = false" class="text-gray-500 hover:text-gray-300 text-lg leading-none">&times;</button>
                </div>

                <!-- Current path -->
                <div class="border-b border-gray-800 px-5 py-2">
                    <div class="flex items-center gap-2">
                        <span class="text-[11px] uppercase tracking-widest text-gray-500 shrink-0">Path</span>
                        <span class="truncate text-sm text-gray-300" x-text="browserCurrent"></span>
                    </div>
                    <div x-show="browserError" x-cloak class="mt-1 text-xs text-rose-400" x-text="browserError"></div>
                </div>

                <!-- Directory list -->
                <div class="flex-1 overflow-y-auto vf-scrollbar px-3 py-2" style="min-height: 200px;">
                    <div x-show="browserLoading" class="py-8 text-center text-sm text-gray-500">Loading...</div>

                    <template x-if="!browserLoading">
                        <div>
                            <!-- Go up -->
                            <template x-if="browserParent !== null">
                                <button
                                    type="button"
                                    class="dir-item flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-blue-400 transition"
                                    @click="browseTo(browserParent)"
                                >
                                    <span class="text-base">&#8593;</span> ..
                                </button>
                            </template>

                            <template x-if="browserDirs.length === 0 && !browserLoading">
                                <div class="py-4 text-center text-xs text-gray-500">No subdirectories</div>
                            </template>

                            <template x-for="d in browserDirs" :key="d">
                                <button
                                    type="button"
                                    class="dir-item flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-gray-200 transition"
                                    @click="browseTo(browserCurrent + '/' + d)"
                                >
                                    <span class="text-gray-500">&#128193;</span>
                                    <span x-text="d"></span>
                                </button>
                            </template>
                        </div>
                    </template>
                </div>

                <!-- Footer -->
                <div class="flex items-center justify-end gap-2 border-t border-gray-800 px-5 py-3">
                    <button
                        type="button"
                        class="rounded-xl border border-gray-700 bg-gray-800 px-4 py-2 text-sm text-gray-300 transition hover:bg-gray-700"
                        @click="browserOpen = false"
                    >
                        Cancel
                    </button>
                    <button
                        type="button"
                        class="rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-blue-500"
                        @click="selectBrowserDir()"
                    >
                        Select
                    </button>
                </div>
            </div>
        </div>
    </template>

    <!-- Generation History Modal -->
    <template x-if="historyOpen">
        <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60" @click.self="historyOpen = false" @keydown.escape.window="historyOpen = false">
            <div class="w-full max-w-2xl rounded-2xl border border-gray-700 bg-gray-900 shadow-2xl flex flex-col" style="max-height: 75vh;">
                <div class="flex items-center justify-between border-b border-gray-800 px-5 py-3">
                    <h3 class="text-sm font-semibold text-gray-200">生成历史</h3>
                    <button @click="historyOpen = false" class="text-gray-500 hover:text-gray-300 text-lg leading-none">&times;</button>
                </div>
                <div class="flex-1 overflow-y-auto vf-scrollbar px-4 py-3" style="min-height: 200px;">
                    <div x-show="historyLoading" class="py-8 text-center text-sm text-gray-500">加载中...</div>
                    <template x-if="!historyLoading && !history.length">
                        <div class="py-8 text-center text-sm text-gray-500">暂无生成记录。</div>
                    </template>
                    <div class="space-y-2">
                        <template x-for="run in history" :key="run.id">
                            <div class="rounded-xl border border-gray-800 bg-gray-950 px-4 py-3">
                                <div class="flex items-center justify-between gap-3">
                                    <div class="min-w-0">
                                        <div class="flex items-center gap-2">
                                            <span class="text-xs text-gray-500" x-text="fmtTime(run.created_at)"></span>
                                            <span class="rounded-full px-2 py-0.5 text-[10px] font-medium"
                                                  :class="{
                                                      'bg-amber-500/20 text-amber-300': run.status === 'running',
                                                      'bg-emerald-500/20 text-emerald-300': run.status === 'completed',
                                                      'bg-rose-500/20 text-rose-300': run.status === 'failed',
                                                      'bg-gray-800 text-gray-400': !['running','completed','failed'].includes(run.status),
                                                  }" x-text="run.status"></span>
                                        </div>
                                        <div class="mt-1 truncate text-sm text-gray-300" x-text="run.input_dir" :title="run.input_dir"></div>
                                        <div class="mt-1 text-xs text-gray-500">
                                            <span class="text-emerald-400" x-text="run.completed"></span>
                                            <span> / </span>
                                            <span x-text="run.total"></span>
                                            <span> 完成</span>
                                            <template x-if="run.failed > 0">
                                                <span class="text-rose-400" x-text="' · ' + run.failed + ' 失败'"></span>
                                            </template>
                                        </div>
                                    </div>
                                    <button @click="openDir(run.input_dir)"
                                            class="shrink-0 rounded-lg border border-gray-700 bg-gray-900 px-3 py-1.5 text-xs text-gray-200 transition hover:bg-gray-800">
                                        打开目录
                                    </button>
                                </div>
                            </div>
                        </template>
                    </div>
                </div>
            </div>
        </div>
    </template>

    <!-- RIGHT PANEL -->
    <aside class="w-[26rem] shrink-0 border-l border-gray-800 bg-gray-950/90 overflow-y-auto vf-scrollbar">
        <div class="p-5">
            <!-- Summary Stats -->
            <div class="mb-4">
                <div class="flex items-center justify-between">
                    <h2 class="text-sm font-semibold uppercase tracking-widest text-gray-400">Run Summary</h2>
                    <button @click="openHistory()" class="text-xs text-gray-400 underline transition hover:text-white">历史</button>
                </div>
                <div x-show="currentRun" x-cloak class="mt-1 text-xs text-gray-500">
                    Run: <span x-text="currentRun ? currentRun.id.slice(0, 8) : ''"></span>
                    <span class="ml-2 rounded-full px-2 py-0.5 text-[10px] font-medium"
                          :class="{
                              'bg-amber-500/20 text-amber-300': currentRun && currentRun.status === 'running',
                              'bg-emerald-500/20 text-emerald-300': currentRun && currentRun.status === 'completed',
                              'bg-rose-500/20 text-rose-300': currentRun && currentRun.status === 'failed',
                              'bg-gray-800 text-gray-400': currentRun && !['running','completed','failed'].includes(currentRun.status),
                          }"
                          x-text="currentRun ? currentRun.status : ''"></span>
                </div>
            </div>

            <div class="grid grid-cols-2 gap-3 mb-5">
                <div class="rounded-2xl border border-gray-800 bg-gray-900/80 p-3">
                    <div class="text-[11px] uppercase tracking-widest text-gray-500">Total</div>
                    <div class="mt-1 text-2xl font-semibold text-white" x-text="summary.total || 0"></div>
                </div>
                <div class="rounded-2xl border border-gray-800 bg-gray-900/80 p-3">
                    <div class="text-[11px] uppercase tracking-widest text-gray-500">Completed</div>
                    <div class="mt-1 text-2xl font-semibold text-emerald-400" x-text="summary.completed || 0"></div>
                </div>
                <div class="rounded-2xl border border-gray-800 bg-gray-900/80 p-3">
                    <div class="text-[11px] uppercase tracking-widest text-gray-500">Failed</div>
                    <div class="mt-1 text-2xl font-semibold text-rose-400" x-text="summary.failed || 0"></div>
                </div>
                <div class="rounded-2xl border border-gray-800 bg-gray-900/80 p-3">
                    <div class="text-[11px] uppercase tracking-widest text-gray-500">ETA</div>
                    <div class="mt-1 text-2xl font-semibold text-gray-300" x-text="summary.eta || '--'"></div>
                </div>
            </div>

            <!-- Speed + Elapsed -->
            <div class="flex gap-3 mb-5">
                <div class="flex-1 rounded-2xl border border-gray-800 bg-gray-900/80 p-3">
                    <div class="text-[11px] uppercase tracking-widest text-gray-500">Elapsed</div>
                    <div class="mt-1 text-lg font-medium text-gray-300" x-text="summary.elapsed || '--'"></div>
                </div>
                <div class="flex-1 rounded-2xl border border-gray-800 bg-gray-900/80 p-3">
                    <div class="text-[11px] uppercase tracking-widest text-gray-500">Speed</div>
                    <div class="mt-1 text-lg font-medium text-gray-300" x-text="summary.speed || '--'"></div>
                </div>
            </div>

            <!-- Progress Bar -->
            <div x-show="summary.total > 0" x-cloak class="mb-5">
                <div class="flex items-center justify-between text-xs text-gray-500 mb-1">
                    <span>Progress</span>
                    <span x-text="summary.total > 0 ? Math.round(((summary.completed || 0) / summary.total) * 100) + '%' : '0%'"></span>
                </div>
                <div class="h-2 rounded-full bg-gray-800 overflow-hidden">
                    <div
                        class="h-full rounded-full bg-gradient-to-r from-blue-600 to-emerald-500 transition-all duration-500"
                        :style="'width: ' + (summary.total > 0 ? ((summary.completed || 0) / summary.total * 100) : 0) + '%'"
                    ></div>
                </div>
            </div>

            <!-- API Key Status -->
            <div class="rounded-2xl border border-gray-800 bg-gray-900/80 p-4">
                <div class="flex items-center justify-between mb-3">
                    <h3 class="text-sm font-semibold uppercase tracking-widest text-gray-400">API Keys</h3>
                    <span class="text-[11px] text-gray-500" x-text="keyStatuses.length + ' keys'"></span>
                </div>

                <template x-if="!keyStatuses.length">
                    <div class="text-xs text-gray-500 py-2">No keys loaded — sign in to lease keys from the server.</div>
                </template>

                <div class="space-y-2">
                    <template x-for="key in keyStatuses" :key="key.index">
                        <div class="flex items-center gap-3 rounded-xl border border-gray-800 bg-gray-950 px-3 py-2.5 transition"
                             :class="{
                                 'border-amber-500/30': key.status === 'active',
                                 'border-emerald-500/30': key.status === 'done',
                                 'border-rose-500/30': key.status === 'error',
                             }"
                        >
                            <span class="status-dot"
                                  :class="{
                                      'dot-idle': key.status === 'idle',
                                      'dot-active': key.status === 'active',
                                      'dot-done': key.status === 'done',
                                      'dot-error': key.status === 'error',
                                  }"></span>
                            <span class="text-sm font-medium text-gray-200 w-12" x-text="key.label"></span>
                            <div class="flex-1 text-right">
                                <span class="text-sm font-medium text-white" x-text="key.done"></span>
                                <span class="text-sm text-gray-500"> / </span>
                                <span class="text-sm text-gray-400" x-text="key.total"></span>
                            </div>
                            <template x-if="key.failed > 0">
                                <span class="rounded-full bg-rose-500/20 px-2 py-0.5 text-[10px] font-medium text-rose-300"
                                      x-text="key.failed + ' fail'"></span>
                            </template>
                        </div>
                    </template>
                </div>
            </div>

            <!-- Legend -->
            <div class="mt-4 flex flex-wrap gap-x-4 gap-y-1 text-[11px] text-gray-500">
                <span class="flex items-center gap-1.5"><span class="status-dot dot-idle"></span> Idle</span>
                <span class="flex items-center gap-1.5"><span class="status-dot dot-active"></span> Active</span>
                <span class="flex items-center gap-1.5"><span class="status-dot dot-done"></span> Done</span>
                <span class="flex items-center gap-1.5"><span class="status-dot dot-error"></span> Failed</span>
            </div>
        </div>
    </aside>
</div>

<script>
function veoforge() {
    return {
        // Auth state
        authenticated: false,
        displayName: '',
        loginUsername: '',
        loginPassword: '',
        loginError: '',
        loginLoading: false,

        // Left panel state
        inputDir: '',
        prompt: 'Based on this product image, generate a realistic short video showcasing the product with smooth camera movement and natural lighting. The product should be the focus, placed on a clean surface with a minimalist background. 9:16 vertical format.',
        videosPerImage: 1,
        imageGroups: [],
        totalImages: 0,
        isScanning: false,
        isSubmitting: false,
        scanError: '',
        statusMsg: '',

        // Directory browser state
        browserOpen: false,
        browserTarget: '',       // 'inputDir'
        browserCurrent: '',
        browserParent: null,
        browserDirs: [],
        browserLoading: false,
        browserError: '',

        // Right panel state
        currentRun: null,
        summary: { total: 0, completed: 0, failed: 0, pending: 0, active: 0, eta: '--', elapsed: '--', speed: '--' },
        keyStatuses: {{ initial_keys_json|safe }},
        engineRunning: false,
        pollHandle: null,
        historyOpen: false,
        history: [],
        historyLoading: false,

        get canRetry() {
            return this.currentRun && (this.summary.failed || 0) > 0 && !this.engineRunning;
        },

        async doLogin() {
            if (!this.loginUsername || !this.loginPassword) return;
            this.loginLoading = true;
            this.loginError = '';
            try {
                const resp = await fetch('/api/auth/login', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ username: this.loginUsername, password: this.loginPassword }),
                });
                let data;
                try { data = await resp.json(); } catch { data = {}; }
                if (!resp.ok) {
                    this.loginError = data.detail || `服务器错误 (${resp.status})`;
                    return;
                }
                this.authenticated = true;
                this.displayName = data.display_name || this.loginUsername;
                this.loginPassword = '';
            } catch (e) {
                this.loginError = 'Network error: ' + e.message;
            } finally {
                this.loginLoading = false;
            }
        },

        async doLogout() {
            try { await fetch('/api/auth/logout', { method: 'POST' }); } catch(e) {}
            this.authenticated = false;
            this.displayName = '';
        },

        async checkAuth() {
            try {
                const resp = await fetch('/api/auth/verify');
                if (resp.ok) {
                    const data = await resp.json();
                    this.authenticated = true;
                    this.displayName = data.username || '';
                }
            } catch(e) {}
        },

        async init() {
            await this.checkAuth();
            await this.loadCurrentRun();
            this.startPolling();
        },

        startPolling() {
            this.stopPolling();
            this.pollHandle = window.setInterval(() => {
                this.loadCurrentRun();
            }, 2000);
            window.addEventListener('beforeunload', () => this.stopPolling(), { once: true });
        },

        stopPolling() {
            if (this.pollHandle) {
                window.clearInterval(this.pollHandle);
                this.pollHandle = null;
            }
        },

        async scanDir() {
            if (!this.inputDir.trim()) return;
            this.isScanning = true;
            this.scanError = '';
            this.imageGroups = [];
            this.totalImages = 0;

            try {
                const resp = await fetch('/api/scan?dir=' + encodeURIComponent(this.inputDir.trim()));
                const data = await resp.json();
                if (!resp.ok) {
                    this.scanError = data.detail || 'Scan failed';
                    return;
                }
                this.imageGroups = data.groups || [];
                this.totalImages = data.total || 0;
                this.statusMsg = `Found ${this.totalImages} images in ${this.imageGroups.length} folder(s).`;
            } catch (e) {
                this.scanError = 'Network error: ' + e.message;
            } finally {
                this.isScanning = false;
            }
        },

        async startRun() {
            if (this.isSubmitting || !this.imageGroups.length) return;
            this.isSubmitting = true;
            this.statusMsg = '';

            try {
                const resp = await fetch('/api/runs', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        input_dir: this.inputDir.trim(),
                        prompt: this.prompt,
                        videos_per_image: this.videosPerImage,
                    }),
                });
                const data = await resp.json();
                if (!resp.ok) {
                    this.statusMsg = data.detail || 'Failed to start run';
                    return;
                }
                this.statusMsg = `Run started: ${data.total_tasks} tasks across ${data.num_keys} keys.`;
                await this.loadCurrentRun();
            } catch (e) {
                this.statusMsg = 'Network error: ' + e.message;
            } finally {
                this.isSubmitting = false;
            }
        },

        async retryFailed() {
            try {
                const resp = await fetch('/api/runs/retry', { method: 'POST' });
                const data = await resp.json();
                this.statusMsg = data.message || 'Retry requested';
                await this.loadCurrentRun();
            } catch (e) {
                this.statusMsg = 'Retry failed: ' + e.message;
            }
        },

        async stopRun() {
            try {
                const resp = await fetch('/api/runs/stop', { method: 'POST' });
                const data = await resp.json();
                this.statusMsg = data.message || 'Stop requested';
                await this.loadCurrentRun();
            } catch (e) {
                this.statusMsg = 'Stop failed: ' + e.message;
            }
        },

        async openBrowser(target) {
            this.browserTarget = target;
            this.browserError = '';
            this.browserOpen = true;
            const startPath = this[target] ? this[target].trim() : '';
            await this.browseTo(startPath || '');
        },

        async browseTo(path) {
            this.browserLoading = true;
            this.browserError = '';
            try {
                const resp = await fetch('/api/browse?path=' + encodeURIComponent(path));
                const data = await resp.json();
                if (!resp.ok) {
                    this.browserError = data.detail || 'Browse failed';
                    this.browserLoading = false;
                    return;
                }
                this.browserCurrent = data.current;
                this.browserParent = data.parent;
                this.browserDirs = data.dirs;
            } catch (e) {
                this.browserError = 'Network error: ' + e.message;
            } finally {
                this.browserLoading = false;
            }
        },

        selectBrowserDir() {
            if (this.browserCurrent) {
                this[this.browserTarget] = this.browserCurrent;
            }
            this.browserOpen = false;
        },

        async loadCurrentRun() {
            try {
                const resp = await fetch('/api/runs/current');
                if (!resp.ok) return;
                const data = await resp.json();
                this.currentRun = data.run;
                this.summary = data.summary || this.summary;
                this.keyStatuses = data.keys || [];
                this.engineRunning = data.engine_running || false;
            } catch (e) {
                // Silently fail on polling errors
            }
        },

        fmtTime(s) {
            if (!s) return '';
            const d = new Date(s.replace(' ', 'T') + 'Z');
            if (isNaN(d.getTime())) return s;
            const p = n => String(n).padStart(2, '0');
            return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
        },

        async openDir(path) {
            if (!path || !path.trim()) return;
            try {
                const resp = await fetch('/api/open-dir', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ path: path.trim() }),
                });
                if (!resp.ok) {
                    const d = await resp.json().catch(() => ({}));
                    this.statusMsg = d.detail || '无法打开目录';
                } else {
                    this.statusMsg = '已在访达中打开目录';
                }
            } catch (e) {
                this.statusMsg = '打开目录失败: ' + e.message;
            }
        },

        async openHistory() {
            this.historyOpen = true;
            this.historyLoading = true;
            this.history = [];
            try {
                const resp = await fetch('/api/runs/history');
                if (resp.ok) {
                    const data = await resp.json();
                    this.history = data.runs || [];
                }
            } catch (e) {
                // ignore
            } finally {
                this.historyLoading = false;
            }
        },
    };
}
</script>
{% endblock %}
