Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,6 @@ templates/*/.env.local
templates/*/.env.*.local
test-reports/
*.tsbuildinfo

# tsup transient bundled config artifacts
tsup.config.bundled_*.mjs
20 changes: 20 additions & 0 deletions templates/assets/actions/list-draft-assets.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import action from "./list-draft-assets.js";

describe("list-draft-assets schema", () => {
it("defaults to no filters when given an empty object", () => {
const parsed = action.schema.parse({});
expect(parsed.libraryId).toBeUndefined();
expect(parsed.limit).toBeUndefined();
});

it("coerces a numeric string limit", () => {
const parsed = action.schema.parse({ limit: "5" });
expect(parsed.limit).toBe(5);
});

it("rejects an out-of-range limit", () => {
expect(() => action.schema.parse({ limit: 0 })).toThrow();
expect(() => action.schema.parse({ limit: 999 })).toThrow();
});
});
49 changes: 49 additions & 0 deletions templates/assets/actions/list-draft-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { defineAction } from "@agent-native/core";
import { z } from "zod";
import { and, desc, eq, inArray, isNull } from "drizzle-orm";
import { accessFilter } from "@agent-native/core/sharing";
import { getDb, schema } from "../server/db/index.js";
import { serializeAsset } from "./_helpers.js";

export default defineAction({
description:
"List unsaved draft generations (generated candidate assets) across accessible libraries, newest first.",
schema: z.object({
libraryId: z.string().optional(),
limit: z.coerce.number().int().min(1).max(500).optional(),
}),
http: { method: "GET" },
Comment on lines +8 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added actions/list-draft-assets.spec.ts covering schema defaults, limit coercion, and out-of-range rejection (mirroring list-assets.spec.ts). Passes locally.

readOnly: true,
run: async ({ libraryId, limit }) => {
const db = getDb();
const libraryFilters = [
accessFilter(schema.assetLibraries, schema.assetLibraryShares),
isNull(schema.assetLibraries.archivedAt),
];
if (libraryId) libraryFilters.push(eq(schema.assetLibraries.id, libraryId));
const accessibleLibraries = await db
.select({ id: schema.assetLibraries.id })
.from(schema.assetLibraries)
.where(and(...libraryFilters));
const libraryIds = accessibleLibraries.map((row) => row.id);
if (!libraryIds.length) return { count: 0, assets: [] };

const rows = await db
.select()
.from(schema.assets)
.where(
and(
inArray(schema.assets.libraryId, libraryIds),
eq(schema.assets.role, "generated"),
eq(schema.assets.status, "candidate"),
),
)
.orderBy(desc(schema.assets.createdAt))
.limit(limit ?? 50);

return {
count: rows.length,
assets: rows.map((row) => serializeAsset(row)),
};
},
});
95 changes: 95 additions & 0 deletions templates/assets/app/components/create/RecentDraftsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Link } from "react-router";
import { IconArrowUpRight, IconPhoto } from "@tabler/icons-react";
import { useActionQuery } from "@agent-native/core/client";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";

type DraftAsset = {
id: string;
title?: string | null;
prompt?: string | null;
mediaType?: string | null;
mimeType?: string | null;
thumbnailUrl?: string | null;
previewUrl?: string | null;
url?: string | null;
};

const RECENT_DRAFTS_LIMIT = 5;

export function RecentDraftsSection() {
const { data, isLoading } = useActionQuery("list-draft-assets", {
limit: RECENT_DRAFTS_LIMIT,
});
const drafts = ((data as any)?.assets ?? []) as DraftAsset[];

if (!isLoading && drafts.length === 0) return null;

return (
<section className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 className="text-sm font-semibold text-foreground">Recent Drafts</h2>
<Button asChild variant="outline" size="sm">
<Link to="/library?tab=drafts">
View all drafts
<IconArrowUpRight size={15} className="ml-1.5" />
</Link>
</Button>
</div>

<div className="grid grid-cols-3 gap-3 sm:grid-cols-5">
{isLoading
? Array.from({ length: RECENT_DRAFTS_LIMIT }).map((_, index) => (
<Skeleton key={index} className="aspect-square rounded-lg" />
))
: drafts.map((draft) => (
<Link
key={draft.id}
to={`/asset/${encodeURIComponent(draft.id)}`}
title={draft.title || draft.prompt || "Draft asset"}
className="group block overflow-hidden rounded-lg border border-border bg-card shadow-sm transition hover:border-primary/60 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<div className="aspect-square bg-muted">
<DraftThumbnail draft={draft} />
</div>
</Link>
))}
</div>
</section>
);
}

function DraftThumbnail({ draft }: { draft: DraftAsset }) {
const isVideo =
draft.mediaType === "video" || draft.mimeType?.startsWith("video/");
const source = draft.thumbnailUrl ?? draft.previewUrl ?? draft.url ?? "";

if (isVideo && !draft.thumbnailUrl) {
return (
<video
src={draft.previewUrl ?? draft.url ?? undefined}
muted
playsInline
preload="metadata"
className="h-full w-full object-cover transition group-hover:scale-[1.02]"
/>
Comment thread
Copilot marked this conversation as resolved.
);
}

if (!source) {
return (
<div className="flex h-full w-full items-center justify-center text-muted-foreground">
<IconPhoto className="size-5" />
</div>
);
}

return (
<img
src={source}
alt={draft.title ?? draft.prompt ?? "Draft asset"}
loading="lazy"
className="h-full w-full object-cover transition group-hover:scale-[1.02]"
/>
);
}
28 changes: 20 additions & 8 deletions templates/assets/app/i18n-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ const brandKitDetailEnUS = {
removeFromReferences: "Remove from References",
close: "Close",
generated: "Generated",
drafts: "Drafts",
slot: "slot",
noAssetsMatch: "No assets match this view",
noAssetsMatchDescription:
Expand Down Expand Up @@ -399,6 +400,9 @@ const assetPickerEnUS = {
searchMedia: "Search {{media}}s",
noMatchingAssets: "No matching {{media}} assets in this library.",
noAssetsYet: "No {{media}} assets in this library yet.",
allLibraries: "All libraries",
noMatchingDrafts: "No matching drafts.",
noDrafts: "No drafts yet.",
openAsset: "Open {{title}}",
copyAsset: "Copy {{title}}",
copyToClipboard: "Copy to clipboard",
Expand Down Expand Up @@ -1453,7 +1457,8 @@ const brandKitDetailHiIN = {
dismissFailedSlotsDescription:
"पैनल से प्रत्येक विफल स्लॉट को हटा देता है। सफल उम्मीदवार रुके रहते हैं.",
dismissFailed: "ख़ारिज करना विफल रहा",
dismissSlotDescription: "इस असफल स्लॉट को लाइव उम्मीदवारों के पैनल से हटा देता है।",
dismissSlotDescription:
"इस असफल स्लॉट को लाइव उम्मीदवारों के पैनल से हटा देता है।",
dismissThisCandidate: "इस उम्मीदवार को बर्खास्त करें?",
dismissThisSlot: "इस स्लॉट को ख़ारिज करें?",
dismissedCandidate: "बर्खास्त उम्मीदवार.",
Expand All @@ -1479,7 +1484,8 @@ const brandKitDetailHiIN = {
"मूल चैट थ्रेड के बिना किसी उम्मीदवार को जारी रखने के लिए डिजाइनरों के लिए साझा संदर्भ।",
handoffSessions: "हैंडऑफ़ सत्र",
imageCandidates: "छवि उम्मीदवार",
imagePromptPlaceholder: "कोल्ड-स्टार्ट विलंबता के बारे में एक लेख के लिए ब्लॉग हीरो",
imagePromptPlaceholder:
"कोल्ड-स्टार्ट विलंबता के बारे में एक लेख के लिए ब्लॉग हीरो",
libraryEyebrow:
"इस फ़िल्टर किए गए दृश्य में संपत्तियाँ सहेजी गईं। उन्हें चिन्हित करें जो भावी पीढ़ियों का मार्गदर्शन करें।",
liveCandidatesActions: "लाइव उम्मीदवारों के कार्य",
Expand All @@ -1494,7 +1500,8 @@ const brandKitDetailHiIN = {
newGenerationPresetDescription:
"बार-बार छवि कार्य के लिए आउटपुट स्वरूप, पहलू अनुपात और पाठ नियमों को सहेजें।",
newGenerationPreset: "नई पीढ़ी पूर्व निर्धारित",
noAssetsMatchDescription: "सभी संपत्तियाँ, एक अलग फ़ोल्डर, या एक विस्तृत खोज आज़माएँ।",
noAssetsMatchDescription:
"सभी संपत्तियाँ, एक अलग फ़ोल्डर, या एक विस्तृत खोज आज़माएँ।",
noAssetsMatch: "कोई भी संपत्ति इस दृश्य से मेल नहीं खाती",
noAssetsToShow: "दिखाने के लिए कोई संपत्ति नहीं.",
noFeedbackYet: "अभी तक कोई प्रतिक्रिया नहीं।",
Expand All @@ -1512,7 +1519,8 @@ const brandKitDetailHiIN = {
openChat: "चैट खोलें",
previewUnavailable: "पूर्वावलोकन अनुपलब्ध",
privateCopyCreated: "निजी ब्रांड किट की प्रतिलिपि बनाई गई",
processingPreviews: "पूर्वावलोकन संसाधित करना और उन्हें ब्रांड किट में सहेजना।",
processingPreviews:
"पूर्वावलोकन संसाधित करना और उन्हें ब्रांड किट में सहेजना।",
promptTemplate: "शीघ्र टेम्पलेट",
readyCandidate: "तैयार उम्मीदवार",
referencesEyebrow: "Assets वर्तमान में पीढ़ी संदर्भ के लिए चिह्नित है।",
Expand All @@ -1535,7 +1543,8 @@ const brandKitDetailHiIN = {
"सर्वर अभी भी इन परिसंपत्तियों को सहेजना समाप्त कर सकता है। हम इस ब्रांड किट की जाँच करते रहेंगे।",
uploadTakingLonger: "अपलोड में अपेक्षा से अधिक समय लग रहा है.",
videoCandidate: "वीडियो उम्मीदवार",
videoPromptPlaceholder: "धीमे कैमरा पुश-इन के साथ आठ सेकंड का उत्पाद प्रकट होता है",
videoPromptPlaceholder:
"धीमे कैमरा पुश-इन के साथ आठ सेकंड का उत्पाद प्रकट होता है",
viewDetails: "विवरण देखें",
} satisfies Partial<Messages["brandKitDetail"]>;

Expand Down Expand Up @@ -4440,7 +4449,8 @@ export const messagesByLocale = {
"S3, R2, Spaces, Tigris, MinIO या कोई compatible provider उपयोग करें.",
available: "उपलब्ध",
connecting: "कनेक्ट हो रहा है",
noManualOptions: "इस item के लिए कोई manual setup options उपलब्ध नहीं हैं.",
noManualOptions:
"इस item के लिए कोई manual setup options उपलब्ध नहीं हैं.",
builderManaged: "Builder managed image generation संभाल रहा है.",
providerConfigured: "{{providers}} configured.",

Expand Down Expand Up @@ -4480,7 +4490,8 @@ export const messagesByLocale = {
modelBestQuality: "सर्वश्रेष्ठ गुणवत्ता",
modelFast: "तेज़",
emptyState: "Assets से पूछें कि क्या बनाना है।",
composerPlaceholder: "एसेट का वर्णन करें - + से images या text context जोड़ें",
composerPlaceholder:
"एसेट का वर्णन करें - + से images या text context जोड़ें",
heroTitle: "हम कौन सा एसेट बनाएं?",
heroDescription:
"hero image, product reveal, reference edit या खोजने की दिशा से शुरू करें।",
Expand Down Expand Up @@ -4611,7 +4622,8 @@ export const messagesByLocale = {
saveSettings: "حفظ الإعدادات",
setupDescription: "أساسيان: التوليد والتخزين الدائم.",
setupTitle: "إعداد Assets",
storageNeedsSetup: "أضف مساحة تخزين متوافقة مع S3 لأصول الإنتاج والصادرات.",
storageNeedsSetup:
"أضف مساحة تخزين متوافقة مع S3 لأصول الإنتاج والصادرات.",
storageReady:
"تتمتع النسخ الأصلية والصور المصغرة ومقاطع الفيديو والصادرات بمكانة متينة.",
},
Expand Down
5 changes: 4 additions & 1 deletion templates/assets/app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { IconPhoto, IconSparkles, IconVideo } from "@tabler/icons-react";
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router";

import { RecentDraftsSection } from "@/components/create/RecentDraftsSection";
import { ASSETS_CHAT_STORAGE_KEY } from "@/lib/chat";

// The composer's model picker shows the chat LLM (Claude/OpenAI/Gemini). The
Expand Down Expand Up @@ -180,6 +180,9 @@ export default function CreatePage() {
</button>
))}
</div>
<div className="mt-8 w-full text-left">
<RecentDraftsSection />
</div>
</div>
}
/>
Expand Down
Loading
Loading