You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

379 lines
12KB

  1. package handlers
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "strconv"
  6. "strings"
  7. "github.com/go-chi/chi/v5"
  8. "qctextbuilder/internal/buildsvc"
  9. "qctextbuilder/internal/domain"
  10. "qctextbuilder/internal/draftsvc"
  11. "qctextbuilder/internal/onboarding"
  12. "qctextbuilder/internal/templatesvc"
  13. )
  14. type API struct {
  15. templateSvc *templatesvc.Service
  16. onboardSvc *onboarding.Service
  17. draftSvc *draftsvc.Service
  18. buildSvc buildsvc.Service
  19. }
  20. func NewAPI(templateSvc *templatesvc.Service, onboardSvc *onboarding.Service, draftSvc *draftsvc.Service, buildSvc buildsvc.Service) *API {
  21. return &API{
  22. templateSvc: templateSvc,
  23. onboardSvc: onboardSvc,
  24. draftSvc: draftSvc,
  25. buildSvc: buildSvc,
  26. }
  27. }
  28. func (a *API) Health(w http.ResponseWriter, _ *http.Request) {
  29. writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
  30. }
  31. func (a *API) SyncTemplates(w http.ResponseWriter, r *http.Request) {
  32. templates, err := a.templateSvc.SyncAITemplates(r.Context())
  33. if err != nil {
  34. writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
  35. return
  36. }
  37. writeJSON(w, http.StatusOK, map[string]any{"count": len(templates), "templates": templates})
  38. }
  39. func (a *API) ListTemplates(w http.ResponseWriter, r *http.Request) {
  40. templates, err := a.templateSvc.ListTemplates(r.Context())
  41. if err != nil {
  42. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  43. return
  44. }
  45. writeJSON(w, http.StatusOK, map[string]any{"count": len(templates), "templates": templates})
  46. }
  47. func (a *API) GetTemplateDetail(w http.ResponseWriter, r *http.Request) {
  48. rawID := chi.URLParam(r, "id")
  49. templateID, err := strconv.ParseInt(rawID, 10, 64)
  50. if err != nil {
  51. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"})
  52. return
  53. }
  54. detail, err := a.templateSvc.GetTemplateDetail(r.Context(), templateID)
  55. if err != nil {
  56. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  57. return
  58. }
  59. writeJSON(w, http.StatusOK, detail)
  60. }
  61. func (a *API) OnboardTemplate(w http.ResponseWriter, r *http.Request) {
  62. rawID := chi.URLParam(r, "id")
  63. templateID, err := strconv.ParseInt(rawID, 10, 64)
  64. if err != nil {
  65. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"})
  66. return
  67. }
  68. manifest, fields, err := a.onboardSvc.OnboardTemplate(r.Context(), templateID)
  69. if err != nil {
  70. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  71. return
  72. }
  73. writeJSON(w, http.StatusOK, map[string]any{
  74. "manifestId": manifest.ID,
  75. "fieldCount": len(fields),
  76. "status": "reviewed",
  77. })
  78. }
  79. type updateTemplateFieldsRequest struct {
  80. ManifestID string `json:"manifestId"`
  81. Fields []updateTemplateFieldItem `json:"fields"`
  82. }
  83. type updateTemplateFieldItem struct {
  84. Path string `json:"path"`
  85. IsEnabled *bool `json:"isEnabled,omitempty"`
  86. IsRequiredByUs *bool `json:"isRequiredByUs,omitempty"`
  87. DisplayLabel *string `json:"displayLabel,omitempty"`
  88. DisplayOrder *int `json:"displayOrder,omitempty"`
  89. WebsiteSection *string `json:"websiteSection,omitempty"`
  90. Notes *string `json:"notes,omitempty"`
  91. }
  92. func (a *API) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) {
  93. rawID := chi.URLParam(r, "id")
  94. templateID, err := strconv.ParseInt(rawID, 10, 64)
  95. if err != nil {
  96. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"})
  97. return
  98. }
  99. var req updateTemplateFieldsRequest
  100. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  101. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  102. return
  103. }
  104. if len(req.Fields) == 0 {
  105. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "fields is required"})
  106. return
  107. }
  108. patches := make([]onboarding.FieldPatch, 0, len(req.Fields))
  109. for _, f := range req.Fields {
  110. patches = append(patches, onboarding.FieldPatch{
  111. Path: f.Path,
  112. IsEnabled: f.IsEnabled,
  113. IsRequiredByUs: f.IsRequiredByUs,
  114. DisplayLabel: f.DisplayLabel,
  115. DisplayOrder: f.DisplayOrder,
  116. WebsiteSection: f.WebsiteSection,
  117. Notes: f.Notes,
  118. })
  119. }
  120. manifest, fields, err := a.onboardSvc.UpdateTemplateFields(r.Context(), templateID, req.ManifestID, patches)
  121. if err != nil {
  122. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  123. return
  124. }
  125. writeJSON(w, http.StatusOK, map[string]any{
  126. "templateId": templateID,
  127. "manifestId": manifest.ID,
  128. "fieldCount": len(fields),
  129. "fields": fields,
  130. })
  131. }
  132. func (a *API) StartBuild(w http.ResponseWriter, r *http.Request) {
  133. var req buildsvc.StartBuildRequest
  134. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  135. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  136. return
  137. }
  138. result, err := a.buildSvc.StartBuild(r.Context(), req)
  139. if err != nil {
  140. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  141. return
  142. }
  143. writeJSON(w, http.StatusAccepted, result)
  144. }
  145. type upsertDraftRequest struct {
  146. TemplateID *int64 `json:"templateId,omitempty"`
  147. ManifestID string `json:"manifestId"`
  148. Source string `json:"source"`
  149. RequestName string `json:"requestName"`
  150. GlobalData map[string]any `json:"globalData"`
  151. FieldValues map[string]string `json:"fieldValues"`
  152. DraftContext *domain.DraftContext `json:"draftContext,omitempty"`
  153. Status string `json:"status"`
  154. Notes string `json:"notes"`
  155. }
  156. type intakeDraftRequest struct {
  157. DraftID string `json:"draftId,omitempty"`
  158. Source string `json:"source"`
  159. RequestName string `json:"requestName"`
  160. TemplateID *int64 `json:"templateId,omitempty"`
  161. GlobalData map[string]any `json:"globalData"`
  162. Notes string `json:"notes"`
  163. WebsiteURL string `json:"websiteUrl,omitempty"`
  164. WebsiteSummary string `json:"websiteSummary,omitempty"`
  165. BusinessType string `json:"businessType,omitempty"`
  166. LocaleStyle string `json:"localeStyle,omitempty"`
  167. MarketStyle string `json:"marketStyle,omitempty"`
  168. AddressMode string `json:"addressMode,omitempty"`
  169. ContentTone string `json:"contentTone,omitempty"`
  170. PromptInstructions string `json:"promptInstructions,omitempty"`
  171. StyleProfile *domain.DraftStyleProfile `json:"styleProfile,omitempty"`
  172. }
  173. func (a *API) IntakeDraft(w http.ResponseWriter, r *http.Request) {
  174. var req intakeDraftRequest
  175. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  176. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  177. return
  178. }
  179. globalData := req.GlobalData
  180. if globalData == nil {
  181. globalData = map[string]any{}
  182. }
  183. if strings.TrimSpace(req.BusinessType) != "" && strings.TrimSpace(getMapString(globalData, "businessType")) == "" {
  184. globalData["businessType"] = strings.TrimSpace(req.BusinessType)
  185. }
  186. styleProfile := domain.DraftStyleProfile{
  187. LocaleStyle: strings.TrimSpace(req.LocaleStyle),
  188. MarketStyle: strings.TrimSpace(req.MarketStyle),
  189. AddressMode: strings.TrimSpace(req.AddressMode),
  190. ContentTone: strings.TrimSpace(req.ContentTone),
  191. PromptInstructions: strings.TrimSpace(req.PromptInstructions),
  192. }
  193. if req.StyleProfile != nil {
  194. styleProfile = *req.StyleProfile
  195. if styleProfile.LocaleStyle == "" {
  196. styleProfile.LocaleStyle = strings.TrimSpace(req.LocaleStyle)
  197. }
  198. if styleProfile.MarketStyle == "" {
  199. styleProfile.MarketStyle = strings.TrimSpace(req.MarketStyle)
  200. }
  201. if styleProfile.AddressMode == "" {
  202. styleProfile.AddressMode = strings.TrimSpace(req.AddressMode)
  203. }
  204. if styleProfile.ContentTone == "" {
  205. styleProfile.ContentTone = strings.TrimSpace(req.ContentTone)
  206. }
  207. if styleProfile.PromptInstructions == "" {
  208. styleProfile.PromptInstructions = strings.TrimSpace(req.PromptInstructions)
  209. }
  210. }
  211. businessType := strings.TrimSpace(req.BusinessType)
  212. if businessType == "" {
  213. businessType = strings.TrimSpace(getMapString(globalData, "businessType"))
  214. }
  215. draftContext := &domain.DraftContext{
  216. IntakeSource: strings.TrimSpace(req.Source),
  217. LLM: domain.DraftLLMContext{
  218. BusinessType: businessType,
  219. WebsiteURL: strings.TrimSpace(req.WebsiteURL),
  220. WebsiteSummary: strings.TrimSpace(req.WebsiteSummary),
  221. StyleProfile: styleProfile,
  222. },
  223. }
  224. draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
  225. DraftID: strings.TrimSpace(req.DraftID),
  226. TemplateID: req.TemplateID,
  227. Source: defaultStr(req.Source, "intake-api"),
  228. RequestName: req.RequestName,
  229. GlobalData: globalData,
  230. FieldValues: map[string]string{},
  231. DraftContext: draftContext,
  232. Status: "draft",
  233. Notes: req.Notes,
  234. })
  235. if err != nil {
  236. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  237. return
  238. }
  239. if strings.TrimSpace(req.DraftID) == "" {
  240. writeJSON(w, http.StatusCreated, draft)
  241. return
  242. }
  243. writeJSON(w, http.StatusOK, draft)
  244. }
  245. func (a *API) ListDrafts(w http.ResponseWriter, r *http.Request) {
  246. limit, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("limit")))
  247. drafts, err := a.draftSvc.ListDrafts(r.Context(), limit)
  248. if err != nil {
  249. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  250. return
  251. }
  252. writeJSON(w, http.StatusOK, map[string]any{"count": len(drafts), "drafts": drafts})
  253. }
  254. func (a *API) GetDraft(w http.ResponseWriter, r *http.Request) {
  255. draftID := strings.TrimSpace(chi.URLParam(r, "id"))
  256. draft, err := a.draftSvc.GetDraft(r.Context(), draftID)
  257. if err != nil {
  258. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  259. return
  260. }
  261. writeJSON(w, http.StatusOK, draft)
  262. }
  263. func (a *API) UpdateDraft(w http.ResponseWriter, r *http.Request) {
  264. draftID := strings.TrimSpace(chi.URLParam(r, "id"))
  265. var req upsertDraftRequest
  266. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  267. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  268. return
  269. }
  270. draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
  271. DraftID: draftID,
  272. TemplateID: req.TemplateID,
  273. ManifestID: req.ManifestID,
  274. Source: req.Source,
  275. RequestName: req.RequestName,
  276. GlobalData: req.GlobalData,
  277. FieldValues: req.FieldValues,
  278. DraftContext: req.DraftContext,
  279. Status: req.Status,
  280. Notes: req.Notes,
  281. })
  282. if err != nil {
  283. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  284. return
  285. }
  286. writeJSON(w, http.StatusOK, draft)
  287. }
  288. func (a *API) GetBuild(w http.ResponseWriter, r *http.Request) {
  289. buildID := chi.URLParam(r, "id")
  290. build, err := a.buildSvc.GetBuild(r.Context(), buildID)
  291. if err != nil {
  292. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  293. return
  294. }
  295. writeJSON(w, http.StatusOK, build)
  296. }
  297. func (a *API) PollBuildOnce(w http.ResponseWriter, r *http.Request) {
  298. buildID := chi.URLParam(r, "id")
  299. if err := a.buildSvc.PollOnce(r.Context(), buildID); err != nil {
  300. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  301. return
  302. }
  303. build, err := a.buildSvc.GetBuild(r.Context(), buildID)
  304. if err != nil {
  305. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  306. return
  307. }
  308. writeJSON(w, http.StatusOK, build)
  309. }
  310. func (a *API) FetchBuildEditorURL(w http.ResponseWriter, r *http.Request) {
  311. buildID := chi.URLParam(r, "id")
  312. if err := a.buildSvc.FetchEditorURL(r.Context(), buildID); err != nil {
  313. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  314. return
  315. }
  316. build, err := a.buildSvc.GetBuild(r.Context(), buildID)
  317. if err != nil {
  318. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  319. return
  320. }
  321. writeJSON(w, http.StatusOK, build)
  322. }
  323. func writeJSON(w http.ResponseWriter, status int, v any) {
  324. w.Header().Set("Content-Type", "application/json")
  325. w.WriteHeader(status)
  326. _ = json.NewEncoder(w).Encode(v)
  327. }
  328. func defaultStr(v, fallback string) string {
  329. if strings.TrimSpace(v) == "" {
  330. return fallback
  331. }
  332. return strings.TrimSpace(v)
  333. }
  334. func getMapString(values map[string]any, key string) string {
  335. if values == nil {
  336. return ""
  337. }
  338. raw, _ := values[key].(string)
  339. return raw
  340. }