| @@ -9,11 +9,13 @@ Die App kann heute: | |||||
| - 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). | - Externen Draft-Intake ueber `POST /api/drafts/intake` verarbeiten (Stammdaten + optional Website-/Stilkontext, kein Direkt-Build). | ||||
| - Globalen Master-Prompt in Settings pflegen sowie Prompt-Bloecke fuer den spaeteren LLM-Flow als Standard konfigurieren. | |||||
| - Im Draft-/Build-UI Prompt-Bloecke je Draft aktivieren/deaktivieren und editieren; Prompt-Aufbau wird als Vorschau angezeigt. | |||||
| - 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 liefert nur Intake-Daten (Stammdaten + optional Kontext) in Drafts. | - 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. | |||||
| - LLM-Autofill ist noch nicht fertig; vorbereitet sind Kontextfelder plus editierbarer Prompt-Generator (Master + Bloecke + Stilsteuerung) im Draft-Review-Flow. | |||||
| ## Lokaler Start | ## Lokaler Start | ||||
| @@ -38,6 +38,7 @@ 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. | - 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). | - Draft-Kontext fuer spaetere LLM-Unterstuetzung ist vorbereitet (Website-URL, Website-Summary, Stilprofil inkl. locale/market/address mode/tone/prompt instructions). | ||||
| - Prompt-Generator als MVP ist im UI vorhanden: globaler Master-Prompt (Settings), editierbare Prompt-Bloecke je Draft, sichtbare Prompt-Zusammensetzung. | |||||
| - 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. | ||||
| @@ -100,7 +101,8 @@ Statusmarker: | |||||
| ### 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 (Kontextfelder vorhanden, produktive Vorschlagslogik offen). | |||||
| - [-] Stilprofil-Logik unter Beruecksichtigung von `businessType` + Tonalitaet (Kontextfelder + Auswahlfelder vorhanden, produktive Vorschlagslogik offen). | |||||
| - [-] Prompt-Generator (Master-Prompt + aktivierbare Prompt-Bloecke + Prompt-Vorschau) als Vorbereitung fuer spaeteren LLM-Runner. | |||||
| ### F) Security und Betriebsreife | ### F) Security und Betriebsreife | ||||
| - [ ] Verbindliche Secret-Strategie (verschluesselte Speicherung statt einfacher Platzhalterlogik). | - [ ] Verbindliche Secret-Strategie (verschluesselte Speicherung statt einfacher Platzhalterlogik). | ||||
| @@ -74,23 +74,31 @@ func New(cfg config.Config) (*App, error) { | |||||
| pollingSvc := polling.New(buildSvc, buildStore, time.Duration(cfg.PollIntervalSeconds)*time.Second, cfg.PollMaxConcurrent, logger) | pollingSvc := polling.New(buildSvc, buildStore, time.Duration(cfg.PollIntervalSeconds)*time.Second, cfg.PollMaxConcurrent, logger) | ||||
| api := handlers.NewAPI(templateSvc, onboardSvc, draftSvc, buildSvc) | api := handlers.NewAPI(templateSvc, onboardSvc, draftSvc, buildSvc) | ||||
| _ = settingsStore.UpsertSettings(context.Background(), domain.AppSettings{ | |||||
| baseSettings := domain.AppSettings{ | |||||
| QCBaseURL: cfg.QCBaseURL, | QCBaseURL: cfg.QCBaseURL, | ||||
| QCBearerTokenEncrypted: cfg.QCToken, | QCBearerTokenEncrypted: cfg.QCToken, | ||||
| LanguageOutputMode: "EN", | LanguageOutputMode: "EN", | ||||
| JobPollIntervalSeconds: cfg.PollIntervalSeconds, | JobPollIntervalSeconds: cfg.PollIntervalSeconds, | ||||
| JobPollTimeoutSeconds: cfg.PollTimeoutSeconds, | JobPollTimeoutSeconds: cfg.PollTimeoutSeconds, | ||||
| }) | |||||
| MasterPrompt: domain.SeedMasterPrompt, | |||||
| PromptBlocks: domain.DefaultPromptBlocks(), | |||||
| } | |||||
| if existing, err := settingsStore.GetSettings(context.Background()); err == nil && existing != nil { | |||||
| baseSettings.MasterPrompt = existing.MasterPrompt | |||||
| baseSettings.PromptBlocks = existing.PromptBlocks | |||||
| } | |||||
| _ = settingsStore.UpsertSettings(context.Background(), baseSettings) | |||||
| renderer, err := views.NewRenderer("web/templates/*.gohtml") | renderer, err := views.NewRenderer("web/templates/*.gohtml") | ||||
| if err != nil { | if err != nil { | ||||
| return nil, fmt.Errorf("init renderer: %w", err) | return nil, fmt.Errorf("init renderer: %w", err) | ||||
| } | } | ||||
| ui := handlers.NewUI(templateSvc, onboardSvc, draftSvc, buildSvc, cfg, renderer) | |||||
| ui := handlers.NewUI(templateSvc, onboardSvc, draftSvc, buildSvc, settingsStore, cfg, renderer) | |||||
| server := httpserver.New(cfg.HTTPAddr, logger, func(r chi.Router) { | server := httpserver.New(cfg.HTTPAddr, logger, func(r chi.Router) { | ||||
| r.Get("/", ui.Home) | r.Get("/", ui.Home) | ||||
| r.Get("/settings", ui.Settings) | r.Get("/settings", ui.Settings) | ||||
| r.Post("/settings/prompt", ui.SavePromptSettings) | |||||
| r.Get("/templates", ui.Templates) | r.Get("/templates", ui.Templates) | ||||
| r.Post("/templates/sync", ui.SyncTemplates) | r.Post("/templates/sync", ui.SyncTemplates) | ||||
| r.Get("/templates/{id}", ui.TemplateDetail) | r.Get("/templates/{id}", ui.TemplateDetail) | ||||
| @@ -94,11 +94,23 @@ type DraftStyleProfile struct { | |||||
| PromptInstructions string `json:"promptInstructions,omitempty"` | PromptInstructions string `json:"promptInstructions,omitempty"` | ||||
| } | } | ||||
| type PromptBlockConfig struct { | |||||
| ID string `json:"id"` | |||||
| Label string `json:"label,omitempty"` | |||||
| Instruction string `json:"instruction,omitempty"` | |||||
| Enabled bool `json:"enabled"` | |||||
| } | |||||
| type DraftPromptConfig struct { | |||||
| Blocks []PromptBlockConfig `json:"blocks,omitempty"` | |||||
| } | |||||
| type DraftLLMContext struct { | type DraftLLMContext struct { | ||||
| BusinessType string `json:"businessType,omitempty"` | BusinessType string `json:"businessType,omitempty"` | ||||
| WebsiteURL string `json:"websiteUrl,omitempty"` | WebsiteURL string `json:"websiteUrl,omitempty"` | ||||
| WebsiteSummary string `json:"websiteSummary,omitempty"` | WebsiteSummary string `json:"websiteSummary,omitempty"` | ||||
| StyleProfile DraftStyleProfile `json:"styleProfile,omitempty"` | StyleProfile DraftStyleProfile `json:"styleProfile,omitempty"` | ||||
| Prompt DraftPromptConfig `json:"prompt,omitempty"` | |||||
| } | } | ||||
| type DraftContext struct { | type DraftContext struct { | ||||
| @@ -107,9 +119,11 @@ type DraftContext struct { | |||||
| } | } | ||||
| type AppSettings struct { | type AppSettings struct { | ||||
| QCBaseURL string `json:"qcBaseUrl"` | |||||
| QCBearerTokenEncrypted string `json:"qcBearerTokenEncrypted"` | |||||
| LanguageOutputMode string `json:"languageOutputMode"` | |||||
| JobPollIntervalSeconds int `json:"jobPollIntervalSeconds"` | |||||
| JobPollTimeoutSeconds int `json:"jobPollTimeoutSeconds"` | |||||
| QCBaseURL string `json:"qcBaseUrl"` | |||||
| QCBearerTokenEncrypted string `json:"qcBearerTokenEncrypted"` | |||||
| LanguageOutputMode string `json:"languageOutputMode"` | |||||
| JobPollIntervalSeconds int `json:"jobPollIntervalSeconds"` | |||||
| JobPollTimeoutSeconds int `json:"jobPollTimeoutSeconds"` | |||||
| MasterPrompt string `json:"masterPrompt,omitempty"` | |||||
| PromptBlocks []PromptBlockConfig `json:"promptBlocks,omitempty"` | |||||
| } | } | ||||
| @@ -0,0 +1,105 @@ | |||||
| package domain | |||||
| import "strings" | |||||
| const SeedMasterPrompt = `Du bist ein Assistenzsystem fuer den QC-Text-Builder. | |||||
| Erstelle nur ueberschreibbare Textvorschlaege fuer Draft-Felder, keine finale Build-Ausfuehrung. | |||||
| Arbeite praezise, branchengerecht und nachvollziehbar. | |||||
| Priorisiere klare, konversionsstarke Website-Texte, die zum Geschaeftskontext passen. | |||||
| Wenn Informationen fehlen, markiere Annahmen transparent statt Inhalte zu erfinden.` | |||||
| func DefaultPromptBlocks() []PromptBlockConfig { | |||||
| return []PromptBlockConfig{ | |||||
| { | |||||
| ID: "business_type", | |||||
| Label: "Business Type / Branche", | |||||
| Instruction: "Nutze den Business-Type als Leitplanke fuer Fachsprache, Leistungsversprechen und relevante Keywords.", | |||||
| Enabled: true, | |||||
| }, | |||||
| { | |||||
| ID: "website_summary", | |||||
| Label: "Website-Zusammenfassung", | |||||
| Instruction: "Nutze die Website-Zusammenfassung als Input fuer USPs, Angebote und vorhandene Botschaften.", | |||||
| Enabled: true, | |||||
| }, | |||||
| { | |||||
| ID: "style_market_locale", | |||||
| Label: "Stil / Markt / Locale", | |||||
| Instruction: "Beachte Locale-Style und Markt-Style fuer Wortwahl, Begriffe und regionale Passung.", | |||||
| Enabled: true, | |||||
| }, | |||||
| { | |||||
| ID: "address_mode", | |||||
| Label: "Address Mode (du/sie)", | |||||
| Instruction: "Halte die Ansprache konsistent im gewaehlten Address Mode.", | |||||
| Enabled: true, | |||||
| }, | |||||
| { | |||||
| ID: "content_tone", | |||||
| Label: "Content Tone", | |||||
| Instruction: "Setze den gewaehlten Ton ueber Headlines, Fliesstext und CTA konsistent um.", | |||||
| Enabled: true, | |||||
| }, | |||||
| { | |||||
| ID: "free_instructions", | |||||
| Label: "Zusaetzliche freie Instruktionen", | |||||
| Instruction: "Beruecksichtige zusaetzliche Prompt-Instruktionen als harte Vorgaben fuer die Vorschlaege.", | |||||
| Enabled: true, | |||||
| }, | |||||
| } | |||||
| } | |||||
| func NormalizeMasterPrompt(value string) string { | |||||
| trimmed := strings.TrimSpace(value) | |||||
| if trimmed == "" { | |||||
| return SeedMasterPrompt | |||||
| } | |||||
| return trimmed | |||||
| } | |||||
| func NormalizePromptBlocks(blocks []PromptBlockConfig) []PromptBlockConfig { | |||||
| defaults := DefaultPromptBlocks() | |||||
| if len(blocks) == 0 { | |||||
| out := make([]PromptBlockConfig, len(defaults)) | |||||
| copy(out, defaults) | |||||
| return out | |||||
| } | |||||
| defaultByID := make(map[string]PromptBlockConfig, len(defaults)) | |||||
| for _, block := range defaults { | |||||
| defaultByID[block.ID] = block | |||||
| } | |||||
| used := make(map[string]struct{}, len(blocks)) | |||||
| out := make([]PromptBlockConfig, 0, len(defaults)) | |||||
| for _, block := range blocks { | |||||
| id := strings.TrimSpace(block.ID) | |||||
| if id == "" { | |||||
| continue | |||||
| } | |||||
| if _, seen := used[id]; seen { | |||||
| continue | |||||
| } | |||||
| base, ok := defaultByID[id] | |||||
| if !ok { | |||||
| base = PromptBlockConfig{ID: id, Label: id} | |||||
| } | |||||
| if strings.TrimSpace(block.Label) != "" { | |||||
| base.Label = strings.TrimSpace(block.Label) | |||||
| } | |||||
| if strings.TrimSpace(block.Instruction) != "" { | |||||
| base.Instruction = strings.TrimSpace(block.Instruction) | |||||
| } | |||||
| base.Enabled = block.Enabled | |||||
| out = append(out, base) | |||||
| used[id] = struct{}{} | |||||
| } | |||||
| for _, block := range defaults { | |||||
| if _, ok := used[block.ID]; ok { | |||||
| continue | |||||
| } | |||||
| out = append(out, block) | |||||
| } | |||||
| return out | |||||
| } | |||||
| @@ -1,6 +1,8 @@ | |||||
| package handlers | package handlers | ||||
| import ( | import ( | ||||
| "bytes" | |||||
| "context" | |||||
| "encoding/json" | "encoding/json" | ||||
| "fmt" | "fmt" | ||||
| "net/http" | "net/http" | ||||
| @@ -18,6 +20,7 @@ import ( | |||||
| "qctextbuilder/internal/domain" | "qctextbuilder/internal/domain" | ||||
| "qctextbuilder/internal/draftsvc" | "qctextbuilder/internal/draftsvc" | ||||
| "qctextbuilder/internal/onboarding" | "qctextbuilder/internal/onboarding" | ||||
| "qctextbuilder/internal/store" | |||||
| "qctextbuilder/internal/templatesvc" | "qctextbuilder/internal/templatesvc" | ||||
| ) | ) | ||||
| @@ -26,6 +29,7 @@ type UI struct { | |||||
| onboardSvc *onboarding.Service | onboardSvc *onboarding.Service | ||||
| draftSvc *draftsvc.Service | draftSvc *draftsvc.Service | ||||
| buildSvc buildsvc.Service | buildSvc buildsvc.Service | ||||
| settings store.SettingsStore | |||||
| cfg config.Config | cfg config.Config | ||||
| render htmlRenderer | render htmlRenderer | ||||
| } | } | ||||
| @@ -54,6 +58,8 @@ type settingsPageData struct { | |||||
| PollMaxConcurrent int | PollMaxConcurrent int | ||||
| TokenConfigured bool | TokenConfigured bool | ||||
| LanguageOutputMode string | LanguageOutputMode string | ||||
| MasterPrompt string | |||||
| PromptBlocks []domain.PromptBlockConfig | |||||
| } | } | ||||
| type templatesPageData struct { | type templatesPageData struct { | ||||
| @@ -142,6 +148,7 @@ type buildNewPageData struct { | |||||
| EditableFields []buildFieldView | EditableFields []buildFieldView | ||||
| EnabledFields []buildFieldView | EnabledFields []buildFieldView | ||||
| Form buildFormInput | Form buildFormInput | ||||
| PromptPreview string | |||||
| } | } | ||||
| type buildFormInput struct { | type buildFormInput struct { | ||||
| @@ -174,6 +181,8 @@ type buildFormInput struct { | |||||
| AddressMode string | AddressMode string | ||||
| ContentTone string | ContentTone string | ||||
| PromptInstructions string | PromptInstructions string | ||||
| MasterPrompt string | |||||
| PromptBlocks []domain.PromptBlockConfig | |||||
| } | } | ||||
| type buildDetailPageData struct { | type buildDetailPageData struct { | ||||
| @@ -185,8 +194,8 @@ type buildDetailPageData struct { | |||||
| AutoRefreshSeconds int | AutoRefreshSeconds int | ||||
| } | } | ||||
| func NewUI(templateSvc *templatesvc.Service, onboardSvc *onboarding.Service, draftSvc *draftsvc.Service, buildSvc buildsvc.Service, cfg config.Config, render htmlRenderer) *UI { | |||||
| return &UI{templateSvc: templateSvc, onboardSvc: onboardSvc, draftSvc: draftSvc, buildSvc: buildSvc, cfg: cfg, render: render} | |||||
| func NewUI(templateSvc *templatesvc.Service, onboardSvc *onboarding.Service, draftSvc *draftsvc.Service, buildSvc buildsvc.Service, settings store.SettingsStore, cfg config.Config, render htmlRenderer) *UI { | |||||
| return &UI{templateSvc: templateSvc, onboardSvc: onboardSvc, draftSvc: draftSvc, buildSvc: buildSvc, settings: settings, cfg: cfg, render: render} | |||||
| } | } | ||||
| func (u *UI) Home(w http.ResponseWriter, r *http.Request) { | func (u *UI) Home(w http.ResponseWriter, r *http.Request) { | ||||
| @@ -199,6 +208,7 @@ func (u *UI) Home(w http.ResponseWriter, r *http.Request) { | |||||
| } | } | ||||
| func (u *UI) Settings(w http.ResponseWriter, r *http.Request) { | func (u *UI) Settings(w http.ResponseWriter, r *http.Request) { | ||||
| settings := u.loadPromptSettings(r.Context()) | |||||
| u.render.Render(w, "settings", settingsPageData{ | u.render.Render(w, "settings", settingsPageData{ | ||||
| pageData: basePageData(r, "Settings", "/settings"), | pageData: basePageData(r, "Settings", "/settings"), | ||||
| QCBaseURL: u.cfg.QCBaseURL, | QCBaseURL: u.cfg.QCBaseURL, | ||||
| @@ -207,9 +217,26 @@ func (u *UI) Settings(w http.ResponseWriter, r *http.Request) { | |||||
| PollMaxConcurrent: u.cfg.PollMaxConcurrent, | PollMaxConcurrent: u.cfg.PollMaxConcurrent, | ||||
| TokenConfigured: strings.TrimSpace(u.cfg.QCToken) != "", | TokenConfigured: strings.TrimSpace(u.cfg.QCToken) != "", | ||||
| LanguageOutputMode: "EN", | LanguageOutputMode: "EN", | ||||
| MasterPrompt: settings.MasterPrompt, | |||||
| PromptBlocks: settings.PromptBlocks, | |||||
| }) | }) | ||||
| } | } | ||||
| func (u *UI) SavePromptSettings(w http.ResponseWriter, r *http.Request) { | |||||
| if err := r.ParseForm(); err != nil { | |||||
| http.Redirect(w, r, "/settings?err=invalid+form", http.StatusSeeOther) | |||||
| return | |||||
| } | |||||
| settings := u.loadPromptSettings(r.Context()) | |||||
| settings.MasterPrompt = domain.NormalizeMasterPrompt(r.FormValue("master_prompt")) | |||||
| settings.PromptBlocks = parsePromptBlocksFromRequest(r) | |||||
| if err := u.settings.UpsertSettings(r.Context(), settings); err != nil { | |||||
| http.Redirect(w, r, "/settings?err="+urlQuery(err.Error()), http.StatusSeeOther) | |||||
| return | |||||
| } | |||||
| http.Redirect(w, r, "/settings?msg=prompt+settings+saved", http.StatusSeeOther) | |||||
| } | |||||
| func (u *UI) Templates(w http.ResponseWriter, r *http.Request) { | func (u *UI) Templates(w http.ResponseWriter, r *http.Request) { | ||||
| templates, err := u.templateSvc.ListTemplates(r.Context()) | templates, err := u.templateSvc.ListTemplates(r.Context()) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -318,12 +345,15 @@ func (u *UI) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) { | |||||
| } | } | ||||
| func (u *UI) BuildNew(w http.ResponseWriter, r *http.Request) { | func (u *UI) BuildNew(w http.ResponseWriter, r *http.Request) { | ||||
| settings := u.loadPromptSettings(r.Context()) | |||||
| selectedTemplateID, _ := strconv.ParseInt(strings.TrimSpace(r.URL.Query().Get("template_id")), 10, 64) | selectedTemplateID, _ := strconv.ParseInt(strings.TrimSpace(r.URL.Query().Get("template_id")), 10, 64) | ||||
| selectedDraftID := strings.TrimSpace(r.URL.Query().Get("draft_id")) | selectedDraftID := strings.TrimSpace(r.URL.Query().Get("draft_id")) | ||||
| form := buildFormInput{ | form := buildFormInput{ | ||||
| DraftID: selectedDraftID, | |||||
| DraftSource: "ui", | |||||
| DraftStatus: "draft", | |||||
| DraftID: selectedDraftID, | |||||
| DraftSource: "ui", | |||||
| DraftStatus: "draft", | |||||
| MasterPrompt: settings.MasterPrompt, | |||||
| PromptBlocks: clonePromptBlocks(settings.PromptBlocks), | |||||
| } | } | ||||
| fieldValues := map[string]string{} | fieldValues := map[string]string{} | ||||
| if selectedDraftID != "" { | if selectedDraftID != "" { | ||||
| @@ -332,8 +362,11 @@ func (u *UI) BuildNew(w http.ResponseWriter, r *http.Request) { | |||||
| selectedTemplateID = draft.TemplateID | selectedTemplateID = draft.TemplateID | ||||
| form = buildFormInputFromDraft(draft) | form = buildFormInputFromDraft(draft) | ||||
| fieldValues = parseFieldValuesJSON(draft.FieldValuesJSON) | fieldValues = parseFieldValuesJSON(draft.FieldValuesJSON) | ||||
| form.MasterPrompt = settings.MasterPrompt | |||||
| form.PromptBlocks = mergePromptBlocks(form.PromptBlocks, settings.PromptBlocks) | |||||
| } | } | ||||
| } | } | ||||
| form.PromptBlocks = applyPromptBlockActivationDefaults(form.PromptBlocks, form) | |||||
| data, err := u.loadBuildNewPageData(r, basePageData(r, "New Build", "/builds/new"), selectedDraftID, selectedTemplateID, form, fieldValues) | data, err := u.loadBuildNewPageData(r, basePageData(r, "New Build", "/builds/new"), selectedDraftID, selectedTemplateID, form, fieldValues) | ||||
| if err != nil { | if err != nil { | ||||
| http.Error(w, err.Error(), http.StatusBadRequest) | http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| @@ -549,6 +582,12 @@ func strPtr(v string) *string { | |||||
| } | } | ||||
| func (u *UI) loadBuildNewPageData(r *http.Request, page pageData, selectedDraftID string, selectedTemplateID int64, form buildFormInput, fieldValues map[string]string) (buildNewPageData, error) { | func (u *UI) loadBuildNewPageData(r *http.Request, page pageData, selectedDraftID string, selectedTemplateID int64, form buildFormInput, fieldValues map[string]string) (buildNewPageData, error) { | ||||
| if strings.TrimSpace(form.MasterPrompt) == "" { | |||||
| form.MasterPrompt = domain.SeedMasterPrompt | |||||
| } | |||||
| form.PromptBlocks = domain.NormalizePromptBlocks(form.PromptBlocks) | |||||
| form.PromptBlocks = applyPromptBlockActivationDefaults(form.PromptBlocks, form) | |||||
| templates, err := u.templateSvc.ListTemplates(r.Context()) | templates, err := u.templateSvc.ListTemplates(r.Context()) | ||||
| if err != nil { | if err != nil { | ||||
| return buildNewPageData{}, err | return buildNewPageData{}, err | ||||
| @@ -565,6 +604,7 @@ func (u *UI) loadBuildNewPageData(r *http.Request, page pageData, selectedDraftI | |||||
| SelectedDraftID: selectedDraftID, | SelectedDraftID: selectedDraftID, | ||||
| SelectedTemplateID: selectedTemplateID, | SelectedTemplateID: selectedTemplateID, | ||||
| Form: form, | Form: form, | ||||
| PromptPreview: composePromptPreview(form), | |||||
| } | } | ||||
| if selectedTemplateID <= 0 { | if selectedTemplateID <= 0 { | ||||
| return data, nil | return data, nil | ||||
| @@ -1136,7 +1176,7 @@ func humanizeKey(key string) string { | |||||
| } | } | ||||
| func buildFormInputFromRequest(r *http.Request) buildFormInput { | func buildFormInputFromRequest(r *http.Request) buildFormInput { | ||||
| return buildFormInput{ | |||||
| form := buildFormInput{ | |||||
| DraftID: strings.TrimSpace(r.FormValue("draft_id")), | DraftID: strings.TrimSpace(r.FormValue("draft_id")), | ||||
| DraftSource: strings.TrimSpace(r.FormValue("draft_source")), | DraftSource: strings.TrimSpace(r.FormValue("draft_source")), | ||||
| DraftStatus: strings.TrimSpace(r.FormValue("draft_status")), | DraftStatus: strings.TrimSpace(r.FormValue("draft_status")), | ||||
| @@ -1166,7 +1206,10 @@ func buildFormInputFromRequest(r *http.Request) buildFormInput { | |||||
| AddressMode: strings.TrimSpace(r.FormValue("address_mode")), | AddressMode: strings.TrimSpace(r.FormValue("address_mode")), | ||||
| ContentTone: strings.TrimSpace(r.FormValue("content_tone")), | ContentTone: strings.TrimSpace(r.FormValue("content_tone")), | ||||
| PromptInstructions: strings.TrimSpace(r.FormValue("prompt_instructions")), | PromptInstructions: strings.TrimSpace(r.FormValue("prompt_instructions")), | ||||
| MasterPrompt: domain.NormalizeMasterPrompt(r.FormValue("master_prompt")), | |||||
| PromptBlocks: parsePromptBlocksFromRequest(r), | |||||
| } | } | ||||
| return form | |||||
| } | } | ||||
| func buildFormInputFromDraft(draft *domain.BuildDraft) buildFormInput { | func buildFormInputFromDraft(draft *domain.BuildDraft) buildFormInput { | ||||
| @@ -1270,6 +1313,7 @@ func mergeDraftContextIntoForm(form *buildFormInput, raw []byte) { | |||||
| form.AddressMode = strings.TrimSpace(ctx.LLM.StyleProfile.AddressMode) | form.AddressMode = strings.TrimSpace(ctx.LLM.StyleProfile.AddressMode) | ||||
| form.ContentTone = strings.TrimSpace(ctx.LLM.StyleProfile.ContentTone) | form.ContentTone = strings.TrimSpace(ctx.LLM.StyleProfile.ContentTone) | ||||
| form.PromptInstructions = strings.TrimSpace(ctx.LLM.StyleProfile.PromptInstructions) | form.PromptInstructions = strings.TrimSpace(ctx.LLM.StyleProfile.PromptInstructions) | ||||
| form.PromptBlocks = clonePromptBlocks(ctx.LLM.Prompt.Blocks) | |||||
| } | } | ||||
| func buildDraftContextFromForm(form buildFormInput, globalData map[string]any) *domain.DraftContext { | func buildDraftContextFromForm(form buildFormInput, globalData map[string]any) *domain.DraftContext { | ||||
| @@ -1290,10 +1334,201 @@ func buildDraftContextFromForm(form buildFormInput, globalData map[string]any) * | |||||
| ContentTone: strings.TrimSpace(form.ContentTone), | ContentTone: strings.TrimSpace(form.ContentTone), | ||||
| PromptInstructions: strings.TrimSpace(form.PromptInstructions), | PromptInstructions: strings.TrimSpace(form.PromptInstructions), | ||||
| }, | }, | ||||
| Prompt: domain.DraftPromptConfig{ | |||||
| Blocks: clonePromptBlocks(form.PromptBlocks), | |||||
| }, | |||||
| }, | }, | ||||
| } | } | ||||
| } | } | ||||
| func (u *UI) loadPromptSettings(ctx context.Context) domain.AppSettings { | |||||
| settings := domain.AppSettings{ | |||||
| QCBaseURL: u.cfg.QCBaseURL, | |||||
| QCBearerTokenEncrypted: u.cfg.QCToken, | |||||
| LanguageOutputMode: "EN", | |||||
| JobPollIntervalSeconds: u.cfg.PollIntervalSeconds, | |||||
| JobPollTimeoutSeconds: u.cfg.PollTimeoutSeconds, | |||||
| MasterPrompt: domain.SeedMasterPrompt, | |||||
| PromptBlocks: domain.DefaultPromptBlocks(), | |||||
| } | |||||
| if u.settings == nil { | |||||
| return settings | |||||
| } | |||||
| stored, err := u.settings.GetSettings(ctx) | |||||
| if err != nil || stored == nil { | |||||
| return settings | |||||
| } | |||||
| if strings.TrimSpace(stored.QCBaseURL) != "" { | |||||
| settings.QCBaseURL = strings.TrimSpace(stored.QCBaseURL) | |||||
| } | |||||
| if strings.TrimSpace(stored.QCBearerTokenEncrypted) != "" { | |||||
| settings.QCBearerTokenEncrypted = strings.TrimSpace(stored.QCBearerTokenEncrypted) | |||||
| } | |||||
| if strings.TrimSpace(stored.LanguageOutputMode) != "" { | |||||
| settings.LanguageOutputMode = strings.TrimSpace(stored.LanguageOutputMode) | |||||
| } | |||||
| if stored.JobPollIntervalSeconds > 0 { | |||||
| settings.JobPollIntervalSeconds = stored.JobPollIntervalSeconds | |||||
| } | |||||
| if stored.JobPollTimeoutSeconds > 0 { | |||||
| settings.JobPollTimeoutSeconds = stored.JobPollTimeoutSeconds | |||||
| } | |||||
| settings.MasterPrompt = domain.NormalizeMasterPrompt(stored.MasterPrompt) | |||||
| settings.PromptBlocks = domain.NormalizePromptBlocks(stored.PromptBlocks) | |||||
| return settings | |||||
| } | |||||
| func parsePromptBlocksFromRequest(r *http.Request) []domain.PromptBlockConfig { | |||||
| count, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("prompt_block_count"))) | |||||
| if count <= 0 { | |||||
| return domain.DefaultPromptBlocks() | |||||
| } | |||||
| out := make([]domain.PromptBlockConfig, 0, count) | |||||
| for i := 0; i < count; i++ { | |||||
| id := strings.TrimSpace(r.FormValue(fmt.Sprintf("prompt_block_id_%d", i))) | |||||
| if id == "" { | |||||
| continue | |||||
| } | |||||
| out = append(out, domain.PromptBlockConfig{ | |||||
| ID: id, | |||||
| Label: strings.TrimSpace(r.FormValue(fmt.Sprintf("prompt_block_label_%d", i))), | |||||
| Instruction: strings.TrimSpace(r.FormValue(fmt.Sprintf("prompt_block_instruction_%d", i))), | |||||
| Enabled: strings.TrimSpace(r.FormValue(fmt.Sprintf("prompt_block_enabled_%d", i))) == "on", | |||||
| }) | |||||
| } | |||||
| return domain.NormalizePromptBlocks(out) | |||||
| } | |||||
| func clonePromptBlocks(blocks []domain.PromptBlockConfig) []domain.PromptBlockConfig { | |||||
| if len(blocks) == 0 { | |||||
| return nil | |||||
| } | |||||
| out := make([]domain.PromptBlockConfig, len(blocks)) | |||||
| copy(out, blocks) | |||||
| return out | |||||
| } | |||||
| func mergePromptBlocks(current []domain.PromptBlockConfig, defaults []domain.PromptBlockConfig) []domain.PromptBlockConfig { | |||||
| merged := make([]domain.PromptBlockConfig, 0, len(defaults)) | |||||
| merged = append(merged, clonePromptBlocks(defaults)...) | |||||
| overrides := make(map[string]domain.PromptBlockConfig, len(current)) | |||||
| for _, block := range current { | |||||
| id := strings.TrimSpace(block.ID) | |||||
| if id == "" { | |||||
| continue | |||||
| } | |||||
| overrides[id] = block | |||||
| } | |||||
| for i := range merged { | |||||
| if override, ok := overrides[merged[i].ID]; ok { | |||||
| if strings.TrimSpace(override.Label) != "" { | |||||
| merged[i].Label = strings.TrimSpace(override.Label) | |||||
| } | |||||
| if strings.TrimSpace(override.Instruction) != "" { | |||||
| merged[i].Instruction = strings.TrimSpace(override.Instruction) | |||||
| } | |||||
| merged[i].Enabled = override.Enabled | |||||
| delete(overrides, merged[i].ID) | |||||
| } | |||||
| } | |||||
| for _, override := range overrides { | |||||
| merged = append(merged, override) | |||||
| } | |||||
| return domain.NormalizePromptBlocks(merged) | |||||
| } | |||||
| func applyPromptBlockActivationDefaults(blocks []domain.PromptBlockConfig, form buildFormInput) []domain.PromptBlockConfig { | |||||
| out := clonePromptBlocks(blocks) | |||||
| if len(out) == 0 { | |||||
| return out | |||||
| } | |||||
| for i := range out { | |||||
| switch out[i].ID { | |||||
| case "business_type": | |||||
| if strings.TrimSpace(form.BusinessType) != "" { | |||||
| out[i].Enabled = true | |||||
| } | |||||
| case "website_summary": | |||||
| if strings.TrimSpace(form.WebsiteSummary) != "" { | |||||
| out[i].Enabled = true | |||||
| } | |||||
| case "address_mode": | |||||
| if strings.TrimSpace(form.AddressMode) != "" { | |||||
| out[i].Enabled = true | |||||
| } | |||||
| case "content_tone": | |||||
| if strings.TrimSpace(form.ContentTone) != "" { | |||||
| out[i].Enabled = true | |||||
| } | |||||
| case "free_instructions": | |||||
| if strings.TrimSpace(form.PromptInstructions) != "" { | |||||
| out[i].Enabled = true | |||||
| } | |||||
| } | |||||
| } | |||||
| return out | |||||
| } | |||||
| func composePromptPreview(form buildFormInput) string { | |||||
| var b bytes.Buffer | |||||
| b.WriteString(strings.TrimSpace(form.MasterPrompt)) | |||||
| b.WriteString("\n\nAktive Prompt-Bloecke:\n") | |||||
| active := 0 | |||||
| for _, block := range form.PromptBlocks { | |||||
| if !block.Enabled { | |||||
| continue | |||||
| } | |||||
| active++ | |||||
| b.WriteString("\n[") | |||||
| b.WriteString(block.ID) | |||||
| b.WriteString("] ") | |||||
| b.WriteString(strings.TrimSpace(block.Instruction)) | |||||
| contextValue := promptContextValueForBlock(block.ID, form) | |||||
| if contextValue != "" { | |||||
| b.WriteString("\nKontext: ") | |||||
| b.WriteString(contextValue) | |||||
| } | |||||
| b.WriteString("\n") | |||||
| } | |||||
| if active == 0 { | |||||
| b.WriteString("\n(keine aktiven Bloecke)\n") | |||||
| } | |||||
| if strings.TrimSpace(form.PromptInstructions) != "" { | |||||
| b.WriteString("\nOptionale zusaetzliche Instructions:\n") | |||||
| b.WriteString(strings.TrimSpace(form.PromptInstructions)) | |||||
| b.WriteString("\n") | |||||
| } | |||||
| return strings.TrimSpace(b.String()) | |||||
| } | |||||
| func promptContextValueForBlock(blockID string, form buildFormInput) string { | |||||
| switch strings.TrimSpace(blockID) { | |||||
| case "business_type": | |||||
| return strings.TrimSpace(form.BusinessType) | |||||
| case "website_summary": | |||||
| return strings.TrimSpace(form.WebsiteSummary) | |||||
| case "style_market_locale": | |||||
| parts := make([]string, 0, 2) | |||||
| if strings.TrimSpace(form.LocaleStyle) != "" { | |||||
| parts = append(parts, "Locale="+strings.TrimSpace(form.LocaleStyle)) | |||||
| } | |||||
| if strings.TrimSpace(form.MarketStyle) != "" { | |||||
| parts = append(parts, "Market="+strings.TrimSpace(form.MarketStyle)) | |||||
| } | |||||
| return strings.Join(parts, ", ") | |||||
| case "address_mode": | |||||
| return strings.TrimSpace(form.AddressMode) | |||||
| case "content_tone": | |||||
| return strings.TrimSpace(form.ContentTone) | |||||
| case "free_instructions": | |||||
| return strings.TrimSpace(form.PromptInstructions) | |||||
| default: | |||||
| return "" | |||||
| } | |||||
| } | |||||
| func getString(v any) string { | func getString(v any) string { | ||||
| s, _ := v.(string) | s, _ := v.(string) | ||||
| return strings.TrimSpace(s) | return strings.TrimSpace(s) | ||||
| @@ -0,0 +1,5 @@ | |||||
| ALTER TABLE app_settings | |||||
| ADD COLUMN master_prompt TEXT NOT NULL DEFAULT ''; | |||||
| ALTER TABLE app_settings | |||||
| ADD COLUMN prompt_blocks_json BLOB; | |||||
| @@ -406,22 +406,30 @@ func (s *Store) UpdateBuildEditorURL(ctx context.Context, buildID string, editor | |||||
| } | } | ||||
| func (s *Store) UpsertSettings(ctx context.Context, settings domain.AppSettings) error { | func (s *Store) UpsertSettings(ctx context.Context, settings domain.AppSettings) error { | ||||
| _, err := s.db.ExecContext(ctx, ` | |||||
| promptBlocksRaw, err := json.Marshal(domain.NormalizePromptBlocks(settings.PromptBlocks)) | |||||
| if err != nil { | |||||
| return fmt.Errorf("marshal prompt blocks: %w", err) | |||||
| } | |||||
| _, err = s.db.ExecContext(ctx, ` | |||||
| INSERT INTO app_settings ( | INSERT INTO app_settings ( | ||||
| id, qc_base_url, qc_bearer_token_encrypted, language_output_mode, job_poll_interval_seconds, job_poll_timeout_seconds, updated_at | |||||
| ) VALUES (1, ?, ?, ?, ?, ?, ?) | |||||
| id, qc_base_url, qc_bearer_token_encrypted, language_output_mode, job_poll_interval_seconds, job_poll_timeout_seconds, master_prompt, prompt_blocks_json, updated_at | |||||
| ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?) | |||||
| ON CONFLICT(id) DO UPDATE SET | ON CONFLICT(id) DO UPDATE SET | ||||
| qc_base_url = excluded.qc_base_url, | qc_base_url = excluded.qc_base_url, | ||||
| qc_bearer_token_encrypted = excluded.qc_bearer_token_encrypted, | qc_bearer_token_encrypted = excluded.qc_bearer_token_encrypted, | ||||
| language_output_mode = excluded.language_output_mode, | language_output_mode = excluded.language_output_mode, | ||||
| job_poll_interval_seconds = excluded.job_poll_interval_seconds, | job_poll_interval_seconds = excluded.job_poll_interval_seconds, | ||||
| job_poll_timeout_seconds = excluded.job_poll_timeout_seconds, | job_poll_timeout_seconds = excluded.job_poll_timeout_seconds, | ||||
| master_prompt = excluded.master_prompt, | |||||
| prompt_blocks_json = excluded.prompt_blocks_json, | |||||
| updated_at = excluded.updated_at`, | updated_at = excluded.updated_at`, | ||||
| settings.QCBaseURL, | settings.QCBaseURL, | ||||
| settings.QCBearerTokenEncrypted, | settings.QCBearerTokenEncrypted, | ||||
| defaultString(settings.LanguageOutputMode, "EN"), | defaultString(settings.LanguageOutputMode, "EN"), | ||||
| settings.JobPollIntervalSeconds, | settings.JobPollIntervalSeconds, | ||||
| settings.JobPollTimeoutSeconds, | settings.JobPollTimeoutSeconds, | ||||
| domain.NormalizeMasterPrompt(settings.MasterPrompt), | |||||
| promptBlocksRaw, | |||||
| time.Now().UTC().Format(time.RFC3339Nano), | time.Now().UTC().Format(time.RFC3339Nano), | ||||
| ) | ) | ||||
| return err | return err | ||||
| @@ -429,22 +437,30 @@ func (s *Store) UpsertSettings(ctx context.Context, settings domain.AppSettings) | |||||
| func (s *Store) GetSettings(ctx context.Context) (*domain.AppSettings, error) { | func (s *Store) GetSettings(ctx context.Context) (*domain.AppSettings, error) { | ||||
| row := s.db.QueryRowContext(ctx, ` | row := s.db.QueryRowContext(ctx, ` | ||||
| SELECT qc_base_url, qc_bearer_token_encrypted, language_output_mode, job_poll_interval_seconds, job_poll_timeout_seconds | |||||
| SELECT qc_base_url, qc_bearer_token_encrypted, language_output_mode, job_poll_interval_seconds, job_poll_timeout_seconds, master_prompt, prompt_blocks_json | |||||
| FROM app_settings | FROM app_settings | ||||
| WHERE id = 1`) | WHERE id = 1`) | ||||
| var settings domain.AppSettings | var settings domain.AppSettings | ||||
| var promptBlocksRaw []byte | |||||
| if err := row.Scan( | if err := row.Scan( | ||||
| &settings.QCBaseURL, | &settings.QCBaseURL, | ||||
| &settings.QCBearerTokenEncrypted, | &settings.QCBearerTokenEncrypted, | ||||
| &settings.LanguageOutputMode, | &settings.LanguageOutputMode, | ||||
| &settings.JobPollIntervalSeconds, | &settings.JobPollIntervalSeconds, | ||||
| &settings.JobPollTimeoutSeconds, | &settings.JobPollTimeoutSeconds, | ||||
| &settings.MasterPrompt, | |||||
| &promptBlocksRaw, | |||||
| ); 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 | ||||
| } | } | ||||
| settings.MasterPrompt = domain.NormalizeMasterPrompt(settings.MasterPrompt) | |||||
| if len(promptBlocksRaw) > 0 { | |||||
| _ = json.Unmarshal(promptBlocksRaw, &settings.PromptBlocks) | |||||
| } | |||||
| settings.PromptBlocks = domain.NormalizePromptBlocks(settings.PromptBlocks) | |||||
| return &settings, nil | return &settings, nil | ||||
| } | } | ||||
| @@ -40,6 +40,7 @@ | |||||
| <input type="hidden" name="template_id" value="{{.SelectedTemplateID}}"> | <input type="hidden" name="template_id" value="{{.SelectedTemplateID}}"> | ||||
| <input type="hidden" name="manifest_id" value="{{.SelectedManifestID}}"> | <input type="hidden" name="manifest_id" value="{{.SelectedManifestID}}"> | ||||
| <input type="hidden" name="field_count" value="{{len .EditableFields}}"> | <input type="hidden" name="field_count" value="{{len .EditableFields}}"> | ||||
| <input type="hidden" name="prompt_block_count" value="{{len .Form.PromptBlocks}}"> | |||||
| <h2>Global Data</h2> | <h2>Global Data</h2> | ||||
| <div class="grid2"> | <div class="grid2"> | ||||
| @@ -68,13 +69,71 @@ | |||||
| <h3>Intake / Website-Kontext</h3> | <h3>Intake / Website-Kontext</h3> | ||||
| <div class="grid2"> | <div class="grid2"> | ||||
| <div><label>Website URL<input type="url" name="website_url" value="{{.Form.WebsiteURL}}" placeholder="https://example.com"></label></div> | <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> | |||||
| <label>Locale Style | |||||
| <select name="locale_style"> | |||||
| <option value="">Bitte waehlen</option> | |||||
| <option value="de-CH" {{if eq .Form.LocaleStyle "de-CH"}}selected{{end}}>de-CH</option> | |||||
| <option value="de-DE" {{if eq .Form.LocaleStyle "de-DE"}}selected{{end}}>de-DE</option> | |||||
| <option value="de-AT" {{if eq .Form.LocaleStyle "de-AT"}}selected{{end}}>de-AT</option> | |||||
| </select> | |||||
| </label> | |||||
| </div> | |||||
| <div> | |||||
| <label>Market Style | |||||
| <select name="market_style"> | |||||
| <option value="">Bitte waehlen</option> | |||||
| <option value="DACH" {{if eq .Form.MarketStyle "DACH"}}selected{{end}}>DACH</option> | |||||
| <option value="Schweiz" {{if eq .Form.MarketStyle "Schweiz"}}selected{{end}}>Schweiz</option> | |||||
| <option value="Deutschland" {{if eq .Form.MarketStyle "Deutschland"}}selected{{end}}>Deutschland</option> | |||||
| <option value="Oesterreich" {{if eq .Form.MarketStyle "Oesterreich"}}selected{{end}}>Oesterreich</option> | |||||
| </select> | |||||
| </label> | |||||
| </div> | |||||
| <div> | |||||
| <label>Address Mode | |||||
| <select name="address_mode"> | |||||
| <option value="">Bitte waehlen</option> | |||||
| <option value="du" {{if eq .Form.AddressMode "du"}}selected{{end}}>du</option> | |||||
| <option value="sie" {{if eq .Form.AddressMode "sie"}}selected{{end}}>sie</option> | |||||
| </select> | |||||
| </label> | |||||
| </div> | |||||
| <div> | |||||
| <label>Content Tone | |||||
| <select name="content_tone"> | |||||
| <option value="">Bitte waehlen</option> | |||||
| <option value="sachlich" {{if eq .Form.ContentTone "sachlich"}}selected{{end}}>sachlich</option> | |||||
| <option value="freundlich" {{if eq .Form.ContentTone "freundlich"}}selected{{end}}>freundlich</option> | |||||
| <option value="modern" {{if eq .Form.ContentTone "modern"}}selected{{end}}>modern</option> | |||||
| <option value="professionell" {{if eq .Form.ContentTone "professionell"}}selected{{end}}>professionell</option> | |||||
| <option value="locker" {{if eq .Form.ContentTone "locker"}}selected{{end}}>locker</option> | |||||
| <option value="premium" {{if eq .Form.ContentTone "premium"}}selected{{end}}>premium</option> | |||||
| </select> | |||||
| </label> | |||||
| </div> | |||||
| </div> | </div> | ||||
| <div><label>Website Summary<textarea name="website_summary">{{.Form.WebsiteSummary}}</textarea></label></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>Prompt-Generator (MVP)</h3> | |||||
| <p>Globaler Master Prompt kommt aus <a href="/settings">Settings</a>.</p> | |||||
| <div><label>Master Prompt (global)<textarea name="master_prompt" readonly>{{.Form.MasterPrompt}}</textarea></label></div> | |||||
| {{range $i, $block := .Form.PromptBlocks}} | |||||
| <input type="hidden" name="prompt_block_id_{{$i}}" value="{{$block.ID}}"> | |||||
| <input type="hidden" name="prompt_block_label_{{$i}}" value="{{$block.Label}}"> | |||||
| <div> | |||||
| <label> | |||||
| <input type="checkbox" name="prompt_block_enabled_{{$i}}" {{if $block.Enabled}}checked{{end}}> | |||||
| Prompt-Block aktiv: {{$block.Label}} | |||||
| </label> | |||||
| <textarea name="prompt_block_instruction_{{$i}}">{{$block.Instruction}}</textarea> | |||||
| </div> | |||||
| {{end}} | |||||
| <div><label>Optionale Prompt Instructions<textarea name="prompt_instructions">{{.Form.PromptInstructions}}</textarea></label></div> | |||||
| <h4>Prompt-Aufbau Vorschau</h4> | |||||
| <pre class="mono">{{.PromptPreview}}</pre> | |||||
| <h3>Kontakt</h3> | <h3>Kontakt</h3> | ||||
| <div class="grid2"> | <div class="grid2"> | ||||
| @@ -10,7 +10,7 @@ | |||||
| {{if .Msg}}<div class="flash flash-ok">{{.Msg}}</div>{{end}} | {{if .Msg}}<div class="flash flash-ok">{{.Msg}}</div>{{end}} | ||||
| {{if .Err}}<div class="flash flash-err">{{.Err}}</div>{{end}} | {{if .Err}}<div class="flash flash-err">{{.Err}}</div>{{end}} | ||||
| <h1>Settings</h1> | <h1>Settings</h1> | ||||
| <p>Read-only summary for milestone 4.</p> | |||||
| <p>QC-Settings plus globaler Prompt-Standard fuer den spaeteren LLM-Flow.</p> | |||||
| <table> | <table> | ||||
| <tr><th>QC Base URL</th><td class="mono">{{.QCBaseURL}}</td></tr> | <tr><th>QC Base URL</th><td class="mono">{{.QCBaseURL}}</td></tr> | ||||
| <tr><th>Bearer token configured</th><td>{{if .TokenConfigured}}yes{{else}}no{{end}}</td></tr> | <tr><th>Bearer token configured</th><td>{{if .TokenConfigured}}yes{{else}}no{{end}}</td></tr> | ||||
| @@ -19,6 +19,29 @@ | |||||
| <tr><th>Poll max concurrent</th><td>{{.PollMaxConcurrent}}</td></tr> | <tr><th>Poll max concurrent</th><td>{{.PollMaxConcurrent}}</td></tr> | ||||
| <tr><th>Language output mode</th><td>{{.LanguageOutputMode}}</td></tr> | <tr><th>Language output mode</th><td>{{.LanguageOutputMode}}</td></tr> | ||||
| </table> | </table> | ||||
| <h2>Globaler Master Prompt</h2> | |||||
| <form method="post" action="/settings/prompt"> | |||||
| <input type="hidden" name="prompt_block_count" value="{{len .PromptBlocks}}"> | |||||
| <div> | |||||
| <label>Master Prompt | |||||
| <textarea name="master_prompt">{{.MasterPrompt}}</textarea> | |||||
| </label> | |||||
| </div> | |||||
| <h3>Prompt-Bloecke (Standard)</h3> | |||||
| {{range $i, $block := .PromptBlocks}} | |||||
| <input type="hidden" name="prompt_block_id_{{$i}}" value="{{$block.ID}}"> | |||||
| <div> | |||||
| <label> | |||||
| <input type="checkbox" name="prompt_block_enabled_{{$i}}" {{if $block.Enabled}}checked{{end}}> | |||||
| {{$block.Label}} | |||||
| </label> | |||||
| <input type="hidden" name="prompt_block_label_{{$i}}" value="{{$block.Label}}"> | |||||
| <textarea name="prompt_block_instruction_{{$i}}">{{$block.Instruction}}</textarea> | |||||
| </div> | |||||
| {{end}} | |||||
| <button type="submit">Prompt-Settings speichern</button> | |||||
| </form> | |||||
| </body> | </body> | ||||
| </html> | </html> | ||||
| {{end}} | {{end}} | ||||