Kaynağa Gözat

Prepare draft intake and LLM context flow

master
Jan Svabenik 1 ay önce
ebeveyn
işleme
99d9b8fa82
13 değiştirilmiş dosya ile 399 ekleme ve 146 silme
  1. +1
    -0
      AGENTS.md
  2. +3
    -1
      README.md
  3. BIN
      data/qctextbuilder.db
  4. BIN
      dist/qctextbuilder.exe
  5. +5
    -3
      docs/TARGET_STATE_AND_ROADMAP.md
  6. +32
    -11
      internal/domain/models.go
  7. +57
    -31
      internal/draftsvc/service.go
  8. +109
    -27
      internal/httpserver/handlers/handlers.go
  9. +124
    -64
      internal/httpserver/handlers/ui.go
  10. +2
    -0
      internal/store/memory/store.go
  11. +33
    -0
      internal/store/sqlite/migrations/003_extend_build_drafts_for_intake_context.sql
  12. +22
    -9
      internal/store/sqlite/store.go
  13. +11
    -0
      web/templates/build_new.gohtml

+ 1
- 0
AGENTS.md Dosyayı Görüntüle

@@ -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.



+ 3
- 1
README.md Dosyayı Görüntüle

@@ -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



BIN
data/qctextbuilder.db Dosyayı Görüntüle


BIN
dist/qctextbuilder.exe Dosyayı Görüntüle


+ 5
- 3
docs/TARGET_STATE_AND_ROADMAP.md Dosyayı Görüntüle

@@ -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).


+ 32
- 11
internal/domain/models.go Dosyayı Görüntüle

@@ -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 {


+ 57
- 31
internal/draftsvc/service.go Dosyayı Görüntüle

@@ -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))
}


+ 109
- 27
internal/httpserver/handlers/handlers.go Dosyayı Görüntüle

@@ -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
}

+ 124
- 64
internal/httpserver/handlers/ui.go Dosyayı Görüntüle

@@ -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)


+ 2
- 0
internal/store/memory/store.go Dosyayı Görüntüle

@@ -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 &copy, 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 {


+ 33
- 0
internal/store/sqlite/migrations/003_extend_build_drafts_for_intake_context.sql Dosyayı Görüntüle

@@ -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;

+ 22
- 9
internal/store/sqlite/store.go Dosyayı Görüntüle

@@ -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
}

+ 11
- 0
web/templates/build_new.gohtml Dosyayı Görüntüle

@@ -65,6 +65,17 @@
<div><label>Website-Sprache<input type="text" name="site_language" value="{{.Form.SiteLanguage}}"></label></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>
<div class="grid2">
<div><label>E-Mail*<input type="email" name="email" value="{{.Form.Email}}" required></label></div>


Yükleniyor…
İptal
Kaydet