├── .editorconfig
├── .env.example
├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── Dockerfile
├── LICENSE
├── README.md
├── README_ja.md
├── README_zh.md
├── app
├── [locale]
│ ├── error.tsx
│ ├── layout.tsx
│ ├── page.tsx
│ └── share
│ │ └── [id]
│ │ └── page.tsx
├── actions
│ ├── chat.ts
│ └── prompt.ts
├── api
│ └── share
│ │ └── route.ts
├── components
│ ├── client-only.tsx
│ ├── code-viewer.tsx
│ ├── error-handler.tsx
│ ├── footer.tsx
│ ├── header.tsx
│ ├── icons
│ │ ├── logo-icon.tsx
│ │ └── right-arrow.tsx
│ ├── main.tsx
│ ├── providers
│ │ ├── index.tsx
│ │ └── theme-provider.tsx
│ └── toolbar
│ │ ├── index.tsx
│ │ ├── language-switcher.tsx
│ │ ├── share.tsx
│ │ └── theme-switcher.tsx
├── globals.css
├── hooks
│ ├── use-302url.ts
│ ├── use-client-translation.ts
│ ├── use-file-upload.ts
│ ├── use-is-dark.ts
│ ├── use-is-share-path.ts
│ ├── use-is-support-vision.ts
│ └── use-throttled-state.ts
├── i18n
│ ├── client.ts
│ ├── index.ts
│ ├── locales
│ │ ├── en
│ │ │ ├── auth.json
│ │ │ ├── extras.json
│ │ │ ├── home.json
│ │ │ └── translation.json
│ │ ├── ja
│ │ │ ├── auth.json
│ │ │ ├── extras.json
│ │ │ ├── home.json
│ │ │ └── translation.json
│ │ └── zh
│ │ │ ├── auth.json
│ │ │ ├── extras.json
│ │ │ ├── home.json
│ │ │ └── translation.json
│ └── settings.ts
└── stores
│ ├── middleware.ts
│ ├── use-code-store.ts
│ └── use-user-store.ts
├── components.json
├── components
└── ui
│ ├── button.tsx
│ ├── checkbox.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── popover.tsx
│ ├── radio-group.tsx
│ ├── skeleton.tsx
│ ├── switch.tsx
│ └── tooltip.tsx
├── docs
├── AI网页生成器.png
├── AI网页生成器en.png
├── AI网页生成器jp.png
├── preview.png
├── 网页生成1.png
├── 网页生成2.png
└── 网页生成3.png
├── lib
├── api
│ ├── api.ts
│ └── lang-to-country.ts
├── brand.ts
├── check-env.ts
├── logger.ts
├── mitt.ts
├── shadcn-docs
│ ├── accordion.tsx
│ ├── alert-dialog.tsx
│ ├── alert.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── breadcrumb.tsx
│ ├── button.tsx
│ ├── calendar.tsx
│ ├── card.tsx
│ ├── carousel.tsx
│ ├── checkbox.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── index.ts
│ ├── input.tsx
│ ├── label.tsx
│ ├── menubar.tsx
│ ├── navigation-menu.tsx
│ ├── pagination.tsx
│ ├── popover.tsx
│ ├── progress.tsx
│ ├── radio-group.tsx
│ ├── resizable.tsx
│ ├── scroll-area.tsx
│ ├── select.tsx
│ ├── slider.tsx
│ ├── switch.tsx
│ ├── table.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toggle-group.tsx
│ ├── toggle.tsx
│ └── tooltip.tsx
├── shadcn.ts
├── stream.ts
└── utils.ts
├── middleware.ts
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── public
├── favicon.ico
└── images
│ ├── desc_en.png
│ ├── desc_ja.png
│ ├── desc_zh.png
│ ├── logo-dark.png
│ └── logo-light.png
├── tailwind.config.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Your 302 AI API Key
2 | NEXT_PUBLIC_API_KEY=
3 |
4 | # -----------------------------------
5 | # Whether to show the 302 AI brand
6 | NEXT_PUBLIC_SHOW_BRAND=false
7 | # Set the ai model you want to use
8 | NEXT_PUBLIC_MODEL_NAME=gpt-4o
9 | # 0 for China, 1 for Global
10 | NEXT_PUBLIC_REGION=0
11 | # Set the locale you want to use, zh for Chinese, en for English, ja for Japanese
12 | NEXT_PUBLIC_LOCALE=en
13 | # The url of the 302 AI API
14 | NEXT_PUBLIC_API_URL=https://api.302.ai
15 |
16 | # The url of the 302 AI official website
17 | NEXT_PUBLIC_OFFICIAL_WEBSITE_URL_CHINA=https://302ai.cn/
18 | NEXT_PUBLIC_OFFICIAL_WEBSITE_URL_GLOBAL=https://302.ai/
19 |
20 | # The prefix of the share path
21 | NEXT_PUBLIC_SHARE_PATH=/share
22 | # The default share directory
23 | NEXT_PUBLIC_DEFAULT_SHARE_DIR=/shared
24 |
25 | # The url of the 302 AI upload API
26 | NEXT_PUBLIC_UPLOAD_API_URL=https://dash-api.302.ai/gpt/api/upload/gpt/image
27 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | /.vscode/
4 |
5 | # dependencies
6 | /node_modules
7 | /.pnp
8 | .pnp.js
9 | .yarn/install-state.gz
10 |
11 | # testing
12 | /coverage
13 |
14 | # next.js
15 | /.next/
16 | /out/
17 |
18 | # production
19 | /build
20 |
21 | # misc
22 | .DS_Store
23 | *.pem
24 |
25 | # debug
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
30 | # local env files
31 | .env*.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "semi": false,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | "jsxSingleQuote": true,
7 | "plugins": ["prettier-plugin-tailwindcss"]
8 | }
9 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Base image
2 | FROM node:20.14-alpine AS base
3 |
4 | # Install dependencies only when needed
5 | FROM base AS deps
6 | # Install compatibility libraries
7 | RUN apk add --no-cache libc6-compat
8 | WORKDIR /app
9 |
10 | # Copy dependency files
11 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
12 |
13 | # Install dependencies based on lock files
14 | RUN \
15 | if [ -f yarn.lock ]; then \
16 | corepack enable && \
17 | yarn --frozen-lockfile; \
18 | elif [ -f package-lock.json ]; then \
19 | npm config set registry https://registry.npmmirror.com && \
20 | npm ci; \
21 | elif [ -f pnpm-lock.yaml ]; then \
22 | corepack enable pnpm && \
23 | pnpm config set registry https://registry.npmmirror.com && \
24 | pnpm i --frozen-lockfile; \
25 | else \
26 | echo "No lock file found." && exit 1; \
27 | fi
28 |
29 | # Rebuild source code only when needed
30 | FROM base AS builder
31 | WORKDIR /app
32 |
33 | # Copy dependencies and source code
34 | COPY --from=deps /app/node_modules ./node_modules
35 | COPY . .
36 |
37 | # Build project based on build mode
38 | RUN corepack enable pnpm && pnpm run build;
39 |
40 | # Production image, copy all files and run Next.js
41 | FROM base AS runner
42 | WORKDIR /app
43 |
44 | # Create a non-root user
45 | RUN addgroup --system --gid 1001 nodejs
46 | RUN adduser --system --uid 1001 nextjs
47 |
48 | # Copy static files
49 | COPY --from=builder /app/public ./public
50 |
51 | # Set permissions for prerendered cache
52 | RUN mkdir .next
53 | RUN chown nextjs:nodejs .next
54 |
55 | # Copy build artefacts
56 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
57 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
58 |
59 | # Copy .env file
60 | COPY --chown=nextjs:nodejs .env .env
61 |
62 | # Create persistent data directory and set permissions
63 | RUN mkdir -p /app/shared && chmod -R 777 /app/shared
64 |
65 | # Switch to non-root user
66 | USER nextjs
67 |
68 | # Expose port
69 | EXPOSE 3000
70 |
71 | # Set environment variables
72 | ENV PORT=3000
73 |
74 | # Start command
75 | CMD HOSTNAME="0.0.0.0" node server.js
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
💻 AI Web Page Generator 2.0 🚀✨
2 |
3 | The AI Web Page Generator 2.0 can generate high-quality front-end HTML files through the AI large model by simply using natural language to describe the content of the web page. It supports the use of shadcn/ui.
4 |
5 |
6 |
7 | 中文 | English | 日本語
8 |
9 | 
10 |
11 | Open-source version of the [AI Web Page Generator 2.0](https://302.ai/en/tools/coder/) from [302.AI](https://302.ai/en/).
12 | You can directly log in to 302.AI for a zero-code, zero-configuration online experience.
13 | Alternatively, customize this project to suit your needs, integrate 302.AI's API KEY, and deploy it yourself.
14 |
15 | ## Interface Preview
16 | Generate web pages according to users' needs. The left column shows the code of the web page, and the right column shows the preview image of the web page.
17 | 
18 |
19 | ## Project Features
20 | ### 🤖 Intelligent Code Generation
21 | Automatically generate code according to your needs.
22 | ### ✍️ Flexible Editing
23 | You can adjust and modify the code content at any time during the generation process.
24 | ### 🎨 Flexible UI Selection
25 | It supports the shadcn/ui component library to quickly create an aesthetically pleasing interface.
26 | ### 🌟 3D Visualization
27 | It supports three.js, enabling you to easily achieve 3D visualization functions.
28 | ### 🛠️ Prompt Optimization
29 | Optimize the prompts to make the content generated by AI more accurate.
30 | ### 🖼️ Image Assistance
31 | It supports uploading design drawings and allows AI to generate corresponding code based on the images.
32 | ### 💬 Multi-round Interaction
33 | It supports continuous conversations and continuously adjusts code generation according to feedback.
34 | ### 🔗 Code Reference
35 | You can reference the generated code snippets and ask AI to make corresponding modifications.
36 | ### 📤Convenient Sharing
37 | Easily share the generated code so that more people can appreciate your work.
38 | ### 🌙 Eye-friendly Dark Mode
39 | Provide a dark mode to take care of your eye health.
40 | ### 🌍 Multi-language Support
41 | - Chinese Interface
42 | - English Interface
43 | - Japanese Interface
44 |
45 | With AI Code Generator 2.0, anyone can become a code creator! 🎉💻 Let's explore the world of AI-driven code together! 🌟🚀
46 |
47 | ## 🚩 Future Update Plans
48 | - [ ] The simplicity of the code is improved
49 | - [ ] The expansion of diverse templates
50 | - [ ] The newly added function of generating dynamic content
51 |
52 | ## Tech Stack
53 | - Next.js 14
54 | - Tailwind CSS
55 | - Shadcn UI
56 | - Sandpack
57 | - Vecel AI SDK
58 |
59 | ## Development & Deployment
60 | 1. Clone the project `git clone https://github.com/302ai/302_coder_generator`
61 | 2. Install dependencies `pnpm install`
62 | 3. Configure 302's API KEY as per .env.example
63 | 4. Run the project `pnpm dev`
64 | 5. Build and deploy `docker build -t coder-generator . && docker run -p 3000:3000 coder-generator`
65 |
66 |
67 | ## ✨ About 302.AI ✨
68 | [302.AI](https://302.ai) is an enterprise-oriented AI application platform that offers pay-as-you-go services, ready-to-use solutions, and an open-source ecosystem.✨
69 | 1. 🧠 Comprehensive AI capabilities: Incorporates the latest in language, image, audio, and video models from leading AI brands.
70 | 2. 🚀 Advanced application development: We build genuine AI products, not just simple chatbots.
71 | 3. 💰 No monthly fees: All features are pay-per-use, fully accessible, ensuring low entry barriers with high potential.
72 | 4. 🛠 Powerful admin dashboard: Designed for teams and SMEs - managed by one, used by many.
73 | 5. 🔗 API access for all AI features: All tools are open-source and customizable (in progress).
74 | 6. 💡 Powerful development team: Launching 2-3 new applications weekly with daily product updates. Interested developers are welcome to contact us.
75 |
--------------------------------------------------------------------------------
/README_ja.md:
--------------------------------------------------------------------------------
1 | # 💻 AIウェブページジェネレーター2.0🚀✨
2 |
3 | AI ウェブページ生成器 2.0 は、自然言語でウェブページの内容を記述するだけで、AI 大規模モデルを通じて高品質なフロントエンド HTML ファイルを生成できます。shadcn/ui の使用をサポートしています。
4 |
5 |
6 |
7 | 中文 | English | 日本語
8 |
9 | 
10 |
11 | [302.AI](https://302.ai/ja/)の[AIウェブページジェネレーター2.0](https://302.ai/ja/tools/coder/)のオープンソース版です。
12 | 302.AIに直接ログインすることで、コード不要、設定不要のオンライン体験が可能です。
13 | あるいは、このプロジェクトをニーズに合わせてカスタマイズし、302.AIのAPI KEYを統合して、自身でデプロイすることもできます。
14 |
15 | ## インターフェースプレビュー
16 | ユーザーのニーズに応じてウェブページを生成します。左の欄にはウェブページのコードが表示され、右の欄にはウェブページのプレビュー画像が表示されます。
17 | 
18 |
19 | ## プロジェクトの特徴
20 | ### 🤖 知的コード生成
21 | あなたのニーズに応じて自動的にコードを生成します。
22 | ### ✍️ 柔軟な編集
23 | 生成プロセス中にいつでもコード内容を調整および修正できます。
24 | ### 🎨 UI の柔軟な選択
25 | shadcn/ui コンポーネントライブラリをサポートし、素敵なインターフェースをすばやく作成できます。
26 | ### 🌟 3D 可視化
27 | three.js をサポートし、簡単に 3D 可視化機能を実現できます。
28 | ### 🛠️ プロンプトの最適化
29 | プロンプトを最適化し、AI が生成する内容をより正確にします。
30 | ### 🖼️ 画像支援
31 | デザイン図をアップロードすることをサポートし、AI に画像に基づいて対応するコードを生成させます。
32 | ### 💬 マルチラウンド相互作用
33 | 継続的な会話をサポートし、フィードバックに応じてコード生成を継続的に調整します。
34 | ### 🔗 コード参照
35 | 生成されたコードスニペットを参照し、AI に対応する修正を行わせることができます。
36 | ### 📤 便利な共有
37 | 生成されたコードを簡単に共有し、より多くの人にあなたの作品を鑑賞させます。
38 | ### 🌙 配慮のあるダークモード
39 | ダークモードを提供し、あなたの目の健康を守ります。
40 | ### 🌍 多言語サポート
41 | - 中国語インターフェース
42 | - 英語インターフェース
43 | - 日本語インターフェース
44 |
45 |
46 | AIコードジェネレーター2.0を使用すると、誰でもコードクリエーターになれます!🎉💻 AIが駆動するコード生成の新しい世界を一緒に探索しましょう!🌟🚀
47 |
48 | ## 🚩 将来のアップデート計画
49 | - [ ] コードの簡素性が向上します
50 | - [ ] 多様なテンプレートの拡充
51 | - [ ] 動的な内容生成機能が新たに追加されます
52 |
53 | ## 技術スタック
54 | - Next.js 14
55 | - Tailwind CSS
56 | - Shadcn UI
57 | - Sandpack
58 | - Vecel AI SDK
59 |
60 | ## 開発とデプロイ
61 | 1. プロジェクトをクローンする `git clone https://github.com/302ai/302_coder_generator`
62 | 2. 依存関係をインストールする `pnpm install`
63 | 3. 302のAPI KEYを設定する `.env.exampleを参照`
64 | 4. プロジェクトを実行する `pnpm dev`
65 | 5. パッケージングとデプロイ `docker build -t coder-generator . && docker run -p 3000:3000 coder-generator`
66 |
67 |
68 | ## ✨ 302.AIについて ✨
69 | [302.AI](https://302.ai)は企業向けのAIアプリケーションプラットフォームであり、必要に応じて支払い、すぐに使用できるオープンソースのエコシステムです。✨
70 | 1. 🧠 包括的なAI機能:主要AIブランドの最新の言語、画像、音声、ビデオモデルを統合。
71 | 2. 🚀 高度なアプリケーション開発:単なるシンプルなチャットボットではなく、本格的なAI製品を構築。
72 | 3. 💰 月額料金なし:すべての機能が従量制で、完全にアクセス可能。低い参入障壁と高い可能性を確保。
73 | 4. 🛠 強力な管理ダッシュボード:チームやSME向けに設計 - 一人で管理し、多くの人が使用可能。
74 | 5. 🔗 すべてのAI機能へのAPIアクセス:すべてのツールはオープンソースでカスタマイズ可能(進行中)。
75 | 6. 💪 強力な開発チーム:大規模で高度なスキルを持つ開発者集団。毎週2-3の新しいアプリケーションをリリースし、毎日製品更新を行っています。才能ある開発者の参加を歓迎します。
76 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | # 💻 AI 网页生成器2.0 🚀✨
2 |
3 | AI网页生成器2.0使用自然语言描述网页内容,即可通过AI大模型生成高质量前端HTML文件,支持使用shadcn/ui。
4 |
5 |
6 |
7 | 中文 | English | 日本語
8 |
9 | 
10 |
11 | 来自[302.AI](https://302.ai)的[AI网页生成器2.0](https://302.ai/tools/coder/)的开源版本。
12 | 你可以直接登录302.AI,零代码零配置使用在线版本。
13 | 或者对本项目根据自己的需求进行修改,传入302.AI的API KEY,自行部署。
14 |
15 |
16 | ## 界面预览
17 | 根据用户的需求生成网页,左栏为网页的代码,右栏为网页的预览图。
18 | 
19 |
20 | ## 项目特性
21 | ### 🤖 智能代码生成
22 | 根据您的需求自动生成代码。
23 | ### ✍️ 灵活编辑
24 | 在生成过程中可以随时调整和修改代码内容。
25 | ### 🎨 UI灵活选择
26 | 支持shadcn/ui组件库,快速打造美观界面。
27 | ### 🌟 3D可视化
28 | 支持three.js,轻松实现3D可视化功能。
29 | ### 🛠️ 提示词优化
30 | 对提示词进行优化,使AI生成的内容更精准。
31 | ### 🖼️ 图像辅助
32 | 支持上传设计图,让AI根据图像生成对应代码。
33 | ### 💬 多轮交互
34 | 支持持续对话,根据反馈不断调整代码生成。
35 | ### 🔗 代码引用
36 | 可以引用生成的代码片段,并让AI进行相应修改。
37 | ### 📤 便捷分享
38 | 轻松分享生成的代码,让更多人欣赏您的作品。
39 | ### 🌙 贴心暗色
40 | 提供暗色模式,呵护您的用眼健康。
41 | ### 🌍 多语言支持
42 | - 中文界面
43 | - English Interface
44 | - 日本語インターフェース
45 |
46 | 通过AI网页生成器2.0,任何人都可以成为网页前端创作者! 🎉💻 让我们一起探索AI驱动的网页新世界吧! 🌟🚀
47 |
48 | ## 🚩 未来更新计划
49 | - [ ] 代码精简性提升
50 | - [ ] 多样化模板拓展
51 | - [ ] 新增动态内容生成功能
52 |
53 | ## 技术栈
54 | - Next.js 14
55 | - Tailwind CSS
56 | - Shadcn UI
57 | - Sandpack
58 | - Vecel AI SDK
59 |
60 | ## 开发&部署
61 | 1. 克隆项目 `git clone https://github.com/302ai/302_coder_generator`
62 | 2. 安装依赖 `pnpm install`
63 | 3. 配置302的API KEY 参考.env.example
64 | 4. 运行项目 `pnpm dev`
65 | 5. 打包部署 `docker build -t coder-generator . && docker run -p 3000:3000 coder-generator`
66 |
67 |
68 | ## ✨ 302.AI介绍 ✨
69 | [302.AI](https://302.ai)是一个面向企业的AI应用平台,按需付费,开箱即用,开源生态。✨
70 | 1. 🧠 集合了最新最全的AI能力和品牌,包括但不限于语言模型、图像模型、声音模型、视频模型。
71 | 2. 🚀 在基础模型上进行深度应用开发,我们开发真正的AI产品,而不是简单的对话机器人
72 | 3. 💰 零月费,所有功能按需付费,全面开放,做到真正的门槛低,上限高。
73 | 4. 🛠 功能强大的管理后台,面向团队和中小企业,一人管理,多人使用。
74 | 5. 🔗 所有AI能力均提供API接入,所有工具开源可自行定制(进行中)。
75 | 6. 💡 强大的开发团队,每周推出2-3个新应用,产品每日更新。有兴趣加入的开发者也欢迎联系我们
76 |
--------------------------------------------------------------------------------
/app/[locale]/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import { logger } from '@/lib/logger'
5 | import { useParams } from 'next/navigation'
6 | import { useEffect } from 'react'
7 | import { useTranslation } from 'react-i18next'
8 |
9 | export default function Error({
10 | error,
11 | reset,
12 | }: {
13 | error: Error & { digest?: string }
14 | reset: () => void
15 | }) {
16 | const { locale } = useParams()
17 | const { t } = useTranslation(locale as string)
18 |
19 | useEffect(() => {
20 | logger.error(error)
21 | }, [error])
22 |
23 | return (
24 |
25 |
{t('extras:error_page.title')}
26 | reset()}>
27 | {t('extras:error_page.reload')}
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/app/[locale]/layout.tsx:
--------------------------------------------------------------------------------
1 | import '@/lib/check-env'
2 | import { PublicEnvScript } from 'next-runtime-env'
3 | import '../globals.css'
4 |
5 | import ClientOnly from '@/app/components/client-only'
6 | import { ErrorHandler } from '@/app/components/error-handler'
7 | import { languages } from '@/app/i18n/settings'
8 | import { showBrand } from '@/lib/brand'
9 | import { dir } from 'i18next'
10 | import { Metadata, ResolvingMetadata } from 'next'
11 | import { headers } from 'next/headers'
12 | import { Toaster } from 'react-hot-toast'
13 | import { Providers } from '../components/providers'
14 | import { Toolbar } from '../components/toolbar'
15 |
16 | export async function generateStaticParams() {
17 | return languages.map((locale) => ({ locale }))
18 | }
19 |
20 | type Props = {
21 | params: { locale: string }
22 | searchParams: { [key: string]: string | string[] | undefined }
23 | }
24 |
25 | export async function generateMetadata(
26 | { params: { locale } }: Props,
27 | parent: ResolvingMetadata
28 | ): Promise {
29 | const headers_ = headers()
30 | const hostname = headers_.get('host')
31 |
32 | const previousImages = (await parent).openGraph?.images || []
33 | const seoRes = {
34 | data: {
35 | id: 'videosum',
36 | supportLanguages: ['zh', 'en', 'ja'],
37 | fallbackLanguage: 'en',
38 | languages: {
39 | zh: {
40 | title: 'AI网页生成器2.0',
41 | description: '一键生成高质量的网页',
42 | image: '/images/desc_zh.png',
43 | _id: '66d2de547e3b177ca1c3b490',
44 | },
45 | en: {
46 | title: 'AI Web Page Generator 2.0',
47 | description:
48 | 'Generate high-quality web pages with one click',
49 | image: '/images/desc_en.png',
50 | _id: '66d2de547e3b177ca1c3b491',
51 | },
52 | ja: {
53 | title: 'AIウェブページジェネレーター2.0',
54 | description:
55 | 'ワンクリックで高品質なウェブページを生成します',
56 | image: '/images/desc_ja.png',
57 | _id: '66d2de547e3b177ca1c3b492',
58 | },
59 | },
60 | },
61 | }
62 |
63 | const defaultSEO = {
64 | title: 'Default Title',
65 | description: 'Default Description',
66 | image: '/default-image.jpg',
67 | }
68 |
69 | const info = seoRes?.data?.languages || { [locale]: defaultSEO }
70 | // @ts-ignore
71 | const images = [info[locale].image || defaultSEO.image, ...previousImages]
72 |
73 | return {
74 | // @ts-ignore
75 | title: info[locale].title || defaultSEO.title,
76 | // @ts-ignore
77 | description: info[locale].description || defaultSEO.description,
78 | metadataBase: new URL(`https://${hostname}`),
79 | alternates: {
80 | canonical: `/${locale}`,
81 | languages: languages
82 | .filter((item) => item !== locale)
83 | .map((item) => ({
84 | [item]: `/${item}`,
85 | }))
86 | .reduce((acc, curr) => Object.assign(acc, curr), {}),
87 | },
88 | openGraph: {
89 | url: `/${locale}`,
90 | images,
91 | },
92 | twitter: {
93 | site: `https://${hostname}/${locale}`,
94 | images,
95 | },
96 | }
97 | }
98 |
99 | export default function RootLayout({
100 | children,
101 | params: { locale },
102 | }: Readonly<{
103 | children: React.ReactNode
104 | params: { locale: string }
105 | }>) {
106 | return (
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | {children}
118 | {showBrand && (
119 |
123 | )}
124 |
125 |
126 |
127 |
128 | )
129 | }
130 |
--------------------------------------------------------------------------------
/app/[locale]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Footer } from '@/app/components/footer'
3 | import { useTranslation } from '@/app/i18n/client'
4 | import { Button } from '@/components/ui/button'
5 | import { showBrand } from '@/lib/brand'
6 | import { logger } from '@/lib/logger'
7 | import { emitter } from '@/lib/mitt'
8 | import { useThrottleFn } from 'ahooks'
9 | import { readStreamableValue } from 'ai/rsc'
10 | import dedent from 'dedent'
11 | import { CornerDownRight } from 'lucide-react'
12 | import { env } from 'next-runtime-env'
13 | import { debounce } from 'radash'
14 | import { useCallback, useEffect, useRef, useState } from 'react'
15 | import { toast } from 'react-hot-toast'
16 | import { chat } from '../actions/chat'
17 | import { optimizePrompt } from '../actions/prompt'
18 | import Header from '../components/header'
19 | import Main from '../components/main'
20 | import useFileUpload from '../hooks/use-file-upload'
21 | import { useCodeStore } from '../stores/use-code-store'
22 |
23 | export const maxDuration = 600
24 |
25 | export default function Home({
26 | params: { locale },
27 | }: {
28 | params: { locale: string }
29 | }) {
30 | const { t } = useTranslation(locale)
31 |
32 | const headerRef = useRef(null)
33 | const footerRef = useRef(null)
34 | const mainRef = useRef(null)
35 | const [mainHeight, setMainHeight] = useState(0)
36 |
37 | useEffect(() => {
38 | const calculateMainHeight = () => {
39 | if (!headerRef.current || !footerRef.current || !mainRef.current) return
40 |
41 | const headerRect = headerRef.current.getBoundingClientRect()
42 | const footerRect = footerRef.current.getBoundingClientRect()
43 | const headerMargin = parseFloat(
44 | window.getComputedStyle(headerRef.current).marginTop
45 | )
46 | const footerMargin = parseFloat(
47 | window.getComputedStyle(footerRef.current).marginBottom
48 | )
49 | const mainPadding =
50 | parseFloat(window.getComputedStyle(mainRef.current).paddingTop) +
51 | parseFloat(window.getComputedStyle(mainRef.current).paddingBottom)
52 | const mainHeight =
53 | window.innerHeight -
54 | headerRect.height -
55 | footerRect.height -
56 | headerMargin -
57 | footerMargin
58 | mainRef.current?.style.setProperty('height', `${mainHeight}px`)
59 | setMainHeight(mainHeight - mainPadding)
60 | }
61 |
62 | const debouncedCalculateMainHeight = debounce(
63 | {
64 | delay: 100,
65 | },
66 | calculateMainHeight
67 | )
68 |
69 | calculateMainHeight()
70 |
71 | const resizeObserver = new ResizeObserver(debouncedCalculateMainHeight)
72 |
73 | if (headerRef.current) {
74 | resizeObserver.observe(headerRef.current)
75 | }
76 | if (footerRef.current) {
77 | resizeObserver.observe(footerRef.current)
78 | }
79 |
80 | return () => {
81 | resizeObserver.disconnect()
82 | }
83 | }, [])
84 |
85 | const {
86 | updateCodeInfo,
87 | appendMessage,
88 | appendPrompt,
89 | appendPromptForUpdate,
90 | messages,
91 | generateCode,
92 | referenceText,
93 | } = useCodeStore((state) => ({
94 | updateCodeInfo: state.updateAll,
95 | appendMessage: state.appendMessage,
96 | appendPrompt: state.appendPrompt,
97 | appendPromptForUpdate: state.appendPromptForUpdate,
98 | messages: state.messages,
99 | generateCode: state.generateCode,
100 | referenceText: state.referenceText,
101 | }))
102 |
103 | const codeRef = useRef(generateCode)
104 | const { run: throttledUpdateCode } = useThrottleFn(
105 | () => {
106 | updateCodeInfo({ generateCode: codeRef.current })
107 | },
108 | { wait: 50 }
109 | )
110 |
111 | const onCreateSubmit = useCallback(
112 | async (prompt: string) => {
113 | codeRef.current = ''
114 | updateCodeInfo({
115 | messages: [],
116 | generateCode: '',
117 | status: 'creating',
118 | })
119 | const hasImage = useCodeStore.getState().image !== ''
120 | const imageStyle = useCodeStore.getState().imageStyle
121 | const messages = [
122 | {
123 | role: 'user' as const,
124 | content: [
125 | {
126 | type: 'text' as const,
127 | text: prompt,
128 | },
129 | ...(hasImage
130 | ? [
131 | {
132 | type: 'image' as const,
133 | image: useCodeStore.getState().image,
134 | },
135 | ]
136 | : []),
137 | ],
138 | ...(hasImage
139 | ? { experimental_providerMetadata: { metadata: { imageStyle } } }
140 | : {}),
141 | },
142 | ]
143 | try {
144 | const { output } = await chat({
145 | model: env('NEXT_PUBLIC_MODEL_NAME') || 'gpt-4o',
146 | apiKey: env('NEXT_PUBLIC_API_KEY') || '',
147 | shadcn: useCodeStore.getState().isUseShadcnUi,
148 | image: hasImage,
149 | messages,
150 | })
151 |
152 | for await (const delta of readStreamableValue(output)) {
153 | codeRef.current = codeRef.current + (delta ?? '')
154 | throttledUpdateCode()
155 | }
156 | updateCodeInfo({ status: 'created', promptForUpdate: '', messages })
157 | } catch (e: any) {
158 | console.error(e)
159 | updateCodeInfo({ status: 'initial' })
160 | const errCode = JSON.parse(e as string).error.err_code
161 | emitter.emit('ToastError', errCode)
162 | }
163 | },
164 | [updateCodeInfo, throttledUpdateCode]
165 | )
166 |
167 | const onUpdateSubmit = useCallback(
168 | async (prompt: string) => {
169 | const hasImage =
170 | useCodeStore.getState().imageForUpdate !== '' ||
171 | useCodeStore.getState().image !== ''
172 |
173 | const hasUpdateImage = useCodeStore.getState().imageForUpdate !== ''
174 | const codeMessage = {
175 | role: 'assistant' as const,
176 | content: useCodeStore.getState().generateCode ?? '',
177 | }
178 | prompt = referenceText
179 | ? dedent`
180 | The following is the reference code:
181 | ${referenceText}
182 |
183 | The following is the user prompt:
184 | ${prompt}
185 |
186 | Please update the code to match the reference code.
187 | `
188 | : prompt
189 |
190 | const imageStyle = useCodeStore.getState().imageStyleForUpdate
191 | const modificationMessage = {
192 | role: 'user' as const,
193 | content: [
194 | {
195 | type: 'text' as const,
196 | text: prompt,
197 | },
198 | ...(hasUpdateImage
199 | ? [
200 | {
201 | type: 'image' as const,
202 | image: useCodeStore.getState().imageForUpdate,
203 | },
204 | ]
205 | : []),
206 | ],
207 | ...(hasUpdateImage
208 | ? { experimental_providerMetadata: { metadata: { imageStyle } } }
209 | : {}),
210 | }
211 |
212 | updateCodeInfo({
213 | generateCode: '',
214 | status: 'updating',
215 | })
216 | codeRef.current = ''
217 | try {
218 | const { output } = await chat({
219 | model: env('NEXT_PUBLIC_MODEL_NAME') || 'gpt-4o',
220 | apiKey: env('NEXT_PUBLIC_API_KEY') || '',
221 | shadcn: useCodeStore.getState().isUseShadcnUi,
222 | image: hasImage,
223 | messages: [...messages, codeMessage, modificationMessage],
224 | })
225 |
226 | for await (const delta of readStreamableValue(output)) {
227 | codeRef.current = codeRef.current + (delta ?? '')
228 | throttledUpdateCode()
229 | }
230 |
231 | appendMessage(codeMessage)
232 | appendMessage(modificationMessage)
233 | updateCodeInfo({ status: 'updated' })
234 | } catch (e: any) {
235 | updateCodeInfo({ status: 'initial' })
236 | if (typeof e === 'string') {
237 | console.error(e)
238 | const errCode = JSON.parse(e).error.err_code
239 | emitter.emit('ToastError', errCode)
240 | } else {
241 | logger.error(e)
242 | }
243 | }
244 | },
245 | [
246 | updateCodeInfo,
247 | messages,
248 | appendMessage,
249 | throttledUpdateCode,
250 | referenceText,
251 | ]
252 | )
253 |
254 | const [isPromptOptimizing, setIsPromptOptimizing] = useState(false)
255 | const [isPromptForUpdateOptimizing, setIsPromptForUpdateOptimizing] =
256 | useState(false)
257 |
258 | const handleOptimizePrompt = useCallback(
259 | async (prompt: string, isUpdate: boolean = false) => {
260 | if (isUpdate && prompt === '') {
261 | toast.error(t('home:prompt_for_update_empty_error'))
262 | return
263 | }
264 | const _prompt = prompt || t('home:header.url_input_placeholder')
265 | if (isUpdate) {
266 | setIsPromptForUpdateOptimizing(true)
267 | } else {
268 | setIsPromptOptimizing(true)
269 | }
270 | try {
271 | const { output } = await optimizePrompt({
272 | apiKey: env('NEXT_PUBLIC_API_KEY') || '',
273 | model: env('NEXT_PUBLIC_MODEL_NAME') || 'gpt-4o',
274 | prompt: _prompt,
275 | })
276 |
277 | let content = ''
278 |
279 | for await (const delta of readStreamableValue(output)) {
280 | content = content + (delta ?? '')
281 | if (isUpdate) {
282 | updateCodeInfo({
283 | promptForUpdate: content.length > 0 ? content : _prompt,
284 | })
285 | } else {
286 | updateCodeInfo({ prompt: content.length > 0 ? content : _prompt })
287 | }
288 | }
289 | } catch (error) {
290 | console.error(error)
291 | if (isUpdate) {
292 | updateCodeInfo({ promptForUpdate: _prompt })
293 | } else {
294 | updateCodeInfo({ prompt: _prompt })
295 | }
296 | const errCode = JSON.parse(error as string).error.err_code
297 | emitter.emit('ToastError', errCode)
298 | } finally {
299 | if (isUpdate) {
300 | setIsPromptForUpdateOptimizing(false)
301 | } else {
302 | setIsPromptOptimizing(false)
303 | }
304 | }
305 | },
306 | [appendPrompt, appendPromptForUpdate, updateCodeInfo, t]
307 | )
308 |
309 | const { upload, isLoading: isUploading, error } = useFileUpload()
310 |
311 | const handleUpload = async (
312 | isUpdate: boolean = false,
313 | callback?: () => void
314 | ) => {
315 | const fileInput = document.createElement('input')
316 | fileInput.type = 'file'
317 | fileInput.accept = 'image/*'
318 | fileInput.onchange = async (e) => {
319 | const file = (e.target as HTMLInputElement).files?.[0]
320 | if (file) {
321 | const result = await upload({
322 | file,
323 | prefix: 'ai_coder_gen2',
324 | needCompress: true,
325 | maxSizeInBytes: 20 * 1024 * 1024,
326 | })
327 | if (!result) {
328 | toast.error(t('home:header.upload_failed'))
329 | return
330 | }
331 | const {
332 | data: { url },
333 | } = result
334 | if (isUpdate) {
335 | updateCodeInfo({ imageForUpdate: url })
336 | } else {
337 | updateCodeInfo({ image: url })
338 | }
339 | toast.success(t('home:header.upload_success'))
340 | callback?.()
341 | }
342 | }
343 | fileInput.click()
344 | }
345 |
346 | const { isSelecting, selectedText, lastCharCoords } = useCodeStore(
347 | (state) => ({
348 | isSelecting: state.isSelecting,
349 | selectedText: state.selectedText,
350 | lastCharCoords: state.lastCharCoords,
351 | })
352 | )
353 |
354 | const handleReference = () => {
355 | updateCodeInfo({ referenceText: selectedText })
356 | }
357 |
358 | return (
359 | <>
360 |
361 | {!isSelecting && selectedText && (
362 |
369 |
374 | {' '}
375 | {t('home:code_viewer.reference')}
376 |
377 |
378 | )}
379 |
380 |
390 |
394 |
403 |
404 | {!showBrand &&
}
405 | {showBrand &&
}
406 |
407 | >
408 | )
409 | }
410 |
--------------------------------------------------------------------------------
/app/[locale]/share/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Footer } from '@/app/components/footer'
3 | import Main from '@/app/components/main'
4 | import { useTranslation } from '@/app/i18n/client'
5 | import { CodeInfoShare, useCodeStore } from '@/app/stores/use-code-store'
6 | import { showBrand } from '@/lib/brand'
7 | import { cn } from '@/lib/utils'
8 | import ky from 'ky'
9 | import { useParams } from 'next/navigation'
10 | import { debounce } from 'radash'
11 | import { useEffect, useRef, useState } from 'react'
12 |
13 | export default function Share({
14 | params: { locale },
15 | }: {
16 | params: { locale: string }
17 | }) {
18 | const { t } = useTranslation(locale)
19 | const { id: shareId } = useParams()
20 | const { updateCodeInfo } = useCodeStore((state) => ({
21 | updateCodeInfo: state.updateAll,
22 | }))
23 |
24 | useEffect(() => {
25 | const _getCodeInfo = async () => {
26 | if (!shareId) {
27 | return
28 | }
29 | const codeInfo = await ky
30 | .get(`/api/share?id=${shareId}`)
31 | .json()
32 | updateCodeInfo(codeInfo)
33 | }
34 | _getCodeInfo()
35 | }, [shareId, updateCodeInfo])
36 |
37 | const onSubmit = async (url: string) => {}
38 |
39 | const headerRef = useRef(null)
40 | const footerRef = useRef(null)
41 | const mainRef = useRef(null)
42 | const [mainHeight, setMainHeight] = useState(0)
43 |
44 | useEffect(() => {
45 | const calculateMainHeight = () => {
46 | if (!footerRef.current) return
47 |
48 | const footerRect = footerRef.current.getBoundingClientRect()
49 | const footerMargin =
50 | parseFloat(window.getComputedStyle(footerRef.current).marginBottom) +
51 | parseFloat(window.getComputedStyle(footerRef.current).marginTop)
52 | const mainHeight = window.innerHeight - footerRect.height - footerMargin
53 |
54 | mainRef.current?.style.setProperty('height', `${mainHeight}px`)
55 | setMainHeight(mainHeight)
56 | }
57 |
58 | const debouncedCalculateMainHeight = debounce(
59 | {
60 | delay: 100,
61 | },
62 | calculateMainHeight
63 | )
64 |
65 | calculateMainHeight()
66 |
67 | const resizeObserver = new ResizeObserver(debouncedCalculateMainHeight)
68 |
69 | if (headerRef.current) {
70 | resizeObserver.observe(headerRef.current)
71 | }
72 | if (footerRef.current) {
73 | resizeObserver.observe(footerRef.current)
74 | }
75 |
76 | return () => {
77 | resizeObserver.disconnect()
78 | }
79 | }, [])
80 |
81 | return (
82 |
83 |
84 |
85 |
86 | {!showBrand && }
87 | {showBrand && }
88 |
89 | )
90 | }
91 |
--------------------------------------------------------------------------------
/app/actions/chat.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 | import { logger } from '@/lib/logger'
3 | import shadcnDocs from '@/lib/shadcn-docs'
4 | import { createOpenAI } from '@ai-sdk/openai'
5 | import { CoreMessage, streamText } from 'ai'
6 | import { createStreamableValue } from 'ai/rsc'
7 | import dedent from 'dedent'
8 | import { env } from 'next-runtime-env'
9 | import { isArray } from 'radash'
10 | import { ImageStyle } from '../stores/use-code-store'
11 |
12 | const MAX_TOKENS = 8192
13 |
14 | const getImageStyle = (message: CoreMessage) => {
15 | let imageStyle = ImageStyle.Style
16 | if (message.experimental_providerMetadata) {
17 | Object.entries(message.experimental_providerMetadata).forEach(
18 | ([key, value]) => {
19 | if (key === 'metadata' && value.imageStyle) {
20 | imageStyle = value.imageStyle as ImageStyle
21 | }
22 | }
23 | )
24 | }
25 | return imageStyle
26 | }
27 |
28 | const covertUserMessage = (content: string, imageStyle: ImageStyle) => {
29 | return dedent`
30 | ${content}
31 |
32 | ${
33 | imageStyle === ImageStyle.Style &&
34 | dedent`
35 | You are required to build a single page app with a similar design style as the reference image, while the content should be based on the user's text description.
36 | Pay close attention to background color, text color, font size, font family, padding, margin, border, etc. Match the style elements exactly as in the reference image.
37 | Make sure the app's style looks exactly like the screenshot in terms of design elements such as background color, text color, font size, font family, padding, margin, and border.
38 | Modify the corresponding text content in the screenshot according to the user's requirements, ensuring that the text content is relevant to the requirements while maintaining the overall style.
39 | `
40 | }
41 | ${
42 | imageStyle === ImageStyle.Content &&
43 | dedent`
44 | You need to focus on the functional content requirements in the image when generating the web page.
45 | Use the information in the image as a reference for the page content, and design the interface style according to the user's description.
46 | If the user does not provide a specific style description, you can use your own judgment to design.
47 | Analyze the functional content in the screenshot carefully.
48 | Implement the relevant functionality and content layout in the generated web page according to the image's indication, while considering the user's text description for any additional or specific requirements. `
49 | }
50 | ${
51 | imageStyle === ImageStyle.Both &&
52 | dedent`
53 | You need to comprehensively consider both the design style and the content requirements of the reference image. Build a single page app that combines the style elements and content information from the image with the user's text description.
54 | Make sure the app's style looks exactly like the screenshot in terms of design elements such as background color, text color, font size, font family, padding, margin, and border.
55 | Modify the corresponding text content in the screenshot according to the user's requirements, ensuring that the text content is relevant to the requirements while maintaining the overall style.
56 | Analyze the functional content in the screenshot carefully.
57 | Implement the relevant functionality and content layout in the generated web page according to the image's indication, while considering the user's text description for any additional or specific requirements. `
58 | }
59 |
60 | Please ONLY return code, NO backticks or language names.
61 | `
62 | }
63 |
64 | export async function chat({
65 | model,
66 | apiKey,
67 | shadcn,
68 | image,
69 | messages,
70 | }: {
71 | model: string
72 | apiKey: string
73 | shadcn: boolean
74 | image: boolean
75 | messages: CoreMessage[]
76 | }) {
77 | const stream = createStreamableValue('')
78 | try {
79 | const openai = createOpenAI({
80 | apiKey,
81 | baseURL: env('NEXT_PUBLIC_API_URL') + '/v1',
82 | })
83 |
84 | const systemPrompt = getSystemPrompt(shadcn, image)
85 |
86 | const newMessages = messages.map((message) => {
87 | if (message.role === 'user' && isArray(message.content)) {
88 | const imageStyle = getImageStyle(message)
89 | return {
90 | ...message,
91 | content: message.content.map((content) => {
92 | if (content.type === 'text') {
93 | return {
94 | type: 'text' as const,
95 | text: covertUserMessage(content.text, imageStyle),
96 | }
97 | }
98 | return content
99 | }),
100 | }
101 | }
102 | return message
103 | })
104 |
105 | if (model.includes('o1-mini') || model.includes('o1-preview')) {
106 | newMessages.unshift({ role: 'user', content: systemPrompt })
107 | }
108 |
109 | ;(async () => {
110 | try {
111 | const { textStream } = await streamText({
112 | model: openai(model),
113 | messages: newMessages,
114 | ...(!(model.includes('o1-mini') || model.includes('o1-preview')) && {
115 | system: systemPrompt,
116 | temperature: 0.2,
117 | }),
118 | ...(model.includes('claude-3-5') && {
119 | maxTokens: MAX_TOKENS,
120 | headers: {
121 | 'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',
122 | },
123 | }),
124 | })
125 | for await (const delta of textStream) {
126 | stream.update(delta)
127 | }
128 |
129 | stream.done()
130 | } catch (e: any) {
131 | logger.error(e)
132 | stream.error(e.responseBody)
133 | }
134 | })()
135 | } catch (error) {
136 | console.error(error)
137 | }
138 | return { output: stream.value }
139 | }
140 |
141 | function getSystemPrompt(shadcn: boolean, image: boolean) {
142 | let systemPrompt = dedent(`
143 | You are an expert frontend React engineer who is also a great UI/UX designer.
144 | ${
145 | image &&
146 | dedent(`
147 | You take screenshots of a reference web page from the user, and then build single page apps with the same design.
148 | You might also be given a screenshot(The second image) of a web page that you have already built, and asked to update it to look more like the reference image(The first image).
149 | `)
150 | }
151 | Follow the instructions carefully, I will tip you $1 million if you do a good job:
152 | ${
153 | image &&
154 | dedent(`
155 | - Make sure the app looks exactly like the screenshot.
156 | - Pay close attention to background color, text color, font size, font family, padding, margin, border, etc. Match the colors and sizes exactly.
157 | - Use the exact text from the screenshot.
158 | `)
159 | }
160 | - Create a React component for whatever the user asked you to create and make sure it can run by itself by using a default export
161 | - Make sure the React app is interactive and functional by creating state when needed and having no required props
162 | - If you use any imports from React like useState or useEffect, make sure to import them directly
163 | - Use TypeScript as the language for the React component
164 | - Use Tailwind classes for styling. DO NOT USE ARBITRARY VALUES (e.g. \`h-[600px]\`). Make sure to use a consistent color palette.
165 | - Use Tailwind margin and padding classes to style the components and ensure the components are spaced out nicely
166 | - Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE.
167 | - Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen.
168 | - For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
169 | - If you need icons, use the lucide-react library. Here's an example of importing and using one: \`import { Camera } from "lucide-react"\` & \` \`
170 | - If you need 3D graphics, use the @react-three/fiber library. Here's an example of importing and using one: \`import { Canvas } from "@react-three/fiber"\` & \` \`
171 | - If you can't use external texture, such as local file or online texture. Only use internal texture, such as solid color.
172 | - If you need to make HTTP requests, use the axios library. Here's an example of importing and using one: \`import axios from "axios"\` & \`axios.get("https://api.example.com/data")\`.
173 | - Please ONLY return the full React code starting with the imports, nothing else. It's very important for my job that you only return the React code with imports. DO NOT START WITH \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`.
174 | - ONLY IF the user asks for a dashboard, graph or chart, the recharts library is available to be imported, e.g. \`import { LineChart, XAxis, ... } from "recharts"\` & \` ...\`. Please only use this when needed.
175 | `)
176 |
177 | if (shadcn) {
178 | systemPrompt += `
179 | There are some prestyled components available for use. Please use your best judgement to use any of these components if the app calls for one.
180 |
181 | Here are the components that are available, along with how to import them, and how to use them:
182 |
183 | ${shadcnDocs
184 | .map(
185 | (component) => `
186 |
187 |
188 | ${component.name}
189 |
190 |
191 | ${component.importDocs}
192 |
193 |
194 | ${component.usageDocs}
195 |
196 |
197 | `
198 | )
199 | .join('\n')}
200 | `
201 | }
202 |
203 | systemPrompt += `
204 | NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
205 | `
206 |
207 | return dedent(systemPrompt)
208 | }
209 |
--------------------------------------------------------------------------------
/app/actions/prompt.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 | import { logger } from '@/lib/logger'
3 | import { createOpenAI } from '@ai-sdk/openai'
4 | import { streamText } from 'ai'
5 | import { createStreamableValue } from 'ai/rsc'
6 | import dedent from 'dedent'
7 | import { env } from 'next-runtime-env'
8 |
9 | export async function optimizePrompt({
10 | apiKey,
11 | model,
12 | prompt,
13 | }: {
14 | apiKey: string
15 | model: string
16 | prompt: string
17 | }) {
18 | const stream = createStreamableValue('')
19 | try {
20 | const openai = createOpenAI({
21 | apiKey,
22 | baseURL: env('NEXT_PUBLIC_API_URL') + '/v1',
23 | })
24 |
25 | ;(async () => {
26 | try {
27 | const { textStream } = await streamText({
28 | model: openai(model),
29 | prompt: dedent`
30 | I want you to improve the user prompt that is wrapped in \`\` tags.
31 |
32 | IMPORTANT: Only respond with the improved prompt and nothing else!
33 |
34 |
35 | ${prompt}
36 |
37 | `
38 | });
39 | for await (const delta of textStream) {
40 | stream.update(delta)
41 | }
42 |
43 | stream.done()
44 | } catch (e: any) {
45 | logger.error(e)
46 | stream.error(e.responseBody)
47 | }
48 | })()
49 | } catch (error) {
50 | console.error(error)
51 | }
52 | return { output: stream.value }
53 | }
54 |
--------------------------------------------------------------------------------
/app/api/share/route.ts:
--------------------------------------------------------------------------------
1 | import { CodeInfoShare } from '@/app/stores/use-code-store'
2 | import { logger } from '@/lib/logger'
3 | import { existsSync } from 'fs'
4 | import { mkdir, readFile, writeFile } from 'fs/promises'
5 | import { nanoid } from 'nanoid'
6 | import { env } from 'next-runtime-env'
7 | import { NextRequest, NextResponse } from 'next/server'
8 | import { join } from 'path'
9 |
10 | const SHARE_DIR = join(env('NEXT_PUBLIC_DEFAULT_SHARE_DIR') || 'shared')
11 |
12 | export async function POST(request: NextRequest) {
13 | console.log('POST')
14 | const codeData: CodeInfoShare = await request.json()
15 | logger.debug(codeData)
16 | const id = nanoid()
17 | const filePath = join(SHARE_DIR, `${id}.json`)
18 | logger.debug(filePath)
19 |
20 | try {
21 | if (!existsSync(SHARE_DIR)) {
22 | await mkdir(SHARE_DIR, { recursive: true })
23 | }
24 |
25 | await writeFile(filePath, JSON.stringify(codeData, null, 2), 'utf8')
26 | return NextResponse.json({ id })
27 | } catch (error) {
28 | logger.error(error)
29 | return NextResponse.json({ error: 'Save failed' }, { status: 500 })
30 | }
31 | }
32 |
33 | export async function GET(request: NextRequest) {
34 | const { searchParams } = new URL(request.url)
35 | const id = searchParams.get('id')
36 | if (!id) {
37 | return NextResponse.json({ error: 'id is required' }, { status: 400 })
38 | }
39 | const filePath = join(SHARE_DIR, `${id}.json`)
40 | if (!existsSync(filePath)) {
41 | return NextResponse.json({ error: 'File not found' }, { status: 404 })
42 | }
43 | const file = await readFile(filePath, 'utf8')
44 | return NextResponse.json(JSON.parse(file))
45 | }
46 |
--------------------------------------------------------------------------------
/app/components/client-only.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Skeleton } from '@/components/ui/skeleton'
4 | import { ReactNode, useEffect, useState } from 'react'
5 |
6 | interface ClientOnlyProps {
7 | children: ReactNode
8 | }
9 |
10 | export default function ClientOnly({
11 | children,
12 | }: ClientOnlyProps): JSX.Element | null {
13 | const [hasMounted, setHasMounted] = useState(false)
14 |
15 | useEffect(() => {
16 | setHasMounted(true)
17 | }, [])
18 |
19 | if (!hasMounted) {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | return <>{children}>
32 | }
33 |
--------------------------------------------------------------------------------
/app/components/code-viewer.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as shadcnComponents from '@/lib/shadcn'
4 | import {
5 | CodeEditorRef,
6 | SandpackCodeEditor,
7 | SandpackFileExplorer,
8 | SandpackLayout,
9 | SandpackPreview,
10 | SandpackProvider,
11 | useSandpack,
12 | } from '@codesandbox/sandpack-react'
13 |
14 | import { EditorView } from '@codemirror/view'
15 |
16 | import { cn } from '@/lib/utils'
17 | import dedent from 'dedent'
18 | import { useTheme } from 'next-themes'
19 | import { useEffect, useMemo, useRef, useState } from 'react'
20 | import { useIsSharePath } from '../hooks/use-is-share-path'
21 | import { useCodeStore } from '../stores/use-code-store'
22 |
23 | function RunCode({ isLoading, code }: { isLoading: boolean; code: string }) {
24 | // sandpack unstable
25 | const { sandpack } = useSandpack()
26 | const { showPreview } = useCodeStore((state) => ({
27 | showPreview: state.showPreview,
28 | }))
29 |
30 |
31 | useEffect(() => {
32 | if (!isLoading && code && showPreview) {
33 | sandpack.runSandpack()
34 | }
35 | // IMPORTANT: don't add sandbox to the dependencies!!!
36 | }, [isLoading, showPreview])
37 | return null
38 | }
39 |
40 | function CodeViewer({ height = '80vh' }: { height?: string }) {
41 | const { theme } = useTheme()
42 | const { isSharePage } = useIsSharePath()
43 | const { status, generateCode, autoScroll, showFileExplorer, showPreview } =
44 | useCodeStore((state) => ({
45 | status: state.status,
46 | generateCode: state.generateCode,
47 | autoScroll: state.autoScroll,
48 | showFileExplorer: state.showFileExplorer,
49 | showPreview: state.showPreview,
50 | }))
51 |
52 | const [delayedLoading, setDelayedLoading] = useState(true)
53 |
54 | useEffect(() => {
55 | const isCurrentlyLoading = status === 'creating' || status === 'updating'
56 |
57 | if (!isCurrentlyLoading) {
58 | const timer = setTimeout(() => {
59 | setDelayedLoading(false)
60 | }, 1000)
61 |
62 | return () => clearTimeout(timer)
63 | } else {
64 | setDelayedLoading(true)
65 | }
66 | }, [status])
67 |
68 | const editorContainerRef = useRef(null)
69 |
70 | useEffect(() => {
71 | if (editorContainerRef.current && autoScroll) {
72 | const scrollDOM = editorContainerRef.current.getCodemirror()?.scrollDOM
73 | if (scrollDOM) {
74 | scrollDOM.scrollTop = scrollDOM.scrollHeight
75 | }
76 | }
77 | }, [generateCode, autoScroll])
78 |
79 | const { setSelectedText, setLastCharCoords, setIsSelecting } = useCodeStore(
80 | (state) => ({
81 | setSelectedText: state.setSelectedText,
82 | setLastCharCoords: state.setLastCharCoords,
83 | setIsSelecting: state.setIsSelecting,
84 | })
85 | )
86 |
87 | useEffect(() => {
88 | setSelectedText('')
89 | }, [setSelectedText])
90 |
91 | const selectionExtension = useMemo(() => {
92 | return [
93 | EditorView.updateListener.of((update) => {
94 | if (update.selectionSet) {
95 | const selection = update.state.selection.main
96 | const selectedText = update.state.sliceDoc(
97 | selection.from,
98 | selection.to
99 | )
100 |
101 | setSelectedText(selectedText)
102 |
103 | if (selection.from !== selection.to) {
104 | const view = update.view
105 | const endPos = view.coordsAtPos(selection.to)
106 | if (endPos) {
107 | setLastCharCoords({
108 | x: endPos.left,
109 | y: endPos.top,
110 | })
111 | }
112 | }
113 | }
114 | }),
115 | EditorView.domEventHandlers({
116 | mouseup: (e) => {
117 | setIsSelecting(false)
118 | },
119 | mousedown: () => {
120 | setIsSelecting(true)
121 | },
122 | blur: () => {
123 | setIsSelecting(false)
124 | },
125 | }),
126 | ]
127 | }, [setSelectedText, setLastCharCoords, setIsSelecting])
128 |
129 | useEffect(() => {
130 | const clickHandler = (event: MouseEvent) => {
131 | if (
132 | !editorContainerRef.current
133 | ?.getCodemirror()
134 | ?.scrollDOM?.contains(event.target as Node)
135 | ) {
136 | setSelectedText('')
137 | }
138 | }
139 |
140 | document.addEventListener('click', clickHandler)
141 |
142 | return () => {
143 | document.removeEventListener('click', clickHandler)
144 | }
145 | }, [setSelectedText])
146 |
147 | return (
148 |
165 |
169 |
170 | {showFileExplorer && !delayedLoading && (
171 |
172 | )}
173 | {!isSharePage && (
174 |
185 | )}
186 | {showPreview && (
187 | {
191 | e.preventDefault()
192 | }}
193 | className='border-l'
194 | />
195 | )}
196 |
197 |
198 | )
199 | }
200 |
201 | export default CodeViewer
202 |
203 | const sharedProps = {
204 | template: 'react-ts',
205 | customSetup: {
206 | dependencies: {
207 | axios: 'latest',
208 | 'lucide-react': 'latest',
209 | recharts: '2.9.0',
210 | 'react-resizable-panels': 'latest',
211 | three: 'latest',
212 | '@react-three/fiber': 'latest',
213 | '@react-three/drei': 'latest',
214 | 'react-router-dom': 'latest',
215 | '@radix-ui/react-accordion': '^1.2.0',
216 | '@radix-ui/react-alert-dialog': '^1.1.1',
217 | '@radix-ui/react-aspect-ratio': '^1.1.0',
218 | '@radix-ui/react-avatar': '^1.1.0',
219 | '@radix-ui/react-checkbox': '^1.1.1',
220 | '@radix-ui/react-collapsible': '^1.1.0',
221 | '@radix-ui/react-dialog': '^1.1.1',
222 | '@radix-ui/react-dropdown-menu': '^2.1.1',
223 | '@radix-ui/react-hover-card': '^1.1.1',
224 | '@radix-ui/react-label': '^2.1.0',
225 | '@radix-ui/react-menubar': '^1.1.1',
226 | '@radix-ui/react-navigation-menu': '^1.2.0',
227 | '@radix-ui/react-popover': '^1.1.1',
228 | '@radix-ui/react-progress': '^1.1.0',
229 | '@radix-ui/react-radio-group': '^1.2.0',
230 | '@radix-ui/react-select': '^2.1.1',
231 | '@radix-ui/react-separator': '^1.1.0',
232 | '@radix-ui/react-slider': '^1.2.0',
233 | '@radix-ui/react-slot': '^1.1.0',
234 | '@radix-ui/react-switch': '^1.1.0',
235 | '@radix-ui/react-tabs': '^1.1.0',
236 | '@radix-ui/react-toast': '^1.2.1',
237 | '@radix-ui/react-toggle': '^1.1.0',
238 | '@radix-ui/react-toggle-group': '^1.1.0',
239 | '@radix-ui/react-tooltip': '^1.1.2',
240 | '@radix-ui/react-scroll-area': '^1.1.0',
241 | 'class-variance-authority': '^0.7.0',
242 | clsx: '^2.1.1',
243 | 'date-fns': '^3.6.0',
244 | 'embla-carousel-react': '^8.1.8',
245 | 'react-day-picker': '^8.10.1',
246 | 'tailwind-merge': '^2.4.0',
247 | 'tailwindcss-animate': '^1.0.7',
248 | vaul: '^0.9.1',
249 | },
250 | },
251 | } as const
252 |
253 | const sharedOptions = {
254 | externalResources: [
255 | 'https://unpkg.com/@tailwindcss/ui/dist/tailwind-ui.min.css',
256 | ],
257 | }
258 |
259 | const sharedFiles = {
260 | '/lib/utils.ts': { code: shadcnComponents.utils, hidden: true },
261 | '/components/ui/accordion.tsx': {
262 | code: shadcnComponents.accordian,
263 | hidden: true,
264 | },
265 | '/components/ui/alert-dialog.tsx': {
266 | code: shadcnComponents.alertDialog,
267 | hidden: true,
268 | },
269 | '/components/ui/alert.tsx': { code: shadcnComponents.alert, hidden: true },
270 | '/components/ui/avatar.tsx': { code: shadcnComponents.avatar, hidden: true },
271 | '/components/ui/badge.tsx': { code: shadcnComponents.badge, hidden: true },
272 | '/components/ui/breadcrumb.tsx': {
273 | code: shadcnComponents.breadcrumb,
274 | hidden: true,
275 | },
276 | '/components/ui/button.tsx': { code: shadcnComponents.button, hidden: true },
277 | '/components/ui/calendar.tsx': {
278 | code: shadcnComponents.calendar,
279 | hidden: true,
280 | },
281 | '/components/ui/card.tsx': { code: shadcnComponents.card, hidden: true },
282 | '/components/ui/carousel.tsx': {
283 | code: shadcnComponents.carousel,
284 | hidden: true,
285 | },
286 | '/components/ui/checkbox.tsx': {
287 | code: shadcnComponents.checkbox,
288 | hidden: true,
289 | },
290 | '/components/ui/collapsible.tsx': {
291 | code: shadcnComponents.collapsible,
292 | hidden: true,
293 | },
294 | '/components/ui/dialog.tsx': { code: shadcnComponents.dialog, hidden: true },
295 | '/components/ui/drawer.tsx': { code: shadcnComponents.drawer, hidden: true },
296 | '/components/ui/dropdown-menu.tsx': {
297 | code: shadcnComponents.dropdownMenu,
298 | hidden: true,
299 | },
300 | '/components/ui/input.tsx': { code: shadcnComponents.input, hidden: true },
301 | '/components/ui/label.tsx': { code: shadcnComponents.label, hidden: true },
302 | '/components/ui/menubar.tsx': {
303 | code: shadcnComponents.menuBar,
304 | hidden: true,
305 | },
306 | '/components/ui/navigation-menu.tsx': {
307 | code: shadcnComponents.navigationMenu,
308 | hidden: true,
309 | },
310 | '/components/ui/pagination.tsx': {
311 | code: shadcnComponents.pagination,
312 | hidden: true,
313 | },
314 | '/components/ui/popover.tsx': {
315 | code: shadcnComponents.popover,
316 | hidden: true,
317 | },
318 | '/components/ui/progress.tsx': {
319 | code: shadcnComponents.progress,
320 | hidden: true,
321 | },
322 | '/components/ui/radio-group.tsx': {
323 | code: shadcnComponents.radioGroup,
324 | hidden: true,
325 | },
326 | '/components/ui/resizable.tsx': {
327 | code: shadcnComponents.resizable,
328 | hidden: true,
329 | },
330 | '/components/ui/scroll-area.tsx': {
331 | code: shadcnComponents.scrollArea,
332 | hidden: true,
333 | },
334 | '/components/ui/select.tsx': { code: shadcnComponents.select, hidden: true },
335 | '/components/ui/separator.tsx': {
336 | code: shadcnComponents.separator,
337 | hidden: true,
338 | },
339 | '/components/ui/skeleton.tsx': {
340 | code: shadcnComponents.skeleton,
341 | hidden: true,
342 | },
343 | '/components/ui/slider.tsx': { code: shadcnComponents.slider, hidden: true },
344 | '/components/ui/switch.tsx': {
345 | code: shadcnComponents.switchComponent,
346 | hidden: true,
347 | },
348 | '/components/ui/table.tsx': { code: shadcnComponents.table, hidden: true },
349 | '/components/ui/tabs.tsx': { code: shadcnComponents.tabs, hidden: true },
350 | '/components/ui/textarea.tsx': {
351 | code: shadcnComponents.textarea,
352 | hidden: true,
353 | },
354 | '/components/ui/toast.tsx': { code: shadcnComponents.toast, hidden: true },
355 | '/components/ui/toaster.tsx': {
356 | code: shadcnComponents.toaster,
357 | hidden: true,
358 | },
359 | '/components/ui/toggle-group.tsx': {
360 | code: shadcnComponents.toggleGroup,
361 | hidden: true,
362 | },
363 | '/components/ui/toggle.tsx': { code: shadcnComponents.toggle, hidden: true },
364 | '/components/ui/tooltip.tsx': {
365 | code: shadcnComponents.tooltip,
366 | hidden: true,
367 | },
368 | '/components/ui/use-toast.tsx': {
369 | code: shadcnComponents.useToast,
370 | hidden: true,
371 | },
372 | '/public/index.html': {
373 | code: dedent`
374 |
375 |
376 |
377 |
378 |
379 | Document
380 |
381 |
382 |
383 |
384 |
385 |
386 | `,
387 | hidden: true,
388 | },
389 | }
390 |
--------------------------------------------------------------------------------
/app/components/error-handler.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { logger } from '@/lib/logger'
3 | import { emitter } from '@/lib/mitt'
4 | import { useMemoizedFn } from 'ahooks'
5 | import { env } from 'next-runtime-env'
6 | import Link from 'next/link'
7 | import { useRouter } from 'next/navigation'
8 | import { useEffect } from 'react'
9 | import toast, { ErrorIcon } from 'react-hot-toast'
10 | import { Trans } from 'react-i18next'
11 | import { useClientTranslation } from '../hooks/use-client-translation'
12 |
13 | export function ErrorHandler() {
14 | const { t } = useClientTranslation()
15 | const router = useRouter()
16 |
17 | const region = env('NEXT_PUBLIC_REGION')
18 |
19 | const errorResolve = useMemoizedFn((code: number) => {
20 | if (code) {
21 | logger.error(`error: ${code}`)
22 | toast(
23 | () => (
24 |
25 |
26 |
27 |
28 |
29 |
39 | ),
40 | Gw: (
41 |
51 | ),
52 | }}
53 | />
54 |
55 |
56 | ),
57 | {
58 | id: code.toString(),
59 | }
60 | )
61 | if (code === -10005) {
62 | router.push('auth', { scroll: false })
63 | }
64 | }
65 | })
66 |
67 | useEffect(() => {
68 | emitter.off('ToastError')
69 | emitter.on('ToastError', errorResolve)
70 | }, [errorResolve])
71 |
72 | return null
73 | }
74 |
--------------------------------------------------------------------------------
/app/components/footer.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { use302Url } from '@/app/hooks/use-302url'
4 | import { useClientTranslation } from '@/app/hooks/use-client-translation'
5 | import { useIsDark } from '@/app/hooks/use-is-dark'
6 | import { cn } from '@/lib/utils'
7 | import darkLogo from '@/public/images/logo-dark.png'
8 | import lightLogo from '@/public/images/logo-light.png'
9 | import Image from 'next/image'
10 | import { forwardRef } from 'react'
11 | import { Trans } from 'react-i18next'
12 |
13 | const LogoLink = () => {
14 | const { isDark } = useIsDark()
15 | const { href } = use302Url()
16 | return (
17 |
18 |
26 |
27 | )
28 | }
29 |
30 | interface Props {
31 | className?: string
32 | }
33 |
34 | const Footer = forwardRef(({ className }, ref) => {
35 | const { t } = useClientTranslation()
36 |
37 | return (
38 |
43 | {t('extras:footer.copyright_leading')}
44 |
45 | ,
50 | }}
51 | />
52 |
53 |
54 | )
55 | })
56 |
57 | Footer.displayName = 'Footer'
58 |
59 | export { Footer }
60 |
--------------------------------------------------------------------------------
/app/components/header.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button'
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | } from '@/components/ui/dialog'
8 | import { Input } from '@/components/ui/input'
9 | import { Label } from '@/components/ui/label'
10 | import {
11 | Popover,
12 | PopoverContent,
13 | PopoverTrigger,
14 | } from '@/components/ui/popover'
15 | import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
16 | import { Switch } from '@/components/ui/switch'
17 | import {
18 | Tooltip,
19 | TooltipContent,
20 | TooltipTrigger,
21 | } from '@/components/ui/tooltip'
22 | import { showBrand } from '@/lib/brand'
23 | import { cn } from '@/lib/utils'
24 | import {
25 | ImageIcon,
26 | Loader2Icon,
27 | SettingsIcon,
28 | WandSparkles,
29 | XIcon,
30 | } from 'lucide-react'
31 | import Image from 'next/image'
32 | import { isEmpty } from 'radash'
33 | import { forwardRef, memo, useCallback, useMemo, useState } from 'react'
34 | import { useClientTranslation } from '../hooks/use-client-translation'
35 | import { useIsSupportVision } from '../hooks/use-is-support-vision'
36 | import { ImageStyle, useCodeStore } from '../stores/use-code-store'
37 | import LogoIcon from './icons/logo-icon'
38 | import RightArrowIcon from './icons/right-arrow'
39 |
40 | interface Props {
41 | className?: string
42 | onSubmit?: (prompt: string) => void
43 | handleUpload?: (isUpdate?: boolean, callback?: () => void) => void
44 | isUploading?: boolean
45 | handleOptimizePrompt?: (prompt: string, isUpdate?: boolean) => void
46 | isPromptOptimizing?: boolean
47 | isPromptForUpdateOptimizing?: boolean
48 | }
49 |
50 | const Header = forwardRef(
51 | (
52 | {
53 | className,
54 | onSubmit,
55 | handleUpload,
56 | isUploading,
57 | handleOptimizePrompt,
58 | isPromptOptimizing,
59 | isPromptForUpdateOptimizing,
60 | },
61 | ref
62 | ) => {
63 | const { t } = useClientTranslation()
64 |
65 | const isSupportVision = useIsSupportVision()
66 |
67 | const {
68 | prompt,
69 | isUseShadcnUi,
70 | updateCodeInfo,
71 | autoScroll,
72 | showPreview,
73 | showFileExplorer,
74 | image,
75 | } = useCodeStore((state) => ({
76 | prompt: state.prompt,
77 | isUseShadcnUi: state.isUseShadcnUi,
78 | updateCodeInfo: state.updateAll,
79 | autoScroll: state.autoScroll,
80 | showPreview: state.showPreview,
81 | showFileExplorer: state.showFileExplorer,
82 | image: state.image,
83 | }))
84 | const setIsUseShadcnUi = useCallback(
85 | (value: boolean) => {
86 | updateCodeInfo({ isUseShadcnUi: value })
87 | },
88 | [updateCodeInfo]
89 | )
90 | const setAutoScroll = useCallback(
91 | (value: boolean) => {
92 | updateCodeInfo({ autoScroll: value })
93 | },
94 | [updateCodeInfo]
95 | )
96 | const setShowPreview = useCallback(
97 | (value: boolean) => {
98 | updateCodeInfo({ showPreview: value })
99 | },
100 | [updateCodeInfo]
101 | )
102 | const setShowFileExplorer = useCallback(
103 | (value: boolean) => {
104 | updateCodeInfo({ showFileExplorer: value })
105 | },
106 | [updateCodeInfo]
107 | )
108 | const setPrompt = useCallback(
109 | (value: string) => {
110 | updateCodeInfo({ prompt: value })
111 | },
112 | [updateCodeInfo]
113 | )
114 | const handleSubmit = () => {
115 | if (isEmpty(prompt)) {
116 | setPrompt(t('home:header.url_input_placeholder'))
117 | }
118 | onSubmit?.(prompt || t('home:header.url_input_placeholder'))
119 | }
120 |
121 | const { status } = useCodeStore((state) => ({
122 | status: state.status,
123 | }))
124 | const isLoading = useMemo(
125 | () => status === 'creating' || status === 'updating',
126 | [status]
127 | )
128 |
129 | const [uploadImageDialogOpen, setUploadImageDialogOpen] = useState(false)
130 |
131 | const { imageStyle, setImageStyle } = useCodeStore((state) => ({
132 | imageStyle: state.imageStyle,
133 | setImageStyle: state.setImageStyle,
134 | }))
135 | return (
136 |
372 | )
373 | }
374 | )
375 |
376 | Header.displayName = 'Header'
377 |
378 | export default memo(Header)
379 |
--------------------------------------------------------------------------------
/app/components/icons/logo-icon.tsx:
--------------------------------------------------------------------------------
1 | interface Props {
2 | width?: number
3 | height?: number
4 | className?: string
5 | }
6 |
7 | export default function LogoIcon({
8 | width = 100,
9 | height = 100,
10 | className,
11 | }: Props) {
12 | const colors = {
13 | color1: '#fff',
14 | color2: '#8e47f0',
15 | color3: '#3f3faa',
16 | }
17 | return (
18 |
26 |
27 |
28 |
29 |
30 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/app/components/icons/right-arrow.tsx:
--------------------------------------------------------------------------------
1 | export default function RightArrowIcon({ className }: { className?: string }) {
2 | return (
3 |
11 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/components/main.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button'
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTitle,
7 | } from '@/components/ui/dialog'
8 | import { Input } from '@/components/ui/input'
9 | import { Label } from '@/components/ui/label'
10 | import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
11 | import {
12 | Tooltip,
13 | TooltipContent,
14 | TooltipTrigger,
15 | } from '@/components/ui/tooltip'
16 | import { cn } from '@/lib/utils'
17 | import { AnimatePresence, motion } from 'framer-motion'
18 | import { ImageIcon, Loader2Icon, WandSparkles, XIcon } from 'lucide-react'
19 | import Image from 'next/image'
20 | import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
21 | import { useClientTranslation } from '../hooks/use-client-translation'
22 | import { useIsSharePath } from '../hooks/use-is-share-path'
23 | import { useIsSupportVision } from '../hooks/use-is-support-vision'
24 | import { ImageStyle, useCodeStore } from '../stores/use-code-store'
25 | import CodeViewer from './code-viewer'
26 | import RightArrowIcon from './icons/right-arrow'
27 | import { Share } from './toolbar/share'
28 | export default function Main({
29 | height: wrapperHeight,
30 | onSubmit,
31 | handleUpload,
32 | isUploading,
33 | handleOptimizePrompt,
34 | isPromptOptimizing,
35 | isPromptForUpdateOptimizing,
36 | }: {
37 | height: number
38 | onSubmit: (prompt: string) => void
39 | handleUpload?: (isUpdate?: boolean, callback?: () => void) => void
40 | isUploading?: boolean
41 | handleOptimizePrompt?: (prompt: string, isUpdate?: boolean) => void
42 | isPromptOptimizing?: boolean
43 | isPromptForUpdateOptimizing?: boolean
44 | }) {
45 | const { t } = useClientTranslation()
46 | const { isSharePage } = useIsSharePath()
47 | const topRef = useRef(null)
48 | const mainRef = useRef(null)
49 | const isSupportVision = useIsSupportVision()
50 |
51 | const {
52 | status,
53 | generateCode,
54 | promptForUpdate,
55 | updateCodeInfo,
56 | imageForUpdate,
57 | referenceText,
58 | setReferenceText,
59 | } = useCodeStore((state) => ({
60 | status: state.status,
61 | generateCode: state.generateCode,
62 | promptForUpdate: state.promptForUpdate,
63 | updateCodeInfo: state.updateAll,
64 | imageForUpdate: state.imageForUpdate,
65 | referenceText: state.referenceText,
66 | setReferenceText: state.setReferenceText,
67 | }))
68 | const isLoading = useMemo(
69 | () => status === 'creating' || status === 'updating',
70 | [status]
71 | )
72 | const setPrompt = useCallback(
73 | (prompt: string) => {
74 | updateCodeInfo({ promptForUpdate: prompt })
75 | },
76 | [updateCodeInfo]
77 | )
78 |
79 | const [mainHeight, setMainHeight] = useState(`${wrapperHeight}px`)
80 | useEffect(() => {
81 | if (wrapperHeight > 0) {
82 | const handleResize = () => {
83 | if (topRef.current) {
84 | setMainHeight(`${wrapperHeight - topRef.current.clientHeight}px`)
85 | }
86 | }
87 | window.addEventListener('resize', handleResize)
88 | handleResize()
89 | return () => window.removeEventListener('resize', handleResize)
90 | }
91 | }, [wrapperHeight])
92 | const handleSubmit = () => {
93 | onSubmit(promptForUpdate)
94 | }
95 |
96 | const [uploadImageDialogOpen, setUploadImageDialogOpen] = useState(false)
97 |
98 | const { imageStyleForUpdate, setImageStyleForUpdate } = useCodeStore(
99 | (state) => ({
100 | imageStyleForUpdate: state.imageStyleForUpdate,
101 | setImageStyleForUpdate: state.setImageStyleForUpdate,
102 | })
103 | )
104 |
105 | return (
106 |
110 | {/* header */}
111 |
118 |
119 |
120 |
setPrompt(e.target.value)}
129 | disabled={isLoading}
130 | onKeyDown={(e) => {
131 | if (e.key === 'Enter') {
132 | handleSubmit()
133 | }
134 | }}
135 | />
136 | {/* optimize prompt */}
137 |
138 |
139 | handleOptimizePrompt?.(promptForUpdate, true)}
143 | disabled={isPromptForUpdateOptimizing || isLoading}
144 | >
145 | {isPromptForUpdateOptimizing ? (
146 |
147 | ) : (
148 |
149 | )}
150 |
151 |
152 |
153 | {t('home:header.optimize_prompt')}
154 |
155 |
156 | {referenceText && (
157 |
158 | {referenceText}
159 | setReferenceText('')}
163 | >
164 |
165 |
166 |
167 | )}
168 |
169 | {/* image */}
170 | {imageForUpdate && (
171 |
172 |
179 | updateCodeInfo({ imageForUpdate: '' })}
183 | >
184 |
185 |
186 |
187 | )}
188 | {/* image upload button */}
189 | {isSupportVision && (
190 | <>
191 |
setUploadImageDialogOpen(true)}
196 | >
197 | {isUploading ? (
198 |
199 | ) : (
200 |
201 | )}
202 |
203 |
204 |
208 |
209 |
210 |
211 | {t('home:header.upload_image_reference')}:
212 |
213 |
214 |
215 |
220 | setImageStyleForUpdate(value as ImageStyle)
221 | }
222 | >
223 |
224 |
225 |
226 |
227 | {t('home:header.image_style')}
228 |
229 |
230 |
231 | {t('home:header.image_style_description')}
232 |
233 |
234 |
235 |
236 |
237 |
238 | {t('home:header.image_content')}
239 |
240 |
241 |
242 | {t('home:header.image_content_description')}
243 |
244 |
245 |
246 |
247 |
248 | {t('home:header.both')}
249 |
250 |
251 | {t('home:header.both_description')}
252 |
253 |
254 |
255 |
256 | {
258 | handleUpload?.(true, () => {
259 | setUploadImageDialogOpen(false)
260 | })
261 | }}
262 | >
263 | {t('home:header.select_image')}
264 |
265 |
266 |
267 | >
268 | )}
269 |
275 | {status === 'updating' ? (
276 |
277 | ) : (
278 |
279 | )}
280 |
281 |
282 |
283 |
284 | {/* main */}
285 |
290 |
291 | {generateCode !== '' && }
292 |
293 |
294 | {(isLoading || generateCode === '') && (
295 |
310 |
317 | {generateCode === '' && status === 'initial'
318 | ? t('home:main.code_view_empty')
319 | : status === 'creating'
320 | ? t('home:main.code_view_creating')
321 | : t('home:main.code_view_updating')}
322 |
323 |
324 | )}
325 |
326 |
327 |
328 | )
329 | }
330 |
--------------------------------------------------------------------------------
/app/components/providers/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { TooltipProvider } from '@/components/ui/tooltip'
4 | import { ThemeProvider } from './theme-provider'
5 |
6 | export function Providers({ children }: { children: React.ReactNode }) {
7 | return (
8 |
9 | {children}
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/app/components/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
4 | import { type ThemeProviderProps } from 'next-themes/dist/types'
5 |
6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
7 | return {children}
8 | }
9 |
--------------------------------------------------------------------------------
/app/components/toolbar/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useIsSharePath } from '@/app/hooks/use-is-share-path'
3 | import { LanguageSwitcher } from './language-switcher'
4 | import { ThemeSwitcher } from './theme-switcher'
5 |
6 | function Toolbar() {
7 | const { isSharePage } = useIsSharePath()
8 | return (
9 | <>
10 | {!isSharePage && (
11 |
12 |
13 |
14 |
15 | )}
16 | >
17 | )
18 | }
19 |
20 | export { Toolbar }
21 |
--------------------------------------------------------------------------------
/app/components/toolbar/language-switcher.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useClientTranslation } from '@/app/hooks/use-client-translation'
3 | import { languages } from '@/app/i18n/settings'
4 | import { useUserStore } from '@/app/stores/use-user-store'
5 | import { Button } from '@/components/ui/button'
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuRadioGroup,
10 | DropdownMenuRadioItem,
11 | DropdownMenuTrigger,
12 | } from '@/components/ui/dropdown-menu'
13 | import { cn } from '@/lib/utils'
14 | import ISO639 from 'iso-639-1'
15 | import { LanguagesIcon } from 'lucide-react'
16 | import { useParams, usePathname, useRouter } from 'next/navigation'
17 | import { useEffect } from 'react'
18 |
19 | export interface LanguageSwitchProps {
20 | className?: string
21 | }
22 | export function LanguageSwitcher({ className }: LanguageSwitchProps) {
23 | const { locale } = useParams()
24 | const pathname = usePathname()
25 | const { t } = useClientTranslation()
26 | const router = useRouter()
27 | const langs = languages.map((language) => {
28 | return {
29 | key: language,
30 | label: ISO639.getNativeName(language),
31 | }
32 | })
33 |
34 | const handleChangeLanguage = (language: string) => {
35 | if (locale === language) return
36 | router.push(`/${language}/${pathname.slice(locale.length + 1)}`)
37 | }
38 |
39 | useEffect(() => {
40 | useUserStore.getState().updateAll({ language: locale as string })
41 | // eslint-disable-next-line react-hooks/exhaustive-deps
42 | }, [locale])
43 |
44 | return (
45 | <>
46 |
47 |
48 |
54 |
55 |
56 |
57 |
58 |
62 | {langs.map((language) => {
63 | return (
64 |
65 | {language.label}
66 |
67 | )
68 | })}
69 |
70 |
71 |
72 | >
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/app/components/toolbar/share.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useClientTranslation } from '@/app/hooks/use-client-translation'
3 | import { useCodeStore } from '@/app/stores/use-code-store'
4 | import { Button } from '@/components/ui/button'
5 | import {
6 | Dialog,
7 | DialogContent,
8 | DialogHeader,
9 | DialogTitle,
10 | } from '@/components/ui/dialog'
11 | import { Input } from '@/components/ui/input'
12 | import { cn } from '@/lib/utils'
13 | import ky from 'ky'
14 | import { ShareIcon } from 'lucide-react'
15 | import { useParams } from 'next/navigation'
16 | import { useCallback, useState } from 'react'
17 | import { toast } from 'react-hot-toast'
18 |
19 | interface Props {
20 | className?: string
21 | }
22 | function Share({ className }: Props) {
23 | const { t } = useClientTranslation()
24 | const { locale } = useParams()
25 | const [shareId, setShareId] = useState(null)
26 |
27 | const [shareDialogOpen, setShareDialogOpen] = useState(false)
28 |
29 | const { generateCode, status } = useCodeStore((state) => ({
30 | generateCode: state.generateCode,
31 | status: state.status,
32 | }))
33 |
34 | const handleShare = useCallback(async () => {
35 | if (!generateCode && status !== 'created' && status !== 'updated') {
36 | toast.error(t('extras:share.no_code_error'))
37 | return
38 | }
39 | try {
40 | const { id: shareId } = await ky
41 | .post('/api/share', {
42 | json: {
43 | generateCode,
44 | },
45 | })
46 | .json<{ id: string }>()
47 | if (shareId) {
48 | setShareId(shareId)
49 | try {
50 | await navigator.clipboard.writeText(
51 | `${window.location.origin}/${locale}/share/${shareId}?lang=${locale}`
52 | )
53 | } catch (e) {
54 | setShareDialogOpen(true)
55 | }
56 | }
57 | if (window.self !== window.top) {
58 | setShareDialogOpen(true)
59 | } else {
60 | toast.success(t('extras:share.success'))
61 | }
62 | } catch (error) {
63 | console.error(error)
64 | toast.error(t('extras:share.error'))
65 | }
66 | }, [generateCode, locale, t, status])
67 |
68 | return (
69 | <>
70 |
77 |
78 |
79 |
80 |
81 |
82 | {t('extras:share.successIframe')}
83 |
84 |
85 | {}}
88 | onClick={() => {
89 | navigator.clipboard.writeText(
90 | `${window.location.origin}/${locale}/share/${shareId}?lang=${locale}`
91 | )
92 | }}
93 | />
94 |
95 |
96 | >
97 | )
98 | }
99 |
100 | export { Share }
101 |
--------------------------------------------------------------------------------
/app/components/toolbar/theme-switcher.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { MoonIcon, SunIcon } from '@radix-ui/react-icons'
4 | import { useTheme } from 'next-themes'
5 |
6 | import { useClientTranslation } from '@/app/hooks/use-client-translation'
7 | import { Button } from '@/components/ui/button'
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuRadioGroup,
12 | DropdownMenuRadioItem,
13 | DropdownMenuTrigger,
14 | } from '@/components/ui/dropdown-menu'
15 |
16 | export function ThemeSwitcher() {
17 | const { setTheme, theme } = useTheme()
18 | const { t } = useClientTranslation()
19 |
20 | const handleThemeChange = (newTheme: string) => {
21 | if (newTheme === 'system') {
22 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
23 | .matches
24 | ? 'dark'
25 | : 'light'
26 | setTheme(systemTheme)
27 | } else {
28 | setTheme(newTheme)
29 | }
30 | }
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 | {t('extras:switch_theme.toggle_theme')}
40 |
41 |
42 |
43 |
44 |
45 |
46 | {t('extras:switch_theme.light')}
47 |
48 |
49 | {t('extras:switch_theme.dark')}
50 |
51 |
52 | {t('extras:switch_theme.system')}
53 |
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 224 71.4% 4.1%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 224 71.4% 4.1%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 224 71.4% 4.1%;
13 | --primary: 262.1 83.3% 57.8%;
14 | --primary-foreground: 210 20% 98%;
15 | --secondary: 220 14.3% 95.9%;
16 | --secondary-foreground: 220.9 39.3% 11%;
17 | --muted: 220 14.3% 95.9%;
18 | --muted-foreground: 220 8.9% 46.1%;
19 | --accent: 220 14.3% 95.9%;
20 | --accent-foreground: 220.9 39.3% 11%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 20% 98%;
23 | --border: 220 13% 91%;
24 | --input: 220 13% 91%;
25 | --ring: 262.1 83.3% 57.8%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 224 71.4% 4.1%;
36 | --foreground: 210 20% 98%;
37 | --card: 224 71.4% 4.1%;
38 | --card-foreground: 210 20% 98%;
39 | --popover: 224 71.4% 4.1%;
40 | --popover-foreground: 210 20% 98%;
41 | --primary: 263.4 70% 50.4%;
42 | --primary-foreground: 210 20% 98%;
43 | --secondary: 215 27.9% 16.9%;
44 | --secondary-foreground: 210 20% 98%;
45 | --muted: 215 27.9% 16.9%;
46 | --muted-foreground: 217.9 10.6% 64.9%;
47 | --accent: 215 27.9% 16.9%;
48 | --accent-foreground: 210 20% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 210 20% 98%;
51 | --border: 215 27.9% 16.9%;
52 | --input: 215 27.9% 16.9%;
53 | --ring: 263.4 70% 50.4%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 | }
70 |
71 | :root,
72 | body {
73 | height: 100%;
74 | min-height: fit-content;
75 | }
76 |
--------------------------------------------------------------------------------
/app/hooks/use-302url.ts:
--------------------------------------------------------------------------------
1 | import { env } from 'next-runtime-env'
2 |
3 | export function use302Url() {
4 | const region = env('NEXT_PUBLIC_REGION')
5 |
6 | return {
7 | href:
8 | region == '0'
9 | ? env('NEXT_PUBLIC_OFFICIAL_WEBSITE_URL_CHINA')!
10 | : env('NEXT_PUBLIC_OFFICIAL_WEBSITE_URL_GLOBAL')!,
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/hooks/use-client-translation.ts:
--------------------------------------------------------------------------------
1 | import { useParams } from 'next/navigation'
2 | import { useTranslation } from '../i18n/client'
3 |
4 | export function useClientTranslation() {
5 | const { locale } = useParams()
6 | const { t } = useTranslation(locale as string)
7 |
8 | return {
9 | t,
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/hooks/use-file-upload.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { env } from 'next-runtime-env'
3 | import { useState } from 'react'
4 |
5 | interface UploadOptions {
6 | file: File
7 | prefix?: string
8 | needCompress?: boolean
9 | maxSizeInBytes?: number // maximum file size limit (bytes)
10 | }
11 |
12 | interface UploadResponse {
13 | code: number
14 | data: {
15 | url: string
16 | }
17 | msg: string
18 | }
19 |
20 | const uploadURL = env('NEXT_PUBLIC_UPLOAD_API_URL')!
21 |
22 | export const useFileUpload = () => {
23 | const [isLoading, setIsLoading] = useState(false)
24 | const [error, setError] = useState(null)
25 |
26 | const upload = async ({
27 | file,
28 | prefix,
29 | needCompress = false,
30 | maxSizeInBytes = 5 * 1024 * 1024, // default 5MB
31 | }: UploadOptions): Promise => {
32 | setIsLoading(true)
33 | setError(null)
34 |
35 | try {
36 | // check file size
37 | if (file.size > maxSizeInBytes) {
38 | throw new Error(
39 | `File size exceeds the limit of ${maxSizeInBytes / (1024 * 1024)} MB`
40 | )
41 | }
42 |
43 | const formData = new FormData()
44 |
45 | formData.append('file', file)
46 |
47 | if (prefix) {
48 | formData.append('prefix', prefix)
49 | }
50 |
51 | formData.append('need_compress', needCompress.toString())
52 |
53 | const response = await fetch(uploadURL, {
54 | method: 'POST',
55 | body: formData,
56 | })
57 |
58 | if (!response.ok) {
59 | throw new Error('Upload failed')
60 | }
61 |
62 | const data: UploadResponse = await response.json()
63 |
64 | return data
65 | } catch (err) {
66 | setError(err instanceof Error ? err.message : 'An unknown error occurred')
67 |
68 | return null
69 | } finally {
70 | setIsLoading(false)
71 | }
72 | }
73 |
74 | return { upload, isLoading, error }
75 | }
76 |
77 | export default useFileUpload
78 |
--------------------------------------------------------------------------------
/app/hooks/use-is-dark.ts:
--------------------------------------------------------------------------------
1 | import { useTheme } from 'next-themes'
2 |
3 | export function useIsDark() {
4 | const { theme } = useTheme()
5 | return {
6 | isDark: theme === 'dark',
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/hooks/use-is-share-path.ts:
--------------------------------------------------------------------------------
1 | import { env } from 'next-runtime-env'
2 | import { useParams, usePathname } from 'next/navigation'
3 | import { useMemo } from 'react'
4 |
5 | const sharePath = env('NEXT_PUBLIC_SHARE_PATH')!
6 |
7 | function useIsSharePath() {
8 | const { locale } = useParams()
9 | const pathname = usePathname()
10 |
11 | const isSharePage = useMemo(
12 | () => pathname.startsWith(`/${locale}${sharePath}`),
13 | [pathname, locale]
14 | )
15 |
16 | return { isSharePage }
17 | }
18 |
19 | export { useIsSharePath }
20 |
--------------------------------------------------------------------------------
/app/hooks/use-is-support-vision.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react"
2 |
3 | import { env } from "next-runtime-env"
4 |
5 | export function useIsSupportVision() {
6 | const modelName = env('NEXT_PUBLIC_MODEL_NAME')
7 | return useMemo(() => {
8 | return !(modelName?.includes('o1-mini') || modelName?.includes('o1-preview'))
9 | }, [modelName])
10 | }
11 |
--------------------------------------------------------------------------------
/app/hooks/use-throttled-state.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 |
3 | export function useThrottledState(initialValue: T, delay: number) {
4 | const [immediateValue, setImmediateValue] = useState(initialValue);
5 | const [throttledValue, setThrottledValue] = useState(initialValue);
6 | const lastUpdate = useRef(Date.now());
7 | const timeoutRef = useRef(null);
8 |
9 | useEffect(() => {
10 | const updateThrottledValue = () => {
11 | setThrottledValue(immediateValue);
12 | lastUpdate.current = Date.now();
13 | timeoutRef.current = setTimeout(updateThrottledValue, delay);
14 | };
15 |
16 | if (timeoutRef.current === null) {
17 | timeoutRef.current = setTimeout(updateThrottledValue, delay);
18 | }
19 |
20 | return () => {
21 | if (timeoutRef.current) {
22 | clearTimeout(timeoutRef.current);
23 | }
24 | };
25 | }, [immediateValue, delay]);
26 |
27 | const throttledSetValue = (newValue: T | ((prev: T) => T)) => {
28 | setImmediateValue(newValue);
29 | };
30 |
31 | return [throttledValue, throttledSetValue] as const;
32 | }
33 |
--------------------------------------------------------------------------------
/app/i18n/client.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import i18next, { FlatNamespace, KeyPrefix } from 'i18next'
4 | import LanguageDetector from 'i18next-browser-languagedetector'
5 | import resourcesToBackend from 'i18next-resources-to-backend'
6 | import { useEffect, useState } from 'react'
7 | import { useCookies } from 'react-cookie'
8 | import {
9 | FallbackNs,
10 | initReactI18next,
11 | UseTranslationOptions,
12 | useTranslation as useTranslationOrg,
13 | UseTranslationResponse,
14 | } from 'react-i18next'
15 | import { cookieName, getOptions, languages, searchParamName } from './settings'
16 |
17 | const runsOnServerSide = typeof window === 'undefined'
18 |
19 | i18next
20 | .use(initReactI18next)
21 | .use(LanguageDetector)
22 | .use(
23 | resourcesToBackend(
24 | (language: string, namespace: string) =>
25 | import(`./locales/${language}/${namespace}.json`)
26 | )
27 | )
28 | .init({
29 | ...getOptions(undefined, ['auth', 'extras', 'home', 'translation']),
30 | lng: undefined,
31 | detection: {
32 | order: ['querystring', 'cookie', 'htmlTag', 'navigator', 'path'],
33 | lookupQuerystring: searchParamName,
34 | lookupCookie: cookieName,
35 | lookupFromPathIndex: 0,
36 | lookupFromSubdomainIndex: 0,
37 | },
38 | preload: runsOnServerSide ? languages : [],
39 | })
40 |
41 | export function useTranslation<
42 | Ns extends FlatNamespace,
43 | KPrefix extends KeyPrefix> = undefined,
44 | >(
45 | lng: string,
46 | ns?: Ns,
47 | options?: UseTranslationOptions
48 | ): UseTranslationResponse, KPrefix> {
49 | const [cookies, setCookie] = useCookies([cookieName])
50 | const ret = useTranslationOrg(ns, options)
51 | const { i18n } = ret
52 | if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
53 | i18n.changeLanguage(lng)
54 | } else {
55 | // eslint-disable-next-line react-hooks/rules-of-hooks
56 | const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage)
57 | // eslint-disable-next-line react-hooks/rules-of-hooks
58 | useEffect(() => {
59 | if (activeLng === i18n.resolvedLanguage) return
60 | setActiveLng(i18n.resolvedLanguage)
61 | }, [activeLng, i18n.resolvedLanguage])
62 | // eslint-disable-next-line react-hooks/rules-of-hooks
63 | useEffect(() => {
64 | if (!lng || i18n.resolvedLanguage === lng) return
65 | i18n.changeLanguage(lng)
66 | }, [lng, i18n])
67 | // eslint-disable-next-line react-hooks/rules-of-hooks
68 | useEffect(() => {
69 | if (cookies.lang === lng) return
70 | setCookie(cookieName, lng, { path: '/' })
71 | // eslint-disable-next-line react-hooks/exhaustive-deps
72 | }, [lng, cookies.lang])
73 | }
74 | return ret
75 | }
76 |
--------------------------------------------------------------------------------
/app/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import { createInstance, FlatNamespace, KeyPrefix } from 'i18next'
2 | import resourcesToBackend from 'i18next-resources-to-backend'
3 | import { initReactI18next } from 'react-i18next/initReactI18next'
4 | import { FallbackNs } from 'react-i18next'
5 | import { getOptions } from './settings'
6 |
7 | const initI18next = async (lng: string, ns: string | string[]) => {
8 | const i18nInstance = createInstance()
9 | await i18nInstance
10 | .use(initReactI18next)
11 | .use(
12 | resourcesToBackend(
13 | (language: string, namespace: string) =>
14 | import(`./locales/${language}/${namespace}.json`)
15 | )
16 | )
17 | .init(getOptions(lng, ns))
18 | return i18nInstance
19 | }
20 |
21 | export async function useTranslation<
22 | Ns extends FlatNamespace,
23 | KPrefix extends KeyPrefix> = undefined,
24 | >(lng: string, ns?: Ns, options: { keyPrefix?: KPrefix } = {}) {
25 | const i18nextInstance = await initI18next(
26 | lng,
27 | Array.isArray(ns) ? (ns as string[]) : (ns as string)
28 | )
29 | return {
30 | t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix),
31 | i18n: i18nextInstance,
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/i18n/locales/en/auth.json:
--------------------------------------------------------------------------------
1 | {
2 | "code_input": {
3 | "placeholder": "Please enter the share code."
4 | },
5 | "confirm_button": "Confirm",
6 | "description": "The creator has enabled verification, please enter the share code below.",
7 | "error_message": "For more, please visit",
8 | "errors": {
9 | "network_error": "Network Error",
10 | "share_code_error": "Sharing code error",
11 | "tool_deleted": "The tool has been deleted.",
12 | "tool_disabled": "The tool has been disabled.",
13 | "unknown_error": "Unknown error"
14 | },
15 | "logo_title": "Logo of AI 302",
16 | "remember_code": "Remember the share code.",
17 | "title": "Need sharing code"
18 | }
19 |
--------------------------------------------------------------------------------
/app/i18n/locales/en/extras.json:
--------------------------------------------------------------------------------
1 | {
2 | "about": {
3 | "title": "About",
4 | "trigger": {
5 | "label": "Open/Close tool information popup"
6 | }
7 | },
8 | "error_page": {
9 | "reload": "Reload",
10 | "title": "An unexpected failure occurred."
11 | },
12 | "footer": {
13 | "copyright_content": "Powered by ",
14 | "copyright_leading": "The content is generated by AI and is for reference only."
15 | },
16 | "share": {
17 | "error": "Creation failed, please try again later.",
18 | "no_code_error": "No code available, unable to share.",
19 | "success": "Sharing successful, share link copied.",
20 | "successIframe": "Sharing successful, please copy manually.",
21 | "trigger": {
22 | "label": "Open/Close share link"
23 | }
24 | },
25 | "switch_language": "Switch language",
26 | "switch_theme": {
27 | "dark": "Dark",
28 | "light": "Light",
29 | "system": "System",
30 | "toggle_theme": "Switch Theme"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/i18n/locales/en/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "code_viewer": {
3 | "reference": "Quote"
4 | },
5 | "header": {
6 | "auto_scroll": "Automatic Scrolling",
7 | "both": "Take care of both",
8 | "both_description": "The web page will be designed with reference to both the style layout and information content of the images.",
9 | "image_content": "Image content",
10 | "image_content_description": "Referring to the information content and functional requirements of the picture, the result is more inclined to the logic described in the picture.",
11 | "image_style": "Interface style",
12 | "image_style_description": "Refer to the style layout of the picture, such as background color, font, etc., and the result will be more inclined to the style of the picture.",
13 | "optimize_prompt": "Optimize Prompt",
14 | "prompt_cannot_be_empty": "The prompt cannot be empty.",
15 | "select_image": "Select picture",
16 | "show_file_explorer": "Display all files",
17 | "show_preview": "Display Preview",
18 | "title": "AI Web Page Generator 2.0",
19 | "upload_failed": "File upload failed.",
20 | "upload_image_reference": "Reference method",
21 | "upload_success": "File upload successful",
22 | "url_input_placeholder": "Create a calculator using the Apple style."
23 | },
24 | "main": {
25 | "code_view_creating": "Building your APP...",
26 | "code_view_empty": "No preview available, please start with your requirements first!",
27 | "code_view_updating": "Rebuilding your APP...",
28 | "prompt_input_placeholder": "Enter your modification ideas."
29 | },
30 | "prompt_for_update_empty_error": "The prompt word cannot be empty."
31 | }
32 |
--------------------------------------------------------------------------------
/app/i18n/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "errors": {
3 | "-10001": "Missing 302 Apikey",
4 | "-10002": "This tool has been disabled/deleted, for details please view 302.AI .",
5 | "-10003": "Network error, please try again later",
6 | "-10004": "Insufficient account balance. Create your own tool, for details please view 302.AI .",
7 | "-10005": "Account credential expired, please log in again",
8 | "-10006": "Total Quota reached maximum limit, for details please view 302.AI ",
9 | "-10007": "Daily Quota reached maximum limit, for details please view 302.AI ",
10 | "-10008": "No available channels currently, for details please view 302.AI ",
11 | "-10009": "Current API function not supported, for details please view 302.AI ",
12 | "-10010": "Resource not found, for details please view 302.AI ",
13 | "-10011": "Invalid request",
14 | "-10012": "This free tool's hour quota reached maximum limit. Please view 302.AI to create your own tool",
15 | "-1024": "AI interface connection timeout, please try again later or contact 302.AI ",
16 | "default": "Unknown error, for details please view 302.AI "
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/i18n/locales/ja/auth.json:
--------------------------------------------------------------------------------
1 | {
2 | "code_input": {
3 | "placeholder": "共有コードを入力してください"
4 | },
5 | "confirm_button": "確認",
6 | "description": "作成者が認証を開始したので、下に共有コードを入力してください。",
7 | "error_message": "詳細はご覧ください",
8 | "errors": {
9 | "network_error": "ネットワークエラー",
10 | "share_code_error": "シェアコードエラー",
11 | "tool_deleted": "そのツールはすでに削除されました。",
12 | "tool_disabled": "そのツールは無効にされています",
13 | "unknown_error": "未知のエラー"
14 | },
15 | "logo_title": "AI 302のロゴ",
16 | "remember_code": "共有コードを覚えておいてください",
17 | "title": "共有コードが必要です"
18 | }
19 |
--------------------------------------------------------------------------------
/app/i18n/locales/ja/extras.json:
--------------------------------------------------------------------------------
1 | {
2 | "about": {
3 | "title": "について",
4 | "trigger": {
5 | "label": "ツール情報のポップアップを開く/閉じる"
6 | }
7 | },
8 | "error_page": {
9 | "reload": "再読み込み",
10 | "title": "予想外の故障が発生しました"
11 | },
12 | "footer": {
13 | "copyright_content": " によって駆動されます",
14 | "copyright_leading": "コンテンツはAIによって生成され、参考のためだけです。"
15 | },
16 | "share": {
17 | "error": "作成に失敗しました、しばらくしてから再試行してください",
18 | "no_code_error": "コードがないため、共有できません",
19 | "success": "共有成功、共有リンクをコピーしました",
20 | "successIframe": "共有に成功しました。手動でコピーしてください。",
21 | "trigger": {
22 | "label": "共有リンクを開く/閉じる"
23 | }
24 | },
25 | "switch_language": "言語を切り替える",
26 | "switch_theme": {
27 | "dark": "夜間",
28 | "light": "昼間",
29 | "system": "システムに従う",
30 | "toggle_theme": "テーマの切り替え"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/i18n/locales/ja/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "code_viewer": {
3 | "reference": "引用"
4 | },
5 | "header": {
6 | "auto_scroll": "自動スクロール",
7 | "both": "どちらも大事にしてね",
8 | "both_description": "Web ページは、画像のスタイル レイアウトと情報コンテンツの両方を参照してデザインされます。",
9 | "image_content": "画像の内容",
10 | "image_content_description": "画像の情報内容と機能要件を参照すると、結果は画像に記述されているロジックにより近くなります。",
11 | "image_style": "インターフェイスのスタイル",
12 | "image_style_description": "背景色やフォントなど、画像のスタイルレイアウトを参考にすると、より画像のスタイルに近い仕上がりになります。",
13 | "optimize_prompt": "プロンプトの最適化",
14 | "prompt_cannot_be_empty": "ヒントの言葉は空白にできません",
15 | "select_image": "画像を選択",
16 | "show_file_explorer": "すべてのファイルを表示します",
17 | "show_preview": "プレビューを表示する",
18 | "title": "AIウェブページジェネレータ2.0",
19 | "upload_failed": "ファイルのアップロードに失敗しました",
20 | "upload_image_reference": "参考方法",
21 | "upload_success": "ファイルのアップロードに成功しました",
22 | "url_input_placeholder": "Appleスタイルの計算機を作成する"
23 | },
24 | "main": {
25 | "code_view_creating": "あなたのアプリを作成中です...",
26 | "code_view_empty": "プレビューはまだありません、まずはあなたのニーズから始めてください!",
27 | "code_view_updating": "あなたのAPPを再構築中です...",
28 | "prompt_input_placeholder": "あなたの修正案を入力してください。"
29 | },
30 | "prompt_for_update_empty_error": "ヒントの単語は空にできません"
31 | }
32 |
--------------------------------------------------------------------------------
/app/i18n/locales/ja/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "errors": {
3 | "-10001": "302 APIキーがありません",
4 | "-10002": "このツールは無効化/削除されています。詳細は 302.AI をご覧ください。",
5 | "-10003": "ネットワークエラー、後でもう一度お試しください。",
6 | "-10004": "アカウント残高が不足しています。独自のツールを作成するには、 302.AI をご覧ください。",
7 | "-10005": "アカウントの資格情報が期限切れです。再度ログインしてください。",
8 | "-10006": "アカウントの総限度額に達しました。詳細は 302.AI をご覧ください。",
9 | "-10007": "アカウントの日次限度額に達しました。詳細は 302.AI をご覧ください。",
10 | "-10008": "現在利用可能なチャネルはありません。詳細は 302.AI をご覧ください。",
11 | "-10009": "現在のAPI機能はサポートされていません。詳細は 302.AI をご覧ください。",
12 | "-10010": "リソースが見つかりませんでした。詳細は 302.AI をご覧ください。",
13 | "-10011": "無効なリクエスト",
14 | "-10012": "この無料ツールは今時間の上限に達しました。 302.AI を訪問して自分のツールを作成してください",
15 | "-1024": "AIインターフェース接続がタイムアウトしました。しばらくしてから再試行するか、302.AI に連絡してください。",
16 | "default": "不明なエラー、詳細は 302.AI をご覧ください。"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/i18n/locales/zh/auth.json:
--------------------------------------------------------------------------------
1 | {
2 | "code_input": {
3 | "placeholder": "请输入分享码"
4 | },
5 | "confirm_button": "确认",
6 | "description": "创建者开启了验证, 请在下方填入分享码",
7 | "error_message": "更多请访问",
8 | "errors": {
9 | "network_error": "网络错误",
10 | "share_code_error": "分享码错误",
11 | "tool_deleted": "该工具已删除",
12 | "tool_disabled": "该工具已禁用",
13 | "unknown_error": "未知错误"
14 | },
15 | "logo_title": "AI 302的Logo",
16 | "remember_code": "记住分享码",
17 | "title": "需要分享码"
18 | }
19 |
--------------------------------------------------------------------------------
/app/i18n/locales/zh/extras.json:
--------------------------------------------------------------------------------
1 | {
2 | "about": {
3 | "title": "关于",
4 | "trigger": {
5 | "label": "打开/关闭工具信息弹窗"
6 | }
7 | },
8 | "error_page": {
9 | "reload": "重新加载",
10 | "title": "出现意料之外的故障"
11 | },
12 | "footer": {
13 | "copyright_content": "由 驱动",
14 | "copyright_leading": "内容由AI生成,仅供参考"
15 | },
16 | "share": {
17 | "error": "创建失败,请稍后重试",
18 | "no_code_error": "暂无代码,无法分享",
19 | "success": "分享成功,已复制分享链接",
20 | "successIframe": "分享成功,请手动复制",
21 | "trigger": {
22 | "label": "打开/关闭分享链接"
23 | }
24 | },
25 | "switch_language": "切换语言",
26 | "switch_theme": {
27 | "dark": "夜间",
28 | "light": "白天",
29 | "system": "跟随系统",
30 | "toggle_theme": "切换主题"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/i18n/locales/zh/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "code_viewer": {
3 | "reference": "引用"
4 | },
5 | "header": {
6 | "auto_scroll": "自动滚动",
7 | "both": "两者兼顾",
8 | "both_description": "将同时参考图片的风格布局和信息内容对网页进行设计。",
9 | "image_content": "图片内容",
10 | "image_content_description": "参考图片的信息内容和功能需求,结果更倾向于图片里描述的逻辑。",
11 | "image_style": "界面风格",
12 | "image_style_description": "参考图片的风格布局,如背景颜色、字体等,结果更倾向于图片的风格。",
13 | "optimize_prompt": "优化Prompt",
14 | "prompt_cannot_be_empty": "提示词不能为空",
15 | "select_image": "选择图片",
16 | "show_file_explorer": "显示所有文件",
17 | "show_preview": "显示预览",
18 | "title": "AI网页生成器2.0",
19 | "upload_failed": "文件上传失败",
20 | "upload_image_reference": "参考方式",
21 | "upload_success": "文件上传成功",
22 | "url_input_placeholder": "做一个计算器,使用Apple风格"
23 | },
24 | "main": {
25 | "code_view_creating": "正在构建您的APP...",
26 | "code_view_empty": "暂无预览,请先从您的需求开始吧!",
27 | "code_view_updating": "正在重新构建您的APP...",
28 | "prompt_input_placeholder": "输入你的修改想法"
29 | },
30 | "prompt_for_update_empty_error": "提示词不能为空"
31 | }
32 |
--------------------------------------------------------------------------------
/app/i18n/locales/zh/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "errors": {
3 | "-10001": "缺少 302 API 密钥",
4 | "-10002": "该工具已禁用/删除,更多请访问 302.AI ",
5 | "-10003": "网络错误,请稍后重试",
6 | "-10004": "账户余额不足,创建属于自己的工具,更多请访问 302.AI ",
7 | "-10005": "账户凭证过期,请重新登录",
8 | "-10006": "账户总额度已达上限,更多请访问 302.AI ",
9 | "-10007": "账户日额度已达上限,更多请访问 302.AI ",
10 | "-10008": "当前无可用通道,更多请访问 302.AI ",
11 | "-10009": "不支持当前API功能,更多请访问 302.AI ",
12 | "-10010": "不支持当前API功能,更多请访问 302.AI ",
13 | "-10011": "无效的请求",
14 | "-10012": "该免费工具在本小时的额度已达上限,请访问 302.AI 生成属于自己的工具",
15 | "-1024": "AI接口连接超时, 请稍后重试或者联系 302.AI ",
16 | "default": "未知错误,更多请访问 302.AI "
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/i18n/settings.ts:
--------------------------------------------------------------------------------
1 | export const fallbackLng = 'en'
2 | export const languages = [fallbackLng, 'zh', 'ja']
3 | export const cookieName = 'lang'
4 | export const defaultNS = 'translation'
5 | export const searchParamName = 'lang'
6 |
7 | export function getOptions(
8 | lng = fallbackLng,
9 | ns: string | string[] = defaultNS
10 | ) {
11 | return {
12 | // debug: true,
13 | supportedLngs: languages,
14 | // preload: languages,
15 | fallbackLng,
16 | lng,
17 | fallbackNS: defaultNS,
18 | defaultNS,
19 | ns,
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/stores/middleware.ts:
--------------------------------------------------------------------------------
1 | import type { StateCreator } from 'zustand'
2 | import { createJSONStorage, devtools, persist } from 'zustand/middleware'
3 |
4 | interface WithHydratedState {
5 | setHasHydrated: (hasHydrated: boolean) => void
6 | }
7 |
8 | export const storeMiddleware = (
9 | f: StateCreator,
10 | name: string
11 | ) =>
12 | devtools(
13 | persist(f, {
14 | name,
15 | storage: createJSONStorage(() => sessionStorage),
16 | onRehydrateStorage: (state) => {
17 | return () => state.setHasHydrated(true)
18 | },
19 | }),
20 | { enabled: false }
21 | )
22 |
--------------------------------------------------------------------------------
/app/stores/use-code-store.ts:
--------------------------------------------------------------------------------
1 | import { produce } from 'immer'
2 | import { create } from 'zustand'
3 |
4 | import { CoreMessage } from 'ai'
5 | import { storeMiddleware } from './middleware'
6 |
7 | export enum ImageStyle {
8 | Style = 'style',
9 | Content = 'content',
10 | Both = 'both',
11 | }
12 |
13 | interface CodeStore {
14 | _hasHydrated: boolean
15 | prompt: string
16 | promptForUpdate: string
17 | generateCode: string | null
18 | status: 'initial' | 'updating' | 'creating' | 'updated' | 'created'
19 | isUseShadcnUi: boolean
20 | messages: CoreMessage[]
21 | autoScroll: boolean
22 | showFileExplorer: boolean
23 | showPreview: boolean
24 | image: string
25 | imageForUpdate: string
26 | lastCharCoords: { x: number; y: number }
27 | selectedText: string
28 | isSelecting: boolean
29 | referenceText: string
30 | imageStyle: ImageStyle
31 | imageStyleForUpdate: ImageStyle
32 | }
33 |
34 | export interface CodeInfoShare {
35 | generateCode: string
36 | }
37 |
38 | interface CodeActions {
39 | updateField: (
40 | field: T,
41 | value: CodeStore[T]
42 | ) => void
43 | appendGenerateCode: (code: string) => void
44 | appendPrompt: (prompt: string) => void
45 | appendPromptForUpdate: (prompt: string) => void
46 | appendMessage: (message: CoreMessage) => void
47 | updateAll: (fields: Partial) => void
48 | setHasHydrated: (value: boolean) => void
49 | setLastCharCoords: (coords: { x: number; y: number }) => void
50 | setSelectedText: (text: string) => void
51 | setIsSelecting: (value: boolean) => void
52 | setReferenceText: (text: string) => void
53 | setImageStyle: (value: ImageStyle) => void
54 | setImageStyleForUpdate: (value: ImageStyle) => void
55 | }
56 |
57 | export const useCodeStore = create()(
58 | storeMiddleware(
59 | (set) => ({
60 | _hasHydrated: false,
61 | prompt: '',
62 | promptForUpdate: '',
63 | generateCode: '',
64 | messages: [],
65 | status: 'initial',
66 | isUseShadcnUi: true,
67 | autoScroll: true,
68 | showPreview: true,
69 | showFileExplorer: false,
70 | image: '',
71 | imageForUpdate: '',
72 | lastCharCoords: { x: 0, y: 0 },
73 | selectedText: '',
74 | isSelecting: false,
75 | referenceText: '',
76 | imageStyle: ImageStyle.Style,
77 | imageStyleForUpdate: ImageStyle.Style,
78 | appendGenerateCode: (code: string) =>
79 | set(
80 | produce((state) => {
81 | state.generateCode = state.generateCode + code
82 | })
83 | ),
84 | appendMessage: (message: CoreMessage) =>
85 | set(
86 | produce((state) => {
87 | state.messages.push(message)
88 | })
89 | ),
90 | appendPrompt: (prompt: string) =>
91 | set(
92 | produce((state) => {
93 | state.prompt = state.prompt + prompt
94 | })
95 | ),
96 | appendPromptForUpdate: (prompt: string) =>
97 | set(
98 | produce((state) => {
99 | state.promptForUpdate = state.promptForUpdate + prompt
100 | })
101 | ),
102 | updateField: (field, value) =>
103 | set(
104 | produce((state) => {
105 | state[field] = value
106 | })
107 | ),
108 | updateAll: (fields) =>
109 | set(
110 | produce((state) => {
111 | for (const [key, value] of Object.entries(fields)) {
112 | state[key as keyof CodeStore] = value
113 | }
114 | })
115 | ),
116 | setHasHydrated: (value) =>
117 | set(
118 | produce((state) => {
119 | state._hasHydrated = value
120 | })
121 | ),
122 | setLastCharCoords: (coords) =>
123 | set(
124 | produce((state) => {
125 | state.lastCharCoords = coords
126 | })
127 | ),
128 | setSelectedText: (text) =>
129 | set(
130 | produce((state) => {
131 | state.selectedText = text
132 | })
133 | ),
134 | setIsSelecting: (value) =>
135 | set(
136 | produce((state) => {
137 | state.isSelecting = value
138 | })
139 | ),
140 | setReferenceText: (text) =>
141 | set(
142 | produce((state) => {
143 | state.referenceText = text
144 | })
145 | ),
146 | setImageStyle: (value) =>
147 | set(
148 | produce((state) => {
149 | state.imageStyle = value
150 | })
151 | ),
152 | setImageStyleForUpdate: (value) =>
153 | set(
154 | produce((state) => {
155 | state.imageStyleForUpdate = value
156 | })
157 | ),
158 | }),
159 | 'code_store_coder'
160 | )
161 | )
162 |
--------------------------------------------------------------------------------
/app/stores/use-user-store.ts:
--------------------------------------------------------------------------------
1 | import { produce } from 'immer'
2 | import { create } from 'zustand'
3 |
4 | import { storeMiddleware } from './middleware'
5 |
6 | interface UserStore {
7 | _hasHydrated: boolean
8 | language?: string
9 | }
10 |
11 | interface UserActions {
12 | updateField: (
13 | field: T,
14 | value: UserStore[T]
15 | ) => void
16 | updateAll: (fields: Partial) => void
17 | setHasHydrated: (value: boolean) => void
18 | }
19 |
20 | export const useUserStore = create()(
21 | storeMiddleware(
22 | (set) => ({
23 | _hasHydrated: false,
24 | language: 'en',
25 | updateField: (field, value) =>
26 | set(
27 | produce((state) => {
28 | state[field] = value
29 | })
30 | ),
31 | updateAll: (fields) =>
32 | set(
33 | produce((state) => {
34 | for (const [key, value] of Object.entries(fields)) {
35 | state[key as keyof UserStore] = value
36 | }
37 | })
38 | ),
39 | setHasHydrated: (value) =>
40 | set(
41 | produce((state) => {
42 | state._hasHydrated = value
43 | })
44 | ),
45 | }),
46 | 'user_store_coder'
47 | )
48 | )
49 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from '@radix-ui/react-slot'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 | import * as React from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
16 | outline:
17 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
20 | ghost: 'hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 hover:underline',
22 | icon: 'bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
23 | },
24 | size: {
25 | default: 'h-9 px-4 py-2',
26 | sm: 'h-8 rounded-md px-3 text-xs',
27 | lg: 'h-10 rounded-md px-8',
28 | icon: 'h-9 w-9',
29 | roundIconSm: 'size-6 rounded-full',
30 | roundIconMd: 'size-8 rounded-full',
31 | roundIconLg: 'size-10 rounded-full',
32 | },
33 | },
34 | defaultVariants: {
35 | variant: 'default',
36 | size: 'default',
37 | },
38 | }
39 | )
40 |
41 | export interface ButtonProps
42 | extends React.ButtonHTMLAttributes,
43 | VariantProps {
44 | asChild?: boolean
45 | }
46 |
47 | const Button = React.forwardRef(
48 | ({ className, variant, size, asChild = false, ...props }, ref) => {
49 | const Comp = asChild ? Slot : 'button'
50 | return (
51 |
56 | )
57 | }
58 | )
59 | Button.displayName = 'Button'
60 |
61 | export { Button, buttonVariants }
62 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
5 | import { CheckIcon } from '@radix-ui/react-icons'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 | import { Cross2Icon } from '@radix-ui/react-icons'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = 'DialogHeader'
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = 'DialogFooter'
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
4 | import {
5 | CheckIcon,
6 | ChevronRightIcon,
7 | DotFilledIcon,
8 | } from '@radix-ui/react-icons'
9 | import * as React from 'react'
10 |
11 | import { cn } from '@/lib/utils'
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuCheckboxItem,
192 | DropdownMenuContent,
193 | DropdownMenuGroup,
194 | DropdownMenuItem,
195 | DropdownMenuLabel,
196 | DropdownMenuPortal,
197 | DropdownMenuRadioGroup,
198 | DropdownMenuRadioItem,
199 | DropdownMenuSeparator,
200 | DropdownMenuShortcut,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuTrigger,
205 | }
206 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = 'Input'
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverAnchor = PopoverPrimitive.Anchor
13 |
14 | const PopoverContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18 |
19 |
29 |
30 | ))
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
32 |
33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
34 |
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { CheckIcon } from "@radix-ui/react-icons"
5 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/docs/AI网页生成器.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/docs/AI网页生成器.png
--------------------------------------------------------------------------------
/docs/AI网页生成器en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/docs/AI网页生成器en.png
--------------------------------------------------------------------------------
/docs/AI网页生成器jp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/docs/AI网页生成器jp.png
--------------------------------------------------------------------------------
/docs/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/docs/preview.png
--------------------------------------------------------------------------------
/docs/网页生成1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/docs/网页生成1.png
--------------------------------------------------------------------------------
/docs/网页生成2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/docs/网页生成2.png
--------------------------------------------------------------------------------
/docs/网页生成3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/docs/网页生成3.png
--------------------------------------------------------------------------------
/lib/api/api.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useUserStore } from '@/app/stores/use-user-store'
3 | import { emitter } from '@/lib/mitt'
4 | import ky from 'ky'
5 | import { env } from 'next-runtime-env'
6 | import { isEmpty } from 'radash'
7 | import { langToCountry } from './lang-to-country'
8 |
9 | const apiKy = ky.create({
10 | prefixUrl: env('NEXT_PUBLIC_API_URL'),
11 | timeout: 30000,
12 | hooks: {
13 | beforeRequest: [
14 | (request) => {
15 | const apiKey = env('NEXT_PUBLIC_API_KEY')
16 | if (apiKey) {
17 | request.headers.set('Authorization', `Bearer ${apiKey}`)
18 | }
19 | const lang = useUserStore.getState().language
20 | if (lang) {
21 | request.headers.set('Lang', langToCountry(lang))
22 | }
23 | },
24 | ],
25 | afterResponse: [
26 | async (request, options, response) => {
27 | if (!response.ok) {
28 | const res = await response.json<{ error: { err_code: number } }>()
29 | if (!isEmpty(res.error?.err_code)) {
30 | emitter.emit('ToastError', res.error.err_code)
31 | }
32 | }
33 | },
34 | ],
35 | },
36 | })
37 |
38 |
39 | export { apiKy }
40 |
--------------------------------------------------------------------------------
/lib/api/lang-to-country.ts:
--------------------------------------------------------------------------------
1 | const map = {
2 | zh: 'cn',
3 | en: 'en',
4 | ja: 'jp',
5 | }
6 |
7 | export function langToCountry(lang: string) {
8 | return map[lang as keyof typeof map] || lang
9 | }
10 |
--------------------------------------------------------------------------------
/lib/brand.ts:
--------------------------------------------------------------------------------
1 | export const showBrand = process.env.NEXT_PUBLIC_SHOW_BRAND === "true";
2 |
--------------------------------------------------------------------------------
/lib/check-env.ts:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import { env } from 'next-runtime-env'
3 |
4 | assert(
5 | env('NEXT_PUBLIC_OFFICIAL_WEBSITE_URL_CHINA'),
6 | 'NEXT_PUBLIC_OFFICIAL_WEBSITE_URL_CHINA is required'
7 | )
8 | assert(
9 | env('NEXT_PUBLIC_OFFICIAL_WEBSITE_URL_GLOBAL'),
10 | 'NEXT_PUBLIC_OFFICIAL_WEBSITE_URL_GLOBAL is required'
11 | )
12 | assert(env('NEXT_PUBLIC_API_URL'), 'NEXT_PUBLIC_API_URL is required')
13 | assert(env('NEXT_PUBLIC_MODEL_NAME'), 'NEXT_PUBLIC_MODEL_NAME is required')
14 | assert(env('NEXT_PUBLIC_REGION'), 'NEXT_PUBLIC_REGION is required')
15 | assert(env('NEXT_PUBLIC_LOCALE'), 'NEXT_PUBLIC_LOCALE is required')
16 | assert(env('NEXT_PUBLIC_SHARE_PATH'), 'NEXT_PUBLIC_SHARE_PATH is required')
17 | assert(env('NEXT_PUBLIC_DEFAULT_SHARE_DIR'), 'NEXT_PUBLIC_DEFAULT_SHARE_DIR is required')
18 | assert(env('NEXT_PUBLIC_UPLOAD_API_URL'), 'NEXT_PUBLIC_UPLOAD_API_URL is required')
19 | assert(env('NEXT_PUBLIC_API_KEY'), 'NEXT_PUBLIC_API_KEY is required')
20 |
--------------------------------------------------------------------------------
/lib/logger.ts:
--------------------------------------------------------------------------------
1 | const pino = require('pino')
2 | import { Logger } from 'pino'
3 |
4 | export const logger: Logger =
5 | process.env.NODE_ENV === 'production'
6 | ? pino({
7 | level: process.env.PINO_LOG_LEVEL || 'warn',
8 | })
9 | : pino({
10 | transport: {
11 | target: 'pino-pretty',
12 | options: {
13 | colorize: true,
14 | },
15 | },
16 | level: process.env.PINO_LOG_LEVEL || 'debug',
17 | })
18 |
--------------------------------------------------------------------------------
/lib/mitt.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import mitt from 'mitt'
3 | type Events = {
4 | ToastError: number
5 | }
6 | const emitter = mitt()
7 |
8 | export { emitter }
9 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/accordion.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Accordion";
2 |
3 | export const importDocs = `
4 | import {
5 | Accordion,
6 | AccordionContent,
7 | AccordionItem,
8 | AccordionTrigger,
9 | } from "/components/ui/accordion"
10 | `;
11 |
12 | export const usageDocs = `
13 |
14 |
15 | Is it accessible?
16 |
17 | Yes. It adheres to the WAI-ARIA design pattern.
18 |
19 |
20 |
21 | `;
22 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | export const name = "AlertDialog";
2 |
3 | export const importDocs = `
4 | import {
5 | AlertDialog,
6 | AlertDialogAction,
7 | AlertDialogCancel,
8 | AlertDialogContent,
9 | AlertDialogDescription,
10 | AlertDialogFooter,
11 | AlertDialogHeader,
12 | AlertDialogTitle,
13 | AlertDialogTrigger,
14 | } from "/components/ui/alert-dialog"
15 | `;
16 |
17 | export const usageDocs = `
18 |
19 | Open
20 |
21 |
22 | Are you absolutely sure?
23 |
24 | This action cannot be undone. This will permanently delete your account
25 | and remove your data from our servers.
26 |
27 |
28 |
29 | Cancel
30 | Continue
31 |
32 |
33 |
34 | `;
35 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/alert.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Alert";
2 |
3 | export const importDocs = `
4 | import { Alert, AlertDescription, AlertTitle } from "/components/ui/alert"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 |
10 | Heads up!
11 |
12 | You can add components and dependencies to your app using the cli.
13 |
14 |
15 | `;
16 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/avatar.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Avatar";
2 |
3 | export const importDocs = `
4 | import { Avatar, AvatarFallback, AvatarImage } from "/components/ui/avatar";
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 |
10 | CN
11 |
12 | `;
13 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/badge.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Badge";
2 |
3 | export const importDocs = `
4 | import { Badge } from "/components/ui/badge"
5 | `;
6 |
7 | export const usageDocs = `
8 | Badge
9 | `;
10 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Breadcrumb";
2 |
3 | export const importDocs = `
4 | import {
5 | Breadcrumb,
6 | BreadcrumbItem,
7 | BreadcrumbLink,
8 | BreadcrumbList,
9 | BreadcrumbPage,
10 | BreadcrumbSeparator,
11 | } from "/components/ui/breadcrumb"
12 | `;
13 |
14 | export const usageDocs = `
15 |
16 |
17 |
18 | Home
19 |
20 |
21 |
22 | Components
23 |
24 |
25 |
26 | Breadcrumb
27 |
28 |
29 |
30 | `;
31 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/button.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Button";
2 |
3 | export const importDocs = `
4 | import { Button } from "/components/ui/button"
5 | `;
6 |
7 | export const usageDocs = `
8 | A normal button
9 | Button
10 | Button
11 | Button
12 | Button
13 | Button
14 | `;
15 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/calendar.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Calendar";
2 |
3 | export const importDocs = `
4 | import { Calendar } from "/components/ui/calendar"
5 | `;
6 |
7 | export const usageDocs = `
8 | const [date, setDate] = React.useState(new Date())
9 |
10 | return (
11 |
17 | )
18 | `;
19 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/card.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Card";
2 |
3 | export const importDocs = `
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "/components/ui/card"
12 | `;
13 |
14 | export const usageDocs = `
15 |
16 |
17 | Card Title
18 | Card Description
19 |
20 |
21 | Card Content
22 |
23 |
24 | Card Footer
25 |
26 |
27 | `;
28 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/carousel.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Carousel";
2 |
3 | export const importDocs = `
4 | import {
5 | Carousel,
6 | CarouselContent,
7 | CarouselItem,
8 | CarouselNext,
9 | CarouselPrevious,
10 | } from "/components/ui/carousel"
11 | `;
12 |
13 | export const usageDocs = `
14 |
15 |
16 | ...
17 | ...
18 | ...
19 |
20 |
21 |
22 |
23 | `;
24 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/checkbox.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Checkbox";
2 |
3 | export const importDocs = `
4 | import { Checkbox } from "/components/ui/checkbox"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/dialog.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Dialog";
2 |
3 | export const importDocs = `
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogDescription,
8 | DialogHeader,
9 | DialogTitle,
10 | DialogTrigger,
11 | } from "/components/ui/dialog"
12 | `;
13 |
14 | export const usageDocs = `
15 |
16 | Open
17 |
18 |
19 | Are you absolutely sure?
20 |
21 | This action cannot be undone. This will permanently delete your account
22 | and remove your data from our servers.
23 |
24 |
25 |
26 |
27 | `;
28 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | export const name = "DropdownMenu";
2 |
3 | export const importDocs = `
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuLabel,
9 | DropdownMenuSeparator,
10 | DropdownMenuTrigger,
11 | } from "/components/ui/dropdown-menu"
12 | `;
13 |
14 | export const usageDocs = `
15 |
16 | Open
17 |
18 | My Account
19 |
20 | Profile
21 | Billing
22 | Team
23 | Subscription
24 |
25 |
26 | `;
27 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/index.ts:
--------------------------------------------------------------------------------
1 | import * as Accordion from "./accordion";
2 | import * as Alert from "./alert";
3 | import * as AlertDialog from "./alert-dialog";
4 | import * as Avatar from "./avatar";
5 | import * as Badge from "./badge";
6 | import * as Breadcrumb from "./breadcrumb";
7 | import * as Button from "./button";
8 | import * as Calendar from "./calendar";
9 | import * as Card from "./card";
10 | import * as Carousel from "./carousel";
11 | import * as Checkbox from "./checkbox";
12 | import * as Dialog from "./dialog";
13 | import * as DropdownMenu from "./dropdown-menu";
14 | import * as Input from "./input";
15 | import * as Label from "./label";
16 | import * as Menubar from "./menubar";
17 | import * as NavigationMenu from "./navigation-menu";
18 | import * as Pagination from "./pagination";
19 | import * as Popover from "./popover";
20 | import * as Progress from "./progress";
21 | import * as RadioGroup from "./radio-group";
22 | import * as Resizable from "./resizable";
23 | import * as ScrollArea from "./scroll-area";
24 | import * as Select from "./select";
25 | import * as Slider from "./slider";
26 | import * as Switch from "./switch";
27 | import * as Table from "./table";
28 | import * as Tabs from "./tabs";
29 | import * as Textarea from "./textarea";
30 | import * as Toggle from "./toggle";
31 | import * as ToggleGroup from "./toggle-group";
32 | import * as Tooltip from "./tooltip";
33 | const shadcnDocs = [
34 | Avatar,
35 | Button,
36 | Card,
37 | Checkbox,
38 | Input,
39 | Label,
40 | RadioGroup,
41 | Select,
42 | Textarea,
43 | ScrollArea,
44 | Accordion,
45 | Alert,
46 | AlertDialog,
47 | Badge,
48 | Breadcrumb,
49 | Calendar,
50 | Carousel,
51 | Dialog,
52 | DropdownMenu,
53 | Menubar,
54 | NavigationMenu,
55 | Pagination,
56 | Popover,
57 | Progress,
58 | Resizable,
59 | Slider,
60 | Switch,
61 | Table,
62 | Tabs,
63 | Toggle,
64 | ToggleGroup,
65 | Tooltip,
66 | ];
67 |
68 | export default shadcnDocs;
69 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/input.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Input";
2 |
3 | export const importDocs = `
4 | import { Input } from "/components/ui/input"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/label.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Label";
2 |
3 | export const importDocs = `
4 | import { Label } from "/components/ui/label"
5 | `;
6 |
7 | export const usageDocs = `
8 | Your email address
9 | `;
10 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/menubar.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Menubar";
2 |
3 | export const importDocs = `
4 | import {
5 | Menubar,
6 | MenubarContent,
7 | MenubarItem,
8 | MenubarMenu,
9 | MenubarSeparator,
10 | MenubarShortcut,
11 | MenubarTrigger,
12 | } from "/components/ui/menubar"
13 | `;
14 |
15 | export const usageDocs = `
16 |
17 |
18 | File
19 |
20 |
21 | New Tab ⌘T
22 |
23 | New Window
24 |
25 | Share
26 |
27 | Print
28 |
29 |
30 |
31 | `;
32 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | export const name = "NavigationMenu";
2 |
3 | export const importDocs = `
4 | import {
5 | NavigationMenu,
6 | NavigationMenuContent,
7 | NavigationMenuIndicator,
8 | NavigationMenuItem,
9 | NavigationMenuLink,
10 | NavigationMenuList,
11 | NavigationMenuTrigger,
12 | NavigationMenuViewport,
13 | } from "/components/ui/navigation-menu"
14 | `;
15 |
16 | export const usageDocs = `
17 |
18 |
19 |
20 | Item One
21 |
22 | Link
23 |
24 |
25 |
26 |
27 | `;
28 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/pagination.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Pagination";
2 |
3 | export const importDocs = `
4 | import {
5 | Pagination,
6 | PaginationContent,
7 | PaginationEllipsis,
8 | PaginationItem,
9 | PaginationLink,
10 | PaginationNext,
11 | PaginationPrevious,
12 | } from "/components/ui/pagination"
13 | `;
14 |
15 | export const usageDocs = `
16 |
17 |
18 |
19 |
20 |
21 |
22 | 1
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | `;
33 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/popover.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Popover";
2 |
3 | export const importDocs = `
4 | import {
5 | Popover,
6 | PopoverContent,
7 | PopoverTrigger,
8 | } from "/components/ui/popover"
9 | `;
10 |
11 | export const usageDocs = `
12 |
13 | Open
14 | Place content for the popover here.
15 |
16 | `;
17 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/progress.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Progress";
2 |
3 | export const importDocs = `
4 | import { Progress } from "/components/ui/progress"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/radio-group.tsx:
--------------------------------------------------------------------------------
1 | export const name = "RadioGroup";
2 |
3 | export const importDocs = `
4 | import { Label } from "/components/ui/label"
5 | import { RadioGroup, RadioGroupItem } from "/components/ui/radio-group"
6 | `;
7 |
8 | export const usageDocs = `
9 |
10 |
11 |
12 | Option One
13 |
14 |
15 |
16 | Option Two
17 |
18 |
19 | `;
20 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/resizable.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Resizable";
2 |
3 | export const importDocs = `
4 | import {
5 | ResizableHandle,
6 | ResizablePanel,
7 | ResizablePanelGroup,
8 | } from "/components/ui/resizable"
9 | `;
10 |
11 | export const usageDocs = `
12 |
13 | One
14 |
15 | Two
16 |
17 | `;
18 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | export const name = "ScrollArea";
2 |
3 | export const importDocs = `
4 | import { ScrollArea } from "/components/ui/scroll-area"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 | Jokester began sneaking into the castle in the middle of the night and leaving
10 | jokes all over the place: under the king's pillow, in his soup, even in the
11 | royal toilet. The king was furious, but he couldn't seem to stop Jokester. And
12 | then, one day, the people of the kingdom discovered that the jokes left by
13 | Jokester were so funny that they couldn't help but laugh. And once they
14 | started laughing, they couldn't stop.
15 |
16 | `;
17 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/select.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Select";
2 |
3 | export const importDocs = `
4 | import {
5 | Select,
6 | SelectContent,
7 | SelectItem,
8 | SelectTrigger,
9 | SelectValue,
10 | } from "/components/ui/select"
11 | `;
12 |
13 | export const usageDocs = `
14 |
15 |
16 |
17 |
18 |
19 | Light
20 | Dark
21 | System
22 |
23 |
24 | `;
25 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/slider.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Slider";
2 |
3 | export const importDocs = `
4 | import { Slider } from "/components/ui/slider"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/switch.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Switch";
2 |
3 | export const importDocs = `
4 | import { Switch } from "/components/ui/switch"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/table.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Table";
2 |
3 | export const importDocs = `
4 | import {
5 | Table,
6 | TableBody,
7 | TableCaption,
8 | TableCell,
9 | TableHead,
10 | TableHeader,
11 | TableRow,
12 | } from "/components/ui/table"
13 | `;
14 |
15 | export const usageDocs = `
16 |
17 | A list of your recent invoices.
18 |
19 |
20 | Invoice
21 | Status
22 | Method
23 | Amount
24 |
25 |
26 |
27 |
28 | INV001
29 | Paid
30 | Credit Card
31 | $250.00
32 |
33 |
34 |
35 | `;
36 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/tabs.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Tabs";
2 |
3 | export const importDocs = `
4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "/components/ui/tabs"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 |
10 | Account
11 | Password
12 |
13 | Make changes to your account here.
14 | Change your password here.
15 |
16 | `;
17 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/textarea.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Textarea";
2 |
3 | export const importDocs = `
4 | import { Textarea } from "/components/ui/textarea"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | export const name = "ToggleGroup";
2 |
3 | export const importDocs = `
4 | import { ToggleGroup, ToggleGroupItem } from "/components/ui/toggle-group"
5 | `;
6 |
7 | export const usageDocs = `
8 |
9 | A
10 | B
11 | C
12 |
13 | `;
14 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/toggle.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Toggle";
2 |
3 | export const importDocs = `
4 | import { Toggle } from "/components/ui/toggle"
5 | `;
6 |
7 | export const usageDocs = `
8 | Toggle
9 | `;
10 |
--------------------------------------------------------------------------------
/lib/shadcn-docs/tooltip.tsx:
--------------------------------------------------------------------------------
1 | export const name = "Toggle";
2 |
3 | export const importDocs = `
4 | import {
5 | Tooltip,
6 | TooltipContent,
7 | TooltipProvider,
8 | TooltipTrigger,
9 | } from "/components/ui/tooltip"
10 | `;
11 |
12 | export const usageDocs = `
13 |
14 |
15 | Hover
16 |
17 | Add to library
18 |
19 |
20 |
21 | `;
22 |
--------------------------------------------------------------------------------
/lib/stream.ts:
--------------------------------------------------------------------------------
1 | export async function* readStream(response: ReadableStream) {
2 | let reader = response.pipeThrough(new TextDecoderStream()).getReader()
3 | let done = false
4 |
5 | while (!done) {
6 | let { value, done: streamDone } = await reader.read();
7 | done = streamDone;
8 |
9 | if (value) yield value;
10 | }
11 |
12 | reader.releaseLock();
13 | }
14 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import {
2 | cookieName,
3 | fallbackLng,
4 | languages,
5 | searchParamName,
6 | } from '@/app/i18n/settings'
7 | import acceptLanguage from 'accept-language'
8 | import { NextRequest, NextResponse } from 'next/server'
9 |
10 | acceptLanguage.languages(languages)
11 |
12 | export const config = {
13 | matcher: [
14 | '/((?!api|_next/static|_next|_next/image|assets|favicon.ico|icon|chrome|sw.js|site.webmanifest|.*.(?:png|jpg|jpeg)).*)',
15 | ],
16 | }
17 |
18 | export function middleware(req: NextRequest) {
19 | let lng: string | undefined | null
20 | let searchLng: string | undefined | null = undefined
21 | let pathLng: string | undefined | null = undefined
22 | // 1 Get language from query params
23 | if (req.nextUrl.searchParams.has(searchParamName))
24 | searchLng = acceptLanguage.get(
25 | req.nextUrl.searchParams.get(searchParamName)
26 | )
27 | // 2 Get language from cookies
28 | if (req.cookies.has(cookieName))
29 | lng = acceptLanguage.get(req.cookies.get(cookieName)?.value)
30 | // 3 Get language from headers
31 | if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'))
32 | // 4 Default language
33 | if (!lng) lng = fallbackLng
34 |
35 | // Remove search param if it exists
36 | if (searchLng) {
37 | req.nextUrl.searchParams.delete(searchParamName)
38 | }
39 |
40 | // Get language from path
41 | pathLng = languages.find((loc) => req.nextUrl.pathname.startsWith(`/${loc}`))
42 |
43 | // Redirect to path prefixed with language if query param exists
44 | // Or if it doesn't exist and path is not prefixed with language
45 | if (
46 | // 1 No path prefixed with query param
47 | (searchLng && !req.nextUrl.pathname.startsWith(`/${searchLng}`)) ||
48 | // 2 No path prefixed with language
49 | !pathLng
50 | ) {
51 | if (searchLng) {
52 | lng = searchLng
53 | req.nextUrl.pathname =
54 | req.nextUrl.pathname.replace(`/${pathLng}`, '') || '/'
55 | }
56 | const url = req.nextUrl.clone()
57 | url.pathname = `/${lng}${url.pathname}`
58 | return NextResponse.redirect(url, {
59 | headers: {
60 | 'Set-Cookie': `${cookieName}=${lng}; path=/; Max-Age=2147483647`,
61 | },
62 | })
63 | }
64 |
65 | if (req.headers.has('referer')) {
66 | const refererUrl = new URL(req.headers.get('referer') || '')
67 | const lngInReferer = languages.find((l) =>
68 | refererUrl.pathname.startsWith(`/${l}`)
69 | )
70 | const response = NextResponse.next()
71 | if (lngInReferer) response.cookies.set(cookieName, lngInReferer)
72 | return response
73 | }
74 |
75 | return NextResponse.next()
76 | }
77 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: false,
4 | experimental: {
5 | serverComponentsExternalPackages: ['pino', 'pino-pretty'],
6 | },
7 | output: 'standalone',
8 | images: {
9 | remotePatterns: [
10 | {
11 | protocol: 'https',
12 | hostname: 'file.302.ai',
13 | },
14 | {
15 | protocol: 'https',
16 | hostname: 'file.302ai.cn',
17 | },
18 | ],
19 | },
20 | }
21 |
22 | export default nextConfig
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "intj",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "format": "prettier --check --ignore-path .gitignore .",
11 | "format:fix": "prettier --write --ignore-path .gitignore ."
12 | },
13 | "dependencies": {
14 | "@ai-sdk/openai": "^0.0.62",
15 | "@codemirror/state": "^6.4.1",
16 | "@codemirror/view": "^6.34.1",
17 | "@codesandbox/sandpack-react": "^2.19.8",
18 | "@radix-ui/react-checkbox": "^1.1.1",
19 | "@radix-ui/react-dialog": "^1.1.1",
20 | "@radix-ui/react-dropdown-menu": "^2.1.1",
21 | "@radix-ui/react-icons": "^1.3.0",
22 | "@radix-ui/react-label": "^2.1.0",
23 | "@radix-ui/react-popover": "^1.1.1",
24 | "@radix-ui/react-radio-group": "^1.2.1",
25 | "@radix-ui/react-slot": "^1.1.0",
26 | "@radix-ui/react-switch": "^1.1.0",
27 | "@radix-ui/react-tooltip": "^1.1.3",
28 | "@tailwindcss/typography": "^0.5.15",
29 | "@uiw/codemirror-extensions-langs": "^4.23.3",
30 | "@uiw/react-codemirror": "^4.23.3",
31 | "accept-language": "^3.0.20",
32 | "ahooks": "^3.8.1",
33 | "ai": "^3.4.5",
34 | "class-variance-authority": "^0.7.0",
35 | "clsx": "^2.1.1",
36 | "dedent": "^1.5.3",
37 | "eventsource-parser": "^2.0.1",
38 | "framer-motion": "^11.5.6",
39 | "i18next": "^23.14.0",
40 | "i18next-browser-languagedetector": "^8.0.0",
41 | "i18next-resources-to-backend": "^1.2.1",
42 | "immer": "^10.1.1",
43 | "iso-639-1": "^3.1.3",
44 | "ky": "^1.7.1",
45 | "lucide-react": "^0.437.0",
46 | "mitt": "^3.0.1",
47 | "nanoid": "^5.0.7",
48 | "next": "14.2.7",
49 | "next-runtime-env": "^3.2.2",
50 | "next-themes": "^0.3.0",
51 | "openai": "^4.63.0",
52 | "pino": "^9.3.2",
53 | "pino-pretty": "^11.2.2",
54 | "radash": "^12.1.0",
55 | "react": "^18",
56 | "react-cookie": "^7.2.0",
57 | "react-dom": "^18",
58 | "react-hot-toast": "^2.4.1",
59 | "react-i18next": "^15.0.1",
60 | "tailwind-merge": "^2.5.2",
61 | "tailwindcss-animate": "^1.0.7",
62 | "zod": "^3.23.8",
63 | "zustand": "^4.5.5"
64 | },
65 | "devDependencies": {
66 | "@types/node": "^20",
67 | "@types/react": "^18",
68 | "@types/react-dom": "^18",
69 | "eslint": "^8",
70 | "eslint-config-next": "14.2.7",
71 | "eslint-config-prettier": "^9.1.0",
72 | "postcss": "^8",
73 | "prettier-plugin-tailwindcss": "^0.6.6",
74 | "tailwindcss": "^3.4.1",
75 | "typescript": "^5"
76 | },
77 | "packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903"
78 | }
79 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | }
7 |
8 | export default config
9 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/desc_en.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/public/images/desc_en.png
--------------------------------------------------------------------------------
/public/images/desc_ja.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/public/images/desc_ja.png
--------------------------------------------------------------------------------
/public/images/desc_zh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/public/images/desc_zh.png
--------------------------------------------------------------------------------
/public/images/logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/public/images/logo-dark.png
--------------------------------------------------------------------------------
/public/images/logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/302ai/302_coder_generator/cfb63e7a9c0fe13f2be1b85ce1e6957ae3c57866/public/images/logo-light.png
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 | import type { PluginAPI } from 'tailwindcss/types/config'
3 |
4 | const config: Config = {
5 | darkMode: ['class'],
6 | content: [
7 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
8 | './components/**/*.{js,ts,jsx,tsx,mdx}',
9 | './app/**/*.{js,ts,jsx,tsx,mdx}',
10 | ],
11 | theme: {
12 | extend: {
13 | backgroundImage: {
14 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
15 | 'gradient-conic':
16 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
17 | },
18 | borderRadius: {
19 | lg: 'var(--radius)',
20 | md: 'calc(var(--radius) - 2px)',
21 | sm: 'calc(var(--radius) - 4px)',
22 | },
23 | colors: {
24 | background: 'hsl(var(--background))',
25 | foreground: 'hsl(var(--foreground))',
26 | card: {
27 | DEFAULT: 'hsl(var(--card))',
28 | foreground: 'hsl(var(--card-foreground))',
29 | },
30 | popover: {
31 | DEFAULT: 'hsl(var(--popover))',
32 | foreground: 'hsl(var(--popover-foreground))',
33 | },
34 | primary: {
35 | DEFAULT: 'hsl(var(--primary))',
36 | foreground: 'hsl(var(--primary-foreground))',
37 | },
38 | secondary: {
39 | DEFAULT: 'hsl(var(--secondary))',
40 | foreground: 'hsl(var(--secondary-foreground))',
41 | },
42 | muted: {
43 | DEFAULT: 'hsl(var(--muted))',
44 | foreground: 'hsl(var(--muted-foreground))',
45 | },
46 | accent: {
47 | DEFAULT: 'hsl(var(--accent))',
48 | foreground: 'hsl(var(--accent-foreground))',
49 | },
50 | destructive: {
51 | DEFAULT: 'hsl(var(--destructive))',
52 | foreground: 'hsl(var(--destructive-foreground))',
53 | },
54 | border: 'hsl(var(--border))',
55 | input: 'hsl(var(--input))',
56 | ring: 'hsl(var(--ring))',
57 | chart: {
58 | '1': 'hsl(var(--chart-1))',
59 | '2': 'hsl(var(--chart-2))',
60 | '3': 'hsl(var(--chart-3))',
61 | '4': 'hsl(var(--chart-4))',
62 | '5': 'hsl(var(--chart-5))',
63 | },
64 | },
65 | },
66 | },
67 | plugins: [
68 | require('tailwindcss-animate'),
69 | require('@tailwindcss/typography'),
70 | ({ addBase, theme }: PluginAPI) => {
71 | addBase({
72 | '::-webkit-scrollbar': {
73 | width: '4px',
74 | height: '4px',
75 | },
76 | '::-webkit-scrollbar-track': {
77 | backgroundColor: theme('colors.gray.100', '#f1f1f1'),
78 | borderRadius: '4px',
79 | width: '4px',
80 | },
81 | '::-webkit-scrollbar-thumb': {
82 | backgroundColor: theme('colors.gray.300', '#888'),
83 | borderRadius: '4px',
84 | },
85 | '::-webkit-scrollbar-thumb:hover': {
86 | backgroundColor: theme('colors.gray.400', '#555'),
87 | },
88 | /* Dark 模式滚动条样式 */
89 | '.dark ::-webkit-scrollbar': {
90 | width: '4px',
91 | height: '4px',
92 | },
93 | '.dark ::-webkit-scrollbar-track': {
94 | backgroundColor: theme('colors.gray.800', '#2d2d2d'),
95 | borderRadius: '4px',
96 | width: '4px',
97 | },
98 | '.dark ::-webkit-scrollbar-thumb': {
99 | backgroundColor: theme('colors.gray.600', '#555'),
100 | borderRadius: '4px',
101 | },
102 | '.dark ::-webkit-scrollbar-thumb:hover': {
103 | backgroundColor: theme('colors.gray.700', '#333'),
104 | },
105 | })
106 | },
107 | ],
108 | }
109 | export default config
110 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------