| |
| |
| |
| |
|
|
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
|
|
| |
| |
| |
|
|
| let client = null; |
| let registryData = []; |
| let pipelines = []; |
|
|
| |
| |
| |
|
|
| async function connect() { |
| try { |
| client = await Client.connect(window.location.origin); |
| toast("Connected to GameForge", "success"); |
| await loadDashboard(); |
| } catch (e) { |
| toast("Failed to connect: " + e.message, "error"); |
| } |
| } |
|
|
| |
| |
| |
|
|
| async function api(endpoint, params = {}) { |
| if (!client) await connect(); |
| try { |
| const result = await client.predict(endpoint, params); |
| return result.data; |
| } catch (e) { |
| toast(`API error (${endpoint}): ${e.message}`, "error"); |
| throw e; |
| } |
| } |
|
|
| |
| |
| |
|
|
| document.querySelectorAll(".nav-item").forEach(item => { |
| item.addEventListener("click", () => { |
| document.querySelectorAll(".nav-item").forEach(i => i.classList.remove("active")); |
| document.querySelectorAll(".tab-content").forEach(t => t.classList.remove("active")); |
| item.classList.add("active"); |
| const tab = document.getElementById("tab-" + item.dataset.tab); |
| if (tab) tab.classList.add("active"); |
|
|
| |
| if (item.dataset.tab === "models") loadModels(); |
| if (item.dataset.tab === "pipelines") loadPipelines(); |
| if (item.dataset.tab === "assets") loadAssets(); |
| if (item.dataset.tab === "batch") loadBatchPipelines(); |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| async function loadDashboard() { |
| try { |
| const [reg, pipes, assets] = await Promise.all([ |
| api("/registry_info"), |
| api("/list_pipelines"), |
| api("/list_assets"), |
| ]); |
|
|
| registryData = reg.models || []; |
| pipelines = pipes || []; |
|
|
| document.getElementById("stat-models").textContent = registryData.length; |
| document.getElementById("stat-free").textContent = registryData.filter(m => m.free).length; |
| document.getElementById("stat-pipelines").textContent = pipelines.length; |
| document.getElementById("stat-assets").textContent = assets.length; |
|
|
| |
| document.querySelectorAll(".quick-gen-card").forEach(card => { |
| card.addEventListener("click", () => { |
| const typeMap = { |
| character: "image", prop: "image", environment: "image", |
| npc_voice: "voice", audio: "music", |
| }; |
| const type = typeMap[card.dataset.pipeline] || "image"; |
| document.getElementById("gen-type").value = type; |
| document.getElementById("gen-prompt").value = card.dataset.prompt; |
| switchTab("generate"); |
| document.getElementById("gen-btn").click(); |
| }); |
| }); |
| } catch (e) { |
| console.error("Dashboard load failed:", e); |
| } |
| } |
|
|
| function switchTab(name) { |
| document.querySelector(`.nav-item[data-tab="${name}"]`)?.click(); |
| } |
|
|
| |
| |
| |
|
|
| const genType = document.getElementById("gen-type"); |
| const genBtn = document.getElementById("gen-btn"); |
| const genPrompt = document.getElementById("gen-prompt"); |
| const genPreview = document.getElementById("gen-preview-box"); |
| const genInfo = document.getElementById("gen-result-info"); |
|
|
| genType.addEventListener("change", () => { |
| const is3d = genType.value === "3d"; |
| document.getElementById("ref-image-group").style.display = is3d ? "block" : "none"; |
| document.getElementById("neg-prompt-group").style.display = (genType.value === "image") ? "block" : "none"; |
| }); |
|
|
| genBtn.addEventListener("click", async () => { |
| const prompt = genPrompt.value.trim(); |
| if (!prompt) { toast("Enter a prompt", "error"); return; } |
|
|
| const type = genType.value; |
| const btnText = genBtn.querySelector(".btn-text"); |
| const btnLoad = genBtn.querySelector(".btn-loading"); |
|
|
| btnText.style.display = "none"; |
| btnLoad.style.display = "inline"; |
| genBtn.disabled = true; |
|
|
| genPreview.innerHTML = '<div class="preview-placeholder"><span>⏳</span><p>Generating...</p></div>'; |
|
|
| try { |
| const endpointMap = { |
| image: "/generate_image", |
| voice: "/generate_voice", |
| music: "/generate_music", |
| video: "/generate_video", |
| sfx: "/generate_sfx", |
| "3d": "/generate_3d", |
| }; |
|
|
| const endpoint = endpointMap[type]; |
| let result; |
|
|
| if (type === "image") { |
| const neg = document.getElementById("gen-negative").value; |
| result = await client.predict(endpoint, { prompt, negative_prompt: neg, steps: 4 }); |
| } else { |
| result = await client.predict(endpoint, { prompt, text: prompt }); |
| } |
|
|
| const fileData = result.data[0]; |
| if (fileData && fileData.url) { |
| showResult(fileData, type); |
| toast("Generated!", "success"); |
| } else { |
| genPreview.innerHTML = '<div class="preview-placeholder"><span>❌</span><p>Generation failed</p></div>'; |
| } |
| } catch (e) { |
| genPreview.innerHTML = `<div class="preview-placeholder"><span>❌</span><p>${e.message}</p></div>`; |
| } finally { |
| btnText.style.display = "inline"; |
| btnLoad.style.display = "none"; |
| genBtn.disabled = false; |
| } |
| }); |
|
|
| function showResult(fileData, type) { |
| const url = fileData.url; |
| let html = ""; |
|
|
| if (type === "image") { |
| html = `<img src="${url}" alt="Generated">`; |
| } else if (type === "video") { |
| html = `<video controls autoplay><source src="${url}" type="video/mp4"></video>`; |
| } else if (["voice", "music", "sfx"].includes(type)) { |
| html = `<audio controls autoplay><source src="${url}"></audio>`; |
| } else { |
| html = `<div class="preview-placeholder"><span>📦</span><p>3D Model Generated</p></div>`; |
| } |
|
|
| genPreview.innerHTML = html; |
| genInfo.style.display = "flex"; |
|
|
| document.getElementById("gen-download").onclick = () => { |
| const a = document.createElement("a"); |
| a.href = url; |
| a.download = `gameforge_${type}_${Date.now()}`; |
| a.click(); |
| }; |
| } |
|
|
| |
| |
| |
|
|
| async function loadModels() { |
| if (!registryData.length) { |
| const reg = await api("/registry_info"); |
| registryData = reg.models || []; |
| } |
|
|
| const typeFilter = document.getElementById("model-filter-type"); |
| const freeOnly = document.getElementById("model-filter-free"); |
| const safeOnly = document.getElementById("model-filter-safe"); |
|
|
| |
| const types = [...new Set(registryData.map(m => m.asset_type))]; |
| if (typeFilter.options.length <= 1) { |
| types.forEach(t => { |
| const opt = document.createElement("option"); |
| opt.value = t; |
| opt.textContent = t; |
| typeFilter.appendChild(opt); |
| }); |
| } |
|
|
| function render() { |
| let models = registryData; |
| if (typeFilter.value) models = models.filter(m => m.asset_type === typeFilter.value); |
| if (freeOnly.checked) models = models.filter(m => m.free); |
| if (safeOnly.checked) models = models.filter(m => m.commercial_safe); |
|
|
| const tbody = document.getElementById("models-tbody"); |
| tbody.innerHTML = models.map(m => ` |
| <tr> |
| <td>${m.asset_type}</td> |
| <td>${m.variant}</td> |
| <td style="font-family:monospace;font-size:12px">${m.model}</td> |
| <td>${m.type}</td> |
| <td>${m.license}</td> |
| <td>${m.hardware}</td> |
| <td><span class="badge ${m.free ? 'badge-free' : 'badge-paid'}">${m.free ? 'FREE' : 'PAID'}</span></td> |
| <td><span class="badge ${m.commercial_safe ? 'badge-safe' : 'badge-check'}">${m.commercial_safe ? 'SAFE' : 'CHECK'}</span></td> |
| </tr> |
| `).join(""); |
| } |
|
|
| typeFilter.onchange = freeOnly.onchange = safeOnly.onchange = render; |
| render(); |
| } |
|
|
| |
| |
| |
|
|
| async function loadPipelines() { |
| if (!pipelines.length) { |
| pipelines = await api("/list_pipelines"); |
| } |
|
|
| const grid = document.getElementById("pipeline-grid"); |
| grid.innerHTML = pipelines.map(p => ` |
| <div class="pipeline-card"> |
| <h3>${p.name}</h3> |
| <p>${p.description}</p> |
| <div class="pipeline-meta"> |
| <span>📋 ${p.steps} steps</span> |
| <span>v${p.version}</span> |
| </div> |
| </div> |
| `).join(""); |
| } |
|
|
| |
| |
| |
|
|
| async function loadAssets() { |
| const assets = await api("/list_assets"); |
| const grid = document.getElementById("assets-grid"); |
|
|
| if (!assets.length) { |
| grid.innerHTML = '<p style="color:var(--text-muted);grid-column:1/-1">No assets generated yet. Go to Generate to create some!</p>'; |
| return; |
| } |
|
|
| grid.innerHTML = assets.map(a => ` |
| <div class="asset-card"> |
| <div class="name">${a.name}</div> |
| <div class="meta">${a.format} · ${(a.size / 1024).toFixed(1)} KB</div> |
| </div> |
| `).join(""); |
| } |
|
|
| document.getElementById("asset-refresh")?.addEventListener("click", loadAssets); |
|
|
| |
| |
| |
|
|
| async function loadBatchPipelines() { |
| if (!pipelines.length) { |
| pipelines = await api("/list_pipelines"); |
| } |
|
|
| const select = document.getElementById("batch-pipeline"); |
| select.innerHTML = pipelines.map(p => |
| `<option value="${p.name}">${p.name} (${p.steps} steps)</option>` |
| ).join(""); |
| } |
|
|
| document.getElementById("batch-count")?.addEventListener("input", (e) => { |
| document.getElementById("batch-count-val").textContent = e.target.value; |
| }); |
|
|
| document.getElementById("batch-btn")?.addEventListener("click", async () => { |
| const pipeline = document.getElementById("batch-pipeline").value; |
| const prompt = document.getElementById("batch-prompt").value; |
| const count = parseInt(document.getElementById("batch-count").value); |
|
|
| if (!prompt) { toast("Enter a prompt", "error"); return; } |
|
|
| toast(`Generating ${count} variants...`, "info"); |
|
|
| |
| const results = document.getElementById("batch-results"); |
| results.innerHTML = ""; |
|
|
| for (let i = 0; i < count; i++) { |
| const card = document.createElement("div"); |
| card.className = "asset-card"; |
| card.innerHTML = `<div class="name">Variant ${i + 1}</div><div class="meta">Generating...</div>`; |
| results.appendChild(card); |
|
|
| try { |
| |
| const variantPrompt = `${prompt}, style variation ${i + 1}`; |
| |
| card.querySelector(".meta").textContent = "Queued"; |
| } catch (e) { |
| card.querySelector(".meta").textContent = "Failed: " + e.message; |
| } |
| } |
| }); |
|
|
| |
| |
| |
|
|
| function toast(message, type = "info") { |
| const container = document.getElementById("toast-container"); |
| const el = document.createElement("div"); |
| el.className = `toast toast-${type}`; |
| el.textContent = message; |
| container.appendChild(el); |
| setTimeout(() => el.remove(), 4000); |
| } |
|
|
| |
| |
| |
|
|
| connect(); |
|
|