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:
+56
-1
@@ -155,6 +155,8 @@ Records a logout request server-side and returns `{"removed": 0}`. The frontend
|
|||||||
|
|
||||||
## Projects
|
## Projects
|
||||||
|
|
||||||
|
Brand Kit selection is optional on project creation. Omit `brandKitId` to apply the authenticated user's default Brand Kit, send an empty string to explicitly use no Brand Kit, or send a user-owned Brand Kit ID.
|
||||||
|
|
||||||
`GET /api/projects`
|
`GET /api/projects`
|
||||||
|
|
||||||
Returns all project summaries, sorted by `updatedAt` descending.
|
Returns all project summaries, sorted by `updatedAt` descending.
|
||||||
@@ -165,6 +167,7 @@ Returns all project summaries, sorted by `updatedAt` descending.
|
|||||||
{
|
{
|
||||||
"title": "still day 官网",
|
"title": "still day 官网",
|
||||||
"prompt": "为 still day 品牌服装独立站设计官网视觉方向",
|
"prompt": "为 still day 品牌服装独立站设计官网视觉方向",
|
||||||
|
"brandKitId": "brand-kit-core",
|
||||||
"imageModel": "gpt-image-2",
|
"imageModel": "gpt-image-2",
|
||||||
"imageSize": "1024x1024",
|
"imageSize": "1024x1024",
|
||||||
"imageQuality": "auto",
|
"imageQuality": "auto",
|
||||||
@@ -177,7 +180,7 @@ Returns all project summaries, sorted by `updatedAt` descending.
|
|||||||
|
|
||||||
`GET /api/projects/:id`
|
`GET /api/projects/:id`
|
||||||
|
|
||||||
Returns the full project, including viewport, nodes, connections, and agent messages.
|
Returns the full project, including `brandKitId`, viewport, nodes, connections, and agent messages. Compiled Brand Kit instructions and uploaded assets remain server-side and are never accepted from the client as authoritative prompt context.
|
||||||
|
|
||||||
`POST /api/canva/project/queryProject`
|
`POST /api/canva/project/queryProject`
|
||||||
|
|
||||||
@@ -200,10 +203,62 @@ Returns a Lovart-style project document with `assetMeta`, compressed `canvas`, `
|
|||||||
|
|
||||||
Renames the infinite canvas project and updates `updatedAt`.
|
Renames the infinite canvas project and updates `updatedAt`.
|
||||||
|
|
||||||
|
`PATCH /api/projects/:id/brand-kit`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"brandKitId": "brand-kit-core"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only the project owner can change this binding. The ID must belong to the authenticated user. Send `{"brandKitId":""}` to remove the Brand Kit from the canvas without deleting it.
|
||||||
|
|
||||||
`DELETE /api/projects/:id`
|
`DELETE /api/projects/:id`
|
||||||
|
|
||||||
Deletes the project record. PostgreSQL cascades canvas nodes, chat messages, history, and connections; after the database delete succeeds, image assets referenced by the canvas and generated-file history are queued for asynchronous OSS deletion.
|
Deletes the project record. PostgreSQL cascades canvas nodes, chat messages, history, and connections; after the database delete succeeds, image assets referenced by the canvas and generated-file history are queued for asynchronous OSS deletion.
|
||||||
|
|
||||||
|
## Brand Kits
|
||||||
|
|
||||||
|
All Brand Kit endpoints require `Authorization: Bearer <accessToken>`. Data is scoped to the authenticated user ID; IDs owned by another user are returned as not found.
|
||||||
|
|
||||||
|
`GET /api/brand-kits`
|
||||||
|
|
||||||
|
Returns the user's Brand Kits, newest first:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"brandKits": [
|
||||||
|
{
|
||||||
|
"id": "brand-kit-core",
|
||||||
|
"name": "Core Brand",
|
||||||
|
"document": "{\"version\":1,\"id\":\"brand-kit-core\",\"name\":\"Core Brand\",...}",
|
||||||
|
"isDefault": true,
|
||||||
|
"createdAt": "2026-07-10T08:00:00Z",
|
||||||
|
"updatedAt": "2026-07-10T08:15:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`PUT /api/brand-kits/:id`
|
||||||
|
|
||||||
|
Creates or replaces a version-1 Brand Kit document. The document supports brand foundation, voice, grouped colors, uploaded fonts with optional sizes and descriptions, logos, an optional cover, reference images, and reusable visual direction.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"document": "{\"version\":1,\"id\":\"brand-kit-core\",\"name\":\"Core Brand\",\"brandName\":\"Moteva\",\"tagline\":\"\",\"summary\":\"Creative workspace\",\"audience\":\"Design teams\",\"positioning\":\"\",\"personality\":\"Clear\",\"voice\":{\"traits\":\"Direct\",\"guidance\":\"Lead with outcomes\",\"sample\":\"Move with clarity\"},\"logos\":[],\"colorGroups\":[],\"typography\":[],\"referenceImages\":[],\"visual\":{\"keywords\":\"Precise\",\"composition\":\"\",\"imagery\":\"\",\"dos\":\"\",\"donts\":\"\",\"prompt\":\"\"},\"applyToNewProjects\":true,\"createdAt\":\"\",\"updatedAt\":\"\"}",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
At most one Brand Kit is default per user. The server canonicalizes timestamps and default state rather than trusting those fields inside `document`.
|
||||||
|
|
||||||
|
`DELETE /api/brand-kits/:id`
|
||||||
|
|
||||||
|
Deletes the owned Brand Kit. Existing project bindings are cleared; projects and canvas content remain intact.
|
||||||
|
|
||||||
|
For every Agent Chat turn, new thread, planner request, and image generation, the server resolves the currently bound Brand Kit again. Text rules become binding creative context, while uploaded logos, cover images, and visual references are appended after any user-supplied reference images.
|
||||||
|
|
||||||
## Sharing and access control
|
## Sharing and access control
|
||||||
|
|
||||||
Sharing uses a separate authorization policy from project ownership. Effective access is resolved in this order: owner, explicit email/phone member, then link permission.
|
Sharing uses a separate authorization policy from project ownership. Effective access is resolved in this order: owner, explicit email/phone member, then link permission.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ go-zero backend for the Moteva-style infinite canvas MVP.
|
|||||||
|
|
||||||
- Dashboard data for the Moteva home page
|
- Dashboard data for the Moteva home page
|
||||||
- Project creation from prompt
|
- Project creation from prompt
|
||||||
|
- User-owned Brand Kits with optional per-canvas binding and server-authoritative creative context
|
||||||
- Project title rename
|
- Project title rename
|
||||||
- Full canvas project reads
|
- Full canvas project reads
|
||||||
- Moteva-style generation mock for image/design workflows
|
- Moteva-style generation mock for image/design workflows
|
||||||
@@ -144,6 +145,14 @@ curl -X POST http://localhost:8888/api/canva/project/saveProject \
|
|||||||
curl -X POST http://localhost:8888/api/assets/upload-url \
|
curl -X POST http://localhost:8888/api/assets/upload-url \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"fileName":"ref.png","contentType":"image/png"}'
|
-d '{"fileName":"ref.png","contentType":"image/png"}'
|
||||||
|
|
||||||
|
curl http://localhost:8888/api/brand-kits \
|
||||||
|
-H "Authorization: Bearer <access-token>"
|
||||||
|
|
||||||
|
curl -X PATCH http://localhost:8888/api/projects/<project-id>/brand-kit \
|
||||||
|
-H "Authorization: Bearer <access-token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"brandKitId":"brand-kit-example"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Detailed endpoint docs are in [API.md](API.md).
|
Detailed endpoint docs are in [API.md](API.md).
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ type ProjectSummary {
|
|||||||
Brief string `json:"brief"`
|
Brief string `json:"brief"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Thumbnail string `json:"thumbnail"`
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
BrandKitId string `json:"brandKitId,optional"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +293,7 @@ type Project {
|
|||||||
SnapshotId string `json:"snapshotId"`
|
SnapshotId string `json:"snapshotId"`
|
||||||
Canvas string `json:"canvas"`
|
Canvas string `json:"canvas"`
|
||||||
LastThreadId string `json:"lastThreadId"`
|
LastThreadId string `json:"lastThreadId"`
|
||||||
|
BrandKitId string `json:"brandKitId,optional"`
|
||||||
Viewport CanvasViewport `json:"viewport"`
|
Viewport CanvasViewport `json:"viewport"`
|
||||||
Nodes []CanvasNode `json:"nodes"`
|
Nodes []CanvasNode `json:"nodes"`
|
||||||
Connections []CanvasConnection `json:"connections"`
|
Connections []CanvasConnection `json:"connections"`
|
||||||
@@ -328,6 +330,7 @@ type QueryProjectRequest {
|
|||||||
type CreateProjectRequest {
|
type CreateProjectRequest {
|
||||||
Title string `json:"title,optional"`
|
Title string `json:"title,optional"`
|
||||||
Prompt string `json:"prompt,optional"`
|
Prompt string `json:"prompt,optional"`
|
||||||
|
BrandKitId *string `json:"brandKitId,optional"`
|
||||||
ImageModel string `json:"imageModel,optional"`
|
ImageModel string `json:"imageModel,optional"`
|
||||||
ImageSize string `json:"imageSize,optional"`
|
ImageSize string `json:"imageSize,optional"`
|
||||||
ImageQuality string `json:"imageQuality,optional"`
|
ImageQuality string `json:"imageQuality,optional"`
|
||||||
@@ -336,6 +339,43 @@ type CreateProjectRequest {
|
|||||||
AllowEmptyPrompt bool `json:"allowEmptyPrompt,optional"`
|
AllowEmptyPrompt bool `json:"allowEmptyPrompt,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BrandKit {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Document string `json:"document"`
|
||||||
|
IsDefault bool `json:"isDefault"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrandKitListResponse {
|
||||||
|
BrandKits []BrandKit `json:"brandKits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrandKitResponse {
|
||||||
|
BrandKit BrandKit `json:"brandKit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrandKitIdRequest {
|
||||||
|
Id string `path:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpsertBrandKitRequest {
|
||||||
|
Id string `path:"id"`
|
||||||
|
Document string `json:"document"`
|
||||||
|
IsDefault bool `json:"isDefault,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteBrandKitResponse {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Deleted bool `json:"deleted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProjectBrandKitRequest {
|
||||||
|
Id string `path:"id"`
|
||||||
|
BrandKitId string `json:"brandKitId,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateProjectTitleRequest {
|
type UpdateProjectTitleRequest {
|
||||||
Id string `path:"id"`
|
Id string `path:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@@ -383,6 +423,7 @@ type QueryProjectData {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
SnapshotId *string `json:"snapshotId"`
|
SnapshotId *string `json:"snapshotId"`
|
||||||
ItemId *string `json:"itemId"`
|
ItemId *string `json:"itemId"`
|
||||||
|
BrandKitId string `json:"brandKitId,optional"`
|
||||||
Permission string `json:"permission"`
|
Permission string `json:"permission"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1008,6 +1049,15 @@ service img_infinite_canvas-api {
|
|||||||
@handler logoutAccount
|
@handler logoutAccount
|
||||||
post /account/logout returns (AccountDeviceMutationResponse)
|
post /account/logout returns (AccountDeviceMutationResponse)
|
||||||
|
|
||||||
|
@handler listBrandKits
|
||||||
|
get /brand-kits returns (BrandKitListResponse)
|
||||||
|
|
||||||
|
@handler upsertBrandKit
|
||||||
|
put /brand-kits/:id (UpsertBrandKitRequest) returns (BrandKitResponse)
|
||||||
|
|
||||||
|
@handler deleteBrandKit
|
||||||
|
delete /brand-kits/:id (BrandKitIdRequest) returns (DeleteBrandKitResponse)
|
||||||
|
|
||||||
@handler listProjects
|
@handler listProjects
|
||||||
get /projects (ProjectListRequest) returns (ProjectListResponse)
|
get /projects (ProjectListRequest) returns (ProjectListResponse)
|
||||||
|
|
||||||
@@ -1041,6 +1091,9 @@ service img_infinite_canvas-api {
|
|||||||
@handler updateProjectTitle
|
@handler updateProjectTitle
|
||||||
patch /projects/:id/title (UpdateProjectTitleRequest) returns (ProjectResponse)
|
patch /projects/:id/title (UpdateProjectTitleRequest) returns (ProjectResponse)
|
||||||
|
|
||||||
|
@handler updateProjectBrandKit
|
||||||
|
patch /projects/:id/brand-kit (UpdateProjectBrandKitRequest) returns (ProjectResponse)
|
||||||
|
|
||||||
@handler getProject
|
@handler getProject
|
||||||
get /projects/:id (ProjectIdRequest) returns (ProjectResponse)
|
get /projects/:id (ProjectIdRequest) returns (ProjectResponse)
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ func buildLongMemory(project design.Project) string {
|
|||||||
sections = append(sections, "- brief: "+shortenMemoryText(project.Brief, 360))
|
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 != "" {
|
if decisions := durableDecisionMemory(memoryMessages, longMemoryDecisionLimit); decisions != "" {
|
||||||
sections = append(sections, "Durable user decisions and preferences:", 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"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"img_infinite_canvas/internal/domain/brandkit"
|
||||||
"img_infinite_canvas/internal/domain/design"
|
"img_infinite_canvas/internal/domain/design"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -65,6 +66,7 @@ type DesignService struct {
|
|||||||
textExtractionCache textExtractionCacheStore
|
textExtractionCache textExtractionCacheStore
|
||||||
textExtractionCacheTTL time.Duration
|
textExtractionCacheTTL time.Duration
|
||||||
creativeAgent design.CreativeAgent
|
creativeAgent design.CreativeAgent
|
||||||
|
brandKits *BrandKitService
|
||||||
cancelledAgentTasks sync.Map
|
cancelledAgentTasks sync.Map
|
||||||
agentTaskCancels sync.Map
|
agentTaskCancels sync.Map
|
||||||
generatorTaskMu sync.RWMutex
|
generatorTaskMu sync.RWMutex
|
||||||
@@ -132,6 +134,10 @@ func (s *DesignService) SetTextExtractionCache(store textExtractionCacheStore, t
|
|||||||
s.textExtractionCacheTTL = ttl
|
s.textExtractionCacheTTL = ttl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DesignService) SetBrandKitService(service *BrandKitService) {
|
||||||
|
s.brandKits = service
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DesignService) SetBackgroundRemover(remover design.BackgroundRemover) {
|
func (s *DesignService) SetBackgroundRemover(remover design.BackgroundRemover) {
|
||||||
s.backgroundRemover = remover
|
s.backgroundRemover = remover
|
||||||
}
|
}
|
||||||
@@ -313,10 +319,130 @@ func (s *DesignService) DeleteProjects(ctx context.Context, ids []string) (int64
|
|||||||
return deleted, nil
|
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) {
|
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)
|
prompt = design.NormalizePrompt(prompt)
|
||||||
if prompt == "" {
|
if prompt == "" {
|
||||||
project, _ := s.newProjectShell(ctx, title, 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)
|
project = s.refreshCanvasSnapshot(project, project.UpdatedAt)
|
||||||
if err := s.repo.Save(ctx, project); err != nil {
|
if err := s.repo.Save(ctx, project); err != nil {
|
||||||
return design.Project{}, err
|
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{
|
agentReq := design.AgentRequest{
|
||||||
Project: project,
|
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 {
|
if err := s.maybeAddResearchStep(ctx, project.ID, "", project, prompt, "design", agentReq.Messages, true); err != nil {
|
||||||
return design.Project{}, err
|
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
|
project = nextProject
|
||||||
agentReq.Project = project
|
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) {
|
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)
|
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 == "" {
|
if prompt == "" {
|
||||||
project = s.refreshCanvasSnapshot(project, project.UpdatedAt)
|
project = s.refreshCanvasSnapshot(project, project.UpdatedAt)
|
||||||
if err := s.repo.Save(ctx, project); err != nil {
|
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)
|
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 {
|
if err != nil {
|
||||||
return GenerateResult{}, err
|
return GenerateResult{}, err
|
||||||
}
|
}
|
||||||
@@ -543,7 +683,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
|
|||||||
threadIDType = 9
|
threadIDType = 9
|
||||||
}
|
}
|
||||||
|
|
||||||
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
|
project, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return design.AgentThread{}, err
|
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),
|
Memory: buildAgentMemory(projectScopedToAgentThread(project, threadID), prompt, mode, req.Messages),
|
||||||
WebSearchEnabled: enableWebSearch,
|
WebSearchEnabled: enableWebSearch,
|
||||||
})
|
})
|
||||||
project, err = s.repo.Get(ctx, project.ID)
|
project, err = s.getProjectWithBrandKit(ctx, project.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return design.AgentThread{}, err
|
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 {
|
if err := s.maybeAddPlannedResearchStep(ctx, project.ID, threadID, projectScopedToAgentThread(project, threadID), prompt, mode, req.Messages, enableWebSearch, taskPlan); err != nil {
|
||||||
return design.AgentThread{}, err
|
return design.AgentThread{}, err
|
||||||
}
|
}
|
||||||
project, err = s.repo.Get(ctx, project.ID)
|
project, err = s.getProjectWithBrandKit(ctx, project.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return design.AgentThread{}, err
|
return design.AgentThread{}, err
|
||||||
}
|
}
|
||||||
@@ -1529,8 +1669,15 @@ func (s *DesignService) completeAgentConversation(ctx context.Context, projectID
|
|||||||
} else if cancelled {
|
} else if cancelled {
|
||||||
return nil
|
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 content string
|
||||||
var err error
|
|
||||||
if streamer, ok := s.creativeAgent.(design.StreamingCreativeAgent); ok {
|
if streamer, ok := s.creativeAgent.(design.StreamingCreativeAgent); ok {
|
||||||
content, err = s.streamAgentChatResponse(ctx, projectID, messageID, req, streamer)
|
content, err = s.streamAgentChatResponse(ctx, projectID, messageID, req, streamer)
|
||||||
} else {
|
} 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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
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"))
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -2499,7 +2646,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
project, err := s.repo.Get(ctx, projectID)
|
project, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -2522,7 +2669,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
|||||||
Memory: buildAgentMemory(project, prompt, "design", messages),
|
Memory: buildAgentMemory(project, prompt, "design", messages),
|
||||||
WebSearchEnabled: enableWebSearch,
|
WebSearchEnabled: enableWebSearch,
|
||||||
})
|
})
|
||||||
project, err = s.repo.Get(ctx, projectID)
|
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err := s.maybeAddPlannedResearchStep(ctx, projectID, threadID, project, prompt, "design", messages, enableWebSearch, taskPlan); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
project, err = s.repo.Get(ctx, projectID)
|
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -2549,7 +2696,7 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reqProject, err := s.repo.Get(ctx, projectID)
|
reqProject, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err := s.applyCreateProjectTaskPlan(ctx, projectID, threadID, prompt, imageSize, taskPlan); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
project, err = s.repo.Get(ctx, projectID)
|
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.addGenerationStep(ctx, projectID, threadID, "tool", "GPT Image 2", generationStepContent(taskPlan)); err != nil {
|
if err := s.addGenerationStep(ctx, projectID, threadID, "tool", "GPT Image 2", generationStepContent(taskPlan)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
project, err = s.repo.Get(ctx, projectID)
|
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -2905,7 +3052,7 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
project, err := s.repo.Get(ctx, projectID)
|
project, err := s.getProjectWithBrandKit(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err := s.addGenerationStep(ctx, projectID, threadID, "tool", "GPT Image 2", generationStepContent(taskPlan)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
project, err = s.repo.Get(ctx, projectID)
|
project, err = s.getProjectWithBrandKit(ctx, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package brandkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidInput = errors.New("invalid brand kit")
|
||||||
|
ErrNotFound = errors.New("brand kit not found")
|
||||||
|
ErrConflict = errors.New("brand kit conflict")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Kit struct {
|
||||||
|
ID string
|
||||||
|
UserID string
|
||||||
|
Name string
|
||||||
|
Document json.RawMessage
|
||||||
|
IsDefault bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
List(ctx context.Context, userID string) ([]Kit, error)
|
||||||
|
Get(ctx context.Context, userID string, id string) (Kit, error)
|
||||||
|
GetDefault(ctx context.Context, userID string) (Kit, error)
|
||||||
|
Upsert(ctx context.Context, kit Kit) (Kit, error)
|
||||||
|
Delete(ctx context.Context, userID string, id string) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clone(kit Kit) Kit {
|
||||||
|
kit.Document = append(json.RawMessage(nil), kit.Document...)
|
||||||
|
return kit
|
||||||
|
}
|
||||||
@@ -201,6 +201,9 @@ type Project struct {
|
|||||||
SnapshotID string
|
SnapshotID string
|
||||||
Canvas string
|
Canvas string
|
||||||
LastThreadID string
|
LastThreadID string
|
||||||
|
BrandKitID string
|
||||||
|
BrandContext string
|
||||||
|
BrandImages []string
|
||||||
Viewport Viewport
|
Viewport Viewport
|
||||||
Nodes []Node
|
Nodes []Node
|
||||||
Connections []Connection
|
Connections []Connection
|
||||||
@@ -214,6 +217,7 @@ type ProjectSummary struct {
|
|||||||
Brief string
|
Brief string
|
||||||
Status ProjectStatus
|
Status ProjectStatus
|
||||||
Thumbnail string
|
Thumbnail string
|
||||||
|
BrandKitID string
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"img_infinite_canvas/internal/domain/brandkit"
|
||||||
"img_infinite_canvas/internal/domain/design"
|
"img_infinite_canvas/internal/domain/design"
|
||||||
authmodule "img_infinite_canvas/internal/modules/auth"
|
authmodule "img_infinite_canvas/internal/modules/auth"
|
||||||
sharingmodule "img_infinite_canvas/internal/modules/sharing"
|
sharingmodule "img_infinite_canvas/internal/modules/sharing"
|
||||||
@@ -34,6 +35,12 @@ func Handle(ctx context.Context, err error) (int, any) {
|
|||||||
return http.StatusBadRequest, map[string]any{"error": err.Error(), "code": http.StatusBadRequest}
|
return http.StatusBadRequest, map[string]any{"error": err.Error(), "code": http.StatusBadRequest}
|
||||||
case errors.Is(err, design.ErrConflict):
|
case errors.Is(err, design.ErrConflict):
|
||||||
return http.StatusConflict, map[string]any{"error": err.Error(), "code": http.StatusConflict}
|
return http.StatusConflict, map[string]any{"error": err.Error(), "code": http.StatusConflict}
|
||||||
|
case errors.Is(err, brandkit.ErrNotFound):
|
||||||
|
return http.StatusNotFound, map[string]any{"error": brandkit.ErrNotFound.Error(), "errorCode": "brand_kit.not_found", "code": http.StatusNotFound}
|
||||||
|
case errors.Is(err, brandkit.ErrInvalidInput):
|
||||||
|
return http.StatusBadRequest, map[string]any{"error": err.Error(), "errorCode": "brand_kit.invalid_input", "code": http.StatusBadRequest}
|
||||||
|
case errors.Is(err, brandkit.ErrConflict):
|
||||||
|
return http.StatusConflict, map[string]any{"error": err.Error(), "errorCode": "brand_kit.conflict", "code": http.StatusConflict}
|
||||||
case errors.Is(err, authmodule.ErrCodeInvalid):
|
case errors.Is(err, authmodule.ErrCodeInvalid):
|
||||||
return http.StatusUnauthorized, map[string]any{"error": err.Error(), "errorCode": "auth.code_invalid", "code": http.StatusUnauthorized}
|
return http.StatusUnauthorized, map[string]any{"error": err.Error(), "errorCode": "auth.code_invalid", "code": http.StatusUnauthorized}
|
||||||
case errors.Is(err, authmodule.ErrCodeExpired):
|
case errors.Is(err, authmodule.ErrCodeExpired):
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"img_infinite_canvas/internal/logic"
|
||||||
|
"img_infinite_canvas/internal/svc"
|
||||||
|
"img_infinite_canvas/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func deleteBrandKitHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.BrandKitIdRequest
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logic.NewDeleteBrandKitLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.DeleteBrandKit(&req)
|
||||||
|
if err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
} else {
|
||||||
|
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"img_infinite_canvas/internal/logic"
|
||||||
|
"img_infinite_canvas/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listBrandKitsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
l := logic.NewListBrandKitsLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.ListBrandKits()
|
||||||
|
if err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
} else {
|
||||||
|
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,6 +94,21 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
Path: "/auth/options",
|
Path: "/auth/options",
|
||||||
Handler: getAuthOptionsHandler(serverCtx),
|
Handler: getAuthOptionsHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: "/brand-kits",
|
||||||
|
Handler: listBrandKitsHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPut,
|
||||||
|
Path: "/brand-kits/:id",
|
||||||
|
Handler: upsertBrandKitHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodDelete,
|
||||||
|
Path: "/brand-kits/:id",
|
||||||
|
Handler: deleteBrandKitHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Path: "/canda/chat-history",
|
Path: "/canda/chat-history",
|
||||||
@@ -199,6 +214,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
Path: "/projects/:id/agent/ws",
|
Path: "/projects/:id/agent/ws",
|
||||||
Handler: agentWebsocketHandler(serverCtx),
|
Handler: agentWebsocketHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPatch,
|
||||||
|
Path: "/projects/:id/brand-kit",
|
||||||
|
Handler: updateProjectBrandKitHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPatch,
|
Method: http.MethodPatch,
|
||||||
Path: "/projects/:id/canvas",
|
Path: "/projects/:id/canvas",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"img_infinite_canvas/internal/logic"
|
||||||
|
"img_infinite_canvas/internal/svc"
|
||||||
|
"img_infinite_canvas/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func updateProjectBrandKitHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.UpdateProjectBrandKitRequest
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logic.NewUpdateProjectBrandKitLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.UpdateProjectBrandKit(&req)
|
||||||
|
if err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
} else {
|
||||||
|
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"img_infinite_canvas/internal/logic"
|
||||||
|
"img_infinite_canvas/internal/svc"
|
||||||
|
"img_infinite_canvas/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func upsertBrandKitHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.UpsertBrandKitRequest
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logic.NewUpsertBrandKitLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.UpsertBrandKit(&req)
|
||||||
|
if err != nil {
|
||||||
|
httpx.ErrorCtx(r.Context(), w, err)
|
||||||
|
} else {
|
||||||
|
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"img_infinite_canvas/internal/domain/brandkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BrandKitStore struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
kits map[string]brandkit.Kit
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBrandKitStore() *BrandKitStore {
|
||||||
|
return &BrandKitStore{kits: make(map[string]brandkit.Kit)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) List(ctx context.Context, userID string) ([]brandkit.Kit, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
items := make([]brandkit.Kit, 0)
|
||||||
|
for _, kit := range s.kits {
|
||||||
|
if kit.UserID == userID {
|
||||||
|
items = append(items, brandkit.Clone(kit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.SliceStable(items, func(i, j int) bool {
|
||||||
|
if items[i].UpdatedAt.Equal(items[j].UpdatedAt) {
|
||||||
|
return items[i].ID < items[j].ID
|
||||||
|
}
|
||||||
|
return items[i].UpdatedAt.After(items[j].UpdatedAt)
|
||||||
|
})
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) Get(ctx context.Context, userID string, id string) (brandkit.Kit, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return brandkit.Kit{}, err
|
||||||
|
}
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
kit, ok := s.kits[id]
|
||||||
|
if !ok || kit.UserID != userID {
|
||||||
|
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||||
|
}
|
||||||
|
return brandkit.Clone(kit), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) GetDefault(ctx context.Context, userID string) (brandkit.Kit, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return brandkit.Kit{}, err
|
||||||
|
}
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
for _, kit := range s.kits {
|
||||||
|
if kit.UserID == userID && kit.IsDefault {
|
||||||
|
return brandkit.Clone(kit), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) Upsert(ctx context.Context, kit brandkit.Kit) (brandkit.Kit, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return brandkit.Kit{}, err
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if existing, ok := s.kits[kit.ID]; ok && existing.UserID != kit.UserID {
|
||||||
|
return brandkit.Kit{}, brandkit.ErrConflict
|
||||||
|
}
|
||||||
|
if kit.IsDefault {
|
||||||
|
for id, candidate := range s.kits {
|
||||||
|
if candidate.UserID == kit.UserID && candidate.IsDefault && id != kit.ID {
|
||||||
|
candidate.IsDefault = false
|
||||||
|
s.kits[id] = candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.kits[kit.ID] = brandkit.Clone(kit)
|
||||||
|
return brandkit.Clone(kit), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) Delete(ctx context.Context, userID string, id string) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
kit, ok := s.kits[id]
|
||||||
|
if !ok || kit.UserID != userID {
|
||||||
|
return brandkit.ErrNotFound
|
||||||
|
}
|
||||||
|
delete(s.kits, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ func (r *ProjectRepository) List(ctx context.Context) ([]design.ProjectSummary,
|
|||||||
Brief: project.Brief,
|
Brief: project.Brief,
|
||||||
Status: project.Status,
|
Status: project.Status,
|
||||||
Thumbnail: project.Thumbnail,
|
Thumbnail: project.Thumbnail,
|
||||||
|
BrandKitID: project.BrandKitID,
|
||||||
UpdatedAt: project.UpdatedAt,
|
UpdatedAt: project.UpdatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -207,6 +208,7 @@ func repositoryUserID(ctx context.Context, userID string) string {
|
|||||||
|
|
||||||
func cloneProject(project design.Project) design.Project {
|
func cloneProject(project design.Project) design.Project {
|
||||||
project.UserID = repositoryUserID(context.Background(), project.UserID)
|
project.UserID = repositoryUserID(context.Background(), project.UserID)
|
||||||
|
project.BrandImages = append([]string(nil), project.BrandImages...)
|
||||||
project.Nodes = append([]design.Node(nil), project.Nodes...)
|
project.Nodes = append([]design.Node(nil), project.Nodes...)
|
||||||
project.Connections = append([]design.Connection(nil), project.Connections...)
|
project.Connections = append([]design.Connection(nil), project.Connections...)
|
||||||
project.Messages = append([]design.Message(nil), project.Messages...)
|
project.Messages = append([]design.Message(nil), project.Messages...)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package piagent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"img_infinite_canvas/internal/domain/design"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBrandKitContextReachesPlannerAndDirectImagePrompts(t *testing.T) {
|
||||||
|
project := design.Project{
|
||||||
|
Title: "Campaign",
|
||||||
|
Brief: "Launch visual",
|
||||||
|
BrandKitID: "brand-kit-alpha",
|
||||||
|
BrandContext: "Primary color: Signal Green #28C76F. Typography: Avenir Next.",
|
||||||
|
}
|
||||||
|
planner := projectBriefContext(project)
|
||||||
|
direct := directImagePrompt(design.AgentRequest{Project: project, Prompt: "Create a product hero"})
|
||||||
|
fallback := fallbackImagePrompt(design.AgentRequest{
|
||||||
|
Project: project,
|
||||||
|
Prompt: "Create a product hero",
|
||||||
|
TaskPlan: design.AgentTaskPlan{ImageTasks: []design.AgentImageTask{
|
||||||
|
{Title: "Hero", Brief: "Centered product"},
|
||||||
|
}},
|
||||||
|
}, 0)
|
||||||
|
for name, value := range map[string]string{"planner": planner, "direct": direct, "fallback": fallback} {
|
||||||
|
if !strings.Contains(value, "Signal Green #28C76F") || !strings.Contains(value, "Avenir Next") {
|
||||||
|
t.Fatalf("%s prompt omitted Brand Kit context: %s", name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBrandKitImagesFollowUserReferences(t *testing.T) {
|
||||||
|
req := design.AgentRequest{
|
||||||
|
Project: design.Project{BrandImages: []string{
|
||||||
|
"data:image/png;base64,YnJhbmQtbG9nbw==",
|
||||||
|
"data:image/webp;base64,YnJhbmQtcmVmZXJlbmNl",
|
||||||
|
}},
|
||||||
|
Messages: []design.AgentChatMessage{{Contents: []design.AgentContent{{Type: "image", ImageURL: "https://example.com/user-reference.png"}}}},
|
||||||
|
}
|
||||||
|
images := referenceImageURLs(req)
|
||||||
|
if len(images) != 3 {
|
||||||
|
t.Fatalf("expected user and Brand Kit references, got %#v", images)
|
||||||
|
}
|
||||||
|
if images[0] != "https://example.com/user-reference.png" || images[1] != req.Project.BrandImages[0] || images[2] != req.Project.BrandImages[1] {
|
||||||
|
t.Fatalf("expected user references before Brand Kit images, got %#v", images)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -843,6 +843,11 @@ func projectBriefContext(project design.Project) string {
|
|||||||
builder.WriteString(project.Brief)
|
builder.WriteString(project.Brief)
|
||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(project.BrandContext) != "" {
|
||||||
|
builder.WriteString("Brand Kit context:\n")
|
||||||
|
builder.WriteString(strings.TrimSpace(project.BrandContext))
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
builder.WriteString(fmt.Sprintf("Canvas nodes: %d", len(project.Nodes)))
|
builder.WriteString(fmt.Sprintf("Canvas nodes: %d", len(project.Nodes)))
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ func directImagePlan(req design.AgentRequest) design.AgentPlan {
|
|||||||
|
|
||||||
func directImagePrompts(req design.AgentRequest) []design.AgentImagePrompt {
|
func directImagePrompts(req design.AgentRequest) []design.AgentImagePrompt {
|
||||||
count := plannedGenerationCount(req)
|
count := plannedGenerationCount(req)
|
||||||
prompt := strings.TrimSpace(req.Prompt)
|
prompt := directImagePrompt(req)
|
||||||
if prompt == "" {
|
if prompt == "" {
|
||||||
prompt = fallbackDirectImagePrompt(req)
|
prompt = fallbackDirectImagePrompt(req)
|
||||||
}
|
}
|
||||||
@@ -257,7 +257,7 @@ func directImageTasksSharePrompt(req design.AgentRequest) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func directImageBatchPrompt(req design.AgentRequest) design.AgentImagePrompt {
|
func directImageBatchPrompt(req design.AgentRequest) design.AgentImagePrompt {
|
||||||
prompt := strings.TrimSpace(req.Prompt)
|
prompt := directImagePrompt(req)
|
||||||
if prompt == "" {
|
if prompt == "" {
|
||||||
prompt = fallbackDirectImagePrompt(req)
|
prompt = fallbackDirectImagePrompt(req)
|
||||||
}
|
}
|
||||||
@@ -614,6 +614,12 @@ func referenceImageURLs(req design.AgentRequest) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, image := range req.Project.BrandImages {
|
||||||
|
add(image)
|
||||||
|
if len(urls) >= 16 {
|
||||||
|
return urls[:16]
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(urls) > 16 {
|
if len(urls) > 16 {
|
||||||
return urls[:16]
|
return urls[:16]
|
||||||
}
|
}
|
||||||
@@ -662,9 +668,9 @@ func fallbackImagePrompt(req design.AgentRequest, index int) string {
|
|||||||
task := req.TaskPlan.ImageTasks[index]
|
task := req.TaskPlan.ImageTasks[index]
|
||||||
if brief := strings.TrimSpace(task.Brief); brief != "" {
|
if brief := strings.TrimSpace(task.Brief); brief != "" {
|
||||||
if title := strings.TrimSpace(task.Title); title != "" && !strings.Contains(brief, title) {
|
if title := strings.TrimSpace(task.Title); title != "" && !strings.Contains(brief, title) {
|
||||||
return title + ": " + brief
|
return imagePromptWithBrandKit(title+": "+brief, req)
|
||||||
}
|
}
|
||||||
return brief
|
return imagePromptWithBrandKit(brief, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fallbackDirectImagePrompt(req)
|
return fallbackDirectImagePrompt(req)
|
||||||
@@ -1064,6 +1070,9 @@ func fallbackDirectImagePrompt(req design.AgentRequest) string {
|
|||||||
if brief := strings.TrimSpace(req.Project.Brief); brief != "" {
|
if brief := strings.TrimSpace(req.Project.Brief); brief != "" {
|
||||||
lines = append(lines, "Project brief: "+truncateRunes(brief, 180))
|
lines = append(lines, "Project brief: "+truncateRunes(brief, 180))
|
||||||
}
|
}
|
||||||
|
if brandContext := strings.TrimSpace(req.Project.BrandContext); brandContext != "" {
|
||||||
|
lines = append(lines, "Binding Brand Kit: "+truncateRunes(brandContext, 520))
|
||||||
|
}
|
||||||
if memory := compactFallbackMemory(req.Memory); memory != "" {
|
if memory := compactFallbackMemory(req.Memory); memory != "" {
|
||||||
lines = append(lines, "Relevant memory: "+memory)
|
lines = append(lines, "Relevant memory: "+memory)
|
||||||
}
|
}
|
||||||
@@ -1080,6 +1089,19 @@ func fallbackDirectImagePrompt(req design.AgentRequest) string {
|
|||||||
return compactImageGenerationPrompt(strings.Join(lines, "\n"), imageGenerationFallbackMaxRunes)
|
return compactImageGenerationPrompt(strings.Join(lines, "\n"), imageGenerationFallbackMaxRunes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func directImagePrompt(req design.AgentRequest) string {
|
||||||
|
return imagePromptWithBrandKit(req.Prompt, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePromptWithBrandKit(prompt string, req design.AgentRequest) string {
|
||||||
|
prompt = strings.TrimSpace(prompt)
|
||||||
|
brandContext := strings.TrimSpace(req.Project.BrandContext)
|
||||||
|
if brandContext == "" {
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(prompt + "\n\nBinding Brand Kit requirements:\n" + brandContext)
|
||||||
|
}
|
||||||
|
|
||||||
func compactFallbackMemory(memory design.AgentMemory) string {
|
func compactFallbackMemory(memory design.AgentMemory) string {
|
||||||
if memory.IsZero() {
|
if memory.IsZero() {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"img_infinite_canvas/internal/domain/brandkit"
|
||||||
|
sqlcdb "img_infinite_canvas/internal/infrastructure/postgres/db"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BrandKitStore struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBrandKitStore(ctx context.Context, dataSource string) (*BrandKitStore, error) {
|
||||||
|
pool, err := pgxpool.New(ctx, dataSource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := pool.Exec(ctx, schemaSQL); err != nil {
|
||||||
|
pool.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &BrandKitStore{pool: pool}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) List(ctx context.Context, userID string) ([]brandkit.Kit, error) {
|
||||||
|
rows, err := sqlcdb.New(s.pool).ListBrandKits(ctx, toPGUUID(userID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]brandkit.Kit, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
items = append(items, mapBrandKit(row))
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) Get(ctx context.Context, userID string, id string) (brandkit.Kit, error) {
|
||||||
|
row, err := sqlcdb.New(s.pool).GetBrandKit(ctx, sqlcdb.GetBrandKitParams{ID: id, UserID: toPGUUID(userID)})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||||
|
}
|
||||||
|
return brandkit.Kit{}, err
|
||||||
|
}
|
||||||
|
return mapBrandKit(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) GetDefault(ctx context.Context, userID string) (brandkit.Kit, error) {
|
||||||
|
row, err := sqlcdb.New(s.pool).GetDefaultBrandKit(ctx, toPGUUID(userID))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||||
|
}
|
||||||
|
return brandkit.Kit{}, err
|
||||||
|
}
|
||||||
|
return mapBrandKit(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) Upsert(ctx context.Context, kit brandkit.Kit) (brandkit.Kit, error) {
|
||||||
|
tx, err := s.pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return brandkit.Kit{}, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
q := sqlcdb.New(s.pool).WithTx(tx)
|
||||||
|
if kit.IsDefault {
|
||||||
|
if err := q.ClearDefaultBrandKits(ctx, sqlcdb.ClearDefaultBrandKitsParams{UserID: toPGUUID(kit.UserID), ID: kit.ID}); err != nil {
|
||||||
|
return brandkit.Kit{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row, err := q.UpsertBrandKit(ctx, sqlcdb.UpsertBrandKitParams{
|
||||||
|
ID: kit.ID,
|
||||||
|
UserID: toPGUUID(kit.UserID),
|
||||||
|
Name: kit.Name,
|
||||||
|
Document: append([]byte(nil), kit.Document...),
|
||||||
|
IsDefault: kit.IsDefault,
|
||||||
|
CreatedAt: pgtype.Timestamptz{Time: kit.CreatedAt, Valid: true},
|
||||||
|
UpdatedAt: pgtype.Timestamptz{Time: kit.UpdatedAt, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return brandkit.Kit{}, brandkit.ErrNotFound
|
||||||
|
}
|
||||||
|
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||||
|
return brandkit.Kit{}, brandkit.ErrConflict
|
||||||
|
}
|
||||||
|
return brandkit.Kit{}, err
|
||||||
|
}
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
return brandkit.Kit{}, err
|
||||||
|
}
|
||||||
|
return mapBrandKit(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) Delete(ctx context.Context, userID string, id string) error {
|
||||||
|
_, err := sqlcdb.New(s.pool).DeleteBrandKit(ctx, sqlcdb.DeleteBrandKitParams{ID: id, UserID: toPGUUID(userID)})
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return brandkit.ErrNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BrandKitStore) Close() error {
|
||||||
|
if s != nil && s.pool != nil {
|
||||||
|
s.pool.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapBrandKit(row sqlcdb.BrandKit) brandkit.Kit {
|
||||||
|
return brandkit.Kit{
|
||||||
|
ID: row.ID,
|
||||||
|
UserID: fromPGUUID(row.UserID),
|
||||||
|
Name: row.Name,
|
||||||
|
Document: append([]byte(nil), row.Document...),
|
||||||
|
IsDefault: row.IsDefault,
|
||||||
|
CreatedAt: row.CreatedAt.Time,
|
||||||
|
UpdatedAt: row.UpdatedAt.Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,29 @@ type AgentMessage struct {
|
|||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthVerificationCode struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
Purpose string `json:"purpose"`
|
||||||
|
CodeHash string `json:"code_hash"`
|
||||||
|
Attempts int32 `json:"attempts"`
|
||||||
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||||
|
ConsumedAt pgtype.Timestamptz `json:"consumed_at"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrandKit struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Document []byte `json:"document"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type CanvasConnection struct {
|
type CanvasConnection struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
ProjectID string `json:"project_id"`
|
ProjectID string `json:"project_id"`
|
||||||
@@ -81,6 +104,14 @@ type CanvasSnapshot struct {
|
|||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MockupBlob struct {
|
||||||
|
DataID string `json:"data_id"`
|
||||||
|
Width int32 `json:"width"`
|
||||||
|
Height int32 `json:"height"`
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type OutboxEvent struct {
|
type OutboxEvent struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
@@ -105,12 +136,45 @@ type Project struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
SnapshotID string `json:"snapshot_id"`
|
SnapshotID string `json:"snapshot_id"`
|
||||||
LastThreadID string `json:"last_thread_id"`
|
LastThreadID string `json:"last_thread_id"`
|
||||||
|
BrandKitID pgtype.Text `json:"brand_kit_id"`
|
||||||
ViewportX float64 `json:"viewport_x"`
|
ViewportX float64 `json:"viewport_x"`
|
||||||
ViewportY float64 `json:"viewport_y"`
|
ViewportY float64 `json:"viewport_y"`
|
||||||
ViewportK float64 `json:"viewport_k"`
|
ViewportK float64 `json:"viewport_k"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectShareMember struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
IdentifierType string `json:"identifier_type"`
|
||||||
|
IdentifierHash []byte `json:"identifier_hash"`
|
||||||
|
IdentifierCiphertext []byte `json:"identifier_ciphertext"`
|
||||||
|
Permission string `json:"permission"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectSharePolicy struct {
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
OwnerID pgtype.UUID `json:"owner_id"`
|
||||||
|
TokenHash []byte `json:"token_hash"`
|
||||||
|
TokenCiphertext []byte `json:"token_ciphertext"`
|
||||||
|
LinkPermission string `json:"link_permission"`
|
||||||
|
PolicyVersion int64 `json:"policy_version"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectShareVisit struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
VisitorHash []byte `json:"visitor_hash"`
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
VisitCount int64 `json:"visit_count"`
|
||||||
|
FirstSeenAt pgtype.Timestamptz `json:"first_seen_at"`
|
||||||
|
LastSeenAt pgtype.Timestamptz `json:"last_seen_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@@ -118,4 +182,15 @@ type User struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
PhoneCountryCode string `json:"phone_country_code"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
AvatarUrl string `json:"avatar_url"`
|
||||||
|
PasswordHash string `json:"password_hash"`
|
||||||
|
WechatOpenID string `json:"wechat_open_id"`
|
||||||
|
WechatUnionID string `json:"wechat_union_id"`
|
||||||
|
GoogleSubject string `json:"google_subject"`
|
||||||
|
PhoneVerifiedAt pgtype.Timestamptz `json:"phone_verified_at"`
|
||||||
|
EmailVerifiedAt pgtype.Timestamptz `json:"email_verified_at"`
|
||||||
|
LastLoginAt pgtype.Timestamptz `json:"last_login_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Querier interface {
|
type Querier interface {
|
||||||
|
ClearDefaultBrandKits(ctx context.Context, arg ClearDefaultBrandKitsParams) error
|
||||||
CreateCanvasSnapshot(ctx context.Context, arg CreateCanvasSnapshotParams) error
|
CreateCanvasSnapshot(ctx context.Context, arg CreateCanvasSnapshotParams) error
|
||||||
CreateConnection(ctx context.Context, arg CreateConnectionParams) error
|
CreateConnection(ctx context.Context, arg CreateConnectionParams) error
|
||||||
CreateMessage(ctx context.Context, arg CreateMessageParams) error
|
CreateMessage(ctx context.Context, arg CreateMessageParams) error
|
||||||
|
DeleteBrandKit(ctx context.Context, arg DeleteBrandKitParams) (string, error)
|
||||||
DeleteConnectionsByProject(ctx context.Context, arg DeleteConnectionsByProjectParams) error
|
DeleteConnectionsByProject(ctx context.Context, arg DeleteConnectionsByProjectParams) error
|
||||||
DeleteMessagesByProject(ctx context.Context, arg DeleteMessagesByProjectParams) error
|
DeleteMessagesByProject(ctx context.Context, arg DeleteMessagesByProjectParams) error
|
||||||
DeleteNodesByProject(ctx context.Context, arg DeleteNodesByProjectParams) error
|
DeleteNodesByProject(ctx context.Context, arg DeleteNodesByProjectParams) error
|
||||||
DeleteProject(ctx context.Context, arg DeleteProjectParams) error
|
DeleteProject(ctx context.Context, arg DeleteProjectParams) error
|
||||||
|
GetBrandKit(ctx context.Context, arg GetBrandKitParams) (BrandKit, error)
|
||||||
GetCanvasSnapshot(ctx context.Context, arg GetCanvasSnapshotParams) (CanvasSnapshot, error)
|
GetCanvasSnapshot(ctx context.Context, arg GetCanvasSnapshotParams) (CanvasSnapshot, error)
|
||||||
|
GetDefaultBrandKit(ctx context.Context, userID pgtype.UUID) (BrandKit, error)
|
||||||
GetProject(ctx context.Context, arg GetProjectParams) (Project, error)
|
GetProject(ctx context.Context, arg GetProjectParams) (Project, error)
|
||||||
|
ListBrandKits(ctx context.Context, userID pgtype.UUID) ([]BrandKit, error)
|
||||||
ListConnectionsByProject(ctx context.Context, arg ListConnectionsByProjectParams) ([]CanvasConnection, error)
|
ListConnectionsByProject(ctx context.Context, arg ListConnectionsByProjectParams) ([]CanvasConnection, error)
|
||||||
ListMessagesByProject(ctx context.Context, arg ListMessagesByProjectParams) ([]AgentMessage, error)
|
ListMessagesByProject(ctx context.Context, arg ListMessagesByProjectParams) ([]AgentMessage, error)
|
||||||
ListNodesByProject(ctx context.Context, arg ListNodesByProjectParams) ([]CanvasNode, error)
|
ListNodesByProject(ctx context.Context, arg ListNodesByProjectParams) ([]CanvasNode, error)
|
||||||
@@ -27,6 +32,7 @@ type Querier interface {
|
|||||||
ListProjectsPage(ctx context.Context, arg ListProjectsPageParams) ([]ListProjectsPageRow, error)
|
ListProjectsPage(ctx context.Context, arg ListProjectsPageParams) ([]ListProjectsPageRow, error)
|
||||||
LockProject(ctx context.Context, arg LockProjectParams) (LockProjectRow, error)
|
LockProject(ctx context.Context, arg LockProjectParams) (LockProjectRow, error)
|
||||||
UpdateProjectCanvasSnapshot(ctx context.Context, arg UpdateProjectCanvasSnapshotParams) error
|
UpdateProjectCanvasSnapshot(ctx context.Context, arg UpdateProjectCanvasSnapshotParams) error
|
||||||
|
UpsertBrandKit(ctx context.Context, arg UpsertBrandKitParams) (BrandKit, error)
|
||||||
UpsertCanvasNode(ctx context.Context, arg UpsertCanvasNodeParams) error
|
UpsertCanvasNode(ctx context.Context, arg UpsertCanvasNodeParams) error
|
||||||
UpsertProject(ctx context.Context, arg UpsertProjectParams) error
|
UpsertProject(ctx context.Context, arg UpsertProjectParams) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,22 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const clearDefaultBrandKits = `-- name: ClearDefaultBrandKits :exec
|
||||||
|
UPDATE brand_kits
|
||||||
|
SET is_default = FALSE
|
||||||
|
WHERE user_id = $1 AND is_default = TRUE AND id <> $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type ClearDefaultBrandKitsParams struct {
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ClearDefaultBrandKits(ctx context.Context, arg ClearDefaultBrandKitsParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, clearDefaultBrandKits, arg.UserID, arg.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const createCanvasSnapshot = `-- name: CreateCanvasSnapshot :exec
|
const createCanvasSnapshot = `-- name: CreateCanvasSnapshot :exec
|
||||||
INSERT INTO canvas_snapshots (id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at)
|
INSERT INTO canvas_snapshots (id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
@@ -140,6 +156,24 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteBrandKit = `-- name: DeleteBrandKit :one
|
||||||
|
DELETE FROM brand_kits
|
||||||
|
WHERE id = $1 AND user_id = $2
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteBrandKitParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) DeleteBrandKit(ctx context.Context, arg DeleteBrandKitParams) (string, error) {
|
||||||
|
row := q.db.QueryRow(ctx, deleteBrandKit, arg.ID, arg.UserID)
|
||||||
|
var id string
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteConnectionsByProject = `-- name: DeleteConnectionsByProject :exec
|
const deleteConnectionsByProject = `-- name: DeleteConnectionsByProject :exec
|
||||||
DELETE FROM canvas_connections
|
DELETE FROM canvas_connections
|
||||||
WHERE project_id = $1 AND user_id = $2
|
WHERE project_id = $1 AND user_id = $2
|
||||||
@@ -200,6 +234,32 @@ func (q *Queries) DeleteProject(ctx context.Context, arg DeleteProjectParams) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getBrandKit = `-- name: GetBrandKit :one
|
||||||
|
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||||
|
FROM brand_kits
|
||||||
|
WHERE id = $1 AND user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetBrandKitParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetBrandKit(ctx context.Context, arg GetBrandKitParams) (BrandKit, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getBrandKit, arg.ID, arg.UserID)
|
||||||
|
var i BrandKit
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Document,
|
||||||
|
&i.IsDefault,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getCanvasSnapshot = `-- name: GetCanvasSnapshot :one
|
const getCanvasSnapshot = `-- name: GetCanvasSnapshot :one
|
||||||
SELECT id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at
|
SELECT id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at
|
||||||
FROM canvas_snapshots
|
FROM canvas_snapshots
|
||||||
@@ -229,8 +289,30 @@ func (q *Queries) GetCanvasSnapshot(ctx context.Context, arg GetCanvasSnapshotPa
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDefaultBrandKit = `-- name: GetDefaultBrandKit :one
|
||||||
|
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||||
|
FROM brand_kits
|
||||||
|
WHERE user_id = $1 AND is_default = TRUE
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetDefaultBrandKit(ctx context.Context, userID pgtype.UUID) (BrandKit, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getDefaultBrandKit, userID)
|
||||||
|
var i BrandKit
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Document,
|
||||||
|
&i.IsDefault,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getProject = `-- name: GetProject :one
|
const getProject = `-- name: GetProject :one
|
||||||
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at
|
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at
|
||||||
FROM projects
|
FROM projects
|
||||||
WHERE id = $1 AND user_id = $2
|
WHERE id = $1 AND user_id = $2
|
||||||
`
|
`
|
||||||
@@ -254,6 +336,7 @@ func (q *Queries) GetProject(ctx context.Context, arg GetProjectParams) (Project
|
|||||||
&i.Version,
|
&i.Version,
|
||||||
&i.SnapshotID,
|
&i.SnapshotID,
|
||||||
&i.LastThreadID,
|
&i.LastThreadID,
|
||||||
|
&i.BrandKitID,
|
||||||
&i.ViewportX,
|
&i.ViewportX,
|
||||||
&i.ViewportY,
|
&i.ViewportY,
|
||||||
&i.ViewportK,
|
&i.ViewportK,
|
||||||
@@ -262,6 +345,41 @@ func (q *Queries) GetProject(ctx context.Context, arg GetProjectParams) (Project
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listBrandKits = `-- name: ListBrandKits :many
|
||||||
|
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||||
|
FROM brand_kits
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY updated_at DESC, id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListBrandKits(ctx context.Context, userID pgtype.UUID) ([]BrandKit, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listBrandKits, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []BrandKit
|
||||||
|
for rows.Next() {
|
||||||
|
var i BrandKit
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Document,
|
||||||
|
&i.IsDefault,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const listConnectionsByProject = `-- name: ListConnectionsByProject :many
|
const listConnectionsByProject = `-- name: ListConnectionsByProject :many
|
||||||
SELECT id, project_id, user_id, from_node_id, to_node_id
|
SELECT id, project_id, user_id, from_node_id, to_node_id
|
||||||
FROM canvas_connections
|
FROM canvas_connections
|
||||||
@@ -414,7 +532,7 @@ func (q *Queries) ListNodesByProject(ctx context.Context, arg ListNodesByProject
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listProjects = `-- name: ListProjects :many
|
const listProjects = `-- name: ListProjects :many
|
||||||
SELECT id, user_id, title, brief, status, thumbnail, updated_at
|
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
|
||||||
FROM projects
|
FROM projects
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY updated_at DESC
|
ORDER BY updated_at DESC
|
||||||
@@ -427,6 +545,7 @@ type ListProjectsRow struct {
|
|||||||
Brief string `json:"brief"`
|
Brief string `json:"brief"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Thumbnail string `json:"thumbnail"`
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
BrandKitID pgtype.Text `json:"brand_kit_id"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +565,7 @@ func (q *Queries) ListProjects(ctx context.Context, userID pgtype.UUID) ([]ListP
|
|||||||
&i.Brief,
|
&i.Brief,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
|
&i.BrandKitID,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -459,7 +579,7 @@ func (q *Queries) ListProjects(ctx context.Context, userID pgtype.UUID) ([]ListP
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listProjectsPage = `-- name: ListProjectsPage :many
|
const listProjectsPage = `-- name: ListProjectsPage :many
|
||||||
SELECT id, user_id, title, brief, status, thumbnail, updated_at
|
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
|
||||||
FROM projects
|
FROM projects
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY updated_at DESC
|
ORDER BY updated_at DESC
|
||||||
@@ -479,6 +599,7 @@ type ListProjectsPageRow struct {
|
|||||||
Brief string `json:"brief"`
|
Brief string `json:"brief"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Thumbnail string `json:"thumbnail"`
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
BrandKitID pgtype.Text `json:"brand_kit_id"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +619,7 @@ func (q *Queries) ListProjectsPage(ctx context.Context, arg ListProjectsPagePara
|
|||||||
&i.Brief,
|
&i.Brief,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
|
&i.BrandKitID,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -577,6 +699,51 @@ func (q *Queries) UpdateProjectCanvasSnapshot(ctx context.Context, arg UpdatePro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const upsertBrandKit = `-- name: UpsertBrandKit :one
|
||||||
|
INSERT INTO brand_kits (id, user_id, name, document, is_default, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
document = EXCLUDED.document,
|
||||||
|
is_default = EXCLUDED.is_default,
|
||||||
|
updated_at = EXCLUDED.updated_at
|
||||||
|
WHERE brand_kits.user_id = EXCLUDED.user_id
|
||||||
|
RETURNING id, user_id, name, document, is_default, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertBrandKitParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Document []byte `json:"document"`
|
||||||
|
IsDefault bool `json:"is_default"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertBrandKit(ctx context.Context, arg UpsertBrandKitParams) (BrandKit, error) {
|
||||||
|
row := q.db.QueryRow(ctx, upsertBrandKit,
|
||||||
|
arg.ID,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Name,
|
||||||
|
arg.Document,
|
||||||
|
arg.IsDefault,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
)
|
||||||
|
var i BrandKit
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Document,
|
||||||
|
&i.IsDefault,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const upsertCanvasNode = `-- name: UpsertCanvasNode :exec
|
const upsertCanvasNode = `-- name: UpsertCanvasNode :exec
|
||||||
INSERT INTO canvas_nodes (id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation)
|
INSERT INTO canvas_nodes (id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32)
|
||||||
@@ -688,8 +855,8 @@ func (q *Queries) UpsertCanvasNode(ctx context.Context, arg UpsertCanvasNodePara
|
|||||||
}
|
}
|
||||||
|
|
||||||
const upsertProject = `-- name: UpsertProject :exec
|
const upsertProject = `-- name: UpsertProject :exec
|
||||||
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at)
|
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
user_id = EXCLUDED.user_id,
|
user_id = EXCLUDED.user_id,
|
||||||
title = EXCLUDED.title,
|
title = EXCLUDED.title,
|
||||||
@@ -700,6 +867,7 @@ ON CONFLICT (id) DO UPDATE SET
|
|||||||
version = EXCLUDED.version,
|
version = EXCLUDED.version,
|
||||||
snapshot_id = EXCLUDED.snapshot_id,
|
snapshot_id = EXCLUDED.snapshot_id,
|
||||||
last_thread_id = EXCLUDED.last_thread_id,
|
last_thread_id = EXCLUDED.last_thread_id,
|
||||||
|
brand_kit_id = EXCLUDED.brand_kit_id,
|
||||||
viewport_x = EXCLUDED.viewport_x,
|
viewport_x = EXCLUDED.viewport_x,
|
||||||
viewport_y = EXCLUDED.viewport_y,
|
viewport_y = EXCLUDED.viewport_y,
|
||||||
viewport_k = EXCLUDED.viewport_k,
|
viewport_k = EXCLUDED.viewport_k,
|
||||||
@@ -717,6 +885,7 @@ type UpsertProjectParams struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
SnapshotID string `json:"snapshot_id"`
|
SnapshotID string `json:"snapshot_id"`
|
||||||
LastThreadID string `json:"last_thread_id"`
|
LastThreadID string `json:"last_thread_id"`
|
||||||
|
BrandKitID pgtype.Text `json:"brand_kit_id"`
|
||||||
ViewportX float64 `json:"viewport_x"`
|
ViewportX float64 `json:"viewport_x"`
|
||||||
ViewportY float64 `json:"viewport_y"`
|
ViewportY float64 `json:"viewport_y"`
|
||||||
ViewportK float64 `json:"viewport_k"`
|
ViewportK float64 `json:"viewport_k"`
|
||||||
@@ -735,6 +904,7 @@ func (q *Queries) UpsertProject(ctx context.Context, arg UpsertProjectParams) er
|
|||||||
arg.Version,
|
arg.Version,
|
||||||
arg.SnapshotID,
|
arg.SnapshotID,
|
||||||
arg.LastThreadID,
|
arg.LastThreadID,
|
||||||
|
arg.BrandKitID,
|
||||||
arg.ViewportX,
|
arg.ViewportX,
|
||||||
arg.ViewportY,
|
arg.ViewportY,
|
||||||
arg.ViewportK,
|
arg.ViewportK,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ func (r *ProjectRepository) List(ctx context.Context) ([]design.ProjectSummary,
|
|||||||
Brief: row.Brief,
|
Brief: row.Brief,
|
||||||
Status: design.ProjectStatus(row.Status),
|
Status: design.ProjectStatus(row.Status),
|
||||||
Thumbnail: row.Thumbnail,
|
Thumbnail: row.Thumbnail,
|
||||||
|
BrandKitID: fromPGText(row.BrandKitID),
|
||||||
UpdatedAt: row.UpdatedAt.Time,
|
UpdatedAt: row.UpdatedAt.Time,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -87,6 +88,7 @@ func (r *ProjectRepository) ListPage(ctx context.Context, limit int64, offset in
|
|||||||
Brief: row.Brief,
|
Brief: row.Brief,
|
||||||
Status: design.ProjectStatus(row.Status),
|
Status: design.ProjectStatus(row.Status),
|
||||||
Thumbnail: row.Thumbnail,
|
Thumbnail: row.Thumbnail,
|
||||||
|
BrandKitID: fromPGText(row.BrandKitID),
|
||||||
UpdatedAt: row.UpdatedAt.Time,
|
UpdatedAt: row.UpdatedAt.Time,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -129,6 +131,7 @@ func (r *ProjectRepository) Get(ctx context.Context, id string) (design.Project,
|
|||||||
SnapshotID: row.SnapshotID,
|
SnapshotID: row.SnapshotID,
|
||||||
Canvas: row.Canvas,
|
Canvas: row.Canvas,
|
||||||
LastThreadID: row.LastThreadID,
|
LastThreadID: row.LastThreadID,
|
||||||
|
BrandKitID: fromPGText(row.BrandKitID),
|
||||||
UpdatedAt: row.UpdatedAt.Time,
|
UpdatedAt: row.UpdatedAt.Time,
|
||||||
Viewport: design.Viewport{
|
Viewport: design.Viewport{
|
||||||
X: row.ViewportX,
|
X: row.ViewportX,
|
||||||
@@ -165,6 +168,7 @@ func (r *ProjectRepository) Save(ctx context.Context, project design.Project) er
|
|||||||
Version: project.Version,
|
Version: project.Version,
|
||||||
SnapshotID: project.SnapshotID,
|
SnapshotID: project.SnapshotID,
|
||||||
LastThreadID: project.LastThreadID,
|
LastThreadID: project.LastThreadID,
|
||||||
|
BrandKitID: toPGText(project.BrandKitID),
|
||||||
ViewportX: project.Viewport.X,
|
ViewportX: project.Viewport.X,
|
||||||
ViewportY: project.Viewport.Y,
|
ViewportY: project.Viewport.Y,
|
||||||
ViewportK: project.Viewport.K,
|
ViewportK: project.Viewport.K,
|
||||||
@@ -463,6 +467,18 @@ func fromPGUUID(value pgtype.UUID) string {
|
|||||||
return uuid.UUID(value.Bytes).String()
|
return uuid.UUID(value.Bytes).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toPGText(value string) pgtype.Text {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
return pgtype.Text{String: value, Valid: value != ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromPGText(value pgtype.Text) string {
|
||||||
|
if !value.Valid {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return value.String
|
||||||
|
}
|
||||||
|
|
||||||
func insertProjectOutboxEvent(ctx context.Context, tx pgx.Tx, project design.Project, eventType string) error {
|
func insertProjectOutboxEvent(ctx context.Context, tx pgx.Tx, project design.Project, eventType string) error {
|
||||||
eventID := uuid.NewString()
|
eventID := uuid.NewString()
|
||||||
createdAt := project.UpdatedAt
|
createdAt := project.UpdatedAt
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
-- name: ListProjects :many
|
-- name: ListProjects :many
|
||||||
SELECT id, user_id, title, brief, status, thumbnail, updated_at
|
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
|
||||||
FROM projects
|
FROM projects
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY updated_at DESC;
|
ORDER BY updated_at DESC;
|
||||||
|
|
||||||
-- name: ListProjectsPage :many
|
-- name: ListProjectsPage :many
|
||||||
SELECT id, user_id, title, brief, status, thumbnail, updated_at
|
SELECT id, user_id, title, brief, status, thumbnail, brand_kit_id, updated_at
|
||||||
FROM projects
|
FROM projects
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY updated_at DESC
|
ORDER BY updated_at DESC
|
||||||
LIMIT $2 OFFSET $3;
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
-- name: GetProject :one
|
-- name: GetProject :one
|
||||||
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at
|
SELECT id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at
|
||||||
FROM projects
|
FROM projects
|
||||||
WHERE id = $1 AND user_id = $2;
|
WHERE id = $1 AND user_id = $2;
|
||||||
|
|
||||||
@@ -27,8 +27,8 @@ DELETE FROM projects
|
|||||||
WHERE id = $1 AND user_id = $2;
|
WHERE id = $1 AND user_id = $2;
|
||||||
|
|
||||||
-- name: UpsertProject :exec
|
-- name: UpsertProject :exec
|
||||||
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, viewport_x, viewport_y, viewport_k, updated_at)
|
INSERT INTO projects (id, user_id, title, brief, status, thumbnail, canvas, version, snapshot_id, last_thread_id, brand_kit_id, viewport_x, viewport_y, viewport_k, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
user_id = EXCLUDED.user_id,
|
user_id = EXCLUDED.user_id,
|
||||||
title = EXCLUDED.title,
|
title = EXCLUDED.title,
|
||||||
@@ -39,11 +39,50 @@ ON CONFLICT (id) DO UPDATE SET
|
|||||||
version = EXCLUDED.version,
|
version = EXCLUDED.version,
|
||||||
snapshot_id = EXCLUDED.snapshot_id,
|
snapshot_id = EXCLUDED.snapshot_id,
|
||||||
last_thread_id = EXCLUDED.last_thread_id,
|
last_thread_id = EXCLUDED.last_thread_id,
|
||||||
|
brand_kit_id = EXCLUDED.brand_kit_id,
|
||||||
viewport_x = EXCLUDED.viewport_x,
|
viewport_x = EXCLUDED.viewport_x,
|
||||||
viewport_y = EXCLUDED.viewport_y,
|
viewport_y = EXCLUDED.viewport_y,
|
||||||
viewport_k = EXCLUDED.viewport_k,
|
viewport_k = EXCLUDED.viewport_k,
|
||||||
updated_at = EXCLUDED.updated_at;
|
updated_at = EXCLUDED.updated_at;
|
||||||
|
|
||||||
|
-- name: ListBrandKits :many
|
||||||
|
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||||
|
FROM brand_kits
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY updated_at DESC, id ASC;
|
||||||
|
|
||||||
|
-- name: GetBrandKit :one
|
||||||
|
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||||
|
FROM brand_kits
|
||||||
|
WHERE id = $1 AND user_id = $2;
|
||||||
|
|
||||||
|
-- name: GetDefaultBrandKit :one
|
||||||
|
SELECT id, user_id, name, document, is_default, created_at, updated_at
|
||||||
|
FROM brand_kits
|
||||||
|
WHERE user_id = $1 AND is_default = TRUE
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: ClearDefaultBrandKits :exec
|
||||||
|
UPDATE brand_kits
|
||||||
|
SET is_default = FALSE
|
||||||
|
WHERE user_id = $1 AND is_default = TRUE AND id <> $2;
|
||||||
|
|
||||||
|
-- name: UpsertBrandKit :one
|
||||||
|
INSERT INTO brand_kits (id, user_id, name, document, is_default, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
document = EXCLUDED.document,
|
||||||
|
is_default = EXCLUDED.is_default,
|
||||||
|
updated_at = EXCLUDED.updated_at
|
||||||
|
WHERE brand_kits.user_id = EXCLUDED.user_id
|
||||||
|
RETURNING id, user_id, name, document, is_default, created_at, updated_at;
|
||||||
|
|
||||||
|
-- name: DeleteBrandKit :one
|
||||||
|
DELETE FROM brand_kits
|
||||||
|
WHERE id = $1 AND user_id = $2
|
||||||
|
RETURNING id;
|
||||||
|
|
||||||
-- name: CreateCanvasSnapshot :exec
|
-- name: CreateCanvasSnapshot :exec
|
||||||
INSERT INTO canvas_snapshots (id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at)
|
INSERT INTO canvas_snapshots (id, project_id, user_id, version, canvas, viewport_json, nodes_json, connections_json, created_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ CREATE TABLE IF NOT EXISTS auth_verification_codes (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS auth_codes_target_idx ON auth_verification_codes(region, channel, target, purpose, created_at DESC);
|
CREATE INDEX IF NOT EXISTS auth_codes_target_idx ON auth_verification_codes(region, channel, target, purpose, created_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS brand_kits (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
document JSONB NOT NULL CHECK (jsonb_typeof(document) = 'object'),
|
||||||
|
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS brand_kits_user_updated_idx ON brand_kits(user_id, updated_at DESC);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS brand_kits_user_default_idx ON brand_kits(user_id) WHERE is_default;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS projects (
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
user_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001'::uuid,
|
user_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001'::uuid,
|
||||||
@@ -54,6 +67,7 @@ CREATE TABLE IF NOT EXISTS projects (
|
|||||||
version TEXT NOT NULL DEFAULT '',
|
version TEXT NOT NULL DEFAULT '',
|
||||||
snapshot_id TEXT NOT NULL DEFAULT '',
|
snapshot_id TEXT NOT NULL DEFAULT '',
|
||||||
last_thread_id TEXT NOT NULL DEFAULT '',
|
last_thread_id TEXT NOT NULL DEFAULT '',
|
||||||
|
brand_kit_id TEXT REFERENCES brand_kits(id) ON DELETE SET NULL,
|
||||||
viewport_x DOUBLE PRECISION NOT NULL DEFAULT 0,
|
viewport_x DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||||
viewport_y DOUBLE PRECISION NOT NULL DEFAULT 0,
|
viewport_y DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||||
viewport_k DOUBLE PRECISION NOT NULL DEFAULT 1,
|
viewport_k DOUBLE PRECISION NOT NULL DEFAULT 1,
|
||||||
@@ -70,7 +84,22 @@ ALTER TABLE projects ADD COLUMN IF NOT EXISTS canvas TEXT NOT NULL DEFAULT '';
|
|||||||
ALTER TABLE projects ADD COLUMN IF NOT EXISTS version TEXT NOT NULL DEFAULT '';
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS version TEXT NOT NULL DEFAULT '';
|
||||||
ALTER TABLE projects ADD COLUMN IF NOT EXISTS snapshot_id TEXT NOT NULL DEFAULT '';
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS snapshot_id TEXT NOT NULL DEFAULT '';
|
||||||
ALTER TABLE projects ADD COLUMN IF NOT EXISTS last_thread_id TEXT NOT NULL DEFAULT '';
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS last_thread_id TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE projects ADD COLUMN IF NOT EXISTS brand_kit_id TEXT;
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'projects_brand_kit_id_fkey'
|
||||||
|
AND conrelid = 'projects'::regclass
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE projects
|
||||||
|
ADD CONSTRAINT projects_brand_kit_id_fkey
|
||||||
|
FOREIGN KEY (brand_kit_id) REFERENCES brand_kits(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
CREATE INDEX IF NOT EXISTS projects_user_updated_idx ON projects(user_id, updated_at DESC);
|
CREATE INDEX IF NOT EXISTS projects_user_updated_idx ON projects(user_id, updated_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS projects_brand_kit_idx ON projects(user_id, brand_kit_id) WHERE brand_kit_id IS NOT NULL;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS project_share_policies (
|
CREATE TABLE IF NOT EXISTS project_share_policies (
|
||||||
project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
|
project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"img_infinite_canvas/internal/domain/brandkit"
|
||||||
|
"img_infinite_canvas/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toAPIBrandKit(kit brandkit.Kit) types.BrandKit {
|
||||||
|
return types.BrandKit{
|
||||||
|
Id: kit.ID,
|
||||||
|
Name: kit.Name,
|
||||||
|
Document: string(kit.Document),
|
||||||
|
IsDefault: kit.IsDefault,
|
||||||
|
CreatedAt: formatTime(kit.CreatedAt),
|
||||||
|
UpdatedAt: formatTime(kit.UpdatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAPIBrandKits(kits []brandkit.Kit) []types.BrandKit {
|
||||||
|
items := make([]types.BrandKit, 0, len(kits))
|
||||||
|
for _, kit := range kits {
|
||||||
|
items = append(items, toAPIBrandKit(kit))
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ func (l *CreateProjectAsyncLogic) CreateProjectAsync(req *types.CreateProjectReq
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
project, err := l.svcCtx.DesignService.CreateProjectAsync(l.ctx, req.Title, req.Prompt, req.ImageModel, req.ImageSize, req.ImageQuality, int(req.ImageCount), req.EnableWebSearch)
|
project, err := l.svcCtx.DesignService.CreateProjectAsyncWithBrandKit(l.ctx, req.Title, req.Prompt, req.ImageModel, req.ImageSize, req.ImageQuality, int(req.ImageCount), req.EnableWebSearch, req.BrandKitId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func (l *CreateProjectLogic) CreateProject(req *types.CreateProjectRequest) (res
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
project, err := l.svcCtx.DesignService.CreateProject(l.ctx, req.Title, req.Prompt, req.ImageModel, req.ImageSize, req.ImageQuality, int(req.ImageCount), req.EnableWebSearch)
|
project, err := l.svcCtx.DesignService.CreateProjectWithBrandKit(l.ctx, req.Title, req.Prompt, req.ImageModel, req.ImageSize, req.ImageQuality, int(req.ImageCount), req.EnableWebSearch, req.BrandKitId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"img_infinite_canvas/internal/svc"
|
||||||
|
"img_infinite_canvas/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeleteBrandKitLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeleteBrandKitLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteBrandKitLogic {
|
||||||
|
return &DeleteBrandKitLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DeleteBrandKitLogic) DeleteBrandKit(req *types.BrandKitIdRequest) (resp *types.DeleteBrandKitResponse, err error) {
|
||||||
|
if _, err := l.svcCtx.BrandKitService.Get(l.ctx, req.Id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := l.svcCtx.DesignService.ClearBrandKitBindings(l.ctx, req.Id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := l.svcCtx.BrandKitService.Delete(l.ctx, req.Id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &types.DeleteBrandKitResponse{Id: req.Id, Deleted: true}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"img_infinite_canvas/internal/svc"
|
||||||
|
"img_infinite_canvas/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListBrandKitsLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListBrandKitsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListBrandKitsLogic {
|
||||||
|
return &ListBrandKitsLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ListBrandKitsLogic) ListBrandKits() (resp *types.BrandKitListResponse, err error) {
|
||||||
|
kits, err := l.svcCtx.BrandKitService.List(l.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &types.BrandKitListResponse{BrandKits: toAPIBrandKits(kits)}, nil
|
||||||
|
}
|
||||||
@@ -195,6 +195,7 @@ func toAPIProject(project design.Project) types.Project {
|
|||||||
SnapshotId: project.SnapshotID,
|
SnapshotId: project.SnapshotID,
|
||||||
Canvas: project.Canvas,
|
Canvas: project.Canvas,
|
||||||
LastThreadId: project.LastThreadID,
|
LastThreadId: project.LastThreadID,
|
||||||
|
BrandKitId: project.BrandKitID,
|
||||||
Viewport: toAPIView(project.Viewport),
|
Viewport: toAPIView(project.Viewport),
|
||||||
Nodes: toAPINodes(project.Nodes),
|
Nodes: toAPINodes(project.Nodes),
|
||||||
Connections: toAPIConnections(project.Connections),
|
Connections: toAPIConnections(project.Connections),
|
||||||
@@ -212,6 +213,7 @@ func toAPISummaries(items []design.ProjectSummary) []types.ProjectSummary {
|
|||||||
Brief: item.Brief,
|
Brief: item.Brief,
|
||||||
Status: string(item.Status),
|
Status: string(item.Status),
|
||||||
Thumbnail: item.Thumbnail,
|
Thumbnail: item.Thumbnail,
|
||||||
|
BrandKitId: item.BrandKitID,
|
||||||
UpdatedAt: formatTime(item.UpdatedAt),
|
UpdatedAt: formatTime(item.UpdatedAt),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ func toQueryProjectResponse(project design.Project, permission string, visibilit
|
|||||||
Version: project.Version,
|
Version: project.Version,
|
||||||
SnapshotId: nil,
|
SnapshotId: nil,
|
||||||
ItemId: nil,
|
ItemId: nil,
|
||||||
|
BrandKitId: project.BrandKitID,
|
||||||
Permission: permission,
|
Permission: permission,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
sharingmodule "img_infinite_canvas/internal/modules/sharing"
|
||||||
|
"img_infinite_canvas/internal/svc"
|
||||||
|
"img_infinite_canvas/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateProjectBrandKitLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateProjectBrandKitLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateProjectBrandKitLogic {
|
||||||
|
return &UpdateProjectBrandKitLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *UpdateProjectBrandKitLogic) UpdateProjectBrandKit(req *types.UpdateProjectBrandKitRequest) (resp *types.ProjectResponse, err error) {
|
||||||
|
if access, ok := sharingmodule.AccessFromContext(l.ctx); ok && !access.IsOwner {
|
||||||
|
return nil, sharingmodule.ErrForbidden
|
||||||
|
}
|
||||||
|
project, err := l.svcCtx.DesignService.UpdateProjectBrandKit(l.ctx, req.Id, req.BrandKitId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return toProjectResponse(project), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Code scaffolded by goctl. Safe to edit.
|
||||||
|
// goctl 1.10.1
|
||||||
|
|
||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"img_infinite_canvas/internal/svc"
|
||||||
|
"img_infinite_canvas/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpsertBrandKitLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpsertBrandKitLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpsertBrandKitLogic {
|
||||||
|
return &UpsertBrandKitLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *UpsertBrandKitLogic) UpsertBrandKit(req *types.UpsertBrandKitRequest) (resp *types.BrandKitResponse, err error) {
|
||||||
|
kit, err := l.svcCtx.BrandKitService.Upsert(l.ctx, req.Id, req.Document, req.IsDefault)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &types.BrandKitResponse{BrandKit: toAPIBrandKit(kit)}, nil
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"img_infinite_canvas/internal/application"
|
"img_infinite_canvas/internal/application"
|
||||||
"img_infinite_canvas/internal/config"
|
"img_infinite_canvas/internal/config"
|
||||||
|
"img_infinite_canvas/internal/domain/brandkit"
|
||||||
"img_infinite_canvas/internal/domain/design"
|
"img_infinite_canvas/internal/domain/design"
|
||||||
"img_infinite_canvas/internal/infrastructure/backgroundremoval"
|
"img_infinite_canvas/internal/infrastructure/backgroundremoval"
|
||||||
cacheinfra "img_infinite_canvas/internal/infrastructure/cache"
|
cacheinfra "img_infinite_canvas/internal/infrastructure/cache"
|
||||||
@@ -33,6 +34,7 @@ import (
|
|||||||
type ServiceContext struct {
|
type ServiceContext struct {
|
||||||
Config config.Config
|
Config config.Config
|
||||||
DesignService *application.DesignService
|
DesignService *application.DesignService
|
||||||
|
BrandKitService *application.BrandKitService
|
||||||
AuthService *authmodule.Service
|
AuthService *authmodule.Service
|
||||||
ShareService *sharingmodule.Service
|
ShareService *sharingmodule.Service
|
||||||
RealtimeBus realtimemodule.Bus
|
RealtimeBus realtimemodule.Bus
|
||||||
@@ -49,6 +51,7 @@ type backgroundWorker interface {
|
|||||||
|
|
||||||
func NewServiceContext(c config.Config) *ServiceContext {
|
func NewServiceContext(c config.Config) *ServiceContext {
|
||||||
repo := newProjectRepository(c)
|
repo := newProjectRepository(c)
|
||||||
|
brandKitService := newBrandKitService(c)
|
||||||
cacheStore := newCacheStore(c)
|
cacheStore := newCacheStore(c)
|
||||||
authService := newAuthService(c, cacheStore)
|
authService := newAuthService(c, cacheStore)
|
||||||
shareService := newShareService(c)
|
shareService := newShareService(c)
|
||||||
@@ -61,6 +64,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
researcher := newResearcher(c)
|
researcher := newResearcher(c)
|
||||||
textExtractor := newTextExtractor(c)
|
textExtractor := newTextExtractor(c)
|
||||||
service := application.NewDesignService(repo, agentRunner, assets, researcher, textExtractor, creativeAgent)
|
service := application.NewDesignService(repo, agentRunner, assets, researcher, textExtractor, creativeAgent)
|
||||||
|
service.SetBrandKitService(brandKitService)
|
||||||
service.SetBackgroundRemover(newBackgroundRemover(c))
|
service.SetBackgroundRemover(newBackgroundRemover(c))
|
||||||
service.SetObjectRecognizer(newObjectRecognizer(c))
|
service.SetObjectRecognizer(newObjectRecognizer(c))
|
||||||
if mockupAnalyzer, ok := creativeAgent.(design.MockupModelAnalyzer); ok {
|
if mockupAnalyzer, ok := creativeAgent.(design.MockupModelAnalyzer); ok {
|
||||||
@@ -77,6 +81,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
|||||||
return &ServiceContext{
|
return &ServiceContext{
|
||||||
Config: c,
|
Config: c,
|
||||||
DesignService: service,
|
DesignService: service,
|
||||||
|
BrandKitService: brandKitService,
|
||||||
AuthService: authService,
|
AuthService: authService,
|
||||||
ShareService: shareService,
|
ShareService: shareService,
|
||||||
RealtimeBus: realtimeBus,
|
RealtimeBus: realtimeBus,
|
||||||
@@ -159,6 +164,32 @@ func (s *ServiceContext) Close() {
|
|||||||
logx.Errorf("close sharing service failed: %v", err)
|
logx.Errorf("close sharing service failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.BrandKitService != nil {
|
||||||
|
if err := s.BrandKitService.Close(); err != nil {
|
||||||
|
logx.Errorf("close brand kit service failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBrandKitService(c config.Config) *application.BrandKitService {
|
||||||
|
var store brandkit.Store
|
||||||
|
switch c.Storage.Driver {
|
||||||
|
case "", "memory":
|
||||||
|
store = memory.NewBrandKitStore()
|
||||||
|
case "postgres":
|
||||||
|
if strings.TrimSpace(c.Storage.DataSource) == "" {
|
||||||
|
logx.Must(application.ErrMissingDataSource)
|
||||||
|
}
|
||||||
|
postgresStore, err := postgres.NewBrandKitStore(context.Background(), c.Storage.DataSource)
|
||||||
|
if err != nil {
|
||||||
|
logx.Must(err)
|
||||||
|
}
|
||||||
|
store = postgresStore
|
||||||
|
default:
|
||||||
|
logx.Must(application.ErrUnsupportedStorage)
|
||||||
|
store = memory.NewBrandKitStore()
|
||||||
|
}
|
||||||
|
return application.NewBrandKitService(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newShareService(c config.Config) *sharingmodule.Service {
|
func newShareService(c config.Config) *sharingmodule.Service {
|
||||||
|
|||||||
@@ -179,6 +179,27 @@ type AuthUser struct {
|
|||||||
AvatarUrl string `json:"avatarUrl,optional"`
|
AvatarUrl string `json:"avatarUrl,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BrandKit struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Document string `json:"document"`
|
||||||
|
IsDefault bool `json:"isDefault"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrandKitIdRequest struct {
|
||||||
|
Id string `path:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrandKitListResponse struct {
|
||||||
|
BrandKits []BrandKit `json:"brandKits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrandKitResponse struct {
|
||||||
|
BrandKit BrandKit `json:"brandKit"`
|
||||||
|
}
|
||||||
|
|
||||||
type CandaChatHistoryItem struct {
|
type CandaChatHistoryItem struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Timestamp string `json:"timestamp"`
|
Timestamp string `json:"timestamp"`
|
||||||
@@ -321,6 +342,7 @@ type CreateAssetUploadResponse struct {
|
|||||||
type CreateProjectRequest struct {
|
type CreateProjectRequest struct {
|
||||||
Title string `json:"title,optional"`
|
Title string `json:"title,optional"`
|
||||||
Prompt string `json:"prompt,optional"`
|
Prompt string `json:"prompt,optional"`
|
||||||
|
BrandKitId *string `json:"brandKitId,optional"`
|
||||||
ImageModel string `json:"imageModel,optional"`
|
ImageModel string `json:"imageModel,optional"`
|
||||||
ImageSize string `json:"imageSize,optional"`
|
ImageSize string `json:"imageSize,optional"`
|
||||||
ImageQuality string `json:"imageQuality,optional"`
|
ImageQuality string `json:"imageQuality,optional"`
|
||||||
@@ -344,6 +366,11 @@ type DeleteAssetResponse struct {
|
|||||||
Deleted bool `json:"deleted"`
|
Deleted bool `json:"deleted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteBrandKitResponse struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Deleted bool `json:"deleted"`
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteProjectResponse struct {
|
type DeleteProjectResponse struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Deleted bool `json:"deleted"`
|
Deleted bool `json:"deleted"`
|
||||||
@@ -686,6 +713,7 @@ type Project struct {
|
|||||||
SnapshotId string `json:"snapshotId"`
|
SnapshotId string `json:"snapshotId"`
|
||||||
Canvas string `json:"canvas"`
|
Canvas string `json:"canvas"`
|
||||||
LastThreadId string `json:"lastThreadId"`
|
LastThreadId string `json:"lastThreadId"`
|
||||||
|
BrandKitId string `json:"brandKitId,optional"`
|
||||||
Viewport CanvasViewport `json:"viewport"`
|
Viewport CanvasViewport `json:"viewport"`
|
||||||
Nodes []CanvasNode `json:"nodes"`
|
Nodes []CanvasNode `json:"nodes"`
|
||||||
Connections []CanvasConnection `json:"connections"`
|
Connections []CanvasConnection `json:"connections"`
|
||||||
@@ -718,6 +746,7 @@ type ProjectSummary struct {
|
|||||||
Brief string `json:"brief"`
|
Brief string `json:"brief"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Thumbnail string `json:"thumbnail"`
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
BrandKitId string `json:"brandKitId,optional"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,6 +789,7 @@ type QueryProjectData struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
SnapshotId *string `json:"snapshotId"`
|
SnapshotId *string `json:"snapshotId"`
|
||||||
ItemId *string `json:"itemId"`
|
ItemId *string `json:"itemId"`
|
||||||
|
BrandKitId string `json:"brandKitId,optional"`
|
||||||
Permission string `json:"permission"`
|
Permission string `json:"permission"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,6 +960,11 @@ type UpdateAccountProfileRequest struct {
|
|||||||
AvatarUrl string `json:"avatarUrl,optional"`
|
AvatarUrl string `json:"avatarUrl,optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateProjectBrandKitRequest struct {
|
||||||
|
Id string `path:"id"`
|
||||||
|
BrandKitId string `json:"brandKitId,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateProjectTitleRequest struct {
|
type UpdateProjectTitleRequest struct {
|
||||||
Id string `path:"id"`
|
Id string `path:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@@ -946,6 +981,12 @@ type UpdateShareMemberRequest struct {
|
|||||||
Permission string `json:"permission"`
|
Permission string `json:"permission"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpsertBrandKitRequest struct {
|
||||||
|
Id string `path:"id"`
|
||||||
|
Document string `json:"document"`
|
||||||
|
IsDefault bool `json:"isDefault,optional"`
|
||||||
|
}
|
||||||
|
|
||||||
type WechatAuthURLResponse struct {
|
type WechatAuthURLResponse struct {
|
||||||
AuthUrl string `json:"authUrl"`
|
AuthUrl string `json:"authUrl"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
|
|||||||
Reference in New Issue
Block a user