@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.
- 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-icon-resizer/app-icon-resizer.spec.ts +1 -1
- package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.container.tsx +6 -8
- package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.spec.ts +1 -1
- 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/cold-start-channel-selector.spec.ts +1 -1
- 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 +31 -26
- package/src/features/tools/competitor-analysis/competitor-analysis.spec.ts +1 -1
- package/src/features/tools/competitor-analysis/widget/competitor-analysis.tsx +47 -16
- package/src/features/tools/disclaimer-generator/disclaimer-generator.package.ts +4 -4
- package/src/features/tools/email-template-generator/email-template-generator.container.tsx +45 -173
- package/src/features/tools/email-template-generator/email-template-generator.spec.ts +1 -1
- 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/product-name-generator.spec.ts +1 -1
- package/src/features/tools/product-name-generator/widget/product-name-generator.tsx +60 -27
- package/src/features/tools/product-poster-generator/product-poster-generator.spec.ts +1 -1
- 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 +101 -29
- package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.container.tsx +2 -3
- package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.spec.ts +1 -1
- package/src/i18n/messages/en/tools/app-icon-resizer.ts +1 -1
- package/src/i18n/messages/en/tools/app-store-screenshot-generator.ts +3 -3
- package/src/i18n/messages/en/tools/cold-start-channel-selector.ts +4 -4
- package/src/i18n/messages/en/tools/competitor-analysis.ts +14 -10
- package/src/i18n/messages/en/tools/email-template-generator.ts +13 -13
- package/src/i18n/messages/en/tools/product-poster-generator.ts +3 -2
- package/src/i18n/messages/en/tools/twitter-gif-downloader.ts +1 -1
- package/src/i18n/messages/zh/tools/app-store-screenshot-generator.ts +4 -4
- package/src/i18n/messages/zh/tools/cold-start-channel-selector.ts +2 -2
- package/src/i18n/messages/zh/tools/competitor-analysis.ts +14 -10
- package/src/i18n/messages/zh/tools/email-template-generator.ts +12 -12
- package/src/i18n/messages/zh/tools/product-poster-generator.ts +2 -1
- package/src/i18n/messages/zh/tools/twitter-gif-downloader.ts +1 -1
- 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 +44 -40
- package/src/i18n/messages/zh-tw/tools/email-template-generator.ts +112 -112
- 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 +48 -48
- 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
- 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
|
-
|
|
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
|
*/
|
|
@@ -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-
|
|
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: "
|
|
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,88 @@ 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
|
+
},
|
|
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
|
|
473
|
-
zh: "
|
|
474
|
-
"zh-tw": "
|
|
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: "
|
|
550
|
+
title: { en: "Full results on this page", zh: "完整结果在本页展示", "zh-tw": "完整結果在本頁顯示" },
|
|
479
551
|
body: {
|
|
480
|
-
en: "
|
|
481
|
-
zh: "
|
|
482
|
-
"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": "登入後,完整候選會直接顯示在表單下方,不會再把你帶到另一個編輯器或流程。",
|
|
483
555
|
},
|
|
484
556
|
},
|
|
485
557
|
{
|
|
@@ -605,14 +677,14 @@ Quality requirements:
|
|
|
605
677
|
},
|
|
606
678
|
{
|
|
607
679
|
question: {
|
|
608
|
-
en: "
|
|
609
|
-
zh: "
|
|
610
|
-
"zh-tw": "
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
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: "
|
|
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",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const competitorAnalysis = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "Competitor Analysis Tool",
|
|
4
|
-
description: "Enter a competitor
|
|
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
|
|
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",
|
|
@@ -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
|
|
131
|
-
howToUse: "
|
|
132
|
-
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.",
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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?",
|