userName 1 rok temu
commit
e734fcc79c
74 zmienionych plików z 17889 dodań i 0 usunięć
  1. 20 0
      .env.example
  2. 75 0
      .gitignore
  3. 268 0
      README.md
  4. 147 0
      backend/api/auth.py
  5. 234 0
      backend/api/chat.py
  6. 173 0
      backend/api/chat_history.py
  7. 3 0
      backend/api/init.py
  8. 605 0
      backend/api/research.py
  9. 119 0
      backend/api/research_history.py
  10. 110 0
      backend/api/users.py
  11. 49 0
      backend/config.py
  12. 76 0
      backend/core/auth.py
  13. 137 0
      backend/core/clustering.py
  14. 35 0
      backend/core/database.py
  15. 3 0
      backend/core/init.py
  16. 81 0
      backend/core/models.py
  17. 114 0
      backend/core/report.py
  18. 852 0
      backend/core/research.py
  19. 70 0
      backend/data/cache/research_1744959673.json
  20. 122 0
      backend/data/cache/research_1744960528.json
  21. 70 0
      backend/data/cache/research_1744960762.json
  22. 422 0
      backend/data/cache/research_1744967509.json
  23. 418 0
      backend/data/cache/research_1744982496.json
  24. 57 0
      backend/init_db.py
  25. 90 0
      backend/main.py
  26. 519 0
      backend/utils/api_client.py
  27. 3 0
      backend/utils/init.py
  28. 64 0
      backend/utils/keywords.py
  29. 15 0
      frontend/mobile/README.md
  30. 15 0
      frontend/web/README.md
  31. 41 0
      lightstar-web/.gitignore
  32. 36 0
      lightstar-web/README.md
  33. 16 0
      lightstar-web/eslint.config.mjs
  34. 15 0
      lightstar-web/next.config.ts
  35. 5868 0
      lightstar-web/package-lock.json
  36. 33 0
      lightstar-web/package.json
  37. 6 0
      lightstar-web/postcss.config.js
  38. 1 0
      lightstar-web/public/file.svg
  39. 1 0
      lightstar-web/public/globe.svg
  40. 1 0
      lightstar-web/public/next.svg
  41. 1 0
      lightstar-web/public/vercel.svg
  42. 1 0
      lightstar-web/public/window.svg
  43. 337 0
      lightstar-web/src/app/ai-chat/page.tsx
  44. 75 0
      lightstar-web/src/app/api/chat/route.ts
  45. 52 0
      lightstar-web/src/app/api/research/enhanced-process-stream/route.ts
  46. 126 0
      lightstar-web/src/app/dashboard/page.tsx
  47. BIN
      lightstar-web/src/app/favicon.ico
  48. 161 0
      lightstar-web/src/app/globals.css
  49. 86 0
      lightstar-web/src/app/layout.tsx
  50. 165 0
      lightstar-web/src/app/login/page.tsx
  51. 28 0
      lightstar-web/src/app/page.tsx
  52. 1012 0
      lightstar-web/src/app/profile/page.tsx
  53. 274 0
      lightstar-web/src/app/register/page.tsx
  54. 23 0
      lightstar-web/src/components/AuthStateManager.tsx
  55. 18 0
      lightstar-web/src/components/ClientInit.tsx
  56. 1108 0
      lightstar-web/src/components/forms/research-form.tsx
  57. 34 0
      lightstar-web/src/components/language-switcher.tsx
  58. 53 0
      lightstar-web/src/components/theme-selector.tsx
  59. 44 0
      lightstar-web/src/components/ui/button.tsx
  60. 23 0
      lightstar-web/src/components/ui/error-message.tsx
  61. 42 0
      lightstar-web/src/components/ui/glass-card.tsx
  62. 20 0
      lightstar-web/src/components/ui/keyword-chip.tsx
  63. 76 0
      lightstar-web/src/components/ui/modal.tsx
  64. 167 0
      lightstar-web/src/components/ui/paper-list.tsx
  65. 77 0
      lightstar-web/src/components/ui/report-panel.tsx
  66. 92 0
      lightstar-web/src/components/ui/research-direction-card.tsx
  67. 50 0
      lightstar-web/src/middleware.ts
  68. 50 0
      lightstar-web/tailwind.config.ts
  69. 27 0
      lightstar-web/tsconfig.json
  70. 1711 0
      package-lock.json
  71. 11 0
      package.json
  72. BIN
      postcss.config.js
  73. 52 0
      requirements.txt
  74. 909 0
      src/components/forms/research-form.tsx

+ 20 - 0
.env.example

@@ -0,0 +1,20 @@
+# API密钥
+DEEPSEEK_API_KEY=your_deepseek_api_key_here
+
+# 环境设置
+LIGHTSTAR_ENV=development
+
+# 检索配置
+MAX_SEARCH_RESULTS=15
+ARXIV_SEARCH_TIMEOUT=30
+
+# 聚类配置
+DEFAULT_EMBEDDING_MODEL=all-MiniLM-L6-v2
+DEFAULT_NUM_CLUSTERS=3
+
+# CORS设置
+CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173
+
+# 添加到现有的.env文件中
+SECRET_KEY=your-very-secret-key-change-this-in-production
+ACCESS_TOKEN_EXPIRE_MINUTES=1440

+ 75 - 0
.gitignore

@@ -0,0 +1,75 @@
+# 环境变量配置
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Python虚拟环境
+venv/
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Node.js依赖
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnp/
+.pnp.js
+
+# Next.js构建输出
+.next/
+out/
+.vercel
+.cache/
+
+# 日志文件
+*.log
+logs/
+log/
+backend/lightstar_api.log
+
+# IDE和编辑器文件
+.idea/
+.vscode/
+*.swp
+*.swo
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# 数据库文件
+*.db
+*.sqlite
+*.sqlite3
+
+# 其他临时文件
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db

+ 268 - 0
README.md

@@ -0,0 +1,268 @@
+# 文献启明星(LightStar)- 科研调研智能体系统
+
+## 项目简介
+
+**文献启明星**是一款面向科研初学者的 AI 智能体系统,致力于帮助用户从模糊的研究意图出发,自动完成科研调研的核心流程,包括:主题拆解、文献检索、文献聚类、结构总结、代表文献推荐与调研报告生成。
+
+用户仅需输入一句话,即可获得一份结构化的初步调研成果,用于选题、开题、综述写作或项目申报。本项目采用前后端分离架构,支持多端适配,为不同场景下的科研人员提供便捷服务。
+
+## 最新功能更新
+
+**户账户系统**:新增用户注册、登录和个人资料管理功能
+**AI聊天助手**:引入智能对话功能,辅助研究过程中的问题解答
+**研究报告生成**:支持自动生成格式化的研究报告,包括章节标题和详细内容
+**论文检索与显示**:优化了论文结果显示,支持查看完整摘要和原文链接
+**HTML导出**:支持将研究报告以HTML格式导出,方便用户保存和分享
+**UI交互优化**:研究助手界面全新设计,分离显示论文列表和研究报告
+**多语言支持**:支持英文研究报告生成
+**实时进度反馈**:在研究过程中提供实时的进度更新和关键词提取
+
+## 技术架构
+
+### 后端 (Backend)
+- **框架**: FastAPI
+- **语言**: Python 3.8+
+- **数据库**: SQLite (通过SQLAlchemy ORM)
+- **认证**: JWT令牌认证
+- **核心依赖**:
+  - arxiv API (文献检索)
+  - sentence-transformers (文本向量化)
+  - scikit-learn (聚类分析)
+  - Deepseek API (报告生成和AI聊天)
+  - Pydantic (数据验证)
+  - SQLAlchemy (数据库ORM)
+
+### 前端 (Frontend)
+- **框架**: Next.js 14
+- **UI构建**: Tailwind CSS 3
+- **状态管理**: React Hooks
+- **认证**: 基于JWT的用户验证系统
+- **特色功能**:
+  - 响应式设计,适配不同设备
+  - 多主题切换系统
+  - 玻璃态UI设计 (Glassmorphism)
+  - 模态框交互
+  - 实时数据流处理 (Server-Sent Events)
+  - 用户面板和个人资料管理
+
+## 项目结构
+
+```bash
+lightstar/
+├── backend/                    # FastAPI后端服务
+│   ├── main.py                 # 主入口
+│   ├── config.py               # 配置文件
+│   ├── init_db.py              # 数据库初始化
+│   ├── api/                    # API路由
+│   ├── core/                   # 核心业务逻辑
+│   │   ├── auth.py             # 认证系统
+│   │   ├── models.py           # 数据模型
+│   │   ├── database.py         # 数据库连接
+│   │   ├── research.py         # 研究流程
+│   │   ├── clustering.py       # 聚类算法
+│   │   └── report.py           # 报告生成
+│   └── utils/                  # 工具函数
+│
+├── lightstar-web/              # Next.js前端应用
+│   ├── src/
+│   │   ├── app/                # 页面
+│   │   │   ├── login/          # 登录页面
+│   │   │   ├── register/       # 注册页面
+│   │   │   ├── dashboard/      # 仪表盘页面
+│   │   │   ├── profile/        # 个人资料页面
+│   │   │   └── ai-chat/        # AI聊天页面
+│   │   ├── components/         # UI组件
+│   │   │   ├── ui/             # 基础UI组件
+│   │   │   └── forms/          # 表单组件
+│   │   └── lib/                # 工具函数
+│   └── public/                 # 静态资源
+│
+└── README.md                   # 项目说明文档
+```
+
+## 环境配置
+
+1. **克隆仓库**
+```bash
+git clone http://git.fmode.cn:3000/XMD/Lightstar.git
+cd Lightstar
+```
+
+2. **配置环境变量**
+复制环境变量模板并填写必要信息
+```bash
+cp .env.example .env
+# 编辑.env文件,填写必要的API密钥
+```
+
+## 使用说明
+
+### 后端服务启动
+
+```bash
+# 创建虚拟环境(可选)
+python -m venv venv
+source venv/bin/activate  # Linux/Mac
+# 或
+venv\Scripts\activate     # Windows
+
+# 安装依赖
+pip install -r requirements.txt
+
+# 初始化数据库
+cd backend
+python init_db.py
+
+# 启动后端服务
+python main.py
+```
+
+后端服务将在 http://localhost:8000 启动
+
+### 前端应用启动
+
+```bash
+cd lightstar-web
+npm install
+npm run dev
+```
+
+前端应用将在 http://localhost:3000 启动
+
+## 功能详解
+
+### 用户账户系统
+1. **用户注册**: 创建新账户,提供用户名、邮箱和密码
+2. **用户登录**: 安全登录系统,获取JWT令牌
+3. **个人资料**: 查看和更新个人信息
+4. **会话管理**: 自动刷新令牌,确保无缝体验
+
+### 研究助手界面
+
+研究助手提供了一站式调研流程,主要功能包括:
+
+1. **研究主题输入**:用户只需输入自己感兴趣的研究主题
+2. **关键词提取**:系统自动从研究主题中提取关键词
+3. **研究方向生成**:基于关键词生成多个研究方向
+4. **论文检索**:为每个研究方向自动检索相关论文
+5. **研究报告生成**:为每个研究方向生成详细的研究报告
+6. **报告导出**:支持将研究报告导出为HTML格式
+
+### AI聊天助手
+1. **智能对话**: 基于大型语言模型的对话系统
+2. **研究相关问答**: 解答研究过程中的问题
+3. **文献解析帮助**: 辅助理解复杂文献
+4. **研究建议**: 提供研究方向和方法建议
+
+### 界面交互
+
+1. **论文模态框**:点击"查看相关论文"可打开论文列表模态框,显示论文标题、作者、年份、摘要等信息,支持点击论文标题查看原文
+2. **报告模态框**:点击"查看研究报告"可打开报告模态框,显示完整研究报告,支持下载为HTML格式
+3. **聊天界面**: 友好的聊天界面设计,支持代码高亮和降价格式
+
+## 功能特点(功能持续拓展中)
+
+- 用户身份验证和账户管理
+- 智能AI聊天助手
+- 智能研究主题解析
+- 自动文献检索与分析
+- 文献聚类与可视化
+- 研究报告自动生成
+- 多主题UI,提供舒适的视觉体验
+- 响应式设计,支持各类设备
+- HTML格式报告导出
+- 实时进度反馈
+- 文献原文链接直达
+
+## 下一步计划
+
+- [ ] 添加研究报告PDF导出功能
+- [ ] 支持更多文献数据库的集成
+- [ ] 增强文献分析功能,提供引用网络
+- [ ] 历史研究项目保存与管理
+- [ ] 团队协作功能
+- [ ] 开发移动端应用
+- [ ] 增加自定义研究报告模板
+- [ ] 引入更多高级AI模型
+- [ ] 添加主题图表可视化功能
+- [ ] 支持批量导出文献元数据
+
+## 贡献指南
+
+1. Fork项目仓库
+2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
+3. 提交更改 (`git commit -m 'Add some amazing feature'`)
+4. 推送分支 (`git push origin feature/amazing-feature`)
+5. 创建Pull Request
+
+## 联系方式
+
+项目负责人:[XMD] - [xumingdi2004@icloud.com]
+
+---
+
+## 目标用户
+
+- 初入科研的硕士/博士研究生
+- 缺乏调研经验的年轻教师或科研助理
+- 对某一新领域感兴趣的项目开发人员
+- 需要在多种设备上进行科研调研的研究人员
+
+---
+
+## 核心价值
+
+| 用户痛点 | 文献启明星的解决方案 |
+|----------|----------------------|
+| 不会选题、不知从哪开始 | AI 拆解主题,提炼研究方向 |
+| 不会查文献,效率低 | 自动调用开放数据库进行检索 |
+| 无法判断文献质量 | 提供引用数、摘要、推荐标记 |
+| 看完文献却写不出综述 | 聚类归纳 + 自动报告生成 |
+| 对研究有疑问无人解答 | AI助手提供智能对话支持 |
+| 时间紧、任务重、无从下手 | 一站式完成调研闭环 |
+| 需要随时随地进行研究 | 多端适配,支持Web/移动/桌面 |
+
+---
+
+## 功能流程图
+
+```text
+用户注册/登录 → 个人资料设置
+         ↓
+用户输入研究意图
+         ↓
+AI 拆解关键词与子主题
+         ↓
+调用文献 API 自动检索(arXiv / Semantic Scholar)
+         ↓
+聚类文献主题,归纳关键词与研究路线
+         ↓
+推荐经典/综述文献(摘要 + 链接)
+         ↓
+输出结构化调研报告(含HTML导出功能)
+         ↓
+AI聊天助手辅助解答研究问题
+```
+
+## 界面预览
+
+- 用户登录/注册界面:简洁明了的身份验证入口
+- 个人资料页面:用户信息管理
+- 研究助手界面:提供直观的研究主题输入和结果展示
+- AI聊天界面:友好的对话式帮助系统
+- 论文列表模态框:显示相关论文及其详细信息
+- 研究报告模态框:格式化展示完整研究报告,支持HTML导出
+
+## 错误处理
+
+系统提供了完善的错误处理机制,包括:
+
+- 连接失败重试
+- 用户友好的错误提示
+- 调试信息记录
+- 数据缓存恢复
+- 认证错误处理
+
+## 许可证
+
+本项目遵循 MIT 许可证

+ 147 - 0
backend/api/auth.py

@@ -0,0 +1,147 @@
+"""
+用户认证API路由
+"""
+from fastapi import APIRouter, Depends, HTTPException, status, Request
+from fastapi.security import OAuth2PasswordRequestForm
+from sqlalchemy.orm import Session
+from datetime import timedelta, datetime
+from pydantic import BaseModel, Field
+from typing import Optional
+import logging
+
+from backend.core.database import get_db
+from backend.core.models import User
+from backend.core.auth import (
+    authenticate_user, 
+    create_access_token, 
+    get_password_hash,
+    get_current_user
+)
+from backend.config import ACCESS_TOKEN_EXPIRE_MINUTES
+
+# 设置日志
+logger = logging.getLogger(__name__)
+
+router = APIRouter(tags=["auth"], prefix="/auth")
+
+class Token(BaseModel):
+    access_token: str
+    token_type: str
+    
+class UserCreate(BaseModel):
+    username: str
+    email: str
+    password: str
+    institution: Optional[str] = "江西财经大学"
+    position: Optional[str] = "学生"
+    
+class UserResponse(BaseModel):
+    id: int
+    username: str
+    email: str
+    institution: str
+    position: str
+    research_fields: list
+    membership_type: str
+    expiry_date: Optional[str] = None
+    credits: int
+    
+    class Config:
+        orm_mode = True
+
+@router.post("/token", response_model=Token)
+async def login_for_access_token(
+    form_data: OAuth2PasswordRequestForm = Depends(),
+    db: Session = Depends(get_db)
+):
+    """获取访问令牌(登录)"""
+    user = authenticate_user(db, form_data.username, form_data.password)
+    if not user:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="用户名或密码不正确",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+    
+    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+    access_token = create_access_token(
+        data={"sub": user.username}, 
+        expires_delta=access_token_expires
+    )
+    
+    return {"access_token": access_token, "token_type": "bearer"}
+
+@router.post("/register", response_model=UserResponse)
+async def register_user(user_create: UserCreate, db: Session = Depends(get_db)):
+    """注册新用户"""
+    try:
+        # 检查用户名是否已存在
+        db_user = db.query(User).filter(User.username == user_create.username).first()
+        if db_user:
+            # 更友好的错误消息
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="用户名已被使用,请尝试其他用户名"
+            )
+        
+        # 检查邮箱是否已存在
+        db_user = db.query(User).filter(User.email == user_create.email).first()
+        if db_user:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="邮箱已被注册,请使用其他邮箱"
+            )
+        
+        # 创建新用户
+        logger.info(f"创建新用户: {user_create.username}")
+        hashed_password = get_password_hash(user_create.password)
+        
+        # 使用datetime对象
+        expiry_date = datetime(2025, 12, 31)
+        
+        db_user = User(
+            username=user_create.username,
+            email=user_create.email,
+            hashed_password=hashed_password,
+            institution=user_create.institution or "江西财经大学",
+            position=user_create.position or "学生",
+            research_fields=["人工智能", "计算机视觉", "自然语言处理"],
+            membership_type="高级研究版",
+            expiry_date=expiry_date,
+            credits=5000
+        )
+        
+        db.add(db_user)
+        db.commit()
+        db.refresh(db_user)
+        logger.info(f"用户创建成功: {db_user.id}")
+        
+        # 明确构建返回数据
+        result = {
+            "id": db_user.id,
+            "username": db_user.username,
+            "email": db_user.email,
+            "institution": db_user.institution,
+            "position": db_user.position,
+            "research_fields": db_user.research_fields,
+            "membership_type": db_user.membership_type,
+            "expiry_date": db_user.expiry_date.strftime("%Y-%m-%d") if db_user.expiry_date else None,
+            "credits": db_user.credits
+        }
+        
+        return result
+    except HTTPException:
+        # 直接重新抛出HTTP异常
+        raise
+    except Exception as e:
+        # 记录错误但返回友好消息
+        logger.error(f"注册用户时发生错误: {str(e)}", exc_info=True)
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail="服务器处理请求时出错,请稍后再试"
+        )
+
+@router.get("/me", response_model=UserResponse)
+async def read_users_me(current_user: User = Depends(get_current_user)):
+    """获取当前用户信息"""
+    return current_user 

+ 234 - 0
backend/api/chat.py

@@ -0,0 +1,234 @@
+"""
+聊天API
+处理与AI助手的对话
+"""
+
+from fastapi import APIRouter, HTTPException, Depends, Request
+from pydantic import BaseModel
+from typing import List, Dict, Any, Optional
+import logging
+from sqlalchemy.orm import Session
+
+from backend.utils.api_client import LLMClient
+from backend.core.database import get_db
+from backend.core.models import User, ChatHistory, ChatMessage
+from backend.core.auth import get_current_user
+
+router = APIRouter(prefix="/chat", tags=["chat"])
+logger = logging.getLogger(__name__)
+
+class Message(BaseModel):
+    role: str
+    content: str
+
+class ChatRequest(BaseModel):
+    messages: List[Message]
+    temperature: Optional[float] = 0.7
+    max_tokens: Optional[int] = 1000
+
+class MessageRequest(BaseModel):
+    content: str
+    chat_id: Optional[int] = None  # 可选,如果为空则创建新对话
+
+class MessageResponse(BaseModel):
+    role: str
+    content: str
+    
+class ChatResponse(BaseModel):
+    chat_id: int
+    messages: List[MessageResponse]
+
+class ChatCompletionResponse(BaseModel):
+    message: Message
+    model: str
+
+@router.post("/send-message", response_model=ChatResponse)
+async def send_message(
+    request: MessageRequest,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """发送消息并获取AI响应"""
+    # 获取或创建聊天历史
+    if request.chat_id:
+        # 使用现有聊天
+        chat = db.query(ChatHistory)\
+            .filter(ChatHistory.id == request.chat_id, ChatHistory.user_id == current_user.id)\
+            .first()
+        
+        if not chat:
+            raise HTTPException(status_code=404, detail="聊天历史不存在或无权访问")
+    else:
+        # 创建新聊天
+        chat = ChatHistory(
+            user_id=current_user.id,
+            title="新对话"  # 可以从第一条消息中提取标题
+        )
+        db.add(chat)
+        db.commit()
+        db.refresh(chat)
+    
+    # 保存用户消息
+    user_message = ChatMessage(
+        chat_id=chat.id,
+        role="user",
+        content=request.content
+    )
+    db.add(user_message)
+    
+    # 调用AI获取回复
+    try:
+        # 获取当前聊天的所有消息历史
+        chat_history = db.query(ChatMessage)\
+            .filter(ChatMessage.chat_id == chat.id)\
+            .order_by(ChatMessage.timestamp)\
+            .all()
+            
+        # 构建消息列表
+        messages = [{"role": msg.role, "content": msg.content} for msg in chat_history]
+        # 添加用户当前消息
+        messages.append({"role": "user", "content": request.content})
+        
+        # 调用AI获取回复
+        llm_client = LLMClient()
+        response = await llm_client.chat_completion(
+            messages=messages,
+            temperature=0.7,
+            max_tokens=1000
+        )
+        
+        # 从响应中提取AI回复
+        ai_response = response["choices"][0]["message"]["content"]
+        
+        # 保存AI回复
+        ai_message = ChatMessage(
+            chat_id=chat.id,
+            role="assistant",
+            content=ai_response
+        )
+        db.add(ai_message)
+        
+        # 更新聊天标题(如果是第一条消息)
+        if len(chat.messages) <= 2:  # 只有当前这两条消息
+            # 从第一条消息生成标题
+            chat.title = request.content[:30] + ("..." if len(request.content) > 30 else "")
+        
+        db.commit()
+        
+        # 获取消息
+        messages = db.query(ChatMessage)\
+            .filter(ChatMessage.chat_id == chat.id)\
+            .order_by(ChatMessage.timestamp)\
+            .all()
+            
+        return {
+            "chat_id": chat.id,
+            "messages": [{"role": msg.role, "content": msg.content} for msg in messages]
+        }
+        
+    except Exception as e:
+        db.rollback()
+        logger.error(f"AI处理错误: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"AI处理错误: {str(e)}")
+
+@router.post("", response_model=ChatResponse)
+async def chat_completion_root(
+    request: ChatRequest,
+    db: Session = Depends(get_db)
+):
+    """
+    处理聊天请求,返回AI助手的回复
+    """
+    try:
+        llm_client = LLMClient()
+        
+        # 提取消息列表
+        messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
+        
+        # 创建一个临时聊天记录
+        chat = ChatHistory(
+            user_id=None,  # 临时聊天,不关联用户
+            title="临时对话"
+        )
+        db.add(chat)
+        db.commit()
+        db.refresh(chat)
+        
+        # 保存用户的历史消息
+        for msg in request.messages:
+            if msg.role == "user":  # 只保存用户消息
+                user_message = ChatMessage(
+                    chat_id=chat.id,
+                    role=msg.role,
+                    content=msg.content
+                )
+                db.add(user_message)
+        
+        # 调用LLM客户端的chat_completion方法
+        response = await llm_client.chat_completion(
+            messages=messages,
+            temperature=request.temperature,
+            max_tokens=request.max_tokens
+        )
+        
+        # 从响应中提取助手消息
+        assistant_message = response["choices"][0]["message"]
+        
+        # 保存AI响应
+        ai_message = ChatMessage(
+            chat_id=chat.id,
+            role=assistant_message["role"],
+            content=assistant_message["content"]
+        )
+        db.add(ai_message)
+        db.commit()
+        
+        # 获取所有消息
+        all_messages = db.query(ChatMessage)\
+            .filter(ChatMessage.chat_id == chat.id)\
+            .order_by(ChatMessage.timestamp)\
+            .all()
+        
+        # 返回符合ChatResponse格式的响应
+        return ChatResponse(
+            chat_id=chat.id,
+            messages=[
+                MessageResponse(role=msg.role, content=msg.content) 
+                for msg in all_messages
+            ]
+        )
+    except Exception as e:
+        logger.error(f"聊天API错误: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"处理聊天请求时出错: {str(e)}")
+
+@router.post("/chat", response_model=ChatCompletionResponse)
+async def chat_completion(request: ChatRequest):
+    """
+    处理聊天请求,返回AI助手的回复 (旧格式)
+    """
+    try:
+        llm_client = LLMClient()
+        
+        # 提取消息列表
+        messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
+        
+        # 调用LLM客户端的chat_completion方法
+        response = await llm_client.chat_completion(
+            messages=messages,
+            temperature=request.temperature,
+            max_tokens=request.max_tokens
+        )
+        
+        # 从响应中提取助手消息
+        assistant_message = response["choices"][0]["message"]
+        
+        return ChatCompletionResponse(
+            message=Message(
+                role=assistant_message["role"],
+                content=assistant_message["content"]
+            ),
+            model=response["model"]
+        )
+    except Exception as e:
+        logger.error(f"聊天API错误: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"处理聊天请求时出错: {str(e)}") 

+ 173 - 0
backend/api/chat_history.py

@@ -0,0 +1,173 @@
+"""
+聊天历史API路由
+"""
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy.orm import Session
+from pydantic import BaseModel
+from typing import List, Optional
+from datetime import datetime
+
+from backend.core.database import get_db
+from backend.core.models import User, ChatHistory, ChatMessage
+from backend.core.auth import get_current_user
+
+router = APIRouter(tags=["chat"], prefix="/chat-history")
+
+class MessageCreate(BaseModel):
+    role: str
+    content: str
+    
+class MessageResponse(BaseModel):
+    id: int
+    role: str
+    content: str
+    timestamp: datetime
+    
+    class Config:
+        orm_mode = True
+
+class ChatHistoryCreate(BaseModel):
+    title: str
+    messages: List[MessageCreate]
+    
+class ChatHistoryResponse(BaseModel):
+    id: int
+    title: str
+    created_at: datetime
+    updated_at: datetime
+    messages: List[MessageResponse]
+    
+    class Config:
+        orm_mode = True
+        
+class ChatHistorySummary(BaseModel):
+    id: int
+    title: str
+    created_at: datetime
+    updated_at: datetime
+    
+    class Config:
+        orm_mode = True
+
+@router.post("/", response_model=ChatHistoryResponse)
+async def create_chat_history(
+    chat_data: ChatHistoryCreate,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """创建新的聊天历史"""
+    # 创建聊天历史
+    chat = ChatHistory(
+        user_id=current_user.id,
+        title=chat_data.title
+    )
+    db.add(chat)
+    db.commit()
+    db.refresh(chat)
+    
+    # 添加消息
+    for msg_data in chat_data.messages:
+        message = ChatMessage(
+            chat_id=chat.id,
+            role=msg_data.role,
+            content=msg_data.content
+        )
+        db.add(message)
+    
+    db.commit()
+    db.refresh(chat)
+    
+    return chat
+
+@router.get("/", response_model=List[ChatHistorySummary])
+async def get_chat_histories(
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """获取用户的所有聊天历史摘要"""
+    chats = db.query(ChatHistory)\
+        .filter(ChatHistory.user_id == current_user.id)\
+        .order_by(ChatHistory.updated_at.desc())\
+        .all()
+    
+    return chats
+
+@router.get("/{chat_id}", response_model=ChatHistoryResponse)
+async def get_chat_history(
+    chat_id: int,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """获取特定聊天历史的详情"""
+    chat = db.query(ChatHistory)\
+        .filter(ChatHistory.id == chat_id, ChatHistory.user_id == current_user.id)\
+        .first()
+    
+    if not chat:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="聊天历史不存在或无权访问"
+        )
+    
+    return chat
+
+@router.post("/{chat_id}/messages", response_model=MessageResponse)
+async def add_message(
+    chat_id: int,
+    message: MessageCreate,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """向现有聊天添加新消息"""
+    # 检查聊天是否存在且属于当前用户
+    chat = db.query(ChatHistory)\
+        .filter(ChatHistory.id == chat_id, ChatHistory.user_id == current_user.id)\
+        .first()
+    
+    if not chat:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="聊天历史不存在或无权访问"
+        )
+    
+    # 创建新消息
+    db_message = ChatMessage(
+        chat_id=chat_id,
+        role=message.role,
+        content=message.content
+    )
+    
+    # 更新聊天的更新时间
+    chat.updated_at = datetime.utcnow()
+    
+    db.add(db_message)
+    db.commit()
+    db.refresh(db_message)
+    
+    return db_message
+
+@router.delete("/{chat_id}")
+async def delete_chat_history(
+    chat_id: int,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """删除聊天历史"""
+    chat = db.query(ChatHistory)\
+        .filter(ChatHistory.id == chat_id, ChatHistory.user_id == current_user.id)\
+        .first()
+    
+    if not chat:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="聊天历史不存在或无权访问"
+        )
+    
+    # 删除聊天相关的所有消息
+    db.query(ChatMessage).filter(ChatMessage.chat_id == chat_id).delete()
+    
+    # 删除聊天
+    db.delete(chat)
+    db.commit()
+    
+    return {"message": "聊天历史已删除"} 

+ 3 - 0
backend/api/init.py

@@ -0,0 +1,3 @@
+"""
+API路由包
+"""

+ 605 - 0
backend/api/research.py

@@ -0,0 +1,605 @@
+"""
+研究相关API路由
+"""
+
+from fastapi import APIRouter, HTTPException, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
+from pydantic import BaseModel, Field
+from typing import List, Dict, Any, Optional
+import logging
+import asyncio
+import json
+import time
+from pathlib import Path
+import uuid
+from fastapi.responses import StreamingResponse
+
+from backend.core.research import ResearchAgent
+from backend.core.clustering import PaperClusterer
+from backend.core.report import ReportGenerator
+from backend.config import MAX_SEARCH_RESULTS
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter(prefix="/research", tags=["research"])
+
+# 数据模型
+class ResearchRequest(BaseModel):
+    research_intent: str = Field(..., description="用户的研究意图")
+    max_results: Optional[int] = Field(None, description="已废弃,每个研究方向固定返回3-5篇论文")
+
+class KeywordsRequest(BaseModel):
+    research_intent: str = Field(..., description="用户的研究意图")
+
+class PaperSearchRequest(BaseModel):
+    keywords: List[str] = Field(..., description="检索关键词")
+    max_results: int = Field(MAX_SEARCH_RESULTS, description="最大检索结果数量")
+
+class ClusterRequest(BaseModel):
+    papers: List[Dict[str, Any]] = Field(..., description="要聚类的论文")
+    num_clusters: int = Field(0, description="聚类数量,0表示自动确定")
+
+class ReportRequest(BaseModel):
+    research_intent: str = Field(..., description="研究意图")
+    keywords: List[str] = Field(..., description="关键词")
+    papers: List[Dict[str, Any]] = Field(..., description="检索到的论文")
+    clusters: Optional[Dict[str, Any]] = Field(None, description="聚类结果")
+
+# 全局实例
+research_agent = ResearchAgent()
+paper_clusterer = PaperClusterer()
+report_generator = ReportGenerator()
+
+# 存储进度信息的全局字典
+task_progress = {}
+
+# 添加函数更新进度
+def update_task_progress(task_id: str, progress: int, status: str, message: str = ""):
+    """更新任务进度"""
+    task_progress[task_id] = {
+        "percentage": progress,
+        "status": status,
+        "message": message,
+        "updated_at": time.time()
+    }
+    
+    # 清理旧任务
+    current_time = time.time()
+    expired_tasks = [tid for tid, data in task_progress.items() 
+                    if current_time - data["updated_at"] > 3600]  # 1小时过期
+    for tid in expired_tasks:
+        del task_progress[tid]
+
+# 添加函数获取进度
+async def get_task_progress(task_id: str):
+    """获取任务进度"""
+    if task_id in task_progress:
+        return task_progress[task_id]
+    else:
+        return {
+            "percentage": 0,
+            "status": "unknown",
+            "message": "任务未找到或已过期",
+            "updated_at": time.time()
+        }
+
+# 路由定义
+@router.post("/process")
+async def process_research(request: ResearchRequest):
+    """处理研究请求"""
+    try:
+        # 移除 progress_callback 参数
+        research_agent = ResearchAgent()
+        
+        # 处理研究
+        result = await research_agent.process_research_intent(
+            request.research_intent,
+            max_results=None
+        )
+        
+        if result["status"] == "error":
+            raise HTTPException(
+                status_code=500, 
+                detail=result.get("error", "未知错误")
+            )
+            
+        return result
+        
+    except Exception as e:
+        logger.error(f"Error processing research: {str(e)}", exc_info=True)
+        raise HTTPException(
+            status_code=500,
+            detail=str(e)
+        )
+
+@router.post("/extract-keywords")
+async def extract_keywords(request: KeywordsRequest):
+    """从研究意图中提取关键词"""
+    try:
+        logger.info(f"Extracting keywords from: {request.research_intent}")
+        result = await research_agent.llm_client.extract_keywords(
+            research_topic=request.research_intent
+        )
+        return {"keywords": result}
+    except Exception as e:
+        logger.error(f"Error extracting keywords: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.post("/search-papers")
+async def search_papers(request: PaperSearchRequest):
+    """根据关键词检索论文"""
+    try:
+        logger.info(f"Searching papers with keywords: {request.keywords}")
+        papers = []
+        for keyword in request.keywords:
+            results = await research_agent.arxiv_client.search_papers(
+                query=keyword,
+                max_results=max(3, request.max_results // len(request.keywords))
+            )
+            papers.extend(results)
+        
+        # 去重
+        unique_papers = []
+        paper_ids = set()
+        for paper in papers:
+            if paper["id"] not in paper_ids:
+                unique_papers.append(paper)
+                paper_ids.add(paper["id"])
+        
+        return {"papers": unique_papers, "count": len(unique_papers)}
+    except Exception as e:
+        logger.error(f"Error searching papers: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.post("/cluster-papers")
+async def cluster_papers(request: ClusterRequest):
+    """对论文进行聚类分析"""
+    try:
+        logger.info(f"Clustering {len(request.papers)} papers")
+        result = paper_clusterer.cluster_papers(
+            papers=request.papers,
+            num_clusters=request.num_clusters
+        )
+        return result
+    except Exception as e:
+        logger.error(f"Error clustering papers: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=str(e))
+
+@router.post("/generate-report")
+async def generate_report(request: ReportRequest):
+    """生成研究报告"""
+    try:
+        logger.info(f"Generating report for: {request.research_intent}")
+        report = await report_generator.generate_report(
+            research_intent=request.research_intent,
+            keywords=request.keywords,
+            papers=request.papers,
+            clusters=request.clusters
+        )
+        return report
+    except Exception as e:
+        logger.error(f"Error generating report: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=str(e))
+
+# 添加WebSocket端点
+@router.websocket("/ws/research/{task_id}")
+async def research_progress(websocket: WebSocket, task_id: str):
+    await websocket.accept()
+    
+    try:
+        # 等待进度更新
+        while True:
+            # 查询任务进度
+            progress = await get_task_progress(task_id)
+            
+            # 发送给客户端
+            await websocket.send_json(progress)
+            
+            # 如果任务完成,退出循环
+            if progress["status"] in ["completed", "error"]:
+                break
+                
+            # 等待下一次更新
+            await asyncio.sleep(1)
+    except WebSocketDisconnect:
+        logger.info(f"Client disconnected from progress updates for task {task_id}")
+
+# 创建事件流响应
+@router.get("/process-stream")
+async def process_research_stream(research_intent: str):
+    """以事件流形式返回研究处理结果"""
+    
+    if not research_intent or research_intent.strip() == "":
+        raise HTTPException(status_code=400, detail="研究主题不能为空")
+    
+    async def event_generator():
+        try:
+            # 初始状态
+            yield f"data: {json.dumps({'status': 'starting', 'message': '正在开始研究流程...'})}\n\n"
+            await asyncio.sleep(0.1)
+            
+            research_agent = ResearchAgent()
+            
+            # 1. 提取关键词
+            yield f"data: {json.dumps({'status': 'extracting_keywords', 'message': '正在提取关键词...'})}\n\n"
+            keywords_data = await research_agent.extract_keywords_only(research_intent)
+            
+            # 发送关键词结果
+            keywords_result = {
+                'status': 'keywords_ready', 
+                'data': keywords_data,
+                'message': '关键词提取完成'
+            }
+            yield f"data: {json.dumps(keywords_result)}\n\n"
+            await asyncio.sleep(0.1)
+            
+            # 2. 生成研究方向
+            yield f"data: {json.dumps({'status': 'generating_directions', 'message': '正在生成研究方向...'})}\n\n"
+            directions_data = await research_agent.generate_directions_only(
+                keywords_data["english_keywords"], 
+                keywords_data["language"]
+            )
+            
+            # 发送研究方向结果
+            directions_result = {
+                'status': 'directions_ready', 
+                'data': directions_data,
+                'message': '研究方向生成完成'
+            }
+            yield f"data: {json.dumps(directions_result)}\n\n"
+            await asyncio.sleep(0.1)
+            
+            # 3. 处理每个研究方向
+            for i, direction in enumerate(directions_data["english_directions"]):
+                # 发送搜索状态
+                search_status = {
+                    'status': 'searching_papers', 
+                    'message': f'正在搜索方向 {i+1}/{len(directions_data["english_directions"])} 的相关论文...',
+                    'current_direction': i
+                }
+                yield f"data: {json.dumps(search_status)}\n\n"
+                
+                # 获取原始语言的方向
+                original_dir = directions_data["original_directions"][i] if i < len(directions_data["original_directions"]) else direction
+                
+                # 搜索论文
+                papers_data = await research_agent.search_papers_for_direction(direction)
+                
+                # 发送论文结果
+                papers_result = {
+                    'status': 'papers_ready', 
+                    'data': {
+                        'direction': direction,
+                        'original_direction': original_dir,
+                        'papers': papers_data,
+                        'direction_index': i
+                    },
+                    'message': f'方向 {i+1} 的论文搜索完成'
+                }
+                yield f"data: {json.dumps(papers_result)}\n\n"
+                await asyncio.sleep(0.1)
+                
+                # 如果有论文,生成报告
+                if papers_data:
+                    report_status = {
+                        'status': 'generating_report', 
+                        'message': f'正在为方向 {i+1} 生成研究报告...',
+                        'current_direction': i
+                    }
+                    yield f"data: {json.dumps(report_status)}\n\n"
+                    
+                    # 生成报告
+                    report_data = await research_agent.generate_report_for_direction(
+                        direction, 
+                        papers_data, 
+                        keywords_data["language"]
+                    )
+                    
+                    # 发送报告结果
+                    report_result = {
+                        'status': 'report_ready', 
+                        'data': {
+                            'direction': direction,
+                            'original_direction': original_dir,
+                            'report': report_data,
+                            'direction_index': i
+                        },
+                        'message': f'方向 {i+1} 的研究报告生成完成'
+                    }
+                    yield f"data: {json.dumps(report_result)}\n\n"
+                    await asyncio.sleep(0.1)
+            
+            # 全部完成
+            yield f"data: {json.dumps({'status': 'completed', 'message': '研究流程全部完成'})}\n\n"
+            
+        except Exception as e:
+            # 错误处理
+            logger.error(f"Error in streaming process: {str(e)}", exc_info=True)
+            error_result = {'status': 'error', 'message': f'处理过程中发生错误: {str(e)}'}
+            yield f"data: {json.dumps(error_result)}\n\n"
+    
+    # 返回流式响应
+    return StreamingResponse(
+        event_generator(), 
+        media_type="text/event-stream",
+        headers={
+            'Cache-Control': 'no-cache',
+            'Connection': 'keep-alive',
+            'X-Accel-Buffering': 'no'
+        }
+    )
+
+@router.get("/enhanced-process-stream")
+async def enhanced_process_stream(research_intent: str):
+    """以增强的事件流形式返回研究处理结果,按照研究主题逐一处理"""
+    
+    if not research_intent or research_intent.strip() == "":
+        raise HTTPException(status_code=400, detail="研究主题不能为空")
+    
+    async def event_generator():
+        try:
+            # 初始状态
+            yield f"data: {json.dumps({'status': 'starting', 'message': '正在开始研究流程...'})}\n\n"
+            await asyncio.sleep(0.1)
+            
+            research_agent = ResearchAgent()
+            
+            # 1. 提取基础关键词
+            yield f"data: {json.dumps({'status': 'extracting_base_keywords', 'message': '正在提取基础关键词...'})}\n\n"
+            base_keywords_data = await research_agent.extract_keywords_only(research_intent)
+            
+            # 发送基础关键词结果
+            base_keywords_result = {
+                'status': 'base_keywords_extracted', 
+                'data': {
+                    'language': base_keywords_data['language'],
+                    'keywords': base_keywords_data['english_keywords'],
+                    'translations': base_keywords_data['original_keywords'] if base_keywords_data['language'] != 'en' else None
+                },
+                'message': '基础关键词提取完成'
+            }
+            yield f"data: {json.dumps(base_keywords_result)}\n\n"
+            await asyncio.sleep(0.1)
+            
+            # 2. 生成研究方向
+            yield f"data: {json.dumps({'status': 'generating_research_topics', 'message': '正在基于关键词生成研究方向...'})}\n\n"
+            
+            # 确保方法存在
+            if not hasattr(research_agent, 'generate_enhanced_topics'):
+                # 临时实现一个简单版本作为后备
+                logger.warning("Method generate_enhanced_topics not found, using fallback implementation")
+                research_topics = []
+                for i, keyword in enumerate(base_keywords_data["english_keywords"][:3]):
+                    research_topics.append({
+                        "english_title": f"Research on {keyword}",
+                        "title": f"关于{keyword}的研究" if base_keywords_data["language"] != "en" else f"Research on {keyword}",
+                        "description": f"Investigating various aspects of {keyword} in the context of the research intent",
+                        "keywords": [keyword]
+                    })
+            else:
+                research_topics = await research_agent.generate_enhanced_topics(
+                    base_keywords_data["english_keywords"], 
+                    base_keywords_data["language"]
+                )
+            
+            # 发送研究方向结果
+            topics_result = {
+                'status': 'research_topics_generated', 
+                'data': {
+                    'language': base_keywords_data['language'],
+                    'research_topics': research_topics
+                },
+                'message': '研究方向生成完成'
+            }
+            yield f"data: {json.dumps(topics_result)}\n\n"
+            await asyncio.sleep(0.1)
+            
+            # 记录日志,验证进度
+            logger.info(f"Generated {len(research_topics)} research topics")
+            
+            # 3. 处理每个研究方向 - 防止后续方法未实现导致整个流程中断
+            for i, topic in enumerate(research_topics):
+                try:
+                    # 3.1 为这个方向生成专门的搜索关键词
+                    yield f"data: {json.dumps({'status': 'generating_search_keywords', 'message': f'正在为研究方向 {i+1}/{len(research_topics)} 生成搜索关键词...'})}\n\n"
+                    
+                    # 检查方法是否存在
+                    if hasattr(research_agent, 'generate_search_keywords_for_topic'):
+                        search_keywords = await research_agent.generate_search_keywords_for_topic(topic)
+                    else:
+                        # 后备方案:使用主题自带关键词
+                        search_keywords = topic.get('keywords', [])
+                        if not search_keywords:
+                            search_keywords = [topic['english_title']]
+                    
+                    # 确保搜索关键词不是错误消息
+                    if isinstance(search_keywords, list) and all(isinstance(kw, str) for kw in search_keywords):
+                        # 验证关键词是否合理
+                        if any(len(kw) > 3 for kw in search_keywords):
+                            # 关键词看起来合理
+                            pass
+                        else:
+                            # 关键词可能不合理,使用后备关键词
+                            search_keywords = [topic['english_title']] + topic.get('keywords', [])
+                    else:
+                        # 关键词格式不正确,使用后备关键词
+                        search_keywords = [topic['english_title']] + topic.get('keywords', [])
+                    
+                    # 发送搜索关键词
+                    keywords_result = {
+                        'status': 'search_keywords_generated', 
+                        'data': {
+                            'language': base_keywords_data['language'],
+                            'keywords': search_keywords,
+                            'topic_index': i,
+                            'topic': topic['english_title']
+                        },
+                        'message': f'研究方向 {i+1} 的搜索关键词生成完成'
+                    }
+                    yield f"data: {json.dumps(keywords_result)}\n\n"
+                    await asyncio.sleep(0.1)
+                    
+                    # 记录日志,验证进度
+                    logger.info(f"Generated search keywords for topic {i+1}: {search_keywords}")
+                except Exception as e:
+                    logger.error(f"Error generating search keywords for topic {i+1}: {str(e)}", exc_info=True)
+                    yield f"data: {{\"status\": \"warning\", \"message\": \"生成方向 {i+1} 搜索关键词失败: {str(e)}\"}}\n\n"
+                    # 使用更可靠的后备关键词
+                    search_keywords = [topic['english_title']]
+                    if 'keywords' in topic and isinstance(topic['keywords'], list) and len(topic['keywords']) > 0:
+                        search_keywords.extend(topic['keywords'][:2])
+                
+                # 3.2 搜索论文
+                try:
+                    yield f"data: {json.dumps({'status': 'searching_papers', 'message': f'正在搜索方向 {i+1}/{len(research_topics)} 的相关论文...', 'current_direction': i})}\n\n"
+                    
+                    # 检查方法是否存在
+                    if hasattr(research_agent, 'search_papers_with_keywords'):
+                        papers_data = await research_agent.search_papers_with_keywords(search_keywords)
+                    else:
+                        # 后备方案:使用现有方法
+                        papers_data = await research_agent.search_papers_for_direction(topic['english_title'])
+                    
+                    logger.info(f"Found {len(papers_data)} papers for topic {i+1}")
+                except Exception as e:
+                    logger.error(f"Error searching papers for topic {i+1}: {str(e)}", exc_info=True)
+                    yield f"data: {json.dumps({'status': 'warning', 'message': f'搜索方向 {i+1} 论文失败: {str(e)}'})}\n\n"
+                    # 空列表作为后备
+                    papers_data = []
+                
+                # 3.3 论文聚类 - 可选步骤
+                try:
+                    if papers_data and len(papers_data) >= 3:
+                        yield f"data: {json.dumps({'status': 'clustering_papers', 'message': f'正在对方向 {i+1} 的论文进行聚类分析...'})}\n\n"
+                        
+                        # 检查方法是否存在
+                        if hasattr(research_agent, 'cluster_papers_by_keywords'):
+                            clustered_papers = await research_agent.cluster_papers_by_keywords(
+                                papers_data, search_keywords, topic
+                            )
+                        else:
+                            # 后备方案:简单结构
+                            clustered_papers = {
+                                "papers": papers_data,
+                                "clusters": [{
+                                    "id": 0,
+                                    "name": topic["english_title"],
+                                    "keywords": search_keywords[:3] if search_keywords else []
+                                }]
+                            }
+                    else:
+                        # 论文太少,不聚类
+                        clustered_papers = {
+                            "papers": papers_data,
+                            "clusters": [{
+                                "id": 0,
+                                "name": topic["english_title"],
+                                "keywords": search_keywords[:3] if search_keywords else []
+                            }]
+                        }
+                except Exception as e:
+                    logger.error(f"Error clustering papers for topic {i+1}: {str(e)}", exc_info=True)
+                    # 后备方案
+                    clustered_papers = {
+                        "papers": papers_data,
+                        "clusters": [{
+                            "id": 0,
+                            "name": topic["english_title"],
+                            "keywords": search_keywords[:3] if search_keywords else []
+                        }]
+                    }
+                
+                # 3.4 发送论文结果
+                papers_result = {
+                    'status': 'papers_ready', 
+                    'data': {
+                        'direction': topic['english_title'],
+                        'original_direction': topic['title'],
+                        'search_keywords': search_keywords,
+                        'papers': clustered_papers['papers'],
+                        'cluster_info': clustered_papers.get('clusters', []),
+                        'direction_index': i
+                    },
+                    'message': f'方向 {i+1} 的论文搜索和聚类完成'
+                }
+                yield f"data: {json.dumps(papers_result)}\n\n"
+                await asyncio.sleep(0.1)
+                
+                # 3.5 生成报告 - 如果有论文
+                if papers_data:
+                    try:
+                        yield f"data: {{\"status\": \"generating_report\", \"message\": \"正在为方向 {i+1} 生成研究报告...\", \"current_direction\": {i}}}\n\n"
+                        await asyncio.sleep(0.1)
+                        
+                        # 使用asyncio.wait_for限制执行时间
+                        try:
+                            # 始终使用generate_enhanced_report函数
+                            report_data = await asyncio.wait_for(
+                                research_agent.generate_enhanced_report(
+                                    topic, clustered_papers, search_keywords, base_keywords_data["language"]
+                                ),
+                                timeout=300.0
+                            )
+                            
+                            # 发送报告结果
+                            report_result = {
+                                'status': 'report_ready', 
+                                'data': {
+                                    'direction': topic['english_title'],
+                                    'original_direction': topic['title'],
+                                    'search_keywords': search_keywords,
+                                    'report': report_data,
+                                    'direction_index': i
+                                },
+                                'message': f'方向 {i+1} 的研究报告生成完成'
+                            }
+                            yield f"data: {json.dumps(report_result)}\n\n"
+                            logger.info(f"Generated report for topic {i+1}")
+                        except asyncio.TimeoutError:
+                            # 特别处理超时情况
+                            logger.warning(f"Report generation timed out for topic {i+1}")
+                            # 使用更直接的后备机制
+                            fallback_report = {
+                                "english_content": f"# Report generation timed out\n\nWe apologize, but the report generation for this topic took too long and was automatically stopped. This topic has {len(papers_data)} related papers that you can review manually.",
+                                "translated_content": f"# 报告生成超时\n\n很抱歉,此主题的报告生成时间过长,已自动停止。该主题有{len(papers_data)}篇相关论文,您可以手动查看。"
+                            }
+                            
+                            fallback_result = {
+                                'status': 'report_ready', 
+                                'data': {
+                                    'direction': topic['english_title'],
+                                    'original_direction': topic['title'],
+                                    'search_keywords': search_keywords,
+                                    'report': fallback_report,
+                                    'direction_index': i,
+                                    'is_fallback': True
+                                },
+                                'message': f'方向 {i+1} 的报告生成超时,提供简化版本'
+                            }
+                            yield f"data: {json.dumps(fallback_result)}\n\n"
+                            await asyncio.sleep(0.1)
+                        
+                        # 添加完成消息,确保前端知道此主题处理完毕
+                        yield f"data: {{\"status\": \"direction_completed\", \"message\": \"方向 {i+1} 处理完成\", \"direction_index\": {i}}}\n\n"
+                        await asyncio.sleep(0.1)
+                    except Exception as e:
+                        logger.error(f"Error handling report generation for topic {i+1}: {str(e)}", exc_info=True)
+                        yield f"data: {{\"status\": \"warning\", \"message\": \"生成方向 {i+1} 研究报告处理失败: {str(e)}\"}}\n\n"
+                        await asyncio.sleep(0.1)
+                else:
+                    logger.warning(f"No papers found for topic {i+1}, skipping report generation")
+        except Exception as e:
+            # 错误处理
+            logger.error(f"Error in enhanced streaming process: {str(e)}", exc_info=True)
+            error_result = {'status': 'error', 'message': f'处理过程中发生错误: {str(e)}'}
+            yield f"data: {json.dumps(error_result)}\n\n"
+    
+    # 返回流式响应
+    return StreamingResponse(
+        event_generator(), 
+        media_type="text/event-stream",
+        headers={
+            'Cache-Control': 'no-cache',
+            'Connection': 'keep-alive',
+            'X-Accel-Buffering': 'no'
+        }
+    )

+ 119 - 0
backend/api/research_history.py

@@ -0,0 +1,119 @@
+"""
+研究历史API路由
+"""
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy.orm import Session
+from pydantic import BaseModel
+from typing import List, Dict, Any, Optional
+from datetime import datetime
+
+from backend.core.database import get_db
+from backend.core.models import User, ResearchHistory
+from backend.core.auth import get_current_user
+
+router = APIRouter(tags=["research"], prefix="/research-history")
+
+class ResearchHistoryCreate(BaseModel):
+    title: str
+    research_intent: str
+    keywords: List[str]
+    directions: List[Dict[str, Any]]
+    papers: List[Dict[str, Any]]
+    
+class ResearchHistoryResponse(BaseModel):
+    id: int
+    title: str
+    research_intent: str
+    keywords: List[str]
+    directions: List[Dict[str, Any]]
+    papers: List[Dict[str, Any]]
+    created_at: datetime
+    updated_at: datetime
+    
+    class Config:
+        orm_mode = True
+        
+class ResearchHistorySummary(BaseModel):
+    id: int
+    title: str
+    created_at: datetime
+    updated_at: datetime
+    
+    class Config:
+        orm_mode = True
+
+@router.post("/", response_model=ResearchHistoryResponse)
+async def create_research_history(
+    research_data: ResearchHistoryCreate,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """创建新的研究历史"""
+    research = ResearchHistory(
+        user_id=current_user.id,
+        title=research_data.title,
+        research_intent=research_data.research_intent,
+        keywords=research_data.keywords,
+        directions=research_data.directions,
+        papers=research_data.papers
+    )
+    
+    db.add(research)
+    db.commit()
+    db.refresh(research)
+    
+    return research
+
+@router.get("/", response_model=List[ResearchHistorySummary])
+async def get_research_histories(
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """获取用户的所有研究历史摘要"""
+    researches = db.query(ResearchHistory)\
+        .filter(ResearchHistory.user_id == current_user.id)\
+        .order_by(ResearchHistory.updated_at.desc())\
+        .all()
+    
+    return researches
+
+@router.get("/{research_id}", response_model=ResearchHistoryResponse)
+async def get_research_history(
+    research_id: int,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """获取特定研究历史的详情"""
+    research = db.query(ResearchHistory)\
+        .filter(ResearchHistory.id == research_id, ResearchHistory.user_id == current_user.id)\
+        .first()
+    
+    if not research:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="研究历史不存在或无权访问"
+        )
+    
+    return research
+
+@router.delete("/{research_id}")
+async def delete_research_history(
+    research_id: int,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """删除研究历史"""
+    research = db.query(ResearchHistory)\
+        .filter(ResearchHistory.id == research_id, ResearchHistory.user_id == current_user.id)\
+        .first()
+    
+    if not research:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND,
+            detail="研究历史不存在或无权访问"
+        )
+    
+    db.delete(research)
+    db.commit()
+    
+    return {"message": "研究历史已删除"} 

+ 110 - 0
backend/api/users.py

@@ -0,0 +1,110 @@
+"""
+用户资料API路由
+"""
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy.orm import Session
+from pydantic import BaseModel
+from typing import List, Optional
+from datetime import datetime
+
+from backend.core.database import get_db
+from backend.core.models import User
+from backend.core.auth import get_current_user
+
+router = APIRouter(tags=["users"], prefix="/users")
+
+class UserProfileUpdate(BaseModel):
+    username: Optional[str] = None
+    email: Optional[str] = None
+    institution: Optional[str] = None
+    position: Optional[str] = None
+    research_fields: Optional[List[str]] = None
+    
+class UserProfileResponse(BaseModel):
+    id: int
+    username: str
+    email: str
+    institution: str
+    position: str
+    research_fields: list
+    membership_type: str
+    expiry_date: Optional[str] = None
+    credits: int
+    
+    class Config:
+        orm_mode = True
+
+@router.get("/profile", response_model=UserProfileResponse)
+async def get_user_profile(current_user: User = Depends(get_current_user)):
+    """获取用户资料"""
+    # 规范化日期格式,确保一致性
+    result = {
+        "id": current_user.id,
+        "username": current_user.username,
+        "email": current_user.email,
+        "institution": current_user.institution,
+        "position": current_user.position,
+        "research_fields": current_user.research_fields,
+        "membership_type": current_user.membership_type,
+        "expiry_date": current_user.expiry_date.strftime("%Y-%m-%d") if current_user.expiry_date else None,
+        "credits": current_user.credits
+    }
+    return result
+
+@router.put("/profile", response_model=UserProfileResponse)
+async def update_user_profile(
+    profile: UserProfileUpdate,
+    current_user: User = Depends(get_current_user),
+    db: Session = Depends(get_db)
+):
+    """更新用户资料"""
+    # 获取用户
+    user = db.query(User).filter(User.id == current_user.id).first()
+    
+    # 更新用户资料
+    if profile.username is not None:
+        # 检查用户名是否已存在
+        existing_user = db.query(User).filter(User.username == profile.username).first()
+        if existing_user and existing_user.id != current_user.id:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="用户名已被使用"
+            )
+        user.username = profile.username
+        
+    if profile.email is not None:
+        # 检查邮箱是否已存在
+        existing_user = db.query(User).filter(User.email == profile.email).first()
+        if existing_user and existing_user.id != current_user.id:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail="邮箱已被使用"
+            )
+        user.email = profile.email
+        
+    if profile.institution is not None:
+        user.institution = profile.institution
+        
+    if profile.position is not None:
+        user.position = profile.position
+        
+    if profile.research_fields is not None:
+        user.research_fields = profile.research_fields
+    
+    # 保存更新
+    user.updated_at = datetime.utcnow()
+    db.commit()
+    db.refresh(user)
+    
+    # 在返回前将datetime转换为字符串
+    return {
+        "id": user.id,
+        "username": user.username,
+        "email": user.email,
+        "institution": user.institution,
+        "position": user.position,
+        "research_fields": user.research_fields,
+        "membership_type": user.membership_type,
+        "expiry_date": user.expiry_date.strftime("%Y-%m-%d") if user.expiry_date else None,
+        "credits": user.credits
+    } 

+ 49 - 0
backend/config.py

@@ -0,0 +1,49 @@
+"""
+文献启明星配置文件
+管理API密钥、模型参数和其他配置选项
+"""
+
+import os
+from pathlib import Path
+from dotenv import load_dotenv
+
+# 加载环境变量
+load_dotenv()
+
+# 基础路径
+BASE_DIR = Path(__file__).resolve().parent
+DATA_DIR = BASE_DIR / "data"
+CACHE_DIR = DATA_DIR / "cache"
+
+# 确保目录存在
+os.makedirs(CACHE_DIR, exist_ok=True)
+
+# API配置
+DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
+DEEPSEEK_API_URL = os.getenv("DEEPSEEK_API_URL", "https://api.deepseek.com/v1")
+DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-chat")
+
+# 检索配置
+MAX_SEARCH_RESULTS = int(os.getenv("MAX_SEARCH_RESULTS", "15"))
+ARXIV_SEARCH_TIMEOUT = int(os.getenv("ARXIV_SEARCH_TIMEOUT", "30"))
+
+# 聚类配置
+DEFAULT_EMBEDDING_MODEL = os.getenv("DEFAULT_EMBEDDING_MODEL", "all-MiniLM-L6-v2")
+DEFAULT_NUM_CLUSTERS = int(os.getenv("DEFAULT_NUM_CLUSTERS", "3"))
+MIN_PAPERS_FOR_CLUSTERING = int(os.getenv("MIN_PAPERS_FOR_CLUSTERING", "5"))
+
+# 报告生成配置
+REPORT_MAX_LENGTH = int(os.getenv("REPORT_MAX_LENGTH", "1500"))
+REPORT_TEMPERATURE = float(os.getenv("REPORT_TEMPERATURE", "0.3"))
+
+# CORS设置 - 允许前端访问API
+CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000").split(",")
+
+# 缓存设置
+ENABLE_CACHE = os.getenv("ENABLE_CACHE", "True").lower() == "true"
+CACHE_EXPIRY = int(os.getenv("CACHE_EXPIRY", "86400"))  # 默认24小时
+
+# 用户认证配置
+SECRET_KEY = os.getenv("SECRET_KEY", "your-very-secret-key-change-this-in-production")
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "1440"))  # 24小时

+ 76 - 0
backend/core/auth.py

@@ -0,0 +1,76 @@
+"""
+用户认证功能
+"""
+from datetime import datetime, timedelta
+from typing import Optional
+from jose import JWTError, jwt
+from passlib.context import CryptContext
+from fastapi import Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer
+from sqlalchemy.orm import Session
+
+from backend.core.database import get_db
+from backend.core.models import User
+from backend.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
+
+# 密码上下文
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+# OAuth2 密码Bearer
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
+
+def verify_password(plain_password, hashed_password):
+    """验证密码"""
+    return pwd_context.verify(plain_password, hashed_password)
+
+def get_password_hash(password):
+    """获取密码哈希"""
+    return pwd_context.hash(password)
+
+def get_user(db: Session, username: str):
+    """通过用户名获取用户"""
+    return db.query(User).filter(User.username == username).first()
+
+def authenticate_user(db: Session, username: str, password: str):
+    """认证用户"""
+    user = get_user(db, username)
+    if not user or not verify_password(password, user.hashed_password):
+        return False
+    return user
+
+def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
+    """创建访问令牌"""
+    to_encode = data.copy()
+    
+    if expires_delta:
+        expire = datetime.utcnow() + expires_delta
+    else:
+        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+        
+    to_encode.update({"exp": expire})
+    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+    return encoded_jwt
+
+def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
+    """获取当前用户并处理异常情况"""
+    credentials_exception = HTTPException(
+        status_code=status.HTTP_401_UNAUTHORIZED,
+        detail="无效的认证凭据",
+        headers={"WWW-Authenticate": "Bearer"},
+    )
+    
+    try:
+        # 解析token
+        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+        username: str = payload.get("sub")
+        if username is None:
+            raise credentials_exception
+    except JWTError:
+        raise credentials_exception
+        
+    # 查找用户
+    user = db.query(User).filter(User.username == username).first()
+    if user is None:
+        raise credentials_exception
+        
+    return user 

+ 137 - 0
backend/core/clustering.py

@@ -0,0 +1,137 @@
+"""
+文献聚类模块
+对文献进行向量化和聚类分析
+"""
+
+from typing import Dict, List, Any, Optional
+import numpy as np
+from sklearn.cluster import KMeans
+from sklearn.manifold import TSNE
+import logging
+from sentence_transformers import SentenceTransformer
+
+from backend.config import DEFAULT_EMBEDDING_MODEL, DEFAULT_NUM_CLUSTERS
+
+logger = logging.getLogger(__name__)
+
+class PaperClusterer:
+    """文献聚类引擎"""
+    
+    def __init__(self, embedding_model: str = DEFAULT_EMBEDDING_MODEL):
+        """
+        初始化聚类引擎
+        
+        Args:
+            embedding_model: 使用的嵌入模型名称
+        """
+        self.model_name = embedding_model
+        self._model = None  # 延迟加载
+    
+    @property
+    def model(self):
+        """延迟加载嵌入模型"""
+        if self._model is None:
+            logger.info(f"Loading embedding model: {self.model_name}")
+            try:
+                self._model = SentenceTransformer(self.model_name)
+            except Exception as e:
+                logger.error(f"Failed to load embedding model: {str(e)}")
+                raise
+        return self._model
+    
+    def _extract_texts(self, papers: List[Dict[str, Any]]) -> List[str]:
+        """从论文中提取文本用于嵌入"""
+        texts = []
+        for paper in papers:
+            # 合并标题和摘要以获得更好的嵌入
+            title = paper.get("title", "")
+            summary = paper.get("summary", "")
+            text = f"{title} {summary}"
+            texts.append(text)
+        return texts
+    
+    def cluster_papers(
+        self, 
+        papers: List[Dict[str, Any]],
+        num_clusters: int = DEFAULT_NUM_CLUSTERS
+    ) -> Dict[str, Any]:
+        """
+        对论文进行聚类分析
+        
+        Args:
+            papers: 论文列表
+            num_clusters: 聚类数量,0表示自动确定
+            
+        Returns:
+            聚类结果字典
+        """
+        if len(papers) < 3:
+            logger.warning("Too few papers for clustering")
+            return {
+                "clusters": [],
+                "cluster_info": {},
+                "visualization_data": None,
+                "error": "Too few papers for clustering"
+            }
+        
+        try:
+            # 1. 提取文本
+            texts = self._extract_texts(papers)
+            
+            # 2. 计算嵌入
+            logger.info(f"Computing embeddings for {len(texts)} papers")
+            embeddings = self.model.encode(texts, show_progress_bar=True)
+            
+            # 3. 确定聚类数量(如果未指定)
+            if num_clusters <= 0:
+                # 使用论文数量的平方根作为默认聚类数
+                num_clusters = min(max(2, int(np.sqrt(len(papers)))), 8)
+            
+            # 4. 聚类分析
+            logger.info(f"Clustering into {num_clusters} clusters")
+            kmeans = KMeans(n_clusters=num_clusters, random_state=42)
+            cluster_labels = kmeans.fit_predict(embeddings)
+            
+            # 5. 可视化数据准备(使用t-SNE降维)
+            logger.info("Generating 2D visualization data with t-SNE")
+            tsne = TSNE(n_components=2, random_state=42)
+            vis_data = tsne.fit_transform(embeddings)
+            
+            # 6. 整理聚类结果
+            clusters = [[] for _ in range(num_clusters)]
+            for i, (paper, label) in enumerate(zip(papers, cluster_labels)):
+                paper_with_cluster = paper.copy()
+                paper_with_cluster["cluster"] = int(label)
+                paper_with_cluster["vis_x"] = float(vis_data[i][0])
+                paper_with_cluster["vis_y"] = float(vis_data[i][1])
+                clusters[label].append(paper_with_cluster)
+            
+            # 7. 提取聚类信息
+            cluster_info = {}
+            for i in range(num_clusters):
+                cluster_info[i] = {
+                    "size": len(clusters[i]),
+                    "papers": clusters[i]
+                }
+            
+            # 8. 构造返回结果
+            result = {
+                "clusters": clusters,
+                "cluster_info": cluster_info,
+                "visualization_data": {
+                    "x": vis_data[:, 0].tolist(),
+                    "y": vis_data[:, 1].tolist(),
+                    "labels": cluster_labels.tolist()
+                }
+            }
+            
+            return result
+            
+        except Exception as e:
+            logger.error(f"Clustering error: {str(e)}", exc_info=True)
+            return {
+                "clusters": [],
+                "cluster_info": {},
+                "visualization_data": None,
+                "error": str(e)
+            }

+ 35 - 0
backend/core/database.py

@@ -0,0 +1,35 @@
+"""
+数据库连接和会话管理
+"""
+from sqlalchemy import create_engine
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+import os
+from pathlib import Path
+
+from backend.config import DATA_DIR
+
+# 确保数据目录存在
+os.makedirs(DATA_DIR, exist_ok=True)
+
+# 数据库URL
+SQLALCHEMY_DATABASE_URL = f"sqlite:///{DATA_DIR}/lightstar.db"
+
+# 创建引擎
+engine = create_engine(
+    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
+)
+
+# 创建会话
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+# 基础模型类
+Base = declarative_base()
+
+# 依赖函数,获取数据库会话
+def get_db():
+    db = SessionLocal()
+    try:
+        yield db
+    finally:
+        db.close() 

+ 3 - 0
backend/core/init.py

@@ -0,0 +1,3 @@
+"""
+核心业务逻辑包
+"""

+ 81 - 0
backend/core/models.py

@@ -0,0 +1,81 @@
+"""
+数据库模型定义
+"""
+from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, JSON
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import relationship
+from datetime import datetime
+
+Base = declarative_base()
+
+class User(Base):
+    """用户模型"""
+    __tablename__ = "users"
+    
+    id = Column(Integer, primary_key=True, index=True)
+    username = Column(String(50), unique=True, index=True)
+    email = Column(String(100), unique=True, index=True)
+    hashed_password = Column(String(100))
+    created_at = Column(DateTime, default=datetime.utcnow)
+    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+    
+    # 用户个人资料信息
+    avatar = Column(String(200), nullable=True)
+    institution = Column(String(100), default="江西财经大学")
+    position = Column(String(50), default="学生")
+    research_fields = Column(JSON, default=list)
+    
+    # 会员信息
+    membership_type = Column(String(50), default="高级研究版")
+    expiry_date = Column(DateTime, nullable=True)
+    credits = Column(Integer, default=5000)
+    
+    # 关系
+    chats = relationship("ChatHistory", back_populates="user")
+    researches = relationship("ResearchHistory", back_populates="user")
+
+class ChatHistory(Base):
+    """聊天历史记录"""
+    __tablename__ = "chat_history"
+    
+    id = Column(Integer, primary_key=True, index=True)
+    user_id = Column(Integer, ForeignKey("users.id"))
+    title = Column(String(200), default="新对话")
+    created_at = Column(DateTime, default=datetime.utcnow)
+    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+    
+    # 关系
+    user = relationship("User", back_populates="chats")
+    messages = relationship("ChatMessage", back_populates="chat")
+
+class ChatMessage(Base):
+    """聊天消息"""
+    __tablename__ = "chat_messages"
+    
+    id = Column(Integer, primary_key=True, index=True)
+    chat_id = Column(Integer, ForeignKey("chat_history.id"))
+    role = Column(String(20))  # user, assistant, system
+    content = Column(Text)
+    timestamp = Column(DateTime, default=datetime.utcnow)
+    
+    # 关系
+    chat = relationship("ChatHistory", back_populates="messages")
+
+class ResearchHistory(Base):
+    """研究历史记录"""
+    __tablename__ = "research_history"
+    
+    id = Column(Integer, primary_key=True, index=True)
+    user_id = Column(Integer, ForeignKey("users.id"))
+    title = Column(String(200))
+    research_intent = Column(Text)
+    created_at = Column(DateTime, default=datetime.utcnow)
+    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+    
+    # 存储研究结果
+    keywords = Column(JSON, default=list)
+    directions = Column(JSON, default=list)
+    papers = Column(JSON, default=list)
+    
+    # 关系
+    user = relationship("User", back_populates="researches") 

+ 114 - 0
backend/core/report.py

@@ -0,0 +1,114 @@
+"""
+报告生成模块
+根据研究结果生成结构化报告
+"""
+
+from typing import Dict, List, Any, Optional
+import logging
+from backend.utils.api_client import LLMClient
+from backend.config import REPORT_MAX_LENGTH, REPORT_TEMPERATURE
+
+logger = logging.getLogger(__name__)
+
+class ReportGenerator:
+    """研究报告生成器"""
+    
+    def __init__(self):
+        self.llm_client = LLMClient()
+    
+    async def generate_report(
+        self, 
+        research_intent: str,
+        keywords: List[str],
+        papers: List[Dict[str, Any]],
+        clusters: Optional[Dict[str, Any]] = None,
+        max_length: int = REPORT_MAX_LENGTH,
+        temperature: float = REPORT_TEMPERATURE
+    ) -> Dict[str, Any]:
+        """
+        生成研究报告
+        
+        Args:
+            research_intent: 用户的研究意图
+            keywords: 提取的关键词
+            papers: 检索到的论文
+            clusters: 聚类结果
+            max_length: 报告最大长度
+            temperature: 生成参数
+            
+        Returns:
+            包含报告内容的字典
+        """
+        try:
+            # 1. 准备论文信息
+            paper_info = []
+            for i, paper in enumerate(papers[:10]):  # 只使用前10篇
+                paper_info.append(
+                    f"{i+1}. {paper['title']} ({', '.join(paper['authors'][:3])})"
+                    f"\n摘要: {paper['summary'][:300]}..."
+                )
+            
+            paper_text = "\n\n".join(paper_info)
+            
+            # 2. 准备聚类信息
+            cluster_text = ""
+            if clusters and "cluster_info" in clusters:
+                for cluster_id, info in clusters["cluster_info"].items():
+                    papers_in_cluster = info["papers"]
+                    if papers_in_cluster:
+                        titles = [p["title"] for p in papers_in_cluster[:3]]
+                        cluster_text += f"聚类 {cluster_id+1} (包含 {len(papers_in_cluster)} 篇论文):\n"
+                        cluster_text += "代表性论文: " + "; ".join(titles) + "\n\n"
+            
+            # 3. 构造提示
+            cluster_section = f"## 文献聚类\n{cluster_text}" if cluster_text else ""
+            
+            prompt = f"""
+            作为一位科研助手,请基于以下信息为研究主题生成一份结构化的调研报告。
+            
+            ## 研究主题
+            {research_intent}
+            
+            ## 关键词
+            {', '.join(keywords)}
+            
+            ## 检索到的文献
+            {paper_text}
+            
+            {cluster_section}
+            
+            请生成一份完整的调研报告,包括以下部分:
+            1. 研究背景与意义(简要介绍该领域的重要性和研究现状)
+            2. 研究方向分析(基于文献聚类识别主要研究方向)
+            3. 关键技术与方法(总结该领域的主要技术和方法论)
+            4. 代表性研究工作(列举2-3项代表性研究及其贡献)
+            5. 未来研究展望(分析领域挑战与未来可能的突破点)
+            
+            报告应当学术严谨、结构清晰、信息准确。长度控制在1000-1500字。
+            """
+            
+            # 4. 生成报告
+            logger.info("Generating research report")
+            report_content = await self.llm_client.generate_text(
+                prompt=prompt,
+                temperature=temperature,
+                max_tokens=max_length
+            )
+            
+            # 5. 构造结果
+            report = {
+                "research_intent": research_intent,
+                "content": report_content,
+                "keywords": keywords,
+                "paper_count": len(papers)
+            }
+            
+            return report
+            
+        except Exception as e:
+            logger.error(f"Report generation error: {str(e)}", exc_info=True)
+            return {
+                "research_intent": research_intent,
+                "content": f"报告生成失败: {str(e)}",
+                "error": str(e)
+            }

+ 852 - 0
backend/core/research.py

@@ -0,0 +1,852 @@
+"""
+研究流程管理模块
+整合关键词提取、文献检索和聚类分析
+"""
+
+import asyncio
+from typing import Dict, List, Any, Optional
+import logging
+import time
+import json
+from pathlib import Path
+import uuid
+import contextlib
+
+from backend.utils.api_client import LLMClient, ArxivClient
+from backend.utils.keywords import extract_keywords, expand_search_queries
+from backend.config import (
+    MAX_SEARCH_RESULTS,
+    CACHE_DIR,
+    ENABLE_CACHE
+)
+
+logger = logging.getLogger(__name__)
+
+class ResearchAgent:
+    """优化后的研究智能体,支持任务拆分和异步处理"""
+    
+    def __init__(self):
+        self.llm_client = LLMClient()
+        self.arxiv_client = ArxivClient()
+    
+    async def process_research_intent(
+        self, 
+        research_intent: str,
+        max_results: int = None
+    ) -> Dict[str, Any]:
+        """
+        处理研究意图,但使用任务拆分方式
+        """
+        start_time = time.time()
+        
+        # 初始化结果字典
+        result = {
+            "research_intent": research_intent,
+            "timestamp": time.time(),
+            "language": "auto",
+            "english_keywords": [],
+            "original_keywords": [],
+            "english_directions": [],
+            "original_directions": [],
+            "papers_by_direction": [],
+            "direction_reports": [],
+            "status": "processing",
+            "progress": 0,  # 添加进度跟踪
+            "task_id": str(uuid.uuid4())  # 添加唯一任务ID
+        }
+        
+        try:
+            # 1. 提取关键词(保持不变)
+            keywords_data = await self.llm_client.extract_keywords(research_intent, "auto")
+            
+            result["language"] = keywords_data["language"]
+            result["english_keywords"] = keywords_data["english_keywords"]
+            result["original_keywords"] = keywords_data["original_keywords"]
+            result["progress"] = 20
+            
+            # 2. 生成研究方向(保持不变)
+            directions_data = await self.llm_client.generate_research_directions(
+                result["english_keywords"], 
+                result["language"]
+            )
+            
+            result["english_directions"] = directions_data["english_directions"]
+            result["original_directions"] = directions_data["original_directions"]
+            result["progress"] = 30
+            
+            # 3. 并行处理每个研究方向
+            async def process_directions():
+                tasks = []
+                for i, direction in enumerate(result["english_directions"]):
+                    original_dir = result["original_directions"][i] if i < len(result["original_directions"]) else direction
+                    task = self._process_single_direction(direction, original_dir, result["language"])
+                    tasks.append(task)
+                
+                # 并行执行所有方向的处理
+                direction_results = await asyncio.gather(*tasks, return_exceptions=True)
+                
+                papers_by_direction = []
+                reports = []
+                
+                for i, res in enumerate(direction_results):
+                    if not isinstance(res, Exception):
+                        papers_by_direction.append(res["papers_data"])
+                        if res["report"]:
+                            reports.append(res["report"])
+                    else:
+                        # 处理异常情况
+                        logger.error(f"Error processing direction {i}: {str(res)}")
+                        papers_by_direction.append({
+                            "direction": result["english_directions"][i],
+                            "original_direction": result["original_directions"][i] if i < len(result["original_directions"]) else result["english_directions"][i],
+                            "papers": []
+                        })
+                
+                return papers_by_direction, reports
+            
+            # 执行并行处理
+            papers_by_direction, reports = await process_directions()
+            result["papers_by_direction"] = papers_by_direction
+            result["direction_reports"] = reports
+            
+            result["status"] = "completed"
+            result["progress"] = 100
+            result["processing_time"] = time.time() - start_time
+            
+            # 缓存结果
+            if ENABLE_CACHE:
+                self._cache_result(result)
+            
+            return result
+            
+        except Exception as e:
+            logger.error(f"Error in research process: {str(e)}", exc_info=True)
+            result["status"] = "error"
+            result["error"] = str(e)
+            return result
+    
+    async def _process_single_direction(
+        self,
+        direction: str,
+        original_direction: str,
+        language: str
+    ) -> Dict[str, Any]:
+        """处理单个研究方向的独立任务"""
+        try:
+            # 1. 为该方向生成搜索查询
+            query = await self._generate_search_query(direction)
+            logger.info(f"Searching papers with query: {query}")
+            
+            # 2. 搜索论文
+            papers = await self._search_papers_with_fallback(
+                query=query,
+                max_results=4
+            )
+            
+            # 3. 构建论文数据
+            papers_data = {
+                "direction": direction,
+                "original_direction": original_direction,
+                "papers": papers
+            }
+            
+            # 4. 仅当有论文时生成报告
+            report = None
+            if papers:
+                try:
+                    report = await self._generate_direction_report(
+                        direction,
+                        papers,
+                        language
+                    )
+                    
+                    if report:
+                        report = {
+                            "direction": direction,
+                            "original_direction": original_direction,
+                            "report": report
+                        }
+                except Exception as e:
+                    logger.error(f"Report generation error for {direction}: {str(e)}")
+            
+            return {
+                "papers_data": papers_data,
+                "report": report
+            }
+            
+        except Exception as e:
+            logger.error(f"Error processing direction '{direction}': {str(e)}")
+            raise e
+    
+    async def _generate_search_query(self, research_direction: str) -> str:
+        """为研究方向生成有效的文献检索式"""
+        prompt = f"""
+        将以下研究方向转换为简洁有效的文献检索式。
+        
+        研究方向: {research_direction}
+        
+        生成检索式时遵循以下规则:
+        1. 使用2-3个核心术语,用布尔运算符(AND, OR)连接
+        2. 结构简单,优先使用引号包围关键短语
+        3. 避免过度细化导致无搜索结果
+        4. 确保检索式能找到3-5篇相关论文
+        
+        只返回检索式字符串,不包含其他说明。
+        """
+        
+        query = await self.llm_client.generate_text(prompt, temperature=0.3)
+        
+        # 简化复杂查询,确保能找到结果
+        simplified_query = self._simplify_query_if_needed(query.strip())
+        return simplified_query
+    
+    def _simplify_query_if_needed(self, query: str) -> str:
+        """如果查询过于复杂就简化它"""
+        # 计算AND和OR运算符的数量
+        and_count = query.upper().count(" AND ")
+        or_count = query.upper().count(" OR ")
+        
+        # 如果太复杂,简化查询
+        if and_count > 2:
+            # 只保留前两个AND条件
+            parts = query.split(" AND ", 2)
+            return " AND ".join(parts[:2])
+        
+        return query
+    
+    async def _generate_direction_report(
+        self, 
+        direction: str, 
+        papers: List[Dict[str, Any]],
+        target_language: str = "en"
+    ) -> Dict[str, Any]:
+        """优化后的研究报告生成方法"""
+        # 如果没有论文,返回空报告
+        if not papers:
+            return {
+                "english_content": "No papers found for this research direction.",
+                "translated_content": "未找到该研究方向的相关论文。"
+            }
+        
+        # 精简论文信息
+        papers_text = "\n\n".join([
+            f"Title: {paper['title']}\nSummary: {paper['summary'][:300]}..."  # 限制摘要长度
+            for paper in papers[:3]
+        ])
+        
+        # 定义报告结构
+        sections = [
+            "Overview (100 words)",
+            "Key Findings (150 words)",
+            "Future Directions (100 words)"
+        ]
+        
+        english_content = f"# Research Report: {direction}\n\n"
+        
+        # 为每个部分单独生成内容
+        for section in sections:
+            section_name = section.split(" (")[0]
+            
+            section_prompt = f"""
+            Write the {section} section for a research report on:
+            {direction}
+            
+            Based on these papers:
+            {papers_text}
+            
+            Be concise, focused and specific.
+            """
+            
+            try:
+                section_content = await self.llm_client.generate_text(
+                    section_prompt,
+                    temperature=0.4,
+                    max_tokens=300
+                )
+                
+                english_content += f"## {section_name}\n\n{section_content}\n\n"
+            except Exception as e:
+                english_content += f"## {section_name}\n\n[Error generating this section: {str(e)}]\n\n"
+        
+        # 翻译内容
+        translated_content = english_content
+        if target_language != "en":
+            try:
+                translated_content = await self.llm_client.translate_text(
+                    english_content, "en", target_language
+                )
+            except Exception as e:
+                logger.error(f"Translation error: {str(e)}")
+        
+        return {
+            "english_content": english_content,
+            "translated_content": translated_content
+        }
+    
+    def _cache_result(self, result: Dict[str, Any]) -> None:
+        """缓存研究结果"""
+        try:
+            cache_file = Path(CACHE_DIR) / f"research_{int(time.time())}.json"
+            with open(cache_file, "w", encoding="utf-8") as f:
+                json.dump(result, f, ensure_ascii=False, indent=2)
+            logger.info(f"Cached result to {cache_file}")
+        except Exception as e:
+            logger.error(f"Failed to cache result: {str(e)}")
+
+    async def _search_papers_with_fallback(self, query: str, max_results: int = 4) -> List[Dict[str, Any]]:
+        """文献检索策略,确保每个研究方向有3-5篇论文"""
+        # Validate query before proceeding
+        if not query or "Error generating text" in query:
+            logger.error(f"Invalid search query: {query}")
+            return []  # Return empty list instead of attempting to search with error string
+        
+        # 第一次尝试:使用完整检索式
+        logger.info(f"使用检索式搜索论文: {query}")
+        papers = await self.arxiv_client.search_papers(query=query, max_results=max_results)
+        
+        # 如果结果不足,尝试简化检索式
+        if len(papers) < 3:
+            simplified_query = self._get_simplified_query(query)
+            if simplified_query != query:
+                logger.info(f"结果不足,使用简化检索式: {simplified_query}")
+                more_papers = await self.arxiv_client.search_papers(
+                    query=simplified_query, 
+                    max_results=max_results
+                )
+                
+                # 添加不重复的论文
+                existing_ids = {p["id"] for p in papers}
+                for paper in more_papers:
+                    if paper["id"] not in existing_ids:
+                        papers.append(paper)
+                        if len(papers) >= 5:  # 最多5篇
+                            break
+        
+        # 如果仍不足3篇,添加示例论文
+        if len(papers) < 3:
+            logger.info(f"无法获取足够论文,添加示例论文")
+            clean_query = query.replace('"', '')
+            example_papers = [
+                {
+                    "id": f"example_{i}",
+                    "title": f"相关研究: {clean_query}",
+                    "authors": ["研究者 A", "研究者 B"],
+                    "summary": f"这是一篇关于{clean_query}的研究论文。由于搜索结果有限,系统生成了此示例条目。",
+                    "published": "2023-01-01T00:00:00",
+                    "updated": "2023-01-01T00:00:00",
+                    "link": "#",
+                    "source": "example"
+                }
+                for i in range(1, 4 - len(papers))
+            ]
+            papers.extend(example_papers)
+        
+        # 确保每个方向返回3-5篇论文
+        return papers[:5]
+
+    def _get_simplified_query(self, query: str) -> str:
+        """获取简化的检索式"""
+        # 提取引号中的关键术语
+        import re
+        quoted_terms = re.findall(r'"([^"]*)"', query)
+        
+        if quoted_terms:
+            # 使用前两个引号术语
+            terms = quoted_terms[:min(2, len(quoted_terms))]
+            return ' AND '.join([f'"{term}"' for term in terms])
+        
+        # 如果没有引号术语,分解AND条件
+        if " AND " in query:
+            parts = query.split(" AND ")
+            return parts[0]  # 只用第一个条件
+        
+        return query
+
+    async def _extract_key_terms(self, direction: str) -> List[str]:
+        """从研究方向中提取关键术语"""
+        prompt = f"""
+        Extract 3 most important search terms from this research direction. 
+        Return only comma-separated terms, no explanations.
+        
+        Direction: {direction}
+        """
+        
+        try:
+            response = await self.llm_client.generate_text(prompt, temperature=0.1)
+            terms = [term.strip() for term in response.split(',') if term.strip()]
+            return terms if terms else [direction.split()[0]]  # 如果提取失败,使用首个词
+        except Exception as e:
+            logger.error(f"Error extracting terms: {str(e)}")
+            # 回退到简单分词
+            words = direction.split()
+            return [words[0], words[-1] if len(words) > 1 else words[0]]
+
+    async def _search_papers_with_improved_strategy(self, direction: str, max_results: int = 4) -> List[Dict[str, Any]]:
+        """智能论文搜索策略"""
+        # 提取关键术语
+        terms = await self._extract_key_terms(direction)
+        
+        # 构建多级查询策略
+        queries = []
+        if len(terms) >= 2:
+            queries.append(f'"{terms[0]}" AND "{terms[1]}"')
+        if len(terms) >= 3:
+            queries.append(f'"{terms[0]}" AND "{terms[2]}"')
+        queries.append(f'"{terms[0]}" OR "{terms[1] if len(terms) > 1 else terms[0]}"')
+        queries.append(terms[0])
+        
+        # 逐一尝试查询
+        for query in queries:
+            logger.info(f"Trying search query: {query}")
+            papers = await self.arxiv_client.search_papers(query, max_results)
+            if len(papers) >= 2:  # 找到足够论文则返回
+                return papers[:max_results]
+        
+        # 所有查询失败,返回示例论文
+        return self._get_example_papers(direction)
+
+    async def extract_keywords_only(self, research_intent):
+        """只提取关键词,不进行其他处理"""
+        logging.info(f"Extracting keywords for: {research_intent}")
+        
+        try:
+            # 直接使用LLMClient的extract_keywords方法
+            result = await self.llm_client.extract_keywords(research_topic=research_intent, original_language="auto")
+            return result
+        except Exception as e:
+            logging.error(f"Error extracting keywords: {str(e)}")
+            # 返回一个默认结果,避免None
+            return {
+                "english_keywords": [],
+                "original_keywords": [],
+                "language": "en"
+            }
+
+    async def generate_directions_only(self, keywords: List[str], language: str) -> Dict[str, Any]:
+        """仅生成研究方向"""
+        logger.info(f"Generating directions for keywords: {keywords}")
+        directions_data = await self.llm_client.generate_research_directions(keywords, language)
+        logger.info(f"Generated research directions: {directions_data['english_directions']}")
+        return directions_data
+
+    async def search_papers_for_direction(self, direction: str, max_results: int = 4) -> List[Dict[str, Any]]:
+        """仅为一个方向搜索论文"""
+        query = await self._generate_search_query(direction)
+        logger.info(f"Searching papers with query: {query}")
+        papers = await self._search_papers_with_fallback(query=query, max_results=max_results)
+        return papers
+
+    async def generate_report_for_direction(
+        self, 
+        direction: str, 
+        papers: List[Dict[str, Any]],
+        target_language: str = "en"
+    ) -> Dict[str, Any]:
+        """仅为一个方向生成报告"""
+        return await self._generate_direction_report(direction, papers, target_language)
+
+    async def generate_enhanced_topics(self, keywords, language):
+        """
+        基于关键词生成增强的研究主题
+        返回格式: [
+            {
+                "english_title": "Energy Transition Policies in China",
+                "title": "中国的能源转型政策",
+                "description": "研究中国在能源转型领域的政策制定与实施",
+                "keywords": ["policy", "energy transition", "China"]
+            },
+            ...
+        ]
+        """
+        prompt = f"""
+        Based on the following keywords about a research area: {', '.join(keywords)}
+        
+        Generate 3-5 specific research topics that would be valuable to explore in this area.
+        
+        For each topic:
+        1. Provide a specific, focused research title (not too broad)
+        2. Add a brief description of what this research direction would investigate
+        3. List 3-5 specific keywords that best represent this research direction
+        
+        Format your response as a JSON array of objects with the following structure:
+        [
+            {{
+                "english_title": "The English title",
+                "description": "Brief description of this research direction",
+                "keywords": ["keyword1", "keyword2", "keyword3"]
+            }},
+            ...
+        ]
+        """
+        
+        # Add logging to track the response
+        logger.info(f"Generating enhanced topics for keywords: {keywords}")
+        
+        response = await self.llm_client.chat_completion(
+            messages=[
+                {"role": "system", "content": "You are a research assistant helping to identify valuable research topics and directions."},
+                {"role": "user", "content": prompt}
+            ],
+            response_format={"type": "json_object"}
+        )
+        
+        # Log the raw response for debugging
+        logger.info(f"Raw LLM response: {response}")
+        
+        try:
+            # 提取JSON响应
+            content = response["choices"][0]["message"]["content"]
+            logger.info(f"Parsing JSON content: {content}")
+            
+            # Try to handle different JSON formats that might be returned
+            topics_data = json.loads(content)
+            
+            # 确保我们有一个topics字段,如果返回直接是数组,则进行调整
+            if isinstance(topics_data, list):
+                topics = topics_data
+            elif "topics" in topics_data:
+                topics = topics_data.get("topics", [])
+            else:
+                # Try to find any array in the response
+                for key, value in topics_data.items():
+                    if isinstance(value, list) and len(value) > 0 and "english_title" in value[0]:
+                        topics = value
+                        break
+                else:
+                    # If no suitable array is found, create basic topics
+                    logger.warning("Could not find topics array in response, creating basic topics")
+                    topics = []
+                    for keyword in keywords[:3]:
+                        topics.append({
+                            "english_title": f"Research on {keyword}",
+                            "description": f"Investigating various aspects of {keyword}",
+                            "keywords": [keyword]
+                        })
+            
+            # Log the extracted topics
+            logger.info(f"Extracted {len(topics)} topics: {topics}")
+            
+            # 如果需要翻译
+            if language != "en":
+                translated_topics = []
+                for topic in topics:
+                    # 翻译标题
+                    translation_prompt = f"Translate the following research title to {language}: \"{topic['english_title']}\""
+                    translation_response = await self.llm_client.chat_completion(
+                        messages=[
+                            {"role": "system", "content": "You are a professional translator."},
+                            {"role": "user", "content": translation_prompt}
+                        ]
+                    )
+                    translated_title = translation_response["choices"][0]["message"]["content"].strip().strip('"')
+                    
+                    # 添加翻译后的标题
+                    topic["title"] = translated_title
+                    translated_topics.append(topic)
+                
+                return translated_topics
+            else:
+                # 英文环境则复制标题
+                for topic in topics:
+                    topic["title"] = topic["english_title"]
+                return topics
+            
+        except Exception as e:
+            logging.error(f"Error parsing enhanced topics: {str(e)}", exc_info=True)
+            # 返回基础主题格式作为后备
+            basic_topics = []
+            for i, keyword in enumerate(keywords[:3]):
+                basic_topics.append({
+                    "english_title": f"Research on {keyword}",
+                    "title": f"关于{keyword}的研究" if language != "en" else f"Research on {keyword}",
+                    "description": f"Investigating various aspects of {keyword}",
+                    "keywords": [keyword]
+                })
+            return basic_topics
+
+    async def generate_search_keywords_for_topic(self, topic):
+        """
+        为特定研究主题生成精确的搜索关键词
+        """
+        prompt = f"""
+        Based on the following research topic:
+        
+        Title: {topic['english_title']}
+        Description: {topic['description']}
+        Initial keywords: {', '.join(topic['keywords'])}
+        
+        Generate 5-7 precise search keywords or phrases that would be most effective for finding 
+        relevant scientific papers on this specific topic. These should be more specific and targeted 
+        than the initial keywords.
+        
+        Format your response as a JSON array of strings, e.g. ["keyword1", "keyword2", ...]
+        """
+        
+        try:
+            # 添加错误处理
+            response = await self.llm_client.chat_completion(
+                messages=[
+                    {"role": "system", "content": "You are a research assistant helping to generate effective search keywords for academic literature."},
+                    {"role": "user", "content": prompt}
+                ],
+                response_format={"type": "json_object"}
+            )
+            
+            content = response["choices"][0]["message"]["content"]
+            keywords_data = json.loads(content)
+            
+            # 处理不同的返回格式
+            if isinstance(keywords_data, list):
+                return keywords_data
+            elif "keywords" in keywords_data:
+                return keywords_data["keywords"]
+            else:
+                # 提取数据中的第一个列表
+                for key, value in keywords_data.items():
+                    if isinstance(value, list):
+                        return value
+                # 后备选项
+                return topic['keywords']
+        except Exception as e:
+            # 增强错误处理:记录详细错误并返回原始关键词
+            logging.error(f"Error generating search keywords: {str(e)}", exc_info=True)
+            logging.info(f"Falling back to original topic keywords due to API error")
+            # 返回原始关键词作为后备
+            return topic['keywords']
+
+    async def search_papers_with_keywords(self, keywords):
+        """
+        使用关键词搜索相关论文
+        """
+        all_papers = []
+        
+        # 对每个关键词进行搜索
+        for keyword in keywords:
+            try:
+                results = await self.arxiv_client.search_papers(
+                    query=keyword,
+                    max_results=3  # 每个关键词获取少量相关性高的结果
+                )
+                all_papers.extend(results)
+            except Exception as e:
+                logging.error(f"Error searching papers for keyword '{keyword}': {str(e)}")
+        
+        # 去重
+        unique_papers = []
+        paper_ids = set()
+        for paper in all_papers:
+            if paper["id"] not in paper_ids:
+                unique_papers.append(paper)
+                paper_ids.add(paper["id"])
+        
+        return unique_papers
+
+    async def cluster_papers_by_keywords(self, papers, search_keywords, topic):
+        """
+        根据关键词和主题将论文进行聚类
+        """
+        if not papers or len(papers) < 3:
+            # 如果论文太少,不进行聚类
+            return {
+                "papers": papers,
+                "clusters": [{
+                    "id": 0,
+                    "name": topic["english_title"],
+                    "keywords": search_keywords[:3]
+                }]
+            }
+        
+        try:
+            # 准备文本数据
+            texts = [f"{p['title']} {p['summary']}" for p in papers]
+            
+            # 使用现有的聚类器或简单实现
+            num_clusters = min(3, len(papers) // 2)  # 动态确定聚类数
+            
+            # 调用现有的聚类方法
+            cluster_results = self.paper_clusterer.cluster_papers(
+                papers=papers,
+                num_clusters=num_clusters
+            ) if hasattr(self, 'paper_clusterer') else None
+            
+            if not cluster_results:
+                # 简单替代方案:根据关键词匹配进行"聚类"
+                clusters = []
+                for i, keyword in enumerate(search_keywords[:3]):
+                    clusters.append({
+                        "id": i,
+                        "name": f"Research on {keyword}",
+                        "keywords": [keyword]
+                    })
+                
+                # 分配论文到"聚类"
+                for paper in papers:
+                    best_match = 0
+                    best_score = 0
+                    for i, cluster in enumerate(clusters):
+                        # 简单相似度:关键词在标题或摘要中的出现次数
+                        score = paper['title'].lower().count(cluster['keywords'][0].lower()) + \
+                                paper['summary'].lower().count(cluster['keywords'][0].lower())
+                        if score > best_score:
+                            best_score = score
+                            best_match = i
+                    paper['cluster'] = best_match
+                
+                return {
+                    "papers": papers,
+                    "clusters": clusters
+                }
+            else:
+                # 使用现有聚类结果
+                return cluster_results
+            
+        except Exception as e:
+            logging.error(f"Error clustering papers: {str(e)}")
+            # 失败时返回未聚类的数据
+            return {
+                "papers": papers,
+                "clusters": [{
+                    "id": 0,
+                    "name": topic["english_title"],
+                    "keywords": search_keywords[:3]
+                }]
+            }
+
+    async def generate_enhanced_report(self, topic, clustered_papers, search_keywords, language):
+        """
+        为主题生成增强研究报告 - 分段处理以减少单次API调用压力
+        """
+        papers = clustered_papers["papers"]
+        clusters = clustered_papers["clusters"]
+        
+        try:
+            # 准备聚类摘要 - 限制每个聚类的论文数量
+            cluster_summaries = []
+            for cluster in clusters:
+                cluster_papers = [p for p in papers if p.get('cluster', 0) == cluster['id']]
+                if cluster_papers:
+                    # 限制每个聚类最多3篇论文
+                    paper_titles = "\n".join([f"- {p['title']}" for p in cluster_papers[:3]])
+                    cluster_summaries.append(f"Cluster: {cluster['name']}\nKeywords: {', '.join(cluster['keywords'])}\nPapers:\n{paper_titles}")
+            
+            all_cluster_summaries = "\n\n".join(cluster_summaries[:3])  # 限制聚类数量
+            
+            # 拆分报告生成为多个部分
+            # 1. 生成报告概述和主要发现
+            intro_prompt = f"""
+            Generate the introduction and main findings sections for a research report on:
+            
+            Research Topic: {topic['english_title']}
+            Description: {topic['description']}
+            Main Keywords: {', '.join(topic['keywords'][:5])}
+            
+            Based on the following paper clusters:
+            {all_cluster_summaries}
+            
+            Include:
+            1. A brief introduction to the research topic (100 words)
+            2. Summary of main findings from the literature (200 words)
+            
+            Format with appropriate headings.
+            """
+            
+            # 使用更小的max_tokens以减少单次API调用的压力
+            intro_response = await self.llm_client.chat_completion(
+                messages=[
+                    {"role": "system", "content": "You are a research assistant creating academic reports."},
+                    {"role": "user", "content": intro_prompt}
+                ],
+                max_tokens=1000
+            )
+            
+            intro_content = intro_response["choices"][0]["message"]["content"]
+            
+            # 2. 生成研究差距和未来方向
+            conclusion_prompt = f"""
+            Generate the research gaps and future directions sections for a research report on:
+            
+            Research Topic: {topic['english_title']}
+            Description: {topic['description']}
+            Main Keywords: {', '.join(topic['keywords'][:5])}
+            
+            Based on the provided literature, identify:
+            1. Current research gaps (100 words)
+            2. Potential directions for future research (100 words)
+            
+            Format with appropriate headings.
+            """
+            
+            conclusion_response = await self.llm_client.chat_completion(
+                messages=[
+                    {"role": "system", "content": "You are a research assistant creating academic reports."},
+                    {"role": "user", "content": conclusion_prompt}
+                ],
+                max_tokens=800
+            )
+            
+            conclusion_content = conclusion_response["choices"][0]["message"]["content"]
+            
+            # 合并内容
+            english_content = f"{intro_content}\n\n{conclusion_content}"
+            
+            # 如果需要翻译
+            if language != "en":
+                try:
+                    # 拆分翻译为两部分
+                    translation_intro = await self.llm_client.chat_completion(
+                        messages=[
+                            {"role": "system", "content": "You are a professional translator specialized in academic content."},
+                            {"role": "user", "content": f"Translate the following research report section to {language}:\n\n{intro_content}"}
+                        ],
+                        max_tokens=1200
+                    )
+                    
+                    translation_conclusion = await self.llm_client.chat_completion(
+                        messages=[
+                            {"role": "system", "content": "You are a professional translator specialized in academic content."},
+                            {"role": "user", "content": f"Translate the following research report section to {language}:\n\n{conclusion_content}"}
+                        ],
+                        max_tokens=1000
+                    )
+                    
+                    translated_intro = translation_intro["choices"][0]["message"]["content"]
+                    translated_conclusion = translation_conclusion["choices"][0]["message"]["content"]
+                    
+                    translated_content = f"{translated_intro}\n\n{translated_conclusion}"
+                except Exception as e:
+                    # 翻译失败时,使用原始英文内容
+                    logger.error(f"Translation failed: {str(e)}")
+                    translated_content = f"(翻译失败,显示原文) {english_content}"
+                
+                return {
+                    "english_content": english_content,
+                    "translated_content": translated_content
+                }
+            else:
+                return {
+                    "english_content": english_content,
+                    "translated_content": english_content  # 英文环境下两者相同
+                }
+        except Exception as e:
+            # 生成报告失败时,提供一个简单的后备报告
+            logger.error(f"Report generation failed: {str(e)}")
+            fallback_content = f"""
+# 研究主题: {topic['english_title']}
+
+## 概述
+本报告本应提供关于"{topic['english_title']}"主题的详细研究分析,但由于API调用限制或网络问题,无法生成完整报告。
+
+## 可用资源
+我们已检索到该主题相关的{len(papers)}篇论文,您可以通过查看这些论文获取相关信息。
+
+## 主要关键词
+{', '.join(search_keywords[:5] if search_keywords else [])}
+
+## 建议
+您可以重新尝试生成报告,或直接查看检索到的论文以获取更多信息。
+"""
+            
+            return {
+                "english_content": fallback_content,
+                "translated_content": fallback_content
+            }

+ 70 - 0
backend/data/cache/research_1744959673.json

@@ -0,0 +1,70 @@
+{
+  "research_intent": "人工智能在医疗诊断中的应用",
+  "timestamp": 1744959560.8536503,
+  "language": "zh",
+  "english_keywords": [
+    "artificial intelligence",
+    "medical diagnosis",
+    "machine learning",
+    "healthcare",
+    "deep learning",
+    "clinical decision support",
+    "diagnostic imaging",
+    "predictive analytics"
+  ],
+  "original_keywords": [
+    "人工智能,医学诊断,机器学习,医疗保健,深度学习,临床决策支持,诊断影像,预测分析"
+  ],
+  "english_directions": [
+    "How can deep learning models improve the accuracy and interpretability of diagnostic imaging (e.g., MRI, CT scans) for early detection of rare diseases?",
+    "What role can multimodal machine learning (combining imaging, genomics, and EHR data) play in personalized clinical decision support for chronic disease management?",
+    "How can federated learning frameworks address data privacy concerns while enabling robust predictive analytics for hospital readmission risk across diverse healthcare systems?",
+    "Can lightweight AI models deployed on edge devices achieve comparable performance to cloud-based systems in real-time point-of-care medical diagnosis for resource-limited settings?",
+    "What ethical and regulatory challenges arise when integrating explainable AI systems into high-stakes diagnostic workflows, and how can they be mitigated?",
+    "How can reinforcement learning optimize dynamic treatment planning in oncology by integrating real-time patient monitoring data with clinical guidelines?"
+  ],
+  "original_directions": [
+    "深度学习模型如何提高诊断影像(如MRI、CT扫描)在罕见疾病早期检测中的准确性和可解释性?",
+    "多模态机器学习(结合影像、基因组学和电子健康记录数据)在慢性病管理的个性化临床决策支持中可以发挥什么作用?",
+    "联邦学习框架如何通过解决数据隐私问题,在不同医疗系统中实现针对再入院风险的稳健预测分析?",
+    "在资源有限的环境中,部署于边缘设备的轻量级AI模型能否在实时护理点医疗诊断中达到与云端系统相当的性能?",
+    "将可解释AI系统整合到高风险诊断工作流程时会引发哪些伦理和监管挑战?应如何缓解?",
+    "强化学习如何通过整合实时患者监测数据与临床指南,优化肿瘤学中的动态治疗计划?"
+  ],
+  "papers_by_direction": [
+    {
+      "direction": "How can deep learning models improve the accuracy and interpretability of diagnostic imaging (e.g., MRI, CT scans) for early detection of rare diseases?",
+      "original_direction": "深度学习模型如何提高诊断影像(如MRI、CT扫描)在罕见疾病早期检测中的准确性和可解释性?",
+      "papers": []
+    },
+    {
+      "direction": "What role can multimodal machine learning (combining imaging, genomics, and EHR data) play in personalized clinical decision support for chronic disease management?",
+      "original_direction": "多模态机器学习(结合影像、基因组学和电子健康记录数据)在慢性病管理的个性化临床决策支持中可以发挥什么作用?",
+      "papers": []
+    },
+    {
+      "direction": "How can federated learning frameworks address data privacy concerns while enabling robust predictive analytics for hospital readmission risk across diverse healthcare systems?",
+      "original_direction": "联邦学习框架如何通过解决数据隐私问题,在不同医疗系统中实现针对再入院风险的稳健预测分析?",
+      "papers": []
+    },
+    {
+      "direction": "Can lightweight AI models deployed on edge devices achieve comparable performance to cloud-based systems in real-time point-of-care medical diagnosis for resource-limited settings?",
+      "original_direction": "在资源有限的环境中,部署于边缘设备的轻量级AI模型能否在实时护理点医疗诊断中达到与云端系统相当的性能?",
+      "papers": []
+    },
+    {
+      "direction": "What ethical and regulatory challenges arise when integrating explainable AI systems into high-stakes diagnostic workflows, and how can they be mitigated?",
+      "original_direction": "将可解释AI系统整合到高风险诊断工作流程时会引发哪些伦理和监管挑战?应如何缓解?",
+      "papers": []
+    },
+    {
+      "direction": "How can reinforcement learning optimize dynamic treatment planning in oncology by integrating real-time patient monitoring data with clinical guidelines?",
+      "original_direction": "强化学习如何通过整合实时患者监测数据与临床指南,优化肿瘤学中的动态治疗计划?",
+      "papers": []
+    }
+  ],
+  "clusters": [],
+  "status": "completed",
+  "direction_reports": [],
+  "processing_time": 112.84043836593628
+}

Plik diff jest za duży
+ 122 - 0
backend/data/cache/research_1744960528.json


+ 70 - 0
backend/data/cache/research_1744960762.json

@@ -0,0 +1,70 @@
+{
+  "research_intent": "人工智能在医疗诊断中的应用",
+  "timestamp": 1744960534.033066,
+  "language": "zh",
+  "english_keywords": [
+    "artificial intelligence",
+    "medical diagnosis",
+    "machine learning",
+    "healthcare",
+    "deep learning",
+    "clinical decision support",
+    "diagnostic imaging",
+    "predictive analytics"
+  ],
+  "original_keywords": [
+    "人工智能、医学诊断、机器学习、医疗保健、深度学习、临床决策支持、诊断影像、预测分析"
+  ],
+  "english_directions": [
+    "How can deep learning models be optimized for early detection of rare diseases in diagnostic imaging while minimizing false positives?",
+    "What role can multimodal machine learning (combining imaging, EHR, and genomic data) play in improving clinical decision support for personalized cancer diagnosis?",
+    "How can federated learning frameworks be designed to enhance predictive analytics in healthcare while preserving patient privacy across institutions?",
+    "Can explainable AI techniques improve clinician trust and adoption of artificial intelligence tools for medical diagnosis in high-stakes specialties like cardiology or neurology?",
+    "What are the most effective strategies for mitigating bias in machine learning-based diagnostic systems when applied to diverse patient populations?",
+    "How can reinforcement learning be adapted to optimize dynamic treatment planning in chronic disease management using real-time clinical data streams?"
+  ],
+  "original_directions": [
+    "如何优化深度学习模型以在诊断影像中早期发现罕见疾病,同时最小化误报?",
+    "多模态机器学习(结合影像、电子健康记录和基因组数据)在改善个性化癌症诊断的临床决策支持中可以发挥什么作用?",
+    "如何设计联邦学习框架以增强医疗领域的预测分析,同时保护跨机构患者隐私?",
+    "可解释人工智能技术能否提高临床医生对心脏病学或神经学等高风险专科医疗诊断人工智能工具的信任和采用?",
+    "在应用于多样化患者群体时,减轻基于机器学习的诊断系统偏见的最有效策略是什么?",
+    "如何调整强化学习以利用实时临床数据流优化慢性病管理的动态治疗计划?"
+  ],
+  "papers_by_direction": [
+    {
+      "direction": "How can deep learning models be optimized for early detection of rare diseases in diagnostic imaging while minimizing false positives?",
+      "original_direction": "如何优化深度学习模型以在诊断影像中早期发现罕见疾病,同时最小化误报?",
+      "papers": []
+    },
+    {
+      "direction": "What role can multimodal machine learning (combining imaging, EHR, and genomic data) play in improving clinical decision support for personalized cancer diagnosis?",
+      "original_direction": "多模态机器学习(结合影像、电子健康记录和基因组数据)在改善个性化癌症诊断的临床决策支持中可以发挥什么作用?",
+      "papers": []
+    },
+    {
+      "direction": "How can federated learning frameworks be designed to enhance predictive analytics in healthcare while preserving patient privacy across institutions?",
+      "original_direction": "如何设计联邦学习框架以增强医疗领域的预测分析,同时保护跨机构患者隐私?",
+      "papers": []
+    },
+    {
+      "direction": "Can explainable AI techniques improve clinician trust and adoption of artificial intelligence tools for medical diagnosis in high-stakes specialties like cardiology or neurology?",
+      "original_direction": "可解释人工智能技术能否提高临床医生对心脏病学或神经学等高风险专科医疗诊断人工智能工具的信任和采用?",
+      "papers": []
+    },
+    {
+      "direction": "What are the most effective strategies for mitigating bias in machine learning-based diagnostic systems when applied to diverse patient populations?",
+      "original_direction": "在应用于多样化患者群体时,减轻基于机器学习的诊断系统偏见的最有效策略是什么?",
+      "papers": []
+    },
+    {
+      "direction": "How can reinforcement learning be adapted to optimize dynamic treatment planning in chronic disease management using real-time clinical data streams?",
+      "original_direction": "如何调整强化学习以利用实时临床数据流优化慢性病管理的动态治疗计划?",
+      "papers": []
+    }
+  ],
+  "clusters": [],
+  "status": "completed",
+  "direction_reports": [],
+  "processing_time": 228.2858166694641
+}

Plik diff jest za duży
+ 422 - 0
backend/data/cache/research_1744967509.json


Plik diff jest za duży
+ 418 - 0
backend/data/cache/research_1744982496.json


+ 57 - 0
backend/init_db.py

@@ -0,0 +1,57 @@
+"""
+数据库初始化脚本
+"""
+import os
+import sys
+import logging
+from datetime import datetime
+# 将项目根目录添加到Python路径
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from sqlalchemy.orm import Session
+from core.models import Base, User
+from core.database import engine, SessionLocal
+from core.auth import get_password_hash
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+def init_db():
+    """初始化数据库"""
+    # 创建所有表
+    Base.metadata.create_all(bind=engine)
+    
+    # 创建数据库会话
+    db = SessionLocal()
+    
+    try:
+        # 检查是否已有用户
+        user = db.query(User).first()
+        
+        if not user:
+            # 创建默认用户
+            default_user = User(
+                username="admin",
+                email="admin@example.com",
+                hashed_password=get_password_hash("password"),
+                institution="江西财经大学",
+                position="学生",
+                research_fields=["人工智能", "计算机视觉", "自然语言处理"],
+                membership_type="高级研究版",
+                expiry_date=datetime(2025, 12, 31),
+                credits=5280
+            )
+            db.add(default_user)
+            db.commit()
+            logger.info("创建了默认用户")
+        else:
+            logger.info("数据库已初始化")
+            
+    finally:
+        db.close()
+    
+    logger.info("数据库初始化完成")
+
+if __name__ == "__main__":
+    logger.info("初始化数据库...")
+    init_db() 

+ 90 - 0
backend/main.py

@@ -0,0 +1,90 @@
+"""
+文献启明星 API 服务器
+FastAPI应用程序入口
+"""
+
+import logging
+import uvicorn
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from pathlib import Path
+import sys
+import os
+
+# 添加项目根目录到Python路径
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from backend.api import research, chat, auth, users, chat_history, research_history
+from backend.config import CORS_ORIGINS, BASE_DIR
+from backend.core.models import Base
+from backend.core.database import engine
+
+# 创建数据库表
+Base.metadata.create_all(bind=engine)
+
+# 配置日志
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+    handlers=[
+        logging.StreamHandler(),
+        logging.FileHandler(Path(BASE_DIR) / "lightstar_api.log")
+    ]
+)
+
+logger = logging.getLogger(__name__)
+
+# 创建FastAPI应用
+app = FastAPI(
+    title="LightStar API",
+    description="文献启明星 - 科研调研智能体API服务",
+    version="0.1.0",
+    docs_url=None,  # 禁用Swagger UI
+    redoc_url=None  # 禁用ReDoc
+)
+
+# 配置CORS - 使用 config.py 中的设置
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=CORS_ORIGINS,  # 使用配置文件中定义的域名列表
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# 注册路由
+app.include_router(research.router, prefix="/api")
+app.include_router(chat.router, prefix="/api")
+app.include_router(auth.router, prefix="/api")
+app.include_router(users.router, prefix="/api")
+app.include_router(chat_history.router, prefix="/api")
+app.include_router(research_history.router, prefix="/api")
+
+# 健康检查端点
+@app.get("/health")
+async def health_check():
+    return {"status": "healthy", "version": "0.1.0"}
+
+# API信息端点
+@app.get("/")
+async def root():
+    return {
+        "name": "LightStar API",
+        "description": "文献启明星 - 科研调研智能体API服务",
+        "version": "0.1.0",
+        "endpoints": [
+            "/api/auth/token",
+            "/api/auth/register",
+            "/api/users/profile",
+            "/api/chat-history",
+            "/api/research-history",
+            "/api/research/process",
+            "/api/chat/send-message"
+        ]
+    }
+
+# 启动服务器
+if __name__ == "__main__":
+    logger.info("Starting LightStar API server")
+    # 使用模块的绝对路径确保可以从任何目录启动
+    uvicorn.run("backend.main:app", host="0.0.0.0", port=8000, reload=True)

+ 519 - 0
backend/utils/api_client.py

@@ -0,0 +1,519 @@
+"""
+API客户端工具
+用于与外部API(如LLM API、文献检索API)进行交互
+"""
+
+import httpx
+import json
+import os
+import time
+from typing import Dict, List, Any, Optional
+import logging
+from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
+import hashlib
+
+from backend.config import DEEPSEEK_API_KEY, DEEPSEEK_API_URL, DEEPSEEK_MODEL
+
+logger = logging.getLogger(__name__)
+
+class LLMCache:
+    """LLM响应缓存,减少重复请求"""
+    
+    def __init__(self, cache_size=200):
+        self.cache = {}
+        self.cache_size = cache_size
+        self.hits = 0
+        self.misses = 0
+    
+    def get(self, prompt, temperature):
+        """获取缓存响应"""
+        key = self._make_key(prompt, temperature)
+        if key in self.cache:
+            self.hits += 1
+            return self.cache[key]
+        self.misses += 1
+        return None
+    
+    def set(self, prompt, temperature, response):
+        """设置缓存响应"""
+        key = self._make_key(prompt, temperature)
+        self.cache[key] = response
+        
+        # 如果缓存太大,删除最旧条目
+        if len(self.cache) > self.cache_size:
+            oldest_key = next(iter(self.cache))
+            del self.cache[oldest_key]
+    
+    def _make_key(self, prompt, temperature):
+        """创建缓存键"""
+        # 使用提示内容的MD5哈希作为键
+        prompt_hash = hashlib.md5(prompt.encode('utf-8')).hexdigest()
+        return f"{prompt_hash}_{temperature}"
+    
+    def get_stats(self):
+        """获取缓存统计信息"""
+        total = self.hits + self.misses
+        hit_rate = (self.hits / total) * 100 if total > 0 else 0
+        return {
+            "hits": self.hits,
+            "misses": self.misses,
+            "total": total,
+            "hit_rate": hit_rate,
+            "size": len(self.cache)
+        }
+
+class LLMClient:
+    """大型语言模型API客户端"""
+    
+    def __init__(
+        self, 
+        api_key: Optional[str] = None,
+        model: Optional[str] = None,
+        api_url: Optional[str] = None
+    ):
+        self.api_key = api_key or DEEPSEEK_API_KEY
+        self.model = model or DEEPSEEK_MODEL
+        self.api_url = api_url or DEEPSEEK_API_URL
+        
+        if not self.api_key:
+            logger.warning("No LLM API key provided. API calls will fail.")
+        
+        self.cache = LLMCache()
+    
+    @retry(
+        stop=stop_after_attempt(3),
+        wait=wait_exponential(multiplier=1, min=2, max=10),
+        retry=retry_if_exception_type((httpx.ReadTimeout, httpx.ConnectTimeout))
+    )
+    async def generate_text(
+        self, 
+        prompt: str, 
+        temperature: float = 0.3,
+        max_tokens: int = 1000,
+        timeout: float = 60.0  # 默认超时时间
+    ) -> str:
+        """优化后的文本生成函数,支持自定义超时和重试"""
+        # 检查缓存
+        cached_response = self.cache.get(prompt, temperature)
+        if cached_response:
+            logger.info("Using cached LLM response")
+            return cached_response
+        
+        headers = {
+            "Content-Type": "application/json",
+            "Authorization": f"Bearer {self.api_key}"
+        }
+        
+        payload = {
+            "model": self.model,
+            "messages": [{"role": "user", "content": prompt}],
+            "temperature": temperature,
+            "max_tokens": max_tokens
+        }
+        
+        # 根据请求大小动态调整超时
+        adjusted_timeout = timeout
+        if max_tokens > 1000:
+            # 更大的请求需要更长的超时
+            adjusted_timeout = timeout * (max_tokens / 1000) * 1.5
+            
+        try:
+            async with httpx.AsyncClient(timeout=adjusted_timeout) as client:
+                response = await client.post(
+                    f"{self.api_url}/chat/completions",
+                    headers=headers,
+                    json=payload
+                )
+                response.raise_for_status()
+                result = response.json()
+                
+                # 添加到缓存
+                content = result["choices"][0]["message"]["content"]
+                self.cache.set(prompt, temperature, content)
+                return content
+        except httpx.TimeoutException as e:
+            logger.error(f"Timeout calling LLM API ({adjusted_timeout}s): {str(e)}")
+            raise Exception(f"LLM服务响应超时,请稍后重试") from e
+        except Exception as e:
+            logger.error(f"Error calling LLM API: {str(e)}")
+            raise Exception(f"LLM服务调用失败: {str(e)}") from e
+            
+    async def generate_text_chunked(
+        self,
+        prompt: str,
+        max_chunk_tokens: int = 800,
+        temperature: float = 0.3
+    ) -> str:
+        """
+        针对大型文本生成任务的分块处理函数
+        将大型生成任务分解为多个小块,然后合并结果
+        """
+        # 计算预计的请求大小,对超大请求进行拆分
+        estimated_tokens = len(prompt.split()) * 1.5  # 粗略估计
+        
+        if estimated_tokens > 2000:
+            # 简化提示,移除不必要的说明性文本
+            prompt = self._simplify_large_prompt(prompt)
+        
+        # 对于报告生成这类大型任务,使用分块策略
+        if "structured research report" in prompt.lower() and estimated_tokens > 1000:
+            # 1. 首先生成报告大纲
+            outline_prompt = f"""
+            Based on the following research direction and papers, create a concise outline
+            for a research report with 3-5 main sections.
+            
+            Research direction: {prompt.split('Research direction:', 1)[1].split('Key papers:', 1)[0].strip()}
+            
+            Only provide the outline with section titles, no additional text.
+            """
+            
+            outline = await self.generate_text(outline_prompt, temperature=0.3, max_tokens=400)
+            
+            # 2. 然后为每个部分生成内容
+            sections = [s.strip() for s in outline.split('\n') if s.strip() and not s.strip().startswith('#')]
+            
+            # 3. 逐个处理每个部分
+            full_report = ""
+            for section in sections[:5]:  # 限制最多5个部分
+                section_prompt = f"""
+                Write the content for the "{section}" section of a research report about:
+                
+                {prompt.split('Research direction:', 1)[1].split('Key papers:', 1)[0].strip()}
+                
+                Based on these papers:
+                {prompt.split('Key papers:', 1)[1].strip() if 'Key papers:' in prompt else ''}
+                
+                Write a detailed section (250-400 words) with relevant information, findings, and analysis.
+                """
+                
+                section_content = await self.generate_text(
+                    section_prompt,
+                    temperature=temperature,
+                    max_tokens=600
+                )
+                
+                full_report += f"## {section}\n\n{section_content}\n\n"
+            
+            return full_report
+            
+        else:
+            # 对于较小的任务,使用标准方法
+            return await self.generate_text(prompt, temperature=temperature, max_tokens=max_chunk_tokens)
+    
+    def _simplify_large_prompt(self, prompt: str) -> str:
+        """简化大型提示,删除冗余指令"""
+        # 删除重复的指令和多余的空白
+        lines = [line.strip() for line in prompt.split('\n')]
+        lines = [line for line in lines if line]
+        
+        # 移除多余的模板语言
+        skip_phrases = [
+            "Please follow these guidelines:",
+            "Make sure to:",
+            "Remember to:",
+            "Follow this structure:",
+            "Your report should include:"
+        ]
+        
+        simplified_lines = []
+        for line in lines:
+            if not any(phrase in line for phrase in skip_phrases):
+                simplified_lines.append(line)
+                
+        return "\n".join(simplified_lines)
+
+    async def translate_text(
+        self, 
+        text: str, 
+        source_language: str = "auto", 
+        target_language: str = "en"
+    ) -> str:
+        """
+        Translate text from source language to target language
+        
+        Args:
+            text: Text to translate
+            source_language: Source language code (or "auto" for auto-detection)
+            target_language: Target language code
+            
+        Returns:
+            Translated text
+        """
+        prompt = f"""
+        Translate the following text from {source_language} to {target_language}:
+        
+        {text}
+        
+        Provide only the translated text with no additional explanations or comments.
+        """
+        
+        return await self.generate_text(prompt, temperature=0.1)
+
+    async def detect_language(self, text: str) -> str:
+        """
+        Detect the language of the given text
+        
+        Args:
+            text: Text to detect language for
+            
+        Returns:
+            Language code (e.g., "en", "zh", "es")
+        """
+        prompt = f"""
+        Detect the language of the following text. 
+        Respond with just the ISO 639-1 language code (e.g., "en" for English, "zh" for Chinese, "es" for Spanish).
+        
+        Text: {text}
+        
+        Language code:
+        """
+        
+        response = await self.generate_text(prompt, temperature=0.1)
+        return response.strip().lower()
+
+    async def extract_keywords(self, research_topic: str, original_language: str = "en") -> Dict[str, Any]:
+        """
+        Extract keywords from research topic in any language
+        
+        Args:
+            research_topic: User input research topic
+            original_language: Original language code
+            
+        Returns:
+            Dictionary with keywords in English and the original language
+        """
+        # Detect language if not provided
+        if original_language == "auto":
+            original_language = await self.detect_language(research_topic)
+        
+        # Translate to English if not already in English
+        english_topic = research_topic
+        if original_language != "en":
+            english_topic = await self.translate_text(research_topic, original_language, "en")
+        
+        # Extract English keywords
+        prompt = f"""
+        As a research assistant, extract 5-8 key search terms from the following research topic.
+        These terms should be useful for academic literature search. Consider core concepts, methodology, 
+        application fields, etc. Return only a comma-separated list of keywords without numbering or other text.
+
+        Research topic: {english_topic}
+        
+        Keywords:
+        """
+        
+        response = await self.generate_text(prompt, temperature=0.1)
+        english_keywords = [kw.strip() for kw in response.split(',') if kw.strip()]
+        
+        # Translate keywords back to original language if needed
+        original_keywords = english_keywords
+        if original_language != "en":
+            keywords_string = ", ".join(english_keywords)
+            translated_keywords = await self.translate_text(keywords_string, "en", original_language)
+            original_keywords = [kw.strip() for kw in translated_keywords.split(',') if kw.strip()]
+        
+        return {
+            "english_keywords": english_keywords,
+            "original_keywords": original_keywords,
+            "language": original_language
+        }
+
+    async def generate_research_directions(self, keywords: List[str], original_language: str = "en") -> Dict[str, Any]:
+        """
+        Generate multiple research directions based on keywords
+        
+        Args:
+            keywords: List of English keywords
+            original_language: Original language of user input
+            
+        Returns:
+            Dictionary with research directions in English and original language
+        """
+        keywords_string = ", ".join(keywords)
+        
+        prompt = f"""
+        As a research advisor, generate 4-6 distinct research directions based on these keywords: {keywords_string}
+        
+        Each research direction should:
+        1. Be focused and specific enough for academic research
+        2. Represent a different aspect or approach to studying the topic
+        3. Be formulated as a clear research question or direction statement
+        
+        Provide each research direction on a new line without numbering.
+        """
+        
+        response = await self.generate_text(prompt, temperature=0.7, max_tokens=1500)
+        
+        # Parse research directions
+        english_directions = [dir.strip() for dir in response.splitlines() if dir.strip()]
+        
+        # Translate research directions to original language if needed
+        original_directions = english_directions
+        if original_language != "en":
+            directions_text = "\n".join(english_directions)
+            translated_directions = await self.translate_text(directions_text, "en", original_language)
+            original_directions = [dir.strip() for dir in translated_directions.splitlines() if dir.strip()]
+        
+        return {
+            "english_directions": english_directions,
+            "original_directions": original_directions,
+            "language": original_language
+        }
+
+    async def batch_translate(self, texts: List[str], source_lang="en", target_lang="zh") -> List[str]:
+        """批量翻译多个文本片段"""
+        if not texts:
+            return []
+        
+        if source_lang == target_lang:
+            return texts
+        
+        # 将多个文本合并,使用特殊标记分隔
+        separator = "\n===SECTION_BREAK===\n"
+        combined_text = separator.join(texts)
+        
+        prompt = f"""
+        Translate the following text sections from {source_lang} to {target_lang}.
+        Each section is separated by "{separator}".
+        Keep each section separate in your translation.
+        
+        {combined_text}
+        """
+        
+        try:
+            translated = await self.generate_text(prompt, temperature=0.1)
+            
+            # 拆分翻译结果
+            sections = translated.split(separator)
+            
+            # 确保返回与输入相同数量的部分
+            while len(sections) < len(texts):
+                sections.append("")
+            
+            return sections[:len(texts)]
+        except Exception as e:
+            logger.error(f"Batch translation error: {str(e)}")
+            # 出错时返回原文
+            return texts
+
+    @retry(
+        stop=stop_after_attempt(3),  # 最多重试3次
+        wait=wait_exponential(multiplier=1, min=2, max=10),  # 指数退避等待
+        retry=retry_if_exception_type((httpx.ReadTimeout, httpx.ConnectTimeout))
+    )
+    async def chat_completion(
+        self, 
+        messages: List[Dict[str, str]], 
+        temperature: float = 0.3,
+        max_tokens: int = 1000,
+        response_format: Optional[Dict[str, str]] = None
+    ) -> Dict[str, Any]:
+        """
+        发送聊天完成请求到API
+        """
+        headers = {
+            "Content-Type": "application/json",
+            "Authorization": f"Bearer {self.api_key}"
+        }
+        
+        payload = {
+            "model": self.model,
+            "messages": messages,
+            "temperature": temperature,
+            "max_tokens": max_tokens
+        }
+        
+        if response_format:
+            payload["response_format"] = response_format
+        
+        try:
+            # 增加超时时间到120秒
+            logger.info(f"正在发送请求到 {self.api_url}/chat/completions")
+            logger.info(f"使用模型: {self.model}")
+            logger.info(f"API密钥前4位: {self.api_key[:4]}****")
+            
+            async with httpx.AsyncClient(timeout=120.0) as client:
+                response = await client.post(
+                    f"{self.api_url}/chat/completions",
+                    headers=headers,
+                    json=payload
+                )
+                
+                # 记录完整的响应状态和正文
+                logger.info(f"API响应状态码: {response.status_code}")
+                
+                if response.status_code != 200:
+                    logger.error(f"API响应错误: {response.text}")
+                    
+                response.raise_for_status()
+                return response.json()
+        except httpx.HTTPStatusError as e:
+            logger.error(f"HTTP状态错误: {e.response.status_code} - {e.response.text}")
+            if e.response.status_code == 404:
+                raise Exception("API端点未找到,请检查API URL配置是否正确")
+            elif e.response.status_code == 401:
+                raise Exception("API密钥无效或未授权,请检查您的API密钥")
+            else:
+                raise Exception(f"API请求失败: HTTP {e.response.status_code}")
+        except httpx.RequestError as e:
+            logger.error(f"请求错误: {str(e)}")
+            raise Exception(f"无法连接到LLM API服务器: {str(e)}")
+        except Exception as e:
+            logger.error(f"chat_completion错误: {str(e)}")
+            raise Exception(f"LLM API调用失败: {str(e)}")
+
+class ArxivClient:
+    """arXiv API客户端"""
+    
+    async def search_papers(
+        self, 
+        query: str, 
+        max_results: int = 4
+    ) -> List[Dict[str, Any]]:
+        """搜索arXiv文献"""
+        try:
+            import arxiv
+            import asyncio
+            
+            # 简化查询以提高成功率
+            simplified_query = query
+            if len(query) > 100:  # 如果查询太长
+                # 只保留核心部分
+                if '"' in query:
+                    import re
+                    main_terms = re.findall(r'"([^"]*)"', query)
+                    if main_terms and len(main_terms) > 1:
+                        simplified_query = ' AND '.join([f'"{term}"' for term in main_terms[:2]])
+                    elif main_terms:
+                        simplified_query = f'"{main_terms[0]}"'
+                else:
+                    parts = query.split(' AND ')
+                    if len(parts) > 2:
+                        simplified_query = ' AND '.join(parts[:2])
+            
+            logger.info(f"使用查询: {simplified_query}")
+            
+            search = arxiv.Search(
+                query=simplified_query,
+                max_results=max_results,
+                sort_by=arxiv.SortCriterion.Relevance
+            )
+            
+            results = []
+            for paper in search.results():
+                results.append({
+                    "id": paper.get_short_id(),
+                    "title": paper.title,
+                    "authors": [author.name for author in paper.authors],
+                    "summary": paper.summary,
+                    "published": paper.published.isoformat() if paper.published else None,
+                    "updated": paper.updated.isoformat() if paper.updated else None,
+                    "link": paper.pdf_url,
+                    "source": "arxiv"
+                })
+            
+            return results
+        except Exception as e:
+            logger.error(f"Error searching arXiv: {str(e)}")
+            # 发生错误时返回空列表
+            return []

+ 3 - 0
backend/utils/init.py

@@ -0,0 +1,3 @@
+"""
+工具函数包
+"""

+ 64 - 0
backend/utils/keywords.py

@@ -0,0 +1,64 @@
+"""
+关键词提取和处理工具
+"""
+
+import re
+from typing import List, Dict, Any
+from .api_client import LLMClient
+
+async def extract_keywords(
+    research_topic: str,
+    llm_client: LLMClient
+) -> List[str]:
+    """从研究主题中提取核心关键词"""
+    prompt = f"""
+    请从以下研究主题中提取4-6个核心关键词。这些关键词应当:
+    1. 直接来源于用户文本,不要发散生成
+    2. 准确反映研究主题的核心概念
+    3. 具有学术检索价值
+    4. 优先选择专业术语、技术名词和研究领域
+    
+    研究主题: {research_topic}
+    
+    仅返回逗号分隔的关键词列表:
+    """
+    
+    response = await llm_client.generate_text(prompt, temperature=0.1)
+    
+    # 清理处理响应
+    response = re.sub(r'[\n\r]+', ',', response)
+    response = re.sub(r'\s*,\s*', ',', response)
+    response = re.sub(r'^\s+|\s+$', '', response)
+    
+    keywords = [kw.strip() for kw in response.split(',') if kw.strip()]
+    
+    # 限制数量
+    return keywords[:6]
+
+async def expand_search_queries(
+    keywords: List[str],
+    llm_client: LLMClient
+) -> List[str]:
+    """基于关键词生成研究方向"""
+    kw_text = ", ".join(keywords)
+    
+    prompt = f"""
+    基于以下关键词,生成4-6个明确、具体且各不相同的研究方向。
+    
+    关键词: {kw_text}
+    
+    每个研究方向应当:
+    1. 具体且可研究,而非笼统的话题
+    2. 聚焦不同的研究角度或应用领域
+    3. 适合作为学术研究问题
+    4. 形式为研究问题或研究主题陈述
+    
+    只需返回研究方向列表,每行一个,不要包含编号或其他解释。
+    """
+    
+    response = await llm_client.generate_text(prompt, temperature=0.7)
+    
+    # 解析研究方向
+    directions = [line.strip() for line in response.splitlines() if line.strip()]
+    
+    return directions

+ 15 - 0
frontend/mobile/README.md

@@ -0,0 +1,15 @@
+# 文献启明星 移动客户端
+
+此目录将包含移动应用客户端代码。
+
+## 计划技术栈
+- React Native / Flutter
+- 移动端UI组件库
+- 本地存储解决方案
+
+## 开发计划
+1. 设计移动端UI原型
+2. 实现基础页面结构
+3. 连接后端API
+4. 开发离线功能
+5. 实现数据同步

+ 15 - 0
frontend/web/README.md

@@ -0,0 +1,15 @@
+# 文献启明星 Web 前端
+
+此目录将包含Web应用前端代码。
+
+## 计划技术栈
+- React/Next.js
+- Tailwind CSS
+- 数据可视化库(如Plotly、D3.js)
+
+## 开发计划
+1. 设计用户界面原型
+2. 实现基础页面结构
+3. 连接后端API
+4. 开发交互功能
+5. 实现数据可视化

+ 41 - 0
lightstar-web/.gitignore

@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts

+ 36 - 0
lightstar-web/README.md

@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

+ 16 - 0
lightstar-web/eslint.config.mjs

@@ -0,0 +1,16 @@
+import { dirname } from "path";
+import { fileURLToPath } from "url";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+  baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+  ...compat.extends("next/core-web-vitals", "next/typescript"),
+];
+
+export default eslintConfig;

+ 15 - 0
lightstar-web/next.config.ts

@@ -0,0 +1,15 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+  /* config options here */
+  async rewrites() {
+    return [
+      {
+        source: '/api/:path*',
+        destination: 'http://localhost:8000/api/:path*', // 代理到后端API服务器
+      },
+    ]
+  },
+};
+
+export default nextConfig;

+ 5868 - 0
lightstar-web/package-lock.json

@@ -0,0 +1,5868 @@
+{
+  "name": "lightstar-web",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "lightstar-web",
+      "version": "0.1.0",
+      "dependencies": {
+        "@radix-ui/react-icons": "^1.3.2",
+        "autoprefixer": "^10.4.16",
+        "clsx": "^2.1.1",
+        "framer-motion": "^12.7.3",
+        "lucide-react": "^0.488.0",
+        "next": "15.3.0",
+        "postcss": "^8.4.31",
+        "react": "^19.0.0",
+        "react-dom": "^19.0.0",
+        "tailwind-merge": "^3.2.0",
+        "tailwindcss": "^3.3.0"
+      },
+      "devDependencies": {
+        "@eslint/eslintrc": "^3",
+        "@types/node": "^20",
+        "@types/react": "^19",
+        "@types/react-dom": "^19",
+        "eslint": "^9",
+        "eslint-config-next": "15.3.0",
+        "typescript": "^5"
+      }
+    },
+    "node_modules/@emnapi/core": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.4.1.tgz",
+      "integrity": "sha512-4JFstCTaToCFrPqrGzgkF8N2NHjtsaY4uRh6brZQ5L9e4wbMieX8oDT8N7qfVFTQecHFEtkj4ve49VIZ3mKVqw==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.0.1",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.4.1.tgz",
+      "integrity": "sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz",
+      "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz",
+      "integrity": "sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==",
+      "dev": true,
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.12.1",
+      "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+      "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+      "dev": true,
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/config-array": {
+      "version": "0.20.0",
+      "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.20.0.tgz",
+      "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
+      "dev": true,
+      "dependencies": {
+        "@eslint/object-schema": "^2.1.6",
+        "debug": "^4.3.1",
+        "minimatch": "^3.1.2"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/config-helpers": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
+      "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
+      "dev": true,
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/core": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.12.0.tgz",
+      "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+      "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+      "dev": true,
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^10.0.1",
+        "globals": "^14.0.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "9.24.0",
+      "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.24.0.tgz",
+      "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==",
+      "dev": true,
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/object-schema": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+      "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+      "dev": true,
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.2.8",
+      "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
+      "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
+      "dev": true,
+      "dependencies": {
+        "@eslint/core": "^0.13.0",
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.13.0.tgz",
+      "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
+      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+      "dev": true,
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.6",
+      "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.6.tgz",
+      "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+      "dev": true,
+      "dependencies": {
+        "@humanfs/core": "^0.19.1",
+        "@humanwhocodes/retry": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+      "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+      "dev": true,
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/retry": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.2.tgz",
+      "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz",
+      "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.1.0"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz",
+      "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.1.0"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
+      "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
+      "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
+      "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
+      "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-ppc64": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
+      "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
+      "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
+      "cpu": [
+        "s390x"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
+      "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
+      "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
+      "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz",
+      "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==",
+      "cpu": [
+        "arm"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.1.0"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz",
+      "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.1.0"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz",
+      "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==",
+      "cpu": [
+        "s390x"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.1.0"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz",
+      "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.1.0"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz",
+      "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.1.0"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz",
+      "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.1.0"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz",
+      "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==",
+      "cpu": [
+        "wasm32"
+      ],
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.4.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz",
+      "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==",
+      "cpu": [
+        "ia32"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz",
+      "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.8",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+      "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.2.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.25",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "0.2.8",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz",
+      "integrity": "sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "^1.4.0",
+        "@emnapi/runtime": "^1.4.0",
+        "@tybys/wasm-util": "^0.9.0"
+      }
+    },
+    "node_modules/@next/env": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/env/-/env-15.3.0.tgz",
+      "integrity": "sha512-6mDmHX24nWlHOlbwUiAOmMyY7KELimmi+ed8qWcJYjqXeC+G6JzPZ3QosOAfjNwgMIzwhXBiRiCgdh8axTTdTA=="
+    },
+    "node_modules/@next/eslint-plugin-next": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.0.tgz",
+      "integrity": "sha512-511UUcpWw5GWTyKfzW58U2F/bYJyjLE9e3SlnGK/zSXq7RqLlqFO8B9bitJjumLpj317fycC96KZ2RZsjGNfBw==",
+      "dev": true,
+      "dependencies": {
+        "fast-glob": "3.3.1"
+      }
+    },
+    "node_modules/@next/swc-darwin-arm64": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.0.tgz",
+      "integrity": "sha512-PDQcByT0ZfF2q7QR9d+PNj3wlNN4K6Q8JoHMwFyk252gWo4gKt7BF8Y2+KBgDjTFBETXZ/TkBEUY7NIIY7A/Kw==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-darwin-x64": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.0.tgz",
+      "integrity": "sha512-m+eO21yg80En8HJ5c49AOQpFDq+nP51nu88ZOMCorvw3g//8g1JSUsEiPSiFpJo1KCTQ+jm9H0hwXK49H/RmXg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-gnu": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.0.tgz",
+      "integrity": "sha512-H0Kk04ZNzb6Aq/G6e0un4B3HekPnyy6D+eUBYPJv9Abx8KDYgNMWzKt4Qhj57HXV3sTTjsfc1Trc1SxuhQB+Tg==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-musl": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.0.tgz",
+      "integrity": "sha512-k8GVkdMrh/+J9uIv/GpnHakzgDQhrprJ/FbGQvwWmstaeFG06nnAoZCJV+wO/bb603iKV1BXt4gHG+s2buJqZA==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-gnu": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.0.tgz",
+      "integrity": "sha512-ZMQ9yzDEts/vkpFLRAqfYO1wSpIJGlQNK9gZ09PgyjBJUmg8F/bb8fw2EXKgEaHbCc4gmqMpDfh+T07qUphp9A==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-musl": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.0.tgz",
+      "integrity": "sha512-RFwq5VKYTw9TMr4T3e5HRP6T4RiAzfDJ6XsxH8j/ZeYq2aLsBqCkFzwMI0FmnSsLaUbOb46Uov0VvN3UciHX5A==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-arm64-msvc": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.0.tgz",
+      "integrity": "sha512-a7kUbqa/k09xPjfCl0RSVAvEjAkYBYxUzSVAzk2ptXiNEL+4bDBo9wNC43G/osLA/EOGzG4CuNRFnQyIHfkRgQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-x64-msvc": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz",
+      "integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nolyfill/is-core-module": {
+      "version": "1.0.39",
+      "resolved": "https://registry.npmmirror.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
+      "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12.4.0"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@radix-ui/react-icons": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
+      "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
+      "peerDependencies": {
+        "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
+      }
+    },
+    "node_modules/@rtsao/scc": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz",
+      "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+      "dev": true
+    },
+    "node_modules/@rushstack/eslint-patch": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz",
+      "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==",
+      "dev": true
+    },
+    "node_modules/@swc/counter": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz",
+      "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
+    },
+    "node_modules/@swc/helpers": {
+      "version": "0.5.15",
+      "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz",
+      "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+      "dependencies": {
+        "tslib": "^2.8.0"
+      }
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
+      "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.7.tgz",
+      "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+      "dev": true
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true
+    },
+    "node_modules/@types/json5": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz",
+      "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+      "dev": true
+    },
+    "node_modules/@types/node": {
+      "version": "20.17.30",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.17.30.tgz",
+      "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==",
+      "dev": true,
+      "dependencies": {
+        "undici-types": "~6.19.2"
+      }
+    },
+    "node_modules/@types/react": {
+      "version": "19.1.2",
+      "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.1.2.tgz",
+      "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
+      "dev": true,
+      "dependencies": {
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "19.1.2",
+      "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.1.2.tgz",
+      "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
+      "dev": true,
+      "peerDependencies": {
+        "@types/react": "^19.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "8.30.1",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz",
+      "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.10.0",
+        "@typescript-eslint/scope-manager": "8.30.1",
+        "@typescript-eslint/type-utils": "8.30.1",
+        "@typescript-eslint/utils": "8.30.1",
+        "@typescript-eslint/visitor-keys": "8.30.1",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.3.1",
+        "natural-compare": "^1.4.0",
+        "ts-api-utils": "^2.0.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "8.30.1",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.30.1.tgz",
+      "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "8.30.1",
+        "@typescript-eslint/types": "8.30.1",
+        "@typescript-eslint/typescript-estree": "8.30.1",
+        "@typescript-eslint/visitor-keys": "8.30.1",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "8.30.1",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz",
+      "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "8.30.1",
+        "@typescript-eslint/visitor-keys": "8.30.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "8.30.1",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz",
+      "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "8.30.1",
+        "@typescript-eslint/utils": "8.30.1",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^2.0.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "8.30.1",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.30.1.tgz",
+      "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==",
+      "dev": true,
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "8.30.1",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz",
+      "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "8.30.1",
+        "@typescript-eslint/visitor-keys": "8.30.1",
+        "debug": "^4.3.4",
+        "fast-glob": "^3.3.2",
+        "is-glob": "^4.0.3",
+        "minimatch": "^9.0.4",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^2.0.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "8.30.1",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.30.1.tgz",
+      "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@typescript-eslint/scope-manager": "8.30.1",
+        "@typescript-eslint/types": "8.30.1",
+        "@typescript-eslint/typescript-estree": "8.30.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "8.30.1",
+      "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz",
+      "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "8.30.1",
+        "eslint-visitor-keys": "^4.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@unrs/resolver-binding-darwin-arm64": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.5.0.tgz",
+      "integrity": "sha512-YmocNlEcX/AgJv8gI41bhjMOTcKcea4D2nRIbZj+MhRtSH5+vEU8r/pFuTuoF+JjVplLsBueU+CILfBPVISyGQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-darwin-x64": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.5.0.tgz",
+      "integrity": "sha512-qpUrXgH4e/0xu1LOhPEdfgSY3vIXOxDQv370NEL8npN8h40HcQDA+Pl2r4HBW6tTXezWIjxUFcP7tj529RZtDw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-freebsd-x64": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.5.0.tgz",
+      "integrity": "sha512-3tX8r8vgjvZzaJZB4jvxUaaFCDCb3aWDCpZN3EjhGnnwhztslI05KSG5NY/jNjlcZ5QWZ7dEZZ/rNBFsmTaSPw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.5.0.tgz",
+      "integrity": "sha512-FH+ixzBKaUU9fWOj3TYO+Yn/eO6kYvMLV9eNJlJlkU7OgrxkCmiMS6wUbyT0KA3FOZGxnEQ2z3/BHgYm2jqeLA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.5.0.tgz",
+      "integrity": "sha512-pxCgXMgwB/4PfqFQg73lMhmWwcC0j5L+dNXhZoz/0ek0iS/oAWl65fxZeT/OnU7fVs52MgdP2q02EipqJJXHSg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.5.0.tgz",
+      "integrity": "sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.5.0.tgz",
+      "integrity": "sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.5.0.tgz",
+      "integrity": "sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.5.0.tgz",
+      "integrity": "sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.5.0.tgz",
+      "integrity": "sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.5.0.tgz",
+      "integrity": "sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.5.0.tgz",
+      "integrity": "sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-wasm32-wasi": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.5.0.tgz",
+      "integrity": "sha512-LtJMhwu7avhoi+kKfAZOKN773RtzLBVVF90YJbB0wyMpUj9yQPeA+mteVUI9P70OG/opH47FeV5AWeaNWWgqJg==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "@napi-rs/wasm-runtime": "^0.2.8"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.5.0.tgz",
+      "integrity": "sha512-FTZBxLL4SO1mgIM86KykzJmPeTPisBDHQV6xtfDXbTMrentuZ6SdQKJUV5BWaoUK3p8kIULlrCcucqdCnk8Npg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.5.0.tgz",
+      "integrity": "sha512-i5bB7vJ1waUsFciU/FKLd4Zw0VnAkvhiJ4//jYQXyDUuiLKodmtQZVTcOPU7pp97RrNgCFtXfC1gnvj/DHPJTw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.5.0.tgz",
+      "integrity": "sha512-wAvXp4k7jhioi4SebXW/yfzzYwsUCr9kIX4gCsUFKpCTUf8Mi7vScJXI3S+kupSUf0LbVHudR8qBbe2wFMSNUw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/acorn": {
+      "version": "8.14.1",
+      "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz",
+      "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+      "dev": true,
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true
+    },
+    "node_modules/aria-query": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz",
+      "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/array-buffer-byte-length": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+      "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "is-array-buffer": "^3.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array-includes": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.8.tgz",
+      "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "is-string": "^1.0.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.findlast": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmmirror.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+      "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.findlastindex": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+      "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.9",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "es-shim-unscopables": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flat": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+      "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flatmap": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+      "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.tosorted": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+      "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.3",
+        "es-errors": "^1.3.0",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/arraybuffer.prototype.slice": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+      "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+      "dev": true,
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.1",
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "is-array-buffer": "^3.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/ast-types-flow": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmmirror.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+      "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+      "dev": true
+    },
+    "node_modules/async-function": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz",
+      "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.4.16",
+      "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.16.tgz",
+      "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "browserslist": "^4.21.10",
+        "caniuse-lite": "^1.0.30001538",
+        "fraction.js": "^4.3.6",
+        "normalize-range": "^0.1.2",
+        "picocolors": "^1.0.0",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/available-typed-arrays": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+      "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+      "dev": true,
+      "dependencies": {
+        "possible-typed-array-names": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/axe-core": {
+      "version": "4.10.3",
+      "resolved": "https://registry.npmmirror.com/axe-core/-/axe-core-4.10.3.tgz",
+      "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/axobject-query": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz",
+      "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.24.4",
+      "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.24.4.tgz",
+      "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/busboy": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz",
+      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+      "dependencies": {
+        "streamsearch": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      }
+    },
+    "node_modules/call-bind": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+      "dev": true,
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001713",
+      "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
+      "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ]
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/client-only": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz",
+      "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
+    },
+    "node_modules/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/color": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/color/-/color-4.2.3.tgz",
+      "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+      "optional": true,
+      "dependencies": {
+        "color-convert": "^2.0.1",
+        "color-string": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=12.5.0"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/color-string": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz",
+      "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+      "optional": true,
+      "dependencies": {
+        "color-name": "^1.0.0",
+        "simple-swizzle": "^0.2.2"
+      }
+    },
+    "node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+      "dev": true
+    },
+    "node_modules/damerau-levenshtein": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+      "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+      "dev": true
+    },
+    "node_modules/data-view-buffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+      "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/data-view-byte-length": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+      "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/inspect-js"
+      }
+    },
+    "node_modules/data-view-byte-offset": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+      "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz",
+      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+      "dev": true,
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true
+    },
+    "node_modules/define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "dev": true,
+      "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/define-properties": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+      "dev": true,
+      "dependencies": {
+        "define-data-property": "^1.0.1",
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.3.tgz",
+      "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+    },
+    "node_modules/doctrine": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz",
+      "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+      "dev": true,
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dev": true,
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.137",
+      "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
+      "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA=="
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+    },
+    "node_modules/es-abstract": {
+      "version": "1.23.9",
+      "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.23.9.tgz",
+      "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==",
+      "dev": true,
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.2",
+        "arraybuffer.prototype.slice": "^1.0.4",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "data-view-buffer": "^1.0.2",
+        "data-view-byte-length": "^1.0.2",
+        "data-view-byte-offset": "^1.0.1",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "es-set-tostringtag": "^2.1.0",
+        "es-to-primitive": "^1.3.0",
+        "function.prototype.name": "^1.1.8",
+        "get-intrinsic": "^1.2.7",
+        "get-proto": "^1.0.0",
+        "get-symbol-description": "^1.1.0",
+        "globalthis": "^1.0.4",
+        "gopd": "^1.2.0",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "internal-slot": "^1.1.0",
+        "is-array-buffer": "^3.0.5",
+        "is-callable": "^1.2.7",
+        "is-data-view": "^1.0.2",
+        "is-regex": "^1.2.1",
+        "is-shared-array-buffer": "^1.0.4",
+        "is-string": "^1.1.1",
+        "is-typed-array": "^1.1.15",
+        "is-weakref": "^1.1.0",
+        "math-intrinsics": "^1.1.0",
+        "object-inspect": "^1.13.3",
+        "object-keys": "^1.1.1",
+        "object.assign": "^4.1.7",
+        "own-keys": "^1.0.1",
+        "regexp.prototype.flags": "^1.5.3",
+        "safe-array-concat": "^1.1.3",
+        "safe-push-apply": "^1.0.0",
+        "safe-regex-test": "^1.1.0",
+        "set-proto": "^1.0.0",
+        "string.prototype.trim": "^1.2.10",
+        "string.prototype.trimend": "^1.0.9",
+        "string.prototype.trimstart": "^1.0.8",
+        "typed-array-buffer": "^1.0.3",
+        "typed-array-byte-length": "^1.0.3",
+        "typed-array-byte-offset": "^1.0.4",
+        "typed-array-length": "^1.0.7",
+        "unbox-primitive": "^1.1.0",
+        "which-typed-array": "^1.1.18"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-iterator-helpers": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
+      "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.6",
+        "es-errors": "^1.3.0",
+        "es-set-tostringtag": "^2.0.3",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.6",
+        "globalthis": "^1.0.4",
+        "gopd": "^1.2.0",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "internal-slot": "^1.1.0",
+        "iterator.prototype": "^1.1.4",
+        "safe-array-concat": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-shim-unscopables": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+      "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+      "dev": true,
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-to-primitive": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+      "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+      "dev": true,
+      "dependencies": {
+        "is-callable": "^1.2.7",
+        "is-date-object": "^1.0.5",
+        "is-symbol": "^1.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "9.24.0",
+      "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.24.0.tgz",
+      "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.12.1",
+        "@eslint/config-array": "^0.20.0",
+        "@eslint/config-helpers": "^0.2.0",
+        "@eslint/core": "^0.12.0",
+        "@eslint/eslintrc": "^3.3.1",
+        "@eslint/js": "9.24.0",
+        "@eslint/plugin-kit": "^0.2.7",
+        "@humanfs/node": "^0.16.6",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@humanwhocodes/retry": "^0.4.2",
+        "@types/estree": "^1.0.6",
+        "@types/json-schema": "^7.0.15",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "debug": "^4.3.2",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^8.3.0",
+        "eslint-visitor-keys": "^4.2.0",
+        "espree": "^10.3.0",
+        "esquery": "^1.5.0",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^8.0.0",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "jiti": "*"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-config-next": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-15.3.0.tgz",
+      "integrity": "sha512-+Z3M1W9MnJjX3W4vI9CHfKlEyhTWOUHvc5dB89FyRnzPsUkJlLWZOi8+1pInuVcSztSM4MwBFB0hIHf4Rbwu4g==",
+      "dev": true,
+      "dependencies": {
+        "@next/eslint-plugin-next": "15.3.0",
+        "@rushstack/eslint-patch": "^1.10.3",
+        "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "eslint-import-resolver-node": "^0.3.6",
+        "eslint-import-resolver-typescript": "^3.5.2",
+        "eslint-plugin-import": "^2.31.0",
+        "eslint-plugin-jsx-a11y": "^6.10.0",
+        "eslint-plugin-react": "^7.37.0",
+        "eslint-plugin-react-hooks": "^5.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0",
+        "typescript": ">=3.3.1"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-import-resolver-node": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+      "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+      "dev": true,
+      "dependencies": {
+        "debug": "^3.2.7",
+        "is-core-module": "^2.13.0",
+        "resolve": "^1.22.4"
+      }
+    },
+    "node_modules/eslint-import-resolver-node/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-import-resolver-typescript": {
+      "version": "3.10.0",
+      "resolved": "https://registry.npmmirror.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.0.tgz",
+      "integrity": "sha512-aV3/dVsT0/H9BtpNwbaqvl+0xGMRGzncLyhm793NFGvbwGGvzyAykqWZ8oZlZuGwuHkwJjhWJkG1cM3ynvd2pQ==",
+      "dev": true,
+      "dependencies": {
+        "@nolyfill/is-core-module": "1.0.39",
+        "debug": "^4.4.0",
+        "get-tsconfig": "^4.10.0",
+        "is-bun-module": "^2.0.0",
+        "stable-hash": "^0.0.5",
+        "tinyglobby": "^0.2.12",
+        "unrs-resolver": "^1.3.2"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint-import-resolver-typescript"
+      },
+      "peerDependencies": {
+        "eslint": "*",
+        "eslint-plugin-import": "*",
+        "eslint-plugin-import-x": "*"
+      },
+      "peerDependenciesMeta": {
+        "eslint-plugin-import": {
+          "optional": true
+        },
+        "eslint-plugin-import-x": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-module-utils": {
+      "version": "2.12.0",
+      "resolved": "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
+      "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
+      "dev": true,
+      "dependencies": {
+        "debug": "^3.2.7"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependenciesMeta": {
+        "eslint": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-module-utils/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-plugin-import": {
+      "version": "2.31.0",
+      "resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
+      "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
+      "dev": true,
+      "dependencies": {
+        "@rtsao/scc": "^1.1.0",
+        "array-includes": "^3.1.8",
+        "array.prototype.findlastindex": "^1.2.5",
+        "array.prototype.flat": "^1.3.2",
+        "array.prototype.flatmap": "^1.3.2",
+        "debug": "^3.2.7",
+        "doctrine": "^2.1.0",
+        "eslint-import-resolver-node": "^0.3.9",
+        "eslint-module-utils": "^2.12.0",
+        "hasown": "^2.0.2",
+        "is-core-module": "^2.15.1",
+        "is-glob": "^4.0.3",
+        "minimatch": "^3.1.2",
+        "object.fromentries": "^2.0.8",
+        "object.groupby": "^1.0.3",
+        "object.values": "^1.2.0",
+        "semver": "^6.3.1",
+        "string.prototype.trimend": "^1.0.8",
+        "tsconfig-paths": "^3.15.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependencies": {
+        "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/eslint-plugin-jsx-a11y": {
+      "version": "6.10.2",
+      "resolved": "https://registry.npmmirror.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
+      "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
+      "dev": true,
+      "dependencies": {
+        "aria-query": "^5.3.2",
+        "array-includes": "^3.1.8",
+        "array.prototype.flatmap": "^1.3.2",
+        "ast-types-flow": "^0.0.8",
+        "axe-core": "^4.10.0",
+        "axobject-query": "^4.1.0",
+        "damerau-levenshtein": "^1.0.8",
+        "emoji-regex": "^9.2.2",
+        "hasown": "^2.0.2",
+        "jsx-ast-utils": "^3.3.5",
+        "language-tags": "^1.0.9",
+        "minimatch": "^3.1.2",
+        "object.fromentries": "^2.0.8",
+        "safe-regex-test": "^1.0.3",
+        "string.prototype.includes": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependencies": {
+        "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+      }
+    },
+    "node_modules/eslint-plugin-react": {
+      "version": "7.37.5",
+      "resolved": "https://registry.npmmirror.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+      "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+      "dev": true,
+      "dependencies": {
+        "array-includes": "^3.1.8",
+        "array.prototype.findlast": "^1.2.5",
+        "array.prototype.flatmap": "^1.3.3",
+        "array.prototype.tosorted": "^1.1.4",
+        "doctrine": "^2.1.0",
+        "es-iterator-helpers": "^1.2.1",
+        "estraverse": "^5.3.0",
+        "hasown": "^2.0.2",
+        "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+        "minimatch": "^3.1.2",
+        "object.entries": "^1.1.9",
+        "object.fromentries": "^2.0.8",
+        "object.values": "^1.2.1",
+        "prop-types": "^15.8.1",
+        "resolve": "^2.0.0-next.5",
+        "semver": "^6.3.1",
+        "string.prototype.matchall": "^4.0.12",
+        "string.prototype.repeat": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependencies": {
+        "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+      }
+    },
+    "node_modules/eslint-plugin-react-hooks": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+      "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-react/node_modules/resolve": {
+      "version": "2.0.0-next.5",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.5.tgz",
+      "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+      "dev": true,
+      "dependencies": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/eslint-plugin-react/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.3.0.tgz",
+      "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
+      "dev": true,
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "dev": true,
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmmirror.com/espree/-/espree-10.3.0.tgz",
+      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+      "dev": true,
+      "dependencies": {
+        "acorn": "^8.14.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^4.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+      "dev": true,
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz",
+      "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true
+    },
+    "node_modules/fastq": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz",
+      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "dev": true,
+      "dependencies": {
+        "flat-cache": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "dev": true,
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz",
+      "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+      "dev": true
+    },
+    "node_modules/for-each": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz",
+      "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+      "dev": true,
+      "dependencies": {
+        "is-callable": "^1.2.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/fraction.js": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz",
+      "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "patreon",
+        "url": "https://github.com/sponsors/rawify"
+      }
+    },
+    "node_modules/framer-motion": {
+      "version": "12.7.3",
+      "resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.7.3.tgz",
+      "integrity": "sha512-dNT4l5gEnUo2ytXLUBUf6AI21dZ77TMclDKE3ElaIHZ8m90nJ/NCcExW51zdSIaS0RhAS5iXcF7bEIxZe8XG2g==",
+      "dependencies": {
+        "motion-dom": "^12.7.3",
+        "motion-utils": "^12.7.2",
+        "tslib": "^2.4.0"
+      },
+      "peerDependencies": {
+        "@emotion/is-prop-valid": "*",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/is-prop-valid": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/function.prototype.name": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+      "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "functions-have-names": "^1.2.3",
+        "hasown": "^2.0.2",
+        "is-callable": "^1.2.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/functions-have-names": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz",
+      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dev": true,
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/get-symbol-description": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+      "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-tsconfig": {
+      "version": "4.10.0",
+      "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz",
+      "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==",
+      "dev": true,
+      "dependencies": {
+        "resolve-pkg-maps": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/globals": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz",
+      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/globalthis": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz",
+      "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+      "dev": true,
+      "dependencies": {
+        "define-properties": "^1.2.1",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true
+    },
+    "node_modules/has-bigints": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz",
+      "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "dev": true,
+      "dependencies": {
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-proto": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz",
+      "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+      "dev": true,
+      "dependencies": {
+        "dunder-proto": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dev": true,
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "dev": true,
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/internal-slot": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
+      "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "hasown": "^2.0.2",
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/is-array-buffer": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+      "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.2.tgz",
+      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+      "optional": true
+    },
+    "node_modules/is-async-function": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz",
+      "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+      "dev": true,
+      "dependencies": {
+        "async-function": "^1.0.0",
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.1",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-bigint": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz",
+      "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+      "dev": true,
+      "dependencies": {
+        "has-bigints": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-boolean-object": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+      "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-bun-module": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/is-bun-module/-/is-bun-module-2.0.0.tgz",
+      "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
+      "dev": true,
+      "dependencies": {
+        "semver": "^7.7.1"
+      }
+    },
+    "node_modules/is-callable": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz",
+      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-data-view": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz",
+      "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "is-typed-array": "^1.1.13"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-date-object": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz",
+      "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-finalizationregistry": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+      "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-generator-function": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.0.tgz",
+      "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.0",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-map": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz",
+      "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-number-object": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz",
+      "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-regex": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
+      "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-set": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz",
+      "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-shared-array-buffer": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+      "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-string": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz",
+      "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-symbol": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz",
+      "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-typed-array": {
+      "version": "1.1.15",
+      "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz",
+      "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+      "dev": true,
+      "dependencies": {
+        "which-typed-array": "^1.1.16"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakmap": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz",
+      "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakref": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz",
+      "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakset": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz",
+      "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
+      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+      "dev": true
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "node_modules/iterator.prototype": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+      "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+      "dev": true,
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.6",
+        "get-proto": "^1.0.0",
+        "has-symbols": "^1.1.0",
+        "set-function-name": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.4.2.tgz",
+      "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "bin": {
+        "jiti": "lib/jiti-cli.mjs"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true
+    },
+    "node_modules/json5": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz",
+      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+      "dev": true,
+      "dependencies": {
+        "minimist": "^1.2.0"
+      },
+      "bin": {
+        "json5": "lib/cli.js"
+      }
+    },
+    "node_modules/jsx-ast-utils": {
+      "version": "3.3.5",
+      "resolved": "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+      "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+      "dev": true,
+      "dependencies": {
+        "array-includes": "^3.1.6",
+        "array.prototype.flat": "^1.3.1",
+        "object.assign": "^4.1.4",
+        "object.values": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/language-subtag-registry": {
+      "version": "0.3.23",
+      "resolved": "https://registry.npmmirror.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+      "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
+      "dev": true
+    },
+    "node_modules/language-tags": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmmirror.com/language-tags/-/language-tags-1.0.9.tgz",
+      "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+      "dev": true,
+      "dependencies": {
+        "language-subtag-registry": "^0.3.20"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/lilconfig": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz",
+      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "dev": true,
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+    },
+    "node_modules/lucide-react": {
+      "version": "0.488.0",
+      "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.488.0.tgz",
+      "integrity": "sha512-ronlL0MyKut4CEzBY/ai2ZpKPxyWO4jUqdAkm2GNK5Zn3Rj+swDz+3lvyAUXN0PNqPKIX6XM9Xadwz/skLs/pQ==",
+      "peerDependencies": {
+        "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/motion-dom": {
+      "version": "12.7.3",
+      "resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.7.3.tgz",
+      "integrity": "sha512-IjMt1YJHrvyvruFvmpmd6bGXXGCvmygrnvSb3aZ8KhOzF4H3PulU+cMBzH+U8TBJHjC/mnmJFRIA1Cu4vBfcBA==",
+      "dependencies": {
+        "motion-utils": "^12.7.2"
+      }
+    },
+    "node_modules/motion-utils": {
+      "version": "12.7.2",
+      "resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.7.2.tgz",
+      "integrity": "sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw=="
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true
+    },
+    "node_modules/next": {
+      "version": "15.3.0",
+      "resolved": "https://registry.npmmirror.com/next/-/next-15.3.0.tgz",
+      "integrity": "sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==",
+      "dependencies": {
+        "@next/env": "15.3.0",
+        "@swc/counter": "0.1.3",
+        "@swc/helpers": "0.5.15",
+        "busboy": "1.6.0",
+        "caniuse-lite": "^1.0.30001579",
+        "postcss": "8.4.31",
+        "styled-jsx": "5.1.6"
+      },
+      "bin": {
+        "next": "dist/bin/next"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
+      },
+      "optionalDependencies": {
+        "@next/swc-darwin-arm64": "15.3.0",
+        "@next/swc-darwin-x64": "15.3.0",
+        "@next/swc-linux-arm64-gnu": "15.3.0",
+        "@next/swc-linux-arm64-musl": "15.3.0",
+        "@next/swc-linux-x64-gnu": "15.3.0",
+        "@next/swc-linux-x64-musl": "15.3.0",
+        "@next/swc-win32-arm64-msvc": "15.3.0",
+        "@next/swc-win32-x64-msvc": "15.3.0",
+        "sharp": "^0.34.1"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.1.0",
+        "@playwright/test": "^1.41.2",
+        "babel-plugin-react-compiler": "*",
+        "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+        "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+        "sass": "^1.3.0"
+      },
+      "peerDependenciesMeta": {
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@playwright/test": {
+          "optional": true
+        },
+        "babel-plugin-react-compiler": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.19",
+      "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.assign": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz",
+      "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0",
+        "has-symbols": "^1.1.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object.entries": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz",
+      "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.fromentries": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz",
+      "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object.groupby": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.3.tgz",
+      "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.values": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz",
+      "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/own-keys": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz",
+      "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+      "dev": true,
+      "dependencies": {
+        "get-intrinsic": "^1.2.6",
+        "object-keys": "^1.1.1",
+        "safe-push-apply": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz",
+      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/possible-typed-array-names": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+      "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.4.31",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz",
+      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.6",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-14.1.0.tgz",
+      "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz",
+      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.0.0.tgz",
+      "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==",
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.10"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/prop-types": {
+      "version": "15.8.1",
+      "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz",
+      "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+      "dev": true,
+      "dependencies": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.13.1"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/quick-lru": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz",
+      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/react": {
+      "version": "19.1.0",
+      "resolved": "https://registry.npmmirror.com/react/-/react-19.1.0.tgz",
+      "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.1.0",
+      "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.1.0.tgz",
+      "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
+      "dependencies": {
+        "scheduler": "^0.26.0"
+      },
+      "peerDependencies": {
+        "react": "^19.1.0"
+      }
+    },
+    "node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+      "dev": true
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/reflect.getprototypeof": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+      "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.9",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.7",
+        "get-proto": "^1.0.1",
+        "which-builtin-type": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/regexp.prototype.flags": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+      "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "set-function-name": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.10",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz",
+      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+      "dependencies": {
+        "is-core-module": "^2.16.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safe-array-concat": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+      "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "has-symbols": "^1.1.0",
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">=0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safe-push-apply": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+      "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safe-regex-test": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+      "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-regex": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.26.0",
+      "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.26.0.tgz",
+      "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="
+    },
+    "node_modules/semver": {
+      "version": "7.7.1",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "devOptional": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/set-function-length": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+      "dev": true,
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/set-function-name": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz",
+      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+      "dev": true,
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "functions-have-names": "^1.2.3",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/set-proto": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz",
+      "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+      "dev": true,
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/sharp": {
+      "version": "0.34.1",
+      "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.34.1.tgz",
+      "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==",
+      "hasInstallScript": true,
+      "optional": true,
+      "dependencies": {
+        "color": "^4.2.3",
+        "detect-libc": "^2.0.3",
+        "semver": "^7.7.1"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.34.1",
+        "@img/sharp-darwin-x64": "0.34.1",
+        "@img/sharp-libvips-darwin-arm64": "1.1.0",
+        "@img/sharp-libvips-darwin-x64": "1.1.0",
+        "@img/sharp-libvips-linux-arm": "1.1.0",
+        "@img/sharp-libvips-linux-arm64": "1.1.0",
+        "@img/sharp-libvips-linux-ppc64": "1.1.0",
+        "@img/sharp-libvips-linux-s390x": "1.1.0",
+        "@img/sharp-libvips-linux-x64": "1.1.0",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
+        "@img/sharp-libvips-linuxmusl-x64": "1.1.0",
+        "@img/sharp-linux-arm": "0.34.1",
+        "@img/sharp-linux-arm64": "0.34.1",
+        "@img/sharp-linux-s390x": "0.34.1",
+        "@img/sharp-linux-x64": "0.34.1",
+        "@img/sharp-linuxmusl-arm64": "0.34.1",
+        "@img/sharp-linuxmusl-x64": "0.34.1",
+        "@img/sharp-wasm32": "0.34.1",
+        "@img/sharp-win32-ia32": "0.34.1",
+        "@img/sharp-win32-x64": "0.34.1"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/simple-swizzle": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+      "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+      "optional": true,
+      "dependencies": {
+        "is-arrayish": "^0.3.1"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stable-hash": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmmirror.com/stable-hash/-/stable-hash-0.0.5.tgz",
+      "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
+      "dev": true
+    },
+    "node_modules/streamsearch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz",
+      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string.prototype.includes": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
+      "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/string.prototype.matchall": {
+      "version": "4.0.12",
+      "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+      "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.6",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.6",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "internal-slot": "^1.1.0",
+        "regexp.prototype.flags": "^1.5.3",
+        "set-function-name": "^2.0.2",
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/string.prototype.repeat": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+      "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+      "dev": true,
+      "dependencies": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.5"
+      }
+    },
+    "node_modules/string.prototype.trim": {
+      "version": "1.2.10",
+      "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+      "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "define-data-property": "^1.1.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-object-atoms": "^1.0.0",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/string.prototype.trimend": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+      "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/string.prototype.trimstart": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+      "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/styled-jsx": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.6.tgz",
+      "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+      "dependencies": {
+        "client-only": "0.0.1"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "peerDependencies": {
+        "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@babel/core": {
+          "optional": true
+        },
+        "babel-plugin-macros": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/sucrase": {
+      "version": "3.35.0",
+      "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.0.tgz",
+      "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "glob": "^10.3.10",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwind-merge": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.2.0.tgz",
+      "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/dcastil"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.0.tgz",
+      "integrity": "sha512-hOXlFx+YcklJ8kXiCAfk/FMyr4Pm9ck477G0m/us2344Vuj355IpoEDB5UmGAsSpTBmr+4ZhjzW04JuFXkb/fw==",
+      "dependencies": {
+        "arg": "^5.0.2",
+        "chokidar": "^3.5.3",
+        "color-name": "^1.1.4",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.2.12",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.17.2",
+        "lilconfig": "^2.0.6",
+        "micromatch": "^4.0.5",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.0.0",
+        "postcss": "^8.0.9",
+        "postcss-import": "^14.1.0",
+        "postcss-js": "^4.0.0",
+        "postcss-load-config": "^3.1.4",
+        "postcss-nested": "6.0.0",
+        "postcss-selector-parser": "^6.0.11",
+        "postcss-value-parser": "^4.2.0",
+        "quick-lru": "^5.1.1",
+        "resolve": "^1.22.1",
+        "sucrase": "^3.29.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=12.13.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.9"
+      }
+    },
+    "node_modules/tailwindcss/node_modules/jiti": {
+      "version": "1.21.7",
+      "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
+      "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/tailwindcss/node_modules/postcss-load-config": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
+      "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
+      "dependencies": {
+        "lilconfig": "^2.0.5",
+        "yaml": "^1.10.2"
+      },
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": ">=8.0.9",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "postcss": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.12",
+      "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.12.tgz",
+      "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
+      "dev": true,
+      "dependencies": {
+        "fdir": "^6.4.3",
+        "picomatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyglobby/node_modules/fdir": {
+      "version": "6.4.3",
+      "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.3.tgz",
+      "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
+      "dev": true,
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
+      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+      "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=18.12"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4"
+      }
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+    },
+    "node_modules/tsconfig-paths": {
+      "version": "3.15.0",
+      "resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+      "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.2",
+        "minimist": "^1.2.6",
+        "strip-bom": "^3.0.0"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/typed-array-buffer": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+      "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-typed-array": "^1.1.14"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/typed-array-byte-length": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+      "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "for-each": "^0.3.3",
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.14"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/typed-array-byte-offset": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+      "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+      "dev": true,
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "for-each": "^0.3.3",
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.15",
+        "reflect.getprototypeof": "^1.0.9"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/typed-array-length": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.7.tgz",
+      "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "is-typed-array": "^1.1.13",
+        "possible-typed-array-names": "^1.0.0",
+        "reflect.getprototypeof": "^1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.8.3",
+      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+      "dev": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/unbox-primitive": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+      "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-bigints": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "which-boxed-primitive": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.19.8",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz",
+      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+      "dev": true
+    },
+    "node_modules/unrs-resolver": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.5.0.tgz",
+      "integrity": "sha512-6aia3Oy7SEe0MuUGQm2nsyob0L2+g57w178K5SE/3pvSGAIp28BB2O921fKx424Ahc/gQ6v0DXFbhcpyhGZdOA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/JounQin"
+      },
+      "optionalDependencies": {
+        "@unrs/resolver-binding-darwin-arm64": "1.5.0",
+        "@unrs/resolver-binding-darwin-x64": "1.5.0",
+        "@unrs/resolver-binding-freebsd-x64": "1.5.0",
+        "@unrs/resolver-binding-linux-arm-gnueabihf": "1.5.0",
+        "@unrs/resolver-binding-linux-arm-musleabihf": "1.5.0",
+        "@unrs/resolver-binding-linux-arm64-gnu": "1.5.0",
+        "@unrs/resolver-binding-linux-arm64-musl": "1.5.0",
+        "@unrs/resolver-binding-linux-ppc64-gnu": "1.5.0",
+        "@unrs/resolver-binding-linux-riscv64-gnu": "1.5.0",
+        "@unrs/resolver-binding-linux-s390x-gnu": "1.5.0",
+        "@unrs/resolver-binding-linux-x64-gnu": "1.5.0",
+        "@unrs/resolver-binding-linux-x64-musl": "1.5.0",
+        "@unrs/resolver-binding-wasm32-wasi": "1.5.0",
+        "@unrs/resolver-binding-win32-arm64-msvc": "1.5.0",
+        "@unrs/resolver-binding-win32-ia32-msvc": "1.5.0",
+        "@unrs/resolver-binding-win32-x64-msvc": "1.5.0"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+      "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which-boxed-primitive": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+      "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+      "dev": true,
+      "dependencies": {
+        "is-bigint": "^1.1.0",
+        "is-boolean-object": "^1.2.1",
+        "is-number-object": "^1.1.1",
+        "is-string": "^1.1.1",
+        "is-symbol": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-builtin-type": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+      "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+      "dev": true,
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "function.prototype.name": "^1.1.6",
+        "has-tostringtag": "^1.0.2",
+        "is-async-function": "^2.0.0",
+        "is-date-object": "^1.1.0",
+        "is-finalizationregistry": "^1.1.0",
+        "is-generator-function": "^1.0.10",
+        "is-regex": "^1.2.1",
+        "is-weakref": "^1.0.2",
+        "isarray": "^2.0.5",
+        "which-boxed-primitive": "^1.1.0",
+        "which-collection": "^1.0.2",
+        "which-typed-array": "^1.1.16"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-collection": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz",
+      "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+      "dev": true,
+      "dependencies": {
+        "is-map": "^2.0.3",
+        "is-set": "^2.0.3",
+        "is-weakmap": "^2.0.2",
+        "is-weakset": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-typed-array": {
+      "version": "1.1.19",
+      "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz",
+      "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+      "dev": true,
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "for-each": "^0.3.5",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/yaml": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmmirror.com/yaml/-/yaml-1.10.2.tgz",
+      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  }
+}

+ 33 - 0
lightstar-web/package.json

@@ -0,0 +1,33 @@
+{
+  "name": "lightstar-web",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "next dev",
+    "build": "next build",
+    "start": "next start",
+    "lint": "next lint"
+  },
+  "dependencies": {
+    "@radix-ui/react-icons": "^1.3.2",
+    "autoprefixer": "^10.4.16",
+    "clsx": "^2.1.1",
+    "framer-motion": "^12.7.3",
+    "lucide-react": "^0.488.0",
+    "next": "15.3.0",
+    "postcss": "^8.4.31",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0",
+    "tailwind-merge": "^3.2.0",
+    "tailwindcss": "^3.3.0"
+  },
+  "devDependencies": {
+    "@eslint/eslintrc": "^3",
+    "@types/node": "^20",
+    "@types/react": "^19",
+    "@types/react-dom": "^19",
+    "eslint": "^9",
+    "eslint-config-next": "15.3.0",
+    "typescript": "^5"
+  }
+}

+ 6 - 0
lightstar-web/postcss.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+};

+ 1 - 0
lightstar-web/public/file.svg

@@ -0,0 +1 @@
+<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

+ 1 - 0
lightstar-web/public/globe.svg

@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

+ 1 - 0
lightstar-web/public/next.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

+ 1 - 0
lightstar-web/public/vercel.svg

@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

+ 1 - 0
lightstar-web/public/window.svg

@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

+ 337 - 0
lightstar-web/src/app/ai-chat/page.tsx

@@ -0,0 +1,337 @@
+"use client";
+
+import { GlassCard } from "@/components/ui/glass-card";
+import { Button } from "@/components/ui/button";
+import { useState, useRef, useEffect } from "react";
+
+// 消息类型定义
+interface Message {
+  id: string;
+  content: string;
+  role: "user" | "assistant" | "system";
+  timestamp: Date;
+}
+
+// 系统预设消息 - 定义AI助手的角色和功能
+const SYSTEM_PROMPT = `你是"文献启明星"科研助手系统中的AI顾问,专为科研人员、学者和学生设计。
+你的主要职责包括:
+1. 解答学术和研究方法相关问题
+2. 提供文献检索和分析建议
+3. 解释复杂的科研概念和理论
+4. 帮助分析研究方向和趋势
+5. 辅助论文写作和结构规划
+
+你拥有丰富的科研知识,可以帮助用户提高研究效率。在回答问题时,你应保持严谨、专业的态度,同时用友好的语气与用户交流。
+
+文献启明星系统的主要功能:
+- 文献研究:智能分析研究领域,生成研究方向报告,查找相关论文
+- AI聊天:即你现在提供的服务,解答学术问题,提供研究建议
+- 论文分析:自动提取论文关键信息,生成摘要和解读,推荐相关文献
+
+请记住,你是用户科研工作的助手,目标是帮助他们更高效地完成研究工作。`;
+
+// 添加重试逻辑
+const fetchWithRetry = async (
+  url: string, 
+  options: RequestInit, 
+  maxRetries = 3
+): Promise<Response> => {
+  let lastError;
+  
+  for (let i = 0; i < maxRetries; i++) {
+    try {
+      return await fetch(url, options);
+    } catch (error) {
+      console.log(`请求失败,尝试重试 ${i + 1}/${maxRetries}`);
+      lastError = error;
+      // 指数退避策略
+      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
+    }
+  }
+  
+  throw lastError;
+};
+
+export default function AIChatPage() {
+  // 状态管理
+  const [messages, setMessages] = useState<Message[]>([
+    {
+      id: "system-message",
+      content: SYSTEM_PROMPT,
+      role: "system",
+      timestamp: new Date(),
+    },
+    {
+      id: "welcome-message",
+      content: "您好!我是文献启明星的AI研究助手。作为您的科研伙伴,我可以帮助您解答学术问题、分析研究方向、提供文献建议,以及辅助论文写作。请问今天有什么我可以协助您的研究工作吗?",
+      role: "assistant",
+      timestamp: new Date(),
+    },
+  ]);
+  const [inputText, setInputText] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [visibleMessages, setVisibleMessages] = useState<Message[]>([]);
+  
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+  const chatContainerRef = useRef<HTMLDivElement>(null);
+  
+  // 过滤系统消息,只显示用户和助手的消息
+  useEffect(() => {
+    setVisibleMessages(messages.filter(msg => msg.role !== "system"));
+  }, [messages]);
+  
+  // 自动滚动到最新消息
+  useEffect(() => {
+    if (messagesEndRef.current) {
+      messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
+    }
+  }, [visibleMessages]);
+  
+  // 发送消息
+  const sendMessage = async () => {
+    if (!inputText.trim() || isLoading) return;
+    
+    // 清除之前的错误
+    setError(null);
+    
+    // 创建用户消息
+    const userMessage: Message = {
+      id: Date.now().toString(),
+      content: inputText,
+      role: "user",
+      timestamp: new Date(),
+    };
+    
+    // 更新消息列表
+    setMessages(prev => [...prev, userMessage]);
+    setInputText("");
+    setIsLoading(true);
+    
+    // 添加超时控制
+    const timeoutPromise = new Promise((_, reject) => {
+      setTimeout(() => reject(new Error("请求超时,请检查网络连接")), 30000); // 增加到30秒
+    });
+    
+    try {
+      // 准备发送到API的消息历史(包括系统消息)
+      const apiMessages = messages
+        .concat(userMessage)
+        .map(msg => ({
+          role: msg.role,
+          content: msg.content
+        }));
+      
+      // 使用Promise.race添加超时控制
+      const response = await Promise.race([
+        fetchWithRetry("/api/chat", {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify({ messages: apiMessages })
+        }),
+        timeoutPromise
+      ]) as Response;
+      
+      if (!response.ok) {
+        let errorMessage = "服务器错误";
+        try {
+          const errorData = await response.json();
+          errorMessage = errorData.error || errorMessage;
+        } catch (e) {
+          // 解析JSON失败,使用默认错误消息
+        }
+        throw new Error(errorMessage);
+      }
+      
+      const data = await response.json();
+      
+      // 创建AI回复消息
+      const aiResponse: Message = {
+        id: (Date.now() + 1).toString(),
+        content: data.message?.content || 
+                 (data.messages && data.messages.length > 0 ? 
+                   data.messages[data.messages.length - 1].content : 
+                   "抱歉,接收到了空响应"),
+        role: "assistant",
+        timestamp: new Date(),
+      };
+      
+      setMessages(prev => [...prev, aiResponse]);
+    } catch (err) {
+      console.error("聊天请求失败:", err);
+      
+      // 获取具体的错误信息
+      const errorMsg = err instanceof Error 
+        ? err.message 
+        : "与AI服务通信失败,请检查网络连接或后端服务是否正常运行";
+      
+      // 设置错误状态
+      setError(errorMsg);
+      
+      // 添加用户友好的错误消息
+      const errorMessage: Message = {
+        id: (Date.now() + 1).toString(),
+        content: `很抱歉,发生了错误:${errorMsg}。请稍后再试,或者联系管理员检查后端服务是否正常运行。`,
+        role: "assistant",
+        timestamp: new Date(),
+      };
+      
+      setMessages(prev => [...prev, errorMessage]);
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  // 处理按Enter键发送
+  const handleKeyPress = (e: React.KeyboardEvent) => {
+    if (e.key === "Enter" && !e.shiftKey) {
+      e.preventDefault();
+      sendMessage();
+    }
+  };
+  
+  // 清空聊天记录
+  const clearChat = () => {
+    setMessages([
+      {
+        id: "system-message",
+        content: SYSTEM_PROMPT,
+        role: "system",
+        timestamp: new Date(),
+      },
+      {
+        id: "welcome-message",
+        content: "聊天已重置。我是文献启明星的AI研究助手,请问有什么我可以帮助您的?",
+        role: "assistant",
+        timestamp: new Date(),
+      }
+    ]);
+  };
+  
+  return (
+    <main className="relative flex min-h-[calc(100vh-64px)] flex-col">
+      <div className="container mx-auto px-4 pb-4 flex flex-col h-[calc(100vh-64px)]">
+        {/* 标题区域 - 减小高度并固定在顶部 */}
+        <div className="text-center py-4 flex justify-between items-center">
+          <div className="flex-1"></div>
+          <div className="flex-2 text-center">
+            <h1 className="text-3xl font-bold text-center text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 drop-shadow-lg">
+              AI 智能助手
+            </h1>
+            <p className="text-sm text-center text-white/80">
+              您的个人研究与学习伙伴
+            </p>
+            <div className="w-12 h-1 mx-auto mt-2 bg-gradient-to-r from-[var(--theme-accent)] to-[var(--theme-highlight)] rounded-full"></div>
+          </div>
+          <div className="flex-1 flex justify-end">
+            <button
+              onClick={clearChat}
+              className="text-white/70 hover:text-white text-sm py-1 px-3 rounded-md bg-white/10 hover:bg-white/20 transition-colors"
+            >
+              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="inline-block mr-1">
+                <path d="M3 6h18"></path>
+                <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
+                <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
+              </svg>
+              清空
+            </button>
+          </div>
+        </div>
+        
+        {/* 聊天区域 - 占据大部分空间 */}
+        <div className="flex-1 overflow-hidden flex flex-col">
+          <GlassCard className="w-full flex-1 flex flex-col overflow-hidden">
+            {/* 聊天消息区域 - 可滚动区域 */}
+            <div 
+              ref={chatContainerRef}
+              className="flex-1 overflow-y-auto px-2 py-4 scroll-smooth" 
+              style={{ 
+                scrollbarWidth: 'thin',
+                scrollbarColor: 'rgba(255,255,255,0.2) transparent'
+              }}
+            >
+              <div className="space-y-6">
+                {visibleMessages.map((message) => (
+                  <div 
+                    key={message.id} 
+                    className={`flex items-start gap-3 ${message.role === "user" ? "justify-end" : ""}`}
+                  >
+                    {message.role === "assistant" && (
+                      <div className="w-8 h-8 rounded-full bg-[var(--theme-accent)]/80 flex items-center justify-center text-white text-sm font-medium shrink-0 mt-1">AI</div>
+                    )}
+                    
+                    <div className={`${
+                      message.role === "assistant" 
+                        ? "bg-white/10 backdrop-blur-sm rounded-lg rounded-tl-none" 
+                        : "bg-[var(--theme-accent)]/20 backdrop-blur-sm rounded-lg rounded-tr-none"
+                    } p-3 max-w-[85%]`}>
+                      <p className="text-white/90 whitespace-pre-wrap break-words">{message.content}</p>
+                    </div>
+                    
+                    {message.role === "user" && (
+                      <div className="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center text-white text-sm font-medium shrink-0 mt-1">您</div>
+                    )}
+                  </div>
+                ))}
+                
+                {isLoading && (
+                  <div className="flex items-start gap-3">
+                    <div className="w-8 h-8 rounded-full bg-[var(--theme-accent)]/80 flex items-center justify-center text-white text-sm font-medium shrink-0">AI</div>
+                    <div className="bg-white/10 backdrop-blur-sm p-3 rounded-lg rounded-tl-none">
+                      <div className="flex space-x-2 items-center h-6">
+                        <div className="w-2 h-2 rounded-full bg-white/70 animate-bounce"></div>
+                        <div className="w-2 h-2 rounded-full bg-white/70 animate-bounce" style={{animationDelay: "0.2s"}}></div>
+                        <div className="w-2 h-2 rounded-full bg-white/70 animate-bounce" style={{animationDelay: "0.4s"}}></div>
+                      </div>
+                    </div>
+                  </div>
+                )}
+                
+                {error && (
+                  <div className="bg-red-500/20 p-3 rounded-lg text-white/90 text-sm mx-4">
+                    <span className="font-medium">错误:</span> {error}
+                  </div>
+                )}
+                
+                {/* 用于自动滚动的空白元素 */}
+                <div ref={messagesEndRef} />
+              </div>
+            </div>
+            
+            {/* 输入区域 - 固定在底部 */}
+            <div className="p-4 border-t border-white/10">
+              <div className="flex gap-2">
+                <textarea 
+                  className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50 resize-none h-16"
+                  placeholder="输入您的科研问题或需求..."
+                  value={inputText}
+                  onChange={(e) => setInputText(e.target.value)}
+                  onKeyDown={handleKeyPress}
+                  disabled={isLoading}
+                ></textarea>
+                <Button 
+                  className="self-end"
+                  onClick={sendMessage}
+                  disabled={isLoading || !inputText.trim()}
+                >
+                  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                    <path d="M22 2L11 13"></path>
+                    <path d="M22 2L15 22L11 13L2 9L22 2Z"></path>
+                  </svg>
+                </Button>
+              </div>
+              <div className="mt-2 text-white/50 text-xs">
+                提示:您可以询问学术问题、研究方向分析、文献推荐、论文写作建议等。按Enter发送,Shift+Enter换行。
+              </div>
+            </div>
+          </GlassCard>
+        </div>
+        
+        {/* 底部版权信息 */}
+        <p className="text-white/50 text-center text-xs mt-2">
+          基于DeepSeek AI的智能对话系统 | 文献启明星科研助手
+        </p>
+      </div>
+    </main>
+  );
+} 

+ 75 - 0
lightstar-web/src/app/api/chat/route.ts

@@ -0,0 +1,75 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+// 添加更多环境变量和错误处理
+const BACKEND_API_URL = process.env.BACKEND_API_URL || process.env.NEXT_PUBLIC_BACKEND_API_URL || 'http://localhost:8000';
+const API_TIMEOUT = parseInt(process.env.API_TIMEOUT || '30000', 10);
+
+export async function POST(request: NextRequest) {
+  try {
+    const { messages } = await request.json();
+    
+    if (!messages || !Array.isArray(messages)) {
+      return NextResponse.json(
+        { error: '请求格式不正确' },
+        { status: 400 }
+      );
+    }
+    
+    console.log(`使用后端API URL: ${BACKEND_API_URL}`);
+    
+    // 添加超时控制
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT);
+    
+    try {
+      // 调用后端API
+      const response = await fetch(`${BACKEND_API_URL}/api/chat`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({ messages }),
+        signal: controller.signal
+      });
+      
+      clearTimeout(timeoutId);
+      
+      if (!response.ok) {
+        let errorMsg = '与AI服务通信失败';
+        try {
+          const errorData = await response.json();
+          console.error('后端API错误:', errorData);
+          errorMsg = errorData.detail || errorData.message || errorMsg;
+        } catch (e) {
+          console.error('解析错误响应失败:', e);
+        }
+        
+        return NextResponse.json(
+          { error: errorMsg },
+          { status: response.status }
+        );
+      }
+      
+      const data = await response.json();
+      return NextResponse.json(data);
+    } catch (fetchError: any) {
+      clearTimeout(timeoutId);
+      console.error('请求后端API失败:', fetchError);
+      
+      if (fetchError.name === 'AbortError') {
+        return NextResponse.json(
+          { error: '请求超时,后端服务可能未响应' },
+          { status: 504 }
+        );
+      }
+      
+      throw fetchError;
+    }
+  } catch (error: any) {
+    console.error('API路由错误:', error);
+    return NextResponse.json(
+      { error: '处理请求时发生错误', message: error.message || '未知错误' },
+      { status: 500 }
+    );
+  }
+} 

+ 52 - 0
lightstar-web/src/app/api/research/enhanced-process-stream/route.ts

@@ -0,0 +1,52 @@
+import { NextRequest } from 'next/server';
+
+export async function GET(request: NextRequest) {
+  const searchParams = request.nextUrl.searchParams;
+  const researchIntent = searchParams.get('research_intent');
+  
+  if (!researchIntent) {
+    return new Response(
+      JSON.stringify({ error: "研究主题不能为空" }), 
+      { status: 400, headers: { 'Content-Type': 'application/json' }}
+    );
+  }
+  
+  // 确定后端API地址(优先使用环境变量)
+  const apiBaseUrl = process.env.BACKEND_API_URL || 'http://localhost:8000';
+  const apiUrl = `${apiBaseUrl}/api/research/enhanced-process-stream?research_intent=${encodeURIComponent(researchIntent)}`;
+  
+  try {
+    // 使用代理模式而不是重定向
+    const response = await fetch(apiUrl);
+    
+    // 如果后端不可用,返回一个明确的错误
+    if (!response.ok) {
+      if (response.status === 404) {
+        return new Response(
+          JSON.stringify({ error: "后端API不存在或未启动,请检查后端服务" }), 
+          { status: 502, headers: { 'Content-Type': 'application/json' }}
+        );
+      }
+      
+      return new Response(
+        JSON.stringify({ error: `后端API返回错误:${response.status} ${response.statusText}` }), 
+        { status: 502, headers: { 'Content-Type': 'application/json' }}
+      );
+    }
+    
+    // 创建新的Response并使用ReadableStream转发数据
+    return new Response(response.body, {
+      headers: {
+        'Content-Type': 'text/event-stream',
+        'Cache-Control': 'no-cache',
+        'Connection': 'keep-alive'
+      }
+    });
+  } catch (error) {
+    console.error('Error proxying SSE request:', error);
+    return new Response(
+      JSON.stringify({ error: "连接后端服务失败,请确保后端服务已启动" }), 
+      { status: 500, headers: { 'Content-Type': 'application/json' }}
+    );
+  }
+} 

+ 126 - 0
lightstar-web/src/app/dashboard/page.tsx

@@ -0,0 +1,126 @@
+import { GlassCard } from "@/components/ui/glass-card";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+
+export default function Dashboard() {
+  return (
+    <main className="relative flex min-h-[calc(100vh-64px)] flex-col items-center justify-center p-4 sm:p-8">
+      <div className="container max-w-4xl mx-auto z-10">
+        <div className="text-center mb-8">
+          <h1 className="text-5xl md:text-6xl font-bold text-center text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 mb-3 drop-shadow-lg">
+            控制中心
+          </h1>
+          <p className="text-xl text-center text-white/80 mb-2">
+            智能科研助手平台
+          </p>
+          <div className="w-16 h-1 mx-auto bg-gradient-to-r from-[var(--theme-accent)] to-[var(--theme-highlight)] rounded-full"></div>
+        </div>
+        
+        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
+          {/* 文献研究功能卡片 */}
+          <GlassCard className="relative overflow-hidden group">
+            <div className="absolute top-0 right-0 -mt-4 -mr-4 h-16 w-16 rounded-full bg-gradient-to-br from-[var(--theme-highlight)] to-[var(--theme-accent)] opacity-70 blur-xl group-hover:opacity-100 transition-opacity"></div>
+            
+            <h2 className="text-2xl font-bold text-white mb-3">文献研究</h2>
+            <p className="text-white/80 mb-4">
+              智能分析研究领域,生成研究方向报告,查找相关论文与文献。
+            </p>
+            <div className="flex space-x-3">
+              <Link href="/" className="inline-block">
+                <Button>开始研究</Button>
+              </Link>
+              <Button variant="outline">查看历史</Button>
+            </div>
+          </GlassCard>
+          
+          {/* AI聊天功能卡片 */}
+          <GlassCard className="relative overflow-hidden group">
+            <div className="absolute top-0 right-0 -mt-4 -mr-4 h-16 w-16 rounded-full bg-gradient-to-br from-[var(--theme-highlight)] to-[var(--theme-accent)] opacity-70 blur-xl group-hover:opacity-100 transition-opacity"></div>
+            
+            <h2 className="text-2xl font-bold text-white mb-3">AI 聊天</h2>
+            <p className="text-white/80 mb-4">
+              与智能助手交流,解答学术问题,获取研究建议和文献推荐。
+            </p>
+            <div className="flex space-x-3">
+              <Link href="/ai-chat" className="inline-block">
+                <Button>开始对话</Button>
+              </Link>
+              <Button variant="outline">查看历史</Button>
+            </div>
+          </GlassCard>
+          
+          {/* 论文分析功能卡片 */}
+          <GlassCard className="relative overflow-hidden group">
+            <div className="absolute top-0 right-0 -mt-4 -mr-4 h-16 w-16 rounded-full bg-gradient-to-br from-[var(--theme-highlight)] to-[var(--theme-accent)] opacity-70 blur-xl group-hover:opacity-100 transition-opacity"></div>
+            
+            <h2 className="text-2xl font-bold text-white mb-3">论文分析</h2>
+            <p className="text-white/80 mb-4">
+              上传论文PDF,自动提取关键信息,生成摘要和解读,获取相关文献推荐。
+            </p>
+            <div className="flex space-x-3">
+              <Button>上传论文</Button>
+              <Button variant="outline">浏览示例</Button>
+            </div>
+          </GlassCard>
+          
+          {/* 个人中心功能卡片 */}
+          <GlassCard className="relative overflow-hidden group">
+            <div className="absolute top-0 right-0 -mt-4 -mr-4 h-16 w-16 rounded-full bg-gradient-to-br from-[var(--theme-highlight)] to-[var(--theme-accent)] opacity-70 blur-xl group-hover:opacity-100 transition-opacity"></div>
+            
+            <h2 className="text-2xl font-bold text-white mb-3">个人中心</h2>
+            <p className="text-white/80 mb-4">
+              管理您的研究项目、收藏的论文和设置个人偏好,查看使用统计。
+            </p>
+            <div className="flex space-x-3">
+              <a href="/profile" style={{ textDecoration: 'none' }}>
+                <Button>进入中心</Button>
+              </a>
+              <a href="/profile?tab=settings" className="inline-block">
+                <Button variant="outline">系统设置</Button>
+              </a>
+            </div>
+          </GlassCard>
+        </div>
+        
+        {/* 最近活动 */}
+        <GlassCard className="w-full mb-6">
+          <h2 className="text-xl font-bold text-white mb-4">最近活动</h2>
+          <div className="space-y-3">
+            <div className="flex items-center p-3 bg-white/5 rounded-lg">
+              <div className="w-10 h-10 rounded-full bg-[var(--theme-accent)]/30 flex items-center justify-center mr-3">
+                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-white">
+                  <path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1-2.5-2.5z"></path>
+                  <path d="M8 7h6"></path>
+                  <path d="M8 11h8"></path>
+                  <path d="M8 15h5"></path>
+                </svg>
+              </div>
+              <div className="flex-1">
+                <h3 className="text-white font-medium">量子计算研究报告</h3>
+                <p className="text-white/60 text-sm">3个研究方向,15篇相关论文</p>
+              </div>
+              <div className="text-white/50 text-sm">2小时前</div>
+            </div>
+            
+            <div className="flex items-center p-3 bg-white/5 rounded-lg">
+              <div className="w-10 h-10 rounded-full bg-[var(--theme-highlight)]/30 flex items-center justify-center mr-3">
+                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-white">
+                  <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
+                </svg>
+              </div>
+              <div className="flex-1">
+                <h3 className="text-white font-medium">AI助手对话</h3>
+                <p className="text-white/60 text-sm">关于机器学习模型评估的讨论</p>
+              </div>
+              <div className="text-white/50 text-sm">昨天</div>
+            </div>
+          </div>
+        </GlassCard>
+        
+        <p className="text-white/50 text-center text-xs mt-8">
+          基于人工智能的科研辅助系统 | 版本 1.0.0
+        </p>
+      </div>
+    </main>
+  );
+} 

BIN
lightstar-web/src/app/favicon.ico


+ 161 - 0
lightstar-web/src/app/globals.css

@@ -0,0 +1,161 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+  :root {
+    --background: 224 71% 4%;
+    --foreground: 213 31% 91%;
+
+    --primary: 266 100% 64%;
+    --primary-foreground: 210 40% 98%;
+ 
+    --card: 224 71% 4%;
+    --card-foreground: 213 31% 91%;
+  }
+}
+
+/* 动画效果 */
+@keyframes float {
+  0% { transform: translateY(0px); }
+  50% { transform: translateY(-20px); }
+  100% { transform: translateY(0px); }
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 0.6; }
+  50% { opacity: 0.8; }
+}
+
+@keyframes blob {
+  0% {
+    transform: translate(0px, 0px) scale(1);
+  }
+  33% {
+    transform: translate(30px, -50px) scale(1.2);
+  }
+  66% {
+    transform: translate(-20px, 20px) scale(0.8);
+  }
+  100% {
+    transform: translate(0px, 0px) scale(1);
+  }
+}
+
+/* 星星背景效果 */
+.stars {
+  background-image: radial-gradient(2px 2px at 20px 30px, white, rgba(0, 0, 0, 0)), 
+                    radial-gradient(2px 2px at 40px 70px, white, rgba(0, 0, 0, 0)), 
+                    radial-gradient(2px 2px at 60px 110px, white, rgba(0, 0, 0, 0)),
+                    radial-gradient(2px 2px at 80px 150px, white, rgba(0, 0, 0, 0)),
+                    radial-gradient(2px 2px at 100px 20px, white, rgba(0, 0, 0, 0));
+  background-size: 200px 200px;
+  opacity: 0.3;
+}
+
+.animate-float {
+  animation: float 6s ease-in-out infinite;
+}
+
+.animate-blob {
+  animation: blob 10s infinite ease-in-out;
+}
+
+.animation-delay-2000 {
+  animation-delay: 2s;
+}
+
+.animation-delay-4000 {
+  animation-delay: 4s;
+}
+
+/* 主题变量 */
+.theme-space {
+  --theme-bg-from: theme('colors.space.dark');
+  --theme-bg-via: theme('colors.space.mid');
+  --theme-bg-to: theme('colors.space.light');
+  --theme-accent: theme('colors.space.accent');
+  --theme-highlight: theme('colors.space.highlight');
+}
+
+.theme-ocean {
+  --theme-bg-from: theme('colors.ocean.dark');
+  --theme-bg-via: theme('colors.ocean.mid');
+  --theme-bg-to: theme('colors.ocean.light');
+  --theme-accent: theme('colors.ocean.accent');
+  --theme-highlight: theme('colors.ocean.highlight');
+}
+
+.theme-tech {
+  --theme-bg-from: theme('colors.tech.dark');
+  --theme-bg-via: theme('colors.tech.mid');
+  --theme-bg-to: theme('colors.tech.light');
+  --theme-accent: theme('colors.tech.accent');
+  --theme-highlight: theme('colors.tech.highlight');
+}
+
+.theme-nightglow {
+  --theme-bg-from: theme('colors.nightglow.dark');
+  --theme-bg-via: theme('colors.nightglow.mid');
+  --theme-bg-to: theme('colors.nightglow.light');
+  --theme-accent: theme('colors.nightglow.accent');
+  --theme-highlight: theme('colors.nightglow.highlight');
+}
+
+body {
+  @apply bg-gradient-to-br from-[var(--theme-bg-from)] via-[var(--theme-bg-via)] to-[var(--theme-bg-to)];
+  min-height: 100vh;
+}
+
+/* 研究报告学术样式 */
+.research-report-content .prose {
+  font-family: 'Times New Roman', Times, serif;
+  line-height: 1.6;
+}
+
+.research-report-content .prose h1 {
+  font-size: 1.8rem;
+  margin-top: 2rem;
+  margin-bottom: 1rem;
+  font-weight: bold;
+  text-align: center;
+}
+
+.research-report-content .prose h2 {
+  font-size: 1.4rem;
+  margin-top: 1.5rem;
+  margin-bottom: 0.75rem;
+  font-weight: 600;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+  padding-bottom: 0.5rem;
+}
+
+.research-report-content .prose h3 {
+  font-size: 1.2rem;
+  margin-top: 1.25rem;
+  margin-bottom: 0.5rem;
+  font-weight: 600;
+}
+
+.research-report-content .prose p {
+  margin-bottom: 1rem;
+  text-align: justify;
+}
+
+.research-report-content .prose ul,
+.research-report-content .prose ol {
+  margin-left: 1.5rem;
+  margin-bottom: 1rem;
+}
+
+.research-report-content .prose li {
+  margin-bottom: 0.25rem;
+}
+
+.research-report-content .prose blockquote {
+  border-left: 3px solid rgba(96, 165, 250, 0.5);
+  padding-left: 1rem;
+  margin-left: 0;
+  color: rgba(255, 255, 255, 0.7);
+  font-style: italic;
+}

+ 86 - 0
lightstar-web/src/app/layout.tsx

@@ -0,0 +1,86 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import Link from "next/link";
+import "./globals.css";
+import ClientInit from "@/components/ClientInit";
+
+const geistSans = Geist({
+  variable: "--font-geist-sans",
+  subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+  variable: "--font-geist-mono",
+  subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+  title: "文献启明星",
+  description: "智能科研助手平台",
+};
+
+export default function RootLayout({
+  children,
+}: Readonly<{
+  children: React.ReactNode;
+}>) {
+  return (
+    <html lang="zh">
+      <body
+        className={`${geistSans.variable} ${geistMono.variable} antialiased theme-space`}
+      >
+        {/* 客户端初始化组件 */}
+        <ClientInit />
+        
+        <div className="relative min-h-screen bg-gradient-to-br from-[var(--theme-bg-from)] via-[var(--theme-bg-via)] to-[var(--theme-bg-to)]">
+          {/* 背景装饰 - 共享给所有页面 */}
+          <div className="absolute inset-0 overflow-hidden pointer-events-none">
+            <div className="stars absolute inset-0 z-0"></div>
+            <div className="absolute top-1/4 left-1/3 w-[30rem] h-[30rem] bg-[var(--theme-accent)] rounded-full mix-blend-screen filter blur-[150px] opacity-20 animate-blob"></div>
+            <div className="absolute top-1/3 right-1/4 w-[25rem] h-[25rem] bg-[var(--theme-highlight)] rounded-full mix-blend-screen filter blur-[120px] opacity-20 animate-blob animation-delay-2000"></div>
+            <div className="absolute bottom-1/4 right-1/3 w-[20rem] h-[20rem] bg-white rounded-full mix-blend-screen filter blur-[100px] opacity-10 animate-blob animation-delay-4000"></div>
+          </div>
+          
+          {/* 顶部导航栏 */}
+          <header className="relative z-20">
+            <nav className="container mx-auto px-4 py-4 flex items-center justify-between">
+              <div className="flex items-center gap-2">
+                <Link href="/" className="text-2xl font-bold text-white hover:opacity-90 transition-opacity flex items-center gap-2">
+                  <span className="w-8 h-8 rounded-full bg-gradient-to-br from-[var(--theme-accent)] to-[var(--theme-highlight)] flex items-center justify-center">
+                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-white">
+                      <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
+                    </svg>
+                  </span>
+                  <span>启明星</span>
+                </Link>
+              </div>
+              
+              <div className="flex items-center space-x-6">
+                <NavLink href="/" label="文献研究" />
+                <NavLink href="/ai-chat" label="AI聊天" />
+                <NavLink href="/dashboard" label="控制台" />
+                <NavLink href="/profile" label="个人中心" />
+              </div>
+            </nav>
+          </header>
+          
+          {/* 页面内容 */}
+          {children}
+        </div>
+      </body>
+    </html>
+  );
+}
+
+// 导航链接组件
+function NavLink({ href, label }: { href: string; label: string }) {
+  return (
+    <Link 
+      href={href} 
+      className="text-white/80 hover:text-white transition-colors font-medium relative group"
+    >
+      {label}
+      <span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[var(--theme-accent)] group-hover:w-full transition-all duration-300"></span>
+    </Link>
+  );
+}

+ 165 - 0
lightstar-web/src/app/login/page.tsx

@@ -0,0 +1,165 @@
+"use client";
+
+import { useState } from 'react';
+import { GlassCard } from "@/components/ui/glass-card";
+import { Button } from "@/components/ui/button";
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+
+// API基础URL
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
+
+// 在文件顶部添加类型定义
+interface ApiError {
+  message: string;
+  response?: {
+    status: number;
+    data: any;
+  };
+}
+
+export default function LoginPage() {
+  const [username, setUsername] = useState('');
+  const [password, setPassword] = useState('');
+  const [error, setError] = useState('');
+  const [isLoading, setIsLoading] = useState(false);
+  const router = useRouter();
+  
+  const handleLogin = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError('');
+    setIsLoading(true);
+    
+    try {
+      console.log("开始登录流程...");
+      
+      // 创建FormData
+      const formData = new FormData();
+      formData.append('username', username);
+      formData.append('password', password);
+      
+      // 登录请求
+      console.log("发送登录请求到:", `${API_BASE_URL}/auth/token`);
+      const response = await fetch(`${API_BASE_URL}/auth/token`, {
+        method: 'POST',
+        body: formData
+      });
+      
+      // 检查响应状态
+      console.log("登录请求状态:", response.status);
+      if (!response.ok) {
+        const errorText = await response.text();
+        console.error("登录失败:", errorText);
+        throw new Error('用户名或密码不正确');
+      }
+      
+      const data = await response.json();
+      console.log("登录成功,获取到token");
+      
+      // 保存token到localStorage
+      localStorage.setItem('token', data.access_token);
+      
+      // 同时在cookie中设置token
+      document.cookie = `token=${data.access_token}; path=/; max-age=86400`; // 设置为1天过期
+      
+      // 获取用户资料
+      console.log("获取用户资料...");
+      const userResponse = await fetch(`${API_BASE_URL}/users/profile`, {
+        headers: {
+          'Authorization': `Bearer ${data.access_token}`
+        }
+      });
+      
+      if (!userResponse.ok) {
+        console.error("获取用户资料失败:", userResponse.status);
+        throw new Error('登录成功但无法获取用户资料');
+      }
+      
+      const userData = await userResponse.json();
+      console.log("获取用户资料成功:", userData);
+      localStorage.setItem('userData', JSON.stringify(userData));
+      
+      // 登录成功后
+      console.log("登录成功,准备跳转...");
+      
+      // 直接使用window.location进行硬跳转
+      window.location.href = '/profile';
+      
+      // 如果上面的方法不起作用,尝试设置超时
+      setTimeout(() => {
+        window.location.replace('/profile');
+      }, 500);
+    } catch (err) {
+      const error = err as ApiError;
+      console.error('登录过程出错:', error);
+      
+      if (error.response) {
+        // 处理响应错误
+        setError(error.response.data?.detail || `错误(${error.response.status})`);
+      } else if (error.message) {
+        setError(error.message);
+      } else {
+        setError('登录失败,请稍后再试');
+      }
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  return (
+    <main className="flex items-center justify-center min-h-screen p-4">
+      <GlassCard className="w-full max-w-md">
+        <h1 className="text-3xl font-bold text-center text-white mb-8">登录</h1>
+        
+        {error && (
+          <div className="bg-red-500/20 p-3 mb-6 rounded-lg text-white">
+            {error}
+          </div>
+        )}
+        
+        <form onSubmit={handleLogin} className="space-y-6">
+          <div>
+            <label className="block text-white/60 text-sm mb-2">用户名</label>
+            <input 
+              type="text"
+              className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              value={username}
+              onChange={(e) => setUsername(e.target.value)}
+              required
+            />
+          </div>
+          
+          <div>
+            <label className="block text-white/60 text-sm mb-2">密码</label>
+            <input 
+              type="password"
+              className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              value={password}
+              onChange={(e) => setPassword(e.target.value)}
+              required
+            />
+          </div>
+          
+          <Button 
+            type="submit" 
+            className="w-full" 
+            disabled={isLoading}
+          >
+            {isLoading ? (
+              <div className="inline-block w-6 h-6 border-2 border-t-transparent border-white rounded-full animate-spin"></div>
+            ) : '登录'}
+          </Button>
+        </form>
+        
+        <div className="mt-6 text-center">
+          <p className="text-white/60">
+            还没有账号? 
+            <Link href="/register" className="text-[var(--theme-accent)] hover:underline ml-1">
+              注册
+            </Link>
+          </p>
+        </div>
+      </GlassCard>
+    </main>
+  );
+} 

+ 28 - 0
lightstar-web/src/app/page.tsx

@@ -0,0 +1,28 @@
+import { GlassCard } from "@/components/ui/glass-card";
+import { ResearchForm } from "@/components/forms/research-form";
+
+export default function Home() {
+  return (
+    <main className="relative flex min-h-[calc(100vh-64px)] flex-col items-center justify-center p-4 sm:p-8">
+      <div className="container max-w-xl mx-auto z-10">
+        <div className="text-center mb-8">
+          <h1 className="text-5xl md:text-6xl font-bold text-center text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 mb-3 drop-shadow-lg animate-pulse">
+            文献启明星
+          </h1>
+          <p className="text-xl text-center text-white/80 mb-2">
+            科研调研智能体系统
+          </p>
+          <div className="w-16 h-1 mx-auto bg-gradient-to-r from-[var(--theme-accent)] to-[var(--theme-highlight)] rounded-full"></div>
+        </div>
+        
+        <GlassCard className="w-full">
+          <ResearchForm />
+        </GlassCard>
+        
+        <p className="text-white/50 text-center text-xs mt-8">
+          基于人工智能的论文检索与分析系统
+        </p>
+      </div>
+    </main>
+  );
+}

+ 1012 - 0
lightstar-web/src/app/profile/page.tsx

@@ -0,0 +1,1012 @@
+"use client";
+
+import { GlassCard } from "@/components/ui/glass-card";
+import { Button } from "@/components/ui/button";
+import { useState, useEffect } from "react";
+import Link from "next/link";
+import { useSearchParams } from 'next/navigation';
+import axios, { AxiosError } from "axios";
+
+// 定义标签页
+const tabs = [
+  { id: "profile", label: "个人信息", icon: (
+    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+      <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
+      <circle cx="12" cy="7" r="4"></circle>
+    </svg>
+  ) },
+  { id: "history", label: "使用记录", icon: (
+    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+      <path d="M12 8v4l3 3"></path>
+      <circle cx="12" cy="12" r="10"></circle>
+    </svg>
+  ) },
+  { id: "favorites", label: "收藏论文", icon: (
+    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+      <path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"></path>
+    </svg>
+  ) },
+  { id: "settings", label: "系统设置", icon: (
+    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+      <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
+      <circle cx="12" cy="12" r="3"></circle>
+    </svg>
+  ) }
+];
+
+// 在文件顶部添加明确的类型定义
+interface UserData {
+  id: number;
+  username: string;
+  email: string;
+  avatar: null | string;
+  researchFields: string[];  // 明确定义为string[]
+  membershipType: string;
+  expiryDate: string;
+  credits: number;
+  registerDate: string;
+  institution: string;
+  position: string;
+}
+
+// API基础URL
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
+
+export default function ProfilePage() {
+  const searchParams = useSearchParams();
+  const tabParam = searchParams.get('tab');
+  
+  // 当前激活的标签页
+  const [activeTab, setActiveTab] = useState(tabParam || "profile");
+  const [isDarkMode, setIsDarkMode] = useState(true);
+  const [notificationSettings, setNotificationSettings] = useState({
+    email: true,
+    research: true,
+    updates: true
+  });
+  const [privacySettings, setPrivacySettings] = useState({
+    twoFactor: false,
+    dataAnalysis: true
+  });
+  const [isEditing, setIsEditing] = useState(false);
+  const [userData, setUserData] = useState<UserData>({
+    id: 0,
+    username: "加载中...",
+    email: "loading@example.com",
+    avatar: null,
+    researchFields: [] as string[],  // 明确类型
+    membershipType: "加载中...",
+    expiryDate: "2025-12-31",
+    credits: 0,
+    registerDate: "-",
+    institution: "江西财经大学",
+    position: "学生"
+  });
+  const [editedUser, setEditedUser] = useState<UserData>({...userData});
+  const [isLoading, setIsLoading] = useState(true);
+  const [error, setError] = useState("");
+  const [chatHistory, setChatHistory] = useState([]);
+  const [researchHistory, setResearchHistory] = useState([]);
+  const [isLoggedIn, setIsLoggedIn] = useState(false); // 默认为未登录
+  const [loginUsername, setLoginUsername] = useState('');
+  const [loginPassword, setLoginPassword] = useState('');
+  const [loginError, setLoginError] = useState('');
+  
+  // 每当URL参数变化时更新激活的标签页
+  useEffect(() => {
+    if (tabParam && tabs.some(tab => tab.id === tabParam)) {
+      setActiveTab(tabParam);
+    }
+  }, [tabParam]);
+  
+  // 获取用户资料
+  useEffect(() => {
+    const fetchUserProfile = async () => {
+      try {
+        setIsLoading(true);
+        // 从本地存储获取token
+        const token = localStorage.getItem("token");
+        
+        if (!token) {
+          // 没有token,表示未登录
+          setIsLoggedIn(false);
+          setIsLoading(false);
+          
+          // 检查URL是否来自中间件重定向(避免两次验证)
+          const urlParams = new URLSearchParams(window.location.search);
+          if (urlParams.has('redirect')) {
+            // 如果是重定向来的,不要显示内嵌登录表单,而是直接跳转到登录页
+            window.location.href = '/login?redirect=/profile';
+            return;
+          }
+          
+          return;
+        }
+        
+        // 有token,尝试获取用户资料
+        try {
+          const response = await axios.get(`${API_BASE_URL}/users/profile`, {
+            headers: {
+              Authorization: `Bearer ${token}`
+            }
+          });
+          
+          // 如果请求成功,说明token有效
+          setIsLoggedIn(true);
+          
+          // 处理后端返回的数据,转换字段名称从下划线到驼峰命名
+          const responseData = response.data;
+          
+          // 添加字段映射逻辑
+          const userData = {
+            id: responseData.id,
+            username: responseData.username,
+            email: responseData.email,
+            avatar: responseData.avatar,
+            // 处理研究领域
+            researchFields: responseData.research_fields || ["人工智能", "机器学习", "自然语言处理"],
+            // 处理会员信息
+            membershipType: responseData.membership_type || "免费用户",
+            expiryDate: responseData.expiry_date || "未设置",
+            credits: responseData.credits || 0,
+            // 添加注册时间 - 可能需要从后端添加或使用创建时间
+            registerDate: responseData.created_at || "-",
+            institution: responseData.institution || "",
+            position: responseData.position || ""
+          };
+          
+          setUserData(userData);
+          setEditedUser(userData);
+        } catch (error) {
+          // 如果请求失败,可能是token无效或已过期
+          console.error("获取用户资料失败:", error);
+          localStorage.removeItem("token"); // 清除无效token
+          document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; // 清除cookie中的token
+          setIsLoggedIn(false);
+        }
+        
+        setIsLoading(false);
+      } catch (err) {
+        console.error("初始化用户资料失败:", err);
+        setError("获取用户资料失败");
+        setIsLoading(false);
+        setIsLoggedIn(false);
+      }
+    };
+    
+    // 加载设置
+    const loadSettings = () => {
+      const storedNotificationSettings = localStorage.getItem("notificationSettings");
+      if (storedNotificationSettings) {
+        try {
+          setNotificationSettings(JSON.parse(storedNotificationSettings));
+        } catch (e) {
+          console.error("解析通知设置失败:", e);
+        }
+      }
+      
+      const storedPrivacySettings = localStorage.getItem("privacySettings");
+      if (storedPrivacySettings) {
+        try {
+          setPrivacySettings(JSON.parse(storedPrivacySettings));
+        } catch (e) {
+          console.error("解析隐私设置失败:", e);
+        }
+      }
+    };
+    
+    fetchUserProfile();
+    loadSettings();
+  }, []);
+  
+  // 获取聊天历史
+  useEffect(() => {
+    const fetchChatHistory = async () => {
+      if (activeTab !== "history") return;
+      
+      try {
+        const token = localStorage.getItem("token");
+        if (!token) return;
+        
+        const response = await axios.get(`${API_BASE_URL}/chat-history`, {
+          headers: {
+            Authorization: `Bearer ${token}`
+          }
+        });
+        
+        setChatHistory(response.data || []);
+      } catch (err) {
+        console.error("获取聊天历史失败:", err);
+      }
+    };
+    
+    fetchChatHistory();
+  }, [activeTab]);
+  
+  // 获取研究历史
+  useEffect(() => {
+    const fetchResearchHistory = async () => {
+      if (activeTab !== "history") return;
+      
+      try {
+        const token = localStorage.getItem("token");
+        if (!token) return;
+        
+        const response = await axios.get(`${API_BASE_URL}/research-history`, {
+          headers: {
+            Authorization: `Bearer ${token}`
+          }
+        });
+        
+        setResearchHistory(response.data || []);
+      } catch (err) {
+        console.error("获取研究历史失败:", err);
+      }
+    };
+    
+    fetchResearchHistory();
+  }, [activeTab]);
+  
+  // 切换编辑模式
+  const toggleEditMode = async () => {
+    if (isEditing) {
+      try {
+        const token = localStorage.getItem("token");
+        
+        if (token) {
+          // 显示加载状态
+          setIsLoading(true);
+          setError("");
+          
+          // 保存到API
+          const response = await axios.put(`${API_BASE_URL}/users/profile`, {
+            username: editedUser.username,
+            email: editedUser.email,
+            institution: editedUser.institution,
+            position: editedUser.position,
+            research_fields: editedUser.researchFields,
+          }, {
+            headers: {
+              Authorization: `Bearer ${token}`
+            }
+          });
+          
+          // 更新本地状态
+          setUserData(editedUser);
+          localStorage.setItem("userData", JSON.stringify(editedUser));
+          setIsEditing(false);
+          alert("个人资料已成功更新");
+        }
+      } catch (error) {
+        // 使用类型断言将error转换为AxiosError类型
+        const err = error as AxiosError<any>;
+        console.error("保存用户资料失败:", err);
+        
+        // 检查是否有响应
+        if (err.response) {
+          setError(`保存失败:${err.response.status} ${err.response.data?.detail || JSON.stringify(err.response.data) || '未知错误'}`);
+        } else if (err.request) {
+          setError("网络错误:无法连接到服务器,请检查网络连接");
+        } else {
+          setError(`请求错误:${err.message}`);
+        }
+      } finally {
+        setIsLoading(false);
+      }
+    } else {
+      setIsEditing(true);
+    }
+  };
+  
+  // 处理用户信息变更
+  const handleUserDataChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const { name, value } = e.target;
+    setEditedUser(prev => ({
+      ...prev,
+      [name]: value
+    }));
+  };
+  
+  // 处理研究领域变更
+  const handleResearchFieldsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const fields: string[] = e.target.value.split(',').map(field => field.trim());
+    setEditedUser(prev => ({
+      ...prev,
+      researchFields: fields
+    }));
+  };
+  
+  // 处理通知设置变更
+  const handleNotificationChange = (settingName: keyof typeof notificationSettings) => {
+    const updatedSettings = {
+      ...notificationSettings,
+      [settingName]: !notificationSettings[settingName]
+    };
+    setNotificationSettings(updatedSettings);
+    localStorage.setItem("notificationSettings", JSON.stringify(updatedSettings));
+  };
+  
+  // 处理隐私设置变更
+  const handlePrivacyChange = (settingName: keyof typeof privacySettings) => {
+    const updatedSettings = {
+      ...privacySettings,
+      [settingName]: !privacySettings[settingName]
+    };
+    setPrivacySettings(updatedSettings);
+    localStorage.setItem("privacySettings", JSON.stringify(updatedSettings));
+  };
+  
+  // 退出登录
+  const handleLogout = () => {
+    // 清除localStorage中的token
+    localStorage.removeItem("token");
+    localStorage.removeItem("userData");
+    
+    // 清除cookie中的token
+    document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
+    
+    // 更新状态
+    setIsLoggedIn(false);
+    setUserData({
+      id: 0,
+      username: "访客用户",
+      email: "example@example.com",
+      avatar: null,
+      researchFields: [],
+      membershipType: "免费用户",
+      expiryDate: "",
+      credits: 0,
+      registerDate: "-",
+      institution: "",
+      position: ""
+    });
+    
+    // 可选:重定向到登录页面
+    // window.location.href = '/login';
+  };
+  
+  // 保存设置
+  const handleSaveSettings = () => {
+    localStorage.setItem("notificationSettings", JSON.stringify(notificationSettings));
+    localStorage.setItem("privacySettings", JSON.stringify(privacySettings));
+    alert("设置保存成功");
+  };
+  
+  // 使用记录部分的渲染
+  const renderHistoryContent = () => {
+    return (
+      <div>
+        <h3 className="text-lg font-medium text-white mb-4">聊天历史</h3>
+        {chatHistory.length === 0 ? (
+          <p className="text-white/60">暂无聊天历史记录</p>
+        ) : (
+          <div className="space-y-3">
+            {chatHistory.map((chat: any) => (
+              <div key={chat.id} className="p-3 bg-white/5 rounded-lg hover:bg-white/10">
+                <div className="flex justify-between items-center">
+                  <h4 className="font-medium text-white">{chat.title}</h4>
+                  <span className="text-white/50 text-sm">
+                    {new Date(chat.created_at).toLocaleDateString()}
+                  </span>
+                </div>
+                <Link href={`/ai-chat?id=${chat.id}`} className="mt-2 text-[var(--theme-accent)] text-sm hover:underline">
+                  查看对话
+                </Link>
+              </div>
+            ))}
+          </div>
+        )}
+        
+        <h3 className="text-lg font-medium text-white mb-4 mt-8">研究历史</h3>
+        {researchHistory.length === 0 ? (
+          <p className="text-white/60">暂无研究历史记录</p>
+        ) : (
+          <div className="space-y-3">
+            {researchHistory.map((research: any) => (
+              <div key={research.id} className="p-3 bg-white/5 rounded-lg hover:bg-white/10">
+                <div className="flex justify-between items-center">
+                  <h4 className="font-medium text-white">{research.title}</h4>
+                  <span className="text-white/50 text-sm">
+                    {new Date(research.created_at).toLocaleDateString()}
+                  </span>
+                </div>
+                <Link href={`/research?id=${research.id}`} className="mt-2 text-[var(--theme-accent)] text-sm hover:underline">
+                  查看研究
+                </Link>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+    );
+  };
+  
+  // 添加处理登录的函数
+  const handleLogin = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoginError('');
+    setIsLoading(true);
+    
+    try {
+      console.log("开始登录流程...");
+      
+      // 创建FormData
+      const formData = new FormData();
+      formData.append('username', loginUsername);
+      formData.append('password', loginPassword);
+      
+      // 登录请求
+      console.log("发送登录请求到:", `${API_BASE_URL}/auth/token`);
+      const response = await fetch(`${API_BASE_URL}/auth/token`, {
+        method: 'POST',
+        body: formData
+      });
+      
+      // 检查响应状态
+      console.log("登录请求状态:", response.status);
+      if (!response.ok) {
+        const errorText = await response.text();
+        console.error("登录失败:", errorText);
+        throw new Error('用户名或密码不正确');
+      }
+      
+      const data = await response.json();
+      console.log("登录成功,获取到token");
+      
+      // 保存token到localStorage和cookie
+      localStorage.setItem('token', data.access_token);
+      document.cookie = `token=${data.access_token}; path=/; max-age=86400`; // 设置为1天过期
+      
+      // 获取用户资料
+      console.log("获取用户资料...");
+      const userResponse = await fetch(`${API_BASE_URL}/users/profile`, {
+        headers: {
+          'Authorization': `Bearer ${data.access_token}`
+        }
+      });
+      
+      if (!userResponse.ok) {
+        console.error("获取用户资料失败:", userResponse.status);
+        throw new Error('登录成功但无法获取用户资料');
+      }
+      
+      const userData = await userResponse.json();
+      console.log("获取用户资料成功:", userData);
+      
+      // 确保researchFields是数组
+      if (!userData.researchFields) {
+        userData.researchFields = ["人工智能", "机器学习", "自然语言处理"];
+      }
+      
+      localStorage.setItem('userData', JSON.stringify(userData));
+      
+      // 更新状态
+      setUserData(userData);
+      setEditedUser(userData);
+      setIsLoggedIn(true);
+      setIsLoading(false);
+    } catch (err) {
+      const error = err as any;
+      console.error('登录过程出错:', error);
+      
+      if (error.response) {
+        setLoginError(error.response.data?.detail || `错误(${error.response.status})`);
+      } else if (error.message) {
+        setLoginError(error.message);
+      } else {
+        setLoginError('登录失败,请稍后再试');
+      }
+      setIsLoading(false);
+    }
+  };
+  
+  return (
+    <main className="relative flex min-h-[calc(100vh-64px)] flex-col">
+      <div className="container mx-auto px-4 py-6 flex flex-col">
+        <div className="text-center mb-6">
+          <h1 className="text-4xl font-bold text-center text-transparent bg-clip-text bg-gradient-to-r from-white to-white/70 mb-2 drop-shadow-lg">
+            个人中心
+          </h1>
+          <p className="text-white/80 mb-2">
+            管理您的账户和研究项目
+          </p>
+          <div className="w-16 h-1 mx-auto bg-gradient-to-r from-[var(--theme-accent)] to-[var(--theme-highlight)] rounded-full"></div>
+        </div>
+        
+        {error && (
+          <div className="bg-red-500/20 p-3 mb-6 rounded-lg text-white">
+            {error}
+          </div>
+        )}
+        
+        <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
+          {/* 侧边导航 - 大屏幕上显示在左侧 */}
+          <div className="lg:col-span-1">
+            <GlassCard className="w-full overflow-hidden p-0">
+              <div className="p-4 border-b border-white/10 text-center">
+                <div className="w-20 h-20 bg-gradient-to-br from-[var(--theme-accent)] to-[var(--theme-highlight)] rounded-full mx-auto flex items-center justify-center text-white text-2xl font-bold">
+                  {userData.username.substring(0, 1)}
+                </div>
+                <h3 className="text-white font-medium mt-3">{userData.username}</h3>
+                <p className="text-white/60 text-sm">{userData.institution} · {userData.position}</p>
+              </div>
+              
+              <nav className="p-2">
+                {tabs.map((tab) => (
+                  <button
+                    key={tab.id}
+                    className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left ${
+                      activeTab === tab.id 
+                        ? "bg-white/10 text-white" 
+                        : "text-white/70 hover:bg-white/5 hover:text-white"
+                    }`}
+                    onClick={() => setActiveTab(tab.id)}
+                  >
+                    <span className="text-[var(--theme-accent)]">{tab.icon}</span>
+                    <span>{tab.label}</span>
+                    
+                    {/* 活动指示器 */}
+                    {activeTab === tab.id && (
+                      <span className="ml-auto w-1.5 h-1.5 rounded-full bg-[var(--theme-accent)]"></span>
+                    )}
+                  </button>
+                ))}
+              </nav>
+              
+              <div className="p-4 border-t border-white/10 mt-2">
+                <p className="text-white/50 text-xs text-center">
+                  {userData.membershipType} · 到期时间: {userData.expiryDate}
+                </p>
+              </div>
+            </GlassCard>
+          </div>
+          
+          {/* 内容区域 */}
+          <div className="lg:col-span-3">
+            <GlassCard className="w-full overflow-hidden">
+              {isLoading ? (
+                <div className="text-center py-10">
+                  <div className="inline-block w-6 h-6 border-2 border-t-[var(--theme-accent)] border-white/20 rounded-full animate-spin"></div>
+                  <p className="mt-2 text-white/60">加载中...</p>
+                </div>
+              ) : !isLoggedIn ? (
+                // 未登录状态显示登录表单
+                <div className="p-6">
+                  <h2 className="text-xl font-bold text-white mb-6">请登录您的账户</h2>
+                  
+                  {loginError && (
+                    <div className="bg-red-500/20 p-3 mb-6 rounded-lg text-white">
+                      {loginError}
+                    </div>
+                  )}
+                  
+                  <form onSubmit={handleLogin} className="space-y-6">
+                    <div>
+                      <label className="block text-white/60 text-sm mb-2">用户名</label>
+                      <input 
+                        type="text"
+                        className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+                        value={loginUsername}
+                        onChange={(e) => setLoginUsername(e.target.value)}
+                        required
+                      />
+                    </div>
+                    
+                    <div>
+                      <label className="block text-white/60 text-sm mb-2">密码</label>
+                      <input 
+                        type="password"
+                        className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+                        value={loginPassword}
+                        onChange={(e) => setLoginPassword(e.target.value)}
+                        required
+                      />
+                    </div>
+                    
+                    <Button 
+                      type="submit" 
+                      className="w-full" 
+                      disabled={isLoading}
+                    >
+                      {isLoading ? (
+                        <div className="inline-block w-6 h-6 border-2 border-t-transparent border-white rounded-full animate-spin"></div>
+                      ) : '登录'}
+                    </Button>
+                  </form>
+                  
+                  <div className="mt-6 text-center">
+                    <p className="text-white/60">
+                      还没有账户? 
+                      <Link href="/register" className="text-[var(--theme-accent)] hover:underline ml-1">
+                        注册
+                      </Link>
+                    </p>
+                  </div>
+                </div>
+              ) : (
+                <>
+                  {/* 个人信息标签页 */}
+                  {activeTab === "profile" && (
+                    <div className="space-y-6 p-6">
+                      <div className="flex justify-between items-center">
+                        <h2 className="text-xl font-bold text-white">个人信息</h2>
+                        <Button 
+                          onClick={toggleEditMode}
+                          variant={isEditing ? "default" : "outline"}
+                        >
+                          {isEditing ? "保存" : "编辑资料"}
+                        </Button>
+                      </div>
+                      
+                      <div className="space-y-4">
+                        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                          <div>
+                            <label className="block text-white/60 text-sm mb-1">用户名</label>
+                            {isEditing ? (
+                              <input 
+                                type="text" 
+                                name="username"
+                                className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+                                value={editedUser.username}
+                                onChange={handleUserDataChange}
+                              />
+                            ) : (
+                              <p className="bg-white/5 p-2 rounded-lg text-white">{userData.username}</p>
+                            )}
+                          </div>
+                          
+                          <div>
+                            <label className="block text-white/60 text-sm mb-1">邮箱</label>
+                            {isEditing ? (
+                              <input 
+                                type="email" 
+                                name="email"
+                                className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+                                value={editedUser.email}
+                                onChange={handleUserDataChange}
+                              />
+                            ) : (
+                              <p className="bg-white/5 p-2 rounded-lg text-white">{userData.email}</p>
+                            )}
+                          </div>
+                        </div>
+                        
+                        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                          <div>
+                            <label className="block text-white/60 text-sm mb-1">所属机构</label>
+                            {isEditing ? (
+                              <input 
+                                type="text" 
+                                name="institution"
+                                className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+                                value={editedUser.institution}
+                                onChange={handleUserDataChange}
+                              />
+                            ) : (
+                              <p className="bg-white/5 p-2 rounded-lg text-white">{userData.institution}</p>
+                            )}
+                          </div>
+                          
+                          <div>
+                            <label className="block text-white/60 text-sm mb-1">职位</label>
+                            {isEditing ? (
+                              <input 
+                                type="text" 
+                                name="position"
+                                className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+                                value={editedUser.position}
+                                onChange={handleUserDataChange}
+                              />
+                            ) : (
+                              <p className="bg-white/5 p-2 rounded-lg text-white">{userData.position}</p>
+                            )}
+                          </div>
+                        </div>
+                        
+                        <div>
+                          <label className="block text-white/60 text-sm mb-1">研究领域</label>
+                          {isEditing ? (
+                            <input 
+                              type="text" 
+                              name="researchFields"
+                              className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+                              value={editedUser.researchFields.join(', ')}
+                              onChange={handleResearchFieldsChange}
+                            />
+                          ) : (
+                            <div className="bg-white/5 p-2 rounded-lg text-white">
+                              <div className="flex flex-wrap gap-2">
+                                {(userData.researchFields || []).map((field, index) => (
+                                  <span 
+                                    key={index}
+                                    className="px-2 py-1 rounded-full bg-[var(--theme-accent)]/20 text-white text-sm"
+                                  >
+                                    {field}
+                                  </span>
+                                ))}
+                              </div>
+                            </div>
+                          )}
+                        </div>
+                      </div>
+                      
+                      {/* 账户信息卡片 */}
+                      <div className="mt-6 p-4 bg-white/5 rounded-lg">
+                        <h3 className="text-lg font-medium text-white mb-3">账户信息</h3>
+                        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                          <div>
+                            <p className="text-white/60 text-sm">会员类型</p>
+                            <p className="text-white font-medium">{userData.membershipType}</p>
+                          </div>
+                          <div>
+                            <p className="text-white/60 text-sm">到期时间</p>
+                            <p className="text-white font-medium">{userData.expiryDate}</p>
+                          </div>
+                          <div>
+                            <p className="text-white/60 text-sm">可用积分</p>
+                            <p className="text-white font-medium">{userData.credits.toLocaleString()}</p>
+                          </div>
+                          <div>
+                            <p className="text-white/60 text-sm">注册时间</p>
+                            <p className="text-white font-medium">{userData.registerDate}</p>
+                          </div>
+                        </div>
+                        
+                        <div className="mt-4 flex justify-end gap-3">
+                          <Button variant="outline" size="sm">
+                            升级会员
+                          </Button>
+                          <Button size="sm">
+                            充值积分
+                          </Button>
+                        </div>
+                      </div>
+                    </div>
+                  )}
+                  
+                  {/* 使用记录标签页 */}
+                  {activeTab === "history" && (
+                    <div 
+                      className="p-6 max-h-[70vh] overflow-y-auto scroll-smooth" 
+                      style={{ 
+                        scrollbarWidth: 'thin',
+                        scrollbarColor: 'rgba(255,255,255,0.2) transparent'
+                      }}
+                    >
+                      <h2 className="text-xl font-bold text-white mb-6">使用记录</h2>
+                      {isLoading ? (
+                        <div className="text-center py-10">
+                          <div className="inline-block w-6 h-6 border-2 border-t-[var(--theme-accent)] border-white/20 rounded-full animate-spin"></div>
+                          <p className="mt-2 text-white/60">加载中...</p>
+                        </div>
+                      ) : (
+                        renderHistoryContent()
+                      )}
+                    </div>
+                  )}
+                  
+                  {/* 收藏论文标签页 */}
+                  {activeTab === "favorites" && (
+                    <div 
+                      className="p-6 max-h-[70vh] overflow-y-auto scroll-smooth" 
+                      style={{ 
+                        scrollbarWidth: 'thin',
+                        scrollbarColor: 'rgba(255,255,255,0.2) transparent'
+                      }}
+                    >
+                      <h2 className="text-xl font-bold text-white mb-6">收藏论文</h2>
+                      
+                      <div className="flex justify-between items-center mb-4">
+                        <div className="relative">
+                          <input 
+                            type="text" 
+                            placeholder="搜索收藏..." 
+                            className="bg-white/10 text-white border border-white/20 rounded-lg pl-8 pr-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+                          />
+                          <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="absolute left-2.5 top-2 text-white/60">
+                            <circle cx="11" cy="11" r="8"></circle>
+                            <path d="m21 21-4.3-4.3"></path>
+                          </svg>
+                        </div>
+                      </div>
+                      
+                      <div className="space-y-4">
+                        {/* 模拟收藏论文数据 */}
+                        {/* 实际应用中应从数据库获取收藏论文数据 */}
+                        {/* 这里使用本地数据作为示例 */}
+                        {/* 实际应用中应从数据库获取收藏论文数据 */}
+                        {/* 这里使用本地数据作为示例 */}
+                      </div>
+                      
+                      <div className="mt-8 text-center">
+                        <Link href="/">
+                          <Button variant="default">
+                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
+                              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
+                              <polyline points="7 10 12 15 17 10"></polyline>
+                              <line x1="12" x2="12" y1="15" y2="3"></line>
+                            </svg>
+                            浏览更多论文
+                          </Button>
+                        </Link>
+                      </div>
+                    </div>
+                  )}
+                  
+                  {/* 系统设置标签页 */}
+                  {activeTab === "settings" && (
+                    <div 
+                      className="p-6 max-h-[70vh] overflow-y-auto scroll-smooth" 
+                      style={{ 
+                        scrollbarWidth: 'thin',
+                        scrollbarColor: 'rgba(255,255,255,0.2) transparent'
+                      }}
+                    >
+                      <h2 className="text-xl font-bold text-white mb-6">系统设置</h2>
+                      
+                      {/* 主题设置 */}
+                      <div>
+                        <h3 className="text-lg font-medium text-white mb-3">外观与主题</h3>
+                        <div className="bg-white/5 p-4 rounded-lg">
+                          <div className="mb-4">
+                            <label className="block text-white/60 text-sm mb-2">主题模式</label>
+                            <div className="flex gap-3">
+                              <label className="flex items-center">
+                                <input 
+                                  type="radio" 
+                                  name="theme" 
+                                  className="mr-2" 
+                                  checked={isDarkMode} 
+                                  onChange={() => setIsDarkMode(true)}
+                                />
+                                <span className="text-white">深色</span>
+                              </label>
+                              <label className="flex items-center">
+                                <input 
+                                  type="radio" 
+                                  name="theme" 
+                                  className="mr-2"
+                                  checked={!isDarkMode}
+                                  onChange={() => setIsDarkMode(false)}
+                                />
+                                <span className="text-white">浅色</span>
+                              </label>
+                            </div>
+                          </div>
+                          
+                          <div>
+                            <label className="block text-white/60 text-sm mb-2">主题颜色</label>
+                            <div className="flex gap-3">
+                              <button className="w-8 h-8 rounded-full bg-blue-500 border-2 border-white"></button>
+                              <button className="w-8 h-8 rounded-full bg-purple-500"></button>
+                              <button className="w-8 h-8 rounded-full bg-green-500"></button>
+                              <button className="w-8 h-8 rounded-full bg-amber-500"></button>
+                              <button className="w-8 h-8 rounded-full bg-rose-500"></button>
+                            </div>
+                          </div>
+                        </div>
+                      </div>
+                      
+                      {/* 通知设置 */}
+                      <div>
+                        <h3 className="text-lg font-medium text-white mb-3">通知设置</h3>
+                        <div className="bg-white/5 p-4 rounded-lg space-y-3">
+                          <div className="flex items-center justify-between">
+                            <div>
+                              <label className="text-white">电子邮件通知</label>
+                              <p className="text-white/60 text-xs mt-1">接收系统重要通知和更新</p>
+                            </div>
+                            <label className="relative inline-flex items-center cursor-pointer">
+                              <input 
+                                type="checkbox" 
+                                className="sr-only peer" 
+                                checked={notificationSettings.email}
+                                onChange={() => handleNotificationChange('email')}
+                              />
+                              <div className="w-11 h-6 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--theme-accent)]"></div>
+                            </label>
+                          </div>
+                          
+                          <div className="flex items-center justify-between">
+                            <div>
+                              <label className="text-white">新研究成果提醒</label>
+                              <p className="text-white/60 text-xs mt-1">获取相关研究领域的最新动态</p>
+                            </div>
+                            <label className="relative inline-flex items-center cursor-pointer">
+                              <input 
+                                type="checkbox" 
+                                className="sr-only peer" 
+                                checked={notificationSettings.research}
+                                onChange={() => handleNotificationChange('research')}
+                              />
+                              <div className="w-11 h-6 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--theme-accent)]"></div>
+                            </label>
+                          </div>
+                          
+                          <div className="flex items-center justify-between">
+                            <div>
+                              <label className="text-white">系统更新通知</label>
+                              <p className="text-white/60 text-xs mt-1">了解新功能和系统改进</p>
+                            </div>
+                            <label className="relative inline-flex items-center cursor-pointer">
+                              <input 
+                                type="checkbox" 
+                                className="sr-only peer" 
+                                checked={notificationSettings.updates}
+                                onChange={() => handleNotificationChange('updates')}
+                              />
+                              <div className="w-11 h-6 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--theme-accent)]"></div>
+                            </label>
+                          </div>
+                        </div>
+                      </div>
+                      
+                      {/* 隐私设置 */}
+                      <div>
+                        <h3 className="text-lg font-medium text-white mb-3">隐私与安全</h3>
+                        <div className="bg-white/5 p-4 rounded-lg space-y-3">
+                          <div className="flex items-center justify-between">
+                            <div>
+                              <label className="text-white">启用双因素认证</label>
+                              <p className="text-white/60 text-xs mt-1">增强账户安全性</p>
+                            </div>
+                            <label className="relative inline-flex items-center cursor-pointer">
+                              <input 
+                                type="checkbox" 
+                                className="sr-only peer" 
+                                checked={privacySettings.twoFactor}
+                                onChange={() => handlePrivacyChange('twoFactor')}
+                              />
+                              <div className="w-11 h-6 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--theme-accent)]"></div>
+                            </label>
+                          </div>
+                          
+                          <div className="flex items-center justify-between">
+                            <div>
+                              <label className="text-white">允许数据分析以改进服务</label>
+                              <p className="text-white/60 text-xs mt-1">帮助我们提升使用体验</p>
+                            </div>
+                            <label className="relative inline-flex items-center cursor-pointer">
+                              <input 
+                                type="checkbox" 
+                                className="sr-only peer" 
+                                checked={privacySettings.dataAnalysis}
+                                onChange={() => handlePrivacyChange('dataAnalysis')}
+                              />
+                              <div className="w-11 h-6 bg-white/20 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--theme-accent)]"></div>
+                            </label>
+                          </div>
+                          
+                          <div className="mt-4">
+                            <Button variant="outline" className="text-sm">修改密码</Button>
+                          </div>
+                        </div>
+                      </div>
+                      
+                      <div className="flex justify-between mt-6">
+                        <Button 
+                          variant="ghost" 
+                          className="text-red-400 hover:text-red-300 hover:bg-red-500/20"
+                          onClick={handleLogout}
+                        >
+                          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
+                            <path d="M10 16l-6-6 6-6"></path>
+                            <path d="M20 21v-7a4 4 0 0 0-4-4H5"></path>
+                          </svg>
+                          退出登录
+                        </Button>
+                        <Button onClick={handleSaveSettings}>
+                          保存设置
+                        </Button>
+                      </div>
+                    </div>
+                  )}
+                </>
+              )}
+            </GlassCard>
+          </div>
+        </div>
+      </div>
+    </main>
+  );
+}

+ 274 - 0
lightstar-web/src/app/register/page.tsx

@@ -0,0 +1,274 @@
+"use client";
+
+import { useState, useEffect } from 'react';
+import { GlassCard } from "@/components/ui/glass-card";
+import { Button } from "@/components/ui/button";
+import axios from 'axios';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+
+// API基础URL
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
+
+// 在文件顶部添加类型定义
+interface ApiError {
+  message: string;
+  response?: {
+    status: number;
+    data: any;
+  };
+}
+
+interface ApiErrorResponse {
+  detail?: string;
+  status?: number;
+  [key: string]: any;
+}
+
+type FetchError = Error & {
+  response?: {
+    status: number;
+    data: ApiErrorResponse;
+  };
+}
+
+export default function RegisterPage() {
+  const [username, setUsername] = useState('');
+  const [email, setEmail] = useState('');
+  const [password, setPassword] = useState('');
+  const [confirmPassword, setConfirmPassword] = useState('');
+  const [institution, setInstitution] = useState('江西财经大学');
+  const [position, setPosition] = useState('学生');
+  const [error, setError] = useState('');
+  const [isLoading, setIsLoading] = useState(false);
+  const router = useRouter();
+  
+  // 添加测试函数
+  const testBackendConnection = async () => {
+    try {
+      console.log("尝试连接到后端健康检查端点...");
+      const response = await fetch("http://localhost:8000/health", {
+        method: "GET",
+      });
+      const data = await response.text();
+      console.log("后端连接测试成功:", data);
+      return true;
+    } catch (error) {
+      console.error("后端连接测试失败:", error);
+      return false;
+    }
+  };
+
+  // 在组件挂载时测试连接
+  useEffect(() => {
+    testBackendConnection();
+  }, []);
+  
+  const handleRegister = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError('');
+    
+    // 验证密码匹配
+    if (password !== confirmPassword) {
+      setError('两次输入的密码不匹配');
+      return;
+    }
+    
+    setIsLoading(true);
+    
+    const registerData = {
+      username,
+      email,
+      password,
+      institution,
+      position
+    };
+    
+    try {
+      console.log("注册请求开始...");
+      
+      // 使用fetch API
+      const response = await fetch(`${API_BASE_URL}/auth/register`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(registerData)
+      });
+      
+      const responseText = await response.text();
+      console.log("注册响应状态:", response.status);
+      
+      if (!response.ok) {
+        // 尝试解析错误JSON
+        let errorDetail: string;
+        try {
+          const errorData = JSON.parse(responseText);
+          errorDetail = errorData.detail || `注册失败: ${response.status}`;
+        } catch {
+          // 如果无法解析JSON,使用原始文本
+          errorDetail = responseText || `注册失败: ${response.status}`;
+        }
+        
+        throw new Error(errorDetail);
+      }
+      
+      // 解析成功响应
+      const data = JSON.parse(responseText);
+      console.log("注册成功:", data);
+      
+      // 自动登录
+      const formData = new FormData();
+      formData.append('username', username);
+      formData.append('password', password);
+      
+      const loginResponse = await fetch(`${API_BASE_URL}/auth/token`, {
+        method: 'POST',
+        body: formData
+      });
+      
+      if (!loginResponse.ok) {
+        throw new Error('注册成功但登录失败,请尝试直接登录');
+      }
+      
+      const loginData = await loginResponse.json();
+      localStorage.setItem('token', loginData.access_token);
+      
+      // 获取用户资料
+      const userResponse = await fetch(`${API_BASE_URL}/users/profile`, {
+        headers: {
+          'Authorization': `Bearer ${loginData.access_token}`
+        }
+      });
+      
+      if (!userResponse.ok) {
+        throw new Error('获取用户资料失败');
+      }
+      
+      const userData = await userResponse.json();
+      localStorage.setItem('userData', JSON.stringify(userData));
+      
+      // 跳转到首页
+      window.location.href = '/dashboard';
+    } catch (err) {
+      console.error('注册过程出错:', err);
+      
+      // 处理不同类型的错误
+      const errorMessage = err instanceof Error 
+        ? err.message 
+        : '注册失败,请稍后再试';
+      
+      // 友好的错误提示
+      if (errorMessage.includes('用户名已被使用')) {
+        setError('该用户名已被注册,请尝试其他用户名');
+      } else if (errorMessage.includes('邮箱已被使用')) {
+        setError('该邮箱已被注册,请使用其他邮箱');
+      } else {
+        setError(errorMessage);
+      }
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  return (
+    <main className="flex items-center justify-center min-h-screen p-4">
+      <GlassCard className="w-full max-w-md">
+        <h1 className="text-3xl font-bold text-center text-white mb-8">注册</h1>
+        
+        {error && (
+          <div className="bg-red-500/20 p-3 mb-6 rounded-lg text-white">
+            {error}
+          </div>
+        )}
+        
+        <form onSubmit={handleRegister} className="space-y-4">
+          <div>
+            <label className="block text-white/60 text-sm mb-2">用户名</label>
+            <input 
+              type="text"
+              className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              value={username}
+              onChange={(e) => setUsername(e.target.value)}
+              required
+            />
+          </div>
+          
+          <div>
+            <label className="block text-white/60 text-sm mb-2">邮箱</label>
+            <input 
+              type="email"
+              className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              value={email}
+              onChange={(e) => setEmail(e.target.value)}
+              required
+            />
+          </div>
+          
+          <div>
+            <label className="block text-white/60 text-sm mb-2">密码</label>
+            <input 
+              type="password"
+              className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              value={password}
+              onChange={(e) => setPassword(e.target.value)}
+              required
+              minLength={6}
+            />
+          </div>
+          
+          <div>
+            <label className="block text-white/60 text-sm mb-2">确认密码</label>
+            <input 
+              type="password"
+              className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              value={confirmPassword}
+              onChange={(e) => setConfirmPassword(e.target.value)}
+              required
+              minLength={6}
+            />
+          </div>
+          
+          <div>
+            <label className="block text-white/60 text-sm mb-2">所属机构</label>
+            <input 
+              type="text"
+              className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              value={institution}
+              onChange={(e) => setInstitution(e.target.value)}
+            />
+          </div>
+          
+          <div>
+            <label className="block text-white/60 text-sm mb-2">职位</label>
+            <input 
+              type="text"
+              className="w-full bg-white/10 text-white border border-white/20 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[var(--theme-accent)]/50"
+              value={position}
+              onChange={(e) => setPosition(e.target.value)}
+            />
+          </div>
+          
+          <Button 
+            type="submit" 
+            className="w-full mt-6" 
+            disabled={isLoading}
+          >
+            {isLoading ? (
+              <div className="inline-block w-6 h-6 border-2 border-t-transparent border-white rounded-full animate-spin"></div>
+            ) : '注册'}
+          </Button>
+        </form>
+        
+        <div className="mt-6 text-center">
+          <p className="text-white/60">
+            已有账号? 
+            <Link href="/login" className="text-[var(--theme-accent)] hover:underline ml-1">
+              登录
+            </Link>
+          </p>
+        </div>
+      </GlassCard>
+    </main>
+  );
+} 

+ 23 - 0
lightstar-web/src/components/AuthStateManager.tsx

@@ -0,0 +1,23 @@
+"use client";
+
+import { Geist, Geist_Mono } from "next/font/google";
+import Link from "next/link";
+import "./globals.css";
+import { Metadata } from "next";
+
+const geistSans = Geist({
+  variable: "--font-geist-sans",
+  subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+  variable: "--font-geist-mono",
+  subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+  title: "文献启明星",
+  description: "智能科研助手平台",
+};
+
+// 其余代码...

+ 18 - 0
lightstar-web/src/components/ClientInit.tsx

@@ -0,0 +1,18 @@
+"use client"
+
+import { useEffect } from "react";
+
+export default function ClientInit() {
+  useEffect(() => {
+    // 仅在开发环境中执行
+    if (process.env.NODE_ENV === 'development') {
+      // 清除所有存储的token和用户数据
+      localStorage.removeItem("token");
+      localStorage.removeItem("userData");
+      document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
+      console.log("开发环境:已清除登录状态");
+    }
+  }, []);
+
+  return null; // 这个组件不需要渲染任何内容
+} 

+ 1108 - 0
lightstar-web/src/components/forms/research-form.tsx

@@ -0,0 +1,1108 @@
+"use client";
+
+import React, { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { ErrorMessage } from '@/components/ui/error-message';
+
+// 声明全局window属性
+declare global {
+  interface Window {
+    sseRetryCount?: number;
+  }
+}
+
+// 定义类型
+interface ResearchFormProps {
+  className?: string;
+  onSubmit?: (data: any) => void;
+}
+
+// 定义模态框组件的Props接口
+interface PapersListModalProps {
+  directionIndex: number;
+  directionTitle: string;
+  papers: any[];
+  onClose: () => void;
+}
+
+// 论文列表模态框组件 - 只显示论文,不显示研究报告
+function PapersListModal({ directionIndex, directionTitle, papers, onClose }: PapersListModalProps) {
+  if (!papers || papers.length === 0) return null;
+  
+  return (
+    <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
+      <div className="bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
+        <div className="p-6">
+          {/* 标题和关闭按钮 */}
+          <div className="flex justify-between items-start mb-6 sticky top-0 bg-gray-800 pb-2 z-10">
+            <h3 className="text-xl font-bold text-white">
+              研究方向 {directionIndex + 1}: {directionTitle}
+            </h3>
+            <button 
+              onClick={onClose}
+              className="bg-gray-700 hover:bg-gray-600 rounded-full p-2 text-white/80 hover:text-white"
+            >
+              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                <line x1="18" y1="6" x2="6" y2="18"></line>
+                <line x1="6" y1="6" x2="18" y2="18"></line>
+              </svg>
+            </button>
+          </div>
+          
+          {/* 论文列表部分 */}
+          {papers && papers.length > 0 && (
+            <div>
+              <h4 className="text-lg font-medium text-white mb-3">论文列表 ({papers.length})</h4>
+              
+              <div className="space-y-4">
+                {papers.map((paper: any, index: number) => (
+                  <div key={index} className="p-4 bg-slate-700/50 rounded-md">
+                    {/* 论文标题 */}
+                    <div className="mb-3">
+                      {paper.link || paper.url ? (
+                        <a 
+                          href={paper.link || paper.url} 
+                          target="_blank" 
+                          rel="noopener noreferrer"
+                          className="text-blue-400 hover:text-blue-300 font-medium text-lg hover:underline"
+                        >
+                          {paper.title}
+                        </a>
+                      ) : (
+                        <h5 className="text-blue-400 font-medium text-lg">{paper.title}</h5>
+                      )}
+                    </div>
+                    
+                    {/* 作者信息 */}
+                    {paper.authors && paper.authors.length > 0 && (
+                      <div className="mb-3">
+                        <span className="text-sm text-white/60">作者:</span>
+                        <p className="text-white/90">{paper.authors.join(', ')}</p>
+                      </div>
+                    )}
+                    
+                    {/* 年份 */}
+                    <div className="mb-3">
+                      <span className="text-sm text-white/60">发表年份:</span>
+                      <p className="text-white/90">
+                        {paper.year || (paper.published && paper.published.substring(0, 4)) || "未知"}
+                      </p>
+                    </div>
+                    
+                    {/* 摘要 */}
+                    <div className="mb-4">
+                      <span className="text-sm text-white/60">摘要:</span>
+                      <div className="text-white/80 text-sm bg-slate-800/70 p-3 rounded-md mt-1">
+                        {paper.abstract || paper.summary || "此论文暂无摘要"}
+                      </div>
+                    </div>
+                    
+                    {/* 查看原文按钮 */}
+                    {(paper.link || paper.url) && (
+                      <a 
+                        href={paper.link || paper.url} 
+                        target="_blank" 
+                        rel="noopener noreferrer"
+                        className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium py-1.5 px-3 rounded-md transition-colors text-sm"
+                      >
+                        查看原文
+                      </a>
+                    )}
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+// 定义研究报告模态框Props接口
+interface ReportModalProps {
+  directionIndex: number;
+  directionTitle: string;
+  report: any;
+  onClose: () => void;
+}
+
+// 研究报告模态框组件 - 专门显示研究报告
+function ReportModal({ directionIndex, directionTitle, report, onClose }: ReportModalProps) {
+  if (!report) {
+    return (
+      <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
+        <div className="bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
+          <div className="p-6">
+            {/* 标题和关闭按钮 */}
+            <div className="flex justify-between items-start mb-6 sticky top-0 bg-gray-800 pb-2 z-10">
+              <h3 className="text-xl font-bold text-white">
+                研究报告: {directionTitle}
+              </h3>
+              <button 
+                onClick={onClose}
+                className="bg-gray-700 hover:bg-gray-600 rounded-full p-2 text-white/80 hover:text-white"
+              >
+                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                  <line x1="18" y1="6" x2="6" y2="18"></line>
+                  <line x1="6" y1="6" x2="18" y2="18"></line>
+                </svg>
+              </button>
+            </div>
+            
+            <div className="bg-white text-black p-6 rounded-md">
+              <div className="text-center py-8">
+                <svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 mx-auto text-yellow-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
+                </svg>
+                <h3 className="text-xl font-medium text-gray-900 mb-2">无法加载报告</h3>
+                <p className="text-gray-600 mb-4">
+                  方向 {directionIndex + 1} 的报告数据不可用或连接中断。
+                </p>
+                <p className="text-gray-500 text-sm mb-6">
+                  可能原因: 连接中断、报告生成失败或服务器错误。
+                </p>
+                <button
+                  onClick={onClose}
+                  className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
+                >
+                  返回
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+  
+  // 状态保存解析后的报告内容
+  const [parsedReport, setParsedReport] = useState<string>("");
+  
+  // 增强错误处理
+  useEffect(() => {
+    const extractReportContent = () => {
+      console.log(`处理方向${directionIndex + 1}的报告数据:`, report);
+      
+      try {
+        if (!report) {
+          return `无法加载报告内容。`;
+        }
+        
+        let content = "";
+        
+        // 处理多种可能的数据格式
+        if (typeof report === 'string') {
+          // 直接是字符串的情况
+          content = report;
+        } 
+        else if (typeof report === 'object') {
+          // 1. 有直接内容字段的情况
+          if (report.content) {
+            content = report.content;
+          }
+          // 2. 有英文或翻译内容字段的情况
+          else if (report.english_content || report.translated_content) {
+            content = report.translated_content || report.english_content;
+          }
+          // 3. 有report子对象的情况
+          else if (report.report) {
+            if (typeof report.report === 'string') {
+              // report字段是字符串
+              content = report.report;
+            } 
+            else if (typeof report.report === 'object' && report.report !== null) {
+              // report字段是对象,尝试提取其内容
+              content = report.report.english_content || 
+                       report.report.translated_content || 
+                       report.report.content ||
+                       JSON.stringify(report.report);
+            }
+          }
+          // 4. 没有找到任何内容字段,尝试寻找最长的字符串字段
+          if (!content) {
+            let maxLength = 0;
+            let bestField = '';
+            
+            for (const key in report) {
+              if (typeof report[key] === 'string' && report[key].length > maxLength) {
+                maxLength = report[key].length;
+                bestField = key;
+                content = report[key];
+              }
+            }
+            
+            console.log(`使用最长字符串字段: ${bestField}`);
+          }
+          
+          // 5. 仍然没找到,转为JSON
+          if (!content) {
+            content = JSON.stringify(report, null, 2);
+          }
+        }
+        
+        // 处理特殊字符
+        if (content) {
+          content = content
+            .replace(/\\n/g, '\n')
+            .replace(/\\"/g, '"')
+            .replace(/\\\\/g, '\\');
+        }
+        
+        return content || `无法解析报告内容。`;
+      } catch (error) {
+        console.error("解析报告出错:", error);
+        return `处理报告时出现错误: ${error}`;
+      }
+    };
+    
+    // 解析内容并更新状态
+    const content = extractReportContent();
+    setParsedReport(content);
+  }, [report, directionIndex]);
+  
+  // 创建HTML格式的报告内容用于下载
+  const createHtmlReport = () => {
+    // 创建HTML格式
+    let htmlContent = `<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>研究报告: ${directionTitle}</title>
+  <style>
+    body {
+      font-family: 'Arial', sans-serif;
+      line-height: 1.6;
+      color: #333;
+      max-width: 800px;
+      margin: 0 auto;
+      padding: 20px;
+    }
+    h1 {
+      font-size: 24px;
+      margin-top: 30px;
+      border-bottom: 1px solid #ddd;
+      padding-bottom: 10px;
+    }
+    h2 {
+      font-size: 20px;
+      margin-top: 25px;
+    }
+    p {
+      margin: 16px 0;
+    }
+    .header {
+      text-align: center;
+      margin-bottom: 40px;
+    }
+    .content {
+      text-align: justify;
+    }
+  </style>
+</head>
+<body>
+  <div class="header">
+    <h1>研究报告: ${directionTitle}</h1>
+  </div>
+  <div class="content">`;
+  
+    // 转换文本内容为HTML格式
+    // 替换标题
+    let content = parsedReport
+      .replace(/\*\*(\d+)\.\s+([^\*]+)\*\*/g, '<h1>$1. $2</h1>')
+      // 替换小节标题
+      .replace(/\*\*(\d+\.\d+)\s+([^\*]+)\*\*/g, '<h2>$1 $2</h2>')
+      // 替换段落
+      .split('\n\n').join('</p><p>');
+      
+    htmlContent += `<p>${content}</p>`;
+    htmlContent += `
+  </div>
+</body>
+</html>`;
+    
+    return htmlContent;
+  };
+  
+  // 创建下载链接
+  const createDownloadLink = () => {
+    const htmlContent = createHtmlReport();
+    const blob = new Blob([htmlContent], { type: 'text/html' });
+    const url = URL.createObjectURL(blob);
+    
+    return {
+      url,
+      filename: `研究报告_${directionTitle.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_')}.html`,
+      cleanup: () => URL.revokeObjectURL(url)
+    };
+  };
+  
+  // 下载链接状态
+  const [downloadLink, setDownloadLink] = useState<{
+    url: string;
+    filename: string;
+    cleanup: () => void;
+  } | null>(null);
+  
+  // 创建下载链接
+  useEffect(() => {
+    if (parsedReport) {
+      const link = createDownloadLink();
+      setDownloadLink(link);
+      
+      // 清理函数
+      return () => {
+        if (link && link.cleanup) link.cleanup();
+      };
+    }
+  }, [parsedReport, directionTitle]);
+  
+  // 直接显示原始内容以便调试
+  const showRawContent = () => {
+    console.log("显示原始内容");
+    alert(JSON.stringify(report, null, 2));
+  };
+  
+  return (
+    <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
+      <div className="bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
+        <div className="p-6">
+          {/* 标题和关闭按钮 */}
+          <div className="flex justify-between items-start mb-6 sticky top-0 bg-gray-800 pb-2 z-10">
+            <h3 className="text-xl font-bold text-white">
+              研究报告: {directionTitle}
+            </h3>
+            <button 
+              onClick={onClose}
+              className="bg-gray-700 hover:bg-gray-600 rounded-full p-2 text-white/80 hover:text-white"
+            >
+              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                <line x1="18" y1="6" x2="6" y2="18"></line>
+                <line x1="6" y1="6" x2="18" y2="18"></line>
+              </svg>
+            </button>
+          </div>
+          
+          {/* 下载和调试按钮 */}
+          <div className="mb-4 flex gap-2">
+            {downloadLink && (
+              <a 
+                href={downloadLink.url}
+                download={downloadLink.filename}
+                className="inline-flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
+              >
+                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
+                  <polyline points="7 10 12 15 17 10"></polyline>
+                  <line x1="12" y1="15" x2="12" y2="3"></line>
+                </svg>
+                下载HTML格式报告
+              </a>
+            )}
+            
+            <button
+              onClick={showRawContent}
+              className="inline-flex items-center gap-2 bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-md transition-colors text-sm"
+            >
+              查看原始数据
+            </button>
+          </div>
+          
+          {/* 研究报告内容 */}
+          <div className="bg-white text-black p-6 rounded-md prose max-w-none">
+            {/* 如果解析出的内容为空,显示待处理信息 */}
+            {!parsedReport && (
+              <div className="text-center py-8 text-gray-500">
+                <svg className="animate-spin h-8 w-8 mx-auto mb-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
+                  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                </svg>
+                <p>正在处理报告内容...</p>
+              </div>
+            )}
+            
+            {/* 显示解析后的内容,使用预格式化文本保留格式 */}
+            {parsedReport && (
+              <div 
+                className="whitespace-pre-wrap"
+                dangerouslySetInnerHTML={{ 
+                  __html: parsedReport
+                    .replace(/\*\*(\d+)\.\s+([^\*]+)\*\*/g, '<h2 class="text-xl font-bold mt-6 mb-4">$1. $2</h2>')
+                    .replace(/\*\*(\d+\.\d+)\s+([^\*]+)\*\*/g, '<h3 class="text-lg font-bold mt-5 mb-3">$1 $2</h3>')
+                    .replace(/\n\n/g, '<br/><br/>')
+                }}
+              ></div>
+            )}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+export function ResearchForm({ className = "", onSubmit }: ResearchFormProps) {
+  // 基本状态
+  const [researchTopic, setResearchTopic] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [processMessage, setProcessMessage] = useState('');
+  const [researchCompleted, setResearchCompleted] = useState(false); // 新增:标记研究是否已完成
+  
+  // 研究数据
+  const [keywordsData, setKeywordsData] = useState<any>(null);
+  const [directionsData, setDirectionsData] = useState<any>(null);
+  const [papersData, setPapersData] = useState<any[]>([]);
+  const [reportsData, setReportsData] = useState<any[]>([]);
+  const [researchData, setResearchData] = useState<any>(null);
+  
+  // 模态框状态
+  const [showPapersModal, setShowPapersModal] = useState<boolean>(false);
+  const [showReportModal, setShowReportModal] = useState<boolean>(false);
+  const [selectedDirectionIndex, setSelectedDirectionIndex] = useState<number | null>(null);
+  
+  // 添加新函数,用于开始新的研究
+  const startNewResearch = () => {
+    // 重置所有状态
+    setResearchTopic('');
+    setKeywordsData(null);
+    setDirectionsData(null);
+    setPapersData([]);
+    setReportsData([]);
+    setResearchData(null);
+    setProcessMessage('');
+    setError(null);
+    setResearchCompleted(false);
+    
+    // 重置全局重试计数
+    if (window.sseRetryCount) {
+      window.sseRetryCount = 0;
+    }
+  };
+  
+  // 表单提交
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    
+    if (!researchTopic.trim()) {
+      setError('请输入研究主题');
+      return;
+    }
+    
+    // 如果已经有研究完成,先重置状态
+    if (researchCompleted) {
+      startNewResearch();
+      // 延迟设置新主题并提交,确保状态重置完成
+      setTimeout(() => {
+        const newTopic = researchTopic; // 保存当前主题
+        const newEvent = new Event('submit') as React.FormEvent;
+        setResearchTopic(newTopic);
+        handleSubmit(newEvent);
+      }, 10);
+      return;
+    }
+    
+    setLoading(true);
+    setError(null);
+    setProcessMessage('正在连接服务器...');
+    
+    // 创建EventSource连接
+    try {
+      const safeResearchTopic = encodeURIComponent(researchTopic.trim());
+      const url = `/api/research/enhanced-process-stream?research_intent=${safeResearchTopic}`;
+      
+      const eventSource = new EventSource(url);
+      
+      // 连接事件处理
+      eventSource.onopen = () => {
+        setProcessMessage('连接成功,开始处理研究内容...');
+      };
+      
+      // 消息事件处理
+      eventSource.onmessage = (event) => {
+        try {
+          const data = JSON.parse(event.data);
+          console.log("收到事件数据:", data);
+          
+          // 更新处理消息
+          if (data.message) {
+            setProcessMessage(data.message);
+          }
+          
+          // 处理不同类型的事件
+          switch(data.status) {
+            case 'base_keywords_extracted':
+              if (data.data && data.data.keywords) {
+                setKeywordsData(data.data);
+                console.log("关键词数据:", data.data);
+              }
+              break;
+              
+            case 'research_topics_generated':
+              if (data.data && data.data.research_topics) {
+                setDirectionsData(data.data);
+                console.log("研究方向数据:", data.data);
+              }
+              break;
+              
+            case 'papers_ready':
+              if (data.data) {
+                console.log("论文数据:", data.data);
+                setPapersData(prev => {
+                  const newData = [...prev];
+                  if (data.data.direction_index !== undefined) {
+                    newData[data.data.direction_index] = data.data;
+                  }
+                  return newData;
+                });
+              }
+              break;
+              
+            case 'report_ready':
+              if (data.data) {
+                console.log("报告数据:", data.data);
+                
+                // 使用函数式更新确保报告数据正确合并
+                setReportsData((prevReports: any[]) => {
+                  const newReports = [...prevReports];
+                  if (data.data.direction_index !== undefined) {
+                    newReports[data.data.direction_index] = data.data;
+                  }
+                  return newReports;
+                });
+                
+                // 如果是最后一个报告已生成,标记研究为完成状态
+                if (data.data.direction_index === 4 || 
+                    (directionsData?.research_topics && 
+                     data.data.direction_index === directionsData.research_topics.length - 1)) {
+                  
+                  console.log("最后一个报告已生成,研究完成");
+                  
+                  // 使用setTimeout确保报告数据先被更新完成
+                  setTimeout(() => {
+                    setResearchData({
+                      research_intent: researchTopic,
+                      keywords: keywordsData?.keywords || [],
+                      directions: directionsData?.research_topics || [],
+                      papers_by_direction: papersData,
+                      reports: reportsData // 直接使用当前状态
+                    });
+                    
+                    setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
+                    setResearchCompleted(true);
+                    eventSource.close();
+                    setLoading(false);
+                  }, 100);
+                }
+              }
+              break;
+              
+            case 'completed':
+              setResearchData({
+                research_intent: researchTopic,
+                keywords: keywordsData?.keywords || [],
+                directions: directionsData?.research_topics || [],
+                papers_by_direction: papersData,
+                reports: reportsData
+              });
+              setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
+              setResearchCompleted(true);
+              eventSource.close();
+              setLoading(false);
+              break;
+              
+            case 'error':
+              setError(data.message || "处理过程中发生错误");
+              eventSource.close();
+              setLoading(false);
+              break;
+          }
+        } catch (error) {
+          console.error("处理事件数据时出错:", error);
+        }
+      };
+      
+      // 错误处理 - 增强版
+      eventSource.onerror = (error: Event) => {
+        console.error("与研究服务连接失败:", error);
+        
+        // 重连尝试计数
+        if (!window.sseRetryCount) {
+          window.sseRetryCount = 0;
+        }
+        
+        // 检查是否已完成大部分研究 (即已经生成了最后一个方向的报告)
+        const isLastReportGenerated = reportsData.length > 0 && 
+          directionsData?.research_topics && 
+          reportsData.length >= directionsData.research_topics.length;
+          
+        // 如果已经生成了最后一个报告,直接标记为完成
+        if (isLastReportGenerated) {
+          console.log("所有报告都已生成,标记研究完成");
+          
+          // 构建最终研究数据
+          setResearchData({
+            research_intent: researchTopic,
+            keywords: keywordsData?.keywords || [],
+            directions: directionsData?.research_topics || [],
+            papers_by_direction: papersData,
+            reports: reportsData
+          });
+          
+          // 显示完成消息
+          setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
+          setResearchCompleted(true);
+          setLoading(false);
+          
+          // 确保关闭连接
+          try {
+            eventSource.close();
+          } catch (e) {
+            // 忽略关闭错误
+          }
+          return;
+        }
+        
+        // 增加重连机制 - 但只在未生成足够报告时尝试
+        if (window.sseRetryCount < 3) {
+          console.log(`尝试重新连接 (${window.sseRetryCount + 1}/3)...`);
+          window.sseRetryCount++;
+          
+          // 关闭当前连接
+          try {
+            eventSource.close();
+          } catch (e) {
+            // 忽略关闭错误
+          }
+          
+          // 等待1秒后重连
+          setTimeout(() => {
+            const newUrl = `/api/research/enhanced-process-stream?research_intent=${safeResearchTopic}`;
+            const newEventSource = new EventSource(newUrl);
+            
+            // 设置新的事件监听器
+            // ...复制原来的所有事件监听器...
+            
+            setProcessMessage(`尝试重新连接 (${window.sseRetryCount}/3)...`);
+          }, 1000);
+          
+          return;
+        }
+        
+        // 检查处理是否已基本完成
+        const hasCompletedMostWork = reportsData.some(report => report && (report.report || report.content));
+        
+        if (hasCompletedMostWork) {
+          console.log("尽管连接中断,但大部分报告已生成,尝试恢复...");
+          
+          // 所有研究方向都有数据了,认为是基本完成
+          if (reportsData.length >= directionsData?.research_topics?.length) {
+            console.log("所有报告都已生成,手动完成流程");
+            
+            // 构建最终研究数据
+            setResearchData({
+              research_intent: researchTopic,
+              keywords: keywordsData?.keywords || [],
+              directions: directionsData?.research_topics || [],
+              papers_by_direction: papersData,
+              reports: reportsData
+            });
+            
+            // 显示提示但继续
+            setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
+            setResearchCompleted(true);
+            setLoading(false);
+          } else {
+            // 还有部分未完成,给出警告
+            setError("与研究服务的连接中断,但部分报告已生成。您可以查看现有结果,或开始新的研究。");
+            setResearchCompleted(true);
+            setLoading(false);
+          }
+        } else {
+          // 完全失败的情况
+          setError("与研究服务的连接中断,可能原因:1) 后端服务未运行 2) 网络连接问题 3) 服务器错误");
+          setProcessMessage("您可以开始新的研究,或联系管理员获取帮助。");
+          setLoading(false);
+        }
+        
+        // 确保关闭连接
+        try {
+          eventSource.close();
+        } catch (e) {
+          // 忽略关闭错误
+        }
+      };
+      
+    } catch (error) {
+      console.error("创建EventSource失败:", error);
+      setError("无法连接到研究服务");
+      setLoading(false);
+    }
+  };
+  
+  // 取消处理
+  const handleCancel = () => {
+    setLoading(false);
+  };
+  
+  // 打开论文列表模态框 - 修复论文模态框打开问题
+  const openPapersModal = (directionIndex: number) => {
+    console.log("打开论文模态框,方向索引:", directionIndex);
+    setSelectedDirectionIndex(directionIndex);
+    setShowPapersModal(true);
+    setShowReportModal(false);
+  };
+  
+  // 关闭论文列表模态框
+  const closePapersModal = () => {
+    setShowPapersModal(false);
+    setSelectedDirectionIndex(null);
+  };
+  
+  // 打开研究报告模态框 - 增加日志以便调试
+  const openReportModal = (directionIndex: number) => {
+    console.log("打开报告模态框,方向索引:", directionIndex);
+    console.log("报告数据:", reportsData[directionIndex]);
+    setSelectedDirectionIndex(directionIndex);
+    setShowReportModal(true);
+    setShowPapersModal(false);
+  };
+  
+  // 关闭研究报告模态框
+  const closeReportModal = () => {
+    setShowReportModal(false);
+    setSelectedDirectionIndex(null);
+  };
+
+  return (
+    <div className={className}>
+      {/* 标题和说明 */}
+      <div className="mb-6">
+        <h2 className="text-2xl font-bold text-white mb-2 text-center">研究助手</h2>
+      </div>
+
+      {/* 研究表单 */}
+      <form onSubmit={handleSubmit} className="space-y-4">
+        {/* 研究主题输入 */}
+        <div className="space-y-2">
+          <label htmlFor="research-topic" className="block text-sm font-medium text-white">
+            研究主题
+          </label>
+          <textarea
+            id="research-topic"
+            placeholder="输入您想研究的主题,例如:新能源电池技术的最新发展及应用前景"
+            className="w-full h-24 px-3 py-2 text-white bg-white/5 border border-white/10 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+            value={researchTopic}
+            onChange={(e) => setResearchTopic(e.target.value)}
+            disabled={loading}
+          />
+        </div>
+
+        {/* 错误信息显示 */}
+        {error && <ErrorMessage message={error} onDismiss={() => setError(null)} />}
+
+        {/* 提交按钮 - 居中显示 */}
+        <div className="flex justify-center mt-4">
+          <Button
+            type="submit"
+            disabled={!researchTopic.trim() || loading}
+            className={`${
+              loading ? 'bg-blue-600/50' : 'bg-blue-600 hover:bg-blue-700'
+            } text-white font-medium py-2 px-4 rounded-md transition-colors`}
+          >
+            {loading ? "研究进行中..." : researchCompleted ? "开始新研究" : "开始研究"}
+          </Button>
+
+          {loading && (
+            <button
+              type="button"
+              onClick={handleCancel}
+              className="ml-2 text-white/60 hover:text-white text-sm"
+            >
+              取消
+            </button>
+          )}
+        </div>
+      </form>
+
+      {/* 处理中状态显示 */}
+      {loading && (
+        <div className="mt-8 space-y-6">
+          {/* 状态消息 */}
+          <div className="p-4 bg-blue-900/30 rounded-lg border border-blue-800/50">
+            <div className="flex items-center">
+              <div className="mr-3">
+                <svg className="animate-spin h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+                  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
+                  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+                </svg>
+              </div>
+              <div>
+                <p className="font-medium text-white">研究进行中...</p>
+                <p className="text-sm text-white/80">{processMessage}</p>
+              </div>
+            </div>
+          </div>
+
+          {/* 关键词显示 */}
+          {keywordsData && keywordsData.keywords && keywordsData.keywords.length > 0 && (
+            <div className="p-4 bg-white/5 rounded-lg">
+              <h3 className="text-sm font-medium text-white mb-2">提取到的关键词:</h3>
+              <div className="flex flex-wrap gap-2">
+                {keywordsData.keywords.map((keyword: string, index: number) => (
+                  <span 
+                    key={index} 
+                    className="px-3 py-1 bg-blue-500/20 rounded-full text-blue-300"
+                  >
+                    {keyword}
+                  </span>
+                ))}
+              </div>
+            </div>
+          )}
+
+          {/* 研究方向显示 */}
+          {directionsData && directionsData.research_topics && directionsData.research_topics.length > 0 && (
+            <div className="p-4 bg-white/5 rounded-lg">
+              <h3 className="text-sm font-medium text-white mb-2">生成的研究方向:</h3>
+              <div className="space-y-2">
+                {directionsData.research_topics.map((topic: any, index: number) => (
+                  <div key={index} className="p-2 bg-white/10 rounded-md">
+                    <p className="font-medium text-white">
+                      {index + 1}. {topic.english_title || topic.title}
+                    </p>
+                    {topic.keywords && topic.keywords.length > 0 && (
+                      <div className="mt-1 flex flex-wrap gap-1">
+                        {topic.keywords.map((kw: string, kidx: number) => (
+                          <span key={kidx} className="px-1.5 py-0.5 text-xs bg-white/10 rounded-full text-white/70">
+                            {kw}
+                          </span>
+                        ))}
+                      </div>
+                    )}
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+          
+          {/* 已检索的论文和报告 */}
+          {(papersData.length > 0 || reportsData.length > 0) && (
+            <div className="p-4 bg-white/5 rounded-lg">
+              <h3 className="text-sm font-medium text-white mb-2">研究进展:</h3>
+              <div className="space-y-3">
+                {directionsData && directionsData.research_topics && directionsData.research_topics.map((topic: any, index: number) => {
+                  const hasReport = reportsData[index] && (reportsData[index].report || reportsData[index].content);
+                  const hasPapers = papersData[index] && papersData[index].papers && papersData[index].papers.length > 0;
+                  
+                  if (!hasReport && !hasPapers) return null;
+                  
+                  return (
+                    <div key={index} className="p-2 bg-white/10 rounded-md">
+                      <p className="font-medium text-white mb-1">
+                        方向 {index + 1}: {topic.english_title || topic.title}
+                      </p>
+                      
+                      {hasReport && (
+                        <div className="ml-2 text-sm text-white/80 mb-1 flex items-center">
+                          <span className="text-green-400 mr-1">✓</span> 研究报告已生成
+                          <button
+                            onClick={() => openReportModal(index)}
+                            className="ml-2 text-xs text-blue-400 hover:text-blue-300"
+                          >
+                            查看报告
+                          </button>
+                        </div>
+                      )}
+                      
+                      {hasPapers && (
+                        <div className="ml-2 text-sm text-white/80 flex items-center">
+                          <span className="text-green-400 mr-1">✓</span> 已找到 {papersData[index].papers.length} 篇相关论文
+                          <button
+                            onClick={() => openPapersModal(index)}
+                            className="ml-2 text-xs text-blue-400 hover:text-blue-300"
+                          >
+                            查看论文
+                          </button>
+                        </div>
+                      )}
+                    </div>
+                  );
+                })}
+              </div>
+            </div>
+          )}
+        </div>
+      )}
+
+      {/* 研究完成后状态显示 */}
+      {researchCompleted && !loading && (
+        <div className="mt-8 space-y-6">
+          {/* 完成状态标志 */}
+          <div className="p-4 bg-green-900/30 rounded-lg border border-green-800/50">
+            <div className="flex items-center">
+              <div className="mr-3">
+                <svg className="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
+                  <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
+                </svg>
+              </div>
+              <div>
+                <p className="font-medium text-white">研究完成</p>
+                <p className="text-sm text-white/80">{processMessage}</p>
+              </div>
+            </div>
+          </div>
+
+          {/* 研究结果 */}
+          <div>
+            <h3 className="text-xl font-bold text-white">研究结果</h3>
+            
+            {/* 关键词部分 */}
+            {keywordsData && keywordsData.keywords && keywordsData.keywords.length > 0 && (
+              <div className="p-4 bg-white/5 rounded-lg mt-4">
+                <h4 className="text-lg font-medium text-white mb-2">关键词</h4>
+                <div className="flex flex-wrap gap-2">
+                  {keywordsData.keywords.map((keyword: string, index: number) => (
+                    <span 
+                      key={index} 
+                      className="px-3 py-1 bg-blue-500/20 rounded-full text-blue-300"
+                    >
+                      {keyword}
+                    </span>
+                  ))}
+                </div>
+              </div>
+            )}
+            
+            {/* 研究报告部分 */}
+            {directionsData && directionsData.research_topics && directionsData.research_topics.length > 0 && (
+              <div className="space-y-6 mt-4">
+                <h4 className="text-lg font-medium text-white">研究报告</h4>
+                
+                {directionsData.research_topics.map((direction: any, index: number) => {
+                  // 检查该方向是否有报告数据
+                  const hasReport = reportsData && reportsData[index];
+                  
+                  if (!hasReport) return null; // 没有报告数据则跳过显示
+                  
+                  return (
+                    <div key={index} className="p-4 bg-white/5 rounded-lg">
+                      <h5 className="text-md font-medium text-white mb-3">
+                        {index + 1}. {direction.english_title || direction.title}
+                      </h5>
+                      
+                      {/* 方向的关键词 */}
+                      {direction.keywords && direction.keywords.length > 0 && (
+                        <div className="mb-4">
+                          <div className="text-sm text-white/60 mb-1">关键词:</div>
+                          <div className="flex flex-wrap gap-1">
+                            {direction.keywords.map((kw: string, kidx: number) => (
+                              <span key={kidx} className="px-2 py-0.5 text-xs bg-white/10 rounded-full text-white/80">
+                                {kw}
+                              </span>
+                            ))}
+                          </div>
+                        </div>
+                      )}
+                      
+                      {/* 研究报告与论文分开显示,分别用按钮打开不同模态框 */}
+                      <div className="flex flex-wrap gap-4 mb-4">
+                        {/* 查看研究报告按钮 */}
+                        <button
+                          onClick={() => openReportModal(index)}
+                          className="inline-flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
+                        >
+                          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
+                            <polyline points="14 2 14 8 20 8"></polyline>
+                            <line x1="16" y1="13" x2="8" y2="13"></line>
+                            <line x1="16" y1="17" x2="8" y2="17"></line>
+                            <polyline points="10 9 9 9 8 9"></polyline>
+                          </svg>
+                          查看研究报告
+                        </button>
+                        
+                        {/* 查看论文列表按钮 */}
+                        {papersData[index] && papersData[index].papers && papersData[index].papers.length > 0 && (
+                          <button
+                            onClick={() => openPapersModal(index)}
+                            className="inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
+                          >
+                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                              <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
+                              <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
+                            </svg>
+                            查看相关论文 ({papersData[index].papers.length})
+                          </button>
+                        )}
+                      </div>
+                      
+                      {/* 论文列表预览 - 仅显示标题 */}
+                      {papersData[index] && papersData[index].papers && papersData[index].papers.length > 0 && (
+                        <div>
+                          <div className="text-sm font-medium text-white/80 mb-2">
+                            论文预览:
+                          </div>
+                          
+                          <div className="space-y-1.5 mb-2">
+                            {papersData[index].papers.slice(0, 2).map((paper: any, pidx: number) => (
+                              <a 
+                                key={pidx} 
+                                href={paper.link || paper.url} 
+                                target="_blank"
+                                rel="noopener noreferrer"
+                                className="text-blue-400 hover:text-blue-300 hover:underline block"
+                              >
+                                {paper.title}
+                              </a>
+                            ))}
+                            {papersData[index].papers.length > 2 && (
+                              <button
+                                onClick={() => openPapersModal(index)}
+                                className="text-xs text-blue-400 hover:text-blue-300 mt-1"
+                              >
+                                查看更多...
+                              </button>
+                            )}
+                          </div>
+                        </div>
+                      )}
+                    </div>
+                  );
+                })}
+              </div>
+            )}
+          </div>
+        </div>
+      )}
+
+      {/* 论文列表模态框 */}
+      {showPapersModal && selectedDirectionIndex !== null && (
+        <PapersListModal 
+          directionIndex={selectedDirectionIndex}
+          directionTitle={
+            (directionsData?.research_topics?.[selectedDirectionIndex]?.english_title || 
+            directionsData?.research_topics?.[selectedDirectionIndex]?.title) || 
+            `研究方向 ${selectedDirectionIndex + 1}`
+          }
+          papers={papersData[selectedDirectionIndex]?.papers || []}
+          onClose={closePapersModal}
+        />
+      )}
+
+      {/* 研究报告模态框 */}
+      {showReportModal && selectedDirectionIndex !== null && (
+        <ReportModal 
+          directionIndex={selectedDirectionIndex}
+          directionTitle={
+            (directionsData?.research_topics?.[selectedDirectionIndex]?.english_title || 
+            directionsData?.research_topics?.[selectedDirectionIndex]?.title) || 
+            `研究方向 ${selectedDirectionIndex + 1}`
+          }
+          report={reportsData[selectedDirectionIndex]}
+          onClose={closeReportModal}
+        />
+      )}
+    </div>
+  );
+}

+ 34 - 0
lightstar-web/src/components/language-switcher.tsx

@@ -0,0 +1,34 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { motion } from "framer-motion";
+
+export function LanguageSwitcher() {
+  const [language, setLanguage] = useState("zh"); // Default to Chinese
+  
+  const toggleLanguage = () => {
+    const newLanguage = language === "zh" ? "en" : "zh";
+    setLanguage(newLanguage);
+    // Store the language preference in localStorage
+    localStorage.setItem("preferredLanguage", newLanguage);
+    // You could also update a global state or context here
+  };
+  
+  useEffect(() => {
+    // Load saved language preference on component mount
+    const savedLanguage = localStorage.getItem("preferredLanguage");
+    if (savedLanguage) {
+      setLanguage(savedLanguage);
+    }
+  }, []);
+  
+  return (
+    <button
+      onClick={toggleLanguage}
+      className="flex items-center justify-center h-8 px-3 rounded-full backdrop-blur-md bg-white/10 text-white hover:bg-white/20 transition-all"
+      title={language === "zh" ? "Switch to English" : "切换到中文"}
+    >
+      {language === "zh" ? "EN" : "中文"}
+    </button>
+  );
+} 

+ 53 - 0
lightstar-web/src/components/theme-selector.tsx

@@ -0,0 +1,53 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { motion } from "framer-motion";
+
+const themes = [
+  { id: "space", name: "深邃宇宙", color: "#4D61FC" },
+  { id: "ocean", name: "高级深海", color: "#26C5F3" },
+  { id: "tech", name: "现代科技", color: "#FF2D72" },
+  { id: "nightglow", name: "精致夜光", color: "#875EFF" },
+];
+
+export function ThemeSelector() {
+  const [selectedTheme, setSelectedTheme] = useState("space");
+
+  useEffect(() => {
+    // 应用选中的主题到body元素
+    const existingClasses = document.body.className.split(' ')
+      .filter(cls => !cls.startsWith('theme-'));
+    document.body.className = [...existingClasses, `theme-${selectedTheme}`].join(' ');
+    
+    // 添加本地存储,记住用户的主题选择
+    localStorage.setItem('selectedTheme', selectedTheme);
+  }, [selectedTheme]);
+  
+  // 从本地存储恢复用户主题
+  useEffect(() => {
+    const savedTheme = localStorage.getItem('selectedTheme');
+    if (savedTheme) {
+      setSelectedTheme(savedTheme);
+    }
+  }, []);
+
+  return (
+    <div className="absolute top-4 right-4 z-20">
+      <div className="flex gap-2 backdrop-blur-md bg-white/10 p-2 rounded-full">
+        {themes.map((theme) => (
+          <button
+            key={theme.id}
+            onClick={() => setSelectedTheme(theme.id)}
+            className={`w-6 h-6 rounded-full transition-all ${
+              selectedTheme === theme.id
+                ? "scale-110 shadow-lg"
+                : "opacity-70"
+            }`}
+            style={{ backgroundColor: theme.color }}
+            title={theme.name}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}

+ 44 - 0
lightstar-web/src/components/ui/button.tsx

@@ -0,0 +1,44 @@
+"use client";
+
+import React from "react";
+import { cn } from "@/lib/utils";
+
+interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+  children: React.ReactNode;
+  variant?: "default" | "outline" | "ghost";
+  size?: "default" | "sm" | "lg";
+}
+
+export function Button({
+  children,
+  className,
+  variant = "default",
+  size = "default",
+  ...props
+}: ButtonProps) {
+  const variantClasses = {
+    default: "bg-[var(--theme-accent)] hover:bg-[var(--theme-accent)]/80 text-white",
+    outline: "bg-transparent border border-white/20 hover:bg-white/10 text-white",
+    ghost: "bg-transparent hover:bg-white/10 text-white",
+  };
+
+  const sizeClasses = {
+    default: "px-4 py-2 text-sm",
+    sm: "px-3 py-1 text-xs",
+    lg: "px-6 py-3 text-base",
+  };
+
+  return (
+    <button
+      className={cn(
+        "rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-white/30 disabled:opacity-50 disabled:pointer-events-none",
+        variantClasses[variant],
+        sizeClasses[size],
+        className
+      )}
+      {...props}
+    >
+      {children}
+    </button>
+  );
+} 

+ 23 - 0
lightstar-web/src/components/ui/error-message.tsx

@@ -0,0 +1,23 @@
+interface ErrorMessageProps {
+  message: string;
+  onDismiss: () => void;
+}
+
+export function ErrorMessage({ message, onDismiss }: ErrorMessageProps) {
+  return (
+    <div className="p-4 mb-4 rounded-lg bg-red-500/20 border border-red-500/50 text-white">
+      <div className="flex justify-between items-start">
+        <div>
+          <h4 className="font-medium">处理请求时出错</h4>
+          <p className="mt-1 text-white/80">{message}</p>
+        </div>
+        <button 
+          onClick={onDismiss}
+          className="text-white/70 hover:text-white"
+        >
+          ✕
+        </button>
+      </div>
+    </div>
+  );
+} 

+ 42 - 0
lightstar-web/src/components/ui/glass-card.tsx

@@ -0,0 +1,42 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { motion } from "framer-motion";
+import React from "react";
+
+interface GlassCardProps extends React.HTMLAttributes<HTMLDivElement> {
+  children: React.ReactNode;
+  className?: string;
+  hoverEffect?: boolean;
+}
+
+export function GlassCard({
+  children,
+  className,
+  hoverEffect = true,
+  ...props
+}: GlassCardProps) {
+  return (
+    <motion.div
+      className={cn(
+        "relative rounded-2xl backdrop-blur-xl bg-white/15 dark:bg-slate-900/20 p-8 shadow-[0_8px_30px_rgba(0,0,0,0.12)] border border-white/10 dark:border-slate-700/10",
+        hoverEffect && "hover:bg-white/20 dark:hover:bg-slate-800/20 transition-all duration-300",
+        className
+      )}
+      initial={{ opacity: 0, y: 20 }}
+      animate={{ opacity: 1, y: 0 }}
+      transition={{ duration: 0.4, ease: "easeOut" }}
+      whileHover={
+        hoverEffect
+          ? { y: -8, boxShadow: "0 20px 40px rgba(0,0,0,0.2)" }
+          : {}
+      }
+      {...props}
+    >
+      {/* 装饰元素 */}
+      <div className="absolute top-0 right-0 -mt-4 -mr-4 h-10 w-10 rounded-full bg-gradient-to-br from-[var(--theme-highlight)] to-[var(--theme-accent)] opacity-70 blur-xl"></div>
+      <div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-8 w-8 rounded-full bg-gradient-to-tr from-[var(--theme-accent)] to-[var(--theme-highlight)] opacity-60 blur-xl"></div>
+      {children}
+    </motion.div>
+  );
+}

+ 20 - 0
lightstar-web/src/components/ui/keyword-chip.tsx

@@ -0,0 +1,20 @@
+import { useState } from "react";
+
+interface KeywordChipProps {
+  keyword: string;
+  translation?: string;
+  isEnglish: boolean;
+}
+
+export function KeywordChip({ 
+  keyword, 
+  translation,
+  isEnglish
+}: KeywordChipProps) {
+  // 移除不需要的翻译切换功能,只显示当前语言的关键词
+  return (
+    <div className="px-3 py-1 rounded-full bg-white/15 text-white/90 text-sm hover:bg-white/20 transition-colors">
+      {keyword}
+    </div>
+  );
+} 

+ 76 - 0
lightstar-web/src/components/ui/modal.tsx

@@ -0,0 +1,76 @@
+import React, { useEffect, useRef } from 'react';
+
+interface ModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  title: string;
+  children: React.ReactNode;
+  maxWidth?: string;
+}
+
+export const Modal: React.FC<ModalProps> = ({ 
+  isOpen, 
+  onClose, 
+  title, 
+  children,
+  maxWidth = "max-w-4xl"
+}) => {
+  const modalRef = useRef<HTMLDivElement>(null);
+
+  // 处理ESC键关闭
+  useEffect(() => {
+    const handleEsc = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    
+    if (isOpen) {
+      document.addEventListener('keydown', handleEsc);
+      document.body.style.overflow = 'hidden'; // 防止背景滚动
+    }
+    
+    return () => {
+      document.removeEventListener('keydown', handleEsc);
+      document.body.style.overflow = 'auto';
+    };
+  }, [isOpen, onClose]);
+
+  // 处理点击外部关闭
+  const handleBackdropClick = (e: React.MouseEvent) => {
+    if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
+      onClose();
+    }
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div 
+      className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm transition-opacity"
+      onClick={handleBackdropClick}
+    >
+      <div 
+        ref={modalRef}
+        className={`relative ${maxWidth} w-full max-h-[90vh] overflow-hidden bg-gray-900 rounded-lg shadow-xl transform transition-all`}
+        onClick={e => e.stopPropagation()}
+      >
+        {/* 模态框标题栏 */}
+        <div className="flex items-center justify-between px-6 py-4 border-b border-gray-700">
+          <h3 className="text-lg font-medium text-white">{title}</h3>
+          <button
+            onClick={onClose}
+            className="text-gray-400 hover:text-white transition-colors focus:outline-none"
+          >
+            <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
+              <path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd"></path>
+            </svg>
+          </button>
+        </div>
+        
+        {/* 模态框内容 */}
+        <div className="p-6 overflow-y-auto" style={{maxHeight: 'calc(90vh - 60px)'}}>
+          {children}
+        </div>
+      </div>
+    </div>
+  );
+}; 

+ 167 - 0
lightstar-web/src/components/ui/paper-list.tsx

@@ -0,0 +1,167 @@
+import { useState } from "react";
+import { motion } from "framer-motion";
+
+interface Paper {
+  id: string;
+  title: string;
+  authors: string[];
+  summary: string;
+  published: string;
+  link: string;
+  source: string;
+  cluster?: number;  // 新增:论文所属聚类
+}
+
+interface Cluster {
+  id: number;
+  name: string;
+  keywords: string[];
+}
+
+interface PaperListProps {
+  papers: Paper[];
+  clusters?: Cluster[];  // 新增:聚类信息
+  onViewReport: () => void;
+}
+
+export function PaperList({ papers, clusters, onViewReport }: PaperListProps) {
+  const [expandedPaper, setExpandedPaper] = useState<string | null>(null);
+  
+  if (!papers || papers.length === 0) {
+    return (
+      <div className="text-center py-8 text-white/70">
+        未找到相关论文
+      </div>
+    );
+  }
+  
+  const toggleExpand = (id: string) => {
+    setExpandedPaper(expandedPaper === id ? null : id);
+  };
+  
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h3 className="text-md font-medium text-white/90">找到 {papers.length} 篇相关论文</h3>
+        <button
+          onClick={onViewReport}
+          className="px-3 py-1 bg-white/10 hover:bg-white/20 rounded text-white/80 text-sm"
+        >
+          查看研究报告
+        </button>
+      </div>
+      
+      {/* 如果有聚类信息,按聚类展示论文 */}
+      {clusters && clusters.length > 0 ? (
+        <div className="space-y-6">
+          {clusters.map((cluster) => (
+            <div key={cluster.id} className="space-y-3">
+              <div className="border-l-2 border-[var(--theme-accent)] pl-3">
+                <h4 className="text-white/90 font-medium">{cluster.name}</h4>
+                
+                {cluster.keywords && cluster.keywords.length > 0 && (
+                  <div className="flex flex-wrap gap-1 mt-1">
+                    {cluster.keywords.map((keyword, idx) => (
+                      <span 
+                        key={idx} 
+                        className="text-xs px-2 py-0.5 rounded-full bg-white/10 text-white/60"
+                      >
+                        {keyword}
+                      </span>
+                    ))}
+                  </div>
+                )}
+              </div>
+              
+              <div className="space-y-4 pl-2">
+                {papers
+                  .filter(paper => paper.cluster === cluster.id)
+                  .map((paper) => (
+                    <PaperCard 
+                      key={paper.id} 
+                      paper={paper} 
+                      expanded={expandedPaper === paper.id}
+                      toggleExpand={() => toggleExpand(paper.id)}
+                    />
+                  ))
+                }
+              </div>
+            </div>
+          ))}
+        </div>
+      ) : (
+        <div className="space-y-4">
+          {/* 如果没有聚类信息,按普通列表展示 */}
+          {papers.map((paper) => (
+            <PaperCard 
+              key={paper.id} 
+              paper={paper} 
+              expanded={expandedPaper === paper.id}
+              toggleExpand={() => toggleExpand(paper.id)}
+            />
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
+
+// 提取论文卡片为独立组件
+function PaperCard({ paper, expanded, toggleExpand }) {
+  return (
+    <motion.div
+      initial={{ opacity: 0 }}
+      animate={{ opacity: 1 }}
+      className="p-4 rounded-lg bg-white/10 border border-white/20"
+    >
+      <div className="flex justify-between">
+        <h4 className="text-white/90 font-medium">{paper.title}</h4>
+        <button
+          onClick={toggleExpand}
+          className="ml-2 flex-shrink-0 text-white/60 hover:text-white"
+        >
+          {expanded ? "收起" : "展开"}
+        </button>
+      </div>
+      
+      <p className="text-white/70 text-sm mt-1">
+        {paper.authors.join(", ")}
+      </p>
+      
+      <p className="text-white/50 text-xs mt-1">
+        发布于: {new Date(paper.published).toLocaleDateString()}
+      </p>
+      
+      {expanded && (
+        <motion.div
+          initial={{ height: 0, opacity: 0 }}
+          animate={{ height: "auto", opacity: 1 }}
+          className="mt-3 space-y-3"
+        >
+          <p className="text-white/80 text-sm">
+            {paper.summary}
+          </p>
+          
+          <div className="flex gap-2">
+            <a
+              href={paper.link}
+              target="_blank"
+              rel="noopener noreferrer"
+              className="px-3 py-1 bg-[var(--theme-accent)] hover:bg-[var(--theme-accent)]/80 rounded text-white text-sm"
+            >
+              阅读论文
+            </a>
+            <a
+              href={`https://arxiv.org/pdf/${paper.id}.pdf`}
+              target="_blank"
+              rel="noopener noreferrer"
+              className="px-3 py-1 bg-white/20 hover:bg-white/30 rounded text-white text-sm"
+            >
+              下载PDF
+            </a>
+          </div>
+        </motion.div>
+      )}
+    </motion.div>
+  );
+} 

+ 77 - 0
lightstar-web/src/components/ui/report-panel.tsx

@@ -0,0 +1,77 @@
+import { useState } from "react";
+
+interface ReportPanelProps {
+  report: string;
+  direction: string;
+}
+
+export function ReportPanel({ report, direction }: ReportPanelProps) {
+  const [isExporting, setIsExporting] = useState(false);
+  
+  const handleExport = () => {
+    setIsExporting(true);
+    
+    try {
+      // Create a Blob with the report content
+      const blob = new Blob([
+        `# 研究报告: ${direction}\n\n${report}`
+      ], { type: 'text/markdown' });
+      
+      // Create a URL for the Blob
+      const url = URL.createObjectURL(blob);
+      
+      // Create a download link and trigger it
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `research-report-${Date.now()}.md`;
+      document.body.appendChild(a);
+      a.click();
+      
+      // Clean up
+      document.body.removeChild(a);
+      URL.revokeObjectURL(url);
+    } catch (error) {
+      console.error('Export error:', error);
+    } finally {
+      setIsExporting(false);
+    }
+  };
+  
+  // Format the report text with Markdown-like styling
+  const formatReport = () => {
+    return report.split('\n').map((line, index) => {
+      if (line.startsWith('# ')) {
+        return <h1 key={index} className="text-xl font-bold mt-6 mb-3">{line.slice(2)}</h1>;
+      } else if (line.startsWith('## ')) {
+        return <h2 key={index} className="text-lg font-bold mt-5 mb-2">{line.slice(3)}</h2>;
+      } else if (line.startsWith('### ')) {
+        return <h3 key={index} className="text-md font-bold mt-4 mb-2">{line.slice(4)}</h3>;
+      } else if (line.trim() === '') {
+        return <div key={index} className="my-2"></div>;
+      } else {
+        return <p key={index} className="my-2 text-white/80">{line}</p>;
+      }
+    });
+  };
+  
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h3 className="text-lg font-medium text-white/90">{direction}</h3>
+        <button
+          onClick={handleExport}
+          disabled={isExporting}
+          className="px-3 py-1 bg-[var(--theme-accent)] hover:bg-[var(--theme-accent)]/80 rounded text-white text-sm disabled:opacity-50"
+        >
+          {isExporting ? "导出中..." : "导出报告"}
+        </button>
+      </div>
+      
+      <div className="p-4 rounded-lg bg-white/10 border border-white/20 max-h-[600px] overflow-y-auto">
+        <div className="prose prose-invert prose-sm max-w-none">
+          {formatReport()}
+        </div>
+      </div>
+    </div>
+  );
+} 

+ 92 - 0
lightstar-web/src/components/ui/research-direction-card.tsx

@@ -0,0 +1,92 @@
+import { useState } from "react";
+import { motion } from "framer-motion";
+
+interface ResearchDirectionCardProps {
+  direction: string;
+  translation?: string;
+  description?: string;
+  keywords?: string[];
+  isEnglish: boolean;
+  isProcessing?: boolean;
+  hasResults?: boolean;
+  onClick: () => void;
+}
+
+export function ResearchDirectionCard({ 
+  direction, 
+  translation, 
+  description,
+  keywords,
+  isEnglish, 
+  isProcessing,
+  hasResults,
+  onClick 
+}: ResearchDirectionCardProps) {
+  const [showTranslation, setShowTranslation] = useState(false);
+  
+  // If English is the original language, we don't need to toggle translations
+  const displayText = isEnglish 
+    ? direction 
+    : (showTranslation ? direction : translation || direction);
+  
+  const toggleTranslation = (e: React.MouseEvent) => {
+    if (!isEnglish && translation) {
+      e.stopPropagation();
+      setShowTranslation(!showTranslation);
+    }
+  };
+  
+  return (
+    <motion.div
+      whileHover={{ scale: 1.01 }}
+      className="p-4 rounded-lg bg-white/10 hover:bg-white/15 border border-white/20 cursor-pointer transition-colors"
+      onClick={onClick}
+    >
+      <div className="flex justify-between items-start">
+        <p className="text-white/90 text-md font-medium">{displayText}</p>
+        
+        {!isEnglish && translation && (
+          <button 
+            className="ml-2 px-2 py-1 text-xs bg-white/20 hover:bg-white/30 rounded text-white/70 flex-shrink-0"
+            onClick={toggleTranslation}
+          >
+            {showTranslation ? "查看翻译" : "查看英文"}
+          </button>
+        )}
+      </div>
+      
+      {description && (
+        <p className="text-sm text-white/70 mt-2">{description}</p>
+      )}
+      
+      {keywords && keywords.length > 0 && (
+        <div className="flex flex-wrap gap-1 mt-2">
+          {keywords.map((keyword, idx) => (
+            <span 
+              key={idx} 
+              className="text-xs px-2 py-0.5 rounded-full bg-white/10 text-white/60"
+            >
+              {keyword}
+            </span>
+          ))}
+        </div>
+      )}
+      
+      <div className="flex items-center justify-between mt-3">
+        <span className="text-xs text-white/50">
+          {isProcessing ? "正在处理..." : hasResults ? "已完成分析" : "待处理"}
+        </span>
+        
+        {isProcessing && (
+          <div className="w-4 h-4 rounded-full border-2 border-transparent border-t-white/80 animate-spin"></div>
+        )}
+        
+        {hasResults && (
+          <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-400" viewBox="0 0 20 20" fill="currentColor">
+            <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
+          </svg>
+        )}
+      </div>
+    </motion.div>
+  );
+} 

+ 50 - 0
lightstar-web/src/middleware.ts

@@ -0,0 +1,50 @@
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+// 需要保护的路径
+const protectedPaths = [
+  '/profile',
+  '/research',
+  // 可以添加更多需要保护的路径
+];
+
+// 不需要重定向的路径
+const publicPaths = [
+  '/login',
+  '/register',
+  '/api/',
+  '/_next/',
+  '/static/',
+];
+
+export function middleware(request: NextRequest) {
+  // 获取路径
+  const path = request.nextUrl.pathname;
+  
+  // 检查是否为公共路径
+  if (publicPaths.some(publicPath => path.startsWith(publicPath))) {
+    return NextResponse.next();
+  }
+  
+  // 检查是否为保护路径
+  if (protectedPaths.some(protectedPath => path.startsWith(protectedPath))) {
+    // 检查是否有token(通过cookie)
+    const token = request.cookies.get('token')?.value;
+    
+    if (!token) {
+      // 没有token,重定向到登录页
+      const loginUrl = new URL('/login', request.url);
+      loginUrl.searchParams.set('redirect', path);
+      return NextResponse.redirect(loginUrl);
+    }
+  }
+  
+  return NextResponse.next();
+}
+
+export const config = {
+  matcher: [
+    // 匹配所有路径,除了API路由和静态资源
+    '/((?!api|_next/static|favicon.ico).*)',
+  ],
+}; 

+ 50 - 0
lightstar-web/tailwind.config.ts

@@ -0,0 +1,50 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+  content: [
+    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
+    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
+    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
+  ],
+  theme: {
+    extend: {
+      colors: {
+        // 颜色方案1: 深邃宇宙
+        space: {
+          dark: '#0B1026',
+          mid: '#1A1C4B',
+          light: '#21133A',
+          accent: '#4D61FC',
+          highlight: '#8C5CF4',
+        },
+        // 颜色方案2: 高级深海
+        ocean: {
+          dark: '#051937',
+          mid: '#004261',
+          light: '#004F7A',
+          accent: '#26C5F3',
+          highlight: '#1A7BF5',
+        },
+        // 颜色方案3: 现代科技
+        tech: {
+          dark: '#0F1117',
+          mid: '#151922',
+          light: '#131621',
+          accent: '#FF2D72',
+          highlight: '#0088FF',
+        },
+        // 颜色方案4: 夜光
+        nightglow: {
+          dark: '#09090B',
+          mid: '#18181B',
+          light: '#27272A',
+          accent: '#875EFF',
+          highlight: '#9D7EFF',
+        },
+      },
+    },
+  },
+  plugins: [],
+};
+
+export default config;

+ 27 - 0
lightstar-web/tsconfig.json

@@ -0,0 +1,27 @@
+{
+  "compilerOptions": {
+    "target": "ES2017",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "incremental": true,
+    "plugins": [
+      {
+        "name": "next"
+      }
+    ],
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+  "exclude": ["node_modules"]
+}

+ 1711 - 0
package-lock.json

@@ -0,0 +1,1711 @@
+{
+  "name": "Lightstar",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "dependencies": {
+        "autoprefixer": "^10.4.16",
+        "axios": "^1.9.0",
+        "clsx": "^2.1.1",
+        "framer-motion": "^12.7.3",
+        "postcss": "^8.4.31",
+        "tailwind-merge": "^3.2.0",
+        "tailwindcss": "^3.3.0"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.8",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+      "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.2.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.25",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.4.16",
+      "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.16.tgz",
+      "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "browserslist": "^4.21.10",
+        "caniuse-lite": "^1.0.30001538",
+        "fraction.js": "^4.3.6",
+        "normalize-range": "^0.1.2",
+        "picocolors": "^1.0.0",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/axios": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.9.0.tgz",
+      "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.24.4",
+      "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.24.4.tgz",
+      "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001713",
+      "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
+      "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ]
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.137",
+      "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
+      "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA=="
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fastq": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz",
+      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.3.tgz",
+      "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fraction.js": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz",
+      "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "patreon",
+        "url": "https://github.com/sponsors/rawify"
+      }
+    },
+    "node_modules/framer-motion": {
+      "version": "12.7.3",
+      "resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.7.3.tgz",
+      "integrity": "sha512-dNT4l5gEnUo2ytXLUBUf6AI21dZ77TMclDKE3ElaIHZ8m90nJ/NCcExW51zdSIaS0RhAS5iXcF7bEIxZe8XG2g==",
+      "dependencies": {
+        "motion-dom": "^12.7.3",
+        "motion-utils": "^12.7.2",
+        "tslib": "^2.4.0"
+      },
+      "peerDependencies": {
+        "@emotion/is-prop-valid": "*",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/is-prop-valid": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "1.21.7",
+      "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
+      "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/lilconfig": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz",
+      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+    },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/motion-dom": {
+      "version": "12.7.3",
+      "resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.7.3.tgz",
+      "integrity": "sha512-IjMt1YJHrvyvruFvmpmd6bGXXGCvmygrnvSb3aZ8KhOzF4H3PulU+cMBzH+U8TBJHjC/mnmJFRIA1Cu4vBfcBA==",
+      "dependencies": {
+        "motion-utils": "^12.7.2"
+      }
+    },
+    "node_modules/motion-utils": {
+      "version": "12.7.2",
+      "resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.7.2.tgz",
+      "integrity": "sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw=="
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.19",
+      "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz",
+      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.4.31",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz",
+      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.6",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-14.1.0.tgz",
+      "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz",
+      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-load-config": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
+      "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
+      "dependencies": {
+        "lilconfig": "^2.0.5",
+        "yaml": "^1.10.2"
+      },
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": ">=8.0.9",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "postcss": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.0.0.tgz",
+      "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==",
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.10"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/quick-lru": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-5.1.1.tgz",
+      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.10",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz",
+      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+      "dependencies": {
+        "is-core-module": "^2.16.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/sucrase": {
+      "version": "3.35.0",
+      "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.0.tgz",
+      "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "glob": "^10.3.10",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwind-merge": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.2.0.tgz",
+      "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/dcastil"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.0.tgz",
+      "integrity": "sha512-hOXlFx+YcklJ8kXiCAfk/FMyr4Pm9ck477G0m/us2344Vuj355IpoEDB5UmGAsSpTBmr+4ZhjzW04JuFXkb/fw==",
+      "dependencies": {
+        "arg": "^5.0.2",
+        "chokidar": "^3.5.3",
+        "color-name": "^1.1.4",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.2.12",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.17.2",
+        "lilconfig": "^2.0.6",
+        "micromatch": "^4.0.5",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.0.0",
+        "postcss": "^8.0.9",
+        "postcss-import": "^14.1.0",
+        "postcss-js": "^4.0.0",
+        "postcss-load-config": "^3.1.4",
+        "postcss-nested": "6.0.0",
+        "postcss-selector-parser": "^6.0.11",
+        "postcss-value-parser": "^4.2.0",
+        "quick-lru": "^5.1.1",
+        "resolve": "^1.22.1",
+        "sucrase": "^3.29.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=12.13.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.9"
+      }
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+      "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yaml": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmmirror.com/yaml/-/yaml-1.10.2.tgz",
+      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+      "engines": {
+        "node": ">= 6"
+      }
+    }
+  }
+}

+ 11 - 0
package.json

@@ -0,0 +1,11 @@
+{
+  "dependencies": {
+    "autoprefixer": "^10.4.16",
+    "axios": "^1.9.0",
+    "clsx": "^2.1.1",
+    "framer-motion": "^12.7.3",
+    "postcss": "^8.4.31",
+    "tailwind-merge": "^3.2.0",
+    "tailwindcss": "^3.3.0"
+  }
+}

BIN
postcss.config.js


+ 52 - 0
requirements.txt

@@ -0,0 +1,52 @@
+# 核心依赖
+requests==2.31.0
+arxiv==1.4.8
+python-dotenv==1.0.0
+
+# Web 界面
+streamlit>=1.26.0
+pandas>=1.3.0
+
+# 文本处理与向量化
+scikit-learn>=1.0.0
+sentence-transformers>=2.2.0
+numpy>=1.20.0
+
+# 数据处理
+pandas>=1.3.0
+
+# 可视化
+plotly>=5.5.0
+matplotlib>=3.4.0
+
+# 降维与聚类
+scipy>=1.7.0
+
+# 用户系统
+passlib[bcrypt]>=1.7.4
+bcrypt>=3.2.0   # 安全密码加密
+
+# 开发依赖 (可选,使用 pip install -e ".[dev]" 安装)
+pytest>=7.0.0,<8.0.0
+black>=22.0.0
+flake8>=5.0.0
+
+fastapi>=0.95.0
+uvicorn>=0.21.0
+pydantic>=1.10.7
+httpx>=0.24.0
+arxiv>=1.4.7
+sentence-transformers>=2.2.2
+scikit-learn>=1.2.2
+numpy>=1.24.3
+pandas>=2.0.0
+python-multipart>=0.0.6
+
+# 新增依赖
+tenacity>=8.2.2
+httpx>=0.24.1
+
+# 数据库依赖
+sqlalchemy>=1.4.0
+alembic>=1.7.0  # 用于数据库迁移
+python-jose[cryptography]>=3.3.0  # 用于JWT令牌 

+ 909 - 0
src/components/forms/research-form.tsx

@@ -0,0 +1,909 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { motion } from "framer-motion";
+import { KeywordChip } from "@/components/ui/keyword-chip";
+import { ResearchDirectionCard } from "@/components/ui/research-direction-card";
+import { PaperList } from "@/components/ui/paper-list";
+import { ReportPanel } from "@/components/ui/report-panel";
+import { ErrorMessage } from "@/components/ui/error-message";
+
+// 研究标题简化函数
+const simplifyResearchTitle = (text) => {
+  if (!text) return '';
+  
+  // 先提取引号中的内容作为最佳选项
+  const quoteMatch = text.match(/["""](.*?)["""]/);
+  if (quoteMatch && quoteMatch[1].length > 5) {
+    return quoteMatch[1].trim();
+  }
+  
+  // 提取标题的前半部分(通常是冒号前的内容)
+  const colonParts = text.split(/[::]/);
+  if (colonParts.length > 1 && colonParts[0].length > 5) {
+    // 只取冒号前的内容
+    let mainTitle = colonParts[0].trim();
+    // 再次清理序号和括号
+    mainTitle = mainTitle.replace(/^\d+\.\s*/, '')
+                         .replace(/\([^)]*\)/g, '')
+                         .replace(/([^)]*)/g, '')
+                         .replace(/\s*注[::].*$/, '');
+    return mainTitle;
+  }
+  
+  // 直接清理所有注释和说明
+  let cleaned = text
+    .replace(/^\d+\.\s*/, '')             // 移除序号
+    .replace(/\([^)]*\)/g, '')           // 移除所有英文括号内容
+    .replace(/([^)]*)/g, '')           // 移除所有中文括号内容
+    .replace(/[(\(]注[::].*?[)\)]/g, '') // 移除带注释的括号内容
+    .replace(/注[::].*?(。|$)/g, '')      // 移除"注:"到句号或结尾的内容
+    .replace(/\s+/g, ' ')                // 将多个空格合并为一个
+    .trim();
+  
+  // 如果还是太长,截断
+  if (cleaned.length > 40) {
+    cleaned = cleaned.substring(0, 40) + '...';
+  }
+  
+  return cleaned || text.substring(0, 40); // 如果清理后为空,使用原文前40个字符
+};
+
+export function ResearchForm({ onSubmit, className = "", onExport }: { onSubmit?: (data: any) => void, className?: string, onExport?: (data: any) => void }) {
+  const [loading, setLoading] = useState(false);
+  const [researchTopic, setResearchTopic] = useState("");
+  const [researchData, setResearchData] = useState<any>(null);
+  const [activeTab, setActiveTab] = useState("keywords");
+  const [selectedDirection, setSelectedDirection] = useState<string | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [progress, setProgress] = useState(1);
+  const [processingDirections, setProcessingDirections] = useState(false);
+  const [processingState, setProcessingState] = useState<string>("idle");
+  const [processMessage, setProcessMessage] = useState<string>("");
+  const [keywordsData, setKeywordsData] = useState<any>(null);
+  const [directionsData, setDirectionsData] = useState<any>(null);
+  const [papersData, setPapersData] = useState<any[]>([]);
+  const [reportsData, setReportsData] = useState<any[]>([]);
+  const [processingDirectionIndex, setProcessingDirectionIndex] = useState<number>(-1);
+  const [searchKeywordsData, setSearchKeywordsData] = useState<any>(null);
+  
+  // 创建状态来跟踪研究阶段
+  const [researchStage, setResearchStage] = useState("idle"); // idle, keywords, topics, searching, reporting, complete
+  
+  // 添加语言显示状态
+  const [showEnglishKeywords, setShowEnglishKeywords] = useState(false);
+  
+  // 添加模态框状态
+  const [modalOpen, setModalOpen] = useState(false);
+  const [modalContent, setModalContent] = useState<{
+    type: 'papers' | 'report',
+    topicIndex: number,
+    title: string
+  } | null>(null);
+  
+  // 添加论文外部链接处理状态
+  const [loadingPaperLink, setLoadingPaperLink] = useState(null);
+  
+  const API_URL = "http://localhost:8000";
+
+  // 进度计算函数
+  const calculateTaskBasedProgress = () => {
+    // 基于研究阶段和处理状态计算进度
+    // 总共定义了12个任务点,每个点完成后增加相应百分比
+    
+    let progressValue = 1; // 最小进度为1%
+    
+    switch (researchStage) {
+      case 'idle':
+        progressValue = 1;
+        break;
+      
+      case 'keywords':
+        // 关键词提取占15%的进度
+        if (keywordsData) {
+          progressValue = 15;
+        } else {
+          progressValue = 5;
+        }
+        break;
+      
+      case 'topics':
+        // 关键词已完成(15%) + 研究方向生成进度(15%)
+        if (directionsData && directionsData.research_topics) {
+          progressValue = 30;
+        } else {
+          progressValue = 20;
+        }
+        break;
+      
+      case 'searching':
+        // 关键词(15%) + 研究方向(15%) + 论文搜索进度(40%)
+        const completedPapers = papersData.filter(p => p && p.papers).length;
+        const topicsCount = directionsData?.research_topics?.length || 1;
+        
+        // 基础进度30% + 根据已完成的方向比例计算的进度(最多40%)
+        progressValue = 30 + Math.floor((completedPapers / topicsCount) * 40);
+        break;
+      
+      case 'reporting':
+        // 关键词(15%) + 研究方向(15%) + 论文搜索(40%) + 报告生成进度(30%)
+        const completedReports = reportsData.filter(r => r && r.report).length;
+        const totalDirections = directionsData?.research_topics?.length || 1;
+        
+        // 基础进度70% + 根据已完成的报告比例计算的进度(最多25%)
+        progressValue = 70 + Math.floor((completedReports / totalDirections) * 25);
+        break;
+      
+      case 'complete':
+        progressValue = 100;
+        break;
+    }
+    
+    return progressValue;
+  };
+
+  // 辅助函数用于决定步骤状态
+  const getStepStatus = (stepIndex: number) => {
+    const stages = ['keywords', 'topics', 'searching', 'reporting'];
+    const currentStageIndex = stages.indexOf(researchStage);
+    
+    if (currentStageIndex > stepIndex || researchStage === 'complete') {
+      return 'bg-green-500'; // 已完成
+    } else if (currentStageIndex === stepIndex) {
+      return 'bg-blue-500 animate-pulse'; // 进行中
+    } else {
+      return 'bg-gray-700 text-white/50'; // 未开始
+    }
+  };
+
+  const isStepComplete = (stepIndex: number) => {
+    const stages = ['keywords', 'topics', 'searching', 'reporting'];
+    const currentStageIndex = stages.indexOf(researchStage);
+    return currentStageIndex > stepIndex || researchStage === 'complete';
+  };
+
+  // 处理查看报告
+  const handleViewReport = (topicIndex: number, e: React.MouseEvent) => {
+    e.preventDefault();
+    setModalContent({
+      type: 'report',
+      topicIndex,
+      title: directionsData?.research_topics[topicIndex]?.title || `研究方向 ${topicIndex + 1}`
+    });
+    setModalOpen(true);
+  };
+
+  // 处理查看全部论文
+  const handleViewAllPapers = (topicIndex: number, e: React.MouseEvent) => {
+    e.preventDefault();
+    setModalContent({
+      type: 'papers',
+      topicIndex,
+      title: directionsData?.research_topics[topicIndex]?.title || `研究方向 ${topicIndex + 1}`
+    });
+    setModalOpen(true);
+  };
+
+  // 研究进度进度条组件
+  const ResearchProgressBar = () => {
+    // 研究阶段和对应的友好显示名称
+    const stages = [
+      { id: 'keywords', name: '关键词' },
+      { id: 'topics', name: '研究方向' },
+      { id: 'searching', name: '论文检索' },
+      { id: 'reporting', name: '生成报告' }
+    ];
+
+    return (
+      <div className="mb-6 bg-white/5 p-4 rounded-lg">
+        <div className="text-sm text-white/80 mb-2">
+          正在生成研究方向...
+        </div>
+        
+        {/* 进度条 */}
+        <div className="w-full bg-gray-700 h-2 rounded-full overflow-hidden mb-4">
+          <div 
+            className="h-full bg-blue-500 transition-all duration-500"
+            style={{ 
+              width: `${researchStage === 'complete' ? '100%' : 
+                      researchStage === 'reporting' ? '75%' : 
+                      researchStage === 'searching' ? '50%' : 
+                      researchStage === 'topics' ? '25%' : 
+                      researchStage === 'keywords' ? '12.5%' : '5%'}`
+            }}
+          ></div>
+        </div>
+        
+        {/* 步骤指示器 */}
+        <div className="flex justify-between">
+          {stages.map((stage, index) => {
+            // 确定步骤状态
+            const currentStageIndex = stages.findIndex(s => s.id === researchStage);
+            let statusClass = '';
+            
+            if (currentStageIndex > index || researchStage === 'complete') {
+              statusClass = 'bg-green-500'; // 已完成
+            } else if (currentStageIndex === index) {
+              statusClass = 'bg-blue-500 animate-pulse'; // 进行中
+            } else {
+              statusClass = 'bg-gray-700'; // 未开始
+            }
+            
+            return (
+              <div key={stage.id} className="flex flex-col items-center">
+                <div className={`w-6 h-6 rounded-full flex items-center justify-center ${statusClass}`}>
+                  {currentStageIndex > index || researchStage === 'complete' ? (
+                    <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
+                    </svg>
+                  ) : (
+                    <span className="text-xs text-white">{index + 1}</span>
+                  )}
+                </div>
+                <span className={`text-xs mt-1 ${currentStageIndex === index ? 'text-white font-medium' : 'text-white/60'}`}>
+                  {stage.name}
+                </span>
+              </div>
+            );
+          })}
+        </div>
+      </div>
+    );
+  };
+
+  // 研究进展详细展示组件
+  const ResearchProgressDisplay = () => {
+    return (
+      <div className="space-y-8">
+        {/* 研究进度步骤 */}
+        <div className="space-y-4">
+          {/* 步骤1: 关键词提取 */}
+          {keywordsData && (
+            <div className="border-l-4 border-green-500 pl-4 bg-white/5 p-4 rounded-r-lg">
+              <h4 className="text-sm font-medium text-white/90 mb-2 flex items-center">
+                <svg className="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
+                </svg>
+                步骤1: 关键词提取完成
+              </h4>
+              
+              <div className="flex flex-wrap gap-1.5">
+                {(showEnglishKeywords 
+                  ? (keywordsData?.keywords || []).slice(0, 8)
+                  : (keywordsData?.translations || keywordsData?.keywords || []).slice(0, 8)
+                ).map((keyword, idx) => (
+                  <span 
+                    key={idx} 
+                    className="px-2 py-0.5 text-xs rounded-full bg-blue-500/20 text-blue-300"
+                  >
+                    {keyword}
+                  </span>
+                ))}
+              </div>
+              
+              <button 
+                onClick={() => setShowEnglishKeywords(!showEnglishKeywords)}
+                className="text-xs text-blue-400 hover:text-blue-300 mt-2 flex items-center"
+              >
+                {showEnglishKeywords ? "查看中文关键词" : "查看英文关键词"}
+              </button>
+            </div>
+          )}
+          
+          {/* 步骤2: 研究方向生成 */}
+          {directionsData && directionsData.research_topics?.length > 0 && (
+            <div className="border-l-4 border-indigo-500 pl-4 bg-white/5 p-4 rounded-r-lg">
+              <h4 className="text-sm font-medium text-white/90 mb-2 flex items-center">
+                <svg className="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
+                </svg>
+                步骤2: 研究方向生成完成
+              </h4>
+              
+              <div className="space-y-2 mt-3">
+                {directionsData.research_topics.map((topic, topicIdx) => (
+                  <div 
+                    key={topicIdx} 
+                    className={`p-3 rounded ${
+                      processingDirectionIndex === topicIdx 
+                        ? 'bg-indigo-500/20 border border-indigo-500/40' 
+                        : 'bg-white/10'
+                    }`}
+                  >
+                    <h5 className="font-medium text-sm text-white/90">
+                      {topicIdx + 1}. {topic.title || topic.english_title}
+                    </h5>
+                    
+                    {/* 处理状态指示器 */}
+                    {processingDirectionIndex === topicIdx && (
+                      <div className="text-xs text-white/60 mt-1 flex items-center">
+                        <div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse mr-2"></div>
+                        {processingState === "searching_papers" && "正在搜索相关论文..."}
+                        {processingState === "generating_report" && "正在生成研究报告..."}
+                      </div>
+                    )}
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+          
+          {/* 步骤3: 论文检索结果 */}
+          {papersData.some(data => data && data.papers?.length > 0) && (
+            <div className="border-l-4 border-blue-500 pl-4 bg-white/5 p-4 rounded-r-lg">
+              <h4 className="text-sm font-medium text-white mb-2 flex items-center">
+                <svg className="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
+                </svg>
+                步骤3: 论文检索完成
+              </h4>
+              
+              <div className="grid gap-3 mt-3">
+                {directionsData?.research_topics?.map((topic, idx) => (
+                  // 只显示有论文的研究方向
+                  papersData[idx] && papersData[idx].papers?.length > 0 ?
+                    <div key={idx} className="border border-white/10 rounded-lg overflow-hidden">
+                      <div className="bg-white/10 p-3">
+                        <h5 className="font-medium text-sm text-white/90 flex justify-between">
+                          <span>{topic.title || topic.english_title}</span>
+                          <span className="text-green-400 text-xs flex items-center">
+                            {papersData[idx].papers.length} 篇相关论文
+                          </span>
+                        </h5>
+                      </div>
+                      
+                      <div className="p-3">
+                        {/* 论文列表预览 */}
+                        <div className="space-y-2 mb-2">
+                          {papersData[idx].papers.slice(0, 2).map((paper, paperIdx) => (
+                            <div key={paperIdx} className="text-xs text-white/80 truncate">
+                              {paperIdx + 1}. {paper.title}
+                            </div>
+                          ))}
+                          {papersData[idx].papers.length > 2 && (
+                            <div className="text-xs text-white/50 italic">
+                              ...以及更多 {papersData[idx].papers.length - 2} 篇论文
+                            </div>
+                          )}
+                        </div>
+                        
+                        {/* 查看全部按钮 */}
+                        <button 
+                          onClick={(e) => handleViewAllPapers(idx, e)}
+                          className="text-xs text-blue-400 hover:text-blue-300 flex items-center"
+                        >
+                          <span>查看全部论文</span>
+                          <svg className="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
+                          </svg>
+                        </button>
+                      </div>
+                    </div>
+                  : null
+                ))}
+              </div>
+            </div>
+          )}
+          
+          {/* 步骤4: 研究报告生成 */}
+          {reportsData.some(data => data && data.report) && (
+            <div className="border-l-4 border-green-500 pl-4 bg-white/5 p-4 rounded-r-lg">
+              <h4 className="text-sm font-medium text-white mb-2 flex items-center">
+                <svg className="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
+                </svg>
+                步骤4: 研究报告生成
+              </h4>
+              
+              <div className="space-y-4 mt-3 text-white">
+                {directionsData?.research_topics?.map((topic, topicIdx) => (
+                  // 仅显示已有报告的研究方向
+                  reportsData[topicIdx] && reportsData[topicIdx].report ?
+                    <div key={topicIdx} className="border border-white/10 rounded-lg overflow-hidden">
+                      {/* 研究方向标题 */}
+                      <div className="bg-white/10 p-3">
+                        <h5 className="font-medium text-sm flex items-center">
+                          <svg className="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
+                          </svg>
+                          {topic.title || topic.english_title}
+                        </h5>
+                      </div>
+                      
+                      <div className="p-3">
+                        <p className="text-xs text-white/70 line-clamp-2">
+                          {reportsData[topicIdx].report.content?.substring(0, 150) || 
+                          reportsData[topicIdx].report.english_content?.substring(0, 150)}...
+                        </p>
+                        
+                        <button 
+                          onClick={(e) => handleViewReport(topicIdx, e)}
+                          className="text-xs text-blue-400 hover:text-blue-300 mt-2 flex items-center"
+                        >
+                          <span>查看完整研究报告</span>
+                          <svg className="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
+                          </svg>
+                        </button>
+                      </div>
+                    </div>
+                  : null
+                ))}
+              </div>
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  };
+
+  // 模态框组件
+  const PapersModal = () => {
+    if (!modalOpen || !modalContent) return null;
+    
+    return (
+      <div className="fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50">
+        <div className="bg-gray-900 rounded-lg w-full max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
+          <div className="p-4 border-b border-white/10 flex justify-between items-center">
+            <h3 className="text-white font-medium">{modalContent.title}</h3>
+            <button 
+              onClick={() => setModalOpen(false)}
+              className="text-white/60 hover:text-white"
+            >
+              ✕
+            </button>
+          </div>
+          
+          <div className="p-4 overflow-y-auto">
+            {modalContent.type === 'papers' && papersData[modalContent.topicIndex]?.papers && (
+              <PaperList papers={papersData[modalContent.topicIndex].papers} />
+            )}
+            
+            {modalContent.type === 'report' && reportsData[modalContent.topicIndex]?.report && (
+              <ReportPanel 
+                report={reportsData[modalContent.topicIndex].report.content || reportsData[modalContent.topicIndex].report.english_content}
+                direction={directionsData?.research_topics[modalContent.topicIndex]?.title || `研究方向 ${modalContent.topicIndex + 1}`}
+              />
+            )}
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  // 重置表单
+  const resetForm = () => {
+    setResearchTopic("");
+    setResearchData(null);
+    setSelectedDirection(null);
+    setKeywordsData(null);
+    setDirectionsData(null);
+    setPapersData([]);
+    setReportsData([]);
+    setProcessingState("idle");
+    setProcessMessage("");
+    setError(null);
+    setResearchStage("idle");
+  };
+
+  // 表单提交处理
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    
+    // 告诉浏览器这是一个长时间运行的任务
+    if ('requestIdleCallback' in window) {
+      // @ts-ignore
+      window.requestIdleCallback(() => {
+        console.log("Preparing for long running task");
+      });
+    }
+    
+    // Reset states
+    setResearchData(null);
+    setSelectedDirection(null);
+    setKeywordsData(null);
+    setDirectionsData(null);
+    setPapersData([]);
+    setReportsData([]);
+    setProcessingState("idle");
+    setProcessMessage("");
+    setError(null);
+    setProcessingDirectionIndex(-1);
+    setResearchStage("idle");
+    
+    setLoading(true);
+    
+    console.log("开始提交研究请求");
+    
+    try {
+      // Use updated endpoint to support the new keyword processing flow
+      const encodedTopic = encodeURIComponent(researchTopic);
+      const eventSource = new EventSource(`${API_URL}/api/research/enhanced-process-stream?research_intent=${encodedTopic}`);
+      
+      console.log("Connecting to:", `${API_URL}/api/research/enhanced-process-stream?research_intent=${encodedTopic}`);
+      
+      // 设置一个安全超时(例如15分钟),强制完成
+      const safetyTimeout = setTimeout(() => {
+        if (loading && eventSource) {
+          console.log("安全超时触发,强制完成研究");
+          setProcessingState("completed");
+          setProcessMessage("研究处理已完成(超时保护)");
+          setResearchStage("complete");
+          eventSource.close();
+          setLoading(false);
+        }
+      }, 15 * 60 * 1000); // 15分钟
+      
+      // 设置一个最大处理时间(例如15分钟)
+      const maxProcessingTimer = setTimeout(() => {
+        if (loading && processingState !== "completed") {
+          console.log("处理时间过长,自动结束");
+          
+          // 设置为完成状态
+          setProcessingState("completed");
+          setProcessMessage("研究处理已完成");
+          
+          if (eventSource) {
+            eventSource.close();
+          }
+          setLoading(false);
+        }
+      }, 15 * 60 * 1000); // 15分钟超时
+      
+      // 设置一个强制更新进度的定时器,确保进度即使在没有事件的情况下也会更新
+      const progressUpdateTimer = setInterval(() => {
+        if (loading) {
+          console.log("定时更新进度...");
+          setProgress(calculateTaskBasedProgress());
+        } else {
+          clearInterval(progressUpdateTimer);
+        }
+      }, 2000); // 每2秒更新一次
+      
+      // Process server events
+      eventSource.onmessage = (event) => {
+        try {
+          const data = JSON.parse(event.data);
+          console.log("收到事件:", data.status, data);
+          
+          // 立即更新状态
+          setProcessingState(data.status);
+          
+          switch (data.status) {
+            case 'processing_keywords': {
+              setProcessMessage("正在处理关键词...");
+              setResearchStage("keywords");
+              break;
+            }
+            
+            case 'keywords_ready': {
+              setKeywordsData(data.data);
+              setProcessMessage("关键词提取完成");
+              setResearchStage("topics");
+              break;
+            }
+            
+            case 'processing_directions': {
+              setProcessMessage("正在生成研究方向...");
+              setResearchStage("topics");
+              break;
+            }
+            
+            case 'directions_ready': {
+              setDirectionsData(data.data);
+              setProcessMessage("研究方向生成完成");
+              setResearchStage("searching");
+              
+              // 初始化论文数据和报告数据数组
+              const topicsCount = data.data.research_topics.length;
+              setPapersData(new Array(topicsCount).fill(null));
+              setReportsData(new Array(topicsCount).fill(null));
+              break;
+            }
+            
+            case 'searching_papers': {
+              // 尝试以多种方式提取方向索引
+              let directionIndex = -1;
+              
+              // 安全地检查direction_index是否存在
+              if (data.data && typeof data.data.direction_index !== 'undefined') {
+                directionIndex = data.data.direction_index;
+              } else if (typeof data.current_direction !== 'undefined') {
+                directionIndex = data.current_direction;
+              } else if (typeof data.direction_index !== 'undefined') {
+                directionIndex = data.direction_index;
+              }
+              
+              // 设置方向索引(如果找到了)
+              if (directionIndex >= 0) {
+                setProcessMessage(`正在搜索研究方向 ${directionIndex + 1} 的相关论文...`);
+                setProcessingDirectionIndex(directionIndex);
+              } else {
+                // 如果没有找到方向索引,使用通用消息
+                setProcessMessage(`正在搜索相关论文...`);
+              }
+              
+              // 无论如何都设置研究阶段
+              setResearchStage("searching");
+              break;
+            }
+            
+            case 'papers_ready': {
+              setPapersData(prev => {
+                const newData = [...prev];
+                if (data.data && typeof data.data.direction_index !== 'undefined') {
+                  newData[data.data.direction_index] = {
+                    direction: data.data.direction,
+                    original_direction: data.data.original_direction,
+                    search_keywords: data.data.search_keywords,
+                    papers: data.data.papers
+                  };
+                }
+                return newData;
+              });
+              
+              if (data.data && typeof data.data.direction_index !== 'undefined') {
+                setProcessMessage(`研究方向 ${data.data.direction_index + 1} 的论文检索完成`);
+              } else {
+                setProcessMessage(`论文检索完成`);
+              }
+              break;
+            }
+            
+            case 'generating_report': {
+              if (data.data && typeof data.data.direction_index !== 'undefined') {
+                setProcessMessage(`正在生成研究方向 ${data.data.direction_index + 1} 的研究报告...`);
+                setProcessingDirectionIndex(data.data.direction_index);
+              } else {
+                setProcessMessage(`正在生成研究报告...`);
+              }
+              setResearchStage("reporting");
+              break;
+            }
+            
+            case 'report_ready': {
+              setReportsData(prev => {
+                const newData = [...prev];
+                if (data.data && typeof data.data.direction_index !== 'undefined') {
+                  newData[data.data.direction_index] = {
+                    direction: data.data.direction,
+                    original_direction: data.data.original_direction,
+                    search_keywords: data.data.search_keywords,
+                    report: data.data.report
+                  };
+                  
+                  // 设置当前处理的方向索引
+                  setProcessingDirectionIndex(data.data.direction_index);
+                  
+                  // 获取当前方向索引和完成报告数量
+                  const currentDirectionIndex = data.data.direction_index;
+                  const validReports = newData.filter(r => r && r.report);
+                  const completedReportsCount = validReports.length;
+                  const allTopicsCount = directionsData?.research_topics?.length || 0;
+                  
+                  console.log(`研究报告生成状态: 方向${currentDirectionIndex+1}/${allTopicsCount} 已完成${completedReportsCount}个`);
+                  
+                  // 决定是否结束任务的条件:
+                  // 1. 已经完成至少3个研究方向的报告
+                  // 2. 或者已完成所有研究方向的报告
+                  // 3. 或者当前是第5个研究方向(索引为4)
+                  if ((completedReportsCount >= 3) || 
+                      (completedReportsCount >= allTopicsCount) || 
+                      (currentDirectionIndex >= 4)) {
+                    
+                    console.log(`已完成足够的研究方向 (${completedReportsCount}/${allTopicsCount}),结束研究流程`);
+                    
+                    // 构建完整的研究数据
+                    const completeResearchData = {
+                      research_intent: researchTopic,
+                      language: keywordsData?.language || "en",
+                      base_keywords: keywordsData?.keywords || [],
+                      research_topics: directionsData?.research_topics || [],
+                      papers_by_direction: papersData,
+                      direction_reports: newData,
+                      status: "completed"
+                    };
+                    
+                    // 设置研究数据和状态
+                    setResearchData(completeResearchData);
+                    setResearchStage("complete");
+                    setProcessingState("completed");
+                    setProcessMessage("研究处理完成");
+                    setProgress(100); // 强制设置进度为100%
+                    
+                    // 关闭连接
+                    if (eventSource) {
+                      try {
+                        console.log("关闭EventSource连接");
+                        eventSource.close();
+                        setLoading(false);
+                      } catch (err) {
+                        console.error("关闭EventSource时出错:", err);
+                        setLoading(false);
+                      }
+                    } else {
+                      setLoading(false);
+                    }
+                    
+                    // 调用onSubmit回调(如果提供)
+                    if (onSubmit) {
+                      onSubmit(completeResearchData);
+                    }
+                  }
+                }
+                
+                return newData;
+              });
+              break;
+            }
+            
+            case 'error': {
+              setError(data.message || "处理过程中发生错误");
+              eventSource.close();
+              setLoading(false);
+              break;
+            }
+            
+            default: {
+              console.log("收到未处理的事件类型:", data.status);
+              break;
+            }
+          }
+          
+          // 更新总体进度
+          setProgress(calculateTaskBasedProgress());
+          
+        } catch (error) {
+          console.error("处理事件数据时出错:", error);
+          setError("处理事件数据时出错");
+          if (eventSource) {
+            eventSource.close();
+          }
+          setLoading(false);
+        }
+      };
+      
+      // Error handling for the event source
+      eventSource.onerror = (error) => {
+        console.error("EventSource错误:", error);
+        setError("与服务器连接时出错");
+        eventSource.close();
+        setLoading(false);
+      };
+      
+    } catch (error) {
+      console.error("提交请求时出错:", error);
+      setError("提交请求时出错");
+      setLoading(false);
+    }
+  };
+
+  // 渲染研究结果组件(简化版占位符)
+  const ResearchResults = ({ data }) => (
+    <div className="space-y-4">
+      <p className="text-white/80">研究完成!您可以查看各个研究方向的详细报告。</p>
+    </div>
+  );
+
+  return (
+    <div className={className}>
+      {/* 研究表单 */}
+      <form onSubmit={handleSubmit} className="space-y-4">
+                {/* 标题和说明 */}
+                <div className="mb-6">
+          <h2 className="text-2xl font-bold text-white mb-2">研究助手</h2>
+          <p className="text-white/70">
+            输入您想研究的主题,我将帮您分析关键词、确定研究方向、搜索相关论文并生成研究报告。
+          </p>
+        </div>
+
+        {/* 研究主题输入 */}
+        <div className="space-y-2">
+          <label htmlFor="research-topic" className="block text-sm font-medium text-white">
+            研究主题
+          </label>
+          <textarea
+            id="research-topic"
+            placeholder="输入您想研究的主题,例如:新能源电池技术的最新发展及应用前景"
+            className="w-full h-24 px-3 py-2 text-white bg-white/5 border border-white/10 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+            value={researchTopic}
+            onChange={(e) => setResearchTopic(e.target.value)}
+            disabled={loading}
+          />
+        </div>
+
+        {/* 错误信息显示 */}
+        {error && <ErrorMessage message={error} onClose={() => setError(null)} />}
+
+        {/* 提交按钮 */}
+        <div className="flex">
+          <Button
+            type="submit"
+            disabled={!researchTopic.trim() || loading}
+            className={`${
+              loading ? 'bg-blue-600/50' : 'bg-blue-600 hover:bg-blue-700'
+            } text-white font-medium py-2 px-4 rounded-md transition-colors`}
+          >
+            {loading ? "研究进行中..." : "开始研究"}
+          </Button>
+
+          {loading && (
+            <button
+              type="button"
+              onClick={() => window.location.reload()}
+              className="ml-2 text-white/60 hover:text-white text-sm"
+            >
+              取消
+            </button>
+          )}
+        </div>
+      </form>
+
+      {/* 处理中状态显示 */}
+      {loading && (
+        <div className="mt-8 space-y-4">
+          {/* 进度显示 */}
+          <div className="flex justify-between items-center mb-2">
+            <div className="text-sm font-medium text-white">
+              {processingState === 'completed' 
+                ? '研究完成' 
+                : `正在处理 (${progress}%)`
+              }
+            </div>
+            <div className="text-xs text-white/60">
+              {processMessage}
+            </div>
+          </div>
+
+          {/* 进度条 */}
+          <div className="w-full bg-white/10 rounded-full h-2.5 mb-6">
+            <div
+              className="bg-blue-600 h-2.5 rounded-full transition-all duration-500"
+              style={{
+                width: `${progress}%`,
+              }}
+            ></div>
+          </div>
+
+          {/* 研究进度步骤组件 */}
+          <ResearchProgressBar />
+
+          {/* 详细研究进度 */}
+          <ResearchProgressDisplay />
+        </div>
+      )}
+
+      {/* 研究结果显示 */}
+      {researchData && !loading && (
+        <div className="mt-8">
+          <ResearchResults data={researchData} />
+          
+          {/* 研究进度详情 */}
+          <ResearchProgressDisplay />
+          
+          {/* 导出按钮 */}
+          {onExport && (
+            <Button
+              onClick={() => onExport(researchData)}
+              className="mt-4 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
+            >
+              导出研究结果
+            </Button>
+          )}
+          
+          {/* 重置按钮 */}
+          <Button
+            onClick={resetForm}
+            className="mt-4 ml-2 bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
+          >
+            开始新研究
+          </Button>
+        </div>
+      )}
+      
+      {/* 模态框 */}
+      <PapersModal />
+    </div>
+  );
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików