package handlers import ( "context" "fmt" "net/http" "strings" "time" "github.com/gin-gonic/gin" "chat-switchboard/providers" "chat-switchboard/roles" "chat-switchboard/store" ) // TitleHandler generates chat titles using the utility role. type TitleHandler struct { stores store.Stores resolver *roles.Resolver } // NewTitleHandler creates a title generation handler. func NewTitleHandler(s store.Stores, r *roles.Resolver) *TitleHandler { return &TitleHandler{stores: s, resolver: r} } // GenerateTitle generates a title for a channel using the utility role. // POST /channels/:id/generate-title func (h *TitleHandler) GenerateTitle(c *gin.Context) { channelID := c.Param("id") userID := c.GetString("user_id") teamID := c.GetString("team_id") // Verify channel access owns, err := h.stores.Channels.UserOwns(c.Request.Context(), channelID, userID) if err != nil || !owns { c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"}) return } ch, err := h.stores.Channels.GetByID(c.Request.Context(), channelID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"}) return } // Load first few messages msgs, err := h.stores.Messages.ListForChannel(c.Request.Context(), channelID, store.ListOptions{ Limit: 4, Order: "asc", }) if err != nil || len(msgs) == 0 { c.JSON(http.StatusOK, gin.H{"title": ch.Title}) // keep existing return } // Build a snippet from the first messages var snippet strings.Builder for _, m := range msgs { content := m.Content if len(content) > 200 { content = content[:200] + "…" } fmt.Fprintf(&snippet, "%s: %s\n", m.Role, content) } // Call utility role ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) defer cancel() var tID *string if teamID != "" { tID = &teamID } result, err := h.resolver.Complete(ctx, roles.RoleUtility, userID, tID, []providers.Message{ { Role: "system", Content: "You are a title generator. Given a conversation snippet, produce a concise title of 6 words or fewer. Respond with ONLY the title, no quotes, no punctuation at the end, no explanation.", }, { Role: "user", Content: "Title this conversation:\n\n" + snippet.String(), }, }) if err != nil { // Fallback: truncate first user message c.JSON(http.StatusOK, gin.H{"title": ch.Title}) return } title := strings.TrimSpace(result.Content) // Strip surrounding quotes if the model added them title = strings.Trim(title, "\"'`\u201c\u201d\u2018\u2019") if title == "" { c.JSON(http.StatusOK, gin.H{"title": ch.Title}) return } // Cap at 100 chars if len(title) > 100 { title = title[:100] } // Update channel _ = h.stores.Channels.Update(c.Request.Context(), channelID, map[string]interface{}{ "title": title, }) c.JSON(http.StatusOK, gin.H{"title": title}) }