@agent-native/core 0.80.10 → 0.80.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  6. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  7. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  8. package/corpus/templates/assets/AGENTS.md +4 -0
  9. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  10. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  11. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  12. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  13. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  14. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  15. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  16. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  17. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  18. package/corpus/templates/assets/app/routes/library.tsx +145 -38
  19. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  20. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  21. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  22. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  23. package/corpus/templates/assets/shared/api.ts +4 -0
  24. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  25. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  26. package/corpus/templates/design/actions/apply-component-prop-edit.ts +13 -5
  27. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  28. package/corpus/templates/design/actions/apply-motion-edit.ts +9 -46
  29. package/corpus/templates/design/actions/delete-design.ts +20 -0
  30. package/corpus/templates/design/actions/get-component-details.ts +16 -7
  31. package/corpus/templates/design/actions/index-design-tokens.ts +8 -4
  32. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  33. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  34. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  35. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  36. package/corpus/templates/design/actions/run-design-audit.ts +21 -6
  37. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +139 -14
  38. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +20 -3
  39. package/corpus/templates/design/app/components/design/EditPanel.tsx +428 -31
  40. package/corpus/templates/design/app/components/design/LayersPanel.tsx +110 -30
  41. package/corpus/templates/design/app/components/design/MotionDock.tsx +9 -0
  42. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1 -1
  43. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +12 -5
  44. package/corpus/templates/design/app/components/design/StatesPanel.tsx +4 -2
  45. package/corpus/templates/design/app/components/design/TokensPanel.tsx +13 -8
  46. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  47. package/corpus/templates/design/app/i18n-data.ts +11 -0
  48. package/corpus/templates/design/app/pages/DesignEditor.tsx +684 -179
  49. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  50. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  51. package/corpus/templates/design/shared/component-model.ts +35 -0
  52. package/corpus/templates/design/shared/motion-compiler.ts +79 -8
  53. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  54. package/dist/agent/production-agent.d.ts.map +1 -1
  55. package/dist/agent/production-agent.js +94 -10
  56. package/dist/agent/production-agent.js.map +1 -1
  57. package/dist/client/AssistantChat.d.ts +14 -0
  58. package/dist/client/AssistantChat.d.ts.map +1 -1
  59. package/dist/client/AssistantChat.js +115 -22
  60. package/dist/client/AssistantChat.js.map +1 -1
  61. package/dist/client/sse-event-processor.d.ts.map +1 -1
  62. package/dist/client/sse-event-processor.js +10 -0
  63. package/dist/client/sse-event-processor.js.map +1 -1
  64. package/dist/observability/routes.d.ts +3 -3
  65. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  66. package/package.json +1 -1
@@ -21,6 +21,7 @@ import {
21
21
  DEFAULT_GENERATION_REFERENCE_LIMIT,
22
22
  generateWithManagedImageProvider,
23
23
  isImageGenerationSetupError,
24
+ resolveImageModelForRequest,
24
25
  selectReferences,
25
26
  } from "../server/lib/generation.js";
26
27
  import { compositeLogo } from "../server/lib/image-processing.js";
@@ -35,7 +36,6 @@ import {
35
36
  IMAGE_SIZES,
36
37
  STYLE_STRENGTHS,
37
38
  type ImageCategory,
38
- type ImageModel,
39
39
  type ImageQualityTier,
40
40
  type StyleBrief,
41
41
  } from "../shared/api.js";
@@ -44,22 +44,11 @@ import {
44
44
  serializeAsset,
45
45
  } from "./_helpers.js";
46
46
  import { readImageModelDefault } from "./_image-model-default.js";
47
+ import { withToolActivity } from "./_tool-activity.js";
47
48
  import { upsertVariantSlot, wasVariantSlotDismissed } from "./variant-slots.js";
48
49
 
49
50
  const IMAGE_GENERATION_TOOL_TIMEOUT_MS = 12 * 60_000;
50
51
 
51
- function resolveModelForTier(
52
- tier: ImageQualityTier | undefined,
53
- category: ImageCategory | undefined,
54
- ): ImageModel | undefined {
55
- if (!tier) return undefined;
56
- if (tier === "fast") return "gemini-3.1-flash-image";
57
- if (tier === "best") return "gemini-3-pro-image";
58
- return ["hero", "landing", "logo", "campaign"].includes(category ?? "")
59
- ? "gemini-3-pro-image"
60
- : "gemini-3.1-flash-image";
61
- }
62
-
63
52
  export default defineAction({
64
53
  description:
65
54
  "Generate one brand-consistent image from a brand kit/library. This is synchronous for images and returns the final asset with preview/download/embed URLs. Use @brand-kit mentions as libraryId and @preset mentions as presetId when present. Use generate-image-batch for multiple independent slots; do not poll image runs after this action returns.",
@@ -71,11 +60,38 @@ export default defineAction({
71
60
  "Brand kit/library ID. Pass the refId from a brand-kit @mention, or choose a kit from view-screen/list-libraries.",
72
61
  ),
73
62
  collectionId: z.string().optional(),
74
- presetId: z.string().optional(),
63
+ presetId: z
64
+ .string()
65
+ .optional()
66
+ .describe(
67
+ "Generation preset ID (from a @preset mention or list-generation-presets). A preset already defines aspectRatio, imageSize, model, tier, and category. When you set presetId, OMIT those args so the preset's values are used; only pass one of them when the user explicitly asks for a value that differs from the preset.",
68
+ ),
75
69
  sessionId: z.string().optional(),
76
70
  prompt: z.string().min(1),
77
- aspectRatio: z.enum(ASPECT_RATIOS).optional(),
78
- imageSize: z.enum(IMAGE_SIZES).optional(),
71
+ embeddedText: z
72
+ .string()
73
+ .optional()
74
+ .describe(
75
+ "Exact words to render inside the image, spelled exactly. When set, the image is allowed to contain this text; when omitted, the image avoids embedded text.",
76
+ ),
77
+ textPlacement: z
78
+ .string()
79
+ .optional()
80
+ .describe(
81
+ "Where/how the embedded text should appear, e.g. 'centered headline', 'lower-left label'.",
82
+ ),
83
+ aspectRatio: z
84
+ .enum(ASPECT_RATIOS)
85
+ .optional()
86
+ .describe(
87
+ "Image aspect ratio. When a presetId is set, omit this — the preset's aspect ratio is used. Pass a value only when there is no preset, or when the user explicitly asks for a ratio different from the preset's.",
88
+ ),
89
+ imageSize: z
90
+ .enum(IMAGE_SIZES)
91
+ .optional()
92
+ .describe(
93
+ "Output resolution tier. When a presetId is set, omit this — the preset's size is used unless the user explicitly requests a different one.",
94
+ ),
79
95
  model: z.enum(IMAGE_MODELS).optional(),
80
96
  tier: z.enum(IMAGE_QUALITY_TIERS).optional(),
81
97
  intent: z.enum(GENERATION_INTENTS).default("generate"),
@@ -251,11 +267,15 @@ export default defineAction({
251
267
  const category = (args.categories?.[0] ??
252
268
  preset?.category ??
253
269
  collection?.category) as ImageCategory | undefined;
254
- const resolvedModel = (args.model ??
255
- imageModelDefault ??
256
- resolveModelForTier(resolvedTier, category) ??
257
- preset?.model ??
258
- "gemini-3.1-flash-image") as (typeof IMAGE_MODELS)[number];
270
+ const resolvedModel = resolveImageModelForRequest({
271
+ explicitModel: args.model,
272
+ imageModelDefault,
273
+ explicitTier: args.tier,
274
+ resolvedTier,
275
+ category,
276
+ presetModel: preset?.model as (typeof IMAGE_MODELS)[number] | undefined,
277
+ embeddedText: args.embeddedText,
278
+ }) as (typeof IMAGE_MODELS)[number];
259
279
  const resolvedCategories =
260
280
  args.categories ??
261
281
  (preset?.category ? ([preset.category] as any) : undefined);
@@ -297,6 +317,8 @@ export default defineAction({
297
317
  .filter((item) => item?.trim())
298
318
  .join("\n\n"),
299
319
  prompt: promptForRun,
320
+ embeddedText: args.embeddedText,
321
+ textPlacement: args.textPlacement,
300
322
  referenceCount: references.length,
301
323
  includeLogo: args.includeLogo,
302
324
  aspectRatio: resolvedAspectRatio,
@@ -346,6 +368,8 @@ export default defineAction({
346
368
  collectionId: resolvedCollectionId ?? null,
347
369
  presetId: preset?.id ?? null,
348
370
  sessionId: session?.id ?? null,
371
+ embeddedText: args.embeddedText ?? null,
372
+ textPlacement: args.textPlacement ?? null,
349
373
  customInstructions: library.customInstructions ?? "",
350
374
  };
351
375
  const dismissibleSlot = args.dismissible !== false && Boolean(slotId);
@@ -357,6 +381,8 @@ export default defineAction({
357
381
  dismissible: dismissibleSlot,
358
382
  sourceAssetId: args.sourceAssetId,
359
383
  subjectAssetId: args.subjectAssetId,
384
+ embeddedText: args.embeddedText,
385
+ textPlacement: args.textPlacement,
360
386
  intent: args.intent,
361
387
  styleStrength: args.styleStrength,
362
388
  tier: resolvedTier,
@@ -404,22 +430,36 @@ export default defineAction({
404
430
  });
405
431
 
406
432
  try {
407
- const generated = await generateWithManagedImageProvider({
408
- prompt: promptForRun,
409
- compiledPrompt,
410
- references,
411
- model: resolvedModel,
412
- aspectRatio: resolvedAspectRatio,
413
- imageSize: resolvedImageSize,
414
- groundingMode: args.groundingMode,
415
- intent: args.intent,
416
- styleStrength: args.styleStrength,
417
- runId,
418
- libraryId: args.libraryId,
419
- collectionId: resolvedCollectionId ?? null,
420
- source: args.source,
421
- callerAppId: args.callerAppId,
422
- });
433
+ const generated = await withToolActivity(
434
+ context,
435
+ {
436
+ label: "Generating image.",
437
+ ongoingLabel: "Still generating image.",
438
+ // Intentionally no explicit `tool`: withToolActivity falls back to
439
+ // context.actionName, which is the ACTUAL dispatched tool. When this
440
+ // action is invoked directly that is "generate-image"; when it runs as
441
+ // a sub-step of generate-image-batch / rerun-generation-run it is the
442
+ // parent tool, so the activity event matches the real tool_start card
443
+ // instead of spawning an orphan "generate-image" activity card.
444
+ },
445
+ () =>
446
+ generateWithManagedImageProvider({
447
+ prompt: promptForRun,
448
+ compiledPrompt,
449
+ references,
450
+ model: resolvedModel,
451
+ aspectRatio: resolvedAspectRatio,
452
+ imageSize: resolvedImageSize,
453
+ groundingMode: args.groundingMode,
454
+ intent: args.intent,
455
+ styleStrength: args.styleStrength,
456
+ runId,
457
+ libraryId: args.libraryId,
458
+ collectionId: resolvedCollectionId ?? null,
459
+ source: args.source,
460
+ callerAppId: args.callerAppId,
461
+ }),
462
+ );
423
463
  await deleteAppState("image-generation-setup").catch(() => {});
424
464
  let image = generated.image;
425
465
  let mimeType = generated.mimeType;
@@ -468,37 +508,48 @@ export default defineAction({
468
508
  Artifacts: [],
469
509
  };
470
510
  }
471
- const asset = await createAssetFromBuffer({
472
- libraryId: args.libraryId,
473
- collectionId: resolvedCollectionId ?? null,
474
- buffer: image,
475
- mimeType,
476
- role: "generated",
477
- status: "candidate",
478
- prompt: args.prompt,
479
- model: generated.model,
480
- aspectRatio: resolvedAspectRatio,
481
- imageSize: resolvedImageSize,
482
- generationRunId: runId,
483
- metadata: {
484
- provider: generated.provider,
485
- compiledPrompt,
486
- referenceAssetIds: references.map((ref) => ref.id),
487
- sourceAssetId: args.sourceAssetId,
488
- subjectAssetId: args.subjectAssetId,
489
- intent: args.intent,
490
- styleStrength: args.styleStrength,
491
- tier: resolvedTier,
492
- includeLogo: args.includeLogo,
493
- presetId: preset?.id,
494
- sessionId: session?.id,
495
- generated: true,
496
- sourceUrl: generated.sourceUrl,
497
- providerGenerationId: generated.providerGenerationId,
498
- creditsCharged: generated.creditsCharged,
511
+ const asset = await withToolActivity(
512
+ context,
513
+ {
514
+ label: "Saving generated image.",
515
+ ongoingLabel: "Still saving generated image.",
516
+ // See above: omit `tool` so the activity is tagged with the real
517
+ // dispatched tool (context.actionName) rather than a hardcoded
518
+ // "generate-image" that orphans under batch/rerun.
499
519
  },
500
- category,
501
- });
520
+ () =>
521
+ createAssetFromBuffer({
522
+ libraryId: args.libraryId,
523
+ collectionId: resolvedCollectionId ?? null,
524
+ buffer: image,
525
+ mimeType,
526
+ role: "generated",
527
+ status: "candidate",
528
+ prompt: args.prompt,
529
+ model: generated.model,
530
+ aspectRatio: resolvedAspectRatio,
531
+ imageSize: resolvedImageSize,
532
+ generationRunId: runId,
533
+ metadata: {
534
+ provider: generated.provider,
535
+ compiledPrompt,
536
+ referenceAssetIds: references.map((ref) => ref.id),
537
+ sourceAssetId: args.sourceAssetId,
538
+ subjectAssetId: args.subjectAssetId,
539
+ intent: args.intent,
540
+ styleStrength: args.styleStrength,
541
+ tier: resolvedTier,
542
+ includeLogo: args.includeLogo,
543
+ presetId: preset?.id,
544
+ sessionId: session?.id,
545
+ generated: true,
546
+ sourceUrl: generated.sourceUrl,
547
+ providerGenerationId: generated.providerGenerationId,
548
+ creditsCharged: generated.creditsCharged,
549
+ },
550
+ category,
551
+ }),
552
+ );
502
553
  if (session) {
503
554
  const itemCreatedAt = nowIso();
504
555
  await db.insert(schema.assetGenerationSessionItems).values({
@@ -0,0 +1,50 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { and, desc, eq, inArray, isNull } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import { serializeAsset } from "./_helpers.js";
8
+
9
+ export default defineAction({
10
+ description:
11
+ "List unsaved draft generations (generated candidate assets) across accessible libraries, newest first.",
12
+ schema: z.object({
13
+ libraryId: z.string().optional(),
14
+ limit: z.coerce.number().int().min(1).max(500).optional(),
15
+ }),
16
+ http: { method: "GET" },
17
+ readOnly: true,
18
+ run: async ({ libraryId, limit }) => {
19
+ const db = getDb();
20
+ const libraryFilters = [
21
+ accessFilter(schema.assetLibraries, schema.assetLibraryShares),
22
+ isNull(schema.assetLibraries.archivedAt),
23
+ ];
24
+ if (libraryId) libraryFilters.push(eq(schema.assetLibraries.id, libraryId));
25
+ const accessibleLibraries = await db
26
+ .select({ id: schema.assetLibraries.id })
27
+ .from(schema.assetLibraries)
28
+ .where(and(...libraryFilters));
29
+ const libraryIds = accessibleLibraries.map((row) => row.id);
30
+ if (!libraryIds.length) return { count: 0, assets: [] };
31
+
32
+ const rows = await db
33
+ .select()
34
+ .from(schema.assets)
35
+ .where(
36
+ and(
37
+ inArray(schema.assets.libraryId, libraryIds),
38
+ eq(schema.assets.role, "generated"),
39
+ eq(schema.assets.status, "candidate"),
40
+ ),
41
+ )
42
+ .orderBy(desc(schema.assets.createdAt))
43
+ .limit(limit ?? 50);
44
+
45
+ return {
46
+ count: rows.length,
47
+ assets: rows.map((row) => serializeAsset(row)),
48
+ };
49
+ },
50
+ });
@@ -1,4 +1,5 @@
1
1
  import { defineAction } from "@agent-native/core";
2
+ import type { ActionRunContext } from "@agent-native/core/action";
2
3
  import { assertAccess } from "@agent-native/core/sharing";
3
4
  import { eq } from "drizzle-orm";
4
5
  import { z } from "zod";
@@ -30,7 +31,10 @@ export default defineAction({
30
31
  ),
31
32
  }),
32
33
  parallelSafe: true,
33
- run: async ({ runId, slotId, sessionId, source, callerAppId }) => {
34
+ run: async (
35
+ { runId, slotId, sessionId, source, callerAppId },
36
+ context?: ActionRunContext,
37
+ ) => {
34
38
  const db = getDb();
35
39
  const [run] = await db
36
40
  .select()
@@ -52,6 +56,8 @@ export default defineAction({
52
56
  intent?: string;
53
57
  styleStrength?: string;
54
58
  subjectAssetId?: string;
59
+ embeddedText?: string | null;
60
+ textPlacement?: string | null;
55
61
  };
56
62
  includeLogo?: boolean;
57
63
  categories?: string[];
@@ -60,39 +66,54 @@ export default defineAction({
60
66
  intent?: string;
61
67
  styleStrength?: string;
62
68
  tier?: string | null;
69
+ embeddedText?: string | null;
70
+ textPlacement?: string | null;
63
71
  }>(run.metadata, {});
64
72
  const categories =
65
73
  metadata.settingsUsed?.categories ?? metadata.categories ?? undefined;
66
74
 
67
- return generateImage.run({
68
- libraryId: run.libraryId,
69
- collectionId: run.collectionId ?? undefined,
70
- presetId: run.presetId ?? undefined,
71
- sessionId: resolvedSessionId,
72
- prompt: run.prompt,
73
- aspectRatio: run.aspectRatio as any,
74
- imageSize: run.imageSize as any,
75
- model: run.model as any,
76
- tier: (metadata.settingsUsed?.tier ?? metadata.tier ?? undefined) as any,
77
- intent: (metadata.settingsUsed?.intent ??
78
- metadata.intent ??
79
- "generate") as any,
80
- styleStrength: (metadata.settingsUsed?.styleStrength ??
81
- metadata.styleStrength ??
82
- "balanced") as any,
83
- categories: categories as any,
84
- includeLogo: Boolean(
85
- metadata.settingsUsed?.includeLogo ?? metadata.includeLogo,
86
- ),
87
- groundingMode: run.groundingMode as any,
88
- sourceAssetId: metadata.sourceAssetId,
89
- subjectAssetId:
90
- metadata.settingsUsed?.subjectAssetId ??
91
- metadata.subjectAssetId ??
92
- undefined,
93
- slotId,
94
- source,
95
- callerAppId,
96
- });
75
+ return generateImage.run(
76
+ {
77
+ libraryId: run.libraryId,
78
+ collectionId: run.collectionId ?? undefined,
79
+ presetId: run.presetId ?? undefined,
80
+ sessionId: resolvedSessionId,
81
+ prompt: run.prompt,
82
+ embeddedText:
83
+ metadata.settingsUsed?.embeddedText ??
84
+ metadata.embeddedText ??
85
+ undefined,
86
+ textPlacement:
87
+ metadata.settingsUsed?.textPlacement ??
88
+ metadata.textPlacement ??
89
+ undefined,
90
+ aspectRatio: run.aspectRatio as any,
91
+ imageSize: run.imageSize as any,
92
+ model: run.model as any,
93
+ tier: (metadata.settingsUsed?.tier ??
94
+ metadata.tier ??
95
+ undefined) as any,
96
+ intent: (metadata.settingsUsed?.intent ??
97
+ metadata.intent ??
98
+ "generate") as any,
99
+ styleStrength: (metadata.settingsUsed?.styleStrength ??
100
+ metadata.styleStrength ??
101
+ "balanced") as any,
102
+ categories: categories as any,
103
+ includeLogo: Boolean(
104
+ metadata.settingsUsed?.includeLogo ?? metadata.includeLogo,
105
+ ),
106
+ groundingMode: run.groundingMode as any,
107
+ sourceAssetId: metadata.sourceAssetId,
108
+ subjectAssetId:
109
+ metadata.settingsUsed?.subjectAssetId ??
110
+ metadata.subjectAssetId ??
111
+ undefined,
112
+ slotId,
113
+ source,
114
+ callerAppId,
115
+ },
116
+ context,
117
+ );
97
118
  },
98
119
  });
@@ -0,0 +1,100 @@
1
+ import { useActionQuery, useT } from "@agent-native/core/client";
2
+ import { IconArrowUpRight, IconPhoto } from "@tabler/icons-react";
3
+ import { Link } from "react-router";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import { Skeleton } from "@/components/ui/skeleton";
7
+
8
+ type DraftAsset = {
9
+ id: string;
10
+ title?: string | null;
11
+ prompt?: string | null;
12
+ mediaType?: string | null;
13
+ mimeType?: string | null;
14
+ thumbnailUrl?: string | null;
15
+ previewUrl?: string | null;
16
+ url?: string | null;
17
+ };
18
+
19
+ const RECENT_DRAFTS_LIMIT = 5;
20
+
21
+ export function RecentDraftsSection() {
22
+ const t = useT();
23
+ const { data, isLoading } = useActionQuery("list-draft-assets", {
24
+ limit: RECENT_DRAFTS_LIMIT,
25
+ });
26
+ const drafts = ((data as any)?.assets ?? []) as DraftAsset[];
27
+
28
+ if (!isLoading && drafts.length === 0) return null;
29
+
30
+ return (
31
+ <section className="space-y-3">
32
+ <div className="flex flex-wrap items-center justify-between gap-3">
33
+ <h2 className="text-sm font-semibold text-foreground">
34
+ {t("library.recentDrafts")}
35
+ </h2>
36
+ <Button asChild variant="outline" size="sm">
37
+ <Link to="/library?tab=drafts">
38
+ {t("library.viewAllDrafts")}
39
+ <IconArrowUpRight size={15} className="ml-1.5" />
40
+ </Link>
41
+ </Button>
42
+ </div>
43
+
44
+ <div className="grid grid-cols-3 gap-3 sm:grid-cols-5">
45
+ {isLoading
46
+ ? Array.from({ length: RECENT_DRAFTS_LIMIT }).map((_, index) => (
47
+ <Skeleton key={index} className="aspect-square rounded-lg" />
48
+ ))
49
+ : drafts.map((draft) => (
50
+ <Link
51
+ key={draft.id}
52
+ to={`/asset/${encodeURIComponent(draft.id)}`}
53
+ title={draft.title || draft.prompt || t("library.draftAsset")}
54
+ 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"
55
+ >
56
+ <div className="aspect-square bg-muted">
57
+ <DraftThumbnail draft={draft} />
58
+ </div>
59
+ </Link>
60
+ ))}
61
+ </div>
62
+ </section>
63
+ );
64
+ }
65
+
66
+ function DraftThumbnail({ draft }: { draft: DraftAsset }) {
67
+ const t = useT();
68
+ const isVideo =
69
+ draft.mediaType === "video" || draft.mimeType?.startsWith("video/");
70
+ const source = draft.thumbnailUrl ?? draft.previewUrl ?? draft.url ?? "";
71
+
72
+ if (isVideo && !draft.thumbnailUrl) {
73
+ return (
74
+ <video
75
+ src={draft.previewUrl ?? draft.url ?? undefined}
76
+ muted
77
+ playsInline
78
+ preload="metadata"
79
+ className="h-full w-full object-cover transition group-hover:scale-[1.02]"
80
+ />
81
+ );
82
+ }
83
+
84
+ if (!source) {
85
+ return (
86
+ <div className="flex h-full w-full items-center justify-center text-muted-foreground">
87
+ <IconPhoto className="size-5" />
88
+ </div>
89
+ );
90
+ }
91
+
92
+ return (
93
+ <img
94
+ src={source}
95
+ alt={draft.title ?? draft.prompt ?? t("library.draftAsset")}
96
+ loading="lazy"
97
+ className="h-full w-full object-cover transition group-hover:scale-[1.02]"
98
+ />
99
+ );
100
+ }
@@ -628,6 +628,13 @@ const messages = {
628
628
  videosCount: "{{count}} 個影片",
629
629
  },
630
630
  library: {
631
+ drafts: "草稿",
632
+ allLibraries: "所有資料庫",
633
+ noDrafts: "尚無草稿。",
634
+ noMatchingDrafts: "沒有符合的草稿。",
635
+ recentDrafts: "最近草稿",
636
+ viewAllDrafts: "查看所有草稿",
637
+ draftAsset: "草稿資產",
631
638
  addedToReferences: "已新增到參考。",
632
639
  addedAssetsToReferences: "已將 {{count}} 個資產新增到參考。",
633
640
  add: "新增",
@@ -826,6 +826,13 @@ const enUS = {
826
826
  noMatchingAssets: "No matching assets across your kits.",
827
827
  noMatchingLibraryAssets:
828
828
  "No matching {{mediaLabel}} assets in this library.",
829
+ drafts: "Drafts",
830
+ allLibraries: "All libraries",
831
+ noDrafts: "No drafts yet.",
832
+ noMatchingDrafts: "No matching drafts.",
833
+ recentDrafts: "Recent Drafts",
834
+ viewAllDrafts: "View all drafts",
835
+ draftAsset: "Draft asset",
829
836
  noReusableAssets:
830
837
  "No reusable assets yet. Select a kit to upload references or generate assets.",
831
838
  noNewAssetsUploaded: "No new assets were uploaded.",
@@ -2762,6 +2769,12 @@ export const messagesByLocale = {
2762
2769
  "zh-TW": mergeMessages(zhTW),
2763
2770
  "zh-CN": mergeMessages({
2764
2771
  library: {
2772
+ allLibraries: "所有库",
2773
+ noDrafts: "暂无草稿。",
2774
+ noMatchingDrafts: "没有匹配的草稿。",
2775
+ recentDrafts: "最近草稿",
2776
+ viewAllDrafts: "查看所有草稿",
2777
+ draftAsset: "草稿资产",
2765
2778
  addAssets: "添加资产",
2766
2779
  addAssetsDescription:
2767
2780
  "上传源素材或生成候选项,然后只将应指导未来生成的资产标记为参考。",
@@ -3500,6 +3513,12 @@ export const messagesByLocale = {
3500
3513
  }),
3501
3514
  "es-ES": mergeMessages({
3502
3515
  library: {
3516
+ allLibraries: "Todas las bibliotecas",
3517
+ noDrafts: "Aún no hay borradores.",
3518
+ noMatchingDrafts: "No hay borradores coincidentes.",
3519
+ recentDrafts: "Borradores recientes",
3520
+ viewAllDrafts: "Ver todos los borradores",
3521
+ draftAsset: "Borrador",
3503
3522
  addAssets: "Agregar activos",
3504
3523
  addAssetsDescription:
3505
3524
  "Cargue material fuente o genere candidatos, luego marque solo los activos que deberían guiar a las generaciones futuras como referencias.",
@@ -3963,6 +3982,12 @@ export const messagesByLocale = {
3963
3982
  }),
3964
3983
  "fr-FR": mergeMessages({
3965
3984
  library: {
3985
+ allLibraries: "Toutes les bibliothèques",
3986
+ noDrafts: "Aucun brouillon pour l'instant.",
3987
+ noMatchingDrafts: "Aucun brouillon correspondant.",
3988
+ recentDrafts: "Brouillons récents",
3989
+ viewAllDrafts: "Voir tous les brouillons",
3990
+ draftAsset: "Brouillon",
3966
3991
  addAssets: "Ajouter des éléments",
3967
3992
  addAssetsDescription:
3968
3993
  "Téléchargez le matériel source ou générez des candidats, puis marquez uniquement les atouts qui devraient guider les générations futures comme références.",
@@ -4404,6 +4429,12 @@ export const messagesByLocale = {
4404
4429
  }),
4405
4430
  "de-DE": mergeMessages({
4406
4431
  library: {
4432
+ allLibraries: "Alle Bibliotheken",
4433
+ noDrafts: "Noch keine Entwürfe.",
4434
+ noMatchingDrafts: "Keine passenden Entwürfe.",
4435
+ recentDrafts: "Letzte Entwürfe",
4436
+ viewAllDrafts: "Alle Entwürfe anzeigen",
4437
+ draftAsset: "Entwurf",
4407
4438
  addAssets: "Assets hinzufügen",
4408
4439
  addAssetsDescription:
4409
4440
  "Laden Sie Quellmaterial hoch oder generieren Sie Kandidaten und markieren Sie dann nur die Assets, die künftigen Generationen als Referenz dienen sollen.",
@@ -4753,6 +4784,12 @@ export const messagesByLocale = {
4753
4784
  }),
4754
4785
  "ja-JP": mergeMessages({
4755
4786
  library: {
4787
+ allLibraries: "すべてのライブラリ",
4788
+ noDrafts: "下書きはまだありません。",
4789
+ noMatchingDrafts: "一致する下書きはありません。",
4790
+ recentDrafts: "最近の下書き",
4791
+ viewAllDrafts: "すべての下書きを表示",
4792
+ draftAsset: "下書きアセット",
4756
4793
  addAssets: "アセットの追加",
4757
4794
  addAssetsDescription:
4758
4795
  "ソース素材をアップロードするか候補を生成し、将来の世代を導く必要がある資産のみを参照としてマークします。",
@@ -5092,6 +5129,12 @@ export const messagesByLocale = {
5092
5129
  }),
5093
5130
  "ko-KR": mergeMessages({
5094
5131
  library: {
5132
+ allLibraries: "모든 라이브러리",
5133
+ noDrafts: "아직 초안이 없습니다.",
5134
+ noMatchingDrafts: "일치하는 초안이 없습니다.",
5135
+ recentDrafts: "최근 초안",
5136
+ viewAllDrafts: "모든 초안 보기",
5137
+ draftAsset: "초안 자산",
5095
5138
  addAssets: "자산 추가",
5096
5139
  addAssetsDescription:
5097
5140
  "원본 자료를 업로드하거나 후보를 생성한 후, 미래 세대를 이끌어야 할 자산만 참고 자료로 표시하세요.",
@@ -5430,6 +5473,12 @@ export const messagesByLocale = {
5430
5473
  }),
5431
5474
  "pt-BR": mergeMessages({
5432
5475
  library: {
5476
+ allLibraries: "Todas as bibliotecas",
5477
+ noDrafts: "Ainda não há rascunhos.",
5478
+ noMatchingDrafts: "Nenhum rascunho correspondente.",
5479
+ recentDrafts: "Rascunhos recentes",
5480
+ viewAllDrafts: "Ver todos os rascunhos",
5481
+ draftAsset: "Rascunho",
5433
5482
  addAssets: "Adicionar recursos",
5434
5483
  addAssetsDescription:
5435
5484
  "Carregue o material de origem ou gere candidatos e marque apenas os ativos que devem orientar as gerações futuras como referências.",
@@ -5870,6 +5919,12 @@ export const messagesByLocale = {
5870
5919
  }),
5871
5920
  "hi-IN": mergeMessages({
5872
5921
  library: {
5922
+ allLibraries: "सभी लाइब्रेरी",
5923
+ noDrafts: "अभी तक कोई ड्राफ़्ट नहीं।",
5924
+ noMatchingDrafts: "कोई मिलान वाला ड्राफ़्ट नहीं।",
5925
+ recentDrafts: "हाल के ड्राफ़्ट",
5926
+ viewAllDrafts: "सभी ड्राफ़्ट देखें",
5927
+ draftAsset: "ड्राफ़्ट एसेट",
5873
5928
  addAssets: "संपत्तियां जोड़ें",
5874
5929
  addAssetsDescription:
5875
5930
  "स्रोत सामग्री अपलोड करें या उम्मीदवार तैयार करें, फिर केवल उन संपत्तियों को चिह्नित करें जो भविष्य की पीढ़ियों को संदर्भ के रूप में मार्गदर्शन करें।",
@@ -6206,6 +6261,12 @@ export const messagesByLocale = {
6206
6261
  }),
6207
6262
  "ar-SA": mergeMessages({
6208
6263
  library: {
6264
+ allLibraries: "جميع المكتبات",
6265
+ noDrafts: "لا توجد مسودات بعد.",
6266
+ noMatchingDrafts: "لا توجد مسودات مطابقة.",
6267
+ recentDrafts: "المسودات الأخيرة",
6268
+ viewAllDrafts: "عرض جميع المسودات",
6269
+ draftAsset: "مسودة",
6209
6270
  addAssets: "أضف الأصول",
6210
6271
  addAssetsDescription:
6211
6272
  "قم بتحميل المواد المصدرية أو قم بإنشاء مرشحين، ثم حدد فقط الأصول التي يجب أن توجه الأجيال القادمة كمراجع.",