Skip to content

Commit 514b00a

Browse files
committed
feat: enhance prompt quality tests and modules
- Updated prompt quality tests to include additional checks for new markers and constraints in both Chinese and English. - Refactored prompt quality module to centralize shared constraints, delivery contracts, and acceptance criteria for better maintainability. - Added new test cases for data integrity in case and recipe prompts, ensuring all prompts contain necessary sections and adhere to defined standards. - Introduced a standalone preparation script for static assets to streamline deployment. - Created a custom error component for localized error handling in the application. - Configured Vitest for testing with path aliasing for cleaner imports.
1 parent da5e479 commit 514b00a

38 files changed

Lines changed: 2754 additions & 2180 deletions

‎eslint.config.mjs‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import { FlatCompat } from '@eslint/eslintrc';
2-
import { dirname } from 'node:path';
3-
import { fileURLToPath } from 'node:url';
1+
import nextConfig from 'eslint-config-next';
2+
import tseslint from 'typescript-eslint';
43

5-
const __dirname = dirname(fileURLToPath(import.meta.url));
6-
const compat = new FlatCompat({ baseDirectory: __dirname });
7-
8-
const eslintConfig = [
4+
export default tseslint.config(
95
{
106
ignores: ['.next/**', '.open-next/**', 'node_modules/**', 'next-env.d.ts'],
117
},
12-
...compat.extends('next/core-web-vitals', 'next/typescript'),
13-
];
14-
15-
export default eslintConfig;
8+
...nextConfig,
9+
);

‎next.config.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const __dirname = path.dirname(__filename);
88
const nextConfig = {
99
output: 'standalone',
1010
outputFileTracingRoot: __dirname,
11+
bundlePagesRouterDependencies: true,
1112
reactStrictMode: true,
1213
poweredByHeader: false,
1314
compress: true,

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
"description": "Beginner-friendly Codex desktop tutorial site for non-developers (Windows & macOS)",
77
"scripts": {
88
"dev": "next dev",
9-
"build:next": "next build",
9+
"build:next": "next build && node scripts/prepare-standalone.mjs",
1010
"build": "npm run build:next && opennextjs-cloudflare build --skipNextBuild",
11-
"start": "next start",
11+
"start": "node .next/standalone/server.js",
1212
"test": "vitest run",
1313
"lint": "eslint .",
1414
"prompt:stats": "node scripts/prompt-stats.mjs",

‎scripts/prepare-standalone.mjs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { cp, rm } from 'node:fs/promises';
2+
import path from 'node:path';
3+
4+
const rootDir = process.cwd();
5+
const standaloneDir = path.join(rootDir, '.next', 'standalone');
6+
7+
const copies = [
8+
{
9+
from: path.join(rootDir, '.next', 'static'),
10+
to: path.join(standaloneDir, '.next', 'static'),
11+
},
12+
{
13+
from: path.join(rootDir, 'public'),
14+
to: path.join(standaloneDir, 'public'),
15+
},
16+
];
17+
18+
for (const { from, to } of copies) {
19+
await rm(to, { recursive: true, force: true });
20+
await cp(from, to, { recursive: true });
21+
}
22+
23+
console.log('Prepared standalone static assets.');

‎scripts/prompt-stats.mjs‎

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,50 @@ const json = args.has('--json');
88
const limitArg = process.argv.find((arg) => arg.startsWith('--limit='));
99
const limit = Number(limitArg?.split('=')[1] ?? 10);
1010

11+
const CONTENT_SIGNALS = {
12+
zh: [
13+
['AI 小白', '不懂代码', '小白'],
14+
['sample-data', '示例数据'],
15+
['烟测'],
16+
['不覆盖原文件'],
17+
['不写死', '不写死密钥'],
18+
['真实接线'],
19+
['友好', '不闪退'],
20+
['降级', '3 次失败'],
21+
],
22+
en: [
23+
['AI beginner', 'non-technical', 'non-developer'],
24+
['sample-data', 'sample data'],
25+
['Smoke test', 'smoke-tested'],
26+
['Never overwrite', 'never overwrite'],
27+
['No hard-coded', 'no hard-coded'],
28+
['Real wiring'],
29+
['friendly', 'no crash'],
30+
['downgrade', '3 times'],
31+
],
32+
};
33+
34+
const STRUCTURE_SIGNALS = {
35+
zh: [
36+
['目标:', '【目标】'],
37+
['功能:', '【核心功能】'],
38+
['平台:', '【平台与技术】', '【环境】'],
39+
['【快速交付】', '【交付】'],
40+
['完成标准', '验收清单', '自检清单'],
41+
['【约束】', '【实现纪律 / 安全】'],
42+
['最终只报', '最终汇报', '使用说明'],
43+
],
44+
en: [
45+
['Goal:', '[Goal]'],
46+
['Features:', '[Core Features]'],
47+
['Platform:', '[Platform & Stack]'],
48+
['[Fast Delivery]', '[Delivery]', '[Desktop Delivery Contract]'],
49+
['Done criteria', 'Acceptance checklist', 'Self-check'],
50+
['[Constraints]', '[Implementation / Safety]'],
51+
['Final report', 'user guide', 'guide'],
52+
],
53+
};
54+
1155
const entry = `
1256
import { caseBundles, getCasePrompt } from './src/data/cases/index.ts';
1357
import { recipes, getRecipePrompt } from './src/data/recipes.ts';
@@ -38,17 +82,11 @@ export function collectPrompts() {
3882
}
3983
`;
4084

41-
function estimateTokens(text) {
42-
const cjk = text.match(/[\u3400-\u9FFF]/gu)?.length ?? 0;
43-
const compactOther = text.replace(/[\s\u3400-\u9FFF]/gu, '');
44-
return Math.ceil(cjk * 1.05 + compactOther.length / 4);
45-
}
46-
47-
function percentile(values, p) {
48-
if (!values.length) return 0;
49-
const sorted = [...values].sort((a, b) => a - b);
50-
const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
51-
return sorted[index];
85+
function signalCount(text, lang, signals) {
86+
const normalized = text.toLowerCase();
87+
return signals[lang].filter((alternatives) =>
88+
alternatives.some((phrase) => normalized.includes(phrase.toLowerCase()))
89+
).length;
5290
}
5391

5492
function summarize(rows) {
@@ -61,16 +99,18 @@ function summarize(rows) {
6199
}
62100

63101
return [...groups.entries()].map(([bucket, list]) => {
64-
const chars = list.map((item) => item.chars);
65-
const tokens = list.map((item) => item.tokens);
102+
const contentSignals = list.map((item) => item.contentSignals);
103+
const structureSignals = list.map((item) => item.structureSignals);
104+
const qualityScore = list.map((item) => item.qualityScore);
66105
return {
67106
bucket,
68107
count: list.length,
69-
avgChars: Math.round(chars.reduce((sum, value) => sum + value, 0) / list.length),
70-
p95Chars: percentile(chars, 95),
71-
maxChars: Math.max(...chars),
72-
avgTokens: Math.round(tokens.reduce((sum, value) => sum + value, 0) / list.length),
73-
maxTokens: Math.max(...tokens),
108+
avgContentSignals: Number((contentSignals.reduce((sum, value) => sum + value, 0) / list.length).toFixed(1)),
109+
minContentSignals: Math.min(...contentSignals),
110+
avgStructureSignals: Number((structureSignals.reduce((sum, value) => sum + value, 0) / list.length).toFixed(1)),
111+
minStructureSignals: Math.min(...structureSignals),
112+
avgQualityScore: Number((qualityScore.reduce((sum, value) => sum + value, 0) / list.length).toFixed(1)),
113+
minQualityScore: Math.min(...qualityScore),
74114
};
75115
});
76116
}
@@ -101,32 +141,37 @@ const { collectPrompts } = await import(moduleUrl);
101141
const rows = collectPrompts()
102142
.map((item) => ({
103143
...item,
104-
chars: [...item.text].length,
105-
bytes: Buffer.byteLength(item.text, 'utf8'),
106-
tokens: estimateTokens(item.text),
144+
contentSignals: signalCount(item.text, item.lang, CONTENT_SIGNALS),
145+
structureSignals: signalCount(item.text, item.lang, STRUCTURE_SIGNALS),
146+
}))
147+
.map((item) => ({
148+
...item,
149+
qualityScore: item.contentSignals + item.structureSignals,
107150
}))
108-
.sort((a, b) => b.tokens - a.tokens);
151+
.sort((a, b) => a.qualityScore - b.qualityScore || a.contentSignals - b.contentSignals);
109152

110153
const result = {
111154
generatedAt: new Date().toISOString(),
112155
total: rows.length,
113156
summary: summarize(rows),
114-
longest: rows.slice(0, Number.isFinite(limit) ? limit : 10),
157+
reviewQueue: rows.slice(0, Number.isFinite(limit) ? limit : 10),
115158
};
116159

117160
if (json) {
118161
console.log(JSON.stringify(result, null, 2));
119162
} else {
120-
console.log(`Prompt stats (${result.total} final copy prompts)`);
163+
console.log(`Prompt quality report (${result.total} final copy prompts)`);
164+
console.log('Focus: content, structure, safety, verification, and beginner usability coverage.');
121165
console.table(result.summary);
122-
console.log(`\nTop ${limit} longest prompts:`);
166+
console.log(`\nReview queue (${limit} lowest quality-coverage prompts):`);
123167
console.table(
124-
result.longest.map(({ group, id, lang, chars, tokens }) => ({
168+
result.reviewQueue.map(({ group, id, lang, contentSignals, structureSignals, qualityScore }) => ({
125169
group,
126170
id,
127171
lang,
128-
chars,
129-
approxTokens: tokens,
172+
contentSignals,
173+
structureSignals,
174+
qualityScore,
130175
}))
131176
);
132177
console.log('\nTip: run npm run prompt:stats -- --json for machine-readable output.');

‎src/app/[locale]/cases/[slug]/page.tsx‎

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,9 @@ import {
1818
PuzzleIcon,
1919
RocketIcon,
2020
SparkleIcon,
21-
FinanceIcon,
22-
OpsIcon,
23-
SupportIcon,
24-
HRIcon,
25-
LogisticsIcon,
26-
ProcurementIcon,
27-
MarketingIcon,
28-
LegalIcon,
29-
DataIcon,
30-
AdminIcon,
31-
ProductIcon,
3221
WindowsIcon,
3322
} from '@/components/icons';
23+
import { deptIcons } from '@/components/cases/deptIcons';
3424

3525
type Props = { params: Promise<{ locale: string; slug: string }> };
3626

@@ -69,20 +59,6 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
6959
};
7060
}
7161

72-
const deptIcons = {
73-
finance: <FinanceIcon size={28} />,
74-
operations: <OpsIcon size={28} />,
75-
'customer-service': <SupportIcon size={28} />,
76-
hr: <HRIcon size={28} />,
77-
logistics: <LogisticsIcon size={28} />,
78-
procurement: <ProcurementIcon size={28} />,
79-
marketing: <MarketingIcon size={28} />,
80-
legal: <LegalIcon size={28} />,
81-
data: <DataIcon size={28} />,
82-
admin: <AdminIcon size={28} />,
83-
product: <ProductIcon size={28} />,
84-
} as const;
85-
8662
export default async function CasePage({ params }: Props) {
8763
const { locale: rawLocale, slug } = await params;
8864
if (!isLocale(rawLocale)) notFound();

‎src/app/[locale]/error.tsx‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
import Link from 'next/link';
5+
6+
type Props = {
7+
error: Error & { digest?: string };
8+
reset: () => void;
9+
};
10+
11+
export default function LocaleError({ error, reset }: Props) {
12+
useEffect(() => {
13+
console.error(error);
14+
}, [error]);
15+
16+
return (
17+
<div className="mx-auto flex max-w-md flex-col items-center gap-4 px-4 py-24 text-center">
18+
<div className="text-6xl font-bold aurora-text">!</div>
19+
<h1 className="text-xl font-semibold text-ink">
20+
出了点问题 / Something went wrong
21+
</h1>
22+
<p className="text-sm text-ink-soft">
23+
页面加载时遇到了错误。可以试试刷新,或者回到首页。
24+
</p>
25+
<p className="text-sm text-ink-soft">
26+
An error occurred while loading this page. Try refreshing or head home.
27+
</p>
28+
<div className="mt-4 flex gap-3">
29+
<button
30+
type="button"
31+
onClick={reset}
32+
className="rounded-full border border-[color:var(--line)] bg-white/70 px-4 py-2 text-sm font-medium text-ink shadow-glass backdrop-blur-xl transition hover:bg-white"
33+
>
34+
重试 / Retry
35+
</button>
36+
<Link
37+
href="/zh"
38+
className="rounded-full border border-[color:var(--line)] bg-white/70 px-4 py-2 text-sm font-medium text-ink shadow-glass backdrop-blur-xl transition hover:bg-white"
39+
>
40+
中文首页
41+
</Link>
42+
<Link
43+
href="/en"
44+
className="rounded-full border border-[color:var(--line)] bg-white/70 px-4 py-2 text-sm font-medium text-ink shadow-glass backdrop-blur-xl transition hover:bg-white"
45+
>
46+
English home
47+
</Link>
48+
</div>
49+
</div>
50+
);
51+
}

‎src/app/globals.css‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,21 @@ html {
144144
.focus-ring {
145145
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--surface)];
146146
}
147+
.aurora-text {
148+
background: linear-gradient(135deg, #FF9EC4 0%, #B8A4FF 45%, #9CCEFF 100%);
149+
-webkit-background-clip: text;
150+
-webkit-text-fill-color: transparent;
151+
background-clip: text;
152+
}
153+
.shadow-glass {
154+
box-shadow: 0 1px 2px rgba(15, 17, 22, 0.04), 0 8px 24px rgba(15, 17, 22, 0.06);
155+
}
156+
.shadow-glass-sm {
157+
box-shadow: 0 1px 2px rgba(15, 17, 22, 0.03), 0 4px 12px rgba(15, 17, 22, 0.05);
158+
}
159+
.text-aurora-violet {
160+
color: #B8A4FF;
161+
}
147162
.prose-body p {
148163
@apply text-[15px] leading-[1.85] text-[color:var(--fg-secondary)];
149164
}

‎src/components/LanguageSwitcher.tsx‎

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { usePathname, useRouter } from 'next/navigation';
44
import { useCallback, useMemo, useTransition } from 'react';
5-
import { localeCookieName, locales, type Locale, localeNames } from '@/i18n/config';
5+
import { localeCookieName, type Locale, localeNames } from '@/i18n/config';
66
import { cn } from '@/lib/cn';
77

88
type Props = {
@@ -79,45 +79,3 @@ export function LanguageSwitcher({ currentLocale, compact = false, className }:
7979
</button>
8080
);
8181
}
82-
83-
export function LocaleList({
84-
currentLocale,
85-
className,
86-
}: {
87-
currentLocale: Locale;
88-
className?: string;
89-
}) {
90-
const router = useRouter();
91-
const pathname = usePathname() ?? '/';
92-
93-
const targetPath = (locale: Locale) => {
94-
const segments = pathname.split('/').filter(Boolean);
95-
if (segments.length === 0) return `/${locale}`;
96-
segments[0] = locale;
97-
return '/' + segments.join('/');
98-
};
99-
100-
return (
101-
<div className={cn('flex flex-wrap gap-2', className)}>
102-
{locales.map((locale) => (
103-
<button
104-
key={locale}
105-
type="button"
106-
onClick={() => {
107-
persistLocalePreference(locale);
108-
router.push(targetPath(locale));
109-
}}
110-
aria-current={locale === currentLocale ? 'true' : undefined}
111-
className={cn(
112-
'focus-ring rounded-full border px-3.5 py-1.5 text-xs font-medium transition',
113-
locale === currentLocale
114-
? 'border-ink/20 bg-white text-ink'
115-
: 'border-[color:var(--line)] bg-white/50 text-ink-soft hover:bg-white/80'
116-
)}
117-
>
118-
{localeNames[locale]}
119-
</button>
120-
))}
121-
</div>
122-
);
123-
}

0 commit comments

Comments
 (0)