Initial commit: img-infinite-canvas AI design workbench MVP
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
package design
|
||||
|
||||
import "context"
|
||||
|
||||
type AgentRequest struct {
|
||||
Project Project
|
||||
Prompt string
|
||||
Mode string
|
||||
ImageModel string
|
||||
ImageSize string
|
||||
Messages []AgentChatMessage
|
||||
Memory AgentMemory
|
||||
TaskPlan AgentTaskPlan
|
||||
}
|
||||
|
||||
type AgentContent struct {
|
||||
Type string
|
||||
Text string
|
||||
TextSource string
|
||||
ImageURL string
|
||||
ImageWidth int64
|
||||
ImageHeight int64
|
||||
}
|
||||
|
||||
type AgentChatMessage struct {
|
||||
Role string
|
||||
Contents []AgentContent
|
||||
}
|
||||
|
||||
type AgentToolConfig struct {
|
||||
PreferToolCategories map[string][]string
|
||||
}
|
||||
|
||||
type AgentChatRequest struct {
|
||||
Messages []AgentChatMessage
|
||||
ToolConfig AgentToolConfig
|
||||
ThreadID string
|
||||
ThreadIDType int64
|
||||
EnableMultimodalInput int64
|
||||
EnableWebSearch bool
|
||||
ImageModel string
|
||||
ImageSize string
|
||||
Mode string
|
||||
}
|
||||
|
||||
type AgentThread struct {
|
||||
ThreadID string
|
||||
Status string
|
||||
ThreadIDType int64
|
||||
Project Project
|
||||
}
|
||||
|
||||
type AgentThreadSummary struct {
|
||||
AgentThreadID string
|
||||
CoverImageURL string
|
||||
Text string
|
||||
ThreadIDType int64
|
||||
UpdateTimestamp int64
|
||||
}
|
||||
|
||||
type GeneratorTaskInputArgs struct {
|
||||
AspectRatio string
|
||||
Image []string
|
||||
ImageURL string
|
||||
Prompt string
|
||||
Resolution string
|
||||
SrcBox *NormalizedBox
|
||||
DstBox *NormalizedBox
|
||||
}
|
||||
|
||||
type GeneratorTaskSubmitRequest struct {
|
||||
CID string
|
||||
ProjectID string
|
||||
GeneratorName string
|
||||
InputArgs GeneratorTaskInputArgs
|
||||
}
|
||||
|
||||
type GeneratorTaskPriceDetail struct {
|
||||
BasePrice int64
|
||||
ExtInfoForUnitCount string
|
||||
GeneratorName string
|
||||
InputArgs GeneratorTaskInputArgs
|
||||
OutputArgs string
|
||||
SearchKey string
|
||||
TimeVariantPeriod string
|
||||
TotalPrice int64
|
||||
UnitCount int64
|
||||
UnitName string
|
||||
UnitPrice int64
|
||||
}
|
||||
|
||||
type GeneratorTaskDisableUnlimitedPrice struct {
|
||||
Balance int64
|
||||
Price int64
|
||||
PriceDetail GeneratorTaskPriceDetail
|
||||
DisableUnlimited bool
|
||||
}
|
||||
|
||||
type GeneratorTaskQueueInfo struct {
|
||||
InQueue bool
|
||||
Position int64
|
||||
TotalQueueCount int64
|
||||
EstimatedEndTime string
|
||||
RemainingTimeSeconds int64
|
||||
DisableUnlimitedPrice GeneratorTaskDisableUnlimitedPrice
|
||||
}
|
||||
|
||||
type GeneratorTaskArtifactMetadata struct {
|
||||
ArtifactID string
|
||||
Format string
|
||||
Height int64
|
||||
Label string
|
||||
NodeID string
|
||||
NodeName string
|
||||
SizeBytes int64
|
||||
Width int64
|
||||
}
|
||||
|
||||
type GeneratorTaskArtifact struct {
|
||||
Type string
|
||||
Content string
|
||||
Metadata GeneratorTaskArtifactMetadata
|
||||
}
|
||||
|
||||
type GeneratorTask struct {
|
||||
GeneratorTaskID string
|
||||
GeneratorName string
|
||||
Status string
|
||||
OriginalInputArgs GeneratorTaskInputArgs
|
||||
QueueInfo GeneratorTaskQueueInfo
|
||||
Artifacts []GeneratorTaskArtifact
|
||||
Project Project
|
||||
}
|
||||
|
||||
type ImageVectorizationRequest struct {
|
||||
ImageDataURL string
|
||||
Prompt string
|
||||
Title string
|
||||
Width int64
|
||||
Height int64
|
||||
}
|
||||
|
||||
type ImageVectorization struct {
|
||||
SVG string
|
||||
Width int64
|
||||
Height int64
|
||||
}
|
||||
|
||||
type ImageVectorizer interface {
|
||||
VectorizeImage(ctx context.Context, req ImageVectorizationRequest) (ImageVectorization, error)
|
||||
}
|
||||
|
||||
type AgentPlan struct {
|
||||
Message Message
|
||||
GeneratedNodes []Node
|
||||
Connections []Connection
|
||||
}
|
||||
|
||||
type AgentImageGenerationResult struct {
|
||||
Index int
|
||||
Node Node
|
||||
}
|
||||
|
||||
type AgentTaskPlanRequest struct {
|
||||
Project Project
|
||||
Prompt string
|
||||
Mode string
|
||||
Messages []AgentChatMessage
|
||||
Memory AgentMemory
|
||||
WebSearchEnabled bool
|
||||
}
|
||||
|
||||
type AgentImageTask struct {
|
||||
Title string `json:"title"`
|
||||
Brief string `json:"brief"`
|
||||
}
|
||||
|
||||
type AgentTaskPlan struct {
|
||||
Intent string
|
||||
Reason string
|
||||
UserFacingResponse string
|
||||
ShouldSearch bool
|
||||
SearchQuery string
|
||||
SearchReason string
|
||||
ImageCount int
|
||||
ImageTasks []AgentImageTask
|
||||
}
|
||||
|
||||
type AgentDecisionRequest struct {
|
||||
Project Project
|
||||
Prompt string
|
||||
Mode string
|
||||
Messages []AgentChatMessage
|
||||
Memory AgentMemory
|
||||
}
|
||||
|
||||
type AgentDecision struct {
|
||||
Intent string
|
||||
Reason string
|
||||
}
|
||||
|
||||
type AgentConversationRequest struct {
|
||||
Project Project
|
||||
Prompt string
|
||||
Mode string
|
||||
Messages []AgentChatMessage
|
||||
Memory AgentMemory
|
||||
TaskPlan AgentTaskPlan
|
||||
}
|
||||
|
||||
type AgentImageSummaryRequest struct {
|
||||
Project Project
|
||||
Prompt string
|
||||
Mode string
|
||||
ImageURL string
|
||||
Memory AgentMemory
|
||||
}
|
||||
|
||||
type AgentImagePromptRequest struct {
|
||||
Project Project
|
||||
Prompt string
|
||||
Mode string
|
||||
Messages []AgentChatMessage
|
||||
Memory AgentMemory
|
||||
TaskPlan AgentTaskPlan
|
||||
}
|
||||
|
||||
type AgentImagePrompt struct {
|
||||
Prompt string
|
||||
Title string
|
||||
}
|
||||
|
||||
type AgentResearchDecisionRequest struct {
|
||||
Project Project
|
||||
Prompt string
|
||||
Mode string
|
||||
Messages []AgentChatMessage
|
||||
Memory AgentMemory
|
||||
}
|
||||
|
||||
type AgentResearchDecision struct {
|
||||
ShouldSearch bool
|
||||
Query string
|
||||
Reason string
|
||||
}
|
||||
|
||||
type CreativeAgent interface {
|
||||
Decide(ctx context.Context, req AgentDecisionRequest) (AgentDecision, error)
|
||||
Respond(ctx context.Context, req AgentConversationRequest) (string, error)
|
||||
SummarizeImage(ctx context.Context, req AgentImageSummaryRequest) (string, error)
|
||||
BuildImagePrompt(ctx context.Context, req AgentImagePromptRequest) (AgentImagePrompt, error)
|
||||
}
|
||||
|
||||
type ResearchDecider interface {
|
||||
DecideResearch(ctx context.Context, req AgentResearchDecisionRequest) (AgentResearchDecision, error)
|
||||
}
|
||||
|
||||
type AgentTaskPlanner interface {
|
||||
PlanAgentTask(ctx context.Context, req AgentTaskPlanRequest) (AgentTaskPlan, error)
|
||||
}
|
||||
|
||||
type AgentPlanningNarrator interface {
|
||||
StreamPlanNarration(ctx context.Context, req AgentTaskPlanRequest, onDelta func(delta string) error) (string, error)
|
||||
}
|
||||
|
||||
type StreamingCreativeAgent interface {
|
||||
CreativeAgent
|
||||
StreamRespond(ctx context.Context, req AgentConversationRequest, onDelta func(delta string) error) (string, error)
|
||||
}
|
||||
|
||||
type AgentReplay struct {
|
||||
ProjectID string
|
||||
Messages []Message
|
||||
}
|
||||
|
||||
type AgentRunner interface {
|
||||
Plan(ctx context.Context, req AgentRequest) (AgentPlan, error)
|
||||
Replay(ctx context.Context, project Project) (AgentReplay, error)
|
||||
}
|
||||
|
||||
type IncrementalAgentRunner interface {
|
||||
AgentRunner
|
||||
PlanIncremental(ctx context.Context, req AgentRequest, onResult func(AgentImageGenerationResult) error) (AgentPlan, error)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package design
|
||||
|
||||
import "context"
|
||||
|
||||
type AssetUploadRequest struct {
|
||||
FileName string
|
||||
ContentType string
|
||||
Size int64
|
||||
}
|
||||
|
||||
type AssetUpload struct {
|
||||
Driver string
|
||||
Bucket string
|
||||
ObjectKey string
|
||||
UploadURL string
|
||||
PublicURL string
|
||||
Headers map[string]string
|
||||
ExpiresIn int64
|
||||
}
|
||||
|
||||
type AssetObjectRequest struct {
|
||||
FileName string
|
||||
ContentType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type AssetObject struct {
|
||||
Driver string
|
||||
Bucket string
|
||||
ObjectKey string
|
||||
PublicURL string
|
||||
}
|
||||
|
||||
type AssetStorage interface {
|
||||
CreateUpload(ctx context.Context, req AssetUploadRequest) (AssetUpload, error)
|
||||
PutObject(ctx context.Context, req AssetObjectRequest) (AssetObject, error)
|
||||
DeleteObject(ctx context.Context, publicURL string) error
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package design
|
||||
|
||||
import "context"
|
||||
|
||||
type BackgroundRemovalRequest struct {
|
||||
Image []byte
|
||||
ContentType string
|
||||
Width int
|
||||
Height int
|
||||
InputSize int
|
||||
}
|
||||
|
||||
type BackgroundRemoval struct {
|
||||
Image []byte
|
||||
ContentType string
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
type BackgroundRemover interface {
|
||||
RemoveBackground(ctx context.Context, req BackgroundRemovalRequest) (BackgroundRemoval, error)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package design
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const CanvasDataPrefix = "SHAKKERDATA://"
|
||||
|
||||
type CanvasSnapshot struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
Version string `json:"version"`
|
||||
SnapshotID string `json:"snapshotId"`
|
||||
Viewport Viewport `json:"viewport"`
|
||||
Nodes []Node `json:"nodes"`
|
||||
Connections []Connection `json:"connections"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func EncodeCanvasSnapshot(project Project) (string, error) {
|
||||
snapshot := CanvasSnapshot{
|
||||
ProjectID: project.ID,
|
||||
Version: project.Version,
|
||||
SnapshotID: project.SnapshotID,
|
||||
Viewport: project.Viewport,
|
||||
Nodes: append([]Node(nil), project.Nodes...),
|
||||
Connections: append([]Connection(nil), project.Connections...),
|
||||
UpdatedAt: project.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var compressed bytes.Buffer
|
||||
writer := gzip.NewWriter(&compressed)
|
||||
if _, err := writer.Write(payload); err != nil {
|
||||
_ = writer.Close()
|
||||
return "", err
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return CanvasDataPrefix + base64.StdEncoding.EncodeToString(compressed.Bytes()), nil
|
||||
}
|
||||
|
||||
func DecodeCanvasSnapshot(canvas string) (CanvasSnapshot, error) {
|
||||
canvas = strings.TrimSpace(canvas)
|
||||
if !strings.HasPrefix(canvas, CanvasDataPrefix) {
|
||||
return CanvasSnapshot{}, fmt.Errorf("%w: canvas snapshot must use %s prefix", ErrInvalidInput, CanvasDataPrefix)
|
||||
}
|
||||
|
||||
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(canvas, CanvasDataPrefix))
|
||||
if err != nil {
|
||||
return CanvasSnapshot{}, err
|
||||
}
|
||||
reader, err := gzip.NewReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return CanvasSnapshot{}, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
payload, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return CanvasSnapshot{}, err
|
||||
}
|
||||
|
||||
var snapshot CanvasSnapshot
|
||||
if err := json.Unmarshal(payload, &snapshot); err != nil {
|
||||
return CanvasSnapshot{}, err
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func CanvasVersion(now time.Time) string {
|
||||
if now.IsZero() {
|
||||
now = time.Now()
|
||||
}
|
||||
return fmt.Sprintf("%d", now.UnixMilli())
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package design
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultGeneratedImageSize = "1024x1024"
|
||||
DefaultGeneratedImageWidth = 1024
|
||||
DefaultGeneratedImageHeight = 1024
|
||||
)
|
||||
|
||||
func ParseImageSize(size string) (float64, float64, bool) {
|
||||
size = strings.ToLower(strings.TrimSpace(size))
|
||||
if size == "" {
|
||||
return 0, 0, false
|
||||
}
|
||||
size = strings.NewReplacer(" ", "", "\t", "", "\n", "", "×", "x", "*", "x").Replace(size)
|
||||
parts := strings.Split(size, "x")
|
||||
if len(parts) != 2 {
|
||||
return 0, 0, false
|
||||
}
|
||||
width, err := strconv.ParseFloat(parts[0], 64)
|
||||
if err != nil || width <= 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
height, err := strconv.ParseFloat(parts[1], 64)
|
||||
if err != nil || height <= 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return width, height, true
|
||||
}
|
||||
|
||||
func GeneratedImageDimensions(size string) (float64, float64) {
|
||||
if width, height, ok := ParseImageSize(size); ok {
|
||||
return width, height
|
||||
}
|
||||
return DefaultGeneratedImageWidth, DefaultGeneratedImageHeight
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package design
|
||||
|
||||
import "context"
|
||||
|
||||
type JobKind string
|
||||
|
||||
const (
|
||||
JobCreateProjectGeneration JobKind = "canvas:create-project-generation"
|
||||
JobFollowupGeneration JobKind = "canvas:followup-generation"
|
||||
JobNodeActionGeneration JobKind = "canvas:node-action-generation"
|
||||
JobAgentConversation JobKind = "canvas:agent-conversation"
|
||||
JobTextExtraction JobKind = "canvas:text-extraction"
|
||||
JobMockupModelGeneration JobKind = "canvas:mockup-model-generation"
|
||||
JobLayerMergeGeneration JobKind = "canvas:layer-merge-generation"
|
||||
JobAssetCleanup JobKind = "asset:cleanup"
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
Kind JobKind
|
||||
UserID string
|
||||
ProjectID string
|
||||
ThreadID string
|
||||
Prompt string
|
||||
Mode string
|
||||
ImageModel string
|
||||
ImageSize string
|
||||
TaskPlan AgentTaskPlan
|
||||
EnableWebSearch bool
|
||||
Isolated bool
|
||||
Target Node
|
||||
Request NodeActionRequest
|
||||
MessageID string
|
||||
Conversation AgentConversationRequest
|
||||
Messages []AgentChatMessage
|
||||
Feature string
|
||||
PlaceholderID string
|
||||
SourceNodes []Node
|
||||
Title string
|
||||
PublicURLs []string
|
||||
}
|
||||
|
||||
type JobQueue interface {
|
||||
Enqueue(ctx context.Context, job Job) error
|
||||
}
|
||||
|
||||
type JobProcessor interface {
|
||||
ProcessJob(ctx context.Context, job Job) error
|
||||
}
|
||||
|
||||
type JobFailureHandler interface {
|
||||
HandleJobFailure(ctx context.Context, job Job, err error) error
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package design
|
||||
|
||||
import "strings"
|
||||
|
||||
type AgentMemory struct {
|
||||
ShortTerm string
|
||||
LongTerm string
|
||||
}
|
||||
|
||||
func (m AgentMemory) IsZero() bool {
|
||||
return strings.TrimSpace(m.ShortTerm) == "" && strings.TrimSpace(m.LongTerm) == ""
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package design
|
||||
|
||||
import "context"
|
||||
|
||||
type NormalizedBox struct {
|
||||
X float64
|
||||
Y float64
|
||||
Width float64
|
||||
Height float64
|
||||
}
|
||||
|
||||
type ObjectRecognitionRequest struct {
|
||||
ImageBase64 string
|
||||
ImageURL string
|
||||
BoundingBox NormalizedBox
|
||||
Lang string
|
||||
Prompt string
|
||||
}
|
||||
|
||||
type ObjectRecognition struct {
|
||||
ID string
|
||||
Label string
|
||||
Confidence float64
|
||||
BBox NormalizedBox
|
||||
}
|
||||
|
||||
type ObjectRecognizer interface {
|
||||
RecognizeObject(ctx context.Context, req ObjectRecognitionRequest) (ObjectRecognition, error)
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package design
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrNotFound = errors.New("project not found")
|
||||
ErrConflict = errors.New("project version conflict")
|
||||
)
|
||||
|
||||
type ProjectStatus string
|
||||
|
||||
const (
|
||||
StatusDraft ProjectStatus = "draft"
|
||||
StatusExploring ProjectStatus = "exploring"
|
||||
StatusReady ProjectStatus = "ready"
|
||||
StatusFailed ProjectStatus = "failed"
|
||||
)
|
||||
|
||||
type NodeType string
|
||||
|
||||
const (
|
||||
NodeTypeBrief NodeType = "brief"
|
||||
NodeTypeReference NodeType = "reference"
|
||||
NodeTypeImage NodeType = "image"
|
||||
NodeTypeText NodeType = "text"
|
||||
NodeTypeFrame NodeType = "frame"
|
||||
)
|
||||
|
||||
type Viewport struct {
|
||||
X float64
|
||||
Y float64
|
||||
K float64
|
||||
}
|
||||
|
||||
type MockupPoint struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
}
|
||||
|
||||
type MockupMesh struct {
|
||||
Columns int `json:"columns"`
|
||||
Rows int `json:"rows"`
|
||||
Points []MockupPoint `json:"points"`
|
||||
}
|
||||
|
||||
type MockupPlacement struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
}
|
||||
|
||||
type MockupSurface struct {
|
||||
TargetID string `json:"targetId"`
|
||||
Polygon []MockupPoint `json:"polygon"`
|
||||
Mesh MockupMesh `json:"mesh"`
|
||||
}
|
||||
|
||||
type MockupSurfaceResult struct {
|
||||
Placement MockupPlacement
|
||||
Surface MockupSurface
|
||||
}
|
||||
|
||||
type MockupModelMap struct {
|
||||
DataID string `json:"dataId"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Data []float64 `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type MockupCameraIntrinsics struct {
|
||||
Matrix []float64 `json:"matrix"`
|
||||
}
|
||||
|
||||
type MockupModel struct {
|
||||
Version int `json:"version"`
|
||||
DepthMap MockupModelMap `json:"depthMap"`
|
||||
DepthScaleMin float64 `json:"depthScaleMin"`
|
||||
DepthScaleMax float64 `json:"depthScaleMax"`
|
||||
ShadeMap MockupModelMap `json:"shadeMap"`
|
||||
SegmentationMap MockupModelMap `json:"segmentationMap"`
|
||||
XOffsetMap MockupModelMap `json:"xOffsetMap"`
|
||||
YOffsetMap MockupModelMap `json:"yOffsetMap"`
|
||||
CameraIntrinsics MockupCameraIntrinsics `json:"cameraIntrinsics"`
|
||||
ColorCorrection string `json:"colorCorrection"`
|
||||
ForegroundImage string `json:"foregroundImage"`
|
||||
}
|
||||
|
||||
type MockupModelResult struct {
|
||||
Placement MockupPlacement
|
||||
Model MockupModel
|
||||
}
|
||||
|
||||
type MockupModelAnalysisRequest struct {
|
||||
Project Project
|
||||
Node Node
|
||||
ImageDataURL string
|
||||
ImageWidth int
|
||||
ImageHeight int
|
||||
}
|
||||
|
||||
type MockupModelAnalysis struct {
|
||||
Placement MockupPlacement
|
||||
Surface MockupSurface
|
||||
Model MockupModel
|
||||
}
|
||||
|
||||
type MockupBlobData struct {
|
||||
DataID string
|
||||
Width int
|
||||
Height int
|
||||
Data []byte
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID string
|
||||
Type NodeType
|
||||
Title string
|
||||
X float64
|
||||
Y float64
|
||||
Width float64
|
||||
Height float64
|
||||
Content string
|
||||
Tone string
|
||||
Status string
|
||||
ParentID string
|
||||
LayerRole string
|
||||
FontSize float64
|
||||
FontFamily string
|
||||
FontWeight string
|
||||
Color string
|
||||
TextAlign string
|
||||
LineHeight float64
|
||||
LetterSpacing float64
|
||||
TextDecoration string
|
||||
TextTransform string
|
||||
Opacity float64
|
||||
FillColor string
|
||||
StrokeColor string
|
||||
StrokeWidth float64
|
||||
StrokeStyle string
|
||||
FlipX bool
|
||||
FlipY bool
|
||||
Rotation float64
|
||||
ImageAdjustments string
|
||||
MockupSurface MockupSurface
|
||||
MockupModel MockupModel
|
||||
MockupSourceContent string
|
||||
}
|
||||
|
||||
type NodeActionRequest struct {
|
||||
Action string
|
||||
Prompt string
|
||||
ImageModel string
|
||||
ImageSize string
|
||||
Mask string
|
||||
Selection string
|
||||
Options map[string]string
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
ID string
|
||||
FromNodeID string
|
||||
ToNodeID string
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID string
|
||||
Role string
|
||||
Type string
|
||||
Title string
|
||||
Content string
|
||||
ThreadID string
|
||||
ActionID string
|
||||
StepID string
|
||||
ToolCallID string
|
||||
Name string
|
||||
ToolHint string
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
ID string
|
||||
UserID string
|
||||
Title string
|
||||
Brief string
|
||||
Status ProjectStatus
|
||||
Thumbnail string
|
||||
UpdatedAt time.Time
|
||||
Version string
|
||||
SnapshotID string
|
||||
Canvas string
|
||||
LastThreadID string
|
||||
Viewport Viewport
|
||||
Nodes []Node
|
||||
Connections []Connection
|
||||
Messages []Message
|
||||
}
|
||||
|
||||
type ProjectSummary struct {
|
||||
ID string
|
||||
UserID string
|
||||
Title string
|
||||
Brief string
|
||||
Status ProjectStatus
|
||||
Thumbnail string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type QuickAction struct {
|
||||
ID string
|
||||
Label string
|
||||
Prompt string
|
||||
Icon string
|
||||
}
|
||||
|
||||
type InspirationItem struct {
|
||||
ID string
|
||||
Title string
|
||||
Category string
|
||||
Description string
|
||||
Accent string
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
List(ctx context.Context) ([]ProjectSummary, error)
|
||||
ListPage(ctx context.Context, limit int64, offset int64) ([]ProjectSummary, error)
|
||||
Get(ctx context.Context, id string) (Project, error)
|
||||
Save(ctx context.Context, project Project) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
type CanvasRepository interface {
|
||||
SaveCanvas(ctx context.Context, project Project, baseVersion string) error
|
||||
}
|
||||
|
||||
type MockupBlobRepository interface {
|
||||
SaveMockupBlob(ctx context.Context, blob MockupBlobData) error
|
||||
GetMockupBlob(ctx context.Context, dataID string) (MockupBlobData, error)
|
||||
}
|
||||
|
||||
type MockupModelAnalyzer interface {
|
||||
AnalyzeMockupModel(ctx context.Context, req MockupModelAnalysisRequest) (MockupModelAnalysis, error)
|
||||
}
|
||||
|
||||
func NormalizeTitle(title string, fallback string) string {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
return fallback
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func NormalizePrompt(prompt string) string {
|
||||
return strings.TrimSpace(prompt)
|
||||
}
|
||||
|
||||
func SortSummaries(items []ProjectSummary) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return items[i].UpdatedAt.After(items[j].UpdatedAt)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package design
|
||||
|
||||
import "context"
|
||||
|
||||
type ResearchResult struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Snippet string `json:"snippet"`
|
||||
}
|
||||
|
||||
type ResearchReport struct {
|
||||
Kind string `json:"kind"`
|
||||
Source string `json:"source"`
|
||||
Query string `json:"query"`
|
||||
Results []ResearchResult `json:"results"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type Researcher interface {
|
||||
Research(ctx context.Context, prompt string) (ResearchReport, error)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package design
|
||||
|
||||
import "context"
|
||||
|
||||
type TextExtractionRequest struct {
|
||||
ImageURL string
|
||||
Prompt string
|
||||
Width float64
|
||||
Height float64
|
||||
}
|
||||
|
||||
type ExtractedTextLayer struct {
|
||||
Text string
|
||||
OriginalText string
|
||||
X float64
|
||||
Y float64
|
||||
Width float64
|
||||
Height float64
|
||||
FontSize float64
|
||||
FontFamily string
|
||||
FontWeight string
|
||||
Color string
|
||||
TextAlign string
|
||||
LineHeight float64
|
||||
LetterSpacing float64
|
||||
StrokeColor string
|
||||
StrokeWidth float64
|
||||
Opacity float64
|
||||
Rotation float64
|
||||
Confidence float64
|
||||
}
|
||||
|
||||
type TextExtraction struct {
|
||||
Layers []ExtractedTextLayer
|
||||
}
|
||||
|
||||
type TextExtractor interface {
|
||||
ExtractText(ctx context.Context, req TextExtractionRequest) (TextExtraction, error)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package design
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const DefaultUserID = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
type userIDContextKey struct{}
|
||||
|
||||
func ContextWithUserID(ctx context.Context, userID string) context.Context {
|
||||
return context.WithValue(ctx, userIDContextKey{}, NormalizeUserID(userID))
|
||||
}
|
||||
|
||||
func UserIDFromContext(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return DefaultUserID
|
||||
}
|
||||
if value, ok := ctx.Value(userIDContextKey{}).(string); ok {
|
||||
return NormalizeUserID(value)
|
||||
}
|
||||
return DefaultUserID
|
||||
}
|
||||
|
||||
func NormalizeUserID(userID string) string {
|
||||
userID = strings.TrimSpace(userID)
|
||||
if userID == "" {
|
||||
return DefaultUserID
|
||||
}
|
||||
if _, err := uuid.Parse(userID); err != nil {
|
||||
return DefaultUserID
|
||||
}
|
||||
return strings.ToLower(userID)
|
||||
}
|
||||
Reference in New Issue
Block a user