Files
root 44406b72db 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>
2026-07-07 23:15:37 +08:00

100 lines
2.8 KiB
Go

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)
}