[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "README.md",
    "content": "# AI 翻译助手\n\n一个功能强大的 AI 驱动的多语言翻译和内容处理平台。\n\n## 主要功能\n\n### 1. 多模态翻译\n- **文本翻译**：支持无限次免费文本翻译，多种语言互译\n- **图片识别**：支持图片内容识别和翻译，可处理多种格式图片\n- **PDF 处理**：支持 PDF 文档内容提取和翻译，优化的文本提取算法\n- **语音识别**：支持语音内容识别和转换，多种语言支持\n- **视频处理**：支持视频内容提取和字幕生成，自动时间轴对齐\n\n### 2. 高级 AI 模型支持\n- **多模型集成**：支持 GPT、Gemini、Kimi、Mistral 等多种 AI 模型\n- **智能模型选择**：根据内容类型自动选择最佳模型处理\n- **模型备份机制**：当主要模型失败时自动切换到备用模型\n- **高质量翻译**：专业级翻译质量，保留原文格式和语义\n\n### 3. 会员订阅系统\n- **分级会员制**：支持试用版、月度会员和年度会员\n- **差异化配额**：不同会员等级享有不同的服务配额\n- **自动续期**：支持 Stripe 订阅自动续费\n- **订阅管理**：支持查看订阅状态、到期时间等\n- **订阅过期处理**：订阅到期后自动重置为试用版状态\n\n### 4. 用户系统\n- **账号管理**：支持邮箱注册和登录\n- **社交登录**：支持 GitHub 和 Google 账号登录\n- **个人资料**：支持修改用户名等基本信息\n- **使用统计**：实时显示各项功能的使用情况\n\n### 5. 配额管理\n- **每日重置**：免费用户的使用配额每日0点自动重置\n- **实时统计**：显示当日各项功能的剩余使用次数\n- **配额升级**：付费会员可获得更多使用次数\n  - **试用版**：\n    - 无限文本翻译\n    - 5次/日图片识别\n    - 3次/日PDF处理\n    - 2次/日语音识别\n    - 1次/日视频处理\n  - **月度会员**：\n    - 无限文本翻译\n    - 50次/日图片识别\n    - 40次/日PDF处理\n    - 30次/日语音识别\n    - 10次/日视频处理\n  - **年度会员**：\n    - 无限文本翻译\n    - 100次/日图片识别\n    - 80次/日PDF处理\n    - 60次/日语音识别\n    - 20次/日视频处理\n\n### 6. 性能优化\n- **PDF处理优化**：\n  - 多种文本提取方法，提高成功率\n  - 备用处理机制，当OCR API失败时使用聊天API提取内容\n  - 减少重试次数和等待时间，提升用户体验\n- **响应式设计**：适配各种设备屏幕尺寸\n- **多语言界面**：支持中英文界面切换\n- **快速响应**：优化的API调用和缓存机制\n\n## 技术栈\n\n- **前端**：Next.js 14, React, TypeScript, Tailwind CSS\n- **后端**：Node.js, PostgreSQL (Neon Serverless)\n- **认证**：NextAuth.js\n- **支付**：Stripe\n- **国际化**：自定义i18n解决方案\n- **云服务**：\n  - 阿里云 OSS（文件存储）\n  - 腾讯云（AI 服务）\n  - 多种AI模型API集成\n\n## 环境变量配置\n\n项目运行需要配置以下环境变量：\n\n```env\n# 数据库配置\nDATABASE_URL=\n\n# 认证相关\nNEXTAUTH_SECRET=\nNEXTAUTH_URL=\n\n# GitHub OAuth\nGITHUB_ID=\nGITHUB_SECRET=\n\n# Google OAuth\nGOOGLE_CLIENT_ID=\nGOOGLE_CLIENT_SECRET=\n\n# Stripe 配置\nSTRIPE_SECRET_KEY=\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\nSTRIPE_WEBHOOK_SECRET=\nNEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID=\nNEXT_PUBLIC_STRIPE_YEARLY_PRICE_ID=\nNEXT_PUBLIC_APP_URL=\n\n# 阿里云配置\nALIYUN_ACCESS_KEY_ID=\nALIYUN_ACCESS_KEY_SECRET=\nALIYUN_OSS_BUCKET=\nALIYUN_OSS_REGION=\nALIYUN_RAM_ROLE_ARN=\n\n# AI 模型 API Keys\nNEXT_PUBLIC_GEMINI_API_KEY=\nNEXT_PUBLIC_DEEPSEEK_API_KEY=\nNEXT_PUBLIC_QWEN_API_KEY=\nNEXT_PUBLIC_ZHIPU_API_KEY=\nNEXT_PUBLIC_TENCENT_API_KEY=\nNEXT_PUBLIC_KIMI_API_KEY=\nNEXT_PUBLIC_OPENAI_API_KEY=\nNEXT_PUBLIC_MINNIMAX_API_KEY=\nNEXT_PUBLIC_SILICONFLOW_API_KEY=\nNEXT_PUBLIC_OPENROUTER_API_KEY=\nMISTRAL_API_KEY=\n```\n\n## 开发说明\n\n1. 克隆项目\n```bash\ngit clone [repository-url]\ncd ai-translation-assistant-pro\n```\n\n2. 安装依赖\n```bash\nnpm install\n```\n\n3. 配置环境变量\n```bash\ncp .env.example .env.local\n# 编辑 .env.local 填入相应的值\n```\n\n4. 运行开发服务器\n```bash\nnpm run dev\n```\n\n## 部署\n\n项目可以部署到任何支持 Node.js 的平台。建议使用 Vercel 进行部署：\n\n1. 在 Vercel 中导入项目\n2. 配置环境变量\n3. 部署完成后即可访问\n\n## 许可证\n\n[MIT License](LICENSE) "
  },
  {
    "path": "app/api/aliyun/oss/sts/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport * as $OpenApi from '@alicloud/openapi-client'\nimport * as $STS20150401 from '@alicloud/sts20150401'\n\nexport async function GET() {\n  try {\n    if (!process.env.ALIYUN_ACCESS_KEY_ID || \n        !process.env.ALIYUN_ACCESS_KEY_SECRET || \n        !process.env.ALIYUN_RAM_ROLE_ARN || \n        !process.env.ALIYUN_OSS_BUCKET ||\n        !process.env.ALIYUN_OSS_REGION) {\n      throw new Error('缺少必要的环境变量配置');\n    }\n\n    // 打印 AccessKey 前缀，用于验证\n    console.log('AccessKey 信息:', {\n      accessKeyIdPrefix: process.env.ALIYUN_ACCESS_KEY_ID.substring(0, 8) + '****',\n      region: process.env.ALIYUN_OSS_REGION,\n      bucket: process.env.ALIYUN_OSS_BUCKET\n    });\n\n    console.log('RAM 角色信息:', {\n      roleArn: process.env.ALIYUN_RAM_ROLE_ARN,\n      accountId: process.env.ALIYUN_RAM_ROLE_ARN.split('::')[1]?.split(':')[0]\n    });\n\n    const config = new $OpenApi.Config({\n      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,\n      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,\n      endpoint: 'sts.aliyuncs.com',\n      regionId: 'cn-hangzhou'  // 使用默认的 cn-hangzhou 地区\n    });\n\n    const client = new $STS20150401.default(config);\n\n    // 获取 STS token，有效期15分钟\n    const result = await client.assumeRole({\n      roleArn: process.env.ALIYUN_RAM_ROLE_ARN,\n      roleSessionName: 'video-upload',\n      durationSeconds: 900\n    });\n\n    console.log('STS 响应成功');\n\n    // 检查返回结果的结构\n    if (!result.body || !result.body.credentials) {\n      console.error('无效的 STS 响应结构:', result);\n      throw new Error('无效的 STS 响应');\n    }\n\n    return NextResponse.json({\n      success: true,\n      data: {\n        region: process.env.ALIYUN_OSS_REGION,\n        bucket: process.env.ALIYUN_OSS_BUCKET,\n        credentials: {\n          accessKeyId: result.body.credentials.accessKeyId,\n          accessKeySecret: result.body.credentials.accessKeySecret,\n          securityToken: result.body.credentials.securityToken,\n          expiration: result.body.credentials.expiration\n        }\n      }\n    });\n  } catch (error: any) {\n    console.error('获取 STS token 失败:', error);\n    console.error('详细错误信息:', {\n      code: error.code,\n      message: error.message,\n      data: error.data,\n      statusCode: error.statusCode,\n      stack: error.stack\n    });\n    return NextResponse.json({\n      success: false,\n      message: `获取上传凭证失败: ${error.message || '未知错误'}`,\n      error: {\n        code: error.code,\n        message: error.message,\n        data: error.data,\n        statusCode: error.statusCode\n      }\n    }, { status: 500 });\n  }\n} "
  },
  {
    "path": "app/api/aliyun/oss/upload/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport OSS from 'ali-oss'\nimport { v4 as uuidv4 } from 'uuid'\n\nexport async function POST(request: Request) {\n  try {\n    const formData = await request.formData()\n    const file = formData.get('file') as File\n    \n    if (!file) {\n      return NextResponse.json(\n        { message: '缺少文件' },\n        { status: 400 }\n      )\n    }\n\n    console.log('文件信息:', {\n      name: file.name,\n      type: file.type,\n      size: file.size\n    })\n\n    // 创建 OSS 客户端\n    const ossClient = new OSS({\n      region: 'oss-cn-shanghai',\n      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || '',\n      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || '',\n      bucket: process.env.ALIYUN_OSS_BUCKET || ''\n    })\n\n    try {\n      // 将文件转换为 Buffer\n      const arrayBuffer = await file.arrayBuffer()\n      const buffer = Buffer.from(arrayBuffer)\n\n      // 上传到 OSS\n      const ext = file.name.split('.').pop()\n      const fileName = `videos/${uuidv4()}.${ext}`\n      console.log('开始上传文件到 OSS...')\n      \n      // 使用 Promise 包装 put 方法\n      const uploadResult = await new Promise((resolve, reject) => {\n        try {\n          // @ts-ignore\n          ossClient.put(fileName, buffer).then(result => {\n            resolve(result)\n          }).catch(err => {\n            reject(err)\n          })\n        } catch (err) {\n          reject(err)\n        }\n      })\n\n      console.log('文件上传成功:', uploadResult)\n\n      return NextResponse.json({\n        success: true,\n        url: (uploadResult as any).url\n      })\n    } catch (uploadError: any) {\n      console.error('OSS上传错误:', {\n        name: uploadError.name,\n        message: uploadError.message,\n        code: uploadError.code,\n        requestId: uploadError.requestId,\n        stack: uploadError.stack\n      })\n      throw new Error(`文件上传失败: ${uploadError.message}`)\n    }\n\n  } catch (error: any) {\n    console.error('处理请求错误:', error)\n    return NextResponse.json(\n      { message: error.message || '文件上传失败' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/aliyun/video-ocr/create/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport RPCClient from '@alicloud/pop-core'\n\ninterface AsyncJobResult {\n  RequestId: string\n  Message: string\n}\n\nexport async function POST(request: Request) {\n  try {\n    const body = await request.json()\n    const { videoUrl } = body\n\n    if (!videoUrl) {\n      return NextResponse.json(\n        { message: '缺少视频URL' },\n        { status: 400 }\n      )\n    }\n\n    // 创建视频识别客户端\n    const client = new RPCClient({\n      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || '',\n      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || '',\n      endpoint: 'https://videorecog.cn-shanghai.aliyuncs.com',\n      apiVersion: '2020-03-20'\n    })\n\n    try {\n      // 创建视频识别任务\n      console.log('开始创建视频识别任务...')\n      const params = {\n        VideoUrl: videoUrl,\n        Params: JSON.stringify([{\n          Type: 'subtitles'\n        }])\n      }\n\n      // 发送请求\n      const result = await client.request<AsyncJobResult>('RecognizeVideoCastCrewList', params, {\n        method: 'POST',\n        formatParams: false,\n        headers: {\n          'content-type': 'application/json'\n        }\n      })\n      \n      console.log('创建任务结果:', result)\n\n      if (!result.RequestId) {\n        throw new Error('创建任务失败：未获取到任务ID')\n      }\n\n      return NextResponse.json({\n        success: true,\n        taskId: result.RequestId,\n        message: result.Message\n      })\n    } catch (createError: any) {\n      console.error('创建任务错误详情:', {\n        name: createError.name,\n        message: createError.message,\n        code: createError.code,\n        requestId: createError.RequestId,\n        stack: createError.stack\n      })\n      throw new Error(`创建视频识别任务失败: ${createError.message}`)\n    }\n\n  } catch (error: any) {\n    console.error('处理请求错误:', error)\n    return NextResponse.json(\n      { message: error.message || '创建视频识别任务失败' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/aliyun/video-ocr/query/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport RPCClient from '@alicloud/pop-core'\n\ninterface AsyncJobQueryResult {\n  RequestId: string\n  Data: {\n    Status: string\n    Result: string\n    JobId: string\n  }\n}\n\nexport async function POST(request: Request) {\n  try {\n    const body = await request.json()\n    const { taskId } = body\n\n    if (!taskId) {\n      return NextResponse.json(\n        { message: '缺少任务ID' },\n        { status: 400 }\n      )\n    }\n\n    // 创建视频识别客户端\n    const client = new RPCClient({\n      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || '',\n      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || '',\n      endpoint: 'https://videorecog.cn-shanghai.aliyuncs.com',\n      apiVersion: '2020-03-20'\n    })\n\n    try {\n      // 查询任务结果\n      console.log('开始查询任务结果, taskId:', taskId)\n      const params = {\n        JobId: taskId\n      }\n\n      // 发送请求\n      const result = await client.request<AsyncJobQueryResult>('GetAsyncJobResult', params)\n      \n      console.log('原始查询结果:', JSON.stringify(result, null, 2))\n\n      if (!result.Data) {\n        console.log('未获取到Data字段:', result)\n        throw new Error('查询任务失败：未获取到任务结果')\n      }\n\n      console.log('任务状态:', result.Data.Status)\n      console.log('任务结果:', result.Data.Result)\n\n      // 如果任务还在处理中，返回特定状态码\n      if (result.Data.Status === 'PROCESS_RUNNING') {\n        console.log('任务正在处理中...')\n        return NextResponse.json({\n          success: true,\n          status: 'running',\n          message: '任务正在处理中'\n        }, { status: 202 })\n      }\n\n      // 如果任务失败，抛出错误\n      if (result.Data.Status === 'PROCESS_FAILED') {\n        console.log('任务处理失败')\n        throw new Error('任务处理失败')\n      }\n\n      // 如果任务成功完成，返回结果\n      if (result.Data.Status === 'PROCESS_SUCCESS') {\n        console.log('任务处理成功，开始解析结果')\n        let ocrResult = {}\n        try {\n          if (typeof result.Data.Result === 'string') {\n            console.log('解析字符串结果')\n            ocrResult = JSON.parse(result.Data.Result)\n          } else {\n            console.log('使用原始结果对象')\n            ocrResult = result.Data.Result\n          }\n          console.log('解析后的结果:', ocrResult)\n        } catch (e) {\n          console.error('解析OCR结果失败:', e)\n          console.log('使用原始结果')\n          ocrResult = result.Data.Result\n        }\n\n        return NextResponse.json({\n          success: true,\n          status: 'success',\n          data: ocrResult\n        })\n      }\n\n      // 其他状态\n      console.log('未知的任务状态:', result.Data.Status)\n      return NextResponse.json({\n        success: false,\n        status: result.Data.Status,\n        message: '未知的任务状态',\n        data: result.Data\n      })\n\n    } catch (queryError: any) {\n      console.error('查询任务错误详情:', {\n        name: queryError.name,\n        message: queryError.message,\n        code: queryError.code,\n        requestId: queryError.RequestId,\n        stack: queryError.stack\n      })\n      throw new Error(`查询视频识别任务失败: ${queryError.message}`)\n    }\n\n  } catch (error: any) {\n    console.error('处理请求错误:', error)\n    return NextResponse.json(\n      { message: error.message || '查询视频识别任务失败' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/aliyun/video-ocr/status/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport RPCClient from '@alicloud/pop-core'\n\ninterface VideoOCRResult {\n  OcrResults?: Array<{\n    DetailInfo: Array<{\n      Text: string\n      TimeStamp: number\n    }>\n    StartTime: number\n    EndTime: number\n  }>\n  VideoOcrResults?: Array<{\n    DetailInfo: Array<{\n      Text: string\n    }>\n    StartTime: number\n    EndTime: number\n  }>\n  SubtitlesResults?: Array<{\n    SubtitlesAllResults?: Record<string, string>\n    SubtitlesChineseResults?: Record<string, string>\n    SubtitlesEnglishResults?: Record<string, string>\n    SubtitlesAllResultsUrl?: string\n    SubtitlesChineseResultsUrl?: string\n    SubtitlesEnglishResultsUrl?: string\n  }>\n}\n\ninterface AsyncJobQueryResult {\n  RequestId: string\n  Data: {\n    Status: string\n    Result: string\n    JobId: string\n  }\n}\n\nexport async function POST(request: Request) {\n  try {\n    const body = await request.json()\n    const { taskId } = body\n\n    if (!taskId) {\n      return NextResponse.json(\n        { message: '缺少任务ID' },\n        { status: 400 }\n      )\n    }\n\n    // 创建视频识别客户端\n    const client = new RPCClient({\n      accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || '',\n      accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || '',\n      endpoint: 'https://videorecog.cn-shanghai.aliyuncs.com',\n      apiVersion: '2020-03-20',\n      opts: {\n        method: 'POST',\n        timeout: 60000\n      }\n    })\n\n    try {\n      // 查询任务结果\n      console.log('开始查询任务结果, taskId:', taskId)\n      const params = {\n        JobId: taskId\n      }\n\n      // 发送请求\n      const result = await client.request<AsyncJobQueryResult>('GetAsyncJobResult', params, {\n        method: 'POST',\n        formatParams: true,\n        headers: {\n          'content-type': 'application/x-www-form-urlencoded'\n        }\n      })\n      \n      console.log('原始查询结果:', JSON.stringify(result, null, 2))\n\n      // 检查结果格式\n      if (!result || !result.Data) {\n        console.log('未获取到Data字段:', result)\n        throw new Error('查询任务失败：未获取到任务结果')\n      }\n\n      console.log('任务状态:', result.Data.Status)\n\n      // 处理不同的任务状态\n      switch (result.Data.Status) {\n        case 'PROCESS_RUNNING':\n          console.log('任务正在处理中...')\n          return NextResponse.json({\n            success: true,\n            status: 'running',\n            message: '任务正在处理中'\n          }, { status: 202 })\n\n        case 'PROCESS_FAILED':\n          console.log('任务处理失败:', result.Data)\n          return NextResponse.json({\n            success: false,\n            status: 'failed',\n            message: '任务处理失败',\n            data: result.Data\n          }, { status: 500 })\n\n        case 'PROCESS_SUCCESS':\n          console.log('任务处理成功，开始解析结果')\n          let ocrResult: VideoOCRResult | null = null\n          \n          try {\n            // 尝试解析结果\n            if (result.Data.Result) {\n              console.log('解析字符串结果:', result.Data.Result)\n              if (typeof result.Data.Result === 'string') {\n                ocrResult = JSON.parse(result.Data.Result)\n              } else {\n                ocrResult = result.Data.Result as VideoOCRResult\n              }\n            }\n\n            // 提取所有文本内容\n            const textContents: string[] = []\n            \n            // 处理 OcrResults\n            if (ocrResult?.OcrResults) {\n              ocrResult.OcrResults.forEach(result => {\n                result.DetailInfo.forEach(detail => {\n                  if (detail.Text) {\n                    textContents.push(detail.Text)\n                  }\n                })\n              })\n            }\n\n            // 处理 VideoOcrResults\n            if (ocrResult?.VideoOcrResults) {\n              ocrResult.VideoOcrResults.forEach(result => {\n                result.DetailInfo.forEach(detail => {\n                  if (detail.Text) {\n                    textContents.push(detail.Text)\n                  }\n                })\n              })\n            }\n\n            // 处理字幕结果\n            if (ocrResult?.SubtitlesResults?.[0]) {\n              const subtitles = ocrResult.SubtitlesResults[0]\n              if (subtitles.SubtitlesChineseResults) {\n                Object.values(subtitles.SubtitlesChineseResults).forEach(text => {\n                  textContents.push(text)\n                })\n              }\n              return NextResponse.json({\n                success: true,\n                status: 'success',\n                data: {\n                  text: textContents.join('\\n'),\n                  subtitles: {\n                    all: subtitles.SubtitlesAllResults,\n                    chinese: subtitles.SubtitlesChineseResults,\n                    english: subtitles.SubtitlesEnglishResults,\n                    allUrl: subtitles.SubtitlesAllResultsUrl,\n                    chineseUrl: subtitles.SubtitlesChineseResultsUrl,\n                    englishUrl: subtitles.SubtitlesEnglishResultsUrl\n                  },\n                  raw: ocrResult\n                }\n              })\n            }\n\n            // 如果没有字幕结果，返回文本内容\n            return NextResponse.json({\n              success: true,\n              status: 'success',\n              data: {\n                text: textContents.join('\\n'),\n                raw: ocrResult\n              }\n            })\n\n          } catch (e) {\n            console.error('解析OCR结果失败:', e)\n            console.log('返回原始结果')\n            return NextResponse.json({\n              success: true,\n              status: 'success',\n              data: {\n                text: result.Data.Result,\n                raw: result.Data.Result\n              }\n            })\n          }\n\n        case 'PROCESS_PENDING':\n          console.log('任务等待处理中...')\n          return NextResponse.json({\n            success: true,\n            status: 'pending',\n            message: '任务等待处理中'\n          }, { status: 202 })\n\n        default:\n          console.log('收到未知任务状态:', result.Data.Status, '完整结果:', result.Data)\n          return NextResponse.json({\n            success: true,\n            status: result.Data.Status,\n            message: '任务状态未知，请继续轮询',\n            data: result.Data\n          }, { status: 202 })\n      }\n\n    } catch (queryError: any) {\n      console.error('查询任务错误详情:', {\n        name: queryError.name,\n        message: queryError.message,\n        code: queryError.code,\n        requestId: queryError.RequestId,\n        stack: queryError.stack\n      })\n      throw new Error(`查询视频识别任务失败: ${queryError.message}`)\n    }\n\n  } catch (error: any) {\n    console.error('处理请求错误:', error)\n    return NextResponse.json(\n      { message: error.message || '查询视频识别任务失败' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/asr/aliyun/recognize/route.ts",
    "content": "import { NextResponse } from 'next/server';\n\nexport async function POST(request: Request) {\n  try {\n    const { audioUrl, appKey, token, taskId } = await request.json();\n\n    // 调用阿里云语音识别 API\n    const response = await fetch('https://nls-gateway.aliyuncs.com/stream/v1/asr', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'X-NLS-Token': token,\n      },\n      body: JSON.stringify({\n        appkey: appKey,\n        audio_url: audioUrl,\n        format: 'wav',\n        sample_rate: 16000,\n        enable_intermediate_result: true,\n        enable_punctuation_prediction: true,\n        enable_inverse_text_normalization: true,\n      }),\n    });\n\n    if (!response.ok) {\n      throw new Error('阿里云 API 请求失败');\n    }\n\n    const data = await response.json();\n    return NextResponse.json(data);\n  } catch (error: any) {\n    return NextResponse.json(\n      { error: error.message || '识别失败' },\n      { status: 500 }\n    );\n  }\n} "
  },
  {
    "path": "app/api/asr/create/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { sign } from '@/lib/server/tencent-sign';\n\nconst endpoint = 'asr.tencentcloudapi.com';\nconst service = 'asr';\nconst version = '2019-06-14';\nconst region = 'ap-guangzhou';\nconst action = 'CreateRecTask';\n\nexport async function POST(request: Request) {\n  try {\n    const { engineType, channelNum, resTextFormat, sourceType, data } = await request.json();\n\n    const timestamp = Math.floor(Date.now() / 1000);\n    const params = {\n      EngineModelType: engineType,\n      ChannelNum: channelNum,\n      ResTextFormat: resTextFormat,\n      SourceType: sourceType,\n      Data: data,\n    };\n\n    const signature = sign({\n      secretId: process.env.TENCENT_SECRET_ID || '',\n      secretKey: process.env.TENCENT_SECRET_KEY || '',\n      endpoint,\n      service,\n      version,\n      region,\n      action,\n      timestamp,\n      payload: params,\n    });\n\n    const response = await fetch(`https://${endpoint}`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': signature,\n        'X-TC-Action': action,\n        'X-TC-Version': version,\n        'X-TC-Region': region,\n        'X-TC-Timestamp': timestamp.toString(),\n      },\n      body: JSON.stringify(params),\n    });\n\n    if (!response.ok) {\n      const errorData = await response.json();\n      throw new Error(errorData.Response?.Error?.Message || 'API request failed');\n    }\n\n    const result = await response.json();\n    return NextResponse.json(result);\n  } catch (error: any) {\n    console.error('创建识别任务失败:', error);\n    return NextResponse.json(\n      { error: error.message || '创建识别任务失败' },\n      { status: 500 }\n    );\n  }\n} "
  },
  {
    "path": "app/api/asr/status/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { sign } from '@/lib/server/tencent-sign';\n\nconst endpoint = 'asr.tencentcloudapi.com';\nconst service = 'asr';\nconst version = '2019-06-14';\nconst region = 'ap-guangzhou';\nconst action = 'DescribeTaskStatus';\n\nexport async function POST(request: Request) {\n  try {\n    const { taskId } = await request.json();\n\n    const timestamp = Math.floor(Date.now() / 1000);\n    const params = {\n      TaskId: taskId,\n    };\n\n    const signature = sign({\n      secretId: process.env.TENCENT_SECRET_ID || '',\n      secretKey: process.env.TENCENT_SECRET_KEY || '',\n      endpoint,\n      service,\n      version,\n      region,\n      action,\n      timestamp,\n      payload: params,\n    });\n\n    const response = await fetch(`https://${endpoint}`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': signature,\n        'X-TC-Action': action,\n        'X-TC-Version': version,\n        'X-TC-Region': region,\n        'X-TC-Timestamp': timestamp.toString(),\n      },\n      body: JSON.stringify(params),\n    });\n\n    if (!response.ok) {\n      const errorData = await response.json();\n      throw new Error(errorData.Response?.Error?.Message || 'API request failed');\n    }\n\n    const result = await response.json();\n    return NextResponse.json(result);\n  } catch (error: any) {\n    console.error('查询任务状态失败:', error);\n    return NextResponse.json(\n      { error: error.message || '查询任务状态失败' },\n      { status: 500 }\n    );\n  }\n} "
  },
  {
    "path": "app/api/auth/[...nextauth]/auth.ts",
    "content": "import { AuthOptions } from 'next-auth'\nimport CredentialsProvider from 'next-auth/providers/credentials'\nimport GitHubProvider from 'next-auth/providers/github'\nimport GoogleProvider from 'next-auth/providers/google'\nimport { neon } from '@neondatabase/serverless'\nimport bcrypt from 'bcryptjs'\n\nconst sql = neon(process.env.DATABASE_URL!)\n\nexport const authOptions: AuthOptions = {\n  debug: process.env.NODE_ENV === 'development',\n  session: {\n    strategy: \"jwt\",\n    maxAge: 30 * 24 * 60 * 60, // 30 days\n  },\n  providers: [\n    CredentialsProvider({\n      name: 'credentials',\n      credentials: {\n        email: { label: \"邮箱\", type: \"email\" },\n        password: { label: \"密码\", type: \"password\" }\n      },\n      async authorize(credentials) {\n        if (!credentials?.email || !credentials?.password) {\n          throw new Error('Please enter email and password')\n        }\n\n        try {\n          const result = await sql`\n            SELECT * FROM auth_users WHERE email = ${credentials.email}\n          `\n          const user = result[0]\n          console.log('查询到的用户数据:', user)\n\n          if (!user || !user.password_hash) {\n            throw new Error('Invalid email or password')\n          }\n\n          const isValid = await bcrypt.compare(credentials.password, user.password_hash)\n          console.log('密码比较结果:', isValid)\n          \n          if (!isValid) {\n            throw new Error('Invalid email or password')\n          }\n\n          return {\n            id: user.id,\n            email: user.email,\n            name: user.name || null\n          }\n        } catch (error) {\n          console.error('登录验证错误:', error)\n          throw new Error('Authentication server error, please try again later')\n        }\n      }\n    }),\n    GoogleProvider({\n      clientId: process.env.GOOGLE_CLIENT_ID!,\n      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n      profile(profile) {\n        return {\n          id: profile.sub,\n          name: profile.name,\n          email: profile.email!,\n          image: profile.picture,\n        }\n      },\n    }),\n    GitHubProvider({\n      clientId: process.env.GITHUB_ID!,\n      clientSecret: process.env.GITHUB_SECRET!,\n      httpOptions: {\n        timeout: 10000\n      },\n      profile(profile) {\n        return {\n          id: String(profile.id),\n          name: profile.name || profile.login,\n          email: profile.email!,\n          image: profile.avatar_url,\n        }\n      },\n    })\n  ],\n  pages: {\n    signIn: '/login',\n    error: '/login',\n  },\n  callbacks: {\n    async signIn({ user, account }) {\n      if (!user.email) return false\n\n      try {\n        // 检查用户是否存在\n        const users = await sql`\n          SELECT id, email FROM auth_users\n          WHERE email = ${user.email}\n        `\n\n        // 如果是首次登录，创建新用户\n        if (users.length === 0 && account?.providerAccountId) {\n          if (account.provider === 'github') {\n            await sql`\n              INSERT INTO auth_users (\n                email,\n                name,\n                github_id,\n                text_quota,\n                image_quota,\n                pdf_quota,\n                speech_quota,\n                video_quota,\n                created_at,\n                updated_at\n              ) VALUES (\n                ${user.email},\n                ${user.name},\n                ${account.providerAccountId},\n                -1,\n                10,\n                8,\n                5,\n                2,\n                CURRENT_TIMESTAMP,\n                CURRENT_TIMESTAMP\n              )\n            `\n          } else {\n            await sql`\n              INSERT INTO auth_users (\n                email,\n                name,\n                google_id,\n                text_quota,\n                image_quota,\n                pdf_quota,\n                speech_quota,\n                video_quota,\n                created_at,\n                updated_at\n              ) VALUES (\n                ${user.email},\n                ${user.name},\n                ${account.providerAccountId},\n                -1,\n                10,\n                8,\n                5,\n                2,\n                CURRENT_TIMESTAMP,\n                CURRENT_TIMESTAMP\n              )\n            `\n          }\n        }\n\n        return true\n      } catch (error) {\n        console.error('Error in signIn callback:', error)\n        return false\n      }\n    },\n    async jwt({ token, user }) {\n      if (user) {\n        token.id = user.id\n      }\n      return token\n    },\n    async session({ session }) {\n      if (session.user?.email) {\n        const users = await sql`\n          SELECT id\n          FROM auth_users\n          WHERE email = ${session.user.email}\n        `\n        if (users.length > 0) {\n          session.user.id = users[0].id\n        }\n      }\n      return session\n    }\n  }\n} "
  },
  {
    "path": "app/api/auth/[...nextauth]/route.ts",
    "content": "import NextAuth from 'next-auth'\nimport { authOptions } from '@/app/api/auth/[...nextauth]/auth'\n\nconst handler = NextAuth(authOptions)\nexport { handler as GET, handler as POST } "
  },
  {
    "path": "app/api/auth/register/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { neon } from '@neondatabase/serverless'\nimport bcrypt from 'bcryptjs'\n\nexport async function POST(req: Request) {\n  const sql = neon(process.env.DATABASE_URL!)\n  \n  try {\n    const { email, password, name } = await req.json()\n\n    if (!email || !password) {\n      return new NextResponse('Missing email or password', { status: 400 })\n    }\n\n    // 检查邮箱是否已被注册\n    const existingUser = await sql`\n      SELECT * FROM users WHERE email = ${email}\n    `\n\n    if (existingUser.length > 0) {\n      return new NextResponse('Email already exists', { status: 400 })\n    }\n\n    // 密码加密\n    const hashedPassword = await bcrypt.hash(password, 10)\n\n    // 创建用户\n    await sql`\n      INSERT INTO users (email, password)\n      VALUES (${email}, ${hashedPassword})\n    `\n\n    return new NextResponse('User created successfully', { status: 201 })\n  } catch (error: any) {\n    console.error('注册失败:', error)\n    return new NextResponse('Internal Server Error', { status: 500 })\n  }\n} "
  },
  {
    "path": "app/api/file/extract/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { Mistral } from '@mistralai/mistralai';\n\nconst KIMI_API_KEY = process.env.NEXT_PUBLIC_KIMI_API_KEY\nconst KIMI_API_URL = 'https://api.moonshot.cn/v1'\nconst MISTRAL_API_KEY = process.env.MISTRAL_API_KEY\n\n// 增加超时时间\nconst TIMEOUT = {\n  UPLOAD: 20000,    // 20秒\n  CONTENT: 30000,   // 30秒\n  PROCESS: 45000    // 45秒\n}\n\n// 带超时的 fetch\nasync function fetchWithTimeout(url: string, options: RequestInit, timeout: number) {\n  const controller = new AbortController()\n  const id = setTimeout(() => controller.abort(), timeout)\n\n  try {\n    const response = await fetch(url, {\n      ...options,\n      signal: controller.signal\n    })\n    clearTimeout(id)\n    \n    // 检查响应状态\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({ error: { message: '请求失败' } }))\n      throw new Error(error.error?.message || `请求失败: ${response.status}`)\n    }\n    \n    return response\n  } catch (error: any) {\n    clearTimeout(id)\n    if (error.name === 'AbortError') {\n      throw new Error('请求超时，请重试')\n    }\n    throw error\n  }\n}\n\n// 分步上传文件\nasync function uploadFile(file: string, filename: string) {\n  try {\n    const formData = new FormData()\n    const fileBlob = new Blob([Buffer.from(file, 'base64')], { type: 'application/pdf' })\n    const pdfFile = new File([fileBlob], filename, { type: 'application/pdf' })\n    formData.append('file', pdfFile)\n    formData.append('purpose', 'file-extract')\n\n    const uploadResponse = await fetchWithTimeout(`${KIMI_API_URL}/files`, {\n      method: 'POST',\n      headers: {\n        'Authorization': `Bearer ${KIMI_API_KEY}`\n      },\n      body: formData\n    }, TIMEOUT.UPLOAD)\n\n    if (!uploadResponse.ok) {\n      const error = await uploadResponse.json().catch(() => ({ error: { message: '文件上传失败' } }))\n      console.error('KIMI文件上传错误:', error)\n      throw new Error(error.error?.message || '文件上传失败')\n    }\n\n    return await uploadResponse.json()\n  } catch (error: any) {\n    if (error.name === 'AbortError') {\n      throw new Error('文件上传超时')\n    }\n    throw error\n  }\n}\n\n// 获取文件内容\nasync function getFileContent(fileId: string) {\n  try {\n    const contentResponse = await fetchWithTimeout(`${KIMI_API_URL}/files/${fileId}/content`, {\n      headers: {\n        'Authorization': `Bearer ${KIMI_API_KEY}`\n      }\n    }, TIMEOUT.CONTENT)\n\n    if (!contentResponse.ok) {\n      const error = await contentResponse.json().catch(() => ({ error: { message: '文件内容获取失败' } }))\n      console.error('KIMI文件内容获取错误:', error)\n      throw new Error(error.error?.message || '文件内容获取失败')\n    }\n\n    return await contentResponse.text()\n  } catch (error: any) {\n    if (error.name === 'AbortError') {\n      throw new Error('文件内容获取超时')\n    }\n    throw error\n  }\n}\n\n// 处理文件内容\nasync function processContent(content: string) {\n  try {\n    const messages = [\n      {\n        role: 'system',\n        content: '你是 Kimi，由 Moonshot AI 提供的人工智能助手。请提取文件中的所有文字内容，保持原文的格式和换行，不需要总结或解释。'\n      },\n      {\n        role: 'system',\n        content\n      },\n      {\n        role: 'user',\n        content: '请直接返回文件的原始内容，保持格式，不要添加任何解释或总结。'\n      }\n    ]\n\n    const chatResponse = await fetchWithTimeout(`${KIMI_API_URL}/chat/completions`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': `Bearer ${KIMI_API_KEY}`\n      },\n      body: JSON.stringify({\n        model: 'moonshot-v1-32k',\n        messages,\n        temperature: 0.3\n      })\n    }, TIMEOUT.PROCESS)\n\n    if (!chatResponse.ok) {\n      const error = await chatResponse.json().catch(() => ({ error: { message: 'API请求失败' } }))\n      console.error('KIMI API错误:', error)\n      throw new Error(error.error?.message || 'API请求失败')\n    }\n\n    const data = await chatResponse.json()\n    if (!data.choices?.[0]?.message?.content) {\n      console.error('KIMI API响应格式错误:', data)\n      throw new Error('API返回格式错误')\n    }\n\n    return data.choices[0].message.content.trim()\n  } catch (error: any) {\n    if (error.name === 'AbortError') {\n      throw new Error('内容处理超时')\n    }\n    throw error\n  }\n}\n\n// 使用 Mistral OCR API 处理 PDF\nasync function processPdfWithMistral(file: string, filename: string) {\n  try {\n    console.log('开始使用 Mistral OCR 处理 PDF...')\n    \n    // 创建 Mistral 客户端\n    const client = new Mistral({ apiKey: MISTRAL_API_KEY || '' });\n    \n    // 将 base64 转换为 Buffer\n    const fileBuffer = Buffer.from(file, 'base64');\n    \n    // 上传文件到 Mistral\n    console.log('上传文件到 Mistral...')\n    let uploadData;\n    try {\n      uploadData = await client.files.upload({\n        file: {\n          fileName: filename,\n          content: fileBuffer,\n        },\n        purpose: \"ocr\"\n      });\n      console.log('文件上传成功，ID:', uploadData.id);\n    } catch (uploadError: any) {\n      console.error('Mistral 文件上传错误:', uploadError);\n      throw new Error(uploadError.message || 'Mistral 文件上传失败');\n    }\n    \n    // 获取签名 URL\n    console.log('获取签名 URL...');\n    let signedUrlData;\n    try {\n      signedUrlData = await client.files.getSignedUrl({\n        fileId: uploadData.id,\n      });\n      console.log('获取签名 URL 成功');\n    } catch (signedUrlError: any) {\n      console.error('获取签名 URL 错误:', signedUrlError);\n      throw new Error(signedUrlError.message || '获取签名 URL 失败');\n    }\n    \n    // 使用 OCR 处理文件\n    console.log('开始 OCR 处理...');\n    let ocrData: any;\n    try {\n      ocrData = await client.ocr.process({\n        model: \"mistral-ocr-latest\",\n        document: {\n          type: \"document_url\",\n          documentUrl: signedUrlData.url,\n        }\n      });\n      console.log('OCR 处理完成，响应数据类型:', typeof ocrData);\n      if (typeof ocrData === 'object' && ocrData !== null) {\n        console.log('OCR 响应数据结构:', Object.keys(ocrData).join(', '));\n        // 输出更详细的响应结构\n        for (const key of Object.keys(ocrData)) {\n          console.log(`OCR 响应字段 ${key} 类型:`, typeof ocrData[key]);\n          if (key === 'pages' && Array.isArray(ocrData.pages)) {\n            console.log('pages 数组长度:', ocrData.pages.length);\n            if (ocrData.pages.length > 0) {\n              console.log('第一页结构:', Object.keys(ocrData.pages[0]).join(', '));\n            }\n          }\n        }\n      }\n      console.log('OCR 响应数据片段:', typeof ocrData === 'string' ? ocrData.substring(0, 500) : JSON.stringify(ocrData).substring(0, 500) + '...');\n    } catch (ocrError: any) {\n      console.error('Mistral OCR 错误:', ocrError);\n      throw new Error(ocrError.message || 'OCR 处理失败');\n    }\n    \n    // 提取文本内容\n    let extractedText = '';\n    \n    // 检查响应格式并记录详细信息\n    if (!ocrData) {\n      console.error('Mistral OCR 返回空响应');\n      throw new Error('OCR 返回空响应');\n    }\n    \n    // 处理 Markdown 格式的响应\n    if (typeof ocrData === 'string') {\n      console.log('OCR 返回 Markdown 格式的文本');\n      // 直接返回 Markdown 文本，不需要额外处理\n      return ocrData.trim();\n    }\n    \n    // 检查响应对象中的各种可能字段\n    if (ocrData.markdown) {\n      console.log('从 markdown 字段提取文本');\n      return String(ocrData.markdown).trim();\n    }\n    \n    // 检查各种可能的文本字段\n    if (ocrData.text) {\n      console.log('从 text 字段提取文本');\n      return String(ocrData.text).trim();\n    }\n    \n    if (ocrData.content) {\n      console.log('从 content 字段提取文本');\n      return typeof ocrData.content === 'string' ? ocrData.content.trim() : JSON.stringify(ocrData.content);\n    }\n    \n    // 检查是否有结果字段\n    if (ocrData.result) {\n      console.log('从 result 字段提取文本');\n      if (typeof ocrData.result === 'string') {\n        return ocrData.result.trim();\n      } else if (typeof ocrData.result === 'object' && ocrData.result !== null) {\n        // 检查result对象中的可能字段\n        if (ocrData.result.text) {\n          return String(ocrData.result.text).trim();\n        } else if (ocrData.result.content) {\n          return typeof ocrData.result.content === 'string' ? ocrData.result.content.trim() : JSON.stringify(ocrData.result.content);\n        } else {\n          return JSON.stringify(ocrData.result);\n        }\n      }\n    }\n    \n    // 检查 ocrData 是否有 pages 属性\n    if (ocrData.pages) {\n      console.log(`发现 pages 字段，包含 ${Array.isArray(ocrData.pages) ? ocrData.pages.length : '未知数量'} 页`);\n      \n      // 正常处理 pages 数组\n      if (Array.isArray(ocrData.pages)) {\n        console.log(`提取 ${ocrData.pages.length} 页的文本`);\n        \n        extractedText = ocrData.pages.map((page: any, index: number) => {\n          if (!page) {\n            console.log(`第 ${index + 1} 页为空`);\n            return '';\n          }\n          \n          // 首先检查markdown字段，这是Mistral OCR的主要输出格式\n          if (page.markdown) {\n            console.log(`从第 ${index + 1} 页的markdown字段提取文本`);\n            return page.markdown;\n          } else if (page.text) {\n            return page.text;\n          } else if (page.content) {\n            return typeof page.content === 'string' ? page.content : JSON.stringify(page.content);\n          } else {\n            console.log(`第 ${index + 1} 页没有文本内容:`, page);\n            return '';\n          }\n        }).join('\\n\\n');\n      } else {\n        console.error('Mistral OCR 响应中 pages 不是数组:', ocrData.pages);\n        // 尝试将 pages 作为文本返回\n        if (typeof ocrData.pages === 'string') {\n          return ocrData.pages.trim();\n        } else {\n          return JSON.stringify(ocrData.pages);\n        }\n      }\n    } else {\n      console.log('OCR 响应中没有找到 pages 字段，尝试从整个响应中提取文本');\n      // 如果找不到任何已知字段，尝试将整个响应作为文本返回\n      return JSON.stringify(ocrData);\n    }\n    \n    console.log(`提取的文本长度: ${extractedText.length} 字符`);\n    \n    // 确保返回非空文本\n    if (!extractedText || extractedText.trim() === '') {\n      console.log('提取的文本为空，返回默认消息');\n      return '无法从PDF中提取文本。这可能是因为PDF包含扫描图像或其他不可提取的内容。请尝试使用其他服务或上传不同的文件。';\n    }\n    \n    return extractedText.trim();\n  } catch (error: any) {\n    console.error('Mistral OCR 处理错误:', error);\n    if (error.name === 'AbortError') {\n      throw new Error('Mistral OCR 处理超时');\n    }\n    throw error;\n  }\n}\n\nexport async function POST(request: Request) {\n  try {\n    const { file, filename, service } = await request.json()\n\n    if (!file) {\n      return NextResponse.json(\n        { error: '未提供文件' },\n        { status: 400 }\n      )\n    }\n\n    // 检查文件大小\n    const base64Size = file.length * 0.75 // base64到字节的近似转换\n    if (base64Size > 5 * 1024 * 1024) { // 5MB\n      return NextResponse.json(\n        { error: '文件大小超过限制' },\n        { status: 400 }\n      )\n    }\n\n    if (service !== 'kimi' && service !== 'mistral') {\n      return NextResponse.json(\n        { error: '不支持的服务' },\n        { status: 400 }\n      )\n    }\n\n    if (service === 'kimi' && !KIMI_API_KEY) {\n      return NextResponse.json(\n        { error: '未配置Kimi API密钥' },\n        { status: 500 }\n      )\n    }\n\n    if (service === 'mistral' && !MISTRAL_API_KEY) {\n      return NextResponse.json(\n        { error: '未配置Mistral API密钥' },\n        { status: 500 }\n      )\n    }\n\n    // 分步处理\n    try {\n      let result = ''\n      \n      if (service === 'kimi') {\n        // 使用 Kimi API 处理\n        console.log('开始使用 Kimi API 处理...')\n        // 步骤1：上传文件\n        console.log('开始上传文件...')\n        const fileObject = await uploadFile(file, filename)\n        \n        // 步骤2：获取文件内容\n        console.log('开始获取文件内容...')\n        const fileContent = await getFileContent(fileObject.id)\n        \n        // 步骤3：处理内容\n        console.log('开始处理文件内容...')\n        result = await processContent(fileContent)\n      } else if (service === 'mistral') {\n        // 使用 Mistral OCR API 处理\n        console.log('开始使用 Mistral OCR API 处理...')\n        result = await processPdfWithMistral(file, filename)\n      }\n\n      // 检查响应大小\n      if (result.length > 5 * 1024 * 1024) { // 5MB\n        throw new Error('响应内容过大')\n      }\n\n      return NextResponse.json({ text: result })\n    } catch (error: any) {\n      console.error('处理步骤错误:', error)\n      if (error.name === 'AbortError' || error.message.includes('超时')) {\n        return NextResponse.json(\n          { error: '处理超时，请稍后重试' },\n          { status: 503 }\n        )\n      }\n      \n      // 根据错误类型返回不同的状态码\n      if (error.message.includes('文件大小超过限制') || error.message.includes('响应内容过大')) {\n        return NextResponse.json(\n          { error: error.message },\n          { status: 413 }\n        )\n      }\n      \n      return NextResponse.json(\n        { error: error.message || 'PDF处理失败' },\n        { status: 500 }\n      )\n    }\n  } catch (error: any) {\n    console.error('PDF处理错误:', error)\n    return NextResponse.json(\n      { error: error.message || 'PDF处理失败' },\n      { status: error.status || 500 }\n    )\n  }\n}"
  },
  {
    "path": "app/api/ocr/kimi/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport OpenAI from 'openai'\n\nconst KIMI_API_KEY = process.env.NEXT_PUBLIC_KIMI_API_KEY\nconst KIMI_API_URL = 'https://api.moonshot.cn/v1'\n\nexport async function POST(request: Request) {\n  try {\n    const { image } = await request.json()\n    \n    if (!image) {\n      return NextResponse.json(\n        { error: 'No image data provided' },\n        { status: 400 }\n      )\n    }\n\n    if (!KIMI_API_KEY) {\n      return NextResponse.json(\n        { error: 'Kimi API key not found' },\n        { status: 500 }\n      )\n    }\n\n    // 从完整的 data URL 中提取 base64 数据\n    const base64Data = image.split(';base64,').pop() || image;\n\n    const openai = new OpenAI({\n      apiKey: KIMI_API_KEY,\n      baseURL: KIMI_API_URL\n    })\n\n    const response = await openai.chat.completions.create({\n      model: 'moonshot-v1-32k-vision-preview',\n      messages: [\n        {\n          role: 'system',\n          content: '你是一个专业的图片文字识别助手。请提取图片中的所有文字，保持原有格式，不要添加任何解释。'\n        },\n        {\n          role: 'user',\n          content: [\n            { type: 'text', text: '请提取这张图片中的所有文字：' },\n            { type: 'image_url', image_url: { url: image } }\n          ]\n        }\n      ],\n      temperature: 0.1\n    })\n\n    const extractedText = response.choices[0]?.message?.content\n    if (!extractedText) {\n      return NextResponse.json(\n        { error: 'No text extracted' },\n        { status: 400 }\n      )\n    }\n\n    return NextResponse.json({ text: extractedText.trim() })\n  } catch (error: any) {\n    console.error('Error extracting text with Kimi:', error)\n    return NextResponse.json(\n      { error: error.message || '文字识别失败，请稍后重试' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/ocr/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport * as tencentcloud from 'tencentcloud-sdk-nodejs-ocr'\n\nconst OcrClient = tencentcloud.ocr.v20181119.Client\n\ninterface TextDetection {\n  DetectedText: string;\n  Confidence: number;\n  Polygon: Array<{\n    X: number;\n    Y: number;\n  }>;\n  AdvancedInfo: string;\n}\n\ninterface OCRResponse {\n  TextDetections: TextDetection[];\n  Language: string;\n  RequestId: string;\n}\n\nconst client = new OcrClient({\n  credential: {\n    secretId: process.env.TENCENT_SECRET_ID || '',\n    secretKey: process.env.TENCENT_SECRET_KEY || '',\n  },\n  region: 'ap-guangzhou',\n  profile: {\n    signMethod: 'TC3-HMAC-SHA256',\n    httpProfile: {\n      reqMethod: 'POST',\n      reqTimeout: 30,\n      endpoint: 'ocr.tencentcloudapi.com',\n    },\n  },\n})\n\nexport async function POST(request: Request) {\n  try {\n    const { image } = await request.json()\n\n    if (!image) {\n      return NextResponse.json(\n        { success: false, message: '未找到图片数据' },\n        { status: 400 }\n      )\n    }\n\n    const result = await client.GeneralBasicOCR({\n      ImageBase64: image,\n    }) as OCRResponse\n\n    if (!result || !result.TextDetections || result.TextDetections.length === 0) {\n      return NextResponse.json(\n        { success: false, message: '未识别到文字' },\n        { status: 400 }\n      )\n    }\n\n    const text = result.TextDetections.map((item: TextDetection) => item.DetectedText).join('\\n')\n\n    return NextResponse.json({\n      success: true,\n      text\n    })\n  } catch (error: any) {\n    console.error('腾讯云OCR错误:', error)\n    return NextResponse.json(\n      { success: false, message: error.message || 'OCR识别失败' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/ocr/step/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport OpenAI from 'openai'\n\n// 设置较长的超时时间\nexport const maxDuration = 60; // 设置为60秒\n\nexport async function POST(request: Request) {\n  const apiKey = process.env.NEXT_PUBLIC_STEP_API_KEY;\n  if (!apiKey) {\n    return NextResponse.json(\n      { error: 'API key not configured' },\n      { status: 500 }\n    );\n  }\n\n  try {\n    const formData = await request.formData();\n    const image = formData.get('image');\n    \n    if (!image) {\n      return NextResponse.json(\n        { error: 'No image provided' },\n        { status: 400 }\n      );\n    }\n\n    const openai = new OpenAI({\n      apiKey: apiKey,\n      baseURL: 'https://api.stepfun.com/v1',\n      dangerouslyAllowBrowser: true,\n      timeout: 30000 // 30秒超时\n    });\n\n    // 最多重试3次\n    let retries = 3;\n    let lastError;\n\n    while (retries > 0) {\n      try {\n        const response = await openai.chat.completions.create({\n          model: 'step-1v-32k',\n          messages: [\n            {\n              role: 'system',\n              content: 'You are an OCR assistant. Extract text from the provided image accurately.'\n            },\n            {\n              role: 'user',\n              content: [\n                { type: 'text', text: 'Please extract text from this image:' },\n                { type: 'image_url', image_url: { url: image instanceof File ? URL.createObjectURL(image) : image.toString() } }\n              ]\n            }\n          ]\n        });\n\n        const extractedText = response.choices[0]?.message?.content;\n        if (!extractedText) {\n          throw new Error('No text extracted');\n        }\n\n        return NextResponse.json({ text: extractedText });\n      } catch (error: any) {\n        lastError = error;\n        if (error.status === 504) {\n          retries--;\n          if (retries > 0) {\n            // 等待1秒后重试\n            await new Promise(resolve => setTimeout(resolve, 1000));\n            continue;\n          }\n        }\n        break;\n      }\n    }\n\n    console.error('Step OCR error after retries:', lastError);\n    return NextResponse.json(\n      { error: lastError?.message || 'OCR failed after retries' },\n      { status: lastError?.status || 500 }\n    );\n  } catch (error: any) {\n    console.error('Step OCR error:', error);\n    return NextResponse.json(\n      { error: error.message || 'OCR failed' },\n      { status: error.status || 500 }\n    );\n  }\n} "
  },
  {
    "path": "app/api/qwen/ocr/route.ts",
    "content": "import { NextResponse } from 'next/server'\n\nif (!process.env.NEXT_PUBLIC_QWEN_API_KEY) {\n  throw new Error('Missing NEXT_PUBLIC_QWEN_API_KEY environment variable')\n}\n\nexport async function POST(request: Request) {\n  try {\n    const { image } = await request.json()\n\n    if (!image) {\n      return NextResponse.json(\n        { message: '缺少图片数据' },\n        { status: 400 }\n      )\n    }\n\n    const response = await fetch('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', {\n      method: 'POST',\n      headers: {\n        'Authorization': `Bearer ${process.env.NEXT_PUBLIC_QWEN_API_KEY}`,\n        'Content-Type': 'application/json'\n      },\n      body: JSON.stringify({\n        model: 'qwen-vl-ocr',\n        messages: [\n          {\n            role: 'user',\n            content: [\n              {\n                type: 'image_url',\n                image_url: {\n                  url: `data:image/jpeg;base64,${image}`\n                },\n                min_pixels: 28 * 28 * 4,\n                max_pixels: 28 * 28 * 1280\n              },\n              {\n                type: 'text',\n                text: 'Read all the text in the image.'\n              }\n            ]\n          }\n        ]\n      })\n    })\n\n    if (!response.ok) {\n      const error = await response.json()\n      throw new Error(error.message || '文字识别失败')\n    }\n\n    const result = await response.json()\n    const text = result.choices[0].message.content\n    return NextResponse.json({ text })\n  } catch (error: any) {\n    console.error('通义千问OCR错误:', error)\n    return NextResponse.json(\n      { message: error.message || '文字识别失败' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/qwen/translate/route.ts",
    "content": "import { NextResponse } from 'next/server';\n\nexport const runtime = 'edge';\n\nexport async function POST(request: Request) {\n  try {\n    const { text, targetLang } = await request.json();\n\n    if (!text || !targetLang) {\n      return NextResponse.json(\n        { error: '缺少必要参数' },\n        { status: 400 }\n      );\n    }\n\n    const response = await fetch('https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': `Bearer ${process.env.NEXT_PUBLIC_QWEN_API_KEY}`,\n      },\n      body: JSON.stringify({\n        model: 'qwen-max',\n        input: {\n          messages: [\n            {\n              role: 'system',\n              content: 'You are a professional translator. Translate the text directly without any explanations.'\n            },\n            {\n              role: 'user',\n              content: `Translate to ${targetLang}:\\n${text}`\n            }\n          ]\n        },\n        parameters: {\n          temperature: 0.1,\n          max_tokens: 2048,\n        }\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      return NextResponse.json(\n        { error: error.message || '翻译请求失败' },\n        { status: response.status }\n      );\n    }\n\n    const result = await response.json();\n    return NextResponse.json({ text: result.output.text });\n  } catch (error: any) {\n    console.error('Error in Qwen translation:', error);\n    return NextResponse.json(\n      { error: error.message || '翻译服务出错' },\n      { status: 500 }\n    );\n  }\n} "
  },
  {
    "path": "app/api/register/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { neon } from '@neondatabase/serverless'\nimport bcrypt from 'bcryptjs'\n\nconst sql = neon(process.env.DATABASE_URL!)\n\nexport async function POST(req: Request) {\n  try {\n    console.log('开始处理注册请求')\n    const { email, password } = await req.json()\n    console.log('收到注册数据:', { email, hasPassword: !!password })\n\n    // 验证输入\n    if (!email || !password) {\n      console.log('输入验证失败')\n      return NextResponse.json(\n        { error: '请输入邮箱和密码' },\n        { status: 400 }\n      )\n    }\n\n    // 检查邮箱是否已存在\n    console.log('检查邮箱是否存在:', email)\n    const existingUser = await sql`\n      SELECT id FROM auth_users WHERE email = ${email}\n    `\n    console.log('查询结果:', existingUser)\n    \n    if (existingUser.length > 0) {\n      console.log('邮箱已存在')\n      return NextResponse.json(\n        { error: '该邮箱已被注册' },\n        { status: 400 }\n      )\n    }\n\n    // 加密密码\n    console.log('开始加密密码')\n    const hashedPassword = await bcrypt.hash(password, 10)\n\n    // 创建用户\n    console.log('开始创建用户')\n    const result = await sql`\n      INSERT INTO auth_users (\n        email, \n        password_hash,\n        text_quota,\n        image_quota,\n        pdf_quota,\n        speech_quota,\n        video_quota\n      )\n      VALUES (\n        ${email}, \n        ${hashedPassword},\n        -1,\n        10,\n        8,\n        5,\n        2\n      )\n      RETURNING *\n    `\n    console.log('创建的用户数据:', result[0])\n\n    return NextResponse.json(\n      { \n        message: '注册成功',\n        user: {\n          id: result[0].id,\n          email: result[0].email\n        }\n      },\n      { status: 201 }\n    )\n  } catch (error: any) {\n    console.error('注册错误:', error)\n    console.error('错误堆栈:', error.stack)\n    return NextResponse.json(\n      { error: error.message || '注册失败' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/subscription/route.ts",
    "content": "import { getServerSession } from 'next-auth'\nimport { NextResponse } from 'next/server'\nimport { stripe, PLANS } from '@/lib/stripe'\nimport { authOptions } from '../auth/[...nextauth]/auth'\n\nexport async function POST(req: Request) {\n  try {\n    const session = await getServerSession(authOptions)\n    if (!session?.user?.email) {\n      return new NextResponse('Unauthorized', { status: 401 })\n    }\n\n    const { priceId } = await req.json()\n    if (!priceId) {\n      return new NextResponse('Price ID is required', { status: 400 })\n    }\n\n    if (!stripe) {\n      return new NextResponse('Stripe is not configured', { status: 500 })\n    }\n\n    // 检查价格 ID 是否有效\n    const paidPlans = [PLANS.monthly, PLANS.yearly]\n    const plan = paidPlans.find(p => p.priceId === priceId)\n    if (!plan) {\n      return new NextResponse('Invalid price ID', { status: 400 })\n    }\n\n    // 创建或获取 Stripe 客户\n    const customer = await stripe.customers.create({\n      email: session.user.email,\n      metadata: {\n        userId: session.user.id\n      }\n    })\n\n    // 创建结账会话\n    const checkoutSession = await stripe.checkout.sessions.create({\n      customer: customer.id,\n      line_items: [\n        {\n          price: priceId,\n          quantity: 1,\n        },\n      ],\n      mode: 'subscription',\n      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/profile?subscription=success`,\n      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,\n      subscription_data: {\n        metadata: {\n          userId: session.user.id,\n        },\n      },\n    })\n\n    return NextResponse.json({ url: checkoutSession.url })\n  } catch (error) {\n    console.error('Subscription error:', error)\n    return new NextResponse('Internal error', { status: 500 })\n  }\n} "
  },
  {
    "path": "app/api/tencent/ocr/route.ts",
    "content": "import { NextResponse } from 'next/server';\n\n// 使用 require 导入腾讯云 SDK\nconst tencentcloud = require(\"tencentcloud-sdk-nodejs\");\nconst OcrClient = tencentcloud.ocr.v20181119.Client;\n\nexport async function POST(request: Request) {\n  try {\n    const { image } = await request.json();\n\n    if (!image) {\n      return NextResponse.json(\n        { success: false, message: '缺少图片数据' },\n        { status: 400 }\n      );\n    }\n\n    const client = new OcrClient({\n      credential: {\n        secretId: process.env.TENCENT_SECRET_ID || '',\n        secretKey: process.env.TENCENT_SECRET_KEY || '',\n      },\n      region: 'ap-guangzhou',\n      profile: {\n        signMethod: 'TC3-HMAC-SHA256',\n        httpProfile: {\n          reqMethod: 'POST',\n          reqTimeout: 30,\n          endpoint: 'ocr.tencentcloudapi.com',\n        },\n      },\n    });\n\n    const base64Data = image.split(',')[1];\n    const result = await client.GeneralBasicOCR({\n      ImageBase64: base64Data,\n      LanguageType: 'auto',\n    });\n\n    if (!result || !result.TextDetections) {\n      throw new Error('文字识别失败');\n    }\n\n    const textLines = result.TextDetections.map((item: any) => item.DetectedText).filter(Boolean);\n    const text = textLines.join('\\n');\n\n    return NextResponse.json({\n      success: true,\n      result: text\n    });\n  } catch (error: any) {\n    console.error('腾讯云 OCR 错误:', error);\n    return NextResponse.json(\n      { \n        success: false, \n        message: error.code === 'AuthFailure' \n          ? '腾讯云认证失败，请检查密钥配置' \n          : '文字识别失败'\n      },\n      { status: 500 }\n    );\n  }\n} "
  },
  {
    "path": "app/api/translate/claude/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport OpenAI from 'openai';\n\nexport async function POST(request: Request) {\n  try {\n    const { text, targetLanguage } = await request.json();\n\n    if (!text || !targetLanguage) {\n      return NextResponse.json(\n        { error: '缺少必要参数' },\n        { status: 400 }\n      );\n    }\n\n    const apiKey = process.env.NEXT_PUBLIC_OPENROUTER_API_KEY;\n    if (!apiKey) {\n      return NextResponse.json(\n        { error: 'OpenRouter API key not found' },\n        { status: 500 }\n      );\n    }\n\n    const openai = new OpenAI({\n      apiKey: apiKey,\n      baseURL: 'https://openrouter.ai/api/v1'\n    });\n\n    const completion = await openai.chat.completions.create({\n      model: 'anthropic/claude-3-haiku',\n      messages: [\n        {\n          role: 'system',\n          content: 'You are a professional translator. Translate the text directly without any explanations.'\n        },\n        {\n          role: 'user',\n          content: `Translate to ${targetLanguage}:\\n${text}`\n        }\n      ],\n      temperature: 0.3,\n      max_tokens: 2000,\n    });\n\n    const translatedText = completion.choices[0].message.content;\n    return NextResponse.json({ text: translatedText });\n\n  } catch (error: any) {\n    console.error('Claude translation error:', error);\n    return NextResponse.json(\n      { error: error.message || '翻译失败' },\n      { status: 500 }\n    );\n  }\n} "
  },
  {
    "path": "app/api/translate/kimi/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport OpenAI from 'openai';\n\nexport async function POST(request: Request) {\n  try {\n    const { text, targetLanguage } = await request.json();\n\n    if (!text || !targetLanguage) {\n      return NextResponse.json(\n        { error: 'Missing required parameters' },\n        { status: 400 }\n      );\n    }\n\n    const apiKey = process.env.NEXT_PUBLIC_KIMI_API_KEY;\n    if (!apiKey) {\n      return NextResponse.json(\n        { error: 'API key not found' },\n        { status: 500 }\n      );\n    }\n\n    const openai = new OpenAI({\n      apiKey: apiKey,\n      baseURL: 'https://api.moonshot.cn/v1',\n    });\n\n    const completion = await openai.chat.completions.create({\n      model: 'moonshot-v1-128k',\n      messages: [\n        {\n          role: 'system',\n          content: `You are a professional translator. Translate the following text to ${targetLanguage}. Keep the original format and style.`\n        },\n        {\n          role: 'user',\n          content: text\n        }\n      ],\n      temperature: 0.3,\n      max_tokens: 2000\n    });\n\n    const translatedText = completion.choices[0]?.message?.content;\n    if (!translatedText) {\n      return NextResponse.json(\n        { error: 'No translation result' },\n        { status: 500 }\n      );\n    }\n\n    return NextResponse.json({ translatedText });\n  } catch (error: any) {\n    console.error('Kimi translation error:', error);\n    return NextResponse.json(\n      { error: error.message || 'Translation failed' },\n      { status: 500 }\n    );\n  }\n} "
  },
  {
    "path": "app/api/translate/route.ts",
    "content": "import OpenAI from 'openai';\nimport { sign } from '@/lib/server/tencent-sign';\nimport { translateWithKimiAPI } from '@/lib/server/translate';\n\nexport async function POST(request: Request) {\n  try {\n    const { text, targetLanguage, service } = await request.json();\n\n    if (!text || !targetLanguage) {\n      return new Response(JSON.stringify({ error: '缺少必要参数' }), {\n        status: 400,\n        headers: { 'Content-Type': 'application/json' },\n      });\n    }\n\n    let translatedText;\n\n    try {\n      switch (service) {\n        case 'deepseek':\n          translatedText = await translateWithDeepSeekAPI(text, targetLanguage);\n          break;\n        case 'qwen':\n          translatedText = await translateWithQwenAPI(text, targetLanguage);\n          break;\n        case 'zhipu':\n          translatedText = await translateWithZhipuAPI(text, targetLanguage);\n          break;\n        case '4o-mini':\n          translatedText = await translateWith4oMiniAPI(text, targetLanguage);\n          break;\n        case 'hunyuan':\n          translatedText = await translateWithHunyuanAPI(text, targetLanguage);\n          break;\n        case 'minnimax':\n          translatedText = await translateWithMinniMaxAPI(text, targetLanguage);\n          break;\n        case 'siliconflow':\n          translatedText = await translateWithSiliconFlowAPI(text, targetLanguage);\n          break;\n        case 'claude_3_5':\n          translatedText = await translateWithClaudeAPI(text, targetLanguage);\n          break;\n        case 'kimi':\n          translatedText = await translateWithKimiAPI(text, targetLanguage);\n          break;\n        case 'step':\n          translatedText = await translateWithStepAPI(text, targetLanguage);\n          break;\n        default:\n          translatedText = await translateWithDeepSeekAPI(text, targetLanguage);\n      }\n    } catch (serviceError: any) {\n      console.error(`${service} translation service error:`, serviceError);\n      // 如果当前服务失败，尝试使用 DeepSeek 作为备选\n      if (service !== 'deepseek') {\n        console.log('Trying DeepSeek as fallback service...');\n        translatedText = await translateWithDeepSeekAPI(text, targetLanguage);\n      } else {\n        throw serviceError;\n      }\n    }\n\n    return new Response(JSON.stringify({ text: translatedText }), {\n      headers: { 'Content-Type': 'application/json' },\n    });\n  } catch (error: any) {\n    console.error('Translation error:', error);\n    return new Response(JSON.stringify({ error: error.message || '翻译失败' }), {\n      status: 500,\n      headers: { 'Content-Type': 'application/json' },\n    });\n  }\n}\n\nasync function translateWithDeepSeekAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY;\n  if (!apiKey) {\n    throw new Error('DeepSeek API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://api.deepseek.com/v1'\n  });\n\n  const response = await openai.chat.completions.create({\n    model: 'deepseek-chat',\n    messages: [\n      {\n        role: 'system',\n        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`\n      },\n      {\n        role: 'user',\n        content: text\n      }\n    ],\n    temperature: 0.3,\n    max_tokens: 2000\n  });\n\n  return response.choices[0].message.content || '';\n}\n\nasync function translateWithQwenAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_QWEN_API_KEY;\n  if (!apiKey) {\n    throw new Error('Qwen API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1'\n  });\n\n  const response = await openai.chat.completions.create({\n    model: 'qwen-max',\n    messages: [\n      {\n        role: 'system',\n        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`\n      },\n      {\n        role: 'user',\n        content: text\n      }\n    ],\n    temperature: 0.3,\n    max_tokens: 2000\n  });\n\n  return response.choices[0].message.content || '';\n}\n\nasync function translateWithZhipuAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_ZHIPU_API_KEY;\n  if (!apiKey) {\n    throw new Error('Zhipu API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://open.bigmodel.cn/api/paas/v4'\n  });\n\n  const response = await openai.chat.completions.create({\n    model: 'glm-4',\n    messages: [\n      {\n        role: 'system',\n        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`\n      },\n      {\n        role: 'user',\n        content: text\n      }\n    ],\n    temperature: 0.3,\n    max_tokens: 2000\n  });\n\n  return response.choices[0].message.content || '';\n}\n\nasync function translateWith4oMiniAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_OPENAI_API_KEY;\n  if (!apiKey) {\n    throw new Error('OpenAI API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://api.openai.com/v1'\n  });\n\n  const response = await openai.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [\n      {\n        role: 'system',\n        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`\n      },\n      {\n        role: 'user',\n        content: text\n      }\n    ],\n    temperature: 0.3,\n    max_tokens: 2000\n  });\n\n  return response.choices[0].message.content || '';\n}\n\nasync function translateWithHunyuanAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_TENCENT_API_KEY;\n  if (!apiKey) {\n    throw new Error('Tencent API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://api.hunyuan.cloud.tencent.com/v1'\n  });\n\n  const response = await openai.chat.completions.create({\n    model: 'hunyuan-turbo',\n    messages: [\n      {\n        role: 'system',\n        content: `你是一个专业的翻译助手，请直接翻译文本，不要添加任何解释。`\n      },\n      {\n        role: 'user',\n        content: `将以下文本翻译成${targetLanguage}：\\n\\n${text}`\n      }\n    ],\n    temperature: 0.1,\n    top_p: 0.7,\n    // @ts-expect-error key is not yet public\n    enable_enhancement: true\n  });\n\n  return response.choices[0].message.content || '';\n}\n\nasync function translateWithMinniMaxAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_MINNIMAX_API_KEY;\n  if (!apiKey) {\n    throw new Error('MinniMax API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://api.minimax.chat/v1',\n  });\n\n  try {\n    const completion = await openai.chat.completions.create({\n      model: 'abab6.5s-chat',\n      messages: [\n        {\n          role: 'system',\n          content: 'You are a professional translator. Translate the text directly without any explanations.'\n        },\n        {\n          role: 'user',\n          content: `Translate to ${targetLanguage}:\\n${text}`\n        }\n      ],\n      temperature: 0.3,\n      max_tokens: 2000,\n    });\n\n    return completion.choices[0].message.content;\n  } catch (error: any) {\n    console.error('MinniMax translation error:', error);\n    throw new Error(error.message || '翻译失败');\n  }\n}\n\nasync function translateWithSiliconFlowAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_SILICONFLOW_API_KEY;\n  if (!apiKey) {\n    throw new Error('SiliconFlow API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://api.siliconflow.com/v1'\n  });\n\n  try {\n    const completion = await openai.chat.completions.create({\n      model: 'meta-llama/Llama-3.3-70B-Instruct',\n      messages: [\n        {\n          role: 'system',\n          content: 'You are a professional translator. Translate the text directly without any explanations.'\n        },\n        {\n          role: 'user',\n          content: `Translate to ${targetLanguage}:\\n${text}`\n        }\n      ],\n      temperature: 0.3,\n      max_tokens: 2000,\n    });\n\n    return completion.choices[0].message.content;\n  } catch (error: any) {\n    console.error('SiliconFlow translation error:', error);\n    throw new Error(error.message || '翻译失败');\n  }\n}\n\nasync function translateWithClaudeAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_OPENROUTER_API_KEY;\n  if (!apiKey) {\n    throw new Error('OpenRouter API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://openrouter.ai/api/v1'\n  });\n\n  const completion = await openai.chat.completions.create({\n    model: 'anthropic/claude-3-haiku',\n    messages: [\n      {\n        role: 'system',\n        content: 'You are a professional translator. Translate the text directly without any explanations.'\n      },\n      {\n        role: 'user',\n        content: `Translate to ${targetLanguage}:\\n${text}`\n      }\n    ],\n    temperature: 0.3,\n    max_tokens: 2000,\n  });\n\n  return completion.choices[0].message.content || '';\n}\n\nasync function translateWithStepAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_STEP_API_KEY;\n  if (!apiKey) {\n    throw new Error('Step API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://api.stepfun.com/v1'\n  });\n\n  const response = await openai.chat.completions.create({\n    model: 'step-2-16k',\n    messages: [\n      {\n        role: 'system',\n        content: `You are a professional translator. Translate the following text to ${targetLanguage}. Only return the translated text, no explanations.`\n      },\n      {\n        role: 'user',\n        content: text\n      }\n    ],\n    temperature: 0.3,\n    max_tokens: 2000\n  });\n\n  return response.choices[0].message.content || '';\n} "
  },
  {
    "path": "app/api/translate/siliconflow/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport OpenAI from 'openai';\n\nexport async function POST(request: Request) {\n  try {\n    const { text, targetLanguage } = await request.json();\n\n    if (!text || !targetLanguage) {\n      return NextResponse.json(\n        { error: '缺少必要参数' },\n        { status: 400 }\n      );\n    }\n\n    const apiKey = process.env.NEXT_PUBLIC_SILICONFLOW_API_KEY;\n    if (!apiKey) {\n      return NextResponse.json(\n        { error: 'SiliconFlow API key not found' },\n        { status: 500 }\n      );\n    }\n\n    const openai = new OpenAI({\n      apiKey: apiKey,\n      baseURL: 'https://api.siliconflow.com/v1'\n    });\n\n    const completion = await openai.chat.completions.create({\n      model: 'meta-llama/Llama-3.3-70B-Instruct',\n      messages: [\n        {\n          role: 'system',\n          content: 'You are a professional translator. Translate the text directly without any explanations.'\n        },\n        {\n          role: 'user',\n          content: `Translate to ${targetLanguage}:\\n${text}`\n        }\n      ],\n      temperature: 0.3,\n      max_tokens: 2000,\n    });\n\n    const translatedText = completion.choices[0].message.content;\n    return NextResponse.json({ text: translatedText });\n\n  } catch (error: any) {\n    console.error('SiliconFlow translation error:', error);\n    return NextResponse.json(\n      { error: error.message || '翻译失败' },\n      { status: 500 }\n    );\n  }\n} "
  },
  {
    "path": "app/api/translate/step/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport OpenAI from 'openai'\n\nexport async function POST(req: Request) {\n  try {\n    const apiKey = process.env.NEXT_PUBLIC_STEP_API_KEY;\n    if (!apiKey) {\n      return new NextResponse('API key not configured', { status: 500 })\n    }\n\n    const openai = new OpenAI({\n      apiKey: apiKey,\n      baseURL: 'https://api.stepfun.com/v1'\n    })\n\n    const { text, targetLanguage } = await req.json()\n\n    if (!text) {\n      return new NextResponse('Text is required', { status: 400 })\n    }\n\n    if (!targetLanguage) {\n      return new NextResponse('Target language is required', { status: 400 })\n    }\n\n    const completion = await openai.chat.completions.create({\n      model: 'step-2-16k',\n      messages: [\n        {\n          role: 'system',\n          content: `You are a professional translator. Translate the following text to ${targetLanguage}. Keep the original format and style.`\n        },\n        {\n          role: 'user',\n          content: text\n        }\n      ],\n      temperature: 0.3,\n      max_tokens: 2000\n    })\n\n    const translation = completion.choices[0]?.message?.content || ''\n    if (!translation) {\n      return new NextResponse('No translation result', { status: 500 })\n    }\n\n    return NextResponse.json({ translation })\n  } catch (error: any) {\n    console.error('Translation error:', error)\n    return new NextResponse(error.message || 'Internal Server Error', {\n      status: error.status || 500,\n    })\n  }\n} "
  },
  {
    "path": "app/api/upload/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { put } from '@vercel/blob'\n\nexport async function POST(request: Request) {\n  try {\n    const { file, type } = await request.json()\n\n    if (!file) {\n      return NextResponse.json(\n        { error: '未提供文件' },\n        { status: 400 }\n      )\n    }\n\n    // 从 base64 中提取实际的文件数据\n    const base64Data = file.replace(/^data:.*?;base64,/, '')\n    const buffer = Buffer.from(base64Data, 'base64')\n\n    // 生成唯一的文件名\n    const filename = `${Date.now()}-${Math.random().toString(36).substring(7)}.${type === 'video' ? 'mp4' : 'jpg'}`\n\n    // 上传到 Vercel Blob\n    const blob = await put(filename, buffer, {\n      access: 'public',\n      contentType: type === 'video' ? 'video/mp4' : 'image/jpeg'\n    })\n\n    return NextResponse.json({ url: blob.url })\n  } catch (error: any) {\n    console.error('文件上传错误:', error)\n    return NextResponse.json(\n      { error: error.message || '文件上传失败' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/user/info/route.ts",
    "content": "import { getServerSession } from 'next-auth'\nimport { NextResponse } from 'next/server'\nimport { neon } from '@neondatabase/serverless'\nimport { authOptions } from '../../auth/[...nextauth]/auth'\n\ninterface User {\n  id: string;\n  email: string;\n  name: string | null;\n  github_id: string | null;\n  google_id: string | null;\n  stripe_customer_id: string | null;\n  stripe_subscription_id: string | null;\n  stripe_price_id: string | null;\n  stripe_current_period_end: string | null;\n  text_quota: number;\n  image_quota: number;\n  pdf_quota: number;\n  speech_quota: number;\n  video_quota: number;\n  quota_reset_at: string | null;\n  created_at: string;\n  updated_at: string;\n}\n\ntype UsageType = 'text' | 'image' | 'pdf' | 'speech' | 'video';\n\ninterface UsageRecord {\n  type: UsageType;\n  count: string;\n}\n\ninterface UsageInfo {\n  [key: string]: number;\n  text: number;\n  image: number;\n  pdf: number;\n  speech: number;\n  video: number;\n}\n\nexport async function GET() {\n  try {\n    console.log('开始获取用户信息')\n    const session = await getServerSession(authOptions)\n    if (!session?.user?.email) {\n      console.log('未找到用户会话')\n      return new NextResponse('Unauthorized', { status: 401 })\n    }\n\n    console.log('用户邮箱:', session.user.email)\n    const sql = neon(process.env.DATABASE_URL!)\n    \n    // 获取用户基本信息和订阅信息\n    const users = await sql`\n      SELECT \n        id,\n        email,\n        name,\n        github_id,\n        google_id,\n        stripe_customer_id,\n        stripe_subscription_id,\n        stripe_price_id,\n        stripe_current_period_end,\n        text_quota,\n        image_quota,\n        pdf_quota,\n        speech_quota,\n        video_quota,\n        quota_reset_at,\n        created_at,\n        updated_at\n      FROM auth_users \n      WHERE email = ${session.user.email}\n    ` as User[]\n\n    if (!users.length) {\n      console.log('未找到用户记录')\n      return new NextResponse('User not found', { status: 404 })\n    }\n\n    const user = users[0]\n    console.log('查询到的用户信息:', {\n      id: user.id,\n      email: user.email,\n      subscription: {\n        customerId: user.stripe_customer_id,\n        subscriptionId: user.stripe_subscription_id,\n        priceId: user.stripe_price_id,\n        currentPeriodEnd: user.stripe_current_period_end\n      },\n      quotas: {\n        text: user.text_quota,\n        image: user.image_quota,\n        pdf: user.pdf_quota,\n        speech: user.speech_quota,\n        video: user.video_quota\n      }\n    })\n\n    // 检查订阅是否已过期\n    if (user.stripe_current_period_end) {\n      const currentPeriodEnd = new Date(user.stripe_current_period_end).getTime()\n      const now = new Date().getTime()\n      \n      if (now > currentPeriodEnd) {\n        console.log('订阅已过期，重置为试用版')\n        await sql`\n          UPDATE auth_users \n          SET \n            stripe_subscription_id = NULL,\n            stripe_price_id = NULL,\n            stripe_current_period_end = NULL,\n            text_quota = -1,\n            image_quota = 5,\n            pdf_quota = 3,\n            speech_quota = 2,\n            video_quota = 1\n          WHERE id = ${user.id}\n        `\n        \n        user.stripe_subscription_id = null\n        user.stripe_price_id = null\n        user.stripe_current_period_end = null\n        user.text_quota = -1\n        user.image_quota = 5\n        user.pdf_quota = 3\n        user.speech_quota = 2\n        user.video_quota = 1\n      }\n    }\n\n    const today = new Date().toISOString().split('T')[0]\n    console.log('当前日期:', today, '上次配额重置日期:', user.quota_reset_at)\n\n    // 如果配额重置日期不是今天，根据用户的订阅计划重置配额\n    if (user.quota_reset_at !== today) {\n      console.log('重置用户配额，订阅计划:', user.stripe_price_id)\n      \n      let quotaUpdate\n      if (user.stripe_price_id === process.env.NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID) {\n        quotaUpdate = {\n          text_quota: -1,\n          image_quota: 50,\n          pdf_quota: 40,\n          speech_quota: 30,\n          video_quota: 10\n        }\n      } else if (user.stripe_price_id === process.env.NEXT_PUBLIC_STRIPE_YEARLY_PRICE_ID) {\n        quotaUpdate = {\n          text_quota: -1,\n          image_quota: 100,\n          pdf_quota: 80,\n          speech_quota: 60,\n          video_quota: 20\n        }\n      } else {\n        quotaUpdate = {\n          text_quota: -1,\n          image_quota: 5,\n          pdf_quota: 3,\n          speech_quota: 2,\n          video_quota: 1\n        }\n      }\n\n      await sql`\n        UPDATE auth_users\n        SET \n          image_quota = ${quotaUpdate.image_quota},\n          pdf_quota = ${quotaUpdate.pdf_quota},\n          speech_quota = ${quotaUpdate.speech_quota},\n          video_quota = ${quotaUpdate.video_quota},\n          quota_reset_at = ${today}\n        WHERE id = ${user.id}\n      `\n      \n      user.text_quota = quotaUpdate.text_quota\n      user.image_quota = quotaUpdate.image_quota\n      user.pdf_quota = quotaUpdate.pdf_quota\n      user.speech_quota = quotaUpdate.speech_quota\n      user.video_quota = quotaUpdate.video_quota\n      console.log('配额重置完成，新配额:', quotaUpdate)\n    }\n\n    // 获取今日使用次数\n    console.log('开始获取今日使用次数')\n    const usage = await sql`\n      SELECT type, COUNT(*) as count\n      FROM usage_records\n      WHERE user_id = ${user.id}\n        AND DATE(used_at) = CURRENT_DATE\n      GROUP BY type\n    ` as UsageRecord[]\n\n    console.log('今日使用记录:', usage)\n\n    // 构建响应数据\n    const response = {\n      user: {\n        id: user.id,\n        email: user.email,\n        name: user.name,\n        github_id: user.github_id,\n        google_id: user.google_id,\n        created_at: user.created_at,\n        updated_at: user.updated_at\n      },\n      subscription: {\n        stripe_customer_id: user.stripe_customer_id,\n        stripe_subscription_id: user.stripe_subscription_id,\n        stripe_price_id: user.stripe_price_id,\n        stripe_current_period_end: user.stripe_current_period_end\n      },\n      quota: {\n        text_quota: user.text_quota,\n        image_quota: user.image_quota,\n        pdf_quota: user.pdf_quota,\n        speech_quota: user.speech_quota,\n        video_quota: user.video_quota\n      },\n      usage: {\n        text: 0,\n        image: 0,\n        pdf: 0,\n        speech: 0,\n        video: 0\n      } as UsageInfo\n    }\n\n    // 填充使用次数\n    usage.forEach((record) => {\n      response.usage[record.type] = parseInt(record.count)\n    })\n\n    console.log('返回的用户信息:', response)\n    return NextResponse.json(response)\n  } catch (error) {\n    console.error('获取用户信息失败:', error)\n    return new NextResponse('Internal error', { status: 500 })\n  }\n} "
  },
  {
    "path": "app/api/user/update/route.ts",
    "content": "import { getServerSession } from 'next-auth'\nimport { NextResponse } from 'next/server'\nimport { neon } from '@neondatabase/serverless'\nimport { authOptions } from '../../auth/[...nextauth]/auth'\n\nexport async function PUT(req: Request) {\n  try {\n    const session = await getServerSession(authOptions)\n    if (!session?.user?.email) {\n      return new NextResponse('Unauthorized', { status: 401 })\n    }\n\n    const body = await req.json()\n    const { name } = body\n\n    if (typeof name !== 'string' || name.length > 50) {\n      return new NextResponse('Invalid name', { status: 400 })\n    }\n\n    const sql = neon(process.env.DATABASE_URL!)\n    \n    // 更新用户名\n    await sql`\n      UPDATE auth_users\n      SET name = ${name}, updated_at = CURRENT_TIMESTAMP\n      WHERE email = ${session.user.email}\n    `\n\n    return new NextResponse('OK')\n  } catch (error) {\n    console.error('Failed to update user:', error)\n    return new NextResponse('Internal error', { status: 500 })\n  }\n} "
  },
  {
    "path": "app/api/user/usage/route.ts",
    "content": "import { NextResponse } from 'next/server'\nimport { getServerSession } from 'next-auth'\nimport { authOptions } from '@/app/api/auth/[...nextauth]/auth'\nimport { neon } from '@neondatabase/serverless'\n\nconst sql = neon(process.env.DATABASE_URL!)\n\n// 检查配额是否足够\nasync function checkQuota(userId: number, type: string) {\n  const quotaField = `${type}_quota`\n  \n  // 获取用户配额信息\n  const result = await sql`\n    SELECT ${sql(quotaField)}, quota_reset_at\n    FROM auth_users\n    WHERE id = ${userId}\n  `\n  \n  if (result.length === 0) {\n    throw new Error('用户不存在')\n  }\n\n  const user = result[0]\n  const today = new Date().toISOString().split('T')[0]\n\n  // 如果是新的一天，重置配额\n  if (user.quota_reset_at !== today) {\n    const defaultQuotas = {\n      text: -1,\n      image: 10,\n      pdf: 8,\n      speech: 5,\n      video: 2\n    }\n\n    await sql`\n      UPDATE auth_users\n      SET \n        image_quota = ${defaultQuotas.image},\n        pdf_quota = ${defaultQuotas.pdf},\n        speech_quota = ${defaultQuotas.speech},\n        video_quota = ${defaultQuotas.video},\n        quota_reset_at = ${today}\n      WHERE id = ${userId}\n    `\n    \n    return defaultQuotas[type as keyof typeof defaultQuotas]\n  }\n\n  return user[quotaField]\n}\n\n// 获取今日使用次数\nasync function getUsageCount(userId: number, type: string) {\n  const result = await sql`\n    SELECT COUNT(*) as count\n    FROM usage_records\n    WHERE user_id = ${userId}\n      AND type = ${type}\n      AND DATE(used_at) = CURRENT_DATE\n  `\n  return parseInt(result[0].count)\n}\n\n// 记录使用并减少配额\nasync function recordUsage(userId: number, type: string) {\n  console.log('开始记录使用情况:', { userId, type })\n  \n  try {\n    // 开始事务\n    await sql`BEGIN`\n\n    try {\n      // 插入使用记录\n      await sql`\n        INSERT INTO usage_records (user_id, type)\n        VALUES (${userId}, ${type})\n      `\n      console.log('使用记录已插入')\n\n      // 如果不是无限制配额，减少剩余次数\n      if (type !== 'text') {\n        // 根据类型选择不同的更新语句\n        let updateResult;\n        switch (type) {\n          case 'image':\n            updateResult = await sql`\n              UPDATE auth_users \n              SET image_quota = GREATEST(image_quota - 1, 0),\n                  updated_at = CURRENT_TIMESTAMP\n              WHERE id = ${userId}\n              RETURNING image_quota as remaining_quota\n            `\n            break;\n          case 'pdf':\n            updateResult = await sql`\n              UPDATE auth_users \n              SET pdf_quota = GREATEST(pdf_quota - 1, 0),\n                  updated_at = CURRENT_TIMESTAMP\n              WHERE id = ${userId}\n              RETURNING pdf_quota as remaining_quota\n            `\n            break;\n          case 'speech':\n            updateResult = await sql`\n              UPDATE auth_users \n              SET speech_quota = GREATEST(speech_quota - 1, 0),\n                  updated_at = CURRENT_TIMESTAMP\n              WHERE id = ${userId}\n              RETURNING speech_quota as remaining_quota\n            `\n            break;\n          case 'video':\n            updateResult = await sql`\n              UPDATE auth_users \n              SET video_quota = GREATEST(video_quota - 1, 0),\n                  updated_at = CURRENT_TIMESTAMP\n              WHERE id = ${userId}\n              RETURNING video_quota as remaining_quota\n            `\n            break;\n        }\n        console.log('配额已更新，剩余:', updateResult?.[0]?.remaining_quota)\n      }\n\n      // 提交事务\n      await sql`COMMIT`\n      console.log('事务已提交')\n    } catch (error) {\n      // 如果出错，回滚事务\n      await sql`ROLLBACK`\n      console.error('事务回滚:', error)\n      throw error\n    }\n  } catch (error) {\n    console.error('记录使用情况失败:', error)\n    throw error\n  }\n}\n\nexport async function POST(req: Request) {\n  try {\n    const session = await getServerSession(authOptions)\n    console.log('当前会话:', session?.user)\n    \n    if (!session?.user?.email) {\n      return NextResponse.json(\n        { error: '未登录' },\n        { status: 401 }\n      )\n    }\n\n    const { type } = await req.json()\n    \n    if (!type || !['text', 'image', 'pdf', 'speech', 'video'].includes(type)) {\n      return NextResponse.json(\n        { error: '无效的使用类型' },\n        { status: 400 }\n      )\n    }\n\n    // 获取用户ID\n    const users = await sql`\n      SELECT id FROM auth_users WHERE email = ${session.user.email}\n    `\n\n    if (users.length === 0) {\n      return NextResponse.json(\n        { error: '用户不存在' },\n        { status: 404 }\n      )\n    }\n\n    const userId = users[0].id\n\n    // 检查配额\n    const quota = await checkQuota(userId, type)\n    const usageCount = await getUsageCount(userId, type)\n\n    // 如果是无限制配额，直接记录使用\n    if (quota === -1) {\n      await recordUsage(userId, type)\n      return NextResponse.json({\n        success: true,\n        remaining: -1\n      })\n    }\n\n    // 计算剩余次数\n    const remainingQuota = quota - usageCount\n\n    console.log('配额检查:', {\n      type,\n      quota,\n      usageCount,\n      remainingQuota\n    })\n\n    // 如果没有剩余次数，返回错误\n    if (remainingQuota <= 0) {\n      return NextResponse.json(\n        { error: '今日使用次数已达上限' },\n        { status: 403 }\n      )\n    }\n\n    // 记录使用\n    await recordUsage(userId, type)\n\n    // 返回更新后的配额信息\n    console.log('更新后的配额:', {\n      type,\n      quota,\n      usageCount: usageCount + 1,\n      remaining: remainingQuota - 1\n    })\n\n    return NextResponse.json({\n      success: true,\n      remaining: remainingQuota - 1\n    })\n\n  } catch (error: any) {\n    console.error('记录使用情况失败:', error)\n    return NextResponse.json(\n      { error: error.message || '记录使用失败' },\n      { status: 500 }\n    )\n  }\n} "
  },
  {
    "path": "app/api/webhook/route.ts",
    "content": "import { headers } from 'next/headers'\nimport { NextResponse } from 'next/server'\nimport { stripe } from '@/lib/stripe'\nimport { neon } from '@neondatabase/serverless'\n\nconst webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!\n\nexport async function POST(req: Request) {\n  console.log('收到 Stripe Webhook 请求')\n  const body = await req.text()\n  const signature = headers().get('stripe-signature')\n\n  let event\n\n  try {\n    if (!stripe) {\n      console.error('Stripe 未配置')\n      return new NextResponse('Stripe is not configured', { status: 500 })\n    }\n    \n    event = stripe.webhooks.constructEvent(\n      body,\n      signature!,\n      webhookSecret\n    )\n    console.log('Webhook 事件验证成功:', event.type)\n  } catch (err: any) {\n    console.error('Webhook 签名验证失败:', err)\n    return new NextResponse(`Webhook Error: ${err.message}`, { status: 400 })\n  }\n\n  const sql = neon(process.env.DATABASE_URL!)\n\n  try {\n    switch (event.type) {\n      case 'customer.subscription.created':\n      case 'customer.subscription.updated': {\n        const subscription = event.data.object\n        const userId = subscription.metadata.userId\n        const priceId = subscription.items.data[0].price.id\n\n        console.log('处理订阅事件:', {\n          type: event.type,\n          userId,\n          priceId,\n          customerId: subscription.customer,\n          subscriptionId: subscription.id,\n          currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString()\n        })\n\n        // 根据价格ID设置对应的配额\n        const quotaUpdate = priceId === process.env.NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID ? {\n          text_quota: -1,\n          image_quota: 50,\n          pdf_quota: 40,\n          speech_quota: 30,\n          video_quota: 10\n        } : priceId === process.env.NEXT_PUBLIC_STRIPE_YEARLY_PRICE_ID ? {\n          text_quota: -1,\n          image_quota: 100,\n          pdf_quota: 80,\n          speech_quota: 60,\n          video_quota: 20\n        } : {\n          text_quota: -1,\n          image_quota: 10,\n          pdf_quota: 8,\n          speech_quota: 5,\n          video_quota: 2\n        }\n\n        console.log('执行数据库更新:', {\n          customerId: subscription.customer,\n          subscriptionId: subscription.id,\n          priceId: subscription.items.data[0].price.id,\n          currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(),\n          quotaUpdate\n        })\n\n        // 更新用户订阅状态和配额\n        const updateResult = await sql`\n          UPDATE auth_users \n          SET \n            stripe_customer_id = ${subscription.customer},\n            stripe_subscription_id = ${subscription.id},\n            stripe_price_id = ${subscription.items.data[0].price.id},\n            stripe_current_period_end = to_timestamp(${subscription.current_period_end}),\n            text_quota = ${quotaUpdate.text_quota},\n            image_quota = ${quotaUpdate.image_quota},\n            pdf_quota = ${quotaUpdate.pdf_quota},\n            speech_quota = ${quotaUpdate.speech_quota},\n            video_quota = ${quotaUpdate.video_quota}\n          WHERE id = ${userId}\n          RETURNING *\n        `\n        console.log('数据库更新结果:', updateResult[0])\n        break\n      }\n\n      case 'customer.subscription.deleted': {\n        const subscription = event.data.object\n        const userId = subscription.metadata.userId\n\n        // 重置用户为免费计划\n        await sql`\n          UPDATE auth_users \n          SET \n            stripe_subscription_id = NULL,\n            stripe_price_id = NULL,\n            stripe_current_period_end = NULL,\n            text_quota = -1,\n            image_quota = 5,\n            pdf_quota = 3,\n            speech_quota = 2,\n            video_quota = 1\n          WHERE id = ${userId}\n        `\n        break\n      }\n\n      case 'invoice.payment_succeeded': {\n        const invoice = event.data.object\n        const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)\n        const userId = subscription.metadata.userId\n\n        // 更新发票支付状态\n        await sql`\n          INSERT INTO payment_history (\n            user_id,\n            stripe_invoice_id,\n            amount,\n            status,\n            payment_date\n          ) VALUES (\n            ${userId},\n            ${invoice.id},\n            ${invoice.amount_paid},\n            'succeeded',\n            to_timestamp(${invoice.created})\n          )\n        `\n        break\n      }\n\n      case 'invoice.payment_failed': {\n        const invoice = event.data.object\n        const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string)\n        const userId = subscription.metadata.userId\n\n        // 记录支付失败\n        await sql`\n          INSERT INTO payment_history (\n            user_id,\n            stripe_invoice_id,\n            amount,\n            status,\n            payment_date\n          ) VALUES (\n            ${userId},\n            ${invoice.id},\n            ${invoice.amount_due},\n            'failed',\n            to_timestamp(${invoice.created})\n          )\n        `\n\n        // 可以在这里添加通知用户的逻辑\n        break\n      }\n    }\n\n    return new NextResponse(null, { status: 200 })\n  } catch (error: any) {\n    console.error('Webhook handler failed:', error)\n    return new NextResponse('Webhook handler failed', { status: 500 })\n  }\n} "
  },
  {
    "path": "app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --foreground-rgb: 0, 0, 0;\n  --background-start-rgb: 214, 219, 220;\n  --background-end-rgb: 255, 255, 255;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --foreground-rgb: 255, 255, 255;\n    --background-start-rgb: 0, 0, 0;\n    --background-end-rgb: 0, 0, 0;\n  }\n}\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n    \n    /* 更新为 #8cc63f 绿色 */\n    --primary: 84 68% 51%;\n    --primary-foreground: 210 40% 98%;\n    \n    /* 更新为匹配绿色的渐变色 */\n    --secondary: 100 65% 45%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n    \n    /* 更新为匹配绿色的渐变色 */\n    --accent: 70 70% 50%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n    \n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n    --chart-1: 84 68% 51%;\n    --chart-2: 70 70% 50%;\n    --chart-3: 100 65% 45%;\n    --chart-4: 120 60% 45%;\n    --chart-5: 140 65% 40%;\n    --radius: 0.5rem;\n  }\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n    \n    /* 更新为 #8cc63f 绿色 */\n    --primary: 84 68% 51%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n    \n    /* 更新为匹配绿色的渐变色 */\n    --secondary: 100 65% 45%;\n    --secondary-foreground: 210 40% 98%;\n    \n    /* 更新为匹配绿色的渐变色 */\n    --accent: 70 70% 50%;\n    --accent-foreground: 210 40% 98%;\n    \n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n    --chart-1: 84 68% 51%;\n    --chart-2: 70 70% 50%;\n    --chart-3: 100 65% 45%;\n    --chart-4: 120 60% 45%;\n    --chart-5: 140 65% 40%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n.animate-fadeIn {\n  animation: fadeIn 0.5s ease-in forwards;\n}\n\n/* Swiper 自定义样式 */\n.testimonials-swiper {\n  padding-bottom: 3rem !important;\n}\n\n.testimonials-swiper .swiper-pagination-bullet {\n  width: 10px;\n  height: 10px;\n  background: hsl(var(--muted-foreground));\n  opacity: 0.5;\n}\n\n.testimonials-swiper .swiper-pagination-bullet-active {\n  background: hsl(var(--primary));\n  opacity: 1;\n}\n\n.testimonials-swiper .swiper-button-next,\n.testimonials-swiper .swiper-button-prev {\n  color: hsl(var(--primary));\n  transition: all 0.2s;\n}\n\n.testimonials-swiper .swiper-button-next:hover,\n.testimonials-swiper .swiper-button-prev:hover {\n  transform: scale(1.1);\n}\n\n.testimonials-swiper .swiper-button-next::after,\n.testimonials-swiper .swiper-button-prev::after {\n  font-size: 1.5rem;\n  font-weight: bold;\n}\n\n/* 暗色模式样式 */\n.dark .testimonials-swiper .swiper-pagination-bullet {\n  background: hsl(var(--muted-foreground));\n}\n\n.dark .testimonials-swiper .swiper-pagination-bullet-active {\n  background: hsl(var(--primary));\n}\n\n.dark .testimonials-swiper .swiper-button-next,\n.dark .testimonials-swiper .swiper-button-prev {\n  color: hsl(var(--primary));\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import './globals.css';\nimport { Inter } from 'next/font/google';\nimport { Providers } from \"./providers\";\nimport GoogleAnalytics from '@/components/google-analytics';\nimport { LanguageProvider } from \"@/components/language-provider\";\nimport type { Metadata, Viewport } from 'next';\n\nconst inter = Inter({ subsets: ['latin'] });\n\nexport const viewport: Viewport = {\n  width: 'device-width',\n  initialScale: 1,\n};\n\nexport const metadata: Metadata = {\n  metadataBase: new URL('https://aitranslate.site'),\n  title: {\n    template: '%s | AI Translation Assistant',\n    default: 'AI Translation Assistant - Smart Multilingual Translation Platform',\n  },\n  description: 'All-in-one intelligent translation solution supporting text, image, PDF, speech, and video translation, making cross-language communication simpler.',\n  keywords: ['AI Translation', 'Multilingual Translation', 'Image Translation', 'PDF Translation', 'Speech Translation', 'Video Translation', 'Machine Translation'],\n  authors: [{ name: 'AI Translation Assistant Team' }],\n  creator: 'AI Translation Assistant Team',\n  publisher: 'AI Translation Assistant',\n  icons: {\n    icon: [\n      { url: '/favicon.ico', sizes: 'any' },\n      { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },\n      { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },\n    ],\n    apple: [\n      { url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },\n    ],\n  },\n  manifest: '/site.webmanifest',\n  robots: {\n    index: true,\n    follow: true,\n  },\n  openGraph: {\n    type: 'website',\n    locale: 'en_US',\n    url: '/',\n    title: 'AI Translation Assistant - Smart Multilingual Translation Platform',\n    description: 'All-in-one intelligent translation solution supporting text, image, PDF, speech, and video translation, making cross-language communication simpler.',\n    siteName: 'AI Translation Assistant',\n    images: [\n      {\n        url: '/og-image.png',\n        width: 1200,\n        height: 630,\n        alt: 'AI Translation Assistant',\n      },\n    ],\n  },\n  twitter: {\n    card: 'summary_large_image',\n    title: 'AI Translation Assistant - Smart Multilingual Translation Platform',\n    description: 'All-in-one intelligent translation solution supporting text, image, PDF, speech, and video translation, making cross-language communication simpler.',\n    images: ['/og-image.png'],\n  },\n  verification: {\n    google: 'yLQ9THm_U56rW0n0VsGzM6IXvWmlbS3fV7NGl-SZT3k',\n  },\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"zh\" suppressHydrationWarning>\n      <head>\n        <link rel=\"manifest\" href=\"/site.webmanifest\" />\n        <link rel=\"icon\" href=\"/favicon.ico\" sizes=\"any\" />\n        <link rel=\"icon\" href=\"/favicon-16x16.png\" type=\"image/png\" sizes=\"16x16\" />\n        <link rel=\"icon\" href=\"/favicon-32x32.png\" type=\"image/png\" sizes=\"32x32\" />\n        <link rel=\"apple-touch-icon\" href=\"/apple-touch-icon.png\" />\n      </head>\n      <body className={inter.className}>\n        <LanguageProvider>\n          <Providers>\n            <GoogleAnalytics />\n            {children}\n          </Providers>\n        </LanguageProvider>\n      </body>\n    </html>\n  );\n}"
  },
  {
    "path": "app/login/error.tsx",
    "content": "'use client';\n\nimport { useSearchParams } from 'next/navigation';\nimport { useEffect } from 'react';\nimport { toast } from 'sonner';\nimport { useI18n } from '@/lib/i18n/use-translations';\n\nexport default function ErrorPage() {\n  const { t } = useI18n();\n  const searchParams = useSearchParams();\n  const error = searchParams.get('error');\n\n  useEffect(() => {\n    if (error === 'AccessDenied') {\n      toast.error(t('auth.error.accessDenied'));\n    } else if (error) {\n      toast.error(t('auth.error.default'));\n    }\n  }, [error, t]);\n\n  return null;\n} "
  },
  {
    "path": "app/login/layout.tsx",
    "content": "import { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  title: 'Sign in',\n  description: 'Sign in to your account',\n}\n\nexport default function LoginLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return children\n} "
  },
  {
    "path": "app/login/page.tsx",
    "content": "\"use client\";\n\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useState, useEffect } from 'react'\nimport { signIn, useSession } from 'next-auth/react'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { toast } from 'sonner'\nimport { useI18n } from '@/lib/i18n/use-translations'\nimport { Github } from 'lucide-react'\nimport { Separator } from '@/components/ui/separator'\n\nexport default function LoginPage() {\n  const { t } = useI18n()\n  const router = useRouter()\n  const searchParams = useSearchParams()\n  const { data: session } = useSession()\n  const [loading, setLoading] = useState(false)\n  const [email, setEmail] = useState('')\n  const [password, setPassword] = useState('')\n\n  const callbackUrl = searchParams.get('returnUrl') || searchParams.get('callbackUrl') || '/'\n\n  useEffect(() => {\n    if (session) {\n      router.push(callbackUrl)\n    }\n  }, [session, router, callbackUrl])\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault()\n    setLoading(true)\n\n    try {\n      if (!email || !password) {\n        throw new Error(t('auth.login.error.required'))\n      }\n\n      console.log('Attempting to sign in with credentials, callbackUrl:', callbackUrl)\n      \n      const result = await signIn('credentials', {\n        email,\n        password,\n        redirect: true,\n        callbackUrl,\n      })\n\n      // 由于设置了 redirect: true，下面的代码不会执行\n      // 登录成功后会自动重定向到 callbackUrl\n      console.log('Sign in result:', result)\n    } catch (error: any) {\n      console.error('Sign in error:', error)\n      toast.error(error.message || t('auth.signIn.error'))\n    } finally {\n      setLoading(false)\n    }\n  }\n\n  const handleGitHubLogin = async () => {\n    setLoading(true)\n    try {\n      console.log('Attempting GitHub login, callbackUrl:', callbackUrl)\n      await signIn('github', { \n        callbackUrl,\n        redirect: true\n      })\n    } catch (error) {\n      console.error('GitHub login error:', error)\n      toast.error(t('auth.signIn.error'))\n    }\n  }\n\n  const handleGoogleLogin = async () => {\n    setLoading(true)\n    try {\n      console.log('Attempting Google login, callbackUrl:', callbackUrl)\n      await signIn('google', { \n        callbackUrl,\n        redirect: true\n      })\n    } catch (error) {\n      console.error('Google login error:', error)\n      toast.error(t('auth.signIn.error'))\n    }\n  }\n\n  if (session) {\n    return null\n  }\n\n  return (\n    <div className=\"min-h-screen flex items-center justify-center bg-background\">\n      <div className=\"mx-auto max-w-[350px] space-y-6\">\n        <div className=\"space-y-2 text-center\">\n          <h1 className=\"text-2xl font-bold\" suppressHydrationWarning>\n            {t('auth.login.title')}\n          </h1>\n          <p className=\"text-sm text-muted-foreground\" suppressHydrationWarning>\n            {t('auth.login.subtitle')}\n          </p>\n        </div>\n        <div className=\"grid grid-cols-1 gap-2\">\n          <Button \n            variant=\"outline\" \n            className=\"w-full\" \n            onClick={handleGitHubLogin}\n            disabled={loading}\n          >\n            <Github className=\"mr-2 h-4 w-4\" />\n            <span suppressHydrationWarning>{t('auth.login.github')}</span>\n          </Button>\n          <Button \n            variant=\"outline\" \n            className=\"w-full\" \n            onClick={handleGoogleLogin}\n            disabled={loading}\n          >\n            <svg className=\"mr-2 h-4 w-4\" aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fab\" data-icon=\"google\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 488 512\">\n              <path fill=\"currentColor\" d=\"M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z\"></path>\n            </svg>\n            <span suppressHydrationWarning>{t('auth.login.google')}</span>\n          </Button>\n        </div>\n        <div className=\"relative\">\n          <div className=\"absolute inset-0 flex items-center\">\n            <Separator className=\"w-full\" />\n          </div>\n          <div className=\"relative flex justify-center text-xs uppercase\">\n            <span className=\"bg-background px-2 text-muted-foreground\" suppressHydrationWarning>\n              {t('auth.login.or')}\n            </span>\n          </div>\n        </div>\n        <form onSubmit={handleSubmit} className=\"space-y-4\">\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"email\" suppressHydrationWarning>\n              {t('auth.login.email')}\n            </Label>\n            <Input\n              id=\"email\"\n              placeholder={t('auth.login.emailPlaceholder')}\n              required\n              type=\"email\"\n              value={email}\n              onChange={(e) => setEmail(e.target.value)}\n              disabled={loading}\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"password\" suppressHydrationWarning>\n              {t('auth.login.password')}\n            </Label>\n            <Input\n              id=\"password\"\n              placeholder={t('auth.login.passwordPlaceholder')}\n              required\n              type=\"password\"\n              value={password}\n              onChange={(e) => setPassword(e.target.value)}\n              disabled={loading}\n            />\n          </div>\n          <Button className=\"w-full\" type=\"submit\" disabled={loading}>\n            <span suppressHydrationWarning>\n              {loading ? t('auth.login.loading') : t('auth.login.button')}\n            </span>\n          </Button>\n        </form>\n        <div className=\"text-center text-sm\">\n          <span suppressHydrationWarning>{t('auth.login.noAccount')}</span>{' '}\n          <Link className=\"underline\" href=\"/register\">\n            <span suppressHydrationWarning>{t('auth.signUp')}</span>\n          </Link>\n        </div>\n      </div>\n    </div>\n  )\n} "
  },
  {
    "path": "app/page.tsx",
    "content": "\"use client\"\n\nimport Link from 'next/link'\nimport { useI18n } from '@/lib/i18n/use-translations'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { cn } from '@/lib/utils'\nimport { motion } from 'framer-motion'\nimport { Swiper, SwiperSlide } from 'swiper/react'\nimport { Autoplay, Navigation, Pagination } from 'swiper/modules'\nimport 'swiper/css'\nimport 'swiper/css/navigation'\nimport 'swiper/css/pagination'\nimport { \n  Languages, \n  Image, \n  FileText, \n  Mic, \n  Video, \n  Moon, \n  Lock, \n  Crown, \n  Globe2, \n  Chrome, \n  MonitorSmartphone, \n  ArrowRight, \n  Sparkles \n} from 'lucide-react'\n\nexport default function Home() {\n  const { t } = useI18n()\n\n  const container = {\n    hidden: { opacity: 0 },\n    show: {\n      opacity: 1,\n      transition: {\n        staggerChildren: 0.1\n      }\n    }\n  }\n\n  const item = {\n    hidden: { opacity: 0, y: 20 },\n    show: { opacity: 1, y: 0 }\n  }\n\n  const features = [\n    {\n      icon: <Languages className=\"h-8 w-8\" />,\n      title: t('landing.features.text.title'),\n      description: t('landing.features.text.description'),\n    },\n    {\n      icon: <Image className=\"h-8 w-8\" />,\n      title: t('landing.features.image.title'),\n      description: t('landing.features.image.description'),\n    },\n    {\n      icon: <FileText className=\"h-8 w-8\" />,\n      title: t('landing.features.pdf.title'),\n      description: t('landing.features.pdf.description'),\n    },\n    {\n      icon: <Mic className=\"h-8 w-8\" />,\n      title: t('landing.features.speech.title'),\n      description: t('landing.features.speech.description'),\n    },\n    {\n      icon: <Video className=\"h-8 w-8\" />,\n      title: t('landing.features.video.title'),\n      description: t('landing.features.video.description'),\n    },\n  ]\n\n  const highlights = [\n    {\n      icon: <Globe2 className=\"h-6 w-6\" />,\n      title: t('landing.highlights.multilingual.title'),\n      description: t('landing.highlights.multilingual.description'),\n    },\n    {\n      icon: <Moon className=\"h-6 w-6\" />,\n      title: t('landing.highlights.theme.title'),\n      description: t('landing.highlights.theme.description'),\n    },\n    {\n      icon: <Lock className=\"h-6 w-6\" />,\n      title: t('landing.highlights.privacy.title'),\n      description: t('landing.highlights.privacy.description'),\n    },\n    {\n      icon: <Chrome className=\"h-6 w-6\" />,\n      title: t('landing.highlights.web.title'),\n      description: t('landing.highlights.web.description'),\n    },\n  ]\n\n  const steps = [\n    {\n      number: \"01\",\n      title: t('landing.steps.select.title'),\n      description: t('landing.steps.select.description'),\n    },\n    {\n      number: \"02\",\n      title: t('landing.steps.upload.title'),\n      description: t('landing.steps.upload.description'),\n    },\n    {\n      number: \"03\",\n      title: t('landing.steps.translate.title'),\n      description: t('landing.steps.translate.description'),\n    },\n  ]\n\n  const testimonials = [\n    {\n      quote: t('landing.testimonials.1.quote'),\n      author: t('landing.testimonials.1.author'),\n      role: t('landing.testimonials.1.role'),\n      rating: 5\n    },\n    {\n      quote: t('landing.testimonials.2.quote'),\n      author: t('landing.testimonials.2.author'),\n      role: t('landing.testimonials.2.role'),\n      rating: 5\n    },\n    {\n      quote: t('landing.testimonials.3.quote'),\n      author: t('landing.testimonials.3.author'),\n      role: t('landing.testimonials.3.role'),\n      rating: 5\n    },\n    {\n      quote: t('landing.testimonials.4.quote'),\n      author: t('landing.testimonials.4.author'),\n      role: t('landing.testimonials.4.role'),\n      rating: 5\n    },\n    {\n      quote: t('landing.testimonials.5.quote'),\n      author: t('landing.testimonials.5.author'),\n      role: t('landing.testimonials.5.role'),\n      rating: 5\n    },\n    {\n      quote: t('landing.testimonials.6.quote'),\n      author: t('landing.testimonials.6.author'),\n      role: t('landing.testimonials.6.role'),\n      rating: 5\n    }\n  ]\n\n  return (\n    <div className=\"flex flex-col items-center\">\n      {/* Hero Section */}\n      <section className=\"relative w-full py-12 md:py-16 lg:py-20 bg-background overflow-hidden\">\n        <div className=\"absolute inset-0\">\n          <div className=\"absolute inset-0 bg-grid-slate-400/[0.05] bg-[size:32px_32px] dark:bg-grid-slate-600/[0.05]\" />\n          <div className=\"absolute inset-0 flex items-center justify-center bg-background [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]\" />\n        </div>\n        <div className=\"absolute inset-0 bg-gradient-to-t from-background to-transparent\" />\n        <motion.div \n          className=\"container relative px-4 md:px-6\"\n          initial={{ opacity: 0, y: 20 }}\n          animate={{ opacity: 1, y: 0 }}\n          transition={{ duration: 0.5 }}\n        >\n          <div className=\"flex flex-col items-center space-y-6 text-center\">\n            <motion.div \n              className=\"space-y-4\"\n              initial={{ opacity: 0, scale: 0.9 }}\n              animate={{ opacity: 1, scale: 1 }}\n              transition={{ duration: 0.5, delay: 0.2 }}\n            >\n              <Badge variant=\"outline\" className=\"px-6 py-2 text-base relative overflow-hidden group mb-4\">\n                <span className=\"relative z-10\" suppressHydrationWarning>{t('landing.hero.badge')}</span>\n                <motion.div\n                  className=\"absolute inset-0 bg-primary/10\"\n                  initial={{ x: '-100%' }}\n                  animate={{ x: '100%' }}\n                  transition={{ duration: 1.5, repeat: Infinity, ease: 'linear' }}\n                />\n              </Badge>\n              <h1 className=\"text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl lg:text-7xl/none mb-6\">\n                <span className=\"inline-block bg-clip-text text-transparent bg-gradient-to-r from-primary via-primary/50 to-primary bg-[200%_auto] animate-gradient\" suppressHydrationWarning>\n                  {t('landing.hero.appTitle')}\n                </span>\n              </h1>\n              <p className=\"mx-auto max-w-[800px] text-gray-500 md:text-xl lg:text-2xl dark:text-gray-400 leading-relaxed\" suppressHydrationWarning>\n                {t('landing.hero.subtitle')}\n              </p>\n            </motion.div>\n            <motion.div \n              className=\"flex flex-col sm:flex-row gap-4 mt-8\"\n              initial={{ opacity: 0, y: 20 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{ duration: 0.5, delay: 0.4 }}\n            >\n              <Link href=\"/translate\" className=\"w-full sm:w-auto\">\n                <Button size=\"lg\" variant=\"outline\" className=\"w-full group relative px-8 py-6 text-lg font-medium overflow-hidden\">\n                  <div className=\"absolute inset-0 bg-gradient-to-r from-primary/10 to-primary/5 opacity-0 group-hover:opacity-100 transition-opacity\" />\n                  <span className=\"relative flex items-center justify-center\">\n                    <Languages className=\"mr-3 h-6 w-6 transition-transform group-hover:scale-110\" />\n                    <span suppressHydrationWarning>{t('landing.hero.cta')}</span>\n                    <ArrowRight className=\"ml-3 h-6 w-6 transition-transform group-hover:translate-x-1\" />\n                  </span>\n                </Button>\n              </Link>\n              <Link href=\"/pricing\" className=\"w-full sm:w-auto\">\n                <Button size=\"lg\" className=\"w-full group relative px-8 py-6 text-lg font-medium overflow-hidden\">\n                  <div className=\"absolute inset-0 bg-gradient-to-r from-primary/50 to-primary/10 opacity-0 group-hover:opacity-100 transition-opacity\" />\n                  <span className=\"relative flex items-center justify-center\">\n                    <Crown className=\"mr-3 h-6 w-6 transition-transform group-hover:scale-110 text-yellow-500\" />\n                    <span suppressHydrationWarning>{t('landing.hero.subscribe')}</span>\n                    <ArrowRight className=\"ml-3 h-6 w-6 transition-transform group-hover:translate-x-1\" />\n                    <Sparkles className=\"absolute top-0 right-0 h-4 w-4 text-yellow-400 animate-pulse\" />\n                  </span>\n                </Button>\n              </Link>\n            </motion.div>\n          </div>\n        </motion.div>\n      </section>\n\n      {/* Features Section */}\n      <motion.section \n        className=\"w-full py-12 bg-primary/5\"\n        variants={container}\n        initial=\"hidden\"\n        whileInView=\"show\"\n        viewport={{ once: true }}\n      >\n        <div className=\"container px-4 md:px-6\">\n          <motion.div \n            className=\"text-center mb-8\"\n            variants={item}\n          >\n            <div className=\"inline-flex items-center space-x-2 mb-4\">\n              <div className=\"h-px w-8 bg-primary/60\" />\n              <h2 className=\"text-2xl font-bold\" suppressHydrationWarning>{t('landing.features.title')}</h2>\n              <div className=\"h-px w-8 bg-primary/60\" />\n            </div>\n            <p className=\"text-gray-500 dark:text-gray-400 max-w-[600px] mx-auto text-sm\" suppressHydrationWarning>\n              {t('landing.features.subtitle')}\n            </p>\n          </motion.div>\n          <div className=\"grid gap-4 lg:grid-cols-5 md:grid-cols-2\">\n            {features.map((feature, i) => (\n              <motion.div key={i} variants={item}>\n                <Card className=\"relative overflow-hidden group hover:shadow-lg transition-all duration-300 bg-card/50 backdrop-blur-sm border-primary/10 h-[240px] flex flex-col\">\n                  <CardContent className=\"p-6 flex flex-col flex-1\">\n                    <div className=\"mb-4 rounded-full w-12 h-12 flex items-center justify-center bg-primary/10 group-hover:bg-primary/20 transition-colors relative\">\n                      <div className=\"absolute inset-0 bg-gradient-to-r from-primary/20 to-primary/0 animate-spin-slow\" />\n                      {feature.icon}\n                    </div>\n                    <h3 className=\"text-lg font-bold mb-2 group-hover:text-primary transition-colors\" suppressHydrationWarning>\n                      {feature.title}\n                    </h3>\n                    <p className=\"text-gray-500 dark:text-gray-400 text-sm flex-1\" suppressHydrationWarning>\n                      {feature.description}\n                    </p>\n                  </CardContent>\n                  <div className=\"absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-transparent via-primary/30 to-transparent transform scale-x-0 group-hover:scale-x-100 transition-transform\" />\n                </Card>\n              </motion.div>\n            ))}\n          </div>\n        </div>\n      </motion.section>\n\n      {/* Highlights Section */}\n      <motion.section \n        className=\"w-full py-12\"\n        variants={container}\n        initial=\"hidden\"\n        whileInView=\"show\"\n        viewport={{ once: true }}\n      >\n        <div className=\"container px-4 md:px-6\">\n          <motion.div \n            className=\"text-center mb-8\"\n            variants={item}\n          >\n            <div className=\"inline-flex items-center space-x-2 mb-4\">\n              <div className=\"h-px w-8 bg-primary/60\" />\n              <h2 className=\"text-2xl font-bold\" suppressHydrationWarning>{t('landing.highlights.title')}</h2>\n              <div className=\"h-px w-8 bg-primary/60\" />\n            </div>\n            <p className=\"text-gray-500 dark:text-gray-400 max-w-[600px] mx-auto text-sm\" suppressHydrationWarning>\n              {t('landing.highlights.subtitle')}\n            </p>\n          </motion.div>\n          <div className=\"grid gap-6 lg:grid-cols-4 md:grid-cols-2\">\n            {highlights.map((highlight, i) => (\n              <motion.div \n                key={i} \n                variants={item}\n                className=\"group\"\n              >\n                <div className=\"relative p-6 bg-background/50 backdrop-blur-sm rounded-lg border border-primary/10 hover:border-primary/30 transition-colors\">\n                  <div className=\"absolute inset-0 bg-gradient-to-br from-primary/5 to-primary/0 rounded-lg\" />\n                  <div className=\"relative\">\n                    <div className=\"mb-4 p-3 rounded-full bg-primary/10 group-hover:bg-primary/20 transition-colors w-12 h-12 flex items-center justify-center\">\n                      {highlight.icon}\n                    </div>\n                    <h3 className=\"text-lg font-bold mb-1 group-hover:text-primary transition-colors\" suppressHydrationWarning>\n                      {highlight.title}\n                    </h3>\n                    <p className=\"text-gray-500 dark:text-gray-400 text-sm\" suppressHydrationWarning>\n                      {highlight.description}\n                    </p>\n                  </div>\n                </div>\n              </motion.div>\n            ))}\n          </div>\n        </div>\n      </motion.section>\n\n      {/* Steps Section */}\n      <motion.section \n        className=\"w-full py-12 bg-primary/5\"\n        variants={container}\n        initial=\"hidden\"\n        whileInView=\"show\"\n        viewport={{ once: true }}\n      >\n        <div className=\"container px-4 md:px-6\">\n          <motion.div \n            className=\"text-center mb-8\"\n            variants={item}\n          >\n            <div className=\"inline-flex items-center space-x-2 mb-4\">\n              <div className=\"h-px w-8 bg-primary/60\" />\n              <h2 className=\"text-2xl font-bold\" suppressHydrationWarning>{t('landing.steps.title')}</h2>\n              <div className=\"h-px w-8 bg-primary/60\" />\n            </div>\n            <p className=\"text-gray-500 dark:text-gray-400 max-w-[600px] mx-auto text-sm\" suppressHydrationWarning>\n              {t('landing.steps.subtitle')}\n            </p>\n          </motion.div>\n          <div className=\"grid gap-6 lg:grid-cols-3\">\n            {steps.map((step, i) => (\n              <motion.div \n                key={i} \n                variants={item}\n                className=\"relative flex flex-col items-center text-center group\"\n              >\n                <div className=\"mb-4\">\n                  <div className=\"relative\">\n                    <div className=\"w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center text-xl font-bold text-primary relative overflow-hidden group-hover:bg-primary/20 transition-colors\">\n                      <div className=\"absolute inset-0 bg-gradient-to-r from-primary/20 to-primary/0 animate-spin-slow\" />\n                      <span className=\"relative\">{step.number}</span>\n                    </div>\n                    {i < steps.length - 1 && (\n                      <div className=\"absolute top-1/2 left-full w-full h-px bg-gradient-to-r from-primary/40 to-primary/0 -translate-y-1/2 hidden lg:block\" />\n                    )}\n                  </div>\n                </div>\n                <h3 className=\"text-lg font-bold mb-1 group-hover:text-primary transition-colors\" suppressHydrationWarning>\n                  {step.title}\n                </h3>\n                <p className=\"text-gray-500 dark:text-gray-400 text-sm\" suppressHydrationWarning>\n                  {step.description}\n                </p>\n              </motion.div>\n            ))}\n          </div>\n        </div>\n      </motion.section>\n\n      {/* Testimonials Section */}\n      <motion.section \n        className=\"w-full py-12\"\n        variants={container}\n        initial=\"hidden\"\n        whileInView=\"show\"\n        viewport={{ once: true }}\n      >\n        <div className=\"container px-4 md:px-6\">\n          <motion.div \n            className=\"text-center mb-8\"\n            variants={item}\n          >\n            <div className=\"inline-flex items-center space-x-2 mb-4\">\n              <div className=\"h-px w-8 bg-primary/60\" />\n              <h2 className=\"text-2xl font-bold\" suppressHydrationWarning>{t('landing.testimonials.title')}</h2>\n              <div className=\"h-px w-8 bg-primary/60\" />\n            </div>\n            <p className=\"text-gray-500 dark:text-gray-400 max-w-[600px] mx-auto text-sm\" suppressHydrationWarning>\n              {t('landing.testimonials.subtitle')}\n            </p>\n          </motion.div>\n          <div className=\"relative overflow-hidden\">\n            <Swiper\n              modules={[Autoplay, Navigation, Pagination]}\n              spaceBetween={30}\n              slidesPerView={1}\n              breakpoints={{\n                640: {\n                  slidesPerView: 2,\n                },\n                1024: {\n                  slidesPerView: 3,\n                },\n              }}\n              autoplay={{\n                delay: 3000,\n                disableOnInteraction: false,\n              }}\n              pagination={{\n                clickable: true,\n              }}\n              navigation\n              loop\n              className=\"testimonials-swiper\"\n            >\n              {testimonials.map((testimonial, i) => (\n                <SwiperSlide key={i}>\n                  <Card className=\"group bg-background/50 backdrop-blur-sm hover:shadow-lg transition-all duration-300 border-primary/10 hover:border-primary/30 h-[280px] flex flex-col\">\n                    <CardContent className=\"p-6 relative flex flex-col flex-1\">\n                      <div className=\"absolute top-0 right-0 w-16 h-16 bg-gradient-to-br from-primary/20 to-transparent rounded-bl-full\" />\n                      <div className=\"mb-3\">\n                        {[...Array(testimonial.rating)].map((_, i) => (\n                          <span key={i} className=\"text-yellow-400 animate-pulse\">★</span>\n                        ))}\n                      </div>\n                      <p className=\"mb-4 text-base italic text-gray-600 dark:text-gray-300 relative flex-1\" suppressHydrationWarning>\n                        <span className=\"absolute -top-2 -left-2 text-4xl text-primary/20\">\"</span>\n                        {testimonial.quote}\n                        <span className=\"absolute -bottom-4 -right-2 text-4xl text-primary/20\">\"</span>\n                      </p>\n                      <div className=\"flex items-center space-x-3 mt-auto\">\n                        <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-sm relative overflow-hidden group-hover:bg-primary/20 transition-colors\">\n                          <div className=\"absolute inset-0 bg-gradient-to-r from-primary/20 to-primary/0 animate-spin-slow\" />\n                          <span className=\"relative\" suppressHydrationWarning>{testimonial.author[0]}</span>\n                        </div>\n                        <div>\n                          <p className=\"font-bold text-sm\" suppressHydrationWarning>{testimonial.author}</p>\n                          <p className=\"text-xs text-gray-500 dark:text-gray-400\" suppressHydrationWarning>{testimonial.role}</p>\n                        </div>\n                      </div>\n                    </CardContent>\n                  </Card>\n                </SwiperSlide>\n              ))}\n            </Swiper>\n          </div>\n        </div>\n      </motion.section>\n    </div>\n  )\n}"
  },
  {
    "path": "app/pricing/layout.tsx",
    "content": "import type { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  title: 'Pricing Plans - AI Translation Assistant',\n  description: 'Affordable pricing plans for AI-powered translation services. Choose from Free, Pro, and Enterprise plans with various features and usage limits.',\n  alternates: {\n    canonical: '/pricing',\n  },\n  openGraph: {\n    title: 'Pricing Plans - AI Translation Assistant',\n    description: 'Affordable pricing plans for AI-powered translation services. Choose from Free, Pro, and Enterprise plans with various features and usage limits.',\n    url: '/pricing',\n    images: [\n      {\n        url: '/og-image.png',\n        width: 1200,\n        height: 630,\n        alt: 'AI Translation Assistant - Pricing Page',\n      },\n    ],\n  },\n}\n\nexport default function PricingLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return children\n}\n"
  },
  {
    "path": "app/pricing/page.tsx",
    "content": "'use client'\n\nimport { Button } from \"@/components/ui/button\"\nimport { Card } from \"@/components/ui/card\"\nimport { Check, Minus, Clock, RefreshCw, CreditCard, Users, MessageCircle, Shield } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport Link from \"next/link\"\nimport { useLanguage } from \"@/components/language-provider\"\nimport { useMemo, useState } from \"react\"\nimport { useSession } from \"next-auth/react\"\nimport { toast } from \"sonner\"\nimport { PLANS } from \"@/lib/stripe\"\n\nexport default function PricingPage() {\n  const { translations: t } = useLanguage()\n  const { data: session } = useSession()\n  const [isLoading, setIsLoading] = useState<string | null>(null)\n  \n  if (!t?.pricing) {\n    return null\n  }\n\n  const pricing = t.pricing as NonNullable<typeof t.pricing>\n\n  const handleSubscribe = async (priceId: string) => {\n    try {\n      console.log('Subscribing with priceId:', priceId)\n      console.log('Session status:', session)\n      setIsLoading(priceId)\n      \n      if (!session) {\n        console.log('No session found, redirecting to login')\n        toast.error(t.auth.error.notLoggedIn)\n        window.location.href = `/login?callbackUrl=${encodeURIComponent('/pricing')}`\n        return\n      }\n\n      console.log('User is logged in, proceeding with subscription')\n      console.log('Making API request to /api/subscription')\n      const response = await fetch('/api/subscription', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          priceId,\n        }),\n      })\n\n      console.log('API Response:', response)\n      const data = await response.json()\n      console.log('API Data:', data)\n      \n      if (!response.ok) {\n        throw new Error(data.message)\n      }\n\n      // 重定向到 Stripe Checkout\n      console.log('Redirecting to:', data.url)\n      window.location.href = data.url\n    } catch (error) {\n      console.error('Subscription error:', error)\n      toast.error(t.error.default)\n    } finally {\n      setIsLoading(null)\n    }\n  }\n\n  const tiers = useMemo(() => {\n    console.log('PLANS:', PLANS)\n    console.log('Monthly Price ID:', PLANS.monthly.priceId)\n    console.log('Yearly Price ID:', PLANS.yearly.priceId)\n    console.log('Env Monthly ID:', process.env.STRIPE_MONTHLY_PRICE_ID)\n    console.log('Env Yearly ID:', process.env.STRIPE_YEARLY_PRICE_ID)\n    return [\n    {\n      id: \"free\" as const,\n      price: \"$0\",\n      features: pricing.tiers.free.features\n    },\n    {\n      id: \"yearly\" as const,\n      price: \"$99.99\",\n      priceId: PLANS.yearly.priceId,\n      isRecommended: true,\n      features: pricing.tiers.yearly.features\n    },\n    {\n      id: \"monthly\" as const,\n      price: \"$9.99\",\n      priceId: PLANS.monthly.priceId,\n      features: pricing.tiers.monthly.features\n    }\n  ]}, [pricing])\n\n  const renderFeatureSection = (features: string[], title: string, tierId?: string) => {\n    // 为免费版使用特殊的键名\n    const sectionTitle = tierId === 'free' && (title === 'advanced' || title === 'support') \n      ? `free${title.charAt(0).toUpperCase() + title.slice(1)}` \n      : title;\n    \n    return (\n      <div className=\"mb-6\">\n        <h4 className=\"text-sm font-medium text-muted-foreground mb-3\">{pricing.features[title]}</h4>\n        <ul className=\"space-y-3\">\n          {features.map((feature) => (\n            <li key={feature} className=\"flex items-center gap-2\">\n              <Check className=\"h-5 w-5 text-primary flex-shrink-0\" />\n              <span>{feature}</span>\n            </li>\n          ))}\n        </ul>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"container py-20\">\n      <div className=\"text-center mb-12\">\n        <h1 className=\"text-4xl font-bold mb-4\">{pricing.title}</h1>\n        <p className=\"text-xl text-muted-foreground\">\n          {pricing.subtitle}\n        </p>\n      </div>\n\n      {/* 桌面版定价方案 */}\n      <div className=\"hidden md:grid md:grid-cols-3 gap-8\">\n        {tiers.map((tier) => (\n          <Card \n            key={tier.id}\n            className={cn(\n              \"relative p-8 rounded-lg border\",\n              tier.isRecommended && \"border-2 border-primary shadow-lg\"\n            )}\n          >\n            {tier.isRecommended && (\n              <div className=\"absolute -top-4 left-1/2 -translate-x-1/2\">\n                <span className=\"bg-primary text-primary-foreground text-sm font-medium px-3 py-1 rounded-full\">\n                  {pricing.tiers.yearly.recommended}\n                </span>\n              </div>\n            )}\n\n            <div className=\"mb-8\">\n              <h2 className=\"text-2xl font-bold mb-2\">{pricing.tiers[tier.id].name}</h2>\n              <p className=\"text-muted-foreground mb-4\">{pricing.tiers[tier.id].description}</p>\n              <div className=\"flex items-baseline gap-1\">\n                <span className=\"text-4xl font-bold\">{tier.price}</span>\n                {tier.id !== \"free\" && (\n                  <span className=\"text-muted-foreground\">\n                    /{pricing.tiers[tier.id].name}\n                  </span>\n                )}\n                {tier.id === \"yearly\" && (\n                  <span className=\"ml-2 inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/10 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-400 ring-1 ring-inset ring-green-600/10\">\n                    {pricing.tiers.yearly.discount}\n                  </span>\n                )}\n              </div>\n            </div>\n\n            <div className=\"space-y-6 mb-8\">\n              {renderFeatureSection(tier.features.basic, 'basic', tier.id)}\n              {renderFeatureSection(tier.features.advanced || tier.features.freeAdvanced, 'advanced', tier.id)}\n              {renderFeatureSection(tier.features.support || tier.features.freeSupport, 'support', tier.id)}\n            </div>\n\n            {tier.id === \"free\" ? (\n              <Link href=\"/translate\" className=\"w-full\">\n                <Button className=\"w-full\" variant=\"outline\">\n                  {pricing.tiers.free.cta}\n                </Button>\n              </Link>\n            ) : (\n              <Button \n                className=\"w-full\"\n                variant={tier.isRecommended ? \"default\" : \"outline\"}\n                onClick={() => handleSubscribe(tier.priceId!)}\n                disabled={isLoading === tier.priceId}\n              >\n                {isLoading === tier.priceId ? t.loading : pricing.tiers[tier.id].cta}\n              </Button>\n            )}\n          </Card>\n        ))}\n      </div>\n\n      {/* 移动端定价方案 */}\n      <div className=\"space-y-6 md:hidden\">\n        {tiers.map((tier) => (\n          <Card \n            key={tier.id}\n            className={cn(\n              \"p-6\",\n              tier.isRecommended && \"border-2 border-primary\"\n            )}\n          >\n            <div className=\"flex justify-between items-center mb-6\">\n              <div>\n                <h3 className=\"text-lg font-semibold\">{pricing.tiers[tier.id].name}</h3>\n                <p className=\"text-sm text-muted-foreground mt-1\">{pricing.tiers[tier.id].description}</p>\n              </div>\n              <div className=\"text-right\">\n                <span className=\"text-2xl font-bold\">{tier.price}</span>\n                {tier.id !== \"free\" && (\n                  <div className=\"text-sm text-muted-foreground\">\n                    /{pricing.tiers[tier.id].name}\n                  </div>\n                )}\n                {tier.id === \"yearly\" && (\n                  <span className=\"mt-1 inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/10 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-400 ring-1 ring-inset ring-green-600/10\">\n                    {pricing.tiers.yearly.discount}\n                  </span>\n                )}\n              </div>\n            </div>\n            <div className=\"space-y-4 mb-6\">\n              {renderFeatureSection(tier.features.basic, 'basic', tier.id)}\n              {renderFeatureSection(tier.features.advanced || tier.features.freeAdvanced, 'advanced', tier.id)}\n              {renderFeatureSection(tier.features.support || tier.features.freeSupport, 'support', tier.id)}\n            </div>\n            {tier.id === \"free\" ? (\n              <Link href=\"/translate\" className=\"w-full\">\n                <Button className=\"w-full\" variant=\"outline\">\n                  {pricing.tiers.free.cta}\n                </Button>\n              </Link>\n            ) : (\n              <Button \n                className=\"w-full\" \n                variant=\"outline\"\n                onClick={() => handleSubscribe(tier.priceId!)}\n                disabled={isLoading === tier.priceId}\n              >\n                {isLoading === tier.priceId ? t.loading : pricing.tiers[tier.id].cta}\n              </Button>\n            )}\n          </Card>\n        ))}\n      </div>\n\n      {/* FAQ部分 */}\n      <div className=\"mt-24 max-w-4xl mx-auto\">\n        <div className=\"text-center mb-12\">\n          <h3 className=\"text-3xl font-bold mb-4\">{pricing.faq.title}</h3>\n          <p className=\"text-muted-foreground\">\n            {pricing.faq.subtitle}\n          </p>\n        </div>\n\n        <div className=\"grid md:grid-cols-2 gap-6\">\n          {Object.entries({\n            security: { icon: Shield },\n            quota: { icon: Clock },\n            payment: { icon: CreditCard },\n            support: { icon: MessageCircle }\n          }).map(([key, { icon: Icon }]) => (\n            <Card key={key} className=\"p-6 hover:shadow-lg transition-shadow\">\n              <h4 className=\"flex items-center gap-2 text-lg font-semibold mb-3\">\n                <div className=\"w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center\">\n                  <Icon className=\"h-4 w-4 text-primary\" />\n                </div>\n                {pricing.faq.cards[key].title}\n              </h4>\n              <p className=\"text-muted-foreground\">\n                {pricing.faq.cards[key].content}\n              </p>\n            </Card>\n          ))}\n        </div>\n      </div>\n    </div>\n  )\n} "
  },
  {
    "path": "app/profile/page.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\"\nimport { useSession, signIn } from \"next-auth/react\"\nimport { useRouter, useSearchParams } from \"next/navigation\"\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport { useI18n } from \"@/lib/i18n/use-translations\"\nimport { useLanguage } from \"@/components/language-provider\"\nimport { Progress } from \"@/components/ui/progress\"\nimport { toast } from 'sonner'\nimport confetti from 'canvas-confetti'\nimport { PLANS } from '@/lib/stripe'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Pencil } from \"lucide-react\"\n\ninterface UserInfo {\n  user: {\n    id: string;\n    email: string;\n    name: string | null;\n    created_at: string;\n    updated_at: string;\n  };\n  subscription: {\n    stripe_customer_id: string | null;\n    stripe_subscription_id: string | null;\n    stripe_price_id: string | null;\n    stripe_current_period_end: string | null;\n  };\n  quota: {\n    text_quota: number;\n    image_quota: number;\n    pdf_quota: number;\n    speech_quota: number;\n    video_quota: number;\n  };\n  usage: {\n    text: number;\n    image: number;\n    pdf: number;\n    speech: number;\n    video: number;\n  };\n}\n\nconst QUOTA_TYPES = ['text', 'image', 'pdf', 'speech', 'video'] as const\n\nexport default function ProfilePage() {\n  const { data: session, status } = useSession()\n  const router = useRouter()\n  const { t } = useI18n()\n  const { language } = useLanguage()\n  const [userInfo, setUserInfo] = useState<UserInfo | null>(null)\n  const searchParams = useSearchParams()\n  const [isEditingName, setIsEditingName] = useState(false)\n  const [newName, setNewName] = useState('')\n  const [isUpdating, setIsUpdating] = useState(false)\n\n  // 获取用户信息的函数\n  const fetchUserInfo = async () => {\n    try {\n      console.log('开始获取用户信息')\n      const response = await fetch('/api/user/info')\n      const data = await response.json()\n      if (response.ok) {\n        console.log('获取到的用户信息:', {\n          id: data.user.id,\n          email: data.user.email,\n          subscription: {\n            customerId: data.subscription.stripe_customer_id,\n            subscriptionId: data.subscription.stripe_subscription_id,\n            priceId: data.subscription.stripe_price_id,\n            currentPeriodEnd: data.subscription.stripe_current_period_end\n          },\n          quota: data.quota,\n          usage: data.usage\n        })\n        setUserInfo(data)\n      } else {\n        console.error('获取用户信息失败:', data.error)\n      }\n    } catch (error) {\n      console.error('获取用户信息失败:', error)\n    }\n  }\n\n  useEffect(() => {\n    // 如果已经确认未登录，则重定向到登录页面\n    if (status === 'unauthenticated') {\n      console.log('用户未登录，重定向到登录页面')\n      router.push('/login?callbackUrl=/profile')\n      return\n    }\n\n    // 如果已登录，获取用户信息\n    if (status === 'authenticated') {\n      console.log('用户已登录，获取用户信息')\n      // 只在页面加载时获取一次用户信息\n      fetchUserInfo()\n    }\n  }, [status, router, t])\n\n  // 订阅成功效果\n  useEffect(() => {\n    if (userInfo && searchParams.get('subscription') === 'success') {\n      console.log('用户信息加载完成，检测到订阅成功参数，显示成功提示')\n      toast.success(t('auth.profile.subscription.success'))\n      confetti({\n        particleCount: 100,\n        spread: 70,\n        origin: { y: 0.6 }\n      })\n      \n      // 清除 URL 中的 subscription 参数，防止刷新页面时重复显示\n      const newUrl = new URL(window.location.href)\n      newUrl.searchParams.delete('subscription')\n      router.replace(newUrl.pathname + newUrl.search)\n    }\n  }, [userInfo, searchParams, t, router])\n\n  // 如果正在检查登录状态，显示加载状态\n  if (status === 'loading') {\n    return (\n      <div className=\"container py-20\">\n        <div className=\"flex items-center justify-center min-h-[200px]\">\n          <div className=\"text-muted-foreground\">Loading...</div>\n        </div>\n      </div>\n    )\n  }\n\n  // 如果未登录，返回 null（会被上面的 useEffect 重定向）\n  if (!session) return null\n\n  // 如果已登录但还没有获取到用户信息，显示加载状态\n  if (!userInfo) {\n    return (\n      <div className=\"container py-20\">\n        <div className=\"flex items-center justify-center min-h-[200px]\">\n          <div className=\"text-muted-foreground\">Loading...</div>\n        </div>\n      </div>\n    )\n  }\n\n  // 更新用户名\n  const updateName = async () => {\n    if (!newName.trim()) {\n      toast.error(t('auth.profile.basicInfo.editUsername.error.empty'))\n      return\n    }\n\n    try {\n      setIsUpdating(true)\n      const response = await fetch('/api/user/update', {\n        method: 'PUT',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ name: newName.trim() }),\n      })\n\n      if (response.ok) {\n        toast.success(t('auth.profile.basicInfo.editUsername.success'))\n        setIsEditingName(false)\n        fetchUserInfo()\n      } else {\n        const data = await response.json()\n        toast.error(data.error || t('auth.profile.basicInfo.editUsername.error.failed'))\n      }\n    } catch (error) {\n      console.error('更新用户名失败:', error)\n      toast.error(t('auth.profile.basicInfo.editUsername.error.failed'))\n    } finally {\n      setIsUpdating(false)\n    }\n  }\n\n  const renderQuotaItem = (type: typeof QUOTA_TYPES[number]) => {\n    const quota = userInfo.quota[`${type}_quota` as keyof typeof userInfo.quota]\n    const used = userInfo.usage[type]\n    const percentage = quota === -1 ? 0 : (used / quota) * 100\n\n    return (\n      <div className=\"p-4 bg-muted rounded-lg\">\n        <div className=\"text-sm text-muted-foreground mb-1\">\n          {t(`auth.profile.subscription.quota.${type}`)}\n        </div>\n        <div className=\"text-2xl font-semibold mb-2\">\n          {quota === -1 ? \n            t('auth.profile.subscription.quota.unlimited') : \n            t('auth.profile.subscription.quota.usage', { used: used.toString(), quota: quota.toString() })\n          }\n        </div>\n        {quota !== -1 && (\n          <Progress value={percentage} className=\"h-2\" />\n        )}\n      </div>\n    )\n  }\n\n  const getPlanName = () => {\n    if (!userInfo.subscription.stripe_price_id) return t('auth.profile.subscription.plan.trial')\n    if (userInfo.subscription.stripe_price_id === PLANS.monthly.priceId) return t('auth.profile.subscription.plan.monthly')\n    if (userInfo.subscription.stripe_price_id === PLANS.yearly.priceId) return t('auth.profile.subscription.plan.yearly')\n    return t('auth.profile.subscription.plan.trial')\n  }\n\n  const formatDate = (dateString: string) => {\n    const date = new Date(dateString)\n    if (language === 'zh') {\n      return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`\n    }\n    return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`\n  }\n\n  const renderNameField = () => {\n    if (isEditingName) {\n      return (\n        <div className=\"flex items-center space-x-2\">\n          <Input\n            value={newName}\n            onChange={(e) => setNewName(e.target.value)}\n            placeholder={t('auth.profile.basicInfo.editUsername.placeholder')}\n            maxLength={50}\n            className=\"max-w-[200px]\"\n          />\n          <Button \n            onClick={updateName} \n            disabled={isUpdating}\n            size=\"sm\"\n          >\n            {t('auth.profile.basicInfo.editUsername.save')}\n          </Button>\n          <Button \n            onClick={() => setIsEditingName(false)}\n            variant=\"outline\"\n            size=\"sm\"\n          >\n            {t('auth.profile.basicInfo.editUsername.cancel')}\n          </Button>\n        </div>\n      )\n    }\n\n    return (\n      <div className=\"flex items-center space-x-2\">\n        <div className=\"text-lg font-medium\">\n          {userInfo?.user.name || '-'}\n        </div>\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"h-8 w-8\"\n          onClick={() => {\n            setNewName(userInfo?.user.name || '')\n            setIsEditingName(true)\n          }}\n        >\n          <Pencil className=\"h-4 w-4\" />\n        </Button>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"container py-20\">\n      <h1 className=\"text-4xl font-bold mb-8\">{t('auth.profile.title')}</h1>\n      <div className=\"space-y-6\">\n        {/* 基本信息卡片 */}\n        <Card className=\"p-6\">\n          <div className=\"flex items-center justify-between mb-4\">\n            <h2 className=\"text-2xl font-semibold\">{t('auth.profile.basicInfo.title')}</h2>\n          </div>\n          <div className=\"space-y-4\">\n            <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\n              <div className=\"p-4 bg-muted rounded-lg\">\n                <div className=\"text-sm text-muted-foreground mb-1\">{t('auth.profile.basicInfo.email')}</div>\n                <div className=\"text-lg font-medium\">\n                  {userInfo?.user.email || '-'}\n                </div>\n              </div>\n              <div className=\"p-4 bg-muted rounded-lg\">\n                <div className=\"text-sm text-muted-foreground mb-1\">{t('auth.profile.basicInfo.username')}</div>\n                {renderNameField()}\n              </div>\n              <div className=\"p-4 bg-muted rounded-lg\">\n                <div className=\"text-sm text-muted-foreground mb-1\">{t('auth.profile.basicInfo.userId')}</div>\n                <div className=\"text-lg font-medium\">\n                  {userInfo?.user.id || '-'}\n                </div>\n              </div>\n              <div className=\"p-4 bg-muted rounded-lg\">\n                <div className=\"text-sm text-muted-foreground mb-1\">{t('auth.profile.basicInfo.registerTime')}</div>\n                <div className=\"text-lg font-medium\">\n                  {userInfo?.user.created_at ? formatDate(userInfo.user.created_at) : '-'}\n                </div>\n              </div>\n            </div>\n          </div>\n        </Card>\n\n        {/* 订阅信息卡片 */}\n        <Card className=\"p-6\">\n          <div className=\"flex items-center justify-between mb-4\">\n            <h2 className=\"text-2xl font-semibold\">{t('auth.profile.subscription.title')}</h2>\n            <Badge variant={userInfo.subscription.stripe_price_id ? \"default\" : \"secondary\"}>\n              {getPlanName()}\n            </Badge>\n          </div>\n          \n          <div className=\"space-y-4\">\n            {userInfo.subscription.stripe_customer_id && (\n              <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4\">\n                <div className=\"p-4 bg-muted rounded-lg\">\n                  <div className=\"text-sm text-muted-foreground mb-1\">{t('auth.profile.subscription.customerId')}</div>\n                  <div className=\"text-lg font-medium\">\n                    {userInfo.subscription.stripe_customer_id}\n                  </div>\n                </div>\n                <div className=\"p-4 bg-muted rounded-lg\">\n                  <div className=\"text-sm text-muted-foreground mb-1\">{t('auth.profile.subscription.subscriptionId')}</div>\n                  <div className=\"text-lg font-medium\">\n                    {userInfo.subscription.stripe_subscription_id || '-'}\n                  </div>\n                </div>\n              </div>\n            )}\n\n            <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\n              {QUOTA_TYPES.map(type => (\n                <div key={type}>\n                  {renderQuotaItem(type)}\n                </div>\n              ))}\n            </div>\n\n            {userInfo.subscription.stripe_current_period_end && (\n              <div className=\"text-sm text-muted-foreground\">\n                {t('auth.profile.subscription.expiryDate', { \n                  date: formatDate(userInfo.subscription.stripe_current_period_end)\n                })}\n              </div>\n            )}\n          </div>\n        </Card>\n      </div>\n    </div>\n  )\n} "
  },
  {
    "path": "app/providers.tsx",
    "content": "\"use client\";\n\nimport { SessionProvider } from \"next-auth/react\";\nimport { useLanguage } from \"@/components/language-provider\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { Header } from '@/components/header';\nimport { Footer } from '@/components/footer';\nimport { Toaster } from '@/components/ui/toaster';\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return (\n    <SessionProvider>\n      <ThemeProvider\n        attribute=\"class\"\n        defaultTheme=\"system\"\n        enableSystem\n        disableTransitionOnChange\n      >\n        <div className=\"min-h-screen flex flex-col\">\n          <Header />\n          <main className=\"container mx-auto px-4 py-8 flex-1\">\n            {children}\n          </main>\n          <Footer />\n        </div>\n        <Toaster />\n      </ThemeProvider>\n    </SessionProvider>\n  );\n} "
  },
  {
    "path": "app/register/layout.tsx",
    "content": "import { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  title: 'Sign up',\n  description: 'Create your account',\n}\n\nexport default function RegisterLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return children\n} "
  },
  {
    "path": "app/register/page.tsx",
    "content": "\"use client\";\n\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useState, useEffect } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { toast } from 'sonner'\nimport { useI18n } from '@/lib/i18n/use-translations'\nimport { useSession, signIn } from 'next-auth/react'\nimport { Github } from 'lucide-react'\nimport { Separator } from '@/components/ui/separator'\n\nexport default function RegisterPage() {\n  const { t } = useI18n()\n  const router = useRouter()\n  const searchParams = useSearchParams()\n  const { data: session } = useSession()\n  const [loading, setLoading] = useState(false)\n  const [email, setEmail] = useState('')\n  const [password, setPassword] = useState('')\n  const [confirmPassword, setConfirmPassword] = useState('')\n\n  const callbackUrl = searchParams.get('returnUrl') || searchParams.get('callbackUrl') || '/'\n\n  useEffect(() => {\n    if (session) {\n      router.push(callbackUrl)\n    }\n  }, [session, router, callbackUrl])\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault()\n    setLoading(true)\n\n    try {\n      if (!email || !password || !confirmPassword) {\n        throw new Error(t('auth.register.error.required'))\n      }\n\n      if (password !== confirmPassword) {\n        throw new Error(t('auth.register.error.passwordMismatch'))\n      }\n\n      console.log('Attempting to register, callbackUrl:', callbackUrl)\n\n      const response = await fetch('/api/register', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          email,\n          password,\n        }),\n      })\n\n      const data = await response.json()\n\n      if (!response.ok) {\n        throw new Error(data.error || t('auth.register.error.emailExists'))\n      }\n\n      toast.success(t('auth.register.success'))\n      \n      console.log('Registration successful, attempting to sign in')\n      \n      await signIn('credentials', {\n        email,\n        password,\n        redirect: true,\n        callbackUrl,\n      })\n\n      // 由于设置了 redirect: true，下面的代码不会执行\n      console.log('Sign in after registration successful')\n    } catch (error: any) {\n      console.error('Registration/sign in error:', error)\n      toast.error(error.message || t('auth.register.error'))\n    } finally {\n      setLoading(false)\n    }\n  }\n\n  const handleGitHubLogin = async () => {\n    setLoading(true)\n    try {\n      console.log('Attempting GitHub login from register page, callbackUrl:', callbackUrl)\n      await signIn('github', { \n        callbackUrl,\n        redirect: true\n      })\n    } catch (error) {\n      console.error('GitHub login error:', error)\n      toast.error(t('auth.signIn.error'))\n    }\n  }\n\n  const handleGoogleLogin = async () => {\n    setLoading(true)\n    try {\n      console.log('Attempting Google login from register page, callbackUrl:', callbackUrl)\n      await signIn('google', { \n        callbackUrl,\n        redirect: true\n      })\n    } catch (error) {\n      console.error('Google login error:', error)\n      toast.error(t('auth.signIn.error'))\n    }\n  }\n\n  if (session) {\n    return null\n  }\n\n  return (\n    <div className=\"min-h-screen flex items-center justify-center bg-background\">\n      <div className=\"mx-auto max-w-[350px] space-y-6\">\n        <div className=\"space-y-2 text-center\">\n          <h1 className=\"text-2xl font-bold\" suppressHydrationWarning>\n            {t('auth.register.title')}\n          </h1>\n          <p className=\"text-sm text-muted-foreground\" suppressHydrationWarning>\n            {t('auth.register.subtitle')}\n          </p>\n        </div>\n        <div className=\"grid grid-cols-1 gap-2\">\n          <Button \n            variant=\"outline\" \n            className=\"w-full\" \n            onClick={handleGitHubLogin}\n            disabled={loading}\n          >\n            <Github className=\"mr-2 h-4 w-4\" />\n            <span suppressHydrationWarning>{t('auth.register.github')}</span>\n          </Button>\n          <Button \n            variant=\"outline\" \n            className=\"w-full\" \n            onClick={handleGoogleLogin}\n            disabled={loading}\n          >\n            <svg className=\"mr-2 h-4 w-4\" aria-hidden=\"true\" focusable=\"false\" data-prefix=\"fab\" data-icon=\"google\" role=\"img\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 488 512\">\n              <path fill=\"currentColor\" d=\"M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z\"></path>\n            </svg>\n            <span suppressHydrationWarning>{t('auth.register.google')}</span>\n          </Button>\n        </div>\n        <div className=\"relative\">\n          <div className=\"absolute inset-0 flex items-center\">\n            <Separator className=\"w-full\" />\n          </div>\n          <div className=\"relative flex justify-center text-xs uppercase\">\n            <span className=\"bg-background px-2 text-muted-foreground\" suppressHydrationWarning>\n              {t('auth.register.or')}\n            </span>\n          </div>\n        </div>\n        <form onSubmit={handleSubmit} className=\"space-y-4\">\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"email\" suppressHydrationWarning>\n              {t('auth.register.email')}\n            </Label>\n            <Input\n              id=\"email\"\n              placeholder={t('auth.register.emailPlaceholder')}\n              required\n              type=\"email\"\n              value={email}\n              onChange={(e) => setEmail(e.target.value)}\n              disabled={loading}\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"password\" suppressHydrationWarning>\n              {t('auth.register.password')}\n            </Label>\n            <Input\n              id=\"password\"\n              placeholder={t('auth.register.passwordPlaceholder')}\n              required\n              type=\"password\"\n              value={password}\n              onChange={(e) => setPassword(e.target.value)}\n              disabled={loading}\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"confirmPassword\" suppressHydrationWarning>\n              {t('auth.register.confirmPassword')}\n            </Label>\n            <Input\n              id=\"confirmPassword\"\n              placeholder={t('auth.register.confirmPasswordPlaceholder')}\n              required\n              type=\"password\"\n              value={confirmPassword}\n              onChange={(e) => setConfirmPassword(e.target.value)}\n              disabled={loading}\n            />\n          </div>\n          <Button className=\"w-full\" type=\"submit\" disabled={loading}>\n            <span suppressHydrationWarning>\n              {loading ? t('auth.register.loading') : t('auth.register.button')}\n            </span>\n          </Button>\n        </form>\n        <div className=\"text-center text-sm\">\n          <span suppressHydrationWarning>{t('auth.register.hasAccount')}</span>{' '}\n          <Link className=\"underline\" href=\"/login\">\n            <span suppressHydrationWarning>{t('auth.signIn')}</span>\n          </Link>\n        </div>\n      </div>\n    </div>\n  )\n} "
  },
  {
    "path": "app/translate/layout.tsx",
    "content": "import type { Metadata } from 'next'\n\nexport const metadata: Metadata = {\n  title: 'Online Translation - AI Translation Assistant',\n  description: 'High-quality multilingual translation powered by AI, supporting text, image, PDF, speech, and video formats.',\n  alternates: {\n    canonical: '/translate',\n  },\n  openGraph: {\n    title: 'Online Translation - AI Translation Assistant',\n    description: 'High-quality multilingual translation powered by AI, supporting text, image, PDF, speech, and video formats.',\n    url: '/translate',\n    images: [\n      {\n        url: '/og-image.png',\n        width: 1200,\n        height: 630,\n        alt: 'AI Translation Assistant - Translation Page',\n      },\n    ],\n  },\n}\n\nexport default function TranslateLayout({\n  children,\n}: {\n  children: React.ReactNode\n}) {\n  return children\n} "
  },
  {
    "path": "app/translate/page.tsx",
    "content": "\"use client\"\n\nimport { useState, useCallback, useEffect, useRef } from 'react'\nimport { Upload, Image as ImageIcon, Languages, Wand2, Mic, MicOff, Video, Loader2, FileText, FileType, X } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/card'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'\nimport { useToast } from '@/components/ui/use-toast'\nimport { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { extractTextFromImage, translateText, improveText } from '@/lib/gemini'\nimport { extractTextWithTencent } from '@/lib/tencent'\nimport { getLanguageCategories, getLanguagesByCategory } from '@/lib/languages'\nimport { useI18n } from '@/lib/i18n/use-translations'\nimport { TencentASRService } from '@/lib/tencent-asr'\nimport { useSession } from 'next-auth/react'\nimport { useRouter } from 'next/navigation'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { translateWithDeepSeek, translateWithQwen, translateWithZhipu, translateWithHunyuan, translateWith4oMini, translateWithMinniMax, translateWithSiliconFlow, translateWithClaude, translateWithStepAPI } from '@/lib/server/translate'\nimport { extractTextWithQwen } from '@/lib/qwen'\nimport { extractTextWithGemini } from '@/lib/gemini'\nimport { extractVideoFrames, analyzeVideoContent, extractTextWithZhipu, extractFileContent } from '@/lib/zhipu'\nimport { cn } from '@/lib/utils'\nimport { Textarea } from '@/components/ui/textarea'\nimport { extractTextWithDeepseek } from '@/lib/deepseek'\nimport { extractPDFWithKimi, extractPDFContent, extractTextWithKimi } from '@/lib/kimi'\nimport { useLanguage } from \"@/components/language-provider\"\nimport { useAnalytics } from '@/lib/hooks/use-analytics'\nimport { uploadToOSS } from '@/lib/aliyun-oss-client'\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\"\nimport { extractTextWithStep } from '@/lib/step'\nimport { SubscriptionDialog } from \"@/components/subscription-dialog\"\n\ninterface QuotaInfo {\n  text_quota: number;\n  image_quota: number;\n  pdf_quota: number;\n  speech_quota: number;\n  video_quota: number;\n  usage: {\n    text: number;\n    image: number;\n    pdf: number;\n    speech: number;\n    video: number;\n  };\n}\n\nexport default function TranslatePage() {\n  const { data: session } = useSession()\n  const router = useRouter()\n  const { toast } = useToast()\n  const { t } = useI18n()\n  const { language } = useLanguage()\n  const [mounted, setMounted] = useState(false)\n  const [image, setImage] = useState<string | null>(null)\n  const [extractedText, setExtractedText] = useState('')\n  const [translatedText, setTranslatedText] = useState('')\n  const [selectedLanguage, setSelectedLanguage] = useState<string>('')\n  const [isProcessing, setIsProcessing] = useState(false)\n  const [isDragging, setIsDragging] = useState(false)\n  const [isListening, setIsListening] = useState(false)\n  const [interimText, setInterimText] = useState('')\n  const [activeTab, setActiveTab] = useState('text')\n  const [asrService, setAsrService] = useState('tencent')\n  const asrServiceRef = useRef<TencentASRService | null>(null)\n  const recognition = useRef<any>(null)\n  const [translationService, setTranslationService] = useState('deepseek')\n  const [ocrService, setOcrService] = useState('qwen')\n  const [sourceText, setSourceText] = useState('')\n  const [videoFile, setVideoFile] = useState<File | null>(null);\n  const [pdfFile, setPdfFile] = useState<File | null>(null);\n  const [pdfPreview, setPdfPreview] = useState<string | null>(null);\n  const [fileContent, setFileContent] = useState<string>('')\n  const [isFileProcessing, setIsFileProcessing] = useState(false)\n  const [fileService, setFileService] = useState('mistral') // 默认使用 Mistral OCR\n  const [videoService, setVideoService] = useState('zhipu')\n  const { trackEvent } = useAnalytics()\n  const [showAuthDialog, setShowAuthDialog] = useState(false)\n  const [quotaInfo, setQuotaInfo] = useState<QuotaInfo | null>(null)\n  const [videoContent, setVideoContent] = useState<string>('');\n  const [showSubscriptionDialog, setShowSubscriptionDialog] = useState(false)\n\n  // 检查是否还有剩余配额\n  const hasRemainingQuota = useCallback((type: keyof Omit<QuotaInfo['usage'], 'text'>) => {\n    if (!quotaInfo) return false\n    const quotaKey = `${type}_quota` as keyof QuotaInfo\n    const quota = quotaInfo[quotaKey]\n    const used = quotaInfo.usage[type]\n    if (typeof quota === 'number' && typeof used === 'number') {\n      return quota === -1 || quota > used\n    }\n    return false\n  }, [quotaInfo])\n\n  // 获取配额信息的函数\n  const fetchQuotaInfo = async () => {\n    try {\n      const response = await fetch('/api/user/info')\n      const data = await response.json()\n      if (data.error) {\n        console.error(t('console.quotaFetchFailed'), t(data.error))\n        return\n      }\n      // 设置配额信息\n      setQuotaInfo({\n        text_quota: data.quota.text_quota,\n        image_quota: data.quota.image_quota,\n        pdf_quota: data.quota.pdf_quota,\n        speech_quota: data.quota.speech_quota,\n        video_quota: data.quota.video_quota,\n        usage: data.usage\n      })\n    } catch (error) {\n      console.error(t('console.quotaInfoFetchFailed'), error)\n    }\n  }\n\n  useEffect(() => {\n    if (session) {\n      // 初始获取配额信息\n      fetchQuotaInfo()\n    }\n  }, [session])\n\n  // 检查并更新使用次数\n  const checkAndUpdateUsage = useCallback(async (type: 'image' | 'pdf' | 'speech' | 'video') => {\n    if (!session) {\n      setShowAuthDialog(true)\n      return false\n    }\n\n    try {\n      const response = await fetch('/api/user/usage', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ type })\n      })\n\n      const data = await response.json()\n\n      if (!response.ok) {\n        toast({\n          title: t('error.usageLimitExceeded'),\n          description: t(`error.${type}LimitExceededDesc`),\n          variant: \"destructive\"\n        })\n        return false\n      }\n\n      // 刷新配额信息\n      fetchQuotaInfo()\n      return true\n    } catch (error) {\n      console.error(t('console.usageUpdateFailed'), error)\n      return false\n    }\n  }, [session, t, toast])\n\n  // 获取使用次数显示文本\n  const getRemainingUsageText = (type: keyof Omit<QuotaInfo['usage'], 'text'>) => {\n    if (!session) {\n      return t('usage.loginToGet')\n    }\n    if (!quotaInfo) {\n      return ''\n    }\n    const quota = quotaInfo[`${type}_quota` as keyof QuotaInfo] as number\n    const used = quotaInfo.usage[type] as number\n    const remaining = quota - used\n    return t('usage.remainingToday', [remaining, quota])\n  }\n\n  // 获取文本翻译使用次数显示文本\n  const getTextUsageText = () => {\n    if (!session) {\n      return t('usage.loginToGet')\n    }\n    return t('usage.unlimited')\n  }\n\n  // 检查登录状态的函数\n  const checkAuth = useCallback(() => {\n    if (!session) {\n      setShowAuthDialog(true)\n      return false\n    }\n    return true\n  }, [session])\n\n  // 处理登录按钮点击\n  const handleLogin = useCallback(() => {\n    setShowAuthDialog(false)\n    router.push('/login?callbackUrl=/translate')\n  }, [router])\n\n  // 处理注册按钮点击\n  const handleRegister = useCallback(() => {\n    setShowAuthDialog(false)\n    router.push('/register?callbackUrl=/translate')\n  }, [router])\n\n  // 添加使用次数状态\n  const [usageCounts, setUsageCounts] = useState({\n    image: 0,\n    pdf: 0,\n    speech: 0,\n    video: 0\n  });\n\n  useEffect(() => {\n    // 初始化腾讯云语音识别服务\n    asrServiceRef.current = new TencentASRService();\n  }, []);\n\n  // 处理图片上传\n  const handleImageUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {\n    if (!checkAuth()) return;\n\n    // 检查配额\n    if (!hasRemainingQuota('image')) {\n      setShowSubscriptionDialog(true);\n      return;\n    }\n\n    const file = e.target.files?.[0];\n    if (!file || !file.type.startsWith('image/')) {\n      toast({\n        title: t('error.invalidImageFile'),\n        description: t('error.invalidImageFileDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    const reader = new FileReader();\n    reader.onloadend = () => {\n      setImage(reader.result as string);\n    };\n    reader.readAsDataURL(file);\n  }, [checkAuth, hasRemainingQuota, setShowSubscriptionDialog, toast, t, setImage]);\n\n  const handleDragOver = useCallback((e: React.DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setIsDragging(true);\n  }, []);\n\n  const handleDragLeave = useCallback((e: React.DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setIsDragging(false);\n  }, []);\n\n  const handleDrop = useCallback((e: React.DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setIsDragging(false);\n\n    if (!checkAuth()) return;\n\n    // 检查配额\n    if (!hasRemainingQuota('image')) {\n      setShowSubscriptionDialog(true);\n      return;\n    }\n\n    const file = e.dataTransfer.files[0];\n    if (file && file.type.startsWith('image/')) {\n      const reader = new FileReader();\n      reader.onloadend = () => {\n        setImage(reader.result as string);\n      };\n      reader.readAsDataURL(file);\n    } else {\n      toast({\n        title: t('error.invalidFile'),\n        description: t('error.invalidFileDesc'),\n        variant: \"destructive\"\n      });\n    }\n  }, [checkAuth, hasRemainingQuota, setShowSubscriptionDialog, toast, t, setImage]);\n\n  const handlePDFUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    if (!checkAuth()) return;\n\n    const file = e.target.files?.[0]\n    if (!file || (!file.name.toLowerCase().endsWith('.pdf') && file.type !== 'application/pdf')) {\n      toast({\n        title: t('error.invalidFile'),\n        description: t('error.invalidFileDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    // 保存PDF文件以便后续处理\n    setPdfFile(file);\n    \n    // 创建PDF预览\n    const reader = new FileReader();\n    reader.onloadend = () => {\n      setPdfPreview(reader.result as string);\n    };\n    reader.readAsDataURL(file);\n    \n    // 清空文件输入框，以便可以再次选择同一文件\n    e.target.value = '';\n  };\n\n  const handleSpeechUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    if (!checkAuth()) return;\n\n    const file = e.target.files?.[0]\n    if (!file || !file.type.startsWith('audio/')) {\n      toast({\n        title: t('error.invalidAudioFile'),\n        description: t('error.invalidAudioFileDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    // 检查配额\n    if (!hasRemainingQuota('speech')) {\n      setShowSubscriptionDialog(true);\n      return;\n    }\n\n    setIsProcessing(true);\n    try {\n      if (!asrServiceRef.current) {\n        asrServiceRef.current = new TencentASRService();\n      }\n\n      const text = await asrServiceRef.current.recognizeAudio(\n        file,\n        (progress) => {\n          setInterimText(progress);\n        },\n        (error) => {\n          toast({\n            title: t('error.audioRecognition'),\n            description: error,\n            variant: \"destructive\"\n          });\n        }\n      );\n      \n      // 处理成功后才记录使用次数\n      if (!await checkAndUpdateUsage('speech')) {\n        setIsProcessing(false);\n        setInterimText('');\n        return;\n      }\n      \n      setExtractedText(text);\n      setInterimText('');\n      toast({\n        title: t('success.audioRecognized'),\n        description: t('success.description')\n      });\n    } catch (error: any) {\n      if (error.message !== '配额不足') {\n        toast({\n          title: t('error.audioProcessing'),\n          description: String(error),\n          variant: \"destructive\"\n        });\n      }\n    } finally {\n      setIsProcessing(false);\n    }\n  };\n\n  const handleSpeechFile = async (file: File): Promise<string> => {\n    if (!file.type.startsWith('audio/')) {\n      throw new Error(t('error.invalidAudioFile'))\n    }\n    return await recognizeAudioFile(file)\n  }\n\n  const handleVideoUpload = async (file: File) => {\n    if (!checkAuth()) return;\n\n    if (!file || !file.type.startsWith('video/')) {\n      toast({\n        title: t('error.invalidVideoFile'),\n        description: t('error.invalidVideoFileDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    // 检查配额\n    if (!hasRemainingQuota('video')) {\n      setShowSubscriptionDialog(true);\n      return;\n    }\n\n    setVideoFile(file);\n    setIsProcessing(true);\n    try {\n      if (videoService === 'zhipu') {\n        console.log(t('console.videoFramesExtracting'));\n        const frames = await extractVideoFrames(file);\n        console.log(t('console.videoFramesExtracted', [frames.length]));\n        console.log(t('console.videoFramesExample', [frames[0].substring(0, 100) + '...']));\n        const text = await analyzeVideoContent(frames);\n        console.log(t('console.videoProcessed', [text.length]));\n        setExtractedText(text);\n      } else if (videoService === 'aliyun') {\n        try {\n          // 直接上传到 OSS\n          const videoUrl = await uploadToOSS(file);\n          console.log(t('console.videoUploaded', [videoUrl]));\n\n          // 创建视频识别任务\n          const createResponse = await fetch('/api/aliyun/video-ocr/create', {\n            method: 'POST',\n            headers: {\n              'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({\n              videoUrl: videoUrl,\n            }),\n          });\n\n          if (!createResponse.ok) {\n            const errorText = await createResponse.text();\n            console.error(t('console.videoTaskCreateFailed', [errorText]));\n            throw new Error(t('error.videoProcessingDesc'));\n          }\n\n          const createResult = await createResponse.json();\n          if (!createResult.taskId) {\n            throw new Error(t('error.videoProcessingDesc'));\n          }\n\n          console.log(t('console.videoTaskPolling'));\n          const result = await pollTaskStatus(createResult.taskId);\n          \n          if (!result || !result.raw) {\n            throw new Error(t('error.videoProcessingDesc'));\n          }\n\n          // 处理OCR结果\n          console.log(t('console.videoOcrProcessing'));\n          \n          // 创建一个Map来存储时间戳对应的文本\n          const textMap = new Map<number, Set<string>>();\n\n          try {\n            // 尝试解析 Result 字符串\n            if (typeof result.raw === 'string') {\n              result.raw = JSON.parse(result.raw);\n            }\n\n            // 处理 OCR 结果\n            if (result.raw?.ocrResults?.length) {\n              result.raw.ocrResults.forEach((item: any) => {\n                if (item.detailInfo?.length) {\n                  item.detailInfo.forEach((detail: any) => {\n                    if (detail.text && detail.timeStamp) {\n                      const timestamp = Math.floor(detail.timeStamp / 1000) * 1000;\n                      const text = String(detail.text).trim();\n                      if (text.length >= 2 && /[\\u4e00-\\u9fa5a-zA-Z0-9]/.test(text)) {\n                        if (!textMap.has(timestamp)) {\n                          textMap.set(timestamp, new Set());\n                        }\n                        textMap.get(timestamp)?.add(text);\n                      }\n                    }\n                  });\n                }\n              });\n            }\n\n            // 处理视频 OCR 结果\n            if (result.raw?.videoOcrResults?.length) {\n              result.raw.videoOcrResults.forEach((item: any) => {\n                if (item.detailInfo?.length) {\n                  item.detailInfo.forEach((detail: any) => {\n                    if (detail.text && detail.timeStamp) {\n                      const timestamp = Math.floor(detail.timeStamp / 1000) * 1000;\n                      const text = String(detail.text).trim();\n                      if (text.length >= 2 && /[\\u4e00-\\u9fa5a-zA-Z0-9]/.test(text)) {\n                        if (!textMap.has(timestamp)) {\n                          textMap.set(timestamp, new Set());\n                        }\n                        textMap.get(timestamp)?.add(text);\n                      }\n                    }\n                  });\n                }\n              });\n            }\n\n            // 处理字幕结果\n            if (result.raw?.subtitlesResults?.[0]?.subtitlesChineseResults) {\n              const subtitles = result.raw.subtitlesResults[0].subtitlesChineseResults;\n              Object.entries(subtitles).forEach(([timeStr, text]: [string, any]) => {\n                // 过滤掉时间轴格式的文本\n                if (text && !timeStr.includes('-->') && !String(text).includes('-->')) {\n                  const textStr = String(text).trim();\n                  // 过滤掉 [object Object] 和其他无效内容\n                  if (textStr.length >= 2 && \n                      /[\\u4e00-\\u9fa5a-zA-Z0-9]/.test(textStr) && \n                      !textStr.includes('[object Object]') &&\n                      !textStr.match(/\\d{2}:\\d{2}:\\d{2},\\d{3}/)) {\n                    // 对于字幕，我们使用一个固定的时间戳，因为我们只关心文本内容\n                    const timestamp = 0;\n                    if (!textMap.has(timestamp)) {\n                      textMap.set(timestamp, new Set());\n                    }\n                    textMap.get(timestamp)?.add(textStr);\n                  }\n                }\n              });\n            }\n\n            // 按时间戳排序并合并文本，去重\n            const sortedTexts = Array.from(textMap.entries())\n              .sort(([a], [b]) => a - b)\n              .map(([_, texts]) => Array.from(texts).join(' '))\n              .filter(text => text.length > 0);\n\n            const combinedText = sortedTexts.join('\\n');\n            console.log(t('console.videoOcrResult', [combinedText]));\n            \n            if (combinedText) {\n              setSourceText(combinedText);\n              setExtractedText(combinedText);\n            } else {\n              console.log(t('console.videoNoText'));\n              toast({\n                title: t('error.noTextExtracted'),\n                description: t('error.noTextExtractedDesc'),\n                variant: \"destructive\"\n              });\n            }\n          } catch (error) {\n            console.error(t('console.videoOcrError'), error);\n            // 如果解析失败，尝试直接使用原始结果\n            if (result.raw && typeof result.raw === 'string') {\n              setSourceText(result.raw);\n              setExtractedText(result.raw);\n            }\n          }\n\n          setIsProcessing(false);\n          setVideoFile(null);\n          \n        } catch (uploadError: any) {\n          console.error(t('console.videoUploadError'), uploadError);\n          toast({\n            title: t('error.videoUploadFailed'),\n            description: uploadError.message || t('error.videoUploadFailedDesc'),\n            variant: \"destructive\"\n          });\n          return;\n        }\n      } else {\n        throw new Error(t('error.videoServiceNotSupported'));\n      }\n\n      // 处理成功后才记录使用次数\n      if (!await checkAndUpdateUsage('video')) {\n        setIsProcessing(false);\n        setVideoFile(null);\n        return;\n      }\n\n      toast({\n        title: t('success.videoExtracted'),\n        description: t('success.description')\n      });\n    } catch (error: any) {\n      console.error(t('console.videoProcessingError'), error);\n      toast({\n        title: t('error.videoProcessing'),\n        description: error.message || t('error.videoProcessingDesc'),\n        variant: \"destructive\"\n      });\n    } finally {\n      setIsProcessing(false);\n      setVideoFile(null);\n    }\n  };\n\n  const handleVideoFile = async (file: File): Promise<string> => {\n    if (!file.type.startsWith('video/')) {\n      throw new Error(t('error.invalidVideoFile'))\n    }\n    return await processVideoFile(file)\n  }\n\n  // 处理文件变更\n  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0]\n    if (!file) return\n\n    // 获取当前标签页类型\n    const currentTab = activeTab.toLowerCase()\n    \n    // 检查配额\n    if (currentTab !== 'text' && !hasRemainingQuota(currentTab as keyof Omit<QuotaInfo['usage'], 'text'>)) {\n      setShowSubscriptionDialog(true);\n      return;\n    }\n\n    // 根据不同类型处理文件\n    try {\n      switch (currentTab) {\n        case 'image':\n          if (!file.type.startsWith('image/')) {\n            throw new Error(t('error.invalidImageFile'))\n          }\n          await handleImageUpload(e)\n          break\n        \n        case 'file':\n          if (!file.name.toLowerCase().endsWith('.pdf') && file.type !== 'application/pdf') {\n            throw new Error(t('error.invalidPDFFile'))\n          }\n          await handlePDFUpload(e)\n          break\n        \n        case 'speech':\n          if (!file.type.startsWith('audio/')) {\n            throw new Error(t('error.invalidAudioFile'))\n          }\n          // 检查配额\n          if (!hasRemainingQuota('speech')) {\n            setShowSubscriptionDialog(true);\n            return;\n          }\n          // 记录使用次数\n          if (!await checkAndUpdateUsage('speech')) {\n            return;\n          }\n          await handleSpeechUpload(e)\n          break\n        \n        case 'video':\n          if (!file.type.startsWith('video/')) {\n            throw new Error(t('error.invalidVideoFile'))\n          }\n          await handleVideoUpload(file)\n          break\n      }\n    } catch (error: any) {\n      console.error(t('console.fileProcessingError'), error)\n      if (error.message !== '配额不足') {\n        toast({\n          variant: \"destructive\",\n          title: t('error'),\n          description: error.message || t('uploadFailed')\n        })\n      }\n    } finally {\n      // 清空文件输入框\n      e.target.value = ''\n    }\n  }\n\n  // 处理图片文件\n  const handleImageFile = async (file: File): Promise<string> => {\n    if (!file.type.startsWith('image/')) {\n      throw new Error(t('error.invalidImageFile'))\n    }\n    return new Promise((resolve, reject) => {\n      const reader = new FileReader()\n      reader.onloadend = () => resolve(reader.result as string)\n      reader.onerror = reject\n      reader.readAsDataURL(file)\n    })\n  }\n\n  // 处理PDF文件\n  const handlePDFFile = async (file: File): Promise<string> => {\n    if (!file.type.endsWith('pdf') && !file.type.startsWith('application/pdf')) {\n      throw new Error(t('error.invalidPDFFile'))\n    }\n    return await extractPDFWithKimi(file)\n  }\n\n  // 处理语音文件\n  const recognizeAudioFile = async (file: File): Promise<string> => {\n    // 检查配额\n    if (!hasRemainingQuota('speech')) {\n      setShowSubscriptionDialog(true);\n      throw new Error('配额不足');\n    }\n\n    // 记录使用次数\n    if (!await checkAndUpdateUsage('speech')) {\n      throw new Error('配额不足');\n    }\n\n    if (!asrServiceRef.current) {\n      asrServiceRef.current = new TencentASRService();\n    }\n\n    return await asrServiceRef.current.recognizeAudio(\n      file,\n      (progress) => {\n        setInterimText(progress);\n      },\n      (error) => {\n        toast({\n          title: t('error.audioRecognition'),\n          description: error,\n          variant: \"destructive\"\n        });\n      }\n    );\n  };\n\n  // 处理视频文件\n  const processVideoFile = async (file: File): Promise<string> => {\n    if (videoService === 'zhipu') {\n      const frames = await extractVideoFrames(file);\n      return await analyzeVideoContent(frames);\n    } else if (videoService === 'aliyun') {\n      const videoUrl = await uploadToOSS(file);\n      const createResponse = await fetch('/api/aliyun/video-ocr/create', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ videoUrl }),\n      });\n\n      if (!createResponse.ok) {\n        throw new Error(t('error.videoProcessingDesc'));\n      }\n\n      const createResult = await createResponse.json();\n      if (!createResult.taskId) {\n        throw new Error(t('error.videoProcessingDesc'));\n      }\n\n      const result = await pollTaskStatus(createResult.taskId);\n      if (!result || !result.raw) {\n        throw new Error(t('error.videoProcessingDesc'));\n      }\n\n      return result.raw;\n    }\n    \n    throw new Error(t('error.videoServiceNotSupported'));\n  };\n\n  // 处理文本翻译\n  const handleTextTranslate = async () => {\n    if (!sourceText) {\n      toast({\n        title: t('error.noText'),\n        description: t('error.noTextDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    if (!selectedLanguage) {\n      toast({\n        title: t('error.noLanguage'),\n        description: t('error.noLanguageDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    setIsProcessing(true);\n    try {\n      let result: string;\n      switch (translationService) {\n        case 'deepseek':\n          result = await translateWithDeepSeek(sourceText, selectedLanguage)\n          break;\n        case 'qwen':\n          result = await translateWithQwen(sourceText, selectedLanguage)\n          break;\n        case 'gemini':\n          result = await translateText(sourceText, selectedLanguage)\n          break;\n        case 'zhipu':\n          result = await translateWithZhipu(sourceText, selectedLanguage)\n          break;\n        case 'hunyuan':\n          result = await translateWithHunyuan(sourceText, selectedLanguage)\n          break;\n        case 'step':\n          result = await translateWithStepAPI(sourceText, selectedLanguage)\n          break;\n        default:\n          result = await translateWithDeepSeek(sourceText, selectedLanguage)\n      }\n      setTranslatedText(result);\n      toast({\n        title: t('success.translated'),\n        description: t('success.description')\n      });\n    } catch (error: any) {\n      toast({\n        title: t('errors.translationError'),\n        description: error.message || t('errors.translationDesc'),\n        variant: \"destructive\"\n      });\n    } finally {\n      setIsProcessing(false);\n    }\n  };\n\n  // 处理文本优化\n  const handleImprove = async () => {\n    if (!translatedText) {\n      toast({\n        title: t('error.noTranslation'),\n        description: t('error.noTranslationDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    setIsProcessing(true);\n    try {\n      const improved = await improveText(translatedText, selectedLanguage);\n      setTranslatedText(improved);\n      toast({\n        title: t('success.improved'),\n        description: t('success.description')\n      });\n    } catch (error: any) {\n      toast({\n        title: t('error.improving'),\n        description: error.message || t('error.improvingDesc'),\n        variant: \"destructive\"\n      });\n    } finally {\n      setIsProcessing(false);\n    }\n  };\n\n  // 处理语音识别开关\n  const toggleSpeechRecognition = async () => {\n    if (!checkAuth()) return;\n\n    // 检查配额\n    if (!isListening && !hasRemainingQuota('speech')) {\n      setShowSubscriptionDialog(true);\n      return;\n    }\n\n    if (!asrServiceRef.current) {\n      toast({\n        title: t('error.speechNotSupported'),\n        description: t('error.speechNotSupportedDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    if (isListening) {\n      if (recognition.current) {\n        recognition.current.stop();\n        recognition.current = null;\n      }\n      setIsListening(false);\n      setInterimText('');\n    } else {\n      // 记录使用次数\n      if (!await checkAndUpdateUsage('speech')) {\n        return;\n      }\n\n      const rec = await asrServiceRef.current.recognizeStream(\n        (text, isFinal) => {\n          if (isFinal) {\n            setExtractedText(text);\n            setInterimText('');\n            toast({\n              title: t('success.speechRecognized'),\n              description: t('success.description')\n            });\n          } else {\n            setInterimText(text);\n          }\n        },\n        (error) => {\n          toast({\n            title: t('error.speechRecognition'),\n            description: error,\n            variant: \"destructive\"\n          });\n          setIsListening(false);\n        }\n      );\n\n      if (rec) {\n        recognition.current = rec;\n        rec.start();\n        setIsListening(true);\n      }\n    }\n  };\n\n  // 处理图片文本提取\n  const handleExtractText = async () => {\n    if (!image) {\n      toast({\n        title: t('error.noImage'),\n        description: t('error.noImageDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    // 检查配额\n    if (!hasRemainingQuota('image')) {\n      setShowSubscriptionDialog(true);\n      return;\n    }\n\n    setIsProcessing(true);\n    try {\n      let result: string;\n      switch (ocrService) {\n        case 'tencent':\n          result = await extractTextWithTencent(image);\n          break;\n        case 'qwen':\n          result = await extractTextWithQwen(image);\n          break;\n        case 'gemini':\n          result = await extractTextFromImage(image);\n          break;\n        case 'zhipu':\n          result = await extractTextWithZhipu(image);\n          break;\n        case 'kimi':\n          result = await extractTextWithKimi(image);\n          break;\n        case 'step':\n          result = await extractTextWithStep(image);\n          break;\n        default:\n          result = await extractTextWithQwen(image);\n      }\n\n      // 处理成功后才记录使用次数\n      if (!await checkAndUpdateUsage('image')) {\n        setIsProcessing(false);\n        return;\n      }\n\n      setExtractedText(result);\n      toast({\n        title: t('success.extracted'),\n        description: t('success.description')\n      });\n    } catch (error: any) {\n      toast({\n        title: t('errors.extract.extractingError'),\n        description: error.message || t('errors.extract.extractingDesc'),\n        variant: \"destructive\"\n      });\n    } finally {\n      setIsProcessing(false);\n    }\n  };\n\n  // 处理翻译\n  const handleTranslate = async () => {\n    if (!extractedText && !fileContent || !selectedLanguage) {\n      toast({\n        title: t('error.translating'),\n        description: t('error.noLanguage'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    setIsProcessing(true);\n    try {\n      let result: string;\n      try {\n        switch (translationService) {\n          case 'deepseek':\n            result = await translateWithDeepSeek(extractedText || fileContent, selectedLanguage);\n            break;\n          case 'qwen':\n            result = await translateWithQwen(extractedText || fileContent, selectedLanguage);\n            break;\n          case 'gemini':\n            result = await translateText(extractedText || fileContent, selectedLanguage);\n            break;\n          case 'zhipu':\n            result = await translateWithZhipu(extractedText || fileContent, selectedLanguage);\n            break;\n          case 'hunyuan':\n            result = await translateWithHunyuan(extractedText || fileContent, selectedLanguage);\n            break;\n          case 'step':\n            result = await translateWithStepAPI(extractedText || fileContent, selectedLanguage);\n            break;\n          default:\n            result = await translateWithDeepSeek(extractedText || fileContent, selectedLanguage);\n        }\n      } catch (serviceError: any) {\n        console.error(`${translationService} ${t('console.translationServiceError')}:`, serviceError);\n        if (translationService !== 'deepseek') {\n          console.log(t('console.tryingDeepSeek'));\n          result = await translateWithDeepSeek(extractedText || fileContent, selectedLanguage);\n        } else {\n          throw serviceError;\n        }\n      }\n\n      setTranslatedText(result);\n      toast({\n        title: t('success.translated'),\n        description: t('success.description')\n      });\n    } catch (error: any) {\n      console.error(t('console.translationError'), error);\n      toast({\n        title: t('errors.translationError'),\n        description: error.message || t('errors.translationDesc'),\n        variant: \"destructive\"\n      });\n    } finally {\n      setIsProcessing(false);\n    }\n  };\n\n  const handleExtractPDFText = async () => {\n    if (!pdfFile) {\n      toast({\n        title: t('error.invalidFile'),\n        description: t('error.invalidFileDesc'),\n        variant: \"destructive\"\n      });\n      return;\n    }\n\n    // 检查配额\n    if (!hasRemainingQuota('pdf')) {\n      setShowSubscriptionDialog(true);\n      return;\n    }\n\n    try {\n      setIsFileProcessing(true);\n      \n      // 使用通用PDF处理函数，传入选择的服务商\n      console.log(t('console.pdfProcessing', [fileService]));\n      const content = await extractPDFContent(pdfFile, fileService as 'kimi' | 'mistral', (status) => {\n        console.log(t('console.pdfStatus', [status]));\n        toast({\n          title: status,\n          description: t('success.description')\n        });\n      });\n      \n      console.log(t('console.pdfProcessed', [content?.length || 0]));\n      console.log(t('console.pdfPreview', [content?.substring(0, 100)]));\n      \n      // 处理成功后才记录使用次数\n      if (!await checkAndUpdateUsage('pdf')) {\n        setIsFileProcessing(false);\n        return;\n      }\n      \n      // 将提取的内容设置到提取文本区域\n      setExtractedText(content);\n      // 同时设置到sourceText，确保翻译时能使用\n      setSourceText(content);\n      \n      toast({\n        title: t('success.fileExtracted'),\n        description: t('success.description')\n      });\n    } catch (error: any) {\n      console.error(t('console.fileProcessingError'), error);\n      toast({\n        title: t('error.fileProcessing'),\n        description: error.message || t('error.fileProcessingDesc'),\n        variant: \"destructive\"\n      });\n    } finally {\n      setIsFileProcessing(false);\n    }\n  };\n\n  useEffect(() => {\n    setMounted(true)\n  }, [])\n\n  // 在切换标签页时清空状态\n  const handleTabChange = (value: string) => {\n    setActiveTab(value);\n    setExtractedText('');\n    setTranslatedText('');\n    setInterimText('');\n    setSourceText('');\n    setSelectedLanguage('');\n    if (value !== 'image') {\n      setImage(null);\n    }\n    if (value !== 'video') {\n      setVideoFile(null);\n    }\n    if (value !== 'file') {\n      setFileContent('');\n    }\n  };\n\n  // 轮询任务状态\n  const pollTaskStatus = async (taskId: string) => {\n    let attempts = 0;\n    const POLL_INTERVAL = 5000; // 5秒\n    const MAX_POLL_ATTEMPTS = 60; // 最多轮询60次，即5分钟\n    \n    while (attempts < MAX_POLL_ATTEMPTS) {\n      try {\n        const response = await fetch('/api/aliyun/video-ocr/status', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify({ taskId }),\n        });\n\n        const result = await response.json();\n        console.log(t('console.videoTaskResult', [result]));\n        \n        if (result.status === 'success') {\n          return result.data;\n        }\n        \n        if (!result.success) {\n          throw new Error(result.message || t('console.videoTaskFailed'));\n        }\n        \n        await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));\n        attempts++;\n        \n      } catch (error) {\n        console.error(t('console.videoTaskQueryFailed'), error);\n        throw error;\n      }\n    }\n    \n    throw new Error(t('console.videoTaskTimeout'));\n  };\n\n  return (\n    <div className=\"container mx-auto px-4 py-8\">\n      {!mounted ? null : (\n        <>\n          <Card className=\"p-4 md:p-6\">\n            <Tabs defaultValue=\"text\" className=\"w-full\" onValueChange={handleTabChange}>\n              <TabsList className=\"grid w-full grid-cols-2 sm:grid-cols-5 gap-2 h-auto mb-6\">\n                <TabsTrigger value=\"text\" className=\"data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2\">\n                  <Languages className=\"w-4 h-4 mr-1 sm:mr-2\" />\n                  <span className=\"text-xs sm:text-sm\">{t('tabs.text')}</span>\n                </TabsTrigger>\n                <TabsTrigger value=\"image\" className=\"data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2\">\n                  <ImageIcon className=\"w-4 h-4 mr-1 sm:mr-2\" />\n                  <span className=\"text-xs sm:text-sm\">{t('tabs.image')}</span>\n                </TabsTrigger>\n                <TabsTrigger value=\"file\" className=\"data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2\">\n                  <FileType className=\"w-4 h-4 mr-1 sm:mr-2\" />\n                  <span className=\"text-xs sm:text-sm\">{t('tabs.pdf')}</span>\n                </TabsTrigger>\n                <TabsTrigger value=\"speech\" className=\"data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2\">\n                  {isListening ? <MicOff className=\"w-4 h-4 mr-1 sm:mr-2\" /> : <Mic className=\"w-4 h-4 mr-1 sm:mr-2\" />}\n                  <span className=\"text-xs sm:text-sm\">{t('tabs.speech')}</span>\n                </TabsTrigger>\n                <TabsTrigger value=\"video\" className=\"data-[state=active]:bg-primary/10 py-2 px-1 sm:px-2\">\n                  <Video className=\"w-4 h-4 mr-1 sm:mr-2\" />\n                  <span className=\"text-xs sm:text-sm\">{t('tabs.video')}</span>\n                </TabsTrigger>\n              </TabsList>\n\n              <TabsContent value=\"text\">\n                <div className=\"flex flex-col items-center justify-center gap-4\">\n                  <textarea\n                    value={sourceText}\n                    onChange={(e) => setSourceText(e.target.value)}\n                    placeholder={t('enterText')}\n                    className=\"w-full h-32 sm:h-40 p-4 rounded-lg border border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-primary text-sm sm:text-base\"\n                  />\n\n                  <div className=\"flex flex-col w-full gap-3\">\n                    <div className=\"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3\">\n                      <Select \n                        onValueChange={(value) => {\n                          console.log(t('console.selectedLanguage', [value]))\n                          setSelectedLanguage(value)\n                        }}\n                      >\n                        <SelectTrigger className=\"w-full\">\n                          <SelectValue placeholder={t('targetLanguage')} />\n                        </SelectTrigger>\n                        <SelectContent>\n                          {getLanguageCategories().map(category => (\n                            <SelectGroup key={category}>\n                              <SelectLabel>{category}</SelectLabel>\n                              {getLanguagesByCategory(category).map(language => (\n                                <SelectItem key={language.code} value={language.name}>\n                                  {language.nativeName} ({language.name})\n                                </SelectItem>\n                              ))}\n                            </SelectGroup>\n                          ))}\n                        </SelectContent>\n                      </Select>\n\n                      <Select onValueChange={setTranslationService} defaultValue=\"deepseek\">\n                        <SelectTrigger className=\"w-full\">\n                          <SelectValue placeholder={t('serviceProvider')} />\n                        </SelectTrigger>\n                        <SelectContent>\n                          <SelectItem value=\"deepseek\">{t('translationServices.deepseek')}</SelectItem>\n                          <SelectItem value=\"qwen\">{t('translationServices.qwen')}</SelectItem>\n                          <SelectItem value=\"gemini\">{t('translationServices.gemini')}</SelectItem>\n                          <SelectItem value=\"zhipu\">{t('translationServices.zhipu')}</SelectItem>\n                          <SelectItem value=\"hunyuan\">{t('translationServices.hunyuan')}</SelectItem>\n                          <SelectItem value=\"4o-mini\">{t('translationServices.4o-mini')}</SelectItem>\n                          <SelectItem value=\"minnimax\">{t('translationServices.minnimax')}</SelectItem>\n                          <SelectItem value=\"siliconflow\">{t('translationServices.siliconflow')}</SelectItem>\n                          <SelectItem value=\"claude_3_5\">{t('translationServices.claude_3_5')}</SelectItem>\n                          <SelectItem value=\"kimi\">{t('translationServices.kimi')}</SelectItem>\n                          <SelectItem value=\"step\">{t('translationServices.step')}</SelectItem>\n                        </SelectContent>\n                      </Select>\n\n                      <Button\n                        onClick={handleTextTranslate}\n                        disabled={!sourceText || !selectedLanguage || isProcessing}\n                        className=\"w-full\"\n                      >\n                        {isProcessing ? (\n                          <div className=\"flex items-center justify-center\">\n                            <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                            <span className=\"text-sm\">{t('translating')}</span>\n                          </div>\n                        ) : (\n                          <div className=\"flex items-center justify-center\">\n                            <Languages className=\"mr-2 h-4 w-4\" />\n                            <span className=\"text-sm\">{t('buttons.startTranslate')}</span>\n                          </div>\n                        )}\n                      </Button>\n\n                      <TooltipProvider>\n                        <Tooltip>\n                          <TooltipTrigger asChild>\n                            <Button\n                              onClick={handleImprove}\n                              disabled={!translatedText || isProcessing}\n                              variant=\"outline\"\n                              className=\"w-full\"\n                            >\n                              <Wand2 className=\"mr-2 h-4 w-4\" />\n                              <span className=\"text-sm\">{t('improveTranslation')}</span>\n                            </Button>\n                          </TooltipTrigger>\n                          <TooltipContent>\n                            <p className=\"text-sm\">{t('improveTooltip')}</p>\n                          </TooltipContent>\n                        </Tooltip>\n                      </TooltipProvider>\n                    </div>\n                  </div>\n\n                  {translatedText && (\n                    <div className=\"w-full mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2 text-sm sm:text-base\">{t('translatedText')}</h3>\n                      <p className=\"whitespace-pre-wrap text-sm sm:text-base\">{translatedText}</p>\n                    </div>\n                  )}\n                </div>\n              </TabsContent>\n\n              <TabsContent value=\"image\">\n                <div className=\"flex flex-col items-center justify-center gap-4\">\n                  <div className=\"text-sm text-gray-500 mb-2\">\n                    {getRemainingUsageText('image')}\n                  </div>\n                  <Card\n                    className={`w-full max-w-xl h-48 flex items-center justify-center border-2 border-dashed ${\n                      isDragging ? 'border-primary' : 'border-muted-foreground'\n                    } relative overflow-hidden`}\n                    onDragOver={handleDragOver}\n                    onDragLeave={handleDragLeave}\n                    onDrop={handleDrop}\n                  >\n                    {image ? (\n                      <div className=\"relative w-full h-full\">\n                        <img\n                          src={image}\n                          alt=\"Uploaded\"\n                          className=\"w-full h-full object-contain\"\n                        />\n                        <Button\n                          variant=\"ghost\"\n                          size=\"icon\"\n                          className=\"absolute top-2 right-2\"\n                          onClick={() => setImage(null)}\n                        >\n                          <X className=\"h-4 w-4\" />\n                        </Button>\n                      </div>\n                    ) : (\n                      <div className=\"flex flex-col items-center justify-center text-center p-4\">\n                        <Upload className=\"h-8 w-8 mb-4 text-muted-foreground\" />\n                        <p className=\"text-sm text-muted-foreground mb-2\">{t('dragAndDrop')}</p>\n                        <div className=\"relative\">\n                          <Button variant=\"secondary\" size=\"sm\" onClick={() => {\n                            if (!checkAuth()) return;\n                          }}>\n                            {t('selectImage')}\n                            <input\n                              type=\"file\"\n                              accept=\"image/*\"\n                              onChange={handleImageUpload}\n                              className=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n                              onClick={(e) => {\n                                if (!checkAuth()) {\n                                  e.preventDefault();\n                                }\n                              }}\n                            />\n                          </Button>\n                        </div>\n                      </div>\n                    )}\n                  </Card>\n\n                  <div className=\"flex flex-col gap-4\">\n                    <div className=\"flex flex-col sm:flex-row justify-center items-center gap-3 sm:gap-4\">\n                      <Select onValueChange={setOcrService} defaultValue=\"qwen\">\n                        <SelectTrigger className=\"w-full sm:w-40\">\n                          <SelectValue placeholder={t('serviceProvider')} />\n                        </SelectTrigger>\n                        <SelectContent>\n                          <SelectItem value=\"tencent\">{t('ocrServices.tencent')}</SelectItem>\n                          <SelectItem value=\"qwen\">{t('ocrServices.qwen')}</SelectItem>\n                          <SelectItem value=\"gemini\">{t('ocrServices.gemini')}</SelectItem>\n                          <SelectItem value=\"zhipu\">{t('ocrServices.zhipu')}</SelectItem>\n                          <SelectItem value=\"kimi\">{t('ocrServices.kimi')}</SelectItem>\n                          <SelectItem value=\"step\">{t('ocrServices.step')}</SelectItem>\n                        </SelectContent>\n                      </Select>\n\n                      <Button\n                        onClick={handleExtractText}\n                        disabled={!image || isProcessing}\n                        className=\"w-full sm:w-40\"\n                      >\n                        {isProcessing ? (\n                          <div className=\"flex items-center\">\n                            <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                            <span>{t('extractingStatus')}</span>\n                          </div>\n                        ) : (\n                          <div className=\"flex items-center\">\n                            <FileText className=\"mr-2 h-4 w-4\" />\n                            <span>{t('extractAction')}</span>\n                          </div>\n                        )}\n                      </Button>\n                    </div>\n\n                    <div className=\"flex flex-col sm:flex-row justify-center items-center gap-3 sm:gap-4\">\n                      <Select onValueChange={setSelectedLanguage}>\n                        <SelectTrigger className=\"w-full sm:w-40\">\n                          <SelectValue placeholder={t('targetLanguage')} />\n                        </SelectTrigger>\n                        <SelectContent>\n                          {getLanguageCategories().map(category => (\n                            <SelectGroup key={category}>\n                              <SelectLabel>{category}</SelectLabel>\n                              {getLanguagesByCategory(category).map(language => (\n                                <SelectItem key={language.code} value={language.name}>\n                                  {language.nativeName} ({language.name})\n                                </SelectItem>\n                              ))}\n                            </SelectGroup>\n                          ))}\n                        </SelectContent>\n                      </Select>\n\n                      <Select onValueChange={setTranslationService} defaultValue=\"deepseek\">\n                        <SelectTrigger className=\"w-full sm:w-40\">\n                          <SelectValue placeholder={t('serviceProvider')} />\n                        </SelectTrigger>\n                        <SelectContent>\n                          <SelectItem value=\"deepseek\">{t('translationServices.deepseek')}</SelectItem>\n                          <SelectItem value=\"qwen\">{t('translationServices.qwen')}</SelectItem>\n                          <SelectItem value=\"gemini\">{t('translationServices.gemini')}</SelectItem>\n                          <SelectItem value=\"zhipu\">{t('translationServices.zhipu')}</SelectItem>\n                          <SelectItem value=\"hunyuan\">{t('translationServices.hunyuan')}</SelectItem>\n                          <SelectItem value=\"4o-mini\">{t('translationServices.4o-mini')}</SelectItem>\n                          <SelectItem value=\"minnimax\">{t('translationServices.minnimax')}</SelectItem>\n                          <SelectItem value=\"siliconflow\">{t('translationServices.siliconflow')}</SelectItem>\n                          <SelectItem value=\"claude_3_5\">{t('translationServices.claude_3_5')}</SelectItem>\n                          <SelectItem value=\"kimi\">{t('translationServices.kimi')}</SelectItem>\n                          <SelectItem value=\"step\">{t('translationServices.step')}</SelectItem>\n                        </SelectContent>\n                      </Select>\n\n                      <Button\n                        onClick={handleTranslate}\n                        disabled={!extractedText || !selectedLanguage || isProcessing}\n                        className=\"w-full sm:w-40\"\n                      >\n                        {isProcessing ? (\n                          <>\n                            <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                            {t('translating')}\n                          </>\n                        ) : (\n                          <>\n                            <Languages className=\"mr-2 h-4 w-4\" />\n                            {t('buttons.startTranslate')}\n                          </>\n                        )}\n                      </Button>\n\n                      <TooltipProvider>\n                        <Tooltip>\n                          <TooltipTrigger asChild>\n                            <Button\n                              onClick={handleImprove}\n                              disabled={!translatedText || isProcessing}\n                              variant=\"outline\"\n                              className=\"w-full sm:w-40\"\n                            >\n                              <Wand2 className=\"mr-2 h-4 w-4\" />\n                              {t('improveTranslation')}\n                            </Button>\n                          </TooltipTrigger>\n                          <TooltipContent>\n                            <p>{t('improveTooltip')}</p>\n                          </TooltipContent>\n                        </Tooltip>\n                      </TooltipProvider>\n                    </div>\n                  </div>\n\n                  {extractedText && (\n                    <div className=\"w-full max-w-2xl mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2\">{t('extractedText')}</h3>\n                      <p className=\"whitespace-pre-wrap\">{extractedText}</p>\n                    </div>\n                  )}\n\n                  {translatedText && (\n                    <div className=\"w-full max-w-2xl mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2\">{t('translatedText')}</h3>\n                      <p className=\"whitespace-pre-wrap\">{translatedText}</p>\n                    </div>\n                  )}\n                </div>\n              </TabsContent>\n\n              <TabsContent value=\"file\">\n                <div className=\"flex flex-col items-center justify-center gap-4\">\n                  <div className=\"text-sm text-gray-500 mb-2\">\n                    {getRemainingUsageText('pdf')}\n                  </div>\n                  <Card\n                    className={cn(\n                      \"w-full max-w-2xl h-48 flex items-center justify-center border-2 border-dashed\",\n                      isDragging ? \"border-primary\" : \"border-muted-foreground\",\n                      \"relative overflow-hidden\"\n                    )}\n                    onDragOver={handleDragOver}\n                    onDragLeave={handleDragLeave}\n                    onDrop={(e) => {\n                      e.preventDefault();\n                      e.stopPropagation();\n                      setIsDragging(false);\n\n                      if (!checkAuth()) return;\n\n                      // 检查配额\n                      if (!hasRemainingQuota('pdf')) {\n                        setShowSubscriptionDialog(true);\n                        return;\n                      }\n\n                      const file = e.dataTransfer.files[0];\n                      if (file && (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'))) {\n                        handlePDFUpload({ target: { files: [file] } } as any);\n                      } else {\n                        toast({\n                          title: t('error.invalidFile'),\n                          description: t('error.invalidFileDesc'),\n                          variant: \"destructive\"\n                        });\n                      }\n                    }}\n                  >\n                    {isFileProcessing ? (\n                      <div className=\"flex flex-col items-center justify-center text-center p-4\">\n                        <Loader2 className=\"h-8 w-8 mb-4 animate-spin text-primary\" />\n                        <p className=\"text-sm text-muted-foreground\">{t('processingStatus')}</p>\n                      </div>\n                    ) : pdfPreview ? (\n                      <div className=\"relative w-full h-full flex items-center justify-center\">\n                        <div className=\"absolute inset-0 flex items-center justify-center\">\n                          <FileText className=\"h-16 w-16 text-primary opacity-20\" />\n                        </div>\n                        <div className=\"absolute top-2 right-2 z-10\">\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={() => {\n                              setPdfPreview(null);\n                              setPdfFile(null);\n                            }}\n                          >\n                            <X className=\"h-4 w-4\" />\n                          </Button>\n                        </div>\n                        <div className=\"absolute bottom-2 left-2 z-10 bg-background/80 px-2 py-1 rounded text-xs\">\n                          {pdfFile?.name}\n                        </div>\n                      </div>\n                    ) : (\n                      <div className=\"flex flex-col items-center justify-center text-center p-4\">\n                        <FileText className=\"h-8 w-8 mb-4 text-muted-foreground\" />\n                        <p className=\"text-sm text-muted-foreground mb-2\">{t('dragAndDropPDF')}</p>\n                        <div className=\"relative\">\n                          <Button \n                            variant=\"secondary\" \n                            size=\"sm\"\n                            className={cn(\n                              \"w-full\",\n                              !hasRemainingQuota('pdf') && \"opacity-50\"\n                            )}\n                            onClick={() => {\n                              if (!checkAuth()) return;\n                            }}\n                          >\n                            {t('selectPDF')}\n                            <input\n                              type=\"file\"\n                              accept=\".pdf,application/pdf\"\n                              onChange={handlePDFUpload}\n                              className={cn(\n                                \"absolute inset-0 w-full h-full opacity-0\",\n                                hasRemainingQuota('pdf') ? \"cursor-pointer\" : \"cursor-not-allowed\"\n                              )}\n                              onClick={(e) => {\n                                if (!checkAuth()) {\n                                  e.preventDefault();\n                                  return;\n                                }\n                                if (!hasRemainingQuota('pdf')) {\n                                  e.preventDefault();\n                                  setShowSubscriptionDialog(true);\n                                }\n                              }}\n                              disabled={isFileProcessing}\n                            />\n                          </Button>\n                        </div>\n                      </div>\n                    )}\n                  </Card>\n\n                  <div className=\"flex flex-col sm:flex-row justify-center items-center gap-3 sm:gap-4 mb-4\">\n                    {/* 添加PDF处理服务商选择 */}\n                    <Select onValueChange={setFileService} defaultValue=\"mistral\">\n                      <SelectTrigger className=\"w-full sm:w-40\">\n                        <SelectValue placeholder={t('serviceProvider')} />\n                      </SelectTrigger>\n                      <SelectContent>\n                        <SelectItem value=\"mistral\">Mistral OCR</SelectItem>\n                        <SelectItem value=\"kimi\">Kimi</SelectItem>\n                      </SelectContent>\n                    </Select>\n\n                    <Button\n                      onClick={handleExtractPDFText}\n                      disabled={!pdfFile || isFileProcessing}\n                      className=\"w-full sm:w-40\"\n                    >\n                      {isFileProcessing ? (\n                        <div className=\"flex items-center justify-center\">\n                          <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                          <span className=\"text-sm\">{t('extractingStatus')}</span>\n                        </div>\n                      ) : (\n                        <div className=\"flex items-center justify-center\">\n                          <FileText className=\"mr-2 h-4 w-4\" />\n                          <span className=\"text-sm\">{t('extractAction')}</span>\n                        </div>\n                      )}\n                    </Button>\n                  </div>\n\n                  <div className=\"flex flex-col sm:flex-row justify-center items-center gap-3 sm:gap-4\">\n                    <Select onValueChange={setSelectedLanguage}>\n                      <SelectTrigger className=\"w-full sm:w-40\">\n                        <SelectValue placeholder={t('targetLanguage')} />\n                      </SelectTrigger>\n                      <SelectContent>\n                        {getLanguageCategories().map(category => (\n                          <SelectGroup key={category}>\n                            <SelectLabel>{category}</SelectLabel>\n                            {getLanguagesByCategory(category).map(language => (\n                              <SelectItem key={language.code} value={language.name}>\n                                {language.nativeName} ({language.name})\n                              </SelectItem>\n                            ))}\n                          </SelectGroup>\n                        ))}\n                      </SelectContent>\n                    </Select>\n\n                    <Select onValueChange={setTranslationService} defaultValue=\"deepseek\">\n                      <SelectTrigger className=\"w-full sm:w-40\">\n                        <SelectValue placeholder={t('serviceProvider')} />\n                      </SelectTrigger>\n                      <SelectContent>\n                        <SelectItem value=\"deepseek\">{t('translationServices.deepseek')}</SelectItem>\n                        <SelectItem value=\"qwen\">{t('translationServices.qwen')}</SelectItem>\n                        <SelectItem value=\"gemini\">{t('translationServices.gemini')}</SelectItem>\n                        <SelectItem value=\"zhipu\">{t('translationServices.zhipu')}</SelectItem>\n                        <SelectItem value=\"hunyuan\">{t('translationServices.hunyuan')}</SelectItem>\n                        <SelectItem value=\"4o-mini\">{t('translationServices.4o-mini')}</SelectItem>\n                        <SelectItem value=\"minnimax\">{t('translationServices.minnimax')}</SelectItem>\n                        <SelectItem value=\"siliconflow\">{t('translationServices.siliconflow')}</SelectItem>\n                        <SelectItem value=\"claude_3_5\">{t('translationServices.claude_3_5')}</SelectItem>\n                        <SelectItem value=\"kimi\">{t('translationServices.kimi')}</SelectItem>\n                        <SelectItem value=\"step\">{t('translationServices.step')}</SelectItem>\n                      </SelectContent>\n                    </Select>\n\n                    <Button\n                      onClick={handleTranslate}\n                      disabled={!extractedText && !fileContent || !selectedLanguage || isProcessing}\n                      className=\"w-full sm:w-40\"\n                    >\n                      <Languages className=\"mr-2 h-4 w-4\" />\n                      {isProcessing ? t('translating') : t('buttons.startTranslate')}\n                    </Button>\n\n                    <TooltipProvider>\n                      <Tooltip>\n                        <TooltipTrigger asChild>\n                          <Button\n                            onClick={handleImprove}\n                            disabled={!translatedText || isProcessing}\n                            variant=\"outline\"\n                            className=\"w-full sm:w-40\"\n                          >\n                            <Wand2 className=\"mr-2 h-4 w-4\" />\n                            {t('improveTranslation')}\n                          </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                          <p>{t('improveTooltip')}</p>\n                        </TooltipContent>\n                      </Tooltip>\n                    </TooltipProvider>\n                  </div>\n\n                  {extractedText && (\n                    <div className=\"w-full max-w-2xl mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2\">{t('extractedText')}</h3>\n                      <p className=\"whitespace-pre-wrap\">{extractedText}</p>\n                    </div>\n                  )}\n\n                  {translatedText && (\n                    <div className=\"w-full max-w-2xl mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2\">{t('translatedText')}</h3>\n                      <p className=\"whitespace-pre-wrap\">{translatedText}</p>\n                    </div>\n                  )}\n                </div>\n              </TabsContent>\n\n              <TabsContent value=\"speech\">\n                <div className=\"flex flex-col items-center justify-center gap-4\">\n                  <div className=\"text-sm text-gray-500 mb-2\">\n                    {getRemainingUsageText('speech')}\n                  </div>\n                  <div className=\"flex flex-col sm:flex-row items-center justify-center gap-4 w-full\">\n                    <div className=\"relative w-[240px]\">\n                      <Button\n                        variant=\"default\"\n                        className={cn(\n                          \"w-full\",\n                          !hasRemainingQuota('speech') && \"opacity-50\"\n                        )}\n                      >\n                        <Upload className=\"mr-2 h-4 w-4\" />\n                        {t('uploadAudio')}\n                        <input\n                          type=\"file\"\n                          accept=\"audio/*\"\n                          onChange={handleSpeechUpload}\n                          className={cn(\n                            \"absolute inset-0 w-full h-full opacity-0\",\n                            hasRemainingQuota('speech') ? \"cursor-pointer\" : \"cursor-not-allowed\"\n                          )}\n                          onClick={(e) => {\n                            if (!checkAuth()) {\n                              e.preventDefault();\n                              return;\n                            }\n                            if (!hasRemainingQuota('speech')) {\n                              e.preventDefault();\n                              setShowSubscriptionDialog(true);\n                            }\n                          }}\n                        />\n                      </Button>\n                    </div>\n\n                    <div className=\"w-[240px]\">\n                      <Button\n                        onClick={() => {\n                          if (!checkAuth()) return;\n                          toggleSpeechRecognition();\n                        }}\n                        variant={isListening ? \"destructive\" : \"outline\"}\n                        className=\"w-full\"\n                        disabled={isProcessing || !asrServiceRef.current}\n                      >\n                        {isListening ? (\n                          <>\n                            <MicOff className=\"mr-2 h-4 w-4\" />\n                            {t('stopListening')}\n                          </>\n                        ) : (\n                          <>\n                            <Mic className=\"mr-2 h-4 w-4\" />\n                            {t('startListening')}\n                          </>\n                        )}\n                      </Button>\n                    </div>\n                  </div>\n\n                  {isProcessing && (\n                    <div className=\"w-full max-w-md p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <p className=\"text-sm text-gray-600 dark:text-gray-300\">{t('processing')}</p>\n                    </div>\n                  )}\n\n                  {interimText && (\n                    <div className=\"w-full max-w-2xl mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2\">{t('interimText')}</h3>\n                      <p className=\"whitespace-pre-wrap\">{interimText}</p>\n                    </div>\n                  )}\n\n                  {extractedText && (\n                    <div className=\"w-full max-w-2xl mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2\">{t('extractedText')}</h3>\n                      <p className=\"whitespace-pre-wrap\">{extractedText}</p>\n                    </div>\n                  )}\n\n                  <div className=\"flex flex-col sm:flex-row justify-center items-center gap-3 sm:gap-4\">\n                    <Select onValueChange={setSelectedLanguage}>\n                      <SelectTrigger className=\"w-full sm:w-40\">\n                        <SelectValue placeholder={t('targetLanguage')} />\n                      </SelectTrigger>\n                      <SelectContent>\n                        {getLanguageCategories().map(category => (\n                          <SelectGroup key={category}>\n                            <SelectLabel>{category}</SelectLabel>\n                            {getLanguagesByCategory(category).map(language => (\n                              <SelectItem key={language.code} value={language.name}>\n                                {language.nativeName} ({language.name})\n                              </SelectItem>\n                            ))}\n                          </SelectGroup>\n                        ))}\n                      </SelectContent>\n                    </Select>\n\n                    <Select onValueChange={setTranslationService} defaultValue=\"deepseek\">\n                      <SelectTrigger className=\"w-full sm:w-40\">\n                        <SelectValue placeholder={t('serviceProvider')} />\n                      </SelectTrigger>\n                      <SelectContent>\n                        <SelectItem value=\"deepseek\">{t('translationServices.deepseek')}</SelectItem>\n                        <SelectItem value=\"qwen\">{t('translationServices.qwen')}</SelectItem>\n                        <SelectItem value=\"gemini\">{t('translationServices.gemini')}</SelectItem>\n                        <SelectItem value=\"zhipu\">{t('translationServices.zhipu')}</SelectItem>\n                        <SelectItem value=\"hunyuan\">{t('translationServices.hunyuan')}</SelectItem>\n                        <SelectItem value=\"4o-mini\">{t('translationServices.4o-mini')}</SelectItem>\n                        <SelectItem value=\"minnimax\">{t('translationServices.minnimax')}</SelectItem>\n                        <SelectItem value=\"siliconflow\">{t('translationServices.siliconflow')}</SelectItem>\n                        <SelectItem value=\"claude_3_5\">{t('translationServices.claude_3_5')}</SelectItem>\n                        <SelectItem value=\"kimi\">{t('translationServices.kimi')}</SelectItem>\n                        <SelectItem value=\"step\">{t('translationServices.step')}</SelectItem>\n                      </SelectContent>\n                    </Select>\n\n                    <Button\n                      onClick={handleTranslate}\n                      disabled={!extractedText || !selectedLanguage || isProcessing}\n                      className=\"w-full sm:w-40\"\n                    >\n                      {isProcessing ? (\n                        <>\n                          <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                          {t('translating')}\n                        </>\n                      ) : (\n                        <>\n                          <Languages className=\"mr-2 h-4 w-4\" />\n                          {t('buttons.startTranslate')}\n                        </>\n                      )}\n                    </Button>\n\n                    <TooltipProvider>\n                      <Tooltip>\n                        <TooltipTrigger asChild>\n                          <Button\n                            onClick={handleImprove}\n                            disabled={!translatedText || isProcessing}\n                            variant=\"outline\"\n                            className=\"w-full sm:w-40\"\n                          >\n                            <Wand2 className=\"mr-2 h-4 w-4\" />\n                            {t('improveTranslation')}\n                          </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                          <p>{t('improveTooltip')}</p>\n                        </TooltipContent>\n                      </Tooltip>\n                    </TooltipProvider>\n                  </div>\n\n                  {translatedText && (\n                    <div className=\"w-full max-w-2xl mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2\">{t('translatedText')}</h3>\n                      <p className=\"whitespace-pre-wrap\">{translatedText}</p>\n                    </div>\n                  )}\n                </div>\n              </TabsContent>\n\n              <TabsContent value=\"video\">\n                <div className=\"flex flex-col items-center justify-center gap-4\">\n                  <div className=\"text-sm text-gray-500 mb-2\">\n                    {getRemainingUsageText('video')}\n                  </div>\n                  <div className=\"flex flex-col sm:flex-row gap-4 w-full max-w-xl justify-center\">\n                    <Select onValueChange={setVideoService} defaultValue=\"zhipu\">\n                      <SelectTrigger className=\"w-full sm:w-40\">\n                        <SelectValue placeholder={t('translate.selectVideoService')} />\n                      </SelectTrigger>\n                      <SelectContent>\n                        <SelectItem value=\"zhipu\">{t('translate.zhipu')}</SelectItem>\n                        <SelectItem value=\"aliyun\">{t('translate.aliyun')}</SelectItem>\n                      </SelectContent>\n                    </Select>\n\n                    <div className=\"relative w-[240px] mx-auto\">\n                      <Button\n                        variant=\"default\"\n                        className={cn(\n                          \"w-full\",\n                          !hasRemainingQuota('video') && \"opacity-50\"\n                        )}\n                      >\n                        <Video className=\"mr-2 h-4 w-4\" />\n                        {t('uploadVideo')}\n                        <input\n                          type=\"file\"\n                          accept=\"video/*\"\n                          onChange={(e) => {\n                            // 阻止事件冒泡\n                            e.stopPropagation();\n                            \n                            if (!checkAuth()) {\n                              e.preventDefault();\n                              return;\n                            }\n                            \n                            // 检查配额\n                            if (!hasRemainingQuota('video')) {\n                              e.preventDefault();\n                              setShowSubscriptionDialog(true);\n                              return;\n                            }\n                            \n                            const file = e.target.files?.[0];\n                            if (file) {\n                              handleVideoUpload(file);\n                            }\n                          }}\n                          className={cn(\n                            \"absolute inset-0 w-full h-full opacity-0\",\n                            hasRemainingQuota('video') ? \"cursor-pointer\" : \"cursor-not-allowed\"\n                          )}\n                          onClick={(e) => {\n                            if (!checkAuth()) {\n                              e.preventDefault();\n                              return;\n                            }\n                            if (!hasRemainingQuota('video')) {\n                              e.preventDefault();\n                              setShowSubscriptionDialog(true);\n                            }\n                          }}\n                        />\n                      </Button>\n                    </div>\n                  </div>\n\n                  {videoFile && (\n                    <div className=\"text-sm text-gray-500\">\n                      {videoFile.name}\n                    </div>\n                  )}\n\n                  {isProcessing && (\n                    <div className=\"w-full max-w-md p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <p className=\"text-sm text-gray-600 dark:text-gray-300\">{t('processing')}</p>\n                    </div>\n                  )}\n\n                  {extractedText && (\n                    <div className=\"w-full max-w-2xl mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2\">{t('extractedText')}</h3>\n                      <p className=\"whitespace-pre-wrap\">{extractedText}</p>\n                    </div>\n                  )}\n\n                  <div className=\"flex flex-col sm:flex-row justify-center items-center gap-3 sm:gap-4\">\n                    <Select onValueChange={setSelectedLanguage}>\n                      <SelectTrigger className=\"w-full sm:w-40\">\n                        <SelectValue placeholder={t('targetLanguage')} />\n                      </SelectTrigger>\n                      <SelectContent>\n                        {getLanguageCategories().map(category => (\n                          <SelectGroup key={category}>\n                            <SelectLabel>{category}</SelectLabel>\n                            {getLanguagesByCategory(category).map(language => (\n                              <SelectItem key={language.code} value={language.name}>\n                                {language.nativeName} ({language.name})\n                              </SelectItem>\n                            ))}\n                          </SelectGroup>\n                        ))}\n                      </SelectContent>\n                    </Select>\n\n                    <Select onValueChange={setTranslationService} defaultValue=\"deepseek\">\n                      <SelectTrigger className=\"w-full sm:w-40\">\n                        <SelectValue placeholder={t('serviceProvider')} />\n                      </SelectTrigger>\n                      <SelectContent>\n                        <SelectItem value=\"deepseek\">{t('translationServices.deepseek')}</SelectItem>\n                        <SelectItem value=\"qwen\">{t('translationServices.qwen')}</SelectItem>\n                        <SelectItem value=\"gemini\">{t('translationServices.gemini')}</SelectItem>\n                        <SelectItem value=\"zhipu\">{t('translationServices.zhipu')}</SelectItem>\n                        <SelectItem value=\"hunyuan\">{t('translationServices.hunyuan')}</SelectItem>\n                        <SelectItem value=\"4o-mini\">{t('translationServices.4o-mini')}</SelectItem>\n                        <SelectItem value=\"minnimax\">{t('translationServices.minnimax')}</SelectItem>\n                        <SelectItem value=\"siliconflow\">{t('translationServices.siliconflow')}</SelectItem>\n                        <SelectItem value=\"claude_3_5\">{t('translationServices.claude_3_5')}</SelectItem>\n                        <SelectItem value=\"kimi\">{t('translationServices.kimi')}</SelectItem>\n                        <SelectItem value=\"step\">{t('translationServices.step')}</SelectItem>\n                      </SelectContent>\n                    </Select>\n\n                    <Button\n                      onClick={handleTranslate}\n                      disabled={!extractedText || !selectedLanguage || isProcessing}\n                      className=\"w-full sm:w-40\"\n                    >\n                      {isProcessing ? (\n                        <>\n                          <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                          {t('translating')}\n                        </>\n                      ) : (\n                        <>\n                          <Languages className=\"mr-2 h-4 w-4\" />\n                          {t('buttons.startTranslate')}\n                        </>\n                      )}\n                    </Button>\n\n                    <TooltipProvider>\n                      <Tooltip>\n                        <TooltipTrigger asChild>\n                          <Button\n                            onClick={handleImprove}\n                            disabled={!translatedText || isProcessing}\n                            variant=\"outline\"\n                            className=\"w-full sm:w-40\"\n                          >\n                            <Wand2 className=\"mr-2 h-4 w-4\" />\n                            {t('improveTranslation')}\n                          </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                          <p>{t('improveTooltip')}</p>\n                        </TooltipContent>\n                      </Tooltip>\n                    </TooltipProvider>\n                  </div>\n\n                  {translatedText && (\n                    <div className=\"w-full max-w-2xl mx-auto p-4 bg-gray-100 dark:bg-gray-800 rounded-lg\">\n                      <h3 className=\"font-medium mb-2\">{t('translatedText')}</h3>\n                      <p className=\"whitespace-pre-wrap\">{translatedText}</p>\n                    </div>\n                  )}\n                </div>\n              </TabsContent>\n            </Tabs>\n          </Card>\n\n          <AlertDialog open={showAuthDialog} onOpenChange={setShowAuthDialog}>\n            <AlertDialogContent>\n              <AlertDialogHeader>\n                <AlertDialogTitle>{t('error.authRequired')}</AlertDialogTitle>\n                <AlertDialogDescription>\n                  {t('error.pleaseLogin')}\n                </AlertDialogDescription>\n              </AlertDialogHeader>\n              <AlertDialogFooter className=\"flex flex-col sm:flex-row gap-2\">\n                <AlertDialogCancel>{t('error.cancelButton')}</AlertDialogCancel>\n                <div className=\"flex gap-2\">\n                  <AlertDialogAction onClick={handleRegister} className=\"bg-primary hover:bg-primary/90\">\n                    {t('error.registerButton')}\n                  </AlertDialogAction>\n                  <AlertDialogAction onClick={handleLogin} className=\"bg-primary hover:bg-primary/90\">\n                    {t('error.loginButton')}\n                  </AlertDialogAction>\n                </div>\n              </AlertDialogFooter>\n            </AlertDialogContent>\n          </AlertDialog>\n\n          <SubscriptionDialog\n            open={showSubscriptionDialog}\n            onOpenChange={setShowSubscriptionDialog}\n          />\n        </>\n      )}\n    </div>\n  );\n}"
  },
  {
    "path": "components/client-layout.tsx",
    "content": "\"use client\"\n\nimport { ThemeProvider } from '@/components/theme-provider';\nimport { LanguageProvider } from '@/components/language-provider';\nimport { Toaster } from '@/components/ui/toaster';\n\nexport function ClientLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <ThemeProvider\n      attribute=\"class\"\n      defaultTheme=\"system\"\n      enableSystem\n      disableTransitionOnChange\n    >\n      <LanguageProvider>\n        {children}\n        <Toaster />\n      </LanguageProvider>\n    </ThemeProvider>\n  );\n} "
  },
  {
    "path": "components/footer.tsx",
    "content": "\"use client\"\n\nimport { Github, Twitter, Globe, Chrome, MonitorSmartphone, Lock } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { useI18n } from '@/lib/i18n/use-translations'\n\nexport function Footer() {\n  const currentYear = new Date().getFullYear()\n  const { t } = useI18n()\n\n  return (\n    <footer className=\"w-full border-t py-4 md:py-6\">\n      <div className=\"container px-4 mx-auto flex flex-col items-center gap-3 md:gap-4\">\n        <div className=\"flex items-center space-x-6\">\n          <div className=\"flex items-center space-x-2\">\n            <div className=\"p-1.5 rounded-full bg-background shadow-sm\">\n              <Chrome className=\"h-4 w-4 md:h-5 md:w-5\" />\n            </div>\n            <div className=\"p-1.5 rounded-full bg-background shadow-sm\">\n              <MonitorSmartphone className=\"h-4 w-4 md:h-5 md:w-5\" />\n            </div>\n            <p className=\"text-xs text-muted-foreground\" suppressHydrationWarning>\n              {t('landing.footer.browsers')}\n            </p>\n          </div>\n          <div className=\"h-4 w-px bg-border\" />\n          <div className=\"flex items-center space-x-1.5 text-primary\">\n            <Lock className=\"h-3.5 w-3.5\" />\n            <p className=\"text-xs\" suppressHydrationWarning>\n              {t('landing.footer.privacy')}\n            </p>\n          </div>\n        </div>\n        <div className=\"flex flex-wrap items-center justify-center gap-4\">\n          <Button variant=\"ghost\" size=\"icon\" asChild className=\"h-8 w-8 md:h-10 md:w-10\">\n            <a\n              href=\"https://github.com/ItusiAI\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              aria-label=\"GitHub\"\n            >\n              <Github className=\"h-4 w-4 md:h-5 md:w-5\" />\n            </a>\n          </Button>\n          <Button variant=\"ghost\" size=\"icon\" asChild className=\"h-8 w-8 md:h-10 md:w-10\">\n            <a\n              href=\"https://twitter.com/zyailive\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              aria-label=\"Twitter\"\n            >\n              <Twitter className=\"h-4 w-4 md:h-5 md:w-5\" />\n            </a>\n          </Button>\n          <Button variant=\"ghost\" size=\"icon\" asChild className=\"h-8 w-8 md:h-10 md:w-10\">\n            <a\n              href=\"https://itusi.cn\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              aria-label=\"Website\"\n            >\n              <Globe className=\"h-4 w-4 md:h-5 md:w-5\" />\n            </a>\n          </Button>\n        </div>\n        <div className=\"text-xs text-muted-foreground\" suppressHydrationWarning>\n          © {currentYear} {t('appName')}. All rights reserved.\n        </div>\n      </div>\n    </footer>\n  )\n} "
  },
  {
    "path": "components/google-analytics.tsx",
    "content": "'use client'\n\nimport Script from 'next/script'\n\nconst GoogleAnalytics = () => {\n  const gaId = process.env.NEXT_PUBLIC_GA_ID\n\n  if (!gaId) {\n    return null\n  }\n\n  return (\n    <>\n      <Script\n        src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}\n        strategy=\"afterInteractive\"\n      />\n      <Script id=\"google-analytics\" strategy=\"afterInteractive\">\n        {`\n          window.dataLayer = window.dataLayer || [];\n          function gtag(){dataLayer.push(arguments);}\n          gtag('js', new Date());\n          gtag('config', '${gaId}');\n        `}\n      </Script>\n    </>\n  )\n}\n\nexport default GoogleAnalytics "
  },
  {
    "path": "components/header.tsx",
    "content": "\"use client\"\n\nimport { MoonIcon, SunIcon, Languages } from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { Button } from \"@/components/ui/button\"\nimport { useI18n } from \"@/lib/i18n/use-translations\"\nimport Image from \"next/image\"\nimport Link from \"next/link\"\nimport { useSession } from \"next-auth/react\"\nimport { useRouter } from \"next/navigation\"\nimport { signOut } from \"next-auth/react\"\nimport { toast } from \"sonner\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  DropdownMenuSeparator,\n} from \"@/components/ui/dropdown-menu\"\nimport { Avatar, AvatarFallback } from \"@/components/ui/avatar\"\nimport { cn } from \"@/lib/utils\"\n\nexport function Header() {\n  const { t, setLanguage } = useI18n()\n  const { setTheme } = useTheme()\n  const { data: session } = useSession()\n  const router = useRouter()\n\n  const handleSignOut = async () => {\n    try {\n      await signOut({ redirect: false })\n      toast.success(t('auth.signOut.success'))\n      router.push('/login')\n    } catch (error) {\n      toast.error(t('auth.signOut.error'))\n    }\n  }\n\n  return (\n    <header className=\"border-b\">\n      <div className=\"container mx-auto px-4 py-3 flex items-center justify-between\">\n        <div className=\"flex items-center space-x-8\">\n          <Link href=\"/\" className=\"flex items-center space-x-2 group\">\n            <Image\n              src=\"/logo.png\"\n              alt=\"AI Translation Assistant\"\n              width={32}\n              height={32}\n              className={cn(\n                \"w-8 h-8\",\n                \"group-hover:scale-110 transition-transform duration-300\"\n              )}\n              priority\n            />\n            <span \n              suppressHydrationWarning \n              className={cn(\n                \"font-semibold text-lg hidden sm:inline-block opacity-0 animate-fadeIn\",\n                \"bg-clip-text text-transparent bg-gradient-to-r from-primary via-accent to-secondary bg-[200%_auto] animate-gradient\",\n                \"group-hover:bg-[100%_auto] transition-[background-position] duration-300\"\n              )}\n            >\n              {t('appName')}\n            </span>\n          </Link>\n        </div>\n\n        <nav className=\"hidden md:flex items-center absolute left-1/2 transform -translate-x-1/2 space-x-12\">\n          <Link href=\"/\" className=\"text-base font-medium hover:text-primary transition-colors\">\n            {t('nav.home')}\n          </Link>\n          <Link href=\"/translate\" className=\"text-base font-medium hover:text-primary transition-colors\">\n            {t('nav.features')}\n          </Link>\n          <Link href=\"/pricing\" className=\"text-base font-medium hover:text-primary transition-colors\">\n            {t('nav.pricing')}\n          </Link>\n        </nav>\n\n        <div className=\"flex items-center space-x-3\">\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button variant=\"ghost\" size=\"icon\">\n                <Languages className=\"h-[1.2rem] w-[1.2rem]\" />\n                <span className=\"sr-only\" suppressHydrationWarning>{t('switchLanguage')}</span>\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              <DropdownMenuItem onClick={() => setLanguage('zh')}>\n                <span suppressHydrationWarning>{t('languages.chinese')}</span>\n              </DropdownMenuItem>\n              <DropdownMenuItem onClick={() => setLanguage('en')}>\n                <span suppressHydrationWarning>{t('languages.english')}</span>\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button variant=\"ghost\" size=\"icon\">\n                <SunIcon className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" />\n                <MoonIcon className=\"absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" />\n                <span className=\"sr-only\" suppressHydrationWarning>{t('toggleTheme')}</span>\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n                <span suppressHydrationWarning>{t('theme.light')}</span>\n              </DropdownMenuItem>\n              <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n                <span suppressHydrationWarning>{t('theme.dark')}</span>\n              </DropdownMenuItem>\n              <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n                <span suppressHydrationWarning>{t('theme.system')}</span>\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n          {session ? (\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <Button variant=\"ghost\" className=\"relative h-8 w-8 rounded-full\">\n                  <Avatar className=\"h-8 w-8\">\n                    <AvatarFallback>\n                      {session.user.email?.[0]?.toUpperCase()}\n                    </AvatarFallback>\n                  </Avatar>\n                </Button>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent className=\"w-56\" align=\"end\" forceMount>\n                <DropdownMenuItem className=\"flex flex-col items-start\">\n                  <div className=\"text-sm font-medium\">\n                    {session.user.email}\n                  </div>\n                </DropdownMenuItem>\n                <DropdownMenuSeparator />\n                <DropdownMenuItem asChild>\n                  <Link href=\"/profile\">\n                    <span suppressHydrationWarning>{t('auth.profileButton')}</span>\n                  </Link>\n                </DropdownMenuItem>\n                <DropdownMenuItem onClick={handleSignOut}>\n                  <span suppressHydrationWarning>{t('auth.signOut.action')}</span>\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          ) : (\n            <div className=\"flex items-center space-x-2\">\n              <Button variant=\"ghost\" onClick={() => {\n                const returnUrl = window.location.pathname\n                router.push(`/login?returnUrl=${encodeURIComponent(returnUrl)}`)\n              }}>\n                <span suppressHydrationWarning>{t('auth.signIn')}</span>\n              </Button>\n              <Button variant=\"default\" onClick={() => {\n                const returnUrl = window.location.pathname\n                router.push(`/register?returnUrl=${encodeURIComponent(returnUrl)}`)\n              }}>\n                <span suppressHydrationWarning>{t('auth.signUp')}</span>\n              </Button>\n            </div>\n          )}\n        </div>\n      </div>\n    </header>\n  )\n}"
  },
  {
    "path": "components/language-provider.tsx",
    "content": "\"use client\"\n\nimport { createContext, useContext, useState, useEffect } from \"react\"\nimport { useI18n } from \"@/lib/i18n/use-translations\"\n\ntype Translations = {\n  tabs?: Record<string, string>\n  buttons?: Record<string, string>\n  uploadImage?: string\n  uploadAudio?: string\n  uploadVideo?: string\n  pricing: {\n    title: string\n    subtitle: string\n    tiers: Record<string, any>\n    features: Record<string, string>\n    faq: Record<string, any>\n  }\n  auth: Record<string, any>\n  error: Record<string, string>\n  loading?: string\n  [key: string]: any\n}\n\ntype LanguageContextType = {\n  language: string\n  setLanguage: (lang: string) => void\n  translations: Translations\n}\n\nconst LanguageContext = createContext<LanguageContextType | undefined>(undefined)\n\nexport function LanguageProvider({ children }: { children: React.ReactNode }) {\n  const { setLanguage: setI18nLanguage, language: i18nLanguage, translations: rawTranslations } = useI18n()\n  const [language, setLanguageState] = useState(() => {\n    if (typeof window !== 'undefined') {\n      return localStorage.getItem(\"language\") || i18nLanguage\n    }\n    return i18nLanguage\n  })\n  const [mounted, setMounted] = useState(false)\n\n  useEffect(() => {\n    setLanguageState(i18nLanguage)\n    setMounted(true)\n  }, [i18nLanguage])\n\n  const setLanguage = (lang: string) => {\n    setLanguageState(lang)\n    setI18nLanguage(lang)\n    localStorage.setItem(\"language\", lang)\n  }\n\n  if (!mounted) {\n    return null\n  }\n\n  const translations = rawTranslations as Translations\n\n  return (\n    <LanguageContext.Provider value={{ language, setLanguage, translations }}>\n      {children}\n    </LanguageContext.Provider>\n  )\n}\n\nexport function useLanguage() {\n  const context = useContext(LanguageContext)\n  if (context === undefined) {\n    throw new Error(\"useLanguage must be used within a LanguageProvider\")\n  }\n  return context\n} "
  },
  {
    "path": "components/language-switcher.tsx",
    "content": "\"use client\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Languages } from \"lucide-react\"\nimport { useI18n } from \"@/lib/i18n/use-translations\"\n\nexport function LanguageSwitcher() {\n  const { language, setLanguage, t } = useI18n()\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"outline\" size=\"icon\">\n          <Languages className=\"h-[1.2rem] w-[1.2rem]\" />\n          <span className=\"sr-only\" suppressHydrationWarning>{t('switchLanguage')}</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem \n          onClick={() => setLanguage('zh')}\n          className={language === 'zh' ? 'bg-accent' : ''}\n        >\n          <span suppressHydrationWarning>{t('languages.chinese')}</span>\n        </DropdownMenuItem>\n        <DropdownMenuItem \n          onClick={() => setLanguage('en')}\n          className={language === 'en' ? 'bg-accent' : ''}\n        >\n          <span suppressHydrationWarning>{t('languages.english')}</span>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}"
  },
  {
    "path": "components/language-toggle.tsx",
    "content": "import { Languages } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { useI18n } from \"@/lib/i18n/use-translations\"\n\nexport function LanguageToggle() {\n  const { t, setLanguage } = useI18n()\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"sm\">\n          <Languages className=\"h-4 w-4\" />\n          <span className=\"sr-only\">Toggle language</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem onClick={() => setLanguage('zh')}>\n          中文\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setLanguage('en')}>\n          English\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n} "
  },
  {
    "path": "components/layout.tsx",
    "content": "import { Languages, Sparkles } from 'lucide-react'\nimport { useI18n } from '@/lib/i18n/use-translations'\nimport { ThemeToggle } from './theme-toggle'\nimport { LanguageToggle } from './language-toggle'\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n  const { t } = useI18n()\n  \n  return (\n    <div className=\"min-h-screen\">\n      <header className=\"sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60\">\n        <div className=\"container flex h-14 items-center\">\n          <div className=\"flex items-center gap-2 mr-4\">\n            <div className=\"relative\">\n              <Languages className=\"w-6 h-6 text-primary\" />\n              <Sparkles className=\"w-3 h-3 text-yellow-500 absolute -top-1 -right-1\" />\n            </div>\n            <span className=\"font-semibold\">{t('appName')}</span>\n          </div>\n          \n          <div className=\"flex flex-1 items-center justify-between space-x-2 md:justify-end\">\n            <div className=\"flex items-center space-x-2\">\n              <LanguageToggle />\n              <ThemeToggle />\n            </div>\n          </div>\n        </div>\n      </header>\n      <main>{children}</main>\n    </div>\n  )\n} "
  },
  {
    "path": "components/subscription-dialog.tsx",
    "content": "import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\"\nimport { Button } from \"@/components/ui/button\"\nimport { useRouter } from \"next/navigation\"\nimport { useI18n } from \"@/lib/i18n/use-translations\"\n\ninterface SubscriptionDialogProps {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n}\n\nexport function SubscriptionDialog({\n  open,\n  onOpenChange,\n}: SubscriptionDialogProps) {\n  const router = useRouter()\n  const { t } = useI18n()\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t('auth.profile.subscription.dialog.title')}</DialogTitle>\n          <DialogDescription>\n            {t('auth.profile.subscription.dialog.description')}\n          </DialogDescription>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"text-sm text-muted-foreground\">\n            {t('auth.profile.subscription.dialog.benefits.title')}\n            <ul className=\"list-disc list-inside mt-2\">\n              <li>{t('auth.profile.subscription.dialog.benefits.imageQuota')}</li>\n              <li>{t('auth.profile.subscription.dialog.benefits.pdfQuota')}</li>\n              <li>{t('auth.profile.subscription.dialog.benefits.speechQuota')}</li>\n              <li>{t('auth.profile.subscription.dialog.benefits.videoQuota')}</li>\n              <li>{t('auth.profile.subscription.dialog.benefits.priority')}</li>\n            </ul>\n          </div>\n          <div className=\"flex justify-end gap-4\">\n            <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n              {t('auth.profile.subscription.dialog.buttons.cancel')}\n            </Button>\n            <Button onClick={() => router.push('/pricing')}>\n              {t('auth.profile.subscription.dialog.buttons.viewPlans')}\n            </Button>\n          </div>\n        </div>\n      </DialogContent>\n    </Dialog>\n  )\n} "
  },
  {
    "path": "components/testimonials.tsx",
    "content": "\"use client\"\n\nimport { useEffect } from 'react'\nimport { useI18n } from '@/lib/i18n/use-translations'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { cn } from '@/lib/utils'\nimport { Quote } from 'lucide-react'\n\n// 导入 Swiper 相关组件和样式\nimport { Swiper, SwiperSlide } from 'swiper/react'\nimport { Autoplay, Navigation, Pagination } from 'swiper/modules'\nimport 'swiper/css'\nimport 'swiper/css/navigation'\nimport 'swiper/css/pagination'\n\nexport function Testimonials() {\n  const { t } = useI18n()\n\n  return (\n    <section className=\"py-16 bg-muted/50\">\n      <div className=\"container\">\n        <div className=\"text-center mb-12\">\n          <h2 className=\"text-3xl font-bold mb-4\">{t('landing.testimonials.title')}</h2>\n          <p className=\"text-muted-foreground\">{t('landing.testimonials.subtitle')}</p>\n        </div>\n\n        <Swiper\n          modules={[Autoplay, Navigation, Pagination]}\n          spaceBetween={30}\n          slidesPerView={1}\n          breakpoints={{\n            640: {\n              slidesPerView: 2,\n            },\n            1024: {\n              slidesPerView: 3,\n            },\n          }}\n          autoplay={{\n            delay: 3000,\n            disableOnInteraction: false,\n          }}\n          pagination={{\n            clickable: true,\n          }}\n          navigation\n          loop\n          className=\"testimonials-swiper\"\n        >\n          {[1, 2, 3, 4, 5, 6].map((index) => (\n            <SwiperSlide key={index}>\n              <Card className=\"h-full\">\n                <CardContent className=\"pt-6\">\n                  <Quote className=\"w-8 h-8 mb-4 text-primary\" />\n                  <blockquote className=\"mb-4 text-lg\">\n                    {t(`landing.testimonials.${index}.quote`)}\n                  </blockquote>\n                  <footer>\n                    <div className=\"font-semibold\">\n                      {t(`landing.testimonials.${index}.author`)}\n                    </div>\n                    <div className=\"text-sm text-muted-foreground\">\n                      {t(`landing.testimonials.${index}.role`)}\n                    </div>\n                  </footer>\n                </CardContent>\n              </Card>\n            </SwiperSlide>\n          ))}\n        </Swiper>\n      </div>\n    </section>\n  )\n} "
  },
  {
    "path": "components/theme-provider.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\"\nimport { type ThemeProviderProps } from \"next-themes/dist/types\"\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>\n}"
  },
  {
    "path": "components/theme-toggle.tsx",
    "content": "import { Moon, Sun } from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { useI18n } from \"@/lib/i18n/use-translations\"\n\nexport function ThemeToggle() {\n  const { setTheme } = useTheme()\n  const { t } = useI18n()\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\">\n          <Sun className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" />\n          <Moon className=\"absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" />\n          <span className=\"sr-only\">{t('toggleTheme')}</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n          {t('theme.light')}\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n          {t('theme.dark')}\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n          {t('theme.system')}\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n} "
  },
  {
    "path": "components/ui/accordion.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as AccordionPrimitive from '@radix-ui/react-accordion';\nimport { ChevronDown } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <AccordionPrimitive.Item\n    ref={ref}\n    className={cn('border-b', className)}\n    {...props}\n  />\n));\nAccordionItem.displayName = 'AccordionItem';\n\nconst AccordionTrigger = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Header className=\"flex\">\n    <AccordionPrimitive.Trigger\n      ref={ref}\n      className={cn(\n        'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n    </AccordionPrimitive.Trigger>\n  </AccordionPrimitive.Header>\n));\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;\n\nconst AccordionContent = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Content\n    ref={ref}\n    className=\"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\"\n    {...props}\n  >\n    <div className={cn('pb-4 pt-0', className)}>{children}</div>\n  </AccordionPrimitive.Content>\n));\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
  },
  {
    "path": "components/ui/alert-dialog.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';\n\nimport { cn } from '@/lib/utils';\nimport { buttonVariants } from '@/components/ui/button';\n\nconst AlertDialog = AlertDialogPrimitive.Root;\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Overlay\n    className={cn(\n      'fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n      className\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPortal>\n    <AlertDialogOverlay />\n    <AlertDialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',\n        className\n      )}\n      {...props}\n    />\n  </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-2 text-center sm:text-left',\n      className\n    )}\n    {...props}\n  />\n);\nAlertDialogHeader.displayName = 'AlertDialogHeader';\n\nconst AlertDialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className\n    )}\n    {...props}\n  />\n);\nAlertDialogFooter.displayName = 'AlertDialogFooter';\n\nconst AlertDialogTitle = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Title\n    ref={ref}\n    className={cn('text-lg font-semibold', className)}\n    {...props}\n  />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nAlertDialogDescription.displayName =\n  AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogAction = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Action>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Action\n    ref={ref}\n    className={cn(buttonVariants(), className)}\n    {...props}\n  />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel\n    ref={ref}\n    className={cn(\n      buttonVariants({ variant: 'outline' }),\n      'mt-2 sm:mt-0',\n      className\n    )}\n    {...props}\n  />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n};\n"
  },
  {
    "path": "components/ui/alert.tsx",
    "content": "import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nconst alertVariants = cva(\n  'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',\n  {\n    variants: {\n      variant: {\n        default: 'bg-background text-foreground',\n        destructive:\n          'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  }\n);\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div\n    ref={ref}\n    role=\"alert\"\n    className={cn(alertVariants({ variant }), className)}\n    {...props}\n  />\n));\nAlert.displayName = 'Alert';\n\nconst AlertTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h5\n    ref={ref}\n    className={cn('mb-1 font-medium leading-none tracking-tight', className)}\n    {...props}\n  />\n));\nAlertTitle.displayName = 'AlertTitle';\n\nconst AlertDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn('text-sm [&_p]:leading-relaxed', className)}\n    {...props}\n  />\n));\nAlertDescription.displayName = 'AlertDescription';\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "components/ui/aspect-ratio.tsx",
    "content": "'use client';\n\nimport * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';\n\nconst AspectRatio = AspectRatioPrimitive.Root;\n\nexport { AspectRatio };\n"
  },
  {
    "path": "components/ui/avatar.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as AvatarPrimitive from '@radix-ui/react-avatar';\n\nimport { cn } from '@/lib/utils';\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',\n      className\n    )}\n    {...props}\n  />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn('aspect-square h-full w-full', className)}\n    {...props}\n  />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      'flex h-full w-full items-center justify-center rounded-full bg-muted',\n      className\n    )}\n    {...props}\n  />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
  },
  {
    "path": "components/ui/badge.tsx",
    "content": "import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nconst badgeVariants = cva(\n  'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',\n  {\n    variants: {\n      variant: {\n        default:\n          'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',\n        secondary:\n          'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',\n        destructive:\n          'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',\n        outline: 'text-foreground',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  }\n);\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "components/ui/breadcrumb.tsx",
    "content": "import * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { ChevronRight, MoreHorizontal } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst Breadcrumb = React.forwardRef<\n  HTMLElement,\n  React.ComponentPropsWithoutRef<'nav'> & {\n    separator?: React.ReactNode;\n  }\n>(({ ...props }, ref) => <nav ref={ref} aria-label=\"breadcrumb\" {...props} />);\nBreadcrumb.displayName = 'Breadcrumb';\n\nconst BreadcrumbList = React.forwardRef<\n  HTMLOListElement,\n  React.ComponentPropsWithoutRef<'ol'>\n>(({ className, ...props }, ref) => (\n  <ol\n    ref={ref}\n    className={cn(\n      'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',\n      className\n    )}\n    {...props}\n  />\n));\nBreadcrumbList.displayName = 'BreadcrumbList';\n\nconst BreadcrumbItem = React.forwardRef<\n  HTMLLIElement,\n  React.ComponentPropsWithoutRef<'li'>\n>(({ className, ...props }, ref) => (\n  <li\n    ref={ref}\n    className={cn('inline-flex items-center gap-1.5', className)}\n    {...props}\n  />\n));\nBreadcrumbItem.displayName = 'BreadcrumbItem';\n\nconst BreadcrumbLink = React.forwardRef<\n  HTMLAnchorElement,\n  React.ComponentPropsWithoutRef<'a'> & {\n    asChild?: boolean;\n  }\n>(({ asChild, className, ...props }, ref) => {\n  const Comp = asChild ? Slot : 'a';\n\n  return (\n    <Comp\n      ref={ref}\n      className={cn('transition-colors hover:text-foreground', className)}\n      {...props}\n    />\n  );\n});\nBreadcrumbLink.displayName = 'BreadcrumbLink';\n\nconst BreadcrumbPage = React.forwardRef<\n  HTMLSpanElement,\n  React.ComponentPropsWithoutRef<'span'>\n>(({ className, ...props }, ref) => (\n  <span\n    ref={ref}\n    role=\"link\"\n    aria-disabled=\"true\"\n    aria-current=\"page\"\n    className={cn('font-normal text-foreground', className)}\n    {...props}\n  />\n));\nBreadcrumbPage.displayName = 'BreadcrumbPage';\n\nconst BreadcrumbSeparator = ({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<'li'>) => (\n  <li\n    role=\"presentation\"\n    aria-hidden=\"true\"\n    className={cn('[&>svg]:size-3.5', className)}\n    {...props}\n  >\n    {children ?? <ChevronRight />}\n  </li>\n);\nBreadcrumbSeparator.displayName = 'BreadcrumbSeparator';\n\nconst BreadcrumbEllipsis = ({\n  className,\n  ...props\n}: React.ComponentProps<'span'>) => (\n  <span\n    role=\"presentation\"\n    aria-hidden=\"true\"\n    className={cn('flex h-9 w-9 items-center justify-center', className)}\n    {...props}\n  >\n    <MoreHorizontal className=\"h-4 w-4\" />\n    <span className=\"sr-only\">More</span>\n  </span>\n);\nBreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';\n\nexport {\n  Breadcrumb,\n  BreadcrumbList,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n  BreadcrumbEllipsis,\n};\n"
  },
  {
    "path": "components/ui/button.tsx",
    "content": "import * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nconst buttonVariants = cva(\n  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',\n  {\n    variants: {\n      variant: {\n        default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n        outline:\n          'border border-input bg-background hover:bg-accent hover:text-accent-foreground',\n        secondary:\n          'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        link: 'text-primary underline-offset-4 hover:underline',\n      },\n      size: {\n        default: 'h-10 px-4 py-2',\n        sm: 'h-9 rounded-md px-3',\n        lg: 'h-11 rounded-md px-8',\n        icon: 'h-10 w-10',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  }\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : 'button';\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "components/ui/calendar.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport { DayPicker } from 'react-day-picker';\n\nimport { cn } from '@/lib/utils';\nimport { buttonVariants } from '@/components/ui/button';\n\nexport type CalendarProps = React.ComponentProps<typeof DayPicker>;\n\nfunction Calendar({\n  className,\n  classNames,\n  showOutsideDays = true,\n  ...props\n}: CalendarProps) {\n  return (\n    <DayPicker\n      showOutsideDays={showOutsideDays}\n      className={cn('p-3', className)}\n      classNames={{\n        months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',\n        month: 'space-y-4',\n        caption: 'flex justify-center pt-1 relative items-center',\n        caption_label: 'text-sm font-medium',\n        nav: 'space-x-1 flex items-center',\n        nav_button: cn(\n          buttonVariants({ variant: 'outline' }),\n          'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'\n        ),\n        nav_button_previous: 'absolute left-1',\n        nav_button_next: 'absolute right-1',\n        table: 'w-full border-collapse space-y-1',\n        head_row: 'flex',\n        head_cell:\n          'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',\n        row: 'flex w-full mt-2',\n        cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',\n        day: cn(\n          buttonVariants({ variant: 'ghost' }),\n          'h-9 w-9 p-0 font-normal aria-selected:opacity-100'\n        ),\n        day_range_end: 'day-range-end',\n        day_selected:\n          'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',\n        day_today: 'bg-accent text-accent-foreground',\n        day_outside:\n          'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',\n        day_disabled: 'text-muted-foreground opacity-50',\n        day_range_middle:\n          'aria-selected:bg-accent aria-selected:text-accent-foreground',\n        day_hidden: 'invisible',\n        ...classNames,\n      }}\n      components={{\n        IconLeft: ({ ...props }) => <ChevronLeft className=\"h-4 w-4\" />,\n        IconRight: ({ ...props }) => <ChevronRight className=\"h-4 w-4\" />,\n      }}\n      {...props}\n    />\n  );\n}\nCalendar.displayName = 'Calendar';\n\nexport { Calendar };\n"
  },
  {
    "path": "components/ui/card.tsx",
    "content": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      'rounded-lg border bg-card text-card-foreground shadow-sm',\n      className\n    )}\n    {...props}\n  />\n));\nCard.displayName = 'Card';\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn('flex flex-col space-y-1.5 p-6', className)}\n    {...props}\n  />\n));\nCardHeader.displayName = 'CardHeader';\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\n      'text-2xl font-semibold leading-none tracking-tight',\n      className\n    )}\n    {...props}\n  />\n));\nCardTitle.displayName = 'CardTitle';\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nCardDescription.displayName = 'CardDescription';\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />\n));\nCardContent.displayName = 'CardContent';\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn('flex items-center p-6 pt-0', className)}\n    {...props}\n  />\n));\nCardFooter.displayName = 'CardFooter';\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "components/ui/carousel.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType,\n} from 'embla-carousel-react';\nimport { ArrowLeft, ArrowRight } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\nimport { Button } from '@/components/ui/button';\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\ntype CarouselProps = {\n  opts?: CarouselOptions;\n  plugins?: CarouselPlugin;\n  orientation?: 'horizontal' | 'vertical';\n  setApi?: (api: CarouselApi) => void;\n};\n\ntype CarouselContextProps = {\n  carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n  api: ReturnType<typeof useEmblaCarousel>[1];\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n} & CarouselProps;\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n\n  if (!context) {\n    throw new Error('useCarousel must be used within a <Carousel />');\n  }\n\n  return context;\n}\n\nconst Carousel = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & CarouselProps\n>(\n  (\n    {\n      orientation = 'horizontal',\n      opts,\n      setApi,\n      plugins,\n      className,\n      children,\n      ...props\n    },\n    ref\n  ) => {\n    const [carouselRef, api] = useEmblaCarousel(\n      {\n        ...opts,\n        axis: orientation === 'horizontal' ? 'x' : 'y',\n      },\n      plugins\n    );\n    const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n    const [canScrollNext, setCanScrollNext] = React.useState(false);\n\n    const onSelect = React.useCallback((api: CarouselApi) => {\n      if (!api) {\n        return;\n      }\n\n      setCanScrollPrev(api.canScrollPrev());\n      setCanScrollNext(api.canScrollNext());\n    }, []);\n\n    const scrollPrev = React.useCallback(() => {\n      api?.scrollPrev();\n    }, [api]);\n\n    const scrollNext = React.useCallback(() => {\n      api?.scrollNext();\n    }, [api]);\n\n    const handleKeyDown = React.useCallback(\n      (event: React.KeyboardEvent<HTMLDivElement>) => {\n        if (event.key === 'ArrowLeft') {\n          event.preventDefault();\n          scrollPrev();\n        } else if (event.key === 'ArrowRight') {\n          event.preventDefault();\n          scrollNext();\n        }\n      },\n      [scrollPrev, scrollNext]\n    );\n\n    React.useEffect(() => {\n      if (!api || !setApi) {\n        return;\n      }\n\n      setApi(api);\n    }, [api, setApi]);\n\n    React.useEffect(() => {\n      if (!api) {\n        return;\n      }\n\n      onSelect(api);\n      api.on('reInit', onSelect);\n      api.on('select', onSelect);\n\n      return () => {\n        api?.off('select', onSelect);\n      };\n    }, [api, onSelect]);\n\n    return (\n      <CarouselContext.Provider\n        value={{\n          carouselRef,\n          api: api,\n          opts,\n          orientation:\n            orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),\n          scrollPrev,\n          scrollNext,\n          canScrollPrev,\n          canScrollNext,\n        }}\n      >\n        <div\n          ref={ref}\n          onKeyDownCapture={handleKeyDown}\n          className={cn('relative', className)}\n          role=\"region\"\n          aria-roledescription=\"carousel\"\n          {...props}\n        >\n          {children}\n        </div>\n      </CarouselContext.Provider>\n    );\n  }\n);\nCarousel.displayName = 'Carousel';\n\nconst CarouselContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const { carouselRef, orientation } = useCarousel();\n\n  return (\n    <div ref={carouselRef} className=\"overflow-hidden\">\n      <div\n        ref={ref}\n        className={cn(\n          'flex',\n          orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',\n          className\n        )}\n        {...props}\n      />\n    </div>\n  );\n});\nCarouselContent.displayName = 'CarouselContent';\n\nconst CarouselItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const { orientation } = useCarousel();\n\n  return (\n    <div\n      ref={ref}\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      className={cn(\n        'min-w-0 shrink-0 grow-0 basis-full',\n        orientation === 'horizontal' ? 'pl-4' : 'pt-4',\n        className\n      )}\n      {...props}\n    />\n  );\n});\nCarouselItem.displayName = 'CarouselItem';\n\nconst CarouselPrevious = React.forwardRef<\n  HTMLButtonElement,\n  React.ComponentProps<typeof Button>\n>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {\n  const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n\n  return (\n    <Button\n      ref={ref}\n      variant={variant}\n      size={size}\n      className={cn(\n        'absolute  h-8 w-8 rounded-full',\n        orientation === 'horizontal'\n          ? '-left-12 top-1/2 -translate-y-1/2'\n          : '-top-12 left-1/2 -translate-x-1/2 rotate-90',\n        className\n      )}\n      disabled={!canScrollPrev}\n      onClick={scrollPrev}\n      {...props}\n    >\n      <ArrowLeft className=\"h-4 w-4\" />\n      <span className=\"sr-only\">Previous slide</span>\n    </Button>\n  );\n});\nCarouselPrevious.displayName = 'CarouselPrevious';\n\nconst CarouselNext = React.forwardRef<\n  HTMLButtonElement,\n  React.ComponentProps<typeof Button>\n>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {\n  const { orientation, scrollNext, canScrollNext } = useCarousel();\n\n  return (\n    <Button\n      ref={ref}\n      variant={variant}\n      size={size}\n      className={cn(\n        'absolute h-8 w-8 rounded-full',\n        orientation === 'horizontal'\n          ? '-right-12 top-1/2 -translate-y-1/2'\n          : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',\n        className\n      )}\n      disabled={!canScrollNext}\n      onClick={scrollNext}\n      {...props}\n    >\n      <ArrowRight className=\"h-4 w-4\" />\n      <span className=\"sr-only\">Next slide</span>\n    </Button>\n  );\n});\nCarouselNext.displayName = 'CarouselNext';\n\nexport {\n  type CarouselApi,\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselPrevious,\n  CarouselNext,\n};\n"
  },
  {
    "path": "components/ui/chart.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as RechartsPrimitive from 'recharts';\n\nimport { cn } from '@/lib/utils';\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: '', dark: '.dark' } as const;\n\nexport type ChartConfig = {\n  [k in string]: {\n    label?: React.ReactNode;\n    icon?: React.ComponentType;\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, string> }\n  );\n};\n\ntype ChartContextProps = {\n  config: ChartConfig;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n  const context = React.useContext(ChartContext);\n\n  if (!context) {\n    throw new Error('useChart must be used within a <ChartContainer />');\n  }\n\n  return context;\n}\n\nconst ChartContainer = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentProps<'div'> & {\n    config: ChartConfig;\n    children: React.ComponentProps<\n      typeof RechartsPrimitive.ResponsiveContainer\n    >['children'];\n  }\n>(({ id, className, children, config, ...props }, ref) => {\n  const uniqueId = React.useId();\n  const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-chart={chartId}\n        ref={ref}\n        className={cn(\n          \"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none\",\n          className\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer>\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  );\n});\nChartContainer.displayName = 'Chart';\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(\n    ([_, config]) => config.theme || config.color\n  );\n\n  if (!colorConfig.length) {\n    return null;\n  }\n\n  return (\n    <style\n      dangerouslySetInnerHTML={{\n        __html: Object.entries(THEMES)\n          .map(\n            ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n  .map(([key, itemConfig]) => {\n    const color =\n      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n      itemConfig.color;\n    return color ? `  --color-${key}: ${color};` : null;\n  })\n  .join('\\n')}\n}\n`\n          )\n          .join('\\n'),\n      }}\n    />\n  );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\nconst ChartTooltipContent = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n    React.ComponentProps<'div'> & {\n      hideLabel?: boolean;\n      hideIndicator?: boolean;\n      indicator?: 'line' | 'dot' | 'dashed';\n      nameKey?: string;\n      labelKey?: string;\n    }\n>(\n  (\n    {\n      active,\n      payload,\n      className,\n      indicator = 'dot',\n      hideLabel = false,\n      hideIndicator = false,\n      label,\n      labelFormatter,\n      labelClassName,\n      formatter,\n      color,\n      nameKey,\n      labelKey,\n    },\n    ref\n  ) => {\n    const { config } = useChart();\n\n    const tooltipLabel = React.useMemo(() => {\n      if (hideLabel || !payload?.length) {\n        return null;\n      }\n\n      const [item] = payload;\n      const key = `${labelKey || item.dataKey || item.name || 'value'}`;\n      const itemConfig = getPayloadConfigFromPayload(config, item, key);\n      const value =\n        !labelKey && typeof label === 'string'\n          ? config[label as keyof typeof config]?.label || label\n          : itemConfig?.label;\n\n      if (labelFormatter) {\n        return (\n          <div className={cn('font-medium', labelClassName)}>\n            {labelFormatter(value, payload)}\n          </div>\n        );\n      }\n\n      if (!value) {\n        return null;\n      }\n\n      return <div className={cn('font-medium', labelClassName)}>{value}</div>;\n    }, [\n      label,\n      labelFormatter,\n      payload,\n      hideLabel,\n      labelClassName,\n      config,\n      labelKey,\n    ]);\n\n    if (!active || !payload?.length) {\n      return null;\n    }\n\n    const nestLabel = payload.length === 1 && indicator !== 'dot';\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl',\n          className\n        )}\n      >\n        {!nestLabel ? tooltipLabel : null}\n        <div className=\"grid gap-1.5\">\n          {payload.map((item, index) => {\n            const key = `${nameKey || item.name || item.dataKey || 'value'}`;\n            const itemConfig = getPayloadConfigFromPayload(config, item, key);\n            const indicatorColor = color || item.payload.fill || item.color;\n\n            return (\n              <div\n                key={item.dataKey}\n                className={cn(\n                  'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',\n                  indicator === 'dot' && 'items-center'\n                )}\n              >\n                {formatter && item?.value !== undefined && item.name ? (\n                  formatter(item.value, item.name, item, index, item.payload)\n                ) : (\n                  <>\n                    {itemConfig?.icon ? (\n                      <itemConfig.icon />\n                    ) : (\n                      !hideIndicator && (\n                        <div\n                          className={cn(\n                            'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]',\n                            {\n                              'h-2.5 w-2.5': indicator === 'dot',\n                              'w-1': indicator === 'line',\n                              'w-0 border-[1.5px] border-dashed bg-transparent':\n                                indicator === 'dashed',\n                              'my-0.5': nestLabel && indicator === 'dashed',\n                            }\n                          )}\n                          style={\n                            {\n                              '--color-bg': indicatorColor,\n                              '--color-border': indicatorColor,\n                            } as React.CSSProperties\n                          }\n                        />\n                      )\n                    )}\n                    <div\n                      className={cn(\n                        'flex flex-1 justify-between leading-none',\n                        nestLabel ? 'items-end' : 'items-center'\n                      )}\n                    >\n                      <div className=\"grid gap-1.5\">\n                        {nestLabel ? tooltipLabel : null}\n                        <span className=\"text-muted-foreground\">\n                          {itemConfig?.label || item.name}\n                        </span>\n                      </div>\n                      {item.value && (\n                        <span className=\"font-mono font-medium tabular-nums text-foreground\">\n                          {item.value.toLocaleString()}\n                        </span>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    );\n  }\n);\nChartTooltipContent.displayName = 'ChartTooltip';\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nconst ChartLegendContent = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentProps<'div'> &\n    Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {\n      hideIcon?: boolean;\n      nameKey?: string;\n    }\n>(\n  (\n    { className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey },\n    ref\n  ) => {\n    const { config } = useChart();\n\n    if (!payload?.length) {\n      return null;\n    }\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          'flex items-center justify-center gap-4',\n          verticalAlign === 'top' ? 'pb-3' : 'pt-3',\n          className\n        )}\n      >\n        {payload.map((item) => {\n          const key = `${nameKey || item.dataKey || 'value'}`;\n          const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n          return (\n            <div\n              key={item.value}\n              className={cn(\n                'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground'\n              )}\n            >\n              {itemConfig?.icon && !hideIcon ? (\n                <itemConfig.icon />\n              ) : (\n                <div\n                  className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n                  style={{\n                    backgroundColor: item.color,\n                  }}\n                />\n              )}\n              {itemConfig?.label}\n            </div>\n          );\n        })}\n      </div>\n    );\n  }\n);\nChartLegendContent.displayName = 'ChartLegend';\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string\n) {\n  if (typeof payload !== 'object' || payload === null) {\n    return undefined;\n  }\n\n  const payloadPayload =\n    'payload' in payload &&\n    typeof payload.payload === 'object' &&\n    payload.payload !== null\n      ? payload.payload\n      : undefined;\n\n  let configLabelKey: string = key;\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === 'string'\n  ) {\n    configLabelKey = payload[key as keyof typeof payload] as string;\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'\n  ) {\n    configLabelKey = payloadPayload[\n      key as keyof typeof payloadPayload\n    ] as string;\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config];\n}\n\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n};\n"
  },
  {
    "path": "components/ui/checkbox.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox';\nimport { Check } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <CheckboxPrimitive.Root\n    ref={ref}\n    className={cn(\n      'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',\n      className\n    )}\n    {...props}\n  >\n    <CheckboxPrimitive.Indicator\n      className={cn('flex items-center justify-center text-current')}\n    >\n      <Check className=\"h-4 w-4\" />\n    </CheckboxPrimitive.Indicator>\n  </CheckboxPrimitive.Root>\n));\nCheckbox.displayName = CheckboxPrimitive.Root.displayName;\n\nexport { Checkbox };\n"
  },
  {
    "path": "components/ui/collapsible.tsx",
    "content": "'use client';\n\nimport * as CollapsiblePrimitive from '@radix-ui/react-collapsible';\n\nconst Collapsible = CollapsiblePrimitive.Root;\n\nconst CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;\n\nconst CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n"
  },
  {
    "path": "components/ui/command.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport { type DialogProps } from '@radix-ui/react-dialog';\nimport { Command as CommandPrimitive } from 'cmdk';\nimport { Search } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\nimport { Dialog, DialogContent } from '@/components/ui/dialog';\n\nconst Command = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive\n    ref={ref}\n    className={cn(\n      'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',\n      className\n    )}\n    {...props}\n  />\n));\nCommand.displayName = CommandPrimitive.displayName;\n\ninterface CommandDialogProps extends DialogProps {}\n\nconst CommandDialog = ({ children, ...props }: CommandDialogProps) => {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0 shadow-lg\">\n        <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst CommandInput = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Input>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n  <div className=\"flex items-center border-b px-3\" cmdk-input-wrapper=\"\">\n    <Search className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n    <CommandPrimitive.Input\n      ref={ref}\n      className={cn(\n        'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',\n        className\n      )}\n      {...props}\n    />\n  </div>\n));\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.List\n    ref={ref}\n    className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}\n    {...props}\n  />\n));\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Empty>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => (\n  <CommandPrimitive.Empty\n    ref={ref}\n    className=\"py-6 text-center text-sm\"\n    {...props}\n  />\n));\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Group>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Group\n    ref={ref}\n    className={cn(\n      'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',\n      className\n    )}\n    {...props}\n  />\n));\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 h-px bg-border', className)}\n    {...props}\n  />\n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50\",\n      className\n    )}\n    {...props}\n  />\n));\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        'ml-auto text-xs tracking-widest text-muted-foreground',\n        className\n      )}\n      {...props}\n    />\n  );\n};\nCommandShortcut.displayName = 'CommandShortcut';\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n};\n"
  },
  {
    "path": "components/ui/context-menu.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as ContextMenuPrimitive from '@radix-ui/react-context-menu';\nimport { Check, ChevronRight, Circle } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst ContextMenu = ContextMenuPrimitive.Root;\n\nconst ContextMenuTrigger = ContextMenuPrimitive.Trigger;\n\nconst ContextMenuGroup = ContextMenuPrimitive.Group;\n\nconst ContextMenuPortal = ContextMenuPrimitive.Portal;\n\nconst ContextMenuSub = ContextMenuPrimitive.Sub;\n\nconst ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;\n\nconst ContextMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <ContextMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </ContextMenuPrimitive.SubTrigger>\n));\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;\n\nconst ContextMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n      className\n    )}\n    {...props}\n  />\n));\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;\n\nconst ContextMenuContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Portal>\n    <ContextMenuPrimitive.Content\n      ref={ref}\n      className={cn(\n        'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n        className\n      )}\n      {...props}\n    />\n  </ContextMenuPrimitive.Portal>\n));\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;\n\nconst ContextMenuItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  />\n));\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <ContextMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.CheckboxItem>\n));\nContextMenuCheckboxItem.displayName =\n  ContextMenuPrimitive.CheckboxItem.displayName;\n\nconst ContextMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <ContextMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.RadioItem>\n));\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;\n\nconst ContextMenuLabel = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      'px-2 py-1.5 text-sm font-semibold text-foreground',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  />\n));\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;\n\nconst ContextMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-border', className)}\n    {...props}\n  />\n));\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;\n\nconst ContextMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        'ml-auto text-xs tracking-widest text-muted-foreground',\n        className\n      )}\n      {...props}\n    />\n  );\n};\nContextMenuShortcut.displayName = 'ContextMenuShortcut';\n\nexport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioItem,\n  ContextMenuLabel,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuGroup,\n  ContextMenuPortal,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuRadioGroup,\n};\n"
  },
  {
    "path": "components/ui/dialog.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { X } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      'fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n      className\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-1.5 text-center sm:text-left',\n      className\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = 'DialogHeader';\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = 'DialogFooter';\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      'text-lg font-semibold leading-none tracking-tight',\n      className\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "components/ui/drawer.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport { Drawer as DrawerPrimitive } from 'vaul';\n\nimport { cn } from '@/lib/utils';\n\nconst Drawer = ({\n  shouldScaleBackground = true,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (\n  <DrawerPrimitive.Root\n    shouldScaleBackground={shouldScaleBackground}\n    {...props}\n  />\n);\nDrawer.displayName = 'Drawer';\n\nconst DrawerTrigger = DrawerPrimitive.Trigger;\n\nconst DrawerPortal = DrawerPrimitive.Portal;\n\nconst DrawerClose = DrawerPrimitive.Close;\n\nconst DrawerOverlay = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Overlay\n    ref={ref}\n    className={cn('fixed inset-0 z-50 bg-black/80', className)}\n    {...props}\n  />\n));\nDrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;\n\nconst DrawerContent = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DrawerPortal>\n    <DrawerOverlay />\n    <DrawerPrimitive.Content\n      ref={ref}\n      className={cn(\n        'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',\n        className\n      )}\n      {...props}\n    >\n      <div className=\"mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted\" />\n      {children}\n    </DrawerPrimitive.Content>\n  </DrawerPortal>\n));\nDrawerContent.displayName = 'DrawerContent';\n\nconst DrawerHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)}\n    {...props}\n  />\n);\nDrawerHeader.displayName = 'DrawerHeader';\n\nconst DrawerFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn('mt-auto flex flex-col gap-2 p-4', className)}\n    {...props}\n  />\n);\nDrawerFooter.displayName = 'DrawerFooter';\n\nconst DrawerTitle = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Title\n    ref={ref}\n    className={cn(\n      'text-lg font-semibold leading-none tracking-tight',\n      className\n    )}\n    {...props}\n  />\n));\nDrawerTitle.displayName = DrawerPrimitive.Title.displayName;\n\nconst DrawerDescription = React.forwardRef<\n  React.ElementRef<typeof DrawerPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DrawerPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nDrawerDescription.displayName = DrawerPrimitive.Description.displayName;\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
  },
  {
    "path": "components/ui/dropdown-menu.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport { Check, ChevronRight, Circle } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n      className\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      'px-2 py-1.5 text-sm font-semibold',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn('ml-auto text-xs tracking-widest opacity-60', className)}\n      {...props}\n    />\n  );\n};\nDropdownMenuShortcut.displayName = 'DropdownMenuShortcut';\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "components/ui/form.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { Slot } from '@radix-ui/react-slot';\nimport {\n  Controller,\n  ControllerProps,\n  FieldPath,\n  FieldValues,\n  FormProvider,\n  useFormContext,\n} from 'react-hook-form';\n\nimport { cn } from '@/lib/utils';\nimport { Label } from '@/components/ui/label';\n\nconst Form = FormProvider;\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue\n);\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error('useFormField should be used within <FormField>');\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n  {} as FormItemContextValue\n);\n\nconst FormItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const id = React.useId();\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div ref={ref} className={cn('space-y-2', className)} {...props} />\n    </FormItemContext.Provider>\n  );\n});\nFormItem.displayName = 'FormItem';\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && 'text-destructive', className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  );\n});\nFormLabel.displayName = 'FormLabel';\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } =\n    useFormField();\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  );\n});\nFormControl.displayName = 'FormControl';\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField();\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn('text-sm text-muted-foreground', className)}\n      {...props}\n    />\n  );\n});\nFormDescription.displayName = 'FormDescription';\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error?.message) : children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn('text-sm font-medium text-destructive', className)}\n      {...props}\n    >\n      {body}\n    </p>\n  );\n});\nFormMessage.displayName = 'FormMessage';\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "components/ui/hover-card.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as HoverCardPrimitive from '@radix-ui/react-hover-card';\n\nimport { cn } from '@/lib/utils';\n\nconst HoverCard = HoverCardPrimitive.Root;\n\nconst HoverCardTrigger = HoverCardPrimitive.Trigger;\n\nconst HoverCardContent = React.forwardRef<\n  React.ElementRef<typeof HoverCardPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>\n>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (\n  <HoverCardPrimitive.Content\n    ref={ref}\n    align={align}\n    sideOffset={sideOffset}\n    className={cn(\n      'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n      className\n    )}\n    {...props}\n  />\n));\nHoverCardContent.displayName = HoverCardPrimitive.Content.displayName;\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent };\n"
  },
  {
    "path": "components/ui/input-otp.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport { OTPInput, OTPInputContext } from 'input-otp';\nimport { Dot } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst InputOTP = React.forwardRef<\n  React.ElementRef<typeof OTPInput>,\n  React.ComponentPropsWithoutRef<typeof OTPInput>\n>(({ className, containerClassName, ...props }, ref) => (\n  <OTPInput\n    ref={ref}\n    containerClassName={cn(\n      'flex items-center gap-2 has-[:disabled]:opacity-50',\n      containerClassName\n    )}\n    className={cn('disabled:cursor-not-allowed', className)}\n    {...props}\n  />\n));\nInputOTP.displayName = 'InputOTP';\n\nconst InputOTPGroup = React.forwardRef<\n  React.ElementRef<'div'>,\n  React.ComponentPropsWithoutRef<'div'>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn('flex items-center', className)} {...props} />\n));\nInputOTPGroup.displayName = 'InputOTPGroup';\n\nconst InputOTPSlot = React.forwardRef<\n  React.ElementRef<'div'>,\n  React.ComponentPropsWithoutRef<'div'> & { index: number }\n>(({ index, className, ...props }, ref) => {\n  const inputOTPContext = React.useContext(OTPInputContext);\n  const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        'relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',\n        isActive && 'z-10 ring-2 ring-ring ring-offset-background',\n        className\n      )}\n      {...props}\n    >\n      {char}\n      {hasFakeCaret && (\n        <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n          <div className=\"h-4 w-px animate-caret-blink bg-foreground duration-1000\" />\n        </div>\n      )}\n    </div>\n  );\n});\nInputOTPSlot.displayName = 'InputOTPSlot';\n\nconst InputOTPSeparator = React.forwardRef<\n  React.ElementRef<'div'>,\n  React.ComponentPropsWithoutRef<'div'>\n>(({ ...props }, ref) => (\n  <div ref={ref} role=\"separator\" {...props}>\n    <Dot />\n  </div>\n));\nInputOTPSeparator.displayName = 'InputOTPSeparator';\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };\n"
  },
  {
    "path": "components/ui/input.tsx",
    "content": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\nInput.displayName = 'Input';\n\nexport { Input };\n"
  },
  {
    "path": "components/ui/label.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nconst labelVariants = cva(\n  'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "components/ui/menubar.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as MenubarPrimitive from '@radix-ui/react-menubar';\nimport { Check, ChevronRight, Circle } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst MenubarMenu = MenubarPrimitive.Menu;\n\nconst MenubarGroup = MenubarPrimitive.Group;\n\nconst MenubarPortal = MenubarPrimitive.Portal;\n\nconst MenubarSub = MenubarPrimitive.Sub;\n\nconst MenubarRadioGroup = MenubarPrimitive.RadioGroup;\n\nconst Menubar = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Root\n    ref={ref}\n    className={cn(\n      'flex h-10 items-center space-x-1 rounded-md border bg-background p-1',\n      className\n    )}\n    {...props}\n  />\n));\nMenubar.displayName = MenubarPrimitive.Root.displayName;\n\nconst MenubarTrigger = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      'flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',\n      className\n    )}\n    {...props}\n  />\n));\nMenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;\n\nconst MenubarSubTrigger = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <MenubarPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </MenubarPrimitive.SubTrigger>\n));\nMenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;\n\nconst MenubarSubContent = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n      className\n    )}\n    {...props}\n  />\n));\nMenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;\n\nconst MenubarContent = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>\n>(\n  (\n    { className, align = 'start', alignOffset = -4, sideOffset = 8, ...props },\n    ref\n  ) => (\n    <MenubarPrimitive.Portal>\n      <MenubarPrimitive.Content\n        ref={ref}\n        align={align}\n        alignOffset={alignOffset}\n        sideOffset={sideOffset}\n        className={cn(\n          'z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n          className\n        )}\n        {...props}\n      />\n    </MenubarPrimitive.Portal>\n  )\n);\nMenubarContent.displayName = MenubarPrimitive.Content.displayName;\n\nconst MenubarItem = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <MenubarPrimitive.Item\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  />\n));\nMenubarItem.displayName = MenubarPrimitive.Item.displayName;\n\nconst MenubarCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <MenubarPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <MenubarPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </MenubarPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </MenubarPrimitive.CheckboxItem>\n));\nMenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;\n\nconst MenubarRadioItem = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <MenubarPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <MenubarPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </MenubarPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </MenubarPrimitive.RadioItem>\n));\nMenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;\n\nconst MenubarLabel = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <MenubarPrimitive.Label\n    ref={ref}\n    className={cn(\n      'px-2 py-1.5 text-sm font-semibold',\n      inset && 'pl-8',\n      className\n    )}\n    {...props}\n  />\n));\nMenubarLabel.displayName = MenubarPrimitive.Label.displayName;\n\nconst MenubarSeparator = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n));\nMenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;\n\nconst MenubarShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        'ml-auto text-xs tracking-widest text-muted-foreground',\n        className\n      )}\n      {...props}\n    />\n  );\n};\nMenubarShortcut.displayname = 'MenubarShortcut';\n\nexport {\n  Menubar,\n  MenubarMenu,\n  MenubarTrigger,\n  MenubarContent,\n  MenubarItem,\n  MenubarSeparator,\n  MenubarLabel,\n  MenubarCheckboxItem,\n  MenubarRadioGroup,\n  MenubarRadioItem,\n  MenubarPortal,\n  MenubarSubContent,\n  MenubarSubTrigger,\n  MenubarGroup,\n  MenubarSub,\n  MenubarShortcut,\n};\n"
  },
  {
    "path": "components/ui/navigation-menu.tsx",
    "content": "import * as React from 'react';\nimport * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';\nimport { cva } from 'class-variance-authority';\nimport { ChevronDown } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst NavigationMenu = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <NavigationMenuPrimitive.Root\n    ref={ref}\n    className={cn(\n      'relative z-10 flex max-w-max flex-1 items-center justify-center',\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <NavigationMenuViewport />\n  </NavigationMenuPrimitive.Root>\n));\nNavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;\n\nconst NavigationMenuList = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.List\n    ref={ref}\n    className={cn(\n      'group flex flex-1 list-none items-center justify-center space-x-1',\n      className\n    )}\n    {...props}\n  />\n));\nNavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;\n\nconst NavigationMenuItem = NavigationMenuPrimitive.Item;\n\nconst navigationMenuTriggerStyle = cva(\n  'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50'\n);\n\nconst NavigationMenuTrigger = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <NavigationMenuPrimitive.Trigger\n    ref={ref}\n    className={cn(navigationMenuTriggerStyle(), 'group', className)}\n    {...props}\n  >\n    {children}{' '}\n    <ChevronDown\n      className=\"relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180\"\n      aria-hidden=\"true\"\n    />\n  </NavigationMenuPrimitive.Trigger>\n));\nNavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;\n\nconst NavigationMenuContent = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.Content\n    ref={ref}\n    className={cn(\n      'left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ',\n      className\n    )}\n    {...props}\n  />\n));\nNavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;\n\nconst NavigationMenuLink = NavigationMenuPrimitive.Link;\n\nconst NavigationMenuViewport = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>\n>(({ className, ...props }, ref) => (\n  <div className={cn('absolute left-0 top-full flex justify-center')}>\n    <NavigationMenuPrimitive.Viewport\n      className={cn(\n        'origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]',\n        className\n      )}\n      ref={ref}\n      {...props}\n    />\n  </div>\n));\nNavigationMenuViewport.displayName =\n  NavigationMenuPrimitive.Viewport.displayName;\n\nconst NavigationMenuIndicator = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.Indicator\n    ref={ref}\n    className={cn(\n      'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in',\n      className\n    )}\n    {...props}\n  >\n    <div className=\"relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md\" />\n  </NavigationMenuPrimitive.Indicator>\n));\nNavigationMenuIndicator.displayName =\n  NavigationMenuPrimitive.Indicator.displayName;\n\nexport {\n  navigationMenuTriggerStyle,\n  NavigationMenu,\n  NavigationMenuList,\n  NavigationMenuItem,\n  NavigationMenuContent,\n  NavigationMenuTrigger,\n  NavigationMenuLink,\n  NavigationMenuIndicator,\n  NavigationMenuViewport,\n};\n"
  },
  {
    "path": "components/ui/pagination.tsx",
    "content": "import * as React from 'react';\nimport { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\nimport { ButtonProps, buttonVariants } from '@/components/ui/button';\n\nconst Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (\n  <nav\n    role=\"navigation\"\n    aria-label=\"pagination\"\n    className={cn('mx-auto flex w-full justify-center', className)}\n    {...props}\n  />\n);\nPagination.displayName = 'Pagination';\n\nconst PaginationContent = React.forwardRef<\n  HTMLUListElement,\n  React.ComponentProps<'ul'>\n>(({ className, ...props }, ref) => (\n  <ul\n    ref={ref}\n    className={cn('flex flex-row items-center gap-1', className)}\n    {...props}\n  />\n));\nPaginationContent.displayName = 'PaginationContent';\n\nconst PaginationItem = React.forwardRef<\n  HTMLLIElement,\n  React.ComponentProps<'li'>\n>(({ className, ...props }, ref) => (\n  <li ref={ref} className={cn('', className)} {...props} />\n));\nPaginationItem.displayName = 'PaginationItem';\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<ButtonProps, 'size'> &\n  React.ComponentProps<'a'>;\n\nconst PaginationLink = ({\n  className,\n  isActive,\n  size = 'icon',\n  ...props\n}: PaginationLinkProps) => (\n  <a\n    aria-current={isActive ? 'page' : undefined}\n    className={cn(\n      buttonVariants({\n        variant: isActive ? 'outline' : 'ghost',\n        size,\n      }),\n      className\n    )}\n    {...props}\n  />\n);\nPaginationLink.displayName = 'PaginationLink';\n\nconst PaginationPrevious = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to previous page\"\n    size=\"default\"\n    className={cn('gap-1 pl-2.5', className)}\n    {...props}\n  >\n    <ChevronLeft className=\"h-4 w-4\" />\n    <span>Previous</span>\n  </PaginationLink>\n);\nPaginationPrevious.displayName = 'PaginationPrevious';\n\nconst PaginationNext = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to next page\"\n    size=\"default\"\n    className={cn('gap-1 pr-2.5', className)}\n    {...props}\n  >\n    <span>Next</span>\n    <ChevronRight className=\"h-4 w-4\" />\n  </PaginationLink>\n);\nPaginationNext.displayName = 'PaginationNext';\n\nconst PaginationEllipsis = ({\n  className,\n  ...props\n}: React.ComponentProps<'span'>) => (\n  <span\n    aria-hidden\n    className={cn('flex h-9 w-9 items-center justify-center', className)}\n    {...props}\n  >\n    <MoreHorizontal className=\"h-4 w-4\" />\n    <span className=\"sr-only\">More pages</span>\n  </span>\n);\nPaginationEllipsis.displayName = 'PaginationEllipsis';\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationEllipsis,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n};\n"
  },
  {
    "path": "components/ui/popover.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\n\nimport { cn } from '@/lib/utils';\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n        className\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n"
  },
  {
    "path": "components/ui/progress.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as ProgressPrimitive from '@radix-ui/react-progress';\n\nimport { cn } from '@/lib/utils';\n\nconst Progress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>\n>(({ className, value, ...props }, ref) => (\n  <ProgressPrimitive.Root\n    ref={ref}\n    className={cn(\n      'relative h-4 w-full overflow-hidden rounded-full bg-secondary',\n      className\n    )}\n    {...props}\n  >\n    <ProgressPrimitive.Indicator\n      className=\"h-full w-full flex-1 bg-primary transition-all\"\n      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n    />\n  </ProgressPrimitive.Root>\n));\nProgress.displayName = ProgressPrimitive.Root.displayName;\n\nexport { Progress };\n"
  },
  {
    "path": "components/ui/radio-group.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group';\nimport { Circle } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst RadioGroup = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Root\n      className={cn('grid gap-2', className)}\n      {...props}\n      ref={ref}\n    />\n  );\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',\n        className\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n        <Circle className=\"h-2.5 w-2.5 fill-current text-current\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "components/ui/resizable.tsx",
    "content": "'use client';\n\nimport { GripVertical } from 'lucide-react';\nimport * as ResizablePrimitive from 'react-resizable-panels';\n\nimport { cn } from '@/lib/utils';\n\nconst ResizablePanelGroup = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (\n  <ResizablePrimitive.PanelGroup\n    className={cn(\n      'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',\n      className\n    )}\n    {...props}\n  />\n);\n\nconst ResizablePanel = ResizablePrimitive.Panel;\n\nconst ResizableHandle = ({\n  withHandle,\n  className,\n  ...props\n}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {\n  withHandle?: boolean;\n}) => (\n  <ResizablePrimitive.PanelResizeHandle\n    className={cn(\n      'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',\n      className\n    )}\n    {...props}\n  >\n    {withHandle && (\n      <div className=\"z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border\">\n        <GripVertical className=\"h-2.5 w-2.5\" />\n      </div>\n    )}\n  </ResizablePrimitive.PanelResizeHandle>\n);\n\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle };\n"
  },
  {
    "path": "components/ui/scroll-area.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';\n\nimport { cn } from '@/lib/utils';\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root\n    ref={ref}\n    className={cn('relative overflow-hidden', className)}\n    {...props}\n  >\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n));\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = 'vertical', ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      'flex touch-none select-none transition-colors',\n      orientation === 'vertical' &&\n        'h-full w-2.5 border-l border-l-transparent p-[1px]',\n      orientation === 'horizontal' &&\n        'h-2.5 flex-col border-t border-t-transparent p-[1px]',\n      className\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "components/ui/select.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as SelectPrimitive from '@radix-ui/react-select';\nimport { Check, ChevronDown, ChevronUp } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      'flex cursor-default items-center justify-center py-1',\n      className\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      'flex cursor-default items-center justify-center py-1',\n      className\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = 'popper', ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n        position === 'popper' &&\n          'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          'p-1',\n          position === 'popper' &&\n            'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "components/ui/separator.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as SeparatorPrimitive from '@radix-ui/react-separator';\n\nimport { cn } from '@/lib/utils';\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = 'horizontal', decorative = true, ...props },\n    ref\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        'shrink-0 bg-border',\n        orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',\n        className\n      )}\n      {...props}\n    />\n  )\n);\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "components/ui/sheet.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as SheetPrimitive from '@radix-ui/react-dialog';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { X } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\n\nconst Sheet = SheetPrimitive.Root;\n\nconst SheetTrigger = SheetPrimitive.Trigger;\n\nconst SheetClose = SheetPrimitive.Close;\n\nconst SheetPortal = SheetPrimitive.Portal;\n\nconst SheetOverlay = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Overlay\n    className={cn(\n      'fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n      className\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nSheetOverlay.displayName = SheetPrimitive.Overlay.displayName;\n\nconst sheetVariants = cva(\n  'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',\n  {\n    variants: {\n      side: {\n        top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n        bottom:\n          'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n        left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',\n        right:\n          'inset-y-0 right-0 h-full w-3/4  border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',\n      },\n    },\n    defaultVariants: {\n      side: 'right',\n    },\n  }\n);\n\ninterface SheetContentProps\n  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,\n    VariantProps<typeof sheetVariants> {}\n\nconst SheetContent = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Content>,\n  SheetContentProps\n>(({ side = 'right', className, children, ...props }, ref) => (\n  <SheetPortal>\n    <SheetOverlay />\n    <SheetPrimitive.Content\n      ref={ref}\n      className={cn(sheetVariants({ side }), className)}\n      {...props}\n    >\n      {children}\n      <SheetPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </SheetPrimitive.Close>\n    </SheetPrimitive.Content>\n  </SheetPortal>\n));\nSheetContent.displayName = SheetPrimitive.Content.displayName;\n\nconst SheetHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col space-y-2 text-center sm:text-left',\n      className\n    )}\n    {...props}\n  />\n);\nSheetHeader.displayName = 'SheetHeader';\n\nconst SheetFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',\n      className\n    )}\n    {...props}\n  />\n);\nSheetFooter.displayName = 'SheetFooter';\n\nconst SheetTitle = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Title\n    ref={ref}\n    className={cn('text-lg font-semibold text-foreground', className)}\n    {...props}\n  />\n));\nSheetTitle.displayName = SheetPrimitive.Title.displayName;\n\nconst SheetDescription = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nSheetDescription.displayName = SheetPrimitive.Description.displayName;\n\nexport {\n  Sheet,\n  SheetPortal,\n  SheetOverlay,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n};\n"
  },
  {
    "path": "components/ui/skeleton.tsx",
    "content": "import { cn } from '@/lib/utils';\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn('animate-pulse rounded-md bg-muted', className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "components/ui/slider.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as SliderPrimitive from '@radix-ui/react-slider';\n\nimport { cn } from '@/lib/utils';\n\nconst Slider = React.forwardRef<\n  React.ElementRef<typeof SliderPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <SliderPrimitive.Root\n    ref={ref}\n    className={cn(\n      'relative flex w-full touch-none select-none items-center',\n      className\n    )}\n    {...props}\n  >\n    <SliderPrimitive.Track className=\"relative h-2 w-full grow overflow-hidden rounded-full bg-secondary\">\n      <SliderPrimitive.Range className=\"absolute h-full bg-primary\" />\n    </SliderPrimitive.Track>\n    <SliderPrimitive.Thumb className=\"block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\" />\n  </SliderPrimitive.Root>\n));\nSlider.displayName = SliderPrimitive.Root.displayName;\n\nexport { Slider };\n"
  },
  {
    "path": "components/ui/sonner.tsx",
    "content": "'use client';\n\nimport { useTheme } from 'next-themes';\nimport { Toaster as Sonner } from 'sonner';\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = 'system' } = useTheme();\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps['theme']}\n      className=\"toaster group\"\n      toastOptions={{\n        classNames: {\n          toast:\n            'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',\n          description: 'group-[.toast]:text-muted-foreground',\n          actionButton:\n            'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',\n          cancelButton:\n            'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',\n        },\n      }}\n      {...props}\n    />\n  );\n};\n\nexport { Toaster };\n"
  },
  {
    "path": "components/ui/switch.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as SwitchPrimitives from '@radix-ui/react-switch';\n\nimport { cn } from '@/lib/utils';\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',\n      className\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "components/ui/table.tsx",
    "content": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst Table = React.forwardRef<\n  HTMLTableElement,\n  React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  <div className=\"relative w-full overflow-auto\">\n    <table\n      ref={ref}\n      className={cn('w-full caption-bottom text-sm', className)}\n      {...props}\n    />\n  </div>\n));\nTable.displayName = 'Table';\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />\n));\nTableHeader.displayName = 'TableHeader';\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn('[&_tr:last-child]:border-0', className)}\n    {...props}\n  />\n));\nTableBody.displayName = 'TableBody';\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\n      'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',\n      className\n    )}\n    {...props}\n  />\n));\nTableFooter.displayName = 'TableFooter';\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',\n      className\n    )}\n    {...props}\n  />\n));\nTableRow.displayName = 'TableRow';\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',\n      className\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = 'TableHead';\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}\n    {...props}\n  />\n));\nTableCell.displayName = 'TableCell';\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn('mt-4 text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nTableCaption.displayName = 'TableCaption';\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "components/ui/tabs.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as TabsPrimitive from '@radix-ui/react-tabs';\n\nimport { cn } from '@/lib/utils';\n\nconst Tabs = TabsPrimitive.Root;\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',\n      className\n    )}\n    {...props}\n  />\n));\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',\n      className\n    )}\n    {...props}\n  />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n      className\n    )}\n    {...props}\n  />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "components/ui/textarea.tsx",
    "content": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\nTextarea.displayName = 'Textarea';\n\nexport { Textarea };\n"
  },
  {
    "path": "components/ui/toast.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ToastProvider = ToastPrimitives.Provider\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n      className\n    )}\n    {...props}\n  />\n))\nToastViewport.displayName = ToastPrimitives.Viewport.displayName\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background text-foreground\",\n        destructive:\n          \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  )\n})\nToast.displayName = ToastPrimitives.Root.displayName\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n      className\n    )}\n    {...props}\n  />\n))\nToastAction.displayName = ToastPrimitives.Action.displayName\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n      className\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <X className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n))\nToastClose.displayName = ToastPrimitives.Close.displayName\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn(\"text-sm font-semibold\", className)}\n    {...props}\n  />\n))\nToastTitle.displayName = ToastPrimitives.Title.displayName\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn(\"text-sm opacity-90\", className)}\n    {...props}\n  />\n))\nToastDescription.displayName = ToastPrimitives.Description.displayName\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n}"
  },
  {
    "path": "components/ui/toaster.tsx",
    "content": "\"use client\"\n\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from \"@/components/ui/toast\"\nimport { useToast } from \"@/components/ui/use-toast\"\n\nexport function Toaster() {\n  const { toasts } = useToast()\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        )\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  )\n}"
  },
  {
    "path": "components/ui/toggle-group.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';\nimport { type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\nimport { toggleVariants } from '@/components/ui/toggle';\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants>\n>({\n  size: 'default',\n  variant: 'default',\n});\n\nconst ToggleGroup = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &\n    VariantProps<typeof toggleVariants>\n>(({ className, variant, size, children, ...props }, ref) => (\n  <ToggleGroupPrimitive.Root\n    ref={ref}\n    className={cn('flex items-center justify-center gap-1', className)}\n    {...props}\n  >\n    <ToggleGroupContext.Provider value={{ variant, size }}>\n      {children}\n    </ToggleGroupContext.Provider>\n  </ToggleGroupPrimitive.Root>\n));\n\nToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;\n\nconst ToggleGroupItem = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &\n    VariantProps<typeof toggleVariants>\n>(({ className, children, variant, size, ...props }, ref) => {\n  const context = React.useContext(ToggleGroupContext);\n\n  return (\n    <ToggleGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  );\n});\n\nToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;\n\nexport { ToggleGroup, ToggleGroupItem };\n"
  },
  {
    "path": "components/ui/toggle.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as TogglePrimitive from '@radix-ui/react-toggle';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nconst toggleVariants = cva(\n  'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',\n  {\n    variants: {\n      variant: {\n        default: 'bg-transparent',\n        outline:\n          'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',\n      },\n      size: {\n        default: 'h-10 px-3',\n        sm: 'h-9 px-2.5',\n        lg: 'h-11 px-5',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  }\n);\n\nconst Toggle = React.forwardRef<\n  React.ElementRef<typeof TogglePrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &\n    VariantProps<typeof toggleVariants>\n>(({ className, variant, size, ...props }, ref) => (\n  <TogglePrimitive.Root\n    ref={ref}\n    className={cn(toggleVariants({ variant, size, className }))}\n    {...props}\n  />\n));\n\nToggle.displayName = TogglePrimitive.Root.displayName;\n\nexport { Toggle, toggleVariants };\n"
  },
  {
    "path": "components/ui/tooltip.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip';\n\nimport { cn } from '@/lib/utils';\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n      className\n    )}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "components/ui/use-toast.ts",
    "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport type {\n  ToastActionElement,\n  ToastProps,\n} from \"@/components/ui/toast\"\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\ntype ToasterToast = ToastProps & {\n  id: string\n  title?: React.ReactNode\n  description?: React.ReactNode\n  action?: ToastActionElement\n}\n\nconst actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const\n\nlet count = 0\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_VALUE\n  return count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"]\n      toast: ToasterToast\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"]\n      toast: Partial<ToasterToast>\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n\ninterface State {\n  toasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId)\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    })\n  }, TOAST_REMOVE_DELAY)\n\n  toastTimeouts.set(toastId, timeout)\n}\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      }\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t\n        ),\n      }\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action\n\n      if (toastId) {\n        addToRemoveQueue(toastId)\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id)\n        })\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t\n        ),\n      }\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        }\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      }\n  }\n}\n\nconst listeners: Array<(state: State) => void> = []\n\nlet memoryState: State = { toasts: [] }\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action)\n  listeners.forEach((listener) => {\n    listener(memoryState)\n  })\n}\n\ntype Toast = Omit<ToasterToast, \"id\">\n\nfunction toast({ ...props }: Toast) {\n  const id = genId()\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    })\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id })\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss()\n      },\n    },\n  })\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  }\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState)\n\n  React.useEffect(() => {\n    listeners.push(setState)\n    return () => {\n      const index = listeners.indexOf(setState)\n      if (index > -1) {\n        listeners.splice(index, 1)\n      }\n    }\n  }, [state])\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  }\n}\n\nexport { useToast, toast }"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  }\n}\n"
  },
  {
    "path": "hooks/use-toast.ts",
    "content": "'use client';\n\n// Inspired by react-hot-toast library\nimport * as React from 'react';\n\nimport type { ToastActionElement, ToastProps } from '@/components/ui/toast';\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n  id: string;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  action?: ToastActionElement;\n};\n\nconst actionTypes = {\n  ADD_TOAST: 'ADD_TOAST',\n  UPDATE_TOAST: 'UPDATE_TOAST',\n  DISMISS_TOAST: 'DISMISS_TOAST',\n  REMOVE_TOAST: 'REMOVE_TOAST',\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER;\n  return count.toString();\n}\n\ntype ActionType = typeof actionTypes;\n\ntype Action =\n  | {\n      type: ActionType['ADD_TOAST'];\n      toast: ToasterToast;\n    }\n  | {\n      type: ActionType['UPDATE_TOAST'];\n      toast: Partial<ToasterToast>;\n    }\n  | {\n      type: ActionType['DISMISS_TOAST'];\n      toastId?: ToasterToast['id'];\n    }\n  | {\n      type: ActionType['REMOVE_TOAST'];\n      toastId?: ToasterToast['id'];\n    };\n\ninterface State {\n  toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return;\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId);\n    dispatch({\n      type: 'REMOVE_TOAST',\n      toastId: toastId,\n    });\n  }, TOAST_REMOVE_DELAY);\n\n  toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case 'ADD_TOAST':\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      };\n\n    case 'UPDATE_TOAST':\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t\n        ),\n      };\n\n    case 'DISMISS_TOAST': {\n      const { toastId } = action;\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId);\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id);\n        });\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t\n        ),\n      };\n    }\n    case 'REMOVE_TOAST':\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        };\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      };\n  }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action);\n  listeners.forEach((listener) => {\n    listener(memoryState);\n  });\n}\n\ntype Toast = Omit<ToasterToast, 'id'>;\n\nfunction toast({ ...props }: Toast) {\n  const id = genId();\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: 'UPDATE_TOAST',\n      toast: { ...props, id },\n    });\n  const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });\n\n  dispatch({\n    type: 'ADD_TOAST',\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss();\n      },\n    },\n  });\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  };\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState);\n\n  React.useEffect(() => {\n    listeners.push(setState);\n    return () => {\n      const index = listeners.indexOf(setState);\n      if (index > -1) {\n        listeners.splice(index, 1);\n      }\n    };\n  }, [state]);\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),\n  };\n}\n\nexport { useToast, toast };\n"
  },
  {
    "path": "lib/aliyun-oss-client.ts",
    "content": "import OSS from 'ali-oss';\n\ninterface STSToken {\n  region: string;\n  bucket: string;\n  credentials: {\n    accessKeyId: string;\n    accessKeySecret: string;\n    securityToken: string;\n    expiration: string;\n  };\n}\n\nexport async function getSTSToken(): Promise<STSToken> {\n  const response = await fetch('/api/aliyun/oss/sts');\n  if (!response.ok) {\n    throw new Error('获取上传凭证失败');\n  }\n  const result = await response.json();\n  if (!result.success) {\n    throw new Error(result.message || '获取上传凭证失败');\n  }\n  return result.data;\n}\n\nexport async function uploadToOSS(file: File): Promise<string> {\n  const stsToken = await getSTSToken();\n  \n  const client = new OSS({\n    region: stsToken.region,\n    accessKeyId: stsToken.credentials.accessKeyId,\n    accessKeySecret: stsToken.credentials.accessKeySecret,\n    stsToken: stsToken.credentials.securityToken,\n    bucket: stsToken.bucket,\n    secure: true,\n    timeout: 120000,\n    refreshSTSToken: async () => {\n      const refreshedToken = await getSTSToken();\n      return {\n        accessKeyId: refreshedToken.credentials.accessKeyId,\n        accessKeySecret: refreshedToken.credentials.accessKeySecret,\n        stsToken: refreshedToken.credentials.securityToken\n      };\n    },\n    retryMax: 3,\n    headerEncoding: 'utf-8'\n  });\n\n  // 生成唯一的文件名\n  const ext = file.name.split('.').pop();\n  const filename = `videos/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;\n\n  try {\n    // 使用断点续传\n    const result = await client.multipartUpload(filename, file, {\n      parallel: 4,\n      partSize: 1024 * 1024,\n      progress: function (p, checkpoint) {\n        // 可以添加上传进度回调\n        console.log('上传进度:', Math.floor(p * 100) + '%');\n      }\n    });\n\n    // 返回文件的 URL\n    return client.generateObjectUrl(result.name);\n  } catch (error: any) {\n    console.error('上传文件到 OSS 失败:', error);\n    throw new Error(error.message || '上传文件失败');\n  }\n} "
  },
  {
    "path": "lib/aliyun-oss-upload.ts",
    "content": "import OSS from 'ali-oss';\n\ninterface STSToken {\n  region: string;\n  bucket: string;\n  credentials: {\n    accessKeyId: string;\n    accessKeySecret: string;\n    securityToken: string;\n    expiration: string;\n  };\n}\n\nexport async function getSTSToken(): Promise<STSToken> {\n  const response = await fetch('/api/aliyun/oss/sts');\n  if (!response.ok) {\n    throw new Error('获取上传凭证失败');\n  }\n  const result = await response.json();\n  if (!result.success) {\n    throw new Error(result.message || '获取上传凭证失败');\n  }\n  return result.data;\n}\n\nexport async function uploadToOSS(file: File): Promise<string> {\n  const stsToken = await getSTSToken();\n  \n  const client = new OSS({\n    region: stsToken.region,\n    accessKeyId: stsToken.credentials.accessKeyId,\n    accessKeySecret: stsToken.credentials.accessKeySecret,\n    stsToken: stsToken.credentials.securityToken,\n    bucket: stsToken.bucket,\n    secure: true,\n    timeout: 120000,  // 增加超时时间到 120 秒\n    refreshSTSToken: async () => {\n      const refreshedToken = await getSTSToken();\n      return {\n        accessKeyId: refreshedToken.credentials.accessKeyId,\n        accessKeySecret: refreshedToken.credentials.accessKeySecret,\n        stsToken: refreshedToken.credentials.securityToken\n      };\n    },\n    retryMax: 3,  // 最大重试次数\n    headerEncoding: 'utf-8'\n  });\n\n  // 生成唯一的文件名\n  const ext = file.name.split('.').pop();\n  const filename = `videos/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;\n\n  try {\n    // 使用断点续传\n    const result = await client.multipartUpload(filename, file, {\n      parallel: 4,  // 并发上传分片数\n      partSize: 1024 * 1024,  // 分片大小 1MB\n      progress: function (p, checkpoint) {\n        // 可以添加上传进度回调\n        console.log('上传进度:', Math.floor(p * 100) + '%');\n      }\n    });\n\n    // 返回文件的 URL\n    return client.generateObjectUrl(result.name);\n  } catch (error: any) {\n    console.error('上传文件到 OSS 失败:', error);\n    throw new Error(error.message || '上传文件失败');\n  }\n} "
  },
  {
    "path": "lib/aliyun-oss.ts",
    "content": "\"use client\"\n\nexport async function uploadToOSS(file: File): Promise<string> {\n  try {\n    const formData = new FormData()\n    formData.append('file', file)\n\n    const response = await fetch('/api/aliyun/oss/upload', {\n      method: 'POST',\n      body: formData,\n    })\n\n    if (!response.ok) {\n      const error = await response.json()\n      throw new Error(error.message || '上传失败')\n    }\n\n    const result = await response.json()\n    return result.url\n  } catch (error: any) {\n    console.error('阿里云OSS上传错误:', error)\n    throw new Error(error.message || '文件上传失败')\n  }\n} "
  },
  {
    "path": "lib/aliyun-video-ocr.ts",
    "content": "\"use client\"\n\ninterface VideoOCRResponse {\n  RequestId: string\n  Data: {\n    Status: string\n    Results: Array<{\n      Text: string\n      Timestamp: number\n    }>\n  }\n}\n\nexport async function extractVideoTextWithAliyun(videoUrl: string): Promise<string> {\n  try {\n    // 创建视频OCR任务\n    const createResponse = await fetch('/api/aliyun/video-ocr/create', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        videoUrl,\n      }),\n    })\n\n    if (!createResponse.ok) {\n      const error = await createResponse.json()\n      throw new Error(error.message || '创建视频识别任务失败')\n    }\n\n    const createData = await createResponse.json()\n    const taskId = createData.TaskId\n\n    // 轮询任务状态\n    let result = ''\n    while (true) {\n      const statusResponse = await fetch('/api/aliyun/video-ocr/status', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          taskId,\n        }),\n      })\n\n      if (!statusResponse.ok) {\n        const error = await statusResponse.json()\n        throw new Error(error.message || '查询任务状态失败')\n      }\n\n      const statusData: VideoOCRResponse = await statusResponse.json()\n      \n      if (statusData.Data.Status === 'FAILED') {\n        throw new Error('视频识别失败')\n      }\n      \n      if (statusData.Data.Status === 'FINISHED') {\n        // 按时间戳排序并合并文本\n        const sortedResults = statusData.Data.Results.sort((a, b) => a.Timestamp - b.Timestamp)\n        result = sortedResults.map(item => item.Text).join('\\n')\n        break\n      }\n\n      // 等待1秒后继续查询\n      await new Promise(resolve => setTimeout(resolve, 1000))\n    }\n\n    return result\n  } catch (error: any) {\n    console.error('阿里云视频OCR错误:', error)\n    throw new Error(error.message || '视频识别失败')\n  }\n} "
  },
  {
    "path": "lib/db/migrate.ts",
    "content": "import { neon } from '@neondatabase/serverless'\nimport fs from 'fs'\nimport path from 'path'\nimport dotenv from 'dotenv'\n\n// 加载环境变量\ndotenv.config({ path: '.env.local' })\n\n// 使用环境变量中的数据库URL，如果没有则使用新的数据库URL\nconst sql = neon(process.env.NEW_DATABASE_URL || process.env.DATABASE_URL!)\n\nasync function migrate() {\n  try {\n    // 启用 UUID 扩展\n    await sql`CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"`\n    console.log('Enabled uuid-ossp extension')\n\n    // 删除现有表\n    await sql`DROP TABLE IF EXISTS usage_records`\n    await sql`DROP TABLE IF EXISTS payment_history`\n    await sql`DROP TABLE IF EXISTS auth_users`\n    console.log('Dropped existing tables')\n\n    // 创建用户表\n    await sql`\n      CREATE TABLE auth_users (\n        id SERIAL PRIMARY KEY,\n        email VARCHAR(255) NOT NULL UNIQUE,\n        password_hash VARCHAR(255),\n        name VARCHAR(255),\n        github_id VARCHAR(255) UNIQUE,\n        google_id VARCHAR(255) UNIQUE,\n        stripe_customer_id VARCHAR(255) UNIQUE,\n        stripe_subscription_id VARCHAR(255) UNIQUE,\n        stripe_price_id VARCHAR(255),\n        stripe_current_period_end TIMESTAMP WITH TIME ZONE,\n        text_quota INTEGER DEFAULT -1,\n        image_quota INTEGER DEFAULT 10,\n        pdf_quota INTEGER DEFAULT 8,\n        speech_quota INTEGER DEFAULT 5,\n        video_quota INTEGER DEFAULT 2,\n        quota_reset_at DATE DEFAULT CURRENT_DATE,\n        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n      )\n    `\n    console.log('Created auth_users table')\n\n    // 创建使用记录表\n    await sql`\n      CREATE TABLE usage_records (\n        id SERIAL PRIMARY KEY,\n        user_id INTEGER REFERENCES auth_users(id),\n        type VARCHAR(20) NOT NULL,\n        used_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n        CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES auth_users(id) ON DELETE CASCADE\n      )\n    `\n    console.log('Created usage_records table')\n\n    // 创建支付历史表\n    await sql`\n      CREATE TABLE payment_history (\n        id SERIAL PRIMARY KEY,\n        user_id INTEGER REFERENCES auth_users(id),\n        stripe_invoice_id VARCHAR(255) UNIQUE,\n        amount INTEGER NOT NULL,\n        status VARCHAR(50) NOT NULL,\n        payment_date TIMESTAMP WITH TIME ZONE,\n        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n        CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES auth_users(id) ON DELETE CASCADE\n      )\n    `\n    console.log('Created payment_history table')\n\n    console.log('Migration completed successfully')\n  } catch (error) {\n    console.error('Migration failed:', error)\n    process.exit(1)\n  }\n}\n\nmigrate() "
  },
  {
    "path": "lib/deepseek.ts",
    "content": "\"use client\"\n\nconst API_URL = 'https://api.deepseek.com/v1/chat/completions'\n\n// 重试函数\nasync function retryWithDelay<T>(\n  fn: () => Promise<T>,\n  retries = 3,\n  delay = 1000,\n  backoff = 2\n): Promise<T> {\n  try {\n    return await fn();\n  } catch (error: any) {\n    if (retries === 0) {\n      throw error;\n    }\n    \n    await new Promise(resolve => setTimeout(resolve, delay));\n    return retryWithDelay(fn, retries - 1, delay * backoff, backoff);\n  }\n}\n\n// 使用 DeepSeek API 进行文本翻译\nexport async function translateWithDeepSeek(text: string, targetLang: string) {\n  try {\n    return await retryWithDelay(async () => {\n      // 分段处理长文本\n      const segments = text.split('\\n\\n');\n      const translatedSegments = [];\n      \n      for (const segment of segments) {\n        if (!segment.trim()) {\n          translatedSegments.push('');\n          continue;\n        }\n        \n        const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n            'Authorization': `Bearer ${process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY}`,\n          },\n          body: JSON.stringify({\n            model: 'deepseek-chat',\n            messages: [\n              {\n                role: 'system',\n                content: 'You are a professional translator. Translate the text directly without any explanations.'\n              },\n              {\n                role: 'user',\n                content: `Translate to ${targetLang}:\\n${segment.trim()}`\n              }\n            ],\n            temperature: 0.1,\n            max_tokens: 2048,\n          }),\n        });\n\n        if (!response.ok) {\n          const error = await response.json();\n          throw new Error(error.error?.message || '翻译请求失败');\n        }\n\n        const result = await response.json();\n        const translatedText = result.choices[0].message.content.trim();\n        translatedSegments.push(translatedText);\n      }\n      \n      return translatedSegments.join('\\n\\n');\n    });\n  } catch (error: any) {\n    console.error('Error translating with DeepSeek:', error);\n    throw new Error(error.message || '翻译失败，请稍后重试');\n  }\n}\n\nexport async function extractTextWithDeepseek(file: File): Promise<string> {\n  try {\n    const formData = new FormData()\n    formData.append('file', file)\n\n    const response = await fetch('/api/file/extract', {\n      method: 'POST',\n      body: formData,\n    })\n\n    if (!response.ok) {\n      const error = await response.json()\n      throw new Error(error.message || '文件识别失败')\n    }\n\n    const data = await response.json()\n    if (!data.success) {\n      throw new Error(data.message || '文件识别失败')\n    }\n\n    return data.result\n  } catch (error: any) {\n    console.error('DeepSeek文件识别错误:', error)\n    throw error\n  }\n} "
  },
  {
    "path": "lib/gemini.ts",
    "content": "\"use client\"\n\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\n\nconst genAI = new GoogleGenerativeAI(process.env.NEXT_PUBLIC_GEMINI_API_KEY || '');\n\n// 重试函数\nasync function retryWithDelay<T>(\n  fn: () => Promise<T>,\n  retries = 3,\n  delay = 1000,\n  backoff = 2\n): Promise<T> {\n  try {\n    return await fn();\n  } catch (error: any) {\n    if (retries === 0 || error?.status !== 429) {\n      throw error;\n    }\n    \n    await new Promise(resolve => setTimeout(resolve, delay));\n    return retryWithDelay(fn, retries - 1, delay * backoff, backoff);\n  }\n}\n\n// 使用 Gemini-1.5-flash 进行图片文字提取\nexport async function extractTextFromImage(imageData: string) {\n  const model = genAI.getGenerativeModel({ model: \"gemini-1.5-flash\" });\n  \n  try {\n    return await retryWithDelay(async () => {\n      const result = await model.generateContent([\n        {\n          inlineData: {\n            mimeType: \"image/jpeg\",\n            data: imageData.split(\",\")[1]\n          }\n        },\n        \"Extract all text from this image and return it as plain text. Please maintain the original text structure and layout as much as possible. If there are multiple languages in the image, please identify and preserve them all.\",\n      ]);\n      const response = await result.response;\n      return response.text();\n    });\n  } catch (error: any) {\n    if (error?.status === 429) {\n      console.error('API quota exceeded. Please try again later.');\n      throw new Error('API 配额已超限，请稍后再试');\n    }\n    if (error?.message?.includes('not found') || error?.message?.includes('deprecated')) {\n      console.error('Model not available:', error);\n      throw new Error('当前模型不可用，请联系开发者更新');\n    }\n    console.error('Error extracting text:', error);\n    throw error;\n  }\n}\n\n// 添加别名导出\nexport const extractTextWithGemini = extractTextFromImage;\n\n// 使用 Gemini Pro 进行文本翻译\nexport async function translateText(text: string, targetLang: string) {\n  const model = genAI.getGenerativeModel({ model: \"gemini-pro\" });\n  \n  try {\n    return await retryWithDelay(async () => {\n      // 分段处理长文本\n      const segments = text.split('\\n\\n');\n      const translatedSegments = [];\n      \n      for (const segment of segments) {\n        if (!segment.trim()) {\n          translatedSegments.push('');\n          continue;\n        }\n        \n        const result = await model.generateContent([\n          `Translate to ${targetLang}:`,\n          segment.trim()\n        ]);\n        \n        const response = await result.response;\n        translatedSegments.push(response.text().trim());\n      }\n      \n      return translatedSegments.join('\\n\\n');\n    });\n  } catch (error: any) {\n    if (error?.status === 429) {\n      console.error('API quota exceeded. Please try again later.');\n      throw new Error('API 配额已超限，请稍后再试');\n    }\n    if (error?.message?.includes('blocked') || error?.message?.includes('SAFETY')) {\n      console.error('Content was blocked:', error);\n      throw new Error('翻译内容被阻止，请尝试修改文本');\n    }\n    console.error('Error translating text:', error);\n    throw error;\n  }\n}\n\n// 使用 Gemini Pro 进行文本优化\nexport async function improveText(text: string, targetLang: string) {\n  const model = genAI.getGenerativeModel({ model: \"gemini-pro\" });\n  \n  try {\n    return await retryWithDelay(async () => {\n      const prompt = `请对以下${targetLang}文本进行润色，使其更加流畅自然。\n\n原文：\n${text}\n\n要求：\n- 直接返回润色后的文本\n- 不要添加任何解释或说明\n- 保持段落和换行格式\n- 保持标点符号的使用`;\n      \n      const result = await model.generateContent({\n        contents: [{ role: \"user\", parts: [{ text: prompt }] }],\n        generationConfig: {\n          temperature: 0.1,\n          topK: 1,\n          topP: 0.1,\n          maxOutputTokens: 2048,\n        },\n      });\n      \n      const response = await result.response;\n      return response.text();\n    });\n  } catch (error: any) {\n    if (error?.status === 429) {\n      console.error('API quota exceeded. Please try again later.');\n      throw new Error('API 配额已超限，请稍后再试');\n    }\n    console.error('Error improving text:', error);\n    throw error;\n  }\n}"
  },
  {
    "path": "lib/hooks/use-analytics.ts",
    "content": "'use client'\n\nexport const useAnalytics = () => {\n  const trackEvent = (\n    action: string,\n    category: string,\n    label: string,\n    value?: number\n  ) => {\n    if (typeof window !== 'undefined' && (window as any).gtag) {\n      ;(window as any).gtag('event', action, {\n        event_category: category,\n        event_label: label,\n        value: value,\n      })\n    }\n  }\n\n  const trackPageView = (url: string) => {\n    if (typeof window !== 'undefined' && (window as any).gtag) {\n      ;(window as any).gtag('config', process.env.NEXT_PUBLIC_GA_ID, {\n        page_path: url,\n      })\n    }\n  }\n\n  return { trackEvent, trackPageView }\n} "
  },
  {
    "path": "lib/hooks/use-quota.ts",
    "content": "import { useState } from 'react'\nimport { useI18n } from '@/lib/i18n/use-translations'\n\ntype UsageType = 'text' | 'image' | 'pdf' | 'speech' | 'video'\n\ninterface UseQuotaOptions {\n  onError?: (error: string) => void\n  onSuccess?: (remaining: number) => void\n}\n\nexport function useQuota(options: UseQuotaOptions = {}) {\n  const [isChecking, setIsChecking] = useState(false)\n  const { t } = useI18n()\n\n  const checkAndRecord = async (type: UsageType) => {\n    try {\n      setIsChecking(true)\n      const response = await fetch('/api/user/usage', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ type }),\n      })\n\n      const data = await response.json()\n\n      if (!response.ok) {\n        throw new Error(data.error ? t(data.error) : t('quota.errors.checkFailed'))\n      }\n\n      options.onSuccess?.(data.remaining)\n      return true\n    } catch (error) {\n      const message = error instanceof Error ? \n        (error.message.startsWith('quota.') ? t(error.message) : error.message) : \n        t('quota.errors.checkFailed')\n      options.onError?.(message)\n      return false\n    } finally {\n      setIsChecking(false)\n    }\n  }\n\n  return {\n    isChecking,\n    checkAndRecord,\n  }\n} "
  },
  {
    "path": "lib/i18n/locales/en.json",
    "content": "{\n  \"tabs\": {\n    \"image\": \"Image Recognition\",\n    \"speech\": \"Speech Recognition\",\n    \"text\": \"Text Translation\",\n    \"video\": \"Video Recognition\",\n    \"file\": \"File Recognition\",\n    \"pdf\": \"PDF Recognition\"\n  },\n  \"buttons\": {\n    \"getStarted\": \"Get Started\",\n    \"startTranslate\": \"Start Translation\"\n  },\n  \"uploadImage\": \"Upload Image\",\n  \"uploadAudio\": \"Upload Audio\",\n  \"uploadVideo\": \"Upload Video\",\n  \"uploadPDF\": \"Upload PDF\",\n  \"dropHere\": \"Drop to upload\",\n  \"dragAndDrop\": \"Drag and drop file here, or click to upload\",\n  \"dragAndDropPDF\": \"Drag and drop PDF file here, or click to select\",\n  \"selectPDF\": \"Select PDF\",\n  \"selectPDFService\": \"Select PDF Service\",\n  \"extractAction\": \"Extract Text\",\n  \"extractingStatus\": \"Extracting...\",\n  \"targetLanguage\": \"Select Target Language\",\n  \"serviceProvider\": \"Select Service\",\n  \"translateAction\": \"Translate\",\n  \"translating\": \"Translating...\",\n  \"improveTranslation\": \"Improve\",\n  \"improveTooltip\": \"Use AI to improve translation\",\n  \"extractedText\": \"Extracted Text\",\n  \"translatedText\": \"Translation Result\",\n  \"noTextExtracted\": \"No text extracted yet\",\n  \"startListening\": \"Start Recording\",\n  \"stopListening\": \"Stop Recording\",\n  \"selectImage\": \"Select Image\",\n  \"processingStatus\": \"Processing...\",\n  \"appName\": \"AI Translation Assistant\",\n  \"switchLanguage\": \"Switch Language\",\n  \"toggleTheme\": \"Toggle Theme\",\n  \"errors\": {\n    \"image\": {\n      \"title\": \"Image Recognition Failed\",\n      \"desc\": \"Please try again later\"\n    },\n    \"pdf\": {\n      \"title\": \"PDF Processing Failed\",\n      \"desc\": \"Please try again later\"\n    },\n    \"text\": {\n      \"title\": \"Translation Failed\",\n      \"desc\": \"Please try again later\"\n    },\n    \"speech\": {\n      \"title\": \"Speech Recognition Failed\",\n      \"desc\": \"Please try again later\"\n    },\n    \"video\": {\n      \"title\": \"Video Processing Failed\",\n      \"desc\": \"Please try again later\"\n    },\n    \"extract\": {\n      \"extractingError\": \"Text Extraction Failed\",\n      \"extractingDesc\": \"Please try again later\"\n    },\n    \"noLanguage\": \"No Target Language\",\n    \"noLanguageDesc\": \"Please select a target language\",\n    \"noTranslation\": \"No Translation\",\n    \"noTranslationDesc\": \"Please translate first\",\n    \"translationError\": \"Translation Failed\",\n    \"translationDesc\": \"An error occurred during translation, please try again\",\n    \"improvementError\": \"Improvement Failed\",\n    \"improvementDesc\": \"An error occurred during improvement, please try again\",\n    \"speechNotSupported\": \"Speech Recognition Not Available\",\n    \"speechNotSupportedDesc\": \"Your browser does not support speech recognition\",\n    \"invalidFile\": \"Invalid File\",\n    \"invalidFileDesc\": \"Please select a valid file\",\n    \"authRequired\": \"Authentication Required\",\n    \"pleaseLogin\": \"Please log in to access this feature\",\n    \"cancelButton\": \"Cancel\",\n    \"registerButton\": \"Register\",\n    \"loginButton\": \"Log In\"\n  },\n  \"error\": {\n    \"authRequired\": \"Authentication Required\",\n    \"pleaseLogin\": \"Please log in to access this feature\",\n    \"cancelButton\": \"Cancel\",\n    \"registerButton\": \"Register\",\n    \"loginButton\": \"Log In\",\n    \"usageLimitExceeded\": \"Usage Limit Exceeded\",\n    \"textLimitExceededDesc\": \"You have reached your daily text translation limit\",\n    \"imageLimitExceededDesc\": \"You have reached your daily image recognition limit\",\n    \"pdfLimitExceededDesc\": \"You have reached your daily PDF processing limit\",\n    \"speechLimitExceededDesc\": \"You have reached your daily speech recognition limit\",\n    \"videoLimitExceededDesc\": \"You have reached your daily video processing limit\",\n    \"invalidImageFile\": \"Invalid Image File\",\n    \"invalidImageFileDesc\": \"Please select a valid image file\",\n    \"invalidFile\": \"Invalid File\",\n    \"invalidFileDesc\": \"Please select a valid file\",\n    \"invalidAudioFile\": \"Invalid Audio File\",\n    \"invalidAudioFileDesc\": \"Please select a valid audio file\",\n    \"audioRecognition\": \"Audio Recognition Failed\",\n    \"audioProcessing\": \"Audio Processing Failed\",\n    \"invalidVideoFile\": \"Invalid Video File\",\n    \"invalidVideoFileDesc\": \"Please select a valid video file\",\n    \"videoProcessingDesc\": \"Failed to process video, please try again\",\n    \"noTextExtracted\": \"No Text Extracted\",\n    \"noTextExtractedDesc\": \"Could not extract any text from the file\",\n    \"videoUploadFailed\": \"Video Upload Failed\",\n    \"videoUploadFailedDesc\": \"Failed to upload video, please try again\",\n    \"videoProcessing\": \"Video Processing Failed\",\n    \"invalidPDFFile\": \"Invalid PDF File\",\n    \"noText\": \"No Text\",\n    \"noTextDesc\": \"Please enter some text to translate\",\n    \"noLanguage\": \"No Target Language\",\n    \"noLanguageDesc\": \"Please select a target language\",\n    \"translationError\": \"Translation Failed\",\n    \"noTranslation\": \"No Translation\",\n    \"noTranslationDesc\": \"Please translate first\",\n    \"improving\": \"Improvement Failed\",\n    \"improvingDesc\": \"Failed to improve translation, please try again\",\n    \"speechNotSupported\": \"Speech Recognition Not Available\",\n    \"speechNotSupportedDesc\": \"Your browser does not support speech recognition\",\n    \"speechRecognition\": \"Speech Recognition Failed\",\n    \"videoServiceNotSupported\": \"Video service not supported\",\n    \"taskProcessingFailed\": \"Task processing failed\",\n    \"taskProcessingTimeout\": \"Task processing timeout\"\n  },\n  \"console\": {\n    \"quotaFetchFailed\": \"Failed to fetch quota:\",\n    \"quotaInfoFetchFailed\": \"Failed to fetch quota information:\",\n    \"usageUpdateFailed\": \"Failed to update usage count:\",\n    \"videoTaskCreateFailed\": \"Failed to create video recognition task:\",\n    \"ocrProcessingError\": \"Error processing OCR result:\",\n    \"videoUploadError\": \"Video upload error:\",\n    \"videoProcessingError\": \"Video processing error:\",\n    \"fileProcessingFailed\": \"File processing failed:\",\n    \"translationServiceError\": \"{0} translation service error:\",\n    \"usingFallbackService\": \"Trying to use DeepSeek as fallback service...\",\n    \"translationError\": \"Translation error:\",\n    \"fileProcessingError\": \"File processing error:\",\n    \"taskQueryFailed\": \"Failed to query task result:\",\n    \"pollingResult\": \"Polling returned result:\",\n    \"startingPdfProcessing\": \"Starting to process PDF file with {0}:\",\n    \"pdfProcessingStatus\": \"PDF processing status:\",\n    \"pdfProcessingComplete\": \"PDF processing complete, extracted text length:\",\n    \"extractedTextPreview\": \"Extracted text preview:\",\n    \"videoFramesExtracting\": \"Starting to extract video frames...\",\n    \"videoFramesExtracted\": \"Video frames extraction complete, frame count: {0}\",\n    \"videoFramesExample\": \"First frame data example: {0}\",\n    \"videoProcessed\": \"Video processing complete, extracted text length: {0}\",\n    \"videoUploaded\": \"Video uploaded successfully, URL: {0}\",\n    \"videoTaskPolling\": \"Starting to poll task status...\",\n    \"videoOcrProcessing\": \"Starting to process OCR result:\",\n    \"videoOcrResult\": \"Extracted text: {0}\",\n    \"videoNoText\": \"No text extracted\",\n    \"videoOcrError\": \"Error processing OCR result:\",\n    \"videoTaskResult\": \"Polling returned result:\",\n    \"videoTaskFailed\": \"Task processing failed\",\n    \"videoTaskQueryFailed\": \"Failed to query task result:\",\n    \"videoTaskTimeout\": \"Task processing timeout\",\n    \"pdfProcessing\": \"Starting to process PDF file with {0}: {1}\",\n    \"pdfStatus\": \"PDF processing status: {0}\",\n    \"pdfProcessed\": \"PDF processing complete, extracted text length: {0}\",\n    \"pdfPreview\": \"Extracted text preview: {0}...\",\n    \"tryingDeepSeek\": \"Trying to use DeepSeek as fallback service...\",\n    \"selectedLanguage\": \"Selected language: {0}\"\n  },\n  \"theme\": {\n    \"light\": \"Light\",\n    \"dark\": \"Dark\",\n    \"system\": \"System\"\n  },\n  \"languages\": {\n    \"english\": \"English\",\n    \"chinese\": \"中文\"\n  },\n  \"selectAsrService\": \"Select Speech Recognition Service\",\n  \"asrServices\": {\n    \"tencent\": \"Tencent Cloud\"\n  },\n  \"enterText\": \"Enter text to translate\",\n  \"chooseFile\": \"Choose File\",\n  \"ocrServices\": {\n    \"tencent\": \"Tencent Cloud\",\n    \"qwen\": \"Qwen\",\n    \"gemini\": \"Gemini\",\n    \"zhipu\": \"Zhipu AI\",\n    \"kimi\": \"Kimi\",\n    \"step\": \"Step AI\"\n  },\n  \"translationServices\": {\n    \"deepseek\": \"DeepSeek\",\n    \"qwen\": \"Qwen\",\n    \"gemini\": \"Gemini\",\n    \"zhipu\": \"Zhipu GLM4\",\n    \"hunyuan\": \"Tencent Hunyuan\",\n    \"4o-mini\": \"OpenAI\",\n    \"minnimax\": \"MinniMax\",\n    \"siliconflow\": \"Llama-3.3\",\n    \"claude_3_5\": \"Claude 3.5\",\n    \"kimi\": \"Kimi\",\n    \"step\": \"Step AI\"\n  },\n  \"videoServices\": {\n    \"gemini\": \"Gemini\",\n    \"zhipu\": \"Zhipu GLM4\",\n    \"qwen\": \"Qwen\",\n    \"kimi\": \"Kimi\"\n  },\n  \"pdfServices\": {\n    \"tencent\": \"Tencent Cloud\",\n    \"qwen\": \"Qwen\",\n    \"gemini\": \"Gemini\",\n    \"zhipu\": \"Zhipu AI\",\n    \"kimi\": \"Kimi\",\n    \"step\": \"Step AI\"\n  },\n  \"title\": \"AI Translation Assistant\",\n  \"description\": \"High-quality multilingual translation powered by AI technology\",\n  \"landing\": {\n    \"hero\": {\n      \"appTitle\": \"AI Translation Assistant\",\n      \"subtitle\": \"One-stop multilingual translation solution, supporting text, image, PDF, speech and video translation\",\n      \"cta\": \"Get Started\",\n      \"badge\": \"AI-Powered · Multilingual Support\",\n      \"subscribe\": \"Subscribe\"\n    },\n    \"features\": {\n      \"title\": \"Core Features\",\n      \"subtitle\": \"Comprehensive translation services for all your needs\",\n      \"text\": {\n        \"title\": \"Text Translation\",\n        \"description\": \"Translate long content with precise semantics and natural style\"\n      },\n      \"image\": {\n        \"title\": \"Image Translation\",\n        \"description\": \"Smart text recognition and translation from images\"\n      },\n      \"pdf\": {\n        \"title\": \"PDF Translation\",\n        \"description\": \"Extract and translate PDF documents while preserving formatting\"\n      },\n      \"speech\": {\n        \"title\": \"Speech Translation\",\n        \"description\": \"Real-time speech recognition and translation in multiple languages\"\n      },\n      \"video\": {\n        \"title\": \"Video Translation\",\n        \"description\": \"Generate multilingual subtitles for learning and content creation\"\n      }\n    },\n    \"highlights\": {\n      \"title\": \"Key Benefits\",\n      \"subtitle\": \"Experience the ultimate translation solution without barriers\",\n      \"multilingual\": {\n        \"title\": \"Multilingual Support\",\n        \"description\": \"Switch between Chinese and English interfaces\"\n      },\n      \"theme\": {\n        \"title\": \"Theme Switching\",\n        \"description\": \"Dark and light modes for better visual experience\"\n      },\n      \"privacy\": {\n        \"title\": \"Privacy Protection\",\n        \"description\": \"No data storage, ensuring information security\"\n      },\n      \"web\": {\n        \"title\": \"Web-based\",\n        \"description\": \"No download required, use anywhere anytime\"\n      }\n    },\n    \"steps\": {\n      \"title\": \"How It Works\",\n      \"subtitle\": \"Three simple steps to get your translation\",\n      \"select\": {\n        \"title\": \"Choose Function\",\n        \"description\": \"Select text, image, speech, or video translation\"\n      },\n      \"upload\": {\n        \"title\": \"Upload Content\",\n        \"description\": \"Upload files or input text directly\"\n      },\n      \"translate\": {\n        \"title\": \"Get Results\",\n        \"description\": \"Receive accurate translation quickly\"\n      }\n    },\n    \"testimonials\": {\n      \"title\": \"Testimonials\",\n      \"subtitle\": \"See what our users say about our service\",\n      \"1\": {\n        \"quote\": \"This translation tool is amazing! It supports multiple formats and delivers high-quality translations. The image text recognition feature has saved me tons of time.\",\n        \"author\": \"Zhang Ming\",\n        \"role\": \"Freelance Translator\"\n      },\n      \"2\": {\n        \"quote\": \"The interface is very intuitive and user-friendly. Dark mode is a blessing for someone like me who often works late.\",\n        \"author\": \"Li Hua\",\n        \"role\": \"Content Creator\"\n      },\n      \"3\": {\n        \"quote\": \"The PDF translation feature is incredibly useful, perfectly preserving the original format. It's been a great help for my academic research.\",\n        \"author\": \"Wang Fang\",\n        \"role\": \"Graduate Student\"\n      },\n      \"4\": {\n        \"quote\": \"As a developer, I appreciate the technical implementation. Fast API response and support for batch processing make it highly efficient.\",\n        \"author\": \"Chen Jie\",\n        \"role\": \"Software Engineer\"\n      },\n      \"5\": {\n        \"quote\": \"The speech translation feature works exceptionally well with high accuracy. It's essential for our international business.\",\n        \"author\": \"Liu Lin\",\n        \"role\": \"Business Manager\"\n      },\n      \"6\": {\n        \"quote\": \"The video subtitle translation feature has solved many problems for me, especially the automatic timeline sync which saves a lot of manual adjustment work.\",\n        \"author\": \"Zhao Yang\",\n        \"role\": \"Video Producer\"\n      }\n    },\n    \"footer\": {\n      \"browsers\": \"Supports all major browsers\",\n      \"privacy\": \"Committed to protecting your privacy\"\n    }\n  },\n  \"translate\": {\n    \"selectVideoService\": \"Select Video Service\",\n    \"videoService\": \"Video Recognition Service\",\n    \"zhipu\": \"Zhipu AI\",\n    \"aliyun\": \"Alibaba Cloud\",\n    \"uploadVideo\": \"Upload Video\"\n  },\n  \"banner\": {\n    \"text\": \"AI Translation Assistant is now live with limited-time free trial. Follow us on Twitter for updates\",\n    \"twitter\": \"https://x.com/zyailive\"\n  },\n  \"usage\": {\n    \"remainingToday\": \"Remaining Today: {0}/{1}\",\n    \"textTranslation\": \"Text Translation\",\n    \"imageRecognition\": \"Image Recognition\",\n    \"pdfRecognition\": \"PDF Recognition\",\n    \"speechRecognition\": \"Speech Recognition\",\n    \"videoRecognition\": \"Video Recognition\",\n    \"dailyLimit\": \"Daily Free Usage\",\n    \"loginToGet\": \"Login to access more features\",\n    \"unlimited\": \"Unlimited text translation\"\n  },\n  \"auth\": {\n    \"signIn\": \"Sign In\",\n    \"signUp\": \"Sign Up\",\n    \"profileButton\": \"Profile\",\n    \"profile\": {\n      \"title\": \"Profile\",\n      \"description\": \"View your account information and usage quotas\",\n      \"basicInfo\": {\n        \"title\": \"Basic Information\",\n        \"email\": \"Email\",\n        \"username\": \"Username\",\n        \"userId\": \"User ID\",\n        \"registerTime\": \"Registration Time\",\n        \"editUsername\": {\n          \"placeholder\": \"Enter new username\",\n          \"save\": \"Save\",\n          \"cancel\": \"Cancel\",\n          \"success\": \"Username updated successfully\",\n          \"error\": {\n            \"empty\": \"Username cannot be empty\",\n            \"failed\": \"Update failed, please try again\"\n          }\n        },\n        \"social\": {\n          \"githubProvider\": \"GitHub\",\n          \"googleProvider\": \"Google\",\n          \"bound\": \"Connected\",\n          \"unbound\": \"Not Connected\",\n          \"bind\": \"Connect %{provider}\",\n          \"bindFailed\": \"Failed to connect %{provider}\"\n        }\n      },\n      \"subscription\": {\n        \"title\": \"Subscription\",\n        \"plan\": {\n          \"trial\": \"Free Plan\",\n          \"monthly\": \"Monthly Plan\",\n          \"yearly\": \"Yearly Plan\"\n        },\n        \"customerId\": \"Customer ID\",\n        \"subscriptionId\": \"Subscription ID\",\n        \"expiryDate\": \"Expires: {date}\",\n        \"success\": \"Subscription successful! Thank you for your support\",\n        \"free\": \"Free\",\n        \"premium\": \"Premium\",\n        \"enterprise\": \"Enterprise\",\n        \"quota\": {\n          \"text\": \"Text Translation\",\n          \"image\": \"Image Recognition\",\n          \"pdf\": \"PDF Processing\",\n          \"speech\": \"Speech Recognition\",\n          \"video\": \"Video Processing\",\n          \"textTranslation\": \"Text Translation\",\n          \"imageRecognition\": \"Image Recognition\",\n          \"pdfRecognition\": \"PDF Recognition\",\n          \"speechRecognition\": \"Speech Recognition\",\n          \"videoRecognition\": \"Video Recognition\",\n          \"unlimited\": \"Unlimited\",\n          \"usage\": \"{used}/{quota}\"\n        },\n        \"dialog\": {\n          \"title\": \"Upgrade to Premium\",\n          \"description\": \"Upgrade to premium for more daily usage and a smoother translation experience.\",\n          \"benefits\": {\n            \"title\": \"Premium Benefits:\",\n            \"imageQuota\": \"50 image recognitions per day\",\n            \"pdfQuota\": \"40 PDF processings per day\",\n            \"speechQuota\": \"30 speech recognitions per day\",\n            \"videoQuota\": \"10 video processings per day\",\n            \"priority\": \"Priority processing queue\"\n          },\n          \"buttons\": {\n            \"cancel\": \"Not Now\",\n            \"viewPlans\": \"View Plans\"\n          }\n        }\n      }\n    },\n    \"signOut\": {\n      \"action\": \"Sign Out\",\n      \"success\": \"Signed out successfully\",\n      \"error\": \"Failed to sign out\"\n    },\n    \"login\": {\n      \"title\": \"Sign in\",\n      \"subtitle\": \"Enter your email and password to sign in\",\n      \"email\": \"Email\",\n      \"emailPlaceholder\": \"Enter your email\",\n      \"password\": \"Password\",\n      \"passwordPlaceholder\": \"Enter your password\",\n      \"button\": \"Sign in\",\n      \"loading\": \"Signing in...\",\n      \"noAccount\": \"Don't have an account?\",\n      \"error\": {\n        \"invalidCredentials\": \"Invalid email or password\",\n        \"required\": \"Please enter email and password\"\n      },\n      \"github\": \"Continue with GitHub\",\n      \"or\": \"or\",\n      \"google\": \"Continue with Google\"\n    },\n    \"register\": {\n      \"title\": \"Sign up\",\n      \"subtitle\": \"Create your account\",\n      \"email\": \"Email\",\n      \"emailPlaceholder\": \"Enter your email\",\n      \"password\": \"Password\",\n      \"passwordPlaceholder\": \"Enter your password\",\n      \"confirmPassword\": \"Confirm password\",\n      \"confirmPasswordPlaceholder\": \"Enter your password again\",\n      \"button\": \"Sign up\",\n      \"loading\": \"Signing up...\",\n      \"hasAccount\": \"Already have an account?\",\n      \"error\": {\n        \"passwordMismatch\": \"Passwords do not match\",\n        \"emailExists\": \"Email is already registered\",\n        \"required\": \"Please fill in all required fields\"\n      },\n      \"github\": \"Continue with GitHub\",\n      \"or\": \"or\",\n      \"success\": \"Registration successful\",\n      \"google\": \"Continue with Google\"\n    },\n    \"error\": {\n      \"accessDenied\": \"Access denied, please try again\",\n      \"default\": \"An error occurred during login, please try again\"\n    }\n  },\n  \"quota\": {\n    \"types\": {\n      \"text\": \"Text Translation\",\n      \"image\": \"Image Recognition\",\n      \"pdf\": \"PDF Recognition\",\n      \"speech\": \"Speech Recognition\",\n      \"video\": \"Video Recognition\"\n    },\n    \"unlimited\": \"Unlimited\",\n    \"times\": \"times\",\n    \"remaining\": \"Remaining: %{count}\",\n    \"errors\": {\n      \"notLoggedIn\": \"Please login first\",\n      \"userNotFound\": \"User not found\",\n      \"invalidType\": \"Invalid usage type\",\n      \"limitReached\": \"Daily usage limit reached\",\n      \"recordFailed\": \"Failed to record usage\",\n      \"checkFailed\": \"Failed to check quota\"\n    }\n  },\n  \"pricing\": {\n    \"title\": \"Pricing Plans\",\n    \"subtitle\": \"Choose your plan and unlock unlimited translation\",\n    \"tiers\": {\n      \"free\": {\n        \"freeName\": \"Free Trial\",\n        \"freeDescription\": \"Experience basic features\",\n        \"freeCta\": \"Start Trial\",\n        \"features\": {\n          \"basic\": [\n            \"Unlimited text translation\",\n            \"5 image recognitions/day\",\n            \"3 PDF recognitions/day\"\n          ],\n          \"freeAdvanced\": [\n            \"2 speech recognitions/day\",\n            \"1 video recognition/day\"\n          ],\n          \"freeSupport\": [\n            \"Basic support\"\n          ]\n        }\n      },\n      \"monthly\": {\n        \"name\": \"Monthly Plan\",\n        \"description\": \"Perfect for individual use\",\n        \"cta\": \"Subscribe Now\",\n        \"period\": \"mo\",\n        \"features\": {\n          \"basic\": [\n            \"Unlimited text translation\",\n            \"50 image recognitions/day\",\n            \"40 PDF recognitions/day\"\n          ],\n          \"advanced\": [\n            \"30 speech recognitions/day\",\n            \"10 video recognitions/day\",\n            \"Priority processing\"\n          ],\n          \"support\": [\n            \"Priority support\"\n          ]\n        }\n      },\n      \"yearly\": {\n        \"name\": \"Annual Plan\",\n        \"description\": \"Best value for money\",\n        \"cta\": \"Subscribe Now\",\n        \"recommended\": \"Recommended\",\n        \"discount\": \"Save 17%\",\n        \"features\": {\n          \"basic\": [\n            \"Unlimited text translation\",\n            \"100 image recognitions/day\",\n            \"80 PDF recognitions/day\"\n          ],\n          \"advanced\": [\n            \"60 speech recognitions/day\",\n            \"20 video recognitions/day\",\n            \"Priority processing\"\n          ],\n          \"support\": [\n            \"24/7 dedicated support\",\n            \"API access\"\n          ]\n        }\n      }\n    },\n    \"features\": {\n      \"basic\": \"Basic Features\",\n      \"advanced\": \"Advanced Features\",\n      \"support\": \"Support Services\"\n    },\n    \"faq\": {\n      \"title\": \"FAQ\",\n      \"subtitle\": \"If you have any other questions, feel free to contact us\",\n      \"cards\": {\n        \"security\": {\n          \"title\": \"Data Security\",\n          \"content\": \"We don't store any of your files or translation content. All data is only temporarily used during processing and won't be uploaded to the cloud, ensuring your privacy throughout.\"\n        },\n        \"quota\": {\n          \"title\": \"Quota Calculation\",\n          \"content\": \"Quotas reset daily at midnight. Unused quotas won't accumulate to the next day.\"\n        },\n        \"payment\": {\n          \"title\": \"Payment Methods\",\n          \"content\": \"We support debit cards, credit cards, WeChat Pay, and Alipay. All payment information is encrypted to ensure your financial security.\"\n        },\n        \"support\": {\n          \"title\": \"Contact Support\",\n          \"content\": \"If you have any questions or need assistance, please email us at wt@wmcircle.cn and we'll get back to you as soon as possible.\"\n        }\n      }\n    }\n  },\n  \"nav\": {\n    \"home\": \"Home\",\n    \"features\": \"Features\",\n    \"pricing\": \"Pricing\"\n  }\n}"
  },
  {
    "path": "lib/i18n/locales/zh.json",
    "content": "{\n  \"tabs\": {\n    \"image\": \"图片识别\",\n    \"speech\": \"语音识别\",\n    \"text\": \"文本翻译\",\n    \"video\": \"视频识别\",\n    \"file\": \"文件识别\",\n    \"pdf\": \"PDF识别\"\n  },\n  \"buttons\": {\n    \"getStarted\": \"立即体验\",\n    \"startTranslate\": \"开始翻译\"\n  },\n  \"uploadImage\": \"上传图片\",\n  \"uploadAudio\": \"上传语音\",\n  \"uploadVideo\": \"上传视频\",\n  \"dropHere\": \"放开以上传\",\n  \"dragAndDrop\": \"拖放图片到此处，或点击选择文件\",\n  \"dragAndDropPDF\": \"拖放PDF文件到此处，或点击选择文件\",\n  \"extractAction\": \"提取文字\",\n  \"extractingStatus\": \"提取中...\",\n  \"targetLanguage\": \"选择目标语言\",\n  \"selectPDFService\": \"选择PDF处理服务\",\n  \"serviceProvider\": \"选择服务\",\n  \"translateAction\": \"开始翻译\",\n  \"translating\": \"翻译中...\",\n  \"improveTranslation\": \"优化\",\n  \"improveTooltip\": \"使用 AI 优化翻译结果\",\n  \"extractedText\": \"提取的文字\",\n  \"translatedText\": \"翻译结果\",\n  \"noTextExtracted\": \"尚未提取文字\",\n  \"noTranslation\": \"尚未翻译\",\n  \"startListening\": \"开始录音识别\",\n  \"stopListening\": \"停止录音\",\n  \"selectImage\": \"选择图片\",\n  \"selectPDF\": \"选择PDF文件\",\n  \"error\": {\n    \"authRequired\": \"需要登录\",\n    \"pleaseLogin\": \"请登录以访问此功能\",\n    \"cancelButton\": \"取消\",\n    \"registerButton\": \"注册\",\n    \"loginButton\": \"登录\",\n    \"usageLimitExceeded\": \"使用次数已达上限\",\n    \"textLimitExceededDesc\": \"您已达到每日文本翻译次数上限\",\n    \"imageLimitExceededDesc\": \"今日图片识别次数已用完，请明天再试\",\n    \"pdfLimitExceededDesc\": \"今日PDF识别次数已用完，请明天再试\",\n    \"speechLimitExceededDesc\": \"今日语音识别次数已用完，请明天再试\",\n    \"videoLimitExceededDesc\": \"今日视频识别次数已用完，请明天再试\",\n    \"invalidImageFile\": \"无效的图片文件\",\n    \"invalidImageFileDesc\": \"请选择有效的图片文件\",\n    \"invalidFile\": \"无效的文件\",\n    \"invalidFileDesc\": \"请上传图片文件\",\n    \"invalidAudioFile\": \"无效的音频文件\",\n    \"invalidAudioFileDesc\": \"请选择有效的音频文件\",\n    \"audioRecognition\": \"音频识别失败\",\n    \"audioProcessing\": \"音频处理失败\",\n    \"invalidVideoFile\": \"无效的视频文件\",\n    \"invalidVideoFileDesc\": \"请上传视频文件\",\n    \"videoProcessingDesc\": \"视频处理过程中出现错误，请重试\",\n    \"noTextExtracted\": \"未提取到文本\",\n    \"noTextExtractedDesc\": \"无法从文件中提取任何文本\",\n    \"noImage\": \"未选择图片\",\n    \"noImageDesc\": \"请先上传一张图片\",\n    \"noText\": \"未输入文本\",\n    \"noTextDesc\": \"请输入需要翻译的文本\",\n    \"noLanguage\": \"未选择语言\",\n    \"noLanguageDesc\": \"请选择目标语言\",\n    \"noTranslation\": \"无翻译内容\",\n    \"noTranslationDesc\": \"请先进行翻译\",\n    \"extractingError\": \"提取文字失败\",\n    \"extractingDesc\": \"请稍后重试\",\n    \"translating\": \"翻译失败\",\n    \"translatingDesc\": \"翻译过程中出现错误，请重试\",\n    \"translationError\": \"翻译失败\",\n    \"improving\": \"优化失败\",\n    \"improvingDesc\": \"优化过程中出现错误，请重试\",\n    \"speechNotSupported\": \"语音识别不可用\",\n    \"speechNotSupportedDesc\": \"您的浏览器不支持语音识别功能\",\n    \"speechRecognition\": \"语音识别错误\",\n    \"videoProcessing\": \"视频处理失败\",\n    \"videoRecognition\": \"视频识别错误\",\n    \"invalidFileType\": \"无效的文件类型\",\n    \"invalidFileTypeDesc\": \"请上传 PDF、Word 或图片文件\",\n    \"fileProcessing\": \"文件处理失败\",\n    \"fileProcessingDesc\": \"文件处理过程中出现错误，请重试\",\n    \"onlyPDF\": \"只支持PDF文件\",\n    \"fileTooLarge\": \"文件太大\",\n    \"fileTooLargeDesc\": \"视频文件大小不能超过 100MB\",\n    \"videoUploadFailed\": \"视频上传失败\",\n    \"videoUploadFailedDesc\": \"请检查网络连接并重试，或尝试上传小一点的视频文件\",\n    \"serverUploadLimit\": \"服务器上传限制\",\n    \"serverUploadLimitDesc\": \"服务器限制单个文件最大上传大小为 10MB，请尝试压缩视频或分段上传\",\n    \"invalidPDFFile\": \"无效的PDF文件\",\n    \"videoServiceNotSupported\": \"不支持的视频服务\",\n    \"taskProcessingFailed\": \"任务处理失败\",\n    \"taskProcessingTimeout\": \"任务处理超时\"\n  },\n  \"console\": {\n    \"quotaFetchFailed\": \"获取配额失败:\",\n    \"quotaInfoFetchFailed\": \"获取配额信息失败:\",\n    \"usageUpdateFailed\": \"更新使用次数失败:\",\n    \"videoTaskCreateFailed\": \"创建视频识别任务失败:\",\n    \"ocrProcessingError\": \"处理OCR结果时出错:\",\n    \"videoUploadError\": \"视频上传错误:\",\n    \"videoProcessingError\": \"视频处理错误:\",\n    \"fileProcessingFailed\": \"文件处理失败:\",\n    \"translationServiceError\": \"{0} 翻译服务错误:\",\n    \"usingFallbackService\": \"尝试使用 DeepSeek 作为备选服务...\",\n    \"translationError\": \"翻译错误:\",\n    \"fileProcessingError\": \"文件处理错误:\",\n    \"taskQueryFailed\": \"查询任务结果失败:\",\n    \"pollingResult\": \"轮询返回结果:\",\n    \"startingPdfProcessing\": \"开始使用 {0} 处理PDF文件:\",\n    \"pdfProcessingStatus\": \"PDF处理状态:\",\n    \"pdfProcessingComplete\": \"PDF处理完成，提取的文本长度:\",\n    \"extractedTextPreview\": \"提取的文本预览:\",\n    \"videoFramesExtracting\": \"开始提取视频帧...\",\n    \"videoFramesExtracted\": \"视频帧提取完成，帧数: {0}\",\n    \"videoFramesExample\": \"第一帧数据示例: {0}\",\n    \"videoProcessed\": \"视频处理完成，提取的文本长度: {0}\",\n    \"videoUploaded\": \"视频上传成功，URL: {0}\",\n    \"videoTaskPolling\": \"开始轮询任务状态...\",\n    \"videoOcrProcessing\": \"开始处理OCR结果:\",\n    \"videoOcrResult\": \"提取的文本: {0}\",\n    \"videoNoText\": \"没有提取到文本\",\n    \"videoOcrError\": \"处理OCR结果时出错:\",\n    \"videoTaskResult\": \"轮询返回结果:\",\n    \"videoTaskFailed\": \"任务处理失败\",\n    \"videoTaskQueryFailed\": \"查询任务结果失败:\",\n    \"videoTaskTimeout\": \"任务处理超时\",\n    \"pdfProcessing\": \"开始使用 {0} 处理PDF文件: {1}\",\n    \"pdfStatus\": \"PDF处理状态: {0}\",\n    \"pdfProcessed\": \"PDF处理完成，提取的文本长度: {0}\",\n    \"pdfPreview\": \"提取的文本预览: {0}...\",\n    \"tryingDeepSeek\": \"尝试使用 DeepSeek 作为备选服务...\",\n    \"selectedLanguage\": \"已选择语言: {0}\"\n  },\n  \"success\": {\n    \"extracted\": \"文字提取成功\",\n    \"translated\": \"翻译成功\",\n    \"improved\": \"优化成功\",\n    \"speechRecognized\": \"语音识别成功\",\n    \"videoExtracted\": \"视频文字提取成功\",\n    \"description\": \"操作已完成\",\n    \"fileExtracted\": \"文件内容提取成功\",\n    \"copied\": \"复制成功\",\n    \"imageUploaded\": \"图片上传成功\",\n    \"audioRecognized\": \"语音识别成功\",\n    \"audioUploaded\": \"语音上传成功\"\n  },\n  \"selectService\": \"选择翻译服务\",\n  \"selectOcrService\": \"选择文字识别服务\",\n  \"processing\": \"正在处理中...\",\n  \"appName\": \"人工智能翻译助手\",\n  \"switchLanguage\": \"切换语言\",\n  \"toggleTheme\": \"切换主题\",\n  \"theme\": {\n    \"light\": \"浅色\",\n    \"dark\": \"深色\",\n    \"system\": \"跟随系统\"\n  },\n  \"languages\": {\n    \"english\": \"English\",\n    \"chinese\": \"中文\"\n  },\n  \"selectAsrService\": \"选择语音识别服务\",\n  \"asrServices\": {\n    \"tencent\": \"腾讯云\"\n  },\n  \"enterText\": \"请输入要翻译的文本\",\n  \"chooseFile\": \"选择文件\",\n  \"uploadFile\": \"上传文件\",\n  \"fileRecognition\": \"文件识别\",\n  \"uploadPDF\": \"上传PDF文件\",\n  \"selectVideoService\": \"选择视频服务\",\n  \"videoServices\": {\n    \"gemini\": \"Gemini\",\n    \"zhipu\": \"Zhipu GLM4\",\n    \"qwen\": \"Qwen\",\n    \"kimi\": \"Kimi\"\n  },\n  \"ocrServices\": {\n    \"tencent\": \"腾讯云\",\n    \"qwen\": \"通义千问\",\n    \"gemini\": \"Gemini\",\n    \"zhipu\": \"智谱 AI\",\n    \"kimi\": \"Kimi\",\n    \"step\": \"阶跃星辰\"\n  },\n  \"translationServices\": {\n    \"deepseek\": \"DeepSeek\",\n    \"qwen\": \"通义千问\",\n    \"gemini\": \"Gemini\",\n    \"zhipu\": \"智谱GLM4\",\n    \"hunyuan\": \"腾讯混元\",\n    \"4o-mini\": \"OpenAI\",\n    \"minnimax\": \"MinniMax\",\n    \"siliconflow\": \"Llama-3.3\",\n    \"claude_3_5\": \"Claude 3.5\",\n    \"kimi\": \"Kimi\",\n    \"step\": \"阶跃星辰\"\n  },\n  \"title\": \"人工智能翻译助手\",\n  \"description\": \"使用AI技术提供高质量的多语言翻译服务\",\n  \"landing\": {\n    \"hero\": {\n      \"appTitle\": \"人工智能翻译助手\",\n      \"subtitle\": \"一站式多语言翻译解决方案，支持文本、图片、PDF、语音和视频翻译\",\n      \"cta\": \"立即体验\",\n      \"badge\": \"AI 驱动 · 多语言支持\",\n      \"subscribe\": \"订阅会员\"\n    },\n    \"features\": {\n      \"title\": \"核心功能\",\n      \"subtitle\": \"为您提供全方位的翻译服务，满足不同场景的需求\",\n      \"text\": {\n        \"title\": \"文本翻译\",\n        \"description\": \"支持长篇内容翻译，语义精准自然，保持原文风格\"\n      },\n      \"image\": {\n        \"title\": \"图片翻译\",\n        \"description\": \"智能识别图片中的文字，快速准确翻译\"\n      },\n      \"pdf\": {\n        \"title\": \"PDF翻译\",\n        \"description\": \"自动提取PDF文档内容，保持格式翻译\"\n      },\n      \"speech\": {\n        \"title\": \"语音翻译\",\n        \"description\": \"实时语音识别与翻译，支持多种语言互译\"\n      },\n      \"video\": {\n        \"title\": \"视频翻译\",\n        \"description\": \"自动生成多语言字幕，适合学习与创作场景\"\n      }\n    },\n    \"highlights\": {\n      \"title\": \"产品特色\",\n      \"subtitle\": \"打造极致的翻译体验，让语言不再成为障碍\",\n      \"multilingual\": {\n        \"title\": \"多语言支持\",\n        \"description\": \"支持中英文界面切换，满足国际化需求\"\n      },\n      \"theme\": {\n        \"title\": \"主题切换\",\n        \"description\": \"提供暗色与亮色模式，保护视觉体验\"\n      },\n      \"privacy\": {\n        \"title\": \"隐私保护\",\n        \"description\": \"不保存用户数据，确保信息安全\"\n      },\n      \"web\": {\n        \"title\": \"纯网页版\",\n        \"description\": \"无需下载安装，随时随地使用\"\n      }\n    },\n    \"steps\": {\n      \"title\": \"使用流程\",\n      \"subtitle\": \"简单三步，轻松完成翻译\",\n      \"select\": {\n        \"title\": \"选择功能\",\n        \"description\": \"根据需求选择文本、图片、语音或视频翻译\"\n      },\n      \"upload\": {\n        \"title\": \"上传内容\",\n        \"description\": \"上传需要翻译的文件或直接输入文本\"\n      },\n      \"translate\": {\n        \"title\": \"获取结果\",\n        \"description\": \"快速获得准确的翻译结果\"\n      }\n    },\n    \"testimonials\": {\n      \"title\": \"用户评价\",\n      \"subtitle\": \"看看其他用户如何评价我们的服务\",\n      \"1\": {\n        \"quote\": \"这个翻译工具太棒了！不仅支持多种格式，翻译质量也非常高。特别是图片文字识别功能，帮我节省了大量时间。\",\n        \"author\": \"张明\",\n        \"role\": \"自由译者\"\n      },\n      \"2\": {\n        \"quote\": \"界面设计非常直观，使用起来特别方便。暗色模式对我这种经常熬夜工作的人来说简直是福音。\",\n        \"author\": \"李华\",\n        \"role\": \"内容创作者\"\n      },\n      \"3\": {\n        \"quote\": \"PDF文档翻译功能非常实用，完美保留了原文格式。对我的学术研究帮助很大。\",\n        \"author\": \"王芳\",\n        \"role\": \"研究生\"\n      },\n      \"4\": {\n        \"quote\": \"作为一名开发者，我很欣赏这个工具的技术实现。API响应速度快，而且支持批量处理，效率很高。\",\n        \"author\": \"陈杰\",\n        \"role\": \"软件工程师\"\n      },\n      \"5\": {\n        \"quote\": \"语音翻译功能特别好用，准确率很高。对我们做国际业务的来说是必备工具。\",\n        \"author\": \"刘琳\",\n        \"role\": \"商务经理\"\n      },\n      \"6\": {\n        \"quote\": \"视频字幕翻译功能帮我解决了很多问题，特别是自动同步时间轴的功能，省去了大量手动调整的工作。\",\n        \"author\": \"赵阳\",\n        \"role\": \"视频制作者\"\n      }\n    },\n    \"footer\": {\n      \"browsers\": \"支持所有主流浏览器\",\n      \"privacy\": \"承诺保护您的隐私\"\n    }\n  },\n  \"translate\": {\n    \"selectVideoService\": \"选择视频服务\",\n    \"videoService\": \"视频识别服务\",\n    \"zhipu\": \"智谱 AI\",\n    \"aliyun\": \"阿里云\",\n    \"uploadVideo\": \"上传视频\",\n    \"dragAndDrop\": \"拖放视频文件到此处，或点击上传\",\n    \"processing\": \"正在处理视频...\",\n    \"extractedText\": \"提取的文本\",\n    \"success\": {\n      \"copied\": \"复制成功\",\n      \"description\": \"文本已复制到剪贴板\"\n    }\n  },\n  \"banner\": {\n    \"text\": \"AI 翻译助手全新上线，限时限量免费体验，功能更新请关注推特\",\n    \"twitter\": \"https://x.com/zyailive\"\n  },\n  \"usage\": {\n    \"remainingToday\": \"今日剩余次数：{0}/{1}\",\n    \"textTranslation\": \"文本翻译\",\n    \"imageRecognition\": \"图片识别\",\n    \"pdfRecognition\": \"PDF识别\",\n    \"speechRecognition\": \"语音识别\",\n    \"videoRecognition\": \"视频识别\",\n    \"dailyLimit\": \"每日免费次数\",\n    \"loginToGet\": \"登录后可使用更多功能\",\n    \"unlimited\": \"文本翻译不限次数\"\n  },\n  \"auth\": {\n    \"signIn\": \"登录\",\n    \"signUp\": \"注册\",\n    \"profileButton\": \"个人资料\",\n    \"profile\": {\n      \"title\": \"个人资料\",\n      \"description\": \"查看您的账户信息和使用配额\",\n      \"basicInfo\": {\n        \"title\": \"基本信息\",\n        \"email\": \"邮箱\",\n        \"username\": \"用户名\",\n        \"userId\": \"用户ID\",\n        \"registerTime\": \"注册时间\",\n        \"editUsername\": {\n          \"placeholder\": \"请输入新的用户名\",\n          \"save\": \"保存\",\n          \"cancel\": \"取消\",\n          \"success\": \"用户名更新成功\",\n          \"error\": {\n            \"empty\": \"用户名不能为空\",\n            \"failed\": \"更新失败，请重试\"\n          }\n        },\n        \"social\": {\n          \"github\": \"GitHub\",\n          \"google\": \"Google\",\n          \"bound\": \"已绑定\",\n          \"unbound\": \"未绑定\",\n          \"bind\": \"绑定{provider}\",\n          \"bindFailed\": \"{provider}绑定失败\"\n        }\n      },\n      \"subscription\": {\n        \"title\": \"订阅信息\",\n        \"plan\": {\n          \"trial\": \"免费版\",\n          \"monthly\": \"月度会员\",\n          \"yearly\": \"年度会员\"\n        },\n        \"customerId\": \"客户ID\",\n        \"subscriptionId\": \"订阅ID\",\n        \"expiryDate\": \"到期时间: {date}\",\n        \"success\": \"订阅成功！感谢您的支持\",\n        \"quota\": {\n          \"text\": \"文本翻译\",\n          \"image\": \"图片识别\",\n          \"pdf\": \"PDF识别\",\n          \"speech\": \"语音识别\",\n          \"video\": \"视频识别\",\n          \"unlimited\": \"无限制\",\n          \"usage\": \"{used}/{quota}\"\n        },\n        \"dialog\": {\n          \"title\": \"升级到会员版\",\n          \"description\": \"升级到会员版即可享受更多的使用次数，让您的翻译体验更加流畅。\",\n          \"benefits\": {\n            \"title\": \"会员版特权：\",\n            \"imageQuota\": \"每日50次图片识别\",\n            \"pdfQuota\": \"每日40次PDF处理\",\n            \"speechQuota\": \"每日30次语音识别\",\n            \"videoQuota\": \"每日10次视频处理\",\n            \"priority\": \"优先处理队列\"\n          },\n          \"buttons\": {\n            \"cancel\": \"暂不升级\",\n            \"viewPlans\": \"查看会员方案\"\n          }\n        }\n      }\n    },\n    \"signOut\": {\n      \"action\": \"退出登录\",\n      \"success\": \"退出登录成功\",\n      \"error\": \"退出登录失败\"\n    },\n    \"login\": {\n      \"title\": \"登录\",\n      \"subtitle\": \"输入您的邮箱和密码登录\",\n      \"email\": \"邮箱\",\n      \"emailPlaceholder\": \"请输入邮箱\",\n      \"password\": \"密码\",\n      \"passwordPlaceholder\": \"请输入密码\",\n      \"button\": \"登录\",\n      \"loading\": \"登录中...\",\n      \"noAccount\": \"还没有账号？\",\n      \"error\": {\n        \"invalidCredentials\": \"邮箱或密码错误\",\n        \"required\": \"请输入邮箱和密码\"\n      },\n      \"github\": \"使用 GitHub 账号登录\",\n      \"or\": \"或者\",\n      \"google\": \"使用 Google 账号登录\"\n    },\n    \"register\": {\n      \"title\": \"注册\",\n      \"subtitle\": \"创建您的账号\",\n      \"email\": \"邮箱\",\n      \"emailPlaceholder\": \"请输入邮箱\",\n      \"password\": \"密码\",\n      \"passwordPlaceholder\": \"请输入密码\",\n      \"confirmPassword\": \"确认密码\",\n      \"confirmPasswordPlaceholder\": \"请再次输入密码\",\n      \"button\": \"注册\",\n      \"loading\": \"注册中...\",\n      \"hasAccount\": \"已有账号？\",\n      \"error\": {\n        \"passwordMismatch\": \"两次输入的密码不一致\",\n        \"emailExists\": \"该邮箱已被注册\",\n        \"required\": \"请填写所有必填项\"\n      },\n      \"github\": \"使用 GitHub 账号注册\",\n      \"or\": \"或者\",\n      \"success\": \"注册成功\",\n      \"google\": \"使用 Google 账号注册\"\n    },\n    \"error\": {\n      \"accessDenied\": \"授权访问被拒绝，请重试\",\n      \"default\": \"登录时发生错误，请重试\"\n    }\n  },\n  \"quota\": {\n    \"types\": {\n      \"text\": \"文本翻译\",\n      \"image\": \"图片识别\",\n      \"pdf\": \"PDF识别\",\n      \"speech\": \"语音识别\",\n      \"video\": \"视频识别\"\n    },\n    \"unlimited\": \"无限制\",\n    \"times\": \"次\",\n    \"remaining\": \"剩余次数: %{count}\",\n    \"errors\": {\n      \"notLoggedIn\": \"请先登录\",\n      \"userNotFound\": \"用户不存在\",\n      \"invalidType\": \"无效的使用类型\",\n      \"limitReached\": \"今日使用次数已达上限\",\n      \"recordFailed\": \"记录使用失败\",\n      \"checkFailed\": \"使用配额检查失败\"\n    }\n  },\n  \"pricing\": {\n    \"title\": \"定价方案\",\n    \"subtitle\": \"选择适合您的方案，开启无限翻译体验\",\n    \"tiers\": {\n      \"free\": {\n        \"name\": \"试用版\",\n        \"description\": \"体验基础功能\",\n        \"cta\": \"开始试用\",\n        \"features\": {\n          \"basic\": [\n            \"无限文本翻译\",\n            \"每日5次图片识别\",\n            \"每日3次PDF处理\"\n          ],\n          \"freeAdvanced\": [\n            \"每日2次语音识别\",\n            \"每日1次视频处理\"\n          ],\n          \"freeSupport\": [\n            \"基础客服支持\"\n          ]\n        }\n      },\n      \"monthly\": {\n        \"name\": \"月度会员\",\n        \"description\": \"适合个人日常使用\",\n        \"cta\": \"立即订阅\",\n        \"period\": \"月\",\n        \"features\": {\n          \"basic\": [\n            \"无限文本翻译\",\n            \"每日50次图片识别\",\n            \"每日40次PDF处理\"\n          ],\n          \"advanced\": [\n            \"每日30次语音识别\",\n            \"每日10次视频处理\",\n            \"优先处理队列\"\n          ],\n          \"support\": [\n            \"优先客服支持\"\n          ]\n        }\n      },\n      \"yearly\": {\n        \"name\": \"年度会员\",\n        \"description\": \"最超值的选择\",\n        \"cta\": \"立即订阅\",\n        \"recommended\": \"推荐方案\",\n        \"discount\": \"省17%\",\n        \"features\": {\n          \"basic\": [\n            \"无限文本翻译\",\n            \"每日100次图片识别\",\n            \"每日80次PDF处理\"\n          ],\n          \"advanced\": [\n            \"每日60次语音识别\",\n            \"每日20次视频处理\",\n            \"优先处理队列\"\n          ],\n          \"support\": [\n            \"24/7专属客服\",\n            \"高级API访问\"\n          ]\n        }\n      }\n    },\n    \"features\": {\n      \"basic\": \"基础功能\",\n      \"advanced\": \"高级功能\",\n      \"support\": \"支持服务\"\n    },\n    \"monthly\": \"月\",\n    \"yearly\": \"年\",\n    \"faq\": {\n      \"title\": \"常见问题\",\n      \"subtitle\": \"如果您还有其他问题，欢迎随时联系我们\",\n      \"cards\": {\n        \"security\": {\n          \"title\": \"数据安全\",\n          \"content\": \"我们不会存储您的任何文件和翻译内容，所有数据仅在处理时临时使用，不会上传云端，全程保护您的隐私安全。\"\n        },\n        \"quota\": {\n          \"title\": \"配额计算规则\",\n          \"content\": \"配额按自然日重置，每天0点更新额度。未使用的配额不会累积到下一天。\"\n        },\n        \"payment\": {\n          \"title\": \"支付方式\",\n          \"content\": \"我们支持储蓄卡、信用卡、微信、支付宝等多种支付方式。所有支付信息都经过加密处理，确保您的资金安全。\"\n        },\n        \"support\": {\n          \"title\": \"联系客服\",\n          \"content\": \"如果您有任何问题或需要帮助，欢迎发送邮件至 wt@wmcircle.cn，我们会尽快回复您。\"\n        }\n      }\n    }\n  },\n  \"nav\": {\n    \"home\": \"首页\",\n    \"features\": \"功能\",\n    \"pricing\": \"价格\"\n  },\n  \"processingStatus\": \"处理中...\"\n}"
  },
  {
    "path": "lib/i18n/translations.ts",
    "content": "import en from './locales/en.json'\nimport zh from './locales/zh.json'\n\nexport const translations = {\n  en,\n  zh,\n} as const"
  },
  {
    "path": "lib/i18n/use-translations.ts",
    "content": "\"use client\"\n\nimport { create } from 'zustand'\nimport zhTranslations from './locales/zh.json'\nimport enTranslations from './locales/en.json'\n\ntype I18nStore = {\n  language: string\n  translations: Record<string, any>\n  setLanguage: (lang: string) => void\n  t: (key: string, params?: Record<string, any>) => string\n}\n\nconst getInitialLanguage = () => {\n  if (typeof window === 'undefined') return 'zh'\n  return localStorage.getItem('language') || \n         (navigator.language.startsWith('zh') ? 'zh' : 'en')\n}\n\nexport const useI18n = create<I18nStore>((set, get) => ({\n  language: getInitialLanguage(),\n  translations: getInitialLanguage() === 'zh' ? zhTranslations : enTranslations,\n  setLanguage: (lang: string) => {\n    const translations = lang === 'zh' ? zhTranslations : enTranslations\n    localStorage.setItem('language', lang)\n    set({ language: lang, translations })\n  },\n  t: (key: string, params?: Record<string, any>) => {\n    const { translations } = get()\n    const keys = key.split('.')\n    let value: any = translations\n    \n    // 遍历键路径获取翻译值\n    for (const k of keys) {\n      value = value?.[k]\n      // 如果在任何层级找不到值，返回键名\n      if (value === undefined) {\n        console.warn(`Translation key not found: ${key}`)\n        return key\n      }\n    }\n    \n    // 确保返回字符串并替换参数\n    if (typeof value === 'string' && params) {\n      return value.replace(/\\{([^}]+)\\}/g, (match, key) => {\n        return params[key] !== undefined ? String(params[key]) : match\n      })\n    }\n    \n    return typeof value === 'string' ? value : key\n  }\n}))"
  },
  {
    "path": "lib/kimi.ts",
    "content": "import { encode } from 'base64-arraybuffer'\n\n// 图片文字识别函数\nexport async function extractTextWithKimi(image: string): Promise<string> {\n  try {\n    // 确保图片数据格式正确\n    let imageData = image;\n    if (!image.includes(';base64,')) {\n      // 如果是纯base64字符串，添加正确的MIME类型前缀\n      if (image.startsWith('/9j/')) {\n        imageData = `data:image/jpeg;base64,${image}`;\n      } else if (image.startsWith('iVBOR')) {\n        imageData = `data:image/png;base64,${image}`;\n      } else {\n        // 默认假设为JPEG\n        imageData = `data:image/jpeg;base64,${image}`;\n      }\n    }\n\n    const response = await fetch('/api/ocr/kimi', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ image: imageData }),\n    });\n\n    if (!response.ok) {\n      const errorData = await response.text();\n      console.error('Kimi OCR error response:', errorData);\n      throw new Error(`OCR request failed: ${response.status} ${errorData}`);\n    }\n\n    const data = await response.json();\n    return data.text;\n  } catch (error: any) {\n    console.error('Error extracting text with Kimi:', error);\n    throw error;\n  }\n}\n\n// 重试函数\nasync function retryWithDelay<T>(\n  fn: () => Promise<T>,\n  retries = 5,\n  delay = 2000,\n  backoff = 1.5,\n  onRetry?: (retriesLeft: number, error: Error) => void\n): Promise<T> {\n  try {\n    return await fn()\n  } catch (error: any) {\n    if (retries === 0 || error.message.includes('API密钥') || error.message.includes('大小超过限制')) {\n      throw error\n    }\n    \n    if (onRetry) {\n      onRetry(retries, error)\n    }\n    await new Promise(resolve => setTimeout(resolve, delay))\n    return retryWithDelay(fn, retries - 1, delay * backoff, backoff, onRetry)\n  }\n}\n\n// 通用PDF处理函数，支持选择服务商\nexport async function extractPDFContent(\n  file: File,\n  service: 'kimi' | 'mistral' = 'mistral', // 默认使用Mistral OCR\n  onProgress?: (status: string) => void\n): Promise<string> {\n  try {\n    // 检查文件大小（限制为5MB，适应服务器限制）\n    const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB\n    if (file.size > MAX_FILE_SIZE) {\n      throw new Error('文件大小不能超过5MB')\n    }\n\n    // 检查文件类型\n    if (!file.type.includes('pdf')) {\n      throw new Error('请上传PDF文件')\n    }\n\n    // 压缩和转换文件\n    const compressedFile = await new Promise<string>((resolve, reject) => {\n      const reader = new FileReader()\n      reader.onloadend = () => {\n        try {\n          const base64 = reader.result as string\n          const base64Data = base64.split(',')[1]\n          // 如果base64字符串太长，可能需要分片处理\n          if (base64Data.length > 5 * 1024 * 1024) { // 5MB in base64\n            reject(new Error('文件太大，请上传更小的文件'))\n            return\n          }\n          resolve(base64Data)\n        } catch (err) {\n          reject(new Error('文件处理失败'))\n        }\n      }\n      reader.onerror = () => reject(new Error('文件读取失败'))\n      reader.readAsDataURL(file)\n    })\n\n    // 使用重试机制发送请求\n    return await retryWithDelay(\n      async () => {\n        if (onProgress) {\n          onProgress(`正在使用${service === 'mistral' ? 'Mistral OCR' : 'Kimi'}处理文件...`)\n        }\n\n        const response = await fetch('/api/file/extract', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify({\n            file: compressedFile,\n            filename: file.name,\n            service: service\n          }),\n        })\n\n        if (!response.ok) {\n          const error = await response.json()\n          if (response.status === 503) {\n            throw new Error(error.error || '服务暂时不可用，正在重试...')\n          }\n          throw new Error(error.error || '文件处理失败')\n        }\n\n        const data = await response.json()\n        \n        // 增强对 API 响应的处理\n        if (!data.text || data.text.trim() === '') {\n          console.warn('API 返回的数据中没有有效的 text 字段:', data)\n          \n          // 尝试从其他可能的字段中获取文本\n          if (data.content) {\n            return typeof data.content === 'string' ? data.content : JSON.stringify(data.content)\n          }\n          \n          if (data.result) {\n            return typeof data.result === 'string' ? data.result : JSON.stringify(data.result)\n          }\n          \n          if (data.extracted_text) {\n            return data.extracted_text\n          }\n          \n          // 如果找不到任何文本内容，返回一个友好的错误消息\n          return '无法从文件中提取文本。请尝试使用其他服务或上传不同的文件。'\n        }\n\n        return data.text\n      },\n      2, // 减少重试次数从 5 次改为 2 次\n      3000, // 增加初始延迟时间，从 2000 改为 3000\n      1.5,\n      (retriesLeft, error) => {\n        if (onProgress) {\n          if (error.message.includes('上传超时')) {\n            onProgress(`文件上传超时，正在重试...（剩余${retriesLeft}次）`)\n          } else if (error.message.includes('内容获取超时')) {\n            onProgress(`文件内容获取超时，正在重试...（剩余${retriesLeft}次）`)\n          } else if (error.message.includes('处理超时')) {\n            onProgress(`内容处理超时，正在重试...（剩余${retriesLeft}次）`)\n          } else {\n            onProgress(`处理失败，正在重试...（剩余${retriesLeft}次）`)\n          }\n        }\n      }\n    )\n  } catch (error: any) {\n    console.error('文件处理错误:', error)\n    throw error\n  }\n}\n\n// 保留原有函数以保持兼容性\nexport async function extractPDFWithKimi(\n  file: File,\n  onProgress?: (status: string) => void\n): Promise<string> {\n  return extractPDFContent(file, 'kimi', onProgress)\n}"
  },
  {
    "path": "lib/languages.ts",
    "content": "export interface Language {\n  code: string;\n  name: string;\n  nativeName: string;\n  category: string;\n}\n\nexport const languages: Language[] = [\n  // East Asian Languages\n  { code: 'zh', name: 'Chinese', nativeName: '中文', category: 'East Asian' },\n  { code: 'ja', name: 'Japanese', nativeName: '日本語', category: 'East Asian' },\n  { code: 'ko', name: 'Korean', nativeName: '한국어', category: 'East Asian' },\n  { code: 'mn', name: 'Mongolian', nativeName: 'Монгол', category: 'East Asian' },\n  \n  // European Languages\n  { code: 'en', name: 'English', nativeName: 'English', category: 'European' },\n  { code: 'fr', name: 'French', nativeName: 'Français', category: 'European' },\n  { code: 'de', name: 'German', nativeName: 'Deutsch', category: 'European' },\n  { code: 'es', name: 'Spanish', nativeName: 'Español', category: 'European' },\n  { code: 'it', name: 'Italian', nativeName: 'Italiano', category: 'European' },\n  { code: 'pt', name: 'Portuguese', nativeName: 'Português', category: 'European' },\n  { code: 'ru', name: 'Russian', nativeName: 'Русский', category: 'European' },\n  { code: 'nl', name: 'Dutch', nativeName: 'Nederlands', category: 'European' },\n  { code: 'pl', name: 'Polish', nativeName: 'Polski', category: 'European' },\n  { code: 'uk', name: 'Ukrainian', nativeName: 'Українська', category: 'European' },\n  { code: 'el', name: 'Greek', nativeName: 'Ελληνικά', category: 'European' },\n  { code: 'cs', name: 'Czech', nativeName: 'Čeština', category: 'European' },\n  { code: 'hu', name: 'Hungarian', nativeName: 'Magyar', category: 'European' },\n  { code: 'ro', name: 'Romanian', nativeName: 'Română', category: 'European' },\n  { code: 'sv', name: 'Swedish', nativeName: 'Svenska', category: 'European' },\n  { code: 'da', name: 'Danish', nativeName: 'Dansk', category: 'European' },\n  { code: 'fi', name: 'Finnish', nativeName: 'Suomi', category: 'European' },\n  { code: 'no', name: 'Norwegian', nativeName: 'Norsk', category: 'European' },\n  \n  // South Asian Languages\n  { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', category: 'South Asian' },\n  { code: 'bn', name: 'Bengali', nativeName: 'বাংলা', category: 'South Asian' },\n  { code: 'ur', name: 'Urdu', nativeName: 'اردو', category: 'South Asian' },\n  { code: 'ta', name: 'Tamil', nativeName: 'தமிழ்', category: 'South Asian' },\n  { code: 'te', name: 'Telugu', nativeName: 'తెలుగు', category: 'South Asian' },\n  { code: 'mr', name: 'Marathi', nativeName: 'मराठी', category: 'South Asian' },\n  { code: 'gu', name: 'Gujarati', nativeName: 'ગુજરાતી', category: 'South Asian' },\n  { code: 'kn', name: 'Kannada', nativeName: 'ಕನ್ನಡ', category: 'South Asian' },\n  { code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം', category: 'South Asian' },\n  { code: 'pa', name: 'Punjabi', nativeName: 'ਪੰਜਾਬੀ', category: 'South Asian' },\n  \n  // Southeast Asian Languages\n  { code: 'th', name: 'Thai', nativeName: 'ไทย', category: 'Southeast Asian' },\n  { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', category: 'Southeast Asian' },\n  { code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', category: 'Southeast Asian' },\n  { code: 'ms', name: 'Malay', nativeName: 'Bahasa Melayu', category: 'Southeast Asian' },\n  { code: 'fil', name: 'Filipino', nativeName: 'Filipino', category: 'Southeast Asian' },\n  { code: 'my', name: 'Burmese', nativeName: 'မြန်မာစာ', category: 'Southeast Asian' },\n  { code: 'km', name: 'Khmer', nativeName: 'ខ្មែរ', category: 'Southeast Asian' },\n  { code: 'lo', name: 'Lao', nativeName: 'ລາວ', category: 'Southeast Asian' },\n  \n  // Middle Eastern Languages\n  { code: 'ar', name: 'Arabic', nativeName: 'العربية', category: 'Middle Eastern' },\n  { code: 'fa', name: 'Persian', nativeName: 'فارسی', category: 'Middle Eastern' },\n  { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', category: 'Middle Eastern' },\n  { code: 'he', name: 'Hebrew', nativeName: 'עברית', category: 'Middle Eastern' }\n];\n\nexport const getLanguageCategories = (): string[] => {\n  const categories = new Set(languages.map(lang => lang.category));\n  return Array.from(categories);\n};\n\nexport const getLanguagesByCategory = (category: string): Language[] => {\n  return languages.filter(lang => lang.category === category);\n};\n\nexport const getLanguageByCode = (code: string): Language | undefined => {\n  return languages.find(lang => lang.code === code);\n};\n\nexport const getLanguageNameByCode = (code: string): string => {\n  const language = getLanguageByCode(code);\n  return language ? language.name : code;\n};"
  },
  {
    "path": "lib/qwen.ts",
    "content": "\"use client\"\n\n// 重试函数\nasync function retryWithDelay<T>(\n  fn: () => Promise<T>,\n  retries = 3,\n  delay = 1000,\n  backoff = 2\n): Promise<T> {\n  try {\n    return await fn();\n  } catch (error: any) {\n    if (retries === 0) {\n      throw error;\n    }\n    \n    await new Promise(resolve => setTimeout(resolve, delay));\n    return retryWithDelay(fn, retries - 1, delay * backoff, backoff);\n  }\n}\n\n// 使用通义千问 API 进行文本翻译\nexport async function translateWithQwen(text: string, targetLang: string) {\n  try {\n    return await retryWithDelay(async () => {\n      // 分段处理长文本\n      const segments = text.split('\\n\\n');\n      const translatedSegments = [];\n      \n      for (const segment of segments) {\n        if (!segment.trim()) {\n          translatedSegments.push('');\n          continue;\n        }\n        \n        const response = await fetch('/api/qwen/translate', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify({\n            text: segment.trim(),\n            targetLang,\n          }),\n        });\n\n        if (!response.ok) {\n          const error = await response.json();\n          throw new Error(error.message || '翻译请求失败');\n        }\n\n        const result = await response.json();\n        translatedSegments.push(result.text.trim());\n      }\n      \n      return translatedSegments.join('\\n\\n');\n    });\n  } catch (error: any) {\n    console.error('Error translating with Qwen:', error);\n    throw new Error(error.message || '翻译失败，请稍后重试');\n  }\n}\n\nexport async function extractTextWithQwen(imageBase64: string): Promise<string> {\n  try {\n    const base64Data = imageBase64.replace(/^data:image\\/(png|jpeg|jpg|webp);base64,/, '')\n\n    const response = await fetch('/api/qwen/ocr', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        image: base64Data,\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || '文字识别失败');\n    }\n\n    const result = await response.json();\n    return result.text.trim();\n  } catch (error: any) {\n    console.error('通义千问OCR错误:', error);\n    throw new Error(error.message || '文字识别失败');\n  }\n} "
  },
  {
    "path": "lib/server/tencent-sign.ts",
    "content": "import { createHmac, createHash } from 'crypto';\n\ninterface SignParams {\n  secretId: string;\n  secretKey: string;\n  endpoint: string;\n  service: string;\n  version: string;\n  region: string;\n  action: string;\n  timestamp: number;\n  payload: any;\n}\n\nfunction sha256hex(message: string): string {\n  const hash = createHash('sha256');\n  hash.update(message);\n  return hash.digest('hex');\n}\n\nfunction getDate(timestamp: number): string {\n  const date = new Date(timestamp * 1000);\n  return date.toISOString().split('T')[0];\n}\n\nexport function sign(params: SignParams): string {\n  const {\n    secretId,\n    secretKey,\n    endpoint,\n    service,\n    version,\n    region,\n    action,\n    timestamp,\n    payload,\n  } = params;\n\n  // 1. 拼接规范请求串\n  const httpRequestMethod = 'POST';\n  const canonicalUri = '/';\n  const canonicalQueryString = '';\n  const canonicalHeaders = `content-type:application/json\\nhost:${endpoint}\\n`;\n  const signedHeaders = 'content-type;host';\n  const hashedRequestPayload = sha256hex(JSON.stringify(payload));\n  const canonicalRequest = `${httpRequestMethod}\\n${canonicalUri}\\n${canonicalQueryString}\\n${canonicalHeaders}\\n${signedHeaders}\\n${hashedRequestPayload}`;\n\n  // 2. 拼接待签名字符串\n  const algorithm = 'TC3-HMAC-SHA256';\n  const date = getDate(timestamp);\n  const credentialScope = `${date}/${service}/tc3_request`;\n  const hashedCanonicalRequest = sha256hex(canonicalRequest);\n  const stringToSign = `${algorithm}\\n${timestamp}\\n${credentialScope}\\n${hashedCanonicalRequest}`;\n\n  // 3. 计算签名\n  const secretDate = createHmac('sha256', `TC3${secretKey}`).update(date).digest();\n  const secretService = createHmac('sha256', secretDate).update(service).digest();\n  const secretSigning = createHmac('sha256', secretService).update('tc3_request').digest();\n  const signature = createHmac('sha256', secretSigning).update(stringToSign).digest('hex');\n\n  // 4. 拼接 Authorization\n  return `${algorithm} Credential=${secretId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;\n} "
  },
  {
    "path": "lib/server/translate.ts",
    "content": "import OpenAI from 'openai';\n\n// 使用 DeepSeek API 进行文本翻译\nexport async function translateWithDeepSeek(text: string, targetLanguage: string) {\n  try {\n    const response = await fetch('/api/translate', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        text: text.trim(),\n        targetLanguage,\n        service: 'deepseek'\n      }),\n    });\n\n    if (!response.ok) {\n      const result = await response.json();\n      throw new Error(result.error || '翻译请求失败');\n    }\n\n    const result = await response.json();\n\n    if (!result.text) {\n      throw new Error('翻译结果为空');\n    }\n\n    return result.text.trim();\n  } catch (error: any) {\n    console.error('DeepSeek translation error:', error);\n    throw error;\n  }\n}\n\n// 使用通义千问 API 进行文本翻译\nexport async function translateWithQwen(text: string, targetLanguage: string) {\n  try {\n    const response = await fetch('/api/translate', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        text: text.trim(),\n        targetLanguage,\n        service: 'qwen'\n      }),\n    });\n\n    const result = await response.json();\n\n    if (!response.ok) {\n      throw new Error(result.error || '翻译请求失败');\n    }\n\n    if (!result.text) {\n      throw new Error('翻译结果为空');\n    }\n\n    return result.text.trim();\n  } catch (error: any) {\n    console.error('Qwen translation error:', error);\n    throw new Error(error.message || '翻译失败，请稍后重试');\n  }\n}\n\n// 使用智谱 GLM4 API 进行文本翻译\nexport async function translateWithZhipu(text: string, targetLanguage: string) {\n  try {\n    const response = await fetch('/api/translate', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        text: text.trim(),\n        targetLanguage,\n        service: 'zhipu'\n      }),\n    });\n\n    const result = await response.json();\n\n    if (!response.ok) {\n      throw new Error(result.error || '翻译请求失败');\n    }\n\n    if (!result.text) {\n      throw new Error('翻译结果为空');\n    }\n\n    return result.text.trim();\n  } catch (error: any) {\n    console.error('Zhipu translation error:', error);\n    throw new Error(error.message || '翻译失败，请稍后重试');\n  }\n}\n\n// 使用腾讯混元 API 进行文本翻译\nexport async function translateWithHunyuan(text: string, targetLang: string) {\n  try {\n    const response = await fetch('/api/translate', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        text: text.trim(),\n        targetLanguage: targetLang,\n        service: 'hunyuan'\n      }),\n    });\n\n    const result = await response.json();\n\n    if (!response.ok) {\n      throw new Error(result.error || '翻译请求失败');\n    }\n\n    if (!result.text) {\n      throw new Error('翻译结果为空');\n    }\n\n    return result.text.trim();\n  } catch (error: any) {\n    console.error('Error translating with Hunyuan:', error);\n    throw new Error(error.message || '翻译失败，请稍后重试');\n  }\n}\n\n// 使用 OpenAI 4o-mini API 进行文本翻译\nexport async function translateWith4oMini(text: string, targetLanguage: string) {\n  try {\n    const response = await fetch('/api/translate', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        text: text.trim(),\n        targetLanguage,\n        service: '4o-mini'\n      }),\n    });\n\n    const result = await response.json();\n\n    if (!response.ok) {\n      throw new Error(result.error || '翻译请求失败');\n    }\n\n    if (!result.text) {\n      throw new Error('翻译结果为空');\n    }\n\n    return result.text.trim();\n  } catch (error: any) {\n    console.error('OpenAI translation error:', error);\n    throw new Error(error.message || '翻译失败，请稍后重试');\n  }\n}\n\n// 使用 MinniMax abab6.5s-chat API 进行文本翻译\nexport async function translateWithMinniMax(text: string, targetLanguage: string) {\n  try {\n    const response = await fetch('/api/translate', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        text: text.trim(),\n        targetLanguage,\n        service: 'minnimax'\n      }),\n    });\n\n    const result = await response.json();\n\n    if (!response.ok) {\n      throw new Error(result.error || '翻译请求失败');\n    }\n\n    if (!result.text) {\n      throw new Error('翻译结果为空');\n    }\n\n    return result.text.trim();\n  } catch (error: any) {\n    console.error('MinniMax translation error:', error);\n    throw new Error(error.message || '翻译失败，请稍后重试');\n  }\n}\n\n// 使用 SiliconFlow Llama-3.3 API 进行文本翻译\nexport async function translateWithSiliconFlow(text: string, targetLanguage: string) {\n  try {\n    const response = await fetch('/api/translate/siliconflow', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        text: text.trim(),\n        targetLanguage,\n      }),\n    });\n\n    const result = await response.json();\n\n    if (!response.ok) {\n      throw new Error(result.error || '翻译请求失败');\n    }\n\n    if (!result.text) {\n      throw new Error('翻译结果为空');\n    }\n\n    return result.text.trim();\n  } catch (error: any) {\n    console.error('SiliconFlow translation error:', error);\n    throw new Error(error.message || '翻译失败，请稍后重试');\n  }\n}\n\n// 使用 OpenRouter API 的 Claude 3.5 进行文本翻译\nexport async function translateWithClaude(text: string, targetLanguage: string) {\n  try {\n    const response = await fetch('/api/translate', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        text: text.trim(),\n        targetLanguage,\n        service: 'claude-3.5'\n      }),\n    });\n\n    const result = await response.json();\n\n    if (!response.ok) {\n      throw new Error(result.error || '翻译请求失败');\n    }\n\n    if (!result.text) {\n      throw new Error('翻译结果为空');\n    }\n\n    return result.text.trim();\n  } catch (error: any) {\n    console.error('Claude translation error:', error);\n    throw new Error(error.message || '翻译失败，请稍后重试');\n  }\n}\n\nexport async function translateWithKimiAPI(text: string, targetLanguage: string) {\n  const apiKey = process.env.NEXT_PUBLIC_KIMI_API_KEY;\n  if (!apiKey) {\n    throw new Error('Kimi API key not found');\n  }\n\n  const openai = new OpenAI({\n    apiKey: apiKey,\n    baseURL: 'https://api.moonshot.cn/v1',\n  });\n\n  try {\n    const completion = await openai.chat.completions.create({\n      model: 'moonshot-v1-128k',\n      messages: [\n        {\n          role: 'system',\n          content: `You are a professional translator. Translate the following text to ${targetLanguage}. Keep the original format and style.`\n        },\n        {\n          role: 'user',\n          content: text\n        }\n      ],\n      temperature: 0.3,\n      max_tokens: 2000\n    });\n\n    const translatedText = completion.choices[0]?.message?.content;\n    if (!translatedText) {\n      throw new Error('No translation result');\n    }\n\n    return translatedText;\n  } catch (error: any) {\n    console.error('Kimi translation error:', error);\n    throw new Error(error.message || 'Translation failed');\n  }\n}\n\nexport async function translateWithStepAPI(text: string, targetLanguage: string) {\n  try {\n    const response = await fetch('/api/translate/step', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        text: text.trim(),\n        targetLanguage\n      }),\n    });\n\n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n\n    const data = await response.json();\n    if (!data.translation) {\n      throw new Error('No translation result');\n    }\n\n    return data.translation;\n  } catch (error: any) {\n    console.error('Step translation error:', error);\n    throw new Error(error.message || 'Translation failed');\n  }\n} "
  },
  {
    "path": "lib/speech.ts",
    "content": "\"use client\"\n\ninterface IWindow extends Window {\n  webkitSpeechRecognition: any;\n}\n\nexport class SpeechRecognitionService {\n  private recognition: any = null;\n  private isListening: boolean = false;\n\n  constructor() {\n    if (typeof window !== 'undefined') {\n      const windowWithWebkit = window as IWindow;\n      const SpeechRecognition = windowWithWebkit.SpeechRecognition || windowWithWebkit.webkitSpeechRecognition;\n      if (SpeechRecognition) {\n        this.recognition = new SpeechRecognition();\n        this.recognition.continuous = true;\n        this.recognition.interimResults = true;\n      }\n    }\n  }\n\n  public start(onResult: (text: string, isFinal: boolean) => void, onError: (error: string) => void) {\n    if (!this.recognition) {\n      onError('Speech recognition is not supported in this browser');\n      return;\n    }\n\n    if (this.isListening) {\n      return;\n    }\n\n    this.isListening = true;\n\n    this.recognition.onresult = (event: any) => {\n      const result = event.results[event.results.length - 1];\n      const text = result[0].transcript;\n      const isFinal = result.isFinal;\n      onResult(text, isFinal);\n    };\n\n    this.recognition.onerror = (event: any) => {\n      onError(event.error);\n      this.isListening = false;\n    };\n\n    try {\n      this.recognition.start();\n    } catch (error) {\n      onError('Error starting speech recognition');\n      this.isListening = false;\n    }\n  }\n\n  public stop() {\n    if (this.recognition && this.isListening) {\n      this.recognition.stop();\n      this.isListening = false;\n    }\n  }\n} "
  },
  {
    "path": "lib/step.ts",
    "content": "export async function extractTextWithStep(image: string | File) {\n  try {\n    const formData = new FormData();\n    formData.append('image', image);\n\n    const response = await fetch('/api/ocr/step', {\n      method: 'POST',\n      body: formData\n    });\n\n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n\n    const data = await response.json();\n    if (data.error) {\n      throw new Error(data.error);\n    }\n\n    return data.text;\n  } catch (error: any) {\n    console.error('Step OCR error:', error);\n    throw error;\n  }\n} "
  },
  {
    "path": "lib/stripe.ts",
    "content": "import Stripe from 'stripe'\n\n// 服务器端 Stripe 实例\nexport const stripe = process.env.STRIPE_SECRET_KEY \n  ? new Stripe(process.env.STRIPE_SECRET_KEY, {\n      apiVersion: '2024-12-18.acacia',\n      typescript: true,\n    })\n  : null\n\n// 客户端 Stripe 配置\nexport const getStripe = async () => {\n  const { loadStripe } = await import('@stripe/stripe-js')\n  const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)\n  return stripePromise\n}\n\nexport const PLANS = {\n  free: {\n    name: '试用版',\n    description: '体验基础功能',\n    price: '$0',\n    features: {\n      basic: [\n        '无限文本翻译',\n        '每日10次图片识别',\n        '每日8次PDF处理'\n      ],\n      advanced: [\n        '每日5次语音识别',\n        '每日2次视频处理'\n      ],\n      support: [\n        '基础客服支持'\n      ]\n    },\n    quota: {\n      text_quota: -1,\n      image_quota: 10,\n      pdf_quota: 8,\n      speech_quota: 5,\n      video_quota: 2\n    }\n  },\n  monthly: {\n    name: '月度会员',\n    priceId: process.env.NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID!,\n    description: '适合个人日常使用',\n    price: '$9.99/月',\n    features: {\n      basic: [\n        '无限文本翻译',\n        '每日50次图片识别',\n        '每日40次PDF处理'\n      ],\n      advanced: [\n        '每日30次语音识别',\n        '每日10次视频处理',\n        '优先处理队列'\n      ],\n      support: [\n        '优先客服支持'\n      ]\n    },\n    quota: {\n      text_quota: -1,\n      image_quota: 50,\n      pdf_quota: 40,\n      speech_quota: 30,\n      video_quota: 10\n    }\n  },\n  yearly: {\n    name: '年度会员',\n    priceId: process.env.NEXT_PUBLIC_STRIPE_YEARLY_PRICE_ID!,\n    description: '最超值的选择',\n    price: '$99.99/年',\n    isRecommended: true,\n    features: {\n      basic: [\n        '无限文本翻译',\n        '每日100次图片识别',\n        '每日80次PDF处理'\n      ],\n      advanced: [\n        '每日60次语音识别',\n        '每日20次视频处理',\n        '优先处理队列'\n      ],\n      support: [\n        '24/7专属客服',\n        '高级API访问'\n      ]\n    },\n    quota: {\n      text_quota: -1,\n      image_quota: 100,\n      pdf_quota: 80,\n      speech_quota: 60,\n      video_quota: 20\n    }\n  }\n} as const "
  },
  {
    "path": "lib/tencent-asr.ts",
    "content": "\"use client\"\n\ndeclare global {\n  interface Window {\n    SpeechRecognition: any;\n    webkitSpeechRecognition: any;\n  }\n}\n\ninterface TaskResponse {\n  Response: {\n    Data: {\n      TaskId: number;\n    };\n    RequestId: string;\n  };\n}\n\ninterface StatusResponse {\n  Response: {\n    Data: {\n      Status: number;\n      Result: string;\n    };\n    RequestId: string;\n  };\n}\n\nexport class TencentASRService {\n  private taskQueue: Array<() => Promise<void>> = [];\n  private isProcessing: boolean = false;\n\n  private async processQueue() {\n    if (this.isProcessing || this.taskQueue.length === 0) return;\n\n    this.isProcessing = true;\n    try {\n      const task = this.taskQueue.shift();\n      if (task) {\n        await task();\n      }\n    } finally {\n      this.isProcessing = false;\n      if (this.taskQueue.length > 0) {\n        await this.processQueue();\n      }\n    }\n  }\n\n  private addToQueue(task: () => Promise<void>) {\n    this.taskQueue.push(task);\n    this.processQueue();\n  }\n\n  private async fileToBase64(file: File): Promise<string> {\n    return new Promise((resolve, reject) => {\n      const reader = new FileReader();\n      reader.onload = () => {\n        const arrayBuffer = reader.result as ArrayBuffer;\n        const base64 = Buffer.from(arrayBuffer).toString('base64');\n        resolve(base64);\n      };\n      reader.onerror = reject;\n      reader.readAsArrayBuffer(file);\n    });\n  }\n\n  // 清理时间戳\n  private cleanTimestamps(text: string): string {\n    // 移除形如 [0:1.740,0:3.480] 的时间戳\n    return text.replace(/\\[\\d+:\\d+\\.\\d+,\\d+:\\d+\\.\\d+\\]\\s*/g, '');\n  }\n\n  async recognizeAudio(\n    file: File,\n    onProgress: (text: string) => void,\n    onError: (error: string) => void\n  ): Promise<string> {\n    try {\n      const base64Data = await this.fileToBase64(file);\n      \n      return new Promise((resolve, reject) => {\n        this.addToQueue(async () => {\n          try {\n            // 创建识别任务\n            const createResponse = await fetch('/api/asr/create', {\n              method: 'POST',\n              headers: {\n                'Content-Type': 'application/json',\n              },\n              body: JSON.stringify({\n                engineType: '16k_zh',\n                channelNum: 1,\n                resTextFormat: 0,\n                sourceType: 1,\n                data: base64Data,\n              }),\n            });\n\n            if (!createResponse.ok) {\n              const errorData = await createResponse.json();\n              throw new Error(errorData.error || '创建识别任务失败');\n            }\n\n            const createData: TaskResponse = await createResponse.json();\n            const taskId = createData.Response.Data.TaskId;\n\n            // 定期查询任务状态\n            const checkStatus = async () => {\n              const statusResponse = await fetch('/api/asr/status', {\n                method: 'POST',\n                headers: {\n                  'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({\n                  taskId,\n                }),\n              });\n\n              if (!statusResponse.ok) {\n                const errorData = await statusResponse.json();\n                throw new Error(errorData.error || '查询任务状态失败');\n              }\n\n              const statusData: StatusResponse = await statusResponse.json();\n              const status = statusData.Response.Data.Status;\n              let resultText = statusData.Response.Data.Result;\n\n              // 清理时间戳\n              if (resultText) {\n                resultText = this.cleanTimestamps(resultText);\n              }\n\n              if (status === 2) { // 成功\n                resolve(resultText);\n              } else if (status === 3) { // 失败\n                reject(new Error(\"识别失败\"));\n              } else { // 进行中\n                if (resultText) {\n                  onProgress(resultText);\n                }\n                setTimeout(checkStatus, 1000);\n              }\n            };\n\n            await checkStatus();\n          } catch (error: any) {\n            reject(error);\n          }\n        });\n      });\n    } catch (error: any) {\n      onError(error.message || \"音频识别失败\");\n      throw error;\n    }\n  }\n\n  async recognizeStream(\n    onResult: (text: string, isFinal: boolean) => void,\n    onError: (error: string) => void\n  ) {\n    // 实时语音识别使用浏览器原生 API\n    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;\n    if (!SpeechRecognition) {\n      onError('浏览器不支持语音识别');\n      return null;\n    }\n\n    const recognition = new SpeechRecognition();\n    recognition.continuous = true;\n    recognition.interimResults = true;\n    recognition.lang = 'zh-CN';\n\n    recognition.onresult = (event: any) => {\n      const result = event.results[event.results.length - 1];\n      const text = result[0].transcript;\n      onResult(text, result.isFinal);\n    };\n\n    recognition.onerror = (event: any) => {\n      onError(`语音识别错误: ${event.error}`);\n    };\n\n    return recognition;\n  }\n} "
  },
  {
    "path": "lib/tencent-sign.ts",
    "content": "\"use client\"\n\ninterface SignParams {\n  secretId: string;\n  secretKey: string;\n  endpoint: string;\n  service: string;\n  version: string;\n  region: string;\n  action: string;\n  timestamp: number;\n  payload: any;\n}\n\nasync function sha256(message: string): Promise<string> {\n  const msgBuffer = new TextEncoder().encode(message);\n  const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);\n  const hashArray = Array.from(new Uint8Array(hashBuffer));\n  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n}\n\nasync function hmacSha256(key: ArrayBuffer, message: string): Promise<ArrayBuffer> {\n  const cryptoKey = await crypto.subtle.importKey(\n    'raw',\n    key,\n    { name: 'HMAC', hash: 'SHA-256' },\n    false,\n    ['sign']\n  );\n  return await crypto.subtle.sign(\n    'HMAC',\n    cryptoKey,\n    new TextEncoder().encode(message)\n  );\n}\n\nfunction getDate(timestamp: number): string {\n  const date = new Date(timestamp * 1000);\n  return date.toISOString().split('T')[0];\n}\n\nfunction arrayBufferToHex(buffer: ArrayBuffer): string {\n  return Array.from(new Uint8Array(buffer))\n    .map(b => b.toString(16).padStart(2, '0'))\n    .join('');\n}\n\nexport async function sign(params: SignParams): Promise<string> {\n  const {\n    secretId,\n    secretKey,\n    endpoint,\n    service,\n    version,\n    region,\n    action,\n    timestamp,\n    payload,\n  } = params;\n\n  // 1. 拼接规范请求串\n  const httpRequestMethod = 'POST';\n  const canonicalUri = '/';\n  const canonicalQueryString = '';\n  const canonicalHeaders = `content-type:application/json\\nhost:${endpoint}\\n`;\n  const signedHeaders = 'content-type;host';\n  const hashedRequestPayload = await sha256(JSON.stringify(payload));\n  const canonicalRequest = `${httpRequestMethod}\\n${canonicalUri}\\n${canonicalQueryString}\\n${canonicalHeaders}\\n${signedHeaders}\\n${hashedRequestPayload}`;\n\n  // 2. 拼接待签名字符串\n  const algorithm = 'TC3-HMAC-SHA256';\n  const date = getDate(timestamp);\n  const credentialScope = `${date}/${service}/tc3_request`;\n  const hashedCanonicalRequest = await sha256(canonicalRequest);\n  const stringToSign = `${algorithm}\\n${timestamp}\\n${credentialScope}\\n${hashedCanonicalRequest}`;\n\n  // 3. 计算签名\n  const tc3SecretKey = new TextEncoder().encode(`TC3${secretKey}`);\n  const secretDate = await hmacSha256(tc3SecretKey, date);\n  const secretService = await hmacSha256(secretDate, service);\n  const secretSigning = await hmacSha256(secretService, 'tc3_request');\n  const signature = arrayBufferToHex(\n    await hmacSha256(secretSigning, stringToSign)\n  );\n\n  // 4. 拼接 Authorization\n  return `${algorithm} Credential=${secretId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;\n} "
  },
  {
    "path": "lib/tencent.ts",
    "content": "\"use client\"\n\nimport { delay } from './utils'\n\nconst MAX_RETRIES = 3\nconst RETRY_DELAY = 1000\n\nasync function retryWithDelay<T>(fn: () => Promise<T>, retries = MAX_RETRIES): Promise<T> {\n  try {\n    return await fn()\n  } catch (error) {\n    if (retries > 0) {\n      await delay(RETRY_DELAY)\n      return retryWithDelay(fn, retries - 1)\n    }\n    throw error\n  }\n}\n\nexport async function extractTextWithTencent(imageBase64: string): Promise<string> {\n  try {\n    const response = await retryWithDelay(() =>\n      fetch('/api/tencent/ocr', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          image: imageBase64,\n        }),\n      })\n    );\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || '文字识别失败');\n    }\n\n    const data = await response.json();\n    \n    if (!data.success || !data.result) {\n      throw new Error(data.message || '文字识别失败');\n    }\n\n    return data.result;\n  } catch (error: any) {\n    console.error('Error extracting text with Tencent:', error);\n    throw error;\n  }\n} "
  },
  {
    "path": "lib/utils.ts",
    "content": "import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport function delay(ms: number): Promise<void> {\n  return new Promise(resolve => setTimeout(resolve, ms));\n}\n"
  },
  {
    "path": "lib/zhipu.ts",
    "content": "const ZHIPU_API_KEY = process.env.NEXT_PUBLIC_ZHIPU_API_KEY\nconst API_URL = 'https://open.bigmodel.cn/api/paas/v4/chat/completions'\nconst VISION_API_URL = 'https://open.bigmodel.cn/api/paas/v4/chat/completions'\n\ninterface Message {\n  role: 'user' | 'assistant'\n  content: string | {\n    type: string\n    text?: string\n    image_url?: {\n      url: string\n    }\n  }[]\n}\n\n// Base64 URL 编码\nfunction base64UrlEncode(str: string): string {\n  return btoa(str)\n    .replace(/=/g, '')\n    .replace(/\\+/g, '-')\n    .replace(/\\//g, '_')\n}\n\n// 将字符串转换为 Uint8Array\nfunction stringToUint8Array(str: string): Uint8Array {\n  return new TextEncoder().encode(str)\n}\n\n// 将 Uint8Array 转换为 Base64URL 字符串\nfunction uint8ArrayToBase64Url(uint8Array: Uint8Array): string {\n  return base64UrlEncode(\n    Array.from(uint8Array)\n      .map(byte => String.fromCharCode(byte))\n      .join('')\n  )\n}\n\nasync function getZhipuToken() {\n  if (!ZHIPU_API_KEY) {\n    throw new Error('智谱API密钥未配置')\n  }\n\n  const [apiId, apiKey] = ZHIPU_API_KEY.split('.')\n  if (!apiId || !apiKey) {\n    throw new Error('智谱API密钥格式错误')\n  }\n\n  const timestamp = Math.floor(Date.now() / 1000)\n  const expiration = timestamp + 3600  // 1小时后过期\n\n  const header = {\n    alg: 'HS256',\n    sign_type: 'SIGN'\n  }\n\n  const payload = {\n    api_key: apiId,\n    exp: expiration,\n    timestamp\n  }\n\n  const headerBase64 = btoa(JSON.stringify(header))\n    .replace(/=/g, '')\n    .replace(/\\+/g, '-')\n    .replace(/\\//g, '_')\n\n  const payloadBase64 = btoa(JSON.stringify(payload))\n    .replace(/=/g, '')\n    .replace(/\\+/g, '-')\n    .replace(/\\//g, '_')\n\n  const signContent = `${headerBase64}.${payloadBase64}`\n  const encoder = new TextEncoder()\n  const key = await crypto.subtle.importKey(\n    'raw',\n    encoder.encode(apiKey),\n    { name: 'HMAC', hash: 'SHA-256' },\n    false,\n    ['sign']\n  )\n\n  const signature = await crypto.subtle.sign(\n    'HMAC',\n    key,\n    encoder.encode(signContent)\n  )\n\n  const signatureBase64 = btoa(String.fromCharCode(...new Uint8Array(signature)))\n    .replace(/=/g, '')\n    .replace(/\\+/g, '-')\n    .replace(/\\//g, '_')\n\n  return `${signContent}.${signatureBase64}`\n}\n\nexport async function translateWithZhipu(text: string, targetLanguage: string): Promise<string> {\n  try {\n    const token = await getZhipuToken()\n    const prompt = `请将以下文本翻译成${targetLanguage}，只返回翻译结果，不要包含任何其他内容：\\n\\n${text}`\n\n    const messages: Message[] = [\n      {\n        role: 'user',\n        content: prompt\n      }\n    ]\n\n    const response = await fetch(API_URL, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': token\n      },\n      body: JSON.stringify({\n        model: 'glm-4-air',\n        messages,\n        stream: false,\n        temperature: 0.3,\n        max_tokens: 2000\n      })\n    })\n\n    if (!response.ok) {\n      const error = await response.json()\n      throw new Error(error.message || '翻译失败')\n    }\n\n    const data = await response.json()\n    return data.choices[0].message.content.trim()\n  } catch (error: any) {\n    console.error('智谱AI翻译错误:', error)\n    throw new Error(error.message || '翻译失败')\n  }\n}\n\nexport async function extractVideoFrames(videoFile: File): Promise<string[]> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onloadend = () => {\n      const base64Data = (reader.result as string).split(',')[1];\n      resolve([base64Data]);\n    };\n    reader.onerror = () => {\n      reject(new Error('视频处理失败'));\n    };\n    reader.readAsDataURL(videoFile);\n  });\n}\n\nexport async function analyzeVideoContent(frames: string[]): Promise<string> {\n  try {\n    const token = await getZhipuToken()\n    const videoBase64 = frames[0]\n\n    const response = await fetch(API_URL, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': token\n      },\n      body: JSON.stringify({\n        model: 'glm-4v-plus',\n        messages: [\n          {\n            role: 'user',\n            content: [\n              {\n                type: 'text',\n                text: '请识别视频中的所有文字内容，包括字幕、标题、显示的文本等。只需要返回文字内容，不需要其他描述。'\n              },\n              {\n                type: 'video_url',\n                video_url: {\n                  url: `data:video/mp4;base64,${videoBase64}`\n                }\n              }\n            ]\n          }\n        ],\n        temperature: 0.95,\n        top_p: 0.7\n      })\n    })\n\n    if (!response.ok) {\n      const error = await response.json()\n      console.error('智谱AI响应错误:', error)\n      throw new Error(error.error?.message || '视频分析失败')\n    }\n\n    const data = await response.json()\n    if (!data.choices || !data.choices[0] || !data.choices[0].message) {\n      console.error('智谱AI响应格式错误:', data)\n      throw new Error('视频分析结果格式错误')\n    }\n    \n    const result = data.choices[0].message.content.trim()\n    if (!result) {\n      throw new Error('未识别到文字内容')\n    }\n\n    // 去除重复内容\n    const lines = result.split('\\n')\n    const uniqueLines = Array.from(new Set(lines.filter((line: string) => line.trim())))\n    const cleanedText = uniqueLines.join('\\n')\n    \n    return cleanedText\n  } catch (error: any) {\n    console.error('智谱AI视频分析错误:', error)\n    throw new Error(error.message || '视频分析失败')\n  }\n}\n\nexport async function extractTextWithZhipu(imageBase64: string): Promise<string> {\n  try {\n    const token = await getZhipuToken()\n    const base64Data = imageBase64.replace(/^data:image\\/(png|jpeg|jpg);base64,/, '')\n\n    const response = await fetch(API_URL, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': token\n      },\n      body: JSON.stringify({\n        model: 'glm-4v-flash',\n        messages: [\n          {\n            role: 'user',\n            content: [\n              {\n                type: 'text',\n                text: '请识别图片中的所有文字内容，只返回文字，不需要其他描述。'\n              },\n              {\n                type: 'image_url',\n                image_url: {\n                  url: `data:image/jpeg;base64,${base64Data}`\n                }\n              }\n            ]\n          }\n        ],\n        temperature: 0.95,\n        top_p: 0.7\n      })\n    })\n\n    if (!response.ok) {\n      const error = await response.json()\n      console.error('智谱AI响应错误:', error)\n      throw new Error(error.error?.message || '文字识别失败')\n    }\n\n    const data = await response.json()\n    if (!data.choices || !data.choices[0] || !data.choices[0].message) {\n      console.error('智谱AI响应格式错误:', data)\n      throw new Error('文字识别结果格式错误')\n    }\n    \n    const result = data.choices[0].message.content.trim()\n    if (!result) {\n      throw new Error('未识别到文字内容')\n    }\n    \n    return result\n  } catch (error: any) {\n    console.error('智谱AI文字识别错误:', error)\n    throw new Error(error.message || '文字识别失败')\n  }\n}\n\nexport async function extractFileContent(file: File): Promise<string> {\n  try {\n    return new Promise((resolve, reject) => {\n      const reader = new FileReader()\n      reader.onloadend = async () => {\n        try {\n          const base64Data = (reader.result as string).replace(/^data:.*?;base64,/, '')\n          const token = await getZhipuToken()\n\n          const response = await fetch(API_URL, {\n            method: 'POST',\n            headers: {\n              'Content-Type': 'application/json',\n              'Authorization': token\n            },\n            body: JSON.stringify({\n              model: 'glm-4v-flash',\n              messages: [\n                {\n                  role: 'user',\n                  content: [\n                    {\n                      type: 'text',\n                      text: '请识别文件中的所有文字内容，只返回文字，不需要其他描述。'\n                    },\n                    {\n                      type: 'image_url',\n                      image_url: {\n                        url: `data:${file.type};base64,${base64Data}`\n                      }\n                    }\n                  ]\n                }\n              ],\n              temperature: 0.95,\n              top_p: 0.7,\n              max_tokens: 1024\n            })\n          })\n\n          if (!response.ok) {\n            const error = await response.json()\n            console.error('智谱AI响应错误:', error)\n            reject(new Error(error.error?.message || '文件识别失败'))\n            return\n          }\n\n          const data = await response.json()\n          if (!data.choices || !data.choices[0] || !data.choices[0].message) {\n            console.error('智谱AI响应格式错误:', data)\n            reject(new Error('文件识别结果格式错误'))\n            return\n          }\n\n          const result = data.choices[0].message.content.trim()\n          if (!result) {\n            reject(new Error('未识别到文字内容'))\n            return\n          }\n\n          resolve(result)\n        } catch (error: any) {\n          console.error('智谱AI文件识别错误:', error)\n          reject(new Error(error.message || '文件识别失败'))\n        }\n      }\n      reader.onerror = () => {\n        reject(new Error('文件读取失败'))\n      }\n      reader.readAsDataURL(file)\n    })\n  } catch (error: any) {\n    console.error('智谱AI文件识别错误:', error)\n    throw new Error(error.message || '文件识别失败')\n  }\n} "
  },
  {
    "path": "middleware.ts",
    "content": "import { withAuth } from \"next-auth/middleware\"\n\n// 导出中间件函数\nexport default withAuth(\n  // `withAuth` augments your `Request` with the user's token.\n  function middleware(req) {\n    console.log('中间件运行:', req.nextauth.token)\n  },\n  {\n    callbacks: {\n      authorized: ({ token, req }) => {\n        // 如果是订阅成功后的重定向，检查token或允许一次性访问\n        if (req.nextUrl.pathname === '/profile' && req.nextUrl.searchParams.get('subscription') === 'success') {\n          // 如果有token，允许访问\n          if (token) {\n            return true\n          }\n          // 如果没有token，重定向到登录页面\n          return false\n        }\n        return !!token\n      }\n    },\n  }\n)\n\n// 配置需要保护的路由\nexport const config = {\n  matcher: [\n    '/dashboard/:path*',\n    '/profile/:path*',\n  ]\n} "
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  eslint: {\n    ignoreDuringBuilds: true,\n  },\n  images: { unoptimized: true },\n};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"nextjs\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"migrate\": \"tsx lib/db/migrate.ts\"\n  },\n  \"dependencies\": {\n    \"@alicloud/ocr-api20210707\": \"^3.1.2\",\n    \"@alicloud/openapi-client\": \"^0.4.12\",\n    \"@alicloud/pop-core\": \"^1.8.0\",\n    \"@alicloud/sts-sdk\": \"^1.0.2\",\n    \"@alicloud/sts20150401\": \"^1.1.4\",\n    \"@alicloud/tea-util\": \"^1.4.9\",\n    \"@alicloud/viapi20230117\": \"^1.0.0\",\n    \"@alicloud/videorecog20200320\": \"^3.0.6\",\n    \"@auth/core\": \"^0.34.2\",\n    \"@formatjs/intl-localematcher\": \"^0.5.10\",\n    \"@google/generative-ai\": \"^0.2.1\",\n    \"@hookform/resolvers\": \"^3.9.0\",\n    \"@mistralai/mistralai\": \"^1.5.2\",\n    \"@neondatabase/serverless\": \"^0.10.4\",\n    \"@next/swc-wasm-nodejs\": \"13.5.1\",\n    \"@radix-ui/react-accordion\": \"^1.2.0\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n    \"@radix-ui/react-aspect-ratio\": \"^1.1.0\",\n    \"@radix-ui/react-avatar\": \"^1.1.0\",\n    \"@radix-ui/react-checkbox\": \"^1.1.1\",\n    \"@radix-ui/react-collapsible\": \"^1.1.0\",\n    \"@radix-ui/react-context-menu\": \"^2.2.1\",\n    \"@radix-ui/react-dialog\": \"^1.1.1\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n    \"@radix-ui/react-hover-card\": \"^1.1.1\",\n    \"@radix-ui/react-label\": \"^2.1.0\",\n    \"@radix-ui/react-menubar\": \"^1.1.1\",\n    \"@radix-ui/react-navigation-menu\": \"^1.2.0\",\n    \"@radix-ui/react-popover\": \"^1.1.1\",\n    \"@radix-ui/react-progress\": \"^1.1.0\",\n    \"@radix-ui/react-radio-group\": \"^1.2.0\",\n    \"@radix-ui/react-scroll-area\": \"^1.1.0\",\n    \"@radix-ui/react-select\": \"^2.1.1\",\n    \"@radix-ui/react-separator\": \"^1.1.0\",\n    \"@radix-ui/react-slider\": \"^1.2.0\",\n    \"@radix-ui/react-slot\": \"^1.1.0\",\n    \"@radix-ui/react-switch\": \"^1.1.0\",\n    \"@radix-ui/react-tabs\": \"^1.1.0\",\n    \"@radix-ui/react-toast\": \"^1.2.1\",\n    \"@radix-ui/react-toggle\": \"^1.1.0\",\n    \"@radix-ui/react-toggle-group\": \"^1.1.0\",\n    \"@radix-ui/react-tooltip\": \"^1.1.2\",\n    \"@stripe/stripe-js\": \"^5.5.0\",\n    \"@types/node\": \"20.6.2\",\n    \"@types/react\": \"18.2.22\",\n    \"@types/react-dom\": \"18.2.7\",\n    \"@types/uuid\": \"^10.0.0\",\n    \"@vercel/blob\": \"^0.27.0\",\n    \"@vercel/postgres\": \"^0.10.0\",\n    \"ali-oss\": \"^6.22.0\",\n    \"autoprefixer\": \"10.4.15\",\n    \"base64-arraybuffer\": \"^1.0.2\",\n    \"bcryptjs\": \"^2.4.3\",\n    \"canvas-confetti\": \"^1.9.3\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"cmdk\": \"^1.0.0\",\n    \"date-fns\": \"^3.6.0\",\n    \"dotenv\": \"^16.4.7\",\n    \"embla-carousel-react\": \"^8.3.0\",\n    \"eslint\": \"8.49.0\",\n    \"eslint-config-next\": \"13.5.1\",\n    \"framer-motion\": \"^11.16.4\",\n    \"input-otp\": \"^1.2.4\",\n    \"lucide-react\": \"^0.469.0\",\n    \"negotiator\": \"^1.0.0\",\n    \"next\": \"^14.2.25\",\n    \"next-auth\": \"^4.24.11\",\n    \"next-themes\": \"^0.3.0\",\n    \"openai\": \"^4.77.4\",\n    \"pdfjs-dist\": \"^4.10.38\",\n    \"pg\": \"^8.13.1\",\n    \"postcss\": \"8.4.30\",\n    \"proxy-agent\": \"^5.0.0\",\n    \"react\": \"18.2.0\",\n    \"react-day-picker\": \"^8.10.1\",\n    \"react-dom\": \"18.2.0\",\n    \"react-hook-form\": \"^7.53.0\",\n    \"react-resizable-panels\": \"^2.1.3\",\n    \"recharts\": \"^2.12.7\",\n    \"sharp\": \"^0.33.5\",\n    \"sonner\": \"^1.5.0\",\n    \"stripe\": \"^17.5.0\",\n    \"swiper\": \"^11.2.1\",\n    \"tailwind-merge\": \"^2.5.2\",\n    \"tailwindcss\": \"3.3.3\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"tencentcloud-sdk-nodejs\": \"^4.0.1009\",\n    \"tencentcloud-sdk-nodejs-hunyuan\": \"^4.0.1007\",\n    \"tencentcloud-sdk-nodejs-ocr\": \"^4.0.1007\",\n    \"typescript\": \"5.2.2\",\n    \"uuid\": \"^8.3.2\",\n    \"vaul\": \"^0.9.9\",\n    \"zod\": \"^3.23.8\",\n    \"zustand\": \"^4.5.2\"\n  },\n  \"devDependencies\": {\n    \"@types/bcryptjs\": \"^2.4.6\",\n    \"@types/canvas-confetti\": \"^1.9.0\",\n    \"@types/negotiator\": \"^0.6.3\",\n    \"coffee-script\": \"^1.12.7\",\n    \"coffeescript\": \"^2.7.0\",\n    \"tsx\": \"^4.19.2\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "public/ads.txt",
    "content": "google.com, pub-9535069756501112, DIRECT, f08c47fec0942fa0"
  },
  {
    "path": "public/site.webmanifest",
    "content": "{\n  \"name\": \"AI Translation Assistant\",\n  \"short_name\": \"AI Translate\",\n  \"description\": \"All-in-one intelligent translation solution supporting text, image, PDF, speech, and video translation\",\n  \"start_url\": \"/\",\n  \"display\": \"standalone\",\n  \"background_color\": \"#020817\",\n  \"theme_color\": \"#0ea5e9\",\n  \"icons\": [\n    {\n      \"src\": \"/android-chrome-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"/android-chrome-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\"\n    }\n  ]\n} "
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: [\"class\"],\n  content: [\n    './pages/**/*.{ts,tsx}',\n    './components/**/*.{ts,tsx}',\n    './app/**/*.{ts,tsx}',\n    './src/**/*.{ts,tsx}',\n\t],\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: 0 },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: 0 },\n        },\n        gradient: {\n          '0%, 100%': { backgroundPosition: '0% center' },\n          '50%': { backgroundPosition: '100% center' },\n        },\n        'spin-slow': {\n          '0%': { transform: 'rotate(0deg)' },\n          '100%': { transform: 'rotate(360deg)' },\n        },\n        scroll: {\n          '0%': { transform: 'translateX(0)' },\n          '100%': { transform: 'translateX(-50%)' },\n        },\n        'scroll-reverse': {\n          '0%': { transform: 'translateX(-50%)' },\n          '100%': { transform: 'translateX(0)' },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n        \"gradient\": \"gradient 8s linear infinite\",\n        \"spin-slow\": \"spin-slow 8s linear infinite\",\n        \"scroll\": \"scroll 40s linear infinite\",\n        \"scroll-reverse\": \"scroll-reverse 40s linear infinite\",\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\")],\n} "
  },
  {
    "path": "tailwind.config.ts",
    "content": "import type { Config } from 'tailwindcss';\n\nconst config: Config = {\n  darkMode: ['class'],\n  content: [\n    './pages/**/*.{js,ts,jsx,tsx,mdx}',\n    './components/**/*.{js,ts,jsx,tsx,mdx}',\n    './app/**/*.{js,ts,jsx,tsx,mdx}',\n  ],\n  theme: {\n    extend: {\n      backgroundImage: {\n        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',\n        'gradient-conic':\n          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',\n      },\n      borderRadius: {\n        lg: 'var(--radius)',\n        md: 'calc(var(--radius) - 2px)',\n        sm: 'calc(var(--radius) - 4px)',\n      },\n      colors: {\n        background: 'hsl(var(--background))',\n        foreground: 'hsl(var(--foreground))',\n        card: {\n          DEFAULT: 'hsl(var(--card))',\n          foreground: 'hsl(var(--card-foreground))',\n        },\n        popover: {\n          DEFAULT: 'hsl(var(--popover))',\n          foreground: 'hsl(var(--popover-foreground))',\n        },\n        primary: {\n          DEFAULT: 'hsl(var(--primary))',\n          foreground: 'hsl(var(--primary-foreground))',\n        },\n        secondary: {\n          DEFAULT: 'hsl(var(--secondary))',\n          foreground: 'hsl(var(--secondary-foreground))',\n        },\n        muted: {\n          DEFAULT: 'hsl(var(--muted))',\n          foreground: 'hsl(var(--muted-foreground))',\n        },\n        accent: {\n          DEFAULT: 'hsl(var(--accent))',\n          foreground: 'hsl(var(--accent-foreground))',\n        },\n        destructive: {\n          DEFAULT: 'hsl(var(--destructive))',\n          foreground: 'hsl(var(--destructive-foreground))',\n        },\n        border: 'hsl(var(--border))',\n        input: 'hsl(var(--input))',\n        ring: 'hsl(var(--ring))',\n        chart: {\n          '1': 'hsl(var(--chart-1))',\n          '2': 'hsl(var(--chart-2))',\n          '3': 'hsl(var(--chart-3))',\n          '4': 'hsl(var(--chart-4))',\n          '5': 'hsl(var(--chart-5))',\n        },\n      },\n      keyframes: {\n        'accordion-down': {\n          from: {\n            height: '0',\n          },\n          to: {\n            height: 'var(--radix-accordion-content-height)',\n          },\n        },\n        'accordion-up': {\n          from: {\n            height: 'var(--radix-accordion-content-height)',\n          },\n          to: {\n            height: '0',\n          },\n        },\n      },\n      animation: {\n        'accordion-down': 'accordion-down 0.2s ease-out',\n        'accordion-up': 'accordion-up 0.2s ease-out',\n      },\n    },\n  },\n  plugins: [require('tailwindcss-animate')],\n};\nexport default config;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2015\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ]\n    },\n    \"downlevelIteration\": true,\n    \"typeRoots\": [\n      \"./node_modules/@types\",\n      \"./types\"\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"./types/**/*.d.ts\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "types/alicloud.d.ts",
    "content": "declare module '@alicloud/ocr-api20210707' {\n  export class Client {\n    constructor(config: any)\n    recognizeVideoContentWithOptions(request: any, runtime: any): Promise<any>\n    getAsyncJobResultWithOptions(request: any, runtime: any): Promise<any>\n  }\n\n  export class RecognizeVideoContentRequest {\n    constructor(config: { videoURL: string })\n  }\n\n  export class GetAsyncJobResultRequest {\n    constructor(config: { jobId: string })\n  }\n}\n\ndeclare module '@alicloud/openapi-client' {\n  export class Config {\n    constructor(config: {\n      accessKeyId: string\n      accessKeySecret: string\n      endpoint: string\n      regionId: string\n    })\n  }\n}\n\ndeclare module '@alicloud/tea-util' {\n  export class RuntimeOptions {\n    constructor(options?: any)\n  }\n}\n\ndeclare module 'ali-oss' {\n  interface OSSOptions {\n    region: string;\n    accessKeyId: string;\n    accessKeySecret: string;\n    bucket: string;\n    stsToken?: string;\n    secure?: boolean;\n    timeout?: number;\n    refreshSTSToken?: () => Promise<{\n      accessKeyId: string;\n      accessKeySecret: string;\n      stsToken: string;\n    }>;\n    retryMax?: number;\n    headerEncoding?: string;\n  }\n\n  interface MultipartUploadOptions {\n    parallel?: number;\n    partSize?: number;\n    progress?: (p: number, checkpoint: any) => void;\n  }\n\n  interface PutResult {\n    name: string;\n    url: string;\n    res: {\n      status: number;\n      statusCode: number;\n      headers: {\n        [key: string]: string;\n      };\n    };\n  }\n\n  class OSS {\n    constructor(options: OSSOptions);\n    put(name: string, file: File | Blob | Buffer): Promise<PutResult>;\n    multipartUpload(name: string, file: File | Blob | Buffer, options?: MultipartUploadOptions): Promise<PutResult>;\n    generateObjectUrl(name: string): string;\n  }\n\n  export = OSS;\n}\n\ndeclare module '@alicloud/sts-sdk' {\n  interface STSOptions {\n    accessKeyId: string;\n    accessKeySecret: string;\n    endpoint: string;\n    region?: string;\n  }\n\n  interface Credentials {\n    AccessKeyId: string;\n    AccessKeySecret: string;\n    SecurityToken: string;\n    Expiration: string;\n  }\n\n  interface AssumeRoleResponse {\n    credentials: Credentials;\n  }\n\n  class STS {\n    constructor(options: STSOptions);\n    assumeRole(\n      roleArn: string,\n      roleSessionName: string,\n      policy?: string,\n      expiration?: number,\n      options?: any\n    ): Promise<AssumeRoleResponse>;\n  }\n\n  export default STS;\n}\n\ndeclare module '@alicloud/sts20150401' {\n  interface AssumeRoleRequest {\n    roleArn: string;\n    roleSessionName: string;\n    policy?: string;\n    durationSeconds?: number;\n  }\n\n  interface Credentials {\n    accessKeyId: string;\n    accessKeySecret: string;\n    securityToken: string;\n    expiration: string;\n  }\n\n  interface AssumeRoleResponse {\n    body: {\n      credentials: Credentials;\n      requestId: string;\n    };\n  }\n\n  class Client {\n    constructor(config: any);\n    assumeRole(request: AssumeRoleRequest): Promise<AssumeRoleResponse>;\n  }\n\n  export default Client;\n} "
  },
  {
    "path": "types/next-auth.d.ts",
    "content": "import NextAuth from \"next-auth\";\nimport { JWT } from \"next-auth/jwt\";\n\ndeclare module \"next-auth\" {\n  interface User {\n    id: string;\n    email: string;\n  }\n\n  interface Session {\n    user: User;\n  }\n}\n\ndeclare module \"next-auth/jwt\" {\n  interface JWT {\n    id: string;\n  }\n} "
  }
]