Skip to content

Commit df800bd

Browse files
authored
feat: asset library (#22)
* feat: implement asset management functionality with CRUD operations and integrate project association * feat: implement asset library with asset listing and deletion functionality; enhance project and asset management * feat: refine asset library styles; adjust button and preview asset container dimensions
1 parent 2bb3a32 commit df800bd

File tree

16 files changed

+504
-46
lines changed

16 files changed

+504
-46
lines changed

app.go

Lines changed: 0 additions & 27 deletions
This file was deleted.

binding/ai/service.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type TextRequest struct {
4040

4141
// ImageRequest defines the parameters for image generation
4242
type ImageRequest struct {
43+
ProjectID int `json:"projectId,omitempty"`
4344
Prompt string `json:"prompt"`
4445
Images []string `json:"images,omitempty"`
4546
Videos []string `json:"videos,omitempty"`
@@ -54,6 +55,7 @@ type ImageRequest struct {
5455

5556
// VideoRequest defines the parameters for video generation
5657
type VideoRequest struct {
58+
ProjectID int `json:"projectId,omitempty"`
5759
Prompt string `json:"prompt"`
5860
Images []string `json:"images,omitempty"`
5961
Videos []string `json:"videos,omitempty"`
@@ -67,6 +69,7 @@ type VideoRequest struct {
6769

6870
// AudioRequest defines the parameters for audio generation
6971
type AudioRequest struct {
72+
ProjectID int `json:"projectId,omitempty"`
7073
Prompt string `json:"prompt"`
7174
Images []string `json:"images,omitempty"`
7275
Videos []string `json:"videos,omitempty"`
@@ -156,7 +159,7 @@ func (s *Service) GenerateImage(req ImageRequest) (*AIResponse, error) {
156159
return nil, err
157160
}
158161

159-
content, err := s.processContent(resp.Data, resp.B64JSON, resp.URL, "image", ".png")
162+
content, err := s.processContent(req.ProjectID, resp.Data, resp.B64JSON, resp.URL, "image", ".png", database.AssetTypeImage)
160163
if err != nil {
161164
return nil, err
162165
}
@@ -190,7 +193,7 @@ func (s *Service) GenerateVideo(req VideoRequest) (*AIResponse, error) {
190193
return nil, err
191194
}
192195

193-
content, err := s.processContent(resp.Data, "", resp.URL, "video", ".mp4")
196+
content, err := s.processContent(req.ProjectID, resp.Data, "", resp.URL, "video", ".mp4", database.AssetTypeVideo)
194197
if err != nil {
195198
return nil, err
196199
}
@@ -227,7 +230,7 @@ func (s *Service) GenerateAudio(req AudioRequest) (*AIResponse, error) {
227230
// Usually audio is returned as bytes.
228231
// We might want to base64 encode it for the frontend or return a Blob URL if we could.
229232
// For now, let's assume valid JSON marshalling or handle it in specific response type
230-
content, err := s.processContent(resp.Data, "", "", "audio", ".mp3")
233+
content, err := s.processContent(req.ProjectID, resp.Data, "", "", "audio", ".mp3", database.AssetTypeAudio)
231234
if err != nil {
232235
return nil, err
233236
}
@@ -279,7 +282,7 @@ func (s *Service) ListModels(providerId *int) ([]aiservice.Model, error) {
279282
return client.ListModels(s.ctx)
280283
}
281284

282-
func (s *Service) processContent(data []byte, b64 string, url string, prefix string, ext string) (string, error) {
285+
func (s *Service) processContent(projectID int, data []byte, b64 string, url string, prefix string, ext string, assetType database.AssetType) (string, error) {
283286
var filename string
284287
var err error
285288

@@ -297,5 +300,21 @@ func (s *Service) processContent(data []byte, b64 string, url string, prefix str
297300
return "", fmt.Errorf("failed to save %s: %w", prefix, err)
298301
}
299302

303+
// Create asset in database if projectID is provided
304+
if projectID > 0 {
305+
_, err = database.CreateAsset(database.Asset{
306+
ProjectID: projectID,
307+
Type: assetType,
308+
Path: filename,
309+
})
310+
if err != nil {
311+
// Log error but don't fail the request (or maybe we should?)
312+
// For now, let's log to console and move on, or return error?
313+
// Better to log and continue, or return error if strict.
314+
// Let's print for now as we don't have a logger struct here.
315+
fmt.Printf("failed to create asset for project %d: %v\n", projectID, err)
316+
}
317+
}
318+
300319
return fmt.Sprintf("http://localhost:34116/%s", filename), nil
301320
}

binding/database/service.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package database
22

33
import (
44
db "firebringer/database"
5+
"fmt"
56
)
67

78
type Service struct{}
@@ -49,3 +50,20 @@ func (s *Service) DeleteProject(id int) error {
4950
func (s *Service) ListProjects() ([]db.Project, error) {
5051
return db.ListProjects()
5152
}
53+
54+
// ListAssets lists all assets for a project (pass 0 for all)
55+
func (s *Service) ListAssets(projectID int) ([]db.Asset, error) {
56+
assets, err := db.ListAssets(projectID)
57+
if err != nil {
58+
return nil, err
59+
}
60+
for i := range assets {
61+
assets[i].URL = fmt.Sprintf("http://localhost:34116/%s", assets[i].Path)
62+
}
63+
return assets, nil
64+
}
65+
66+
// DeleteAsset deletes an asset
67+
func (s *Service) DeleteAsset(id int) error {
68+
return db.DeleteAsset(id)
69+
}

database/db.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ func InitDB() error {
4343
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
4444
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
4545
);
46+
47+
CREATE TABLE IF NOT EXISTS assets (
48+
id INTEGER PRIMARY KEY AUTOINCREMENT,
49+
project_id INTEGER NOT NULL,
50+
type TEXT NOT NULL,
51+
path TEXT NOT NULL,
52+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
53+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
54+
FOREIGN KEY(project_id) REFERENCES projects(id)
55+
);
4656
`
4757
_, err = DB.Exec(schema)
4858
if err != nil {

database/models.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,22 @@ type Project struct {
3131
CreatedAt time.Time `db:"created_at" json:"createdAt"`
3232
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
3333
}
34+
35+
type AssetType string
36+
37+
const (
38+
AssetTypeImage AssetType = "image"
39+
AssetTypeVideo AssetType = "video"
40+
AssetTypeAudio AssetType = "audio"
41+
)
42+
43+
// Asset represents a stored item (image/video/audio) associated with a project/workflow
44+
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"`
52+
}

database/repository.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package database
33
import (
44
"database/sql"
55
"errors"
6+
"firebringer/storage"
67
)
78

89
// GetModelProviderByType retrieves a model provider configuration by its type (e.g., openai, gemini)
@@ -128,3 +129,71 @@ func ListProjects() ([]Project, error) {
128129
}
129130
return projects, nil
130131
}
132+
133+
// CreateAsset creates a new asset
134+
func CreateAsset(asset Asset) (*Asset, error) {
135+
// Insert
136+
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)
139+
`, asset)
140+
if err != nil {
141+
return nil, err
142+
}
143+
id, err := result.LastInsertId()
144+
if err != nil {
145+
return nil, err
146+
}
147+
asset.ID = int(id)
148+
return GetAsset(asset.ID)
149+
}
150+
151+
// GetAsset retrieves an asset by ID
152+
func GetAsset(id int) (*Asset, error) {
153+
var asset Asset
154+
err := DB.Get(&asset, "SELECT * FROM assets WHERE id = ?", id)
155+
if err != nil {
156+
if errors.Is(err, sql.ErrNoRows) {
157+
return nil, nil // Not found
158+
}
159+
return nil, err
160+
}
161+
return &asset, nil
162+
}
163+
164+
// ListAssets lists all assets for a project. If projectID is 0, lists all assets.
165+
func ListAssets(projectID int) ([]Asset, error) {
166+
var assets []Asset
167+
var err error
168+
if projectID == 0 {
169+
err = DB.Select(&assets, "SELECT * FROM assets ORDER BY created_at DESC")
170+
} else {
171+
err = DB.Select(&assets, "SELECT * FROM assets WHERE project_id = ? ORDER BY created_at DESC", projectID)
172+
}
173+
if err != nil {
174+
return nil, err
175+
}
176+
return assets, nil
177+
}
178+
179+
// DeleteAsset deletes an asset and its associated file
180+
func DeleteAsset(id int) error {
181+
// First, get the asset to obtain the file path
182+
asset, err := GetAsset(id)
183+
if err != nil {
184+
return err
185+
}
186+
if asset == nil {
187+
return errors.New("asset not found")
188+
}
189+
190+
// Delete the database record
191+
_, err = DB.Exec("DELETE FROM assets WHERE id = ?", id)
192+
if err != nil {
193+
return err
194+
}
195+
196+
_ = storage.DeleteGeneratedContent(asset.Path)
197+
198+
return nil
199+
}

frontend/src/App.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
22
import { ProjectList } from "@/components/project-list";
33
import { CanvasView } from "@/components/canvas-view";
4-
import { AppSidebar } from "@/components/app-sidebar";
4+
import { AssetLibrary } from "@/components/asset-library";
5+
import { AppSidebar, type AppView } from "@/components/app-sidebar";
56
import { SettingsDialog } from "@/components/settings/settings-dialog";
67
import { useState } from "react";
78
import { database } from "../wailsjs/go/models";
@@ -10,6 +11,7 @@ export default function App() {
1011
const [selectedProject, setSelectedProject] = useState<database.Project | null>(
1112
null
1213
);
14+
const [activeView, setActiveView] = useState<AppView>("projects");
1315
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
1416

1517
const handleProjectClick = (project: database.Project) => {
@@ -38,9 +40,17 @@ export default function App() {
3840
style={{ "--wails-draggable": "drag" } as React.CSSProperties}
3941
></div>
4042
<SidebarProvider>
41-
<AppSidebar onSettingsClick={() => setIsSettingsOpen(true)} />
42-
<SidebarInset className="m-2 rounded-2xl z-10 border">
43-
<ProjectList onProjectClick={handleProjectClick} />
43+
<AppSidebar
44+
onSettingsClick={() => setIsSettingsOpen(true)}
45+
activeView={activeView}
46+
onViewChange={setActiveView}
47+
/>
48+
<SidebarInset className="m-2 rounded-2xl z-10 border bg-background overflow-hidden relative">
49+
{activeView === "projects" ? (
50+
<ProjectList onProjectClick={handleProjectClick} />
51+
) : (
52+
<AssetLibrary />
53+
)}
4454
</SidebarInset>
4555
</SidebarProvider>
4656
<SettingsDialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen} />

frontend/src/components/app-sidebar.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,17 @@ import { NavUser } from "@/components/nav-user";
1414
import { Home, Image, Settings, Telescope } from "lucide-react";
1515
import { toast } from "sonner"
1616

17+
export type AppView = "projects" | "assets";
1718

18-
export function AppSidebar({ onSettingsClick }: { onSettingsClick: () => void }) {
19+
export function AppSidebar({
20+
activeView,
21+
onViewChange,
22+
onSettingsClick,
23+
}: {
24+
activeView: AppView;
25+
onViewChange: (view: AppView) => void;
26+
onSettingsClick: () => void;
27+
}) {
1928

2029
function wip() {
2130
toast("👷 Work in progress")
@@ -33,13 +42,19 @@ export function AppSidebar({ onSettingsClick }: { onSettingsClick: () => void })
3342
<SidebarGroupContent>
3443
<SidebarMenu>
3544
<SidebarMenuItem>
36-
<SidebarMenuButton isActive>
45+
<SidebarMenuButton
46+
isActive={activeView === "projects"}
47+
onClick={() => onViewChange("projects")}
48+
>
3749
<Home />
3850
<span>项目</span>
3951
</SidebarMenuButton>
4052
</SidebarMenuItem>
4153
<SidebarMenuItem>
42-
<SidebarMenuButton onClick={() => wip()}>
54+
<SidebarMenuButton
55+
isActive={activeView === "assets"}
56+
onClick={() => onViewChange("assets")}
57+
>
4358
<Image />
4459
<span>素材库</span>
4560
</SidebarMenuButton>

0 commit comments

Comments
 (0)