├── .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 | ![Interface Preview](docs/AI网页生成器en.png) 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 | ![Interface Preview](docs/网页生成2.png) 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 | ![インターフェースプレビュー](docs/AI网页生成器jp.png) 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 | ![インターフェースプレビュー](docs/网页生成3.png) 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 | ![界面预览](docs/AI网页生成器.png) 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 | ![界面预览](docs/网页生成1.png) 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 | 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 | 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 | ai-302 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 |
143 |
144 | {showBrand && } 145 |

146 | {t('home:header.title')} 147 |

148 |
149 |
150 |
151 | 152 | 153 | 160 | 161 | 162 |
163 |
164 | 170 | setIsUseShadcnUi(!isUseShadcnUi)} 174 | /> 175 |
176 |
177 | 183 | setAutoScroll(!autoScroll)} 187 | /> 188 |
189 |
190 | 196 | setShowPreview(!showPreview)} 200 | /> 201 |
202 |
203 | 209 | 213 | setShowFileExplorer(!showFileExplorer) 214 | } 215 | /> 216 |
217 |
218 |
219 |
220 | 221 |
222 | setPrompt(e.target.value)} 229 | onKeyDown={(e) => { 230 | if (e.key === 'Enter') { 231 | handleSubmit() 232 | } 233 | }} 234 | /> 235 | {/* optimize prompt */} 236 | 237 | 238 | 250 | 251 | 252 |

{t('home:header.optimize_prompt')}

253 |
254 |
255 |
256 | {/* image */} 257 | {image && ( 258 |
259 | image 266 | 273 |
274 | )} 275 | {/* image upload button */} 276 | {isSupportVision && ( 277 | <> 278 | 290 | 294 | 295 | 296 | 297 | {t('home:header.upload_image_reference')}: 298 | 299 | 300 | 301 | 306 | setImageStyle(value as ImageStyle) 307 | } 308 | > 309 |
310 |
311 | 312 | 315 |
316 |

317 | {t('home:header.image_style_description')} 318 |

319 |
320 |
321 |
322 | 323 | 326 |
327 |

328 | {t('home:header.image_content_description')} 329 |

330 |
331 |
332 |
333 | 334 | 337 |
338 |

339 | {t('home:header.both_description')} 340 |

341 |
342 |
343 | 344 | 353 |
354 |
355 | 356 | )} 357 | 369 |
370 |
371 |
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 | 151 | 152 | 153 |

{t('home:header.optimize_prompt')}

154 |
155 |
156 | {referenceText && ( 157 |

158 | {referenceText} 159 | 166 |

167 | )} 168 |
169 | {/* image */} 170 | {imageForUpdate && ( 171 |
172 | image 179 | 186 |
187 | )} 188 | {/* image upload button */} 189 | {isSupportVision && ( 190 | <> 191 | 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 | 229 |
230 |

231 | {t('home:header.image_style_description')} 232 |

233 |
234 |
235 |
236 | 237 | 240 |
241 |

242 | {t('home:header.image_content_description')} 243 |

244 |
245 |
246 |
247 | 248 | 249 |
250 |

251 | {t('home:header.both_description')} 252 |

253 |
254 |
255 | 256 | 265 |
266 |
267 | 268 | )} 269 | 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 | 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 | 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 | 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 | 9 | 10 | 11 | 12 | 13 | 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 | 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 | 13 |
14 |
15 | 16 | 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 | 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 |