Enhance canvas functionality by adding media preview capabilities and image upload handling. Introduce compressed image previews during uploads, improve media library integration, and implement retry logic for bridge edge creation. Update dashboard to display media previews and optimize image node handling.

This commit is contained in:
Matthias
2026-04-08 20:44:31 +02:00
parent a7eb2bc99c
commit b7f24223f2
43 changed files with 4064 additions and 148 deletions

View File

@@ -0,0 +1,92 @@
import { describe, expect, it } from "vitest";
import {
buildGraphSnapshot,
resolveRenderPreviewInputFromGraph,
} from "@/lib/canvas-render-preview";
describe("resolveRenderPreviewInputFromGraph", () => {
it("includes crop in collected pipeline steps", () => {
const graph = buildGraphSnapshot(
[
{
id: "image-1",
type: "image",
data: { url: "https://cdn.example.com/source.png" },
},
{
id: "crop-1",
type: "crop",
data: { cropRect: { x: 0.1, y: 0.2, width: 0.4, height: 0.3 } },
},
{
id: "render-1",
type: "render",
data: {},
},
],
[
{ source: "image-1", target: "crop-1" },
{ source: "crop-1", target: "render-1" },
],
);
const preview = resolveRenderPreviewInputFromGraph({
nodeId: "render-1",
graph,
});
expect(preview.steps).toEqual([
{
nodeId: "crop-1",
type: "crop",
params: { cropRect: { x: 0.1, y: 0.2, width: 0.4, height: 0.3 } },
},
]);
});
it("derives proxied pexels video source URL from mp4Url", () => {
const mp4Url = "https://player.pexels.com/videos/example.mp4";
const graph = buildGraphSnapshot(
[
{
id: "video-1",
type: "video",
data: { mp4Url },
},
{
id: "render-1",
type: "render",
data: {},
},
],
[{ source: "video-1", target: "render-1" }],
);
const preview = resolveRenderPreviewInputFromGraph({ nodeId: "render-1", graph });
expect(preview.sourceUrl).toBe(`/api/pexels-video?u=${encodeURIComponent(mp4Url)}`);
});
it("uses ai-video data.url as source URL when available", () => {
const graph = buildGraphSnapshot(
[
{
id: "ai-video-1",
type: "ai-video",
data: { url: "https://cdn.example.com/generated-video.mp4" },
},
{
id: "render-1",
type: "render",
data: {},
},
],
[{ source: "ai-video-1", target: "render-1" }],
);
const preview = resolveRenderPreviewInputFromGraph({ nodeId: "render-1", graph });
expect(preview.sourceUrl).toBe("https://cdn.example.com/generated-video.mp4");
});
});

View File

@@ -4,15 +4,20 @@ import { beforeEach, describe, expect, it } from "vitest";
import {
clearDashboardSnapshotCache,
emitDashboardSnapshotCacheInvalidationSignal,
invalidateDashboardSnapshotForLastSignedInUser,
readDashboardSnapshotCache,
writeDashboardSnapshotCache,
} from "@/lib/dashboard-snapshot-cache";
const USER_ID = "user-cache-test";
const LAST_DASHBOARD_USER_KEY = "ls-last-dashboard-user";
const INVALIDATION_SIGNAL_KEY = "lemonspace.dashboard:snapshot:invalidate:v1";
describe("dashboard snapshot cache", () => {
beforeEach(() => {
const data = new Map<string, string>();
const sessionData = new Map<string, string>();
const localStorageMock = {
getItem: (key: string) => data.get(key) ?? null,
setItem: (key: string, value: string) => {
@@ -22,11 +27,24 @@ describe("dashboard snapshot cache", () => {
data.delete(key);
},
};
const sessionStorageMock = {
getItem: (key: string) => sessionData.get(key) ?? null,
setItem: (key: string, value: string) => {
sessionData.set(key, value);
},
removeItem: (key: string) => {
sessionData.delete(key);
},
};
Object.defineProperty(window, "localStorage", {
value: localStorageMock,
configurable: true,
});
Object.defineProperty(window, "sessionStorage", {
value: sessionStorageMock,
configurable: true,
});
clearDashboardSnapshotCache(USER_ID);
});
@@ -70,4 +88,26 @@ describe("dashboard snapshot cache", () => {
expect(readDashboardSnapshotCache(USER_ID)).toBeNull();
});
it("invalidates cache for the last signed-in user", () => {
writeDashboardSnapshotCache(USER_ID, { generatedAt: 1 });
window.sessionStorage.setItem(LAST_DASHBOARD_USER_KEY, USER_ID);
invalidateDashboardSnapshotForLastSignedInUser();
expect(readDashboardSnapshotCache(USER_ID)).toBeNull();
expect(window.sessionStorage.getItem(LAST_DASHBOARD_USER_KEY)).toBe(USER_ID);
});
it("does not fail if no last dashboard user exists", () => {
expect(() => invalidateDashboardSnapshotForLastSignedInUser()).not.toThrow();
});
it("emits a localStorage invalidation signal", () => {
emitDashboardSnapshotCacheInvalidationSignal();
const signal = window.localStorage.getItem(INVALIDATION_SIGNAL_KEY);
expect(typeof signal).toBe("string");
expect(Number(signal)).toBeGreaterThan(0);
});
});