From 47668b8a740a986c36b835efa7f7970c8954b285 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Mon, 18 Aug 2025 23:42:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=92=AD=E5=AE=A2?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现播客详情页功能,包括: 1. 新增 PodcastContent 组件展示播客详情 2. 添加 AudioPlayerControls 和 PodcastTabs 组件 3. 实现分享功能组件 ShareButton 4. 优化音频文件命名规则和缓存机制 5. 完善类型定义和 API 接口 6. 调整 UI 布局和响应式设计 7. 修复积分不足状态码问题 --- .gitignore | 3 +- main.py | 73 +- podcast_generator.py | 10 +- web/package-lock.json | 1079 +++++++++++++++++++- web/package.json | 4 + web/src/app/api/audio-info/route.ts | 65 ++ web/src/app/api/config/route.ts | 50 +- web/src/app/api/generate-podcast/route.ts | 2 +- web/src/app/api/points/route.ts | 8 +- web/src/app/api/tts-providers/route.ts | 22 + web/src/app/page.tsx | 222 ++-- web/src/app/podcast/[fileName]/page.tsx | 15 + web/src/components/AudioPlayer.tsx | 25 +- web/src/components/AudioPlayerControls.tsx | 55 + web/src/components/ConfigSelector.tsx | 177 ++-- web/src/components/ContentSection.tsx | 28 +- web/src/components/MarkdownRenderer.tsx | 27 + web/src/components/PodcastCard.tsx | 22 +- web/src/components/PodcastContent.tsx | 123 +++ web/src/components/PodcastTabs.tsx | 66 ++ web/src/components/PointsOverview.tsx | 18 +- web/src/components/ShareButton.tsx | 41 + web/src/components/Sidebar.tsx | 8 +- web/src/lib/podcastApi.ts | 57 ++ web/src/types/index.ts | 15 +- web/start.bat | 4 +- 26 files changed, 1943 insertions(+), 276 deletions(-) create mode 100644 web/src/app/api/audio-info/route.ts create mode 100644 web/src/app/podcast/[fileName]/page.tsx create mode 100644 web/src/components/AudioPlayerControls.tsx create mode 100644 web/src/components/MarkdownRenderer.tsx create mode 100644 web/src/components/PodcastContent.tsx create mode 100644 web/src/components/PodcastTabs.tsx create mode 100644 web/src/components/ShareButton.tsx diff --git a/.gitignore b/.gitignore index 95d4e3e..4d2f669 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ output/ excalidraw.log config/tts_providers-local.json .claude -.serena \ No newline at end of file +.serena +/node_modules \ No newline at end of file diff --git a/main.py b/main.py index 1c009ae..eb5f1c0 100644 --- a/main.py +++ b/main.py @@ -76,13 +76,31 @@ stop_scheduler_event = threading.Event() output_dir = "output" time_after = 30 +# 内存中存储任务结果 +# {task_id: {"auth_id": auth_id, "status": TaskStatus, "result": any, "timestamp": float}} +task_results: Dict[str, Dict[UUID, Dict]] = {} +# 新增字典对象,key为音频文件名,value为task_results[auth_id][task_id]的值 +audio_file_mapping: Dict[str, Dict] = {} + +# 签名验证配置 +SECRET_KEY = os.getenv("PODCAST_API_SECRET_KEY", "your-super-secret-key") # 在生产环境中请务必修改! +# 定义从 tts_provider 名称到其配置文件路径的映射 +tts_provider_map = { + "index-tts": "config/index-tts.json", + "doubao-tts": "config/doubao-tts.json", + "edge-tts": "config/edge-tts.json", + "fish-audio": "config/fish-audio.json", + "gemini-tts": "config/gemini-tts.json", + "minimax": "config/minimax.json", +} + # 定义一个函数来清理输出目录 def clean_output_directory(): """Removes files from the output directory that are older than 30 minutes.""" print(f"Cleaning output directory: {output_dir}") now = time.time() # 30 minutes in seconds - threshold = time_after * 60 + threshold = time_after * 60 # 存储需要删除的 task_results 中的任务,避免在迭代时修改 tasks_to_remove_from_memory = [] @@ -105,7 +123,7 @@ def clean_output_directory(): # 只有 COMPLETED 的任务才应该被清理,PENDING/RUNNING 任务的输出文件可能还未生成或正在使用 task_output_filename = task_info.get("output_audio_filepath") if task_output_filename == filename and task_info["status"] == TaskStatus.COMPLETED: - tasks_to_remove_from_memory.append((auth_id, task_id)) + tasks_to_remove_from_memory.append((auth_id, task_id, task_info)) elif os.path.isdir(file_path): # 可选地,递归删除旧的子目录或其中的文件 # 目前只跳过目录 @@ -114,30 +132,20 @@ def clean_output_directory(): print(f"Failed to delete {file_path}. Reason: {e}") # 在文件删除循环结束后统一处理 task_results 的删除 - for auth_id, task_id in tasks_to_remove_from_memory: + for auth_id, task_id, task_info_to_remove in tasks_to_remove_from_memory: if auth_id in task_results and task_id in task_results[auth_id]: del task_results[auth_id][task_id] print(f"Removed task {task_id} for auth_id {auth_id} from task_results.") + # 从 audio_file_mapping 中删除对应的条目 + task_output_filename = task_info_to_remove.get("output_audio_filepath") + if task_output_filename and task_output_filename in audio_file_mapping: + del audio_file_mapping[task_output_filename] + print(f"Removed audio_file_mapping entry for {task_output_filename}.") # 如果该 auth_id 下没有其他任务,则删除 auth_id 的整个条目 if not task_results[auth_id]: del task_results[auth_id] print(f"Removed empty auth_id {auth_id} from task_results.") -# 内存中存储任务结果 -# {task_id: {"auth_id": auth_id, "status": TaskStatus, "result": any, "timestamp": float}} -task_results: Dict[str, Dict[UUID, Dict]] = {} - -# 签名验证配置 -SECRET_KEY = os.getenv("PODCAST_API_SECRET_KEY", "your-super-secret-key") # 在生产环境中请务必修改! -# 定义从 tts_provider 名称到其配置文件路径的映射 -tts_provider_map = { - "index-tts": "config/index-tts.json", - "doubao-tts": "config/doubao-tts.json", - "edge-tts": "config/edge-tts.json", - "fish-audio": "config/fish-audio.json", - "gemini-tts": "config/gemini-tts.json", - "minimax": "config/minimax.json", -} async def get_auth_id(x_auth_id: str = Header(..., alias="X-Auth-Id")): """ @@ -214,6 +222,14 @@ async def _generate_podcast_task( task_results[auth_id][task_id]["status"] = TaskStatus.COMPLETED task_results[auth_id][task_id].update(podcast_generation_results) print(f"\nPodcast generation completed for task {task_id}. Output file: {podcast_generation_results.get('output_audio_filepath')}") + # 更新 audio_file_mapping + output_audio_filepath = podcast_generation_results.get('output_audio_filepath') + if output_audio_filepath: + # 从完整路径中提取文件名 + filename = os.path.basename(output_audio_filepath) + filename = filename.split(".")[0] + # 将任务信息添加到 audio_file_mapping + audio_file_mapping[filename] = task_results[auth_id][task_id] # 生成并编码像素头像 avatar_bytes = generate_pixel_avatar(str(task_id)) # 使用 task_id 作为种子 @@ -230,7 +246,8 @@ async def _generate_podcast_task( "task_id": str(task_id), "auth_id": auth_id, "task_results": task_results[auth_id][task_id], - "timestamp": int(time.time()), # 确保发送整数秒级时间戳 + "timestamp": int(time.time()), + "status": task_results[auth_id][task_id]["status"], } MAX_RETRIES = 3 # 定义最大重试次数 @@ -290,7 +307,8 @@ async def generate_podcast_submission( "status": TaskStatus.PENDING, "result": None, "timestamp": time.time(), - "callback_url": callback_url # 存储回调地址 + "callback_url": callback_url, # 存储回调地址 + "auth_id": auth_id, # 存储 auth_id } background_tasks.add_task( @@ -345,6 +363,21 @@ async def download_podcast(file_name: str): raise HTTPException(status_code=404, detail="File not found.") return FileResponse(file_path, media_type='audio/mpeg', filename=file_name) +@app.get("/get-audio-info/") +async def get_audio_info(file_name: str): + """ + 根据文件名从 audio_file_mapping 中获取对应的任务信息。 + """ + # 移除文件扩展名(如果存在),因为 audio_file_mapping 的键是文件名(不含扩展名) + base_file_name = os.path.splitext(file_name)[0] + + audio_info = audio_file_mapping.get(base_file_name) + if audio_info: + # 返回任务信息的副本,避免直接暴露内部字典引用 + return JSONResponse(content={k: str(v) if isinstance(v, UUID) else v for k, v in audio_info.items()}) + else: + raise HTTPException(status_code=404, detail="Audio file information not found.") + @app.get("/avatar/{username}") async def get_avatar(username: str): """ diff --git a/podcast_generator.py b/podcast_generator.py index 7940f61..e82677a 100644 --- a/podcast_generator.py +++ b/podcast_generator.py @@ -112,10 +112,14 @@ def generate_speaker_id_text(pod_users, voices_list): return "。".join(speaker_info) + "。" def merge_audio_files(): + # 生成一个唯一的UUID + unique_id = str(uuid.uuid4()) + # 获取当前时间戳 timestamp = int(time.time()) - output_audio_filename_wav = f"podcast_{timestamp}.wav" + # 组合UUID和时间戳作为文件名,去掉 'podcast_' 前缀 + output_audio_filename_wav = f"{unique_id}{timestamp}.wav" output_audio_filepath_wav = os.path.join(output_dir, output_audio_filename_wav) - output_audio_filename_mp3 = f"podcast_{timestamp}.mp3" + output_audio_filename_mp3 = f"{unique_id}{timestamp}.mp3" output_audio_filepath_mp3 = os.path.join(output_dir, output_audio_filename_mp3) # Use ffmpeg to concatenate audio files @@ -221,7 +225,7 @@ def _parse_arguments(): parser.add_argument("--base-url", default="https://api.openai.com/v1", help="OpenAI API base URL (default: https://api.openai.com/v1).") parser.add_argument("--model", default="gpt-3.5-turbo", help="OpenAI model to use (default: gpt-3.5-turbo).") parser.add_argument("--threads", type=int, default=1, help="Number of threads to use for audio generation (default: 1).") - parser.add_argument("--output-language", type=str, default="Chinese", help="Language for the podcast overview and script (default: Chinese).") + parser.add_argument("--output-language", type=str, default=None, help="Language for the podcast overview and script (default: Chinese).") parser.add_argument("--usetime", type=str, default=None, help="Specific time to be mentioned in the podcast script, e.g., '今天', '昨天'.") return parser.parse_args() diff --git a/web/package-lock.json b/web/package-lock.json index 00eefdb..3a292ba 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -30,10 +30,14 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1", + "remark": "^15.0.1", + "remark-html": "^16.0.1", + "remark-parse": "^11.0.0", "socket.io-client": "^4.7.5", "tailwind-merge": "^2.4.0", "tailwindcss": "^3.4.7", "typescript": "^5.5.4", + "unified": "^11.0.5", "use-debounce": "^10.0.5" }, "devDependencies": { @@ -1235,6 +1239,8 @@ "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.15.11.tgz", "integrity": "sha512-JB8RWRs+cAbHX35/dQ9wD3m4W5EVGevq1fFqiHKTT4Pa5HR7WrcGRVT+8NL2M7gtTlOvyPh9zzms2DPLBCswig==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@libsql/core": "^0.15.11", "@libsql/hrana-client": "^0.7.0", @@ -1248,6 +1254,8 @@ "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.15.11.tgz", "integrity": "sha512-DQDYnEhCSYOsx30ASlOGuOqcQhvwELhOS2qM4dnIP+ZhKki2epZU1j5VZSNeQlrQXHkByMcWBy+wt7tBNx/9uA==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "js-base64": "^3.7.5" } @@ -1263,7 +1271,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@libsql/darwin-x64": { "version": "0.5.17", @@ -1276,13 +1285,16 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@libsql/hrana-client": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.7.0.tgz", "integrity": "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@libsql/isomorphic-fetch": "^0.3.1", "@libsql/isomorphic-ws": "^0.1.5", @@ -1295,6 +1307,8 @@ "resolved": "https://registry.npmjs.org/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.3.1.tgz", "integrity": "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18.0.0" } @@ -1304,6 +1318,8 @@ "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" @@ -1320,7 +1336,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@libsql/linux-arm-musleabihf": { "version": "0.5.17", @@ -1333,7 +1350,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@libsql/linux-arm64-gnu": { "version": "0.5.17", @@ -1346,7 +1364,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@libsql/linux-arm64-musl": { "version": "0.5.17", @@ -1359,7 +1378,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@libsql/linux-x64-gnu": { "version": "0.5.17", @@ -1372,7 +1392,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@libsql/linux-x64-musl": { "version": "0.5.17", @@ -1385,7 +1406,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@libsql/win32-x64-msvc": { "version": "0.5.17", @@ -1398,7 +1420,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", @@ -1417,7 +1440,9 @@ "version": "0.0.4", "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@next/env": { "version": "14.2.31", @@ -1719,6 +1744,8 @@ "integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==", "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=18.18" }, @@ -2553,12 +2580,31 @@ "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2566,6 +2612,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", @@ -2600,11 +2661,19 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "*" } @@ -2884,7 +2953,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { @@ -3538,6 +3606,16 @@ "node": ">= 0.4" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3926,6 +4004,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3943,6 +4031,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4041,6 +4159,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4120,6 +4248,8 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 12" } @@ -4182,7 +4312,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4196,6 +4325,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -4280,6 +4422,15 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -4303,6 +4454,19 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -5304,6 +5468,12 @@ "optional": true, "peer": true }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -5401,6 +5571,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -5516,6 +5688,8 @@ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "fetch-blob": "^3.1.2" }, @@ -5953,6 +6127,67 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-sanitize": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", + "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6332,6 +6567,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6548,7 +6795,9 @@ "version": "3.7.8", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true, + "peer": true }, "node_modules/js-tokens": { "version": "4.0.0", @@ -6683,11 +6932,13 @@ "arm" ], "license": "MIT", + "optional": true, "os": [ "darwin", "linux", "win32" ], + "peer": true, "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" @@ -6709,6 +6960,8 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -6768,6 +7021,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6805,6 +7068,99 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6814,6 +7170,448 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -7076,6 +7874,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10.5.0" } @@ -7085,6 +7885,8 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -7798,7 +8600,9 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/prop-types": { "version": "15.8.1", @@ -7812,6 +8616,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -8121,6 +8935,70 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-html": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-16.0.1.tgz", + "integrity": "sha512-B9JqA5i0qZe0Nsf49q3OXyGvyXuZFDzAP2iOFLEumymuYJITVpiH1IgsTEwTpdptDmZlMDMWeDmSawdaJIGCXQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "hast-util-sanitize": "^5.0.0", + "hast-util-to-html": "^9.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -8636,6 +9514,16 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -8852,6 +9740,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -9186,6 +10088,26 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -9404,6 +10326,93 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -9540,11 +10549,41 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 8" } @@ -9826,6 +10865,16 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/web/package.json b/web/package.json index 161d35e..12e7e15 100644 --- a/web/package.json +++ b/web/package.json @@ -34,10 +34,14 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1", + "remark": "^15.0.1", + "remark-html": "^16.0.1", + "remark-parse": "^11.0.0", "socket.io-client": "^4.7.5", "tailwind-merge": "^2.4.0", "tailwindcss": "^3.4.7", "typescript": "^5.5.4", + "unified": "^11.0.5", "use-debounce": "^10.0.5" }, "devDependencies": { diff --git a/web/src/app/api/audio-info/route.ts b/web/src/app/api/audio-info/route.ts new file mode 100644 index 0000000..97464f8 --- /dev/null +++ b/web/src/app/api/audio-info/route.ts @@ -0,0 +1,65 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAudioInfo, getUserInfo } from '@/lib/podcastApi'; + + +/** + * 处理 GET 请求,用于代理查询后端 FastAPI 应用的 /get-audio-info 接口。 + * 查询参数:file_name + */ +export async function GET(req: NextRequest) { + // 从请求 URL 中获取查询参数 + const { searchParams } = new URL(req.url); + const fileName = searchParams.get('file_name'); + + // 检查是否提供了 fileName 参数 + if (!fileName) { + return NextResponse.json( + { success: false, error: '缺少 file_name 查询参数' }, + { status: 400 } + ); + } + + try { + // 调用前端的 podcastApi 模块中的 getAudioInfo 函数 + const result = await getAudioInfo(fileName); + + if (!result.success) { + // 转发 getAudioInfo 返回的错误信息和状态码 + return NextResponse.json( + { success: false, error: result.error }, + { status: result.statusCode || 500 } + ); + } + + const authId = result.data?.auth_id; // 确保 auth_id 存在且安全访问 + let userInfoData = null; + + + if (authId) { + const userInfo = await getUserInfo(authId); + if (userInfo.success && userInfo.data) { + userInfoData = { + name: userInfo.data.name, + email: userInfo.data.email, + image: userInfo.data.image, + }; + } + } + + // 合并 result.data 和 userInfoData + const { auth_id, callback_url, ...restData } = result.data || {}; // 保留解构,但确保是来自 result.data + const responseData = { + ...restData, + user: userInfoData // 将用户信息作为嵌套对象添加 + }; + + return NextResponse.json({ success: true, data: responseData }, { status: 200 }); + + } catch (error) { + console.error('代理 /api/audio-info 失败:', error); + return NextResponse.json( + { success: false, error: '内部服务器错误或无法连接到后端服务' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/web/src/app/api/config/route.ts b/web/src/app/api/config/route.ts index 1644ce4..15c9237 100644 --- a/web/src/app/api/config/route.ts +++ b/web/src/app/api/config/route.ts @@ -3,6 +3,27 @@ import path from 'path'; import fs from 'fs/promises'; import type { TTSConfig } from '@/types'; +// 缓存对象,存储响应数据和时间戳 +const cache = new Map(); +const CACHE_TTL = 30 * 60 * 1000; // 30 分钟 + +function getCache(key: string) { + const entry = cache.get(key); + if (!entry) { + return null; + } + // 检查缓存是否过期 + if (Date.now() - entry.timestamp > CACHE_TTL) { + cache.delete(key); // 缓存过期,删除 + return null; + } + return entry.data; +} + +function setCache(key: string, data: any) { + cache.set(key, { data, timestamp: Date.now() }); +} + const TTS_PROVIDER_ORDER = [ 'edge-tts', 'doubao-tts', @@ -14,6 +35,17 @@ const TTS_PROVIDER_ORDER = [ // 获取配置文件列表 export async function GET() { + const cacheKey = 'config_files_list'; + const cachedData = getCache(cacheKey); + + if (cachedData) { + console.log('Returning config files list from cache.'); + return NextResponse.json({ + success: true, + data: cachedData, + }); + } + try { const configDir = path.join(process.cwd(), '..', 'config'); const files = await fs.readdir(configDir); @@ -41,6 +73,8 @@ export async function GET() { return aIndex - bIndex; }); + setCache(cacheKey, configFiles); // 存储到缓存 + return NextResponse.json({ success: true, data: configFiles, @@ -56,9 +90,19 @@ export async function GET() { // 获取特定配置文件内容 export async function POST(request: NextRequest) { + const { configFile } = await request.json(); + const cacheKey = `config_file_${configFile}`; + const cachedData = getCache(cacheKey); + + if (cachedData) { + console.log(`Returning config file "${configFile}" from cache.`); + return NextResponse.json({ + success: true, + data: cachedData, + }); + } + try { - const { configFile } = await request.json(); - if (!configFile || !configFile.endsWith('.json')) { return NextResponse.json( { success: false, error: '无效的配置文件名' }, @@ -70,6 +114,8 @@ export async function POST(request: NextRequest) { const configContent = await fs.readFile(configPath, 'utf-8'); const config: TTSConfig = JSON.parse(configContent); + setCache(cacheKey, config); // 存储到缓存 + return NextResponse.json({ success: true, data: config, diff --git a/web/src/app/api/generate-podcast/route.ts b/web/src/app/api/generate-podcast/route.ts index ec67f88..bf7e22d 100644 --- a/web/src/app/api/generate-podcast/route.ts +++ b/web/src/app/api/generate-podcast/route.ts @@ -25,7 +25,7 @@ export async function POST(request: NextRequest) { if (currentPoints === null || currentPoints < POINTS_PER_PODCAST) { return NextResponse.json( { success: false, error: `积分不足,生成一个播客需要 ${POINTS_PER_PODCAST} 积分,您当前只有 ${currentPoints || 0} 积分。` }, - { status: 403 } // 403 Forbidden - 权限不足,因为积分不足 + { status: 402 } // 402 Forbidden - 权限不足,因为积分不足 ); } diff --git a/web/src/app/api/points/route.ts b/web/src/app/api/points/route.ts index 2551400..ed3a3ce 100644 --- a/web/src/app/api/points/route.ts +++ b/web/src/app/api/points/route.ts @@ -26,8 +26,11 @@ export async function GET() { } export async function PUT(request: NextRequest) { + const { task_id, auth_id, timestamp, status } = await request.json(); try { - const { task_id, auth_id, timestamp } = await request.json(); + if(status !== 'completed') { + return NextResponse.json({ success: false, error: "Invalid status" }, { status: 400 }); + } // 1. 参数校验 if (!task_id || !auth_id || typeof timestamp !== 'number') { @@ -67,7 +70,8 @@ export async function PUT(request: NextRequest) { if (error instanceof Error) { // 区分积分不足的错误 if (error.message.includes("积分不足")) { - return NextResponse.json({ success: false, error: error.message }, { status: 403 }); // Forbidden + console.error("积分不足错误: %s %s %s", auth_id, task_id, error); + return NextResponse.json({ success: false, error: error.message }, { status: 402 }); // Forbidden } return NextResponse.json({ success: false, error: error.message }, { status: 500 }); } diff --git a/web/src/app/api/tts-providers/route.ts b/web/src/app/api/tts-providers/route.ts index 6a51995..3fa72ee 100644 --- a/web/src/app/api/tts-providers/route.ts +++ b/web/src/app/api/tts-providers/route.ts @@ -2,13 +2,35 @@ import { NextRequest, NextResponse } from 'next/server'; import path from 'path'; import fs from 'fs/promises'; +// 定义缓存变量和缓存过期时间 +let ttsProvidersCache: any = null; +let cacheTimestamp: number = 0; +const CACHE_DURATION = 30 * 60 * 1000; // 30分钟,单位毫秒 + // 获取 tts_providers.json 文件内容 export async function GET() { try { + const now = Date.now(); + + // 检查缓存是否有效 + if (ttsProvidersCache && (now - cacheTimestamp < CACHE_DURATION)) { + console.log('从缓存中返回 tts_providers.json 数据'); + return NextResponse.json({ + success: true, + data: ttsProvidersCache, + }); + } + + // 缓存无效或不存在,读取文件并更新缓存 const configPath = path.join(process.cwd(), '..', 'config', 'tts_providers.json'); const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent); + // 更新缓存 + ttsProvidersCache = config; + cacheTimestamp = now; + console.log('重新加载并缓存 tts_providers.json 数据'); + return NextResponse.json({ success: true, data: config, diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index f37420e..35d9483 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,25 +1,77 @@ 'use client'; import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; import Sidebar from '@/components/Sidebar'; import PodcastCreator from '@/components/PodcastCreator'; import ContentSection from '@/components/ContentSection'; import AudioPlayer from '@/components/AudioPlayer'; import SettingsForm from '@/components/SettingsForm'; import PointsOverview from '@/components/PointsOverview'; // 导入 PointsOverview +import LoginModal from '@/components/LoginModal'; // 导入 LoginModal import { ToastContainer, useToast } from '@/components/Toast'; import { usePreventDuplicateCall } from '@/hooks/useApiCall'; import { trackedFetch } from '@/utils/apiCallTracker'; import type { PodcastGenerationRequest, PodcastItem, UIState, PodcastGenerationResponse, SettingsFormData } from '@/types'; import { getTTSProviders } from '@/lib/config'; -import LoginModal from '@/components/LoginModal'; // 导入 LoginModal import { getSessionData } from '@/lib/server-actions'; const enableTTSConfigPage = process.env.NEXT_PUBLIC_ENABLE_TTS_CONFIG_PAGE === 'true'; +// 辅助函数:规范化设置数据 +const normalizeSettings = (savedSettings: any): SettingsFormData => { + return { + apikey: savedSettings.apikey || '', + model: savedSettings.model || '', + baseurl: savedSettings.baseurl || '', + index: { + api_url: savedSettings.index?.api_url || '', + }, + edge: { + api_url: savedSettings.edge?.api_url || '', + }, + doubao: { + 'X-Api-App-Id': savedSettings.doubao?.['X-Api-App-Id'] || '', + 'X-Api-Access-Key': savedSettings.doubao?.['X-Api-Access-Key'] || '', + }, + fish: { + api_key: savedSettings.fish?.api_key || '', + }, + minimax: { + group_id: savedSettings.minimax?.group_id || '', + api_key: savedSettings.minimax?.api_key || '', + }, + gemini: { + api_key: savedSettings.gemini?.api_key || '', + }, + }; +}; + export default function HomePage() { const { toasts, success, error, warning, info, removeToast } = useToast(); const { executeOnce } = usePreventDuplicateCall(); + const router = useRouter(); // Initialize useRouter + + // 辅助函数:将 API 响应映射为 PodcastItem 数组 + const mapApiResponseToPodcasts = (tasks: PodcastGenerationResponse[]): PodcastItem[] => { + return tasks.map((task: any) => ({ + id: task.task_id, + title: task.title ? task.title : task.status === 'failed' ? '播客生成失败,请重试' : ' ', + description: task.tags ? task.tags.split('#').map((tag: string) => tag.trim()).filter((tag: string) => !!tag).join(', ') : task.status === 'failed' ? task.error || '待生成的播客标签' : '待生成的播客标签', + thumbnail: task.avatar_base64 ? `data:image/png;base64,${task.avatar_base64}` : '', + author: { + name: '', + avatar: '', + }, + audio_duration: task.audio_duration || '00:00', + playCount: 0, + createdAt: task.timestamp ? new Date(task.timestamp * 1000).toISOString() : new Date().toISOString(), + audioUrl: task.audioUrl ? task.audioUrl : '', + tags: task.tags ? task.tags.split('#').map((tag: string) => tag.trim()).filter((tag: string) => !!tag) : task.status === 'failed' ? [task.error] : ['待生成的播客标签'], + status: task.status, + file_name: task.output_audio_filepath || '', + })); + }; const [uiState, setUIState] = useState({ sidebarCollapsed: true, @@ -41,7 +93,8 @@ export default function HomePage() { // 音频播放器状态 const [currentPodcast, setCurrentPodcast] = useState(null); const [isPlaying, setIsPlaying] = useState(false); - + + // 播客详情页状态 // 从后端获取积分数据和初始化数据加载 const initialized = React.useRef(false); // 使用 useRef 追踪是否已初始化 @@ -51,8 +104,10 @@ export default function HomePage() { if (!initialized.current) { initialized.current = true; - // 首次加载时获取播客列表 + // 首次加载时获取播客列表和积分/用户信息 fetchRecentPodcasts(); + // fetchCreditsAndUserInfo(); // 在fetchRecentPodcasts中调用 + } // 设置定时器每20秒刷新一次 @@ -69,32 +124,7 @@ export default function HomePage() { const loadSettings = async () => { const savedSettings = await getTTSProviders(); if (savedSettings) { - // 确保从 localStorage 加载的设置中,所有预期的字符串字段都为字符串 - const normalizedSettings: SettingsFormData = { - apikey: savedSettings.apikey || '', - model: savedSettings.model || '', - baseurl: savedSettings.baseurl || '', - index: { - api_url: savedSettings.index?.api_url || '', - }, - edge: { - api_url: savedSettings.edge?.api_url || '', - }, - doubao: { - 'X-Api-App-Id': savedSettings.doubao?.['X-Api-App-Id'] || '', - 'X-Api-Access-Key': savedSettings.doubao?.['X-Api-Access-Key'] || '', - }, - fish: { - api_key: savedSettings.fish?.api_key || '', - }, - minimax: { - group_id: savedSettings.minimax?.group_id || '', - api_key: savedSettings.minimax?.api_key || '', - }, - gemini: { - api_key: savedSettings.gemini?.api_key || '', - }, - }; + const normalizedSettings = normalizeSettings(savedSettings); setSettings(normalizedSettings); } }; @@ -153,6 +183,9 @@ export default function HomePage() { if(response.status === 401) { throw new Error('生成播客失败,请检查API Key是否正确,或登录状态。'); } + if(response.status === 402) { + throw new Error('生成播客失败,请检查积分是否足够。'); + } if(response.status === 403) { setIsLoginModalOpen(true); // 显示登录模态框 throw new Error('生成播客失败,请登录后重试。'); @@ -185,6 +218,11 @@ export default function HomePage() { } }; + // 处理播客标题点击 + const handleTitleClick = (podcast: PodcastItem) => { + router.push(`/podcast/${podcast.file_name.split(".")[0]}`); + }; + const handlePlayPodcast = (podcast: PodcastItem) => { if (currentPodcast?.id === podcast.id) { setIsPlaying(prev => !prev); @@ -221,91 +259,65 @@ export default function HomePage() { try { const apiResponse: { success: boolean; tasks?: { message: string; tasks: PodcastGenerationResponse[]; }; error?: string } = result; - if (apiResponse.success && apiResponse.tasks && Array.isArray(apiResponse.tasks)) { // 检查 tasks 属性是否存在且为数组 - const newPodcasts: PodcastItem[] = apiResponse.tasks.map((task: any) => ({ // 遍历 tasks 属性 - id: task.task_id, // 使用 task_id - title: task.title ? task.title : task.status === 'failed' ? '播客生成失败,请重试' : ' ', - description: task.tags ? task.tags.split('#').map((tag: string) => tag.trim()).join(', ') : task.status === 'failed' ? task.error || '待生成的播客标签' : '待生成的播客标签', - thumbnail: task.avatar_base64 ? `data:image/png;base64,${task.avatar_base64}` : '', - author: { - name: '', - avatar: '', - }, - duration: parseDurationToSeconds(task.audio_duration || '00:00'), - playCount: 0, - createdAt: task.timestamp ? new Date(task.timestamp * 1000).toISOString() : new Date().toISOString(), - audioUrl: task.audioUrl ? task.audioUrl : '', - tags: task.tags ? task.tags.split('#').map((tag: string) => tag.trim()) : task.status === 'failed' ? [task.error] : ['待生成的播客标签'], - status: task.status, - })); - // 直接倒序,确保最新生成的播客排在前面 + if (apiResponse.success && apiResponse.tasks && Array.isArray(apiResponse.tasks)) { + const newPodcasts = mapApiResponseToPodcasts(apiResponse.tasks); const reversedPodcasts = newPodcasts.reverse(); setExplorePodcasts(reversedPodcasts); - // 如果有最新生成的播客,自动播放 } } catch (err) { console.error('Error processing podcast data:', err); error('数据处理失败', err instanceof Error ? err.message : '无法处理播客列表数据'); } - const fetchCredits = async () => { - try { - const pointsResponse = await fetch('/api/points'); - if (pointsResponse.ok) { - const data = await pointsResponse.json(); - if (data.success) { - setCredits(data.points); - } else { - console.error('Failed to fetch credits:', data.error); - setCredits(0); // 获取失败则设置为0 - } - } else { - console.error('Failed to fetch credits with status:', pointsResponse.status); - setCredits(0); // 获取失败则设置为0 - } - } catch (error) { - console.error('Error fetching credits:', error); - setCredits(0); // 发生错误则设置为0 - } - - try { - const transactionsResponse = await fetch('/api/points/transactions'); - if (transactionsResponse.ok) { - const data = await transactionsResponse.json(); - if (data.success) { - setPointHistory(data.transactions); - } else { - console.error('Failed to fetch point transactions:', data.error); - setPointHistory([]); - } - } else { - console.error('Failed to fetch point transactions with status:', transactionsResponse.status); - setPointHistory([]); - } - } catch (error) { - console.error('Error fetching point transactions:', error); - setPointHistory([]); - } - - const { session, user } = await getSessionData(); - setUser(user); // 设置用户信息 - }; - - fetchCredits(); // 调用获取积分函数 + fetchCreditsAndUserInfo(); }; - // 辅助函数:解析时长字符串为秒数 - const parseDurationToSeconds = (durationStr: string): number => { - const parts = durationStr.split(':'); - if (parts.length === 2) { - return parseInt(parts[0]) * 60 + parseInt(parts[1]); - } else if (parts.length === 3) { // 支持 HH:MM:SS 格式 - return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]); - } - return 0; + // 新增辅助函数:获取积分和用户信息 + const fetchCreditsAndUserInfo = async () => { + try { + const pointsResponse = await fetch('/api/points'); + if (pointsResponse.ok) { + const data = await pointsResponse.json(); + if (data.success) { + setCredits(data.points); + } else { + console.error('Failed to fetch credits:', data.error); + setCredits(0); // 获取失败则设置为0 + } + } else { + console.error('Failed to fetch credits with status:', pointsResponse.status); + setCredits(0); // 获取失败则设置为0 + } + } catch (error) { + console.error('Error fetching credits:', error); + setCredits(0); // 发生错误则设置为0 + } + + try { + const transactionsResponse = await fetch('/api/points/transactions'); + if (transactionsResponse.ok) { + const data = await transactionsResponse.json(); + if (data.success) { + setPointHistory(data.transactions); + } else { + console.error('Failed to fetch point transactions:', data.error); + setPointHistory([]); + } + } else { + console.error('Failed to fetch point transactions with status:', transactionsResponse.status); + setPointHistory([]); + } + } catch (error) { + console.error('Error fetching point transactions:', error); + setPointHistory([]); + } + + const { session, user } = await getSessionData(); + setUser(user); // 设置用户信息 }; const renderMainContent = () => { + switch (uiState.currentView) { case 'home': return ( @@ -325,8 +337,9 @@ export default function HomePage() { subtitle="数据只保留30分钟,请尽快下载保存" items={explorePodcasts} onPlayPodcast={handlePlayPodcast} - currentPodcast={currentPodcast} - isPlaying={isPlaying} + onTitleClick={handleTitleClick} // 传递 handleTitleClick + currentPodcast={currentPodcast} // 继续传递给 ContentSection + isPlaying={isPlaying} // 继续传递给 ContentSection variant="compact" layout="grid" showRefreshButton={true} @@ -339,6 +352,7 @@ export default function HomePage() { title="为你推荐" items={[...explorePodcasts].slice(0, 6)} onPlayPodcast={handlePlayPodcast} + onTitleClick={handleTitleClick} // 传递 handleTitleClick variant="default" layout="horizontal" /> */} @@ -415,7 +429,7 @@ export default function HomePage() {
-
+
{renderMainContent()}
diff --git a/web/src/app/podcast/[fileName]/page.tsx b/web/src/app/podcast/[fileName]/page.tsx new file mode 100644 index 0000000..3f77a3a --- /dev/null +++ b/web/src/app/podcast/[fileName]/page.tsx @@ -0,0 +1,15 @@ +import PodcastContent from '@/components/PodcastContent'; + +interface PodcastDetailPageProps { + params: { + fileName: string; + }; +} + +export default async function PodcastDetailPage({ params }: PodcastDetailPageProps) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/web/src/components/AudioPlayer.tsx b/web/src/components/AudioPlayer.tsx index af01ad7..a5e61a7 100644 --- a/web/src/components/AudioPlayer.tsx +++ b/web/src/components/AudioPlayer.tsx @@ -17,6 +17,7 @@ import { cn, formatTime, downloadFile } from '@/lib/utils'; import AudioVisualizer from './AudioVisualizer'; import { useIsSmallScreen } from '@/hooks/useMediaQuery'; // 导入新的 Hook import type { AudioPlayerState, PodcastItem } from '@/types'; +import { useToast } from '@/components/Toast'; interface AudioPlayerProps { podcast: PodcastItem; @@ -35,7 +36,8 @@ const AudioPlayer: React.FC = ({ }) => { const audioRef = useRef(null); const progressRef = useRef(null); - + const { success: toastSuccess } = useToast(); // 使用 useToast Hook + const [playerState, setPlayerState] = useState>({ currentTime: 0, duration: 0, @@ -185,21 +187,32 @@ const AudioPlayer: React.FC = ({ }; const handleShare = async () => { + // 从 podcast.audioUrl 中提取文件名 + const audioFileName = podcast.file_name; + if (!audioFileName) { + console.error("无法获取音频文件名进行分享。"); + toastSuccess('分享失败:无法获取音频文件名。'); + return; + } + + // 构建分享链接:网站根目录 + podcast/路径 + 音频文件名 + const shareUrl = `${window.location.origin}/podcast/${audioFileName}`; + if (navigator.share) { try { await navigator.share({ title: podcast.title, text: podcast.description, - url: window.location.href, + url: shareUrl, // 使用构建的分享链接 }); } catch (err) { console.log('Share cancelled', err); } } else { - // 降级到复制链接 - await navigator.clipboard.writeText(window.location.href); - // 这里可以显示一个toast提示 - alert('链接已复制到剪贴板!'); // 简单替代Toast + // 降级到复制音频链接 + await navigator.clipboard.writeText(shareUrl); // 使用构建的分享链接 + // 使用Toast提示 + toastSuccess('播放链接已复制到剪贴板!'); } }; diff --git a/web/src/components/AudioPlayerControls.tsx b/web/src/components/AudioPlayerControls.tsx new file mode 100644 index 0000000..f4b1dc8 --- /dev/null +++ b/web/src/components/AudioPlayerControls.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { Play, Pause } from 'lucide-react'; + +interface AudioPlayerControlsProps { + audioUrl: string; + audioDuration?: string; +} + +export default function AudioPlayerControls({ audioUrl, audioDuration }: AudioPlayerControlsProps) { + const [isPlaying, setIsPlaying] = useState(false); + const audioRef = useRef(null); + + const togglePlayPause = () => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }; + + useEffect(() => { + const audio = audioRef.current; + if (audio) { + const onEnded = () => { + setIsPlaying(false); + }; + audio.addEventListener('ended', onEnded); + return () => { + audio.removeEventListener('ended', onEnded); + }; + } + }, []); + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/web/src/components/ConfigSelector.tsx b/web/src/components/ConfigSelector.tsx index 6e68f5c..4e81149 100644 --- a/web/src/components/ConfigSelector.tsx +++ b/web/src/components/ConfigSelector.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import { Check } from 'lucide-react'; -import { usePreventDuplicateCall } from '@/hooks/useApiCall'; import type { TTSConfig, Voice } from '@/types'; import { getTTSProviders } from '@/lib/config'; const enableTTSConfigPage = process.env.NEXT_PUBLIC_ENABLE_TTS_CONFIG_PAGE === 'true'; @@ -28,7 +27,7 @@ const ConfigSelector: React.FC = ({ const [voices, setVoices] = useState([]); // 新增 voices 状态 const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { executeOnce } = usePreventDuplicateCall(); + const loadConfigFilesCalled = React.useRef(false); // 检查TTS配置是否已设置 const isTTSConfigured = (configName: string, settings: any): boolean => { @@ -54,18 +53,46 @@ const ConfigSelector: React.FC = ({ } }; - // 加载配置文件列表 - 使用防重复调用机制 - const loadConfigFiles = async () => { - const result = await executeOnce(async () => { - const response = await fetch('/api/config'); - return response.json(); - }); + // 加载特定配置文件 + const loadConfig = async (configFile: string) => { + setIsLoading(true); + try { + const configResponse = await fetch('/api/config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ configFile }), + }); - if (!result) { - return; // 如果是重复调用,直接返回 + const configResult = await configResponse.json(); + + let fetchedVoices: Voice[] = []; + if (configResult.success) { + fetchedVoices = configResult.data.voices; + setVoices(fetchedVoices); // 更新 voices 状态 + setCurrentConfig(configResult.data); + onConfigChange?.(configResult.data, configFile, fetchedVoices); // 传递 fetchedVoices + } + } catch (error) { + console.error('Failed to load config or voices:', error); + } finally { + setIsLoading(false); } + }; + + // 加载配置文件列表 + const loadConfigFiles = async () => { + // 防止重复调用 + if (loadConfigFilesCalled.current) { + return; + } + loadConfigFilesCalled.current = true; try { + const response = await fetch('/api/config'); + const result = await response.json(); + if (result.success && Array.isArray(result.data)) { // 过滤出已配置的TTS选项 const settings = await getTTSProviders(); @@ -96,7 +123,7 @@ const ConfigSelector: React.FC = ({ useEffect(() => { loadConfigFiles(); - }, [executeOnce]); // 添加 executeOnce 到依赖项 + }, []); // 监听localStorage变化,重新加载配置 useEffect(() => { @@ -114,35 +141,7 @@ const ConfigSelector: React.FC = ({ window.removeEventListener('storage', handleStorageChange); window.removeEventListener('settingsUpdated', handleStorageChange); }; - }, [selectedConfig, executeOnce]); - - // 加载特定配置文件 - const loadConfig = async (configFile: string) => { - setIsLoading(true); - try { - const configResponse = await fetch('/api/config', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ configFile }), - }); - - const configResult = await configResponse.json(); - - let fetchedVoices: Voice[] = []; - if (configResult.success) { - fetchedVoices = configResult.data.voices; - setVoices(fetchedVoices); // 更新 voices 状态 - setCurrentConfig(configResult.data); - onConfigChange?.(configResult.data, configFile, fetchedVoices); // 传递 fetchedVoices - } - } catch (error) { - console.error('Failed to load config or voices:', error); - } finally { - setIsLoading(false); - } - }; + }, [selectedConfig]); const handleConfigSelect = (configFile: string) => { setSelectedConfig(configFile); @@ -153,59 +152,59 @@ const ConfigSelector: React.FC = ({ const selectedConfigFile = Array.isArray(configFiles) ? configFiles.find(f => f.name === selectedConfig) : null; return ( -
- {/* 配置选择器 */} - +
+ {/* 配置选择器 */} + - {/* 下拉菜单 */} - {isOpen && ( -
- {Array.isArray(configFiles) && configFiles.length > 0 ? configFiles.map((config) => ( - + )) : ( +
+
暂无可用的TTS配置
+
请先在设置中配置TTS服务
- {selectedConfig === config.name && ( - - )} - - )) : ( -
-
暂无可用的TTS配置
-
请先在设置中配置TTS服务
-
- )} -
- )} + )} +
+ )} - - {/* 点击外部关闭下拉菜单 */} - {isOpen && ( -
setIsOpen(false)} - /> - )} -
+ + {/* 点击外部关闭下拉菜单 */} + {isOpen && ( +
setIsOpen(false)} + /> + )} +
); }; diff --git a/web/src/components/ContentSection.tsx b/web/src/components/ContentSection.tsx index 351dfae..e354660 100644 --- a/web/src/components/ContentSection.tsx +++ b/web/src/components/ContentSection.tsx @@ -3,7 +3,7 @@ import React, { useRef, useEffect } from 'react'; import { ChevronRight, RotateCw } from 'lucide-react'; import PodcastCard from './PodcastCard'; -import type { PodcastItem } from '@/types'; +import type { PodcastItem } from '@/types'; // 移除了 PodcastGenerationResponse interface ContentSectionProps { title: string; @@ -11,13 +11,14 @@ interface ContentSectionProps { items: PodcastItem[]; onViewAll?: () => void; onPlayPodcast?: (podcast: PodcastItem) => void; - currentPodcast?: PodcastItem | null; - isPlaying?: boolean; loading?: boolean; variant?: 'default' | 'compact'; layout?: 'grid' | 'horizontal'; - showRefreshButton?: boolean; // 新增刷新按钮属性 - onRefresh?: () => void; // 新增刷新回调函数 + showRefreshButton?: boolean; + onRefresh?: () => void; + onTitleClick?: (podcast: PodcastItem) => void; // 确保传入 onTitleClick + currentPodcast?: PodcastItem | null; // Keep this prop for PodcastCard + isPlaying?: boolean; // Keep this prop for PodcastCard } const ContentSection: React.FC = ({ @@ -26,14 +27,16 @@ const ContentSection: React.FC = ({ items, onViewAll, onPlayPodcast, - currentPodcast, - isPlaying, loading = false, variant = 'default', layout = 'grid', - showRefreshButton, // 直接解构 - onRefresh // 直接解构 + showRefreshButton, + onRefresh, + onTitleClick, // 确保解构 + currentPodcast, // 确保解构 + isPlaying // 确保解构 }) => { + if (loading) { return (
@@ -136,6 +139,7 @@ const ContentSection: React.FC = ({ items={items} onPlayPodcast={onPlayPodcast} variant={variant} + onTitleClick={onTitleClick} /> ) : ( // 网格布局 @@ -152,6 +156,7 @@ const ContentSection: React.FC = ({ variant={variant} currentPodcast={currentPodcast} isPlaying={isPlaying} + onTitleClick={onTitleClick} /> ))}
@@ -165,12 +170,14 @@ interface HorizontalScrollSectionProps { items: PodcastItem[]; onPlayPodcast?: (podcast: PodcastItem) => void; variant?: 'default' | 'compact'; + onTitleClick?: (podcast: PodcastItem) => void; } const HorizontalScrollSection: React.FC = ({ items, onPlayPodcast, - variant = 'default' + variant = 'default', + onTitleClick }) => { const scrollRef = useRef(null); const intervalRef = useRef(null); @@ -250,6 +257,7 @@ const HorizontalScrollSection: React.FC = ({ className={`flex-shrink-0 ${ variant === 'compact' ? 'w-80' : 'w-72' }`} + onTitleClick={onTitleClick} /> ))}
diff --git a/web/src/components/MarkdownRenderer.tsx b/web/src/components/MarkdownRenderer.tsx new file mode 100644 index 0000000..9094917 --- /dev/null +++ b/web/src/components/MarkdownRenderer.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkHtml from 'remark-html'; + +interface MarkdownRendererProps { + content: string; +} + +export default function MarkdownRenderer({ content }: MarkdownRendererProps) { + const [htmlContent, setHtmlContent] = useState(''); + + useEffect(() => { + async function processMarkdown() { + const processedContent = await unified() + .use(remarkParse) + .use(remarkHtml) + .process(content); + setHtmlContent(processedContent.toString()); + } + processMarkdown(); + }, [content]); + + return
; +} \ No newline at end of file diff --git a/web/src/components/PodcastCard.tsx b/web/src/components/PodcastCard.tsx index 12effe8..06bb017 100644 --- a/web/src/components/PodcastCard.tsx +++ b/web/src/components/PodcastCard.tsx @@ -13,6 +13,7 @@ interface PodcastCardProps { variant?: 'default' | 'compact'; currentPodcast?: PodcastItem | null; isPlaying?: boolean; + onTitleClick?: (podcast: PodcastItem) => void; // 新增 onTitleClick 回调 } const PodcastCard: React.FC = ({ @@ -22,6 +23,7 @@ const PodcastCard: React.FC = ({ variant = 'default', currentPodcast, isPlaying, + onTitleClick, // 解构 onTitleClick }) => { const [isLiked, setIsLiked] = useState(false); const [isHovered, setIsHovered] = useState(false); @@ -44,6 +46,10 @@ const PodcastCard: React.FC = ({ // 更多操作菜单 }; + const handleTitleClick = () => { + onTitleClick?.(podcast); // 调用传入的 onTitleClick 回调 + }; + // 根据变体返回不同的布局 if (variant === 'compact') { return ( @@ -88,7 +94,10 @@ const PodcastCard: React.FC = ({ {/* 内容 */}
-

+

{podcast.title}

@@ -97,7 +106,7 @@ const PodcastCard: React.FC = ({

- {formatTime(podcast.duration)} + {podcast.audio_duration} {/* @@ -190,15 +199,18 @@ const PodcastCard: React.FC = ({ {/* 时长标签 */}
- {formatTime(podcast.duration)} + {podcast.audio_duration}
{/* 内容区域 */}
{/* 标题 */} -

+

{podcast.title}

diff --git a/web/src/components/PodcastContent.tsx b/web/src/components/PodcastContent.tsx new file mode 100644 index 0000000..edc21c3 --- /dev/null +++ b/web/src/components/PodcastContent.tsx @@ -0,0 +1,123 @@ +import { ArrowLeft } from 'lucide-react'; +import { getAudioInfo, getUserInfo } from '@/lib/podcastApi'; +import AudioPlayerControls from './AudioPlayerControls'; +import PodcastTabs from './PodcastTabs'; +import ShareButton from './ShareButton'; // 导入 ShareButton 组件 + +// 脚本解析函数 (与 page.tsx 中保持一致) +const parseTranscript = ( + transcript: { speaker_id: number; dialog: string }[] | undefined, + podUsers: { role: string; code: string; name: string; usedname: string }[] | undefined +) => { + if (!transcript) return []; + + return transcript.map((item, index) => { + let speakerName: string | null = null; + if (podUsers && podUsers[item.speaker_id]) { + speakerName = podUsers[item.speaker_id].usedname; // 使用 podUsers 中的 usedname 字段作为 speakerName + } else { + speakerName = `Speaker ${item.speaker_id}`; // 回退到 Speaker ID + } + return { id: index, speaker: speakerName, dialogue: item.dialog }; + }); +}; + +interface PodcastContentProps { + fileName: string; +} + + +export default async function PodcastContent({ fileName }: PodcastContentProps) { + const result = await getAudioInfo(fileName); + + if (!result.success || !result.data || result.data.status!='completed') { + return ( +
+

无法加载播客详情:{result.error || '未知错误'}

+ + 返回首页 + +
+ ); + } + + const authId = result.data?.auth_id; // 确保 auth_id 存在且安全访问 + let userInfoData = null; + if (authId) { + const userInfo = await getUserInfo(authId); + if (userInfo.success && userInfo.data) { + userInfoData = { + name: userInfo.data.name, + email: userInfo.data.email, + image: userInfo.data.image, + }; + } + } + const responseData = { + ...result.data, + user: userInfoData // 将用户信息作为嵌套对象添加 + }; + + const audioInfo = responseData; + const parsedScript = parseTranscript(audioInfo.podcast_script?.podcast_transcripts || [], audioInfo.podUsers); + + return ( +
+ {/* 返回首页按钮和分享按钮 */} +
{/* 修改为 justify-between 和 items-center */} + + + 返回首页 + + {/* 添加分享按钮 */} +
+ {/* 1. 顶部信息区 */} +
+ {/* 缩略图 */} +
+ {audioInfo.avatar_base64 && ( + Podcast Thumbnail + )} +
+ + {/* 标题 */} +

+ {audioInfo.title} +

+ + {/* 元数据栏 */} +
+
+
+ {audioInfo.user?.name +
+ {audioInfo.user?.name} +
+
+
+ + {/* 2. 播放控制区 - 使用客户端组件 */} + + + {/* 3. 内容导航区和内容展示区 - 使用客户端组件 */} + +
+ ); +} \ No newline at end of file diff --git a/web/src/components/PodcastTabs.tsx b/web/src/components/PodcastTabs.tsx new file mode 100644 index 0000000..c14ea76 --- /dev/null +++ b/web/src/components/PodcastTabs.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useState } from 'react'; +import MarkdownRenderer from './MarkdownRenderer'; + +interface PodcastTabsProps { + parsedScript: { id: number; speaker: string | null; dialogue: string }[]; + overviewContent?: string; +} + +export default function PodcastTabs({ parsedScript, overviewContent }: PodcastTabsProps) { + const [activeTab, setActiveTab] = useState<'script' | 'overview'>('script'); + + return ( + <> + {/* 3. 内容导航区 */} +
+
+
+ {/* 脚本 */} + + {/* 大纲 */} + +
+
+
+ + {/* 4. 内容展示区 */} +
+
+ {activeTab === 'script' ? ( + parsedScript.map(({ id, speaker, dialogue }) => ( +

+ {speaker && ( + {speaker}: + )} + {dialogue} +

+ )) + ) : ( + // 大纲内容 + overviewContent ? ( + + ) : ( +

暂无大纲内容。

+ ) + )} +
+
+ + ); +} \ No newline at end of file diff --git a/web/src/components/PointsOverview.tsx b/web/src/components/PointsOverview.tsx index 4ee349e..09a837a 100644 --- a/web/src/components/PointsOverview.tsx +++ b/web/src/components/PointsOverview.tsx @@ -29,14 +29,14 @@ const PointsOverview: React.FC = ({
{/* Upper Section: Total Points and User Info */}
-
+
User Avatar
-

{user.name}

+

{user.name}

{user.email}

@@ -47,17 +47,17 @@ const PointsOverview: React.FC = ({
{/* Small text for mobile view only */} -

+

仅显示最近20条积分明细。

{/* Lower Section: Point Details */} -
-

+
+

积分明细

{pointHistory.length === 0 ? ( -

暂无积分明细。

+

暂无积分明细。

) : (
    {pointHistory @@ -65,14 +65,14 @@ const PointsOverview: React.FC = ({ .map((entry) => (
  • -

    +

    {entry.description}

    -

    +

    {new Date(entry.createdAt).toLocaleString()}

    -
    0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' }`}> {entry.pointsChange > 0 ? '+' : ''} {entry.pointsChange} diff --git a/web/src/components/ShareButton.tsx b/web/src/components/ShareButton.tsx new file mode 100644 index 0000000..617c0fb --- /dev/null +++ b/web/src/components/ShareButton.tsx @@ -0,0 +1,41 @@ +'use client'; + +import React from 'react'; +import { Share2 } from 'lucide-react'; +import { useToast } from './Toast'; // 确保路径正确 +import { usePathname } from 'next/navigation'; // next/navigation 用于获取当前路径 + +interface ShareButtonProps { + className?: string; // 允许外部传入样式 +} + +const ShareButton: React.FC = ({ className }) => { + const { success, error } = useToast(); + const pathname = usePathname(); // 获取当前路由路径 + + const handleShare = async () => { + console.log('handleShare clicked'); // 添加点击日志 + try { + const currentUrl = window.location.origin + pathname; // 构建完整的当前页面 URL + await navigator.clipboard.writeText(currentUrl); + success('复制成功', '页面链接已复制到剪贴板!'); + console.log('页面链接已复制:', currentUrl); // 添加成功日志 + } catch (err) { + console.error('复制失败:', err); // 保留原有错误日志 + error('复制失败', '无法复制页面链接到剪贴板。'); + console.error('无法复制页面链接到剪贴板,错误信息:', err); // 添加详细错误日志 + } + }; + + return ( + + ); +}; + +export default ShareButton; \ No newline at end of file diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index 00fedb9..b52618c 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -58,6 +58,7 @@ const Sidebar: React.FC = ({ const router = useRouter(); // 初始化 useRouter 钩子 useEffect(() => { + // 首次加载时获取 session if (!didFetch.current) { didFetch.current = true; // 标记为已执行,避免在开发模式下重复执行 const fetchSession = async () => { @@ -67,9 +68,8 @@ const Sidebar: React.FC = ({ }; fetchSession(); } - }, []); // 空依赖数组表示只在组件挂载时执行一次 - - useEffect(() => { + + // 检查 session 是否过期 if (session?.expiresAt) { const expirationTime = session.expiresAt.getTime(); const currentTime = new Date().getTime(); @@ -87,7 +87,7 @@ const Sidebar: React.FC = ({ }); } } - }, [session, router]); // 监听 session 变化和 router(因为 signOut 中使用了 router.push) + }, [session, router, onCreditsChange]); // 监听 session 变化和 router(因为 signOut 中使用了 router.push),并添加 onCreditsChange const mainNavItems: NavItem[] = [ { id: 'home', label: '首页', icon: Home }, diff --git a/web/src/lib/podcastApi.ts b/web/src/lib/podcastApi.ts index 853946e..aee50d0 100644 --- a/web/src/lib/podcastApi.ts +++ b/web/src/lib/podcastApi.ts @@ -1,5 +1,8 @@ import { HttpError } from '@/types'; import type { PodcastGenerationRequest, PodcastGenerationResponse, ApiResponse, PodcastStatusResponse } from '@/types'; +import { db } from "@/lib/database"; +import * as schema from "../../drizzle-schema"; +import { eq } from "drizzle-orm"; const API_BASE_URL = process.env.NEXT_PUBLIC_PODCAST_API_BASE_URL || 'http://192.168.1.232:8000'; @@ -65,4 +68,58 @@ export async function getPodcastStatus(userId: string): Promise> 返回用户信息或null + */ +export async function getUserInfo(userId: string): Promise> { + try { + const userInfo = await db + .select() + .from(schema.user) + .where(eq(schema.user.id, userId)) + .limit(1); + + if (userInfo.length > 0) { + return { success: true, data: userInfo[0] }; + } else { + return { success: true, data: null }; // 用户不存在 + } + } catch (error: any) { + console.error(`获取用户 ${userId} 信息失败:`, error); + return { success: false, error: error.message || '获取用户信息失败', statusCode: 500 }; + } } \ No newline at end of file diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 921a7a6..daf8295 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -16,10 +16,10 @@ export interface PodcastGenerationResponse { id?: string; // 任务ID status: 'pending' | 'running' | 'completed' | 'failed' ; task_id?: string; - podUsers?: Array<{ role: string; code: string; }>; + podUsers?: Array<{ role: string; code: string; name: string; usedname: string; }>; output_audio_filepath?: string; overview_content?: string; - podcast_script?: { podcast_transcripts: Array<{ speaker_id: number; dialog: string; }>; }; + podcast_script?: { podcast_transcripts: Array<{ speaker_id: number; dialog: string; }>; }; // Changed from string to Array avatar_base64?: string; audio_duration?: string; title?: string; @@ -29,6 +29,14 @@ export interface PodcastGenerationResponse { audioUrl?: string; estimatedTime?: number; // 新增预估时间 progress?: number; // 新增进度百分比 + auth_id?: string; // 新增 auth_id + callback_url?: string; // 新增 callback_url + user?: { + name: string; + email: string; + image: string; + }; + listens?: number; } export interface PodcastScript { @@ -116,12 +124,13 @@ export interface PodcastItem { name: string; avatar: string; }; - duration: number; + audio_duration: string; playCount: number; createdAt: string; audioUrl: string; tags: string[]; status: 'pending' | 'running' | 'completed' | 'failed'; // 添加status属性 + file_name: string; } // 设置表单数据类型 - 从 SettingsForm.tsx 复制过来并导出 diff --git a/web/start.bat b/web/start.bat index ef649b4..529f20a 100644 --- a/web/start.bat +++ b/web/start.bat @@ -21,8 +21,8 @@ if %errorlevel% neq 0 ( echo. echo 3. 启动开发服务器... -echo 应用将在 http://localhost:3000 启动 +echo 应用将在 http://localhost:3001 启动 echo 按 Ctrl+C 停止服务器 echo. -npm run dev \ No newline at end of file +set PORT=3001 && npm run dev \ No newline at end of file