Add video generation outage banner and notice dialog

This commit is contained in:
2026-08-27 09:06:25 +08:00
parent 8d630944ca
commit 001239a809
+4 -2
View File
@@ -222,7 +222,7 @@ func renderStatus(w http.ResponseWriter, src string, data any, status int) {
template.Must(template.New("page").Parse(src)).Execute(w, data)
}
const head = `<!doctype html><html lang="en" data-theme="silk"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light"><meta name="htmx-config" content='{"responseHandling":[{"code":"204","swap":false},{"code":"[23]..","swap":true},{"code":"[45]..","swap":true,"error":true},{"code":"...","swap":false,"error":true}]}'><title>{{if .Title}}{{.Title}} · {{end}}Preface Tools</title><link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin><link rel="preconnect" href="https://www.preface.ai" crossorigin><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daisyui@5.6.3/daisyui.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daisyui@5.6.3/themes.css"><link rel="stylesheet" href="/static/app.css"><script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" defer></script><script src="/static/app.js" defer></script></head><body>`
const head = `<!doctype html><html lang="en" data-theme="silk"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light"><meta name="htmx-config" content='{"responseHandling":[{"code":"204","swap":false},{"code":"[23]..","swap":true},{"code":"[45]..","swap":true,"error":true},{"code":"...","swap":false,"error":true}]}'><title>{{if .Title}}{{.Title}} · {{end}}Preface Tools</title><link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin><link rel="preconnect" href="https://www.preface.ai" crossorigin><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daisyui@5.6.3/daisyui.css"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daisyui@5.6.3/themes.css"><link rel="stylesheet" href="/static/app.css"><script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" defer></script><script src="/static/app.js" defer></script></head><body><div class="service-banner" role="alert"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 9v4m0 4h.01M10.3 3.7 2.4 17.4A2 2 0 0 0 4.1 20h15.8a2 2 0 0 0 1.7-2.6L13.7 3.7a2 2 0 0 0-3.4 0Z"/></svg><div class="service-banner-text"><strong>Video generation is currently unavailable.</strong><span>The provided API key has expired or run out of credits, so new video generations cannot be started until it has been renewed. Prompt generation may also be affected.</span></div></div><dialog id="outage-dialog" class="modal"><div class="modal-box"><div class="modal-header"><div><span class="eyebrow service-eyebrow">Service notice</span><h2>Video generation unavailable</h2><p>New animations cannot be started right now.</p></div><form method="dialog"><button class="btn btn-circle btn-ghost btn-sm" aria-label="Close notice">×</button></form></div><div class="modal-content"><p>Unfortunately, video generation is currently unoperational because the provided API key has either expired or run out of credits. It will work again once the key has been renewed or topped up.</p><div class="modal-action"><form method="dialog"><button class="btn btn-primary">Understood</button></form></div></div></div><form method="dialog" class="modal-backdrop"><button aria-label="Close dialog">close</button></form></dialog>`
const loginTemplate = head + `<main class="login-simple"><section class="login-box"><div class="login-brand"><img src="https://www.preface.ai/_next/image/?url=https%3A%2F%2Fres.cloudinary.com%2Fpreface%2Fimage%2Fupload%2Fw_1024%2Cc_limit%2Cf_auto%2Fv1633826301%2Fr2021%2Fassets%2Fwebsite-revamp%2Fpreface_logo&w=256&q=75" alt="Preface"><h1>Preface Tools</h1><p>Choose your classroom access</p></div>{{with .Error}}<div class="alert alert-error" role="alert"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 9v4m0 4h.01M10.3 3.7 2.4 17.4A2 2 0 0 0 4.1 20h15.8a2 2 0 0 0 1.7-2.6L13.7 3.7a2 2 0 0 0-3.4 0Z"/></svg><span>{{.}}</span></div>{{end}}<div class="login-options"><form class="card login-card" method="post" action="/login/student"><div class="card-body"><div class="login-card-title"><span class="login-card-icon student-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m3 10 9-5 9 5-9 5-9-5Zm3 2.5V17c3 2.7 9 2.7 12 0v-4.5"/></svg></span><div><h2>Student</h2><p>Access classroom tools</p></div></div><label class="form-control"><span class="label-text">Student PIN</span><input class="input input-bordered" type="password" name="pin" inputmode="numeric" required autocomplete="current-password" placeholder="Enter PIN"></label><button class="btn btn-primary btn-block">Continue</button></div></form><form class="card login-card" method="post" action="/login/instructor"><div class="card-body"><div class="login-card-title"><span class="login-card-icon instructor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm8-3 2 2 4-4"/></svg></span><div><h2>Instructor</h2><p>Review and recover outputs</p></div></div><label class="form-control"><span class="label-text">Instructor PIN</span><input class="input input-bordered" type="password" name="pin" inputmode="numeric" required autocomplete="current-password" placeholder="Enter PIN"></label><button class="btn btn-neutral btn-block">Continue</button></div></form></div></section></main></body></html>`
const shellTemplate = head + `<header class="app-header"><div class="navbar app-navbar"><div class="navbar-start"><a class="brand" href="/"><img class="brand-logo" src="https://www.preface.ai/_next/image/?url=https%3A%2F%2Fres.cloudinary.com%2Fpreface%2Fimage%2Fupload%2Fw_1024%2Cc_limit%2Cf_auto%2Fv1633826301%2Fr2021%2Fassets%2Fwebsite-revamp%2Fpreface_logo&w=256&q=75" alt="Preface"><span class="brand-divider"></span><span>Preface Tools</span></a></div><div class="navbar-end app-actions">{{if .Instructor}}<div class="role-chip"><span class="status status-success"></span>Instructor console</div>{{else}}<label class="tool-picker"><span>Current tool</span><span class="select-wrap"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 3 1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3Z"/></svg><select class="select" id="tool-selector" aria-label="Choose tool">{{range .Tools}}<option value="/tools/{{.Definition.Key}}/" {{if eq .Definition.Key $.Selected}}selected{{end}}>{{.Definition.Name}}</option>{{end}}</select></span></label>{{end}}<form method="post" action="/logout"><input type="hidden" name="csrf_token" value="{{.CSRF}}"><button class="btn btn-ghost btn-sm" aria-label="Log out"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 17l5-5-5-5m5 5H3m10-9h6a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-6"/></svg><span>Log out</span></button></form></div></div></header><main class="app-main">{{.Content}}</main></body></html>`
@@ -239,9 +239,11 @@ func css(w http.ResponseWriter, r *http.Request) {
.recent-section .section-heading>span{display:none}.generate-card .step-number{font-size:0}.generate-card .step-number:before{font-size:.78rem;content:"03"}
@media(max-width:1050px){.tool-grid{grid-template-columns:1fr 1.25fr}.generate-card{grid-column:1/-1}.generate-card .card-body{display:grid;grid-template-columns:1fr 1fr;align-items:center}.generate-card .card-heading{grid-column:1/-1}.login-page{grid-template-columns:.75fr 1.25fr}.login-options{grid-template-columns:1fr}}
@media(max-width:720px){.app-navbar{min-height:4rem}.brand-logo{width:5.4rem}.brand-divider,.brand>span:last-child{display:none}.tool-picker>span:first-child,.app-actions .btn span{display:none}.tool-picker .select{width:9rem}.app-main{padding:1rem}.workspace-heading{align-items:flex-start;flex-direction:column;gap:1rem}.workflow-steps{overflow:auto;width:100%;padding-bottom:.25rem}.workflow-step{white-space:nowrap}.tool-grid{grid-template-columns:1fr}.generate-card{grid-column:auto}.generate-card .card-body{display:flex}.login-simple{padding:1rem}.login-simple .login-options{grid-template-columns:1fr}.login-brand{margin-bottom:1.5rem}.login-brand img{width:7.5rem}.generation{grid-template-columns:1fr}.generation-actions{grid-column:1}.output-grid{grid-template-columns:1fr}.upload-zone{min-height:16rem}}
.service-banner{display:flex;align-items:flex-start;gap:.75rem;border-bottom:1px solid rgba(182,106,22,.25);background:#fdf3e0;color:#7a4d10;padding:.75rem clamp(1rem,3vw,2.5rem)}.service-banner svg{flex:none;width:1.2rem;height:1.2rem;margin-top:.1rem;color:#b66a16}.service-banner-text{display:grid;gap:.15rem}.service-banner-text strong{font-size:.85rem;font-weight:750;color:#8a5510}.service-banner-text span{font-size:.78rem;line-height:1.5;color:#7a4d10}
.service-eyebrow{color:#b66a16}
`)
}
func js(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript")
fmt.Fprint(w, `const placePrompt=()=>{const p=document.querySelector("#prompt-result"),s=document.querySelector(".generate-card .settings-list");if(p&&s&&!s.contains(p)){s.replaceChildren(p)}};const promptButton=e=>e.detail.elt?.matches?.('[hx-post="prompt"]')?e.detail.elt:null;document.addEventListener("DOMContentLoaded",placePrompt);document.addEventListener("htmx:beforeRequest",e=>{const b=promptButton(e);if(b){b.disabled=true;b.setAttribute("aria-busy","true")}});document.addEventListener("htmx:afterRequest",e=>{const b=promptButton(e);if(b){b.disabled=false;b.removeAttribute("aria-busy")}});document.addEventListener("htmx:afterSwap",placePrompt);document.addEventListener("change",e=>{if(e.target.id==="tool-selector")location.href=e.target.value});document.addEventListener("click",e=>{if(e.target.closest("[data-open-approval]"))document.querySelector("#approval-dialog")?.showModal();if(e.target.closest("[data-close-approval]"))document.querySelector("#approval-dialog")?.close()});document.addEventListener("generationQueued",()=>{const d=document.querySelector("#approval-dialog"),p=d?.querySelector('[name="instructor_pin"]');if(p)p.value="";d?.close()});`)
fmt.Fprint(w, `const placePrompt=()=>{const p=document.querySelector("#prompt-result"),s=document.querySelector(".generate-card .settings-list");if(p&&s&&!s.contains(p)){s.replaceChildren(p)}};const promptButton=e=>e.detail.elt?.matches?.('[hx-post="prompt"]')?e.detail.elt:null;document.addEventListener("DOMContentLoaded",placePrompt);document.addEventListener("htmx:beforeRequest",e=>{const b=promptButton(e);if(b){b.disabled=true;b.setAttribute("aria-busy","true")}});document.addEventListener("htmx:afterRequest",e=>{const b=promptButton(e);if(b){b.disabled=false;b.removeAttribute("aria-busy")}});document.addEventListener("htmx:afterSwap",placePrompt);document.addEventListener("change",e=>{if(e.target.id==="tool-selector")location.href=e.target.value});document.addEventListener("click",e=>{if(e.target.closest("[data-open-approval]"))document.querySelector("#approval-dialog")?.showModal();if(e.target.closest("[data-close-approval]"))document.querySelector("#approval-dialog")?.close()});document.addEventListener("generationQueued",()=>{const d=document.querySelector("#approval-dialog"),p=d?.querySelector('[name="instructor_pin"]');if(p)p.value="";d?.close()});document.addEventListener("DOMContentLoaded",()=>{document.querySelector("#outage-dialog")?.showModal()});`)
}