@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -15
- package/corpus/core/src/client/AssistantChat.tsx +123 -26
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
- package/corpus/templates/assets/AGENTS.md +4 -0
- package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
- package/corpus/templates/assets/actions/generate-image.ts +118 -67
- package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
- package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/assets/app/i18n-data.ts +61 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +145 -38
- package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +135 -11
- package/corpus/templates/assets/shared/api.ts +4 -0
- package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +13 -5
- package/corpus/templates/design/actions/apply-design-state.ts +17 -4
- package/corpus/templates/design/actions/apply-motion-edit.ts +9 -46
- package/corpus/templates/design/actions/delete-design.ts +20 -0
- package/corpus/templates/design/actions/get-component-details.ts +16 -7
- package/corpus/templates/design/actions/index-design-tokens.ts +8 -4
- package/corpus/templates/design/actions/list-design-states.ts +19 -1
- package/corpus/templates/design/actions/open-component-source.ts +7 -2
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
- package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
- package/corpus/templates/design/actions/run-design-audit.ts +21 -6
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +139 -14
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +20 -3
- package/corpus/templates/design/app/components/design/EditPanel.tsx +428 -31
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +110 -30
- package/corpus/templates/design/app/components/design/MotionDock.tsx +9 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1 -1
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +12 -5
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +4 -2
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +13 -8
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +11 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +684 -179
- package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +94 -2
- package/corpus/templates/design/shared/component-model.ts +35 -0
- package/corpus/templates/design/shared/motion-compiler.ts +79 -8
- package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +94 -10
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +14 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +115 -22
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { IconPhoto, IconSparkles, IconVideo } from "@tabler/icons-react";
|
|
|
9
9
|
import { useEffect } from "react";
|
|
10
10
|
import { useNavigate, useParams } from "react-router";
|
|
11
11
|
|
|
12
|
+
import { RecentDraftsSection } from "@/components/create/RecentDraftsSection";
|
|
12
13
|
import { GenerationResults } from "@/components/generation/GenerationResults";
|
|
13
14
|
import { useImageModelMenu } from "@/hooks/use-image-model-menu";
|
|
14
15
|
import { ASSETS_CHAT_STORAGE_KEY } from "@/lib/chat";
|
|
@@ -122,6 +123,9 @@ export default function CreatePage() {
|
|
|
122
123
|
</button>
|
|
123
124
|
))}
|
|
124
125
|
</div>
|
|
126
|
+
<div className="mt-8 w-full text-left">
|
|
127
|
+
<RecentDraftsSection />
|
|
128
|
+
</div>
|
|
125
129
|
</div>
|
|
126
130
|
}
|
|
127
131
|
/>
|
|
@@ -102,16 +102,22 @@ import {
|
|
|
102
102
|
type VariantSlot,
|
|
103
103
|
} from "./brand-kits.$id";
|
|
104
104
|
|
|
105
|
-
type AssetTab = "all" | "generated" | "references";
|
|
105
|
+
type AssetTab = "all" | "generated" | "drafts" | "references";
|
|
106
106
|
|
|
107
107
|
function isGeneratedAsset(asset: Asset) {
|
|
108
108
|
const role = asset.role ?? "";
|
|
109
109
|
return role === "generated" || role === "active" || role === "candidate";
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function isDraftAsset(asset: Asset) {
|
|
113
|
+
return asset.role === "generated" && asset.status === "candidate";
|
|
114
|
+
}
|
|
115
|
+
|
|
112
116
|
function assetMatchesTab(asset: Asset, tab: AssetTab) {
|
|
113
117
|
if (tab === "all") return true;
|
|
114
|
-
if (tab === "
|
|
118
|
+
if (tab === "drafts") return isDraftAsset(asset);
|
|
119
|
+
if (tab === "generated")
|
|
120
|
+
return isGeneratedAsset(asset) && !isDraftAsset(asset);
|
|
115
121
|
return !isGeneratedAsset(asset);
|
|
116
122
|
}
|
|
117
123
|
|
|
@@ -1957,12 +1963,29 @@ export function LibraryWorkspace({
|
|
|
1957
1963
|
|
|
1958
1964
|
export function AssetPickerSurface() {
|
|
1959
1965
|
const t = useT();
|
|
1960
|
-
const [searchParams] = useSearchParams();
|
|
1966
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
1961
1967
|
const searchParamsKey = searchParams.toString();
|
|
1968
|
+
const urlAssetTab = useMemo<AssetTab | null>(() => {
|
|
1969
|
+
const tab = new URLSearchParams(searchParamsKey).get("tab");
|
|
1970
|
+
return tab === "all" ||
|
|
1971
|
+
tab === "generated" ||
|
|
1972
|
+
tab === "drafts" ||
|
|
1973
|
+
tab === "references"
|
|
1974
|
+
? tab
|
|
1975
|
+
: null;
|
|
1976
|
+
}, [searchParamsKey]);
|
|
1977
|
+
// The active tab is the only host-irrelevant search param. Exclude it from the
|
|
1978
|
+
// host-config key so toggling tabs (which writes `?tab=`) doesn't retrigger the
|
|
1979
|
+
// effect that resets media type / query / library from the URL.
|
|
1980
|
+
const hostParamsKey = useMemo(() => {
|
|
1981
|
+
const params = new URLSearchParams(searchParamsKey);
|
|
1982
|
+
params.delete("tab");
|
|
1983
|
+
return params.toString();
|
|
1984
|
+
}, [searchParamsKey]);
|
|
1962
1985
|
const mcpChatBridgeActive =
|
|
1963
1986
|
searchParamsRequestPicker(searchParams) || isEmbedMcpChatBridgeActive();
|
|
1964
1987
|
const urlHostConfig = useMemo(() => {
|
|
1965
|
-
const params = new URLSearchParams(
|
|
1988
|
+
const params = new URLSearchParams(hostParamsKey);
|
|
1966
1989
|
return {
|
|
1967
1990
|
mediaType: normalizeMediaType(params.get("mediaType")),
|
|
1968
1991
|
prompt: params.get("prompt") ?? undefined,
|
|
@@ -1983,7 +2006,7 @@ export function AssetPickerSurface() {
|
|
|
1983
2006
|
),
|
|
1984
2007
|
autoGenerate: normalizeBoolean(params.get("autoGenerate")) ?? false,
|
|
1985
2008
|
} satisfies HostConfig;
|
|
1986
|
-
}, [
|
|
2009
|
+
}, [hostParamsKey]);
|
|
1987
2010
|
const bridgeRef = useRef<EmbeddedAppBridge | null>(null);
|
|
1988
2011
|
const embedded = useMemo(
|
|
1989
2012
|
() =>
|
|
@@ -2008,7 +2031,7 @@ export function AssetPickerSurface() {
|
|
|
2008
2031
|
);
|
|
2009
2032
|
const [presetId, setPresetId] = useState(() => hostConfig.presetId ?? "none");
|
|
2010
2033
|
const [count, setCount] = useState(() => hostConfig.count ?? 3);
|
|
2011
|
-
const [assetTab, setAssetTab] = useState<AssetTab>("all");
|
|
2034
|
+
const [assetTab, setAssetTab] = useState<AssetTab>(urlAssetTab ?? "all");
|
|
2012
2035
|
const [selectedLibraryId, setSelectedLibraryId] = useState(
|
|
2013
2036
|
() => hostConfig.libraryId ?? "",
|
|
2014
2037
|
);
|
|
@@ -2032,6 +2055,30 @@ export function AssetPickerSurface() {
|
|
|
2032
2055
|
setVisibleCandidateRunIds(urlHostConfig.candidateRunIds ?? []);
|
|
2033
2056
|
}, [urlHostConfig]);
|
|
2034
2057
|
|
|
2058
|
+
useEffect(() => {
|
|
2059
|
+
// Reset to "all" when the tab param is removed (e.g. back/forward nav)
|
|
2060
|
+
// since the component stays mounted across search-param changes.
|
|
2061
|
+
setAssetTab(urlAssetTab ?? "all");
|
|
2062
|
+
}, [urlAssetTab]);
|
|
2063
|
+
|
|
2064
|
+
const handleAssetTabChange = useCallback(
|
|
2065
|
+
(value: AssetTab) => {
|
|
2066
|
+
setAssetTab(value);
|
|
2067
|
+
// Keep the tab reflected in the URL so it survives refresh/share and
|
|
2068
|
+
// stays consistent with the `?tab=` deep link from the home page.
|
|
2069
|
+
setSearchParams(
|
|
2070
|
+
(prev) => {
|
|
2071
|
+
const next = new URLSearchParams(prev);
|
|
2072
|
+
if (value === "all") next.delete("tab");
|
|
2073
|
+
else next.set("tab", value);
|
|
2074
|
+
return next;
|
|
2075
|
+
},
|
|
2076
|
+
{ replace: true },
|
|
2077
|
+
);
|
|
2078
|
+
},
|
|
2079
|
+
[setSearchParams],
|
|
2080
|
+
);
|
|
2081
|
+
|
|
2035
2082
|
const librariesQuery = useActionQuery("list-libraries", {
|
|
2036
2083
|
compact: true,
|
|
2037
2084
|
} as any) as {
|
|
@@ -2116,23 +2163,54 @@ export function AssetPickerSurface() {
|
|
|
2116
2163
|
!selectedPreset &&
|
|
2117
2164
|
Boolean(waitingForPresetData);
|
|
2118
2165
|
const mediaLabel = mediaType === "video" ? "video" : "image";
|
|
2166
|
+
const viewingDrafts = assetTab === "drafts";
|
|
2167
|
+
// The standalone Library "Drafts" tab mirrors the home Recent Drafts section:
|
|
2168
|
+
// every unsaved draft across all accessible libraries, regardless of media
|
|
2169
|
+
// type. The embedded picker keeps its library + media-type scope so an
|
|
2170
|
+
// image-only picker never surfaces video drafts.
|
|
2171
|
+
const draftsGlobalView = viewingDrafts && !embedded;
|
|
2119
2172
|
const assetsParams = useMemo(
|
|
2120
2173
|
() => ({
|
|
2121
2174
|
libraryId: selectedLibraryId,
|
|
2122
2175
|
mediaType,
|
|
2176
|
+
// Drafts are exactly generated candidates — filter them server-side
|
|
2177
|
+
// instead of fetching the whole library and filtering on the client.
|
|
2178
|
+
role: viewingDrafts ? "generated" : undefined,
|
|
2179
|
+
status: viewingDrafts ? "candidate" : undefined,
|
|
2123
2180
|
query: query.trim() || undefined,
|
|
2124
2181
|
includeCandidates:
|
|
2125
|
-
|
|
2182
|
+
viewingDrafts ||
|
|
2183
|
+
(mediaType === "image" && visibleCandidateRunIds.length > 0),
|
|
2184
|
+
// The Drafts tab shows every unsaved draft, not just the latest run batch.
|
|
2126
2185
|
candidateRunIds:
|
|
2127
|
-
visibleCandidateRunIds.length > 0
|
|
2186
|
+
!viewingDrafts && visibleCandidateRunIds.length > 0
|
|
2187
|
+
? visibleCandidateRunIds
|
|
2188
|
+
: undefined,
|
|
2128
2189
|
}),
|
|
2129
|
-
[
|
|
2190
|
+
[
|
|
2191
|
+
viewingDrafts,
|
|
2192
|
+
mediaType,
|
|
2193
|
+
query,
|
|
2194
|
+
selectedLibraryId,
|
|
2195
|
+
visibleCandidateRunIds,
|
|
2196
|
+
],
|
|
2130
2197
|
);
|
|
2131
2198
|
const { data: assetData, isLoading: assetsLoading } = useActionQuery(
|
|
2132
2199
|
"list-assets",
|
|
2133
2200
|
assetsParams as any,
|
|
2134
|
-
{
|
|
2201
|
+
{
|
|
2202
|
+
enabled:
|
|
2203
|
+
Boolean(selectedLibraryId) && !usingStarterLibrary && !draftsGlobalView,
|
|
2204
|
+
} as any,
|
|
2135
2205
|
) as { data?: { assets?: Asset[] }; isLoading: boolean };
|
|
2206
|
+
const { data: globalDraftsData, isLoading: globalDraftsLoading } =
|
|
2207
|
+
useActionQuery(
|
|
2208
|
+
"list-draft-assets",
|
|
2209
|
+
{ limit: 500 } as any,
|
|
2210
|
+
{
|
|
2211
|
+
enabled: draftsGlobalView,
|
|
2212
|
+
} as any,
|
|
2213
|
+
) as { data?: { assets?: Asset[] }; isLoading: boolean };
|
|
2136
2214
|
const starterAssets: Asset[] = useMemo(
|
|
2137
2215
|
() =>
|
|
2138
2216
|
STARTER_PRESET.referenceImages.map((reference) => ({
|
|
@@ -2159,11 +2237,23 @@ export function AssetPickerSurface() {
|
|
|
2159
2237
|
.some((value) => String(value).toLowerCase().includes(needle)),
|
|
2160
2238
|
);
|
|
2161
2239
|
}, [query, starterAssets]);
|
|
2162
|
-
const
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2240
|
+
const globalDrafts = useMemo(() => {
|
|
2241
|
+
const drafts = globalDraftsData?.assets ?? [];
|
|
2242
|
+
const needle = query.trim().toLowerCase();
|
|
2243
|
+
if (!needle) return drafts;
|
|
2244
|
+
return drafts.filter((asset) =>
|
|
2245
|
+
[asset.title, asset.description, asset.prompt]
|
|
2246
|
+
.filter(Boolean)
|
|
2247
|
+
.some((value) => String(value).toLowerCase().includes(needle)),
|
|
2248
|
+
);
|
|
2249
|
+
}, [globalDraftsData, query]);
|
|
2250
|
+
const allAssets = draftsGlobalView
|
|
2251
|
+
? globalDrafts
|
|
2252
|
+
: usingStarterLibrary
|
|
2253
|
+
? mediaType === "image"
|
|
2254
|
+
? visibleStarterAssets
|
|
2255
|
+
: []
|
|
2256
|
+
: (assetData?.assets ?? []);
|
|
2167
2257
|
const currentLibrary = useMemo(
|
|
2168
2258
|
() =>
|
|
2169
2259
|
displayLibraries.find((library) => library.id === selectedLibraryId) ??
|
|
@@ -2174,6 +2264,7 @@ export function AssetPickerSurface() {
|
|
|
2174
2264
|
() => allAssets.filter((asset) => assetMatchesTab(asset, assetTab)),
|
|
2175
2265
|
[allAssets, assetTab],
|
|
2176
2266
|
);
|
|
2267
|
+
const listLoading = draftsGlobalView ? globalDraftsLoading : assetsLoading;
|
|
2177
2268
|
const [previewAsset, setPreviewAsset] = useState<Asset | null>(null);
|
|
2178
2269
|
const [standaloneSelection, setStandaloneSelection] = useState<ReturnType<
|
|
2179
2270
|
typeof assetPayload
|
|
@@ -2845,27 +2936,35 @@ export function AssetPickerSurface() {
|
|
|
2845
2936
|
{displayLibraries.length > 0 && (
|
|
2846
2937
|
<div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
2847
2938
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2939
|
+
{draftsGlobalView ? (
|
|
2940
|
+
<div className="flex h-9 items-center rounded-md border border-border/70 bg-background px-3 text-sm text-muted-foreground sm:w-48">
|
|
2941
|
+
{t("library.allLibraries")}
|
|
2942
|
+
</div>
|
|
2943
|
+
) : (
|
|
2944
|
+
<Select
|
|
2945
|
+
value={selectedLibraryId}
|
|
2946
|
+
onValueChange={setSelectedLibraryId}
|
|
2947
|
+
>
|
|
2948
|
+
<SelectTrigger className="h-9 w-full border-border/70 bg-background sm:w-48">
|
|
2949
|
+
<SelectValue placeholder={t("library.library")} />
|
|
2950
|
+
</SelectTrigger>
|
|
2951
|
+
<SelectContent>
|
|
2952
|
+
<SelectGroup>
|
|
2953
|
+
{displayLibraries.map((library) => (
|
|
2954
|
+
<SelectItem key={library.id} value={library.id}>
|
|
2955
|
+
{library.title}
|
|
2956
|
+
</SelectItem>
|
|
2957
|
+
))}
|
|
2958
|
+
</SelectGroup>
|
|
2959
|
+
</SelectContent>
|
|
2960
|
+
</Select>
|
|
2961
|
+
)}
|
|
2865
2962
|
{selectedLibraryId && (
|
|
2866
2963
|
<Tabs
|
|
2867
2964
|
value={assetTab}
|
|
2868
|
-
onValueChange={(value) =>
|
|
2965
|
+
onValueChange={(value) =>
|
|
2966
|
+
handleAssetTabChange(value as AssetTab)
|
|
2967
|
+
}
|
|
2869
2968
|
>
|
|
2870
2969
|
<TabsList>
|
|
2871
2970
|
<TabsTrigger value="all">
|
|
@@ -2874,6 +2973,9 @@ export function AssetPickerSurface() {
|
|
|
2874
2973
|
<TabsTrigger value="generated">
|
|
2875
2974
|
{t("library.generated")}
|
|
2876
2975
|
</TabsTrigger>
|
|
2976
|
+
<TabsTrigger value="drafts">
|
|
2977
|
+
{t("library.drafts")}
|
|
2978
|
+
</TabsTrigger>
|
|
2877
2979
|
<TabsTrigger value="references">
|
|
2878
2980
|
{t("library.references")}
|
|
2879
2981
|
</TabsTrigger>
|
|
@@ -2897,6 +2999,7 @@ export function AssetPickerSurface() {
|
|
|
2897
2999
|
{mediaType === "image" &&
|
|
2898
3000
|
selectedLibraryId &&
|
|
2899
3001
|
!usingStarterLibrary &&
|
|
3002
|
+
!viewingDrafts &&
|
|
2900
3003
|
pickerVariantScopeId && (
|
|
2901
3004
|
<LibraryCandidateStage
|
|
2902
3005
|
activeLibraryId={selectedLibraryId}
|
|
@@ -2914,7 +3017,7 @@ export function AssetPickerSurface() {
|
|
|
2914
3017
|
</div>
|
|
2915
3018
|
)}
|
|
2916
3019
|
|
|
2917
|
-
{selectedLibraryId &&
|
|
3020
|
+
{selectedLibraryId && listLoading && (
|
|
2918
3021
|
<div className="assets-library-dense-grid grid grid-cols-2 gap-3">
|
|
2919
3022
|
{Array.from({ length: 8 }).map((_, index) => (
|
|
2920
3023
|
<Skeleton key={index} className="aspect-square rounded-md" />
|
|
@@ -2922,12 +3025,16 @@ export function AssetPickerSurface() {
|
|
|
2922
3025
|
</div>
|
|
2923
3026
|
)}
|
|
2924
3027
|
|
|
2925
|
-
{selectedLibraryId && !
|
|
3028
|
+
{selectedLibraryId && !listLoading && assets.length === 0 && (
|
|
2926
3029
|
<div className="flex h-full items-center justify-center text-center">
|
|
2927
3030
|
<div className="max-w-sm text-sm text-muted-foreground">
|
|
2928
|
-
{
|
|
2929
|
-
?
|
|
2930
|
-
|
|
3031
|
+
{draftsGlobalView
|
|
3032
|
+
? query
|
|
3033
|
+
? t("library.noMatchingDrafts")
|
|
3034
|
+
: t("library.noDrafts")
|
|
3035
|
+
: query
|
|
3036
|
+
? t("library.noMatchingLibraryAssets", { mediaLabel })
|
|
3037
|
+
: t("library.noAssetsInLibrary", { mediaLabel })}
|
|
2931
3038
|
</div>
|
|
2932
3039
|
</div>
|
|
2933
3040
|
)}
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
GenerationIntent,
|
|
12
12
|
ImageCategory,
|
|
13
13
|
ImageModel,
|
|
14
|
+
ImageQualityTier,
|
|
14
15
|
ImageSize,
|
|
15
16
|
StyleStrength,
|
|
16
17
|
StyleBrief,
|
|
@@ -564,7 +565,8 @@ export async function analyzeStyleWithGemini(input: {
|
|
|
564
565
|
{
|
|
565
566
|
text: [
|
|
566
567
|
"Analyze these brand/style reference images for a reusable image generation style brief.",
|
|
567
|
-
"Return only compact JSON with keys: description, medium, mood, subjectMatter, texture, composition, lighting, typographyPolicy, doNot.",
|
|
568
|
+
"Return only compact JSON with keys: description, medium, mood, subjectMatter, texture, composition, lighting, fontFamilies, fontWeights, letterforms, caseStyle, typographyPolicy, doNot.",
|
|
569
|
+
"For typography, describe specific letterforms and font traits, not just broad sans/serif categories; omit any typography field you cannot determine confidently.",
|
|
568
570
|
"Use specific trait-locking phrases that can be reused across future image prompts.",
|
|
569
571
|
"Do not name brands, artists, studios, franchises, or copyrighted works unless they appear as user-provided brand identity.",
|
|
570
572
|
"Keep doNot as an array of short constraints. Omit uncertain fields instead of guessing.",
|
|
@@ -608,16 +610,24 @@ function extractJsonObject(text: string): string {
|
|
|
608
610
|
return "{}";
|
|
609
611
|
}
|
|
610
612
|
|
|
611
|
-
function sanitizeStyleBrief(value: Record<string, unknown>): StyleBrief {
|
|
613
|
+
export function sanitizeStyleBrief(value: Record<string, unknown>): StyleBrief {
|
|
612
614
|
const stringField = (key: string) => {
|
|
613
615
|
const raw = value[key];
|
|
614
616
|
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
|
|
615
617
|
};
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
618
|
+
const stringArrayField = (key: string) => {
|
|
619
|
+
const raw = value[key];
|
|
620
|
+
return Array.isArray(raw)
|
|
621
|
+
? raw
|
|
622
|
+
.filter(
|
|
623
|
+
(item): item is string => typeof item === "string" && !!item.trim(),
|
|
624
|
+
)
|
|
625
|
+
.map((item) => item.trim())
|
|
626
|
+
: undefined;
|
|
627
|
+
};
|
|
628
|
+
const fontFamilies = stringArrayField("fontFamilies");
|
|
629
|
+
const fontWeights = stringArrayField("fontWeights");
|
|
630
|
+
const doNot = stringArrayField("doNot");
|
|
621
631
|
return {
|
|
622
632
|
description: stringField("description"),
|
|
623
633
|
medium: stringField("medium"),
|
|
@@ -626,8 +636,12 @@ function sanitizeStyleBrief(value: Record<string, unknown>): StyleBrief {
|
|
|
626
636
|
texture: stringField("texture"),
|
|
627
637
|
composition: stringField("composition"),
|
|
628
638
|
lighting: stringField("lighting"),
|
|
639
|
+
fontFamilies: fontFamilies?.length ? fontFamilies : undefined,
|
|
640
|
+
fontWeights: fontWeights?.length ? fontWeights : undefined,
|
|
641
|
+
letterforms: stringField("letterforms"),
|
|
642
|
+
caseStyle: stringField("caseStyle"),
|
|
629
643
|
typographyPolicy: stringField("typographyPolicy"),
|
|
630
|
-
doNot: doNot?.length ? doNot
|
|
644
|
+
doNot: doNot?.length ? doNot : undefined,
|
|
631
645
|
};
|
|
632
646
|
}
|
|
633
647
|
|
|
@@ -774,6 +788,66 @@ function toBuilderImageModel(model: ImageModel) {
|
|
|
774
788
|
return model;
|
|
775
789
|
}
|
|
776
790
|
|
|
791
|
+
function resolveModelForTier(
|
|
792
|
+
tier: ImageQualityTier | undefined,
|
|
793
|
+
category: ImageCategory | undefined,
|
|
794
|
+
): ImageModel | undefined {
|
|
795
|
+
if (!tier) return undefined;
|
|
796
|
+
if (tier === "fast") return "gemini-3.1-flash-image";
|
|
797
|
+
if (tier === "best") return "gemini-3-pro-image";
|
|
798
|
+
return ["hero", "landing", "logo", "campaign"].includes(category ?? "")
|
|
799
|
+
? "gemini-3-pro-image"
|
|
800
|
+
: "gemini-3.1-flash-image";
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function resolveImageModelForRequest(input: {
|
|
804
|
+
explicitModel?: ImageModel;
|
|
805
|
+
imageModelDefault?: ImageModel;
|
|
806
|
+
explicitTier?: ImageQualityTier;
|
|
807
|
+
resolvedTier?: ImageQualityTier;
|
|
808
|
+
category?: ImageCategory;
|
|
809
|
+
presetModel?: ImageModel | null;
|
|
810
|
+
embeddedText?: string | null;
|
|
811
|
+
}): ImageModel {
|
|
812
|
+
if (input.explicitModel) return input.explicitModel;
|
|
813
|
+
|
|
814
|
+
// Exact embedded text is materially better on Gemini Pro, so upgrade to it by
|
|
815
|
+
// default for text requests — but never override a model or tier the caller
|
|
816
|
+
// or a tagged preset already chose (presets "own" model/tier per the action
|
|
817
|
+
// docs). Only auto-upgrade when there is no tier (explicit or preset-derived)
|
|
818
|
+
// and no preset model in play; this still upgrades a bare text request over
|
|
819
|
+
// the composer default.
|
|
820
|
+
const textAccurateModel: ImageModel | undefined =
|
|
821
|
+
input.embeddedText?.trim() &&
|
|
822
|
+
!input.explicitTier &&
|
|
823
|
+
!input.resolvedTier &&
|
|
824
|
+
!input.presetModel
|
|
825
|
+
? "gemini-3-pro-image"
|
|
826
|
+
: undefined;
|
|
827
|
+
|
|
828
|
+
// Precedence: explicit per-request model (handled above) > embedded-text
|
|
829
|
+
// upgrade > an EXPLICIT per-request tier > the preset's saved model > the
|
|
830
|
+
// preset-DERIVED tier mapping > the sticky composer default > the floor.
|
|
831
|
+
//
|
|
832
|
+
// Two subtleties:
|
|
833
|
+
// - An explicit per-request tier outranks the preset's saved model (the
|
|
834
|
+
// caller is deliberately overriding the preset this turn).
|
|
835
|
+
// - The preset's explicit saved model outranks its OWN derived tier: a
|
|
836
|
+
// preset's `model` column and `settings.tier` can drift out of sync
|
|
837
|
+
// (update-generation-preset can change one without the other), and the
|
|
838
|
+
// explicitly saved model is the authoritative choice.
|
|
839
|
+
// - The composer default ranks LAST of the real signals: it is a global
|
|
840
|
+
// stored preference and must not defeat a tagged preset or a tier request.
|
|
841
|
+
return (
|
|
842
|
+
textAccurateModel ??
|
|
843
|
+
resolveModelForTier(input.explicitTier, input.category) ??
|
|
844
|
+
input.presetModel ??
|
|
845
|
+
resolveModelForTier(input.resolvedTier, input.category) ??
|
|
846
|
+
input.imageModelDefault ??
|
|
847
|
+
"gemini-3.1-flash-image"
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
777
851
|
function toBuilderReferenceRole(role: string) {
|
|
778
852
|
switch (role) {
|
|
779
853
|
case "style_reference":
|
|
@@ -799,6 +873,8 @@ export function compilePrompt(input: {
|
|
|
799
873
|
styleBrief: StyleBrief;
|
|
800
874
|
customInstructions?: string | null;
|
|
801
875
|
prompt: string;
|
|
876
|
+
embeddedText?: string | null;
|
|
877
|
+
textPlacement?: string | null;
|
|
802
878
|
referenceCount: number;
|
|
803
879
|
includeLogo: boolean;
|
|
804
880
|
aspectRatio?: AspectRatio;
|
|
@@ -815,6 +891,11 @@ export function compilePrompt(input: {
|
|
|
815
891
|
const doNot = style.doNot?.length
|
|
816
892
|
? `\nAvoid: ${style.doNot.join("; ")}.`
|
|
817
893
|
: "";
|
|
894
|
+
const typographyBlock = formatBrandTypography(style);
|
|
895
|
+
const requestedEmbeddedText = hasEmbeddedText(input.embeddedText);
|
|
896
|
+
const textInstruction = requestedEmbeddedText
|
|
897
|
+
? formatEmbeddedTextInstruction(input.embeddedText, input.textPlacement)
|
|
898
|
+
: "Do not render headlines, body text, UI labels, or prompt wording inside the image unless the user explicitly asks for exact visible text.";
|
|
818
899
|
const logoInstruction = input.includeLogo
|
|
819
900
|
? "\nLeave a clean uncluttered area in the upper-right for the real brand logo; do not draw or approximate the logo yourself."
|
|
820
901
|
: "";
|
|
@@ -839,14 +920,24 @@ export function compilePrompt(input: {
|
|
|
839
920
|
: "No reference images are attached for this run. Use the style brief and custom instructions as the source of truth.";
|
|
840
921
|
|
|
841
922
|
if (intent === "edit") {
|
|
923
|
+
const editTypographyInstruction =
|
|
924
|
+
requestedEmbeddedText && typographyBlock ? `\n\n${typographyBlock}` : "";
|
|
925
|
+
const editTextInstruction = requestedEmbeddedText
|
|
926
|
+
? `\n\n${formatEmbeddedTextInstruction(input.embeddedText, input.textPlacement)}`
|
|
927
|
+
: "";
|
|
928
|
+
const editConstraint = requestedEmbeddedText
|
|
929
|
+
? "Do not reimagine the image, change its aspect ratio, or alter unrelated subjects. Do not add any other new text. Return the full image."
|
|
930
|
+
: "Do not reimagine the image, change its aspect ratio, add new text, or alter unrelated subjects. Return the full image.";
|
|
842
931
|
return `Edit the attached image for the "${input.libraryTitle}" asset library.
|
|
843
932
|
|
|
844
933
|
${referenceInstruction}
|
|
934
|
+
${editTypographyInstruction}
|
|
845
935
|
|
|
846
936
|
Make only this change:
|
|
847
937
|
${input.prompt}
|
|
938
|
+
${editTextInstruction}
|
|
848
939
|
|
|
849
|
-
|
|
940
|
+
${editConstraint}`;
|
|
850
941
|
}
|
|
851
942
|
|
|
852
943
|
return `Create a brand-consistent image for the "${input.libraryTitle}" asset library.
|
|
@@ -861,15 +952,48 @@ ${style.subjectMatter ? `\nSubject matter: ${style.subjectMatter}.` : ""}
|
|
|
861
952
|
${style.texture ? `\nTexture/material treatment: ${style.texture}.` : ""}
|
|
862
953
|
${style.composition ? `\nComposition: ${style.composition}.` : ""}
|
|
863
954
|
${style.lighting ? `\nLighting: ${style.lighting}.` : ""}
|
|
864
|
-
${
|
|
955
|
+
${typographyBlock ? `\n${typographyBlock}` : ""}${frameInstruction}
|
|
865
956
|
${doNot}${logoInstruction}${diagramInstruction}${customInstructions}
|
|
866
957
|
|
|
867
|
-
|
|
958
|
+
${textInstruction}
|
|
868
959
|
|
|
869
960
|
User request:
|
|
870
961
|
${input.prompt}`;
|
|
871
962
|
}
|
|
872
963
|
|
|
964
|
+
function hasEmbeddedText(value?: string | null): boolean {
|
|
965
|
+
return typeof value === "string" && !!value.trim();
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function formatEmbeddedTextInstruction(
|
|
969
|
+
embeddedText?: string | null,
|
|
970
|
+
textPlacement?: string | null,
|
|
971
|
+
): string {
|
|
972
|
+
const placement = textPlacement?.trim();
|
|
973
|
+
return [
|
|
974
|
+
`Render this text exactly, spelled correctly, inside the image: ${JSON.stringify(embeddedText ?? "")}.`,
|
|
975
|
+
placement ? `Placement: ${placement}.` : "",
|
|
976
|
+
"Match the brand typography described above where specified; keep it legible and well-kerned.",
|
|
977
|
+
]
|
|
978
|
+
.filter(Boolean)
|
|
979
|
+
.join(" ");
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function formatBrandTypography(style: StyleBrief): string {
|
|
983
|
+
const parts = [
|
|
984
|
+
style.fontFamilies?.length
|
|
985
|
+
? `families ${style.fontFamilies.join(", ")}`
|
|
986
|
+
: "",
|
|
987
|
+
style.fontWeights?.length ? `weights ${style.fontWeights.join(", ")}` : "",
|
|
988
|
+
style.letterforms ? `letterforms: ${style.letterforms}` : "",
|
|
989
|
+
style.caseStyle ? `case: ${style.caseStyle}` : "",
|
|
990
|
+
style.typographyPolicy,
|
|
991
|
+
].filter((part): part is string => typeof part === "string" && !!part.trim());
|
|
992
|
+
|
|
993
|
+
if (!parts.length) return "";
|
|
994
|
+
return `Brand typography: ${parts.join("; ")}. Match these for any rendered text.`;
|
|
995
|
+
}
|
|
996
|
+
|
|
873
997
|
// A content-only reference is an image the user attached as subject/content for
|
|
874
998
|
// a single request (not a reusable brand/style reference). It should never count
|
|
875
999
|
// as a brand-kit style reference.
|
|
@@ -112,6 +112,10 @@ export interface StyleBrief {
|
|
|
112
112
|
texture?: string;
|
|
113
113
|
composition?: string;
|
|
114
114
|
lighting?: string;
|
|
115
|
+
fontFamilies?: string[];
|
|
116
|
+
fontWeights?: string[];
|
|
117
|
+
letterforms?: string;
|
|
118
|
+
caseStyle?: string;
|
|
115
119
|
typographyPolicy?: string;
|
|
116
120
|
doNot?: string[];
|
|
117
121
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List pending clips AI requests for the current user.
|
|
3
|
+
*
|
|
4
|
+
* Collapses the per-recording `clips-ai-request-<id>` polling done by the
|
|
5
|
+
* auto-title bridge into a single call. Application state is already
|
|
6
|
+
* session-scoped, and we additionally filter the referenced recordings through
|
|
7
|
+
* `accessFilter` so we never return a request for a recording the user can't
|
|
8
|
+
* access. Only requests for `status = "ready"` recordings are returned — the
|
|
9
|
+
* bridge ignores non-ready recordings, so excluding them avoids sending large
|
|
10
|
+
* transcript blobs for recordings the hook will skip this tick.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { defineAction } from "@agent-native/core";
|
|
14
|
+
import { listAppState } from "@agent-native/core/application-state";
|
|
15
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
16
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
20
|
+
|
|
21
|
+
const REQUEST_PREFIX = "clips-ai-request-";
|
|
22
|
+
|
|
23
|
+
export default defineAction({
|
|
24
|
+
description:
|
|
25
|
+
"List pending clips AI requests (regenerate-title, summary, chapters, etc.) for recordings the current user can access.",
|
|
26
|
+
schema: z.object({}),
|
|
27
|
+
http: { method: "GET" },
|
|
28
|
+
run: async () => {
|
|
29
|
+
const entries = await listAppState(REQUEST_PREFIX);
|
|
30
|
+
|
|
31
|
+
const requests = entries
|
|
32
|
+
.map((e) => e.value as Record<string, unknown>)
|
|
33
|
+
.filter(
|
|
34
|
+
(v): v is Record<string, unknown> & { recordingId: string } =>
|
|
35
|
+
!!v && typeof v.recordingId === "string" && v.recordingId.length > 0,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (requests.length === 0) return { requests: [] };
|
|
39
|
+
|
|
40
|
+
const recordingIds = [...new Set(requests.map((r) => r.recordingId))];
|
|
41
|
+
|
|
42
|
+
const db = getDb();
|
|
43
|
+
const accessible = await db
|
|
44
|
+
.select({ id: schema.recordings.id })
|
|
45
|
+
.from(schema.recordings)
|
|
46
|
+
.where(
|
|
47
|
+
and(
|
|
48
|
+
accessFilter(schema.recordings, schema.recordingShares),
|
|
49
|
+
inArray(schema.recordings.id, recordingIds),
|
|
50
|
+
eq(schema.recordings.status, "ready"),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const accessibleIds = new Set(accessible.map((r) => r.id));
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
requests: requests.filter((r) => accessibleIds.has(r.recordingId)),
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|