refactor: 主题系统 → monorepo 五个独立网站

- packages/core: @aidir/core 底座库(src/pages 页面组件体 1:1 镜像路由树 / kit 站点外壳工厂 / content/blog / scripts)
- sites/: 5 个独立 Next.js 网站(ai-stack / launchai / the-ai-ledger / devai-tools / findai)
  - 每站独立 package.json / next.config / tailwind / messages / .env / 构建
  - src/app 约 56 个薄路由文件(re-export 底座页面 + 内联路由段配置 revalidate/dynamic)
  - src/site/brand.tsx 站点配置 + src/site/home.tsx 站点首页(createMarketingLayout / createHomePage 工厂)
- generate-site.mjs: 站点生成器(新增网站 = SITES 加条目,零 Core 修改)
- start-all-sites.sh: 一键启动 3001-3005
- next-config.mjs: 底座统一 Next 配置工厂(next-intl + MDX + standalone)
- 五站共享同一套 PostgreSQL / Better Auth / Admin(设计文档 §42)
- 验证: 5 站 build OK / typecheck OK / 55 路由冒烟 200 / 品牌-内容-截图 差异确认 / 登录-投票-重复检测 e2e PASS
This commit is contained in:
AI Directory Platform
2026-09-08 19:19:33 +00:00
parent e46b4b52a3
commit 4a1a034386
699 changed files with 19829 additions and 2029 deletions
+12
View File
@@ -0,0 +1,12 @@
node_modules
.next
.git
.env
.env.*
!.env.example
Dockerfile
docker-compose.yml
*.md
.claude
.vscode
.idea
+109
View File
@@ -0,0 +1,109 @@
# ====================================
# Database Configuration
# ====================================
# PostgreSQL connection string
# Format: postgresql://username:password@host:port/database?sslmode=require
DATABASE_URL="postgresql://username:password@host/database?sslmode=require"
# ====================================
# Authentication (Better Auth)
# ====================================
# Secret key for Better Auth (must be at least 32 characters in production)
# Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET="your-super-secret-key-at-least-32-characters-long-for-production"
# The URL where your app is hosted
BETTER_AUTH_URL="http://localhost:3000"
# ====================================
# Google OAuth (Optional)
# ====================================
# Get these from: https://console.cloud.google.com/
# Guide: https://authjs.dev/getting-started/providers/google
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
# Public Google OAuth settings
NEXT_PUBLIC_AUTH_GOOGLE_ENABLED="true"
NEXT_PUBLIC_AUTH_GOOGLE_ONE_TAP_ENABLED="true"
# ====================================
# Email Service (Required for email features)
# ====================================
# Resend API for transactional emails
# Get your API key from: https://resend.com
RESEND_API_KEY="re_your_api_key"
# Email Configuration - Choose one of the following options:
#
# Option A (recommended, highest priority): set a complete from email address
RESEND_FROM_EMAIL="Your App <noreply@yourdomain.com>"
# Option B: set a verified domain (production requires A or B),
# from address becomes "{RESEND_FROM_NAME} <noreply@{RESEND_VERIFIED_DOMAIN}>"
RESEND_VERIFIED_DOMAIN=""
RESEND_FROM_NAME=""
# Optional: Resend Audience ID for newsletter/marketing
RESEND_AUDIENCE_ID="your-resend-audience-id"
# Optional: where contact form notifications are sent (admin inbox)
CONTACT_RECEIVER_EMAIL=""
# ====================================
# Analytics & Monitoring (Optional)
# ====================================
# Posthog Analytics
NEXT_PUBLIC_POSTHOG_KEY=""
NEXT_PUBLIC_POSTHOG_HOST=""
# Google Analytics
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=""
# Microsoft Clarity
NEXT_PUBLIC_CLARITY_PROJECT_ID=""
# Analytics Feature Flags(可选)
NEXT_PUBLIC_ANALYTICS_ENABLE_IN_DEVELOPMENT=""
# ====================================
# Storage Configuration (Optional - for file uploads)
# ====================================
# R2/S3-compatible storage for file uploads
# Works with Cloudflare R2, AWS S3, MinIO, etc.
STORAGE_ACCESS_KEY_ID=""
STORAGE_SECRET_ACCESS_KEY=""
STORAGE_ENDPOINT=""
STORAGE_BUCKET_NAME="uploads"
STORAGE_PUBLIC_URL=""
# ====================================
# Application Settings
# ====================================
# Public app URL (update in production)
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# App name used in email sender and titles (optional)
NEXT_PUBLIC_APP_NAME="Toolso.AI"
# Node environment
NODE_ENV="development"
# ====================================
# Feature Flags (Optional)
# ====================================
# Enable/disable features
NEXT_PUBLIC_MAINTENANCE_MODE="false"
# ====================================
# AI Directory Platform 扩展配置
# ====================================
# GitHub OAuth(配置后登录页自动显示 GitHub 按钮)
AUTH_GITHUB_ID=""
AUTH_GITHUB_SECRET=""
NEXT_PUBLIC_AUTH_GITHUB_ENABLED="false"
# 站点主题(构建时生效):classic | product | editorial | niche | minimal
# 多主题构建:SITE_THEME=product NEXT_DIST_DIR=.next-product bun run build
# 多主题启动:SITE_THEME=product NEXT_DIST_DIR=.next-product next start -p 3002
SITE_THEME="classic"
+32
View File
@@ -0,0 +1,32 @@
# 自动检测文本文件并标准化行尾符
* text=auto
# 强制使用 LF 行尾符的文件类型 (防止 Windows CRLF 问题)
*.js text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.jsx text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
*.json text eol=lf
*.md text eol=lf
*.mdx text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.html text eol=lf
*.sh text eol=lf
# 二进制文件
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.pdf binary
+66
View File
@@ -0,0 +1,66 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Editor directories
.vscode/
.idea/
*.swp
*.swo
*~
.env
# local documentation
.local-docs/
# scripts
/scripts/manage-users.ts
# 多主题构建产物
/.next-product
/.next-editorial
/.next-niche
/.next-minimal
+272
View File
@@ -0,0 +1,272 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概览
这是一个**开源的 AI 工具导航站 (Toolso.AI)**,帮助用户发现和探索各类优秀的 AI 工具。基于 Next.js 16 构建,拥有现代化的响应式界面、强大的搜索和筛选功能、多语言支持。
**核心特性**:
- AI 工具目录 (分类展示、搜索筛选、定价筛选、工具详情、分类落地页)
- 用户收藏 (工具卡片/详情页收藏,仪表盘查看)
- 管理后台 (工具 CRUD、用户管理、分类/标签管理)
- 博客系统 (MDX 驱动,AI 资讯和教程)
- Newsletter (页脚订阅 + Resend Audience 同步 + 退订)
- 国际化支持 (中文/英文,`pnpm i18n:check` 校验键一致性)
- 用户认证 (Better Auth + Google OAuth)
## 技术栈
### 前端
- **框架**: Next.js 16 (App Router) + React 19
- **样式**: Tailwind CSS + Framer Motion 动画
- **UI 组件**: 自定义组件库 + Radix UI
- **表单**: React Hook Form + Zod 验证
- **主题**: next-themes (深色/浅色模式)
- **国际化**: next-intl (动态路由 `[locale]`)
### 后端
- **身份认证**: Better Auth (邮箱密码 + Google OAuth)
- **数据库**: PostgreSQL + Drizzle ORM
- **邮件**: Resend (验证邮件 + Newsletter)
- **存储**: Cloudflare R2 / AWS S3 兼容 (可选)
### 开发工具
- **类型安全**: TypeScript (strict mode)
- **代码规范**: ESLint
- **包管理**: pnpm
## 项目结构
```
├── src/ # 源代码目录
│ ├── app/[locale]/ # 国际化路由
│ │ ├── (auth)/ # 认证页面 (登录、注册、忘记密码)
│ │ ├── (marketing)/ # 营销页面 (首页、博客、联系、工具目录)
│ │ ├── (protected)/ # 需要认证的页面 (仪表板、个人资料)
│ │ ├── (admin)/ # 管理后台 (用户管理、工具管理)
│ │ ├── check-email/ # 邮箱验证提示页
│ │ └── verify-email/ # 邮箱验证处理页
│ ├── app/api/ # API 路由
│ │ ├── auth/ # Better Auth + 自定义认证端点
│ │ ├── admin/ # 管理员操作 API
│ │ ├── user/ # 用户信息 API
│ │ └── upload/ # 文件上传 API
│ ├── components/ # 可重用 UI 组件
│ ├── features/ # 功能模块
│ │ ├── auth/ # 认证组件和逻辑
│ │ ├── admin/ # 管理后台组件
│ │ ├── navigation/ # 导航组件
│ │ ├── tools/ # AI 工具目录模块
│ │ └── forms/ # 表单组件
│ ├── lib/ # 核心业务逻辑
│ │ ├── auth.ts # Better Auth 配置
│ │ ├── auth-client.ts # 前端认证客户端
│ │ ├── db/ # 数据库连接和 Schema
│ │ ├── email.ts # 邮件发送逻辑
│ │ ├── blog.ts # 博客系统逻辑
│ │ ├── r2-storage.ts # R2/S3 存储
│ │ ├── i18n.ts # 国际化配置
│ │ └── metadata.ts # SEO 元数据生成
│ ├── constants/ # 常量定义
│ │ └── website.ts # 网站基础信息
│ ├── context/ # React Context
│ ├── emails/ # 邮件模板 (React Email)
│ ├── layouts/ # 布局组件
│ └── drizzle/ # 数据库迁移文件
├── messages/ # 国际化翻译文件
│ ├── en.json # 英文 (含 SEO)
│ └── zh.json # 中文 (含 SEO)
├── public/ # 静态资源
└── scripts/ # 构建和管理脚本
```
## 数据库架构
当前包含 12 个表:
```sql
-- 认证模块 (src/lib/db/schema/auth.ts)
user (id, name, email, emailVerified, image, role, banned, banReason, banExpires, ...)
session (id, expiresAt, token, ipAddress, userAgent, userId, ...)
account (id, accountId, providerId, userId, accessToken, refreshToken, password, ...)
verification (id, identifier, value, expiresAt, ...) -- 邮箱验证 + 密码重置令牌均存于此
-- 工具模块 (src/lib/db/schema/tool.ts)
tool (id, slug, domain, websiteUrl, coverImageUrl, logoUrl, nameEn/Zh, descriptionEn/Zh,
pricing, featured, viewCount, status, ...)
-- status: draft | published
-- pricing: free | freemium | paid
category (id, slug, icon, sortOrder, nameEn/Zh, descriptionEn/Zh, ...)
tag (id, slug, nameEn/Zh, ...)
tool_category (toolId, categoryId) -- 多对多
tool_tag (toolId, tagId) -- 多对多
favorite (userId, toolId, createdAt) -- 用户收藏,联合主键
-- 营销模块 (src/lib/db/schema/marketing.ts)
newsletter_subscription (id, email, userId, status, unsubscribeToken, subscribedAt, ...)
contact_message (id, name, email, company, message, createdAt)
```
Schema 汇出入口: `src/lib/db/schema.ts`
## 常用开发命令
```bash
# 启动开发服务器
pnpm dev
# 构建生产版本
pnpm build
# 启动生产服务器
pnpm start
# 代码检查
pnpm lint # ESLint 9 (flat config)
pnpm typecheck # tsc --noEmit
pnpm i18n:check # 翻译键一致性检查
# 数据库操作
pnpm db:generate # 生成 Drizzle 迁移文件
pnpm db:migrate # 执行数据库迁移
pnpm db:push # 推送 schema 到数据库 (开发环境)
pnpm db:studio # 启动 Drizzle Studio 数据库管理界面
# 管理员工具
pnpm admin:setup # 创建管理员账户
pnpm seed:tools # 导入工具种子数据(50 工具/10 分类/30 标签)
# 博客清单生成
pnpm generate:blog-manifest
```
## 环境变量配置
**必需的环境变量** (参考 `.env.example`):
```env
# 数据库
DATABASE_URL="postgresql://user:password@host/db?sslmode=require"
# Better Auth
BETTER_AUTH_SECRET="至少32字符的随机密钥"
BETTER_AUTH_URL="http://localhost:3000"
# Resend 邮件
RESEND_API_KEY="re_your_api_key"
RESEND_FROM_EMAIL="Your App <noreply@yourdomain.com>"
# 应用 URL
NEXT_PUBLIC_APP_URL="http://localhost:3000"
```
**可选的环境变量**:
```env
# Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
# R2/S3 存储
STORAGE_ACCESS_KEY_ID="..."
STORAGE_SECRET_ACCESS_KEY="..."
STORAGE_ENDPOINT="..."
STORAGE_BUCKET_NAME="..."
STORAGE_PUBLIC_URL="..."
# 分析工具
NEXT_PUBLIC_POSTHOG_KEY="..."
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID="..."
NEXT_PUBLIC_CLARITY_PROJECT_ID="..."
```
## 国际化 (i18n)
- 框架: `next-intl`
- 支持语言: 英文 (`en`), 中文 (`zh`)
- 翻译文件: `messages/en.json`, `messages/zh.json` (含 UI 文本和 SEO 配置)
- 路由格式: `/en/...`, `/zh/...`
**添加新语言**:
1. 复制 `messages/en.json` 到 `messages/新语言代码.json`
2. 翻译 JSON 文件内容
3. 更新 `i18n.config.ts` 添加新语言代码
## 路由权限控制
- **公开路由**: `(marketing)`, `(auth)`
- **受保护路由**: `(protected)` - 需要登录,通过 `SessionGuard` 组件保护
- **管理员路由**: `(admin)` - 需要 `role='admin'`,通过 `AdminGuard` 组件保护
## 认证流程
### 注册流程
1. 用户填写注册表单
2. 创建用户和账户记录
3. 发送验证邮件
4. 用户点击链接验证邮箱
### 密码重置流程
1. 用户输入邮箱
2. 生成重置令牌存入 `verification` 表(identifier 带 reset 前缀)
3. 发送重置邮件
4. 用户点击链接设置新密码
## 管理后台
管理员权限通过 `user.role = 'admin'` 标识。
**功能**:
- 用户列表和搜索、查看详情、修改角色、禁用/解禁
- 工具 CRUD、上下架、精选标记
- 分类与标签管理
**创建管理员账户**:
```bash
pnpm admin:setup
# 或手动在数据库中将 user.role 设为 'admin'
```
## 部署清单
1. **环境准备**
- PostgreSQL 数据库 (推荐 Supabase/Neon/Vercel Postgres)
- Resend API Key (https://resend.com)
- (可选) Google OAuth 凭据
- (可选) R2/S3 存储配置
2. **数据库初始化**
```bash
pnpm db:push
```
3. **创建管理员账户**
```bash
pnpm admin:setup
```
4. **环境变量检查**
确保 `BETTER_AUTH_URL`, `NEXT_PUBLIC_APP_URL`, `RESEND_FROM_EMAIL` 使用正确的域名。
## 扩展指南
### 添加新功能模块
1. 在 `src/features/` 下创建新目录
2. 包含 `components/`, `actions/`, `schemas.ts` 等
3. 在 `src/app/` 下创建对应路由
### 添加新数据库表
1. 在 `src/lib/db/schema.ts` 定义新表
2. 运行 `pnpm db:generate` 生成迁移
3. 运行 `pnpm db:push` 或 `pnpm db:migrate` 应用更改
### 自定义邮件模板
编辑 `src/emails/` 目录下的 React Email 组件
## 相关文档
- [Better Auth 文档](https://better-auth.com/)
- [Drizzle ORM 文档](https://orm.drizzle.team/)
- [Next.js 文档](https://nextjs.org/docs)
- [next-intl 文档](https://next-intl-docs.vercel.app/)
- [Resend 文档](https://resend.com/docs)
+38
View File
@@ -0,0 +1,38 @@
# syntax=docker/dockerfile:1
# ==================== 依赖安装 ====================
FROM node:22-alpine AS deps
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# ==================== 构建 ====================
FROM node:22-alpine AS builder
RUN corepack enable
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
# 构建期占位变量:镜像构建阶段连不上数据库时,
# 动态页面会自动回退为运行时按需生成(ISR),真实值在运行时注入
ENV DATABASE_URL="postgresql://build:build@127.0.0.1:5432/build"
ENV BETTER_AUTH_SECRET="build-time-placeholder-secret-32-chars!!"
RUN pnpm build
# ==================== 运行 ====================
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
CMD ["node", "server.js"]
+190
View File
@@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2025 CoolChange.AI
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+499
View File
@@ -0,0 +1,499 @@
<div align="center">
# Toolso.AI
**An open-source AI tools directory to help you discover the best AI tools**
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=next.js)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19-61DAFB?logo=react)](https://react.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-3-06B6D4?logo=tailwindcss)](https://tailwindcss.com/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-336791?logo=postgresql)](https://www.postgresql.org/)
[English](#english) | [中文](#中文)
<br/>
**Discover, explore, and share the best AI tools.**
</div>
---
<a name="english"></a>
## Overview
Toolso.AI is an open-source AI tools directory designed to help users discover the best AI tools across various categories. Built with Next.js 16, React 19, and TypeScript, it features a modern, responsive interface with powerful search and filtering capabilities, multilingual support, and a comprehensive admin panel for tool management.
### Key Features
| Feature | Description | Benefits |
|---------|-------------|----------|
| **AI Tools Directory** | Categorized AI tools listing | Easy discovery and exploration |
| **Search & Filter** | Powerful search with category filters | Find the right tool quickly |
| **Admin Panel** | Tool & user management | Manage content with ease |
| **Favorites** | Save tools to your personal collection | Sign in and build your toolbox |
| **Newsletter** | Footer subscription backed by Resend | Grow your audience |
| **Blog System** | MDX-powered multilingual blog | SEO-optimized AI insights |
| **i18n** | next-intl internationalization | English & Chinese out of the box |
| **Authentication** | Better Auth + Google OAuth | User accounts and submissions |
| **Theming** | Light/Dark mode | Comfortable browsing experience |
## Quick Start
### Prerequisites
- Node.js 18+
- pnpm 8+
- PostgreSQL database
### Installation
```bash
# Clone the repository
git clone https://github.com/coolchange-ai/toolso-ai-open.git
cd toolso-ai-open
# Install dependencies
pnpm install
# Set up environment variables
cp .env.example .env.local
# Push database schema
pnpm db:push
# (Optional) Seed 50 AI tools, 10 categories and 30 tags
pnpm seed:tools
# (Optional) Create an admin account
pnpm admin:setup
# Start development server
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) to see your app.
### Environment Configuration
Create `.env.local` with your configuration:
```env
# Required
DATABASE_URL="postgresql://user:password@host/db?sslmode=require"
BETTER_AUTH_SECRET="your-secret-key-at-least-32-characters"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# Email (Required for auth features)
RESEND_API_KEY="re_your_api_key"
RESEND_FROM_EMAIL="Your App <noreply@yourdomain.com>"
# Optional: Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
```
See [.env.example](.env.example) for all available options.
## Project Structure
```
src/
├── app/
│ ├── [locale]/ # Internationalized routes
│ │ ├── (auth)/ # Authentication pages
│ │ ├── (marketing)/ # Public pages (home, blog, tools)
│ │ ├── (protected)/ # User dashboard, profile
│ │ └── (admin)/ # Admin panel
│ └── api/ # API routes
├── components/ # Reusable UI components
├── features/ # Feature modules
│ ├── auth/ # Authentication logic
│ ├── admin/ # Admin components
│ ├── tools/ # AI tools directory
│ └── navigation/ # Navigation components
├── lib/ # Core utilities
│ ├── auth.ts # Better Auth config
│ ├── db/ # Database schema & connection
│ └── email.ts # Email utilities
└── emails/ # React Email templates
messages/ # i18n translation files
public/ # Static assets
scripts/ # Build & admin scripts
```
## Tech Stack
### Frontend
- **Framework**: Next.js 16 (App Router)
- **UI Library**: React 19
- **Styling**: Tailwind CSS + Framer Motion
- **Components**: Custom components + Radix UI
- **Forms**: React Hook Form + Zod validation
- **Themes**: next-themes (dark/light mode)
### Backend
- **Authentication**: Better Auth
- **Database**: PostgreSQL + Drizzle ORM
- **Email**: Resend
- **Storage**: Cloudflare R2 / AWS S3
### Developer Experience
- **Language**: TypeScript (strict mode)
- **Linting**: ESLint
- **Package Manager**: pnpm
## Available Scripts
```bash
pnpm dev # Start development server
pnpm build # Build for production
pnpm start # Start production server
pnpm lint # Run ESLint (flat config)
pnpm typecheck # Run TypeScript type check
pnpm i18n:check # Verify translation key consistency
# Database
pnpm db:generate # Generate Drizzle migrations
pnpm db:migrate # Run migrations
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
pnpm seed:tools # Seed AI tools data
# Admin
pnpm admin:setup # Create admin account
# Content
pnpm generate:blog-manifest # Generate blog manifest
```
## Deployment
### Vercel (Recommended)
1. Push your code to GitHub
2. Import project in [Vercel](https://vercel.com)
3. Add environment variables
4. Deploy
### Docker
```bash
# Start PostgreSQL + app
docker compose up -d --build
# Initialize database (run from host)
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm db:push
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm seed:tools
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm admin:setup
```
### Other Platforms
The template works with any platform supporting Node.js:
- Railway
- Render
- Self-hosted
### Deployment Checklist
- [ ] Set up PostgreSQL database (Supabase, Neon, or Vercel Postgres)
- [ ] Configure Resend for email
- [ ] Update `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` with production domain
- [ ] Run `pnpm db:push` to initialize database
- [ ] Create admin account with `pnpm admin:setup`
## Internationalization
The template supports multiple languages via `next-intl`:
- **Supported**: English (`en`), Chinese (`zh`)
- **Route Format**: English is the default locale without prefix (`/...`), Chinese uses `/zh/...` (`localePrefix: 'as-needed'`)
- **Translation Files**: `messages/en.json`, `messages/zh.json`
- **Consistency Check**: `pnpm i18n:check`
### Adding a New Language
1. Copy `messages/en.json` to `messages/{locale}.json`
2. Translate the content
3. Update `i18n.config.ts`
> Note: UI copy is fully translatable via JSON files, but tool/category/tag **data** is stored as bilingual columns (`nameEn`/`nameZh`) in the database. Adding a third content language requires extending the schema and queries.
## Roadmap
- [ ] User tool submission with review workflow
- [ ] Tool ratings & comments
- [ ] Automated tool data import pipeline
## Contributing
We welcome contributions! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
---
<a name="中文"></a>
## 项目概览
Toolso.AI 是一个开源的 AI 工具导航站,旨在帮助用户发现各类优秀的 AI 工具。基于 Next.js 16、React 19 和 TypeScript 构建,拥有现代化的响应式界面、强大的搜索和筛选功能、多语言支持,以及完善的后台管理系统。
### 核心特性
| 特性 | 描述 | 优势 |
|------|------|------|
| **AI 工具目录** | 分类展示 AI 工具 | 轻松发现和探索 |
| **搜索与筛选** | 强大的搜索和分类筛选 | 快速找到合适的工具 |
| **管理后台** | 工具和用户管理 | 轻松管理内容 |
| **收藏功能** | 登录用户可收藏工具 | 打造个人工具箱 |
| **Newsletter** | 页脚订阅(基于 Resend) | 沉淀你的用户 |
| **博客系统** | MDX 驱动的多语言博客 | SEO 优化的 AI 资讯 |
| **国际化** | next-intl 国际化 | 中英文开箱即用 |
| **身份认证** | Better Auth + Google OAuth | 用户账户和提交功能 |
| **主题切换** | 深色/浅色模式 | 舒适的浏览体验 |
## 快速开始
### 前置要求
- Node.js 18+
- pnpm 8+
- PostgreSQL 数据库
### 安装步骤
```bash
# 克隆仓库
git clone https://github.com/coolchange-ai/toolso-ai-open.git
cd toolso-ai-open
# 安装依赖
pnpm install
# 设置环境变量
cp .env.example .env.local
# 推送数据库结构
pnpm db:push
# (可选)导入 50 个 AI 工具、10 个分类、30 个标签种子数据
pnpm seed:tools
# (可选)创建管理员账户
pnpm admin:setup
# 启动开发服务器
pnpm dev
```
打开 [http://localhost:3000](http://localhost:3000) 查看应用。
### 环境变量配置
创建 `.env.local` 并配置:
```env
# 必需配置
DATABASE_URL="postgresql://user:password@host/db?sslmode=require"
BETTER_AUTH_SECRET="至少32字符的密钥"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# 邮件(认证功能必需)
RESEND_API_KEY="re_your_api_key"
RESEND_FROM_EMAIL="Your App <noreply@yourdomain.com>"
# 可选:Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
```
完整配置请参考 [.env.example](.env.example)。
## 项目结构
```
src/
├── app/
│ ├── [locale]/ # 国际化路由
│ │ ├── (auth)/ # 认证页面
│ │ ├── (marketing)/ # 公开页面(首页、博客、工具)
│ │ ├── (protected)/ # 用户仪表板、个人资料
│ │ └── (admin)/ # 管理后台
│ └── api/ # API 路由
├── components/ # 可复用 UI 组件
├── features/ # 功能模块
│ ├── auth/ # 认证逻辑
│ ├── admin/ # 管理后台组件
│ ├── tools/ # AI 工具目录
│ └── navigation/ # 导航组件
├── lib/ # 核心工具库
│ ├── auth.ts # Better Auth 配置
│ ├── db/ # 数据库 Schema 和连接
│ └── email.ts # 邮件工具
└── emails/ # React Email 模板
messages/ # 国际化翻译文件
public/ # 静态资源
scripts/ # 构建和管理脚本
```
## 技术栈
### 前端
- **框架**: Next.js 16 (App Router)
- **UI 库**: React 19
- **样式**: Tailwind CSS + Framer Motion
- **组件**: 自定义组件 + Radix UI
- **表单**: React Hook Form + Zod 验证
- **主题**: next-themes (深色/浅色模式)
### 后端
- **身份认证**: Better Auth
- **数据库**: PostgreSQL + Drizzle ORM
- **邮件**: Resend
- **存储**: Cloudflare R2 / AWS S3
### 开发体验
- **语言**: TypeScript (严格模式)
- **代码检查**: ESLint
- **包管理器**: pnpm
## 可用脚本
```bash
pnpm dev # 启动开发服务器
pnpm build # 构建生产版本
pnpm start # 启动生产服务器
pnpm lint # 运行 ESLint(flat config)
pnpm typecheck # TypeScript 类型检查
pnpm i18n:check # 校验翻译键一致性
# 数据库
pnpm db:generate # 生成 Drizzle 迁移
pnpm db:migrate # 运行迁移
pnpm db:push # 推送 schema 到数据库
pnpm db:studio # 打开 Drizzle Studio
pnpm seed:tools # 导入工具种子数据
# 管理员
pnpm admin:setup # 创建管理员账户
# 内容
pnpm generate:blog-manifest # 生成博客清单
```
## 部署指南
### Vercel(推荐)
1. 将代码推送到 GitHub
2. 在 [Vercel](https://vercel.com) 导入项目
3. 添加环境变量
4. 部署
### Docker
```bash
# 启动 PostgreSQL + 应用
docker compose up -d --build
# 初始化数据库(在宿主机执行)
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm db:push
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm seed:tools
DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm admin:setup
```
### 其他平台
模板支持任何支持 Node.js 的平台:
- Railway
- Render
- 自托管
### 部署清单
- [ ] 设置 PostgreSQL 数据库(Supabase、Neon 或 Vercel Postgres)
- [ ] 配置 Resend 邮件服务
- [ ] 更新 `BETTER_AUTH_URL` 和 `NEXT_PUBLIC_APP_URL` 为生产域名
- [ ] 运行 `pnpm db:push` 初始化数据库
- [ ] 使用 `pnpm admin:setup` 创建管理员账户
## 国际化
模板通过 `next-intl` 支持多语言:
- **已支持**: 英文 (`en`)、中文 (`zh`)
- **路由格式**: 英文为默认语言不带前缀(`/...`),中文为 `/zh/...`(`localePrefix: 'as-needed'`)
- **翻译文件**: `messages/en.json`、`messages/zh.json`
- **一致性检查**: `pnpm i18n:check`
### 添加新语言
1. 复制 `messages/en.json` 到 `messages/{语言代码}.json`
2. 翻译内容
3. 更新 `i18n.config.ts`
> 注意:UI 文案可完全通过 JSON 文件翻译,但工具/分类/标签的**数据**在数据库中以双语列(`nameEn`/`nameZh`)存储。新增第三种内容语言需要扩展 schema 和查询。
## Roadmap
- [ ] 用户工具提交与审核工作流
- [ ] 工具评分与评论
- [ ] 自动化工具数据采集管线
## 贡献指南
欢迎贡献!请随时提交 Pull Request。
1. Fork 仓库
2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 开启 Pull Request
## 开源协议
本项目基于 Apache License 2.0 开源 - 详情请查看 [LICENSE](LICENSE) 文件。
---
<div align="center">
## Related Projects
| Project | Description | Link |
|---------|-------------|------|
| **Toolso.AI** | AI Tools Directory | [https://toolso.ai](https://toolso.ai) |
| **CoolChange.AI** | AI Development Studio | [https://coolchange.ai](https://coolchange.ai) |
| **chatPPT.app** | AI Presentation Generator | [https://chatppt.app](https://chatppt.app) |
| **AIHealthGuide** | AI Health Assistant | [https://aihealthguide.app](https://aihealthguide.app) |
<br/>
---
<br/>
**Built with love by [CoolChange.AI](https://coolchange.ai)**
<br/>
If you find this project helpful, please consider giving it a star!
</div>
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: toolso
POSTGRES_PASSWORD: toolso
POSTGRES_DB: toolso
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U toolso"]
interval: 5s
timeout: 5s
retries: 10
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://toolso:toolso@db:5432/toolso
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-please-change-me-to-a-random-32-char-secret}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
RESEND_API_KEY: ${RESEND_API_KEY:-}
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-}
depends_on:
db:
condition: service_healthy
volumes:
db-data:
# 首次启动后初始化数据库(在宿主机执行):
# DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm db:push
# DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm seed:tools
# DATABASE_URL=postgresql://toolso:toolso@localhost:5432/toolso pnpm admin:setup
+14
View File
@@ -0,0 +1,14 @@
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";
// 加载环境变量
dotenv.config({ path: ".env.local" });
export default {
schema: "./src/lib/db/schema.ts",
out: "./src/drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config;
+20
View File
@@ -0,0 +1,20 @@
import { defineConfig, globalIgnores } from "eslint/config";
import coreWebVitals from "eslint-config-next/core-web-vitals";
export default defineConfig([
...coreWebVitals,
globalIgnores([
".next/**",
"node_modules/**",
"src/drizzle/**",
"src/lib/blog-manifest.generated.ts",
]),
{
rules: {
// eslint-config-next 16 引入的 React Compiler 严格规则,
// 存量代码待逐步重构,先降为警告避免阻塞 CI
"react-hooks/set-state-in-effect": "warn",
"react-hooks/purity": "warn",
},
},
]);
+796
View File
@@ -0,0 +1,796 @@
{
"common": {
"brand": {
"name": "Toolso.AI",
"tagline": "Discover the Best AI Tools",
"copyright": "Copyright © 2025",
"allRightsReserved": "All rights reserved"
},
"actions": {
"signIn": "Sign in",
"signUp": "Sign Up",
"signOut": "Sign Out",
"getStarted": "Get Started",
"startFree": "Start Free",
"learnMore": "Learn More",
"viewAll": "View All",
"submit": "Submit",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"update": "Update",
"continue": "Continue",
"back": "Back",
"next": "Next",
"contactSales": "Contact Sales",
"startTrial": "Start Free Trial"
},
"status": {
"loading": "Loading...",
"verified": "Verified",
"pending": "Pending",
"active": "Active",
"inactive": "Inactive"
}
},
"navigation": {
"main": {
"tools": "Tools",
"blog": "Blog",
"contact": "Contact",
"dashboard": "Dashboard",
"profile": "Profile"
},
"footer": {
"legal": {
"terms": "Terms of Service",
"privacy": "Privacy Policy",
"cookies": "Cookie Policy"
},
"social": {
"twitter": "Twitter",
"linkedin": "LinkedIn",
"github": "GitHub"
}
}
},
"hero": {
"badge": "Discover the Best AI Tools",
"title": "Discover the Best AI Tools",
"description": "Explore our curated collection of AI tools. Find the perfect tools for productivity, creativity, development, and more.",
"cta": {
"primary": "Explore Tools",
"secondary": "Read Blog"
}
},
"features": {
"title": "Everything You Need to Build Fast",
"subtitle": "Stop wasting time on boilerplate. Focus on your unique features while this template handles the foundation.",
"items": {
"aiOptimized": {
"title": "Modern Tech Stack",
"description": "Built with Next.js 14, TypeScript, Tailwind CSS, and PostgreSQL. Production-ready from day one."
},
"zeroPipeline": {
"title": "Authentication Ready",
"description": "Email/password and Google OAuth configured. Session management and protected routes ready to use."
},
"nonTechnical": {
"title": "Admin Dashboard",
"description": "Complete admin panel for user management. Monitor your users and manage permissions easily."
},
"productionGrade": {
"title": "Blog System Included",
"description": "MDX-powered blog with i18n support. Create content in multiple languages with SEO optimization."
}
}
},
"testimonials": {
"title": "Trusted by AI Enthusiasts",
"subtitle": "Join thousands of users discovering the best AI tools for their workflows.",
"items": [
{
"name": "Sarah Chen",
"quote": "Toolso.AI helped me find the perfect AI writing assistant. The categorization makes discovery so easy!",
"designation": "Content Creator"
},
{
"name": "Michael Park",
"quote": "Finally, a comprehensive AI tools directory that's well-organized. Saved me hours of research.",
"designation": "Product Manager"
},
{
"name": "Emily Rodriguez",
"quote": "I discover new AI tools every week on Toolso.AI. It's become my go-to resource for AI exploration.",
"designation": "Startup Founder"
},
{
"name": "David Kim",
"quote": "The search and filter features are incredibly helpful. Found exactly what I needed in minutes.",
"designation": "Software Engineer"
},
{
"name": "Lisa Wang",
"quote": "Love the multilingual support! Makes it easy to explore AI tools in both English and Chinese.",
"designation": "Digital Marketer"
},
{
"name": "James Liu",
"quote": "Great resource for staying up-to-date with the latest AI tools. The blog section is also very informative.",
"designation": "AI Researcher"
}
]
},
"companies": {
"title": "Built with Industry-Leading Technologies",
"subtitle": "Built on modern stack trusted by the world's best teams"
},
"gridFeatures": {
"aiReady": {
"title": "Extensible Architecture",
"description": "Clean, modular code structure. Easy to add new features and integrations."
},
"creem": {
"title": "Database Ready",
"description": "PostgreSQL with Drizzle ORM configured. Type-safe queries and migrations ready."
},
"auth": {
"title": "Authentication System",
"description": "Better Auth with email/password and OAuth. Session management and protected routes ready."
},
"deploy": {
"title": "One-Click Deploy",
"description": "Deploy to Vercel, Railway, or any platform instantly. Production-ready from day one."
},
"typescript": {
"title": "TypeScript + Tailwind",
"description": "Modern tech stack with Next.js 14, TypeScript, Tailwind CSS, and shadcn/ui components."
},
"database": {
"title": "Admin Dashboard",
"description": "Complete admin panel for user management and system monitoring."
},
"seo": {
"title": "SEO & Marketing",
"description": "Meta tags, Open Graph, sitemap, and analytics. Everything for organic growth."
},
"vibeCoding": {
"title": "Blog System",
"description": "MDX-powered blog with multi-language support. Create content that ranks."
}
},
"cta": {
"title": "Find Your Perfect AI Tool",
"description": "Browse our curated collection of AI tools and discover what works best for you.",
"button": "Explore Tools"
},
"auth": {
"login": {
"title": "Sign in to your account",
"emailLabel": "Email address",
"emailPlaceholder": "hello@example.com",
"passwordLabel": "Password",
"passwordPlaceholder": "••••••••",
"forgotPassword": "Forgot password?",
"noAccount": "Don't have an account?",
"signUpLink": "Sign Up",
"signInButton": "Sign in",
"signingIn": "Signing in...",
"errors": {
"invalidEmail": "Please enter valid email",
"emailRequired": "Please enter email",
"passwordRequired": "Please enter password",
"loginFailed": "Login failed. Please try again.",
"googleLoginFailed": "Google login failed. Please try again."
}
},
"signup": {
"title": "Sign up for an account",
"nameLabel": "Full name",
"namePlaceholder": "John Doe",
"emailLabel": "Email address",
"emailPlaceholder": "hello@example.com",
"passwordLabel": "Password",
"passwordPlaceholder": "••••••••",
"hasAccount": "Already have an account?",
"signInLink": "Sign in",
"signUpButton": "Sign Up",
"signingUp": "Signing up...",
"errors": {
"nameRequired": "Please enter your name",
"invalidEmail": "Please enter valid email",
"emailRequired": "Please enter email",
"passwordRequired": "Please enter password",
"signupFailed": "Sign up failed. Please try again.",
"googleSignupFailed": "Google sign up failed. Please try again."
}
},
"social": {
"googleSignIn": "Continue with Google",
"or": "Or continue with",
"githubSignIn": "Continue with GitHub"
},
"checkEmail": {
"title": "Check Your Email",
"description": "We've sent a verification link to your email address. Please click the link to verify your account and get started.",
"notReceived": "Didn't receive the email?",
"checkSpam": "Check your spam or junk folder",
"checkEmailCorrect": "Make sure you entered the correct email",
"waitAndCheck": "Wait a few minutes and check again",
"resendButton": "Resend Verification Email",
"resending": "Sending...",
"resendSuccess": "Verification email sent successfully! Please check your inbox.",
"resendError": "Failed to resend verification email.",
"backToLogin": "Back to Login",
"securityNote": "For your security, the verification link will expire in 24 hours."
},
"verifyEmail": {
"verifying": "Verifying Email",
"verifyingDescription": "Please wait while we verify your email address...",
"success": "Email Verified!",
"successDescription": "Your email has been successfully verified! Redirecting to login...",
"alreadyVerified": "Already Verified",
"failed": "Verification Failed",
"goToLogin": "Go to Login",
"backToSignUp": "Back to Sign Up",
"requestNew": "Request New Verification Email",
"checkSpamNote": "Check your spam folder if you don't see our emails"
},
"emailVerifiedGuard": {
"title": "Email Verification Required",
"description": "Please verify your email address to access this page. Check your inbox for the verification link we sent to:",
"resendButton": "Resend Verification Email",
"resending": "Sending...",
"resendSuccess": "Verification email sent! Please check your inbox.",
"resendError": "Failed to send verification email. Please try again.",
"signOutLink": "Sign out and try a different account"
}
},
"Admin": {
"sidebar": {
"title": "Admin Panel",
"subtitle": "System Management",
"dashboard": "Dashboard",
"tools": "Tools",
"categories": "Categories",
"tags": "Tags",
"users": "Users",
"backToDashboard": "Back to User Dashboard",
"submissions": "Submissions",
"claims": "Claims",
"reviews": "Reviews",
"discovery": "Discovery",
"votes": "Vote Audit",
"analytics": "Analytics"
},
"header": {
"welcome": "Welcome",
"adminBadge": "Admin"
},
"dashboard": {
"title": "Admin Dashboard",
"totalUsers": "Total Users",
"activeUsers": "Active Users",
"recentUsers": "Recent Users",
"viewAllUsers": "View All Users"
},
"toolsManagement": {
"title": "Tools Management",
"totalCount": "{count} tools",
"searchPlaceholder": "Search tool name or slug...",
"allStatus": "All Status",
"published": "Published",
"draft": "Draft",
"addTool": "Add Tool",
"tool": "Tool",
"slug": "Slug",
"status": "Status",
"createdAt": "Created",
"actions": "Actions",
"noData": "No tools data",
"previous": "Previous",
"next": "Next",
"confirmDelete": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
"deleted": "Tool deleted",
"deleteFailed": "Delete failed",
"statusUpdated": "Status updated",
"statusUpdateFailed": "Status update failed",
"toDraft": "Set to draft",
"publish": "Publish",
"edit": "Edit",
"visitWebsite": "Visit website",
"delete": "Delete",
"modal": {
"editTitle": "Edit Tool",
"addTitle": "Add Tool",
"basicInfo": "Basic Information",
"linksAndImages": "Links & Images",
"categories": "Categories",
"tags": "Tags",
"slug": "Slug",
"status": "Status",
"nameEn": "English Name",
"nameZh": "Chinese Name",
"descriptionEn": "English Description",
"descriptionZh": "Chinese Description",
"domain": "Domain",
"websiteUrl": "Website URL",
"logoUrl": "Logo URL",
"coverImageUrl": "Cover Image URL",
"cancel": "Cancel",
"saving": "Saving...",
"saveChanges": "Save Changes",
"createTool": "Create Tool",
"requiredField": "Please fill in required fields",
"updated": "Tool updated",
"created": "Tool created",
"updateFailed": "Update failed",
"createFailed": "Create failed",
"statusDraft": "Draft",
"statusPublished": "Published",
"pricing": "Pricing",
"pricingNone": "Not set",
"pricingFree": "Free",
"pricingFreemium": "Freemium",
"pricingPaid": "Paid",
"featured": "Featured on homepage"
}
},
"categoriesManagement": {
"title": "Categories Management",
"addCategory": "Add Category",
"order": "Order",
"category": "Category",
"slug": "Slug",
"toolCount": "Tools",
"toolCountLabel": "{count} tools",
"actions": "Actions",
"noData": "No categories data",
"edit": "Edit",
"delete": "Delete",
"cannotDelete": "This category has {count} tools, cannot delete",
"confirmDelete": "Are you sure you want to delete category \"{name}\"?",
"deleted": "Category deleted",
"deleteFailed": "Delete failed",
"modal": {
"editTitle": "Edit Category",
"addTitle": "Add Category",
"slug": "Slug",
"icon": "Icon (emoji)",
"nameEn": "English Name",
"nameZh": "Chinese Name",
"sortOrder": "Sort Order",
"descriptionEn": "English Description",
"descriptionZh": "Chinese Description",
"cancel": "Cancel",
"saving": "Saving...",
"saveChanges": "Save Changes",
"createCategory": "Create Category",
"requiredField": "Please fill in required fields",
"updated": "Category updated",
"created": "Category created",
"updateFailed": "Update failed",
"createFailed": "Create failed"
}
},
"tagsManagement": {
"title": "Tags Management",
"searchPlaceholder": "Search tags...",
"addTag": "Add Tag",
"toolCountLabel": "{count} tools",
"noData": "No tags data",
"edit": "Edit",
"delete": "Delete",
"confirmDeleteWithTools": "This tag is used by {count} tools. Deleting it will remove the tag from these tools. Continue?",
"confirmDelete": "Are you sure you want to delete tag \"{name}\"?",
"deleted": "Tag deleted",
"deleteFailed": "Delete failed",
"modal": {
"editTitle": "Edit Tag",
"addTitle": "Add Tag",
"slug": "Slug",
"nameEn": "English Name",
"nameZh": "Chinese Name",
"cancel": "Cancel",
"saving": "Saving...",
"saveChanges": "Save Changes",
"createTag": "Create Tag",
"requiredField": "Please fill in required fields",
"updated": "Tag updated",
"created": "Tag created",
"updateFailed": "Update failed",
"createFailed": "Create failed"
}
},
"users": {
"searchPlaceholder": "Search by name or email...",
"user": "User",
"role": "Role",
"status": "Status",
"joined": "Joined",
"actions": "Actions",
"admin": "Admin",
"banned": "Banned",
"active": "Active",
"unverified": "Unverified",
"verified": "Verified",
"emailVerified": "Email Verified",
"edit": "Edit",
"ban": "Ban",
"unban": "Unban",
"viewDetails": "View Details",
"banReason": "Ban Reason",
"banExpires": "Ban Expires",
"userDetails": "User Details",
"name": "Name",
"email": "Email",
"lastActive": "Last Active",
"close": "Close",
"roleUpdated": "Role updated successfully",
"roleUpdateFailed": "Failed to update role",
"userBanned": "User has been banned",
"userUnbanned": "User has been unbanned",
"banFailed": "Failed to ban/unban user",
"cancel": "Cancel",
"pagination": {
"previous": "Previous",
"next": "Next",
"page": "Page {current} of {total}"
}
}
},
"dashboard": {
"title": "Dashboard",
"welcome": "Welcome back",
"cards": {
"personalInfo": {
"title": "Personal Info",
"labels": {
"name": "Name",
"email": "Email",
"status": "Status",
"notSet": "Not set"
}
},
"quickActions": {
"title": "Quick Actions",
"editProfile": "Edit Profile",
"accountSettings": "Account Settings",
"securitySettings": "Security Settings"
}
},
"favorites": {
"title": "My Favorites",
"empty": "No favorite tools yet.",
"browseTools": "Browse tools and save your favorites"
}
},
"profile": {
"title": "Profile",
"subtitle": "Manage your account settings and preferences",
"sections": {
"basicInfo": {
"title": "Basic Information",
"fullName": "Full Name",
"email": "Email Address",
"verificationStatus": "Verification Status",
"notSet": "Not set",
"nameNotSet": "Name not set",
"accountId": "Account ID"
},
"accountSettings": {
"title": "Account Settings",
"memberSince": "Member Since",
"lastLogin": "Last Login",
"accountType": "Account Type"
}
},
"form": {
"nameLabel": "Name",
"namePlaceholder": "Enter your name",
"emailLabel": "Email",
"emailPlaceholder": "Enter your email",
"updateButton": "Update Profile",
"updating": "Updating...",
"updateSuccess": "Profile updated successfully!",
"updateError": "Failed to update profile. Please try again."
}
},
"contact": {
"title": "Contact Us",
"subtitle": "We'd love to hear from you. Send us a message and we'll respond as soon as possible.",
"description": "Please reach out to us and we will get back to you shortly.",
"testimonial": {
"title": "We're here to help",
"description": "Our team is ready to assist you with any questions or feedback."
},
"form": {
"nameLabel": "Full Name",
"namePlaceholder": "John Doe",
"emailLabel": "Email address",
"emailPlaceholder": "hello@example.com",
"companyLabel": "Company",
"companyPlaceholder": "Your Company",
"messageLabel": "Message",
"messagePlaceholder": "Enter your message here",
"submitButton": "Submit",
"submitting": "Sending...",
"submitSuccess": "Message sent successfully!",
"submitError": "Failed to send message. Please try again."
}
},
"blog": {
"title": "Blog",
"subtitle": "Latest insights and updates from our team",
"readMore": "Read More",
"minRead": "min read"
},
"errors": {
"404": {
"title": "404 - Page Not Found",
"message": "The page you are looking for doesn't exist.",
"backHome": "Go to Homepage"
},
"500": {
"title": "Something Went Wrong",
"message": "We're experiencing technical difficulties. Please try again later.",
"backHome": "Back to Home"
},
"general": {
"title": "An Error Occurred",
"message": "Something unexpected happened. Please try again.",
"tryAgain": "Try Again"
}
},
"featuredTestimonials": {
"people": [
{
"name": "John Doe",
"designation": "Software Engineer"
},
{
"name": "Jane Smith",
"designation": "Product Manager"
},
{
"name": "Bob Johnson",
"designation": "Designer"
},
{
"name": "Alice Brown",
"designation": "Developer"
}
]
},
"footer": {
"tagline": "Discover the best AI tools to boost your productivity.",
"product": {
"title": "Product",
"features": "Features",
"documentation": "Documentation"
},
"company": {
"title": "Company",
"about": "About",
"blog": "Blog",
"careers": "Careers"
},
"legal": {
"title": "Legal",
"privacy": "Privacy",
"terms": "Terms",
"cookies": "Cookies"
},
"social": {
"title": "Social",
"twitter": "Twitter",
"linkedin": "LinkedIn",
"github": "GitHub"
},
"newsletter": {
"title": "Subscribe to Newsletter",
"description": "Get the latest updates and news directly in your inbox.",
"placeholder": "Enter your email",
"subscribe": "Subscribe",
"success": "Subscribed! Thanks for joining.",
"invalidEmail": "Please enter a valid email address.",
"error": "Subscription failed. Please try again."
}
},
"tools": {
"title": "AI Tools Directory",
"subtitle": "Discover the best AI tools to boost your productivity",
"searchPlaceholder": "Search tools...",
"all": "All",
"foundTools": "Found {count} tools",
"visitWebsite": "Visit website",
"noTools": "No tools found",
"backToTools": "Back to tools",
"about": "About",
"categories": "Categories",
"tags": "Tags",
"visitButton": "Visit Website",
"toolInfo": "Tool Info",
"domain": "Domain",
"added": "Added",
"notFound": "Tool Not Found",
"favorites": {
"add": "Save to Favorites",
"remove": "Remove from Favorites",
"loginRequired": "Please sign in to save favorites",
"error": "Action failed. Please try again."
},
"pricing": {
"free": "Free",
"freemium": "Freemium",
"paid": "Paid"
},
"categoryPage": {
"viewAll": "View all {count} tools"
},
"reviewCta": "Ratings and reviews come from verified community members. Voted Helpful counts are shown next to each review.",
"alternatives": "Alternatives",
"seeAllAlternatives": "See all alternatives",
"stats": {
"votes": "Upvotes",
"views": "Views"
},
"share": {
"share": "Share",
"copy": "Copy",
"copyLink": "Copy link",
"copied": "Link copied to clipboard"
},
"sort": {
"latest": "Latest",
"trending": "Trending"
}
},
"seo": {
"home": {
"title": "Toolso.AI - Discover the Best AI Tools Directory",
"description": "Explore our curated collection of AI tools across various categories. Find the perfect AI tools for productivity, creativity, development, and more.",
"ogImage": "https://toolso.ai/banner.png"
},
"blog": {
"title": "Blog - Toolso.AI",
"description": "Latest insights, tutorials and updates about AI tools. Stay informed about the best AI tools and industry trends.",
"ogImage": "https://toolso.ai/banner.png"
},
"contact": {
"title": "Contact Us - Toolso.AI",
"description": "Get in touch with us. We'd love to hear your feedback and tool suggestions.",
"ogImage": "https://toolso.ai/banner.png"
},
"login": {
"title": "Login - Toolso.AI",
"description": "Sign in to your account to submit tools, save favorites, and contribute to our AI tools directory.",
"ogImage": "https://toolso.ai/banner.png"
},
"signup": {
"title": "Sign Up - Toolso.AI",
"description": "Create your account and start contributing to our AI tools directory. Submit tools and join our community.",
"ogImage": "https://toolso.ai/banner.png"
},
"terms": {
"title": "Terms of Service - Toolso.AI",
"description": "Read our terms of service to understand the rules and guidelines for using Toolso.AI.",
"ogImage": "https://toolso.ai/banner.png"
},
"privacy": {
"title": "Privacy Policy - Toolso.AI",
"description": "Learn how we collect, use, and protect your personal information and data on Toolso.AI.",
"ogImage": "https://toolso.ai/banner.png"
},
"cookies": {
"title": "Cookie Policy - Toolso.AI",
"description": "Understand how we use cookies and similar tracking technologies on Toolso.AI.",
"ogImage": "https://toolso.ai/banner.png"
},
"tools": {
"title": "AI Tools Directory - Discover the Best AI Tools",
"description": "Explore our curated collection of AI tools. Find the best AI tools for productivity, creativity, and more."
}
},
"featuredTools": {
"title": "Featured Tools",
"subtitle": "Curated collection of AI tools to boost your productivity",
"visitWebsite": "Visit website",
"empty": "No tools yet. Add some in the admin panel.",
"viewAll": "View All Tools"
},
"voting": {
"vote": "Upvote",
"voted": "Upvoted",
"unvote": "Remove vote",
"loginRequired": "Please sign in to vote — you'll be brought right back.",
"rateLimited": "Slow down a bit — too many votes in a short time.",
"error": "Vote failed. Please try again."
},
"reviews": {
"title": "Reviews",
"writeReview": "Write a review",
"empty": "No reviews yet — be the first to share your experience.",
"loadMore": "Load more",
"loading": "Loading...",
"helpful": "Helpful",
"verifiedUser": "Verified user",
"pros": "Pros",
"cons": "Cons",
"yourRating": "Your rating",
"ratingRequired": "Please pick a star rating first.",
"titlePlaceholder": "Sum it up in one line (optional)",
"contentPlaceholder": "What did you use it for? How well does it work?",
"prosPlaceholder": "What do you like about it?",
"consPlaceholder": "What could be better?",
"useCasePlaceholder": "Your use case (e.g. social media content)",
"saving": "Saving...",
"submit": "Publish review",
"update": "Update review",
"cancel": "Cancel",
"delete": "Delete my review",
"deleted": "Review deleted",
"tooShort": "Review is a bit short — tell us at least a sentence.",
"error": "Could not save your review. Please try again.",
"basedOn": "Based on {count} reviews"
},
"submission": {
"name": "Tool name",
"websiteUrl": "Website URL",
"tagline": "Tagline",
"taglinePlaceholder": "One-line pitch shown on cards",
"description": "Description",
"descriptionPlaceholder": "What does it do? Who is it for? What makes it different?",
"logoUrl": "Logo URL",
"category": "Category",
"pricingLabel": "Pricing model",
"pricing": {
"free": "Free",
"freemium": "Freemium",
"paid": "Paid",
"freeTrial": "Free Trial",
"openSource": "Open Source",
"contact": "Contact for pricing"
},
"founderName": "Founder / company",
"contactEmail": "Contact email",
"socialLinks": "Social links",
"note": "Notes for the editors",
"notePlaceholder": "Anything that helps us review faster (launch date, metrics...)",
"selectPlaceholder": "Select...",
"submit": "Submit for review",
"submitting": "Submitting...",
"submitted": "Submitted! We'll review it within 48 hours.",
"reviewNote": "Free listing · reviewed within 48h",
"duplicateFound": "Looks like this tool may already be listed:",
"duplicateBlocked": "This tool appears to be already listed. Duplicate submissions are not allowed.",
"error": "Submission failed. Please check your inputs."
},
"claim": {
"claimThisTool": "Claim this tool",
"verifiedOwner": "Verified owner",
"formTitle": "Claim ownership of this listing",
"rolePlaceholder": "Your role (e.g. Founder, Marketing lead)",
"messagePlaceholder": "How should we verify you? (same-domain email, press page...)",
"proofPlaceholder": "Proof URL (optional)",
"submit": "Send claim",
"submitting": "Sending...",
"submitted": "Claim sent! We'll review it shortly.",
"pendingExists": "You already have a pending claim for this tool.",
"cancel": "Cancel",
"error": "Could not send your claim."
},
"rankings": {
"title": "Community Rankings",
"subtitle": "Seven live leaderboards powered by votes, reviews, saves and engagement — not sponsored placement.",
"trending": "Trending",
"popular": "Popular",
"mostVoted": "Most Voted",
"mostSaved": "Most Saved",
"highestRated": "Highest Rated",
"new": "New",
"editorsChoice": "Editor's Choice",
"empty": "No tools on this board yet."
}
}
+804
View File
@@ -0,0 +1,804 @@
{
"common": {
"brand": {
"name": "Toolso.AI",
"tagline": "发现最好的 AI 工具",
"copyright": "版权所有 © 2025",
"allRightsReserved": "保留所有权利"
},
"actions": {
"signIn": "登录",
"signUp": "注册",
"signOut": "退出登录",
"getStarted": "开始使用",
"startFree": "免费开始",
"startTrial": "开始试用",
"contactSales": "联系销售",
"learnMore": "了解更多",
"viewAll": "查看全部",
"submit": "提交",
"cancel": "取消",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"update": "更新",
"continue": "继续",
"back": "返回",
"next": "下一步"
},
"status": {
"loading": "加载中...",
"verified": "已验证",
"pending": "待处理",
"active": "已激活",
"inactive": "未激活"
}
},
"navigation": {
"main": {
"tools": "工具库",
"blog": "博客",
"contact": "联系我们",
"dashboard": "仪表盘",
"profile": "个人资料"
},
"footer": {
"legal": {
"terms": "服务条款",
"privacy": "隐私政策",
"cookies": "Cookie 政策"
},
"social": {
"twitter": "Twitter",
"linkedin": "领英",
"github": "GitHub"
}
}
},
"hero": {
"badge": "发现最好的 AI 工具",
"title": "发现最好的AI工具",
"description": "精选优质 AI 工具集合,涵盖效率提升、创意设计、开发辅助等多个领域,帮你找到最适合的 AI 助手。",
"cta": {
"primary": "探索工具",
"secondary": "阅读博客"
}
},
"features": {
"title": "开始构建所需的一切",
"subtitle": "告别重复的样板代码。专注于你的独特功能,基础设施已为你准备就绪。",
"items": {
"aiOptimized": {
"title": "现代技术栈",
"description": "基于 Next.js 14、TypeScript、Tailwind CSS 和 PostgreSQL 构建。采用行业最佳实践和企业级架构。"
},
"zeroPipeline": {
"title": "开箱即用的认证",
"description": "Better Auth 驱动的完整认证系统,支持邮箱密码和 OAuth 登录、邮箱验证、密码重置和会话管理。"
},
"nonTechnical": {
"title": "管理后台",
"description": "功能齐全的管理面板,包含用户管理、角色权限控制和业务数据监控。轻松扩展满足你的需求。"
},
"productionGrade": {
"title": "博客系统",
"description": "内置 MDX 博客系统,支持语法高亮、SEO 优化和国际化。非常适合内容营销和产品更新。"
}
}
},
"testimonials": {
"title": "用户评价",
"subtitle": "来自使用 Toolso.AI 发现优质 AI 工具的真实用户反馈。",
"items": [
{
"name": "张伟",
"quote": "Toolso.AI 帮我找到了完美的 AI 写作助手,分类清晰让发现变得非常简单!",
"designation": "内容创作者"
},
{
"name": "王强",
"quote": "终于有了一个组织良好的 AI 工具目录,节省了我大量的研究时间。",
"designation": "产品经理"
},
{
"name": "李娜",
"quote": "每周都能在 Toolso.AI 上发现新的 AI 工具,已经成为我探索 AI 的首选资源。",
"designation": "创业者"
},
{
"name": "陈婷",
"quote": "搜索和筛选功能非常好用,几分钟就找到了我需要的工具。",
"designation": "软件工程师"
},
{
"name": "赵明",
"quote": "喜欢中英文双语支持!可以轻松探索各种 AI 工具。",
"designation": "数字营销专家"
},
{
"name": "朱悦",
"quote": "了解最新 AI 工具的好资源,博客板块也很有价值。",
"designation": "AI 研究员"
}
]
},
"companies": {
"title": "采用行业领先技术构建",
"subtitle": "基于现代技术栈,被全球顶尖团队信赖"
},
"gridFeatures": {
"aiReady": {
"title": "可扩展架构",
"description": "模块化设计,轻松添加新功能。遵循 Next.js 最佳实践,便于维护。"
},
"creem": {
"title": "邮件系统",
"description": "集成 Resend 邮件服务,支持交易邮件、验证邮件和新闻通讯。"
},
"auth": {
"title": "身份认证",
"description": "Better Auth 提供邮箱密码和 OAuth 支持。会话管理和路由保护已就绪。"
},
"deploy": {
"title": "一键部署",
"description": "即刻部署到 Vercel、Railway 或任何平台。从第一天起即可投入生产。"
},
"typescript": {
"title": "TypeScript + Tailwind",
"description": "现代技术栈包括 Next.js 14、TypeScript、Tailwind CSS 和 shadcn/ui 组件。"
},
"database": {
"title": "数据库 + ORM",
"description": "PostgreSQL 配合 Drizzle ORM。类型安全的查询和迁移已配置。"
},
"seo": {
"title": "SEO 和营销",
"description": "元标签、Open Graph、站点地图和分析。为有机增长准备的一切。"
},
"vibeCoding": {
"title": "国际化支持",
"description": "内置中英文支持,使用 next-intl 实现。轻松添加更多语言。"
}
},
"cta": {
"title": "找到最适合你的 AI 工具",
"description": "浏览精选 AI 工具集合,发现能帮助你提升效率的最佳工具。",
"button": "探索工具"
},
"auth": {
"login": {
"title": "登录您的账户",
"emailLabel": "邮箱地址",
"emailPlaceholder": "hello@example.com",
"passwordLabel": "密码",
"passwordPlaceholder": "••••••••",
"forgotPassword": "忘记密码?",
"noAccount": "还没有账户?",
"signUpLink": "注册",
"signInButton": "登录",
"signingIn": "登录中...",
"errors": {
"invalidEmail": "请输入有效的邮箱地址",
"emailRequired": "请输入邮箱地址",
"passwordRequired": "请输入密码",
"loginFailed": "登录失败,请重试。",
"googleLoginFailed": "Google 登录失败,请重试。"
}
},
"signup": {
"title": "注册一个新账户",
"nameLabel": "姓名",
"namePlaceholder": "张三",
"emailLabel": "邮箱地址",
"emailPlaceholder": "hello@example.com",
"passwordLabel": "密码",
"passwordPlaceholder": "••••••••",
"hasAccount": "已经有账户了?",
"signInLink": "登录",
"signUpButton": "注册",
"signingUp": "注册中...",
"errors": {
"nameRequired": "请输入您的姓名",
"invalidEmail": "请输入有效的邮箱地址",
"emailRequired": "请输入邮箱地址",
"passwordRequired": "请输入密码",
"signupFailed": "注册失败,请重试。",
"googleSignupFailed": "Google 注册失败,请重试。"
}
},
"social": {
"googleSignIn": "使用 Google 继续",
"or": "或者继续使用",
"githubSignIn": "使用 GitHub 登录"
},
"checkEmail": {
"title": "请查收您的邮件",
"description": "我们已向您的邮箱发送了验证链接。请点击链接验证您的账户并开始使用。",
"notReceived": "没有收到邮件?",
"checkSpam": "检查垃圾邮件文件夹",
"checkEmailCorrect": "确认输入的邮箱地址是否正确",
"waitAndCheck": "等待几分钟后再次查看",
"resendButton": "重新发送验证邮件",
"resending": "发送中...",
"resendSuccess": "验证邮件发送成功!请查收您的邮箱。",
"resendError": "发送验证邮件失败,请重试。",
"backToLogin": "返回登录",
"securityNote": "为了您的安全,验证链接将在24小时后失效。"
},
"verifyEmail": {
"verifying": "正在验证邮箱",
"verifyingDescription": "请稍候,我们正在验证您的邮箱地址...",
"success": "邮箱验证成功!",
"successDescription": "您的邮箱已成功验证!正在跳转到登录页面...",
"alreadyVerified": "已验证",
"failed": "验证失败",
"goToLogin": "前往登录",
"backToSignUp": "返回注册",
"requestNew": "重新发送验证邮件",
"checkSpamNote": "如果没有收到邮件,请检查垃圾邮件文件夹"
},
"emailVerifiedGuard": {
"title": "需要验证邮箱",
"description": "请验证您的邮箱地址以访问此页面。请查收我们发送到以下邮箱的验证链接:",
"resendButton": "重新发送验证邮件",
"resending": "发送中...",
"resendSuccess": "验证邮件已发送!请查收您的邮箱。",
"resendError": "发送验证邮件失败,请重试。",
"signOutLink": "退出并使用其他账户"
}
},
"Admin": {
"sidebar": {
"title": "管理后台",
"subtitle": "系统管理",
"dashboard": "仪表板",
"tools": "工具管理",
"categories": "分类管理",
"tags": "标签管理",
"users": "用户管理",
"backToDashboard": "返回用户面板",
"submissions": "提交审核",
"claims": "认领审核",
"reviews": "评论审核",
"discovery": "发现管理",
"votes": "投票审计",
"analytics": "数据分析"
},
"header": {
"welcome": "欢迎",
"adminBadge": "管理员"
},
"dashboard": {
"title": "管理员仪表板",
"totalUsers": "总用户数",
"activeUsers": "活跃用户",
"recentUsers": "最近注册用户",
"viewAllUsers": "查看所有用户"
},
"toolsManagement": {
"title": "工具管理",
"totalCount": "共 {count} 个工具",
"searchPlaceholder": "搜索工具名称或 slug...",
"allStatus": "全部状态",
"published": "已发布",
"draft": "草稿",
"addTool": "添加工具",
"tool": "工具",
"slug": "Slug",
"status": "状态",
"createdAt": "创建时间",
"actions": "操作",
"noData": "暂无工具数据",
"previous": "上一页",
"next": "下一页",
"confirmDelete": "确定要删除工具 \"{name}\" 吗?此操作不可恢复。",
"deleted": "工具已删除",
"deleteFailed": "删除失败",
"statusUpdated": "状态已更新",
"statusUpdateFailed": "状态更新失败",
"toDraft": "转为草稿",
"publish": "发布",
"edit": "编辑",
"visitWebsite": "访问网站",
"delete": "删除",
"modal": {
"editTitle": "编辑工具",
"addTitle": "添加工具",
"basicInfo": "基本信息",
"linksAndImages": "链接和图片",
"categories": "分类",
"tags": "标签",
"slug": "Slug",
"status": "状态",
"nameEn": "英文名称",
"nameZh": "中文名称",
"descriptionEn": "英文描述",
"descriptionZh": "中文描述",
"domain": "域名",
"websiteUrl": "网站链接",
"logoUrl": "Logo URL",
"coverImageUrl": "封面图 URL",
"cancel": "取消",
"saving": "保存中...",
"saveChanges": "保存更改",
"createTool": "创建工具",
"requiredField": "请填写必填字段",
"updated": "工具已更新",
"created": "工具已创建",
"updateFailed": "更新失败",
"createFailed": "创建失败",
"statusDraft": "草稿",
"statusPublished": "已发布",
"pricing": "定价",
"pricingNone": "未设置",
"pricingFree": "免费",
"pricingFreemium": "免费增值",
"pricingPaid": "付费",
"featured": "首页精选"
}
},
"categoriesManagement": {
"title": "分类管理",
"addCategory": "添加分类",
"order": "排序",
"category": "分类",
"slug": "Slug",
"toolCount": "工具数",
"toolCountLabel": "{count} 个工具",
"actions": "操作",
"noData": "暂无分类数据",
"edit": "编辑",
"delete": "删除",
"cannotDelete": "该分类下有 {count} 个工具,无法删除",
"confirmDelete": "确定要删除分类 \"{name}\" 吗?",
"deleted": "分类已删除",
"deleteFailed": "删除失败",
"modal": {
"editTitle": "编辑分类",
"addTitle": "添加分类",
"slug": "Slug",
"icon": "图标 (emoji)",
"nameEn": "英文名称",
"nameZh": "中文名称",
"sortOrder": "排序",
"descriptionEn": "英文描述",
"descriptionZh": "中文描述",
"cancel": "取消",
"saving": "保存中...",
"saveChanges": "保存更改",
"createCategory": "创建分类",
"requiredField": "请填写必填字段",
"updated": "分类已更新",
"created": "分类已创建",
"updateFailed": "更新失败",
"createFailed": "创建失败"
}
},
"tagsManagement": {
"title": "标签管理",
"searchPlaceholder": "搜索标签...",
"addTag": "添加标签",
"toolCountLabel": "{count} 个工具",
"noData": "暂无标签数据",
"edit": "编辑",
"delete": "删除",
"confirmDeleteWithTools": "该标签被 {count} 个工具使用,删除后这些工具将失去该标签。确定要删除吗?",
"confirmDelete": "确定要删除标签 \"{name}\" 吗?",
"deleted": "标签已删除",
"deleteFailed": "删除失败",
"modal": {
"editTitle": "编辑标签",
"addTitle": "添加标签",
"slug": "Slug",
"nameEn": "英文名称",
"nameZh": "中文名称",
"cancel": "取消",
"saving": "保存中...",
"saveChanges": "保存更改",
"createTag": "创建标签",
"requiredField": "请填写必填字段",
"updated": "标签已更新",
"created": "标签已创建",
"updateFailed": "更新失败",
"createFailed": "创建失败"
}
},
"users": {
"searchPlaceholder": "搜索用户姓名或邮箱...",
"user": "用户",
"role": "角色",
"status": "状态",
"joined": "加入时间",
"actions": "操作",
"admin": "管理员",
"banned": "已禁用",
"active": "活跃",
"unverified": "未验证",
"edit": "编辑",
"ban": "禁用",
"unban": "解禁",
"viewDetails": "查看详情",
"banReason": "禁用原因",
"banExpires": "禁用到期",
"userDetails": "用户详情",
"name": "姓名",
"email": "邮箱",
"emailVerified": "邮箱验证",
"verified": "已验证",
"lastActive": "最后活跃",
"close": "关闭",
"roleUpdated": "角色已更新",
"roleUpdateFailed": "角色更新失败",
"userBanned": "用户已禁用",
"userUnbanned": "用户已解禁",
"banFailed": "禁用操作失败",
"pagination": {
"previous": "上一页",
"next": "下一页",
"page": "第 {current} / {total} 页"
},
"cancel": "取消"
}
},
"dashboard": {
"title": "仪表盘",
"welcome": "欢迎回来",
"cards": {
"personalInfo": {
"title": "个人信息",
"labels": {
"name": "姓名",
"email": "邮箱",
"status": "状态",
"notSet": "未设置"
}
},
"quickActions": {
"title": "快捷操作",
"editProfile": "编辑个人资料",
"accountSettings": "账户设置",
"securitySettings": "安全设置"
}
},
"favorites": {
"title": "我的收藏",
"empty": "还没有收藏的工具。",
"browseTools": "去浏览工具并收藏"
}
},
"profile": {
"title": "个人资料",
"subtitle": "管理您的账户设置和偏好",
"sections": {
"basicInfo": {
"title": "基本信息",
"fullName": "姓名",
"email": "邮箱地址",
"verificationStatus": "验证状态",
"notSet": "未设置",
"nameNotSet": "姓名未设置",
"accountId": "账户 ID"
},
"accountSettings": {
"title": "账户详情",
"memberSince": "注册于",
"lastLogin": "最近登录",
"accountType": "账户类型"
}
},
"form": {
"nameLabel": "姓名",
"namePlaceholder": "请输入您的姓名",
"emailLabel": "邮箱",
"emailPlaceholder": "请输入您的邮箱地址",
"updateButton": "更新资料",
"updating": "更新中...",
"updateSuccess": "个人资料更新成功",
"updateError": "个人资料更新失败"
}
},
"contact": {
"title": "联系我们",
"subtitle": "我们很乐意倾听您的声音。给我们发送消息,我们会尽快回复。",
"description": "请联系我们,我们会尽快回复。",
"testimonial": {
"title": "帮助用户发现最佳 AI 工具",
"description": "Toolso.AI 致力于为用户提供精选的 AI 工具目录,帮助大家找到最适合的 AI 助手。"
},
"form": {
"nameLabel": "姓名",
"namePlaceholder": "张三",
"emailLabel": "邮箱地址",
"emailPlaceholder": "hello@example.com",
"companyLabel": "公司",
"companyPlaceholder": "某某科技有限公司",
"messageLabel": "消息",
"messagePlaceholder": "请在此处输入您的消息",
"submitButton": "提交",
"submitting": "发送中...",
"submitSuccess": "消息发送成功!",
"submitError": "消息发送失败,请重试。"
}
},
"blog": {
"title": "博客",
"subtitle": "来自我们团队的最新见解与更新",
"readMore": "阅读更多",
"minRead": "分钟阅读"
},
"errors": {
"404": {
"title": "404 - 页面未找到",
"message": "您正在寻找的页面不存在。",
"backHome": "返回首页"
},
"500": {
"title": "出错了",
"message": "我们遇到了一些技术问题,请稍后重试。",
"backHome": "返回首页"
},
"general": {
"title": "发生错误",
"message": "发生了一些意外情况,请重试。",
"tryAgain": "重试"
}
},
"featuredTestimonials": {
"people": [
{
"name": "张伟",
"designation": "软件工程师"
},
{
"name": "王强",
"designation": "产品经理"
},
{
"name": "李娜",
"designation": "数据科学家"
},
{
"name": "陈婷",
"designation": "UX设计师"
},
{
"name": "赵明",
"designation": "全栈开发者"
},
{
"name": "朱悦",
"designation": "独立开发者"
}
]
},
"footer": {
"tagline": "发现最好的 AI 工具,提升你的工作效率。",
"product": {
"title": "产品",
"features": "功能",
"documentation": "文档"
},
"company": {
"title": "公司",
"about": "关于我们",
"blog": "博客",
"careers": "招聘"
},
"legal": {
"title": "法律",
"privacy": "隐私政策",
"terms": "服务条款",
"cookies": "Cookie 政策"
},
"social": {
"title": "社交",
"twitter": "Twitter",
"linkedin": "领英",
"github": "GitHub"
},
"newsletter": {
"title": "订阅邮件",
"description": "获取最新动态和资讯,直接发送到您的邮箱。",
"placeholder": "输入您的邮箱",
"subscribe": "订阅",
"success": "订阅成功!感谢加入。",
"invalidEmail": "请输入有效的邮箱地址。",
"error": "订阅失败,请重试。"
}
},
"tools": {
"title": "AI 工具库",
"subtitle": "发现最好的 AI 工具,提升您的工作效率",
"searchPlaceholder": "搜索工具...",
"all": "全部",
"foundTools": "共找到 {count} 个工具",
"visitWebsite": "访问网站",
"noTools": "暂无工具",
"backToTools": "返回工具列表",
"about": "介绍",
"categories": "分类",
"tags": "标签",
"visitButton": "访问网站",
"toolInfo": "工具信息",
"domain": "域名",
"added": "添加时间",
"notFound": "工具未找到",
"favorites": {
"add": "收藏",
"remove": "取消收藏",
"loginRequired": "请先登录后再收藏",
"error": "操作失败,请重试。"
},
"pricing": {
"free": "免费",
"freemium": "免费增值",
"paid": "付费"
},
"categoryPage": {
"viewAll": "查看该分类全部 {count} 个工具"
},
"reviewCta": "评分与评论来自已验证的社区用户,每条评论旁展示“有用”计数。",
"alternatives": "替代品",
"seeAllAlternatives": "查看全部替代品",
"stats": {
"votes": "票数",
"views": "浏览量"
},
"share": {
"share": "分享",
"copy": "复制",
"copyLink": "复制链接",
"copied": "链接已复制"
},
"sort": {
"latest": "最新",
"trending": "最热"
}
},
"seo": {
"home": {
"title": "Toolso.AI - 发现最好的 AI 工具导航站",
"description": "探索精选的 AI 工具集合,涵盖多个类别。找到最适合效率提升、创意设计、开发辅助等场景的 AI 工具。",
"ogImage": "https://toolso.ai/banner.png"
},
"blog": {
"title": "博客 - Toolso.AI",
"description": "关于 AI 工具的最新资讯、教程和更新。了解最佳 AI 工具和行业趋势。",
"ogImage": "https://toolso.ai/banner.png"
},
"contact": {
"title": "联系我们 - Toolso.AI",
"description": "与我们取得联系。我们期待听到您的反馈和工具推荐建议。",
"ogImage": "https://toolso.ai/banner.png"
},
"login": {
"title": "登录 - Toolso.AI",
"description": "登录您的账户,提交工具、收藏喜爱的工具,为 AI 工具目录做贡献。",
"ogImage": "https://toolso.ai/banner.png"
},
"signup": {
"title": "注册 - Toolso.AI",
"description": "创建您的账户,开始为 AI 工具目录做贡献。提交工具、加入我们的社区。",
"ogImage": "https://toolso.ai/banner.png"
},
"terms": {
"title": "服务条款 - Toolso.AI",
"description": "阅读我们的服务条款,了解使用 Toolso.AI 的规则和指南。",
"ogImage": "https://toolso.ai/banner.png"
},
"privacy": {
"title": "隐私政策 - Toolso.AI",
"description": "了解我们如何在 Toolso.AI 上收集、使用和保护您的个人信息和数据。",
"ogImage": "https://toolso.ai/banner.png"
},
"cookies": {
"title": "Cookie 政策 - Toolso.AI",
"description": "了解我们如何在 Toolso.AI 上使用 cookies 和类似的跟踪技术。",
"ogImage": "https://toolso.ai/banner.png"
},
"tools": {
"title": "AI 工具库 - 发现最好的 AI 工具",
"description": "浏览精选的 AI 工具集合。找到提升效率、激发创意的最佳 AI 工具。"
}
},
"featuredTools": {
"title": "精选工具",
"subtitle": "精选优质 AI 工具,助力提升工作效率",
"visitWebsite": "访问网站",
"empty": "暂无工具,请先在后台添加",
"viewAll": "查看全部工具"
},
"voting": {
"vote": "投票",
"voted": "已投",
"unvote": "取消投票",
"loginRequired": "投票需要先登录,登录后会自动回到本页。",
"rateLimited": "操作太快了,请稍后再试。",
"error": "投票失败,请重试。"
},
"reviews": {
"title": "用户评论",
"writeReview": "写评论",
"empty": "还没有评论——来分享你的使用体验吧。",
"loadMore": "加载更多",
"loading": "加载中...",
"helpful": "有用",
"verifiedUser": "已验证用户",
"pros": "优点",
"cons": "缺点",
"yourRating": "你的评分",
"ratingRequired": "请先选择星级评分。",
"titlePlaceholder": "一句话总结(可选)",
"contentPlaceholder": "你用它做什么?效果如何?",
"prosPlaceholder": "它哪里做得好?",
"consPlaceholder": "哪里还可以改进?",
"useCasePlaceholder": "你的使用场景(如 社媒内容创作)",
"saving": "保存中...",
"submit": "发布评论",
"update": "更新评论",
"cancel": "取消",
"delete": "删除我的评论",
"deleted": "评论已删除",
"tooShort": "评论有点短——至少写一句话吧。",
"error": "评论保存失败,请重试。",
"basedOn": "共 {count} 条评论"
},
"submission": {
"name": "工具名称",
"websiteUrl": "官网地址",
"tagline": "一句话简介",
"taglinePlaceholder": "展示在卡片上的一句话卖点",
"description": "详细介绍",
"descriptionPlaceholder": "它是做什么的?适合谁?有什么不同?",
"logoUrl": "Logo 地址",
"category": "分类",
"pricingLabel": "定价模式",
"pricing": {
"free": "免费",
"freemium": "免费增值",
"paid": "付费",
"freeTrial": "免费试用",
"openSource": "开源",
"contact": "联系定价"
},
"founderName": "创始人 / 公司",
"contactEmail": "联系邮箱",
"socialLinks": "社交链接",
"note": "给编辑的备注",
"notePlaceholder": "任何能加快审核的信息(上线时间、数据等)",
"selectPlaceholder": "请选择...",
"submit": "提交审核",
"submitting": "提交中...",
"submitted": "已提交!我们会在 48 小时内完成审核。",
"reviewNote": "免费收录 · 48 小时内审核",
"duplicateFound": "这个工具可能已经被收录:",
"duplicateBlocked": "该工具似乎已被收录,不允许重复提交。",
"error": "提交失败,请检查填写内容。"
},
"claim": {
"claimThisTool": "认领此工具",
"verifiedOwner": "已认证所有者",
"formTitle": "认领该收录页的所有权",
"rolePlaceholder": "你的身份(如 创始人、市场负责人)",
"messagePlaceholder": "我们如何验证你的身份?(同域名邮箱、官方报道页等)",
"proofPlaceholder": "证明链接(可选)",
"submit": "发送认领申请",
"submitting": "发送中...",
"submitted": "认领申请已发送,我们会尽快审核。",
"pendingExists": "你已有一条待审核的认领申请。",
"cancel": "取消",
"error": "认领申请发送失败。"
},
"rankings": {
"title": "社区榜单",
"subtitle": "七个实时榜单由投票、评论、收藏与互动数据驱动——不接受赞助排名。",
"trending": "趋势榜",
"popular": "热门榜",
"mostVoted": "最多投票",
"mostSaved": "最多收藏",
"highestRated": "最高评分",
"new": "最新收录",
"editorsChoice": "编辑精选",
"empty": "该榜单暂无工具。"
}
}
+54
View File
@@ -0,0 +1,54 @@
// AI Directory Platform — 站点 Next.js 配置工厂
// 说明:站点 next.config.mjs 只需 `export { createSiteConfig as default } from "@aidir/core/next-config"`
// MDX/intl/turbopack 等构建依赖由底座包解析,站点零构建依赖。
import fs from "node:fs";
import path from "node:path";
import rehypePrism from "@mapbox/rehype-prism";
import nextMDX from "@next/mdx";
import remarkGfm from "remark-gfm";
import createNextIntlPlugin from "next-intl/plugin";
/**
* # 函数 createSiteConfig
* 说明:底座统一的站点 Next 配置(next-intl + MDX + 图片域名 + standalone)。
* 参数:opts.requestPath 站点 i18n request 文件相对路径
* 返回值:完整 Next 配置
*/
export function createSiteConfig({
requestPath = "./src/i18n/request.ts",
} = {}) {
const withNextIntl = createNextIntlPlugin(requestPath);
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
transpilePackages: ["@aidir/core"],
images: {
// 工具 logo 指向各官网 favicon,域名不可枚举,放开 https 通配
remotePatterns: [{ protocol: "https", hostname: "**" }],
},
pageExtensions: ["ts", "tsx", "mdx"],
turbopack: {
rules: {
"*.mdx": {
loaders: ["@mdx-js/loader"],
as: "*.js",
},
},
},
};
const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrism],
},
});
return withNextIntl(withMDX(nextConfig));
}
export default createSiteConfig;
+85
View File
@@ -0,0 +1,85 @@
import fs from "node:fs";
import path from "node:path";
import rehypePrism from "@mapbox/rehype-prism";
import nextMDX from "@next/mdx";
import remarkGfm from "remark-gfm";
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin('./src/lib/i18n.ts');
const pnpmManagedPath = path.join(process.cwd(), "node_modules", ".pnpm");
const snapshotManagedPaths = fs.existsSync(pnpmManagedPath) ? [pnpmManagedPath] : [];
/** @type {import('next').NextConfig} */
const nextConfig = {
// Docker 部署使用 standalone 输出
// 多主题构建:SITE_THEME=<id> NEXT_DIST_DIR=.next-<id> next build
// 每个主题独立产物(一个构建 = 一个站点),由 next start 直接服务
...(process.env.NEXT_DIST_DIR
? { distDir: process.env.NEXT_DIST_DIR }
: { output: "standalone" }),
images: {
remotePatterns: [
{
protocol: "https",
hostname: "i.pravatar.cc",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
{
protocol: "https",
hostname: "a.offerget.pro",
},
{
protocol: "https",
hostname: "lh3.googleusercontent.com",
},
// Google favicon 服务(种子数据 logo 来源之一)
{
protocol: "https",
hostname: "www.google.com",
},
// R2/S3 公共访问域名(上传的 logo/封面)
...(process.env.STORAGE_PUBLIC_URL
? [
{
protocol: "https",
hostname: new URL(process.env.STORAGE_PUBLIC_URL).hostname,
},
]
: []),
// 工具 logo 指向各工具官网的 favicon,域名不可枚举,放开 https 通配
{
protocol: "https",
hostname: "**",
},
],
},
pageExtensions: ["ts", "tsx", "mdx"],
turbopack: {
rules: {
'*.mdx': {
loaders: ['@mdx-js/loader'],
as: '*.js',
},
},
},
webpack(config) {
config.snapshot ??= {};
config.snapshot.managedPaths = snapshotManagedPaths;
return config;
},
};
const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrism],
},
});
export default withNextIntl(withMDX(nextConfig));
+124
View File
@@ -0,0 +1,124 @@
{
"name": "@aidir/core",
"version": "1.0.0",
"description": "AI Directory Platform core — shared library for all directory sites",
"private": true,
"keywords": [
"ai-tools",
"ai-directory",
"ai-tools-directory",
"artificial-intelligence",
"machine-learning",
"nextjs",
"typescript",
"tailwindcss",
"open-source"
],
"license": "Apache-2.0",
"homepage": "https://toolso.ai",
"repository": {
"type": "git",
"url": "https://github.com/coolchange-ai/toolso-ai-open.git"
},
"bugs": {
"url": "https://github.com/coolchange-ai/toolso-ai-open/issues"
},
"author": {
"name": "CoolChange.AI",
"url": "https://coolchange.ai"
},
"scripts": {
"lint": "eslint .",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"admin:setup": "tsx scripts/setup-admin.ts",
"seed:tools": "tsx scripts/seed-tools.ts",
"generate:blog-manifest": "node scripts/generate-blog-manifest.mjs",
"typecheck": "tsc --noEmit",
"i18n:check": "node scripts/check-i18n.mjs",
"seed:platform": "bun scripts/seed-platform.ts",
"test:e2e": "bun scripts/e2e-interaction.ts",
"shots": "bun scripts/screenshot-pages.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.958.0",
"@better-auth/client": "0.0.2-alpha.3",
"@hookform/resolvers": "^3.4.2",
"@mapbox/rehype-prism": "^0.9.0",
"@mdx-js/loader": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"@next/mdx": "^14.2.3",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"@react-email/components": "^0.5.3",
"@tabler/icons-react": "^3.5.0",
"@tailwindcss/typography": "^0.5.13",
"@tanstack/react-store": "^0.7.5",
"@types/mdx": "^2.0.13",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"better-auth": "^1.4.10",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cobe": "^0.6.3",
"date-fns": "^3.6.0",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.5",
"fast-glob": "^3.3.2",
"framer-motion": "^12",
"geist": "^1.3.0",
"lucide-react": "^0.562.0",
"nanoid": "^5.1.6",
"next": "16.1.1",
"next-intl": "^4.7.0",
"next-themes": "^0.4.6",
"next-view-transitions": "^0.3.5",
"postgres": "^3.4.7",
"prettier": "^3.7.4",
"prismjs": "^1.29.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-email": "^4.2.11",
"react-hook-form": "^7.51.5",
"react-icons": "^5.2.1",
"react-markdown": "^10.1.0",
"react-wrap-balancer": "^1.1.1",
"reading-time": "^1.5.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.0",
"resend": "^6.1.0",
"sonner": "^2.0.7",
"tailwind-merge": "^2.3.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/bun": "^1.4.2",
"@types/node": "^20",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"dotenv": "^17.2.2",
"eslint": "^9.39.5",
"eslint-config-next": "16.1.1",
"pg": "^8.16.3",
"playwright": "^1.57.0",
"png-to-ico": "^3.0.1",
"postcss": "^8",
"sharp": "^0.34.5",
"tailwindcss": "^3.4.1",
"tsx": "^4.20.5",
"typescript": "^5"
},
"packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912",
"exports": {
"./pages/*": "./src/pages/*",
"./kit/*": "./src/kit/*",
"./src/*": "./src/*",
"./styles/globals.css": "./src/styles/globals.css",
"./messages/*": "./messages/*",
"./proxy": "./src/proxy.ts",
"./i18n.config": "./src/i18n.config.ts",
"./next-config": "./next-config.mjs"
}
}
+10035
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+24
View File
@@ -0,0 +1,24 @@
# robots.txt for Toolso.AI - AI Tools Directory
# https://toolso.ai
User-agent: *
Allow: /
# Disallow admin and API pages
Disallow: /admin/
Disallow: /api/
# Disallow authentication pages
Disallow: /*/signup
Disallow: /*/login
Disallow: /*/check-email
Disallow: /*/verify-email
Disallow: /*/forgot-password
Disallow: /*/reset-password
# Disallow protected user pages
Disallow: /*/dashboard
Disallow: /*/profile
# Sitemap location
Sitemap: https://toolso.ai/sitemap.xml
Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* i18n 键一致性检查:对比 messages/ 下所有语言文件的键集合,
* 存在差异时打印缺失键并以非零码退出(可挂进 CI)。
*
* 用法: pnpm i18n:check
*/
import { readFileSync, readdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const messagesDir = join(dirname(fileURLToPath(import.meta.url)), "..", "messages");
function flattenKeys(obj, prefix = "") {
return Object.entries(obj).flatMap(([key, value]) => {
const path = prefix ? `${prefix}.${key}` : key;
return value && typeof value === "object" && !Array.isArray(value)
? flattenKeys(value, path)
: [path];
});
}
const files = readdirSync(messagesDir).filter((f) => f.endsWith(".json"));
const keySets = new Map(
files.map((f) => [
f,
new Set(flattenKeys(JSON.parse(readFileSync(join(messagesDir, f), "utf8")))),
])
);
const allKeys = new Set([...keySets.values()].flatMap((s) => [...s]));
let hasError = false;
for (const [file, keys] of keySets) {
const missing = [...allKeys].filter((k) => !keys.has(k));
if (missing.length > 0) {
hasError = true;
console.error(`\n❌ ${file} 缺失 ${missing.length} 个键:`);
for (const key of missing.sort()) console.error(` - ${key}`);
}
}
if (hasError) {
process.exit(1);
}
console.log(`✅ i18n 键一致 (${files.join(", ")},共 ${allKeys.size} 个键)`);
+67
View File
@@ -0,0 +1,67 @@
/**
* # 脚本 e2e-interaction
* 说明:端到端交互验证 —— 登录 → 投票 → 计数变化 → 退出登录态下投票被拦截。
*/
import { chromium } from "playwright";
const BASE = "http://localhost:3001";
const results: string[] = [];
const browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await context.newPage();
// 1) 登录
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
await page.fill('input[name="email"], input[type="email"]', "alice@demo.dev");
await page.fill('input[name="password"], input[type="password"]', "password123");
await page.click('button[type="submit"]');
await page.waitForURL(/dashboard|\/$/, { timeout: 15000 }).catch(() => {});
results.push(`登录后 URL: ${page.url()}`);
// 2) 打开工具详情页,记录票数
await page.goto(`${BASE}/tools/claude`, { waitUntil: "domcontentloaded" });
await page.waitForTimeout(1000);
const voteButton = page.locator("button", { hasText: /Upvote/ }).first();
const before = await voteButton.textContent();
results.push(`投票前按钮: ${(before ?? "").trim()}`);
// 3) 投票
await voteButton.click();
await page.waitForTimeout(1500);
const after = await voteButton.textContent();
results.push(`投票后按钮: ${(after ?? "").trim()}`);
results.push(`投票生效: ${(before ?? "") !== (after ?? "") ? "PASS" : "FAIL"}`);
// 4) 写评论
try {
const hasReviewForm = await page.locator('button:has-text("Write a review")').count();
results.push(`找到 Write a review 按钮: ${hasReviewForm}`);
if (hasReviewForm > 0) {
await page.locator('button:has-text("Write a review")').first().click();
await page.waitForTimeout(800);
await page.screenshot({ path: "/tmp/aidir-shots/e2e-review-form.png" });
// 选 5 星(表单内的大号星级)
await page.locator('form button[aria-label="5"]').first().click();
await page.locator("form textarea").first().fill("Interactive e2e test review - Claude handles long documents exceptionally well.");
await page.locator('form button:has-text("Publish review")').click();
await page.waitForTimeout(2000);
const reviewVisible = await page.locator("text=Interactive e2e test review").count();
results.push(`评论发布: ${reviewVisible > 0 ? "PASS" : "FAIL"}`);
}
} catch (error) {
await page.screenshot({ path: "/tmp/aidir-shots/e2e-debug.png" });
results.push(`评论步骤异常: ${String(error).slice(0, 100)}`);
}
// 5) 提交工具(重复检测)
await page.goto(`${BASE}/submit`, { waitUntil: "domcontentloaded" });
await page.fill('input[placeholder="ChatGPT"]', "ChatGPT");
await page.fill('input[type="url"]', "https://chatgpt.com");
await page.locator('input[type="url"]').blur();
await page.waitForTimeout(1200);
const dupVisible = await page.locator("text=already be listed").count();
results.push(`重复检测: ${dupVisible > 0 ? "PASS" : "FAIL"}`);
await browser.close();
console.log(results.join("\n"));
@@ -0,0 +1,63 @@
/**
* # 脚本 e2e-interaction
* 说明:端到端交互验证 —— 登录 → 投票(DB 前后对比)→ 重复提交检测。
* 在 monorepo 多站点架构下运行(共享同一套 Auth/DB)。
*/
import { chromium } from "playwright";
import postgres from "postgres";
const BASE = process.env.E2E_BASE || "http://localhost:3001";
const sql = postgres(
process.env.DATABASE_URL || "postgresql://postgres:postgres@127.0.0.1:15433/ai_directory",
{ prepare: false }
);
const results: string[] = [];
const browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await context.newPage();
const countClaudeVotes = async () =>
Number((await sql`SELECT COUNT(*) FROM vote WHERE tool_id='tool_claude'`)[0].count);
// 1) 登录
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
await page.fill('input[type="email"]', "bob@demo.dev");
await page.fill('input[type="password"]', "password123");
await page.click('button[type="submit"]');
await page.waitForTimeout(2500);
results.push(`1) 登录后 URL: ${page.url()}`);
await page.screenshot({ path: "/tmp/aidir-mono/e2e-after-login.png" });
results.push(`1b) 登录后用户菜单存在: ${await page.locator("header img, header button").count() > 0}`);
// 2) 投票 toggle(DB 前后对比)
const before = await countClaudeVotes();
await page.goto(`${BASE}/tools/claude`, { waitUntil: "domcontentloaded" });
await page.waitForTimeout(1500);
await page.screenshot({ path: "/tmp/aidir-mono/e2e-detail.png" });
const btnCount = await page.locator("button", { hasText: /Upvote|Upvoted/ }).count();
results.push(`2a) 找到投票按钮数: ${btnCount}`);
const btn = page.locator("button", { hasText: /Upvote|Upvoted/ }).first();
await btn.click();
await page.waitForTimeout(2500);
const after = await countClaudeVotes();
results.push(`2) 投票: DB ${before}→${after},按钮 "${((await btn.textContent()) ?? "").trim()}" → ${before !== after ? "PASS" : "FAIL"}`);
await btn.click();
await page.waitForTimeout(2500);
const restored = await countClaudeVotes();
results.push(`3) 取消投票: DB 回到 ${restored} → ${restored === before ? "PASS" : "FAIL"}`);
// 4) 重复提交检测
await page.goto(`${BASE}/submit`, { waitUntil: "domcontentloaded" });
await page.fill('input[placeholder="ChatGPT"]', "ChatGPT");
await page.fill('input[type="url"]', "https://chatgpt.com");
await page.locator('input[type="url"]').blur();
await page.waitForTimeout(1500);
const dupVisible = await page.locator("text=already be listed").count();
results.push(`4) 重复检测: ${dupVisible > 0 ? "PASS" : "FAIL"}`);
await page.screenshot({ path: "/tmp/aidir-mono/e2e-duplicate.png" });
await browser.close();
await sql.end();
console.log(results.join("\n"));
@@ -0,0 +1,74 @@
import fs from "node:fs";
import path from "node:path";
import glob from "fast-glob";
async function main() {
const blogRoot = path.join(process.cwd(), "src/content/blog");
const entries = await glob("**/*.mdx", { cwd: blogRoot });
const manifest = new Map();
for (const entry of entries) {
const segments = entry.split("/");
if (segments.length < 2) {
continue;
}
const [slug, filename] = segments;
if (!slug || slug.startsWith("[")) {
continue;
}
const locale = filename.replace(/\.mdx$/, "");
if (!locale) {
continue;
}
if (!manifest.has(slug)) {
manifest.set(slug, new Set());
}
manifest.get(slug).add(locale);
}
const sortedManifest = Array.from(manifest.entries()).sort(([a], [b]) =>
a.localeCompare(b)
);
const lines = [];
lines.push("// This file is auto-generated by scripts/generate-blog-manifest.mjs");
lines.push("// Do not edit manually.\n");
lines.push("export const blogModuleLoaders = {");
for (const [slug, locales] of sortedManifest) {
lines.push(` "${slug}": {`);
const sortedLocales = Array.from(locales).sort((a, b) => a.localeCompare(b));
for (const locale of sortedLocales) {
// 使用 path.posix 确保导入路径始终使用 / (跨平台兼容)
const importPath = path.posix.join(
"..",
"content",
"blog",
slug,
`${locale}.mdx`
);
lines.push(` "${locale}": () => import("${importPath}"),`);
}
lines.push(" },");
}
lines.push("} as const;\n");
const targetPath = path.join(process.cwd(), "src/lib/blog-manifest.generated.ts");
fs.writeFileSync(targetPath, lines.join("\n"), "utf-8");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
+705
View File
@@ -0,0 +1,705 @@
/**
* # 脚本 generate-site
* 说明:从底座生成一个独立网站应用(sites/<id>/)。
* - src/app/**:全部为薄路由(re-export core/src/pages 的同名模块)
* - (marketing)/layout + 首页:站点组合器(品牌 + 站点首页组件)
* - messages/:core 基础文案 + 站点品牌 SEO 覆盖
* - 配置:独立 package.json / next.config / tsconfig / tailwind / postcss
*
* 用法:bun scripts/generate-site.mjs <siteKey>
* 站点定义见本文件 SITES 常量。
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CORE_ROOT = path.resolve(__dirname, "..");
const REPO_ROOT = path.resolve(CORE_ROOT, "..", "..");
const SITES_ROOT = path.join(REPO_ROOT, "sites");
// ====================================================================
// 站点定义:一个条目 = 一个独立网站(品牌 / 导航 / 页脚 / 首页 / 端口)
// ====================================================================
const SITES = {
"ai-stack": {
port: 3001,
theme: "classic",
brand: {
name: "AI Stack",
tagline: "The professional AI tools directory — ranked by the community.",
headline: "Find the best AI tools, ranked by real users",
subheadline:
"1,000+ AI tools across 14 categories. Compare votes, reviews and pricing to pick the right one.",
},
navItems: [
{ label: "Discover", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Best Lists", href: "/best" },
{ label: "Rankings", href: "/rankings" },
{ label: "Blog", href: "/blog" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Discover",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Best Lists", href: "/best" },
{ label: "Rankings", href: "/rankings" },
],
},
{
title: "Community",
links: [
{ label: "Submit a Tool", href: "/submit" },
{ label: "Blog", href: "/blog" },
{ label: "Dashboard", href: "/dashboard" },
],
},
{
title: "Legal",
links: [
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
{ label: "Contact", href: "/contact" },
],
},
],
searchPlaceholder: "Search AI tools...",
seo: {
en: {
title: "AI Stack — Find the Best AI Tools, Ranked by Real Users",
description:
"The professional AI tools directory. 1,000+ AI tools across 14 categories ranked by community votes, reviews and engagement.",
},
zh: {
title: "AI Stack — 由真实用户排名的 AI 工具目录",
description: "专业 AI 工具导航:14 个分类、1000+ 工具,由社区投票、评论与互动数据实时排名。",
},
},
},
launchai: {
port: 3002,
theme: "product",
brand: {
name: "LaunchAI",
tagline: "Discover and upvote today's AI products.",
headline: "Today's AI product launches",
subheadline: "Fresh AI products, every day. Upvote the best ones to the top.",
},
navItems: [
{ label: "Today", href: "/" },
{ label: "Leaderboard", href: "/rankings" },
{ label: "Categories", href: "/tools" },
{ label: "Collections", href: "/best" },
{ label: "Blog", href: "/blog" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Community",
links: [
{ label: "Submit a Product", href: "/submit" },
{ label: "Leaderboard", href: "/rankings" },
{ label: "Dashboard", href: "/dashboard" },
],
},
{
title: "Explore",
links: [
{ label: "All Products", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Collections", href: "/best" },
],
},
{
title: "About",
links: [
{ label: "Blog", href: "/blog" },
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
],
},
],
searchPlaceholder: "Search AI products...",
seo: {
en: {
title: "LaunchAI — Discover and Upvote Today's AI Products",
description:
"A daily launch feed for AI products. Upvote the best AI tools, follow makers and climb the leaderboard.",
},
zh: {
title: "LaunchAI — 发现并投票今日 AI 新品",
description: "AI 产品每日发布社区:为最好的 AI 工具投票,登上社区排行榜。",
},
},
},
"the-ai-ledger": {
port: 3003,
theme: "editorial",
brand: {
name: "The AI Ledger",
tagline: "Independent journalism on AI tools, with a community-ranked directory.",
headline: "AI tools, reviewed and ranked",
subheadline:
"Guides, comparisons and a community-powered directory of the best AI tools.",
},
navItems: [
{ label: "Stories", href: "/blog" },
{ label: "Guides", href: "/ai-tools-for" },
{ label: "Best Picks", href: "/best" },
{ label: "Directory", href: "/tools" },
{ label: "Rankings", href: "/rankings" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Read",
links: [
{ label: "Latest Stories", href: "/blog" },
{ label: "Buying Guides", href: "/best" },
{ label: "Use Case Guides", href: "/ai-tools-for" },
],
},
{
title: "Directory",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Community Rankings", href: "/rankings" },
{ label: "Submit a Tool", href: "/submit" },
],
},
{
title: "Company",
links: [
{ label: "Contact", href: "/contact" },
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
],
},
],
searchPlaceholder: "Search the directory...",
seo: {
en: {
title: "The AI Ledger — AI Tools, Reviewed and Ranked",
description:
"Independent journalism on AI: buying guides, comparisons and a community-ranked directory of the best AI tools.",
},
zh: {
title: "The AI Ledger — AI 工具评测与排名",
description: "独立 AI 媒体:深度指南、横向对比,以及由社区排名的 AI 工具目录。",
},
},
},
"devai-tools": {
port: 3004,
theme: "niche",
brand: {
name: "DevAI.tools",
tagline: "AI tools for developers — curated, tested, ranked by the community.",
headline: "AI tools for developers",
subheadline:
"Code assistants, DevOps automation, testing copilots and everything else you ship with.",
},
navItems: [
{ label: "Tools", href: "/tools" },
{ label: "Workflows", href: "/best" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Rankings", href: "/rankings" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Directory",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Rankings", href: "/rankings" },
{ label: "Use Cases", href: "/ai-tools-for" },
],
},
{
title: "Build",
links: [
{ label: "Workflows", href: "/best" },
{ label: "Submit a Tool", href: "/submit" },
{ label: "Blog", href: "/blog" },
],
},
{
title: "Legal",
links: [
{ label: "Terms", href: "/terms" },
{ label: "Privacy", href: "/privacy" },
],
},
],
searchPlaceholder: "Search dev tools...",
seo: {
en: {
title: "DevAI.tools — AI Tools for Developers",
description:
"Code assistants, DevOps automation and testing copilots — the AI tools developers actually upvote, ranked daily.",
},
zh: {
title: "DevAI.tools — 开发者 AI 工具",
description: "面向开发者的 AI 工具目录:代码助手、运维自动化、测试副驾驶,由社区每日排名。",
},
},
},
findai: {
port: 3005,
theme: "minimal",
brand: {
name: "findai",
tagline: "The minimal AI tool search engine.",
headline: "What are you trying to do?",
subheadline: "",
},
navItems: [
{ label: "Search", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Rankings", href: "/rankings" },
{ label: "Submit", href: "/submit" },
],
footerGroups: [
{
title: "Search",
links: [
{ label: "All Tools", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Rankings", href: "/rankings" },
],
},
{
title: "About",
links: [
{ label: "Submit a Tool", href: "/submit" },
{ label: "Privacy", href: "/privacy" },
{ label: "Terms", href: "/terms" },
],
},
],
searchPlaceholder: "What do you need?",
seo: {
en: {
title: "findai — The Minimal AI Tool Search Engine",
description:
"One search box, every AI tool. Describe what you're trying to do and find the right AI tool, ranked by the community.",
},
zh: {
title: "findai — 极简 AI 工具搜索引擎",
description: "一个搜索框,找遍全部 AI 工具。说出你想做的事,社区排名替你选。",
},
},
},
};
// ====================================================================
// 生成逻辑
// ====================================================================
/** 遍历 core/src/pages 下所有 page.tsx / route.ts,返回相对路由信息 */
function collectRoutes() {
const pagesRoot = path.join(CORE_ROOT, "src", "pages");
const routes = [];
function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (
entry.name === "page.tsx" ||
entry.name === "route.ts" ||
entry.name === "layout.tsx" ||
entry.name === "not-found.tsx" ||
entry.name === "manifest.ts" ||
entry.name === "sitemap.ts"
) {
const rel = path.relative(pagesRoot, full);
const kind =
entry.name === "route.ts"
? "route"
: entry.name === "layout.tsx"
? "layout"
: "page";
routes.push({ rel, kind, full });
}
}
}
walk(pagesRoot);
return routes;
}
/** 从 core 页面模块源码中提取可 re-export 的导出名 */
/**
* 从 core 页面模块源码中提取导出:
* - 函数导出(default/generateMetadata/generateStaticParams...)→ re-export
* - 路由段配置常量(revalidate/dynamic...)→ 内联字面量(Next 要求静态可解析,不能 re-export)
*/
function extractExports(file, kind) {
const srcText = fs.readFileSync(file, "utf8");
const names = new Set();
const inlineConfigs = [];
if (kind !== "route" && /export\s+default/.test(srcText)) {
names.add("default");
}
const fnRe = /export\s+(?:async\s+)?function\s+([A-Za-z0-9_]+)/g;
for (const m of srcText.matchAll(fnRe)) names.add(m[1]);
const configNames = ["revalidate", "dynamic", "runtime", "fetchCache", "maxDuration", "dynamicParams"];
const constRe = /export\s+const\s+([A-Za-z0-9_]+)\s*=\s*([^;\n]+);/g;
for (const m of srcText.matchAll(constRe)) {
const full = m[0];
const name = m[1];
if (configNames.includes(name)) {
inlineConfigs.push(full);
}
}
// route 的解构导出(如 export const { POST, GET } = ...)→ 整句 re-export
const destructureRe = /export\s+const\s*\{([^}]+)\}\s*=[^;\n]+;/g;
for (const m of srcText.matchAll(destructureRe)) {
for (const raw of m[1].split(",")) {
const name = raw.trim().split(":")[0].trim();
if (/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/.test(name)) names.add(name);
}
}
return {
names: [...names].filter((n) => !n.startsWith("type")),
inlineConfigs,
};
}
function writeFileSafe(file, content) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content);
}
function generateSite(siteKey) {
const site = SITES[siteKey];
if (!site) throw new Error(`Unknown site: ${siteKey}. 可选: ${Object.keys(SITES).join(", ")}`);
const siteRoot = path.join(SITES_ROOT, siteKey);
console.log(`\n=== 生成站点 ${siteKey} (port ${site.port}, theme ${site.theme}) ===`);
fs.mkdirSync(siteRoot, { recursive: true });
// 清空生成物,但保留本地环境与依赖
for (const entry of fs.readdirSync(siteRoot)) {
if (entry === ".env.local" || entry === "node_modules") continue;
fs.rmSync(path.join(siteRoot, entry), { recursive: true, force: true });
}
const corePkg = "@aidir/core";
// ---------- 1) 薄路由 ----------
const skipRoutes = new Set([
// 站点组合器文件由下方单独生成(营销布局 + 首页)
"[locale]/(marketing)/layout.tsx",
"[locale]/(marketing)/page.tsx",
// 根布局单独生成(需引入站点样式)
"layout.tsx",
]);
let routeCount = 0;
for (const route of collectRoutes()) {
if (skipRoutes.has(route.rel)) continue;
const rel = route.rel.replace(/\\/g, "/");
const target = path.join(siteRoot, "src", "app", rel);
const importPath = `${corePkg}/pages/${rel.replace(/\.tsx?$/, "")}`;
const { names, inlineConfigs } = extractExports(route.full, route.kind);
if (names.length === 0 && inlineConfigs.length === 0) continue;
let content = `// 自动生成:${rel} 的薄路由\n`;
for (const cfg of inlineConfigs) content += `${cfg}\n`;
if (names.length > 0) {
content += `export { ${names.join(", ")} } from "${importPath}";\n`;
}
writeFileSafe(target, content);
routeCount++;
}
// ---------- 2) 根布局(css + 透传) ----------
writeFileSafe(
path.join(siteRoot, "src/app/layout.tsx"),
`import "${corePkg}/styles/globals.css";\n\nexport { default } from "${corePkg}/pages/app-layout";\n`
);
// ---------- 3) 营销布局 + 首页(站点组合器) ----------
writeFileSafe(
path.join(siteRoot, "src/app/[locale]/(marketing)/layout.tsx"),
`import { createMarketingLayout } from "${corePkg}/kit/create-marketing-layout";\nimport { siteConfig } from "@/site/brand";\n\nexport default createMarketingLayout(siteConfig);\n`
);
writeFileSafe(
path.join(siteRoot, "src/app/[locale]/(marketing)/page.tsx"),
`import { createHomePage, createHomeMetadata } from "${corePkg}/kit/create-home";\nimport { siteConfig } from "@/site/brand";\nimport { SiteHome } from "@/site/home";\n\n// 首页内容变化不频繁,ISR 每小时再生\nexport const revalidate = 3600;\n\nexport const generateMetadata = createHomeMetadata(siteConfig);\nexport default createHomePage(siteConfig, SiteHome);\n`
);
// ---------- 4) 站点品牌 + 首页组件 ----------
const themeSrc = fs.readFileSync(
path.join(CORE_ROOT, "src/kit/site-themes", site.theme, "index.tsx"),
"utf8"
);
// 改写 import:kit 路径 + 站点品牌
let homeSrc = themeSrc
.replace(/from "[.\/]*shared\/(section|home-data)"/g, 'from "@/kit/$1"')
.replace(/from "[.\/]*shared\/theme-header"/g, 'from "@/kit/theme-header"')
.replace(/from "[.\/]*shared\/theme-footer"/g, 'from "@/kit/theme-footer"')
.replace(/import type \{ ThemeModule \} from "[.\/]*types";\n/g, "");
// 去掉主题文件里的 brand 常量与尾部主题注册导出(品牌来自 brand.tsx)
homeSrc = homeSrc.replace(/const brand = \{[\s\S]*?\};/, "");
const tailIdx = homeSrc.indexOf("\nexport const ");
if (tailIdx !== -1) homeSrc = homeSrc.slice(0, tailIdx);
// 统一导出名为 SiteHome(原函数如 ClassicHome / ProductHome...)
homeSrc = homeSrc.replace(
/(async\s+function\s+)(\w+Home)(\s*\()/,
"$1SiteHome$3"
);
homeSrc += "\nexport { SiteHome };\n";
// 未使用的 brand 引用替换
homeSrc = homeSrc.replace(/\bbrand\.name\b/g, "siteConfig.brand.name")
.replace(/\bbrand\.headline\b/g, "siteConfig.brand.headline")
.replace(/\bbrand\.subheadline\b/g, "siteConfig.brand.subheadline")
.replace(/\bbrand\.tagline\b/g, "siteConfig.brand.tagline");
// 追加 import siteConfig
const firstImport = homeSrc.indexOf('import ');
homeSrc =
homeSrc.slice(0, firstImport) +
'import { siteConfig } from "@/site/brand";\n' +
homeSrc.slice(firstImport);
homeSrc =
`// 自动生成:站点首页组件(源自底座 ${site.theme} 形态,品牌来自 @/site/brand)\n` +
homeSrc;
writeFileSafe(path.join(siteRoot, "src/site/home.tsx"), homeSrc);
// brand.tsx(SiteConfig,不含 theme 生成元数据)
const { theme: _theme, ...siteConfigData } = site;
siteConfigData.id = siteKey;
writeFileSafe(
path.join(siteRoot, "src/site/brand.tsx"),
`import type { SiteConfig } from "@/kit/types";\n\n// ${site.brand.name} —— 独立站点的全部品牌/导航/页脚配置\nexport const siteConfig: SiteConfig = ${JSON.stringify(siteConfigData, null, 2)};\n`
);
// ---------- 5) i18n request(站点文案覆盖) ----------
writeFileSafe(
path.join(siteRoot, "src/i18n/request.ts"),
`import { getRequestConfig } from "next-intl/server";
import { notFound } from "next/navigation";
import { locales, defaultLocale, type Locale } from "${corePkg}/i18n.config";
/** 站点品牌 SEO 文案覆盖(每个网站不同) */
const brandMessages = {
en: {
seo: { home: ${JSON.stringify({ title: site.seo.en.title, description: site.seo.en.description, ogImage: "/og-image.png" })} },
hero: { title: ${JSON.stringify(site.seo.en.title)}, description: ${JSON.stringify(site.seo.en.description)} },
common: { brand: { name: ${JSON.stringify(site.brand.name)}, tagline: ${JSON.stringify(site.brand.tagline)} } },
},
zh: {
seo: { home: ${JSON.stringify({ title: site.seo.zh.title, description: site.seo.zh.description, ogImage: "/og-image.png" })} },
hero: { title: ${JSON.stringify(site.seo.zh.title)}, description: ${JSON.stringify(site.seo.zh.description)} },
common: { brand: { name: ${JSON.stringify(site.brand.name)}, tagline: ${JSON.stringify(site.brand.tagline)} } },
},
} as const;
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !locales.includes(locale as Locale)) {
locale = defaultLocale;
}
try {
// 底座基础文案 + 站点品牌覆盖(浅合并到顶层命名空间)
const base = (await import(\`${corePkg}/messages/\${locale}.json\`)).default;
const override = brandMessages[locale as keyof typeof brandMessages];
const messages = { ...base };
for (const [ns, value] of Object.entries(override)) {
messages[ns] = { ...(messages[ns] ?? {}), ...value };
}
return { locale, messages };
} catch (error) {
console.error("Error loading messages for locale:", locale, error);
notFound();
}
});
`
);
// ---------- 6) proxy / mdx-components ----------
writeFileSafe(
path.join(siteRoot, "src/proxy.ts"),
`import { proxy } from "${corePkg}/proxy";\n\nexport default proxy;\n\nexport const config = {\n matcher: [\n "/",\n "/((?!api|_next|_vercel|.*\\\\..*).*)"\n ]\n};\n`
);
writeFileSafe(
path.join(siteRoot, "mdx-components.tsx"),
`export { useMDXComponents } from "${corePkg}/src/mdx-components";\n`
);
// ---------- 7) 配置文件 ----------
writeFileSafe(
path.join(siteRoot, "package.json"),
JSON.stringify(
{
name: `site-${siteKey}`,
version: "1.0.0",
description: `${site.brand.name} — AI directory site powered by @aidir/core`,
private: true,
scripts: {
dev: `next dev -p ${site.port}`,
build: "next build",
start: `next start -p ${site.port}`,
typecheck: "tsc --noEmit",
},
dependencies: {
"@aidir/core": "workspace:*",
next: "16.1.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"next-intl": "^4.7.0",
"next-themes": "^0.4.6",
"next-view-transitions": "^0.3.5",
"sonner": "^2.0.7",
"geist": "^1.3.0",
"lucide-react": "^0.562.0",
"framer-motion": "^12.43.0",
},
devDependencies: {
"@types/node": "^20",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/mdx": "^2.0.13",
"typescript": "^5",
"tailwindcss": "^3.4.1",
"postcss": "^8",
},
},
null,
2
) + "\n"
);
writeFileSafe(
path.join(siteRoot, "next.config.mjs"),
`import createSiteConfig from "@aidir/core/next-config";\n\nexport default createSiteConfig({\n requestPath: "./src/i18n/request.ts",\n});\n`
);
writeFileSafe(
path.join(siteRoot, "tsconfig.json"),
JSON.stringify(
{
compilerOptions: {
target: "ES2017",
lib: ["dom", "dom.iterable", "esnext"],
allowJs: true,
skipLibCheck: true,
strict: true,
noEmit: true,
esModuleInterop: true,
module: "esnext",
moduleResolution: "bundler",
resolveJsonModule: true,
isolatedModules: true,
jsx: "preserve",
incremental: true,
plugins: [{ name: "next" }],
paths: {
"@/*": ["./src/*", "../../packages/core/src/*"],
"@aidir/core/pages/*": ["../../packages/core/src/pages/*"],
"@aidir/core/kit/*": ["../../packages/core/src/kit/*"],
"@aidir/core/src/*": ["../../packages/core/src/*"],
"@aidir/core/proxy": ["../../packages/core/src/proxy.ts"],
"@aidir/core/i18n.config": ["../../packages/core/src/i18n.config.ts"],
"@aidir/core/styles/globals.css": ["../../packages/core/src/styles/globals.css"],
"@aidir/core/*": ["../../packages/core/src/*", "../../packages/core/*"],
},
},
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
exclude: ["node_modules"],
},
null,
2
) + "\n"
);
writeFileSafe(
path.join(siteRoot, "postcss.config.mjs"),
`const config = {\n plugins: {\n tailwindcss: {},\n },\n};\n\nexport default config;\n`
);
// tailwind:content 扫描站点 + 底座;主题/插件来自底座 preset
writeFileSafe(
path.join(siteRoot, "tailwind.config.ts"),
`import type { Config } from "tailwindcss";\n\nconst config: Config = {\n content: [\n "./src/**/*.{js,ts,jsx,tsx,mdx}",\n "../../packages/core/src/**/*.{js,ts,jsx,tsx,mdx}",\n ],\n presets: [require("../../packages/core/tailwind.preset.cjs")],\n};\n\nexport default config;\n`
);
// ---------- 8) messages(base 由 i18n request 运行时合并;站点仍保留一份覆盖说明) ----------
// 静态拷贝 core messages 便于独立部署检查
for (const locale of ["en", "zh"]) {
const base = JSON.parse(fs.readFileSync(path.join(CORE_ROOT, "messages", `${locale}.json`), "utf8"));
// 品牌覆盖(静态合并进 messages,运行时 request.ts 仍会覆盖以双保险)
base.common = base.common ?? {};
base.common.brand = { ...(base.common.brand ?? {}), name: site.brand.name, tagline: site.brand.tagline };
base.seo = base.seo ?? {};
base.seo.home = { ...(base.seo.home ?? {}), ...site.seo[locale] };
base.hero = { ...(base.hero ?? {}), title: site.seo[locale].title, description: site.seo[locale].description };
fs.mkdirSync(path.join(siteRoot, "messages"), { recursive: true });
fs.writeFileSync(
path.join(siteRoot, "messages", `${locale}.json`),
JSON.stringify(base, null, 2) + "\n"
);
}
// 但 request.ts 目前从 core 读基础文案 —— 改为读本站 messages(保证站点内容独立可改)
let requestSrc = fs.readFileSync(path.join(siteRoot, "src/i18n/request.ts"), "utf8");
requestSrc = requestSrc.replace(
'const base = (await import(`@aidir/core/messages/${locale}.json`)).default;',
'const base = (await import(`../../messages/${locale}.json`)).default;'
);
writeFileSafe(path.join(siteRoot, "src/i18n/request.ts"), requestSrc);
// ---------- 9) public + env ----------
fs.cpSync(path.join(CORE_ROOT, "public"), path.join(siteRoot, "public"), { recursive: true });
// 品牌首字母 favicon(SVG,无 Emoji)
const initial = site.brand.name.slice(0, 1).toUpperCase();
writeFileSafe(
path.join(siteRoot, "public", "icon.svg"),
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="currentColor"/><text x="32" y="44" font-family="system-ui,sans-serif" font-size="36" font-weight="700" text-anchor="middle" fill="#fff">${initial}</text></svg>\n`
);
writeFileSafe(
path.join(siteRoot, ".env.example"),
`# ${site.brand.name} 环境变量(与底座共享同一套 PostgreSQL / Better Auth / Admin)
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:15433/ai_directory"
BETTER_AUTH_SECRET="dev-secret-ai-directory-platform-0123456789abcdef"
BETTER_AUTH_URL="http://localhost:${site.port}"
AUTH_GOOGLE_ID=""
AUTH_GOOGLE_SECRET=""
NEXT_PUBLIC_AUTH_GOOGLE_ENABLED="false"
AUTH_GITHUB_ID=""
AUTH_GITHUB_SECRET=""
NEXT_PUBLIC_AUTH_GITHUB_ENABLED="false"
RESEND_API_KEY=""
RESEND_FROM_EMAIL="${site.brand.name} <noreply@localhost>"
NEXT_PUBLIC_APP_URL="http://localhost:${site.port}"
NEXT_PUBLIC_APP_NAME="${site.brand.name}"
`
);
// ---------- 10) .gitignore ----------
writeFileSafe(
path.join(siteRoot, ".gitignore"),
"/node_modules\n/.next\n/.env*.local\nnext-env.d.ts\n*.tsbuildinfo\n"
);
console.log(`路由薄文件: ${routeCount + 2}(含根布局/营销布局/首页组合器)`);
console.log(`输出: sites/${siteKey}`);
}
// main
const key = process.argv[2];
if (!key || key === "all") {
for (const k of Object.keys(SITES)) generateSite(k);
} else {
generateSite(key);
}
+33
View File
@@ -0,0 +1,33 @@
import { spawn } from "node:child_process";
import { platform } from "node:os";
const cleanEnv = { ...process.env };
for (const key of [
"npm_config_verify_deps_before_run",
"NPM_CONFIG_VERIFY_DEPS_BEFORE_RUN",
"npm_config__jsr_registry",
"NPM_CONFIG__JSR_REGISTRY",
]) {
if (key in cleanEnv) {
delete cleanEnv[key];
}
}
// Windows 兼容性修复: Windows 需要 .cmd 扩展名和 shell
const isWindows = platform() === "win32";
const command = isWindows ? "next.cmd" : "next";
const child = spawn(command, ["dev"], {
stdio: "inherit",
env: cleanEnv,
shell: isWindows,
});
child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code ?? 0);
}
});
+61
View File
@@ -0,0 +1,61 @@
/**
* # 脚本 screenshot-pages
* 说明:用 Playwright 对五个主题站点与关键页面截图 + 控制台错误采集。
* 用于交付前的视觉验收。
*/
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const OUT = "/tmp/aidir-shots";
mkdirSync(OUT, { recursive: true });
const targets = [
{ name: "classic-home", url: "http://localhost:3001/" },
{ name: "classic-rankings", url: "http://localhost:3001/rankings" },
{ name: "classic-tool-detail", url: "http://localhost:3001/tools/claude" },
{ name: "classic-tools-list", url: "http://localhost:3001/tools" },
{ name: "classic-best", url: "http://localhost:3001/best/best-ai-writing-tools" },
{ name: "classic-usecase", url: "http://localhost:3001/ai-tools-for/blogging" },
{ name: "classic-compare", url: "http://localhost:3001/tools/cursor-vs-windsurf" },
{ name: "classic-alt", url: "http://localhost:3001/tools/midjourney-alternatives" },
{ name: "classic-zh", url: "http://localhost:3001/zh" },
{ name: "classic-submit", url: "http://localhost:3001/submit" },
{ name: "product-home", url: "http://localhost:3002/" },
{ name: "editorial-home", url: "http://localhost:3003/" },
{ name: "niche-home", url: "http://localhost:3004/" },
{ name: "minimal-home", url: "http://localhost:3005/" },
{ name: "minimal-mobile", url: "http://localhost:3005/", mobile: true },
{ name: "product-mobile", url: "http://localhost:3002/", mobile: true },
];
const browser = await chromium.launch();
const errors: string[] = [];
for (const target of targets) {
const context = await browser.newContext(
target.mobile
? { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true }
: { viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 }
);
const page = await context.newPage();
page.on("console", (msg) => {
if (msg.type() === "error") errors.push(`${target.name}: ${msg.text().slice(0, 200)}`);
});
page.on("pageerror", (err) => errors.push(`${target.name}: PAGEERROR ${String(err).slice(0, 200)}`));
try {
await page.goto(target.url, { waitUntil: "domcontentloaded", timeout: 30000 });
await page.waitForTimeout(1200);
await page.screenshot({ path: `${OUT}/${target.name}.png`, fullPage: false });
// 顺便记录 H1
const h1 = await page.locator("h1").first().textContent().catch(() => "(no h1)");
console.log(`OK ${target.name} | h1: ${h1?.trim().slice(0, 60)}`);
} catch (error) {
console.log(`FAIL ${target.name}: ${String(error).slice(0, 120)}`);
}
await context.close();
}
await browser.close();
console.log("\n=== 控制台错误 ===");
console.log(errors.length === 0 ? "(none)" : errors.slice(0, 12).join("\n"));
+520
View File
@@ -0,0 +1,520 @@
/**
* # 脚本 seed-platform
* 说明:AI Directory Platform 一体化种子数据。
* - 14 分类(无 Emoji 图标)/ 16 用例 / 9 受众
* - 40 个真实 AI 工具(含 tagline/定价/分类/用例/受众关联)
* - 6 个演示用户(含 1 个管理员,密码 password123)
* - 投票 / 评论(含优缺点)/ 收藏 / 合集 / 事件流(近 30 天)
* - 同步冗余统计列(voteCount/ratingAvg/viewCount...)
*
* 用法:bun scripts/seed-platform.ts (可加 --force 清空重灌)
*/
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import dotenv from "dotenv";
import { resolve } from "path";
import {
tool,
category,
tag,
toolCategory,
toolTag,
useCase,
audience,
toolUseCase,
toolAudience,
collection,
collectionTool,
vote,
review,
favorite,
event,
submission,
claim,
user,
} from "../src/lib/db/schema";
import { sql } from "drizzle-orm";
import { nanoid } from "nanoid";
import { hashPassword } from "better-auth/crypto";
dotenv.config({ path: resolve(process.cwd(), ".env.local") });
const client = postgres(process.env.DATABASE_URL!, { prepare: false });
const db = drizzle(client);
// ========== 确定性伪随机(可重复灌数) ==========
let seedState = 42;
function rand(): number {
seedState = (seedState * 1103515245 + 12345) % 2147483648;
return seedState / 2147483648;
}
function randInt(min: number, max: number): number {
return Math.floor(rand() * (max - min + 1)) + min;
}
function pick<T>(arr: T[]): T {
return arr[randInt(0, arr.length - 1)];
}
function daysAgo(days: number): Date {
return new Date(Date.now() - days * 24 * 3600 * 1000);
}
// ========== 分类(14,设计文档 §11) ==========
const categories = [
["chatbot", "AI Chatbots", "Conversational AI assistants", "AI 聊天机器人", "对话式 AI 助手"],
["writing", "Writing", "AI-powered writing assistants and content tools", "写作与内容", "AI 驱动的写作助手和内容工具"],
["image", "Image Generation", "Create and edit images with AI", "图像生成", "用 AI 创作和编辑图像"],
["video", "Video", "AI video generation, editing and animation", "视频", "AI 视频生成、编辑与动画"],
["audio", "Audio & Music", "Music generation, voice and audio AI", "音频与音乐", "AI 音乐生成、语音与音频"],
["coding", "Coding", "AI coding assistants and developer tools", "编程开发", "AI 编程助手与开发者工具"],
["marketing", "Marketing", "AI for marketing, SEO and growth", "营销", "面向营销、SEO 与增长的 AI"],
["productivity", "Productivity", "Work faster with AI helpers", "效率办公", "用 AI 提升工作效率"],
["research", "Research", "AI research and analysis assistants", "研究分析", "AI 研究与数据分析助手"],
["business", "Business", "AI for business operations", "商业", "面向商业运营的 AI"],
["education", "Education", "AI tutoring and learning tools", "教育", "AI 教学与学习工具"],
["finance", "Finance", "AI for finance and investing", "金融", "面向金融与投资的 AI"],
["design", "Design", "AI design and creative tools", "设计", "AI 设计与创意工具"],
["automation", "Automation", "Workflow and task automation with AI", "自动化", "AI 工作流与任务自动化"],
["agent", "AI Agent", "Autonomous agents and copilots", "智能体", "自主智能体与副驾驶"],
] as const;
// ========== 用例(16,设计文档 §11 Use Case) ==========
const useCasesData = [
["blogging", "Blogging", "Write, outline and edit blog posts faster", "写博客", "更快地撰写、构思和编辑博客"],
["presentation", "Making Presentations", "Generate slides and decks from prompts", "制作 PPT", "从提示词生成幻灯片"],
["logo-design", "Logo Design", "Create logos and brand marks", "生成 Logo", "创作 Logo 与品牌标识"],
["video-editing", "Video Editing", "Cut, caption and polish videos", "视频剪辑", "剪辑、加字幕和精修视频"],
["meeting-notes", "Meeting Notes", "Transcribe and summarize meetings", "会议记录", "转写并总结会议内容"],
["subtitles", "Subtitles & Captions", "Generate and translate subtitles", "生成字幕", "生成与翻译字幕"],
["write-code", "Writing Code", "Autocomplete, refactor and explain code", "写代码", "代码补全、重构与解释"],
["seo", "SEO", "Keyword research and content optimization", "搜索引擎优化", "关键词研究与内容优化"],
["customer-support", "Customer Support", "Chatbots and helpdesk automation", "客户支持", "客服机器人与工单自动化"],
["social-media", "Social Media", "Schedule, write and repurpose posts", "社交媒体", "撰写、排期与复用社媒内容"],
["market-analysis", "Market Analysis", "Research markets and competitors", "市场分析", "研究市场与竞品"],
["image-upscale", "Image Upscaling", "Upscale and restore images", "图像放大", "放大与修复图像"],
["copywriting", "Copywriting", "Ads, landing pages and email copy", "文案写作", "广告、落地页与邮件文案"],
["data-extraction", "Data Extraction", "Parse documents and extract structured data", "数据提取", "解析文档并提取结构化数据"],
["language-learning", "Language Learning", "Practice and translate languages", "语言学习", "练习与翻译语言"],
["music-production", "Music Production", "Compose beats and produce tracks", "音乐制作", "作曲与音乐制作"],
] as const;
// ========== 受众(9,设计文档 §11 Audience) ==========
const audiencesData = [
["developers", "Developers", "Engineers shipping software", "开发者", "写代码的工程师"],
["designers", "Designers", "Product and visual designers", "设计师", "产品与视觉设计师"],
["marketers", "Marketers", "Growth, SEO and content marketers", "营销人员", "增长、SEO 与内容营销"],
["students", "Students", "Learning and studying", "学生", "学习与备考人群"],
["creators", "Creators", "YouTubers, podcasters and streamers", "创作者", "视频博主、播客与主播"],
["founders", "Founders", "Startup founders and solopreneurs", "创业者", "创始人与独立开发者"],
["researchers", "Researchers", "Academic and market researchers", "研究人员", "学术与市场研究"],
["business", "Business Teams", "Ops, sales and finance teams", "商业团队", "运营、销售与财务团队"],
["enterprise", "Enterprise", "Large organizations with compliance needs", "大型企业", "有合规需求的大型组织"],
] as const;
// ========== 工具(40 个真实产品) ==========
// [name, slug, domain, taglineEn, taglineZh, descEn, pricing, categories, useCases, audiences, featured]
const toolsData: [string, string, string, string, string, string, string, string[], string[], string[], boolean][] = [
["ChatGPT", "chatgpt", "chatgpt.com", "The AI chatbot that started it all", "开创先河的 AI 聊天机器人", "ChatGPT is OpenAI's conversational AI assistant capable of writing, coding, analysis and more. It popularized large language models for everyday users and now supports multimodal inputs, custom GPTs and web browsing.", "freemium", ["chatbot", "writing", "coding"], ["write-code", "blogging", "copywriting"], ["developers", "marketers", "students", "founders"], true],
["Claude", "claude", "claude.ai", "Thoughtful AI assistant by Anthropic", "Anthropic 出品的可靠 AI 助手", "Claude is Anthropic's AI assistant, known for long-context understanding, careful reasoning and strong writing. Ideal for document analysis, coding and nuanced drafting.", "freemium", ["chatbot", "writing", "coding"], ["write-code", "blogging", "data-extraction"], ["developers", "researchers", "business"], true],
["Midjourney", "midjourney", "midjourney.com", "Industry-leading AI image generation", "业界领先的 AI 图像生成", "Midjourney creates striking, artistic images from text prompts. It is widely used for concept art, marketing visuals and creative exploration, with a distinctive aesthetic quality.", "paid", ["image", "design"], ["logo-design", "image-upscale"], ["designers", "creators", "marketers"], true],
["DALL·E 3", "dall-e-3", "openai.com", "Text-to-image from OpenAI, built into ChatGPT", "OpenAI 的文生图模型,已集成到 ChatGPT", "DALL·E 3 generates detailed images from natural language with excellent prompt adherence, integrated directly into ChatGPT for conversational image creation and iteration.", "freemium", ["image"], ["logo-design", "image-upscale"], ["designers", "marketers"], false],
["Stable Diffusion", "stable-diffusion", "stability.ai", "Open-source image generation you can self-host", "可自部署的开源图像生成", "Stable Diffusion is an open-source latent diffusion model for image generation, fine-tuning and local deployment, powering countless creative tools and pipelines.", "open_source", ["image"], ["logo-design", "image-upscale"], ["designers", "developers"], false],
["Runway", "runway", "runwayml.com", "Generative video for creators", "面向创作者的生成式视频", "Runway provides AI video generation and editing — text-to-video, motion brush, inpainting and green screen — used across film, advertising and social content.", "freemium", ["video"], ["video-editing"], ["creators", "designers", "marketers"], true],
["Pika", "pika", "pika.art", "Playful text-to-video generation", "有趣的文生视频工具", "Pika turns text prompts and images into short animated videos with a friendly creative workflow, popular for social clips and concept visualization.", "freemium", ["video"], ["video-editing", "social-media"], ["creators", "marketers"], false],
["ElevenLabs", "elevenlabs", "elevenlabs.io", "The most realistic AI voices", "最逼真的 AI 语音", "ElevenLabs generates hyper-realistic speech in dozens of languages, with voice cloning, dubbing and long-form narration for videos, audiobooks and products.", "freemium", ["audio"], ["subtitles", "video-editing"], ["creators", "developers"], true],
["Suno", "suno", "suno.com", "Create songs from text prompts", "从文字生成完整歌曲", "Suno composes full songs — vocals, lyrics and instrumentation — from simple prompts, enabling anyone to produce original music in minutes.", "freemium", ["audio"], ["music-production"], ["creators"], false],
["GitHub Copilot", "github-copilot", "github.com", "Your AI pair programmer", "你的 AI 结对程序员", "GitHub Copilot suggests whole lines and functions inside your editor, trained on public code. It accelerates everyday programming across all major languages.", "paid", ["coding"], ["write-code"], ["developers"], true],
["Cursor", "cursor", "cursor.com", "The AI-first code editor", "AI 优先的代码编辑器", "Cursor is a fork of VS Code rebuilt around AI: multi-file edits, codebase-aware chat and agent mode that writes and refactors code with full project context.", "freemium", ["coding"], ["write-code"], ["developers"], true],
["Windsurf", "windsurf", "codeium.com", "Agentic IDE that keeps you in flow", "保持心流的智能体 IDE", "Windsurf (by Codeium) is an agentic IDE with deep codebase awareness — it plans, writes and runs terminal commands to complete tasks end to end.", "freemium", ["coding"], ["write-code"], ["developers"], false],
["Tabnine", "tabnine", "tabnine.com", "Private, secure AI code completion", "私有化部署的 AI 代码补全", "Tabnine offers AI code completion with privacy-first deployment options — self-hosted or air-gapped — for teams with strict compliance requirements.", "freemium", ["coding", "business"], ["write-code"], ["developers", "enterprise"], false],
["Jasper", "jasper", "jasper.ai", "AI copywriting for marketing teams", "面向营销团队的 AI 文案", "Jasper helps marketing teams produce on-brand blog posts, ads and emails at scale, with templates, tone control and campaign workflows.", "paid", ["writing", "marketing"], ["copywriting", "blogging", "seo"], ["marketers", "business"], false],
["Copy.ai", "copy-ai", "copy.ai", "Sales and marketing copy, automated", "销售与营销文案自动化", "Copy.ai generates marketing and sales copy — from cold emails to product descriptions — with workflow automation for go-to-market teams.", "freemium", ["writing", "marketing"], ["copywriting", "customer-support"], ["marketers", "founders"], false],
["Surfer SEO", "surfer-seo", "surferseo.com", "Data-driven on-page SEO optimization", "数据驱动的页面 SEO 优化", "Surfer analyzes SERPs and guides content optimization with keyword density, structure and entity recommendations, helping articles rank faster.", "paid", ["marketing"], ["seo", "blogging"], ["marketers", "business"], false],
["Semrush", "semrush", "semrush.com", "The all-in-one SEO and PPC platform", "一站式 SEO 与 PPC 平台", "Semrush provides keyword research, site audits, competitor analysis and rank tracking — the standard toolkit for SEO professionals.", "paid", ["marketing"], ["seo", "market-analysis"], ["marketers", "business"], false],
["Notion AI", "notion-ai", "notion.com", "AI built into your workspace", "工作区内置 AI", "Notion AI brings writing, summarization and Q&A into Notion pages and databases, so notes, docs and project data become instantly actionable.", "freemium", ["productivity"], ["meeting-notes", "blogging"], ["business", "founders", "students"], false],
["Mem", "mem", "get.mem.ai", "Self-organizing AI notes", "自动整理的 AI 笔记", "Mem is an AI notes app that organizes itself — capture anything and Mem connects, tags and resurfaces it when relevant.", "freemium", ["productivity"], ["meeting-notes"], ["founders", "researchers"], false],
["Otter.ai", "otter-ai", "otter.ai", "AI meeting assistant and transcription", "AI 会议助手与转写", "Otter records, transcribes and summarizes meetings in real time, generating action items and searchable transcripts for teams.", "freemium", ["productivity", "audio"], ["meeting-notes", "subtitles"], ["business", "creators"], false],
["Perplexity", "perplexity", "perplexity.ai", "The answer engine", "答案引擎", "Perplexity is an AI-powered answer engine that searches the web in real time and responds with cited, sourced summaries — a research shortcut.", "freemium", ["research", "chatbot"], ["market-analysis", "blogging"], ["researchers", "students", "business"], true],
["Elicit", "elicit", "elicit.com", "AI research assistant for papers", "面向论文的 AI 研究助手", "Elicit helps researchers find and synthesize academic papers — extracting methods, findings and limitations into structured tables.", "freemium", ["research"], ["market-analysis", "data-extraction"], ["researchers", "students"], false],
["Scite", "scite", "scite.ai", "Smart citations for research", "智能引用分析", "Scite shows how papers cite each other — supporting or contrasting — helping researchers evaluate evidence quality quickly.", "paid", ["research", "education"], ["market-analysis"], ["researchers"], false],
["Synthesia", "synthesia", "synthesia.io", "AI video avatars for training", "面向培训的 AI 数字人视频", "Synthesia creates studio-quality videos with AI avatars and voiceover from plain text — used for corporate training and product explainers.", "paid", ["video", "business"], ["video-editing"], ["business", "enterprise"], false],
["HeyGen", "heygen", "heygen.com", "Spokesperson videos in minutes", "几分钟生成数字人口播视频", "HeyGen generates avatar-led videos with lip-synced translation in 40+ languages, popular for sales outreach and localization.", "freemium", ["video"], ["video-editing", "social-media"], ["marketers", "business"], false],
["Canva Magic Studio", "canva", "canva.com", "Design suite with AI everywhere", "全面融入 AI 的设计套件", "Canva's Magic Studio adds AI image generation, magic editing and brand controls to the world's most popular online design suite.", "freemium", ["design", "image"], ["logo-design", "presentation", "social-media"], ["marketers", "students", "business"], false],
["Looka", "looka", "looka.com", "AI logo maker for new brands", "面向新品牌的 AI Logo 生成", "Looka generates professional logo sets and brand kits from your preferences — an easy starting point for new businesses.", "freemium", ["design"], ["logo-design"], ["founders"], false],
["Figma AI", "figma-ai", "figma.com", "AI in the collaborative design tool", "协作设计工具中的 AI", "Figma integrates AI to generate designs, rename layers, write copy and search files — embedded in the leading collaborative design platform.", "freemium", ["design"], ["presentation"], ["designers"], false],
["Zapier AI", "zapier-ai", "zapier.com", "Automate work across 6,000+ apps", "连接 6000+ 应用的自动化", "Zapier connects thousands of apps and adds AI steps — parsing, drafting, routing — so teams automate workflows without code.", "freemium", ["automation"], ["customer-support", "data-extraction"], ["business", "marketers"], false],
["Make", "make", "make.com", "Visual automation with AI modules", "可视化自动化平台", "Make (formerly Integromat) offers visual workflow automation with AI modules for content generation, classification and routing.", "freemium", ["automation"], ["data-extraction"], ["business", "developers"], false],
["LangChain", "langchain", "langchain.com", "Framework for LLM applications", "大模型应用开发框架", "LangChain is the popular framework for building LLM apps — chains, agents, retrieval and observability for production AI.", "open_source", ["coding", "agent"], ["write-code"], ["developers"], false],
["AutoGPT", "autogpt", "agpt.co", "The pioneering autonomous agent", "开创性的自主智能体", "AutoGPT popularized autonomous AI agents that plan, browse and execute multi-step tasks toward a goal.", "open_source", ["agent"], ["write-code", "market-analysis"], ["developers", "founders"], false],
["Hugging Face", "hugging-face", "huggingface.co", "The AI models community hub", "AI 模型社区中枢", "Hugging Face hosts hundreds of thousands of models, datasets and demos — the de facto open platform for machine learning.", "freemium", ["agent", "coding", "research"], ["write-code"], ["developers", "researchers"], false],
["Grammarly", "grammarly", "grammarly.com", "Writing assistant that follows you everywhere", "随身的写作助手", "Grammarly checks grammar, clarity and tone across apps, with generative rewriting to improve any text you write.", "freemium", ["writing", "productivity"], ["copywriting", "blogging"], ["students", "business", "marketers"], false],
["QuillBot", "quillbot", "quillbot.com", "Paraphrasing and summarizing tool", "改写与摘要工具", "QuillBot paraphrases, summarizes and checks grammar, widely used by students and writers to rewrite text quickly.", "freemium", ["writing", "education"], ["blogging", "copywriting"], ["students", "creators"], false],
["Duolingo Max", "duolingo-max", "duolingo.com", "Language learning with GPT-4", "GPT-4 加持的语言学习", "Duolingo Max adds AI conversation practice and detailed explanations to the world's most popular language learning app.", "freemium", ["education"], ["language-learning"], ["students"], false],
["Khanmigo", "khanmigo", "khanmigo.ai", "Khan Academy's AI tutor", "可汗学院的 AI 导师", "Khanmigo is Khan Academy's AI tutor that guides students through problems step by step instead of giving answers away.", "paid", ["education"], ["language-learning"], ["students"], false],
["AlphaSense", "alpha-sense", "alpha-sense.com", "Market intelligence search", "市场情报搜索", "AlphaSense searches earnings calls, filings and expert transcripts with NLP for investment and competitive research.", "contact", ["finance", "research"], ["market-analysis", "data-extraction"], ["business", "enterprise"], false],
["Betterment", "betterment", "betterment.com", "Investing with AI-guided portfolios", "AI 引导的投资组合", "Betterment uses automated portfolio management and tax-loss harvesting to make investing hands-off.", "freemium", ["finance"], ["market-analysis"], ["business"], false],
["Beautiful.ai", "beautiful-ai", "beautiful.ai", "Presentations that design themselves", "自动设计的演示文稿", "Beautiful.ai applies smart design rules as you add content, keeping slides polished without manual layout work.", "paid", ["productivity", "design"], ["presentation"], ["business", "marketers"], false],
] as const;
// ========== 标签 ==========
const tags = [
["free", "Free", "免费"],
["freemium", "Freemium", "免费增值"],
["paid", "Paid", "付费"],
["open-source", "Open Source", "开源"],
["api", "API Available", "提供 API"],
["llm", "LLM", "大语言模型"],
["beginner", "Beginner Friendly", "新手友好"],
["enterprise", "Enterprise", "企业级"],
["real-time", "Real-time", "实时"],
["collaboration", "Collaboration", "协作"],
] as const;
// ========== 合集(Best Lists) ==========
const collectionsData = [
["best-ai-writing-tools", "Best AI Writing Tools in 2026", "Our community-ranked shortlist of AI writing tools for blogs, ads and long-form content — based on votes, reviews and saves.", "2026 年最佳 AI 写作工具:基于投票、评论与收藏的社区榜单,覆盖博客、广告与长文写作。", ["chatgpt", "claude", "jasper", "copy-ai", "grammarly", "quillbot"]],
["best-free-ai-tools", "Best Free AI Tools", "Genuinely useful AI tools with free plans — no trials that vanish in 7 days. Ranked by the community.", "真正好用的免费 AI 工具合集——不是 7 天就失效的试用。由社区投票排名。", ["chatgpt", "claude", "stable-diffusion", "grammarly", "canva", "perplexity"]],
["best-ai-coding-tools", "Best AI Coding Tools", "From autocomplete to agentic IDEs: the coding AI that developers actually upvote.", "从代码补全到智能体 IDE:开发者真正会投票的编程 AI。", ["cursor", "github-copilot", "claude", "windsurf", "tabnine"]],
["best-ai-video-generators", "Best AI Video Generators", "Text-to-video and editing AI compared on quality, speed and price — with community votes breaking ties.", "文生视频与视频剪辑 AI 对比:画质、速度与价格,由社区投票分高下。", ["runway", "pika", "synthesia", "heygen"]],
["best-ai-tools-for-marketing", "Best AI Tools for Marketing Teams", "The marketing stack that ranked highest with our community: SEO, copy and social.", "社区评分最高的营销工具栈:SEO、文案与社交媒体。", ["surfer-seo", "semrush", "jasper", "copy-ai", "heygen"]],
["best-chatgpt-alternatives", "Best ChatGPT Alternatives", "Looking beyond ChatGPT? These assistants ranked highest with users for writing, coding and research.", "ChatGPT 之外的选择:这些助手在写作、编程和研究上获得最高用户评价。", ["claude", "perplexity", "chatgpt", "notion-ai"]],
] as const;
// ========== 主流程 ==========
async function main() {
const force = process.argv.includes("--force");
const existing = await db.select({ cnt: sql<number>`count(*)` }).from(tool);
if (Number(existing[0].cnt) > 0 && !force) {
console.log(`数据库已有 ${Number(existing[0].cnt)} 个工具,跳过种子。使用 --force 清空重灌。`);
await client.end();
return;
}
console.log("清空平台数据...");
await db.delete(event);
await db.delete(vote);
await db.delete(review);
await db.delete(favorite);
await db.delete(collectionTool);
await db.delete(collection);
await db.delete(toolUseCase);
await db.delete(toolAudience);
await db.delete(toolCategory);
await db.delete(toolTag);
await db.delete(tool);
await db.delete(useCase);
await db.delete(audience);
await db.delete(category);
await db.delete(tag);
await db.delete(submission);
await db.delete(claim);
await db.delete(user);
console.log("写入用户...");
const passwordHash = await hashPassword("password123");
const usersData = [
["user_admin", "Platform Admin", "admin@aidirectory.dev", "admin"],
["user_alice", "Alice Chen", "alice@demo.dev", "user"],
["user_bob", "Bob Martinez", "bob@demo.dev", "user"],
["user_carol", "Carol Wang", "carol@demo.dev", "user"],
["user_dave", "Dave Kim", "dave@demo.dev", "user"],
["user_erin", "Erin Patel", "erin@demo.dev", "user"],
] as const;
for (const [id, name, email, role] of usersData) {
await db.insert(user).values({
id,
name,
email,
emailVerified: true,
role,
createdAt: daysAgo(randInt(30, 120)),
updatedAt: daysAgo(randInt(0, 5)),
// better-auth 凭据字段(credential 账户,演示密码 password123)
...({}),
});
// 写入 account 表(credential provider)—— 简化:直接操作 SQL
await client`
INSERT INTO account (id, account_id, provider_id, user_id, password, created_at, updated_at)
VALUES (${nanoid()}, ${id}, ${"credential"}, ${id}, ${passwordHash}, NOW(), NOW())
ON CONFLICT DO NOTHING`;
}
console.log("写入分类 / 标签 / 用例 / 受众...");
for (const [i, [slug, en, descEn, zh, descZh]] of categories.entries()) {
await db.insert(category).values({
id: `cat_${slug}`,
slug,
icon: null, // 设计文档 §4.1:禁止 Emoji 图标
sortOrder: i + 1,
nameEn: en,
descriptionEn: descEn,
nameZh: zh,
descriptionZh: descZh,
});
}
for (const [slug, en, zh] of tags) {
await db.insert(tag).values({ id: `tag_${slug}`, slug, nameEn: en, nameZh: zh });
}
for (const [i, [slug, en, descEn, zh, descZh]] of useCasesData.entries()) {
await db.insert(useCase).values({
id: `uc_${slug}`,
slug,
sortOrder: i + 1,
nameEn: en,
descriptionEn: descEn,
nameZh: zh,
descriptionZh: descZh,
});
}
for (const [i, [slug, en, descEn, zh, descZh]] of audiencesData.entries()) {
await db.insert(audience).values({
id: `aud_${slug}`,
slug,
sortOrder: i + 1,
nameEn: en,
descriptionEn: descEn,
nameZh: zh,
descriptionZh: descZh,
});
}
console.log("写入工具与关联...");
const toolIds: string[] = [];
for (const [i, row] of toolsData.entries()) {
const [name, slug, domain, taglineEn, taglineZh, descEn, pricing, cats, ucs, auds, featured] = row;
const id = `tool_${slug}`;
toolIds.push(id);
await db.insert(tool).values({
id,
slug,
domain,
websiteUrl: `https://${domain}`,
// favicon 作为演示 logo(next.config 已放开 https 图片域名)
logoUrl: `https://www.google.com/s2/favicons?domain=${domain}&sz=128`,
nameEn: name,
nameZh: name,
taglineEn,
taglineZh,
descriptionEn: descEn,
descriptionZh: descEn, // 演示数据中英共用描述
pricing,
featured,
status: "published",
// 时间分布:越靠前的工具越早收录(新工具在后面),制造真实的榜单时间差
createdAt: daysAgo(300 - i * 6 + randInt(0, 5)),
updatedAt: daysAgo(randInt(0, 10)),
});
for (const cat of cats) {
await db.insert(toolCategory).values({ toolId: id, categoryId: `cat_${cat}` }).onConflictDoNothing();
}
const tagPick = [pricing === "free" ? "free" : pricing === "open_source" ? "open-source" : pricing === "freemium" ? "freemium" : "paid", pick(["api", "llm", "beginner"]), pick(["real-time", "collaboration", "enterprise"])];
for (const t of tagPick) {
await db.insert(toolTag).values({ toolId: id, tagId: `tag_${t}` }).onConflictDoNothing();
}
for (const uc of ucs) {
await db.insert(toolUseCase).values({ toolId: id, useCaseId: `uc_${uc}` }).onConflictDoNothing();
}
for (const aud of auds) {
await db.insert(toolAudience).values({ toolId: id, audienceId: `aud_${aud}` }).onConflictDoNothing();
}
}
console.log("写入投票 / 收藏 / 评论...");
const reviewTexts = [
["Saved me hours every week", "I use this daily and it consistently saves me hours of repetitive work. The output quality keeps improving.", "Fast, reliable output", "Occasionally needs a second pass on nuance"],
["Great for our team workflow", "Rolled it out to the whole team. Onboarding took minutes and everyone found it immediately useful.", "Easy onboarding, great docs", "Pricey at team tier"],
["Solid but not perfect", "Does the core job well. Some advanced features feel half-finished, but the fundamentals are strong.", "Strong core features", "Advanced features feel unfinished"],
["Worth every penny", "Paid plan pays for itself within the first week. Support responded within hours when I hit an issue.", "Excellent ROI and support", "No offline mode"],
["Good, with caveats", "Impressive capability for the price, though I hit some rate limits during peak hours.", "Great value for money", "Peak-hour rate limits"],
] as const;
const voteCountByTool = new Map<string, number>();
const favCountByTool = new Map<string, number>();
for (const [i, toolId] of toolIds.entries()) {
const popularity = Math.max(1, 40 - i); // 列表靠前 = 更热门
const voters = usersData.filter(([, , , role]) => role === "user" || role === "admin").map(([id]) => id);
// 投票:热门工具更多近 7 天投票(驱动 Trending)
const totalVotes = Math.min(voters.length, Math.round((popularity / 4) * (0.5 + rand())));
const recentVotes = Math.min(totalVotes, Math.ceil(totalVotes * (i < 12 ? 0.8 : 0.3)));
const votedUsers = new Set<string>();
for (let v = 0; v < totalVotes; v++) {
const userId = pick(voters);
if (votedUsers.has(userId)) continue;
votedUsers.add(userId);
const isRecent = v < recentVotes;
const createdAt = isRecent ? daysAgo(randInt(0, 6)) : daysAgo(randInt(8, 120));
await db.insert(vote).values({ id: nanoid(), userId, toolId, createdAt }).onConflictDoNothing();
}
voteCountByTool.set(toolId, votedUsers.size);
// 收藏
const favUsers = voters.filter(() => rand() < popularity / 60);
for (const userId of favUsers) {
await db.insert(favorite).values({ userId, toolId, createdAt: daysAgo(randInt(0, 30)) }).onConflictDoNothing();
}
favCountByTool.set(toolId, favUsers.length);
// 评论(热门工具更容易被评论)
if (rand() < popularity / 14) {
const numReviews = randInt(1, 3);
const reviewingUsers = new Set<string>();
for (let r = 0; r < numReviews; r++) {
const userId = pick(voters);
if (reviewingUsers.has(userId)) continue;
reviewingUsers.add(userId);
const [title, content, pros, cons] = pick([...reviewTexts]);
const rating = i < 10 ? randInt(4, 5) : randInt(3, 5);
await db.insert(review).values({
id: nanoid(),
userId,
toolId,
rating,
title,
content,
pros,
cons,
status: "published",
helpfulCount: randInt(0, 12),
createdAt: daysAgo(randInt(1, 40)),
}).onConflictDoNothing();
}
}
}
console.log("同步冗余统计列...");
// 投票 / 收藏数
for (const [toolId, cnt] of voteCountByTool) {
await db.update(tool).set({ voteCount: cnt, favoriteCount: favCountByTool.get(toolId) ?? 0 }).where(sql`${tool.id} = ${toolId}`);
}
// 评分(真实聚合)
await client`
UPDATE tool t SET
review_count = COALESCE(agg.cnt, 0),
rating_avg = COALESCE(ROUND(agg.avg::numeric, 2), 0)
FROM (
SELECT tool_id, COUNT(*) AS cnt, AVG(rating) AS avg
FROM review WHERE status = 'published'
GROUP BY tool_id
) agg
WHERE t.id = agg.tool_id`;
console.log("写入合集...");
for (const [i, [slug, nameEn, introEn, introZh, toolSlugs]] of collectionsData.entries()) {
const id = `col_${slug}`;
await db.insert(collection).values({
id,
slug,
sortOrder: i + 1,
nameEn,
nameZh: nameEn,
introEn,
introZh,
status: "published",
});
for (const [order, toolSlug] of toolSlugs.entries()) {
await db.insert(collectionTool).values({
collectionId: id,
toolId: `tool_${toolSlug}`,
sortOrder: order,
}).onConflictDoNothing();
}
}
console.log("写入事件流(近 30 天)...");
const events: { id: string; event: string; entityType: string | null; entityId: string | null; userId: string | null; sessionId: string | null; metadata: Record<string, unknown> | null; createdAt: Date }[] = [];
for (const [i, toolId] of toolIds.entries()) {
const popularity = Math.max(1, 40 - i);
const views = Math.round(popularity * (4 + rand() * 8));
for (let v = 0; v < views; v++) {
events.push({
id: nanoid(),
event: "tool_view",
entityType: "tool",
entityId: toolId,
userId: null,
sessionId: nanoid(10),
metadata: null,
createdAt: daysAgo(randInt(0, 29)),
});
}
const clicks = Math.round(views * (0.15 + rand() * 0.2));
for (let v = 0; v < clicks; v++) {
events.push({
id: nanoid(),
event: "tool_click",
entityType: "tool",
entityId: toolId,
userId: null,
sessionId: nanoid(10),
metadata: null,
createdAt: daysAgo(randInt(0, 29)),
});
}
}
const searchTerms = ["ai writing", "video generator", "code assistant", "logo maker", "free ai tools", "meeting notes", "seo", "chatbot", "voice cloning", "image upscaler"];
for (let v = 0; v < 120; v++) {
events.push({
id: nanoid(),
event: "search",
entityType: null,
entityId: null,
userId: null,
sessionId: nanoid(10),
metadata: { query: pick(searchTerms) },
createdAt: daysAgo(randInt(0, 29)),
});
}
// 分批插入
for (let i = 0; i < events.length; i += 500) {
await db.insert(event).values(events.slice(i, i + 500));
}
// 同步 viewCount / clickCount
await client`
UPDATE tool t SET
view_count = COALESCE(v.cnt, 0),
click_count = COALESCE(c.cnt, 0)
FROM (SELECT entity_id, COUNT(*) cnt FROM event WHERE event = 'tool_view' GROUP BY entity_id) v
FULL OUTER JOIN (SELECT entity_id, COUNT(*) cnt FROM event WHERE event = 'tool_click' GROUP BY entity_id) c
ON v.entity_id = c.entity_id
WHERE t.id = COALESCE(v.entity_id, c.entity_id)`;
console.log("写入演示提交与认领...");
await db.insert(submission).values({
id: nanoid(),
userId: "user_alice",
name: "Demo New Tool",
websiteUrl: "https://demonewtool.example.com",
domain: "demonewtool.example.com",
tagline: "An example submission waiting for review",
description: "This is a sample submission in the admin queue so you can try the approve/reject flow immediately. Approving it will create a published tool listing.",
pricing: "freemium",
founderName: "Alice Chen",
contactEmail: "alice@demo.dev",
status: "pending",
});
await db.insert(claim).values({
id: nanoid(),
toolId: "tool_notion-ai",
userId: "user_bob",
role: "Product Manager",
message: "I work at the company and can verify via corporate email. Happy to provide additional proof if needed.",
proofUrl: "https://www.linkedin.com/in/example",
status: "pending",
});
const [{ cnt: toolCnt }] = await db.select({ cnt: sql<number>`count(*)` }).from(tool);
const [{ cnt: voteCnt }] = await db.select({ cnt: sql<number>`count(*)` }).from(vote);
const [{ cnt: reviewCnt }] = await db.select({ cnt: sql<number>`count(*)` }).from(review);
const [{ cnt: eventCnt }] = await db.select({ cnt: sql<number>`count(*)` }).from(event);
console.log(`\n种子完成:${Number(toolCnt)} 工具 / ${Number(voteCnt)} 投票 / ${Number(reviewCnt)} 评论 / ${Number(eventCnt)} 事件`);
console.log("管理员:admin@aidirectory.dev / password123");
await client.end();
}
main().catch(async (error) => {
console.error("种子失败:", error);
await client.end();
process.exit(1);
});
+580
View File
@@ -0,0 +1,580 @@
#!/usr/bin/env tsx
/**
* AI 工具种子数据导入脚本
*
* 用法:
* pnpm seed:tools
*
* 功能:
* - 导入 10 个分类
* - 导入 30 个标签
* - 导入 50 个 AI 工具
* - 建立工具-分类、工具-标签关联
*/
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import dotenv from "dotenv";
import { resolve } from "path";
import { tool, category, tag, toolCategory, toolTag } from "../src/lib/db/schema";
// 加载环境变量
dotenv.config({ path: resolve(process.cwd(), ".env.local") });
// ========== 种子数据 ==========
const categoriesData = [
{ id: 'cat_chatbot', slug: 'chatbot', icon: '💬', sortOrder: 1, nameEn: 'AI Chatbots', descriptionEn: 'Conversational AI assistants and chatbots', nameZh: 'AI 聊天机器人', descriptionZh: '对话式AI助手和聊天机器人' },
{ id: 'cat_writing', slug: 'writing', icon: '✍️', sortOrder: 2, nameEn: 'Writing & Content', descriptionEn: 'AI-powered writing assistants and content generation tools', nameZh: 'AI 写作', descriptionZh: 'AI驱动的写作助手和内容生成工具' },
{ id: 'cat_image', slug: 'image-generation', icon: '🎨', sortOrder: 3, nameEn: 'Image Generation', descriptionEn: 'AI tools for creating and editing images', nameZh: 'AI 图像生成', descriptionZh: 'AI图像创作和编辑工具' },
{ id: 'cat_video', slug: 'video', icon: '🎬', sortOrder: 4, nameEn: 'Video & Animation', descriptionEn: 'AI video generation, editing and animation tools', nameZh: 'AI 视频', descriptionZh: 'AI视频生成、编辑和动画工具' },
{ id: 'cat_audio', slug: 'audio-music', icon: '🎵', sortOrder: 5, nameEn: 'Audio & Music', descriptionEn: 'AI music generation, voice synthesis and audio tools', nameZh: 'AI 音频与音乐', descriptionZh: 'AI音乐生成、语音合成和音频工具' },
{ id: 'cat_coding', slug: 'coding', icon: '💻', sortOrder: 6, nameEn: 'Coding & Development', descriptionEn: 'AI coding assistants and developer tools', nameZh: 'AI 编程', descriptionZh: 'AI编程助手和开发者工具' },
{ id: 'cat_productivity', slug: 'productivity', icon: '⚡', sortOrder: 7, nameEn: 'Productivity', descriptionEn: 'AI tools to boost your work efficiency', nameZh: 'AI 效率工具', descriptionZh: '提升工作效率的AI工具' },
{ id: 'cat_research', slug: 'research', icon: '🔬', sortOrder: 8, nameEn: 'Research & Analysis', descriptionEn: 'AI research assistants and data analysis tools', nameZh: 'AI 研究与分析', descriptionZh: 'AI研究助手和数据分析工具' },
{ id: 'cat_design', slug: 'design', icon: '🎯', sortOrder: 9, nameEn: 'Design & Creative', descriptionEn: 'AI tools for designers and creative professionals', nameZh: 'AI 设计', descriptionZh: '面向设计师和创意专业人士的AI工具' },
{ id: 'cat_business', slug: 'business', icon: '📊', sortOrder: 10, nameEn: 'Business & Marketing', descriptionEn: 'AI tools for business operations and marketing', nameZh: 'AI 商业与营销', descriptionZh: '商业运营和营销AI工具' },
];
const tagsData = [
// 功能标签
{ id: 'tag_free', slug: 'free', nameEn: 'Free', nameZh: '免费' },
{ id: 'tag_freemium', slug: 'freemium', nameEn: 'Freemium', nameZh: '免费增值' },
{ id: 'tag_paid', slug: 'paid', nameEn: 'Paid', nameZh: '付费' },
{ id: 'tag_open_source', slug: 'open-source', nameEn: 'Open Source', nameZh: '开源' },
{ id: 'tag_api', slug: 'api-available', nameEn: 'API Available', nameZh: '提供API' },
// 技术标签
{ id: 'tag_gpt', slug: 'gpt-powered', nameEn: 'GPT Powered', nameZh: 'GPT驱动' },
{ id: 'tag_claude', slug: 'claude-powered', nameEn: 'Claude Powered', nameZh: 'Claude驱动' },
{ id: 'tag_diffusion', slug: 'diffusion-model', nameEn: 'Diffusion Model', nameZh: '扩散模型' },
{ id: 'tag_llm', slug: 'llm', nameEn: 'Large Language Model', nameZh: '大语言模型' },
// 用途标签
{ id: 'tag_beginner', slug: 'beginner-friendly', nameEn: 'Beginner Friendly', nameZh: '新手友好' },
{ id: 'tag_enterprise', slug: 'enterprise', nameEn: 'Enterprise', nameZh: '企业级' },
{ id: 'tag_creator', slug: 'for-creators', nameEn: 'For Creators', nameZh: '创作者适用' },
{ id: 'tag_developer', slug: 'for-developers', nameEn: 'For Developers', nameZh: '开发者适用' },
{ id: 'tag_marketer', slug: 'for-marketers', nameEn: 'For Marketers', nameZh: '营销人员适用' },
{ id: 'tag_student', slug: 'for-students', nameEn: 'For Students', nameZh: '学生适用' },
// 特性标签
{ id: 'tag_realtime', slug: 'real-time', nameEn: 'Real-time', nameZh: '实时' },
{ id: 'tag_collaborative', slug: 'collaborative', nameEn: 'Collaborative', nameZh: '协作' },
{ id: 'tag_mobile', slug: 'mobile-app', nameEn: 'Mobile App', nameZh: '移动应用' },
{ id: 'tag_browser', slug: 'browser-extension', nameEn: 'Browser Extension', nameZh: '浏览器插件' },
{ id: 'tag_desktop', slug: 'desktop-app', nameEn: 'Desktop App', nameZh: '桌面应用' },
// 输出类型标签
{ id: 'tag_text', slug: 'text-output', nameEn: 'Text Output', nameZh: '文本输出' },
{ id: 'tag_image_out', slug: 'image-output', nameEn: 'Image Output', nameZh: '图像输出' },
{ id: 'tag_video_out', slug: 'video-output', nameEn: 'Video Output', nameZh: '视频输出' },
{ id: 'tag_audio_out', slug: 'audio-output', nameEn: 'Audio Output', nameZh: '音频输出' },
{ id: 'tag_code_out', slug: 'code-output', nameEn: 'Code Output', nameZh: '代码输出' },
// 热门标签
{ id: 'tag_trending', slug: 'trending', nameEn: 'Trending', nameZh: '热门' },
{ id: 'tag_new', slug: 'new', nameEn: 'New', nameZh: '新上线' },
{ id: 'tag_popular', slug: 'popular', nameEn: 'Popular', nameZh: '受欢迎' },
{ id: 'tag_editors_choice', slug: 'editors-choice', nameEn: "Editor's Choice", nameZh: '编辑推荐' },
{ id: 'tag_best_2025', slug: 'best-of-2025', nameEn: 'Best of 2025', nameZh: '2025精选' },
];
const toolsData = [
// AI 聊天机器人 (8个)
{ id: 'tool_chatgpt', slug: 'chatgpt', domain: 'chat.openai.com', websiteUrl: 'https://chat.openai.com', logoUrl: 'https://chat.openai.com/favicon.ico', nameEn: 'ChatGPT', descriptionEn: "OpenAI's advanced conversational AI assistant powered by GPT-4. Capable of natural conversations, code generation, creative writing, and complex reasoning.", nameZh: 'ChatGPT', descriptionZh: 'OpenAI推出的先进对话式AI助手,由GPT-4驱动。能够进行自然对话、代码生成、创意写作和复杂推理。', status: 'published' },
{ id: 'tool_claude', slug: 'claude', domain: 'claude.ai', websiteUrl: 'https://claude.ai', logoUrl: 'https://claude.ai/favicon.ico', nameEn: 'Claude', descriptionEn: "Anthropic's helpful, harmless, and honest AI assistant. Known for nuanced understanding, long context windows, and thoughtful responses.", nameZh: 'Claude', descriptionZh: 'Anthropic推出的有用、无害、诚实的AI助手。以细腻的理解、长上下文窗口和深思熟虑的回复著称。', status: 'published' },
{ id: 'tool_gemini', slug: 'gemini', domain: 'gemini.google.com', websiteUrl: 'https://gemini.google.com', logoUrl: 'https://gemini.google.com/favicon.ico', nameEn: 'Google Gemini', descriptionEn: "Google's multimodal AI assistant that can understand text, images, audio, and video. Integrated with Google services.", nameZh: 'Google Gemini', descriptionZh: 'Google的多模态AI助手,能够理解文本、图像、音频和视频。与Google服务深度集成。', status: 'published' },
{ id: 'tool_perplexity', slug: 'perplexity', domain: 'perplexity.ai', websiteUrl: 'https://perplexity.ai', logoUrl: 'https://perplexity.ai/favicon.ico', nameEn: 'Perplexity AI', descriptionEn: 'AI-powered search engine that provides direct answers with cited sources. Combines web search with language model capabilities.', nameZh: 'Perplexity AI', descriptionZh: 'AI驱动的搜索引擎,提供带引用来源的直接答案。结合网络搜索与语言模型能力。', status: 'published' },
{ id: 'tool_poe', slug: 'poe', domain: 'poe.com', websiteUrl: 'https://poe.com', logoUrl: 'https://poe.com/favicon.ico', nameEn: 'Poe', descriptionEn: 'Platform by Quora offering access to multiple AI models including GPT-4, Claude, and custom bots in one interface.', nameZh: 'Poe', descriptionZh: 'Quora推出的平台,在一个界面中提供多个AI模型访问,包括GPT-4、Claude和自定义机器人。', status: 'published' },
{ id: 'tool_character', slug: 'character-ai', domain: 'character.ai', websiteUrl: 'https://character.ai', logoUrl: 'https://character.ai/favicon.ico', nameEn: 'Character.AI', descriptionEn: 'Create and chat with AI characters. Popular for roleplay, creative storytelling, and entertainment purposes.', nameZh: 'Character.AI', descriptionZh: '创建AI角色并与之聊天。在角色扮演、创意故事讲述和娱乐方面广受欢迎。', status: 'published' },
{ id: 'tool_coze', slug: 'coze', domain: 'coze.com', websiteUrl: 'https://coze.com', logoUrl: 'https://coze.com/favicon.ico', nameEn: 'Coze', descriptionEn: "ByteDance's AI chatbot platform for creating custom AI bots with plugins, workflows, and knowledge bases.", nameZh: 'Coze', descriptionZh: '字节跳动的AI聊天机器人平台,可创建带插件、工作流和知识库的自定义AI机器人。', status: 'published' },
{ id: 'tool_pi', slug: 'pi-ai', domain: 'pi.ai', websiteUrl: 'https://pi.ai', logoUrl: 'https://pi.ai/favicon.ico', nameEn: 'Pi', descriptionEn: 'Personal AI assistant by Inflection AI, designed for supportive conversations and emotional intelligence.', nameZh: 'Pi', descriptionZh: 'Inflection AI的个人AI助手,专为支持性对话和情商设计。', status: 'published' },
// AI 写作 (7个)
{ id: 'tool_jasper', slug: 'jasper', domain: 'jasper.ai', websiteUrl: 'https://jasper.ai', logoUrl: 'https://jasper.ai/favicon.ico', nameEn: 'Jasper', descriptionEn: 'AI content creation platform for marketing teams. Generate blog posts, ads, emails, and social media content at scale.', nameZh: 'Jasper', descriptionZh: '面向营销团队的AI内容创作平台。大规模生成博客文章、广告、邮件和社交媒体内容。', status: 'published' },
{ id: 'tool_copy_ai', slug: 'copy-ai', domain: 'copy.ai', websiteUrl: 'https://copy.ai', logoUrl: 'https://copy.ai/favicon.ico', nameEn: 'Copy.ai', descriptionEn: 'AI-powered copywriting tool for creating marketing copy, product descriptions, and sales content.', nameZh: 'Copy.ai', descriptionZh: 'AI驱动的文案工具,用于创建营销文案、产品描述和销售内容。', status: 'published' },
{ id: 'tool_writesonic', slug: 'writesonic', domain: 'writesonic.com', websiteUrl: 'https://writesonic.com', logoUrl: 'https://writesonic.com/favicon.ico', nameEn: 'Writesonic', descriptionEn: 'AI writer for creating SEO-optimized articles, blog posts, and marketing content with fact-checking capabilities.', nameZh: 'Writesonic', descriptionZh: 'AI写作工具,用于创建SEO优化的文章、博客和营销内容,具有事实核查功能。', status: 'published' },
{ id: 'tool_notion_ai', slug: 'notion-ai', domain: 'notion.so', websiteUrl: 'https://notion.so', logoUrl: 'https://notion.so/favicon.ico', nameEn: 'Notion AI', descriptionEn: 'AI writing assistant integrated into Notion. Helps with drafting, summarizing, brainstorming, and improving writing.', nameZh: 'Notion AI', descriptionZh: '集成在Notion中的AI写作助手。帮助起草、总结、头脑风暴和改进写作。', status: 'published' },
{ id: 'tool_grammarly', slug: 'grammarly', domain: 'grammarly.com', websiteUrl: 'https://grammarly.com', logoUrl: 'https://grammarly.com/favicon.ico', nameEn: 'Grammarly', descriptionEn: 'AI-powered writing assistant that checks grammar, spelling, punctuation, and offers style improvements.', nameZh: 'Grammarly', descriptionZh: 'AI驱动的写作助手,检查语法、拼写、标点符号,并提供风格改进建议。', status: 'published' },
{ id: 'tool_quillbot', slug: 'quillbot', domain: 'quillbot.com', websiteUrl: 'https://quillbot.com', logoUrl: 'https://quillbot.com/favicon.ico', nameEn: 'QuillBot', descriptionEn: 'AI paraphrasing and writing tool. Helps rephrase sentences, summarize text, and improve writing clarity.', nameZh: 'QuillBot', descriptionZh: 'AI改写和写作工具。帮助改述句子、总结文本和提高写作清晰度。', status: 'published' },
{ id: 'tool_rytr', slug: 'rytr', domain: 'rytr.me', websiteUrl: 'https://rytr.me', logoUrl: 'https://rytr.me/favicon.ico', nameEn: 'Rytr', descriptionEn: 'AI writing assistant for generating content in 30+ languages and 20+ tones. Affordable alternative for content creators.', nameZh: 'Rytr', descriptionZh: 'AI写作助手,支持30多种语言和20多种语气生成内容。创作者的经济实惠选择。', status: 'published' },
// AI 图像生成 (8个)
{ id: 'tool_midjourney', slug: 'midjourney', domain: 'midjourney.com', websiteUrl: 'https://midjourney.com', logoUrl: 'https://midjourney.com/favicon.ico', nameEn: 'Midjourney', descriptionEn: 'Leading AI art generator known for stunning, artistic image generation. Creates images from text prompts via Discord.', nameZh: 'Midjourney', descriptionZh: '领先的AI艺术生成器,以惊艳的艺术图像生成著称。通过Discord从文本提示创建图像。', status: 'published' },
{ id: 'tool_dalle', slug: 'dall-e', domain: 'openai.com/dall-e', websiteUrl: 'https://openai.com/dall-e', logoUrl: 'https://openai.com/favicon.ico', nameEn: 'DALL·E 3', descriptionEn: "OpenAI's latest image generation model with exceptional prompt understanding and photorealistic outputs.", nameZh: 'DALL·E 3', descriptionZh: 'OpenAI最新的图像生成模型,具有出色的提示理解能力和逼真的输出效果。', status: 'published' },
{ id: 'tool_stable_diffusion', slug: 'stable-diffusion', domain: 'stability.ai', websiteUrl: 'https://stability.ai', logoUrl: 'https://stability.ai/favicon.ico', nameEn: 'Stable Diffusion', descriptionEn: 'Open-source image generation model by Stability AI. Can be run locally with full customization.', nameZh: 'Stable Diffusion', descriptionZh: 'Stability AI的开源图像生成模型。可在本地运行并完全自定义。', status: 'published' },
{ id: 'tool_leonardo', slug: 'leonardo-ai', domain: 'leonardo.ai', websiteUrl: 'https://leonardo.ai', logoUrl: 'https://leonardo.ai/favicon.ico', nameEn: 'Leonardo AI', descriptionEn: 'AI image generator with fine-tuned models for game assets, concept art, and consistent character generation.', nameZh: 'Leonardo AI', descriptionZh: 'AI图像生成器,提供针对游戏资产、概念艺术和一致性角色生成的微调模型。', status: 'published' },
{ id: 'tool_ideogram', slug: 'ideogram', domain: 'ideogram.ai', websiteUrl: 'https://ideogram.ai', logoUrl: 'https://ideogram.ai/favicon.ico', nameEn: 'Ideogram', descriptionEn: 'AI image generator excelling at rendering text within images accurately. Great for logos and posters.', nameZh: 'Ideogram', descriptionZh: 'AI图像生成器,擅长准确渲染图像中的文字。适合制作Logo和海报。', status: 'published' },
{ id: 'tool_playground', slug: 'playground-ai', domain: 'playground.com', websiteUrl: 'https://playground.com', logoUrl: 'https://playground.com/favicon.ico', nameEn: 'Playground AI', descriptionEn: 'Free AI image generator with powerful editing features. Great for beginners with intuitive interface.', nameZh: 'Playground AI', descriptionZh: '免费AI图像生成器,具有强大的编辑功能。界面直观,适合新手。', status: 'published' },
{ id: 'tool_canva_ai', slug: 'canva-ai', domain: 'canva.com', websiteUrl: 'https://canva.com', logoUrl: 'https://canva.com/favicon.ico', nameEn: 'Canva AI', descriptionEn: 'AI-powered design features within Canva including Magic Design, text-to-image, and background removal.', nameZh: 'Canva AI', descriptionZh: 'Canva中的AI设计功能,包括Magic Design、文生图和背景移除。', status: 'published' },
{ id: 'tool_adobe_firefly', slug: 'adobe-firefly', domain: 'firefly.adobe.com', websiteUrl: 'https://firefly.adobe.com', logoUrl: 'https://firefly.adobe.com/favicon.ico', nameEn: 'Adobe Firefly', descriptionEn: "Adobe's generative AI for creative professionals. Trained on licensed content for commercial safety.", nameZh: 'Adobe Firefly', descriptionZh: 'Adobe面向创意专业人士的生成式AI。在授权内容上训练,商用安全。', status: 'published' },
// AI 视频 (6个)
{ id: 'tool_runway', slug: 'runway', domain: 'runway.com', websiteUrl: 'https://runway.com', logoUrl: 'https://runway.com/favicon.ico', nameEn: 'Runway', descriptionEn: 'AI video generation and editing platform. Gen-2 creates videos from text or images with cinematic quality.', nameZh: 'Runway', descriptionZh: 'AI视频生成和编辑平台。Gen-2可从文本或图像创建电影级质量的视频。', status: 'published' },
{ id: 'tool_pika', slug: 'pika', domain: 'pika.art', websiteUrl: 'https://pika.art', logoUrl: 'https://pika.art/favicon.ico', nameEn: 'Pika', descriptionEn: 'AI video generator that creates and edits videos from text prompts. Known for smooth animations and effects.', nameZh: 'Pika', descriptionZh: 'AI视频生成器,从文本提示创建和编辑视频。以流畅的动画和效果著称。', status: 'published' },
{ id: 'tool_heygen', slug: 'heygen', domain: 'heygen.com', websiteUrl: 'https://heygen.com', logoUrl: 'https://heygen.com/favicon.ico', nameEn: 'HeyGen', descriptionEn: 'AI video generator for creating professional videos with AI avatars. Perfect for marketing and training content.', nameZh: 'HeyGen', descriptionZh: 'AI视频生成器,用AI数字人创建专业视频。适合营销和培训内容。', status: 'published' },
{ id: 'tool_synthesia', slug: 'synthesia', domain: 'synthesia.io', websiteUrl: 'https://synthesia.io', logoUrl: 'https://synthesia.io/favicon.ico', nameEn: 'Synthesia', descriptionEn: 'Create AI videos with realistic avatars in 120+ languages. Popular for corporate training and marketing.', nameZh: 'Synthesia', descriptionZh: '用逼真数字人创建120多种语言的AI视频。在企业培训和营销领域广受欢迎。', status: 'published' },
{ id: 'tool_luma', slug: 'luma-ai', domain: 'lumalabs.ai', websiteUrl: 'https://lumalabs.ai', logoUrl: 'https://lumalabs.ai/favicon.ico', nameEn: 'Luma AI', descriptionEn: 'AI video generation with Dream Machine. Creates cinematic videos from text and images with impressive quality.', nameZh: 'Luma AI', descriptionZh: 'Dream Machine AI视频生成。从文本和图像创建高质量的电影级视频。', status: 'published' },
{ id: 'tool_kling', slug: 'kling-ai', domain: 'klingai.com', websiteUrl: 'https://klingai.com', logoUrl: 'https://klingai.com/favicon.ico', nameEn: 'Kling AI', descriptionEn: "Kuaishou's AI video generator with impressive motion and physics understanding. Creates realistic videos.", nameZh: 'Kling AI', descriptionZh: '快手的AI视频生成器,具有出色的运动和物理理解能力。创建逼真视频。', status: 'published' },
// AI 音频与音乐 (5个)
{ id: 'tool_elevenlabs', slug: 'elevenlabs', domain: 'elevenlabs.io', websiteUrl: 'https://elevenlabs.io', logoUrl: 'https://elevenlabs.io/favicon.ico', nameEn: 'ElevenLabs', descriptionEn: 'Leading AI voice synthesis platform. Creates ultra-realistic voiceovers, dubbing, and voice cloning.', nameZh: 'ElevenLabs', descriptionZh: '领先的AI语音合成平台。创建超逼真的配音、翻译配音和声音克隆。', status: 'published' },
{ id: 'tool_suno', slug: 'suno', domain: 'suno.com', websiteUrl: 'https://suno.com', logoUrl: 'https://suno.com/favicon.ico', nameEn: 'Suno', descriptionEn: 'AI music generator that creates complete songs with vocals from text prompts. Revolutionary for music creation.', nameZh: 'Suno', descriptionZh: 'AI音乐生成器,从文本提示创建带人声的完整歌曲。音乐创作的革命性工具。', status: 'published' },
{ id: 'tool_udio', slug: 'udio', domain: 'udio.com', websiteUrl: 'https://udio.com', logoUrl: 'https://udio.com/favicon.ico', nameEn: 'Udio', descriptionEn: 'AI music generation platform creating studio-quality songs with vocals. Strong competitor to Suno.', nameZh: 'Udio', descriptionZh: 'AI音乐生成平台,创建带人声的录音室品质歌曲。Suno的强劲竞争对手。', status: 'published' },
{ id: 'tool_murf', slug: 'murf', domain: 'murf.ai', websiteUrl: 'https://murf.ai', logoUrl: 'https://murf.ai/favicon.ico', nameEn: 'Murf', descriptionEn: 'AI voice generator for creating professional voiceovers for videos, podcasts, and presentations.', nameZh: 'Murf', descriptionZh: 'AI语音生成器,为视频、播客和演示文稿创建专业配音。', status: 'published' },
{ id: 'tool_descript', slug: 'descript', domain: 'descript.com', websiteUrl: 'https://descript.com', logoUrl: 'https://descript.com/favicon.ico', nameEn: 'Descript', descriptionEn: 'AI-powered audio and video editor. Edit audio by editing text transcripts. Includes Overdub voice cloning.', nameZh: 'Descript', descriptionZh: 'AI驱动的音视频编辑器。通过编辑文字稿来编辑音频。包含Overdub声音克隆功能。', status: 'published' },
// AI 编程 (6个)
{ id: 'tool_github_copilot', slug: 'github-copilot', domain: 'github.com/features/copilot', websiteUrl: 'https://github.com/features/copilot', logoUrl: 'https://github.com/favicon.ico', nameEn: 'GitHub Copilot', descriptionEn: 'AI pair programmer by GitHub and OpenAI. Suggests code completions, functions, and entire files in your IDE.', nameZh: 'GitHub Copilot', descriptionZh: 'GitHub和OpenAI的AI结对编程助手。在IDE中建议代码补全、函数和完整文件。', status: 'published' },
{ id: 'tool_cursor', slug: 'cursor', domain: 'cursor.sh', websiteUrl: 'https://cursor.sh', logoUrl: 'https://cursor.sh/favicon.ico', nameEn: 'Cursor', descriptionEn: 'AI-first code editor built on VS Code. Deep AI integration for code generation, editing, and chat.', nameZh: 'Cursor', descriptionZh: '以AI为核心的代码编辑器,基于VS Code构建。深度AI集成,支持代码生成、编辑和对话。', status: 'published' },
{ id: 'tool_replit', slug: 'replit', domain: 'replit.com', websiteUrl: 'https://replit.com', logoUrl: 'https://replit.com/favicon.ico', nameEn: 'Replit AI', descriptionEn: 'Browser-based IDE with AI assistant. Code, deploy, and collaborate with AI-powered code generation.', nameZh: 'Replit AI', descriptionZh: '基于浏览器的IDE,内置AI助手。通过AI驱动的代码生成进行编码、部署和协作。', status: 'published' },
{ id: 'tool_codeium', slug: 'codeium', domain: 'codeium.com', websiteUrl: 'https://codeium.com', logoUrl: 'https://codeium.com/favicon.ico', nameEn: 'Codeium', descriptionEn: 'Free AI coding assistant supporting 70+ languages. Fast completions with IDE extensions for major editors.', nameZh: 'Codeium', descriptionZh: '免费AI编程助手,支持70多种语言。快速补全,为主流编辑器提供IDE插件。', status: 'published' },
{ id: 'tool_tabnine', slug: 'tabnine', domain: 'tabnine.com', websiteUrl: 'https://tabnine.com', logoUrl: 'https://tabnine.com/favicon.ico', nameEn: 'Tabnine', descriptionEn: 'AI code assistant with enterprise-grade privacy. Train on your codebase for personalized suggestions.', nameZh: 'Tabnine', descriptionZh: '企业级隐私的AI代码助手。在你的代码库上训练,提供个性化建议。', status: 'published' },
{ id: 'tool_claude_code', slug: 'claude-code', domain: 'claude.ai/code', websiteUrl: 'https://claude.ai/code', logoUrl: 'https://claude.ai/favicon.ico', nameEn: 'Claude Code', descriptionEn: "Anthropic's agentic coding tool. Natural language coding with full project context understanding.", nameZh: 'Claude Code', descriptionZh: 'Anthropic的代理式编程工具。支持自然语言编程,具有完整的项目上下文理解能力。', status: 'published' },
// AI 效率工具 (4个)
{ id: 'tool_otter', slug: 'otter-ai', domain: 'otter.ai', websiteUrl: 'https://otter.ai', logoUrl: 'https://otter.ai/favicon.ico', nameEn: 'Otter.ai', descriptionEn: 'AI meeting assistant that transcribes, summarizes, and creates action items from meetings automatically.', nameZh: 'Otter.ai', descriptionZh: 'AI会议助手,自动转录、总结会议并创建待办事项。', status: 'published' },
{ id: 'tool_fireflies', slug: 'fireflies', domain: 'fireflies.ai', websiteUrl: 'https://fireflies.ai', logoUrl: 'https://fireflies.ai/favicon.ico', nameEn: 'Fireflies.ai', descriptionEn: 'AI notetaker for meetings. Records, transcribes, and summarizes meetings across all major platforms.', nameZh: 'Fireflies.ai', descriptionZh: '会议AI笔记助手。在所有主流平台上录制、转录和总结会议。', status: 'published' },
{ id: 'tool_mem', slug: 'mem-ai', domain: 'mem.ai', websiteUrl: 'https://mem.ai', logoUrl: 'https://mem.ai/favicon.ico', nameEn: 'Mem', descriptionEn: 'AI-powered note-taking app that organizes and surfaces relevant information automatically.', nameZh: 'Mem', descriptionZh: 'AI驱动的笔记应用,自动组织和呈现相关信息。', status: 'published' },
{ id: 'tool_motion', slug: 'motion', domain: 'usemotion.com', websiteUrl: 'https://usemotion.com', logoUrl: 'https://usemotion.com/favicon.ico', nameEn: 'Motion', descriptionEn: 'AI calendar and project management. Automatically schedules tasks and meetings for optimal productivity.', nameZh: 'Motion', descriptionZh: 'AI日历和项目管理工具。自动安排任务和会议以实现最佳生产力。', status: 'published' },
// AI 研究与分析 (3个)
{ id: 'tool_consensus', slug: 'consensus', domain: 'consensus.app', websiteUrl: 'https://consensus.app', logoUrl: 'https://consensus.app/favicon.ico', nameEn: 'Consensus', descriptionEn: 'AI-powered academic search engine. Find and understand scientific research with AI summaries.', nameZh: 'Consensus', descriptionZh: 'AI驱动的学术搜索引擎。通过AI摘要查找和理解科学研究。', status: 'published' },
{ id: 'tool_elicit', slug: 'elicit', domain: 'elicit.com', websiteUrl: 'https://elicit.com', logoUrl: 'https://elicit.com/favicon.ico', nameEn: 'Elicit', descriptionEn: 'AI research assistant that helps find, summarize, and extract data from academic papers.', nameZh: 'Elicit', descriptionZh: 'AI研究助手,帮助查找、总结和提取学术论文中的数据。', status: 'published' },
{ id: 'tool_scite', slug: 'scite', domain: 'scite.ai', websiteUrl: 'https://scite.ai', logoUrl: 'https://scite.ai/favicon.ico', nameEn: 'Scite', descriptionEn: 'AI tool for evaluating scientific claims. Shows how papers cite each other with supporting or contrasting evidence.', nameZh: 'Scite', descriptionZh: 'AI科学声明评估工具。显示论文如何相互引用,包含支持或对比证据。', status: 'published' },
// AI 设计 (3个)
{ id: 'tool_figma_ai', slug: 'figma-ai', domain: 'figma.com', websiteUrl: 'https://figma.com', logoUrl: 'https://figma.com/favicon.ico', nameEn: 'Figma AI', descriptionEn: 'AI features in Figma for generating designs, renaming layers, and creating content automatically.', nameZh: 'Figma AI', descriptionZh: 'Figma中的AI功能,用于生成设计、重命名图层和自动创建内容。', status: 'published' },
{ id: 'tool_uizard', slug: 'uizard', domain: 'uizard.io', websiteUrl: 'https://uizard.io', logoUrl: 'https://uizard.io/favicon.ico', nameEn: 'Uizard', descriptionEn: 'AI-powered UI design tool. Transform sketches and screenshots into editable designs.', nameZh: 'Uizard', descriptionZh: 'AI驱动的UI设计工具。将草图和截图转换为可编辑的设计。', status: 'published' },
{ id: 'tool_magician', slug: 'magician', domain: 'magician.design', websiteUrl: 'https://magician.design', logoUrl: 'https://magician.design/favicon.ico', nameEn: 'Magician', descriptionEn: 'AI design assistant plugin for Figma. Generate icons, images, and copy with AI.', nameZh: 'Magician', descriptionZh: 'Figma的AI设计助手插件。用AI生成图标、图像和文案。', status: 'published' },
];
// 工具-分类关联数据
const toolCategoryData = [
// 聊天机器人
{ toolId: 'tool_chatgpt', categoryId: 'cat_chatbot' },
{ toolId: 'tool_claude', categoryId: 'cat_chatbot' },
{ toolId: 'tool_gemini', categoryId: 'cat_chatbot' },
{ toolId: 'tool_perplexity', categoryId: 'cat_chatbot' },
{ toolId: 'tool_perplexity', categoryId: 'cat_research' },
{ toolId: 'tool_poe', categoryId: 'cat_chatbot' },
{ toolId: 'tool_character', categoryId: 'cat_chatbot' },
{ toolId: 'tool_coze', categoryId: 'cat_chatbot' },
{ toolId: 'tool_pi', categoryId: 'cat_chatbot' },
// 写作
{ toolId: 'tool_jasper', categoryId: 'cat_writing' },
{ toolId: 'tool_jasper', categoryId: 'cat_business' },
{ toolId: 'tool_copy_ai', categoryId: 'cat_writing' },
{ toolId: 'tool_copy_ai', categoryId: 'cat_business' },
{ toolId: 'tool_writesonic', categoryId: 'cat_writing' },
{ toolId: 'tool_notion_ai', categoryId: 'cat_writing' },
{ toolId: 'tool_notion_ai', categoryId: 'cat_productivity' },
{ toolId: 'tool_grammarly', categoryId: 'cat_writing' },
{ toolId: 'tool_quillbot', categoryId: 'cat_writing' },
{ toolId: 'tool_rytr', categoryId: 'cat_writing' },
// 图像生成
{ toolId: 'tool_midjourney', categoryId: 'cat_image' },
{ toolId: 'tool_dalle', categoryId: 'cat_image' },
{ toolId: 'tool_stable_diffusion', categoryId: 'cat_image' },
{ toolId: 'tool_leonardo', categoryId: 'cat_image' },
{ toolId: 'tool_ideogram', categoryId: 'cat_image' },
{ toolId: 'tool_playground', categoryId: 'cat_image' },
{ toolId: 'tool_canva_ai', categoryId: 'cat_image' },
{ toolId: 'tool_canva_ai', categoryId: 'cat_design' },
{ toolId: 'tool_adobe_firefly', categoryId: 'cat_image' },
{ toolId: 'tool_adobe_firefly', categoryId: 'cat_design' },
// 视频
{ toolId: 'tool_runway', categoryId: 'cat_video' },
{ toolId: 'tool_pika', categoryId: 'cat_video' },
{ toolId: 'tool_heygen', categoryId: 'cat_video' },
{ toolId: 'tool_heygen', categoryId: 'cat_business' },
{ toolId: 'tool_synthesia', categoryId: 'cat_video' },
{ toolId: 'tool_synthesia', categoryId: 'cat_business' },
{ toolId: 'tool_luma', categoryId: 'cat_video' },
{ toolId: 'tool_kling', categoryId: 'cat_video' },
// 音频
{ toolId: 'tool_elevenlabs', categoryId: 'cat_audio' },
{ toolId: 'tool_suno', categoryId: 'cat_audio' },
{ toolId: 'tool_udio', categoryId: 'cat_audio' },
{ toolId: 'tool_murf', categoryId: 'cat_audio' },
{ toolId: 'tool_descript', categoryId: 'cat_audio' },
{ toolId: 'tool_descript', categoryId: 'cat_video' },
// 编程
{ toolId: 'tool_github_copilot', categoryId: 'cat_coding' },
{ toolId: 'tool_cursor', categoryId: 'cat_coding' },
{ toolId: 'tool_replit', categoryId: 'cat_coding' },
{ toolId: 'tool_codeium', categoryId: 'cat_coding' },
{ toolId: 'tool_tabnine', categoryId: 'cat_coding' },
{ toolId: 'tool_claude_code', categoryId: 'cat_coding' },
// 效率
{ toolId: 'tool_otter', categoryId: 'cat_productivity' },
{ toolId: 'tool_fireflies', categoryId: 'cat_productivity' },
{ toolId: 'tool_mem', categoryId: 'cat_productivity' },
{ toolId: 'tool_motion', categoryId: 'cat_productivity' },
// 研究
{ toolId: 'tool_consensus', categoryId: 'cat_research' },
{ toolId: 'tool_elicit', categoryId: 'cat_research' },
{ toolId: 'tool_scite', categoryId: 'cat_research' },
// 设计
{ toolId: 'tool_figma_ai', categoryId: 'cat_design' },
{ toolId: 'tool_uizard', categoryId: 'cat_design' },
{ toolId: 'tool_magician', categoryId: 'cat_design' },
];
// 工具-标签关联数据
const toolTagData = [
// ChatGPT
{ toolId: 'tool_chatgpt', tagId: 'tag_freemium' },
{ toolId: 'tool_chatgpt', tagId: 'tag_gpt' },
{ toolId: 'tool_chatgpt', tagId: 'tag_llm' },
{ toolId: 'tool_chatgpt', tagId: 'tag_api' },
{ toolId: 'tool_chatgpt', tagId: 'tag_popular' },
{ toolId: 'tool_chatgpt', tagId: 'tag_mobile' },
{ toolId: 'tool_chatgpt', tagId: 'tag_text' },
// Claude
{ toolId: 'tool_claude', tagId: 'tag_freemium' },
{ toolId: 'tool_claude', tagId: 'tag_claude' },
{ toolId: 'tool_claude', tagId: 'tag_llm' },
{ toolId: 'tool_claude', tagId: 'tag_api' },
{ toolId: 'tool_claude', tagId: 'tag_popular' },
{ toolId: 'tool_claude', tagId: 'tag_text' },
{ toolId: 'tool_claude', tagId: 'tag_code_out' },
// Gemini
{ toolId: 'tool_gemini', tagId: 'tag_free' },
{ toolId: 'tool_gemini', tagId: 'tag_llm' },
{ toolId: 'tool_gemini', tagId: 'tag_api' },
{ toolId: 'tool_gemini', tagId: 'tag_mobile' },
{ toolId: 'tool_gemini', tagId: 'tag_text' },
// Perplexity
{ toolId: 'tool_perplexity', tagId: 'tag_freemium' },
{ toolId: 'tool_perplexity', tagId: 'tag_llm' },
{ toolId: 'tool_perplexity', tagId: 'tag_realtime' },
{ toolId: 'tool_perplexity', tagId: 'tag_trending' },
{ toolId: 'tool_perplexity', tagId: 'tag_text' },
// Poe
{ toolId: 'tool_poe', tagId: 'tag_freemium' },
{ toolId: 'tool_poe', tagId: 'tag_llm' },
{ toolId: 'tool_poe', tagId: 'tag_beginner' },
{ toolId: 'tool_poe', tagId: 'tag_mobile' },
// Character.AI
{ toolId: 'tool_character', tagId: 'tag_free' },
{ toolId: 'tool_character', tagId: 'tag_llm' },
{ toolId: 'tool_character', tagId: 'tag_creator' },
{ toolId: 'tool_character', tagId: 'tag_mobile' },
// Coze
{ toolId: 'tool_coze', tagId: 'tag_free' },
{ toolId: 'tool_coze', tagId: 'tag_llm' },
{ toolId: 'tool_coze', tagId: 'tag_api' },
{ toolId: 'tool_coze', tagId: 'tag_developer' },
// Pi
{ toolId: 'tool_pi', tagId: 'tag_free' },
{ toolId: 'tool_pi', tagId: 'tag_llm' },
{ toolId: 'tool_pi', tagId: 'tag_beginner' },
{ toolId: 'tool_pi', tagId: 'tag_mobile' },
// Jasper
{ toolId: 'tool_jasper', tagId: 'tag_paid' },
{ toolId: 'tool_jasper', tagId: 'tag_gpt' },
{ toolId: 'tool_jasper', tagId: 'tag_enterprise' },
{ toolId: 'tool_jasper', tagId: 'tag_marketer' },
{ toolId: 'tool_jasper', tagId: 'tag_text' },
// Copy.ai
{ toolId: 'tool_copy_ai', tagId: 'tag_freemium' },
{ toolId: 'tool_copy_ai', tagId: 'tag_gpt' },
{ toolId: 'tool_copy_ai', tagId: 'tag_marketer' },
{ toolId: 'tool_copy_ai', tagId: 'tag_text' },
// Writesonic
{ toolId: 'tool_writesonic', tagId: 'tag_freemium' },
{ toolId: 'tool_writesonic', tagId: 'tag_gpt' },
{ toolId: 'tool_writesonic', tagId: 'tag_marketer' },
{ toolId: 'tool_writesonic', tagId: 'tag_text' },
// Notion AI
{ toolId: 'tool_notion_ai', tagId: 'tag_paid' },
{ toolId: 'tool_notion_ai', tagId: 'tag_collaborative' },
{ toolId: 'tool_notion_ai', tagId: 'tag_text' },
{ toolId: 'tool_notion_ai', tagId: 'tag_popular' },
// Grammarly
{ toolId: 'tool_grammarly', tagId: 'tag_freemium' },
{ toolId: 'tool_grammarly', tagId: 'tag_browser' },
{ toolId: 'tool_grammarly', tagId: 'tag_desktop' },
{ toolId: 'tool_grammarly', tagId: 'tag_popular' },
{ toolId: 'tool_grammarly', tagId: 'tag_text' },
// QuillBot
{ toolId: 'tool_quillbot', tagId: 'tag_freemium' },
{ toolId: 'tool_quillbot', tagId: 'tag_student' },
{ toolId: 'tool_quillbot', tagId: 'tag_browser' },
{ toolId: 'tool_quillbot', tagId: 'tag_text' },
// Rytr
{ toolId: 'tool_rytr', tagId: 'tag_freemium' },
{ toolId: 'tool_rytr', tagId: 'tag_beginner' },
{ toolId: 'tool_rytr', tagId: 'tag_text' },
// Midjourney
{ toolId: 'tool_midjourney', tagId: 'tag_paid' },
{ toolId: 'tool_midjourney', tagId: 'tag_diffusion' },
{ toolId: 'tool_midjourney', tagId: 'tag_creator' },
{ toolId: 'tool_midjourney', tagId: 'tag_popular' },
{ toolId: 'tool_midjourney', tagId: 'tag_image_out' },
{ toolId: 'tool_midjourney', tagId: 'tag_editors_choice' },
// DALL-E
{ toolId: 'tool_dalle', tagId: 'tag_freemium' },
{ toolId: 'tool_dalle', tagId: 'tag_diffusion' },
{ toolId: 'tool_dalle', tagId: 'tag_api' },
{ toolId: 'tool_dalle', tagId: 'tag_image_out' },
{ toolId: 'tool_dalle', tagId: 'tag_popular' },
// Stable Diffusion
{ toolId: 'tool_stable_diffusion', tagId: 'tag_free' },
{ toolId: 'tool_stable_diffusion', tagId: 'tag_open_source' },
{ toolId: 'tool_stable_diffusion', tagId: 'tag_diffusion' },
{ toolId: 'tool_stable_diffusion', tagId: 'tag_developer' },
{ toolId: 'tool_stable_diffusion', tagId: 'tag_image_out' },
// Leonardo AI
{ toolId: 'tool_leonardo', tagId: 'tag_freemium' },
{ toolId: 'tool_leonardo', tagId: 'tag_diffusion' },
{ toolId: 'tool_leonardo', tagId: 'tag_creator' },
{ toolId: 'tool_leonardo', tagId: 'tag_image_out' },
// Ideogram
{ toolId: 'tool_ideogram', tagId: 'tag_freemium' },
{ toolId: 'tool_ideogram', tagId: 'tag_diffusion' },
{ toolId: 'tool_ideogram', tagId: 'tag_image_out' },
{ toolId: 'tool_ideogram', tagId: 'tag_trending' },
// Playground AI
{ toolId: 'tool_playground', tagId: 'tag_free' },
{ toolId: 'tool_playground', tagId: 'tag_diffusion' },
{ toolId: 'tool_playground', tagId: 'tag_beginner' },
{ toolId: 'tool_playground', tagId: 'tag_image_out' },
// Canva AI
{ toolId: 'tool_canva_ai', tagId: 'tag_freemium' },
{ toolId: 'tool_canva_ai', tagId: 'tag_beginner' },
{ toolId: 'tool_canva_ai', tagId: 'tag_popular' },
{ toolId: 'tool_canva_ai', tagId: 'tag_image_out' },
// Adobe Firefly
{ toolId: 'tool_adobe_firefly', tagId: 'tag_freemium' },
{ toolId: 'tool_adobe_firefly', tagId: 'tag_diffusion' },
{ toolId: 'tool_adobe_firefly', tagId: 'tag_enterprise' },
{ toolId: 'tool_adobe_firefly', tagId: 'tag_image_out' },
// Runway
{ toolId: 'tool_runway', tagId: 'tag_freemium' },
{ toolId: 'tool_runway', tagId: 'tag_diffusion' },
{ toolId: 'tool_runway', tagId: 'tag_creator' },
{ toolId: 'tool_runway', tagId: 'tag_video_out' },
{ toolId: 'tool_runway', tagId: 'tag_editors_choice' },
// Pika
{ toolId: 'tool_pika', tagId: 'tag_freemium' },
{ toolId: 'tool_pika', tagId: 'tag_diffusion' },
{ toolId: 'tool_pika', tagId: 'tag_video_out' },
{ toolId: 'tool_pika', tagId: 'tag_trending' },
// HeyGen
{ toolId: 'tool_heygen', tagId: 'tag_paid' },
{ toolId: 'tool_heygen', tagId: 'tag_enterprise' },
{ toolId: 'tool_heygen', tagId: 'tag_marketer' },
{ toolId: 'tool_heygen', tagId: 'tag_video_out' },
// Synthesia
{ toolId: 'tool_synthesia', tagId: 'tag_paid' },
{ toolId: 'tool_synthesia', tagId: 'tag_enterprise' },
{ toolId: 'tool_synthesia', tagId: 'tag_video_out' },
{ toolId: 'tool_synthesia', tagId: 'tag_popular' },
// Luma AI
{ toolId: 'tool_luma', tagId: 'tag_freemium' },
{ toolId: 'tool_luma', tagId: 'tag_diffusion' },
{ toolId: 'tool_luma', tagId: 'tag_video_out' },
{ toolId: 'tool_luma', tagId: 'tag_trending' },
// Kling AI
{ toolId: 'tool_kling', tagId: 'tag_freemium' },
{ toolId: 'tool_kling', tagId: 'tag_diffusion' },
{ toolId: 'tool_kling', tagId: 'tag_video_out' },
{ toolId: 'tool_kling', tagId: 'tag_new' },
// ElevenLabs
{ toolId: 'tool_elevenlabs', tagId: 'tag_freemium' },
{ toolId: 'tool_elevenlabs', tagId: 'tag_api' },
{ toolId: 'tool_elevenlabs', tagId: 'tag_creator' },
{ toolId: 'tool_elevenlabs', tagId: 'tag_audio_out' },
{ toolId: 'tool_elevenlabs', tagId: 'tag_popular' },
// Suno
{ toolId: 'tool_suno', tagId: 'tag_freemium' },
{ toolId: 'tool_suno', tagId: 'tag_creator' },
{ toolId: 'tool_suno', tagId: 'tag_audio_out' },
{ toolId: 'tool_suno', tagId: 'tag_trending' },
{ toolId: 'tool_suno', tagId: 'tag_editors_choice' },
// Udio
{ toolId: 'tool_udio', tagId: 'tag_freemium' },
{ toolId: 'tool_udio', tagId: 'tag_creator' },
{ toolId: 'tool_udio', tagId: 'tag_audio_out' },
{ toolId: 'tool_udio', tagId: 'tag_new' },
// Murf
{ toolId: 'tool_murf', tagId: 'tag_freemium' },
{ toolId: 'tool_murf', tagId: 'tag_marketer' },
{ toolId: 'tool_murf', tagId: 'tag_audio_out' },
// Descript
{ toolId: 'tool_descript', tagId: 'tag_freemium' },
{ toolId: 'tool_descript', tagId: 'tag_creator' },
{ toolId: 'tool_descript', tagId: 'tag_audio_out' },
{ toolId: 'tool_descript', tagId: 'tag_video_out' },
{ toolId: 'tool_descript', tagId: 'tag_desktop' },
// GitHub Copilot
{ toolId: 'tool_github_copilot', tagId: 'tag_paid' },
{ toolId: 'tool_github_copilot', tagId: 'tag_gpt' },
{ toolId: 'tool_github_copilot', tagId: 'tag_developer' },
{ toolId: 'tool_github_copilot', tagId: 'tag_code_out' },
{ toolId: 'tool_github_copilot', tagId: 'tag_popular' },
{ toolId: 'tool_github_copilot', tagId: 'tag_editors_choice' },
// Cursor
{ toolId: 'tool_cursor', tagId: 'tag_freemium' },
{ toolId: 'tool_cursor', tagId: 'tag_claude' },
{ toolId: 'tool_cursor', tagId: 'tag_developer' },
{ toolId: 'tool_cursor', tagId: 'tag_code_out' },
{ toolId: 'tool_cursor', tagId: 'tag_trending' },
{ toolId: 'tool_cursor', tagId: 'tag_desktop' },
// Replit
{ toolId: 'tool_replit', tagId: 'tag_freemium' },
{ toolId: 'tool_replit', tagId: 'tag_llm' },
{ toolId: 'tool_replit', tagId: 'tag_developer' },
{ toolId: 'tool_replit', tagId: 'tag_beginner' },
{ toolId: 'tool_replit', tagId: 'tag_code_out' },
// Codeium
{ toolId: 'tool_codeium', tagId: 'tag_free' },
{ toolId: 'tool_codeium', tagId: 'tag_llm' },
{ toolId: 'tool_codeium', tagId: 'tag_developer' },
{ toolId: 'tool_codeium', tagId: 'tag_code_out' },
// Tabnine
{ toolId: 'tool_tabnine', tagId: 'tag_freemium' },
{ toolId: 'tool_tabnine', tagId: 'tag_enterprise' },
{ toolId: 'tool_tabnine', tagId: 'tag_developer' },
{ toolId: 'tool_tabnine', tagId: 'tag_code_out' },
// Claude Code
{ toolId: 'tool_claude_code', tagId: 'tag_paid' },
{ toolId: 'tool_claude_code', tagId: 'tag_claude' },
{ toolId: 'tool_claude_code', tagId: 'tag_developer' },
{ toolId: 'tool_claude_code', tagId: 'tag_code_out' },
{ toolId: 'tool_claude_code', tagId: 'tag_new' },
// Otter.ai
{ toolId: 'tool_otter', tagId: 'tag_freemium' },
{ toolId: 'tool_otter', tagId: 'tag_collaborative' },
{ toolId: 'tool_otter', tagId: 'tag_realtime' },
{ toolId: 'tool_otter', tagId: 'tag_text' },
{ toolId: 'tool_otter', tagId: 'tag_mobile' },
// Fireflies.ai
{ toolId: 'tool_fireflies', tagId: 'tag_freemium' },
{ toolId: 'tool_fireflies', tagId: 'tag_collaborative' },
{ toolId: 'tool_fireflies', tagId: 'tag_enterprise' },
{ toolId: 'tool_fireflies', tagId: 'tag_text' },
// Mem
{ toolId: 'tool_mem', tagId: 'tag_freemium' },
{ toolId: 'tool_mem', tagId: 'tag_llm' },
{ toolId: 'tool_mem', tagId: 'tag_text' },
// Motion
{ toolId: 'tool_motion', tagId: 'tag_paid' },
{ toolId: 'tool_motion', tagId: 'tag_collaborative' },
// Consensus
{ toolId: 'tool_consensus', tagId: 'tag_freemium' },
{ toolId: 'tool_consensus', tagId: 'tag_llm' },
{ toolId: 'tool_consensus', tagId: 'tag_student' },
{ toolId: 'tool_consensus', tagId: 'tag_text' },
// Elicit
{ toolId: 'tool_elicit', tagId: 'tag_freemium' },
{ toolId: 'tool_elicit', tagId: 'tag_llm' },
{ toolId: 'tool_elicit', tagId: 'tag_student' },
{ toolId: 'tool_elicit', tagId: 'tag_text' },
// Scite
{ toolId: 'tool_scite', tagId: 'tag_paid' },
{ toolId: 'tool_scite', tagId: 'tag_student' },
{ toolId: 'tool_scite', tagId: 'tag_text' },
// Figma AI
{ toolId: 'tool_figma_ai', tagId: 'tag_paid' },
{ toolId: 'tool_figma_ai', tagId: 'tag_collaborative' },
{ toolId: 'tool_figma_ai', tagId: 'tag_popular' },
{ toolId: 'tool_figma_ai', tagId: 'tag_image_out' },
// Uizard
{ toolId: 'tool_uizard', tagId: 'tag_freemium' },
{ toolId: 'tool_uizard', tagId: 'tag_beginner' },
{ toolId: 'tool_uizard', tagId: 'tag_image_out' },
// Magician
{ toolId: 'tool_magician', tagId: 'tag_paid' },
{ toolId: 'tool_magician', tagId: 'tag_image_out' },
{ toolId: 'tool_magician', tagId: 'tag_text' },
];
// ========== 主函数 ==========
async function seedTools() {
if (!process.env.DATABASE_URL) {
console.error("❌ 未找到 DATABASE_URL 环境变量");
console.log("请确保 .env.local 文件中设置了 DATABASE_URL");
process.exit(1);
}
const queryClient = postgres(process.env.DATABASE_URL);
const db = drizzle(queryClient);
try {
console.log("🚀 开始导入 AI 工具种子数据...\n");
// 1. 导入分类
console.log("📁 导入分类数据 (10 个)...");
for (const cat of categoriesData) {
await db.insert(category).values(cat).onConflictDoNothing();
}
console.log("✅ 分类数据导入完成\n");
// 2. 导入标签
console.log("🏷️ 导入标签数据 (30 个)...");
for (const t of tagsData) {
await db.insert(tag).values(t).onConflictDoNothing();
}
console.log("✅ 标签数据导入完成\n");
// 3. 导入工具(pricing 从定价标签推导,featured 从"编辑推荐"标签推导)
console.log("🔧 导入工具数据 (50 个)...");
const pricingTagMap: Record<string, string> = {
tag_free: "free",
tag_freemium: "freemium",
tag_paid: "paid",
};
const pricingByTool = new Map<string, string>();
const featuredToolIds = new Set<string>();
for (const tt of toolTagData) {
const pricing = pricingTagMap[tt.tagId];
if (pricing) pricingByTool.set(tt.toolId, pricing);
if (tt.tagId === "tag_editors_choice") featuredToolIds.add(tt.toolId);
}
for (const t of toolsData) {
await db
.insert(tool)
.values({
...t,
pricing: pricingByTool.get(t.id) ?? null,
featured: featuredToolIds.has(t.id),
})
.onConflictDoNothing();
}
console.log("✅ 工具数据导入完成\n");
// 4. 导入工具-分类关联
console.log("🔗 导入工具-分类关联...");
for (const tc of toolCategoryData) {
await db.insert(toolCategory).values(tc).onConflictDoNothing();
}
console.log("✅ 工具-分类关联导入完成\n");
// 5. 导入工具-标签关联
console.log("🏷️ 导入工具-标签关联...");
for (const tt of toolTagData) {
await db.insert(toolTag).values(tt).onConflictDoNothing();
}
console.log("✅ 工具-标签关联导入完成\n");
console.log("====================================");
console.log("🎉 所有种子数据导入成功!");
console.log("====================================");
console.log("\n📊 数据统计:");
console.log(` - 分类: ${categoriesData.length} 个`);
console.log(` - 标签: ${tagsData.length} 个`);
console.log(` - 工具: ${toolsData.length} 个`);
console.log(` - 工具-分类关联: ${toolCategoryData.length} 条`);
console.log(` - 工具-标签关联: ${toolTagData.length} 条`);
} catch (error) {
console.error("❌ 导入失败:", error);
await queryClient.end();
process.exit(1);
}
await queryClient.end();
process.exit(0);
}
// 运行脚本
seedTools();
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env tsx
/**
* 管理员初始化脚本
* 用于设置第一个管理员账号
*
* 使用方法:
* 1. 设置环境变量 ADMIN_EMAIL
* 2. 运行: pnpm admin:setup
* 或直接运行: ADMIN_EMAIL=admin@example.com pnpm admin:setup
*/
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { eq } from "drizzle-orm";
import dotenv from "dotenv";
import { resolve } from "path";
import { pgTable, text, timestamp, boolean, integer } from "drizzle-orm/pg-core";
// 加载环境变量
dotenv.config({ path: resolve(process.cwd(), ".env.local") });
// 定义 user 表结构(复制自 schema)
const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
credits: integer("credits").default(0).notNull(),
role: text("role").default("user").notNull(),
banned: boolean("banned").default(false).notNull(),
banReason: text("ban_reason"),
banExpires: timestamp("ban_expires"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
});
async function setupAdmin() {
const adminEmail = process.env.ADMIN_EMAIL;
if (!adminEmail) {
console.error("❌ 请设置环境变量 ADMIN_EMAIL");
console.log("例如: ADMIN_EMAIL=admin@example.com pnpm admin:setup");
process.exit(1);
}
if (!process.env.DATABASE_URL) {
console.error("❌ 未找到 DATABASE_URL 环境变量");
console.log("请确保 .env.local 文件中设置了 DATABASE_URL");
process.exit(1);
}
// 创建数据库连接
const queryClient = postgres(process.env.DATABASE_URL);
const db = drizzle(queryClient);
try {
console.log("🔍 正在查找用户:", adminEmail);
// 检查用户是否存在
const existingUser = await db
.select()
.from(user)
.where(eq(user.email, adminEmail))
.limit(1);
if (existingUser.length === 0) {
console.error(`❌ 用户 ${adminEmail} 不存在`);
console.log("请先使用此邮箱注册一个账号,然后再运行此脚本");
await queryClient.end();
process.exit(1);
}
const currentUser = existingUser[0];
console.log(`✓ 找到用户: ${currentUser.name} (当前角色: ${currentUser.role})`);
if (currentUser.role === "admin") {
console.log(`✅ 用户 ${adminEmail} 已经是管理员`);
await queryClient.end();
process.exit(0);
}
// 更新用户为管理员
console.log("📝 正在更新用户角色...");
await db
.update(user)
.set({
role: "admin",
updatedAt: new Date()
})
.where(eq(user.email, adminEmail));
console.log(`✅ 成功将用户 ${adminEmail} 设置为管理员`);
console.log("🚀 现在你可以访问 /admin 路径来进入管理后台");
} catch (error) {
console.error("❌ 设置管理员失败:", error);
await queryClient.end();
process.exit(1);
}
// 关闭数据库连接
await queryClient.end();
process.exit(0);
}
// 运行脚本
setupAdmin();
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# 启动五个独立网站(monorepo sites/*,共享同一套数据库/Auth/Admin)
# ai-stack:3001 launchai:3002 the-ai-ledger:3003 devai-tools:3004 findai:3005
set -e
cd "$(dirname "$0")/../../.."
SITES=(ai-stack:3001 launchai:3002 the-ai-ledger:3003 devai-tools:3004 findai:3005)
for entry in "${SITES[@]}"; do
site="${entry%%:*}"
port="${entry##*:}"
(cd "sites/$site" && bun run start > "/tmp/site-$site.log" 2>&1) &
echo "$site → http://localhost:$port (pid $!)"
done
echo "等待启动..."
sleep 5
for entry in "${SITES[@]}"; do
site="${entry%%:*}"
port="${entry##*:}"
code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port/" || echo "000")
echo "$site [$port] → HTTP $code"
done
wait
+37
View File
@@ -0,0 +1,37 @@
import { chromium } from "playwright";
import postgres from "postgres";
const sql = postgres(process.env.DATABASE_URL || "postgresql://postgres:postgres@127.0.0.1:15433/ai_directory", { prepare: false });
const count = async () => Number((await sql`SELECT COUNT(*) FROM vote WHERE tool_id='tool_claude'`)[0].count);
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// 登录
await page.goto("http://localhost:3001/login", { waitUntil: "domcontentloaded" });
await page.fill('input[type="email"]', "bob@demo.dev");
await page.fill('input[type="password"]', "password123");
await page.click('button[type="submit"]');
await page.waitForTimeout(2500);
// 记录 → 点击 → 对比
const before = await count();
await page.goto("http://localhost:3001/tools/claude", { waitUntil: "domcontentloaded" });
await page.waitForTimeout(1500);
const btn = page.locator("button", { hasText: /Upvote|Upvoted/ }).first();
await btn.click();
await page.waitForTimeout(2500);
const after = await count();
const label = await btn.textContent();
console.log(`DB votes before=${before} after=${after} | button="${(label ?? "").trim()}"`);
console.log(before !== after ? "TOGGLE PASS" : "TOGGLE FAIL");
// 再点一次恢复
await btn.click();
await page.waitForTimeout(2500);
const restored = await count();
console.log(`再次点击后 DB=${restored} (${restored === before ? "恢复原状 PASS" : "FAIL"})`);
await browser.close();
await sql.end();
+28
View File
@@ -0,0 +1,28 @@
import GoogleAnalytics from "./google-analytics";
import ClarityAnalytics from "./clarity-analytics";
import { analyticsConfig } from "@/constants/website";
export type AnalyticsProps = {
readonly forceEnableInDevelopment?: boolean;
};
export function Analytics({ forceEnableInDevelopment = false }: AnalyticsProps = {}) {
const { enableInDevelopment } = analyticsConfig;
if (
process.env.NODE_ENV !== "production" &&
!enableInDevelopment &&
!forceEnableInDevelopment
) {
return null;
}
return (
<>
<GoogleAnalytics />
<ClarityAnalytics />
</>
);
}
export default Analytics;
@@ -0,0 +1,30 @@
import Script from "next/script";
export type ClarityAnalyticsProps = {
readonly projectId?: string;
};
export default function ClarityAnalytics({ projectId }: ClarityAnalyticsProps = {}) {
const clarityProjectId = projectId ?? process.env.NEXT_PUBLIC_CLARITY_PROJECT_ID;
if (!clarityProjectId) {
return null;
}
const sanitizedId = clarityProjectId.trim();
return (
<Script id="microsoft-clarity" strategy="afterInteractive">
{`
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);
t.async=1;
t.src="https://www.clarity.ms/tag/${sanitizedId}";
y=l.getElementsByTagName(r)[0];
y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${sanitizedId}");
`}
</Script>
);
}
@@ -0,0 +1,32 @@
import Script from "next/script";
export type GoogleAnalyticsProps = {
readonly trackingId?: string;
};
export default function GoogleAnalytics({ trackingId }: GoogleAnalyticsProps = {}) {
const gaTrackingId = trackingId ?? process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID;
if (!gaTrackingId) {
return null;
}
const sanitizedId = gaTrackingId.trim();
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(sanitizedId)}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${sanitizedId}', { anonymize_ip: true });
`}
</Script>
</>
);
}
+40
View File
@@ -0,0 +1,40 @@
"use client";
import Link from "next/link";
import React from "react";
export const LogoIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
>
{/* 圆形背景 */}
<circle cx="12" cy="12" r="10" fill="currentColor" />
{/* 字母 T - 代表 Toolso */}
<path
d="M8 8h8M12 8v8"
stroke="hsl(var(--background))"
strokeWidth="2.5"
strokeLinecap="round"
/>
{/* AI 小星芒装饰 */}
<circle cx="17" cy="7" r="1.5" fill="hsl(var(--background))" />
</svg>
);
};
export const Logo = () => {
return (
<Link
href="/"
className="font-normal flex space-x-2 items-center text-sm mr-4 text-foreground px-2 py-1 relative z-20"
>
<LogoIcon />
<span className="font-medium text-foreground">Toolso.AI</span>
</Link>
);
};
+130
View File
@@ -0,0 +1,130 @@
"use client";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { useTheme } from "next-themes";
import React, { useId } from "react";
export const Background = () => {
return (
<div className="absolute inset-0 h-full w-full overflow-hidden pointer-events-none z-0">
<div className="absolute inset-0 h-full w-full bg-background pointer-events-none [mask-image:radial-gradient(ellipse_at_center,transparent,white)]" />
{Array.from({ length: 6 }).map((_, index) => (
<div className="flex" key={"grid-column" + index}>
{Array.from({ length: 10 }).map((_, index) => (
<GridBlock key={`grid-row` + index} />
))}
</div>
))}
</div>
);
};
const GridBlock = () => {
return (
<div className="flex flex-col items-start justify-center w-60">
<div className="flex items-center justify-center">
<Dot />
<SVG />
{/* <Dot /> */}
</div>
<SVGVertical className="ml-3" />
</div>
);
};
const Dot = () => {
return (
<div className="h-6 w-6 bg-background flex items-center justify-center rounded-full">
<div className="h-2 w-2 bg-muted rounded-full" />
</div>
);
};
const SVGVertical = ({ className }: { className?: string }) => {
const width = 1;
const height = 140;
const id = useId();
return (
<motion.svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn("text-border", className)}
>
<path d="M0.5 0.5V479" stroke="currentColor" strokeWidth={2} />
<motion.path
d="M0.5 0.5V479"
stroke={`url(#gradient-${id})`}
strokeWidth={2}
/>
<defs>
<motion.linearGradient
id={`gradient-${id}`}
initial={{ x1: 2, y1: -200, x2: 2, y2: -100 }}
animate={{ x1: 2, y1: 400, x2: 2, y2: 600 }}
transition={{
repeat: Infinity,
duration: Math.random() * 2 + 1,
delay: Math.floor(Math.random() * 6) + 5,
}}
gradientUnits="userSpaceOnUse"
>
<motion.stop offset="0%" stopColor="transparent" />
<motion.stop offset="50%" stopColor="var(--neutral-200)" />
<motion.stop offset="100%" stopColor="transparent" />
</motion.linearGradient>
</defs>
</motion.svg>
);
};
const SVG = ({ className }: { className?: string }) => {
const width = 300;
const height = 1;
const id = useId();
return (
<motion.svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn("text-border", className)}
>
<path d="M0.5 0.5H479" stroke="currentColor" />
<motion.path
d="M0.5 0.5H479"
stroke={`url(#gradient-${id})`}
strokeWidth={1}
/>
<defs>
<motion.linearGradient
id={`gradient-${id}`}
initial={{ x1: -200, y1: 0, x2: -100, y2: 0 }}
animate={{ x1: 400, y1: 0, x2: 600, y2: 0 }}
transition={{
repeat: Infinity,
duration: Math.random() * 2 + 1,
delay: Math.floor(Math.random() * 6) + 5,
}}
gradientUnits="userSpaceOnUse"
>
<motion.stop offset="0%" stopColor="transparent" />
<motion.stop offset="50%" stopColor="var(--neutral-200)" />
<motion.stop offset="100%" stopColor="transparent" />
</motion.linearGradient>
</defs>
</motion.svg>
);
};
// Use the below rect to debug linear gradient
{
/* <motion.rect width={width} height={width} fill={`url(#gradient-${id})`} /> */
}
+35
View File
@@ -0,0 +1,35 @@
import React from "react";
export const Badge: React.FC<
{ children: React.ReactNode } & React.ComponentPropsWithoutRef<"button">
> = ({ children, ...props }) => {
return (
<button
{...props}
className="bg-secondary no-underline group cursor-pointer relative md:shadow-2xl rounded-full p-px text-[10px] sm:text-xs font-semibold leading-6 text-muted-foreground inline-block w-fit mx-auto"
>
<span className="absolute inset-0 overflow-hidden rounded-full">
<span className="absolute inset-0 rounded-full opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
</span>
<div className="relative flex space-x-2 items-center z-10 rounded-full bg-muted py-1.5 px-4 ring-1 ring-border">
<span>{children}</span>
<svg
fill="none"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.75 8.75L14.25 12L10.75 15.25"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
/>
</svg>
</div>
<span className="absolute -bottom-0 left-[1.125rem] h-px w-[calc(100%-2.25rem)] bg-gradient-to-r from-muted-foreground/0 via-muted-foreground/90 to-muted-foreground/0 transition-opacity duration-500 group-hover:opacity-40" />
</button>
);
};
@@ -0,0 +1,52 @@
import { Link } from "next-view-transitions";
import React from "react";
import { BlurImage } from "./blur-image";
import { LogoIcon } from "./Logo";
import Image from "next/image";
import Balancer from "react-wrap-balancer";
import { BlogWithSlug } from "@/lib/blog";
export const BlogCard = ({ blog }: { blog: BlogWithSlug }) => {
const truncate = (text: string, length: number) => {
return text.length > length ? text.slice(0, length) + "..." : text;
};
return (
<Link
className="shadow-derek rounded-3xl border border-border w-full bg-card text-card-foreground overflow-hidden hover:scale-[1.02] transition duration-200"
href={`/blog/${blog.slug}`}
>
{blog.image ? (
<BlurImage
src={blog.image || ""}
alt={blog.title}
height="800"
width="800"
className="h-52 object-cover object-top w-full"
/>
) : (
<div className="h-52 flex items-center justify-center bg-card">
<LogoIcon />
</div>
)}
<div className="p-4 md:p-8 bg-card">
<div className="flex space-x-2 items-center mb-2">
<Image
src={blog.author.src}
alt={blog.author.name}
width={20}
height={20}
className="rounded-full h-5 w-5"
unoptimized={blog.author.src.startsWith('http')}
/>
<p className="text-sm font-normal text-muted-foreground">{blog.author.name}</p>
</div>
<p className="text-lg font-bold mb-4 text-foreground">
<Balancer>{blog.title}</Balancer>
</p>
<p className="text-left text-sm mt-2 text-muted-foreground">
{truncate(blog.description, 100)}
</p>
</div>
</Link>
);
};
@@ -0,0 +1,78 @@
"use client";
import { BlogWithSlug } from "@/lib/blog";
import { IconArrowLeft } from "@tabler/icons-react";
import { Container } from "./container";
import Image from "next/image";
import { Logo } from "./Logo";
import Link from "next/link";
import { format } from "date-fns";
export function BlogLayout({
blog,
children,
}: {
blog: BlogWithSlug;
children: React.ReactNode;
}) {
return (
<Container className="mt-16 lg:mt-32">
<div className="flex justify-between items-center px-2 py-8">
<Link href="/blog" className="flex space-x-2 items-center">
<IconArrowLeft className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Back</span>
</Link>
<div className="flex space-x-2 items-center">
<div className="flex space-x-2 items-center ">
<Image
src={blog.author.src}
alt={blog.author.name}
width={20}
height={20}
className="rounded-full h-5 w-5"
/>
<p className="text-sm font-normal text-muted-foreground">
{blog.author.name}
</p>
</div>
<div className="h-5 rounded-lg w-0.5 bg-border" />
<time dateTime={blog.date} className="flex items-center text-base ">
<span className="text-muted-foreground text-sm">
{format(new Date(blog.date), "MMMM dd, yyyy")}
</span>
</time>
</div>
</div>
<div className="max-w-4xl mx-auto">
{blog.image ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={blog.image}
className="h-40 md:h-96 w-full aspect-square object-cover rounded-3xl"
alt={blog.title}
/>
) : (
<div className="h-40 md:h-96 w-full aspect-square rounded-3xl shadow-derek bg-card flex items-center justify-center">
<Logo />
</div>
)}
</div>
<div className="xl:relative">
<div className="mx-auto max-w-2xl">
<article className="pb-8">
<header className="flex flex-col">
<h1 className="mt-8 text-4xl font-bold tracking-tight text-foreground sm:text-5xl ">
{blog.title}
</h1>
</header>
<div
className="mt-8 prose prose-sm dark:prose-invert"
data-mdx-content
>
{children}
</div>
</article>
</div>
</div>
</Container>
);
}
@@ -0,0 +1,49 @@
"use client";
import clsx from "clsx";
import Image from "next/image";
import React, { useState } from "react";
interface IBlurImage {
height?: any;
width?: any;
src?: string | any;
objectFit?: any;
className?: string | any;
alt?: string | undefined;
[x: string]: any;
}
export const BlurImage = ({
height,
width,
src,
className,
objectFit,
alt,
...rest
}: IBlurImage) => {
const [isLoading, setLoading] = useState(true);
// 判断是否为远程图片
const isRemoteImage = typeof src === 'string' && src.startsWith('http');
return (
<Image
className={clsx(
"transition duration-300 transform",
isLoading ? "blur-sm scale-105" : "blur-0 scale-100",
className
)}
onLoad={() => setLoading(false)}
src={src}
width={width}
height={height}
loading="lazy"
decoding="async"
blurDataURL={isRemoteImage ? undefined : src}
alt={alt ? alt : "Avatar"}
unoptimized={isRemoteImage}
{...rest}
/>
);
};
+47
View File
@@ -0,0 +1,47 @@
import { cn } from "@/lib/utils";
import React from "react";
export const Button: React.FC<{
children?: React.ReactNode;
className?: string;
variant?: "simple" | "outline" | "primary";
size?: "sm" | "md" | "lg";
as?: React.ElementType;
[x: string]: any;
}> = ({
children,
className,
variant = "primary",
size = "md",
as: Tag = "button",
...props
}) => {
const sizeClass =
size === "sm"
? "text-xs px-3 py-1.5"
: size === "lg"
? "text-base px-6 py-3"
: "text-sm md:text-sm px-4 py-2";
const variantClass =
variant === "simple"
? "bg-transparent hover:bg-hover border border-transparent text-foreground hover:text-hover-foreground transition font-medium duration-200 rounded-full flex items-center justify-center"
: variant === "outline"
? "bg-background hover:bg-primary hover:shadow-xl text-foreground border border-border hover:text-primary-foreground transition font-medium duration-200 rounded-full flex items-center justify-center"
: variant === "primary"
? "bg-primary hover:bg-primary/90 border border-transparent text-primary-foreground transition font-medium duration-200 rounded-full flex items-center justify-center shadow-[0px_-1px_0px_0px_rgba(255,255,255,0.25)_inset,_0px_1px_0px_0px_rgba(255,255,255,0.25)_inset]"
: "";
return (
<Tag
className={cn(
"relative z-10",
variantClass,
sizeClass,
className
)}
{...props}
>
{children ?? `Get Started`}
</Tag>
);
};
@@ -0,0 +1,159 @@
.gridLineHorizontal {
--background: #ffffff;
--color: rgba(0, 0, 0, 0.2);
--height: 1px;
--width: 5px;
--fade-stop: 90%;
/* Bleed in or out from the container */
--offset: -100px;
position: absolute;
width: calc(100% + var(--offset));
height: var(--height);
left: calc(var(--offset) / 2 * -1);
background: linear-gradient(
to right,
var(--color),
var(--color) 50%,
transparent 0,
transparent
);
background-size: var(--width) var(--height);
/* Fade out the edges */
mask: linear-gradient(
to left,
var(--background) var(--fade-stop),
transparent
),
linear-gradient(to right, var(--background) var(--fade-stop), transparent),
linear-gradient(black, black);
-webkit-mask: linear-gradient(
to left,
var(--background) var(--fade-stop),
transparent
),
linear-gradient(to right, var(--background) var(--fade-stop), transparent),
linear-gradient(black, black);
mask-composite: exclude;
-webkit-mask-composite: exclude;
z-index: 20;
}
.gridLineHorizontalDark {
--background: #ffffff;
--color: rgba(255, 255, 255, 0.2);
--height: 1px;
--width: 5px;
--fade-stop: 90%;
/* Bleed in or out from the container */
--offset: -100px;
position: absolute;
width: calc(100% + var(--offset));
height: var(--height);
left: calc(var(--offset) / 2 * -1);
background: linear-gradient(
to right,
var(--color),
var(--color) 50%,
transparent 0,
transparent
);
background-size: var(--width) var(--height);
/* Fade out the edges */
mask: linear-gradient(
to left,
var(--background) var(--fade-stop),
transparent
),
linear-gradient(to right, var(--background) var(--fade-stop), transparent),
linear-gradient(black, black);
-webkit-mask: linear-gradient(
to left,
var(--background) var(--fade-stop),
transparent
),
linear-gradient(to right, var(--background) var(--fade-stop), transparent),
linear-gradient(black, black);
mask-composite: exclude;
-webkit-mask-composite: exclude;
z-index: 20;
}
.gridLineVertical {
--background: #ffffff;
--color: rgba(0, 0, 0, 0.2);
--height: 5px;
--width: 1px;
--fade-stop: 90%;
--offset: -100px;
position: absolute;
height: calc(100% + var(--offset));
width: var(--width);
top: calc(var(--offset) / 2 * -1);
background: linear-gradient(
to bottom,
var(--color),
var(--color) 50%,
transparent 0,
transparent
);
background-size: var(--width) var(--height);
mask: linear-gradient(to top, var(--background) var(--fade-stop), transparent),
linear-gradient(to bottom, var(--background) var(--fade-stop), transparent),
linear-gradient(black, black);
-webkit-mask: linear-gradient(
to top,
var(--background) var(--fade-stop),
transparent
),
linear-gradient(to bottom, var(--background) var(--fade-stop), transparent),
linear-gradient(black, black);
mask-composite: exclude;
-webkit-mask-composite: exclude;
z-index: 20;
}
.gridLineVerticalDark {
--background: #ffffff;
--color: rgba(255, 255, 255, 0.2);
--height: 5px;
--width: 1px;
--fade-stop: 90%;
--offset: -100px;
position: absolute;
height: calc(100% + var(--offset));
width: var(--width);
top: calc(var(--offset) / 2 * -1);
background: linear-gradient(
to bottom,
var(--color),
var(--color) 50%,
transparent 0,
transparent
);
background-size: var(--width) var(--height);
mask: linear-gradient(to top, var(--background) var(--fade-stop), transparent),
linear-gradient(to bottom, var(--background) var(--fade-stop), transparent),
linear-gradient(black, black);
-webkit-mask: linear-gradient(
to top,
var(--background) var(--fade-stop),
transparent
),
linear-gradient(to bottom, var(--background) var(--fade-stop), transparent),
linear-gradient(black, black);
mask-composite: exclude;
-webkit-mask-composite: exclude;
z-index: 20;
}
@@ -0,0 +1,14 @@
import { cn } from "@/lib/utils";
import React from "react";
export const Container = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<div className={cn(`max-w-7xl mx-auto px-4 `, className)}>{children}</div>
);
};
+45
View File
@@ -0,0 +1,45 @@
"use client";
import React from "react";
import { Button } from "./button";
import { useTranslations } from 'next-intl';
import { LocaleLink } from "@/components/locale-link";
export const CTA = () => {
const t = useTranslations('cta');
return (
<section className="py-60 w-full overflow-hidden relative z-30">
<div className="bg-background">
<div className="mx-auto w-full relative z-20 sm:max-w-[40rem] md:max-w-[48rem] lg:max-w-[64rem] xl:max-w-[80rem] bg-gradient-to-br from-slate-800 to-gray-900 dark:from-neutral-900 dark:to-neutral-950 sm:rounded-2xl">
<div className="relative -mx-6 sm:mx-0 sm:rounded-2xl overflow-hidden px-6 md:px-8 ">
<div
className="absolute inset-0 w-full h-full opacity-10 bg-noise fade-vignette [mask-image:radial-gradient(#fff,transparent,75%)]"
style={{
backgroundImage: "url(/noise.webp)",
backgroundSize: "30%",
}}
></div>
<div
className="pointer-events-none absolute inset-y-0 right-0 select-none overflow-hidden rounded-2xl"
style={{
mask: "radial-gradient(33.875rem 33.875rem at calc(100% - 8.9375rem) 0, white 3%, transparent 70%)",
}}
></div>
<div className="relative px-6 pb-14 pt-20 sm:px-10 sm:pb-20 lg:px-[4.5rem]">
<h2 className="text-center text-balance mx-auto text-3xl md:text-5xl font-semibold tracking-[-0.015em] text-white">
{t('title')}
</h2>
<p className="mt-4 max-w-[26rem] text-center mx-auto text-base/6 text-white/80">
{t('description')}
</p>
<div className="relative z-10 mx-auto flex justify-center mt-6">
<Button as={LocaleLink} href="/tools">{t('button')}</Button>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
@@ -0,0 +1,30 @@
"use client";
import React from "react";
import { AnimatedTooltip } from "@/components/ui/animated-tooltip";
import { useTranslations } from 'next-intl';
const images = [
"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3387&q=80",
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8YXZhdGFyfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60",
"https://images.unsplash.com/photo-1580489944761-15a19d654956?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NXx8YXZhdGFyfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60",
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGF2YXRhcnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60",
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3540&q=80",
"https://images.unsplash.com/photo-1544725176-7c40e5a71c5e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3534&q=80",
];
export function FeaturedTestimonials() {
const t = useTranslations('featuredTestimonials');
const people = (t.raw('people') as any[]).map((person, index) => ({
id: index + 1,
name: person.name,
designation: person.designation,
image: images[index],
}));
return (
<div className="flex flex-row items-center justify-center mb-10 w-full">
<AnimatedTooltip items={people} />
</div>
);
}
@@ -0,0 +1,131 @@
"use client";
import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
import { ExternalLink, ArrowRight } from "lucide-react";
import { Heading } from "./heading";
import { Subheading } from "./subheading";
import { ToolLogo } from "./tool-logo";
interface ToolData {
id: string;
slug: string;
domain: string | null;
websiteUrl: string | null;
logoUrl: string | null;
nameEn: string;
descriptionEn: string | null;
nameZh: string | null;
descriptionZh: string | null;
}
interface CategoryData {
id: string;
slug: string;
icon: string | null;
nameEn: string;
nameZh: string | null;
toolCount: number;
}
interface FeaturedToolsProps {
tools: ToolData[];
categories: CategoryData[];
}
export function FeaturedTools({ tools, categories }: FeaturedToolsProps) {
const locale = useLocale();
const t = useTranslations("featuredTools");
const getName = (item: { nameEn: string; nameZh: string | null }) =>
locale === "zh" && item.nameZh ? item.nameZh : item.nameEn;
const getDescription = (tool: ToolData) =>
locale === "zh" && tool.descriptionZh
? tool.descriptionZh
: tool.descriptionEn;
return (
<div className="relative z-20">
<Heading as="h2">{t("title")}</Heading>
<Subheading className="text-center">{t("subtitle")}</Subheading>
{/* 分类导航 */}
{categories.length > 0 && (
<div className="flex flex-wrap justify-center gap-3 mt-8">
{categories.slice(0, 6).map((category) => (
<Link
key={category.id}
href={`/${locale}/tools?category=${category.slug}`}
className="px-4 py-2 rounded-full bg-secondary text-foreground hover:bg-hover transition-colors text-sm font-medium"
>
{category.icon && <span className="mr-1">{category.icon}</span>}
{getName(category)}
<span className="ml-1 text-muted-foreground">
({category.toolCount})
</span>
</Link>
))}
</div>
)}
{/* 工具网格 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mt-12">
{tools.slice(0, 8).map((tool) => (
<Link
key={tool.id}
href={`/${locale}/tools/${tool.slug}`}
className="group bg-background rounded-xl border border-border p-5 hover:shadow-lg transition-all hover:border-foreground/20"
>
<div className="flex items-start gap-4">
<ToolLogo src={tool.logoUrl} alt={getName(tool)} size={48} />
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground group-hover:text-primary truncate">
{getName(tool)}
</h3>
{tool.domain && (
<p className="text-sm text-muted-foreground truncate">
{tool.domain}
</p>
)}
</div>
</div>
{getDescription(tool) && (
<p className="mt-3 text-sm text-muted-foreground line-clamp-2">
{getDescription(tool)}
</p>
)}
{tool.websiteUrl && (
<div className="mt-4 flex items-center gap-1 text-sm text-muted-foreground group-hover:text-foreground">
<ExternalLink className="h-3 w-3" />
<span>{t("visitWebsite")}</span>
</div>
)}
</Link>
))}
</div>
{/* 空状态 */}
{tools.length === 0 && (
<div className="text-center py-16 text-muted-foreground">
{t("empty")}
</div>
)}
{/* 查看更多按钮 */}
{tools.length > 0 && (
<div className="flex justify-center mt-12">
<Link
href={`/${locale}/tools`}
className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-foreground text-background hover:opacity-90 transition-opacity font-medium"
>
{t("viewAll")}
<ArrowRight className="h-4 w-4" />
</Link>
</div>
)}
</div>
);
}
+123
View File
@@ -0,0 +1,123 @@
"use client";
import React from "react";
import { Heading } from "./heading";
import { Subheading } from "./subheading";
import { cn } from "@/lib/utils";
import { GridLineHorizontal, GridLineVertical } from "./grid-lines";
import { SkeletonOne } from "./skeletons/first";
import { SkeletonTwo } from "./skeletons/second";
import { SkeletonFour } from "./skeletons/fourth";
import { SkeletonThree } from "./skeletons/third";
import { useTranslations } from 'next-intl';
export const Features = () => {
const t = useTranslations('features');
const features = [
{
title: t('items.aiOptimized.title'),
description: t('items.aiOptimized.description'),
skeleton: <SkeletonOne />,
className:
"col-span-1 md:col-span-4 border-b border-r border-border",
},
{
title: t('items.zeroPipeline.title'),
description: t('items.zeroPipeline.description'),
skeleton: <SkeletonTwo />,
className: "border-b col-span-1 md:col-span-2 border-border",
},
{
title: t('items.nonTechnical.title'),
description: t('items.nonTechnical.description'),
skeleton: <SkeletonThree />,
className: "col-span-1 md:col-span-3 border-r border-border",
},
{
title: t('items.productionGrade.title'),
description: t('items.productionGrade.description'),
skeleton: <SkeletonFour />,
className: "col-span-1 md:col-span-3",
},
];
return (
<div className="relative z-20 py-10 md:py-40">
<Heading as="h2">{t('title')}</Heading>
<Subheading className="text-center ">
{t('subtitle')}
</Subheading>
<div className="relative overflow-hidden">
<div className="grid grid-cols-1 md:grid-cols-6 mt-12">
{features.map((feature) => (
<FeatureCard key={feature.title} className={feature.className}>
<FeatureTitle>{feature.title}</FeatureTitle>
<FeatureDescription>{feature.description}</FeatureDescription>
<div className=" h-full w-full">{feature.skeleton}</div>
</FeatureCard>
))}
</div>
<GridLineHorizontal
style={{
top: 0,
left: "-10%",
width: "120%",
}}
/>
<GridLineHorizontal
style={{
bottom: 0,
left: "-10%",
width: "120%",
}}
/>
<GridLineVertical
style={{
top: "-10%",
right: 0,
height: "120%",
}}
/>
<GridLineVertical
style={{
top: "-10%",
left: 0,
height: "120%",
}}
/>
</div>
</div>
);
};
const FeatureCard = ({
children,
className,
}: {
children?: React.ReactNode;
className?: string;
}) => {
return (
<div className={cn(`p-4 sm:p-8 relative overflow-hidden`, className)}>
{children}
</div>
);
};
const FeatureTitle = ({ children }: { children?: React.ReactNode }) => {
return (
<Heading as="h3" size="sm" className="text-left">
{children}
</Heading>
);
};
const FeatureDescription = ({ children }: { children?: React.ReactNode }) => {
return (
<Subheading className="text-left max-w-sm mx-0 md:text-sm my-2">
{children}
</Subheading>
);
};
+103
View File
@@ -0,0 +1,103 @@
"use client";
import React from "react";
import { Logo } from "./Logo";
import { useTranslations } from 'next-intl';
import { LocaleLink } from './locale-link';
import { NewsletterForm } from '@/features/marketing/components/newsletter-form';
export const Footer = () => {
const t = useTranslations();
const links = [
{
name: t('navigation.main.blog'),
href: "/blog",
},
{
name: t('navigation.main.contact'),
href: "/contact",
},
];
const legal = [
{
name: t('navigation.footer.legal.terms'),
href: "/terms",
},
{
name: t('navigation.footer.legal.privacy'),
href: "/privacy",
},
{
name: t('navigation.footer.legal.cookies'),
href: "/cookies",
},
];
const socials = [
{
name: t('footer.social.twitter'),
href: "https://x.com/ToolsoAI",
external: true,
},
{
name: t('footer.social.github'),
href: "https://github.com/coolchange-ai/toolso-ai-open",
external: true,
},
];
return (
<div className="relative">
<div className="border-t border-border px-8 pt-20 pb-32 relative bg-background">
<div className="max-w-7xl mx-auto">
{/* Footer Links Section */}
<div className="text-sm text-muted-foreground flex sm:flex-row flex-col justify-between items-start">
<div>
<div className="mr-4 md:flex mb-4">
<Logo />
</div>
<div>{t('common.brand.copyright')}</div>
<div className="mt-2">{t('common.brand.allRightsReserved')}</div>
<NewsletterForm />
</div>
<div className="grid grid-cols-3 gap-10 items-start mt-10 md:mt-0">
<div className="flex justify-center space-y-4 flex-col mt-4">
{links.map((link) => (
<LocaleLink
key={link.name}
className="transition-colors hover:text-foreground text-muted-foreground text-xs sm:text-sm"
href={link.href}
>
{link.name}
</LocaleLink>
))}
</div>
<div className="flex justify-center space-y-4 flex-col mt-4">
{legal.map((link) => (
<LocaleLink
key={link.name}
className="transition-colors hover:text-foreground text-muted-foreground text-xs sm:text-sm"
href={link.href}
>
{link.name}
</LocaleLink>
))}
</div>
<div className="flex justify-center space-y-4 flex-col mt-4">
{socials.map((link) => (
<a
key={link.name}
className="transition-colors hover:text-foreground text-muted-foreground text-xs sm:text-sm"
href={link.href}
target="_blank"
rel="noopener noreferrer"
>
{link.name}
</a>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
+52
View File
@@ -0,0 +1,52 @@
"use client";
import createGlobe from "cobe";
import { useEffect, useRef } from "react";
// https://github.com/shuding/cobe
export const Globe = ({ className }: { className?: string }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let phi = 0;
if (!canvasRef.current) return;
const globe = createGlobe(canvasRef.current, {
devicePixelRatio: 2,
width: 600 * 2,
height: 600 * 2,
phi: 0,
theta: 0,
dark: 1,
diffuse: 1.2,
mapSamples: 16000,
mapBrightness: 6,
baseColor: [0.3, 0.3, 0.3],
markerColor: [0.1, 0.8, 1],
glowColor: [1, 1, 1],
markers: [
// longitude latitude
{ location: [37.7595, -122.4367], size: 0.03 },
{ location: [40.7128, -74.006], size: 0.1 },
],
onRender: (state) => {
// Called on every animation frame.
// `state` will be an empty object, return updated params.
state.phi = phi;
phi += 0.01;
},
});
return () => {
globe.destroy();
};
}, []);
return (
<canvas
ref={canvasRef}
style={{ width: 600, height: 600, maxWidth: "100%", aspectRatio: 1 }}
className={className}
/>
);
};
@@ -0,0 +1,32 @@
import { cn } from "@/lib/utils";
import styles from "./common.module.css";
export const GridLineHorizontal = ({ ...props }) => {
return (
<>
<div
className={cn(styles.gridLineHorizontalDark, "hidden dark:block ")}
{...props}
/>
<div
className={cn(styles.gridLineHorizontal, "block dark:hidden ")}
{...props}
/>
</>
);
};
export const GridLineVertical = ({ ...props }) => {
return (
<>
<div
className={cn(styles.gridLineVerticalDark, "hidden dark:block ")}
{...props}
/>
<div
className={cn(styles.gridLineVertical, "block dark:hidden ")}
{...props}
/>
</>
);
};
+39
View File
@@ -0,0 +1,39 @@
import { cn } from "@/lib/utils";
import { MotionProps, Transition } from "framer-motion";
import React from "react";
export const Heading = ({
className,
as: Tag = "h2",
children,
size = "md",
...props
}: {
className?: string;
as?: any;
children: any;
size?: "sm" | "md" | "xl" | "2xl";
props?: React.HTMLAttributes<HTMLHeadingElement | Transition>;
} & MotionProps &
React.HTMLAttributes<HTMLHeadingElement | Transition>) => {
const sizeVariants = {
sm: "text-xl md:text-2xl md:leading-snug",
md: "text-3xl md:text-5xl md:leading-tight",
xl: "text-4xl md:text-6xl md:leading-none",
"2xl": "text-5xl md:text-7xl md:leading-none",
};
return (
<Tag
className={cn(
"text-3xl md:text-5xl md:leading-tight max-w-5xl mx-auto text-center tracking-tight",
"font-medium",
"text-foreground",
sizeVariants[size],
className
)}
{...props}
>
{children}
</Tag>
);
};
+83
View File
@@ -0,0 +1,83 @@
"use client";
import { Button } from "./button";
import { HiArrowRight } from "react-icons/hi2";
import { motion } from "framer-motion";
import { LocaleLink } from "@/components/locale-link";
import { useTranslations } from 'next-intl';
export const Hero = () => {
const t = useTranslations('hero');
return (
<div className="flex flex-col relative overflow-hidden">
<motion.h1
initial={{
y: 40,
opacity: 0,
}}
animate={{
y: 0,
opacity: 1,
}}
transition={{
ease: "easeOut",
duration: 0.5,
}}
className="text-4xl md:text-6xl lg:text-[5rem] font-semibold max-w-6xl mx-auto text-center mt-6 relative z-10"
>
{t('title')}
</motion.h1>
<motion.h2
initial={{
y: 40,
opacity: 0,
}}
animate={{
y: 0,
opacity: 1,
}}
transition={{
ease: "easeOut",
duration: 0.5,
delay: 0.2,
}}
className="text-center mt-6 text-base md:text-xl text-muted-foreground max-w-3xl mx-auto relative z-10 font-normal"
>
{t('description')}
</motion.h2>
<motion.div
initial={{
y: 80,
opacity: 0,
}}
animate={{
y: 0,
opacity: 1,
}}
transition={{
ease: "easeOut",
duration: 0.5,
delay: 0.4,
}}
className="flex items-center gap-4 justify-center mt-6 relative z-10"
>
<Button
as={LocaleLink}
href="/tools"
>
{t('cta.primary')}
</Button>
<Button
variant="simple"
as={LocaleLink}
href="/blog"
className="flex space-x-2 items-center group"
>
<span>{t('cta.secondary')}</span>
<HiArrowRight className="text-muted-foreground group-hover:translate-x-1 stroke-[1px] h-3 w-3 transition-transform duration-200" />
</Button>
</motion.div>
</div>
);
};
@@ -0,0 +1,50 @@
"use client";
import { cn } from "@/lib/utils";
import { useId } from "react";
import { motion } from "framer-motion";
export const HorizontalGradient = ({
className,
...props
}: {
className: string;
[x: string]: any;
}) => {
const id = useId();
return (
<svg
width="1595"
height="2"
viewBox="0 0 1595 2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn(
"absolute w-full object-contain pointer-events-none",
className
)}
{...props}
>
<path
d="M0 1H1594.5"
stroke={`url(#line-path-gradient-${id})`}
strokeDasharray="8 8"
/>
<defs>
<linearGradient
id={`line-path-gradient-${id}`}
x1="0"
y1="1.5"
x2="1594.5"
y2="1.5"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" stopOpacity="0" />
<stop offset="0.2" stopColor="var(--neutral-400)" />
<stop offset="0.8" stopColor="var(--neutral-400)" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
);
};
@@ -0,0 +1,18 @@
"use client";
import { useRef } from "react";
import { useInView } from "framer-motion";
export function InViewDiv({
children,
...props
}: { children: React.ReactNode } & any) {
const containerRef = useRef<HTMLDivElement>(null);
let isInView = useInView(containerRef, { once: true, amount: 0.4 });
return (
<div ref={containerRef} {...props}>
{isInView ? children : null}
</div>
);
}
+256
View File
@@ -0,0 +1,256 @@
interface WebSiteJsonLdProps {
name: string
description: string
url: string
locale: string
}
/**
* WebSite structured data for the homepage
* Helps search engines understand the site is an AI tools directory
*/
export function WebSiteJsonLd({ name, description, url, locale }: WebSiteJsonLdProps) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name,
description,
url,
inLanguage: locale === 'zh' ? 'zh-CN' : 'en-US',
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${url}/tools?search={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
interface OrganizationJsonLdProps {
name: string
url: string
logo: string
sameAs?: string[]
}
/**
* Organization structured data
*/
export function OrganizationJsonLd({ name, url, logo, sameAs = [] }: OrganizationJsonLdProps) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Organization',
name,
url,
logo,
sameAs,
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
interface CollectionPageJsonLdProps {
name: string
description: string
url: string
itemCount?: number
}
/**
* CollectionPage structured data for the tools listing page
*/
export function CollectionPageJsonLd({
name,
description,
url,
itemCount,
}: CollectionPageJsonLdProps) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name,
description,
url,
...(itemCount && {
mainEntity: {
'@type': 'ItemList',
numberOfItems: itemCount,
},
}),
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
// ========== 扩展 Schema(AI Directory Platform) ==========
interface SoftwareApplicationJsonLdExtendedProps {
name: string
description: string
url: string
image?: string
category?: string
applicationCategory?: string
/** 平均评分(0-5),> 0 时输出 AggregateRating */
ratingAvg?: number
/** 评分数 */
ratingCount?: number
/** offers 价格信息(可选) */
price?: string
}
/**
* SoftwareApplicationJsonLd(扩展版)
* 说明:带 AggregateRating 的工具结构化数据(新页面使用)。
*/
export function SoftwareApplicationJsonLd({
name,
description,
url,
image,
category,
applicationCategory = 'AI Tool',
ratingAvg,
ratingCount,
price,
}: SoftwareApplicationJsonLdExtendedProps) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name,
description,
url,
...(image && { image }),
...(category && { category }),
applicationCategory,
operatingSystem: 'Web',
...((ratingAvg ?? 0) > 0 &&
(ratingCount ?? 0) > 0 && {
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: ratingAvg,
ratingCount: ratingCount,
bestRating: 5,
worstRating: 1,
},
}),
...(price && {
offers: { '@type': 'Offer', price, priceCurrency: 'USD' },
}),
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
interface FaqPageJsonLdProps {
faqs: { question: string; answer: string }[]
}
/**
* FaqPageJsonLd
* 说明:FAQPage 结构化数据(Tool Detail / Best / Use Case 页共用)。
*/
export function FaqPageJsonLd({ faqs }: FaqPageJsonLdProps) {
if (!faqs || faqs.length === 0) return null
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: { '@type': 'Answer', text: faq.answer },
})),
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
interface BreadcrumbJsonLdProps {
items: { name: string; url: string }[]
}
/**
* BreadcrumbJsonLd
* 说明:BreadcrumbList 结构化数据。
*/
export function BreadcrumbJsonLd({ items }: BreadcrumbJsonLdProps) {
if (!items || items.length === 0) return null
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
interface ItemListJsonLdProps {
name: string
url: string
items: { name: string; url: string }[]
}
/**
* ItemListJsonLd
* 说明:ItemList 结构化数据(榜单 / 合集 / 用例页的工具列表)。
*/
export function ItemListJsonLd({ name, url, items }: ItemListJsonLdProps) {
if (!items || items.length === 0) return null
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'ItemList',
name,
url,
numberOfItems: items.length,
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
url: item.url,
})),
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
)
}
@@ -0,0 +1,137 @@
"use client";
import { useLocale } from 'next-intl';
import { useRouter, usePathname } from 'next/navigation';
import { locales, localeNames } from '@/i18n.config';
import type { Locale } from '@/i18n.config';
import { useState, useRef, useEffect } from 'react';
import { ChevronDown, Globe, Check } from 'lucide-react';
import { cn } from '@/lib/utils';
export function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const switchLocale = (newLocale: Locale) => {
if (newLocale === locale) {
setIsOpen(false);
return;
}
let path = pathname;
for (const loc of locales) {
if (pathname === `/${loc}` || pathname.startsWith(`/${loc}/`)) {
path = pathname.slice(loc.length + 1) || '/';
break;
}
}
const normalizedPath = path === '/' ? '' : path;
router.push(`/${newLocale}${normalizedPath}`);
setIsOpen(false);
};
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Close on escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
};
}
}, [isOpen]);
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className={cn(
"flex items-center gap-2 px-3 py-1.5 text-sm rounded-full transition-all duration-200",
"border border-border",
"bg-popover text-popover-foreground",
"hover:bg-hover",
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
)}
aria-expanded={isOpen}
aria-haspopup="true"
aria-label="Select language"
>
<Globe className="w-4 h-4" />
<span className="font-medium">{localeNames[locale as Locale]}</span>
<ChevronDown
className={cn(
"w-3 h-3 transition-transform duration-200",
isOpen && "rotate-180"
)}
/>
</button>
{/* Dropdown Menu */}
{isOpen && (
<div
className={cn(
"absolute right-0 mt-2 py-1 min-w-[150px]",
"bg-popover text-popover-foreground",
"border border-border",
"rounded-lg shadow-lg",
"animate-in fade-in-0 zoom-in-95",
"z-50"
)}
role="menu"
aria-orientation="vertical"
>
{locales.map((loc) => {
const isActive = loc === locale;
return (
<button
key={loc}
onClick={() => switchLocale(loc)}
className={cn(
"flex items-center justify-between w-full px-3 py-2 text-sm",
"hover:bg-hover",
"transition-colors duration-150",
isActive && "bg-accent text-accent-foreground"
)}
role="menuitem"
>
<span className={cn(
"font-medium",
isActive ? "text-accent-foreground" : "text-muted-foreground"
)}>
{localeNames[loc]}
</span>
{isActive && (
<Check className="w-4 h-4 text-primary" />
)}
</button>
);
})}
</div>
)}
</div>
);
}
@@ -0,0 +1,32 @@
"use client";
import { useLocale } from 'next-intl';
import { Link } from 'next-view-transitions';
interface LocaleLinkProps {
href: string;
children: React.ReactNode;
className?: string;
[key: string]: any;
}
export function LocaleLink({ href, children, ...props }: LocaleLinkProps) {
const locale = useLocale();
if (href.startsWith('http') || href.startsWith('//')) {
return <Link href={href} {...props}>{children}</Link>;
}
const normalizedHref = href.startsWith('/') ? href : `/${href}`;
if (
normalizedHref === `/${locale}` ||
normalizedHref.startsWith(`/${locale}/`)
) {
return <Link href={normalizedHref} {...props}>{children}</Link>;
}
const localizedHref = `/${locale}${normalizedHref === '/' ? '' : normalizedHref}`;
return <Link href={localizedHref} {...props}>{children}</Link>;
}
@@ -0,0 +1,120 @@
"use client";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { ReactNode } from "react";
interface MarkdownMessageProps {
content: string;
isStreaming?: boolean;
}
export function MarkdownMessage({ content, isStreaming }: MarkdownMessageProps) {
return (
<div className="markdown-content prose prose-neutral dark:prose-invert max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// 自定义渲染组件
h1: ({ children }) => (
<h1 className="text-2xl font-bold mt-4 mb-2 text-foreground">{children}</h1>
),
h2: ({ children }) => (
<h2 className="text-xl font-semibold mt-3 mb-2 text-foreground">{children}</h2>
),
h3: ({ children }) => (
<h3 className="text-lg font-medium mt-2 mb-1 text-foreground">{children}</h3>
),
p: ({ children }) => (
<p className="mb-2 text-foreground leading-relaxed">{children}</p>
),
ul: ({ children }) => (
<ul className="list-disc list-inside mb-2 space-y-1 text-foreground">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="list-decimal list-inside mb-2 space-y-1 text-foreground">
{children}
</ol>
),
li: ({ children }) => (
<li className="ml-2 text-foreground">{children}</li>
),
code: ({ inline, children }: { inline?: boolean; children?: ReactNode }) => {
if (inline) {
return (
<code className="px-1.5 py-0.5 rounded bg-muted text-sm font-mono text-muted-foreground">
{children}
</code>
);
}
return (
<code className="block p-3 rounded-lg bg-muted text-sm font-mono overflow-x-auto">
{children}
</code>
);
},
pre: ({ children }) => (
<pre className="mb-2 overflow-x-auto">{children}</pre>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-border pl-4 my-2 italic text-muted-foreground">
{children}
</blockquote>
),
strong: ({ children }) => (
<strong className="font-bold text-foreground">{children}</strong>
),
em: ({ children }) => (
<em className="italic">{children}</em>
),
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground underline hover:text-foreground"
>
{children}
</a>
),
table: ({ children }) => (
<div className="overflow-x-auto mb-4">
<table className="min-w-full divide-y divide-border">
{children}
</table>
</div>
),
thead: ({ children }) => (
<thead className="bg-muted">{children}</thead>
),
tbody: ({ children }) => (
<tbody className="bg-card divide-y divide-border">
{children}
</tbody>
),
tr: ({ children }) => <tr>{children}</tr>,
th: ({ children }) => (
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
{children}
</th>
),
td: ({ children }) => (
<td className="px-3 py-2 whitespace-nowrap text-sm text-foreground">
{children}
</td>
),
hr: () => (
<hr className="my-4 border-border" />
),
}}
>
{content}
</ReactMarkdown>
{isStreaming && (
<span className="inline-block w-1 h-4 ml-1 bg-muted-foreground animate-pulse" />
)}
</div>
);
}
@@ -0,0 +1,70 @@
"use client";
import * as React from "react";
import { useTheme } from "next-themes";
import { MoonIcon } from "lucide-react";
import { IconSunLow } from "@tabler/icons-react";
import { motion } from "framer-motion";
export function ModeToggle() {
const { theme, setTheme } = useTheme();
const [isClient, setIsClient] = React.useState(false);
React.useEffect(() => {
setIsClient(true);
}, []);
return (
isClient && (
<button
onClick={() => {
theme === "dark" ? setTheme("light") : setTheme("dark");
}}
className="w-10 h-10 flex hover:bg-hover rounded-lg items-center justify-center outline-none focus:ring-0 focus:outline-none active:ring-0 active:outline-none overflow-hidden"
>
{theme === "light" && (
<motion.div
key={theme}
initial={{
x: 40,
opacity: 0,
}}
animate={{
x: 0,
opacity: 1,
}}
transition={{
duration: 0.3,
ease: "easeOut",
}}
>
<IconSunLow className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
</motion.div>
)}
{theme === "dark" && (
<motion.div
key={theme}
initial={{
x: 40,
opacity: 0,
}}
animate={{
x: 0,
opacity: 1,
}}
transition={{
ease: "easeOut",
duration: 0.3,
}}
>
<MoonIcon className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
</motion.div>
)}
<span className="sr-only">Toggle theme</span>
</button>
)
);
}
+43
View File
@@ -0,0 +1,43 @@
import { EyeIcon, EyeOffIcon } from "lucide-react";
import React, { forwardRef, useState } from "react";
import { cn } from "@/lib/utils";
interface PasswordProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Password = forwardRef<HTMLInputElement, PasswordProps>(
({ className, type, ...props }, ref) => {
const [show, setShow] = useState(false);
return (
<div className="relative">
<input
{...props}
type={show ? "text" : type ?? "password"}
ref={ref}
className={cn(
"block w-full rounded-md border-0 bg-input px-4 pr-10 py-1.5 text-foreground shadow-aceternity placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring",
className
)}
/>
<div className="absolute right-3 top-[30%]">
{!show && (
<EyeIcon
onClick={() => setShow(true)}
className="text-muted-foreground cursor-pointer h-4"
/>
)}
{show && (
<EyeOffIcon
onClick={() => setShow(false)}
className="text-muted-foreground cursor-pointer h-4"
/>
)}
</div>
</div>
);
});
Password.displayName = "Password";
export default Password;
@@ -0,0 +1,88 @@
"use client";
import { useState } from "react";
import { useLocale, useTranslations } from "next-intl";
import { toast } from "sonner";
import { Share2, Link2, Twitter, Linkedin } from "lucide-react";
/**
* # 组件 ShareButton
* 说明:分享(Copy Link / X / LinkedIn)。图标全部为 SVG(Lucide)。
*/
export function ShareButton({ toolName }: { toolName: string }) {
const t = useTranslations("tools.share");
const locale = useLocale();
const [open, setOpen] = useState(false);
const url = () =>
`${window.location.origin}/${locale}/tools/${window.location.pathname
.split("/")
.pop()}`;
const copy = async () => {
await navigator.clipboard.writeText(url());
toast.success(t("copied"));
setOpen(false);
};
const popup = (shareUrl: string) => {
window.open(shareUrl, "_blank", "noopener,width=600,height=500");
setOpen(false);
};
if (!open) {
return (
<button
type="button"
onClick={() => setOpen(true)}
aria-label={t("share")}
className="inline-flex w-full items-center justify-center gap-2 rounded-lg border border-border px-4 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-hover"
>
<Share2 className="h-4 w-4" />
{t("share")}
</button>
);
}
return (
<div className="grid grid-cols-3 gap-2">
<button
type="button"
onClick={copy}
aria-label={t("copyLink")}
className="inline-flex items-center justify-center gap-1.5 rounded-lg border border-border px-2 py-2 text-xs font-medium text-foreground hover:bg-hover"
>
<Link2 className="h-3.5 w-3.5" />
{t("copy")}
</button>
<button
type="button"
onClick={() =>
popup(
`https://twitter.com/intent/tweet?text=${encodeURIComponent(
`${toolName} — AI tool`
)}&url=${encodeURIComponent(url())}`
)
}
aria-label="Share on X"
className="inline-flex items-center justify-center gap-1.5 rounded-lg border border-border px-2 py-2 text-xs font-medium text-foreground hover:bg-hover"
>
<Twitter className="h-3.5 w-3.5" />
X
</button>
<button
type="button"
onClick={() =>
popup(
`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url())}`
)
}
aria-label="Share on LinkedIn"
className="inline-flex items-center justify-center gap-1.5 rounded-lg border border-border px-2 py-2 text-xs font-medium text-foreground hover:bg-hover"
>
<Linkedin className="h-3.5 w-3.5" />
LinkedIn
</button>
</div>
);
}
@@ -0,0 +1,113 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
import Image from "next/image";
import { BlurImage } from "../blur-image";
export const SkeletonOne = () => {
return (
<div className="relative flex p-8 gap-10 h-full">
<div className=" w-full md:w-[90%] p-5 mx-auto bg-card shadow-2xl group h-full">
<div className="flex flex-1 w-full h-full flex-col space-y-2 opacity-20 dark:opacity-60 ">
<UserMessage>
I want to generate an image of two people, fighting outside a bar.
They fight to the core. Once they&apos;re done, they sit down and
drink beer.
</UserMessage>
<AIMessage>
Certainly, I&apos;m generating this picture for you in a while. BTW
are you talking about THAT movie?
</AIMessage>
<UserMessage>
I don&apos;t know what you&apos;re talking about.
</UserMessage>
<AIMessage>Are you sure?</AIMessage>
<UserMessage>
Yes, I&apos;m sure. But if you&apos;re generating that scene, make
sure the fighters have clown shoes and rubber chickens instead of
fists!
</UserMessage>
<AIMessage>Affirmative, here&apos;s your image.</AIMessage>
</div>
</div>
<div className="flex flex-col gap-4 absolute inset-0">
<div className="p-2 border border-border bg-muted rounded-[32px] r h-[250px] w-[250px] md:h-[300px] md:w-[300px] mx-auto flex-shrink-0 z-20 group-hover:scale-[1.02] transition duration-200">
<div className="p-2 bg-card border border-border rounded-[24px] flex-shrink-0">
<BlurImage
src="/skeleton-one.png"
alt="header"
width={800}
height={800}
className="rounded-[20px] w-full h-full object-cover object-bottom aspect-square flex-shrink-0 grayscale"
/>
</div>
</div>
<div className="p-2 border border-border bg-muted rounded-[32px] r h-[250px] w-[250px] md:h-[300px] md:w-[300px] mx-auto flex-shrink-0 z-20 group-hover:scale-[1.02] transition duration-200">
<div className="p-2 bg-card border border-border rounded-[24px] flex-shrink-0">
<BlurImage
src="/tyler.jpeg"
alt="header"
width={800}
height={800}
className="rounded-[20px] w-full h-full object-cover object-bottom aspect-square flex-shrink-0 grayscale"
/>
</div>
</div>
</div>
<div className="absolute bottom-0 z-40 inset-x-0 h-60 bg-gradient-to-t from-background via-background to-transparent w-full pointer-events-none" />
<div className="absolute top-0 z-40 inset-x-0 h-60 bg-gradient-to-b from-background via-transparent to-transparent w-full pointer-events-none" />
</div>
);
};
const UserMessage = ({ children }: { children: React.ReactNode }) => {
const variants = {
initial: {
x: 0,
},
animate: {
x: 5,
transition: {
duration: 0.2,
},
},
};
return (
<motion.div
variants={variants}
className="flex flex-row rounded-2xl p-2 items-start space-x-2 bg-card"
>
<Image
src="/avatar.jpeg"
alt="avatar"
height="100"
width="100"
className="rounded-full h-4 w-4 md:h-10 md:w-10"
/>
<p className="text-[10px] sm:text-sm text-muted-foreground">{children}</p>
</motion.div>
);
};
const AIMessage = ({ children }: { children: React.ReactNode }) => {
const variantsSecond = {
initial: {
x: 0,
},
animate: {
x: 10,
transition: {
duration: 0.2,
},
},
};
return (
<motion.div
variants={variantsSecond}
className="flex flex-row rounded-2xl p-2 items-center justify-start space-x-2 bg-card "
>
<div className="h-4 w-4 md:h-10 md:w-10 rounded-full bg-gradient-to-r from-pink-500 to-violet-500 flex-shrink-0" />
<p className="text-[10px] sm:text-sm text-muted-foreground">{children}</p>
</motion.div>
);
};
@@ -0,0 +1,274 @@
import React from "react";
import { cn } from "@/lib/utils";
import { InfiniteMovingCards } from "@/components/ui/infinite-moving-cards";
import { Globe } from "../globe";
export const SkeletonFour = () => {
return (
<div className="h-full flex flex-col items-center relative bg-background mt-10">
<InfiniteMovingCards speed="fast" direction="left">
<MovingGrid />
</InfiniteMovingCards>
<InfiniteMovingCards speed="slow" direction="right">
<MovingGrid />
</InfiniteMovingCards>
<InfiniteMovingCards speed="normal" direction="left">
<MovingGrid />
</InfiniteMovingCards>
<Globe className="absolute -right-2 md:-right-40 -bottom-40" />
</div>
);
};
const MovingGrid = () => {
return (
<div className="flex space-x-4 flex-shrink-0 mb-4 relative z-40">
<span
className={cn(
" space-x-1 min-w-24 flex justify-center items-center bg-card px-2 py-1 text-sm rounded-md shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]"
)}
>
<svg
width="800px"
height="800px"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="none"
className="h-4 w-4 object-contain"
>
<path
fill="#2396ED"
d="M12.342 4.536l.15-.227.262.159.116.083c.28.216.869.768.996 1.684.223-.04.448-.06.673-.06.534 0 .893.124 1.097.227l.105.057.068.045.191.156-.066.2a2.044 2.044 0 01-.47.73c-.29.299-.8.652-1.609.698l-.178.005h-.148c-.37.977-.867 2.078-1.702 3.066a7.081 7.081 0 01-1.74 1.488 7.941 7.941 0 01-2.549.968c-.644.125-1.298.187-1.953.185-1.45 0-2.73-.288-3.517-.792-.703-.449-1.243-1.182-1.606-2.177a8.25 8.25 0 01-.461-2.83.516.516 0 01.432-.516l.068-.005h10.54l.092-.007.149-.016c.256-.034.646-.11.92-.27-.328-.543-.421-1.178-.268-1.854a3.3 3.3 0 01.3-.81l.108-.187zM2.89 5.784l.04.007a.127.127 0 01.077.082l.006.04v1.315l-.006.041a.127.127 0 01-.078.082l-.039.006H1.478a.124.124 0 01-.117-.088l-.007-.04V5.912l.007-.04a.127.127 0 01.078-.083l.039-.006H2.89zm1.947 0l.039.007a.127.127 0 01.078.082l.006.04v1.315l-.007.041a.127.127 0 01-.078.082l-.039.006H3.424a.125.125 0 01-.117-.088L3.3 7.23V5.913a.13.13 0 01.085-.123l.039-.007h1.413zm1.976 0l.039.007a.127.127 0 01.077.082l.007.04v1.315l-.007.041a.127.127 0 01-.078.082l-.039.006H5.4a.124.124 0 01-.117-.088l-.006-.04V5.912l.006-.04a.127.127 0 01.078-.083l.039-.006h1.413zm1.952 0l.039.007a.127.127 0 01.078.082l.007.04v1.315a.13.13 0 01-.085.123l-.04.006H7.353a.124.124 0 01-.117-.088l-.006-.04V5.912l.006-.04a.127.127 0 01.078-.083l.04-.006h1.412zm1.97 0l.039.007a.127.127 0 01.078.082l.006.04v1.315a.13.13 0 01-.085.123l-.039.006H9.322a.124.124 0 01-.117-.088l-.006-.04V5.912l.006-.04a.127.127 0 01.078-.083l.04-.006h1.411zM4.835 3.892l.04.007a.127.127 0 01.077.081l.007.041v1.315a.13.13 0 01-.085.123l-.039.007H3.424a.125.125 0 01-.117-.09l-.007-.04V4.021a.13.13 0 01.085-.122l.039-.007h1.412zm1.976 0l.04.007a.127.127 0 01.077.081l.007.041v1.315a.13.13 0 01-.085.123l-.039.007H5.4a.125.125 0 01-.117-.09l-.006-.04V4.021l.006-.04a.127.127 0 01.078-.082l.039-.007h1.412zm1.953 0c.054 0 .1.037.117.088l.007.041v1.315a.13.13 0 01-.085.123l-.04.007H7.353a.125.125 0 01-.117-.09l-.006-.04V4.021l.006-.04a.127.127 0 01.078-.082l.04-.007h1.412zm0-1.892c.054 0 .1.037.117.088l.007.04v1.316a.13.13 0 01-.085.123l-.04.006H7.353a.124.124 0 01-.117-.088l-.006-.04V2.128l.006-.04a.127.127 0 01.078-.082L7.353 2h1.412z"
/>
</svg>
<span>Docker</span>
</span>
<span
className={cn(
" space-x-1 min-w-24 flex justify-center items-center bg-card px-2 py-1 text-sm rounded-md shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]"
)}
>
<svg
width="800px"
height="800px"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill="none"
className="h-4 w-4 object-contain"
>
<g fill="#0080FF">
<path d="M8 12.208v2.652c4.495 0 7.988-4.34 6.518-9.053a6.642 6.642 0 00-4.333-4.333C5.48.004 1.14 3.504 1.14 7.992h2.652c0-2.815 2.792-4.993 5.756-3.92a3.975 3.975 0 012.372 2.372c1.08 2.961-1.1 5.75-3.912 5.756V9.563H5.363v2.645H8z" />
<path d="M5.363 14.245H3.325v-2.037h2.038v2.037zM1.622 12.208h1.703v-1.704H1.622v1.704z" />
</g>
</svg>
<span>Digital Ocean</span>
</span>
<span
className={cn(
" space-x-1 min-w-24 flex justify-center items-center bg-card px-2 py-1 text-sm rounded-md shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]"
)}
>
<svg
width="800px"
height="800px"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 object-contain"
>
<title>file_type_tailwind</title>
<path
d="M9,13.7q1.4-5.6,7-5.6c5.6,0,6.3,4.2,9.1,4.9q2.8.7,4.9-2.1-1.4,5.6-7,5.6c-5.6,0-6.3-4.2-9.1-4.9Q11.1,10.9,9,13.7ZM2,22.1q1.4-5.6,7-5.6c5.6,0,6.3,4.2,9.1,4.9q2.8.7,4.9-2.1-1.4,5.6-7,5.6c-5.6,0-6.3-4.2-9.1-4.9Q4.1,19.3,2,22.1Z"
fill="#44a8b3"
/>
</svg>
<span>Tailwind CSS</span>
</span>
<span
className={cn(
" space-x-1 min-w-24 flex justify-center items-center bg-card px-2 py-1 text-sm rounded-md shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]"
)}
>
<svg
width="800px"
height="800px"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 object-contain"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 30C23.732 30 30 23.732 30 16C30 8.26801 23.732 2 16 2C8.26801 2 2 8.26801 2 16C2 23.732 8.26801 30 16 30Z"
fill="white"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9 20.3335H16V27.0002L9 20.3335Z"
fill="#0055FF"
/>
<path d="M16 13.6665H9V20.3332H23L16 13.6665Z" fill="#00AAFF" />
<path d="M9 7L16 13.6667H23V7H9Z" fill="#88DDFF" />
</svg>
<span>Framer Motion</span>
</span>
<span
className={cn(
" space-x-1 min-w-24 flex justify-center items-center bg-card px-2 py-1 text-sm rounded-md shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]"
)}
>
<svg
width="800px"
height="800px"
viewBox="0 -18 256 256"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
className="h-4 w-4 object-contain"
>
<g>
<path
d="M245.969687,168.943256 C232.308259,176.064479 161.536048,205.163388 146.468577,213.017633 C131.402107,220.873879 123.031844,220.797876 111.129473,215.107699 C99.2271007,209.417521 23.9127473,178.99557 10.3463234,172.511368 C3.56511141,169.270267 0,166.535181 0,163.9511 L0,138.075292 C0,138.075292 98.0490639,116.729625 113.878559,111.051447 C129.707053,105.372269 135.199225,105.167264 148.669646,110.101418 C162.141067,115.036572 242.686583,129.569026 256,134.445178 C256,134.445178 255.993999,157.5559 255.993999,159.954975 C255.996,162.513055 252.923904,165.319143 245.969687,168.943256"
fill="#912626"
></path>
<path
d="M245.964922,143.220067 C232.303935,150.33806 161.534003,179.438032 146.467017,187.292024 C131.401031,195.149018 123.031039,195.072017 111.12905,189.382023 C99.2260618,183.696028 23.9151336,153.269057 10.3491466,146.788063 C-3.21684053,140.303069 -3.50184026,135.840074 9.82514705,130.622079 C23.1511343,125.402084 98.0490629,96.0171117 113.880047,90.3381172 C129.708033,84.6611226 135.199028,84.4541228 148.669014,89.3901181 C162.140002,94.3241134 232.487935,122.325087 245.799922,127.200082 C259.11491,132.081078 259.625908,136.099073 245.964922,143.220067"
fill="#C6302B"
></path>
<path
d="M245.969687,127.074354 C232.308259,134.195577 161.536048,163.294486 146.468577,171.151732 C131.402107,179.004977 123.031844,178.928975 111.129473,173.238797 C99.2261007,167.551619 23.9127473,137.126668 10.3463234,130.642465 C3.56511141,127.401364 0,124.669279 0,122.085199 L0,96.2063895 C0,96.2063895 98.0490639,74.8617226 113.878559,69.182545 C129.707053,63.5043676 135.199225,63.2983612 148.669646,68.2325154 C162.141067,73.1676697 242.686583,87.6971237 256,92.5742761 C256,92.5742761 255.993999,115.684998 255.993999,118.087073 C255.996,120.644153 252.923904,123.450241 245.969687,127.074354"
fill="#912626"
></path>
<path
d="M245.964922,101.351164 C232.303935,108.471157 161.534003,137.569129 146.467017,145.426122 C131.401031,153.280114 123.031039,153.203114 111.12905,147.51312 C99.2260618,141.827125 23.9151336,111.401154 10.3491466,104.91916 C-3.21684053,98.4361664 -3.50184026,93.9721706 9.82514705,88.7521756 C23.1511343,83.5351806 98.0490629,54.1482087 113.880047,48.4702141 C129.708033,42.7922195 135.199028,42.5862197 148.669014,47.521215 C162.140002,52.4552102 232.487935,80.4541835 245.799922,85.3311789 C259.11491,90.2101742 259.625908,94.2301704 245.964922,101.350163 L245.964922,101.351164"
fill="#C6302B"
></path>
<path
d="M245.969687,83.6525661 C232.308259,90.7737887 161.536048,119.873698 146.468577,127.730944 C131.402107,135.585189 123.031844,135.508187 111.129473,129.818008 C99.2261007,124.130831 23.9127473,93.7048802 10.3463234,87.2226777 C3.56511141,83.9805764 0,81.2474909 0,78.6654102 L0,52.7856015 C0,52.7856015 98.0490639,31.4419345 113.878559,25.7637571 C129.707053,20.0845797 135.199225,19.8795733 148.669646,24.8137275 C162.141067,29.7488817 242.686583,44.2783357 256,49.1554881 C256,49.1554881 255.993999,72.2662103 255.993999,74.6672853 C255.996,77.2223652 252.923904,80.0284528 245.969687,83.6525661"
fill="#912626"
></path>
<path
d="M245.964922,57.929387 C232.303935,65.0493802 161.534003,94.1493524 146.467017,102.004345 C131.401031,109.858338 123.031039,109.781338 111.12905,104.093343 C99.2270617,98.4053484 23.9151336,67.9813773 10.3491466,61.4983836 C-3.21684053,55.0153898 -3.50184026,50.550394 9.82514705,45.331399 C23.1511343,40.113404 98.0490629,10.729432 113.880047,5.04943744 C129.708033,-0.629557148 135.199028,-0.833556953 148.669014,4.10143834 C162.140002,9.03643363 232.487935,37.0354069 245.799922,41.9124022 C259.11491,46.7883976 259.625908,50.8093938 245.964922,57.929387"
fill="#C6302B"
></path>
<path
d="M159.282977,32.7570853 L137.273922,35.0422326 L132.346419,46.8976124 L124.387597,33.667969 L98.973147,31.383814 L117.936992,24.5452403 L112.247442,14.0472558 L130.001736,20.9910078 L146.739969,15.5108217 L142.21631,26.3660155 L159.282977,32.7570853"
fill="#FFFFFF"
></path>
<path
d="M131.03169,90.2747287 L89.9546047,73.2378295 L148.815752,64.2034109 L131.03169,90.2747287"
fill="#FFFFFF"
></path>
<path
d="M74.0816124,39.3466047 C91.4568682,39.3466047 105.541829,44.8069457 105.541829,51.5413333 C105.541829,58.2767132 91.4568682,63.736062 74.0816124,63.736062 C56.7073488,63.736062 42.6213953,58.2767132 42.6213953,51.5413333 C42.6213953,44.8069457 56.7073488,39.3466047 74.0816124,39.3466047"
fill="#FFFFFF"
></path>
<path
d="M185.29476,35.9977674 L220.130605,49.7642171 L185.324527,63.5167752 L185.29476,35.9977674"
fill="#621B1C"
></path>
<path
d="M146.754853,51.2426667 L185.29476,35.9977674 L185.324527,63.5167752 L181.546047,64.9952248 L146.754853,51.2426667"
fill="#9A2928"
></path>
</g>
</svg>
<span>Redis</span>
</span>
<span
className={cn(
" space-x-1 min-w-24 px-2 py-1 bg-card text-sm rounded-md flex justify-center items-center shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]"
)}
>
<VercelLogo /> <span>Vercel</span>
</span>
<span
className={cn(
" min-w-24 flex justify-center bg-card items-center px-2 py-1 space-x-1 text-sm rounded-md shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]"
)}
>
<AWSLogo /> <span>AWS</span>
</span>
<span
className={cn(
" space-x-1 min-w-24 flex justify-center items-center bg-card px-2 py-1 text-sm rounded-md shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]"
)}
>
<GitHubLogo /> <span>GitHub</span>
</span>
</div>
);
};
// Logos
const GitHubLogo = () => {
return (
<svg
width="800px"
height="800px"
viewBox="0 0 20 20"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 object-contain"
>
<g
id="Page-1"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
>
<g
id="Dribbble-Light-Preview"
transform="translate(-140.000000, -7559.000000)"
fill="#000000"
>
<g id="icons" transform="translate(56.000000, 160.000000)">
<path
d="M94,7399 C99.523,7399 104,7403.59 104,7409.253 C104,7413.782 101.138,7417.624 97.167,7418.981 C96.66,7419.082 96.48,7418.762 96.48,7418.489 C96.48,7418.151 96.492,7417.047 96.492,7415.675 C96.492,7414.719 96.172,7414.095 95.813,7413.777 C98.04,7413.523 100.38,7412.656 100.38,7408.718 C100.38,7407.598 99.992,7406.684 99.35,7405.966 C99.454,7405.707 99.797,7404.664 99.252,7403.252 C99.252,7403.252 98.414,7402.977 96.505,7404.303 C95.706,7404.076 94.85,7403.962 94,7403.958 C93.15,7403.962 92.295,7404.076 91.497,7404.303 C89.586,7402.977 88.746,7403.252 88.746,7403.252 C88.203,7404.664 88.546,7405.707 88.649,7405.966 C88.01,7406.684 87.619,7407.598 87.619,7408.718 C87.619,7412.646 89.954,7413.526 92.175,7413.785 C91.889,7414.041 91.63,7414.493 91.54,7415.156 C90.97,7415.418 89.522,7415.871 88.63,7414.304 C88.63,7414.304 88.101,7413.319 87.097,7413.247 C87.097,7413.247 86.122,7413.234 87.029,7413.87 C87.029,7413.87 87.684,7414.185 88.139,7415.37 C88.139,7415.37 88.726,7417.2 91.508,7416.58 C91.513,7417.437 91.522,7418.245 91.522,7418.489 C91.522,7418.76 91.338,7419.077 90.839,7418.982 C86.865,7417.627 84,7413.783 84,7409.253 C84,7403.59 88.478,7399 94,7399"
id="github-[#142]"
></path>
</g>
</g>
</g>
</svg>
);
};
const VercelLogo = () => {
return (
<svg
fill="#000000"
width="800px"
height="800px"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 object-contain"
>
<path fillRule="evenodd" d="M256,48,496,464H16Z" />
</svg>
);
};
const AWSLogo = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width="48px"
height="48px"
className="h-4 w-4 object-contain"
>
<path
fill="#252f3e"
d="M13.527,21.529c0,0.597,0.064,1.08,0.176,1.435c0.128,0.355,0.287,0.742,0.511,1.161 c0.08,0.129,0.112,0.258,0.112,0.371c0,0.161-0.096,0.322-0.303,0.484l-1.006,0.677c-0.144,0.097-0.287,0.145-0.415,0.145 c-0.16,0-0.319-0.081-0.479-0.226c-0.224-0.242-0.415-0.5-0.575-0.758c-0.16-0.274-0.319-0.58-0.495-0.951 c-1.245,1.483-2.81,2.225-4.694,2.225c-1.341,0-2.411-0.387-3.193-1.161s-1.181-1.806-1.181-3.096c0-1.37,0.479-2.483,1.453-3.321 s2.267-1.258,3.911-1.258c0.543,0,1.102,0.048,1.692,0.129s1.197,0.21,1.836,0.355v-1.177c0-1.225-0.255-2.08-0.75-2.58 c-0.511-0.5-1.373-0.742-2.602-0.742c-0.559,0-1.133,0.064-1.724,0.21c-0.591,0.145-1.165,0.322-1.724,0.548 c-0.255,0.113-0.447,0.177-0.559,0.21c-0.112,0.032-0.192,0.048-0.255,0.048c-0.224,0-0.335-0.161-0.335-0.5v-0.79 c0-0.258,0.032-0.451,0.112-0.564c0.08-0.113,0.224-0.226,0.447-0.339c0.559-0.29,1.229-0.532,2.012-0.726 c0.782-0.21,1.612-0.306,2.49-0.306c1.9,0,3.289,0.435,4.183,1.306c0.878,0.871,1.325,2.193,1.325,3.966v5.224H13.527z M7.045,23.979c0.527,0,1.07-0.097,1.644-0.29c0.575-0.193,1.086-0.548,1.517-1.032c0.255-0.306,0.447-0.645,0.543-1.032 c0.096-0.387,0.16-0.855,0.16-1.403v-0.677c-0.463-0.113-0.958-0.21-1.469-0.274c-0.511-0.064-1.006-0.097-1.501-0.097 c-1.07,0-1.852,0.21-2.379,0.645s-0.782,1.048-0.782,1.854c0,0.758,0.192,1.322,0.591,1.709 C5.752,23.786,6.311,23.979,7.045,23.979z M19.865,25.721c-0.287,0-0.479-0.048-0.607-0.161c-0.128-0.097-0.239-0.322-0.335-0.629 l-3.752-12.463c-0.096-0.322-0.144-0.532-0.144-0.645c0-0.258,0.128-0.403,0.383-0.403h1.565c0.303,0,0.511,0.048,0.623,0.161 c0.128,0.097,0.223,0.322,0.319,0.629l2.682,10.674l2.49-10.674c0.08-0.322,0.176-0.532,0.303-0.629 c0.128-0.097,0.351-0.161,0.639-0.161h1.277c0.303,0,0.511,0.048,0.639,0.161c0.128,0.097,0.239,0.322,0.303,0.629l2.522,10.803 l2.762-10.803c0.096-0.322,0.208-0.532,0.319-0.629c0.128-0.097,0.335-0.161,0.623-0.161h1.485c0.255,0,0.399,0.129,0.399,0.403 c0,0.081-0.016,0.161-0.032,0.258s-0.048,0.226-0.112,0.403l-3.847,12.463c-0.096,0.322-0.208,0.532-0.335,0.629 s-0.335,0.161-0.607,0.161h-1.373c-0.303,0-0.511-0.048-0.639-0.161c-0.128-0.113-0.239-0.322-0.303-0.645l-2.474-10.4 L22.18,24.915c-0.08,0.322-0.176,0.532-0.303,0.645c-0.128,0.113-0.351,0.161-0.639,0.161H19.865z M40.379,26.156 c-0.83,0-1.66-0.097-2.458-0.29c-0.798-0.193-1.421-0.403-1.836-0.645c-0.255-0.145-0.431-0.306-0.495-0.451 c-0.064-0.145-0.096-0.306-0.096-0.451v-0.822c0-0.339,0.128-0.5,0.367-0.5c0.096,0,0.192,0.016,0.287,0.048 c0.096,0.032,0.239,0.097,0.399,0.161c0.543,0.242,1.133,0.435,1.756,0.564c0.639,0.129,1.261,0.193,1.9,0.193 c1.006,0,1.788-0.177,2.331-0.532c0.543-0.355,0.83-0.871,0.83-1.532c0-0.451-0.144-0.822-0.431-1.129 c-0.287-0.306-0.83-0.58-1.612-0.838l-2.315-0.726c-1.165-0.371-2.027-0.919-2.554-1.645c-0.527-0.709-0.798-1.499-0.798-2.338 c0-0.677,0.144-1.274,0.431-1.79s0.671-0.967,1.149-1.322c0.479-0.371,1.022-0.645,1.66-0.838C39.533,11.081,40.203,11,40.906,11 c0.351,0,0.718,0.016,1.07,0.064c0.367,0.048,0.702,0.113,1.038,0.177c0.319,0.081,0.623,0.161,0.91,0.258s0.511,0.193,0.671,0.29 c0.224,0.129,0.383,0.258,0.479,0.403c0.096,0.129,0.144,0.306,0.144,0.532v0.758c0,0.339-0.128,0.516-0.367,0.516 c-0.128,0-0.335-0.064-0.607-0.193c-0.91-0.419-1.932-0.629-3.065-0.629c-0.91,0-1.628,0.145-2.123,0.451 c-0.495,0.306-0.75,0.774-0.75,1.435c0,0.451,0.16,0.838,0.479,1.145c0.319,0.306,0.91,0.613,1.756,0.887l2.267,0.726 c1.149,0.371,1.98,0.887,2.474,1.548s0.734,1.419,0.734,2.257c0,0.693-0.144,1.322-0.415,1.87 c-0.287,0.548-0.671,1.032-1.165,1.419c-0.495,0.403-1.086,0.693-1.772,0.903C41.943,26.043,41.193,26.156,40.379,26.156z"
/>
<path
fill="#f90"
d="M43.396,33.992c-5.252,3.918-12.883,5.998-19.445,5.998c-9.195,0-17.481-3.434-23.739-9.142 c-0.495-0.451-0.048-1.064,0.543-0.709c6.769,3.966,15.118,6.369,23.755,6.369c5.827,0,12.229-1.225,18.119-3.741 C43.508,32.364,44.258,33.347,43.396,33.992z M45.583,31.477c-0.671-0.871-4.438-0.419-6.146-0.21 c-0.511,0.064-0.591-0.387-0.128-0.726c3.001-2.128,7.934-1.516,8.509-0.806c0.575,0.726-0.16,5.708-2.969,8.094 c-0.431,0.371-0.846,0.177-0.655-0.306C44.833,35.927,46.254,32.331,45.583,31.477z"
/>
</svg>
);
};
@@ -0,0 +1,71 @@
"use client";
import { stagger, useAnimate } from "framer-motion";
import React, { useState } from "react";
export const SkeletonTwo = () => {
const [scope, animate] = useAnimate();
const [animating, setAnimating] = useState(false);
const handleAnimation = async () => {
if (animating) return;
setAnimating(true);
await animate(
".message",
{
opacity: [0, 1],
y: [20, 0],
},
{
delay: stagger(0.5),
}
);
setAnimating(false);
};
return (
<div className="relative h-full w-full mt-4">
<div className="absolute bottom-0 inset-x-0 h-40 bg-gradient-to-t from-background via-background to-transparent w-full pointer-events-none" />
<div className="p-4 border border-border bg-muted rounded-[32px] h-full z-20">
<div className="p-2 bg-card border border-border rounded-[24px] h-full">
<div className="w-20 rounded-full bg-muted/80 mx-auto h-6" />
<div
onMouseEnter={handleAnimation}
ref={scope}
className="content mt-4 w-[90%] mx-auto"
>
<UserMessage>
Hello chat! Give me all the links from this website -
https://ui.aceternity.com
</UserMessage>
<AIMessage>Why don&apos;t you do it yourself?</AIMessage>
<UserMessage>
Umm.. Because I&apos;m paying $20/mo for your services?
</UserMessage>
<AIMessage>You think I work for the money?</AIMessage>
<UserMessage>Who do you think you are?</UserMessage>
<AIMessage>I&apos; batman.</AIMessage>
<AIMessage>
Now Playing <br />{" "}
<span className="italic">Something in the way - Nirvana</span>
</AIMessage>
</div>
</div>
</div>
</div>
);
};
const UserMessage = ({ children }: { children: React.ReactNode }) => {
return (
<div className="message bg-muted text-foreground p-2 sm:p-4 text-[10px] sm:text-xs my-4 rounded-md">
{children}
</div>
);
};
const AIMessage = ({ children }: { children: React.ReactNode }) => {
return (
<div className="message bg-foreground text-background p-2 sm:p-4 text-[10px] sm:text-xs my-4 rounded-md">
{children}
</div>
);
};
@@ -0,0 +1,59 @@
"use client";
import React, { useState } from "react";
import { motion } from "framer-motion";
import Image from "next/image";
import { IconDots, IconPlus } from "@tabler/icons-react";
import { Switch } from "../switch";
export const SkeletonThree = () => {
return (
<div className="h-full w-full sm:w-[80%] mx-auto bg-card shadow-2xl mt-10 group rounded-md">
<div className="absolute bottom-0 inset-x-0 h-40 bg-gradient-to-t from-background via-background to-transparent w-full pointer-events-none z-[11]" />
<div className="flex flex-1 w-full h-full flex-col space-y-2 ">
<div className="flex justify-between border-b border-border pb-2 p-4">
<p className="text-muted-foreground text-sm font-bold">
Add LLM
</p>
<p className="shadow-derek text-muted-foreground text-sm px-2 py-1 rounded-md flex-shrink-0 flex space-x-1 items-center bg-muted">
<IconPlus className="h-4 w-4 text-muted-foreground" />{" "}
<span>Add</span>
</p>
</div>
<div className="flex flex-col space-y-4 p-4">
<Row title="Groq LLM" updatedAt="23rd March" />
<Row title="OpenAI GPT0" updatedAt="21st March" active />
<Row title="Stable DIffusion" updatedAt="3rd May" />
<Row title="Llama 2" updatedAt="1st April" active />
<Row title="Claude 200k" updatedAt="2nd June" active />
</div>
</div>
</div>
);
};
export const Row = ({
title,
updatedAt,
active = false,
}: {
title: string;
updatedAt: string;
active?: boolean;
}) => {
const [checked, setChecked] = useState(active);
return (
<div className="flex justify-between items-center">
<div className="flex space-x-2 items-center">
<p className="text-muted-foreground text-xs shadow-aceternity bg-muted px-1 py-0.5 rounded-md">
{title}
</p>
<p className="text-muted-foreground text-xs">{updatedAt}</p>
</div>
<div className="flex items-center space-x-1">
<Switch checked={checked} setChecked={setChecked} />
<IconDots className="h-4 w-4 text-muted-foreground" />
</div>
</div>
);
};
@@ -0,0 +1,28 @@
import { cn } from "@/lib/utils";
import { MotionProps, Transition } from "framer-motion";
import React from "react";
export const Subheading = ({
className,
as: Tag = "p",
children,
...props
}: {
className?: string;
as?: any;
children: any;
props?: React.HTMLAttributes<HTMLHeadingElement | Transition>;
} & MotionProps &
React.HTMLAttributes<HTMLHeadingElement | Transition>) => {
return (
<Tag
className={cn(
"text-sm md:text-base max-w-4xl text-left my-4 mx-auto",
"text-muted-foreground text-center font-normal",
className
)}
>
{children}
</Tag>
);
};
+48
View File
@@ -0,0 +1,48 @@
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
import { useId } from "react";
export const Switch = ({
checked,
setChecked,
}: {
checked: boolean;
setChecked: (checked: boolean) => void;
}) => {
const id = useId();
return (
<form className="flex space-x-4 antialiased items-center">
<label
htmlFor={id}
className={cn(
"h-4 px-1 w-[40px] flex items-center border border-transparent shadow-[inset_0px_0px_12px_rgba(0,0,0,0.25)] rounded-full relative cursor-pointer transition duration-200",
checked ? "bg-primary" : "bg-muted border-border"
)}
>
<motion.div
initial={{
width: "10px",
x: checked ? -2 : 20,
}}
animate={{
height: ["12px", "10px", "12px"],
width: ["12px", "18px", "12px", "12px"],
x: checked ? 20 : -2,
}}
transition={{
duration: 0.3,
delay: 0.1,
}}
key={String(checked)}
className={cn("h-[20px] block rounded-full bg-background shadow-md z-10")}
></motion.div>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
className="hidden"
id={id}
/>
</label>
</form>
);
};
@@ -0,0 +1,43 @@
"use client";
import { cn } from "@/lib/utils";
import { useEffect, useRef, useState } from "react";
export function TestimonialColumnContainer({
className,
shift = 0,
children,
}: {
className?: string;
shift?: number;
children: React.ReactNode;
}) {
let columnRef = useRef<React.ElementRef<"div">>(null);
let [columnHeight, setColumnHeight] = useState(0);
let duration = `${columnHeight * shift}ms`;
useEffect(() => {
if (!columnRef.current) {
return;
}
let resizeObserver = new window.ResizeObserver(() => {
setColumnHeight(columnRef.current?.offsetHeight ?? 0);
});
resizeObserver.observe(columnRef.current);
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<div
ref={columnRef}
className={cn("animate-marquee space-y-8 py-4", className)}
style={{ "--marquee-duration": duration } as React.CSSProperties}
>
{children}
</div>
);
}
@@ -0,0 +1,188 @@
"use client";
import { useState } from "react";
import { Heading } from "./heading";
import { Subheading } from "./subheading";
import { cn } from "@/lib/utils";
import { InViewDiv } from "./in-view-div";
import { useMemo } from "react";
import { TestimonialColumnContainer } from "./testimonial-column-container";
import Image from "next/image";
import { useTranslations } from 'next-intl';
export const Testimonials = () => {
const t = useTranslations('testimonials');
return (
<div className="relative z-20 py-10 md:py-40">
<Heading as="h2">{t('title')}</Heading>
<Subheading className="text-center max-w-lg mx-auto">
{t('subtitle')}
</Subheading>
<TestimonialGrid />
</div>
);
};
interface Testimonial {
name: string;
quote: string;
src: string;
designation?: string;
}
function Testimonial({
name,
quote,
src,
designation,
className,
...props
}: Omit<React.ComponentPropsWithoutRef<"figure">, keyof Testimonial> &
Testimonial) {
let animationDelay = useMemo(() => {
let possibleAnimationDelays = [
"0s",
"0.1s",
"0.2s",
"0.3s",
"0.4s",
"0.5s",
];
return possibleAnimationDelays[
Math.floor(Math.random() * possibleAnimationDelays.length)
];
}, []);
const boxStyle = {};
return (
<figure
className={cn(
"animate-fade-in rounded-3xl bg-card p-8 opacity-0 shadow-derek",
className
)}
style={{
animationDelay,
}}
{...props}
>
<div className="flex flex-col items-start">
<div className="flex gap-2">
<Image
src={src}
width={150}
height={150}
className="h-10 w-10 rounded-full"
alt={name}
/>
<div>
<h3 className="text-sm font-medium text-muted-foreground">
{name}
</h3>
<p className="text-sm font-normal text-muted-foreground">
{designation}
</p>
</div>
</div>
<p className="text-base text-muted-foreground mt-4">
{quote}
</p>
</div>
</figure>
);
}
function TestimonialColumn({
testimonials,
className,
containerClassName,
shift = 0,
}: {
testimonials: Testimonial[];
className?: string;
containerClassName?: (reviewIndex: number) => string;
shift?: number;
}) {
return (
<TestimonialColumnContainer className={cn(className)} shift={shift}>
{testimonials
.concat(testimonials)
.map((testimonial, testimonialIndex) => (
<Testimonial
name={testimonial.name}
quote={testimonial.quote}
src={testimonial.src}
designation={testimonial.designation}
key={testimonialIndex}
className={containerClassName?.(
testimonialIndex % testimonials.length
)}
/>
))}
</TestimonialColumnContainer>
);
}
function splitArray<T>(array: Array<T>, numParts: number) {
let result: Array<Array<T>> = [];
for (let i = 0; i < array.length; i++) {
let index = i % numParts;
if (!result[index]) {
result[index] = [];
}
result[index].push(array[i]);
}
return result;
}
function TestimonialGrid() {
const t = useTranslations('testimonials');
// Get all testimonials from translations
// Using raw to get the array directly
const items = t.raw('items') as Array<{
name: string;
quote: string;
designation: string;
}>;
const testimonials = items.map((item, i) => ({
name: item.name,
quote: item.quote,
designation: item.designation,
src: `https://i.pravatar.cc/150?img=${i + 1}`
}));
let columns = splitArray(testimonials, 3);
let column1 = columns[0];
let column2 = columns[1];
let column3 = splitArray(columns[2], 2);
return (
<InViewDiv className="relative -mx-4 mt-16 grid h-[49rem] max-h-[150vh] grid-cols-1 items-start gap-8 overflow-hidden px-4 sm:mt-20 md:grid-cols-2 lg:grid-cols-3">
<TestimonialColumn
testimonials={[...column1, ...column3.flat(), ...column2]}
containerClassName={(tIndex) =>
cn(
tIndex >= column1.length + column3[0].length && "md:hidden",
tIndex >= column1.length && "lg:hidden"
)
}
shift={10}
/>
<TestimonialColumn
testimonials={[...column2, ...column3[1]]}
className="hidden md:block"
containerClassName={(tIndex) =>
tIndex >= column2.length ? "lg:hidden" : ""
}
shift={15}
/>
<TestimonialColumn
testimonials={column3.flat()}
className="hidden lg:block"
shift={10}
/>
<div className="pointer-events-none absolute inset-x-0 top-0 h-32 bg-gradient-to-b from-background" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-background" />
</InViewDiv>
);
}
+161
View File
@@ -0,0 +1,161 @@
import { ExternalLink } from "lucide-react";
import { LocaleLink } from "@/components/locale-link";
import { ToolLogo } from "@/components/tool-logo";
import { FavoriteButton } from "@/features/tools/components/favorite-button";
import { VoteButton } from "@/features/voting/components/vote-button";
import { StarRating } from "@/features/reviews/components/star-rating";
import { cn } from "@/lib/utils";
export interface ToolCardData {
id: string;
slug: string;
domain: string | null;
websiteUrl: string | null;
logoUrl: string | null;
nameEn: string;
nameZh: string | null;
taglineEn?: string | null;
taglineZh?: string | null;
descriptionEn?: string | null;
descriptionZh?: string | null;
pricing?: string | null;
voteCount?: number;
ratingAvg?: number;
reviewCount?: number;
}
interface ToolCardProps {
tool: ToolCardData;
locale: string;
/** compact: 单行(榜单);default: 带描述卡片 */
variant?: "default" | "compact";
/** 排名序号(榜单场景展示) */
rank?: number;
className?: string;
}
const pricingLabels: Record<string, { en: string; zh: string }> = {
free: { en: "Free", zh: "免费" },
freemium: { en: "Freemium", zh: "免费增值" },
paid: { en: "Paid", zh: "付费" },
free_trial: { en: "Free Trial", zh: "免费试用" },
contact: { en: "Contact", zh: "联系定价" },
open_source: { en: "Open Source", zh: "开源" },
};
/**
* # 组件 ToolCard
* 说明:Core 通用工具卡片(服务端组件,内嵌客户端投票/收藏按钮)。
* 五套主题均可复用;variant 控制密度。
*/
export function ToolCard({
tool,
locale,
variant = "default",
rank,
className,
}: ToolCardProps) {
const name = locale === "zh" && tool.nameZh ? tool.nameZh : tool.nameEn;
const tagline =
locale === "zh"
? tool.taglineZh || tool.descriptionZh || tool.taglineEn || tool.descriptionEn
: tool.taglineEn || tool.descriptionEn || tool.taglineZh || tool.descriptionZh;
const pricing = tool.pricing ? pricingLabels[tool.pricing] : null;
return (
<div
className={cn(
"group relative flex gap-4 rounded-xl border border-border bg-background p-4 transition-all hover:border-primary/30 hover:shadow-sm",
className
)}
>
{typeof rank === "number" && (
<span className="absolute -left-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs font-bold text-primary-foreground shadow tabular-nums">
{rank}
</span>
)}
{/* Product Hunt 风格投票块 */}
<VoteButton toolId={tool.id} variant="card" initialCount={tool.voteCount ?? 0} />
<LocaleLink
href={`/tools/${tool.slug}`}
className="flex min-w-0 flex-1 items-start gap-3"
>
<ToolLogo src={tool.logoUrl} alt={name} size={44} className="shrink-0 rounded-lg" />
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="truncate text-sm font-semibold text-foreground group-hover:text-primary">
{name}
</h3>
{pricing && (
<span className="rounded-full bg-secondary px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
{locale === "zh" ? pricing.zh : pricing.en}
</span>
)}
{(tool.ratingAvg ?? 0) > 0 && (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
<StarRating value={tool.ratingAvg ?? 0} />
<span className="tabular-nums">{(tool.ratingAvg ?? 0).toFixed(1)}</span>
</span>
)}
</div>
{tagline && (
<p
className={cn(
"mt-1 text-xs leading-relaxed text-muted-foreground",
variant === "compact" ? "line-clamp-1" : "line-clamp-2"
)}
>
{tagline}
</p>
)}
</div>
</LocaleLink>
<div className="flex shrink-0 flex-col items-center gap-1">
<FavoriteButton toolId={tool.id} />
{tool.websiteUrl && (
<a
href={`/api/outbound?tool=${tool.slug}`}
target="_blank"
rel="noopener noreferrer nofollow"
aria-label={`Visit ${name}`}
className="rounded-full p-2 text-muted-foreground/50 transition-colors hover:text-primary"
>
<ExternalLink className="h-4 w-4" />
</a>
)}
</div>
</div>
);
}
/**
* # 组件 ToolCardGrid
* 说明:卡片网格容器(响应式,Mobile First)。
*/
export function ToolCardGrid({
tools,
locale,
ranked = false,
className,
}: {
tools: ToolCardData[];
locale: string;
ranked?: boolean;
className?: string;
}) {
return (
<div className={cn("grid gap-3 md:grid-cols-2", className)}>
{tools.map((tool, index) => (
<ToolCard
key={tool.id}
tool={tool}
locale={locale}
rank={ranked ? index + 1 : undefined}
/>
))}
</div>
);
}
@@ -0,0 +1,63 @@
import Image from "next/image";
import { cn } from "@/lib/utils";
interface ToolLogoProps {
src: string | null;
alt: string;
size?: number;
className?: string;
}
/**
* 工具 Logo:走 next/image(懒加载 + 布局占位),但标记 unoptimized——
* logo 多为各官网的 favicon.ico,sharp 无法解码 ICO 格式,经优化器必然裂图;
* 且 48-80px 小图优化收益为零,直接由浏览器加载。
* data: URL(未配置 R2 时的上传回退)next/image 不支持,降级为原生 img;
* 无图时显示首字母占位。
*/
export function ToolLogo({ src, alt, size = 48, className }: ToolLogoProps) {
const style = { width: size, height: size };
if (!src) {
return (
<div
style={style}
className={cn(
"flex flex-shrink-0 items-center justify-center rounded-lg bg-secondary",
className
)}
>
<span
className="font-bold text-muted-foreground"
style={{ fontSize: size * 0.4 }}
>
{alt.charAt(0).toUpperCase()}
</span>
</div>
);
}
if (src.startsWith("data:")) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={src}
alt={alt}
style={style}
className={cn("flex-shrink-0 rounded-lg object-cover", className)}
/>
);
}
return (
<Image
src={src}
alt={alt}
width={size}
height={size}
style={style}
unoptimized
className={cn("flex-shrink-0 rounded-lg object-cover", className)}
/>
);
}
@@ -0,0 +1,93 @@
"use client";
import Image from "next/image";
import React, { useState } from "react";
import {
motion,
useTransform,
AnimatePresence,
useMotionValue,
useSpring,
} from "framer-motion";
export const AnimatedTooltip = ({
items,
}: {
items: {
id: number;
name: string;
designation: string;
image: string;
}[];
}) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const springConfig = { stiffness: 100, damping: 5 };
const x = useMotionValue(0); // going to set this value on mouse move
// rotate the tooltip
const rotate = useSpring(
useTransform(x, [-100, 100], [-45, 45]),
springConfig
);
// translate the tooltip
const translateX = useSpring(
useTransform(x, [-100, 100], [-50, 50]),
springConfig
);
const handleMouseMove = (event: any) => {
const halfWidth = event.target.offsetWidth / 2;
x.set(event.nativeEvent.offsetX - halfWidth); // set the x value, which is then used in transform and rotate
};
return (
<>
{items.map((item) => (
<div
className="-mr-4 relative group"
key={item.name}
onMouseEnter={() => setHoveredIndex(item.id)}
onMouseLeave={() => setHoveredIndex(null)}
>
<AnimatePresence>
{hoveredIndex === item.id && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.6 }}
animate={{
opacity: 1,
y: 0,
scale: 1,
transition: {
type: "spring",
stiffness: 260,
damping: 10,
},
}}
exit={{ opacity: 0, y: 20, scale: 0.6 }}
style={{
translateX: translateX,
rotate: rotate,
whiteSpace: "nowrap",
}}
className="absolute -top-16 -left-1/2 translate-x-1/2 flex text-xs flex-col items-center justify-center rounded-md bg-popover text-popover-foreground z-50 shadow-xl px-4 py-2 border border-border"
>
<div className="absolute inset-x-10 z-30 w-[20%] -bottom-px bg-gradient-to-r from-transparent via-emerald-500 to-transparent h-px " />
<div className="absolute left-10 w-[40%] z-30 -bottom-px bg-gradient-to-r from-transparent via-sky-500 to-transparent h-px " />
<div className="font-bold text-popover-foreground relative z-30 text-base">
{item.name}
</div>
<div className="text-popover-foreground text-xs">{item.designation}</div>
</motion.div>
)}
</AnimatePresence>
<Image
onMouseMove={handleMouseMove}
height={100}
width={100}
src={item.image}
alt={item.name}
unoptimized
className="object-cover !m-0 !p-0 object-top rounded-full h-14 w-14 border-2 group-hover:scale-105 group-hover:z-30 border-border relative transition duration-500"
/>
</div>
))}
</>
);
};
+181
View File
@@ -0,0 +1,181 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(
"text-[#9393A3] pb-0 text-sm",
error && "text-destructive",
className
)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-xs font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};
@@ -0,0 +1,73 @@
"use client";
import { cn } from "@/lib/utils";
import React, { useEffect, useState } from "react";
export const InfiniteMovingCards = ({
direction = "left",
speed = "fast",
pauseOnHover = true,
children,
}: {
direction?: "left" | "right";
speed?: "fast" | "normal" | "slow";
pauseOnHover?: boolean;
children: React.ReactNode;
}) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLDivElement>(null);
const hasInitialized = React.useRef(false);
const [start, setStart] = useState(false);
useEffect(() => {
if (!containerRef.current || !scrollerRef.current || hasInitialized.current) {
return;
}
const scrollerContent = Array.from(scrollerRef.current.children);
scrollerContent.forEach((item) => {
const duplicatedItem = item.cloneNode(true);
scrollerRef.current?.appendChild(duplicatedItem);
});
hasInitialized.current = true;
setStart(true);
}, []);
useEffect(() => {
if (!containerRef.current) {
return;
}
const animationDirection = direction === "left" ? "forwards" : "reverse";
containerRef.current.style.setProperty("--animation-direction", animationDirection);
}, [direction]);
useEffect(() => {
if (!containerRef.current) {
return;
}
const duration = speed === "fast" ? "20s" : speed === "normal" ? "40s" : "80s";
containerRef.current.style.setProperty("--animation-duration", duration);
}, [speed]);
return (
<div
ref={containerRef}
className={cn(
"scroller relative z-20 group max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]"
)}
>
<div
ref={scrollerRef}
className={cn(
" flex min-w-full shrink-0 gap-4 py-4 w-max flex-nowrap",
start && "animate-scroll [animation-play-state:running]",
pauseOnHover && "group-hover:[animation-play-state:paused]"
)}
>
{children}
</div>
</div>
);
};
+26
View File
@@ -0,0 +1,26 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
+6
View File
@@ -0,0 +1,6 @@
const toBoolean = (value: string | undefined): boolean =>
value?.toLowerCase() === "true";
export const analyticsConfig = {
enableInDevelopment: toBoolean(process.env.NEXT_PUBLIC_ANALYTICS_ENABLE_IN_DEVELOPMENT),
};
@@ -0,0 +1,107 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-28",
title: "AI Chatbots for Business: ChatGPT, Claude, and Gemini Compared",
description:
"A detailed comparison of enterprise AI chatbots. Learn which AI assistant is best for your business needs, from customer service to content creation.",
image:
"https://images.unsplash.com/photo-1531746790731-6c087fecd65a?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## The Business AI Assistant Revolution
AI chatbots have evolved from simple FAQ bots to sophisticated business partners. Today's enterprise AI assistants can draft emails, analyze data, write code, and even help with strategic decisions.
## The Big Three: An Overview
### ChatGPT (OpenAI)
**Best for:** General-purpose business tasks, content creation, coding assistance
ChatGPT remains the most widely adopted AI assistant in the business world. With GPT-4 Turbo and the new GPT-4o model, it offers:
- **128K context window** - Process entire documents at once
- **Multimodal capabilities** - Analyze images and charts
- **Custom GPTs** - Build specialized assistants for your team
- **API access** - Integrate into your existing workflows
**Pricing:** $20/month (Plus) or $25/user/month (Team)
### Claude (Anthropic)
**Best for:** Long document analysis, nuanced writing, safety-critical applications
Claude has carved out a niche for businesses requiring thoughtful, nuanced AI interactions:
- **200K context window** - Industry-leading document processing
- **Constitutional AI** - Built-in safety and ethics guidelines
- **Excellent at following complex instructions**
- **Strong coding and analysis capabilities**
**Pricing:** $20/month (Pro) or custom enterprise pricing
### Gemini (Google)
**Best for:** Google Workspace integration, research, real-time information
Google's Gemini offers unique advantages for businesses in the Google ecosystem:
- **Deep Google integration** - Works with Docs, Sheets, Gmail
- **Real-time information** - Access to current web data
- **Multimodal from the ground up** - Native image and video understanding
- **Competitive pricing** - Generous free tier
**Pricing:** $20/month (Advanced) or included with Workspace plans
## Head-to-Head Comparison
| Feature | ChatGPT | Claude | Gemini |
|---------|---------|--------|--------|
| Context Window | 128K | 200K | 1M (Gemini 1.5) |
| Coding | Excellent | Very Good | Good |
| Writing | Very Good | Excellent | Good |
| Analysis | Very Good | Excellent | Very Good |
| Integration | API-focused | API-focused | Google-native |
| Custom Training | Yes (GPTs) | Limited | Limited |
## Use Case Recommendations
### Customer Support
**Winner: ChatGPT** - Best ecosystem for building custom support bots
### Content Marketing
**Winner: Claude** - Superior long-form writing quality
### Data Analysis
**Winner: Gemini** - Seamless Google Sheets integration
### Software Development
**Winner: ChatGPT** - Most comprehensive coding assistance
### Research & Analysis
**Winner: Claude** - Best for processing lengthy documents
## Implementation Tips
1. **Start with a pilot** - Test with a small team before company-wide rollout
2. **Define clear use cases** - Know what problems you're solving
3. **Train your team** - Invest in prompt engineering education
4. **Monitor usage** - Track ROI and identify optimization opportunities
5. **Stay compliant** - Ensure AI use aligns with data privacy regulations
## Conclusion
There's no one-size-fits-all AI chatbot for business. Your choice should depend on your specific needs, existing tech stack, and budget. Many businesses find value in using multiple AI assistants for different tasks.
Explore our complete directory of AI chatbots and assistants on Toolso.AI to find the perfect fit for your organization.
@@ -0,0 +1,107 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-28",
title: "企业级AI聊天机器人对比:ChatGPT、Claude和Gemini",
description:
"深入对比企业级AI聊天机器人。了解哪款AI助手最适合您的业务需求,从客户服务到内容创作。",
image:
"https://images.unsplash.com/photo-1531746790731-6c087fecd65a?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## 商业AI助手的革命
AI聊天机器人已从简单的FAQ机器人演变为成熟的商业伙伴。如今的企业级AI助手可以起草邮件、分析数据、编写代码,甚至协助战略决策。
## 三巨头概览
### ChatGPT (OpenAI)
**最适合:** 通用商业任务、内容创作、编程辅助
ChatGPT仍然是商业领域最广泛采用的AI助手。凭借GPT-4 Turbo和新的GPT-4o模型,它提供:
- **128K上下文窗口** - 一次处理整个文档
- **多模态能力** - 分析图像和图表
- **自定义GPTs** - 为团队构建专属助手
- **API访问** - 集成到现有工作流程
**定价:** $20/月(Plus)或 $25/用户/月(Team)
### Claude (Anthropic)
**最适合:** 长文档分析、细腻写作、安全关键应用
Claude为需要深思熟虑、细腻AI交互的企业开辟了一片天地:
- **200K上下文窗口** - 行业领先的文档处理能力
- **宪法AI** - 内置安全和道德准则
- **擅长遵循复杂指令**
- **强大的编码和分析能力**
**定价:** $20/月(Pro)或定制企业定价
### Gemini (Google)
**最适合:** Google Workspace集成、研究、实时信息
Google的Gemini为Google生态系统中的企业提供独特优势:
- **深度Google集成** - 与Docs、Sheets、Gmail协同工作
- **实时信息** - 访问当前网络数据
- **原生多模态** - 原生图像和视频理解
- **有竞争力的定价** - 慷慨的免费层级
**定价:** $20/月(Advanced)或包含在Workspace计划中
## 正面对比
| 特性 | ChatGPT | Claude | Gemini |
|------|---------|--------|--------|
| 上下文窗口 | 128K | 200K | 1M (Gemini 1.5) |
| 编程 | 优秀 | 很好 | 好 |
| 写作 | 很好 | 优秀 | 好 |
| 分析 | 很好 | 优秀 | 很好 |
| 集成 | 以API为中心 | 以API为中心 | Google原生 |
| 自定义训练 | 是(GPTs) | 有限 | 有限 |
## 使用场景推荐
### 客户支持
**推荐:ChatGPT** - 构建自定义支持机器人的最佳生态系统
### 内容营销
**推荐:Claude** - 卓越的长篇写作质量
### 数据分析
**推荐:Gemini** - 与Google Sheets无缝集成
### 软件开发
**推荐:ChatGPT** - 最全面的编码辅助
### 研究与分析
**推荐:Claude** - 处理长篇文档的最佳选择
## 实施建议
1. **从试点开始** - 在全公司推广前先用小团队测试
2. **明确使用场景** - 清楚您要解决什么问题
3. **培训团队** - 投资提示词工程教育
4. **监控使用情况** - 跟踪ROI并识别优化机会
5. **保持合规** - 确保AI使用符合数据隐私法规
## 结论
没有一款AI聊天机器人能适用于所有企业。您的选择应取决于具体需求、现有技术栈和预算。许多企业发现,针对不同任务使用多个AI助手更有价值。
在 Toolso.AI 浏览我们完整的AI聊天机器人和助手目录,找到最适合您组织的工具。
@@ -0,0 +1,213 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-10",
title: "AI Code Assistants: GitHub Copilot vs Cursor vs Codeium",
description:
"The ultimate guide to AI coding assistants. Compare features, pricing, and real-world performance to find the best AI pair programmer for your development workflow.",
image:
"https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## The AI Coding Revolution
AI code assistants have evolved from simple autocomplete to intelligent pair programmers. In 2025, they're not just nice-to-have - they're essential tools for competitive developers.
## The Big Three Compared
### GitHub Copilot
**Developer:** GitHub (Microsoft)
**Model:** GPT-4 based (Copilot X)
**Key Features:**
- Inline code completion
- Chat interface for explanations
- Pull request summaries
- Documentation generation
- CLI assistance
**Supported IDEs:**
- VS Code (best support)
- JetBrains IDEs
- Neovim
- Visual Studio
**Strengths:**
- Largest training dataset (GitHub's entire codebase)
- Excellent context understanding
- Strong enterprise features
- Active development and updates
**Weaknesses:**
- Privacy concerns for enterprise
- Subscription required
- Can suggest copyrighted code
**Pricing:**
- Individual: $10/month
- Business: $19/user/month
- Enterprise: Custom pricing
### Cursor
**Developer:** Cursor Inc.
**Model:** GPT-4 + Claude
**Key Features:**
- Full IDE built around AI
- Codebase-aware chat
- AI-powered refactoring
- Multi-file editing
- Natural language code generation
**Supported IDEs:**
- Cursor (standalone, VS Code fork)
**Strengths:**
- Best codebase understanding
- Powerful chat capabilities
- Multi-file context
- Fast iteration on features
**Weaknesses:**
- Requires switching IDE
- Newer, less battle-tested
- Limited to their IDE
**Pricing:**
- Hobby: Free (limited)
- Pro: $20/month
- Business: $40/user/month
### Codeium
**Developer:** Exafunction
**Model:** Proprietary
**Key Features:**
- Unlimited autocomplete
- In-editor chat
- Search functionality
- Multi-language support
- Self-hosted option
**Supported IDEs:**
- VS Code
- JetBrains IDEs
- Neovim/Vim
- Emacs
- 40+ editors
**Strengths:**
- Generous free tier
- Wide IDE support
- Fast completions
- Privacy-focused options
**Weaknesses:**
- Less sophisticated than GPT-4 based tools
- Chat not as powerful
- Smaller community
**Pricing:**
- Individual: Free (unlimited)
- Teams: $12/user/month
- Enterprise: Custom
## Feature Comparison
| Feature | Copilot | Cursor | Codeium |
|---------|---------|--------|---------|
| Code Completion | ★★★★★ | ★★★★☆ | ★★★★☆ |
| Chat Quality | ★★★★☆ | ★★★★★ | ★★★☆☆ |
| Codebase Context | ★★★☆☆ | ★★★★★ | ★★★☆☆ |
| IDE Support | ★★★★☆ | ★★☆☆☆ | ★★★★★ |
| Free Tier | ★☆☆☆☆ | ★★☆☆☆ | ★★★★★ |
| Enterprise Ready | ★★★★★ | ★★★☆☆ | ★★★★☆ |
## Other Notable AI Coding Tools
### Amazon CodeWhisperer
- Free for individual use
- Best for AWS development
- Security scanning included
### Tabnine
- Privacy-focused
- On-premise option
- Team learning features
### Sourcegraph Cody
- Excellent code search
- Open source friendly
- Strong enterprise features
### Replit AI
- Browser-based development
- Great for learning
- Integrated deployment
## Choosing the Right Tool
### Choose Copilot If:
- You're in the Microsoft/GitHub ecosystem
- Enterprise compliance is critical
- You want the most mature solution
- VS Code is your primary IDE
### Choose Cursor If:
- You want the best AI-native experience
- Large codebase understanding is crucial
- You're open to switching IDEs
- You value cutting-edge features
### Choose Codeium If:
- Budget is a primary concern
- You use multiple IDEs
- Privacy is paramount
- You want unlimited free usage
## Productivity Impact
Real-world studies show:
- **55% faster** task completion (GitHub research)
- **40% less** context switching
- **30% fewer** bugs in initial code
- **2x faster** onboarding to new codebases
## Best Practices
1. **Learn the shortcuts** - Speed comes from muscle memory
2. **Review all suggestions** - AI makes mistakes
3. **Write good comments** - Better prompts = better code
4. **Use chat for explanations** - Faster than documentation
5. **Iterate with AI** - Let it refine your code
## Privacy Considerations
| Tool | Code Retention | Training on Your Code | Self-Hosted |
|------|----------------|----------------------|-------------|
| Copilot | Optional | Business: No | No |
| Cursor | Minimal | No | No |
| Codeium | None | No | Yes |
## Conclusion
AI code assistants are the biggest productivity boost for developers since the IDE. Whether you choose Copilot's maturity, Cursor's innovation, or Codeium's accessibility, you'll write better code faster.
Explore our complete collection of AI development tools on Toolso.AI to find the perfect coding companion.
@@ -0,0 +1,213 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-10",
title: "AI编程助手对比:GitHub Copilot vs Cursor vs Codeium",
description:
"AI编程助手终极指南。比较功能、价格和实际表现,找到最适合你开发工作流的AI结对编程伙伴。",
image:
"https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## AI编程革命
AI编程助手已从简单的自动补全演变为智能结对编程伙伴。在2025年,它们不再是可有可无——而是有竞争力的开发者必备的工具。
## 三大工具对比
### GitHub Copilot
**开发者:** GitHub (Microsoft)
**模型:** 基于GPT-4 (Copilot X)
**主要功能:**
- 行内代码补全
- 聊天界面解释代码
- Pull Request摘要
- 文档生成
- CLI辅助
**支持的IDE:**
- VS Code(最佳支持)
- JetBrains IDE系列
- Neovim
- Visual Studio
**优势:**
- 最大的训练数据集(GitHub全部代码库)
- 出色的上下文理解
- 强大的企业功能
- 积极的开发和更新
**劣势:**
- 企业隐私顾虑
- 需要订阅
- 可能建议受版权保护的代码
**价格:**
- 个人版:$10/月
- 商业版:$19/用户/月
- 企业版:定制价格
### Cursor
**开发者:** Cursor Inc.
**模型:** GPT-4 + Claude
**主要功能:**
- 围绕AI构建的完整IDE
- 代码库感知聊天
- AI驱动的重构
- 多文件编辑
- 自然语言生成代码
**支持的IDE:**
- Cursor(独立版,VS Code分支)
**优势:**
- 最佳代码库理解
- 强大的聊天能力
- 多文件上下文
- 功能快速迭代
**劣势:**
- 需要切换IDE
- 较新,经验较少
- 仅限于其IDE
**价格:**
- 爱好者版:免费(有限制)
- Pro版:$20/月
- 商业版:$40/用户/月
### Codeium
**开发者:** Exafunction
**模型:** 专有模型
**主要功能:**
- 无限自动补全
- 编辑器内聊天
- 搜索功能
- 多语言支持
- 自托管选项
**支持的IDE:**
- VS Code
- JetBrains IDE系列
- Neovim/Vim
- Emacs
- 40+编辑器
**优势:**
- 慷慨的免费层级
- 广泛的IDE支持
- 快速补全
- 注重隐私的选项
**劣势:**
- 不如基于GPT-4的工具复杂
- 聊天功能不够强大
- 社区较小
**价格:**
- 个人版:免费(无限制)
- 团队版:$12/用户/月
- 企业版:定制
## 功能对比
| 功能 | Copilot | Cursor | Codeium |
|------|---------|--------|---------|
| 代码补全 | ★★★★★ | ★★★★☆ | ★★★★☆ |
| 聊天质量 | ★★★★☆ | ★★★★★ | ★★★☆☆ |
| 代码库上下文 | ★★★☆☆ | ★★★★★ | ★★★☆☆ |
| IDE支持 | ★★★★☆ | ★★☆☆☆ | ★★★★★ |
| 免费层级 | ★☆☆☆☆ | ★★☆☆☆ | ★★★★★ |
| 企业就绪 | ★★★★★ | ★★★☆☆ | ★★★★☆ |
## 其他值得关注的AI编程工具
### Amazon CodeWhisperer
- 个人使用免费
- 最适合AWS开发
- 包含安全扫描
### Tabnine
- 注重隐私
- 本地部署选项
- 团队学习功能
### Sourcegraph Cody
- 出色的代码搜索
- 开源友好
- 强大的企业功能
### Replit AI
- 基于浏览器的开发
- 适合学习
- 集成部署
## 选择正确的工具
### 选择Copilot如果:
- 你在Microsoft/GitHub生态系统中
- 企业合规至关重要
- 你想要最成熟的解决方案
- VS Code是你的主要IDE
### 选择Cursor如果:
- 你想要最佳AI原生体验
- 大型代码库理解至关重要
- 你愿意切换IDE
- 你重视前沿功能
### 选择Codeium如果:
- 预算是主要考虑因素
- 你使用多个IDE
- 隐私至上
- 你想要无限免费使用
## 生产力影响
实际研究表明:
- **55%更快**完成任务(GitHub研究)
- **40%更少**上下文切换
- **30%更少**初始代码bug
- **2倍更快**熟悉新代码库
## 最佳实践
1. **学习快捷键** - 速度来自肌肉记忆
2. **审查所有建议** - AI会犯错
3. **写好注释** - 更好的提示 = 更好的代码
4. **用聊天获取解释** - 比查文档更快
5. **与AI迭代** - 让它优化你的代码
## 隐私考虑
| 工具 | 代码保留 | 用你的代码训练 | 自托管 |
|------|----------|----------------|--------|
| Copilot | 可选 | 商业版:否 | 否 |
| Cursor | 最小化 | 否 | 否 |
| Codeium | 无 | 否 | 是 |
## 结论
AI编程助手是自IDE以来开发者最大的生产力提升。无论你选择Copilot的成熟度、Cursor的创新性还是Codeium的可及性,你都会更快地写出更好的代码。
在 Toolso.AI 探索我们完整的AI开发工具集合,找到完美的编程伙伴。
@@ -0,0 +1,83 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2025-01-02",
title: "AI Image Generators Compared: Midjourney vs DALL-E vs Stable Diffusion",
description:
"A comprehensive comparison of the top AI image generators. Learn the strengths and weaknesses of each platform to choose the best one for your creative needs.",
image:
"https://images.unsplash.com/photo-1547954575-855750c57bd3?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## The Rise of AI Image Generation
AI image generation has evolved from a novelty to an essential creative tool. In this guide, we compare the three leading platforms to help you make an informed choice.
## Midjourney
### Strengths
- **Artistic quality** - Produces stunning, artistic images with minimal effort
- **Community** - Active Discord community for inspiration
- **Consistency** - Reliable style and quality across generations
### Weaknesses
- Discord-only interface (web app in beta)
- Limited fine control over outputs
- Subscription required for all usage
### Best For
Artists, designers, and anyone seeking high-quality artistic imagery.
## DALL-E 3
### Strengths
- **Prompt understanding** - Best-in-class text comprehension
- **Integration** - Built into ChatGPT and Microsoft products
- **Text rendering** - Excellent at including text in images
### Weaknesses
- More conservative content policies
- Less stylistic flexibility
- Credit-based pricing
### Best For
Marketers, content creators, and those needing precise prompt adherence.
## Stable Diffusion
### Strengths
- **Open source** - Run locally, modify freely
- **Customization** - Extensive model fine-tuning options
- **Cost** - Free to run on your own hardware
### Weaknesses
- Steeper learning curve
- Requires technical setup for best results
- Quality depends on model and settings
### Best For
Developers, researchers, and power users who want full control.
## Comparison Table
| Feature | Midjourney | DALL-E 3 | Stable Diffusion |
|---------|------------|----------|------------------|
| Image Quality | Excellent | Very Good | Good to Excellent |
| Ease of Use | Good | Excellent | Moderate |
| Price | $10-60/mo | Pay per use | Free (self-hosted) |
| Customization | Limited | Limited | Extensive |
| Local Running | No | No | Yes |
## Conclusion
Each platform has its place in the AI image generation ecosystem. Consider your specific needs, technical comfort level, and budget when making your choice. Explore more AI image tools on Toolso.AI.
@@ -0,0 +1,83 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2025-01-02",
title: "AI图像生成器对比:Midjourney vs DALL-E vs Stable Diffusion",
description:
"全面对比顶级AI图像生成器。了解每个平台的优缺点,选择最适合您创意需求的工具。",
image:
"https://images.unsplash.com/photo-1547954575-855750c57bd3?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## AI图像生成的崛起
AI图像生成已经从新奇事物演变为必不可少的创意工具。在本指南中,我们对比三大领先平台,帮助您做出明智的选择。
## Midjourney
### 优势
- **艺术质量** - 以最小的努力产出令人惊叹的艺术图像
- **社区** - 活跃的Discord社区提供灵感
- **一致性** - 跨代生成的风格和质量可靠
### 劣势
- 仅支持Discord界面(网页应用处于测试阶段)
- 对输出的精细控制有限
- 所有使用都需要订阅
### 最适合
艺术家、设计师,以及任何追求高质量艺术图像的人。
## DALL-E 3
### 优势
- **提示词理解** - 业界最佳的文本理解能力
- **集成** - 内置于ChatGPT和Microsoft产品中
- **文字渲染** - 在图像中包含文字的效果极佳
### 劣势
- 内容政策更为保守
- 风格灵活性较低
- 基于积分的定价
### 最适合
营销人员、内容创作者,以及需要精确遵循提示词的用户。
## Stable Diffusion
### 优势
- **开源** - 可本地运行,自由修改
- **定制化** - 广泛的模型微调选项
- **成本** - 在自己的硬件上免费运行
### 劣势
- 学习曲线较陡
- 需要技术设置才能获得最佳结果
- 质量取决于模型和设置
### 最适合
开发者、研究人员,以及想要完全控制的高级用户。
## 对比表格
| 特性 | Midjourney | DALL-E 3 | Stable Diffusion |
|------|------------|----------|------------------|
| 图像质量 | 优秀 | 很好 | 好到优秀 |
| 易用性 | 好 | 优秀 | 中等 |
| 价格 | $10-60/月 | 按使用付费 | 免费(自托管) |
| 定制化 | 有限 | 有限 | 广泛 |
| 本地运行 | 否 | 否 | 是 |
## 结论
每个平台在AI图像生成生态系统中都有其位置。在做出选择时,请考虑您的具体需求、技术熟练程度和预算。在 Toolso.AI 探索更多AI图像工具。
@@ -0,0 +1,208 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-15",
title: "10 AI Productivity Tools That Will Transform Your Workflow in 2025",
description:
"Supercharge your productivity with these game-changing AI tools. From email management to meeting summaries, discover how AI can save you hours every week.",
image:
"https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## AI: Your New Productivity Partner
The average knowledge worker spends 60% of their time on "work about work" - emails, meetings, searching for information. AI tools are changing this dramatically, automating mundane tasks and freeing up time for meaningful work.
## Top 10 AI Productivity Tools
### 1. Notion AI
**What it does:** Integrated AI assistant within your workspace
**Key features:**
- Summarize meeting notes instantly
- Generate action items from documents
- Write and edit content in context
- Q&A over your entire workspace
**Time saved:** 3-5 hours/week
**Price:** Included with Notion plans ($10/month add-on for free users)
### 2. Otter.ai
**What it does:** AI meeting transcription and summarization
**Key features:**
- Real-time transcription
- Automatic meeting summaries
- Action item extraction
- Speaker identification
**Time saved:** 2-4 hours/week
**Price:** Free tier available, Pro at $16.99/month
### 3. Superhuman
**What it does:** AI-powered email client
**Key features:**
- AI email writing assistance
- Instant reply suggestions
- Email prioritization
- Scheduled send optimization
**Time saved:** 4-6 hours/week
**Price:** $30/month
### 4. Reclaim.ai
**What it does:** Intelligent calendar management
**Key features:**
- Auto-schedule tasks and habits
- Smart meeting scheduling
- Focus time protection
- Team coordination
**Time saved:** 2-3 hours/week
**Price:** Free tier available, Pro at $8/month
### 5. Mem
**What it does:** AI-powered note-taking and knowledge management
**Key features:**
- Auto-organizes notes
- Smart search across all content
- AI-generated connections
- Meeting integration
**Time saved:** 2-4 hours/week
**Price:** Free tier available, Pro at $15/month
### 6. Jasper
**What it does:** AI content creation platform
**Key features:**
- Brand voice training
- Multiple content templates
- Team collaboration
- Content repurposing
**Time saved:** 5-10 hours/week (for content creators)
**Price:** Starting at $49/month
### 7. Fireflies.ai
**What it does:** Meeting recording and intelligence
**Key features:**
- Auto-join meetings
- Searchable transcripts
- Topic tracking
- CRM integration
**Time saved:** 3-5 hours/week
**Price:** Free tier available, Pro at $18/month
### 8. Motion
**What it does:** AI project and task management
**Key features:**
- Intelligent task scheduling
- Project timeline optimization
- Meeting scheduling
- Deadline management
**Time saved:** 3-5 hours/week
**Price:** $19/month
### 9. Grammarly
**What it does:** AI writing assistant
**Key features:**
- Grammar and spelling check
- Tone detection
- Clarity suggestions
- Plagiarism detection
**Time saved:** 1-2 hours/week
**Price:** Free tier available, Premium at $12/month
### 10. Bardeen
**What it does:** AI workflow automation
**Key features:**
- No-code automations
- Cross-app integrations
- AI-powered suggestions
- Browser automation
**Time saved:** 2-5 hours/week
**Price:** Free tier available, Pro at $10/month
## Building Your AI Productivity Stack
### For Individual Contributors
1. **Communication:** Superhuman + Otter.ai
2. **Organization:** Notion AI + Reclaim.ai
3. **Writing:** Grammarly
### For Managers
1. **Meetings:** Otter.ai + Fireflies.ai
2. **Scheduling:** Reclaim.ai + Motion
3. **Team docs:** Notion AI
### For Content Creators
1. **Writing:** Jasper + Grammarly
2. **Research:** Mem
3. **Automation:** Bardeen
## Implementation Tips
1. **Start with one tool** - Master it before adding more
2. **Measure your time** - Track actual time saved
3. **Integrate gradually** - Build habits around each tool
4. **Review monthly** - Drop tools that don't deliver value
5. **Train your team** - Shared tools need shared knowledge
## The ROI of AI Productivity Tools
| Tool Category | Avg. Cost/Month | Hours Saved/Week | Value Created |
|---------------|-----------------|------------------|---------------|
| Email | $30 | 5 | $325/week |
| Meetings | $20 | 4 | $260/week |
| Writing | $15 | 3 | $195/week |
| Scheduling | $15 | 3 | $195/week |
*Based on $65/hour knowledge worker rate*
## Conclusion
AI productivity tools are no longer optional for competitive professionals. The right stack can save 10-20 hours per week - that's an extra month of productive time per year.
Explore our complete collection of AI productivity tools on Toolso.AI and start reclaiming your time today.
@@ -0,0 +1,208 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-15",
title: "2025年将改变你工作流程的10款AI生产力工具",
description:
"用这些改变游戏规则的AI工具提升你的生产力。从邮件管理到会议总结,发现AI如何每周为你节省数小时。",
image:
"https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## AI:你的新生产力伙伴
普通知识工作者将60%的时间花在"关于工作的工作"上——邮件、会议、搜索信息。AI工具正在彻底改变这一状况,自动化处理琐碎任务,释放时间用于有意义的工作。
## 十大AI生产力工具
### 1. Notion AI
**功能:** 工作空间内的集成AI助手
**主要特性:**
- 即时总结会议记录
- 从文档生成行动项
- 在上下文中写作和编辑
- 对整个工作空间进行问答
**节省时间:** 每周3-5小时
**价格:** 包含在Notion计划中(免费用户$10/月附加)
### 2. Otter.ai
**功能:** AI会议转录和总结
**主要特性:**
- 实时转录
- 自动会议摘要
- 行动项提取
- 说话人识别
**节省时间:** 每周2-4小时
**价格:** 提供免费层级,Pro版$16.99/月
### 3. Superhuman
**功能:** AI驱动的邮件客户端
**主要特性:**
- AI邮件写作辅助
- 即时回复建议
- 邮件优先级排序
- 定时发送优化
**节省时间:** 每周4-6小时
**价格:** $30/月
### 4. Reclaim.ai
**功能:** 智能日历管理
**主要特性:**
- 自动安排任务和习惯
- 智能会议调度
- 专注时间保护
- 团队协调
**节省时间:** 每周2-3小时
**价格:** 提供免费层级,Pro版$8/月
### 5. Mem
**功能:** AI驱动的笔记和知识管理
**主要特性:**
- 自动组织笔记
- 跨所有内容的智能搜索
- AI生成的关联
- 会议集成
**节省时间:** 每周2-4小时
**价格:** 提供免费层级,Pro版$15/月
### 6. Jasper
**功能:** AI内容创作平台
**主要特性:**
- 品牌语调训练
- 多种内容模板
- 团队协作
- 内容再利用
**节省时间:** 每周5-10小时(内容创作者)
**价格:** 起价$49/月
### 7. Fireflies.ai
**功能:** 会议录制和智能分析
**主要特性:**
- 自动加入会议
- 可搜索的转录文本
- 主题追踪
- CRM集成
**节省时间:** 每周3-5小时
**价格:** 提供免费层级,Pro版$18/月
### 8. Motion
**功能:** AI项目和任务管理
**主要特性:**
- 智能任务调度
- 项目时间线优化
- 会议安排
- 截止日期管理
**节省时间:** 每周3-5小时
**价格:** $19/月
### 9. Grammarly
**功能:** AI写作助手
**主要特性:**
- 语法和拼写检查
- 语气检测
- 清晰度建议
- 抄袭检测
**节省时间:** 每周1-2小时
**价格:** 提供免费层级,Premium版$12/月
### 10. Bardeen
**功能:** AI工作流自动化
**主要特性:**
- 无代码自动化
- 跨应用集成
- AI驱动的建议
- 浏览器自动化
**节省时间:** 每周2-5小时
**价格:** 提供免费层级,Pro版$10/月
## 构建你的AI生产力工具栈
### 个人贡献者
1. **沟通:** Superhuman + Otter.ai
2. **组织:** Notion AI + Reclaim.ai
3. **写作:** Grammarly
### 管理者
1. **会议:** Otter.ai + Fireflies.ai
2. **日程:** Reclaim.ai + Motion
3. **团队文档:** Notion AI
### 内容创作者
1. **写作:** Jasper + Grammarly
2. **研究:** Mem
3. **自动化:** Bardeen
## 实施建议
1. **从一个工具开始** - 掌握后再添加更多
2. **衡量你的时间** - 跟踪实际节省的时间
3. **逐步整合** - 围绕每个工具建立习惯
4. **每月回顾** - 放弃不能带来价值的工具
5. **培训团队** - 共享工具需要共享知识
## AI生产力工具的投资回报
| 工具类别 | 平均月费 | 每周节省时间 | 创造价值 |
|----------|----------|--------------|----------|
| 邮件 | $30 | 5小时 | $325/周 |
| 会议 | $20 | 4小时 | $260/周 |
| 写作 | $15 | 3小时 | $195/周 |
| 日程 | $15 | 3小时 | $195/周 |
*基于$65/小时知识工作者费率*
## 结论
对于有竞争力的专业人士来说,AI生产力工具已不再是可选项。正确的工具组合每周可节省10-20小时——相当于每年多出一个月的生产时间。
在 Toolso.AI 探索我们完整的AI生产力工具集合,今天就开始夺回你的时间。
@@ -0,0 +1,164 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-20",
title: "AI Video Generation Tools: The Future of Content Creation",
description:
"Explore the revolutionary world of AI video generation. From text-to-video to AI avatars, discover tools that are transforming how we create video content.",
image:
"https://images.unsplash.com/photo-1492619375914-88005aa9e8fb?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## The AI Video Revolution
Video content has always been resource-intensive to produce. AI is changing that dramatically, making professional-quality video creation accessible to everyone.
## Categories of AI Video Tools
### Text-to-Video Generation
These tools create videos from text descriptions alone.
**Runway Gen-2**
- Industry-leading quality
- Motion brush for precise control
- Up to 4 seconds per generation
- **Price:** Starting at $15/month
**Pika Labs**
- Excellent motion quality
- Discord and web interface
- Free tier available
- **Price:** Free tier + paid plans
**Sora (OpenAI)**
- Groundbreaking quality (limited access)
- Up to 60 seconds
- Realistic physics simulation
- **Price:** Not yet publicly available
### AI Avatar Videos
Create videos with AI-generated presenters.
**HeyGen**
- 100+ AI avatars
- Voice cloning
- Multi-language support
- **Best for:** Corporate training, marketing
**Synthesia**
- Enterprise-focused
- Custom avatar creation
- SOC 2 compliant
- **Best for:** Large organizations
**D-ID**
- Photo-to-video animation
- API available
- Quick turnaround
- **Best for:** Personalized videos
### Video Editing & Enhancement
AI-powered tools for improving existing footage.
**Runway**
- Remove backgrounds
- Object removal
- Color grading
- **Best for:** Professional editors
**Descript**
- Text-based video editing
- Filler word removal
- Overdub voice cloning
- **Best for:** Podcasters and educators
**CapCut**
- Auto-captions
- Background removal
- Free to use
- **Best for:** Social media creators
### AI Video Upscaling
Enhance video quality with AI.
**Topaz Video AI**
- Up to 8K upscaling
- Frame rate conversion
- Noise reduction
- **Best for:** Restoring old footage
**Neural Love**
- Online upscaling
- Multiple enhancement options
- Batch processing
- **Best for:** Quick online upscaling
## Use Cases and Recommendations
### Marketing & Advertising
**Recommended:** HeyGen + Runway
- Create spokesperson videos without actors
- Generate B-roll footage from descriptions
### Social Media Content
**Recommended:** CapCut + Pika Labs
- Quick editing with auto-captions
- Eye-catching AI-generated clips
### Corporate Training
**Recommended:** Synthesia + Descript
- Consistent AI presenters
- Easy script updates
### Film & Entertainment
**Recommended:** Runway + Topaz
- Professional-grade generation
- High-quality upscaling
## Current Limitations
Be aware of these limitations:
1. **Length constraints** - Most tools limited to seconds, not minutes
2. **Consistency issues** - Characters may change between shots
3. **Text rendering** - Text in videos often appears garbled
4. **Hands and faces** - Still challenging for AI
5. **Computing costs** - High-quality generation is expensive
## The Future of AI Video
What's coming:
- **Longer generations** - Minutes instead of seconds
- **Better consistency** - Maintain characters across scenes
- **Real-time generation** - Interactive AI video
- **Improved audio sync** - Natural lip movements
- **Accessible pricing** - More affordable for individuals
## Getting Started Tips
1. **Start with free tiers** - Test before committing
2. **Learn prompting** - Better prompts = better results
3. **Combine tools** - Use multiple tools for best results
4. **Plan your workflow** - AI is a tool, not a replacement
5. **Stay ethical** - Be transparent about AI use
## Conclusion
AI video generation is still in its early stages but evolving rapidly. The tools available today can already save significant time and resources for content creators. As the technology matures, we'll see even more impressive capabilities.
Explore our curated collection of AI video tools on Toolso.AI to find the perfect solution for your video creation needs.
@@ -0,0 +1,164 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-20",
title: "AI视频生成工具:内容创作的未来",
description:
"探索AI视频生成的革命性世界。从文字转视频到AI虚拟人,发现正在改变我们创作视频内容方式的工具。",
image:
"https://images.unsplash.com/photo-1492619375914-88005aa9e8fb?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## AI视频革命
视频内容的制作一直是资源密集型的。AI正在彻底改变这一点,让专业品质的视频创作变得人人可及。
## AI视频工具分类
### 文字转视频生成
这些工具仅从文字描述就能创建视频。
**Runway Gen-2**
- 行业领先的质量
- 运动笔刷精确控制
- 每次生成最多4秒
- **价格:** 起价$15/月
**Pika Labs**
- 出色的运动质量
- Discord和网页界面
- 提供免费层级
- **价格:** 免费层级 + 付费计划
**Sora (OpenAI)**
- 突破性的质量(限制访问)
- 最长60秒
- 逼真的物理模拟
- **价格:** 尚未公开发布
### AI虚拟人视频
使用AI生成的主持人创建视频。
**HeyGen**
- 100+AI虚拟人形象
- 语音克隆
- 多语言支持
- **最适合:** 企业培训、营销
**Synthesia**
- 专注企业
- 自定义虚拟人创建
- SOC 2合规
- **最适合:** 大型组织
**D-ID**
- 照片转视频动画
- 提供API
- 快速周转
- **最适合:** 个性化视频
### 视频编辑与增强
用于改进现有素材的AI驱动工具。
**Runway**
- 移除背景
- 物体移除
- 色彩调整
- **最适合:** 专业剪辑师
**Descript**
- 基于文本的视频编辑
- 移除填充词
- Overdub语音克隆
- **最适合:** 播客主和教育者
**CapCut**
- 自动字幕
- 背景移除
- 免费使用
- **最适合:** 社交媒体创作者
### AI视频升级
用AI增强视频质量。
**Topaz Video AI**
- 最高8K升级
- 帧率转换
- 降噪
- **最适合:** 修复老旧素材
**Neural Love**
- 在线升级
- 多种增强选项
- 批量处理
- **最适合:** 快速在线升级
## 使用场景和推荐
### 营销与广告
**推荐:** HeyGen + Runway
- 无需演员即可创建代言人视频
- 从描述生成B-roll素材
### 社交媒体内容
**推荐:** CapCut + Pika Labs
- 带自动字幕的快速编辑
- 引人注目的AI生成短片
### 企业培训
**推荐:** Synthesia + Descript
- 一致的AI主持人
- 轻松更新脚本
### 影视娱乐
**推荐:** Runway + Topaz
- 专业级生成
- 高质量升级
## 当前局限性
请注意以下局限:
1. **时长限制** - 大多数工具限于几秒,而非几分钟
2. **一致性问题** - 角色可能在不同镜头间变化
3. **文字渲染** - 视频中的文字经常显示乱码
4. **手部和面部** - 对AI来说仍具挑战性
5. **计算成本** - 高质量生成很昂贵
## AI视频的未来
即将到来的发展:
- **更长的生成** - 分钟级而非秒级
- **更好的一致性** - 跨场景保持角色
- **实时生成** - 交互式AI视频
- **改进的音频同步** - 自然的唇部动作
- **亲民的定价** - 个人用户更能负担
## 入门建议
1. **从免费层级开始** - 承诺前先测试
2. **学习提示词技巧** - 更好的提示词 = 更好的结果
3. **组合工具** - 使用多个工具获得最佳效果
4. **规划工作流程** - AI是工具,不是替代品
5. **保持道德** - 对AI使用保持透明
## 结论
AI视频生成仍处于早期阶段,但发展迅速。目前可用的工具已经能为内容创作者节省大量时间和资源。随着技术成熟,我们将看到更令人印象深刻的能力。
在 Toolso.AI 探索我们精选的AI视频工具集合,找到满足您视频创作需求的完美解决方案。
@@ -0,0 +1,71 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2025-01-03",
title: "Best AI Tools in 2025: A Complete Guide",
description:
"Discover the most powerful AI tools of 2025. From ChatGPT to Midjourney, we cover the best AI applications for productivity, creativity, and business.",
image:
"https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## The AI Revolution in 2025
Artificial Intelligence has transformed the way we work, create, and communicate. In 2025, AI tools have become more accessible, powerful, and diverse than ever before. Whether you're a developer, designer, writer, or business professional, there's an AI tool designed to enhance your workflow.
## Top AI Categories
### 1. AI Chatbots & Assistants
The conversational AI space has seen remarkable growth:
- **ChatGPT** - OpenAI's flagship product continues to lead with GPT-4 and beyond
- **Claude** - Anthropic's AI assistant known for nuanced understanding
- **Gemini** - Google's multimodal AI powerhouse
### 2. AI Image Generation
Visual AI tools have revolutionized creative work:
- **Midjourney** - Industry leader for artistic image generation
- **DALL-E 3** - OpenAI's image generator with exceptional prompt understanding
- **Stable Diffusion** - Open-source flexibility for developers
### 3. AI Writing Tools
Content creation has been transformed:
- **Jasper** - Enterprise-focused AI writing platform
- **Copy.ai** - Marketing-focused content generation
- **Notion AI** - Integrated writing assistance in your workspace
### 4. AI Video Tools
Video creation is now accessible to everyone:
- **Runway** - Professional video generation and editing
- **Pika** - Text-to-video with stunning results
- **HeyGen** - AI avatar video creation
## How to Choose the Right AI Tool
When selecting an AI tool, consider:
1. **Your specific use case** - What problem are you solving?
2. **Integration needs** - Does it work with your existing tools?
3. **Pricing** - Free tier vs. paid features
4. **Learning curve** - How quickly can you get started?
## Conclusion
The AI landscape in 2025 offers unprecedented opportunities for productivity and creativity. Explore our curated collection of AI tools on Toolso.AI to find the perfect solutions for your needs.
@@ -0,0 +1,71 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2025-01-03",
title: "2025年最佳AI工具完全指南",
description:
"探索2025年最强大的AI工具。从ChatGPT到Midjourney,我们为您介绍生产力、创意和商业领域最佳的AI应用。",
image:
"https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## 2025年的AI革命
人工智能已经彻底改变了我们工作、创作和沟通的方式。在2025年,AI工具变得比以往任何时候都更加易用、强大和多样化。无论您是开发者、设计师、作家还是商业专业人士,都有专门为增强您工作流程而设计的AI工具。
## 顶级AI工具分类
### 1. AI聊天机器人与助手
对话式AI领域取得了显著增长:
- **ChatGPT** - OpenAI的旗舰产品,凭借GPT-4及更高版本继续领先
- **Claude** - Anthropic的AI助手,以细腻的理解能力著称
- **Gemini** - Google的多模态AI强者
### 2. AI图像生成
视觉AI工具彻底改变了创意工作:
- **Midjourney** - 艺术图像生成的行业领导者
- **DALL-E 3** - OpenAI的图像生成器,具有卓越的提示词理解能力
- **Stable Diffusion** - 为开发者提供开源灵活性
### 3. AI写作工具
内容创作已被彻底变革:
- **Jasper** - 专注企业的AI写作平台
- **Copy.ai** - 专注营销的内容生成
- **Notion AI** - 工作空间中的集成写作助手
### 4. AI视频工具
视频创作现在人人可及:
- **Runway** - 专业视频生成和编辑
- **Pika** - 效果惊艳的文字转视频
- **HeyGen** - AI虚拟人视频创作
## 如何选择合适的AI工具
选择AI工具时,请考虑:
1. **您的具体用例** - 您要解决什么问题?
2. **集成需求** - 它能与您现有的工具配合使用吗?
3. **定价** - 免费版与付费功能的对比
4. **学习曲线** - 您能多快上手?
## 结论
2025年的AI领域为生产力和创意提供了前所未有的机遇。浏览 Toolso.AI 精选的AI工具集合,找到最适合您需求的解决方案。
@@ -0,0 +1,170 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-25",
title: "Best Free AI Tools in 2025: No Credit Card Required",
description:
"Discover powerful AI tools that won't cost you a penny. From image generation to writing assistants, explore the best free AI resources available today.",
image:
"https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## Free AI Tools That Actually Deliver
You don't need to spend a fortune to harness the power of AI. Many cutting-edge tools offer generous free tiers that can handle most personal and small business needs.
## Free AI Chatbots
### ChatGPT Free
- Access to GPT-3.5
- Unlimited conversations
- Basic coding assistance
- **Limitation:** No GPT-4 access, slower during peak times
### Claude.ai Free
- Access to Claude 3 Sonnet
- Generous daily message limit
- Long document analysis
- **Limitation:** Lower priority during high demand
### Google Gemini Free
- Access to Gemini Pro
- Real-time web access
- Google integration
- **Limitation:** Limited advanced features
### Microsoft Copilot Free
- GPT-4 powered
- Image generation included
- No sign-up required
- **Limitation:** Daily conversation limits
## Free AI Image Generators
### Bing Image Creator
- DALL-E 3 powered
- High-quality outputs
- 15 free boosts daily
- **Best for:** Quick, quality image generation
### Leonardo.ai Free
- 150 daily tokens
- Multiple model options
- Community models access
- **Best for:** Artistic and game asset creation
### Playground AI
- 500 free images daily
- Multiple AI models
- Image editing tools
- **Best for:** Volume image generation
### Canva AI (Free Tier)
- Limited AI image generation
- Design template integration
- Easy editing tools
- **Best for:** Marketing materials
## Free AI Writing Tools
### Notion AI (Limited)
- Free trial available
- Integrated with Notion workspace
- Summarization and writing help
- **Best for:** Note-taking and organization
### Grammarly Free
- Basic grammar checking
- Tone detection
- Browser extension
- **Best for:** Error-free writing
### QuillBot Free
- Paraphrasing tool
- Grammar checker
- Summarizer (limited)
- **Best for:** Rewriting and paraphrasing
### Copy.ai Free
- 2,000 words per month
- 90+ copywriting tools
- Multiple languages
- **Best for:** Marketing copy snippets
## Free AI Coding Assistants
### GitHub Copilot (Students/Open Source)
- Free for students and maintainers
- VS Code integration
- Code completion
- **Best for:** Students and open source contributors
### Codeium Free
- Unlimited code completion
- Multiple IDE support
- Chat assistance
- **Best for:** Professional developers on a budget
### Amazon CodeWhisperer Free
- Individual tier is free
- AWS integration
- Security scanning
- **Best for:** AWS developers
### Replit AI (Free Tier)
- Basic code assistance
- Browser-based IDE
- Deployment included
- **Best for:** Learning and prototyping
## Free AI Audio & Video Tools
### ElevenLabs Free
- 10,000 characters/month
- High-quality voice synthesis
- Multiple voices
- **Best for:** Short voiceover projects
### Kapwing Free
- Basic video editing
- AI-powered features
- Watermark on exports
- **Best for:** Social media clips
### Descript Free
- 1 hour of transcription
- Basic editing
- Screen recording
- **Best for:** Podcast editing
## Tips for Maximizing Free AI Tools
1. **Combine multiple free tools** - Use different tools for different tasks
2. **Create multiple accounts** - Where terms allow, for different projects
3. **Time your usage** - Use free tiers during off-peak hours
4. **Export and save** - Don't lose your generated content
5. **Stay updated** - Free tiers often change
## When to Upgrade
Consider paid plans when:
- You hit free limits regularly
- Quality becomes critical
- Time savings justify the cost
- You need advanced features
## Conclusion
The free AI tool landscape is remarkably capable. For many users, free tiers provide everything needed to boost productivity and creativity. Start with free options and upgrade only when necessary.
Browse our complete collection of free AI tools on Toolso.AI - we've tagged all tools with free tiers for easy discovery.
@@ -0,0 +1,170 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2024-12-25",
title: "2025年最佳免费AI工具:无需信用卡",
description:
"发现不花一分钱就能使用的强大AI工具。从图像生成到写作助手,探索当今最佳的免费AI资源。",
image:
"https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## 真正好用的免费AI工具
利用AI的力量不需要花费大量金钱。许多尖端工具提供慷慨的免费层级,可以满足大多数个人和小型企业的需求。
## 免费AI聊天机器人
### ChatGPT 免费版
- 访问GPT-3.5
- 无限对话
- 基础编程辅助
- **限制:** 无法访问GPT-4,高峰期较慢
### Claude.ai 免费版
- 访问Claude 3 Sonnet
- 慷慨的每日消息限制
- 长文档分析
- **限制:** 高需求期间优先级较低
### Google Gemini 免费版
- 访问Gemini Pro
- 实时网络访问
- Google集成
- **限制:** 高级功能受限
### Microsoft Copilot 免费版
- GPT-4驱动
- 包含图像生成
- 无需注册
- **限制:** 每日对话限制
## 免费AI图像生成器
### Bing Image Creator
- DALL-E 3驱动
- 高质量输出
- 每日15次免费加速
- **最适合:** 快速、高质量图像生成
### Leonardo.ai 免费版
- 每日150个代币
- 多种模型选项
- 社区模型访问
- **最适合:** 艺术和游戏资产创作
### Playground AI
- 每日500张免费图片
- 多种AI模型
- 图像编辑工具
- **最适合:** 批量图像生成
### Canva AI(免费层级)
- 有限的AI图像生成
- 设计模板集成
- 简单编辑工具
- **最适合:** 营销材料
## 免费AI写作工具
### Notion AI(有限)
- 提供免费试用
- 与Notion工作空间集成
- 摘要和写作帮助
- **最适合:** 笔记和组织
### Grammarly 免费版
- 基础语法检查
- 语气检测
- 浏览器扩展
- **最适合:** 无错误写作
### QuillBot 免费版
- 改写工具
- 语法检查器
- 摘要器(有限)
- **最适合:** 改写和释义
### Copy.ai 免费版
- 每月2,000字
- 90+文案工具
- 多语言支持
- **最适合:** 营销文案片段
## 免费AI编程助手
### GitHub Copilot(学生/开源)
- 对学生和维护者免费
- VS Code集成
- 代码补全
- **最适合:** 学生和开源贡献者
### Codeium 免费版
- 无限代码补全
- 多IDE支持
- 聊天辅助
- **最适合:** 预算有限的专业开发者
### Amazon CodeWhisperer 免费版
- 个人层级免费
- AWS集成
- 安全扫描
- **最适合:** AWS开发者
### Replit AI(免费层级)
- 基础代码辅助
- 基于浏览器的IDE
- 包含部署
- **最适合:** 学习和原型开发
## 免费AI音视频工具
### ElevenLabs 免费版
- 每月10,000字符
- 高质量语音合成
- 多种声音
- **最适合:** 短配音项目
### Kapwing 免费版
- 基础视频编辑
- AI驱动功能
- 导出带水印
- **最适合:** 社交媒体短片
### Descript 免费版
- 1小时转录
- 基础编辑
- 屏幕录制
- **最适合:** 播客编辑
## 最大化利用免费AI工具的技巧
1. **组合多个免费工具** - 不同任务使用不同工具
2. **创建多个账户** - 在条款允许的情况下,用于不同项目
3. **合理安排使用时间** - 在非高峰时段使用免费层级
4. **导出并保存** - 不要丢失生成的内容
5. **保持更新** - 免费层级经常变化
## 何时升级
在以下情况考虑付费计划:
- 经常达到免费限制
- 质量变得至关重要
- 节省的时间值得付费
- 需要高级功能
## 结论
免费AI工具的能力非常出色。对于许多用户来说,免费层级提供了提升生产力和创造力所需的一切。从免费选项开始,只在必要时升级。
在 Toolso.AI 浏览我们完整的免费AI工具集合 - 我们已为所有带免费层级的工具添加标签,方便发现。
@@ -0,0 +1,130 @@
import { BlogLayout } from "@/components/blog-layout";
export const blog = {
author: { name: "Toolso.AI", src: "/avatar.jpeg" },
date: "2025-01-01",
title: "How to Choose the Right AI Writing Tool for Your Needs",
description:
"Navigate the crowded AI writing tool market with our comprehensive guide. From blogging to marketing copy, find the perfect AI assistant for your content creation.",
image:
"https://images.unsplash.com/photo-1455390582262-044cdead277a?w=800&auto=format&fit=crop&q=60",
};
export const metadata = {
title: blog.title,
description: blog.description,
openGraph: {
images: [blog.image],
},
};
export default (props) => <BlogLayout blog={blog} {...props} />;
## The AI Writing Tool Landscape
With dozens of AI writing tools flooding the market, choosing the right one can feel overwhelming. This guide will help you cut through the noise and find your perfect match.
## Understanding Your Needs
Before diving into tools, ask yourself:
1. **What type of content do you create?**
- Blog posts and articles
- Marketing copy
- Technical documentation
- Social media content
- Email campaigns
2. **What's your workflow like?**
- Do you need standalone tools or integrations?
- How important is collaboration?
- What's your budget?
## Top AI Writing Tools by Use Case
### For Long-Form Content
**ChatGPT / Claude**
- Best for brainstorming and drafting
- Excellent for research assistance
- Flexible prompting for any content type
**Jasper**
- Purpose-built for marketing content
- Templates for various content types
- Brand voice consistency features
### For Marketing Copy
**Copy.ai**
- Quick generation of marketing variants
- A/B testing suggestions
- Social media post generation
**Writesonic**
- Landing page copy
- Product descriptions
- Ad copy variations
### For SEO Content
**Surfer SEO + AI**
- SEO-optimized content suggestions
- Keyword integration guidance
- Content score tracking
**Clearscope**
- Comprehensive content optimization
- Competitor analysis
- Readability scoring
### For Technical Writing
**GitHub Copilot**
- Code documentation
- Technical explanations
- API documentation
**Notion AI**
- Integrated workspace writing
- Meeting notes summarization
- Technical specifications
## Key Features to Consider
### 1. Output Quality
Not all AI writing is equal. Test each tool with your specific use cases.
### 2. Customization
Can you train the tool on your brand voice? Does it learn from your feedback?
### 3. Integration
Does it work with your existing tools? Consider:
- Google Docs / Microsoft Word
- Content management systems
- SEO tools
### 4. Pricing Model
- Per-word pricing
- Monthly subscriptions
- Usage-based credits
## Our Recommendations
| Use Case | Recommended Tool | Why |
|----------|------------------|-----|
| General Writing | ChatGPT Plus | Versatility and quality |
| Marketing | Jasper | Purpose-built features |
| SEO Content | Surfer SEO | Optimization focus |
| Technical | GitHub Copilot | Code-aware |
| Quick Copy | Copy.ai | Speed and templates |
## Getting Started
1. **Start with free trials** - Most tools offer them
2. **Test with real projects** - Use your actual content needs
3. **Compare outputs** - Try the same prompt across tools
4. **Consider the learning curve** - Factor in time to proficiency
## Conclusion
The best AI writing tool is the one that fits your specific workflow and content needs. Don't just follow the hype - test thoroughly and choose based on your requirements. Browse our complete collection of AI writing tools on Toolso.AI.

Some files were not shown because too many files have changed in this diff Show More