feat(server): add brand kit management and project binding
Add a brand kit domain with memory and postgres stores, a BrandKitService for CRUD and resolution, and REST endpoints to list, upsert, and delete brand kits. Projects can bind a brand kit whose resolved context and reference images feed into the agent's long-term memory and design prompts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,9 @@ func buildLongMemory(project design.Project) string {
|
||||
sections = append(sections, "- brief: "+shortenMemoryText(project.Brief, 360))
|
||||
}
|
||||
}
|
||||
if brandContext := strings.TrimSpace(project.BrandContext); brandContext != "" {
|
||||
sections = append(sections, "Binding Brand Kit context:", brandContext)
|
||||
}
|
||||
if decisions := durableDecisionMemory(memoryMessages, longMemoryDecisionLimit); decisions != "" {
|
||||
sections = append(sections, "Durable user decisions and preferences:", decisions)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"img_infinite_canvas/internal/domain/brandkit"
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
)
|
||||
|
||||
const (
|
||||
maxBrandKitDocumentBytes = 24 << 20
|
||||
maxBrandKitContextRunes = 12000
|
||||
)
|
||||
|
||||
var (
|
||||
brandKitIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$`)
|
||||
brandColorPattern = regexp.MustCompile(`^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$`)
|
||||
)
|
||||
|
||||
type BrandKitService struct {
|
||||
store brandkit.Store
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type ResolvedBrandKit struct {
|
||||
ID string
|
||||
Context string
|
||||
ReferenceImages []string
|
||||
}
|
||||
|
||||
type brandKitDocument struct {
|
||||
Version int `json:"version"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
BrandName string `json:"brandName"`
|
||||
Tagline string `json:"tagline"`
|
||||
Summary string `json:"summary"`
|
||||
Audience string `json:"audience"`
|
||||
Positioning string `json:"positioning"`
|
||||
Personality string `json:"personality"`
|
||||
Voice brandKitVoice `json:"voice"`
|
||||
Logos []brandKitLogo `json:"logos"`
|
||||
CoverImage *brandKitReferenceImage `json:"coverImage,omitempty"`
|
||||
ColorGroups []brandKitColorGroup `json:"colorGroups"`
|
||||
Typography []brandKitTypography `json:"typography"`
|
||||
ReferenceImages []brandKitReferenceImage `json:"referenceImages"`
|
||||
Visual brandKitVisual `json:"visual"`
|
||||
ApplyToNewProjects bool `json:"applyToNewProjects"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type brandKitVoice struct {
|
||||
Traits string `json:"traits"`
|
||||
Guidance string `json:"guidance"`
|
||||
Sample string `json:"sample"`
|
||||
}
|
||||
|
||||
type brandKitLogo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Variant string `json:"variant"`
|
||||
Usage string `json:"usage"`
|
||||
Source string `json:"source"`
|
||||
DataURL string `json:"dataUrl,omitempty"`
|
||||
}
|
||||
|
||||
type brandKitColorGroup struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Colors []brandKitColor `json:"colors"`
|
||||
}
|
||||
|
||||
type brandKitColor struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Hex string `json:"hex"`
|
||||
Usage string `json:"usage"`
|
||||
}
|
||||
|
||||
type brandKitTypography struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Family string `json:"family"`
|
||||
Weight string `json:"weight"`
|
||||
Sizes string `json:"sizes"`
|
||||
Description string `json:"description"`
|
||||
FontFile *brandKitFontFile `json:"fontFile,omitempty"`
|
||||
}
|
||||
|
||||
type brandKitFontFile struct {
|
||||
Name string `json:"name"`
|
||||
Format string `json:"format"`
|
||||
Size int64 `json:"size"`
|
||||
DataURL string `json:"dataUrl"`
|
||||
}
|
||||
|
||||
type brandKitReferenceImage struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
DataURL string `json:"dataUrl,omitempty"`
|
||||
}
|
||||
|
||||
type brandKitVisual struct {
|
||||
Keywords string `json:"keywords"`
|
||||
Composition string `json:"composition"`
|
||||
Imagery string `json:"imagery"`
|
||||
Dos string `json:"dos"`
|
||||
Donts string `json:"donts"`
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
|
||||
func NewBrandKitService(store brandkit.Store) *BrandKitService {
|
||||
return &BrandKitService{store: store, now: time.Now}
|
||||
}
|
||||
|
||||
func (s *BrandKitService) Close() error {
|
||||
if s == nil || s.store == nil {
|
||||
return nil
|
||||
}
|
||||
return s.store.Close()
|
||||
}
|
||||
|
||||
func (s *BrandKitService) List(ctx context.Context) ([]brandkit.Kit, error) {
|
||||
if s == nil || s.store == nil {
|
||||
return []brandkit.Kit{}, nil
|
||||
}
|
||||
return s.store.List(ctx, design.UserIDFromContext(ctx))
|
||||
}
|
||||
|
||||
func (s *BrandKitService) Get(ctx context.Context, id string) (brandkit.Kit, error) {
|
||||
id = strings.TrimSpace(id)
|
||||
if !brandKitIDPattern.MatchString(id) {
|
||||
return brandkit.Kit{}, fmt.Errorf("%w: invalid id", brandkit.ErrInvalidInput)
|
||||
}
|
||||
return s.store.Get(ctx, design.UserIDFromContext(ctx), id)
|
||||
}
|
||||
|
||||
func (s *BrandKitService) Upsert(ctx context.Context, id string, document string, isDefault bool) (brandkit.Kit, error) {
|
||||
if s == nil || s.store == nil {
|
||||
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||
}
|
||||
id = strings.TrimSpace(id)
|
||||
if !brandKitIDPattern.MatchString(id) {
|
||||
return brandkit.Kit{}, fmt.Errorf("%w: invalid id", brandkit.ErrInvalidInput)
|
||||
}
|
||||
if len(document) == 0 || len(document) > maxBrandKitDocumentBytes {
|
||||
return brandkit.Kit{}, fmt.Errorf("%w: document size is invalid", brandkit.ErrInvalidInput)
|
||||
}
|
||||
|
||||
now := s.now().UTC()
|
||||
createdAt := now
|
||||
if existing, err := s.store.Get(ctx, design.UserIDFromContext(ctx), id); err == nil {
|
||||
createdAt = existing.CreatedAt
|
||||
} else if !errors.Is(err, brandkit.ErrNotFound) {
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
doc, err := normalizeBrandKitDocument(id, []byte(document), isDefault, createdAt, now)
|
||||
if err != nil {
|
||||
return brandkit.Kit{}, err
|
||||
}
|
||||
canonical, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
return brandkit.Kit{}, fmt.Errorf("%w: encode document", brandkit.ErrInvalidInput)
|
||||
}
|
||||
return s.store.Upsert(ctx, brandkit.Kit{
|
||||
ID: id,
|
||||
UserID: design.UserIDFromContext(ctx),
|
||||
Name: doc.Name,
|
||||
Document: canonical,
|
||||
IsDefault: isDefault,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BrandKitService) Delete(ctx context.Context, id string) error {
|
||||
id = strings.TrimSpace(id)
|
||||
if !brandKitIDPattern.MatchString(id) {
|
||||
return fmt.Errorf("%w: invalid id", brandkit.ErrInvalidInput)
|
||||
}
|
||||
return s.store.Delete(ctx, design.UserIDFromContext(ctx), id)
|
||||
}
|
||||
|
||||
func (s *BrandKitService) ResolveContext(ctx context.Context, id string) (string, error) {
|
||||
resolved, err := s.Resolve(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resolved.Context, nil
|
||||
}
|
||||
|
||||
func (s *BrandKitService) Resolve(ctx context.Context, id string) (ResolvedBrandKit, error) {
|
||||
kit, err := s.Get(ctx, id)
|
||||
if err != nil {
|
||||
return ResolvedBrandKit{}, err
|
||||
}
|
||||
compiled, err := compileBrandKitContext(kit.Document)
|
||||
if err != nil {
|
||||
return ResolvedBrandKit{}, err
|
||||
}
|
||||
references, err := brandKitReferenceImages(kit.Document)
|
||||
if err != nil {
|
||||
return ResolvedBrandKit{}, err
|
||||
}
|
||||
return ResolvedBrandKit{ID: kit.ID, Context: compiled, ReferenceImages: references}, nil
|
||||
}
|
||||
|
||||
func (s *BrandKitService) ResolveDefaultContext(ctx context.Context) (string, string, error) {
|
||||
resolved, err := s.ResolveDefault(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return resolved.ID, resolved.Context, nil
|
||||
}
|
||||
|
||||
func (s *BrandKitService) ResolveDefault(ctx context.Context) (ResolvedBrandKit, error) {
|
||||
if s == nil || s.store == nil {
|
||||
return ResolvedBrandKit{}, brandkit.ErrNotFound
|
||||
}
|
||||
kit, err := s.store.GetDefault(ctx, design.UserIDFromContext(ctx))
|
||||
if err != nil {
|
||||
return ResolvedBrandKit{}, err
|
||||
}
|
||||
compiled, err := compileBrandKitContext(kit.Document)
|
||||
if err != nil {
|
||||
return ResolvedBrandKit{}, err
|
||||
}
|
||||
references, err := brandKitReferenceImages(kit.Document)
|
||||
if err != nil {
|
||||
return ResolvedBrandKit{}, err
|
||||
}
|
||||
return ResolvedBrandKit{ID: kit.ID, Context: compiled, ReferenceImages: references}, nil
|
||||
}
|
||||
|
||||
func normalizeBrandKitDocument(id string, raw []byte, isDefault bool, createdAt time.Time, updatedAt time.Time) (brandKitDocument, error) {
|
||||
var doc brandKitDocument
|
||||
decoder := json.NewDecoder(strings.NewReader(string(raw)))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&doc); err != nil {
|
||||
return brandKitDocument{}, fmt.Errorf("%w: malformed document", brandkit.ErrInvalidInput)
|
||||
}
|
||||
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
||||
return brandKitDocument{}, fmt.Errorf("%w: malformed document", brandkit.ErrInvalidInput)
|
||||
}
|
||||
if doc.Version != 1 || strings.TrimSpace(doc.ID) != id {
|
||||
return brandKitDocument{}, fmt.Errorf("%w: unsupported version or mismatched id", brandkit.ErrInvalidInput)
|
||||
}
|
||||
doc.Name = strings.TrimSpace(doc.Name)
|
||||
if doc.Name == "" || !textWithin(doc.Name, 120) {
|
||||
return brandKitDocument{}, fmt.Errorf("%w: name is required", brandkit.ErrInvalidInput)
|
||||
}
|
||||
if err := validateBrandKitDocument(doc); err != nil {
|
||||
return brandKitDocument{}, err
|
||||
}
|
||||
doc.ID = id
|
||||
doc.ApplyToNewProjects = isDefault
|
||||
doc.CreatedAt = createdAt.UTC().Format(time.RFC3339)
|
||||
doc.UpdatedAt = updatedAt.UTC().Format(time.RFC3339)
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func validateBrandKitDocument(doc brandKitDocument) error {
|
||||
for _, value := range []string{doc.BrandName, doc.Tagline, doc.Summary, doc.Audience, doc.Positioning, doc.Personality, doc.Voice.Traits, doc.Voice.Guidance, doc.Voice.Sample} {
|
||||
if !textWithin(value, 4000) {
|
||||
return fmt.Errorf("%w: text field is too long", brandkit.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
if len(doc.Logos) > 12 || len(doc.ColorGroups) > 20 || len(doc.Typography) > 20 || len(doc.ReferenceImages) > 20 {
|
||||
return fmt.Errorf("%w: too many document items", brandkit.ErrInvalidInput)
|
||||
}
|
||||
for _, logo := range doc.Logos {
|
||||
if !brandKitIDPattern.MatchString(logo.ID) || !oneOf(logo.Variant, "primary", "secondary", "mark") || !oneOf(logo.Source, "upload", "moteva-mark") || !textWithin(logo.Name, 240) || !textWithin(logo.Usage, 4000) || !validImageDataURL(logo.DataURL) {
|
||||
return fmt.Errorf("%w: invalid logo", brandkit.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
if doc.CoverImage != nil {
|
||||
if err := validateReferenceImage(*doc.CoverImage); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, group := range doc.ColorGroups {
|
||||
if !brandKitIDPattern.MatchString(group.ID) || !textWithin(group.Name, 120) || len(group.Colors) > 32 {
|
||||
return fmt.Errorf("%w: invalid color group", brandkit.ErrInvalidInput)
|
||||
}
|
||||
for _, color := range group.Colors {
|
||||
if !brandKitIDPattern.MatchString(color.ID) || !brandColorPattern.MatchString(strings.TrimSpace(color.Hex)) || !textWithin(color.Name, 120) || !textWithin(color.Usage, 1000) {
|
||||
return fmt.Errorf("%w: invalid color", brandkit.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range doc.Typography {
|
||||
if !brandKitIDPattern.MatchString(item.ID) || !textWithin(item.Label, 120) || !textWithin(item.Family, 240) || !textWithin(item.Weight, 80) || !textWithin(item.Sizes, 240) || !textWithin(item.Description, 4000) {
|
||||
return fmt.Errorf("%w: invalid typography", brandkit.ErrInvalidInput)
|
||||
}
|
||||
if item.FontFile != nil && (strings.TrimSpace(item.FontFile.Name) == "" || !textWithin(item.FontFile.Name, 240) || !textWithin(item.FontFile.Format, 32) || item.FontFile.Size < 0 || item.FontFile.Size > 8<<20 || !validDataURL(item.FontFile.DataURL)) {
|
||||
return fmt.Errorf("%w: invalid font file", brandkit.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
for _, image := range doc.ReferenceImages {
|
||||
if err := validateReferenceImage(image); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, value := range []string{doc.Visual.Keywords, doc.Visual.Composition, doc.Visual.Imagery, doc.Visual.Dos, doc.Visual.Donts} {
|
||||
if !textWithin(value, 4000) {
|
||||
return fmt.Errorf("%w: visual field is too long", brandkit.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
if !textWithin(doc.Visual.Prompt, 8000) {
|
||||
return fmt.Errorf("%w: reusable direction is too long", brandkit.ErrInvalidInput)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateReferenceImage(image brandKitReferenceImage) error {
|
||||
if !brandKitIDPattern.MatchString(image.ID) || !oneOf(image.Source, "upload", "moteva-board") || !textWithin(image.Name, 240) || !textWithin(image.Description, 4000) || !validImageDataURL(image.DataURL) {
|
||||
return fmt.Errorf("%w: invalid reference image", brandkit.ErrInvalidInput)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compileBrandKitContext(raw []byte) (string, error) {
|
||||
var doc brandKitDocument
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return "", fmt.Errorf("%w: malformed stored document", brandkit.ErrInvalidInput)
|
||||
}
|
||||
sections := []string{
|
||||
"Brand Kit: " + doc.Name,
|
||||
brandKitContextSection("Brand", compactValues(doc.BrandName, doc.Tagline, doc.Summary)),
|
||||
brandKitContextSection("Audience", compactValues(doc.Audience)),
|
||||
brandKitContextSection("Positioning and personality", compactValues(doc.Positioning, doc.Personality)),
|
||||
brandKitContextSection("Voice", compactValues(doc.Voice.Traits, doc.Voice.Guidance, doc.Voice.Sample)),
|
||||
}
|
||||
var logoLines []string
|
||||
for _, logo := range doc.Logos {
|
||||
logoLines = appendNonEmpty(logoLines, strings.Join(compactValues(logo.Name, logo.Variant, logo.Usage), " | "))
|
||||
}
|
||||
sections = append(sections, brandKitContextSection("Logo rules", logoLines))
|
||||
if doc.CoverImage != nil {
|
||||
sections = append(sections, brandKitContextSection("Cover image", compactValues(doc.CoverImage.Name, doc.CoverImage.Description)))
|
||||
}
|
||||
var colorLines []string
|
||||
for _, group := range doc.ColorGroups {
|
||||
for _, color := range group.Colors {
|
||||
colorLines = append(colorLines, strings.Join(compactValues(group.Name+" / "+color.Name, color.Hex, color.Usage), " | "))
|
||||
}
|
||||
}
|
||||
sections = append(sections, brandKitContextSection("Color system", colorLines))
|
||||
var typeLines []string
|
||||
for _, item := range doc.Typography {
|
||||
fileName := ""
|
||||
if item.FontFile != nil {
|
||||
fileName = item.FontFile.Name
|
||||
}
|
||||
typeLines = appendNonEmpty(typeLines, strings.Join(compactValues(item.Label, item.Family, fileName, item.Weight, item.Sizes, item.Description), " | "))
|
||||
}
|
||||
sections = append(sections, brandKitContextSection("Typography", typeLines))
|
||||
var referenceLines []string
|
||||
for _, image := range doc.ReferenceImages {
|
||||
referenceLines = appendNonEmpty(referenceLines, strings.Join(compactValues(image.Name, image.Description), ": "))
|
||||
}
|
||||
sections = append(sections, brandKitContextSection("Reference images", referenceLines))
|
||||
sections = append(sections, brandKitContextSection("Attached Brand Kit images (appended after user references)", brandKitReferenceImageContext(doc)))
|
||||
sections = append(sections,
|
||||
brandKitContextSection("Visual direction", compactValues(doc.Visual.Keywords, doc.Visual.Composition, doc.Visual.Imagery)),
|
||||
brandKitContextSection("Use", compactValues(doc.Visual.Dos)),
|
||||
brandKitContextSection("Avoid", compactValues(doc.Visual.Donts)),
|
||||
brandKitContextSection("Reusable direction", compactValues(doc.Visual.Prompt)),
|
||||
)
|
||||
sections = compactValues(sections...)
|
||||
contextBlock := "Binding creative system for this canvas. Apply it to planning, copy, composition, typography, color, and generated imagery unless the user explicitly overrides a rule in the current request.\n\n" + strings.Join(sections, "\n\n")
|
||||
return clampRunes(contextBlock, maxBrandKitContextRunes), nil
|
||||
}
|
||||
|
||||
func brandKitReferenceImages(raw []byte) ([]string, error) {
|
||||
var doc brandKitDocument
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, fmt.Errorf("%w: malformed stored document", brandkit.ErrInvalidInput)
|
||||
}
|
||||
images := make([]string, 0, len(doc.Logos)+len(doc.ReferenceImages)+1)
|
||||
seen := make(map[string]struct{})
|
||||
add := func(value string) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
if _, exists := seen[value]; exists {
|
||||
return
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
images = append(images, value)
|
||||
}
|
||||
for _, logo := range doc.Logos {
|
||||
add(logo.DataURL)
|
||||
}
|
||||
if doc.CoverImage != nil {
|
||||
add(doc.CoverImage.DataURL)
|
||||
}
|
||||
for _, image := range doc.ReferenceImages {
|
||||
add(image.DataURL)
|
||||
}
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func brandKitReferenceImageContext(doc brandKitDocument) []string {
|
||||
lines := make([]string, 0, len(doc.Logos)+len(doc.ReferenceImages)+1)
|
||||
index := 0
|
||||
for _, logo := range doc.Logos {
|
||||
if strings.TrimSpace(logo.DataURL) == "" {
|
||||
continue
|
||||
}
|
||||
index++
|
||||
lines = append(lines, fmt.Sprintf("Brand image %d | logo | %s", index, strings.Join(compactValues(logo.Name, logo.Variant, logo.Usage), " | ")))
|
||||
}
|
||||
if doc.CoverImage != nil && strings.TrimSpace(doc.CoverImage.DataURL) != "" {
|
||||
index++
|
||||
lines = append(lines, fmt.Sprintf("Brand image %d | cover | %s", index, strings.Join(compactValues(doc.CoverImage.Name, doc.CoverImage.Description), " | ")))
|
||||
}
|
||||
for _, image := range doc.ReferenceImages {
|
||||
if strings.TrimSpace(image.DataURL) == "" {
|
||||
continue
|
||||
}
|
||||
index++
|
||||
lines = append(lines, fmt.Sprintf("Brand image %d | visual reference | %s", index, strings.Join(compactValues(image.Name, image.Description), " | ")))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func brandKitContextSection(title string, values []string) string {
|
||||
values = compactValues(values...)
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return title + ":\n- " + strings.Join(values, "\n- ")
|
||||
}
|
||||
|
||||
func compactValues(values ...string) []string {
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func appendNonEmpty(values []string, value string) []string {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
return append(values, value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func textWithin(value string, max int) bool {
|
||||
return utf8.RuneCountInString(value) <= max
|
||||
}
|
||||
|
||||
func validDataURL(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
return value == "" || (strings.HasPrefix(value, "data:") && len(value) <= 12<<20)
|
||||
}
|
||||
|
||||
func validImageDataURL(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return true
|
||||
}
|
||||
if len(value) > 12<<20 {
|
||||
return false
|
||||
}
|
||||
for _, prefix := range []string{
|
||||
"data:image/png;base64,",
|
||||
"data:image/jpeg;base64,",
|
||||
"data:image/webp;base64,",
|
||||
"data:image/svg+xml;base64,",
|
||||
} {
|
||||
if strings.HasPrefix(value, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func oneOf(value string, allowed ...string) bool {
|
||||
for _, candidate := range allowed {
|
||||
if value == candidate {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func clampRunes(value string, max int) string {
|
||||
if max <= 0 || utf8.RuneCountInString(value) <= max {
|
||||
return value
|
||||
}
|
||||
runes := []rune(value)
|
||||
return string(runes[:max])
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"img_infinite_canvas/internal/domain/brandkit"
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
"img_infinite_canvas/internal/infrastructure/memory"
|
||||
)
|
||||
|
||||
const (
|
||||
brandKitTestUserA = "11111111-1111-4111-8111-111111111111"
|
||||
brandKitTestUserB = "22222222-2222-4222-8222-222222222222"
|
||||
)
|
||||
|
||||
func TestBrandKitServiceEnforcesOwnershipAndSingleDefault(t *testing.T) {
|
||||
service := NewBrandKitService(memory.NewBrandKitStore())
|
||||
ctxA := design.ContextWithUserID(context.Background(), brandKitTestUserA)
|
||||
ctxB := design.ContextWithUserID(context.Background(), brandKitTestUserB)
|
||||
|
||||
first, err := service.Upsert(ctxA, "brand-kit-alpha", testBrandKitDocument("brand-kit-alpha", "Alpha"), true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !first.IsDefault {
|
||||
t.Fatal("expected first kit to be default")
|
||||
}
|
||||
if _, err := service.Upsert(ctxA, "brand-kit-beta", testBrandKitDocument("brand-kit-beta", "Beta"), true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kits, err := service.List(ctxA)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaults := 0
|
||||
for _, kit := range kits {
|
||||
if kit.IsDefault {
|
||||
defaults++
|
||||
if kit.ID != "brand-kit-beta" {
|
||||
t.Fatalf("unexpected default kit %q", kit.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if defaults != 1 {
|
||||
t.Fatalf("expected one default, got %d", defaults)
|
||||
}
|
||||
if _, err := service.Get(ctxB, first.ID); !errors.Is(err, brandkit.ErrNotFound) {
|
||||
t.Fatalf("expected user isolation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrandKitServiceCompilesAuthoritativeContext(t *testing.T) {
|
||||
service := NewBrandKitService(memory.NewBrandKitStore())
|
||||
ctx := design.ContextWithUserID(context.Background(), brandKitTestUserA)
|
||||
kit, err := service.Upsert(ctx, "brand-kit-alpha", testBrandKitDocument("brand-kit-alpha", "Alpha"), false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compiled, err := service.ResolveContext(ctx, kit.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, expected := range []string{"Binding creative system", "Alpha", "Signal Green", "#28C76F", "Display"} {
|
||||
if !strings.Contains(compiled, expected) {
|
||||
t.Fatalf("compiled context missing %q: %s", expected, compiled)
|
||||
}
|
||||
}
|
||||
if strings.Contains(compiled, "data:") {
|
||||
t.Fatal("compiled context must not include embedded asset data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectBrandKitBindingUsesDefaultAndExplicitNone(t *testing.T) {
|
||||
store := memory.NewBrandKitStore()
|
||||
brandKits := NewBrandKitService(store)
|
||||
ctx := design.ContextWithUserID(context.Background(), brandKitTestUserA)
|
||||
if _, err := brandKits.Upsert(ctx, "brand-kit-alpha", testBrandKitDocument("brand-kit-alpha", "Alpha"), true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo := memory.NewProjectRepository()
|
||||
designs := NewDesignService(repo, nil, nil, nil, nil)
|
||||
designs.SetBrandKitService(brandKits)
|
||||
|
||||
withDefault, err := designs.CreateProjectWithBrandKit(ctx, "Default", "", "", "", "", 0, false, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if withDefault.BrandKitID != "brand-kit-alpha" || !strings.Contains(withDefault.BrandContext, "Alpha") {
|
||||
t.Fatalf("default Brand Kit was not bound: %#v", withDefault)
|
||||
}
|
||||
|
||||
none := ""
|
||||
withoutKit, err := designs.CreateProjectWithBrandKit(ctx, "None", "", "", "", "", 0, false, &none)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if withoutKit.BrandKitID != "" || withoutKit.BrandContext != "" {
|
||||
t.Fatalf("explicit None should bypass the default: %#v", withoutKit)
|
||||
}
|
||||
|
||||
bound, err := designs.UpdateProjectBrandKit(ctx, withoutKit.ID, "brand-kit-alpha")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bound.BrandKitID != "brand-kit-alpha" {
|
||||
t.Fatalf("expected explicit binding, got %q", bound.BrandKitID)
|
||||
}
|
||||
if err := designs.ClearBrandKitBindings(ctx, "brand-kit-alpha"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleared, err := repo.Get(ctx, bound.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cleared.BrandKitID != "" {
|
||||
t.Fatalf("expected binding cleanup, got %q", cleared.BrandKitID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectBrandKitBindingRejectsAnotherUsersKit(t *testing.T) {
|
||||
brandKits := NewBrandKitService(memory.NewBrandKitStore())
|
||||
ctxA := design.ContextWithUserID(context.Background(), brandKitTestUserA)
|
||||
ctxB := design.ContextWithUserID(context.Background(), brandKitTestUserB)
|
||||
if _, err := brandKits.Upsert(ctxA, "brand-kit-alpha", testBrandKitDocument("brand-kit-alpha", "Alpha"), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
repo := memory.NewProjectRepository()
|
||||
designs := NewDesignService(repo, nil, nil, nil, nil)
|
||||
designs.SetBrandKitService(brandKits)
|
||||
none := ""
|
||||
project, err := designs.CreateProjectAsyncWithBrandKit(ctxB, "Other user", "", "", "", "", 0, false, &none)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := designs.UpdateProjectBrandKit(ctxB, project.ID, "brand-kit-alpha"); !errors.Is(err, brandkit.ErrNotFound) {
|
||||
t.Fatalf("expected ownership check to hide the Brand Kit, got %v", err)
|
||||
}
|
||||
stored, err := repo.Get(ctxB, project.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stored.BrandKitID != "" {
|
||||
t.Fatalf("failed binding must not mutate the project, got %q", stored.BrandKitID)
|
||||
}
|
||||
}
|
||||
|
||||
func testBrandKitDocument(id string, name string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"version": 1,
|
||||
"id": %q,
|
||||
"name": %q,
|
||||
"brandName": %q,
|
||||
"tagline": "Make the signal clear",
|
||||
"summary": "A focused product brand.",
|
||||
"audience": "Creative teams",
|
||||
"positioning": "Clear and precise",
|
||||
"personality": "Confident",
|
||||
"voice": {"traits": "Direct", "guidance": "Lead with outcomes", "sample": "Move with clarity"},
|
||||
"logos": [{"id": "logo-primary", "name": "Primary", "variant": "primary", "usage": "Light backgrounds", "source": "upload", "dataUrl": "data:image/png;base64,AA=="}],
|
||||
"colorGroups": [{"id": "colors-primary", "name": "Primary", "colors": [{"id": "color-signal", "name": "Signal Green", "hex": "#28C76F", "usage": "Key actions"}]}],
|
||||
"typography": [{"id": "type-display", "label": "Display", "family": "Avenir Next", "weight": "700", "sizes": "48", "description": "Headlines"}],
|
||||
"referenceImages": [],
|
||||
"visual": {"keywords": "precise", "composition": "strict grid", "imagery": "real product", "dos": "use space", "donts": "avoid noise", "prompt": "Keep the product clear"},
|
||||
"applyToNewProjects": false,
|
||||
"createdAt": "2026-01-01T00:00:00Z",
|
||||
"updatedAt": "2026-01-01T00:00:00Z"
|
||||
}`, id, name, name)
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"img_infinite_canvas/internal/domain/brandkit"
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -65,6 +66,7 @@ type DesignService struct {
|
||||
textExtractionCache textExtractionCacheStore
|
||||
textExtractionCacheTTL time.Duration
|
||||
creativeAgent design.CreativeAgent
|
||||
brandKits *BrandKitService
|
||||
cancelledAgentTasks sync.Map
|
||||
agentTaskCancels sync.Map
|
||||
generatorTaskMu sync.RWMutex
|
||||
@@ -132,6 +134,10 @@ func (s *DesignService) SetTextExtractionCache(store textExtractionCacheStore, t
|
||||
s.textExtractionCacheTTL = ttl
|
||||
}
|
||||
|
||||
func (s *DesignService) SetBrandKitService(service *BrandKitService) {
|
||||
s.brandKits = service
|
||||
}
|
||||
|
||||
func (s *DesignService) SetBackgroundRemover(remover design.BackgroundRemover) {
|
||||
s.backgroundRemover = remover
|
||||
}
|
||||
@@ -313,10 +319,130 @@ func (s *DesignService) DeleteProjects(ctx context.Context, ids []string) (int64
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func (s *DesignService) UpdateProjectBrandKit(ctx context.Context, projectID string, brandKitID string) (design.Project, error) {
|
||||
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
project.BrandKitID = strings.TrimSpace(brandKitID)
|
||||
project.BrandContext = ""
|
||||
project.BrandImages = nil
|
||||
if project.BrandKitID != "" {
|
||||
if s.brandKits == nil {
|
||||
return design.Project{}, brandkit.ErrNotFound
|
||||
}
|
||||
resolved, err := s.brandKits.Resolve(ctx, project.BrandKitID)
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
project.BrandContext = resolved.Context
|
||||
project.BrandImages = append([]string(nil), resolved.ReferenceImages...)
|
||||
}
|
||||
project.UpdatedAt = s.now()
|
||||
if err := s.repo.Save(ctx, project); err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *DesignService) ClearBrandKitBindings(ctx context.Context, brandKitID string) error {
|
||||
brandKitID = strings.TrimSpace(brandKitID)
|
||||
if brandKitID == "" {
|
||||
return nil
|
||||
}
|
||||
items, err := s.repo.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
project, err := s.repo.Get(ctx, item.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if project.BrandKitID != brandKitID {
|
||||
continue
|
||||
}
|
||||
project.BrandKitID = ""
|
||||
project.BrandContext = ""
|
||||
project.BrandImages = nil
|
||||
project.UpdatedAt = s.now()
|
||||
if err := s.repo.Save(ctx, project); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DesignService) applyInitialBrandKit(ctx context.Context, project design.Project, requestedID *string) (design.Project, error) {
|
||||
if s.brandKits == nil {
|
||||
return project, nil
|
||||
}
|
||||
if requestedID != nil {
|
||||
project.BrandKitID = strings.TrimSpace(*requestedID)
|
||||
if project.BrandKitID == "" {
|
||||
return project, nil
|
||||
}
|
||||
resolved, err := s.brandKits.Resolve(ctx, project.BrandKitID)
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
project.BrandContext = resolved.Context
|
||||
project.BrandImages = append([]string(nil), resolved.ReferenceImages...)
|
||||
return project, nil
|
||||
}
|
||||
resolved, err := s.brandKits.ResolveDefault(ctx)
|
||||
if errors.Is(err, brandkit.ErrNotFound) {
|
||||
return project, nil
|
||||
}
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
project.BrandKitID = resolved.ID
|
||||
project.BrandContext = resolved.Context
|
||||
project.BrandImages = append([]string(nil), resolved.ReferenceImages...)
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *DesignService) hydrateBrandKitContext(ctx context.Context, project design.Project) (design.Project, error) {
|
||||
project.BrandContext = ""
|
||||
project.BrandImages = nil
|
||||
if strings.TrimSpace(project.BrandKitID) == "" || s.brandKits == nil {
|
||||
return project, nil
|
||||
}
|
||||
resolved, err := s.brandKits.Resolve(ctx, project.BrandKitID)
|
||||
if errors.Is(err, brandkit.ErrNotFound) {
|
||||
project.BrandKitID = ""
|
||||
return project, nil
|
||||
}
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
project.BrandContext = resolved.Context
|
||||
project.BrandImages = append([]string(nil), resolved.ReferenceImages...)
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *DesignService) getProjectWithBrandKit(ctx context.Context, projectID string) (design.Project, error) {
|
||||
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
return s.hydrateBrandKitContext(ctx, project)
|
||||
}
|
||||
|
||||
func (s *DesignService) CreateProject(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool) (design.Project, error) {
|
||||
return s.CreateProjectWithBrandKit(ctx, title, prompt, imageModel, imageSize, imageQuality, imageCount, enableWebSearch, nil)
|
||||
}
|
||||
|
||||
func (s *DesignService) CreateProjectWithBrandKit(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool, brandKitID *string) (design.Project, error) {
|
||||
prompt = design.NormalizePrompt(prompt)
|
||||
if prompt == "" {
|
||||
project, _ := s.newProjectShell(ctx, title, prompt)
|
||||
var err error
|
||||
project, err = s.applyInitialBrandKit(ctx, project, brandKitID)
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
project = s.refreshCanvasSnapshot(project, project.UpdatedAt)
|
||||
if err := s.repo.Save(ctx, project); err != nil {
|
||||
return design.Project{}, err
|
||||
@@ -345,6 +471,11 @@ func (s *DesignService) CreateProject(ctx context.Context, title string, prompt
|
||||
},
|
||||
},
|
||||
}
|
||||
var err error
|
||||
project, err = s.applyInitialBrandKit(ctx, project, brandKitID)
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
|
||||
agentReq := design.AgentRequest{
|
||||
Project: project,
|
||||
@@ -370,7 +501,7 @@ func (s *DesignService) CreateProject(ctx context.Context, title string, prompt
|
||||
if err := s.maybeAddResearchStep(ctx, project.ID, "", project, prompt, "design", agentReq.Messages, true); err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
if nextProject, err := s.repo.Get(ctx, project.ID); err == nil {
|
||||
if nextProject, err := s.getProjectWithBrandKit(ctx, project.ID); err == nil {
|
||||
project = nextProject
|
||||
agentReq.Project = project
|
||||
}
|
||||
@@ -404,7 +535,16 @@ func (s *DesignService) CreateProject(ctx context.Context, title string, prompt
|
||||
}
|
||||
|
||||
func (s *DesignService) CreateProjectAsync(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool) (design.Project, error) {
|
||||
return s.CreateProjectAsyncWithBrandKit(ctx, title, prompt, imageModel, imageSize, imageQuality, imageCount, enableWebSearch, nil)
|
||||
}
|
||||
|
||||
func (s *DesignService) CreateProjectAsyncWithBrandKit(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool, brandKitID *string) (design.Project, error) {
|
||||
project, prompt := s.newProjectShell(ctx, title, prompt)
|
||||
var err error
|
||||
project, err = s.applyInitialBrandKit(ctx, project, brandKitID)
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
if prompt == "" {
|
||||
project = s.refreshCanvasSnapshot(project, project.UpdatedAt)
|
||||
if err := s.repo.Save(ctx, project); err != nil {
|
||||
@@ -437,7 +577,7 @@ func (s *DesignService) Generate(ctx context.Context, projectID string, prompt s
|
||||
return GenerateResult{}, fmt.Errorf("%w: prompt is required", design.ErrInvalidInput)
|
||||
}
|
||||
|
||||
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
|
||||
project, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return GenerateResult{}, err
|
||||
}
|
||||
@@ -543,7 +683,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
|
||||
threadIDType = 9
|
||||
}
|
||||
|
||||
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
|
||||
project, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return design.AgentThread{}, err
|
||||
}
|
||||
@@ -590,7 +730,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
|
||||
Memory: buildAgentMemory(projectScopedToAgentThread(project, threadID), prompt, mode, req.Messages),
|
||||
WebSearchEnabled: enableWebSearch,
|
||||
})
|
||||
project, err = s.repo.Get(ctx, project.ID)
|
||||
project, err = s.getProjectWithBrandKit(ctx, project.ID)
|
||||
if err != nil {
|
||||
return design.AgentThread{}, err
|
||||
}
|
||||
@@ -627,7 +767,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
|
||||
if err := s.maybeAddPlannedResearchStep(ctx, project.ID, threadID, projectScopedToAgentThread(project, threadID), prompt, mode, req.Messages, enableWebSearch, taskPlan); err != nil {
|
||||
return design.AgentThread{}, err
|
||||
}
|
||||
project, err = s.repo.Get(ctx, project.ID)
|
||||
project, err = s.getProjectWithBrandKit(ctx, project.ID)
|
||||
if err != nil {
|
||||
return design.AgentThread{}, err
|
||||
}
|
||||
@@ -1529,8 +1669,15 @@ func (s *DesignService) completeAgentConversation(ctx context.Context, projectID
|
||||
} else if cancelled {
|
||||
return nil
|
||||
}
|
||||
currentProject, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Project.BrandKitID = currentProject.BrandKitID
|
||||
req.Project.BrandContext = currentProject.BrandContext
|
||||
req.Project.BrandImages = append([]string(nil), currentProject.BrandImages...)
|
||||
req.Memory = buildAgentMemory(req.Project, req.Prompt, req.Mode, req.Messages)
|
||||
var content string
|
||||
var err error
|
||||
if streamer, ok := s.creativeAgent.(design.StreamingCreativeAgent); ok {
|
||||
content, err = s.streamAgentChatResponse(ctx, projectID, messageID, req, streamer)
|
||||
} else {
|
||||
@@ -1590,7 +1737,7 @@ func (s *DesignService) createAgentChatResponse(ctx context.Context, req design.
|
||||
}
|
||||
|
||||
func (s *DesignService) updateAgentMessage(ctx context.Context, projectID string, messageID string, content string, status string, projectStatus design.ProjectStatus) error {
|
||||
project, err := s.repo.Get(ctx, projectID)
|
||||
project, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1905,7 +2052,7 @@ func (s *DesignService) completeNodeActionGeneration(ctx context.Context, projec
|
||||
return s.markNodeActionFailed(detachedUserContext(ctx), projectID, threadID, resultNodeID, fmt.Errorf("image action returned no image"))
|
||||
}
|
||||
|
||||
project, err = s.repo.Get(ctx, projectID)
|
||||
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2499,7 +2646,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
||||
}
|
||||
return err
|
||||
}
|
||||
project, err := s.repo.Get(ctx, projectID)
|
||||
project, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2522,7 +2669,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
||||
Memory: buildAgentMemory(project, prompt, "design", messages),
|
||||
WebSearchEnabled: enableWebSearch,
|
||||
})
|
||||
project, err = s.repo.Get(ctx, projectID)
|
||||
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2540,7 +2687,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
||||
if err := s.maybeAddPlannedResearchStep(ctx, projectID, threadID, project, prompt, "design", messages, enableWebSearch, taskPlan); err != nil {
|
||||
return err
|
||||
}
|
||||
project, err = s.repo.Get(ctx, projectID)
|
||||
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2549,7 +2696,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqProject, err := s.repo.Get(ctx, projectID)
|
||||
reqProject, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2565,14 +2712,14 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
||||
if err := s.applyCreateProjectTaskPlan(ctx, projectID, threadID, prompt, imageSize, taskPlan); err != nil {
|
||||
return err
|
||||
}
|
||||
project, err = s.repo.Get(ctx, projectID)
|
||||
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.addGenerationStep(ctx, projectID, threadID, "tool", "GPT Image 2", generationStepContent(taskPlan)); err != nil {
|
||||
return err
|
||||
}
|
||||
project, err = s.repo.Get(ctx, projectID)
|
||||
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2905,7 +3052,7 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
|
||||
}
|
||||
return err
|
||||
}
|
||||
project, err := s.repo.Get(ctx, projectID)
|
||||
project, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2928,7 +3075,7 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
|
||||
if err := s.addGenerationStep(ctx, projectID, threadID, "tool", "GPT Image 2", generationStepContent(taskPlan)); err != nil {
|
||||
return err
|
||||
}
|
||||
project, err = s.repo.Get(ctx, projectID)
|
||||
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user