Files
chorus/portal/web/web.go
T

200 lines
5.2 KiB
Go

package web
import (
"embed"
"fmt"
"html/template"
"io"
"io/fs"
"strings"
"time"
"unicode/utf8"
"git.ilapage.cn/OPC/chorus/internal/core/model"
"git.ilapage.cn/OPC/chorus/portal/service"
)
//go:embed templates/*.html static/* static/vendor/*
var assets embed.FS
type Page struct {
Title string
DisplayName string
CSRFToken string
ReturnTo string
History []model.Generation
HistoryCursor string
HistoryHasMore bool
HistoryLimit int
Current *service.Detail
MaxPromptBytes int
MaxImages int
ImageGenerateAvailable bool
ImageEditAvailable bool
}
type Renderer struct{ templates *template.Template }
func NewRenderer() (*Renderer, error) {
functions := template.FuncMap{
"statusLabel": statusLabel, "statusTone": statusTone, "kindLabel": kindLabel, "kindIcon": kindIcon,
"shortPrompt": shortPrompt, "relativeTime": relativeTime, "terminal": func(status model.GenerationStatus) bool { return status.Terminal() },
"historyDuration": func(g model.Generation) string { return generationDuration(g, time.Now(), false) },
"resultDuration": func(g model.Generation) string { return generationDuration(g, time.Now(), true) },
"timeAttr": optionalTimeAttr,
"outputMetadata": outputMetadata,
"value": func(value *string) string {
if value == nil {
return ""
}
return *value
},
}
templates, err := template.New("portal").Funcs(functions).ParseFS(assets, "templates/*.html")
if err != nil {
return nil, fmt.Errorf("parse portal templates: %w", err)
}
return &Renderer{templates: templates}, nil
}
func (r *Renderer) Render(writer io.Writer, name string, data any) error {
return r.templates.ExecuteTemplate(writer, name, data)
}
func Static() (fs.FS, error) { return fs.Sub(assets, "static") }
func statusLabel(status model.GenerationStatus) string {
switch status {
case model.StatusPending:
return "排队中"
case model.StatusRunning:
return "生成中"
case model.StatusSucceeded:
return "已完成"
case model.StatusFailed:
return "失败"
default:
return "未知"
}
}
func statusTone(status model.GenerationStatus) string {
switch status {
case model.StatusPending:
return "pending"
case model.StatusRunning:
return "running"
case model.StatusFailed:
return "failed"
default:
return "success"
}
}
func kindLabel(kind model.GenerationKind) string {
if kind == model.KindImage {
return "图片"
}
return "文本"
}
func kindIcon(kind model.GenerationKind) string {
if kind == model.KindImage {
return "image"
}
return "file-text"
}
func shortPrompt(value string) string {
value = strings.TrimSpace(value)
if utf8.RuneCountInString(value) <= 28 {
return value
}
return string([]rune(value)[:28]) + "..."
}
func relativeTime(value time.Time) string {
elapsed := time.Since(value)
switch {
case elapsed < time.Minute:
return "刚刚"
case elapsed < time.Hour:
return fmt.Sprintf("%d 分钟前", int(elapsed.Minutes()))
case elapsed < 24*time.Hour:
return fmt.Sprintf("%d 小时前", int(elapsed.Hours()))
default:
return value.Local().Format("01-02 15:04")
}
}
func generationDuration(generation model.Generation, now time.Time, includeLabel bool) string {
label := "总耗时"
end := now
if !generation.Status.Terminal() {
if generation.Status == model.StatusPending {
label = "已等待"
} else {
label = "已用时"
}
} else if generation.CompletedAt != nil {
end = *generation.CompletedAt
} else {
return "耗时未知"
}
if generation.CreatedAt.IsZero() || end.Before(generation.CreatedAt) {
return "时间异常"
}
value := formatDuration(end.Sub(generation.CreatedAt))
if includeLabel || !generation.Status.Terminal() {
return label + " " + value
}
return value
}
func formatDuration(duration time.Duration) string {
if duration < time.Second {
return "<1秒"
}
seconds := int64(duration.Round(time.Second) / time.Second)
if seconds < 60 {
return fmt.Sprintf("%d秒", seconds)
}
minutes := seconds / 60
seconds %= 60
if minutes < 60 {
return fmt.Sprintf("%d分%02d秒", minutes, seconds)
}
hours := minutes / 60
minutes %= 60
return fmt.Sprintf("%d小时%02d分%02d秒", hours, minutes, seconds)
}
func optionalTimeAttr(value *time.Time) string {
if value == nil || value.IsZero() {
return ""
}
return value.UTC().Format(time.RFC3339Nano)
}
func outputMetadata(output model.GenerationOutput) string {
parts := make([]string, 0, 3)
if output.MIMEType != nil && strings.TrimSpace(*output.MIMEType) != "" {
parts = append(parts, strings.ToUpper(strings.TrimPrefix(strings.TrimSpace(*output.MIMEType), "image/")))
}
if output.Width != nil && output.Height != nil && *output.Width > 0 && *output.Height > 0 {
parts = append(parts, fmt.Sprintf("%d x %d", *output.Width, *output.Height))
}
if output.SizeBytes != nil {
parts = append(parts, formatBytes(*output.SizeBytes))
}
if len(parts) == 0 {
return "图片结果"
}
return strings.Join(parts, " · ")
}
func formatBytes(bytes uint64) string {
if bytes < 1024 {
return fmt.Sprintf("%d B", bytes)
}
if bytes < 1024*1024 {
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
}
return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
}