Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

381 řádky
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. SuggestionState *domain.DraftSuggestionState `json:"suggestionState,omitempty"`
  154. Status string `json:"status"`
  155. Notes string `json:"notes"`
  156. }
  157. type intakeDraftRequest struct {
  158. DraftID string `json:"draftId,omitempty"`
  159. Source string `json:"source"`
  160. RequestName string `json:"requestName"`
  161. TemplateID *int64 `json:"templateId,omitempty"`
  162. GlobalData map[string]any `json:"globalData"`
  163. Notes string `json:"notes"`
  164. WebsiteURL string `json:"websiteUrl,omitempty"`
  165. WebsiteSummary string `json:"websiteSummary,omitempty"`
  166. BusinessType string `json:"businessType,omitempty"`
  167. LocaleStyle string `json:"localeStyle,omitempty"`
  168. MarketStyle string `json:"marketStyle,omitempty"`
  169. AddressMode string `json:"addressMode,omitempty"`
  170. ContentTone string `json:"contentTone,omitempty"`
  171. PromptInstructions string `json:"promptInstructions,omitempty"`
  172. StyleProfile *domain.DraftStyleProfile `json:"styleProfile,omitempty"`
  173. }
  174. func (a *API) IntakeDraft(w http.ResponseWriter, r *http.Request) {
  175. var req intakeDraftRequest
  176. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  177. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  178. return
  179. }
  180. globalData := req.GlobalData
  181. if globalData == nil {
  182. globalData = map[string]any{}
  183. }
  184. if strings.TrimSpace(req.BusinessType) != "" && strings.TrimSpace(getMapString(globalData, "businessType")) == "" {
  185. globalData["businessType"] = strings.TrimSpace(req.BusinessType)
  186. }
  187. styleProfile := domain.DraftStyleProfile{
  188. LocaleStyle: strings.TrimSpace(req.LocaleStyle),
  189. MarketStyle: strings.TrimSpace(req.MarketStyle),
  190. AddressMode: strings.TrimSpace(req.AddressMode),
  191. ContentTone: strings.TrimSpace(req.ContentTone),
  192. PromptInstructions: strings.TrimSpace(req.PromptInstructions),
  193. }
  194. if req.StyleProfile != nil {
  195. styleProfile = *req.StyleProfile
  196. if styleProfile.LocaleStyle == "" {
  197. styleProfile.LocaleStyle = strings.TrimSpace(req.LocaleStyle)
  198. }
  199. if styleProfile.MarketStyle == "" {
  200. styleProfile.MarketStyle = strings.TrimSpace(req.MarketStyle)
  201. }
  202. if styleProfile.AddressMode == "" {
  203. styleProfile.AddressMode = strings.TrimSpace(req.AddressMode)
  204. }
  205. if styleProfile.ContentTone == "" {
  206. styleProfile.ContentTone = strings.TrimSpace(req.ContentTone)
  207. }
  208. if styleProfile.PromptInstructions == "" {
  209. styleProfile.PromptInstructions = strings.TrimSpace(req.PromptInstructions)
  210. }
  211. }
  212. businessType := strings.TrimSpace(req.BusinessType)
  213. if businessType == "" {
  214. businessType = strings.TrimSpace(getMapString(globalData, "businessType"))
  215. }
  216. draftContext := &domain.DraftContext{
  217. IntakeSource: strings.TrimSpace(req.Source),
  218. LLM: domain.DraftLLMContext{
  219. BusinessType: businessType,
  220. WebsiteURL: strings.TrimSpace(req.WebsiteURL),
  221. WebsiteSummary: strings.TrimSpace(req.WebsiteSummary),
  222. StyleProfile: styleProfile,
  223. },
  224. }
  225. draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
  226. DraftID: strings.TrimSpace(req.DraftID),
  227. TemplateID: req.TemplateID,
  228. Source: defaultStr(req.Source, "intake-api"),
  229. RequestName: req.RequestName,
  230. GlobalData: globalData,
  231. FieldValues: map[string]string{},
  232. DraftContext: draftContext,
  233. Status: "draft",
  234. Notes: req.Notes,
  235. })
  236. if err != nil {
  237. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  238. return
  239. }
  240. if strings.TrimSpace(req.DraftID) == "" {
  241. writeJSON(w, http.StatusCreated, draft)
  242. return
  243. }
  244. writeJSON(w, http.StatusOK, draft)
  245. }
  246. func (a *API) ListDrafts(w http.ResponseWriter, r *http.Request) {
  247. limit, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("limit")))
  248. drafts, err := a.draftSvc.ListDrafts(r.Context(), limit)
  249. if err != nil {
  250. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  251. return
  252. }
  253. writeJSON(w, http.StatusOK, map[string]any{"count": len(drafts), "drafts": drafts})
  254. }
  255. func (a *API) GetDraft(w http.ResponseWriter, r *http.Request) {
  256. draftID := strings.TrimSpace(chi.URLParam(r, "id"))
  257. draft, err := a.draftSvc.GetDraft(r.Context(), draftID)
  258. if err != nil {
  259. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  260. return
  261. }
  262. writeJSON(w, http.StatusOK, draft)
  263. }
  264. func (a *API) UpdateDraft(w http.ResponseWriter, r *http.Request) {
  265. draftID := strings.TrimSpace(chi.URLParam(r, "id"))
  266. var req upsertDraftRequest
  267. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  268. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  269. return
  270. }
  271. draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
  272. DraftID: draftID,
  273. TemplateID: req.TemplateID,
  274. ManifestID: req.ManifestID,
  275. Source: req.Source,
  276. RequestName: req.RequestName,
  277. GlobalData: req.GlobalData,
  278. FieldValues: req.FieldValues,
  279. DraftContext: req.DraftContext,
  280. SuggestionState: req.SuggestionState,
  281. Status: req.Status,
  282. Notes: req.Notes,
  283. })
  284. if err != nil {
  285. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  286. return
  287. }
  288. writeJSON(w, http.StatusOK, draft)
  289. }
  290. func (a *API) GetBuild(w http.ResponseWriter, r *http.Request) {
  291. buildID := chi.URLParam(r, "id")
  292. build, err := a.buildSvc.GetBuild(r.Context(), buildID)
  293. if err != nil {
  294. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  295. return
  296. }
  297. writeJSON(w, http.StatusOK, build)
  298. }
  299. func (a *API) PollBuildOnce(w http.ResponseWriter, r *http.Request) {
  300. buildID := chi.URLParam(r, "id")
  301. if err := a.buildSvc.PollOnce(r.Context(), buildID); err != nil {
  302. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  303. return
  304. }
  305. build, err := a.buildSvc.GetBuild(r.Context(), buildID)
  306. if err != nil {
  307. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  308. return
  309. }
  310. writeJSON(w, http.StatusOK, build)
  311. }
  312. func (a *API) FetchBuildEditorURL(w http.ResponseWriter, r *http.Request) {
  313. buildID := chi.URLParam(r, "id")
  314. if err := a.buildSvc.FetchEditorURL(r.Context(), buildID); err != nil {
  315. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  316. return
  317. }
  318. build, err := a.buildSvc.GetBuild(r.Context(), buildID)
  319. if err != nil {
  320. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  321. return
  322. }
  323. writeJSON(w, http.StatusOK, build)
  324. }
  325. func writeJSON(w http.ResponseWriter, status int, v any) {
  326. w.Header().Set("Content-Type", "application/json")
  327. w.WriteHeader(status)
  328. _ = json.NewEncoder(w).Encode(v)
  329. }
  330. func defaultStr(v, fallback string) string {
  331. if strings.TrimSpace(v) == "" {
  332. return fallback
  333. }
  334. return strings.TrimSpace(v)
  335. }
  336. func getMapString(values map[string]any, key string) string {
  337. if values == nil {
  338. return ""
  339. }
  340. raw, _ := values[key].(string)
  341. return raw
  342. }