@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.
Files changed (43) hide show
  1. package/package.json +2 -1
  2. package/src/components/tools/engine/package-widget-runtime.tsx +5 -1
  3. package/src/components/tools/engine/task-recovery.test.ts +65 -0
  4. package/src/components/tools/engine/task-recovery.ts +109 -0
  5. package/src/components/tools/engine/use-legacy-llm-task.ts +16 -3
  6. package/src/components/tools/engine/use-tool-task.ts +77 -53
  7. package/src/features/tools/app-store-screenshot-generator/widget/app-store-screenshot-generator.tsx +44 -6
  8. package/src/features/tools/app-store-screenshot-generator/widget/generation.test.ts +126 -0
  9. package/src/features/tools/app-store-screenshot-generator/widget/generation.ts +52 -14
  10. package/src/features/tools/cold-start-channel-selector/widget/cold-start-channel-selector.tsx +45 -16
  11. package/src/features/tools/competitor-analysis/competitor-analysis.container.tsx +26 -20
  12. package/src/features/tools/competitor-analysis/widget/competitor-analysis.tsx +47 -16
  13. package/src/features/tools/disclaimer-generator/disclaimer-generator.package.ts +3 -3
  14. package/src/features/tools/email-template-generator/widget/email-template-generator.tsx +37 -4
  15. package/src/features/tools/package/to-tool-module.ts +15 -0
  16. package/src/features/tools/product-name-generator/widget/product-name-generator.tsx +60 -27
  17. package/src/features/tools/product-poster-generator/widget/constants.ts +3 -1
  18. package/src/features/tools/product-poster-generator/widget/helpers.test.ts +51 -0
  19. package/src/features/tools/product-poster-generator/widget/helpers.ts +27 -0
  20. package/src/features/tools/product-poster-generator/widget/product-poster-generator.tsx +112 -12
  21. package/src/features/tools/slogan-generator/slogan-generator.package.ts +28 -28
  22. package/src/i18n/messages/en/tools/app-icon-resizer.ts +1 -1
  23. package/src/i18n/messages/en/tools/app-store-screenshot-generator.ts +2 -2
  24. package/src/i18n/messages/en/tools/cold-start-channel-selector.ts +4 -4
  25. package/src/i18n/messages/en/tools/competitor-analysis.ts +9 -8
  26. package/src/i18n/messages/en/tools/email-template-generator.ts +10 -10
  27. package/src/i18n/messages/en/tools/product-poster-generator.ts +1 -0
  28. package/src/i18n/messages/zh/tools/app-store-screenshot-generator.ts +3 -3
  29. package/src/i18n/messages/zh/tools/cold-start-channel-selector.ts +2 -2
  30. package/src/i18n/messages/zh/tools/competitor-analysis.ts +10 -9
  31. package/src/i18n/messages/zh/tools/email-template-generator.ts +11 -11
  32. package/src/i18n/messages/zh/tools/product-poster-generator.ts +1 -0
  33. package/src/i18n/messages/zh-tw/tools/app-store-screenshot-generator.ts +44 -44
  34. package/src/i18n/messages/zh-tw/tools/cold-start-channel-selector.ts +105 -105
  35. package/src/i18n/messages/zh-tw/tools/competitor-analysis.ts +41 -40
  36. package/src/i18n/messages/zh-tw/tools/email-template-generator.ts +111 -111
  37. package/src/i18n/messages/zh-tw/tools/product-name-generator.ts +92 -92
  38. package/src/i18n/messages/zh-tw/tools/product-poster-generator.ts +74 -73
  39. package/src/i18n/messages/zh-tw/tools/twitter-gif-downloader.ts +47 -47
  40. package/src/lib/tools/cold-start-channel-selector-schema.test.ts +39 -0
  41. package/src/lib/tools/cold-start-channel-selector-schema.ts +8 -4
  42. package/src/lib/tools/competitor-analysis-schema.test.ts +69 -0
  43. 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
- setResult({ phase: "polling", taskId, status: submitted.status });
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
- for (let attempt = 0; attempt < MAX_TASK_POLL_ATTEMPTS; attempt += 1) {
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 link = document.createElement("a");
382
- link.href = result.data.imageUrl;
383
- link.download = `${result.data.fileBaseName}-poster.webp`;
384
- link.target = "_blank";
385
- link.rel = "noreferrer";
386
- link.click();
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
- * 免登录页面内直出;每条候选带风格标签 + why-it-works 理由(ahrefs 只有裸文本);
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: "Free AI slogan generator: describe your business and get 10 catchy slogan and tagline ideas across styles from bold and minimal to playful and benefit-driven — each with a style tag and a why-it-works note. No sign-up, results on this page.",
47
- zh: "免费 AI Slogan 生成器:描述你的业务,一次获得 10 条广告语和 tagline 候选,覆盖大胆、极简、俏皮、利益导向等多种风格,每条附风格标签和入选理由。免注册,结果直接在本页给出。",
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
- "免費 AI Slogan 生成器:描述你的業務,一次獲得 10 條廣告語和 tagline 候選,覆蓋大膽、極簡、俏皮、利益導向等多種風格,每條附風格標籤和入選理由。免註冊,結果直接在本頁給出。",
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", "free slogan generator", "ai slogan generator", "tagline generator", "business slogan generator"],
64
- zh: ["slogan 生成器", "slogan 制作", "广告语生成器", "品牌口号生成", "tagline 生成器", "免费广告语工具", "AI 广告语"],
65
- "zh-tw": ["slogan 生成器", "slogan 製作", "廣告語生成器", "品牌口號生成", "tagline 生成器", "免費廣告語工具", "AI 廣告語"],
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": "生成 10 條 Slogan" },
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": "正在生成 Slogan" },
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: "Get 10 slogan candidates worth comparing",
240
- zh: "一次拿到 10 条值得比较的 Slogan",
241
- "zh-tw": "一次拿到 10 條值得比較的 Slogan",
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: "Free, no sign-up", zh: "免费免注册", "zh-tw": "免費免註冊" },
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 the usual free slogan tools",
545
- zh: "为什么比常见的免费 slogan 工具好用",
546
- "zh-tw": "為什麼比常見的免費 slogan 工具好用",
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: "Results on this page, no account", zh: "本页直出,无需注册", "zh-tw": "本頁直出,無需註冊" },
550
+ title: { en: "Full results on this page", zh: "完整结果在本页展示", "zh-tw": "完整結果在本頁顯示" },
551
551
  body: {
552
- en: "Slogan creators from Shopify, Copy.ai, and Grammarly route you into a sign-up or product funnel before the full result. Here the complete list renders right below the form.",
553
- zh: "Shopify、Copy.ai、Grammarly 的 slogan 工具在给出完整结果前会把你引进注册或产品漏斗。这里的完整候选就渲染在表单下方。",
554
- "zh-tw": "Shopify、Copy.ai、Grammarly 的 slogan 工具在給出完整結果前會把你引進註冊或產品漏斗。這裡的完整候選就渲染在表單下方。",
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: "Is this slogan generator really free?",
681
- zh: "这个 slogan 生成器真的免费吗?",
682
- "zh-tw": "這個 slogan 生成器真的免費嗎?",
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 no account, no credit card, and the full result renders on this page. Heavy usage is rate-limited per minute to keep the tool fast for everyone.",
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 icon package generator",
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. A cloud Agent uses Skills-OL to fetch supporting context, plan the complete set, and submit live image tasks.",
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 a cloud Agent and Skills-OL to submit live image tasks, but it does not connect to App Store Connect or Google Play Console.",
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: "Growth Channel Strategy Generator",
4
- description: "Build an early-product growth channel strategy. Add your product, audience, time, and budget to choose the first test and the signal that guides the next step.",
5
- intro: "Choose the first growth channel to test, then use real signals to decide what comes next.",
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: "Growth Channel Strategy",
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 a cloud agent researches public signals into a structured profile.",
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 agent is reading the site and searching public signals for traffic, scale, team, and funding. This usually takes 1-3 minutes.",
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 preview (visual pending)",
134
- howToUse: "Competitor analysis workflow illustration (visual pending)",
135
- result: "Competitor profile result guidance illustration (visual pending)",
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
- "A cloud agent reads the site and searches public signals for traffic, scale, team, and funding.",
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 on a cloud agent using public web and search only. The page renders the structured result only — no local template fallback.",
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 agent reads public pages and public search results only — it does not crawl private, authenticated, or paywalled content.",
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 Agent returns a structured MJML-first package you can preview, revise, copy, or export.",
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: "Agent Revision Loop",
49
- body: "After generation, describe the exact wording or module change and regenerate a new structured result from the Agent.",
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 Agent can revise structure, wording, and sections from follow-up instructions.",
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 Agent to remove sales language.",
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 send revision instructions back to the Agent. The next version is written back as a new structured result.",
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: "The Agent is building your template package",
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: "Ask the Agent to revise",
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 sending it to the Agent.",
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 Agent returned a result that did not match the email template schema.",
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",