| @@ -16,6 +16,7 @@ Dieses Dokument definiert projektlokale Leitplanken fuer Menschen und Agenten, d | |||||
| - AI-Template-Sync, Discovery/Onboarding und bearbeitbare Manifest-Felder. | - AI-Template-Sync, Discovery/Onboarding und bearbeitbare Manifest-Felder. | ||||
| - Draft-Intake, Draft-Bearbeitung und Statuswechsel (`draft`, `reviewed`, `submitted`). | - Draft-Intake, Draft-Bearbeitung und Statuswechsel (`draft`, `reviewed`, `submitted`). | ||||
| - Externer Draft-Intake-Vertrag (`POST /api/drafts/intake`) fuer Stammdaten plus optionalen Website-/Stilkontext. | |||||
| - Build-Start aus geprueften Daten, Polling und Editor-URL-Abruf. | - Build-Start aus geprueften Daten, Polling und Editor-URL-Abruf. | ||||
| - SQLite als Default-Datenhaltung fuer lokalen Betrieb. | - SQLite als Default-Datenhaltung fuer lokalen Betrieb. | ||||
| @@ -8,10 +8,12 @@ Die App kann heute: | |||||
| - AI-Templates aus QC synchronisieren und anzeigen. | - AI-Templates aus QC synchronisieren und anzeigen. | ||||
| - Templates onboarden (Discovery/Manifest) und Felder fuer Mapping/Review bearbeiten. | - Templates onboarden (Discovery/Manifest) und Felder fuer Mapping/Review bearbeiten. | ||||
| - Drafts anlegen, aktualisieren und im Status `draft` -> `reviewed` -> `submitted` fuehren. | - Drafts anlegen, aktualisieren und im Status `draft` -> `reviewed` -> `submitted` fuehren. | ||||
| - Externen Draft-Intake ueber `POST /api/drafts/intake` verarbeiten (Stammdaten + optional Website-/Stilkontext, kein Direkt-Build). | |||||
| - Builds aus geprueften Daten starten sowie Job-Status pollen und Editor-URL nachladen. | - Builds aus geprueften Daten starten sowie Job-Status pollen und Editor-URL nachladen. | ||||
| Wichtig: | Wichtig: | ||||
| - Leadharvester-Intake und LLM-Autofill sind geplant, aber noch nicht fertig integriert. | |||||
| - Leadharvester liefert nur Intake-Daten (Stammdaten + optional Kontext) in Drafts. | |||||
| - LLM-Autofill ist noch nicht fertig; vorbereitet sind nur Kontextfelder (Website/Style/Ton) im Draft-Review-Flow. | |||||
| ## Lokaler Start | ## Lokaler Start | ||||
| @@ -36,6 +36,8 @@ Soll-Logik: | |||||
| Aktueller Stand: | Aktueller Stand: | ||||
| - Draft-Erfassung, Listing, Update und UI-Weiterbearbeitung sind vorhanden. | - Draft-Erfassung, Listing, Update und UI-Weiterbearbeitung sind vorhanden. | ||||
| - Definierter externer Intake unter `POST /api/drafts/intake` ist vorhanden; `templateId` ist optional, Build wird dort nicht ausgeloest. | |||||
| - Draft-Kontext fuer spaetere LLM-Unterstuetzung ist vorbereitet (Website-URL, Website-Summary, Stilprofil inkl. locale/market/address mode/tone/prompt instructions). | |||||
| - Build-Start erfordert bereits einen Template-Manifest-Status `reviewed`/`validated`. | - Build-Start erfordert bereits einen Template-Manifest-Status `reviewed`/`validated`. | ||||
| - Prozessuale Review-Gates (z. B. Freigabe-Policy, Rollen, Pflichtchecks pro Feld) sind noch nicht vollstaendig ausgebaut. | - Prozessuale Review-Gates (z. B. Freigabe-Policy, Rollen, Pflichtchecks pro Feld) sind noch nicht vollstaendig ausgebaut. | ||||
| @@ -91,14 +93,14 @@ Statusmarker: | |||||
| - [-] Review-Gate ist noch nicht als strikter, rollenbasierter Freigabeprozess umgesetzt. | - [-] Review-Gate ist noch nicht als strikter, rollenbasierter Freigabeprozess umgesetzt. | ||||
| ### D) Leadharvester-Integration | ### D) Leadharvester-Integration | ||||
| - [ ] Definierter Intake-Adapter von Leadharvester in `POST /api/drafts/intake`. | |||||
| - [ ] Mapping-Contract fuer Stammdaten + optionale Website-Zusammenfassung. | |||||
| - [x] Definierter Intake-Adapter von Leadharvester in `POST /api/drafts/intake`. | |||||
| - [x] Mapping-Contract fuer Stammdaten + optionale Website-Zusammenfassung. | |||||
| - [ ] Monitoring/Fehlerbild fuer Intake-Qualitaet und Nachbearbeitungsquote. | - [ ] Monitoring/Fehlerbild fuer Intake-Qualitaet und Nachbearbeitungsquote. | ||||
| ### E) LLM-Assistenz | ### E) LLM-Assistenz | ||||
| - [ ] Feldvorschlaege fuer fehlende Inhalte im Draft. | - [ ] Feldvorschlaege fuer fehlende Inhalte im Draft. | ||||
| - [ ] Draft-Autofill mit nachvollziehbarer Herkunft je Feld. | - [ ] Draft-Autofill mit nachvollziehbarer Herkunft je Feld. | ||||
| - [ ] Stilprofil-Logik unter Beruecksichtigung von `businessType` + Tonalitaet. | |||||
| - [-] Stilprofil-Logik unter Beruecksichtigung von `businessType` + Tonalitaet (Kontextfelder vorhanden, produktive Vorschlagslogik offen). | |||||
| ### F) Security und Betriebsreife | ### F) Security und Betriebsreife | ||||
| - [ ] Verbindliche Secret-Strategie (verschluesselte Speicherung statt einfacher Platzhalterlogik). | - [ ] Verbindliche Secret-Strategie (verschluesselte Speicherung statt einfacher Platzhalterlogik). | ||||
| @@ -72,17 +72,38 @@ type SiteBuild struct { | |||||
| } | } | ||||
| type BuildDraft struct { | type BuildDraft struct { | ||||
| ID string `json:"id"` | |||||
| TemplateID int64 `json:"templateId"` | |||||
| ManifestID string `json:"manifestId"` | |||||
| Source string `json:"source"` | |||||
| RequestName string `json:"requestName"` | |||||
| GlobalDataJSON json.RawMessage `json:"globalDataJson"` | |||||
| FieldValuesJSON json.RawMessage `json:"fieldValuesJson"` | |||||
| Status string `json:"status"` | |||||
| Notes string `json:"notes"` | |||||
| CreatedAt time.Time `json:"createdAt"` | |||||
| UpdatedAt time.Time `json:"updatedAt"` | |||||
| ID string `json:"id"` | |||||
| TemplateID int64 `json:"templateId"` | |||||
| ManifestID string `json:"manifestId"` | |||||
| Source string `json:"source"` | |||||
| RequestName string `json:"requestName"` | |||||
| GlobalDataJSON json.RawMessage `json:"globalDataJson"` | |||||
| FieldValuesJSON json.RawMessage `json:"fieldValuesJson"` | |||||
| DraftContextJSON json.RawMessage `json:"draftContextJson"` | |||||
| Status string `json:"status"` | |||||
| Notes string `json:"notes"` | |||||
| CreatedAt time.Time `json:"createdAt"` | |||||
| UpdatedAt time.Time `json:"updatedAt"` | |||||
| } | |||||
| type DraftStyleProfile struct { | |||||
| 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"` | |||||
| } | |||||
| type DraftLLMContext struct { | |||||
| BusinessType string `json:"businessType,omitempty"` | |||||
| WebsiteURL string `json:"websiteUrl,omitempty"` | |||||
| WebsiteSummary string `json:"websiteSummary,omitempty"` | |||||
| StyleProfile DraftStyleProfile `json:"styleProfile,omitempty"` | |||||
| } | |||||
| type DraftContext struct { | |||||
| IntakeSource string `json:"intakeSource,omitempty"` | |||||
| LLM DraftLLMContext `json:"llm,omitempty"` | |||||
| } | } | ||||
| type AppSettings struct { | type AppSettings struct { | ||||
| @@ -14,15 +14,16 @@ import ( | |||||
| ) | ) | ||||
| type UpsertDraftRequest struct { | type UpsertDraftRequest struct { | ||||
| DraftID string `json:"draftId,omitempty"` | |||||
| TemplateID int64 `json:"templateId"` | |||||
| ManifestID string `json:"manifestId,omitempty"` | |||||
| Source string `json:"source,omitempty"` | |||||
| RequestName string `json:"requestName,omitempty"` | |||||
| GlobalData map[string]any `json:"globalData"` | |||||
| FieldValues map[string]string `json:"fieldValues"` | |||||
| Status string `json:"status,omitempty"` | |||||
| Notes string `json:"notes,omitempty"` | |||||
| DraftID string `json:"draftId,omitempty"` | |||||
| TemplateID *int64 `json:"templateId,omitempty"` | |||||
| ManifestID string `json:"manifestId,omitempty"` | |||||
| Source string `json:"source,omitempty"` | |||||
| RequestName string `json:"requestName,omitempty"` | |||||
| GlobalData map[string]any `json:"globalData"` | |||||
| FieldValues map[string]string `json:"fieldValues"` | |||||
| DraftContext *domain.DraftContext `json:"draftContext,omitempty"` | |||||
| Status string `json:"status,omitempty"` | |||||
| Notes string `json:"notes,omitempty"` | |||||
| } | } | ||||
| type Service struct { | type Service struct { | ||||
| @@ -40,9 +41,14 @@ func New(draftStore store.DraftStore, templateStore store.TemplateStore, manifes | |||||
| } | } | ||||
| func (s *Service) SaveDraft(ctx context.Context, req UpsertDraftRequest) (*domain.BuildDraft, error) { | func (s *Service) SaveDraft(ctx context.Context, req UpsertDraftRequest) (*domain.BuildDraft, error) { | ||||
| templateID := req.TemplateID | |||||
| templateID := int64(0) | |||||
| if req.TemplateID != nil { | |||||
| templateID = *req.TemplateID | |||||
| } | |||||
| var existing *domain.BuildDraft | |||||
| if strings.TrimSpace(req.DraftID) != "" { | if strings.TrimSpace(req.DraftID) != "" { | ||||
| existing, err := s.drafts.GetDraftByID(ctx, strings.TrimSpace(req.DraftID)) | |||||
| var err error | |||||
| existing, err = s.drafts.GetDraftByID(ctx, strings.TrimSpace(req.DraftID)) | |||||
| if err != nil { | if err != nil { | ||||
| return nil, fmt.Errorf("get draft: %w", err) | return nil, fmt.Errorf("get draft: %w", err) | ||||
| } | } | ||||
| @@ -50,20 +56,21 @@ func (s *Service) SaveDraft(ctx context.Context, req UpsertDraftRequest) (*domai | |||||
| templateID = existing.TemplateID | templateID = existing.TemplateID | ||||
| } | } | ||||
| } | } | ||||
| if templateID <= 0 { | |||||
| return nil, errors.New("templateId is required") | |||||
| } | |||||
| template, err := s.templates.GetTemplateByID(ctx, templateID) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("get template: %w", err) | |||||
| } | |||||
| if !template.IsAITemplate { | |||||
| return nil, errors.New("only ai templates are allowed") | |||||
| if templateID > 0 { | |||||
| template, err := s.templates.GetTemplateByID(ctx, templateID) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("get template: %w", err) | |||||
| } | |||||
| if !template.IsAITemplate { | |||||
| return nil, errors.New("only ai templates are allowed") | |||||
| } | |||||
| } | } | ||||
| manifestID := strings.TrimSpace(req.ManifestID) | manifestID := strings.TrimSpace(req.ManifestID) | ||||
| if manifestID == "" { | |||||
| if templateID <= 0 { | |||||
| manifestID = "" | |||||
| } else if manifestID == "" { | |||||
| manifest, err := s.manifests.GetActiveManifestByTemplateID(ctx, templateID) | manifest, err := s.manifests.GetActiveManifestByTemplateID(ctx, templateID) | ||||
| if err != nil { | if err != nil { | ||||
| return nil, fmt.Errorf("get active manifest: %w", err) | return nil, fmt.Errorf("get active manifest: %w", err) | ||||
| @@ -79,6 +86,10 @@ func (s *Service) SaveDraft(ctx context.Context, req UpsertDraftRequest) (*domai | |||||
| if err != nil { | if err != nil { | ||||
| return nil, errors.New("fieldValues is invalid JSON") | return nil, errors.New("fieldValues is invalid JSON") | ||||
| } | } | ||||
| draftContextJSON, err := buildDraftContextJSON(req, existing) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| now := time.Now().UTC() | now := time.Now().UTC() | ||||
| source := defaultString(req.Source, "ui") | source := defaultString(req.Source, "ui") | ||||
| @@ -88,16 +99,17 @@ func (s *Service) SaveDraft(ctx context.Context, req UpsertDraftRequest) (*domai | |||||
| } | } | ||||
| draft := domain.BuildDraft{ | draft := domain.BuildDraft{ | ||||
| ID: strings.TrimSpace(req.DraftID), | |||||
| TemplateID: templateID, | |||||
| ManifestID: manifestID, | |||||
| Source: source, | |||||
| RequestName: strings.TrimSpace(req.RequestName), | |||||
| GlobalDataJSON: globalDataJSON, | |||||
| FieldValuesJSON: fieldValuesJSON, | |||||
| Status: status, | |||||
| Notes: strings.TrimSpace(req.Notes), | |||||
| UpdatedAt: now, | |||||
| ID: strings.TrimSpace(req.DraftID), | |||||
| TemplateID: templateID, | |||||
| ManifestID: manifestID, | |||||
| Source: source, | |||||
| RequestName: strings.TrimSpace(req.RequestName), | |||||
| GlobalDataJSON: globalDataJSON, | |||||
| FieldValuesJSON: fieldValuesJSON, | |||||
| DraftContextJSON: draftContextJSON, | |||||
| Status: status, | |||||
| Notes: strings.TrimSpace(req.Notes), | |||||
| UpdatedAt: now, | |||||
| } | } | ||||
| if draft.ID == "" { | if draft.ID == "" { | ||||
| draft.ID = strconv.FormatInt(time.Now().UnixNano(), 10) | draft.ID = strconv.FormatInt(time.Now().UnixNano(), 10) | ||||
| @@ -119,6 +131,20 @@ func (s *Service) SaveDraft(ctx context.Context, req UpsertDraftRequest) (*domai | |||||
| return s.drafts.GetDraftByID(ctx, draft.ID) | return s.drafts.GetDraftByID(ctx, draft.ID) | ||||
| } | } | ||||
| func buildDraftContextJSON(req UpsertDraftRequest, existing *domain.BuildDraft) (json.RawMessage, error) { | |||||
| if req.DraftContext == nil { | |||||
| if existing != nil { | |||||
| return existing.DraftContextJSON, nil | |||||
| } | |||||
| return nil, nil | |||||
| } | |||||
| raw, err := json.Marshal(req.DraftContext) | |||||
| if err != nil { | |||||
| return nil, errors.New("draftContext is invalid JSON") | |||||
| } | |||||
| return raw, nil | |||||
| } | |||||
| func (s *Service) GetDraft(ctx context.Context, draftID string) (*domain.BuildDraft, error) { | func (s *Service) GetDraft(ctx context.Context, draftID string) (*domain.BuildDraft, error) { | ||||
| return s.drafts.GetDraftByID(ctx, strings.TrimSpace(draftID)) | return s.drafts.GetDraftByID(ctx, strings.TrimSpace(draftID)) | ||||
| } | } | ||||
| @@ -9,6 +9,7 @@ import ( | |||||
| "github.com/go-chi/chi/v5" | "github.com/go-chi/chi/v5" | ||||
| "qctextbuilder/internal/buildsvc" | "qctextbuilder/internal/buildsvc" | ||||
| "qctextbuilder/internal/domain" | |||||
| "qctextbuilder/internal/draftsvc" | "qctextbuilder/internal/draftsvc" | ||||
| "qctextbuilder/internal/onboarding" | "qctextbuilder/internal/onboarding" | ||||
| "qctextbuilder/internal/templatesvc" | "qctextbuilder/internal/templatesvc" | ||||
| @@ -164,37 +165,109 @@ func (a *API) StartBuild(w http.ResponseWriter, r *http.Request) { | |||||
| } | } | ||||
| type upsertDraftRequest struct { | type upsertDraftRequest struct { | ||||
| TemplateID int64 `json:"templateId"` | |||||
| ManifestID string `json:"manifestId"` | |||||
| Source string `json:"source"` | |||||
| RequestName string `json:"requestName"` | |||||
| GlobalData map[string]any `json:"globalData"` | |||||
| FieldValues map[string]string `json:"fieldValues"` | |||||
| Status string `json:"status"` | |||||
| Notes string `json:"notes"` | |||||
| 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"` | |||||
| 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) { | func (a *API) IntakeDraft(w http.ResponseWriter, r *http.Request) { | ||||
| var req upsertDraftRequest | |||||
| var req intakeDraftRequest | |||||
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | ||||
| writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"}) | writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"}) | ||||
| return | 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{ | draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{ | ||||
| TemplateID: req.TemplateID, | |||||
| ManifestID: req.ManifestID, | |||||
| Source: defaultStr(req.Source, "intake-api"), | |||||
| RequestName: req.RequestName, | |||||
| GlobalData: req.GlobalData, | |||||
| FieldValues: req.FieldValues, | |||||
| Status: defaultStr(req.Status, "draft"), | |||||
| Notes: req.Notes, | |||||
| 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 { | if err != nil { | ||||
| writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) | writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) | ||||
| return | return | ||||
| } | } | ||||
| writeJSON(w, http.StatusCreated, draft) | |||||
| 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) { | func (a *API) ListDrafts(w http.ResponseWriter, r *http.Request) { | ||||
| @@ -225,15 +298,16 @@ func (a *API) UpdateDraft(w http.ResponseWriter, r *http.Request) { | |||||
| return | return | ||||
| } | } | ||||
| draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{ | 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, | |||||
| Status: req.Status, | |||||
| Notes: req.Notes, | |||||
| DraftID: draftID, | |||||
| TemplateID: req.TemplateID, | |||||
| ManifestID: req.ManifestID, | |||||
| Source: req.Source, | |||||
| RequestName: req.RequestName, | |||||
| GlobalData: req.GlobalData, | |||||
| FieldValues: req.FieldValues, | |||||
| DraftContext: req.DraftContext, | |||||
| Status: req.Status, | |||||
| Notes: req.Notes, | |||||
| }) | }) | ||||
| if err != nil { | if err != nil { | ||||
| writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) | writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) | ||||
| @@ -294,3 +368,11 @@ func defaultStr(v, fallback string) string { | |||||
| } | } | ||||
| return strings.TrimSpace(v) | return strings.TrimSpace(v) | ||||
| } | } | ||||
| func getMapString(values map[string]any, key string) string { | |||||
| if values == nil { | |||||
| return "" | |||||
| } | |||||
| raw, _ := values[key].(string) | |||||
| return raw | |||||
| } | |||||
| @@ -145,28 +145,35 @@ type buildNewPageData struct { | |||||
| } | } | ||||
| type buildFormInput struct { | type buildFormInput struct { | ||||
| DraftID string | |||||
| DraftSource string | |||||
| DraftStatus string | |||||
| DraftNotes string | |||||
| RequestName string | |||||
| CompanyName string | |||||
| BusinessType string | |||||
| Username string | |||||
| Email string | |||||
| Phone string | |||||
| OrgNumber string | |||||
| StartDate string | |||||
| Mission string | |||||
| DescriptionShort string | |||||
| DescriptionLong string | |||||
| SiteLanguage string | |||||
| AddressLine1 string | |||||
| AddressLine2 string | |||||
| AddressCity string | |||||
| AddressRegion string | |||||
| AddressZIP string | |||||
| AddressCountry string | |||||
| DraftID string | |||||
| DraftSource string | |||||
| DraftStatus string | |||||
| DraftNotes string | |||||
| RequestName string | |||||
| CompanyName string | |||||
| BusinessType string | |||||
| Username string | |||||
| Email string | |||||
| Phone string | |||||
| OrgNumber string | |||||
| StartDate string | |||||
| Mission string | |||||
| DescriptionShort string | |||||
| DescriptionLong string | |||||
| SiteLanguage string | |||||
| AddressLine1 string | |||||
| AddressLine2 string | |||||
| AddressCity string | |||||
| AddressRegion string | |||||
| AddressZIP string | |||||
| AddressCountry string | |||||
| WebsiteURL string | |||||
| WebsiteSummary string | |||||
| LocaleStyle string | |||||
| MarketStyle string | |||||
| AddressMode string | |||||
| ContentTone string | |||||
| PromptInstructions string | |||||
| } | } | ||||
| type buildDetailPageData struct { | type buildDetailPageData struct { | ||||
| @@ -391,15 +398,16 @@ func (u *UI) CreateBuild(w http.ResponseWriter, r *http.Request) { | |||||
| if form.DraftID != "" { | if form.DraftID != "" { | ||||
| _, _ = u.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{ | _, _ = u.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{ | ||||
| DraftID: form.DraftID, | |||||
| TemplateID: templateID, | |||||
| ManifestID: strings.TrimSpace(r.FormValue("manifest_id")), | |||||
| Source: form.DraftSource, | |||||
| RequestName: form.RequestName, | |||||
| GlobalData: globalData, | |||||
| FieldValues: fieldValues, | |||||
| Status: "submitted", | |||||
| Notes: form.DraftNotes, | |||||
| DraftID: form.DraftID, | |||||
| TemplateID: int64Ptr(templateID), | |||||
| ManifestID: strings.TrimSpace(r.FormValue("manifest_id")), | |||||
| Source: form.DraftSource, | |||||
| RequestName: form.RequestName, | |||||
| GlobalData: globalData, | |||||
| FieldValues: fieldValues, | |||||
| DraftContext: buildDraftContextFromForm(form, globalData), | |||||
| Status: "submitted", | |||||
| Notes: form.DraftNotes, | |||||
| }) | }) | ||||
| } | } | ||||
| @@ -438,15 +446,16 @@ func (u *UI) SaveDraft(w http.ResponseWriter, r *http.Request) { | |||||
| AddressCountry: form.AddressCountry, | AddressCountry: form.AddressCountry, | ||||
| }) | }) | ||||
| draft, err := u.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{ | draft, err := u.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{ | ||||
| DraftID: form.DraftID, | |||||
| TemplateID: templateID, | |||||
| ManifestID: strings.TrimSpace(r.FormValue("manifest_id")), | |||||
| Source: form.DraftSource, | |||||
| RequestName: form.RequestName, | |||||
| GlobalData: globalData, | |||||
| FieldValues: fieldValues, | |||||
| Status: defaultDraftStatus(form.DraftStatus), | |||||
| Notes: form.DraftNotes, | |||||
| DraftID: form.DraftID, | |||||
| TemplateID: int64Ptr(templateID), | |||||
| ManifestID: strings.TrimSpace(r.FormValue("manifest_id")), | |||||
| Source: form.DraftSource, | |||||
| RequestName: form.RequestName, | |||||
| GlobalData: globalData, | |||||
| FieldValues: fieldValues, | |||||
| DraftContext: buildDraftContextFromForm(form, globalData), | |||||
| Status: defaultDraftStatus(form.DraftStatus), | |||||
| Notes: form.DraftNotes, | |||||
| }) | }) | ||||
| if err != nil { | if err != nil { | ||||
| data, loadErr := u.loadBuildNewPageData(r, pageData{ | data, loadErr := u.loadBuildNewPageData(r, pageData{ | ||||
| @@ -532,8 +541,9 @@ func urlQuery(s string) string { | |||||
| return url.QueryEscape(s) | return url.QueryEscape(s) | ||||
| } | } | ||||
| func boolPtr(v bool) *bool { return &v } | |||||
| func intPtr(v int) *int { return &v } | |||||
| func boolPtr(v bool) *bool { return &v } | |||||
| func intPtr(v int) *int { return &v } | |||||
| func int64Ptr(v int64) *int64 { return &v } | |||||
| func strPtr(v string) *string { | func strPtr(v string) *string { | ||||
| return &v | return &v | ||||
| } | } | ||||
| @@ -1127,28 +1137,35 @@ func humanizeKey(key string) string { | |||||
| func buildFormInputFromRequest(r *http.Request) buildFormInput { | func buildFormInputFromRequest(r *http.Request) buildFormInput { | ||||
| return buildFormInput{ | return buildFormInput{ | ||||
| DraftID: strings.TrimSpace(r.FormValue("draft_id")), | |||||
| DraftSource: strings.TrimSpace(r.FormValue("draft_source")), | |||||
| DraftStatus: strings.TrimSpace(r.FormValue("draft_status")), | |||||
| DraftNotes: strings.TrimSpace(r.FormValue("draft_notes")), | |||||
| RequestName: strings.TrimSpace(r.FormValue("request_name")), | |||||
| CompanyName: strings.TrimSpace(r.FormValue("company_name")), | |||||
| BusinessType: strings.TrimSpace(r.FormValue("business_type")), | |||||
| Username: strings.TrimSpace(r.FormValue("username")), | |||||
| Email: strings.TrimSpace(r.FormValue("email")), | |||||
| Phone: strings.TrimSpace(r.FormValue("phone")), | |||||
| OrgNumber: strings.TrimSpace(r.FormValue("org_number")), | |||||
| StartDate: strings.TrimSpace(r.FormValue("start_date")), | |||||
| Mission: strings.TrimSpace(r.FormValue("mission")), | |||||
| DescriptionShort: strings.TrimSpace(r.FormValue("description_short")), | |||||
| DescriptionLong: strings.TrimSpace(r.FormValue("description_long")), | |||||
| SiteLanguage: strings.TrimSpace(r.FormValue("site_language")), | |||||
| AddressLine1: strings.TrimSpace(r.FormValue("address_line1")), | |||||
| AddressLine2: strings.TrimSpace(r.FormValue("address_line2")), | |||||
| AddressCity: strings.TrimSpace(r.FormValue("address_city")), | |||||
| AddressRegion: strings.TrimSpace(r.FormValue("address_region")), | |||||
| AddressZIP: strings.TrimSpace(r.FormValue("address_zip")), | |||||
| AddressCountry: strings.TrimSpace(r.FormValue("address_country")), | |||||
| DraftID: strings.TrimSpace(r.FormValue("draft_id")), | |||||
| DraftSource: strings.TrimSpace(r.FormValue("draft_source")), | |||||
| DraftStatus: strings.TrimSpace(r.FormValue("draft_status")), | |||||
| DraftNotes: strings.TrimSpace(r.FormValue("draft_notes")), | |||||
| RequestName: strings.TrimSpace(r.FormValue("request_name")), | |||||
| CompanyName: strings.TrimSpace(r.FormValue("company_name")), | |||||
| BusinessType: strings.TrimSpace(r.FormValue("business_type")), | |||||
| Username: strings.TrimSpace(r.FormValue("username")), | |||||
| Email: strings.TrimSpace(r.FormValue("email")), | |||||
| Phone: strings.TrimSpace(r.FormValue("phone")), | |||||
| OrgNumber: strings.TrimSpace(r.FormValue("org_number")), | |||||
| StartDate: strings.TrimSpace(r.FormValue("start_date")), | |||||
| Mission: strings.TrimSpace(r.FormValue("mission")), | |||||
| DescriptionShort: strings.TrimSpace(r.FormValue("description_short")), | |||||
| DescriptionLong: strings.TrimSpace(r.FormValue("description_long")), | |||||
| SiteLanguage: strings.TrimSpace(r.FormValue("site_language")), | |||||
| AddressLine1: strings.TrimSpace(r.FormValue("address_line1")), | |||||
| AddressLine2: strings.TrimSpace(r.FormValue("address_line2")), | |||||
| AddressCity: strings.TrimSpace(r.FormValue("address_city")), | |||||
| AddressRegion: strings.TrimSpace(r.FormValue("address_region")), | |||||
| AddressZIP: strings.TrimSpace(r.FormValue("address_zip")), | |||||
| AddressCountry: strings.TrimSpace(r.FormValue("address_country")), | |||||
| WebsiteURL: strings.TrimSpace(r.FormValue("website_url")), | |||||
| WebsiteSummary: strings.TrimSpace(r.FormValue("website_summary")), | |||||
| LocaleStyle: strings.TrimSpace(r.FormValue("locale_style")), | |||||
| MarketStyle: strings.TrimSpace(r.FormValue("market_style")), | |||||
| AddressMode: strings.TrimSpace(r.FormValue("address_mode")), | |||||
| ContentTone: strings.TrimSpace(r.FormValue("content_tone")), | |||||
| PromptInstructions: strings.TrimSpace(r.FormValue("prompt_instructions")), | |||||
| } | } | ||||
| } | } | ||||
| @@ -1161,6 +1178,7 @@ func buildFormInputFromDraft(draft *domain.BuildDraft) buildFormInput { | |||||
| RequestName: draft.RequestName, | RequestName: draft.RequestName, | ||||
| } | } | ||||
| mergeGlobalDataIntoForm(&form, draft.GlobalDataJSON) | mergeGlobalDataIntoForm(&form, draft.GlobalDataJSON) | ||||
| mergeDraftContextIntoForm(&form, draft.DraftContextJSON) | |||||
| return form | return form | ||||
| } | } | ||||
| @@ -1234,6 +1252,48 @@ func mergeGlobalDataIntoForm(form *buildFormInput, raw []byte) { | |||||
| form.AddressCountry = getString(address["country"]) | form.AddressCountry = getString(address["country"]) | ||||
| } | } | ||||
| func mergeDraftContextIntoForm(form *buildFormInput, raw []byte) { | |||||
| if form == nil || len(raw) == 0 { | |||||
| return | |||||
| } | |||||
| var ctx domain.DraftContext | |||||
| if err := json.Unmarshal(raw, &ctx); err != nil { | |||||
| return | |||||
| } | |||||
| if strings.TrimSpace(form.BusinessType) == "" { | |||||
| form.BusinessType = strings.TrimSpace(ctx.LLM.BusinessType) | |||||
| } | |||||
| form.WebsiteURL = strings.TrimSpace(ctx.LLM.WebsiteURL) | |||||
| form.WebsiteSummary = strings.TrimSpace(ctx.LLM.WebsiteSummary) | |||||
| form.LocaleStyle = strings.TrimSpace(ctx.LLM.StyleProfile.LocaleStyle) | |||||
| form.MarketStyle = strings.TrimSpace(ctx.LLM.StyleProfile.MarketStyle) | |||||
| form.AddressMode = strings.TrimSpace(ctx.LLM.StyleProfile.AddressMode) | |||||
| form.ContentTone = strings.TrimSpace(ctx.LLM.StyleProfile.ContentTone) | |||||
| form.PromptInstructions = strings.TrimSpace(ctx.LLM.StyleProfile.PromptInstructions) | |||||
| } | |||||
| func buildDraftContextFromForm(form buildFormInput, globalData map[string]any) *domain.DraftContext { | |||||
| businessType := strings.TrimSpace(form.BusinessType) | |||||
| if businessType == "" { | |||||
| businessType = strings.TrimSpace(getString(globalData["businessType"])) | |||||
| } | |||||
| return &domain.DraftContext{ | |||||
| IntakeSource: strings.TrimSpace(form.DraftSource), | |||||
| LLM: domain.DraftLLMContext{ | |||||
| BusinessType: businessType, | |||||
| WebsiteURL: strings.TrimSpace(form.WebsiteURL), | |||||
| WebsiteSummary: strings.TrimSpace(form.WebsiteSummary), | |||||
| StyleProfile: domain.DraftStyleProfile{ | |||||
| LocaleStyle: strings.TrimSpace(form.LocaleStyle), | |||||
| MarketStyle: strings.TrimSpace(form.MarketStyle), | |||||
| AddressMode: strings.TrimSpace(form.AddressMode), | |||||
| ContentTone: strings.TrimSpace(form.ContentTone), | |||||
| PromptInstructions: strings.TrimSpace(form.PromptInstructions), | |||||
| }, | |||||
| }, | |||||
| } | |||||
| } | |||||
| func getString(v any) string { | func getString(v any) string { | ||||
| s, _ := v.(string) | s, _ := v.(string) | ||||
| return strings.TrimSpace(s) | return strings.TrimSpace(s) | ||||
| @@ -288,6 +288,7 @@ func (s *Store) GetDraftByID(_ context.Context, id string) (*domain.BuildDraft, | |||||
| copy := draft | copy := draft | ||||
| copy.GlobalDataJSON = cloneRaw(draft.GlobalDataJSON) | copy.GlobalDataJSON = cloneRaw(draft.GlobalDataJSON) | ||||
| copy.FieldValuesJSON = cloneRaw(draft.FieldValuesJSON) | copy.FieldValuesJSON = cloneRaw(draft.FieldValuesJSON) | ||||
| copy.DraftContextJSON = cloneRaw(draft.DraftContextJSON) | |||||
| return ©, nil | return ©, nil | ||||
| } | } | ||||
| @@ -299,6 +300,7 @@ func (s *Store) ListDrafts(_ context.Context, limit int) ([]domain.BuildDraft, e | |||||
| copy := draft | copy := draft | ||||
| copy.GlobalDataJSON = cloneRaw(draft.GlobalDataJSON) | copy.GlobalDataJSON = cloneRaw(draft.GlobalDataJSON) | ||||
| copy.FieldValuesJSON = cloneRaw(draft.FieldValuesJSON) | copy.FieldValuesJSON = cloneRaw(draft.FieldValuesJSON) | ||||
| copy.DraftContextJSON = cloneRaw(draft.DraftContextJSON) | |||||
| out = append(out, copy) | out = append(out, copy) | ||||
| } | } | ||||
| sort.Slice(out, func(i, j int) bool { | sort.Slice(out, func(i, j int) bool { | ||||
| @@ -0,0 +1,33 @@ | |||||
| PRAGMA foreign_keys=OFF; | |||||
| CREATE TABLE IF NOT EXISTS build_drafts_new ( | |||||
| id TEXT PRIMARY KEY, | |||||
| template_id INTEGER, | |||||
| manifest_id TEXT NOT NULL DEFAULT '', | |||||
| source TEXT NOT NULL DEFAULT 'ui', | |||||
| request_name TEXT NOT NULL DEFAULT '', | |||||
| global_data_json BLOB, | |||||
| field_values_json BLOB, | |||||
| draft_context_json BLOB, | |||||
| status TEXT NOT NULL DEFAULT 'draft', | |||||
| notes TEXT NOT NULL DEFAULT '', | |||||
| created_at TEXT NOT NULL, | |||||
| updated_at TEXT NOT NULL, | |||||
| FOREIGN KEY (template_id) REFERENCES qc_templates(id) ON DELETE RESTRICT | |||||
| ); | |||||
| INSERT INTO build_drafts_new ( | |||||
| id, template_id, manifest_id, source, request_name, global_data_json, | |||||
| field_values_json, draft_context_json, status, notes, created_at, updated_at | |||||
| ) | |||||
| SELECT | |||||
| id, template_id, manifest_id, source, request_name, global_data_json, | |||||
| field_values_json, NULL, status, notes, created_at, updated_at | |||||
| FROM build_drafts; | |||||
| DROP TABLE build_drafts; | |||||
| ALTER TABLE build_drafts_new RENAME TO build_drafts; | |||||
| CREATE INDEX IF NOT EXISTS idx_drafts_updated_at ON build_drafts(updated_at DESC); | |||||
| PRAGMA foreign_keys=ON; | |||||
| @@ -452,10 +452,10 @@ func (s *Store) CreateDraft(ctx context.Context, draft domain.BuildDraft) error | |||||
| _, err := s.db.ExecContext(ctx, ` | _, err := s.db.ExecContext(ctx, ` | ||||
| INSERT INTO build_drafts ( | INSERT INTO build_drafts ( | ||||
| id, template_id, manifest_id, source, request_name, global_data_json, | id, template_id, manifest_id, source, request_name, global_data_json, | ||||
| field_values_json, status, notes, created_at, updated_at | |||||
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, | |||||
| draft.ID, draft.TemplateID, draft.ManifestID, draft.Source, draft.RequestName, asRaw(draft.GlobalDataJSON), | |||||
| asRaw(draft.FieldValuesJSON), draft.Status, draft.Notes, draft.CreatedAt.UTC().Format(time.RFC3339Nano), draft.UpdatedAt.UTC().Format(time.RFC3339Nano), | |||||
| field_values_json, draft_context_json, status, notes, created_at, updated_at | |||||
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, | |||||
| draft.ID, nullableInt64(draft.TemplateID), draft.ManifestID, draft.Source, draft.RequestName, asRaw(draft.GlobalDataJSON), | |||||
| asRaw(draft.FieldValuesJSON), asRaw(draft.DraftContextJSON), draft.Status, draft.Notes, draft.CreatedAt.UTC().Format(time.RFC3339Nano), draft.UpdatedAt.UTC().Format(time.RFC3339Nano), | |||||
| ) | ) | ||||
| return err | return err | ||||
| } | } | ||||
| @@ -463,10 +463,10 @@ func (s *Store) CreateDraft(ctx context.Context, draft domain.BuildDraft) error | |||||
| func (s *Store) UpdateDraft(ctx context.Context, draft domain.BuildDraft) error { | func (s *Store) UpdateDraft(ctx context.Context, draft domain.BuildDraft) error { | ||||
| res, err := s.db.ExecContext(ctx, ` | res, err := s.db.ExecContext(ctx, ` | ||||
| UPDATE build_drafts | UPDATE build_drafts | ||||
| SET template_id = ?, manifest_id = ?, source = ?, request_name = ?, global_data_json = ?, field_values_json = ?, | |||||
| SET template_id = ?, manifest_id = ?, source = ?, request_name = ?, global_data_json = ?, field_values_json = ?, draft_context_json = ?, | |||||
| status = ?, notes = ?, updated_at = ? | status = ?, notes = ?, updated_at = ? | ||||
| WHERE id = ?`, | WHERE id = ?`, | ||||
| draft.TemplateID, draft.ManifestID, draft.Source, draft.RequestName, asRaw(draft.GlobalDataJSON), asRaw(draft.FieldValuesJSON), | |||||
| nullableInt64(draft.TemplateID), draft.ManifestID, draft.Source, draft.RequestName, asRaw(draft.GlobalDataJSON), asRaw(draft.FieldValuesJSON), asRaw(draft.DraftContextJSON), | |||||
| draft.Status, draft.Notes, draft.UpdatedAt.UTC().Format(time.RFC3339Nano), draft.ID, | draft.Status, draft.Notes, draft.UpdatedAt.UTC().Format(time.RFC3339Nano), draft.ID, | ||||
| ) | ) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -481,7 +481,7 @@ func (s *Store) UpdateDraft(ctx context.Context, draft domain.BuildDraft) error | |||||
| func (s *Store) GetDraftByID(ctx context.Context, id string) (*domain.BuildDraft, error) { | func (s *Store) GetDraftByID(ctx context.Context, id string) (*domain.BuildDraft, error) { | ||||
| row := s.db.QueryRowContext(ctx, ` | row := s.db.QueryRowContext(ctx, ` | ||||
| SELECT id, template_id, manifest_id, source, request_name, global_data_json, field_values_json, status, notes, created_at, updated_at | |||||
| SELECT id, template_id, manifest_id, source, request_name, global_data_json, field_values_json, draft_context_json, status, notes, created_at, updated_at | |||||
| FROM build_drafts | FROM build_drafts | ||||
| WHERE id = ?`, id) | WHERE id = ?`, id) | ||||
| return scanDraft(row.Scan) | return scanDraft(row.Scan) | ||||
| @@ -489,7 +489,7 @@ func (s *Store) GetDraftByID(ctx context.Context, id string) (*domain.BuildDraft | |||||
| func (s *Store) ListDrafts(ctx context.Context, limit int) ([]domain.BuildDraft, error) { | func (s *Store) ListDrafts(ctx context.Context, limit int) ([]domain.BuildDraft, error) { | ||||
| query := ` | query := ` | ||||
| SELECT id, template_id, manifest_id, source, request_name, global_data_json, field_values_json, status, notes, created_at, updated_at | |||||
| SELECT id, template_id, manifest_id, source, request_name, global_data_json, field_values_json, draft_context_json, status, notes, created_at, updated_at | |||||
| FROM build_drafts | FROM build_drafts | ||||
| ORDER BY updated_at DESC` | ORDER BY updated_at DESC` | ||||
| args := make([]any, 0, 1) | args := make([]any, 0, 1) | ||||
| @@ -662,20 +662,26 @@ func scanBuild(scan func(dest ...any) error) (*domain.SiteBuild, error) { | |||||
| func scanDraft(scan func(dest ...any) error) (*domain.BuildDraft, error) { | func scanDraft(scan func(dest ...any) error) (*domain.BuildDraft, error) { | ||||
| var d domain.BuildDraft | var d domain.BuildDraft | ||||
| var templateID sql.NullInt64 | |||||
| var globalRaw []byte | var globalRaw []byte | ||||
| var fieldsRaw []byte | var fieldsRaw []byte | ||||
| var draftContextRaw []byte | |||||
| var createdAtRaw string | var createdAtRaw string | ||||
| var updatedAtRaw string | var updatedAtRaw string | ||||
| if err := scan( | if err := scan( | ||||
| &d.ID, &d.TemplateID, &d.ManifestID, &d.Source, &d.RequestName, &globalRaw, &fieldsRaw, &d.Status, &d.Notes, &createdAtRaw, &updatedAtRaw, | |||||
| &d.ID, &templateID, &d.ManifestID, &d.Source, &d.RequestName, &globalRaw, &fieldsRaw, &draftContextRaw, &d.Status, &d.Notes, &createdAtRaw, &updatedAtRaw, | |||||
| ); err != nil { | ); err != nil { | ||||
| if err == sql.ErrNoRows { | if err == sql.ErrNoRows { | ||||
| return nil, store.ErrNotFound | return nil, store.ErrNotFound | ||||
| } | } | ||||
| return nil, err | return nil, err | ||||
| } | } | ||||
| if templateID.Valid { | |||||
| d.TemplateID = templateID.Int64 | |||||
| } | |||||
| d.GlobalDataJSON = cloneBytes(globalRaw) | d.GlobalDataJSON = cloneBytes(globalRaw) | ||||
| d.FieldValuesJSON = cloneBytes(fieldsRaw) | d.FieldValuesJSON = cloneBytes(fieldsRaw) | ||||
| d.DraftContextJSON = cloneBytes(draftContextRaw) | |||||
| d.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAtRaw) | d.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAtRaw) | ||||
| d.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updatedAtRaw) | d.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updatedAtRaw) | ||||
| return &d, nil | return &d, nil | ||||
| @@ -735,3 +741,10 @@ func parseTimePtr(value sql.NullString) *time.Time { | |||||
| } | } | ||||
| return &ts | return &ts | ||||
| } | } | ||||
| func nullableInt64(value int64) any { | |||||
| if value <= 0 { | |||||
| return nil | |||||
| } | |||||
| return value | |||||
| } | |||||
| @@ -65,6 +65,17 @@ | |||||
| <div><label>Website-Sprache<input type="text" name="site_language" value="{{.Form.SiteLanguage}}"></label></div> | <div><label>Website-Sprache<input type="text" name="site_language" value="{{.Form.SiteLanguage}}"></label></div> | ||||
| </div> | </div> | ||||
| <h3>Intake / Website-Kontext</h3> | |||||
| <div class="grid2"> | |||||
| <div><label>Website URL<input type="url" name="website_url" value="{{.Form.WebsiteURL}}" placeholder="https://example.com"></label></div> | |||||
| <div><label>Locale Style (z.B. de-CH)<input type="text" name="locale_style" value="{{.Form.LocaleStyle}}" placeholder="de-CH"></label></div> | |||||
| <div><label>Market Style (z.B. DACH)<input type="text" name="market_style" value="{{.Form.MarketStyle}}" placeholder="de-CH, de-DE, de-AT"></label></div> | |||||
| <div><label>Address Mode<input type="text" name="address_mode" value="{{.Form.AddressMode}}" placeholder="du oder sie"></label></div> | |||||
| <div><label>Content Tone<input type="text" name="content_tone" value="{{.Form.ContentTone}}" placeholder="sachlich, freundlich, ..."></label></div> | |||||
| </div> | |||||
| <div><label>Website Summary<textarea name="website_summary">{{.Form.WebsiteSummary}}</textarea></label></div> | |||||
| <div><label>Prompt Instructions<textarea name="prompt_instructions">{{.Form.PromptInstructions}}</textarea></label></div> | |||||
| <h3>Kontakt</h3> | <h3>Kontakt</h3> | ||||
| <div class="grid2"> | <div class="grid2"> | ||||
| <div><label>E-Mail*<input type="email" name="email" value="{{.Form.Email}}" required></label></div> | <div><label>E-Mail*<input type="email" name="email" value="{{.Form.Email}}" required></label></div> | ||||