@tomako/tools-runtime 0.1.5 → 0.1.6
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/package.json +2 -1
- package/src/components/tools/engine/package-widget-runtime.tsx +5 -1
- package/src/components/tools/engine/task-recovery.test.ts +65 -0
- package/src/components/tools/engine/task-recovery.ts +109 -0
- package/src/components/tools/engine/use-legacy-llm-task.ts +16 -3
- package/src/components/tools/engine/use-tool-task.ts +77 -53
- package/src/features/tools/app-store-screenshot-generator/widget/app-store-screenshot-generator.tsx +44 -6
- package/src/features/tools/app-store-screenshot-generator/widget/generation.test.ts +126 -0
- package/src/features/tools/app-store-screenshot-generator/widget/generation.ts +52 -14
- package/src/features/tools/cold-start-channel-selector/widget/cold-start-channel-selector.tsx +45 -16
- package/src/features/tools/competitor-analysis/competitor-analysis.container.tsx +26 -20
- package/src/features/tools/competitor-analysis/widget/competitor-analysis.tsx +47 -16
- package/src/features/tools/disclaimer-generator/disclaimer-generator.package.ts +3 -3
- package/src/features/tools/email-template-generator/widget/email-template-generator.tsx +37 -4
- package/src/features/tools/package/to-tool-module.ts +15 -0
- package/src/features/tools/product-name-generator/widget/product-name-generator.tsx +60 -27
- package/src/features/tools/product-poster-generator/widget/constants.ts +3 -1
- package/src/features/tools/product-poster-generator/widget/helpers.test.ts +51 -0
- package/src/features/tools/product-poster-generator/widget/helpers.ts +27 -0
- package/src/features/tools/product-poster-generator/widget/product-poster-generator.tsx +112 -12
- package/src/features/tools/slogan-generator/slogan-generator.package.ts +28 -28
- package/src/i18n/messages/en/tools/app-icon-resizer.ts +1 -1
- package/src/i18n/messages/en/tools/app-store-screenshot-generator.ts +2 -2
- package/src/i18n/messages/en/tools/cold-start-channel-selector.ts +4 -4
- package/src/i18n/messages/en/tools/competitor-analysis.ts +9 -8
- package/src/i18n/messages/en/tools/email-template-generator.ts +10 -10
- package/src/i18n/messages/en/tools/product-poster-generator.ts +1 -0
- package/src/i18n/messages/zh/tools/app-store-screenshot-generator.ts +3 -3
- package/src/i18n/messages/zh/tools/cold-start-channel-selector.ts +2 -2
- package/src/i18n/messages/zh/tools/competitor-analysis.ts +10 -9
- package/src/i18n/messages/zh/tools/email-template-generator.ts +11 -11
- package/src/i18n/messages/zh/tools/product-poster-generator.ts +1 -0
- package/src/i18n/messages/zh-tw/tools/app-store-screenshot-generator.ts +44 -44
- package/src/i18n/messages/zh-tw/tools/cold-start-channel-selector.ts +105 -105
- package/src/i18n/messages/zh-tw/tools/competitor-analysis.ts +41 -40
- package/src/i18n/messages/zh-tw/tools/email-template-generator.ts +111 -111
- package/src/i18n/messages/zh-tw/tools/product-name-generator.ts +92 -92
- package/src/i18n/messages/zh-tw/tools/product-poster-generator.ts +74 -73
- package/src/i18n/messages/zh-tw/tools/twitter-gif-downloader.ts +47 -47
- package/src/lib/tools/cold-start-channel-selector-schema.test.ts +39 -0
- package/src/lib/tools/cold-start-channel-selector-schema.ts +8 -4
- package/src/lib/tools/competitor-analysis-schema.test.ts +69 -0
- package/src/lib/tools/competitor-analysis-schema.ts +53 -5
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { fetchProductPosterPng } from "./helpers";
|
|
5
|
+
|
|
6
|
+
test("fetchProductPosterPng fetches through the image proxy and returns a .png download", async () => {
|
|
7
|
+
let requestedUrl = "";
|
|
8
|
+
let requestedInit: RequestInit | undefined;
|
|
9
|
+
const fetcher = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
10
|
+
requestedUrl = String(input);
|
|
11
|
+
requestedInit = init;
|
|
12
|
+
return new Response(new Uint8Array([137, 80, 78, 71]), {
|
|
13
|
+
status: 200,
|
|
14
|
+
headers: { "content-type": "image/png" },
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const file = await fetchProductPosterPng(
|
|
19
|
+
"https://cdn.example.com/generated/poster.png",
|
|
20
|
+
"example-product",
|
|
21
|
+
fetcher,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
assert.equal(
|
|
25
|
+
requestedUrl,
|
|
26
|
+
"/tools-api/image-download?url=https%3A%2F%2Fcdn.example.com%2Fgenerated%2Fposter.png",
|
|
27
|
+
);
|
|
28
|
+
assert.equal(requestedInit?.cache, "no-store");
|
|
29
|
+
assert.equal(file.fileName, "example-product-poster.png");
|
|
30
|
+
assert.equal(file.blob.type, "image/png");
|
|
31
|
+
assert.equal(file.blob.size, 4);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("fetchProductPosterPng rejects failed or non-PNG responses", async () => {
|
|
35
|
+
await assert.rejects(
|
|
36
|
+
fetchProductPosterPng("https://cdn.example.com/poster.png", "poster", async () =>
|
|
37
|
+
new Response(null, { status: 502 }),
|
|
38
|
+
),
|
|
39
|
+
/Poster download failed: 502/,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
await assert.rejects(
|
|
43
|
+
fetchProductPosterPng("https://cdn.example.com/poster.webp", "poster", async () =>
|
|
44
|
+
new Response(new Uint8Array([1]), {
|
|
45
|
+
status: 200,
|
|
46
|
+
headers: { "content-type": "image/webp" },
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
/Poster download returned image\/webp/,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ProductPosterGeneratorInput,
|
|
6
6
|
ProductPosterScene,
|
|
7
7
|
} from "../../../../lib/tools/product-poster-generator-schema";
|
|
8
|
+
import { imageDownloadUrl } from "../../../../lib/tools/image-download";
|
|
8
9
|
|
|
9
10
|
import { BRAND_COLOR_PRESETS, POSTER_INTENT_OPTIONS } from "./constants";
|
|
10
11
|
import type {
|
|
@@ -65,6 +66,32 @@ export function slugifyFileName(value: string) {
|
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
export async function fetchProductPosterPng(
|
|
70
|
+
imageUrl: string,
|
|
71
|
+
fileBaseName: string,
|
|
72
|
+
fetcher: typeof fetch = fetch,
|
|
73
|
+
) {
|
|
74
|
+
const response = await fetcher(imageDownloadUrl(imageUrl), { cache: "no-store" });
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`Poster download failed: ${response.status}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
80
|
+
if (!contentType.startsWith("image/png")) {
|
|
81
|
+
throw new Error(`Poster download returned ${contentType || "an unknown content type"}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const blob = await response.blob();
|
|
85
|
+
if (blob.size === 0) {
|
|
86
|
+
throw new Error("Poster download returned an empty PNG");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
blob,
|
|
91
|
+
fileName: `${fileBaseName}-poster.png`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
export function sizeToDimensions(size: string) {
|
|
69
96
|
const [width, height] = size.split("x").map((part) => Number(part));
|
|
70
97
|
return {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { ChevronDown, FileImage } from "lucide-react";
|
|
4
4
|
import {
|
|
5
5
|
useMemo,
|
|
6
|
+
useEffect,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
8
9
|
type ChangeEvent,
|
|
@@ -22,8 +23,10 @@ import {
|
|
|
22
23
|
PRODUCT_POSTER_STYLES,
|
|
23
24
|
productPosterGeneratorInputSchema,
|
|
24
25
|
type ProductPosterFormat,
|
|
26
|
+
type ProductPosterGeneratorInput,
|
|
25
27
|
type ProductPosterStyle,
|
|
26
28
|
} from "../../../../lib/tools/product-poster-generator-schema";
|
|
29
|
+
import { downloadBlob } from "../../../../lib/file/download";
|
|
27
30
|
import { cn } from "@tomako/ui/cn";
|
|
28
31
|
import {
|
|
29
32
|
fetchProductPosterGenerationTask,
|
|
@@ -50,6 +53,11 @@ import {
|
|
|
50
53
|
toolTextareaClass,
|
|
51
54
|
type ToolLoadingStep,
|
|
52
55
|
} from "../../../../components/tools/workspace";
|
|
56
|
+
import {
|
|
57
|
+
clearToolTaskRecovery,
|
|
58
|
+
persistToolTaskRecovery,
|
|
59
|
+
readToolTaskRecovery,
|
|
60
|
+
} from "../../../../components/tools/engine/task-recovery";
|
|
53
61
|
import {
|
|
54
62
|
MAX_TASK_POLL_ATTEMPTS,
|
|
55
63
|
POSTER_FORMAT_OPTIONS,
|
|
@@ -70,6 +78,7 @@ import {
|
|
|
70
78
|
getPosterIntentId,
|
|
71
79
|
getPosterResultError,
|
|
72
80
|
getSafeErrorMessage,
|
|
81
|
+
fetchProductPosterPng,
|
|
73
82
|
loadingProgress,
|
|
74
83
|
loadingStepIndex,
|
|
75
84
|
readImageAsDataUrl,
|
|
@@ -86,6 +95,13 @@ import type {
|
|
|
86
95
|
ResultState,
|
|
87
96
|
} from "./types";
|
|
88
97
|
|
|
98
|
+
const RECOVERY_SLUG = "product-poster-generator";
|
|
99
|
+
|
|
100
|
+
type PosterTaskRecoveryInput = {
|
|
101
|
+
input: ProductPosterGeneratorInput;
|
|
102
|
+
settingsSummary: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
89
105
|
export function ProductPosterGenerator() {
|
|
90
106
|
const locale = useLocale();
|
|
91
107
|
const taskLocale = locale as AppLocale;
|
|
@@ -99,6 +115,8 @@ export function ProductPosterGenerator() {
|
|
|
99
115
|
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
|
100
116
|
const [formError, setFormError] = useState<string | null>(null);
|
|
101
117
|
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
118
|
+
const [downloading, setDownloading] = useState(false);
|
|
119
|
+
const [downloadError, setDownloadError] = useState<string | null>(null);
|
|
102
120
|
|
|
103
121
|
const formatLabels = t.raw("formatLabels") as Record<ProductPosterFormat, string>;
|
|
104
122
|
const styleLabels = t.raw("styleLabels") as Record<ProductPosterStyle, string>;
|
|
@@ -158,6 +176,39 @@ export function ProductPosterGenerator() {
|
|
|
158
176
|
|
|
159
177
|
const busy = result.phase === "submitting" || result.phase === "polling";
|
|
160
178
|
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
const recovered = readToolTaskRecovery<PosterTaskRecoveryInput>(RECOVERY_SLUG, {
|
|
181
|
+
parseInput: (value) => {
|
|
182
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
183
|
+
const candidate = value as Partial<PosterTaskRecoveryInput>;
|
|
184
|
+
const parsed = productPosterGeneratorInputSchema.safeParse(candidate.input);
|
|
185
|
+
if (!parsed.success || typeof candidate.settingsSummary !== "string") return null;
|
|
186
|
+
return { input: parsed.data, settingsSummary: candidate.settingsSummary };
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
if (!recovered) return;
|
|
190
|
+
const generationId = generationSeqRef.current + 1;
|
|
191
|
+
generationSeqRef.current = generationId;
|
|
192
|
+
setForm({
|
|
193
|
+
...defaultForm,
|
|
194
|
+
...recovered.input.input,
|
|
195
|
+
screenshotNames: [],
|
|
196
|
+
screenshotDataUrls: [],
|
|
197
|
+
logoName: undefined,
|
|
198
|
+
logoDataUrl: undefined,
|
|
199
|
+
});
|
|
200
|
+
void watchPosterTask(
|
|
201
|
+
recovered.taskId,
|
|
202
|
+
recovered.input.input,
|
|
203
|
+
recovered.input.settingsSummary,
|
|
204
|
+
generationId,
|
|
205
|
+
recovered.startedAtMs,
|
|
206
|
+
"RESTORING",
|
|
207
|
+
);
|
|
208
|
+
// Restore only once for this mounted tool route.
|
|
209
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
161
212
|
function scrollToResult() {
|
|
162
213
|
window.requestAnimationFrame(() => {
|
|
163
214
|
resultRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
@@ -289,6 +340,7 @@ export function ProductPosterGenerator() {
|
|
|
289
340
|
}
|
|
290
341
|
|
|
291
342
|
setFormError(null);
|
|
343
|
+
setDownloadError(null);
|
|
292
344
|
setFieldErrors({});
|
|
293
345
|
const generationId = generationSeqRef.current + 1;
|
|
294
346
|
generationSeqRef.current = generationId;
|
|
@@ -320,9 +372,39 @@ export function ProductPosterGenerator() {
|
|
|
320
372
|
if (generationSeqRef.current !== generationId) return;
|
|
321
373
|
|
|
322
374
|
const taskId = submitted.taskId;
|
|
323
|
-
|
|
375
|
+
const startedAtMs = Date.now();
|
|
376
|
+
persistToolTaskRecovery<PosterTaskRecoveryInput>(
|
|
377
|
+
RECOVERY_SLUG,
|
|
378
|
+
taskId,
|
|
379
|
+
{ input: posterInput, settingsSummary: posterSettingsSummary },
|
|
380
|
+
startedAtMs,
|
|
381
|
+
);
|
|
382
|
+
await watchPosterTask(
|
|
383
|
+
taskId,
|
|
384
|
+
posterInput,
|
|
385
|
+
posterSettingsSummary,
|
|
386
|
+
generationId,
|
|
387
|
+
startedAtMs,
|
|
388
|
+
submitted.status,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
324
391
|
|
|
325
|
-
|
|
392
|
+
async function watchPosterTask(
|
|
393
|
+
taskId: string,
|
|
394
|
+
posterInput: ProductPosterGeneratorInput,
|
|
395
|
+
posterSettingsSummary: string,
|
|
396
|
+
generationId: number,
|
|
397
|
+
startedAtMs: number,
|
|
398
|
+
initialStatus: string,
|
|
399
|
+
) {
|
|
400
|
+
setResult({ phase: "polling", taskId, status: initialStatus });
|
|
401
|
+
const elapsedAttempts = Math.max(
|
|
402
|
+
0,
|
|
403
|
+
Math.floor((Date.now() - startedAtMs) / TASK_POLL_INTERVAL_MS),
|
|
404
|
+
);
|
|
405
|
+
const remainingAttempts = Math.max(1, MAX_TASK_POLL_ATTEMPTS - elapsedAttempts);
|
|
406
|
+
|
|
407
|
+
for (let attempt = 0; attempt < remainingAttempts; attempt += 1) {
|
|
326
408
|
try {
|
|
327
409
|
const task = await fetchProductPosterGenerationTask(taskId);
|
|
328
410
|
if (generationSeqRef.current !== generationId) return;
|
|
@@ -376,24 +458,40 @@ export function ProductPosterGenerator() {
|
|
|
376
458
|
}
|
|
377
459
|
}
|
|
378
460
|
|
|
379
|
-
function downloadPosterPng() {
|
|
380
|
-
if (result.phase !== "ready") return;
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
461
|
+
async function downloadPosterPng() {
|
|
462
|
+
if (result.phase !== "ready" || downloading) return;
|
|
463
|
+
const generationId = generationSeqRef.current;
|
|
464
|
+
setDownloading(true);
|
|
465
|
+
setDownloadError(null);
|
|
466
|
+
try {
|
|
467
|
+
const file = await fetchProductPosterPng(
|
|
468
|
+
result.data.imageUrl,
|
|
469
|
+
result.data.fileBaseName,
|
|
470
|
+
);
|
|
471
|
+
if (generationSeqRef.current !== generationId) return;
|
|
472
|
+
downloadBlob(file.blob, file.fileName);
|
|
473
|
+
} catch {
|
|
474
|
+
if (generationSeqRef.current === generationId) {
|
|
475
|
+
setDownloadError(t("errors.downloadFailed"));
|
|
476
|
+
}
|
|
477
|
+
} finally {
|
|
478
|
+
if (generationSeqRef.current === generationId) {
|
|
479
|
+
setDownloading(false);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
387
482
|
}
|
|
388
483
|
|
|
389
484
|
function reset() {
|
|
390
485
|
generationSeqRef.current += 1;
|
|
486
|
+
clearToolTaskRecovery(RECOVERY_SLUG);
|
|
391
487
|
if (logoInputRef.current) logoInputRef.current.value = "";
|
|
392
488
|
if (screenshotInputRef.current) screenshotInputRef.current.value = "";
|
|
393
489
|
setForm(defaultForm);
|
|
394
490
|
setResult({ phase: "idle" });
|
|
395
491
|
setFieldErrors({});
|
|
396
492
|
setFormError(null);
|
|
493
|
+
setDownloadError(null);
|
|
494
|
+
setDownloading(false);
|
|
397
495
|
setAdvancedOpen(false);
|
|
398
496
|
}
|
|
399
497
|
|
|
@@ -599,14 +697,16 @@ export function ProductPosterGenerator() {
|
|
|
599
697
|
body={result.data.summary ?? t("readyHint")}
|
|
600
698
|
actions={
|
|
601
699
|
<ToolResultActions
|
|
602
|
-
downloadLabel={t("downloadPng")}
|
|
700
|
+
downloadLabel={downloading ? t("loadingSteps.finish") : t("downloadPng")}
|
|
603
701
|
resetLabel={t("reset")}
|
|
604
|
-
onDownload={downloadPosterPng}
|
|
702
|
+
onDownload={() => void downloadPosterPng()}
|
|
605
703
|
onReset={reset}
|
|
606
704
|
/>
|
|
607
705
|
}
|
|
608
706
|
/>
|
|
609
707
|
|
|
708
|
+
{downloadError ? <ToolErrorBanner>{downloadError}</ToolErrorBanner> : null}
|
|
709
|
+
|
|
610
710
|
<figure className="flex min-h-[28rem] items-center justify-center overflow-hidden rounded-[1.25rem] border border-[#E4E9F1] bg-white p-3">
|
|
611
711
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
612
712
|
<img
|
|
@@ -6,7 +6,7 @@ import type { ToolPackage } from "../package/types";
|
|
|
6
6
|
* Slogan Generator(direct 车道)。
|
|
7
7
|
* 证据:docs/seo-keyword-briefs/slogan-generator.md(slogan generator 12,100/mo LOW + 竞品拆解)。
|
|
8
8
|
* 反超点(相对 logo.com / ahrefs / shopify / copy.ai / grammarly):
|
|
9
|
-
*
|
|
9
|
+
* 登录后在页面内直出;每条候选带风格标签 + why-it-works 理由(ahrefs 只有裸文本);
|
|
10
10
|
* 一次 10 条覆盖多风格;输出附挑选/测试建议;支持可选品牌名生成 name-based slogan。
|
|
11
11
|
* scene "slogan_generator" 需在 168 compose 的 LLM_DIRECT_ALLOWED_SCENES 白名单内。
|
|
12
12
|
*/
|
|
@@ -41,12 +41,12 @@ export const sloganGeneratorPackage: ToolPackage = {
|
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
copy: {
|
|
44
|
-
title: { en: "Slogan Generator", zh: "Slogan 广告语生成器", "zh-tw": "Slogan
|
|
44
|
+
title: { en: "AI Slogan Generator for Business", zh: "AI Slogan 广告语生成器", "zh-tw": "AI Slogan 廣告語產生器" },
|
|
45
45
|
description: {
|
|
46
|
-
en: "
|
|
47
|
-
zh: "
|
|
46
|
+
en: "Describe your business and get 10 AI slogan and tagline ideas across distinct styles, each with a why-it-works note. Sign in to generate and compare the full set.",
|
|
47
|
+
zh: "描述你的业务,一次生成 10 条 Slogan 和 tagline 候选,覆盖大胆、极简、俏皮、利益导向等多种风格,每条附风格标签和入选理由。登录后可在本页生成并比较完整结果。",
|
|
48
48
|
"zh-tw":
|
|
49
|
-
"
|
|
49
|
+
"描述你的業務,一次產生 10 條 Slogan 和 tagline 候選,涵蓋大膽、極簡、俏皮、利益導向等多種風格,每條附風格標籤和入選理由。登入後可在本頁產生並比較完整結果。",
|
|
50
50
|
},
|
|
51
51
|
intro: {
|
|
52
52
|
en: "A good slogan is 3-6 words that carry your brand promise. Instead of staring at a blank page or paying an agency, describe what you do and compare structured candidates side by side — with reasons, not just word salad.",
|
|
@@ -60,9 +60,9 @@ export const sloganGeneratorPackage: ToolPackage = {
|
|
|
60
60
|
"zh-tw": "品牌工具",
|
|
61
61
|
},
|
|
62
62
|
keywords: {
|
|
63
|
-
en: ["slogan generator", "slogan creator", "slogan maker", "
|
|
64
|
-
zh: ["slogan 生成器", "slogan 制作", "广告语生成器", "品牌口号生成", "tagline 生成器", "
|
|
65
|
-
"zh-tw": ["slogan
|
|
63
|
+
en: ["slogan generator", "slogan creator", "slogan maker", "ai slogan generator", "tagline generator", "business slogan generator"],
|
|
64
|
+
zh: ["slogan 生成器", "slogan 制作", "广告语生成器", "品牌口号生成", "tagline 生成器", "AI 广告语"],
|
|
65
|
+
"zh-tw": ["slogan 產生器", "slogan 製作", "廣告語產生器", "品牌口號產生", "tagline 產生器", "AI 廣告語"],
|
|
66
66
|
},
|
|
67
67
|
inputSignal: {
|
|
68
68
|
en: "One-sentence business description + optional brand name",
|
|
@@ -214,13 +214,13 @@ Quality requirements:
|
|
|
214
214
|
zh: "一句说清业务就够了。品牌名、目标人群和风格方向会让候选更贴合。",
|
|
215
215
|
"zh-tw": "一句說清業務就夠了。品牌名、目標人群和風格方向會讓候選更貼合。",
|
|
216
216
|
},
|
|
217
|
-
submitLabel: { en: "Generate 10 slogans", zh: "生成 10 条 Slogan", "zh-tw": "
|
|
217
|
+
submitLabel: { en: "Generate 10 slogans", zh: "生成 10 条 Slogan", "zh-tw": "產生 10 條 Slogan" },
|
|
218
218
|
resultTitle: { en: "Compare your candidates", zh: "横向比较候选", "zh-tw": "橫向比較候選" },
|
|
219
|
-
pendingTitle: { en: "Writing your slogans", zh: "正在生成 Slogan", "zh-tw": "
|
|
219
|
+
pendingTitle: { en: "Writing your slogans", zh: "正在生成 Slogan", "zh-tw": "正在產生 Slogan" },
|
|
220
220
|
pendingHint: {
|
|
221
221
|
en: "Usually takes 15-30 seconds. Candidates across multiple styles are being written and annotated.",
|
|
222
222
|
zh: "通常需要 15-30 秒。正在生成多风格候选并为每条写入选理由。",
|
|
223
|
-
"zh-tw": "通常需要 15-30
|
|
223
|
+
"zh-tw": "通常需要 15-30 秒。正在產生多風格候選,並為每條寫入入選理由。",
|
|
224
224
|
},
|
|
225
225
|
errorOverrides: {
|
|
226
226
|
validation: {
|
|
@@ -236,16 +236,16 @@ Quality requirements:
|
|
|
236
236
|
page: {
|
|
237
237
|
workspace: {
|
|
238
238
|
heading: {
|
|
239
|
-
en: "
|
|
240
|
-
zh: "
|
|
241
|
-
"zh-tw": "
|
|
239
|
+
en: "AI slogan generator: 10 ideas worth comparing",
|
|
240
|
+
zh: "AI Slogan 生成器:一次拿到 10 条值得比较的候选",
|
|
241
|
+
"zh-tw": "AI Slogan 產生器:一次取得 10 條值得比較的候選",
|
|
242
242
|
},
|
|
243
243
|
hint: {
|
|
244
244
|
en: "Describe the business in one or two sentences, optionally add the brand name and audience. You get 10 candidates across multiple styles, each with a style tag and a note on why the line works — ready to shortlist, test, and refine.",
|
|
245
245
|
zh: "用一两句话描述业务,可选填品牌名和目标人群。你会得到覆盖多种风格的 10 条候选,每条带风格标签和入选理由——直接进入筛选、测试和打磨。",
|
|
246
246
|
"zh-tw": "用一兩句話描述業務,可選填品牌名和目標人群。你會得到覆蓋多種風格的 10 條候選,每條帶風格標籤和入選理由——直接進入篩選、測試和打磨。",
|
|
247
247
|
},
|
|
248
|
-
badge: { en: "
|
|
248
|
+
badge: { en: "Sign in to generate", zh: "登录后生成", "zh-tw": "登入後產生" },
|
|
249
249
|
},
|
|
250
250
|
standard: {
|
|
251
251
|
benefits: {
|
|
@@ -541,17 +541,17 @@ Quality requirements:
|
|
|
541
541
|
{
|
|
542
542
|
kind: "cards",
|
|
543
543
|
heading: {
|
|
544
|
-
en: "Why this beats
|
|
545
|
-
zh: "
|
|
546
|
-
"zh-tw": "
|
|
544
|
+
en: "Why this beats basic slogan lists",
|
|
545
|
+
zh: "为什么比常见的 slogan 列表更好用",
|
|
546
|
+
"zh-tw": "為什麼比常見的 slogan 清單更好用",
|
|
547
547
|
},
|
|
548
548
|
items: [
|
|
549
549
|
{
|
|
550
|
-
title: { en: "
|
|
550
|
+
title: { en: "Full results on this page", zh: "完整结果在本页展示", "zh-tw": "完整結果在本頁顯示" },
|
|
551
551
|
body: {
|
|
552
|
-
en: "
|
|
553
|
-
zh: "
|
|
554
|
-
"zh-tw": "
|
|
552
|
+
en: "After you sign in once, the complete candidate set renders directly below the form instead of sending you to a separate editor or workflow.",
|
|
553
|
+
zh: "登录后,完整候选会直接显示在表单下方,不会再把你带到另一个编辑器或流程。",
|
|
554
|
+
"zh-tw": "登入後,完整候選會直接顯示在表單下方,不會再把你帶到另一個編輯器或流程。",
|
|
555
555
|
},
|
|
556
556
|
},
|
|
557
557
|
{
|
|
@@ -677,14 +677,14 @@ Quality requirements:
|
|
|
677
677
|
},
|
|
678
678
|
{
|
|
679
679
|
question: {
|
|
680
|
-
en: "
|
|
681
|
-
zh: "
|
|
682
|
-
"zh-tw": "
|
|
680
|
+
en: "Do I need an account to generate slogans?",
|
|
681
|
+
zh: "生成 Slogan 需要登录吗?",
|
|
682
|
+
"zh-tw": "產生 Slogan 需要登入嗎?",
|
|
683
683
|
},
|
|
684
684
|
answer: {
|
|
685
|
-
en: "Yes
|
|
686
|
-
zh: "
|
|
687
|
-
"zh-tw": "
|
|
685
|
+
en: "Yes. Sign in before generation so the model request can run securely. After sign-in, the complete candidate set and its review notes render on this page.",
|
|
686
|
+
zh: "需要。生成前请先登录,以便安全运行模型请求;登录后,完整候选和复核说明会直接显示在本页。",
|
|
687
|
+
"zh-tw": "需要。產生前請先登入,以便安全執行模型請求;登入後,完整候選與複核說明會直接顯示在本頁。",
|
|
688
688
|
},
|
|
689
689
|
},
|
|
690
690
|
],
|
|
@@ -21,7 +21,7 @@ const appIconResizer = {
|
|
|
21
21
|
guide: {
|
|
22
22
|
heroVisualAlt:
|
|
23
23
|
"App icon asset package visual showing one source icon exported into store submission, Xcode, and Android launcher files.",
|
|
24
|
-
workspaceHeading: "App
|
|
24
|
+
workspaceHeading: "App Icon Resizer and Asset Package",
|
|
25
25
|
workspaceHint:
|
|
26
26
|
"One source, one local ZIP for store and engineering handoff.",
|
|
27
27
|
workspaceBadge: "PNG + ZIP",
|
|
@@ -18,7 +18,7 @@ const appStoreScreenshotGenerator = {
|
|
|
18
18
|
"Hero visual for an app store listing image generator, showing a complete app store screenshot set.",
|
|
19
19
|
workspaceHeading: "App Store Screenshot Generator",
|
|
20
20
|
workspaceHint:
|
|
21
|
-
"Upload real UI screenshots.
|
|
21
|
+
"Upload real UI screenshots. An online image generation workflow reads supporting context, plans the complete set, and submits live image tasks.",
|
|
22
22
|
whatYouGetHeading: "What you get after generation",
|
|
23
23
|
valueVisualAlt:
|
|
24
24
|
"App store listing image export bundle with phone screenshots, a feature graphic, PNG files, and SVG files.",
|
|
@@ -78,7 +78,7 @@ const appStoreScreenshotGenerator = {
|
|
|
78
78
|
supportBody: [
|
|
79
79
|
"You must upload local UI screenshots. Product URL, App Store or Google Play links, product name, and selling points provide supporting context for title, description, and page-summary clues.",
|
|
80
80
|
"Apple App Store and Google Play directions are supported, including complete screenshot sets, portrait screenshots, and feature graphics.",
|
|
81
|
-
"This version uses
|
|
81
|
+
"This version uses an online image generation workflow to submit live image tasks, but it does not connect to App Store Connect or Google Play Console.",
|
|
82
82
|
"If a screenshot includes test accounts, real users, private data, or unauthorized brands, redact it before use.",
|
|
83
83
|
"If no result appears, check that the URL is not empty, the uploaded file is PNG/JPG/WebP, browser file reading is allowed, and the selected input source matches your data.",
|
|
84
84
|
"Before real submission, review the latest platform dimensions, review policies, trademark or copyright permissions, privacy content, copy claims, and readability on target devices.",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const coldStartChannelSelector = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "
|
|
4
|
-
description: "
|
|
5
|
-
intro: "Choose the first
|
|
3
|
+
title: "Customer Acquisition Channel Selector",
|
|
4
|
+
description: "Choose customer acquisition channels to test first. Add your product, audience, time, and budget to get ranked options, first actions, and validation signals.",
|
|
5
|
+
intro: "Choose the first customer acquisition channel to test, then use real signals to decide what comes next.",
|
|
6
6
|
category: "Growth strategy tools",
|
|
7
7
|
keywords: [
|
|
8
8
|
"customer acquisition channels",
|
|
@@ -33,7 +33,7 @@ const coldStartChannelSelector = {
|
|
|
33
33
|
},
|
|
34
34
|
],
|
|
35
35
|
guide: {
|
|
36
|
-
workspaceTitle: "
|
|
36
|
+
workspaceTitle: "Choose the First Customer Acquisition Channels to Test",
|
|
37
37
|
workspaceHint:
|
|
38
38
|
"Add your product, customer, time, and budget. Get a growth channel strategy to test, why it fits, and the signal that tells you what to do next.",
|
|
39
39
|
workspaceBadge: "Early-stage growth channel strategy",
|
|
@@ -37,10 +37,10 @@ const competitorAnalysis = {
|
|
|
37
37
|
running: "Researching…",
|
|
38
38
|
emptyTitle: "The competitor profile will appear here",
|
|
39
39
|
emptyHint:
|
|
40
|
-
"Enter a competitor URL and
|
|
40
|
+
"Enter a competitor URL and an online research workflow turns public signals into a structured profile.",
|
|
41
41
|
runningTitle: "Researching this competitor",
|
|
42
42
|
runningHint:
|
|
43
|
-
"The
|
|
43
|
+
"The research workflow is reading the site and searching public signals for traffic, scale, team, and funding. This usually takes 1-3 minutes.",
|
|
44
44
|
status: {
|
|
45
45
|
default: "Researching",
|
|
46
46
|
PENDING: "Queued",
|
|
@@ -130,20 +130,21 @@ const competitorAnalysis = {
|
|
|
130
130
|
workspaceHint: "Enter a public competitor URL to build a sourced profile with confidence and unknowns shown for every major finding.",
|
|
131
131
|
workspaceBadge: "Public sources only",
|
|
132
132
|
visualAlt: {
|
|
133
|
-
hero: "Competitor profile report
|
|
134
|
-
howToUse: "
|
|
135
|
-
result: "Competitor
|
|
133
|
+
hero: "Competitor profile report with sourced findings and confidence levels.",
|
|
134
|
+
howToUse: "Workflow from a public competitor URL to a sourced competitor profile.",
|
|
135
|
+
result: "Competitor findings separated into evidence, estimates, confidence, and unknowns.",
|
|
136
|
+
useCases: "Teams applying competitor research to positioning, pricing, and launch decisions.",
|
|
136
137
|
},
|
|
137
138
|
howToUseKicker: "How it works",
|
|
138
139
|
howToUse: "How to analyze a competitor",
|
|
139
140
|
howToUseSteps: [
|
|
140
141
|
"Paste the competitor's public website URL; optionally add a name and the dimensions you care about.",
|
|
141
|
-
"
|
|
142
|
+
"An online research workflow reads the site and searches public signals for traffic, scale, team, and funding.",
|
|
142
143
|
"Results come back as a structured profile — each dimension shows confidence and degrades instead of inventing data.",
|
|
143
144
|
"Start with the overview and traffic trend, then turn “Positioning & openings” into your own positioning, pricing, or channel hypotheses.",
|
|
144
145
|
],
|
|
145
146
|
runtimeNote:
|
|
146
|
-
"Research runs
|
|
147
|
+
"Research runs through an online workflow using public web and search only. The page renders the structured result only — no local template fallback.",
|
|
147
148
|
resultKicker: "Result guidance",
|
|
148
149
|
resultGuidance: "How to read the profile",
|
|
149
150
|
guidanceBullets: [
|
|
@@ -177,7 +178,7 @@ const competitorAnalysis = {
|
|
|
177
178
|
{
|
|
178
179
|
question: "Does it access private or logged-in pages?",
|
|
179
180
|
answer:
|
|
180
|
-
"No. The
|
|
181
|
+
"No. The research workflow reads public pages and public search results only — it does not crawl private, authenticated, or paywalled content.",
|
|
181
182
|
},
|
|
182
183
|
{
|
|
183
184
|
question: "What should I do with the result?",
|
|
@@ -18,7 +18,7 @@ const emailTemplateGenerator = {
|
|
|
18
18
|
heroImageAlt: "Visual of a rough email brief becoming MJML, HTML, plain text, variables, and an export bundle.",
|
|
19
19
|
workspaceHeading: "MJML Email Template Generator",
|
|
20
20
|
workspaceHint:
|
|
21
|
-
"Describe the email goal, audience, scenario, and brand tone. The
|
|
21
|
+
"Describe the email goal, audience, scenario, and brand tone. The generation workflow returns a structured MJML-first package you can preview, revise, copy, or export.",
|
|
22
22
|
workspaceBadge: "MJML package",
|
|
23
23
|
outputs: "What You Get",
|
|
24
24
|
outputsIntro:
|
|
@@ -45,8 +45,8 @@ const emailTemplateGenerator = {
|
|
|
45
45
|
body: "See compliance, accessibility, rendering, and missing-information notes before handing the template to development.",
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
|
-
title: "
|
|
49
|
-
body: "After generation, describe the exact wording or module change and regenerate a new structured result
|
|
48
|
+
title: "Guided revision loop",
|
|
49
|
+
body: "After generation, describe the exact wording or module change and regenerate a new structured result.",
|
|
50
50
|
},
|
|
51
51
|
],
|
|
52
52
|
valueVisualAlt: "Visual of an email template package with MJML, HTML, variables, and developer handoff notes.",
|
|
@@ -72,7 +72,7 @@ const emailTemplateGenerator = {
|
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
title: "Fast Iteration",
|
|
75
|
-
body: "A rough brief can become a first package quickly, then the
|
|
75
|
+
body: "A rough brief can become a first package quickly, then the generation workflow can revise structure, wording, and sections from follow-up instructions.",
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
title: "Useful Preview",
|
|
@@ -103,7 +103,7 @@ const emailTemplateGenerator = {
|
|
|
103
103
|
title: "How To Recover",
|
|
104
104
|
items: [
|
|
105
105
|
"If the result is too generic, add real product value, user segment, offer, CTA, objections, and brand examples.",
|
|
106
|
-
"If the template feels too promotional, switch to a transactional scenario or ask the
|
|
106
|
+
"If the template feels too promotional, switch to a transactional scenario or ask for the sales language to be removed.",
|
|
107
107
|
"If a merge field or link is missing, add the required variable name and regenerate through the revision box.",
|
|
108
108
|
],
|
|
109
109
|
},
|
|
@@ -153,7 +153,7 @@ const emailTemplateGenerator = {
|
|
|
153
153
|
{
|
|
154
154
|
question: "Can I edit the result?",
|
|
155
155
|
answer:
|
|
156
|
-
"Yes. You can inspect sections and
|
|
156
|
+
"Yes. You can inspect sections and submit revision instructions. The next version is returned as a new structured result.",
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
question: "Is the generated HTML ready to send?",
|
|
@@ -207,7 +207,7 @@ const emailTemplateGenerator = {
|
|
|
207
207
|
emptyTitle: "Your generated package will appear here",
|
|
208
208
|
emptyHint:
|
|
209
209
|
"The result includes HTML preview, MJML source, HTML, plain text, variables, review notes, and export actions.",
|
|
210
|
-
runningTitle: "
|
|
210
|
+
runningTitle: "Your template package is being generated",
|
|
211
211
|
runningHint:
|
|
212
212
|
"It is writing MJML, matching HTML, plain text, variables, and review notes. Short result fetch delays are normal.",
|
|
213
213
|
previewInput: "Input",
|
|
@@ -248,7 +248,7 @@ const emailTemplateGenerator = {
|
|
|
248
248
|
missing: "Missing information",
|
|
249
249
|
empty: "No items returned.",
|
|
250
250
|
},
|
|
251
|
-
revisionLabel: "
|
|
251
|
+
revisionLabel: "Revise the result",
|
|
252
252
|
revisionHint: "Use a specific change request",
|
|
253
253
|
revisionPlaceholder:
|
|
254
254
|
"Example: Make the hero shorter, change the CTA to book a demo, and add {{calendar_url}} as a variable.",
|
|
@@ -262,12 +262,12 @@ const emailTemplateGenerator = {
|
|
|
262
262
|
},
|
|
263
263
|
errors: {
|
|
264
264
|
invalid: "Add a real product name, audience, and email brief. Placeholder or numeric input cannot produce a useful template.",
|
|
265
|
-
revision: "Write a specific revision instruction before
|
|
265
|
+
revision: "Write a specific revision instruction before submitting it.",
|
|
266
266
|
copy: "Copy failed. You can still select and copy the text manually.",
|
|
267
267
|
submit: "The request could not be submitted.",
|
|
268
268
|
task: "The generation task failed.",
|
|
269
269
|
timeout: "The structured result did not arrive in time.",
|
|
270
|
-
schema: "The
|
|
270
|
+
schema: "The generated result could not be read safely. Please try again.",
|
|
271
271
|
generic: "The template could not be generated.",
|
|
272
272
|
recover:
|
|
273
273
|
"Check the brief, try again, or add more concrete product, audience, CTA, and variable details.",
|
|
@@ -228,6 +228,7 @@ const productPosterGenerator = {
|
|
|
228
228
|
"The poster image could not be generated. Simplify the benefits, choose another format, or try again later.",
|
|
229
229
|
unreadableResult: "The result cannot be read right now. Try again later.",
|
|
230
230
|
timeout: "Generation is taking longer than expected. Try again later.",
|
|
231
|
+
downloadFailed: "The PNG could not be downloaded. Try again in a moment.",
|
|
231
232
|
},
|
|
232
233
|
readyKicker: "Poster ready",
|
|
233
234
|
readyTitle: "Product promo poster is ready",
|