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,99 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
)
|
||||
|
||||
const streamingThinkingThrottle = 450 * time.Millisecond
|
||||
|
||||
type messageEventState struct {
|
||||
fingerprints map[string]string
|
||||
emittedAt map[string]time.Time
|
||||
}
|
||||
|
||||
func newMessageEventState() *messageEventState {
|
||||
return &messageEventState{
|
||||
fingerprints: make(map[string]string),
|
||||
emittedAt: make(map[string]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *messageEventState) shouldEmit(message design.Message, now time.Time) bool {
|
||||
if s == nil {
|
||||
return true
|
||||
}
|
||||
key := messageEventKey(message)
|
||||
fingerprint := messageEventFingerprint(message)
|
||||
if s.fingerprints[key] == fingerprint {
|
||||
return false
|
||||
}
|
||||
if _, seen := s.fingerprints[key]; !seen || messageEventBypassesThrottle(message) {
|
||||
s.record(key, fingerprint, now)
|
||||
return true
|
||||
}
|
||||
if last := s.emittedAt[key]; last.IsZero() || now.Sub(last) >= streamingThinkingThrottle {
|
||||
s.record(key, fingerprint, now)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *messageEventState) record(key string, fingerprint string, now time.Time) {
|
||||
s.fingerprints[key] = fingerprint
|
||||
s.emittedAt[key] = now
|
||||
}
|
||||
|
||||
func messageEventKey(message design.Message) string {
|
||||
if key := strings.TrimSpace(message.ID); key != "" {
|
||||
return key
|
||||
}
|
||||
return fmt.Sprintf("%s:%s:%s:%s:%s", message.ThreadID, message.Role, message.Type, message.Title, message.CreatedAt.UTC().Format(time.RFC3339Nano))
|
||||
}
|
||||
|
||||
func messageEventBypassesThrottle(message design.Message) bool {
|
||||
if message.Type == "text_extraction_result" {
|
||||
return true
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(message.Status)) {
|
||||
case "running", "streaming":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func projectEventFingerprint(project design.Project) string {
|
||||
payload := struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
Title string `json:"title"`
|
||||
Brief string `json:"brief"`
|
||||
Status design.ProjectStatus `json:"status"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
LastThreadID string `json:"lastThreadId"`
|
||||
Viewport design.Viewport `json:"viewport"`
|
||||
Nodes []design.Node `json:"nodes"`
|
||||
Connections []design.Connection `json:"connections"`
|
||||
}{
|
||||
ID: project.ID,
|
||||
UserID: project.UserID,
|
||||
Title: project.Title,
|
||||
Brief: project.Brief,
|
||||
Status: project.Status,
|
||||
Thumbnail: project.Thumbnail,
|
||||
LastThreadID: project.LastThreadID,
|
||||
Viewport: project.Viewport,
|
||||
Nodes: append([]design.Node(nil), project.Nodes...),
|
||||
Connections: append([]design.Connection(nil), project.Connections...),
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s:%s:%s:%d:%d", project.ID, project.Status, project.LastThreadID, len(project.Nodes), len(project.Connections))
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
Reference in New Issue
Block a user