@tomako/tools-runtime 0.1.4 → 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 (57) 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-icon-resizer/app-icon-resizer.spec.ts +1 -1
  8. package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.container.tsx +6 -8
  9. package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.spec.ts +1 -1
  10. package/src/features/tools/app-store-screenshot-generator/widget/app-store-screenshot-generator.tsx +44 -6
  11. package/src/features/tools/app-store-screenshot-generator/widget/generation.test.ts +126 -0
  12. package/src/features/tools/app-store-screenshot-generator/widget/generation.ts +52 -14
  13. package/src/features/tools/cold-start-channel-selector/cold-start-channel-selector.spec.ts +1 -1
  14. package/src/features/tools/cold-start-channel-selector/widget/cold-start-channel-selector.tsx +45 -16
  15. package/src/features/tools/competitor-analysis/competitor-analysis.container.tsx +31 -26
  16. package/src/features/tools/competitor-analysis/competitor-analysis.spec.ts +1 -1
  17. package/src/features/tools/competitor-analysis/widget/competitor-analysis.tsx +47 -16
  18. package/src/features/tools/disclaimer-generator/disclaimer-generator.package.ts +4 -4
  19. package/src/features/tools/email-template-generator/email-template-generator.container.tsx +45 -173
  20. package/src/features/tools/email-template-generator/email-template-generator.spec.ts +1 -1
  21. package/src/features/tools/email-template-generator/widget/email-template-generator.tsx +37 -4
  22. package/src/features/tools/package/to-tool-module.ts +15 -0
  23. package/src/features/tools/product-name-generator/product-name-generator.spec.ts +1 -1
  24. package/src/features/tools/product-name-generator/widget/product-name-generator.tsx +60 -27
  25. package/src/features/tools/product-poster-generator/product-poster-generator.spec.ts +1 -1
  26. package/src/features/tools/product-poster-generator/widget/constants.ts +3 -1
  27. package/src/features/tools/product-poster-generator/widget/helpers.test.ts +51 -0
  28. package/src/features/tools/product-poster-generator/widget/helpers.ts +27 -0
  29. package/src/features/tools/product-poster-generator/widget/product-poster-generator.tsx +112 -12
  30. package/src/features/tools/slogan-generator/slogan-generator.package.ts +101 -29
  31. package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.container.tsx +2 -3
  32. package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.spec.ts +1 -1
  33. package/src/i18n/messages/en/tools/app-icon-resizer.ts +1 -1
  34. package/src/i18n/messages/en/tools/app-store-screenshot-generator.ts +3 -3
  35. package/src/i18n/messages/en/tools/cold-start-channel-selector.ts +4 -4
  36. package/src/i18n/messages/en/tools/competitor-analysis.ts +14 -10
  37. package/src/i18n/messages/en/tools/email-template-generator.ts +13 -13
  38. package/src/i18n/messages/en/tools/product-poster-generator.ts +3 -2
  39. package/src/i18n/messages/en/tools/twitter-gif-downloader.ts +1 -1
  40. package/src/i18n/messages/zh/tools/app-store-screenshot-generator.ts +4 -4
  41. package/src/i18n/messages/zh/tools/cold-start-channel-selector.ts +2 -2
  42. package/src/i18n/messages/zh/tools/competitor-analysis.ts +14 -10
  43. package/src/i18n/messages/zh/tools/email-template-generator.ts +12 -12
  44. package/src/i18n/messages/zh/tools/product-poster-generator.ts +2 -1
  45. package/src/i18n/messages/zh/tools/twitter-gif-downloader.ts +1 -1
  46. package/src/i18n/messages/zh-tw/tools/app-store-screenshot-generator.ts +44 -44
  47. package/src/i18n/messages/zh-tw/tools/cold-start-channel-selector.ts +105 -105
  48. package/src/i18n/messages/zh-tw/tools/competitor-analysis.ts +44 -40
  49. package/src/i18n/messages/zh-tw/tools/email-template-generator.ts +112 -112
  50. package/src/i18n/messages/zh-tw/tools/product-name-generator.ts +92 -92
  51. package/src/i18n/messages/zh-tw/tools/product-poster-generator.ts +74 -73
  52. package/src/i18n/messages/zh-tw/tools/twitter-gif-downloader.ts +48 -48
  53. package/src/lib/tools/cold-start-channel-selector-schema.test.ts +39 -0
  54. package/src/lib/tools/cold-start-channel-selector-schema.ts +8 -4
  55. package/src/lib/tools/competitor-analysis-schema.test.ts +69 -0
  56. package/src/lib/tools/competitor-analysis-schema.ts +53 -5
  57. package/src/lib/tools/ops-catalog.ts +10 -1
@@ -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
  */
@@ -35,18 +35,18 @@ export const sloganGeneratorPackage: ToolPackage = {
35
35
  toolType: "generator",
36
36
  keywordEvidence: "docs/seo-keyword-briefs/slogan-generator.md",
37
37
  seo: {
38
- updatedAt: "2026-07-04",
38
+ updatedAt: "2026-08-30",
39
39
  readingTime: "6 min",
40
40
  ogImage: "/tools/slogan-generator-hero.webp",
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,88 @@ 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
+ },
250
+ standard: {
251
+ benefits: {
252
+ title: {
253
+ en: "Compare distinct slogan directions, not filler variations",
254
+ zh: "比较真正不同的 Slogan 方向,而不是凑数变体",
255
+ "zh-tw": "比較真正不同的 Slogan 方向,而不是湊數變體",
256
+ },
257
+ cards: [
258
+ {
259
+ title: { en: "10 varied candidates", zh: "10 条多风格候选", "zh-tw": "10 條多風格候選" },
260
+ body: { en: "Compare bold, minimal, playful, premium, and benefit-led lines side by side.", zh: "并排比较大胆、极简、俏皮、高端和利益导向的表达。", "zh-tw": "並排比較大膽、極簡、俏皮、高端和利益導向的表達。" },
261
+ },
262
+ {
263
+ title: { en: "Reasons you can judge", zh: "每条都有判断依据", "zh-tw": "每條都有判斷依據" },
264
+ body: { en: "Each line explains the rhythm, contrast, specificity, or promise that makes it work.", zh: "每条都说明节奏、对比、具体性或承诺为什么成立。", "zh-tw": "每條都說明節奏、對比、具體性或承諾為什麼成立。" },
265
+ },
266
+ {
267
+ title: { en: "A shortlist with next steps", zh: "带下一步的候选清单", "zh-tw": "帶下一步的候選清單" },
268
+ body: { en: "Use read-aloud, recall, trademark, and domain checks before choosing a final line.", zh: "定稿前继续做朗读、记忆、商标和域名检查。", "zh-tw": "定稿前繼續做朗讀、記憶、商標和網域檢查。" },
269
+ },
270
+ ],
271
+ },
272
+ imageTextSections: [
273
+ {
274
+ id: "section-primary",
275
+ title: { en: "Start with a brief the generator can understand", zh: "先给生成器一份能读懂的业务简介", "zh-tw": "先給產生器一份能讀懂的業務簡介" },
276
+ body: {
277
+ en: ["State who the business serves and what changes for them.", "Add the brand name only when you want name-based wordplay; vague one-word prompts produce generic lines."],
278
+ zh: ["写清业务服务谁,以及为他们带来什么变化。", "只有需要名字相关的文字游戏时才填写品牌名;单个空泛词只会得到通用句子。"],
279
+ "zh-tw": ["寫清業務服務誰,以及為他們帶來什麼變化。", "只有需要名字相關的文字遊戲時才填寫品牌名;單一空泛詞只會得到通用句子。"],
280
+ },
281
+ image: { src: "/tools/slogan-generator-value.webp", alt: { en: "A concise slogan standing out from vague drafts.", zh: "一句清晰口号从空泛草案中脱颖而出。", "zh-tw": "一句清晰口號從空泛草案中脫穎而出。" }, side: "left" },
282
+ imageFirst: true,
283
+ },
284
+ {
285
+ id: "section-variation",
286
+ title: { en: "Compare styles before polishing words", zh: "先比较风格方向,再打磨具体词句", "zh-tw": "先比較風格方向,再打磨具體詞句" },
287
+ body: {
288
+ en: ["Shortlist candidates from different styles rather than near-duplicates.", "Read them aloud and test recall beside the logo with people who do not already know the brand."],
289
+ zh: ["从不同风格中挑选候选,不要只留同一个想法的近似变体。", "逐条朗读,并把它们和 Logo 放在一起,让不了解品牌的人测试记忆度。"],
290
+ "zh-tw": ["從不同風格中挑選候選,不要只留同一個想法的近似變體。", "逐條朗讀,並把它們和 Logo 放在一起,讓不了解品牌的人測試記憶度。"],
291
+ },
292
+ image: { src: "/tools/slogan-generator-standard.webp", alt: { en: "Slogan candidates compared for fit and memorability.", zh: "按贴合度和记忆度比较口号候选。", "zh-tw": "按貼合度和記憶度比較口號候選。" }, side: "right" },
293
+ },
294
+ {
295
+ id: "section-support",
296
+ title: { en: "Treat every result as a draft to clear", zh: "把每条结果当成仍需检查的草案", "zh-tw": "把每條結果當成仍需檢查的草案" },
297
+ body: {
298
+ en: ["The tool does not check trademarks, domains, or social handles.", "Search the exact phrase in your market and reject lines that overpromise what the product can deliver."],
299
+ zh: ["工具不会检查商标、域名或社媒账号占用。", "请在目标市场搜索候选原文,并淘汰任何超出产品真实能力的承诺。"],
300
+ "zh-tw": ["工具不會檢查商標、網域或社群帳號佔用。", "請在目標市場搜尋候選原文,並淘汰任何超出產品真實能力的承諾。"],
301
+ },
302
+ image: { src: "/tools/slogan-generator-support.webp", alt: { en: "A slogan shortlist passing brand and trademark checks.", zh: "口号候选清单经过品牌和商标检查。", "zh-tw": "口號候選清單經過品牌和商標檢查。" }, side: "left" },
303
+ imageFirst: true,
304
+ },
305
+ ],
306
+ generation: {
307
+ title: { en: "Move from candidate list to a tested slogan", zh: "从候选清单走到经过测试的最终口号", "zh-tw": "從候選清單走到經過測試的最終口號" },
308
+ image: { src: "/tools/slogan-generator-result-use.webp", alt: { en: "A shortlisted slogan tested beside a logo and landing page.", zh: "入围口号与 Logo 和落地页一起测试。", "zh-tw": "入圍口號與 Logo 和網站首頁一起測試。" }, side: "left" },
309
+ steps: [
310
+ { title: { en: "Shortlist three directions", zh: "选出三个不同方向", "zh-tw": "選出三個不同方向" }, body: { en: "Keep candidates that make one clear, honest promise.", zh: "保留只表达一个清晰、诚实承诺的候选。", "zh-tw": "保留只表達一個清晰、誠實承諾的候選。" } },
311
+ { title: { en: "Test sound and recall", zh: "测试读感和记忆度", "zh-tw": "測試讀感和記憶度" }, body: { en: "Read each aloud and ask unfamiliar readers what they remember later.", zh: "逐条朗读,并询问陌生读者过一段时间后还记得哪条。", "zh-tw": "逐條朗讀,並詢問陌生讀者過一段時間後還記得哪條。" } },
312
+ { title: { en: "Clear conflicts before launch", zh: "上线前排查冲突", "zh-tw": "上線前排查衝突" }, body: { en: "Check exact-phrase search, trademarks, domains, and handles before committing.", zh: "定稿前检查原文搜索、商标、域名和账号占用。", "zh-tw": "定稿前檢查原文搜尋、商標、網域和帳號佔用。" } },
313
+ ],
314
+ ctaLabel: { en: "Generate slogan candidates", zh: "生成 Slogan 候选", "zh-tw": "產生 Slogan 候選" },
315
+ },
316
+ faq: {
317
+ title: { en: "Slogan generator FAQs", zh: "Slogan 生成器常见问题", "zh-tw": "Slogan 產生器常見問題" },
318
+ intro: { en: "Generate a broad shortlist, then test and clear the final phrase before using it.", zh: "先生成覆盖不同方向的候选,再测试并检查最终短语。", "zh-tw": "先產生涵蓋不同方向的候選,再測試並檢查最終短語。" },
319
+ ctaLabel: { en: "Create slogan candidates", zh: "生成 Slogan 候选", "zh-tw": "產生 Slogan 候選" },
320
+ },
249
321
  },
250
322
  sections: [
251
323
  {
@@ -469,17 +541,17 @@ Quality requirements:
469
541
  {
470
542
  kind: "cards",
471
543
  heading: {
472
- en: "Why this beats the usual free slogan tools",
473
- zh: "为什么比常见的免费 slogan 工具好用",
474
- "zh-tw": "為什麼比常見的免費 slogan 工具好用",
544
+ en: "Why this beats basic slogan lists",
545
+ zh: "为什么比常见的 slogan 列表更好用",
546
+ "zh-tw": "為什麼比常見的 slogan 清單更好用",
475
547
  },
476
548
  items: [
477
549
  {
478
- title: { en: "Results on this page, no account", zh: "本页直出,无需注册", "zh-tw": "本頁直出,無需註冊" },
550
+ title: { en: "Full results on this page", zh: "完整结果在本页展示", "zh-tw": "完整結果在本頁顯示" },
479
551
  body: {
480
- 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.",
481
- zh: "Shopify、Copy.ai、Grammarly 的 slogan 工具在给出完整结果前会把你引进注册或产品漏斗。这里的完整候选就渲染在表单下方。",
482
- "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": "登入後,完整候選會直接顯示在表單下方,不會再把你帶到另一個編輯器或流程。",
483
555
  },
484
556
  },
485
557
  {
@@ -605,14 +677,14 @@ Quality requirements:
605
677
  },
606
678
  {
607
679
  question: {
608
- en: "Is this slogan generator really free?",
609
- zh: "这个 slogan 生成器真的免费吗?",
610
- "zh-tw": "這個 slogan 生成器真的免費嗎?",
680
+ en: "Do I need an account to generate slogans?",
681
+ zh: "生成 Slogan 需要登录吗?",
682
+ "zh-tw": "產生 Slogan 需要登入嗎?",
611
683
  },
612
684
  answer: {
613
- 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.",
614
- zh: "免费——不需要账号和信用卡,完整结果就在本页显示。为了保证所有人的速度,每分钟的生成次数有上限。",
615
- "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": "需要。產生前請先登入,以便安全執行模型請求;登入後,完整候選與複核說明會直接顯示在本頁。",
616
688
  },
617
689
  },
618
690
  ],
@@ -40,14 +40,13 @@ export async function TwitterGifDownloaderContainer({ locale, spec }: ToolPagePr
40
40
  <ToolPageShell
41
41
  locale={locale}
42
42
  spec={spec}
43
- heroVariant="immersive"
44
- heroBackgroundImage="/tools/twitter-gif-downloader-hero.webp"
45
- heroBackgroundAlt={t("heroVisualAlt")}
43
+ showDefaultHeader={false}
46
44
  >
47
45
  <ToolWorkspaceSection
48
46
  heading={t("workspaceHeading")}
49
47
  hint={t("workspaceHint")}
50
48
  badge={t("workspaceBadge")}
49
+ headingLevel="h1"
51
50
  >
52
51
  <TwitterGifDownloader />
53
52
  </ToolWorkspaceSection>
@@ -5,7 +5,7 @@ export const twitterGifDownloaderSpec: ToolSpec = {
5
5
  toolType: "downloader",
6
6
  seo: {
7
7
  "publishedAt": "2026-06-29",
8
- "updatedAt": "2026-08-04",
8
+ "updatedAt": "2026-08-30",
9
9
  "readingTime": "5 min",
10
10
  "featured": false,
11
11
  "ogImage": "/tools/twitter-gif-downloader-hero.webp"
@@ -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",
@@ -16,9 +16,9 @@ const appStoreScreenshotGenerator = {
16
16
  guide: {
17
17
  heroVisualAlt:
18
18
  "Hero visual for an app store listing image generator, showing a complete app store screenshot set.",
19
- workspaceHeading: "Upload UI screenshots to generate listing images",
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 a growth channel strategy for an early product. Add your product, audience, time, and budget to choose the first channel to 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",
@@ -1,7 +1,7 @@
1
1
  const competitorAnalysis = {
2
2
  meta: {
3
- title: "Competitor Analysis Tool",
4
- description: "Enter a competitor's URL and a cloud agent researches public signals into a structured profile: overview, traffic and growth, business scale, team, and funding with sources and confidence.",
3
+ title: "AI Competitor Analysis Tool: Sources",
4
+ description: "Enter a competitor URL and get a sourced profile of its product, traffic, growth, business scale, team, and funding, with confidence shown for each finding.",
5
5
  intro: "Turn a single competitor URL into a one-page profile. Built from public web and search signals, every dimension shows its confidence so you can act on positioning, growth, and competitive decisions.",
6
6
  category: "Market research tools",
7
7
  keywords: [
@@ -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",
@@ -126,21 +126,25 @@ const competitorAnalysis = {
126
126
  },
127
127
  },
128
128
  guide: {
129
+ workspaceHeading: "AI Competitor Analysis Tool",
130
+ workspaceHint: "Enter a public competitor URL to build a sourced profile with confidence and unknowns shown for every major finding.",
131
+ workspaceBadge: "Public sources only",
129
132
  visualAlt: {
130
- hero: "Competitor profile report preview (visual pending)",
131
- howToUse: "Competitor analysis workflow illustration (visual pending)",
132
- 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.",
133
137
  },
134
138
  howToUseKicker: "How it works",
135
139
  howToUse: "How to analyze a competitor",
136
140
  howToUseSteps: [
137
141
  "Paste the competitor's public website URL; optionally add a name and the dimensions you care about.",
138
- "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.",
139
143
  "Results come back as a structured profile — each dimension shows confidence and degrades instead of inventing data.",
140
144
  "Start with the overview and traffic trend, then turn “Positioning & openings” into your own positioning, pricing, or channel hypotheses.",
141
145
  ],
142
146
  runtimeNote:
143
- "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.",
144
148
  resultKicker: "Result guidance",
145
149
  resultGuidance: "How to read the profile",
146
150
  guidanceBullets: [
@@ -174,7 +178,7 @@ const competitorAnalysis = {
174
178
  {
175
179
  question: "Does it access private or logged-in pages?",
176
180
  answer:
177
- "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.",
178
182
  },
179
183
  {
180
184
  question: "What should I do with the result?",