package comicanimator import ( "fmt" "html/template" "mime" "net/http" "os" "path/filepath" "sort" "strings" "time" ) func render(w http.ResponseWriter, src string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := template.Must(template.New("page").Parse(src)).Execute(w, data); err != nil { http.Error(w, "render error", 500) } } func fragmentError(w http.ResponseWriter, status int, message string) { w.WriteHeader(status) fmt.Fprintf(w, ``, template.HTMLEscapeString(message)) } func (t *Tool) renderCard(w http.ResponseWriter, g Generation, instructor bool) { poll := "" if g.Status != Completed && g.Status != Failed && !instructor { poll = ` hx-get="generations/` + template.HTMLEscapeString(g.ID) + `/status" hx-trigger="every 5s" hx-swap="outerHTML"` } fmt.Fprintf(w, `
%s%s
`, poll, template.HTMLEscapeString(strings.ReplaceAll(string(g.Status), "_", " ")), g.CreatedAt.Format(time.RFC3339)) if instructor { fmt.Fprintf(w, `
Reviewed prompt

%s

`, template.HTMLEscapeString(g.ReviewedPrompt)) if g.ProviderJobID != "" { fmt.Fprintf(w, `Provider job: %s`, template.HTMLEscapeString(g.ProviderJobID)) } } if g.Status == Completed { prefix := "" if instructor { prefix = "generations/" } fmt.Fprintf(w, `Download`, prefix, g.ID, prefix, g.ID) } else if g.Status == Failed { fmt.Fprintf(w, `

%s

`, template.HTMLEscapeString(g.ErrorMessage)) } else { fmt.Fprint(w, `

Working…

`) } fmt.Fprint(w, "
") } func (t *Tool) instructorPage(w http.ResponseWriter, r *http.Request) { render(w, instructorPage, map[string]any{"Outputs": t.outputs()}) } func (t *Tool) instructorGenerations(w http.ResponseWriter, r *http.Request) { for _, g := range t.store.list("") { t.renderCard(w, g, true) } } func (t *Tool) instructorVideo(w http.ResponseWriter, r *http.Request) { t.generationFile(w, r, true, false) } func (t *Tool) instructorDownload(w http.ResponseWriter, r *http.Request) { t.generationFile(w, r, true, true) } type outputFile struct { Name string Size int64 Modified time.Time } func (t *Tool) outputs() []outputFile { entries, err := os.ReadDir(t.cfg.OutputDirectory) if err != nil { return nil } out := []outputFile{} for _, e := range entries { if e.IsDir() { continue } ext := strings.ToLower(filepath.Ext(e.Name())) if ext != ".mp4" && ext != ".webm" && ext != ".mov" { continue } info, err := e.Info() if err == nil { out = append(out, outputFile{Name: e.Name(), Size: info.Size(), Modified: info.ModTime()}) } } sort.Slice(out, func(i, j int) bool { return out[i].Modified.After(out[j].Modified) }) return out } func (t *Tool) outputDownload(w http.ResponseWriter, r *http.Request) { name := r.PathValue("filename") if name != filepath.Base(name) { http.NotFound(w, r) return } path := filepath.Join(t.cfg.OutputDirectory, name) if _, err := os.Stat(path); err != nil { http.NotFound(w, r) return } serveFile(w, r, path, mime.TypeByExtension(filepath.Ext(name)), r.URL.Query().Get("preview") != "1") } const toolPage = `

1. Source image

2. Describe motion

Working…

4. Generate

Recent animations

Instructor approval

An instructor PIN is required for this paid generation.

` const instructorPage = `

Current process generations

Downloaded output files

Files survive restarts; this is not a complete audit log.

{{range .Outputs}}
{{.Name}}{{.Modified.UTC.Format "2006-01-02 15:04:05Z"}} · {{.Size}} bytesDownload
{{else}}

No downloaded videos yet.

{{end}}
`