commit b43e3725ee6939988f5eca1e9b6e462bd79881a2 Author: freedakgmail Date: Thu Jul 9 22:10:49 2026 +0800 feat: 添加浅色主题支持 - 配置浅色主题配色方案和 CSS 变量 - 修复组件在浅色模式下的样式适配 - 统一文字颜色类名使用 CSS 变量 - 优化玻璃效果在浅色主题下的显示 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..942a63a --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# DeepSeek API Key +DEEPSEEK_API_KEY=your-api-key-here \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7d989d --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +.venv/ +env/ +.env +*.egg-info/ +dist/ +build/ + +# Node +node_modules/ +.npm +.yarn +dist/ +dist-ssr/ +*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Database +*.db +*.sqlite + +# Logs +*.log +logs/ + +# OS +.DS_Store +Thumbs.db + +# Environment +.env.local +.env.*.local \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..42d777b --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# 智能会议记录系统 + +基于 FunASR + DeepSeek AI 的智能会议记录系统,支持音频转录、自动摘要、Markdown 导出。 + +## 技术栈 + +- **前端**: React 18 + Vite + TailwindCSS + Zustand +- **后端**: FastAPI + Uvicorn +- **数据库**: SQLite + SQLAlchemy +- **语音识别**: FunASR (SenseVoiceSmall) +- **摘要生成**: DeepSeek API + +## 快速开始 + +### 1. 安装后端依赖 + +```bash +cd /Users/freedak/Documents/AIDashboard/meeting_recorder +pip install -r requirements.txt +``` + +### 2. 启动后端服务 + +```bash +# 方式一:直接运行 +python main.py + +# 方式二:使用 uvicorn +uvicorn main:app --reload --port 8501 +``` + +### 3. 安装前端依赖并启动 + +```bash +cd frontend +npm install +npm run dev +``` + +### 4. 访问应用 + +- 前端: http://localhost:3000 +- API 文档: http://localhost:8501/docs + +## 项目结构 + +``` +meeting_recorder/ +├── main.py # FastAPI 后端入口 +├── app.py # Streamlit 旧版界面(已弃用) +├── database.py # 数据库模块 +├── processor.py # FunASR 处理器 +├── requirements.txt # Python 依赖 +├── data/ +│ ├── audio/ # 音频文件目录 +│ └── meetings.db # SQLite 数据库 +└── frontend/ # React 前端 + ├── src/ + │ ├── components/ # UI 组件 + │ ├── pages/ # 页面组件 + │ ├── api/ # API 调用 + │ └── store/ # 状态管理 + └── package.json +``` + +## 功能特性 + +- [x] 音频文件上传 +- [x] FunASR 自动转录 +- [x] 说话人分离 +- [x] 音频播放与转写对照 +- [x] 会议摘要生成 +- [x] Markdown 导出 +- [x] 会议搜索与筛选 +- [ ] 实时录音 +- [ ] 批量处理 + +## 环境变量 + +可选配置 DeepSeek API: + +```bash +export DEEPSEEK_API_KEY="your-api-key" +``` + +## 注意事项 + +- 默认使用 CPU 运行,如需 GPU 支持请安装 CUDA 环境 +- 音频文件大小建议不超过 500MB +- 长音频转录可能需要几分钟时间 \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..f8a431b --- /dev/null +++ b/app.py @@ -0,0 +1,1171 @@ +""" +会议记录系统 - Streamlit 主界面 (专业美观版) +功能: 音频上传/录音 → FunASR 转录 → 播放对照 → Markdown 导出 +""" + +import os +import sys +import time +import uuid +import json +import base64 +import tempfile +from datetime import datetime +from typing import Optional + +import streamlit as st +import streamlit.components.v1 as components + +# 导入项目模块 +from processor import MeetingProcessor, get_device +from database import Database, init_db + +# ============ 页面配置 ============ +st.set_page_config( + page_title="会议记录系统", + page_icon="🎙️", + layout="wide", + initial_sidebar_state="collapsed" +) + +# ============ 自定义 CSS - 现代设计风格 ============ +st.markdown(""" + +""", unsafe_allow_html=True) + + +# ============ 会话状态初始化 ============ +def init_session_state(): + """初始化会话状态""" + if 'processor' not in st.session_state: + st.session_state.processor = None + if 'db' not in st.session_state: + st.session_state.db = None + if 'current_page' not in st.session_state: + st.session_state.current_page = "dashboard" + if 'current_meeting' not in st.session_state: + st.session_state.current_meeting = None + if 'selected_segment' not in st.session_state: + st.session_state.selected_segment = None + + +@st.cache_resource +def get_processor(): + """获取处理器(带缓存)""" + device = get_device() + return MeetingProcessor(device=device) + + +@st.cache_resource +def get_database(): + """获取数据库(带缓存)""" + return init_db() + + +# ============ 工具函数 ============ +def format_duration(seconds: float) -> str: + """格式化时长""" + if seconds < 60: + return f"{seconds:.0f}秒" + elif seconds < 3600: + mins = int(seconds // 60) + secs = int(seconds % 60) + return f"{mins}分{secs}秒" + else: + hours = int(seconds // 3600) + mins = int((seconds % 3600) // 60) + return f"{hours}小时{mins}分" + + +def format_timestamp(seconds: float) -> str: + """格式化时间戳""" + mins = int(seconds // 60) + secs = int(seconds % 60) + return f"{mins:02d}:{secs:02d}" + + +def save_uploaded_file(uploaded_file) -> str: + """保存上传的文件""" + project_root = os.path.dirname(os.path.abspath(__file__)) + data_dir = os.path.join(project_root, "data", "audio") + os.makedirs(data_dir, exist_ok=True) + + ext = os.path.splitext(uploaded_file.name)[1] or ".wav" + filename = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}{ext}" + filepath = os.path.join(data_dir, filename) + + with open(filepath, "wb") as f: + f.write(uploaded_file.getbuffer()) + + return filepath + + +def export_to_markdown(meeting: dict) -> str: + """导出为 Markdown(包含摘要)""" + lines = [ + f"# {meeting['title']}", + "", + f"**日期**: {meeting.get('date', '未知')[:10]}", + f"**时长**: {format_duration(meeting.get('duration', 0))}", + f"**说话人数**: {meeting.get('speaker_count', 0)} 人", + "", + ] + + brief_summary = meeting.get('brief_summary') + if brief_summary: + lines.extend(["## 📋 会议摘要", "", brief_summary, ""]) + + detailed_summary = meeting.get('detailed_summary') + if detailed_summary: + lines.extend(["## 📝 详细纪要", "", detailed_summary, ""]) + + lines.extend(["---", "", "## 🎤 转写内容", ""]) + + segments = meeting.get('segments', []) + current_speaker = None + + for seg in segments: + speaker = seg.get('speaker', 0) + if speaker != current_speaker: + lines.append(f"### 说话人 {speaker}") + lines.append("") + current_speaker = speaker + + start = format_timestamp(seg.get('start', 0)) + text = seg.get('text', '') + lines.append(f"[{start}] {text}") + lines.append("") + + lines.extend(["---", "", "*本记录由 FunASR 自动生成*"]) + return "\n".join(lines) + + +# ============ 页面组件 ============ + +def render_top_nav(): + """渲染顶部导航""" + pages = [ + ("dashboard", "📊 概览", "首页"), + ("meetings", "📋 会议", "会议列表"), + ("upload", "➕ 新建", "新建会议"), + ("settings", "⚙️ 设置", "设置"), + ] + + cols = st.columns([2, 1]) + with cols[0]: + st.markdown(""" + + """, unsafe_allow_html=True) + + with cols[1]: + selected = st.radio( + "导航", + [p[0] for p in pages], + format_func=lambda x: next(p[1] for p in pages if p[0] == x), + horizontal=True, + label_visibility="collapsed", + key="nav_radio" + ) + + # 同步 session_state + st.session_state.current_page = selected + + return selected + + +def render_dashboard(): + """仪表盘页面""" + db = get_database() + meetings = db.get_all_meetings(limit=1000) + + # 统计卡片 + total = len(meetings) + completed = sum(1 for m in meetings if m['status'] == 'completed') + processing = sum(1 for m in meetings if m['status'] == 'processing') + total_duration = sum(m.get('duration', 0) for m in meetings) + + st.markdown(""" +
+
+
📋
+
{}
+
总会议数
+
+
+
+
{}
+
已完成
+
+
+
🔄
+
{}
+
处理中
+
+
+
⏱️
+
{}
+
总时长
+
+
+ """.format(total, completed, processing, format_duration(total_duration)), unsafe_allow_html=True) + + # 最近会议 + st.markdown("### 📌 最近会议") + + recent = meetings[:5] + if not recent: + st.markdown(""" +
+
📋
+

暂无会议记录

+

点击上方「新建会议」开始

+
+ """, unsafe_allow_html=True) + else: + for meeting in recent: + render_meeting_card(meeting) + + +def render_meeting_card(meeting: dict): + """渲染会议卡片""" + status_map = { + "pending": ("待处理", "status-pending"), + "processing": ("处理中", "status-processing"), + "completed": ("已完成", "status-completed"), + "failed": ("失败", "status-failed"), + } + status_text, status_class = status_map.get(meeting['status'], ("未知", "")) + + has_brief = bool(meeting.get('brief_summary')) + has_detailed = bool(meeting.get('detailed_summary')) + summary_tag = "" + if meeting['status'] == 'completed': + if has_brief and has_detailed: + summary_tag = '📋📝' + elif has_brief: + summary_tag = '📋' + + st.markdown(f""" +
+
+
+
{meeting['title']}{summary_tag}
+
+ 📅 {meeting.get('date', '未知')[:10]} + ⏱️ {format_duration(meeting.get('duration', 0))} + 👥 {meeting.get('speaker_count', 0)} 人 +
+
+ {status_text} +
+
+ """, unsafe_allow_html=True) + + # 隐藏按钮用于点击卡片 + col1, col2, col3 = st.columns([4, 1, 1]) + with col1: + pass + with col2: + if st.button("查看详情", key=f"view_{meeting['meeting_id']}"): + st.session_state.current_meeting = meeting['meeting_id'] + st.session_state.current_page = "detail" + st.rerun() + with col3: + if meeting['status'] == 'completed': + if st.button("转写", key=f"trans_{meeting['meeting_id']}"): + st.session_state.current_meeting = meeting['meeting_id'] + st.session_state.current_page = "detail" + st.rerun() + + +def render_meetings_page(): + """会议列表页面""" + st.markdown("### 📋 全部会议") + + db = get_database() + meetings = db.get_all_meetings() + + # 搜索和筛选 + col1, col2, col3 = st.columns([2, 1, 1]) + with col1: + search = st.text_input("🔍 搜索会议", placeholder="输入关键词...", label_visibility="collapsed") + with col2: + status_filter = st.selectbox("状态", ["全部", "待处理", "处理中", "已完成", "失败"], label_visibility="collapsed") + with col3: + if st.button("🔄 刷新"): + st.rerun() + + # 过滤 + if search: + meetings = db.search_meetings(search) + + if status_filter != "全部": + status_map = {"待处理": "pending", "处理中": "processing", "已完成": "completed", "失败": "failed"} + meetings = [m for m in meetings if m['status'] == status_map[status_filter]] + + # 显示列表 + st.markdown(f"**共 {len(meetings)} 个会议**") + + for meeting in meetings: + render_meeting_card(meeting) + st.markdown("") # 间距 + + +def render_upload_page(): + """新建会议页面""" + st.markdown("### ➕ 新建会议") + + tab1, tab2 = st.tabs(["📤 上传音频文件", "🎤 实时录音"]) + + with tab1: + render_upload_tab() + + with tab2: + render_record_tab() + + +def render_upload_tab(): + """上传音频标签页""" + col1, col2 = st.columns([1, 1]) + + with col1: + st.markdown(""" +
+
📁
+
拖拽文件或点击上传
+
+ 支持 WAV, MP3, M4A, FLAC, OGG +
+
+ """, unsafe_allow_html=True) + + uploaded_file = st.file_uploader( + "选择音频文件", + type=['wav', 'mp3', 'm4a', 'flac', 'ogg'], + help="拖拽或点击上传" + ) + + with col2: + if uploaded_file: + st.audio(uploaded_file) + title = st.text_input("会议标题", value=uploaded_file.name.rsplit('.', 1)[0]) + + if st.button("🚀 开始转录", type="primary", use_container_width=True): + process_uploaded_file(uploaded_file, title) + + +def render_record_tab(): + """录音标签页""" + st.info("🎤 实时录音功能开发中,请先使用上传功能") + + col1, col2 = st.columns([1, 1]) + with col1: + title = st.text_input("会议标题", value=f"录音会议_{datetime.now().strftime('%H%M%S')}") + with col2: + max_duration = st.number_input("最大时长(秒)", value=3600, min_value=60, max_value=7200) + + st.markdown(""" +
+
🎙️
+

点击下方按钮开始录音

+
+ """, unsafe_allow_html=True) + + col1, col2, col3 = st.columns([1, 1, 1]) + with col2: + if st.button("🔴 开始录音", type="primary", use_container_width=True): + st.warning("录音功能开发中") + + +def render_detail_page(): + """会议详情页面""" + if not st.session_state.current_meeting: + st.session_state.current_page = "meetings" + st.rerun() + return + + db = get_database() + meeting = db.get_meeting(st.session_state.current_meeting) + + if not meeting: + st.error("会议不存在") + if st.button("返回列表"): + st.session_state.current_page = "meetings" + st.rerun() + return + + # 返回按钮 + if st.button("← 返回会议列表"): + st.session_state.current_page = "meetings" + st.rerun() + + # 详情头部 + status_map = { + "pending": ("待处理", "status-pending"), + "processing": ("处理中", "status-processing"), + "completed": ("已完成", "status-completed"), + "failed": ("失败", "status-failed"), + } + status_text, status_class = status_map.get(meeting['status'], ("未知", "")) + + st.markdown(f""" +
+
{meeting['title']}
+
+
+ 📅 + 日期: {meeting.get('date', '未知')[:10]} +
+
+ ⏱️ + 时长: {format_duration(meeting.get('duration', 0))} +
+
+ 👥 + 说话人: {meeting.get('speaker_count', 0)} 人 +
+
+ 📊 + 状态: {status_text} +
+
+
+ """, unsafe_allow_html=True) + + # 根据状态显示不同内容 + if meeting['status'] == 'completed': + render_completed_detail(meeting) + elif meeting['status'] == 'processing': + render_processing_state() + else: + render_pending_state(meeting) + + +def render_completed_detail(meeting: dict): + """已完成会议详情""" + col1, col2 = st.columns([1, 2]) + + with col1: + # 音频播放器 + audio_path = meeting.get('audio_path') + if audio_path and os.path.exists(audio_path): + st.markdown("#### 🔊 音频播放") + st.audio(open(audio_path, "rb").read(), format="audio/wav") + else: + st.warning("音频文件不存在") + + # 导出 + st.markdown("#### 📥 导出") + md_content = export_to_markdown(meeting) + st.download_button( + "📄 完整 Markdown", + md_content, + file_name=f"{meeting['title']}.md", + mime="text/markdown", + use_container_width=True + ) + + brief_summary = meeting.get('brief_summary') + detailed_summary = meeting.get('detailed_summary') + if brief_summary or detailed_summary: + summary_lines = [f"# {meeting['title']}", ""] + if brief_summary: + summary_lines.extend(["## 📋 会议摘要", "", brief_summary, ""]) + if detailed_summary: + summary_lines.extend(["## 📝 详细纪要", "", detailed_summary, ""]) + st.download_button( + "📋 仅导出摘要", + "\n".join(summary_lines), + file_name=f"{meeting['title']}_摘要.md", + mime="text/markdown", + use_container_width=True + ) + + with col2: + # 摘要区域 + brief_summary = meeting.get('brief_summary') + detailed_summary = meeting.get('detailed_summary') + + if brief_summary or detailed_summary: + with st.expander("📋 会议摘要", expanded=True): + if brief_summary: + st.markdown("**简要摘要**") + st.markdown(brief_summary) + if detailed_summary: + st.markdown("---") + st.markdown("**详细纪要**") + st.markdown(detailed_summary) + + # 转写内容 + st.markdown("#### 🎤 转写内容") + segments = meeting.get('segments', []) + + if segments: + # 搜索 + search_text = st.text_input("🔍 搜索转写内容", placeholder="输入关键词...", label_visibility="collapsed") + + if search_text: + filtered = [s for s in segments if search_text.lower() in s.get('text', '').lower()] + else: + filtered = segments + + st.caption(f"共 {len(filtered)} / {len(segments)} 条") + + for seg in filtered: + render_transcript_segment(seg) + else: + st.info("暂无转写内容") + + +def render_transcript_segment(seg: dict): + """渲染转写片段""" + start = seg.get('start', 0) + end = seg.get('end', 0) + speaker = seg.get('speaker', 0) + text = seg.get('text', '') + emotion = seg.get('emotion', 'NEUTRAL') + + emotion_map = { + "NEUTRAL": ("neutral", "中性"), + "happy": ("happy", "开心"), + "sad": ("sad", "悲伤"), + "angry": ("angry", "愤怒"), + "surprise": ("surprise", "惊讶"), + } + emotion_class, emotion_text = emotion_map.get(emotion, ("neutral", "中性")) + + st.markdown(f""" +
+
+ 👤 说话人 {speaker} + + {format_timestamp(start)} - {format_timestamp(end)} + {emotion_text} + +
+
{text}
+
+ """, unsafe_allow_html=True) + + +def render_processing_state(): + """处理中状态""" + st.markdown(""" +
+
🔄
+
正在转录中...
+
请稍候,预计需要几分钟时间
+
+ """, unsafe_allow_html=True) + st.progress(0.5, text="处理中...") + + +def render_pending_state(meeting: dict): + """待处理状态""" + st.markdown(""" +
+
+
会议待转录
+
点击下方按钮开始转录
+
+ """, unsafe_allow_html=True) + + if st.button("🚀 开始转录", type="primary"): + process_meeting(st.session_state.current_meeting) + + +def render_settings_page(): + """设置页面""" + st.markdown("### ⚙️ 系统设置") + + col1, col2 = st.columns(2) + + with col1: + st.markdown("#### 🖥️ 运行环境") + device = get_device() + device_icon = "🖥️" if device == "cpu" else "🎮" + st.info(f"计算设备: **{device_icon} {device.upper()}**") + st.info("FunASR 版本: **1.3.14**") + + with col2: + st.markdown("#### 📁 存储信息") + db = get_database() + st.info(f"数据库: `{db.db_path}`") + + st.markdown("---") + + # 清理操作 + st.markdown("#### 🗑️ 数据管理") + col1, col2 = st.columns([1, 1]) + with col1: + if st.button("清理已完成会议", type="secondary"): + st.warning("功能开发中") + + st.markdown("---") + + # 关于 + st.markdown(""" +
+

智能会议记录系统 v1.0

+

基于 FunASR + DeepSeek AI

+
+ """, unsafe_allow_html=True) + + +# ============ 业务逻辑 ============ + +def process_uploaded_file(uploaded_file, title: str): + """处理上传的文件""" + if not uploaded_file: + st.error("请先上传音频文件") + return + + with st.spinner("保存文件并开始转录..."): + audio_path = save_uploaded_file(uploaded_file) + + db = get_database() + meeting_id = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + uuid.uuid4().hex[:6] + + db.create_meeting( + meeting_id=meeting_id, + title=title, + audio_path=audio_path, + status="processing" + ) + + try: + processor = get_processor() + result = processor.process_audio(audio_path, title=title) + + db.update_meeting( + meeting_id=meeting_id, + segments=[s.__dict__ for s in result.segments], + status="completed", + speaker_count=result.speaker_count, + duration=result.duration + ) + + st.success("✅ 转录完成!") + st.session_state.current_meeting = meeting_id + st.session_state.current_page = "detail" + st.rerun() + + except Exception as e: + db.update_meeting(meeting_id=meeting_id, status="failed") + st.error(f"转录失败: {str(e)}") + + +def process_meeting(meeting_id: str): + """处理会议转录""" + db = get_database() + meeting = db.get_meeting(meeting_id) + + if not meeting: + return + + try: + processor = get_processor() + result = processor.process_audio(meeting['audio_path'], title=meeting['title']) + + db.update_meeting( + meeting_id=meeting_id, + segments=[s.__dict__ for s in result.segments], + status="completed", + speaker_count=result.speaker_count, + duration=result.duration + ) + + st.success("✅ 转录完成!") + st.rerun() + + except Exception as e: + db.update_meeting(meeting_id=meeting_id, status="failed") + st.error(f"转录失败: {str(e)}") + + +# ============ 主函数 ============ +def main(): + init_session_state() + + # 顶部导航 + current_page = render_top_nav() + + st.markdown("
", unsafe_allow_html=True) + + # 根据当前页面渲染 + if current_page == "dashboard": + render_dashboard() + elif current_page == "meetings": + render_meetings_page() + elif current_page == "upload": + render_upload_page() + elif current_page == "settings": + render_settings_page() + elif current_page == "detail": + render_detail_page() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/audio_utils.py b/audio_utils.py new file mode 100644 index 0000000..391b246 --- /dev/null +++ b/audio_utils.py @@ -0,0 +1,161 @@ +""" +音频处理辅助工具 +支持格式转换、音频切片等 +""" + +import os +import subprocess +from typing import Tuple, Optional +import numpy as np + + +def get_audio_info(audio_path: str) -> dict: + """ + 获取音频文件信息 + + Returns: + dict: { + "duration": float, # 秒 + "sample_rate": int, + "channels": int, + "format": str + } + """ + try: + import soundfile as sf + info = sf.info(audio_path) + return { + "duration": info.duration, + "sample_rate": info.samplerate, + "channels": info.channels, + "format": info.format + } + except ImportError: + # 降级方案:使用 ffprobe + import json + cmd = [ + "ffprobe", "-v", "quiet", + "-print_format", "json", + "-show_format", "-show_streams", + audio_path + ] + result = subprocess.run(cmd, capture_output=True, text=True) + data = json.loads(result.stdout) + + audio_stream = next((s for s in data.get("streams", []) if s.get("codec_type") == "audio"), {}) + duration = float(data.get("format", {}).get("duration", 0)) + + return { + "duration": duration, + "sample_rate": int(audio_stream.get("sample_rate", 16000)), + "channels": int(audio_stream.get("channels", 1)), + "format": audio_stream.get("codec_name", "unknown") + } + + +def convert_to_wav(input_path: str, output_path: Optional[str] = None, sample_rate: int = 16000) -> str: + """ + 转换音频为 WAV 格式(16kHz 单声道) + + Args: + input_path: 输入文件路径 + output_path: 输出文件路径(可选) + sample_rate: 采样率,默认 16000 + + Returns: + 输出文件路径 + """ + if output_path is None: + output_path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name + + cmd = [ + "ffmpeg", "-y", # 覆盖已存在的文件 + "-i", input_path, + "-ar", str(sample_rate), # 采样率 + "-ac", "1", # 单声道 + "-acodec", "pcm_s16le", # 16bit PCM + output_path + ] + + subprocess.run(cmd, capture_output=True, check=True) + return output_path + + +def slice_audio(audio_path: str, start: float, end: float, output_path: Optional[str] = None) -> str: + """ + 切片音频 + + Args: + audio_path: 输入文件路径 + start: 开始时间(秒) + end: 结束时间(秒) + output_path: 输出文件路径 + + Returns: + 输出文件路径 + """ + if output_path is None: + output_path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name + + cmd = [ + "ffmpeg", "-y", + "-i", audio_path, + "-ss", str(start), + "-to", str(end), + "-c", "copy", # 无损复制 + output_path + ] + + subprocess.run(cmd, capture_output=True, check=True) + return output_path + + +def merge_audio(audio_paths: list, output_path: Optional[str] = None) -> str: + """ + 合并多个音频文件 + + Args: + audio_paths: 音频文件路径列表 + output_path: 输出文件路径 + + Returns: + 输出文件路径 + """ + if output_path is None: + output_path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name + + # 创建临时文件列表 + list_file = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) + for path in audio_paths: + list_file.write(f"file '{path}'\n") + list_file.close() + + cmd = [ + "ffmpeg", "-y", + "-f", "concat", + "-safe", "0", + "-i", list_file.name, + "-c", "copy", + output_path + ] + + subprocess.run(cmd, capture_output=True, check=True) + + # 清理临时文件 + os.unlink(list_file.name) + + return output_path + + +def is_ffmpeg_available() -> bool: + """检查 ffmpeg 是否可用""" + try: + subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + return False + + +if __name__ == "__main__": + # 测试 + print("FFmpeg 可用:", is_ffmpeg_available()) \ No newline at end of file diff --git a/data/audio/20260709_085400_4a68fb75.mp3 b/data/audio/20260709_085400_4a68fb75.mp3 new file mode 100644 index 0000000..ff54e35 Binary files /dev/null and b/data/audio/20260709_085400_4a68fb75.mp3 differ diff --git a/data/audio/20260709_102403_b1536b7c.mp3 b/data/audio/20260709_102403_b1536b7c.mp3 new file mode 100644 index 0000000..a54c300 Binary files /dev/null and b/data/audio/20260709_102403_b1536b7c.mp3 differ diff --git a/data/audio/20260709_102928_dc909bea.mp3 b/data/audio/20260709_102928_dc909bea.mp3 new file mode 100644 index 0000000..c60e9fe Binary files /dev/null and b/data/audio/20260709_102928_dc909bea.mp3 differ diff --git a/database.py b/database.py new file mode 100644 index 0000000..63338d7 --- /dev/null +++ b/database.py @@ -0,0 +1,226 @@ +""" +会议记录数据库 - SQLite 存储 +""" + +import os +import json +import sqlite3 +from datetime import datetime +from typing import List, Optional, Dict +from contextlib import contextmanager + +from sqlalchemy import create_engine, Column, String, Float, Integer, DateTime, JSON, Text +from sqlalchemy.orm import sessionmaker, declarative_base +from sqlalchemy.pool import StaticPool + +Base = declarative_base() + + +class Meeting(Base): + """会议表""" + __tablename__ = "meetings" + + meeting_id = Column(String, primary_key=True) + title = Column(String, nullable=False) + date = Column(DateTime, default=datetime.now) + duration = Column(Float, default=0) # 秒 + audio_path = Column(String, nullable=True) # 音频文件路径 + transcript_path = Column(String, nullable=True) # 转录文件路径 + speaker_count = Column(Integer, default=0) + status = Column(String, default="pending") # pending, processing, completed, failed + segments_json = Column(Text, nullable=True) # 存储片段 JSON + brief_summary = Column(Text, nullable=True) # 简要摘要 + detailed_summary = Column(Text, nullable=True) # 详细摘要 + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class Database: + """数据库管理器""" + + def __init__(self, db_path: str = None): + if db_path is None: + # 默认放在项目根目录 + project_root = os.path.dirname(os.path.abspath(__file__)) + db_path = os.path.join(project_root, "data", "meetings.db") + + # 确保目录存在 + os.makedirs(os.path.dirname(db_path), exist_ok=True) + + self.db_path = db_path + self.engine = create_engine( + f"sqlite:///{db_path}", + connect_args={"check_same_thread": False}, + poolclass=StaticPool + ) + Base.metadata.create_all(self.engine) + self.Session = sessionmaker(bind=self.engine) + + @contextmanager + def get_session(self): + """获取数据库会话""" + session = self.Session() + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() + + def create_meeting( + self, + meeting_id: str, + title: str, + duration: float = 0, + audio_path: str = None, + status: str = "pending" + ) -> Meeting: + """创建会议记录""" + meeting = Meeting( + meeting_id=meeting_id, + title=title, + duration=duration, + audio_path=audio_path, + status=status + ) + with self.get_session() as session: + session.add(meeting) + return meeting + + def update_meeting( + self, + meeting_id: str, + segments: List[Dict] = None, + status: str = None, + transcript_path: str = None, + speaker_count: int = None, + duration: float = None, + brief_summary: str = None, + detailed_summary: str = None, + title: str = None + ): + """更新会议记录""" + with self.get_session() as session: + meeting = session.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() + if meeting: + if title is not None: + meeting.title = title + if segments is not None: + meeting.segments_json = json.dumps(segments, ensure_ascii=False) + if status is not None: + meeting.status = status + if transcript_path is not None: + meeting.transcript_path = transcript_path + if speaker_count is not None: + meeting.speaker_count = speaker_count + if duration is not None: + meeting.duration = duration + if brief_summary is not None: + meeting.brief_summary = brief_summary + if detailed_summary is not None: + meeting.detailed_summary = detailed_summary + + def get_meeting(self, meeting_id: str) -> Optional[Dict]: + """获取单个会议记录""" + with self.get_session() as session: + meeting = session.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() + if meeting: + return self._meeting_to_dict(meeting) + return None + + def get_all_meetings(self, limit: int = 100) -> List[Dict]: + """获取所有会议记录""" + with self.get_session() as session: + meetings = session.query(Meeting).order_by(Meeting.created_at.desc()).limit(limit).all() + return [self._meeting_to_dict(m) for m in meetings] + + def search_meetings(self, keyword: str) -> List[Dict]: + """搜索会议记录""" + with self.get_session() as session: + meetings = session.query(Meeting).filter( + Meeting.title.like(f"%{keyword}%") + ).order_by(Meeting.created_at.desc()).all() + return [self._meeting_to_dict(m) for m in meetings] + + def delete_meeting(self, meeting_id: str): + """删除会议记录""" + with self.get_session() as session: + meeting = session.query(Meeting).filter(Meeting.meeting_id == meeting_id).first() + if meeting: + session.delete(meeting) + + def _meeting_to_dict(self, meeting: Meeting) -> Dict: + """转换为字典""" + segments = [] + if meeting.segments_json: + try: + segments = json.loads(meeting.segments_json) + except json.JSONDecodeError: + segments = [] + + return { + "meeting_id": meeting.meeting_id, + "title": meeting.title, + "date": meeting.date.isoformat() if meeting.date else None, + "duration": meeting.duration, + "audio_path": meeting.audio_path, + "transcript_path": meeting.transcript_path, + "speaker_count": meeting.speaker_count, + "status": meeting.status, + "segments": segments, + "brief_summary": meeting.brief_summary, + "detailed_summary": meeting.detailed_summary, + "created_at": meeting.created_at.isoformat() if meeting.created_at else None, + "updated_at": meeting.updated_at.isoformat() if meeting.updated_at else None + } + + def get_meetings_by_date(self, date: str) -> List[Dict]: + """按日期获取会议""" + with self.get_session() as session: + meetings = session.query(Meeting).filter( + Meeting.date >= datetime.fromisoformat(date), + Meeting.date < datetime.fromisoformat(date + "T23:59:59") + ).order_by(Meeting.date.desc()).all() + return [self._meeting_to_dict(m) for m in meetings] + + +"""数据库单例实例""" +_db_instance = None + +def init_db(db_path: str = None) -> Database: + """初始化数据库(单例模式)""" + global _db_instance + if _db_instance is None: + _db_instance = Database(db_path) + return _db_instance + + +if __name__ == "__main__": + # 测试 + db = init_db(":memory:") + + # 创建测试数据 + meeting_id = "test_001" + db.create_meeting( + meeting_id=meeting_id, + title="测试会议", + duration=300 + ) + + db.update_meeting( + meeting_id=meeting_id, + segments=[ + {"start": 0, "end": 10, "speaker": 0, "text": "测试文本1"}, + {"start": 10, "end": 20, "speaker": 1, "text": "测试文本2"} + ], + status="completed", + speaker_count=2 + ) + + # 查询 + meeting = db.get_meeting(meeting_id) + print(f"会议: {meeting['title']}") + print(f"状态: {meeting['status']}") + print(f"片段数: {len(meeting['segments'])}") \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9878023 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + 智能会议记录系统 + + + + + +
+ + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..9e8cd5e --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3104 @@ +{ + "name": "meeting-recorder-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meeting-recorder-frontend", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.2", + "clsx": "^2.0.0", + "date-fns": "^2.30.0", + "lucide-react": "^0.294.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "zustand": "^4.4.7" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "vite": "^5.0.8" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmmirror.com/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "ISC", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.2", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "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.18.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "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==", + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "dev": true, + "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" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "ISC", + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "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==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "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==", + "license": "MIT", + "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/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.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==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.294.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.294.0.tgz", + "integrity": "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.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==", + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a5dae7b --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "meeting-recorder-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "axios": "^1.6.2", + "zustand": "^4.4.7", + "lucide-react": "^0.294.0", + "clsx": "^2.0.0", + "date-fns": "^2.30.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "vite": "^5.0.8" + } +} \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..e99ebc2 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..2bb58ce --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,21 @@ +import { Routes, Route } from 'react-router-dom' +import Layout from './components/Layout' +import Dashboard from './pages/Dashboard' +import Meetings from './pages/Meetings' +import MeetingDetail from './pages/MeetingDetail' +import Upload from './pages/Upload' +import Settings from './pages/Settings' + +export default function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + + + ) +} \ No newline at end of file diff --git a/frontend/src/api/meeting.js b/frontend/src/api/meeting.js new file mode 100644 index 0000000..37c12fa --- /dev/null +++ b/frontend/src/api/meeting.js @@ -0,0 +1,119 @@ +import axios from 'axios' + +const API_BASE = '/api' + +const api = axios.create({ + baseURL: API_BASE, + timeout: 300000, // 5分钟超时 +}) + +// 获取所有会议 +export const getMeetings = async () => { + const res = await api.get('/meetings') + return res.data +} + +// 获取单个会议 +export const getMeeting = async (meetingId) => { + const res = await api.get(`/meetings/${meetingId}`) + return res.data +} + +// 上传会议 +export const uploadMeeting = (file, title, onProgress) => { + const formData = new FormData() + formData.append('file', file) + formData.append('title', title) + + return api.post('/meetings/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: (progressEvent) => { + const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total) + onProgress?.(percent) + }, + }) +} + +// 删除会议 +export const deleteMeeting = async (meetingId) => { + const res = await api.delete(`/meetings/${meetingId}`) + return res.data +} + +// 生成摘要 +export const generateSummary = async (meetingId) => { + const res = await api.post(`/meetings/${meetingId}/summarize`) + return res.data +} + +// 更新会议标题 +export const updateMeetingTitle = async (meetingId, title) => { + const formData = new FormData() + formData.append('title', title) + const res = await api.patch(`/meetings/${meetingId}`, formData) + return res.data +} + +// 搜索会议 +export const searchMeetings = async (keyword) => { + const res = await api.get('/meetings/search', { params: { keyword } }) + return res.data +} + +// 下载 Markdown +export const downloadMarkdown = (meeting) => { + let content = `# ${meeting.title}\n\n` + content += `**日期**: ${meeting.date?.slice(0, 10) || '未知'}\n` + content += `**时长**: ${formatDuration(meeting.duration)}\n` + content += `**说话人数**: ${meeting.speaker_count || 0} 人\n\n` + + if (meeting.brief_summary) { + content += `## 📋 会议摘要\n\n${meeting.brief_summary}\n\n` + } + + if (meeting.detailed_summary) { + content += `## 📝 详细纪要\n\n${meeting.detailed_summary}\n\n` + } + + content += `---\n\n## 🎤 转写内容\n\n` + + let currentSpeaker = null + meeting.segments?.forEach(seg => { + if (seg.speaker !== currentSpeaker) { + content += `### 说话人 ${seg.speaker}\n\n` + currentSpeaker = seg.speaker + } + content += `[${formatTimestamp(seg.start)}] ${seg.text}\n\n` + }) + + content += `---\n\n*本记录由 FunASR 自动生成*` + + const blob = new Blob([content], { type: 'text/markdown' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${meeting.title}.md` + a.click() + URL.revokeObjectURL(url) +} + +// 工具函数 +export const formatDuration = (seconds = 0) => { + if (seconds < 60) return `${seconds.toFixed(0)}秒` + if (seconds < 3600) { + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins}分${secs}秒` + } + const hours = Math.floor(seconds / 3600) + const mins = Math.floor((seconds % 3600) / 60) + return `${hours}小时${mins}分` +} + +export const formatTimestamp = (seconds = 0) => { + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` +} + +export default api \ No newline at end of file diff --git a/frontend/src/components/Button.jsx b/frontend/src/components/Button.jsx new file mode 100644 index 0000000..7326308 --- /dev/null +++ b/frontend/src/components/Button.jsx @@ -0,0 +1,54 @@ +import clsx from 'clsx' +import { Loader2 } from 'lucide-react' + +const variants = { + primary: [ + 'bg-accent/20 text-accent border-accent/30 hover:bg-accent/30 hover:border-accent/50', + ], + secondary: [ + 'bg-black/5 dark:bg-white/10 text-primary border-black/10 dark:border-white/10 hover:bg-black/10 dark:hover:bg-white/15', + ], + ghost: [ + 'bg-transparent text-secondary border-transparent hover:bg-black/5 dark:hover:bg-white/10 hover:text-primary', + ], + danger: [ + 'bg-error/15 text-error border-error/30 hover:bg-error/25 hover:border-error/50', + ], +} + +const sizes = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2 text-sm', + lg: 'px-5 py-2.5 text-base', +} + +export default function Button({ + children, + variant = 'primary', + size = 'md', + className, + loading, + disabled, + ...props +}) { + const baseClass = variants[variant]?.[0] || variants.primary[0] + + return ( + + ) +} \ No newline at end of file diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx new file mode 100644 index 0000000..d50f758 --- /dev/null +++ b/frontend/src/components/Layout.jsx @@ -0,0 +1,74 @@ +import { Link, useLocation } from 'react-router-dom' +import { LayoutDashboard, FileText, Plus, Settings, Mic } from 'lucide-react' +import clsx from 'clsx' +import ThemeToggle from './ThemeToggle' + +const navItems = [ + { path: '/', icon: LayoutDashboard, label: '概览' }, + { path: '/meetings', icon: FileText, label: '会议' }, + { path: '/upload', icon: Plus, label: '新建' }, + { path: '/settings', icon: Settings, label: '设置' }, +] + +export default function Layout({ children }) { + const location = useLocation() + + return ( +
+ {/* 侧边栏 */} + + + {/* 主内容区 */} +
+
+ {children} +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/MeetingCard.jsx b/frontend/src/components/MeetingCard.jsx new file mode 100644 index 0000000..a2cd896 --- /dev/null +++ b/frontend/src/components/MeetingCard.jsx @@ -0,0 +1,92 @@ +import clsx from 'clsx' +import { + Calendar, Clock, Users, FileText, CheckCircle2, Loader2, AlertCircle +} from 'lucide-react' +import { formatDuration } from '../api/meeting' + +const statusConfig = { + pending: { + label: '待处理', + class: 'bg-warning/10 text-warning border-warning/20', + icon: AlertCircle + }, + processing: { + label: '处理中', + class: 'bg-info/10 text-info border-info/20', + icon: Loader2 + }, + completed: { + label: '已完成', + class: 'bg-success/10 text-success border-success/20', + icon: CheckCircle2 + }, + failed: { + label: '失败', + class: 'bg-error/10 text-error border-error/20', + icon: AlertCircle + }, +} + +export default function MeetingCard({ meeting, onClick }) { + const status = statusConfig[meeting.status] || statusConfig.pending + const StatusIcon = status.icon + const hasSummary = meeting.brief_summary || meeting.detailed_summary + + return ( +
onClick?.(meeting)} + className="glass-card rounded-xl p-4 cursor-pointer + hover:glass-card-hover transition-all duration-300 + group" + > + {/* 头部 */} +
+
+

+ {meeting.title} +

+ {hasSummary && meeting.status === 'completed' && ( + + + 有摘要 + + )} +
+ + + {status.label} + +
+ + {/* 元信息 */} +
+ + + {meeting.date?.slice(0, 10) || '未知'} + + + + {formatDuration(meeting.duration)} + + + + {meeting.speaker_count || 0} 人 + +
+ + {/* 进度条 */} + {meeting.status === 'processing' && ( +
+
+
+
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/StatsCard.jsx b/frontend/src/components/StatsCard.jsx new file mode 100644 index 0000000..beb87c9 --- /dev/null +++ b/frontend/src/components/StatsCard.jsx @@ -0,0 +1,49 @@ +import clsx from 'clsx' +import { FileText, CheckCircle2, Loader2, Timer } from 'lucide-react' +import { formatDuration } from '../api/meeting' + +const config = { + total: { + icon: FileText, + color: 'text-accent', + bgColor: 'bg-accent/10', + }, + completed: { + icon: CheckCircle2, + color: 'text-success', + bgColor: 'bg-success/10', + }, + processing: { + icon: Loader2, + color: 'text-warning', + bgColor: 'bg-warning/10', + }, + duration: { + icon: Timer, + color: 'text-info', + bgColor: 'bg-info/10', + }, +} + +export default function StatsCard({ type, value, label }) { + const { icon: Icon, color, bgColor } = config[type] || config.total + + return ( +
+
+ +
+ +
+ {type === 'duration' ? formatDuration(value) : value} +
+ +
+ {label} +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/ThemeToggle.jsx b/frontend/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..91fff8b --- /dev/null +++ b/frontend/src/components/ThemeToggle.jsx @@ -0,0 +1,44 @@ +import { useState, useEffect } from 'react' +import { Sun, Moon } from 'lucide-react' +import clsx from 'clsx' + +export default function ThemeToggle() { + const [isDark, setIsDark] = useState(true) + + useEffect(() => { + // 从 localStorage 读取主题 + const saved = localStorage.getItem('theme') + if (saved) { + setIsDark(saved === 'dark') + document.documentElement.classList.toggle('light', saved === 'light') + } else { + // 检测系统偏好 + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + setIsDark(prefersDark) + if (!prefersDark) { + document.documentElement.classList.add('light') + } + } + }, []) + + const toggleTheme = () => { + const newIsDark = !isDark + setIsDark(newIsDark) + document.documentElement.classList.toggle('light', !newIsDark) + localStorage.setItem('theme', newIsDark ? 'dark' : 'light') + } + + return ( + + ) +} \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..630c4df --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,31 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter, Routes, Route } from 'react-router-dom' +import Layout from './components/Layout' +import Dashboard from './pages/Dashboard' +import Meetings from './pages/Meetings' +import MeetingDetail from './pages/MeetingDetail' +import Upload from './pages/Upload' +import Settings from './pages/Settings' +import './styles/index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + } /> + } /> + } /> + } /> + } /> + + + + , +) \ No newline at end of file diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 0000000..056f456 --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,91 @@ +import { useEffect } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { ArrowRight, Plus, FileText } from 'lucide-react' +import { useAppStore } from '../store/useAppStore' +import StatsCard from '../components/StatsCard' +import MeetingCard from '../components/MeetingCard' +import Button from '../components/Button' + +export default function Dashboard() { + const navigate = useNavigate() + const { meetings, stats, loading, refreshMeetings } = useAppStore() + + useEffect(() => { + refreshMeetings() + }, []) + + const recentMeetings = meetings.slice(0, 4) + + const handleCardClick = (meeting) => { + navigate(`/meetings/${meeting.meeting_id}`) + } + + return ( +
+ {/* 页面标题 */} +
+
+

仪表盘

+

会议记录概览

+
+ + + +
+ + {/* 统计卡片 */} +
+ + + + +
+ + {/* 最近会议 */} +
+

+ + 最近会议 +

+ + 查看全部 + + +
+ + {loading ? ( +
+ {[1, 2, 3, 4].map(i => ( +
+ ))} +
+ ) : recentMeetings.length > 0 ? ( +
+ {recentMeetings.map(meeting => ( + + ))} +
+ ) : ( +
+
+ +
+

暂无会议记录

+

开始您的第一次会议转录

+ + + +
+ )} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/pages/MeetingDetail.jsx b/frontend/src/pages/MeetingDetail.jsx new file mode 100644 index 0000000..4dcc1f6 --- /dev/null +++ b/frontend/src/pages/MeetingDetail.jsx @@ -0,0 +1,330 @@ +import { useEffect, useState } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { + ArrowLeft, Download, Calendar, Clock, Users, Search, + FileText, AlignLeft, Loader2, User, Sparkles, Edit2, Check, X, + ChevronDown, Copy, CheckCheck +} from 'lucide-react' +import { useAppStore } from '../store/useAppStore' +import { getMeeting, downloadMarkdown, formatDuration, formatTimestamp, generateSummary, updateMeetingTitle } from '../api/meeting' +import Button from '../components/Button' + +export default function MeetingDetail() { + const { id } = useParams() + const navigate = useNavigate() + const { currentMeeting, loading, setCurrentMeeting, refreshMeetings } = useAppStore() + const [search, setSearch] = useState('') + const [summarizing, setSummarizing] = useState(false) + const [editingTitle, setEditingTitle] = useState(false) + const [titleInput, setTitleInput] = useState('') + const [summaryCollapsed, setSummaryCollapsed] = useState(false) + const [briefCopied, setBriefCopied] = useState(false) + const [detailedCopied, setDetailedCopied] = useState(false) + + useEffect(() => { + loadMeeting() + }, [id]) + + const loadMeeting = async () => { + const data = await getMeeting(id) + setCurrentMeeting(data) + } + + const handleGenerateSummary = async () => { + setSummarizing(true) + try { + const updated = await generateSummary(id) + setCurrentMeeting(updated) + } catch (err) { + alert('生成摘要失败: ' + err.message) + } finally { + setSummarizing(false) + } + } + + const handleEditTitle = () => { + setTitleInput(currentMeeting?.title || '') + setEditingTitle(true) + } + + const handleSaveTitle = async () => { + if (!titleInput.trim()) return + try { + const updated = await updateMeetingTitle(id, titleInput.trim()) + setCurrentMeeting(updated) + refreshMeetings() + } catch (err) { + alert('保存失败: ' + err.message) + } + setEditingTitle(false) + } + + const handleCancelTitle = () => { + setEditingTitle(false) + setTitleInput('') + } + + const copyToClipboard = async (text, type) => { + await navigator.clipboard.writeText(text) + if (type === 'brief') { + setBriefCopied(true) + setTimeout(() => setBriefCopied(false), 2000) + } else { + setDetailedCopied(true) + setTimeout(() => setDetailedCopied(false), 2000) + } + } + + if (loading || !currentMeeting) { + return ( +
+
+
+
+
+
+
+
+ ) + } + + const meeting = currentMeeting + const segments = meeting.segments || [] + const filteredSegments = search + ? segments.filter(s => s.text.toLowerCase().includes(search.toLowerCase())) + : segments + + return ( +
+ {/* 返回按钮 */} + + + {/* 详情头部 */} +
+
+
+ {editingTitle ? ( +
+ setTitleInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle()} + className="text-lg font-semibold text-primary glass-input rounded-lg px-3 py-1 focus:outline-none w-full max-w-sm" + autoFocus + /> + + +
+ ) : ( +
+

{meeting.title}

+ +
+ )} +
+ + + {meeting.date?.slice(0, 10) || '未知'} + + + + {formatDuration(meeting.duration)} + + + + {meeting.speaker_count || 0} 人 + +
+
+ + {meeting.status === 'completed' && ( +
+ + +
+ )} +
+ + {/* 音频播放器 */} + {meeting.audio_path && meeting.status === 'completed' && ( +
+ +
+ )} +
+ + {/* 处理中状态 */} + {meeting.status === 'processing' && ( +
+
+ +
+

正在转录中...

+

请稍候,预计需要几分钟时间

+
+ )} + + {/* 摘要区域 */} + {(meeting.brief_summary || meeting.detailed_summary) && ( +
+ + + {!summaryCollapsed && ( +
+ {meeting.brief_summary && ( +
+
+

+ + 简要摘要 +

+ +
+

+ {meeting.brief_summary} +

+
+ )} + + {meeting.detailed_summary && ( +
+
+

+ + 详细纪要 +

+ +
+

+ {meeting.detailed_summary} +

+
+ )} +
+ )} +
+ )} + + {/* 转写内容 */} + {meeting.status === 'completed' && ( +
+
+

+ + 转写内容 +

+
+ + setSearch(e.target.value)} + className="pl-8 pr-3 py-1.5 glass-input rounded-md text-xs text-primary + placeholder-muted focus:outline-none w-40" + /> +
+
+ +

+ {filteredSegments.length} / {segments.length} 条 +

+ +
+ {filteredSegments.map((seg, i) => ( +
+
+ + + 说话人 {seg.speaker} + + + {formatTimestamp(seg.start)} + +
+

{seg.text}

+
+ ))} +
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/pages/Meetings.jsx b/frontend/src/pages/Meetings.jsx new file mode 100644 index 0000000..83e5dc9 --- /dev/null +++ b/frontend/src/pages/Meetings.jsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { Search, RefreshCw, FileText } from 'lucide-react' +import { useAppStore } from '../store/useAppStore' +import MeetingCard from '../components/MeetingCard' +import Button from '../components/Button' + +export default function Meetings() { + const navigate = useNavigate() + const { meetings, loading, refreshMeetings } = useAppStore() + const [search, setSearch] = useState('') + const [statusFilter, setStatusFilter] = useState('all') + + useEffect(() => { + refreshMeetings() + }, []) + + const filteredMeetings = meetings.filter(m => { + const matchSearch = m.title.toLowerCase().includes(search.toLowerCase()) + const matchStatus = statusFilter === 'all' || m.status === statusFilter + return matchSearch && matchStatus + }) + + return ( +
+ {/* 页面标题 */} +
+

会议列表

+

管理所有会议记录

+
+ + {/* 搜索和筛选 */} +
+
+ + setSearch(e.target.value)} + className="w-full pl-9 pr-4 py-2 glass-input rounded-lg + text-sm text-primary placeholder-muted focus:outline-none" + /> +
+ + + + +
+ + {/* 结果统计 */} +

+ 共 {filteredMeetings.length} 个会议 +

+ + {/* 会议列表 */} + {loading ? ( +
+ {[1, 2, 3, 4].map(i => ( +
+ ))} +
+ ) : filteredMeetings.length > 0 ? ( +
+ {filteredMeetings.map(meeting => ( + navigate(`/meetings/${meeting.meeting_id}`)} + /> + ))} +
+ ) : ( +
+
+ +
+

未找到匹配的会议

+

尝试调整搜索条件

+
+ )} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx new file mode 100644 index 0000000..72d5a4c --- /dev/null +++ b/frontend/src/pages/Settings.jsx @@ -0,0 +1,81 @@ +import { Cpu, Database, Info, Monitor } from 'lucide-react' + +export default function Settings() { + return ( +
+ {/* 页面标题 */} +
+

系统设置

+

系统信息与配置

+
+ + {/* 运行环境 */} +
+

+ + 运行环境 +

+ +
+
+ + + 计算设备 + + CPU +
+
+ ASR 模型 + SenseVoiceSmall +
+
+ FunASR 版本 + 1.3.14 +
+
+
+ + {/* 存储信息 */} +
+

+ + 存储信息 +

+ +
+
+ 数据库类型 + SQLite +
+
+ 数据库路径 + data/meetings.db +
+
+
+ + {/* 关于 */} +
+

+ + 关于 +

+ +
+
+ 版本 + v1.0.0 +
+
+ 前端框架 + React 18 + Vite +
+
+ 技术栈 + FunASR + DeepSeek +
+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/pages/Upload.jsx b/frontend/src/pages/Upload.jsx new file mode 100644 index 0000000..0702ddf --- /dev/null +++ b/frontend/src/pages/Upload.jsx @@ -0,0 +1,535 @@ +import { useState, useRef, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { Upload, FileAudio, Mic, X, Square, Circle, Zap, FileText } from 'lucide-react' +import { clsx } from 'clsx' +import { useAppStore } from '../store/useAppStore' +import Button from '../components/Button' +import { formatDuration, formatTimestamp } from '../api/meeting' + +const RECORD_MODES = { + OFFLINE: 'offline', + REALTIME: 'realtime' +} + +export default function UploadPage() { + const navigate = useNavigate() + const { uploadMeeting, pollMeetingStatus, refreshMeetings } = useAppStore() + const fileInputRef = useRef() + + const [file, setFile] = useState(null) + const [title, setTitle] = useState('') + const [uploading, setUploading] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0) + const [transcribing, setTranscribing] = useState(false) + const [transcribeProgress, setTranscribeProgress] = useState(0) + const [dragOver, setDragOver] = useState(false) + + // 录音相关 + const [recordMode, setRecordMode] = useState(RECORD_MODES.OFFLINE) + const [isRecording, setIsRecording] = useState(false) + const [recordingTime, setRecordingTime] = useState(0) + const [audioLevel, setAudioLevel] = useState(0) + const [showRecorder, setShowRecorder] = useState(false) + const [recordTitle, setRecordTitle] = useState('') + + // 实时转写 + const [realtimeSegments, setRealtimeSegments] = useState([]) + const [wsConnected, setWsConnected] = useState(false) + const [savedMeetingId, setSavedMeetingId] = useState(null) + + const mediaRecorderRef = useRef(null) + const audioContextRef = useRef(null) + const analyserRef = useRef(null) + const timerRef = useRef(null) + const canvasRef = useRef(null) + const streamRef = useRef(null) + const animationRef = useRef(null) + const wsRef = useRef(null) + + const handleFileSelect = (e) => { + const selected = e.target.files?.[0] + if (selected) { + setFile(selected) + if (!title) { + setTitle(selected.name.replace(/\.[^/.]+$/, '')) + } + } + } + + const handleDrop = (e) => { + e.preventDefault() + setDragOver(false) + const dropped = e.dataTransfer.files?.[0] + if (dropped && dropped.type.startsWith('audio/')) { + setFile(dropped) + if (!title) { + setTitle(dropped.name.replace(/\.[^/.]+$/, '')) + } + } + } + + const handleUpload = async () => { + if (!file || !title) return + + setUploading(true) + setTranscribing(false) + setUploadProgress(0) + setTranscribeProgress(0) + + try { + const result = await uploadMeeting(file, title, (progress) => { + setUploadProgress(progress) + }) + + setUploading(false) + setTranscribing(true) + setTranscribeProgress(10) + + await pollMeetingStatus(result.meeting_id, (status) => { + if (status === 'processing') { + setTranscribeProgress(prev => Math.min(prev + 5, 90)) + } + }) + + setTranscribeProgress(100) + await refreshMeetings() + navigate('/meetings') + } catch (err) { + alert('处理失败: ' + err.message) + setUploading(false) + setTranscribing(false) + } + } + + const clearFile = () => { + setFile(null) + setTitle('') + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + + const connectWebSocket = () => { + const sessionId = `realtime_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` + const ws = new WebSocket(`ws://localhost:8501/ws/realtime/${sessionId}`) + + ws.onopen = () => { + setWsConnected(true) + const titleMsg = new Uint8Array(8 + title.length) + titleMsg.set(new TextEncoder().encode('TYPE'), 0) + titleMsg.set(new TextEncoder().encode('TITLE'), 4) + titleMsg.set(new TextEncoder().encode(title || '实时录音'), 8) + ws.send(titleMsg) + } + + ws.onmessage = (event) => { + const data = JSON.parse(event.data) + if (data.type === 'segment') { + setRealtimeSegments(prev => [...prev, { + start: data.start, + end: data.end, + speaker: data.speaker, + text: data.text + }]) + } else if (data.type === 'saved') { + setSavedMeetingId(data.meeting_id) + } + } + + ws.onclose = () => setWsConnected(false) + wsRef.current = ws + } + + const startRealtimeRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: { channelCount: 1, sampleRate: 16000, echoCancellation: true, noiseSuppression: true } + }) + streamRef.current = stream + + audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)() + const source = audioContextRef.current.createMediaStreamSource(stream) + analyserRef.current = audioContextRef.current.createAnalyser() + analyserRef.current.fftSize = 256 + source.connect(analyserRef.current) + visualizeAudio() + + connectWebSocket() + + setIsRecording(true) + setRecordingTime(0) + setRealtimeSegments([]) + + timerRef.current = setInterval(() => setRecordingTime(prev => prev + 1), 1000) + } catch (err) { + alert('无法访问麦克风') + } + } + + const startOfflineRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + streamRef.current = stream + + audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)() + const source = audioContextRef.current.createMediaStreamSource(stream) + analyserRef.current = audioContextRef.current.createAnalyser() + analyserRef.current.fftSize = 256 + source.connect(analyserRef.current) + visualizeAudio() + + const mediaRecorder = new MediaRecorder(stream) + mediaRecorderRef.current = mediaRecorder + const chunks = [] + + mediaRecorder.ondataavailable = (e) => { + if (e.data.size > 0) chunks.push(e.data) + } + + mediaRecorder.onstop = () => { + const blob = new Blob(chunks, { type: 'audio/webm' }) + const audioFile = new File([blob], `recording_${Date.now()}.webm`, { type: 'audio/webm' }) + setFile(audioFile) + if (!recordTitle) { + setRecordTitle(`录音_${new Date().toLocaleString()}`) + } + } + + mediaRecorder.start(1000) + setIsRecording(true) + setRecordingTime(0) + + timerRef.current = setInterval(() => setRecordingTime(prev => prev + 1), 1000) + } catch (err) { + alert('无法访问麦克风') + } + } + + const visualizeAudio = () => { + const analyser = analyserRef.current + const canvas = canvasRef.current + if (!analyser || !canvas) return + + const ctx = canvas.getContext('2d') + const bufferLength = analyser.frequencyBinCount + const dataArray = new Uint8Array(bufferLength) + + const draw = () => { + if (!isRecording) return + animationRef.current = requestAnimationFrame(draw) + analyser.getByteFrequencyData(dataArray) + const average = dataArray.reduce((a, b) => a + b, 0) / bufferLength + setAudioLevel(average / 255) + + ctx.fillStyle = 'rgba(9, 9, 11, 0.3)' + ctx.fillRect(0, 0, canvas.width, canvas.height) + + const barWidth = (canvas.width / bufferLength) * 2.5 + let x = 0 + for (let i = 0; i < bufferLength; i++) { + const barHeight = (dataArray[i] / 255) * canvas.height + ctx.fillStyle = `rgba(129, 140, 248, ${0.5 + dataArray[i] / 512})` + ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight) + x += barWidth + 1 + } + } + draw() + } + + const stopRecording = () => { + if (recordMode === RECORD_MODES.REALTIME) { + if (wsRef.current?.readyState === WebSocket.OPEN) { + const endMsg = new Uint8Array(12) + endMsg.set(new TextEncoder().encode('TYPE'), 0) + endMsg.set(new TextEncoder().encode('END___'), 4) + wsRef.current.send(endMsg) + } + } else { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop() + } + } + + setIsRecording(false) + clearInterval(timerRef.current) + cancelAnimationFrame(animationRef.current) + streamRef.current?.getTracks().forEach(track => track.stop()) + audioContextRef.current?.close() + setAudioLevel(0) + } + + const cancelRecording = () => { + wsRef.current?.close() + stopRecording() + setRecordingTime(0) + setRealtimeSegments([]) + } + + const handleSaveRecording = () => { + setTitle(recordTitle) + setShowRecorder(false) + } + + useEffect(() => { + return () => { + clearInterval(timerRef.current) + cancelAnimationFrame(animationRef.current) + streamRef.current?.getTracks().forEach(track => track.stop()) + wsRef.current?.close() + } + }, []) + + return ( +
+ {/* 页面标题 */} +
+

新建会议

+

上传音频或开始录音

+
+ + {/* 上传区域 */} +
{ e.preventDefault(); setDragOver(true) }} + onDragLeave={() => setDragOver(false)} + onDrop={handleDrop} + onClick={() => fileInputRef.current?.click()} + className={clsx( + 'glass-card rounded-xl p-8 text-center cursor-pointer mb-4 transition-all duration-200', + dragOver && 'glass-card-hover border-accent/30' + )} + > + + +
+
+ +
+

+ {dragOver ? '释放以上传' : '拖拽音频文件或点击上传'} +

+

WAV, MP3, M4A, FLAC, OGG

+
+
+ + {/* 已选文件 */} + {file && ( +
+
+
+ +
+
+

{file.name}

+

{(file.size / 1024 / 1024).toFixed(2)} MB

+
+ +
+ +