Skip to content

Commit d4072f1

Browse files
authored
feat: add basic editable node & clipboard paste (#32)
* Update localization files for English and Chinese languages - Added new translations for "No content yet, double-click to edit" in both English and Chinese. - Updated existing translations and their corresponding source file references in messages.po for better accuracy. - Adjusted line numbers in translation references to reflect changes in component structure. - Ensured consistency in terminology across both language files. * feat: enhance TextNode with hover effects and wheel event handling * feat: Implement asset management with MD5-based deduplication for user-provided files and refactor storage to use a dedicated assets directory. * feat: Centralize node creation with a new `createNode` store action and integrate it into canvas paste operations, adding toast notifications for asset uploads. * Fix: Clarify toast messages for asset pasting operations. * feat: Prevent user-provided nodes from running or displaying target handles, and improve text node content display by preserving whitespace.
1 parent f286f19 commit d4072f1

File tree

23 files changed

+721
-335
lines changed

23 files changed

+721
-335
lines changed

binding/ai/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ func (s *Service) processContent(projectID int, data []byte, b64 string, url str
285285
var err error
286286

287287
if len(data) > 0 {
288-
filename, err = storage.SaveGeneratedContent(data, prefix, ext)
288+
filename, err = storage.SaveAssetContent(data, prefix, ext)
289289
} else if b64 != "" {
290290
filename, err = storage.SaveBase64Content(b64, prefix, ext)
291291
} else if url != "" {

binding/database/service.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
package database
22

33
import (
4+
"crypto/md5"
5+
"encoding/hex"
46
"fmt"
7+
"os"
8+
"path/filepath"
9+
"time"
510
db "visionflow/database"
11+
"visionflow/storage"
12+
13+
"golang.design/x/clipboard"
614
)
715

8-
type Service struct{}
16+
type Service struct {
17+
}
918

1019
func NewService() *Service {
20+
// Initialize clipboard
21+
err := clipboard.Init()
22+
if err != nil {
23+
fmt.Printf("Failed to initialize clipboard: %v\n", err)
24+
}
1125
return &Service{}
1226
}
1327

@@ -67,3 +81,70 @@ func (s *Service) ListAssets(projectID int) ([]db.Asset, error) {
6781
func (s *Service) DeleteAsset(id int) error {
6882
return db.DeleteAsset(id)
6983
}
84+
85+
// CreateAssetFromFile saves a file provided as bytes as an asset
86+
func (s *Service) CreateAssetFromFile(name string, data []byte) (*db.Asset, error) {
87+
// Calculate MD5 hash
88+
hash := md5.Sum(data)
89+
md5Hash := hex.EncodeToString(hash[:])
90+
91+
// Check if asset with same MD5 already exists
92+
existingAsset, err := db.GetAssetByMD5(md5Hash)
93+
if err != nil {
94+
return nil, fmt.Errorf("failed to check for existing asset: %w", err)
95+
}
96+
if existingAsset != nil {
97+
// Return existing asset with URL
98+
existingAsset.URL = fmt.Sprintf("http://127.0.0.1:34116/%s", existingAsset.Path)
99+
return existingAsset, nil
100+
}
101+
102+
// Determine file extension
103+
ext := filepath.Ext(name)
104+
if ext == "" {
105+
ext = ".dat"
106+
}
107+
108+
// Generate filename
109+
filename := fmt.Sprintf("file_%d%s", time.Now().Unix(), ext)
110+
111+
// Save to storage
112+
assetsDir, err := storage.GetAssetsDir()
113+
if err != nil {
114+
return nil, fmt.Errorf("failed to get assets directory: %w", err)
115+
}
116+
destPath := filepath.Join(assetsDir, filename)
117+
if err := os.WriteFile(destPath, data, 0644); err != nil {
118+
return nil, fmt.Errorf("failed to save file: %w", err)
119+
}
120+
121+
// Determine asset type
122+
var assetType db.AssetType
123+
switch ext {
124+
case ".png", ".jpg", ".jpeg", ".gif", ".webp":
125+
assetType = db.AssetTypeImage
126+
case ".mp4", ".mov", ".avi", ".webm":
127+
assetType = db.AssetTypeVideo
128+
case ".mp3", ".wav", ".ogg":
129+
assetType = db.AssetTypeAudio
130+
}
131+
132+
// Create asset record
133+
asset := db.Asset{
134+
Type: assetType,
135+
Path: filename,
136+
IsUserProvided: true,
137+
MD5: md5Hash,
138+
}
139+
140+
createdAsset, err := db.CreateAsset(asset)
141+
if err != nil {
142+
// Clean up file if database insert fails
143+
os.Remove(destPath)
144+
return nil, fmt.Errorf("failed to create asset record: %w", err)
145+
}
146+
147+
// Add URL for immediate use
148+
createdAsset.URL = fmt.Sprintf("http://127.0.0.1:34116/%s", createdAsset.Path)
149+
return createdAsset, nil
150+
}

database/db.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,18 @@ func InitDB() error {
4646
4747
CREATE TABLE IF NOT EXISTS assets (
4848
id INTEGER PRIMARY KEY AUTOINCREMENT,
49-
project_id INTEGER NOT NULL,
49+
project_id INTEGER,
5050
type TEXT NOT NULL,
5151
path TEXT NOT NULL,
52+
is_user_provided BOOLEAN DEFAULT 0,
53+
md5 TEXT DEFAULT '',
5254
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
5355
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
5456
FOREIGN KEY(project_id) REFERENCES projects(id)
5557
);
58+
59+
60+
CREATE INDEX IF NOT EXISTS idx_assets_md5 ON assets(md5);
5661
5762
CREATE TABLE IF NOT EXISTS user_preferences (
5863
key TEXT PRIMARY KEY,

database/models.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ const (
4242

4343
// Asset represents a stored item (image/video/audio) associated with a project/workflow
4444
type Asset struct {
45-
ID int `db:"id" json:"id"`
46-
ProjectID int `db:"project_id" json:"projectId"`
47-
Type AssetType `db:"type" json:"type"`
48-
Path string `db:"path" json:"path"`
49-
URL string `db:"-" json:"url"`
50-
CreatedAt time.Time `db:"created_at" json:"createdAt"`
51-
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
45+
ID int `db:"id" json:"id"`
46+
ProjectID int `db:"project_id" json:"projectId"`
47+
Type AssetType `db:"type" json:"type"`
48+
Path string `db:"path" json:"path"`
49+
URL string `db:"-" json:"url"`
50+
IsUserProvided bool `db:"is_user_provided" json:"isUserProvided"`
51+
MD5 string `db:"md5" json:"md5"`
52+
CreatedAt time.Time `db:"created_at" json:"createdAt"`
53+
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
5254
}
5355

5456
// UserPreference represents a user preference key-value pair

database/repository.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ func ListProjects() ([]Project, error) {
134134
func CreateAsset(asset Asset) (*Asset, error) {
135135
// Insert
136136
result, err := DB.NamedExec(`
137-
INSERT INTO assets (project_id, type, path, created_at, updated_at)
138-
VALUES (:project_id, :type, :path, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
137+
INSERT INTO assets (project_id, type, path, is_user_provided, md5, created_at, updated_at)
138+
VALUES (:project_id, :type, :path, :is_user_provided, :md5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
139139
`, asset)
140140
if err != nil {
141141
return nil, err
@@ -148,6 +148,22 @@ func CreateAsset(asset Asset) (*Asset, error) {
148148
return GetAsset(asset.ID)
149149
}
150150

151+
// GetAssetByMD5 retrieves an asset by its MD5 hash
152+
func GetAssetByMD5(md5 string) (*Asset, error) {
153+
if md5 == "" {
154+
return nil, nil
155+
}
156+
var asset Asset
157+
err := DB.Get(&asset, "SELECT * FROM assets WHERE md5 = ? LIMIT 1", md5)
158+
if err != nil {
159+
if errors.Is(err, sql.ErrNoRows) {
160+
return nil, nil // Not found
161+
}
162+
return nil, err
163+
}
164+
return &asset, nil
165+
}
166+
151167
// GetAsset retrieves an asset by ID
152168
func GetAsset(id int) (*Asset, error) {
153169
var asset Asset
@@ -193,7 +209,7 @@ func DeleteAsset(id int) error {
193209
return err
194210
}
195211

196-
_ = storage.DeleteGeneratedContent(asset.Path)
212+
_ = storage.DeleteAssetContent(asset.Path)
197213

198214
return nil
199215
}

0 commit comments

Comments
 (0)