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,562 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
sqlcdb "img_infinite_canvas/internal/infrastructure/postgres/db"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
//go:embed schema.sql
|
||||
var schemaSQL string
|
||||
|
||||
type ProjectRepository struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewProjectRepository(ctx context.Context, dataSource string) (*ProjectRepository, 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 &ProjectRepository{pool: pool}, nil
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) Close() {
|
||||
r.pool.Close()
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) List(ctx context.Context) ([]design.ProjectSummary, error) {
|
||||
rows, err := sqlcdb.New(r.pool).ListProjects(ctx, toPGUUID(design.UserIDFromContext(ctx)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]design.ProjectSummary, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, design.ProjectSummary{
|
||||
ID: row.ID,
|
||||
UserID: fromPGUUID(row.UserID),
|
||||
Title: row.Title,
|
||||
Brief: row.Brief,
|
||||
Status: design.ProjectStatus(row.Status),
|
||||
Thumbnail: row.Thumbnail,
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) ListPage(ctx context.Context, limit int64, offset int64) ([]design.ProjectSummary, error) {
|
||||
if limit <= 0 {
|
||||
return []design.ProjectSummary{}, nil
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
rows, err := sqlcdb.New(r.pool).ListProjectsPage(ctx, sqlcdb.ListProjectsPageParams{
|
||||
UserID: toPGUUID(design.UserIDFromContext(ctx)),
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]design.ProjectSummary, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, design.ProjectSummary{
|
||||
ID: row.ID,
|
||||
UserID: fromPGUUID(row.UserID),
|
||||
Title: row.Title,
|
||||
Brief: row.Brief,
|
||||
Status: design.ProjectStatus(row.Status),
|
||||
Thumbnail: row.Thumbnail,
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) Get(ctx context.Context, id string) (design.Project, error) {
|
||||
q := sqlcdb.New(r.pool)
|
||||
userID := design.UserIDFromContext(ctx)
|
||||
pgUserID := toPGUUID(userID)
|
||||
row, err := q.GetProject(ctx, sqlcdb.GetProjectParams{ID: id, UserID: pgUserID})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return design.Project{}, design.ErrNotFound
|
||||
}
|
||||
return design.Project{}, err
|
||||
}
|
||||
|
||||
nodes, err := q.ListNodesByProject(ctx, sqlcdb.ListNodesByProjectParams{ProjectID: id, UserID: pgUserID})
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
connections, err := q.ListConnectionsByProject(ctx, sqlcdb.ListConnectionsByProjectParams{ProjectID: id, UserID: pgUserID})
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
messages, err := q.ListMessagesByProject(ctx, sqlcdb.ListMessagesByProjectParams{ProjectID: id, UserID: pgUserID})
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
|
||||
project := design.Project{
|
||||
ID: row.ID,
|
||||
UserID: fromPGUUID(row.UserID),
|
||||
Title: row.Title,
|
||||
Brief: row.Brief,
|
||||
Status: design.ProjectStatus(row.Status),
|
||||
Thumbnail: row.Thumbnail,
|
||||
Version: row.Version,
|
||||
SnapshotID: row.SnapshotID,
|
||||
Canvas: row.Canvas,
|
||||
LastThreadID: row.LastThreadID,
|
||||
UpdatedAt: row.UpdatedAt.Time,
|
||||
Viewport: design.Viewport{
|
||||
X: row.ViewportX,
|
||||
Y: row.ViewportY,
|
||||
K: row.ViewportK,
|
||||
},
|
||||
Nodes: mapNodes(nodes),
|
||||
Connections: mapConnections(connections),
|
||||
Messages: mapMessages(messages),
|
||||
}
|
||||
if err := r.applyCurrentSnapshot(ctx, q, &project); err != nil {
|
||||
return design.Project{}, err
|
||||
}
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) Save(ctx context.Context, project design.Project) error {
|
||||
project.UserID = repositoryUserID(ctx, project.UserID)
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
q := sqlcdb.New(r.pool).WithTx(tx)
|
||||
if err := q.UpsertProject(ctx, sqlcdb.UpsertProjectParams{
|
||||
ID: project.ID,
|
||||
UserID: toPGUUID(project.UserID),
|
||||
Title: project.Title,
|
||||
Brief: project.Brief,
|
||||
Status: string(project.Status),
|
||||
Thumbnail: project.Thumbnail,
|
||||
Canvas: project.Canvas,
|
||||
Version: project.Version,
|
||||
SnapshotID: project.SnapshotID,
|
||||
LastThreadID: project.LastThreadID,
|
||||
ViewportX: project.Viewport.X,
|
||||
ViewportY: project.Viewport.Y,
|
||||
ViewportK: project.Viewport.K,
|
||||
UpdatedAt: pgtype.Timestamptz{Time: project.UpdatedAt, Valid: true},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := q.DeleteConnectionsByProject(ctx, sqlcdb.DeleteConnectionsByProjectParams{ProjectID: project.ID, UserID: toPGUUID(project.UserID)}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.DeleteNodesByProject(ctx, sqlcdb.DeleteNodesByProjectParams{ProjectID: project.ID, UserID: toPGUUID(project.UserID)}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.DeleteMessagesByProject(ctx, sqlcdb.DeleteMessagesByProjectParams{ProjectID: project.ID, UserID: toPGUUID(project.UserID)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for index, node := range project.Nodes {
|
||||
if err := q.UpsertCanvasNode(ctx, sqlcdb.UpsertCanvasNodeParams{
|
||||
ID: node.ID,
|
||||
ProjectID: project.ID,
|
||||
UserID: toPGUUID(project.UserID),
|
||||
NodeType: string(node.Type),
|
||||
Title: node.Title,
|
||||
X: node.X,
|
||||
Y: node.Y,
|
||||
Width: node.Width,
|
||||
Height: node.Height,
|
||||
Content: node.Content,
|
||||
Tone: node.Tone,
|
||||
Status: node.Status,
|
||||
SortOrder: int32(index),
|
||||
ParentID: node.ParentID,
|
||||
LayerRole: node.LayerRole,
|
||||
FontSize: node.FontSize,
|
||||
FontFamily: node.FontFamily,
|
||||
FontWeight: node.FontWeight,
|
||||
Color: node.Color,
|
||||
TextAlign: node.TextAlign,
|
||||
LineHeight: node.LineHeight,
|
||||
LetterSpacing: node.LetterSpacing,
|
||||
TextDecoration: node.TextDecoration,
|
||||
TextTransform: node.TextTransform,
|
||||
Opacity: node.Opacity,
|
||||
FillColor: node.FillColor,
|
||||
StrokeColor: node.StrokeColor,
|
||||
StrokeWidth: node.StrokeWidth,
|
||||
StrokeStyle: node.StrokeStyle,
|
||||
FlipX: node.FlipX,
|
||||
FlipY: node.FlipY,
|
||||
Rotation: node.Rotation,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, connection := range project.Connections {
|
||||
if err := q.CreateConnection(ctx, sqlcdb.CreateConnectionParams{
|
||||
ID: connection.ID,
|
||||
ProjectID: project.ID,
|
||||
UserID: toPGUUID(project.UserID),
|
||||
FromNodeID: connection.FromNodeID,
|
||||
ToNodeID: connection.ToNodeID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, message := range project.Messages {
|
||||
if err := q.CreateMessage(ctx, sqlcdb.CreateMessageParams{
|
||||
ID: message.ID,
|
||||
ProjectID: project.ID,
|
||||
UserID: toPGUUID(project.UserID),
|
||||
Role: message.Role,
|
||||
MessageType: message.Type,
|
||||
Title: message.Title,
|
||||
Content: message.Content,
|
||||
ThreadID: message.ThreadID,
|
||||
ActionID: message.ActionID,
|
||||
StepID: message.StepID,
|
||||
ToolCallID: message.ToolCallID,
|
||||
Name: message.Name,
|
||||
ToolHint: message.ToolHint,
|
||||
Status: message.Status,
|
||||
CreatedAt: pgtype.Timestamptz{Time: message.CreatedAt, Valid: true},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := r.createCanvasSnapshot(ctx, q, project); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := insertProjectOutboxEvent(ctx, tx, project, "project.updated"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) SaveCanvas(ctx context.Context, project design.Project, baseVersion string) error {
|
||||
project.UserID = repositoryUserID(ctx, project.UserID)
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
q := sqlcdb.New(r.pool).WithTx(tx)
|
||||
locked, err := q.LockProject(ctx, sqlcdb.LockProjectParams{ID: project.ID, UserID: toPGUUID(project.UserID)})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return design.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
baseVersion = strings.TrimSpace(baseVersion)
|
||||
if baseVersion != "" && locked.Version != "" && baseVersion != locked.Version {
|
||||
return fmt.Errorf("%w: expected %s but current version is %s", design.ErrConflict, baseVersion, locked.Version)
|
||||
}
|
||||
if err := r.createCanvasSnapshot(ctx, q, project); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.UpdateProjectCanvasSnapshot(ctx, sqlcdb.UpdateProjectCanvasSnapshotParams{
|
||||
ID: project.ID,
|
||||
UserID: toPGUUID(project.UserID),
|
||||
Thumbnail: project.Thumbnail,
|
||||
Canvas: project.Canvas,
|
||||
Version: project.Version,
|
||||
SnapshotID: project.SnapshotID,
|
||||
ViewportX: project.Viewport.X,
|
||||
ViewportY: project.Viewport.Y,
|
||||
ViewportK: project.Viewport.K,
|
||||
UpdatedAt: pgtype.Timestamptz{Time: project.UpdatedAt, Valid: true},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := insertProjectOutboxEvent(ctx, tx, project, "canvas.snapshot.created"); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) Delete(ctx context.Context, id string) error {
|
||||
q := sqlcdb.New(r.pool)
|
||||
userID := design.UserIDFromContext(ctx)
|
||||
pgUserID := toPGUUID(userID)
|
||||
project, err := q.GetProject(ctx, sqlcdb.GetProjectParams{ID: id, UserID: pgUserID})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return design.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := sqlcdb.New(r.pool).WithTx(tx)
|
||||
if err := qtx.DeleteProject(ctx, sqlcdb.DeleteProjectParams{ID: id, UserID: pgUserID}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := insertProjectOutboxEvent(ctx, tx, design.Project{ID: project.ID, UserID: fromPGUUID(project.UserID), LastThreadID: project.LastThreadID, UpdatedAt: project.UpdatedAt.Time}, "project.deleted"); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) SaveMockupBlob(ctx context.Context, blob design.MockupBlobData) error {
|
||||
if strings.TrimSpace(blob.DataID) == "" || len(blob.Data) == 0 {
|
||||
return design.ErrInvalidInput
|
||||
}
|
||||
createdAt := blob.CreatedAt
|
||||
if createdAt.IsZero() {
|
||||
createdAt = time.Now()
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
INSERT INTO mockup_blobs (data_id, width, height, data, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (data_id) DO UPDATE SET
|
||||
width = EXCLUDED.width,
|
||||
height = EXCLUDED.height,
|
||||
data = EXCLUDED.data
|
||||
`, blob.DataID, blob.Width, blob.Height, blob.Data, pgtype.Timestamptz{Time: createdAt, Valid: true})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) GetMockupBlob(ctx context.Context, dataID string) (design.MockupBlobData, error) {
|
||||
dataID = strings.TrimSpace(dataID)
|
||||
if dataID == "" {
|
||||
return design.MockupBlobData{}, design.ErrNotFound
|
||||
}
|
||||
var blob design.MockupBlobData
|
||||
var createdAt pgtype.Timestamptz
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT data_id, width, height, data, created_at
|
||||
FROM mockup_blobs
|
||||
WHERE data_id = $1
|
||||
`, dataID).Scan(&blob.DataID, &blob.Width, &blob.Height, &blob.Data, &createdAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return design.MockupBlobData{}, design.ErrNotFound
|
||||
}
|
||||
return design.MockupBlobData{}, err
|
||||
}
|
||||
if createdAt.Valid {
|
||||
blob.CreatedAt = createdAt.Time
|
||||
}
|
||||
blob.Data = append([]byte(nil), blob.Data...)
|
||||
return blob, nil
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) applyCurrentSnapshot(ctx context.Context, q *sqlcdb.Queries, project *design.Project) error {
|
||||
if project == nil || strings.TrimSpace(project.SnapshotID) == "" {
|
||||
return nil
|
||||
}
|
||||
snapshot, err := q.GetCanvasSnapshot(ctx, sqlcdb.GetCanvasSnapshotParams{
|
||||
ID: project.SnapshotID,
|
||||
ProjectID: project.ID,
|
||||
UserID: toPGUUID(project.UserID),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var viewport design.Viewport
|
||||
var nodes []design.Node
|
||||
var connections []design.Connection
|
||||
if err := json.Unmarshal(snapshot.ViewportJson, &viewport); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(snapshot.NodesJson, &nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(snapshot.ConnectionsJson, &connections); err != nil {
|
||||
return err
|
||||
}
|
||||
project.Viewport = viewport
|
||||
project.Nodes = nodes
|
||||
project.Connections = connections
|
||||
project.Canvas = snapshot.Canvas
|
||||
project.Version = snapshot.Version
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProjectRepository) createCanvasSnapshot(ctx context.Context, q *sqlcdb.Queries, project design.Project) error {
|
||||
if strings.TrimSpace(project.SnapshotID) == "" || strings.TrimSpace(project.Canvas) == "" {
|
||||
return nil
|
||||
}
|
||||
viewport, err := json.Marshal(project.Viewport)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nodes, err := json.Marshal(project.Nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connections, err := json.Marshal(project.Connections)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return q.CreateCanvasSnapshot(ctx, sqlcdb.CreateCanvasSnapshotParams{
|
||||
ID: project.SnapshotID,
|
||||
ProjectID: project.ID,
|
||||
UserID: toPGUUID(project.UserID),
|
||||
Version: project.Version,
|
||||
Canvas: project.Canvas,
|
||||
ViewportJson: viewport,
|
||||
NodesJson: nodes,
|
||||
ConnectionsJson: connections,
|
||||
CreatedAt: pgtype.Timestamptz{Time: project.UpdatedAt, Valid: true},
|
||||
})
|
||||
}
|
||||
|
||||
func repositoryUserID(ctx context.Context, userID string) string {
|
||||
userID = strings.TrimSpace(userID)
|
||||
if userID != "" {
|
||||
return design.NormalizeUserID(userID)
|
||||
}
|
||||
return design.UserIDFromContext(ctx)
|
||||
}
|
||||
|
||||
func toPGUUID(userID string) pgtype.UUID {
|
||||
parsed, err := uuid.Parse(design.NormalizeUserID(userID))
|
||||
if err != nil {
|
||||
parsed = uuid.MustParse(design.DefaultUserID)
|
||||
}
|
||||
return pgtype.UUID{Bytes: parsed, Valid: true}
|
||||
}
|
||||
|
||||
func fromPGUUID(value pgtype.UUID) string {
|
||||
if !value.Valid {
|
||||
return design.DefaultUserID
|
||||
}
|
||||
return uuid.UUID(value.Bytes).String()
|
||||
}
|
||||
|
||||
func insertProjectOutboxEvent(ctx context.Context, tx pgx.Tx, project design.Project, eventType string) error {
|
||||
eventID := uuid.NewString()
|
||||
createdAt := project.UpdatedAt
|
||||
if createdAt.IsZero() {
|
||||
createdAt = time.Now()
|
||||
}
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"id": eventID,
|
||||
"type": eventType,
|
||||
"userId": project.UserID,
|
||||
"projectId": project.ID,
|
||||
"threadId": project.LastThreadID,
|
||||
"version": project.Version,
|
||||
"snapshotId": project.SnapshotID,
|
||||
"createdAt": createdAt.UTC().Format(time.RFC3339Nano),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx, `
|
||||
INSERT INTO outbox_events (id, user_id, aggregate_type, aggregate_id, event_type, thread_id, payload_json, status, created_at)
|
||||
VALUES ($1, $2, 'project', $3, $4, $5, $6, 'pending', $7)
|
||||
`, eventID, toPGUUID(project.UserID), project.ID, eventType, project.LastThreadID, payload, pgtype.Timestamptz{Time: createdAt, Valid: true})
|
||||
return err
|
||||
}
|
||||
|
||||
func mapNodes(rows []sqlcdb.CanvasNode) []design.Node {
|
||||
nodes := make([]design.Node, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
nodes = append(nodes, design.Node{
|
||||
ID: row.ID,
|
||||
Type: design.NodeType(row.NodeType),
|
||||
Title: row.Title,
|
||||
X: row.X,
|
||||
Y: row.Y,
|
||||
Width: row.Width,
|
||||
Height: row.Height,
|
||||
Content: row.Content,
|
||||
Tone: row.Tone,
|
||||
Status: row.Status,
|
||||
ParentID: row.ParentID,
|
||||
LayerRole: row.LayerRole,
|
||||
FontSize: row.FontSize,
|
||||
FontFamily: row.FontFamily,
|
||||
FontWeight: row.FontWeight,
|
||||
Color: row.Color,
|
||||
TextAlign: row.TextAlign,
|
||||
LineHeight: row.LineHeight,
|
||||
LetterSpacing: row.LetterSpacing,
|
||||
TextDecoration: row.TextDecoration,
|
||||
TextTransform: row.TextTransform,
|
||||
Opacity: row.Opacity,
|
||||
FillColor: row.FillColor,
|
||||
StrokeColor: row.StrokeColor,
|
||||
StrokeWidth: row.StrokeWidth,
|
||||
StrokeStyle: row.StrokeStyle,
|
||||
FlipX: row.FlipX,
|
||||
FlipY: row.FlipY,
|
||||
Rotation: row.Rotation,
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func mapConnections(rows []sqlcdb.CanvasConnection) []design.Connection {
|
||||
connections := make([]design.Connection, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
connections = append(connections, design.Connection{
|
||||
ID: row.ID,
|
||||
FromNodeID: row.FromNodeID,
|
||||
ToNodeID: row.ToNodeID,
|
||||
})
|
||||
}
|
||||
return connections
|
||||
}
|
||||
|
||||
func mapMessages(rows []sqlcdb.AgentMessage) []design.Message {
|
||||
messages := make([]design.Message, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
messages = append(messages, design.Message{
|
||||
ID: row.ID,
|
||||
Role: row.Role,
|
||||
Type: row.MessageType,
|
||||
Title: row.Title,
|
||||
Content: row.Content,
|
||||
ThreadID: row.ThreadID,
|
||||
ActionID: row.ActionID,
|
||||
StepID: row.StepID,
|
||||
ToolCallID: row.ToolCallID,
|
||||
Name: row.Name,
|
||||
ToolHint: row.ToolHint,
|
||||
Status: row.Status,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
})
|
||||
}
|
||||
return messages
|
||||
}
|
||||
Reference in New Issue
Block a user