Add a per-user lead agent and make the right pane task-focused
This commit is contained in:
+2
-156
@@ -1,14 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
@@ -43,163 +36,16 @@ func (s *Server) GetInboxItem(w http.ResponseWriter, r *http.Request, itemID str
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
type httpError struct {
|
||||
status int
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *Server) RespondInbox(w http.ResponseWriter, r *http.Request) {
|
||||
req := api.InboxRespond{}
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Responses) == 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("responses is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
resolutions := []store.Resolution{}
|
||||
sessions := []string{}
|
||||
seen := map[string]bool{}
|
||||
|
||||
for _, create := range req.Responses {
|
||||
if seen[create.ItemId] {
|
||||
writeError(w, http.StatusBadRequest, fmt.Errorf("item %s answered twice in one submission", create.ItemId))
|
||||
return
|
||||
}
|
||||
seen[create.ItemId] = true
|
||||
|
||||
item, err := s.store.GetInboxItem(r.Context(), create.ItemId)
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
state, herr := checkResponse(item, create)
|
||||
if herr != nil {
|
||||
writeError(w, herr.status, herr.err)
|
||||
return
|
||||
}
|
||||
|
||||
attachments := []api.Attachment{}
|
||||
if create.Attachments != nil {
|
||||
attachments = *create.Attachments
|
||||
}
|
||||
|
||||
resolutions = append(resolutions, store.Resolution{
|
||||
State: state,
|
||||
Response: api.InboxResponse{
|
||||
Id: store.NewID(),
|
||||
ItemId: item.Id,
|
||||
User: actor(r),
|
||||
Type: create.Type,
|
||||
Options: create.Options,
|
||||
Text: create.Text,
|
||||
Attachments: attachments,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
},
|
||||
})
|
||||
if !slices.Contains(sessions, item.AgentSessionId) {
|
||||
sessions = append(sessions, item.AgentSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
err := s.store.Respond(r.Context(), actor(r), resolutions)
|
||||
items, err := s.agents.Respond(r.Context(), actor(r), req.Responses)
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, id := range sessions {
|
||||
go func() {
|
||||
err := s.agents.Deliver(context.Background(), id)
|
||||
if err != nil {
|
||||
slog.Error("deliver inbox responses", "session", id, "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
items := []api.InboxItem{}
|
||||
for _, res := range resolutions {
|
||||
item, err := s.store.GetInboxItem(r.Context(), res.Response.ItemId)
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, items)
|
||||
}
|
||||
|
||||
func checkResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxState, *httpError) {
|
||||
if item.State != api.Open {
|
||||
return "", &httpError{http.StatusConflict, fmt.Errorf("item %s is %s", item.Id, item.State)}
|
||||
}
|
||||
|
||||
text := ""
|
||||
if c.Text != nil {
|
||||
text = strings.TrimSpace(*c.Text)
|
||||
}
|
||||
question := item.Kind == api.Question
|
||||
bad := func(format string, args ...any) (api.InboxState, *httpError) {
|
||||
return "", &httpError{http.StatusBadRequest, fmt.Errorf("item %s: "+format, append([]any{item.Id}, args...)...)}
|
||||
}
|
||||
|
||||
switch c.Type {
|
||||
case api.Option:
|
||||
if !question {
|
||||
return bad("only questions have options")
|
||||
}
|
||||
if c.Options == nil || len(*c.Options) == 0 {
|
||||
return bad("option needs the chosen options")
|
||||
}
|
||||
if item.Payload["multiple"] != true && len(*c.Options) != 1 {
|
||||
return bad("a pick-one question takes exactly one option")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, o := range *c.Options {
|
||||
if !hasOption(item, o) {
|
||||
return bad("%q is not one of the question's options", o)
|
||||
}
|
||||
if seen[o] {
|
||||
return bad("%q chosen twice", o)
|
||||
}
|
||||
seen[o] = true
|
||||
}
|
||||
if text != "" && item.Payload["multiple"] != true {
|
||||
return bad("a pick-one option takes no text; use other")
|
||||
}
|
||||
return api.Answered, nil
|
||||
|
||||
case api.Other:
|
||||
if item.Kind == api.Report {
|
||||
return bad("reports are only dismissed")
|
||||
}
|
||||
if text == "" {
|
||||
return bad("other needs text")
|
||||
}
|
||||
return api.Answered, nil
|
||||
|
||||
case api.Dismiss:
|
||||
if question {
|
||||
return bad("questions need an option or other, not a dismissal")
|
||||
}
|
||||
return api.Dismissed, nil
|
||||
}
|
||||
|
||||
return bad("unknown response type %q", c.Type)
|
||||
}
|
||||
|
||||
func hasOption(item api.InboxItem, option string) bool {
|
||||
options, ok := item.Payload["options"].([]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, o := range options {
|
||||
if o == option {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user