This commit is contained in:
songtianlun 2025-07-29 22:21:16 +08:00
parent 7024c76f3a
commit 8d24d3c36e
12 changed files with 718 additions and 52 deletions

124
messages/en.json Normal file
View File

@ -0,0 +1,124 @@
{
"navigation": {
"home": "Home",
"studio": "Studio",
"profile": "Profile",
"signIn": "Sign In",
"signUp": "Sign Up",
"signOut": "Sign Out"
},
"common": {
"loading": "Loading...",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"search": "Search",
"filter": "Filter",
"language": "Language",
"theme": "Theme",
"light": "Light",
"dark": "Dark",
"system": "System"
},
"auth": {
"signIn": "Sign In",
"signUp": "Sign Up",
"email": "Email",
"password": "Password",
"username": "Username",
"confirmPassword": "Confirm Password",
"forgotPassword": "Forgot Password?",
"noAccount": "Don't have an account?",
"hasAccount": "Already have an account?",
"signInWithGoogle": "Sign in with Google",
"signUpWithGoogle": "Sign up with Google",
"signInTitle": "Welcome Back",
"signInSubtitle": "Sign in to your Prmbr account",
"signUpTitle": "Create Account",
"signUpSubtitle": "Join Prmbr - AI Prompt Studio"
},
"profile": {
"title": "Profile Settings",
"personalInfo": "Personal Information",
"accountSettings": "Account Settings",
"preferences": "Preferences",
"username": "Username",
"email": "Email",
"bio": "Bio",
"avatar": "Avatar",
"language": "Language",
"changePassword": "Change Password",
"currentPassword": "Current Password",
"newPassword": "New Password",
"confirmNewPassword": "Confirm New Password"
},
"studio": {
"title": "AI Prompt Studio",
"myPrompts": "My Prompts",
"createPrompt": "Create Prompt",
"promptName": "Prompt Name",
"promptContent": "Prompt Content",
"promptAlbum": "Album",
"promptTag": "Tags",
"version": "Version",
"testRun": "Test Run",
"versions": "Versions",
"createVersion": "Create Version",
"runTest": "Run Test",
"noPrompts": "No prompts yet",
"createFirstPrompt": "Create your first prompt to get started"
},
"home": {
"hero": {
"title": "Prmbr - AI Prompt Studio",
"subtitle": "Build, manage, and optimize your AI prompts with version control and testing capabilities",
"getStarted": "Get Started",
"learnMore": "Learn More"
},
"features": {
"title": "Features",
"promptManager": {
"title": "Prompt Management",
"description": "Organize your prompts with albums, tags, and powerful search capabilities"
},
"versionControl": {
"title": "Version Control",
"description": "Track changes and manage different versions of your prompts"
},
"testing": {
"title": "Testing & Validation",
"description": "Run tests on your prompts to ensure optimal performance"
}
}
},
"pricing": {
"title": "Pricing Plans",
"free": {
"title": "Free",
"price": "$0",
"features": [
"20 Prompt Limit",
"3 Versions per Prompt",
"$5 AI Credit Monthly"
]
},
"pro": {
"title": "Pro",
"price": "$19.9",
"features": [
"500 Prompt Limit",
"10 Versions per Prompt",
"$20 AI Credit Monthly"
]
}
},
"errors": {
"generic": "Something went wrong. Please try again.",
"network": "Network error. Please check your connection.",
"unauthorized": "You are not authorized to perform this action.",
"notFound": "The requested resource was not found.",
"validationError": "Please check your input and try again."
}
}

124
messages/zh.json Normal file
View File

@ -0,0 +1,124 @@
{
"navigation": {
"home": "首页",
"studio": "工作室",
"profile": "个人资料",
"signIn": "登录",
"signUp": "注册",
"signOut": "退出登录"
},
"common": {
"loading": "加载中...",
"save": "保存",
"cancel": "取消",
"delete": "删除",
"edit": "编辑",
"create": "创建",
"search": "搜索",
"filter": "筛选",
"language": "语言",
"theme": "主题",
"light": "浅色",
"dark": "深色",
"system": "跟随系统"
},
"auth": {
"signIn": "登录",
"signUp": "注册",
"email": "邮箱",
"password": "密码",
"username": "用户名",
"confirmPassword": "确认密码",
"forgotPassword": "忘记密码?",
"noAccount": "还没有账户?",
"hasAccount": "已有账户?",
"signInWithGoogle": "使用 Google 登录",
"signUpWithGoogle": "使用 Google 注册",
"signInTitle": "欢迎回来",
"signInSubtitle": "登录您的 Prmbr 账户",
"signUpTitle": "创建账户",
"signUpSubtitle": "加入 Prmbr - AI 提示词工作室"
},
"profile": {
"title": "个人设置",
"personalInfo": "个人信息",
"accountSettings": "账户设置",
"preferences": "偏好设置",
"username": "用户名",
"email": "邮箱",
"bio": "个人简介",
"avatar": "头像",
"language": "语言",
"changePassword": "修改密码",
"currentPassword": "当前密码",
"newPassword": "新密码",
"confirmNewPassword": "确认新密码"
},
"studio": {
"title": "AI 提示词工作室",
"myPrompts": "我的提示词",
"createPrompt": "创建提示词",
"promptName": "提示词名称",
"promptContent": "提示词内容",
"promptAlbum": "专辑",
"promptTag": "标签",
"version": "版本",
"testRun": "测试运行",
"versions": "版本管理",
"createVersion": "创建版本",
"runTest": "运行测试",
"noPrompts": "暂无提示词",
"createFirstPrompt": "创建您的第一个提示词开始使用"
},
"home": {
"hero": {
"title": "Prmbr - AI 提示词工作室",
"subtitle": "构建、管理和优化您的 AI 提示词,支持版本控制和测试功能",
"getStarted": "立即开始",
"learnMore": "了解更多"
},
"features": {
"title": "功能特性",
"promptManager": {
"title": "提示词管理",
"description": "使用专辑、标签和强大的搜索功能组织您的提示词"
},
"versionControl": {
"title": "版本控制",
"description": "跟踪变更并管理提示词的不同版本"
},
"testing": {
"title": "测试与验证",
"description": "对您的提示词进行测试以确保最佳性能"
}
}
},
"pricing": {
"title": "价格方案",
"free": {
"title": "免费版",
"price": "免费",
"features": [
"20 个提示词限制",
"每个提示词 3 个版本",
"每月 5 美元 AI 积分"
]
},
"pro": {
"title": "专业版",
"price": "$19.9",
"features": [
"500 个提示词限制",
"每个提示词 10 个版本",
"每月 20 美元 AI 积分"
]
}
},
"errors": {
"generic": "出现错误,请重试。",
"network": "网络错误,请检查您的网络连接。",
"unauthorized": "您无权执行此操作。",
"notFound": "未找到请求的资源。",
"validationError": "请检查您的输入并重试。"
}
}

View File

@ -1,5 +1,31 @@
import { createServerClient } from '@supabase/ssr' import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server' import { NextResponse, type NextRequest } from 'next/server'
import { locales, defaultLocale } from './src/i18n/config'
function getLocaleFromHeaders(acceptLanguage: string | null): string {
if (!acceptLanguage) return defaultLocale;
const languages = acceptLanguage
.split(',')
.map(lang => lang.split(';')[0].trim().toLowerCase());
// Check for exact matches first
for (const lang of languages) {
if (locales.includes(lang as any)) {
return lang;
}
}
// Check for language prefix matches (e.g., 'zh-CN' -> 'zh')
for (const lang of languages) {
const prefix = lang.split('-')[0];
if (locales.includes(prefix as any)) {
return prefix;
}
}
return defaultLocale;
}
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
let response = NextResponse.next({ let response = NextResponse.next({
@ -8,6 +34,20 @@ export async function middleware(request: NextRequest) {
}, },
}) })
// Handle locale detection and cookie setting
const currentLocale = request.cookies.get('locale')?.value;
const acceptLanguage = request.headers.get('accept-language');
if (!currentLocale) {
const detectedLocale = getLocaleFromHeaders(acceptLanguage);
response.cookies.set('locale', detectedLocale, {
maxAge: 60 * 60 * 24 * 365, // 1 year
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
});
}
const supabase = createServerClient( const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,

View File

@ -1,4 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin('./src/i18n/config.ts');
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
images: { images: {
@ -31,4 +34,4 @@ const nextConfig: NextConfig = {
}, },
}; };
export default nextConfig; export default withNextIntl(nextConfig);

135
package-lock.json generated
View File

@ -16,6 +16,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.532.0", "lucide-react": "^0.532.0",
"next": "15.4.4", "next": "15.4.4",
"next-intl": "^4.3.4",
"prisma": "^6.12.0", "prisma": "^6.12.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
@ -234,6 +235,66 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.1",
"decimal.js": "^10.4.3",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/icu-skeleton-parser": "1.8.14",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
"license": "MIT",
"dependencies": {
"tslib": "2"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1074,6 +1135,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@schummar/icu-type-parser": {
"version": "1.21.5",
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT"
},
"node_modules/@stitches/core": { "node_modules/@stitches/core": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz", "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz",
@ -2744,6 +2811,12 @@
} }
} }
}, },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -3930,6 +4003,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/intl-messageformat": {
"version": "10.7.16",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.2",
"tslib": "^2.8.0"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -4945,6 +5030,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/next": { "node_modules/next": {
"version": "15.4.4", "version": "15.4.4",
"resolved": "https://registry.npmjs.org/next/-/next-15.4.4.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.4.4.tgz",
@ -4997,6 +5091,33 @@
} }
} }
}, },
"node_modules/next-intl": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.3.4.tgz",
"integrity": "sha512-VWLIDlGbnL/o4LnveJTJD1NOYN8lh3ZAGTWw2krhfgg53as3VsS4jzUVnArJdqvwtlpU/2BIDbWTZ7V4o1jFEw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/amannn"
}
],
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"negotiator": "^1.0.0",
"use-intl": "^4.3.4"
},
"peerDependencies": {
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
"typescript": "^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -6343,6 +6464,20 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-intl": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.3.4.tgz",
"integrity": "sha512-sHfiU0QeJ1rirNWRxvCyvlSh9+NczcOzRnPyMeo2rtHXhVnBsvMRjE+UG4eh3lRhCxrvcqei/I0lBxsc59on1w==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "^2.2.0",
"@schummar/icu-type-parser": "1.21.5",
"intl-messageformat": "^10.5.14"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@ -17,6 +17,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.532.0", "lucide-react": "^0.532.0",
"next": "15.4.4", "next": "15.4.4",
"next-intl": "^4.3.4",
"prisma": "^6.12.0", "prisma": "^6.12.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",

View File

@ -1,5 +1,7 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getLocale } from 'next-intl/server';
import { ThemeProvider } from "@/contexts/ThemeContext"; import { ThemeProvider } from "@/contexts/ThemeContext";
import "./globals.css"; import "./globals.css";
@ -54,13 +56,16 @@ export const viewport: Viewport = {
], ],
}; };
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const locale = await getLocale();
const messages = await getMessages();
return ( return (
<html lang="en" suppressHydrationWarning> <html lang={locale} suppressHydrationWarning>
<head> <head>
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<script <script
@ -84,7 +89,9 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen`} className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen`}
> >
<ThemeProvider defaultTheme="system" storageKey="prmbr-theme"> <ThemeProvider defaultTheme="system" storageKey="prmbr-theme">
{children} <NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>

View File

@ -1,10 +1,14 @@
'use client' 'use client'
import { useTranslations } from 'next-intl'
import { Header } from '@/components/layout/Header' import { Header } from '@/components/layout/Header'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Zap, Target, Layers, BarChart3, Check } from 'lucide-react' import { Zap, Target, Layers, BarChart3, Check } from 'lucide-react'
export default function Home() { export default function Home() {
const t = useTranslations('home')
const tPricing = useTranslations('pricing')
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<Header /> <Header />
@ -14,18 +18,17 @@ export default function Home() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24">
<div className="text-center"> <div className="text-center">
<h1 className="text-4xl md:text-6xl font-bold text-foreground mb-6"> <h1 className="text-4xl md:text-6xl font-bold text-foreground mb-6">
AI Prompt Studio {t('hero.title')}
</h1> </h1>
<p className="text-xl text-muted-foreground mb-8 max-w-3xl mx-auto"> <p className="text-xl text-muted-foreground mb-8 max-w-3xl mx-auto">
Build, test, and manage your AI prompts with precision. {t('hero.subtitle')}
Version control, collaboration tools, and analytics in one professional platform.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button size="lg" onClick={() => window.location.href = '/studio'}> <Button size="lg" onClick={() => window.location.href = '/studio'}>
Start Building {t('hero.getStarted')}
</Button> </Button>
<Button variant="outline" size="lg"> <Button variant="outline" size="lg">
View Showcase {t('hero.learnMore')}
</Button> </Button>
</div> </div>
</div> </div>
@ -37,7 +40,7 @@ export default function Home() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl font-bold text-foreground mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
Everything you need to master prompts {t('features.title')}
</h2> </h2>
<p className="text-xl text-muted-foreground"> <p className="text-xl text-muted-foreground">
Professional tools for prompt engineering and management Professional tools for prompt engineering and management
@ -48,30 +51,30 @@ export default function Home() {
<div className="bg-card p-6 rounded-lg shadow-sm border"> <div className="bg-card p-6 rounded-lg shadow-sm border">
<Target className="h-12 w-12 text-primary mb-4" /> <Target className="h-12 w-12 text-primary mb-4" />
<h3 className="text-lg font-semibold text-card-foreground mb-2"> <h3 className="text-lg font-semibold text-card-foreground mb-2">
Prompt Builder {t('features.promptManager.title')}
</h3> </h3>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Intuitive interface for crafting and refining AI prompts with real-time preview. {t('features.promptManager.description')}
</p> </p>
</div> </div>
<div className="bg-card p-6 rounded-lg shadow-sm border"> <div className="bg-card p-6 rounded-lg shadow-sm border">
<Layers className="h-12 w-12 text-primary mb-4" /> <Layers className="h-12 w-12 text-primary mb-4" />
<h3 className="text-lg font-semibold text-card-foreground mb-2"> <h3 className="text-lg font-semibold text-card-foreground mb-2">
Version Control {t('features.versionControl.title')}
</h3> </h3>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Track changes, compare versions, and rollback to previous iterations seamlessly. {t('features.versionControl.description')}
</p> </p>
</div> </div>
<div className="bg-card p-6 rounded-lg shadow-sm border"> <div className="bg-card p-6 rounded-lg shadow-sm border">
<BarChart3 className="h-12 w-12 text-primary mb-4" /> <BarChart3 className="h-12 w-12 text-primary mb-4" />
<h3 className="text-lg font-semibold text-card-foreground mb-2"> <h3 className="text-lg font-semibold text-card-foreground mb-2">
Test & Analytics {t('features.testing.title')}
</h3> </h3>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Run tests, analyze performance, and optimize your prompts with detailed metrics. {t('features.testing.description')}
</p> </p>
</div> </div>
@ -93,7 +96,7 @@ export default function Home() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl font-bold text-foreground mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
Simple, transparent pricing {tPricing('title')}
</h2> </h2>
<p className="text-xl text-muted-foreground"> <p className="text-xl text-muted-foreground">
Choose the plan that fits your needs Choose the plan that fits your needs
@ -103,22 +106,22 @@ export default function Home() {
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto"> <div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<div className="bg-card p-8 rounded-lg shadow-sm border"> <div className="bg-card p-8 rounded-lg shadow-sm border">
<div className="text-center mb-6"> <div className="text-center mb-6">
<h3 className="text-2xl font-bold text-card-foreground mb-2">Free</h3> <h3 className="text-2xl font-bold text-card-foreground mb-2">{tPricing('free.title')}</h3>
<div className="text-4xl font-bold text-card-foreground mb-2">$0</div> <div className="text-4xl font-bold text-card-foreground mb-2">{tPricing('free.price')}</div>
<p className="text-muted-foreground">Perfect for getting started</p> <p className="text-muted-foreground">Perfect for getting started</p>
</div> </div>
<ul className="space-y-3 mb-8"> <ul className="space-y-3 mb-8">
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" /> <Check className="h-5 w-5 text-green-500 mr-3" />
<span className="text-card-foreground">20 prompts</span> <span className="text-card-foreground">20 Prompt Limit</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" /> <Check className="h-5 w-5 text-green-500 mr-3" />
<span className="text-card-foreground">3 versions per prompt</span> <span className="text-card-foreground">3 Versions per Prompt</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" /> <Check className="h-5 w-5 text-green-500 mr-3" />
<span className="text-card-foreground">$5 AI credits monthly</span> <span className="text-card-foreground">$5 AI Credit Monthly</span>
</li> </li>
</ul> </ul>
<Button className="w-full" onClick={() => window.location.href = '/signup'}> <Button className="w-full" onClick={() => window.location.href = '/signup'}>
@ -131,26 +134,26 @@ export default function Home() {
Popular Popular
</div> </div>
<div className="text-center mb-6"> <div className="text-center mb-6">
<h3 className="text-2xl font-bold mb-2">Pro</h3> <h3 className="text-2xl font-bold mb-2">{tPricing('pro.title')}</h3>
<div className="text-4xl font-bold mb-2">$19.9</div> <div className="text-4xl font-bold mb-2">{tPricing('pro.price')}</div>
<p className="text-primary-foreground/80">per month</p> <p className="text-primary-foreground/80">per month</p>
</div> </div>
<ul className="space-y-3 mb-8"> <ul className="space-y-3 mb-8">
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" /> <Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
<span>500 prompts</span> <span>500 Prompt Limit</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" /> <Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
<span>10 versions per prompt</span> <span>10 Versions per Prompt</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" /> <Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
<span>$20 AI credits monthly</span> <span>$20 AI Credit Monthly</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" /> <Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
<span>Priority support</span> <span>Priority Support</span>
</li> </li>
</ul> </ul>
<Button variant="outline" className="w-full bg-primary-foreground text-primary hover:bg-primary-foreground/90" onClick={() => window.location.href = '/signup'}> <Button variant="outline" className="w-full bg-primary-foreground text-primary hover:bg-primary-foreground/90" onClick={() => window.location.href = '/signup'}>

View File

@ -13,7 +13,6 @@ import {
Save, Save,
Copy, Copy,
Settings, Settings,
FileText,
Folder, Folder,
Plus, Plus,
Search, Search,
@ -75,7 +74,7 @@ export default function StudioPage() {
// Mock response // Mock response
setTestResult(`Test result for: "${promptContent.substring(0, 50)}${promptContent.length > 50 ? '...' : ''}"\n\nThis is a simulated response from the AI model. In a real implementation, this would be the actual output from your chosen AI provider (OpenAI, Anthropic, etc.).\n\nResponse quality: Good\nToken usage: 150 tokens\nLatency: 1.2s`) setTestResult(`Test result for: "${promptContent.substring(0, 50)}${promptContent.length > 50 ? '...' : ''}"\n\nThis is a simulated response from the AI model. In a real implementation, this would be the actual output from your chosen AI provider (OpenAI, Anthropic, etc.).\n\nResponse quality: Good\nToken usage: 150 tokens\nLatency: 1.2s`)
} catch (error) { } catch {
setTestResult('Error: Failed to run prompt. Please try again.') setTestResult('Error: Failed to run prompt. Please try again.')
} finally { } finally {
setIsRunning(false) setIsRunning(false)
@ -308,7 +307,7 @@ Please write a professional email that..."
<div> <div>
<Zap className="h-12 w-12 text-muted-foreground mx-auto mb-4" /> <Zap className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Click "Run Test" to see your prompt results Click &quot;Run Test&quot; to see your prompt results
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,13 +1,17 @@
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import Link from 'next/link'
import { useTranslations } from 'next-intl'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ThemeToggle, MobileThemeToggle } from '@/components/ui/theme-toggle' import { ThemeToggle, MobileThemeToggle } from '@/components/ui/theme-toggle'
import { LanguageToggle, MobileLanguageToggle } from '@/components/ui/language-toggle'
import { Menu, X, Zap } from 'lucide-react' import { Menu, X, Zap } from 'lucide-react'
export function Header() { export function Header() {
const { user, signOut } = useAuth() const { user, signOut } = useAuth()
const t = useTranslations('navigation')
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return ( return (
@ -24,38 +28,39 @@ export function Header() {
{/* Desktop Navigation */} {/* Desktop Navigation */}
<div className="hidden md:block"> <div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4"> <div className="ml-10 flex items-baseline space-x-4">
<a href="#features" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors"> <Link href="/" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
Features {t('home')}
</a> </Link>
<Link href="/studio" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
{t('studio')}
</Link>
<a href="#pricing" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors"> <a href="#pricing" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
Pricing Pricing
</a> </a>
<a href="#showcase" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
Showcase
</a>
</div> </div>
</div> </div>
{/* Desktop Auth */} {/* Desktop Auth */}
<div className="hidden md:block"> <div className="hidden md:block">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<LanguageToggle variant="dropdown" showLabel={false} />
<ThemeToggle variant="dropdown" showLabel={false} /> <ThemeToggle variant="dropdown" showLabel={false} />
{user ? ( {user ? (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button variant="ghost" size="sm" onClick={() => window.location.href = '/profile'}> <Button variant="ghost" size="sm" onClick={() => window.location.href = '/profile'}>
Profile {t('profile')}
</Button> </Button>
<Button variant="outline" onClick={signOut}> <Button variant="outline" onClick={signOut}>
Sign Out {t('signOut')}
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button variant="ghost" onClick={() => window.location.href = '/signin'}> <Button variant="ghost" onClick={() => window.location.href = '/signin'}>
Sign In {t('signIn')}
</Button> </Button>
<Button onClick={() => window.location.href = '/signup'}> <Button onClick={() => window.location.href = '/signup'}>
Sign Up {t('signUp')}
</Button> </Button>
</div> </div>
)} )}
@ -64,6 +69,7 @@ export function Header() {
{/* Mobile controls */} {/* Mobile controls */}
<div className="md:hidden flex items-center space-x-2"> <div className="md:hidden flex items-center space-x-2">
<LanguageToggle variant="button" />
<ThemeToggle variant="button" /> <ThemeToggle variant="button" />
<button <button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
@ -79,18 +85,23 @@ export function Header() {
{mobileMenuOpen && ( {mobileMenuOpen && (
<div className="md:hidden"> <div className="md:hidden">
<div className="px-2 pt-2 pb-3 space-y-1 border-t"> <div className="px-2 pt-2 pb-3 space-y-1 border-t">
<a href="#features" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent"> <Link href="/" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
Features {t('home')}
</a> </Link>
<Link href="/studio" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
{t('studio')}
</Link>
<a href="#pricing" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent"> <a href="#pricing" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
Pricing Pricing
</a> </a>
<a href="#showcase" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
Showcase {/* Mobile Language Toggle */}
</a> <div className="pt-4 pb-2 border-t">
<MobileLanguageToggle />
</div>
{/* Mobile Theme Toggle */} {/* Mobile Theme Toggle */}
<div className="pt-4 pb-2 border-t"> <div className="pt-2 pb-2 border-t">
<MobileThemeToggle /> <MobileThemeToggle />
</div> </div>
@ -98,19 +109,19 @@ export function Header() {
{user ? ( {user ? (
<div className="space-y-2"> <div className="space-y-2">
<Button variant="ghost" className="w-full justify-start" onClick={() => window.location.href = '/profile'}> <Button variant="ghost" className="w-full justify-start" onClick={() => window.location.href = '/profile'}>
Profile {t('profile')}
</Button> </Button>
<Button variant="outline" className="w-full" onClick={signOut}> <Button variant="outline" className="w-full" onClick={signOut}>
Sign Out {t('signOut')}
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
<Button variant="ghost" className="w-full" onClick={() => window.location.href = '/signin'}> <Button variant="ghost" className="w-full" onClick={() => window.location.href = '/signin'}>
Sign In {t('signIn')}
</Button> </Button>
<Button className="w-full" onClick={() => window.location.href = '/signup'}> <Button className="w-full" onClick={() => window.location.href = '/signup'}>
Sign Up {t('signUp')}
</Button> </Button>
</div> </div>
)} )}

View File

@ -0,0 +1,153 @@
'use client'
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import { Languages, Check } from 'lucide-react'
interface LanguageToggleProps {
variant?: 'dropdown' | 'button'
showLabel?: boolean
}
export function LanguageToggle({ variant = 'dropdown', showLabel = true }: LanguageToggleProps) {
const t = useTranslations('common')
const [currentLocale, setCurrentLocale] = useState<string>('en')
const [dropdownOpen, setDropdownOpen] = useState(false)
const languages = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'zh', name: 'Chinese', nativeName: '中文' }
]
useEffect(() => {
// Get current locale from cookie
const localeCookie = document.cookie
.split('; ')
.find(row => row.startsWith('locale='))
?.split('=')[1];
if (localeCookie) {
setCurrentLocale(localeCookie)
}
}, [])
const changeLanguage = (locale: string) => {
// Set cookie
document.cookie = `locale=${locale}; path=/; max-age=${60 * 60 * 24 * 365}; samesite=lax`
// Reload page to apply new locale
window.location.reload()
}
if (variant === 'button') {
return (
<Button
variant="ghost"
size="sm"
onClick={() => {
const nextLocale = currentLocale === 'en' ? 'zh' : 'en'
changeLanguage(nextLocale)
}}
className="h-9 w-9 px-0"
>
<Languages className="h-4 w-4" />
<span className="sr-only">{t('language')}</span>
</Button>
)
}
return (
<div className="relative">
<Button
variant="ghost"
size="sm"
onClick={() => setDropdownOpen(!dropdownOpen)}
className="h-9 gap-2"
>
<Languages className="h-4 w-4" />
{showLabel && <span className="hidden sm:inline">{t('language')}</span>}
</Button>
{dropdownOpen && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setDropdownOpen(false)}
/>
<div className="absolute right-0 top-full z-50 mt-2 w-48 rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
{languages.map((language) => (
<button
key={language.code}
onClick={() => {
changeLanguage(language.code)
setDropdownOpen(false)
}}
className="relative flex w-full items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<span className="flex-1 text-left">
{language.nativeName}
</span>
{currentLocale === language.code && (
<Check className="h-4 w-4" />
)}
</button>
))}
</div>
</>
)}
</div>
)
}
export function MobileLanguageToggle() {
const t = useTranslations('common')
const [currentLocale, setCurrentLocale] = useState<string>('en')
const languages = [
{ code: 'en', name: 'English', nativeName: 'English' },
{ code: 'zh', name: 'Chinese', nativeName: '中文' }
]
useEffect(() => {
// Get current locale from cookie
const localeCookie = document.cookie
.split('; ')
.find(row => row.startsWith('locale='))
?.split('=')[1];
if (localeCookie) {
setCurrentLocale(localeCookie)
}
}, [])
const changeLanguage = (locale: string) => {
// Set cookie
document.cookie = `locale=${locale}; path=/; max-age=${60 * 60 * 24 * 365}; samesite=lax`
// Reload page to apply new locale
window.location.reload()
}
return (
<div className="space-y-2">
<div className="flex items-center gap-2 px-3 py-2">
<Languages className="h-4 w-4" />
<span className="text-sm font-medium">{t('language')}</span>
</div>
<div className="grid grid-cols-2 gap-2">
{languages.map((language) => (
<Button
key={language.code}
variant={currentLocale === language.code ? "default" : "outline"}
size="sm"
onClick={() => changeLanguage(language.code)}
className="justify-start"
>
{language.nativeName}
</Button>
))}
</div>
</div>
)
}

66
src/i18n/config.ts Normal file
View File

@ -0,0 +1,66 @@
import { getRequestConfig } from 'next-intl/server';
import { headers } from 'next/headers';
export const locales = ['en', 'zh'] as const;
export const defaultLocale = 'en' as const;
export type Locale = (typeof locales)[number];
async function getLocaleFromHeaders(): Promise<Locale> {
const headersList = await headers();
const acceptLanguage = headersList.get('accept-language');
if (acceptLanguage) {
// Parse accept-language header
const languages = acceptLanguage
.split(',')
.map(lang => lang.split(';')[0].trim().toLowerCase());
// Check for exact matches first
for (const lang of languages) {
if (locales.includes(lang as Locale)) {
return lang as Locale;
}
}
// Check for language prefix matches (e.g., 'zh-CN' -> 'zh')
for (const lang of languages) {
const prefix = lang.split('-')[0];
if (locales.includes(prefix as Locale)) {
return prefix as Locale;
}
}
}
return defaultLocale;
}
export default getRequestConfig(async () => {
// Get locale from user preference (cookie) or browser headers
const headersList = await headers();
const cookieHeader = headersList.get('cookie');
let locale: Locale = defaultLocale;
// Parse locale from cookies
if (cookieHeader) {
const cookies = cookieHeader.split(';');
const localeCookie = cookies.find(cookie => cookie.trim().startsWith('locale='));
if (localeCookie) {
const localeValue = localeCookie.split('=')[1];
if (locales.includes(localeValue as Locale)) {
locale = localeValue as Locale;
}
}
}
// If no locale found in cookies, detect from headers
if (locale === defaultLocale) {
locale = await getLocaleFromHeaders();
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});