diff --git a/AGENTS.md b/AGENTS.md index 6199bf7..f98b6e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ Dieses Dokument definiert projektlokale Leitplanken fuer Menschen und Agenten, d - AI-Template-Sync, Discovery/Onboarding und bearbeitbare Manifest-Felder. - 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. - SQLite als Default-Datenhaltung fuer lokalen Betrieb. diff --git a/README.md b/README.md index aa70665..5bb9620 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,12 @@ Die App kann heute: - AI-Templates aus QC synchronisieren und anzeigen. - Templates onboarden (Discovery/Manifest) und Felder fuer Mapping/Review bearbeiten. - 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. 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 diff --git a/data/qctextbuilder.db b/data/qctextbuilder.db index 41c8749..fb57b09 100644 Binary files a/data/qctextbuilder.db and b/data/qctextbuilder.db differ diff --git a/dist/qctextbuilder.exe b/dist/qctextbuilder.exe index 736c2d3..088c1af 100644 Binary files a/dist/qctextbuilder.exe and b/dist/qctextbuilder.exe differ diff --git a/docs/TARGET_STATE_AND_ROADMAP.md b/docs/TARGET_STATE_AND_ROADMAP.md index 3f1fad0..7aa4259 100644 --- a/docs/TARGET_STATE_AND_ROADMAP.md +++ b/docs/TARGET_STATE_AND_ROADMAP.md @@ -36,6 +36,8 @@ Soll-Logik: Aktueller Stand: - 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`. - 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. ### 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. ### E) LLM-Assistenz - [ ] Feldvorschlaege fuer fehlende Inhalte im Draft. - [ ] 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 - [ ] Verbindliche Secret-Strategie (verschluesselte Speicherung statt einfacher Platzhalterlogik). diff --git a/internal/domain/models.go b/internal/domain/models.go index b9bd364..783f6a0 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -72,17 +72,38 @@ type SiteBuild 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 { diff --git a/internal/draftsvc/service.go b/internal/draftsvc/service.go index ab19544..7fccf79 100644 --- a/internal/draftsvc/service.go +++ b/internal/draftsvc/service.go @@ -14,15 +14,16 @@ import ( ) 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 { @@ -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) { - templateID := req.TemplateID + templateID := int64(0) + if req.TemplateID != nil { + templateID = *req.TemplateID + } + var existing *domain.BuildDraft 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 { 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 } } - 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) - if manifestID == "" { + if templateID <= 0 { + manifestID = "" + } else if manifestID == "" { manifest, err := s.manifests.GetActiveManifestByTemplateID(ctx, templateID) if err != nil { 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 { return nil, errors.New("fieldValues is invalid JSON") } + draftContextJSON, err := buildDraftContextJSON(req, existing) + if err != nil { + return nil, err + } now := time.Now().UTC() source := defaultString(req.Source, "ui") @@ -88,16 +99,17 @@ func (s *Service) SaveDraft(ctx context.Context, req UpsertDraftRequest) (*domai } 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 == "" { 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) } +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) { return s.drafts.GetDraftByID(ctx, strings.TrimSpace(draftID)) } diff --git a/internal/httpserver/handlers/handlers.go b/internal/httpserver/handlers/handlers.go index 1de2669..ce274ed 100644 --- a/internal/httpserver/handlers/handlers.go +++ b/internal/httpserver/handlers/handlers.go @@ -9,6 +9,7 @@ import ( "github.com/go-chi/chi/v5" "qctextbuilder/internal/buildsvc" + "qctextbuilder/internal/domain" "qctextbuilder/internal/draftsvc" "qctextbuilder/internal/onboarding" "qctextbuilder/internal/templatesvc" @@ -164,37 +165,109 @@ func (a *API) StartBuild(w http.ResponseWriter, r *http.Request) { } 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) { - var req upsertDraftRequest + 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{ - 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 { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) 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) { @@ -225,15 +298,16 @@ func (a *API) UpdateDraft(w http.ResponseWriter, r *http.Request) { 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, - 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 { writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) @@ -294,3 +368,11 @@ func defaultStr(v, fallback string) string { } return strings.TrimSpace(v) } + +func getMapString(values map[string]any, key string) string { + if values == nil { + return "" + } + raw, _ := values[key].(string) + return raw +} diff --git a/internal/httpserver/handlers/ui.go b/internal/httpserver/handlers/ui.go index 4c94304..ccb00b9 100644 --- a/internal/httpserver/handlers/ui.go +++ b/internal/httpserver/handlers/ui.go @@ -145,28 +145,35 @@ type buildNewPageData 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 { @@ -391,15 +398,16 @@ func (u *UI) CreateBuild(w http.ResponseWriter, r *http.Request) { if form.DraftID != "" { _, _ = 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, }) 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 { data, loadErr := u.loadBuildNewPageData(r, pageData{ @@ -532,8 +541,9 @@ func urlQuery(s string) string { 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 { return &v } @@ -1127,28 +1137,35 @@ func humanizeKey(key string) string { func buildFormInputFromRequest(r *http.Request) 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, } mergeGlobalDataIntoForm(&form, draft.GlobalDataJSON) + mergeDraftContextIntoForm(&form, draft.DraftContextJSON) return form } @@ -1234,6 +1252,48 @@ func mergeGlobalDataIntoForm(form *buildFormInput, raw []byte) { 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 { s, _ := v.(string) return strings.TrimSpace(s) diff --git a/internal/store/memory/store.go b/internal/store/memory/store.go index d73b585..710b543 100644 --- a/internal/store/memory/store.go +++ b/internal/store/memory/store.go @@ -288,6 +288,7 @@ func (s *Store) GetDraftByID(_ context.Context, id string) (*domain.BuildDraft, copy := draft copy.GlobalDataJSON = cloneRaw(draft.GlobalDataJSON) copy.FieldValuesJSON = cloneRaw(draft.FieldValuesJSON) + copy.DraftContextJSON = cloneRaw(draft.DraftContextJSON) return ©, nil } @@ -299,6 +300,7 @@ func (s *Store) ListDrafts(_ context.Context, limit int) ([]domain.BuildDraft, e copy := draft copy.GlobalDataJSON = cloneRaw(draft.GlobalDataJSON) copy.FieldValuesJSON = cloneRaw(draft.FieldValuesJSON) + copy.DraftContextJSON = cloneRaw(draft.DraftContextJSON) out = append(out, copy) } sort.Slice(out, func(i, j int) bool { diff --git a/internal/store/sqlite/migrations/003_extend_build_drafts_for_intake_context.sql b/internal/store/sqlite/migrations/003_extend_build_drafts_for_intake_context.sql new file mode 100644 index 0000000..dfe8ad2 --- /dev/null +++ b/internal/store/sqlite/migrations/003_extend_build_drafts_for_intake_context.sql @@ -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; diff --git a/internal/store/sqlite/store.go b/internal/store/sqlite/store.go index ed19e29..e7684a4 100644 --- a/internal/store/sqlite/store.go +++ b/internal/store/sqlite/store.go @@ -452,10 +452,10 @@ func (s *Store) CreateDraft(ctx context.Context, draft domain.BuildDraft) error _, err := s.db.ExecContext(ctx, ` INSERT INTO build_drafts ( 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 } @@ -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 { res, err := s.db.ExecContext(ctx, ` 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 = ? 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, ) 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) { 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 WHERE id = ?`, id) 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) { 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 ORDER BY updated_at DESC` 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) { var d domain.BuildDraft + var templateID sql.NullInt64 var globalRaw []byte var fieldsRaw []byte + var draftContextRaw []byte var createdAtRaw string var updatedAtRaw string 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 { if err == sql.ErrNoRows { return nil, store.ErrNotFound } return nil, err } + if templateID.Valid { + d.TemplateID = templateID.Int64 + } d.GlobalDataJSON = cloneBytes(globalRaw) d.FieldValuesJSON = cloneBytes(fieldsRaw) + d.DraftContextJSON = cloneBytes(draftContextRaw) d.CreatedAt, _ = time.Parse(time.RFC3339Nano, createdAtRaw) d.UpdatedAt, _ = time.Parse(time.RFC3339Nano, updatedAtRaw) return &d, nil @@ -735,3 +741,10 @@ func parseTimePtr(value sql.NullString) *time.Time { } return &ts } + +func nullableInt64(value int64) any { + if value <= 0 { + return nil + } + return value +} diff --git a/web/templates/build_new.gohtml b/web/templates/build_new.gohtml index 10c9525..96b1b2f 100644 --- a/web/templates/build_new.gohtml +++ b/web/templates/build_new.gohtml @@ -65,6 +65,17 @@
+

Intake / Website-Kontext

+
+
+
+
+
+
+
+
+
+

Kontakt