|
- package handlers
-
- import (
- "encoding/json"
- "net/http"
- "strconv"
- "strings"
-
- "github.com/go-chi/chi/v5"
-
- "qctextbuilder/internal/buildsvc"
- "qctextbuilder/internal/domain"
- "qctextbuilder/internal/draftsvc"
- "qctextbuilder/internal/onboarding"
- "qctextbuilder/internal/templatesvc"
- )
-
- type API struct {
- templateSvc *templatesvc.Service
- onboardSvc *onboarding.Service
- draftSvc *draftsvc.Service
- buildSvc buildsvc.Service
- }
-
- func NewAPI(templateSvc *templatesvc.Service, onboardSvc *onboarding.Service, draftSvc *draftsvc.Service, buildSvc buildsvc.Service) *API {
- return &API{
- templateSvc: templateSvc,
- onboardSvc: onboardSvc,
- draftSvc: draftSvc,
- buildSvc: buildSvc,
- }
- }
-
- func (a *API) Health(w http.ResponseWriter, _ *http.Request) {
- writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
- }
-
- func (a *API) SyncTemplates(w http.ResponseWriter, r *http.Request) {
- templates, err := a.templateSvc.SyncAITemplates(r.Context())
- if err != nil {
- writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]any{"count": len(templates), "templates": templates})
- }
-
- func (a *API) ListTemplates(w http.ResponseWriter, r *http.Request) {
- templates, err := a.templateSvc.ListTemplates(r.Context())
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]any{"count": len(templates), "templates": templates})
- }
-
- func (a *API) GetTemplateDetail(w http.ResponseWriter, r *http.Request) {
- rawID := chi.URLParam(r, "id")
- templateID, err := strconv.ParseInt(rawID, 10, 64)
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"})
- return
- }
-
- detail, err := a.templateSvc.GetTemplateDetail(r.Context(), templateID)
- if err != nil {
- writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, detail)
- }
-
- func (a *API) OnboardTemplate(w http.ResponseWriter, r *http.Request) {
- rawID := chi.URLParam(r, "id")
- templateID, err := strconv.ParseInt(rawID, 10, 64)
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"})
- return
- }
-
- manifest, fields, err := a.onboardSvc.OnboardTemplate(r.Context(), templateID)
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]any{
- "manifestId": manifest.ID,
- "fieldCount": len(fields),
- "status": "reviewed",
- })
- }
-
- type updateTemplateFieldsRequest struct {
- ManifestID string `json:"manifestId"`
- Fields []updateTemplateFieldItem `json:"fields"`
- }
-
- type updateTemplateFieldItem struct {
- Path string `json:"path"`
- IsEnabled *bool `json:"isEnabled,omitempty"`
- IsRequiredByUs *bool `json:"isRequiredByUs,omitempty"`
- DisplayLabel *string `json:"displayLabel,omitempty"`
- DisplayOrder *int `json:"displayOrder,omitempty"`
- WebsiteSection *string `json:"websiteSection,omitempty"`
- Notes *string `json:"notes,omitempty"`
- }
-
- func (a *API) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) {
- rawID := chi.URLParam(r, "id")
- templateID, err := strconv.ParseInt(rawID, 10, 64)
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"})
- return
- }
-
- var req updateTemplateFieldsRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
- return
- }
- if len(req.Fields) == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": "fields is required"})
- return
- }
-
- patches := make([]onboarding.FieldPatch, 0, len(req.Fields))
- for _, f := range req.Fields {
- patches = append(patches, onboarding.FieldPatch{
- Path: f.Path,
- IsEnabled: f.IsEnabled,
- IsRequiredByUs: f.IsRequiredByUs,
- DisplayLabel: f.DisplayLabel,
- DisplayOrder: f.DisplayOrder,
- WebsiteSection: f.WebsiteSection,
- Notes: f.Notes,
- })
- }
-
- manifest, fields, err := a.onboardSvc.UpdateTemplateFields(r.Context(), templateID, req.ManifestID, patches)
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
- return
- }
-
- writeJSON(w, http.StatusOK, map[string]any{
- "templateId": templateID,
- "manifestId": manifest.ID,
- "fieldCount": len(fields),
- "fields": fields,
- })
- }
-
- func (a *API) StartBuild(w http.ResponseWriter, r *http.Request) {
- var req buildsvc.StartBuildRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
- return
- }
-
- result, err := a.buildSvc.StartBuild(r.Context(), req)
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusAccepted, result)
- }
-
- type upsertDraftRequest struct {
- TemplateID *int64 `json:"templateId,omitempty"`
- ManifestID string `json:"manifestId"`
- Source string `json:"source"`
- RequestName string `json:"requestName"`
- GlobalData map[string]any `json:"globalData"`
- FieldValues map[string]string `json:"fieldValues"`
- DraftContext *domain.DraftContext `json:"draftContext,omitempty"`
- SuggestionState *domain.DraftSuggestionState `json:"suggestionState,omitempty"`
- Status string `json:"status"`
- Notes string `json:"notes"`
- }
-
- type intakeDraftRequest struct {
- DraftID string `json:"draftId,omitempty"`
- Source string `json:"source"`
- RequestName string `json:"requestName"`
- TemplateID *int64 `json:"templateId,omitempty"`
- GlobalData map[string]any `json:"globalData"`
- Notes string `json:"notes"`
- WebsiteURL string `json:"websiteUrl,omitempty"`
- WebsiteSummary string `json:"websiteSummary,omitempty"`
- BusinessType string `json:"businessType,omitempty"`
- LocaleStyle string `json:"localeStyle,omitempty"`
- MarketStyle string `json:"marketStyle,omitempty"`
- AddressMode string `json:"addressMode,omitempty"`
- ContentTone string `json:"contentTone,omitempty"`
- PromptInstructions string `json:"promptInstructions,omitempty"`
- StyleProfile *domain.DraftStyleProfile `json:"styleProfile,omitempty"`
- }
-
- func (a *API) IntakeDraft(w http.ResponseWriter, r *http.Request) {
- var req intakeDraftRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
- return
- }
-
- globalData := req.GlobalData
- if globalData == nil {
- globalData = map[string]any{}
- }
- if strings.TrimSpace(req.BusinessType) != "" && strings.TrimSpace(getMapString(globalData, "businessType")) == "" {
- globalData["businessType"] = strings.TrimSpace(req.BusinessType)
- }
-
- styleProfile := domain.DraftStyleProfile{
- LocaleStyle: strings.TrimSpace(req.LocaleStyle),
- MarketStyle: strings.TrimSpace(req.MarketStyle),
- AddressMode: strings.TrimSpace(req.AddressMode),
- ContentTone: strings.TrimSpace(req.ContentTone),
- PromptInstructions: strings.TrimSpace(req.PromptInstructions),
- }
- if req.StyleProfile != nil {
- styleProfile = *req.StyleProfile
- if styleProfile.LocaleStyle == "" {
- styleProfile.LocaleStyle = strings.TrimSpace(req.LocaleStyle)
- }
- if styleProfile.MarketStyle == "" {
- styleProfile.MarketStyle = strings.TrimSpace(req.MarketStyle)
- }
- if styleProfile.AddressMode == "" {
- styleProfile.AddressMode = strings.TrimSpace(req.AddressMode)
- }
- if styleProfile.ContentTone == "" {
- styleProfile.ContentTone = strings.TrimSpace(req.ContentTone)
- }
- if styleProfile.PromptInstructions == "" {
- styleProfile.PromptInstructions = strings.TrimSpace(req.PromptInstructions)
- }
- }
- businessType := strings.TrimSpace(req.BusinessType)
- if businessType == "" {
- businessType = strings.TrimSpace(getMapString(globalData, "businessType"))
- }
- draftContext := &domain.DraftContext{
- IntakeSource: strings.TrimSpace(req.Source),
- LLM: domain.DraftLLMContext{
- BusinessType: businessType,
- WebsiteURL: strings.TrimSpace(req.WebsiteURL),
- WebsiteSummary: strings.TrimSpace(req.WebsiteSummary),
- StyleProfile: styleProfile,
- },
- }
-
- draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
- DraftID: strings.TrimSpace(req.DraftID),
- TemplateID: req.TemplateID,
- Source: defaultStr(req.Source, "intake-api"),
- RequestName: req.RequestName,
- GlobalData: globalData,
- FieldValues: map[string]string{},
- DraftContext: draftContext,
- Status: "draft",
- Notes: req.Notes,
- })
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
- return
- }
- if strings.TrimSpace(req.DraftID) == "" {
- writeJSON(w, http.StatusCreated, draft)
- return
- }
- writeJSON(w, http.StatusOK, draft)
- }
-
- func (a *API) ListDrafts(w http.ResponseWriter, r *http.Request) {
- limit, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("limit")))
- drafts, err := a.draftSvc.ListDrafts(r.Context(), limit)
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]any{"count": len(drafts), "drafts": drafts})
- }
-
- func (a *API) GetDraft(w http.ResponseWriter, r *http.Request) {
- draftID := strings.TrimSpace(chi.URLParam(r, "id"))
- draft, err := a.draftSvc.GetDraft(r.Context(), draftID)
- if err != nil {
- writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, draft)
- }
-
- func (a *API) UpdateDraft(w http.ResponseWriter, r *http.Request) {
- draftID := strings.TrimSpace(chi.URLParam(r, "id"))
- var req upsertDraftRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
- return
- }
- draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
- DraftID: draftID,
- TemplateID: req.TemplateID,
- ManifestID: req.ManifestID,
- Source: req.Source,
- RequestName: req.RequestName,
- GlobalData: req.GlobalData,
- FieldValues: req.FieldValues,
- DraftContext: req.DraftContext,
- SuggestionState: req.SuggestionState,
- Status: req.Status,
- Notes: req.Notes,
- })
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, draft)
- }
-
- func (a *API) GetBuild(w http.ResponseWriter, r *http.Request) {
- buildID := chi.URLParam(r, "id")
- build, err := a.buildSvc.GetBuild(r.Context(), buildID)
- if err != nil {
- writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, build)
- }
-
- func (a *API) PollBuildOnce(w http.ResponseWriter, r *http.Request) {
- buildID := chi.URLParam(r, "id")
- if err := a.buildSvc.PollOnce(r.Context(), buildID); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
- return
- }
-
- build, err := a.buildSvc.GetBuild(r.Context(), buildID)
- if err != nil {
- writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, build)
- }
-
- func (a *API) FetchBuildEditorURL(w http.ResponseWriter, r *http.Request) {
- buildID := chi.URLParam(r, "id")
- if err := a.buildSvc.FetchEditorURL(r.Context(), buildID); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
- return
- }
-
- build, err := a.buildSvc.GetBuild(r.Context(), buildID)
- if err != nil {
- writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, build)
- }
-
- func writeJSON(w http.ResponseWriter, status int, v any) {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(status)
- _ = json.NewEncoder(w).Encode(v)
- }
-
- func defaultStr(v, fallback string) string {
- if strings.TrimSpace(v) == "" {
- return fallback
- }
- return strings.TrimSpace(v)
- }
-
- func getMapString(values map[string]any, key string) string {
- if values == nil {
- return ""
- }
- raw, _ := values[key].(string)
- return raw
- }
|