package web import ( "net/http" "time" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) // ChatModel is one entry in the chat page's model picker. type ChatModel struct { Name string // Simulated marks a model that returns synthetic text rather than // inference output. The picker says so, because the two are // indistinguishable from the outside until the reply arrives. Simulated bool // OnDemand marks a model with nothing running that loads when asked. It is // offered anyway: the first message is what asks, so hiding it until it // runs meant it could never be started from here. OnDemand bool // Hardware is what the model's running replicas compute on: "cpu", or a // GPU class. Several when it runs on both, and empty when nothing runs // or nothing said. Hardware []string } // Runs names the hardware for the picker: "GPU", "CPU", or "GPU + CPU" for a // model with replicas on both, where which one answers is the router's choice // and each reply says which it was. func (m ChatModel) Runs() string { var gpu, cpu bool for _, h := range m.Hardware { if h == "cpu" { cpu = true } else { gpu = true } } switch { case gpu && cpu: return "GPU + CPU" case gpu: return "GPU" case cpu: return "CPU" } return "" } // ChatContext is one thing the chat page can spend against: a project, or the // person themselves. type ChatContext struct { Value string Label string } type chatData struct { Nav Nav Contexts []ChatContext Context string User store.User Models []ChatModel CSRF string Version string Now string } // Chat renders the in-browser chat for one signed-in person. func Chat(user store.User, models []ChatModel, contexts []ChatContext, current, version string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { noStore(w) render(w, "chat", chatData{ Nav: navFrom(r, "chat"), Contexts: contexts, Context: current, User: user, Models: models, CSRF: csrfFor(r), Version: version, Now: time.Now().UTC().Format(time.RFC3339), }) } } // Guard runs a handler only for a signed-in, approved account. // // Exported because the chat lives in the router — it needs the endpoint // registry and the request path — while the session machinery lives here. The // alternative was a second copy of the session check next to the one thing that // must never have two copies. // // Its POSTs carry the CSRF token in a header rather than a form field, because // they are JSON. A header is a stronger check than a field: a cross-site form // post cannot set one at all. func (p *AccountPages) Guard( next func(w http.ResponseWriter, r *http.Request, user store.User), ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { noStore(w) sess, user, ok := p.session(r) if !ok { if r.Method == http.MethodGet { http.Redirect(w, r, "/login", http.StatusSeeOther) } else { http.Error(w, "not signed in", http.StatusUnauthorized) } return } if r.Method != http.MethodGet { token := r.Header.Get("X-CSRF-Token") if token == "" { token = r.FormValue("csrf") } if !p.accounts.CheckCSRF(sess, token) { http.Error(w, "bad request", http.StatusBadRequest) return } } next(w, p.withNav(r, user), user) } }