botrun-horse 2.29.2 → 2.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/botrun-c/.claude/skills/DOCX/351/226/261/350/256/200/346/212/200/350/203/275/SKILL.md +103 -0
- package/botrun-c/.claude/skills/DOCX/351/226/261/350/256/200/346/212/200/350/203/275/scripts/docx-to-markdown.mjs +206 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/SKILL.md +1093 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-branch.mjs +73 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-branches.mjs +77 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-checkout.mjs +72 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-clone.mjs +72 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-commit.mjs +75 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-delete-branch.mjs +75 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-delete-file.mjs +72 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-diff.mjs +80 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-list-tree.mjs +336 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-list.mjs +199 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-merge.mjs +86 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-move.mjs +75 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-pr-create.mjs +81 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-pr-list.mjs +74 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-pr-merge.mjs +83 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-pr-view.mjs +71 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-push.mjs +71 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-read.mjs +277 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-search.mjs +370 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-stash.mjs +116 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-sync.mjs +71 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-write.mjs +74 -0
- package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/lib/path-validator.mjs +167 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/SKILL.md +605 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-create.mjs +127 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-delete.mjs +77 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-diff.mjs +87 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-history.mjs +99 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-list.mjs +174 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-read.mjs +214 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-restore.mjs +75 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-search.mjs +270 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-sync-back.mjs +155 -0
- package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-update.mjs +100 -0
- package/botrun-c/.claude/skills/HTML/347/224/237/346/210/220/346/212/200/350/203/275/SKILL.md +414 -0
- package/botrun-c/.claude/skills/HTML/347/224/237/346/210/220/346/212/200/350/203/275/scripts/create-project.mjs +91 -0
- package/botrun-c/.claude/skills/HTML/347/224/237/346/210/220/346/212/200/350/203/275/scripts/finalize-project.mjs +162 -0
- package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/SKILL.md +206 -0
- package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/package.json +19 -0
- package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/pdf-analyze.mjs +309 -0
- package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/pdf-compress.mjs +315 -0
- package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/pdf-split.mjs +275 -0
- package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/pdf-to-text.mjs +336 -0
- package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/test-pdf-scripts.mjs +491 -0
- package/botrun-c/.claude/skills/docx-to-markdown/SKILL.md +76 -0
- package/botrun-c/.claude/skills/docx-to-markdown/scripts/convert_docx.py +183 -0
- package/botrun-c/.claude/skills/gcp-coord-ml-ocr/SKILL.md +133 -0
- package/botrun-c/.claude/skills/gcp-coord-ml-ocr/scripts/ocr_processor.py +381 -0
- package/botrun-c/.claude/skills/gemini-transcribe/SKILL.md +115 -0
- package/botrun-c/.claude/skills/gemini-transcribe/scripts/transcribe.py +499 -0
- package/botrun-c/.claude/skills/nchc-transcribe/SKILL.md +131 -0
- package/botrun-c/.claude/skills/nchc-transcribe/scripts/transcribe.py +522 -0
- package/botrun-c/.claude/skills/p320-moj-review/SKILL.md +200 -0
- package/botrun-c/.claude/skills/p320-moj-review/references/guideline.md +118 -0
- package/botrun-c/.claude/skills/pdf-multimodal-processor/SKILL.md +186 -0
- package/botrun-c/.claude/skills/pdf-multimodal-processor/scripts/process_pdf.py +515 -0
- package/botrun-c/.claude/skills/ripgrep/346/220/234/345/260/213/346/212/200/350/203/275/SKILL.md +81 -0
- package/botrun-c/.claude/skills/ripgrep/346/220/234/345/260/213/346/212/200/350/203/275/scripts/package.json +13 -0
- package/botrun-c/.claude/skills/ripgrep/346/220/234/345/260/213/346/212/200/350/203/275/scripts/secure-search.mjs +232 -0
- package/botrun-c/.claude/skills/top100/351/240/230/350/242/226/351/240/220/346/270/254/346/212/200/350/203/275/SKILL.md +518 -0
- package/botrun-c/.claude/skills//345/216/273/350/255/230/345/210/245/346/212/200/350/203/275/SKILL.md +125 -0
- package/botrun-c/.claude/skills//345/233/236/346/206/266/346/212/200/350/203/275/SKILL.md +123 -0
- package/botrun-c/.claude/skills//345/233/236/346/206/266/346/212/200/350/203/275/scripts/recall.mjs +339 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/SKILL.md +156 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/scripts/__tests__/utils.test.mjs +139 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/scripts/constants.mjs +40 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/scripts/gcs-uploader.mjs +195 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/scripts/gemini-image-client.mjs +307 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/scripts/generate-image.mjs +103 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/scripts/image-session-manager.mjs +219 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/scripts/output-formatter.mjs +209 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/scripts/package.json +20 -0
- package/botrun-c/.claude/skills//345/234/226/347/211/207/347/224/237/346/210/220/346/212/200/350/203/275/scripts/utils.mjs +115 -0
- package/botrun-c/.claude/skills//345/244/232/346/250/241/346/205/213/351/226/261/350/256/200/346/212/200/350/203/275/SKILL.md +86 -0
- package/botrun-c/.claude/skills//345/244/232/346/250/241/346/205/213/351/226/261/350/256/200/346/212/200/350/203/275/scripts/multimodal-read.mjs +304 -0
- package/botrun-c/.claude/skills//345/244/232/346/250/241/346/205/213/351/226/261/350/256/200/346/212/200/350/203/275/scripts/package.json +17 -0
- package/botrun-c/.claude/skills//345/255/265/345/214/226/346/212/200/350/203/275/SKILL.md +131 -0
- package/botrun-c/.claude/skills//345/255/265/345/214/226/346/212/200/350/203/275/scripts/skill-manager.mjs +542 -0
- package/botrun-c/.claude/skills//346/234/203/350/255/260/350/250/230/351/214/204/346/212/200/350/203/275/SKILL.md +127 -0
- package/botrun-c/.claude/skills//347/233/256/351/214/204/345/210/227/350/241/250/346/212/200/350/203/275/SKILL.md +53 -0
- package/botrun-c/.claude/skills//347/233/256/351/214/204/345/210/227/350/241/250/346/212/200/350/203/275/scripts/package.json +13 -0
- package/botrun-c/.claude/skills//347/233/256/351/214/204/345/210/227/350/241/250/346/212/200/350/203/275/scripts/tree-view.mjs +149 -0
- package/botrun-c/.claude/skills//347/264/224/346/226/207/345/255/227/345/244/232/346/252/224/346/241/210/350/256/200/345/217/226/346/212/200/350/203/275/SKILL.md +82 -0
- package/botrun-c/.claude/skills//347/264/224/346/226/207/345/255/227/345/244/232/346/252/224/346/241/210/350/256/200/345/217/226/346/212/200/350/203/275/scripts/package.json +16 -0
- package/botrun-c/.claude/skills//347/264/224/346/226/207/345/255/227/345/244/232/346/252/224/346/241/210/350/256/200/345/217/226/346/212/200/350/203/275/scripts/secure-read.mjs +260 -0
- package/botrun-c/.claude/skills//347/266/262/350/267/257/346/220/234/345/260/213/346/212/200/350/203/275/SKILL.md +156 -0
- package/botrun-c/.claude/skills//347/266/262/350/267/257/346/220/234/345/260/213/346/212/200/350/203/275/scripts/package.json +16 -0
- package/botrun-c/.claude/skills//347/266/262/350/267/257/346/220/234/345/260/213/346/212/200/350/203/275/scripts/search.js +139 -0
- package/botrun-c/.claude/skills//347/266/262/350/267/257/346/220/234/345/260/213/346/212/200/350/203/275/scripts/search.mjs +272 -0
- package/botrun-c/.claude/skills//347/266/262/350/267/257/346/220/234/345/260/213/346/212/200/350/203/275/scripts/search.ts +166 -0
- package/botrun-c/.claude/skills//350/250/230/346/206/266/346/212/200/350/203/275/SKILL.md +114 -0
- package/botrun-c/.claude/skills//350/250/230/346/206/266/346/212/200/350/203/275/scripts/remember.mjs +159 -0
- package/botrun-c/.claude/skills//350/250/230/346/206/266/346/212/200/350/203/275/scripts/test-advanced.mjs +342 -0
- package/botrun-c/.claude/skills//350/250/230/346/206/266/346/212/200/350/203/275/scripts/test.mjs +430 -0
- package/botrun-c/.claude/skills//350/252/236/351/237/263/350/275/211/346/226/207/345/255/227/346/212/200/350/203/275/SKILL.md +30 -0
- package/botrun-c/.claude/skills//350/252/236/351/237/263/350/275/211/346/226/207/345/255/227/346/212/200/350/203/275/scripts/package.json +13 -0
- package/botrun-c/.claude/skills//350/252/236/351/237/263/350/275/211/346/226/207/345/255/227/346/212/200/350/203/275/scripts/transcribe.mjs +294 -0
- package/botrun-c/.claude/skills//351/233/266/345/271/273/350/246/272/350/255/211/346/230/216/346/212/200/350/203/275/SKILL.md +338 -0
- package/botrun-c/.dockerignore +131 -0
- package/botrun-c/.dockeroptimize +13 -0
- package/botrun-c/.firebase/hosting.b3V0.cache +30 -0
- package/botrun-c/.firebaserc +5 -0
- package/botrun-c/.gcloudignore +56 -0
- package/botrun-c/.ruby-version +1 -0
- package/botrun-c/CHANGELOG.md +224 -0
- package/botrun-c/Dockerfile.gcp +365 -0
- package/botrun-c/Dockerfile.slim +195 -0
- package/botrun-c/Gemfile +8 -0
- package/botrun-c/Gemfile.lock +233 -0
- package/botrun-c/LOCAL-DEVELOPMENT-GUIDE.md +393 -0
- package/botrun-c/PLAN.md +131 -0
- package/botrun-c/README.md +129 -0
- package/botrun-c/TODO-CAPACITOR-INTEGRATION.md +482 -0
- package/botrun-c/VERIFICATION.md +121 -0
- package/botrun-c/admin/RATE_LIMIT_MANAGEMENT.md +312 -0
- package/botrun-c/admin/README.md +338 -0
- package/botrun-c/admin/botrun-billing-report-1765235244038.csv +22 -0
- package/botrun-c/admin/botrun-billing-report-1765235323089.csv +22 -0
- package/botrun-c/admin/botrun-dashboard-1765235244039.html +278 -0
- package/botrun-c/admin/botrun-dashboard-1765235323089.html +278 -0
- package/botrun-c/admin/botrun-timeseries-074xGzIKWyfKTBt1NMIj9lxi5mO2-1765235244039.html +161 -0
- package/botrun-c/admin/botrun-timeseries-074xGzIKWyfKTBt1NMIj9lxi5mO2-1765235323090.html +161 -0
- package/botrun-c/admin/check-billing.ts +48 -0
- package/botrun-c/admin/count-all-users.ts +65 -0
- package/botrun-c/admin/generate-analytics-report.ts +188 -0
- package/botrun-c/admin/jest.config.ts +43 -0
- package/botrun-c/admin/lib/__tests__/formatters.test.ts +264 -0
- package/botrun-c/admin/lib/__tests__/mocks/firebase.mock.ts +112 -0
- package/botrun-c/admin/lib/__tests__/services/UserService.test.ts +83 -0
- package/botrun-c/admin/lib/__tests__/user-resolver.test.ts +439 -0
- package/botrun-c/admin/lib/analyzers/BillingAnalyticsService.ts +242 -0
- package/botrun-c/admin/lib/analyzers/CsvExporter.ts +263 -0
- package/botrun-c/admin/lib/analyzers/HtmlChartGenerator.ts +530 -0
- package/botrun-c/admin/lib/analyzers/SpikeAnalyzer.ts +608 -0
- package/botrun-c/admin/lib/analyzers/TimeSeriesAnalyzer.ts +344 -0
- package/botrun-c/admin/lib/cli/CLIBootstrapper.ts +40 -0
- package/botrun-c/admin/lib/commands/AbstractCommand.ts +69 -0
- package/botrun-c/admin/lib/commands/AnalyticsCommand.ts +215 -0
- package/botrun-c/admin/lib/commands/ListCommand.ts +85 -0
- package/botrun-c/admin/lib/commands/QueryCommand.ts +81 -0
- package/botrun-c/admin/lib/commands/SpikeCommand.ts +83 -0
- package/botrun-c/admin/lib/domain/User.ts +38 -0
- package/botrun-c/admin/lib/firebase.ts +92 -0
- package/botrun-c/admin/lib/firestore-safe.ts +105 -0
- package/botrun-c/admin/lib/formatters.ts +154 -0
- package/botrun-c/admin/lib/rate-limit-detailed.ts +355 -0
- package/botrun-c/admin/lib/rate-limit.ts +286 -0
- package/botrun-c/admin/lib/repositories/UserRepository.ts +104 -0
- package/botrun-c/admin/lib/services/UserService.ts +108 -0
- package/botrun-c/admin/lib/types.ts +56 -0
- package/botrun-c/admin/lib/user-resolver.ts +275 -0
- package/botrun-c/admin/manage-auth-domains.js +178 -0
- package/botrun-c/admin/migrate-plan-free.sh +64 -0
- package/botrun-c/admin/package-lock.json +5656 -0
- package/botrun-c/admin/package.json +28 -0
- package/botrun-c/admin/rate-limit.sh +55 -0
- package/botrun-c/admin/remove-custom-limits.ts +75 -0
- package/botrun-c/admin/reset-rate.sh +70 -0
- package/botrun-c/admin/reset-trial.sh +68 -0
- package/botrun-c/admin/seed-trial-data.sh +38 -0
- package/botrun-c/admin/trial.sh +55 -0
- package/botrun-c/admin/tsconfig.json +20 -0
- package/botrun-c/admin/users-cli.py +298 -0
- package/botrun-c/admin/users.sh +12 -0
- package/botrun-c/admin/users.ts +1321 -0
- package/botrun-c/api/admin-tools/check-whitelist.mjs.migration-bak +74 -0
- package/botrun-c/api/admin-tools/create-user.mjs +92 -0
- package/botrun-c/api/admin-tools/create-user.mjs.migration-bak +92 -0
- package/botrun-c/api/admin-tools/firebase-auth-domains-v2.py +287 -0
- package/botrun-c/api/admin-tools/firebase-auth-domains-v2.py.migration-bak +287 -0
- package/botrun-c/api/admin-tools/init-firestore.mjs +75 -0
- package/botrun-c/api/admin-tools/init-firestore.mjs.migration-bak +101 -0
- package/botrun-c/api/admin-tools/list-users.mjs +43 -0
- package/botrun-c/api/admin-tools/list-users.mjs.migration-bak +43 -0
- package/botrun-c/api/admin-tools/manage-authorized-domains.ts +255 -0
- package/botrun-c/api/admin-tools/manage-authorized-domains.ts.migration-bak +255 -0
- package/botrun-c/api/admin-tools/setup-custom-claims.ts.migration-bak +155 -0
- package/botrun-c/api/admin-tools/test-claims.mjs +69 -0
- package/botrun-c/api/admin-tools/test-claims.mjs.migration-bak +69 -0
- package/botrun-c/api/admin-tools/update-service-urls-gcloud.mjs +72 -0
- package/botrun-c/api/admin-tools/update-service-urls-gcloud.mjs.migration-bak +72 -0
- package/botrun-c/api/admin-tools/update-service-urls-to-lb.mjs +159 -0
- package/botrun-c/api/admin-tools/update-service-urls-to-lb.mjs.migration-bak +159 -0
- package/botrun-c/api/admin-tools/update-service-urls.mjs +82 -0
- package/botrun-c/api/admin-tools/update-service-urls.mjs.migration-bak +82 -0
- package/botrun-c/api/package-lock.json +5031 -0
- package/botrun-c/api/package.json +63 -0
- package/botrun-c/api/pnpm-lock.yaml +4157 -0
- package/botrun-c/api/rate-limit-export-1763620678737.csv +79 -0
- package/botrun-c/api/scripts/README-migrate-plan-free.md +197 -0
- package/botrun-c/api/scripts/check-users.ts +45 -0
- package/botrun-c/api/scripts/delete-firestore-docs.js +43 -0
- package/botrun-c/api/scripts/dump-subscriptions.ts +395 -0
- package/botrun-c/api/scripts/list-all-users.ts +65 -0
- package/botrun-c/api/scripts/migrate-plan-free-rpd-16to32.ts +262 -0
- package/botrun-c/api/scripts/migrate-plan-names-to-lite-max.ts +269 -0
- package/botrun-c/api/scripts/query-user-info.ts +304 -0
- package/botrun-c/api/scripts/reset-rate-limit.ts +206 -0
- package/botrun-c/api/scripts/reset-trial-subscription.ts +166 -0
- package/botrun-c/api/scripts/rollback-plan-names-from-lite-max.ts +139 -0
- package/botrun-c/api/scripts/seed-rate-limit-test-data.ts +114 -0
- package/botrun-c/api/scripts/seed-subscription-test-data.ts +173 -0
- package/botrun-c/api/scripts/test-rate-limit-message.ts +108 -0
- package/botrun-c/api/test-billing-write.ts +103 -0
- package/botrun-c/api/test-billing.ts +81 -0
- package/botrun-c/api/test-chat-upload.sh +162 -0
- package/botrun-c/api/test-nchc-whisper.ts +87 -0
- package/botrun-c/api/test-skills-loading.js +77 -0
- package/botrun-c/api/test-skills-symlink.js +118 -0
- package/botrun-c/api/test-upload-multimodal.sh +167 -0
- package/botrun-c/api/tsconfig.json +27 -0
- package/botrun-c/api/verify-form-2-1.mjs +229 -0
- package/botrun-c/api/vitest.config.ts +9 -0
- package/botrun-c/appium-capabilities.json +20 -0
- package/botrun-c/cors.json +26 -0
- package/botrun-c/delete-old.sh +44 -0
- package/botrun-c/design/google-drive/01-architecture.md +186 -0
- package/botrun-c/design/google-drive/02-api-design.md +314 -0
- package/botrun-c/design/google-drive/03-frontend-design.md +278 -0
- package/botrun-c/design/google-drive/04-skill-design.md +384 -0
- package/botrun-c/design/google-drive/05-test-plan.md +507 -0
- package/botrun-c/design/google-drive/README.md +32 -0
- package/botrun-c/dev/mcp-token-calculator/README.md +92 -0
- package/botrun-c/dev/mcp-token-calculator/RESULTS.md +188 -0
- package/botrun-c/dev/mcp-token-calculator/calculate-current-tokens.ts +369 -0
- package/botrun-c/dev/mcp-token-calculator/calculate-mcp-tokens.ts +464 -0
- package/botrun-c/dev/mcp-token-calculator/optimization-proposals.ts +316 -0
- package/botrun-c/dev/mcp-token-calculator/package-lock.json +599 -0
- package/botrun-c/dev/mcp-token-calculator/package.json +22 -0
- package/botrun-c/dev/mcp-token-calculator/test-all-schemas.ts +314 -0
- package/botrun-c/dev/mcp-token-calculator/test-schema-correctness.ts +221 -0
- package/botrun-c/dev/mcp-token-calculator/verify-optimization.ts +159 -0
- package/botrun-c/fastlane/ANDROID-DEPLOYMENT-SETUP.md +825 -0
- package/botrun-c/fastlane/IOS-DEPLOYMENT-SETUP.md +431 -0
- package/botrun-c/fastlane/Pluginfile +5 -0
- package/botrun-c/fastlane/QUICK-START.md +133 -0
- package/botrun-c/fastlane/README.md +424 -0
- package/botrun-c/fastlane/RUBY-SETUP.md +449 -0
- package/botrun-c/fastlane/android/Appfile +11 -0
- package/botrun-c/fastlane/android/Fastfile +240 -0
- package/botrun-c/firebase.json +46 -0
- package/botrun-c/firestore-init-data.json +40 -0
- package/botrun-c/firestore.indexes.json +55 -0
- package/botrun-c/firestore.rules +53 -0
- package/botrun-c/fix-codex-permissions.sh +56 -0
- package/botrun-c/gcs-lifecycle-30day-retention.json +12 -0
- package/botrun-c/html-architecture/architecture/deployment.html +664 -0
- package/botrun-c/html-architecture/architecture/index.html +309 -0
- package/botrun-c/html-architecture/architecture/styles.css +500 -0
- package/botrun-c/html-architecture/architecture/system-components.html +538 -0
- package/botrun-c/html-architecture/architecture/user-flow.html +370 -0
- package/botrun-c/mobile-android/FIREBASE-AUTH-SETUP.md +302 -0
- package/botrun-c/mobile-android/WSL-ANDROID-SETUP.md +252 -0
- package/botrun-c/mobile-android/android/app/build.gradle +75 -0
- package/botrun-c/mobile-android/android/app/capacitor.build.gradle +31 -0
- package/botrun-c/mobile-android/android/app/proguard-rules.pro +21 -0
- package/botrun-c/mobile-android/android/build.gradle +29 -0
- package/botrun-c/mobile-android/android/capacitor.settings.gradle +42 -0
- package/botrun-c/mobile-android/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/botrun-c/mobile-android/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/botrun-c/mobile-android/android/gradle.properties +22 -0
- package/botrun-c/mobile-android/android/gradlew +252 -0
- package/botrun-c/mobile-android/android/gradlew.bat +94 -0
- package/botrun-c/mobile-android/android/settings.gradle +5 -0
- package/botrun-c/mobile-android/android/variables.gradle +16 -0
- package/botrun-c/mobile-android/capacitor.config.ts +44 -0
- package/botrun-c/mobile-android/fastlane/Appfile +11 -0
- package/botrun-c/mobile-android/fastlane/Fastfile +202 -0
- package/botrun-c/mobile-android/fastlane/README.md +96 -0
- package/botrun-c/mobile-android/google-services.json.template +39 -0
- package/botrun-c/mobile-android/package.json +32 -0
- package/botrun-c/mobile-android/resources/icon.png +0 -0
- package/botrun-c/mobile-android/tsconfig.json +15 -0
- package/botrun-c/mobile-ios/.ruby-version +1 -0
- package/botrun-c/mobile-ios/BUILD-LOG.md +82 -0
- package/botrun-c/mobile-ios/Gemfile +4 -0
- package/botrun-c/mobile-ios/Gemfile.lock +299 -0
- package/botrun-c/mobile-ios/GoogleService-Info.plist.template +34 -0
- package/botrun-c/mobile-ios/IOS-PERMISSIONS.md +80 -0
- package/botrun-c/mobile-ios/QUICK-START.md +189 -0
- package/botrun-c/mobile-ios/README.md +231 -0
- package/botrun-c/mobile-ios/capacitor.config.ts +46 -0
- package/botrun-c/mobile-ios/fastlane/Fastfile +326 -0
- package/botrun-c/mobile-ios/fastlane/README.md +88 -0
- package/botrun-c/mobile-ios/ios/App/App/App.entitlements +10 -0
- package/botrun-c/mobile-ios/ios/App/App/AppDelegate.swift +49 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Contents.json +6 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json +56 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany-dark.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany-dark.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany-dark.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png +0 -0
- package/botrun-c/mobile-ios/ios/App/App/Base.lproj/LaunchScreen.storyboard +32 -0
- package/botrun-c/mobile-ios/ios/App/App/Base.lproj/Main.storyboard +19 -0
- package/botrun-c/mobile-ios/ios/App/App/Info.plist +72 -0
- package/botrun-c/mobile-ios/ios/App/App.xcodeproj/project.pbxproj +446 -0
- package/botrun-c/mobile-ios/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +78 -0
- package/botrun-c/mobile-ios/ios/App/App.xcworkspace/contents.xcworkspacedata +10 -0
- package/botrun-c/mobile-ios/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/botrun-c/mobile-ios/ios/App/Podfile +52 -0
- package/botrun-c/mobile-ios/ios/App/Podfile.lock +205 -0
- package/botrun-c/mobile-ios/ios/App/add_plist.rb +30 -0
- package/botrun-c/mobile-ios/ios/App/fix_plist_path.rb +22 -0
- package/botrun-c/mobile-ios/package.json +29 -0
- package/botrun-c/mobile-ios/resources/icon.png +0 -0
- package/botrun-c/mobile-ios/run-simulator.sh +34 -0
- package/botrun-c/mobile-ios/screenshot-after-clean-rebuild.png +0 -0
- package/botrun-c/mobile-ios/screenshot-final-version.png +0 -0
- package/botrun-c/mobile-ios/screenshot-simulator.png +0 -0
- package/botrun-c/mobile-ios/screenshot-version-1009.png +0 -0
- package/botrun-c/mobile-ios/screenshot-with-version.png +0 -0
- package/botrun-c/mobile-ios/screenshot-working.png +0 -0
- package/botrun-c/mobile-ios/setup-ios.sh +106 -0
- package/botrun-c/mobile-ios/tsconfig.json +16 -0
- package/botrun-c/org-policy-allow-all-users.yaml +3 -0
- package/botrun-c/package.json +68 -0
- package/botrun-c/pnpm-lock.yaml +10198 -0
- package/botrun-c/pnpm-workspace.yaml +6 -0
- package/botrun-c/prompts/BOTRUN-1.md +13 -0
- package/botrun-c/prompts/BOTRUN-2.md +70 -0
- package/botrun-c/prompts/BOTRUN-3.md +139 -0
- package/botrun-c/prototypes/firestore-gax-debug/package-lock.json +1204 -0
- package/botrun-c/prototypes/firestore-gax-debug/package.json +12 -0
- package/botrun-c/prototypes/firestore-gax-debug/pnpm-workspace.yaml +2 -0
- package/botrun-c/prototypes/firestore-gax-debug/test.js +98 -0
- package/botrun-c/prototypes/taiwan-haiku/README.md +93 -0
- package/botrun-c/prototypes/taiwan-haiku/index.ts +139 -0
- package/botrun-c/prototypes/taiwan-haiku/package.json +23 -0
- package/botrun-c/prototypes/taiwan-haiku/pnpm-lock.yaml +730 -0
- package/botrun-c/prototypes/taiwan-haiku/test-vertex-direct.ts +83 -0
- package/botrun-c/prototypes/taiwan-haiku/tsconfig.json +18 -0
- package/botrun-c/prototypes/web-search/FINDINGS-STAGE-1.md +48 -0
- package/botrun-c/prototypes/web-search/README.md +46 -0
- package/botrun-c/prototypes/web-search/VERIFICATION-REPORT.md +310 -0
- package/botrun-c/prototypes/web-search/package-lock.json +595 -0
- package/botrun-c/prototypes/web-search/package.json +18 -0
- package/botrun-c/prototypes/web-search/stage-1.1-basic-connection.js +123 -0
- package/botrun-c/prototypes/web-search/stage-1.2-with-websearch.js +154 -0
- package/botrun-c/prototypes/web-search/stage-1.3-search-query.js +192 -0
- package/botrun-c/prototypes/web-search/stage-2-agent-sdk-websearch.js +265 -0
- package/botrun-c/runtime-scripts/env-detect.sh +64 -0
- package/botrun-c/scripts/github-collaborator-manager.sh +297 -0
- package/botrun-c/scripts/github-collaborator-manager.ts +325 -0
- package/botrun-c/scripts/manage-collaborators.sh +91 -0
- package/botrun-c/scripts/setup-secrets.sh +202 -0
- package/botrun-c/scripts/stress-test-17-users.sh +113 -0
- package/botrun-c/scripts/stress-test-api.sh +119 -0
- package/botrun-c/scripts/stress-test-full-report.sh +385 -0
- package/botrun-c/specs/infra-vm/design.md +0 -0
- package/botrun-c/specs/share-skill/design.md +1146 -0
- package/botrun-c/specs/share-skill/tasks.md +444 -0
- package/botrun-c/stress-test-results/20260121-162630/config.json +7 -0
- package/botrun-c/stress-test-results/20260121-162630/user-1-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162630/user-1-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162630/user-1.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/config.json +7 -0
- package/botrun-c/stress-test-results/20260121-162646/user-1-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-1-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-1.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-10-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-10-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-10.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-11-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-11-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-11.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-12-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-12-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-12.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-13-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-13-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-13.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-14-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-14-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-14.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-15-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-15-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-15.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-16-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-16-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-16.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-17-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-17-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-17.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-2-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-2-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-2.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-3-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-3-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-3.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-4-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-4-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-4.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-5-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-5-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-5.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-6-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-6-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-6.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-7-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-7-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-7.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-8-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-8-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-8.json +2 -0
- package/botrun-c/stress-test-results/20260121-162646/user-9-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-9-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-162646/user-9.json +2 -0
- package/botrun-c/stress-test-results/20260121-163148/REPORT.html +344 -0
- package/botrun-c/stress-test-results/20260121-163148/REPORT.md +93 -0
- package/botrun-c/stress-test-results/20260121-163148/REPORT.pdf +0 -0
- package/botrun-c/stress-test-results/20260121-163148/config.json +7 -0
- package/botrun-c/stress-test-results/20260121-163148/summary.json +26 -0
- package/botrun-c/stress-test-results/20260121-163148/user-1-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-1-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-1.json +206 -0
- package/botrun-c/stress-test-results/20260121-163148/user-10-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-10-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-10.json +110 -0
- package/botrun-c/stress-test-results/20260121-163148/user-11-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-11-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-11.json +104 -0
- package/botrun-c/stress-test-results/20260121-163148/user-12-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-12-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-12.json +98 -0
- package/botrun-c/stress-test-results/20260121-163148/user-13-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-13-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-13.json +68 -0
- package/botrun-c/stress-test-results/20260121-163148/user-14-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-14-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-14.json +116 -0
- package/botrun-c/stress-test-results/20260121-163148/user-15-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-15-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-15.json +131 -0
- package/botrun-c/stress-test-results/20260121-163148/user-16-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-16-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-16.json +110 -0
- package/botrun-c/stress-test-results/20260121-163148/user-17-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-17-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-17.json +116 -0
- package/botrun-c/stress-test-results/20260121-163148/user-2-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-2-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-2.json +98 -0
- package/botrun-c/stress-test-results/20260121-163148/user-3-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-3-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-3.json +62 -0
- package/botrun-c/stress-test-results/20260121-163148/user-4-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-4-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-4.json +113 -0
- package/botrun-c/stress-test-results/20260121-163148/user-5-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-5-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-5.json +83 -0
- package/botrun-c/stress-test-results/20260121-163148/user-6-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-6-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-6.json +125 -0
- package/botrun-c/stress-test-results/20260121-163148/user-7-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-7-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-7.json +62 -0
- package/botrun-c/stress-test-results/20260121-163148/user-8-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-8-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-8.json +41 -0
- package/botrun-c/stress-test-results/20260121-163148/user-9-status.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-9-timing.txt +1 -0
- package/botrun-c/stress-test-results/20260121-163148/user-9.json +53 -0
- package/botrun-c/test-merge.md +1 -0
- package/botrun-c/version.txt +1 -0
- package/botrun-c/web/.version +1 -0
- package/botrun-c/web/ALLOW-PUBLIC-ACCESS.md +57 -0
- package/botrun-c/web/FINAL-SETUP-STEPS.md +71 -0
- package/botrun-c/web/FIREBASE-MANUAL-STEPS.md +41 -0
- package/botrun-c/web/QUICK-SETUP-HELPER.md +79 -0
- package/botrun-c/web/README.md +162 -0
- package/botrun-c/web/__tests__/image-extractor.test.ts +147 -0
- package/botrun-c/web/__tests__/useGitHub.test.ts +245 -0
- package/botrun-c/web/app/favicon.ico +0 -0
- package/botrun-c/web/app/globals.css +215 -0
- package/botrun-c/web/app/image-preview/page.tsx +213 -0
- package/botrun-c/web/app/layout.tsx +46 -0
- package/botrun-c/web/app/page.tsx +45 -0
- package/botrun-c/web/components/CapacitorProvider.tsx +43 -0
- package/botrun-c/web/components/ChatPage.tsx +1736 -0
- package/botrun-c/web/components/ConversationList.tsx +237 -0
- package/botrun-c/web/components/ConversationListItem.tsx +121 -0
- package/botrun-c/web/components/LoginPage.tsx +212 -0
- package/botrun-c/web/components/Providers.tsx +12 -0
- package/botrun-c/web/components/StatusBarProvider.tsx +16 -0
- package/botrun-c/web/components/attachment-preview.tsx +193 -0
- package/botrun-c/web/components/botrun-incubator/BotrunIncubator.tsx +266 -0
- package/botrun-c/web/components/botrun-incubator/index.ts +1 -0
- package/botrun-c/web/components/chat-input.tsx +1251 -0
- package/botrun-c/web/components/chat-message.tsx +598 -0
- package/botrun-c/web/components/drop-zone.tsx +143 -0
- package/botrun-c/web/components/gdrive/gdrive-add-dialog.tsx +254 -0
- package/botrun-c/web/components/gdrive/gdrive-drawer.tsx +236 -0
- package/botrun-c/web/components/gdrive/gdrive-selector.tsx +226 -0
- package/botrun-c/web/components/gdrive/index.ts +7 -0
- package/botrun-c/web/components/generated-image-card.tsx +363 -0
- package/botrun-c/web/components/github/GitHubConnect.tsx +366 -0
- package/botrun-c/web/components/github/index.ts +2 -0
- package/botrun-c/web/components/import-skill-dialog.tsx +208 -0
- package/botrun-c/web/components/scroll-to-bottom-button.tsx +90 -0
- package/botrun-c/web/components/share-skill-dialog.tsx +175 -0
- package/botrun-c/web/components/skills-editor.tsx +443 -0
- package/botrun-c/web/components/skills-manager-drawer.tsx +586 -0
- package/botrun-c/web/components/smart-button-group.tsx +132 -0
- package/botrun-c/web/config/smart-buttons.ts +295 -0
- package/botrun-c/web/config/welcome-messages.ts +29 -0
- package/botrun-c/web/contexts/AuthContext.tsx +220 -0
- package/botrun-c/web/hooks/use-toast.ts +187 -0
- package/botrun-c/web/hooks/useAppStoreUpdate.ts +356 -0
- package/botrun-c/web/hooks/useAppVersion.ts +113 -0
- package/botrun-c/web/hooks/useAttachments.ts +269 -0
- package/botrun-c/web/hooks/useAuthToken.ts +59 -0
- package/botrun-c/web/hooks/useAutoWelcome.ts +89 -0
- package/botrun-c/web/hooks/useCapacitorPlatform.ts +113 -0
- package/botrun-c/web/hooks/useCapawesomeLiveUpdate.ts +149 -0
- package/botrun-c/web/hooks/useClaudeModel.ts +118 -0
- package/botrun-c/web/hooks/useConversation.ts +115 -0
- package/botrun-c/web/hooks/useConversations.ts +235 -0
- package/botrun-c/web/hooks/useGdriveFolders.ts +199 -0
- package/botrun-c/web/hooks/useGdriveSync.ts +107 -0
- package/botrun-c/web/hooks/useGitHub.ts +409 -0
- package/botrun-c/web/hooks/useScrollToBottom.ts +150 -0
- package/botrun-c/web/hooks/useStatusBar.ts +42 -0
- package/botrun-c/web/hooks/useUserPlan.ts +153 -0
- package/botrun-c/web/lib/api-config.ts +162 -0
- package/botrun-c/web/lib/api-url.ts +62 -0
- package/botrun-c/web/lib/api.ts +105 -0
- package/botrun-c/web/lib/attachment-utils.ts +168 -0
- package/botrun-c/web/lib/build-version.ts +11 -0
- package/botrun-c/web/lib/clipboard-utils.ts +199 -0
- package/botrun-c/web/lib/constants.ts +62 -0
- package/botrun-c/web/lib/conversation-storage.ts +324 -0
- package/botrun-c/web/lib/firebase-capacitor.ts +135 -0
- package/botrun-c/web/lib/firebase.ts +137 -0
- package/botrun-c/web/lib/firestore-auth.ts +61 -0
- package/botrun-c/web/lib/firestore-auth.ts.migration-bak +319 -0
- package/botrun-c/web/lib/image-extractor.ts +156 -0
- package/botrun-c/web/lib/path-utils.ts +48 -0
- package/botrun-c/web/lib/rehype-del-to-mark.ts +19 -0
- package/botrun-c/web/lib/upload-service.ts +326 -0
- package/botrun-c/web/lib/utils.ts +6 -0
- package/botrun-c/web/next.config.js +49 -0
- package/botrun-c/web/package-lock.json +6711 -0
- package/botrun-c/web/package.json +63 -0
- package/botrun-c/web/postcss.config.js +6 -0
- package/botrun-c/web/public/apple_icon.png +0 -0
- package/botrun-c/web/public/google_icon.png +0 -0
- package/botrun-c/web/public/logo-48-small.png +0 -0
- package/botrun-c/web/public/logo-48.png +0 -0
- package/botrun-c/web/scripts/generate-version.js +36 -0
- package/botrun-c/web/scripts/update-build-version.js +41 -0
- package/botrun-c/web/tailwind.config.ts +80 -0
- package/botrun-c/web/test-welcome-message.mjs +115 -0
- package/botrun-c/web/tsconfig.json +42 -0
- package/botrun-c/web/types/agent.ts +6 -0
- package/botrun-c/web/types/attachment.ts +39 -0
- package/botrun-c/web/types/conversation.ts +68 -0
- package/botrun-c/web/types/flutter.d.ts +6 -0
- package/botrun-c/web/types/project.ts +41 -0
- package/botrun-c/web/types/skills.ts +13 -0
- package/botrun-c/web//350/250/272/346/226/267-Skills-/345/211/215/347/253/257/351/241/257/347/244/272/345/225/217/351/241/214.md +83 -0
- package/lib/prompt/prompts/zero-framework/fullstack.md +17 -0
- package/lib/prompt/prompts/zero-framework/segment.md +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1736 @@
|
|
|
1
|
+
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect, useRef } from "react";
|
|
5
|
+
import { ChatMessage, type Message } from "@/components/chat-message";
|
|
6
|
+
import { ChatInput } from "@/components/chat-input";
|
|
7
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
8
|
+
import { LogOut, MoreVertical, MessageSquarePlus, Sparkles, Copy, Check, Mail, Star, Info, UserX, RefreshCw, User, Hammer, Trash2, Brain, ChevronDown, FolderSync, Plus, History, Github, GitBranch, Link2, Unlink, FolderGit2, Search, X, AlertCircle, Download, CheckCircle2 } from "lucide-react";
|
|
9
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
10
|
+
import Image from "next/image";
|
|
11
|
+
import { useAuth } from "@/contexts/AuthContext";
|
|
12
|
+
import { useToast } from "@/hooks/use-toast";
|
|
13
|
+
import { Button } from "@/components/ui/button";
|
|
14
|
+
import { useAutoWelcome } from "@/hooks/useAutoWelcome";
|
|
15
|
+
import { useCapacitorPlatform } from "@/hooks/useCapacitorPlatform";
|
|
16
|
+
import { useScrollToBottom } from "@/hooks/useScrollToBottom";
|
|
17
|
+
import { useAppStoreUpdate, openAppStore } from "@/hooks/useAppStoreUpdate";
|
|
18
|
+
import { useUserPlan } from "@/hooks/useUserPlan";
|
|
19
|
+
import { ScrollToBottomButton } from "@/components/scroll-to-bottom-button";
|
|
20
|
+
import {
|
|
21
|
+
DropdownMenu,
|
|
22
|
+
DropdownMenuContent,
|
|
23
|
+
DropdownMenuItem,
|
|
24
|
+
DropdownMenuTrigger,
|
|
25
|
+
DropdownMenuSeparator,
|
|
26
|
+
} from "@/components/ui/dropdown-menu";
|
|
27
|
+
import { BotrunIncubator } from "@/components/botrun-incubator";
|
|
28
|
+
import { SkillsManagerDrawer } from "@/components/skills-manager-drawer";
|
|
29
|
+
import { SkillsEditor } from "@/components/skills-editor";
|
|
30
|
+
import { GdriveAddDialog } from "@/components/gdrive/gdrive-add-dialog";
|
|
31
|
+
import { useGdriveFolders } from "@/hooks/useGdriveFolders";
|
|
32
|
+
import { useGdriveSync } from "@/hooks/useGdriveSync";
|
|
33
|
+
import { useGitHub } from "@/hooks/useGitHub";
|
|
34
|
+
import { useAuthToken } from "@/hooks/useAuthToken";
|
|
35
|
+
import { useClaudeModel, type ClaudeModelId } from "@/hooks/useClaudeModel";
|
|
36
|
+
import {
|
|
37
|
+
AlertDialog,
|
|
38
|
+
AlertDialogAction,
|
|
39
|
+
AlertDialogCancel,
|
|
40
|
+
AlertDialogContent,
|
|
41
|
+
AlertDialogDescription,
|
|
42
|
+
AlertDialogFooter,
|
|
43
|
+
AlertDialogHeader,
|
|
44
|
+
AlertDialogTitle,
|
|
45
|
+
} from "@/components/ui/alert-dialog";
|
|
46
|
+
import { getApiUrl } from '@/lib/api-url';
|
|
47
|
+
import {
|
|
48
|
+
saveSessionId,
|
|
49
|
+
loadSessionId,
|
|
50
|
+
clearSessionId,
|
|
51
|
+
saveMessages,
|
|
52
|
+
loadMessages,
|
|
53
|
+
clearMessages,
|
|
54
|
+
clearAllUserData,
|
|
55
|
+
} from '@/lib/conversation-storage';
|
|
56
|
+
import { copyAllMessages } from '@/lib/clipboard-utils';
|
|
57
|
+
import { ConversationList } from '@/components/ConversationList';
|
|
58
|
+
import { useConversations } from '@/hooks/useConversations';
|
|
59
|
+
import { useConversation } from '@/hooks/useConversation';
|
|
60
|
+
import type { Conversation } from '@/types/conversation';
|
|
61
|
+
|
|
62
|
+
// API URL - 使用統一的 API URL 邏輯
|
|
63
|
+
const API_URL = (() => {
|
|
64
|
+
if (typeof window === 'undefined') return '';
|
|
65
|
+
|
|
66
|
+
// Capacitor (iOS/Android App) - 必須檢查 isNativePlatform()
|
|
67
|
+
// ⚠️ window.Capacitor 在 web 環境也會存在,需要額外判斷
|
|
68
|
+
const capacitor = (window as any).Capacitor;
|
|
69
|
+
if (capacitor && capacitor.isNativePlatform && capacitor.isNativePlatform()) {
|
|
70
|
+
// 真正的 Native App 環境:使用環境變數或預設 Cloud Run URL
|
|
71
|
+
return process.env.NEXT_PUBLIC_API_URL || 'https://botrun-c-257949799705.asia-east1.run.app';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Next.js Dev Server (localhost:3000) - 使用 lib/api-url.ts 的統一邏輯
|
|
75
|
+
// 本地開發:前端在 3000,API 在 8080
|
|
76
|
+
const currentPort = window.location.port;
|
|
77
|
+
if (currentPort === '3000') {
|
|
78
|
+
console.log('🔍 [ChatPage] Detected Next.js dev server (port 3000), using: http://localhost:8080');
|
|
79
|
+
return 'http://localhost:8080';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Load Balancer 架構:檢查是否在個人後端 (/u/{hash}/)
|
|
83
|
+
const pathname = window.location.pathname;
|
|
84
|
+
if (pathname.startsWith('/u/')) {
|
|
85
|
+
// 提取 /u/{hash}/ 前綴 (hash 是 8 位 16 進制)
|
|
86
|
+
const match = pathname.match(/^\/u\/[0-9a-f]{8}/);
|
|
87
|
+
if (match) {
|
|
88
|
+
console.log('🔍 [ChatPage] Detected personal backend path:', match[0]);
|
|
89
|
+
return match[0]; // 例如: /u/53288a47
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 使用環境變數設定的 API URL(如果有的話)
|
|
94
|
+
if (process.env.NEXT_PUBLIC_API_URL) {
|
|
95
|
+
return process.env.NEXT_PUBLIC_API_URL;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 統一前端或其他情況:使用相對路徑(空字串)
|
|
99
|
+
return '';
|
|
100
|
+
})();
|
|
101
|
+
|
|
102
|
+
// Debug: Log environment
|
|
103
|
+
if (typeof window !== 'undefined') {
|
|
104
|
+
console.log('🔍 [ChatPage] Environment:', {
|
|
105
|
+
isCapacitor: !!(window as any).Capacitor,
|
|
106
|
+
API_URL,
|
|
107
|
+
userAgent: navigator.userAgent
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function log(message: string, data?: any) {
|
|
112
|
+
const timestamp = new Date().toLocaleTimeString("zh-TW");
|
|
113
|
+
if (data) {
|
|
114
|
+
console.log(`[${timestamp}] 🌐 ${message}`, data);
|
|
115
|
+
} else {
|
|
116
|
+
console.log(`[${timestamp}] 🌐 ${message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 轉錄模型選項
|
|
121
|
+
const TRANSCRIPTION_MODELS = [
|
|
122
|
+
{ id: 'whisper-local', name: 'Whisper v3 Turbo (本地)', description: '本地 MLX Whisper,物理隔離,Apple Silicon 優化' },
|
|
123
|
+
{ id: 'whisper-Breeze-ASR-25', name: 'Whisper Breeze ASR 2.5 (國網中心)', description: '台灣國網中心優化版本,適合中文' },
|
|
124
|
+
{ id: 'Whisper-Large-V3', name: 'Whisper Large V3 (國網中心)', description: 'OpenAI 大型模型,支援多語言' },
|
|
125
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro (Google)', description: 'Google 最新模型,高準確度' },
|
|
126
|
+
] as const;
|
|
127
|
+
|
|
128
|
+
export type TranscriptionModel = typeof TRANSCRIPTION_MODELS[number]['id'];
|
|
129
|
+
|
|
130
|
+
// 雲端模型(需要網路)
|
|
131
|
+
const CLOUD_MODELS: TranscriptionModel[] = ['whisper-Breeze-ASR-25', 'Whisper-Large-V3', 'gemini-2.5-pro'];
|
|
132
|
+
|
|
133
|
+
// 本地模型(物理隔離)
|
|
134
|
+
const LOCAL_MODELS: TranscriptionModel[] = ['whisper-local'];
|
|
135
|
+
|
|
136
|
+
export function ChatPage() {
|
|
137
|
+
const { user, signOut } = useAuth();
|
|
138
|
+
const { toast } = useToast();
|
|
139
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
140
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
141
|
+
const [sessionId, setSessionId] = useState<string | undefined>();
|
|
142
|
+
const [isStopping, setIsStopping] = useState(false);
|
|
143
|
+
const [streamingMessageId, setStreamingMessageId] = useState<string | null>(null);
|
|
144
|
+
const [isIncubatorOpen, setIsIncubatorOpen] = useState(false);
|
|
145
|
+
const [isSkillsDrawerOpen, setIsSkillsDrawerOpen] = useState(false);
|
|
146
|
+
// 選單折疊狀態
|
|
147
|
+
const [menuSections, setMenuSections] = useState({
|
|
148
|
+
chat: false,
|
|
149
|
+
incubator: false,
|
|
150
|
+
gdrive: false,
|
|
151
|
+
github: false,
|
|
152
|
+
model: false,
|
|
153
|
+
about: false,
|
|
154
|
+
});
|
|
155
|
+
const [isGdriveAddDialogOpen, setIsGdriveAddDialogOpen] = useState(false);
|
|
156
|
+
const [skillsEditorOpen, setSkillsEditorOpen] = useState(false);
|
|
157
|
+
const [skillToEdit, setSkillToEdit] = useState<{ id: string; name: string; description: string; icon: string; source: "official" | "user"; editable: boolean } | null>(null);
|
|
158
|
+
const [clearFilesDialogOpen, setClearFilesDialogOpen] = useState(false);
|
|
159
|
+
const [fileStats, setFileStats] = useState<{ uploads: { count: number; size: number }; pdfPages: { count: number; size: number }; total: { count: number; size: number } } | null>(null);
|
|
160
|
+
const [isClearingFiles, setIsClearingFiles] = useState(false);
|
|
161
|
+
const [isLoadingStats, setIsLoadingStats] = useState(false);
|
|
162
|
+
// 🗂️ 對話歷史狀態
|
|
163
|
+
const [isConversationListOpen, setIsConversationListOpen] = useState(false);
|
|
164
|
+
const [currentConversationId, setCurrentConversationId] = useState<string | undefined>();
|
|
165
|
+
|
|
166
|
+
// 🎯 根據 ?local=true 決定預設轉錄模型
|
|
167
|
+
const [transcriptionModel, setTranscriptionModel] = useState<TranscriptionModel>(() => {
|
|
168
|
+
if (typeof window === 'undefined') return 'whisper-Breeze-ASR-25';
|
|
169
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
170
|
+
const isLocalMode = searchParams.get('local') === 'true';
|
|
171
|
+
return isLocalMode ? 'whisper-local' : 'whisper-Breeze-ASR-25';
|
|
172
|
+
});
|
|
173
|
+
const [isCopiedAll, setIsCopiedAll] = useState(false);
|
|
174
|
+
const [isSharingEmail, setIsSharingEmail] = useState(false);
|
|
175
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
176
|
+
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
177
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
178
|
+
|
|
179
|
+
// 偵測 Capacitor 平台
|
|
180
|
+
const { isCapacitor, isIOS, isAndroid, platform, getBottomPadding } = useCapacitorPlatform();
|
|
181
|
+
|
|
182
|
+
// 🎯 檢查 App 更新(iOS/Android)
|
|
183
|
+
const { updateAvailable, latestVersion, hasSeenUpdateNotification, markUpdateAsSeen } = useAppStoreUpdate();
|
|
184
|
+
|
|
185
|
+
// 🎯 取得使用者訂閱方案
|
|
186
|
+
const { planName, isLoading: isPlanLoading, refetch: refetchUserPlan } = useUserPlan(user);
|
|
187
|
+
|
|
188
|
+
// 🧠 Claude 模型選擇(Haiku 4.5 / Sonnet 4)
|
|
189
|
+
const { currentModel: claudeModel, setModel: setClaudeModel, models: claudeModels, getModelInfo } = useClaudeModel();
|
|
190
|
+
|
|
191
|
+
// 🎯 取得 Auth Token(用於 Google Drive 等需要認證的功能)
|
|
192
|
+
const { token: authToken } = useAuthToken();
|
|
193
|
+
|
|
194
|
+
// 🗂️ Google Drive 相關 hooks
|
|
195
|
+
const {
|
|
196
|
+
folders: gdriveFolders,
|
|
197
|
+
history: gdriveHistory,
|
|
198
|
+
isLoading: isGdriveFoldersLoading,
|
|
199
|
+
addFolder: addGdriveFolder,
|
|
200
|
+
removeFolder: removeGdriveFolder,
|
|
201
|
+
toggleSelect: toggleGdriveSelect,
|
|
202
|
+
refetch: refetchGdriveFolders,
|
|
203
|
+
} = useGdriveFolders(authToken || undefined);
|
|
204
|
+
|
|
205
|
+
const {
|
|
206
|
+
isSyncing: isGdriveSyncing,
|
|
207
|
+
syncAll: syncAllGdrive,
|
|
208
|
+
} = useGdriveSync(authToken || undefined);
|
|
209
|
+
|
|
210
|
+
// 🐙 GitHub 整合
|
|
211
|
+
const {
|
|
212
|
+
connected: isGitHubConnected,
|
|
213
|
+
loading: isGitHubLoading,
|
|
214
|
+
connectedRepos,
|
|
215
|
+
availableRepos,
|
|
216
|
+
connect: connectGitHub,
|
|
217
|
+
disconnect: disconnectGitHub,
|
|
218
|
+
cloneRepo: cloneGitHubRepo,
|
|
219
|
+
syncRepo: syncGitHubRepo,
|
|
220
|
+
deleteRepo: deleteGitHubRepo,
|
|
221
|
+
refresh: refreshGitHub,
|
|
222
|
+
} = useGitHub(authToken || undefined);
|
|
223
|
+
const [syncingRepoName, setSyncingRepoName] = useState<string | null>(null);
|
|
224
|
+
const [cloningRepoName, setCloningRepoName] = useState<string | null>(null);
|
|
225
|
+
const [deletingRepoName, setDeletingRepoName] = useState<string | null>(null);
|
|
226
|
+
const [showGitHubReposModal, setShowGitHubReposModal] = useState(false);
|
|
227
|
+
|
|
228
|
+
// 🐙 GitHub 連結回調處理(從 GitHub OAuth 跳轉回來時)
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (typeof window === 'undefined') return;
|
|
231
|
+
|
|
232
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
233
|
+
const githubConnected = urlParams.get('github_connected');
|
|
234
|
+
const githubError = urlParams.get('github_error');
|
|
235
|
+
|
|
236
|
+
if (githubConnected === 'true') {
|
|
237
|
+
toast({
|
|
238
|
+
title: '✅ GitHub 連結成功',
|
|
239
|
+
description: '現在可以管理你的 Repository 了',
|
|
240
|
+
});
|
|
241
|
+
// 清除 URL 參數
|
|
242
|
+
window.history.replaceState({}, '', window.location.pathname);
|
|
243
|
+
// 重新載入 GitHub 狀態
|
|
244
|
+
refreshGitHub();
|
|
245
|
+
} else if (githubError) {
|
|
246
|
+
const errorMessages: Record<string, string> = {
|
|
247
|
+
missing_state: '連結參數遺失,請重新連結',
|
|
248
|
+
invalid_state: '連結已過期,請重新連結',
|
|
249
|
+
invalid_installation: 'GitHub App 安裝驗證失敗',
|
|
250
|
+
install_failed: 'GitHub 連結失敗,請重試',
|
|
251
|
+
};
|
|
252
|
+
toast({
|
|
253
|
+
title: '❌ GitHub 連結失敗',
|
|
254
|
+
description: errorMessages[githubError] || '未知錯誤',
|
|
255
|
+
variant: 'destructive',
|
|
256
|
+
});
|
|
257
|
+
// 清除 URL 參數
|
|
258
|
+
window.history.replaceState({}, '', window.location.pathname);
|
|
259
|
+
}
|
|
260
|
+
}, [toast, refreshGitHub]);
|
|
261
|
+
|
|
262
|
+
// 🗂️ 對話歷史 hooks
|
|
263
|
+
const {
|
|
264
|
+
conversations,
|
|
265
|
+
isLoading: isConversationsLoading,
|
|
266
|
+
error: conversationsError,
|
|
267
|
+
refetch: refetchConversations,
|
|
268
|
+
updateTitle: updateConversationTitle,
|
|
269
|
+
deleteConversation,
|
|
270
|
+
} = useConversations(authToken || undefined);
|
|
271
|
+
|
|
272
|
+
const {
|
|
273
|
+
conversation: selectedConversation,
|
|
274
|
+
messages: selectedMessages,
|
|
275
|
+
isLoading: isSelectedConversationLoading,
|
|
276
|
+
} = useConversation(authToken || undefined, currentConversationId);
|
|
277
|
+
|
|
278
|
+
// Debug: 顯示當前平台和底部間距
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
console.log('🔍 [ChatPage] Platform detection result:', {
|
|
281
|
+
platform,
|
|
282
|
+
isCapacitor,
|
|
283
|
+
bottomPadding: getBottomPadding(),
|
|
284
|
+
buttonBottom: getBottomPadding('6rem'),
|
|
285
|
+
});
|
|
286
|
+
}, [platform, isCapacitor, getBottomPadding]);
|
|
287
|
+
|
|
288
|
+
// 🔄 頁面載入時恢復 sessionId 和 messages(避免閒置後失憶)
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (!user) return;
|
|
291
|
+
|
|
292
|
+
// 恢復 sessionId
|
|
293
|
+
const savedSessionId = loadSessionId(user.uid);
|
|
294
|
+
if (savedSessionId) {
|
|
295
|
+
setSessionId(savedSessionId);
|
|
296
|
+
log('🔄 Restored sessionId from localStorage');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 恢復 messages(可選,快速恢復對話內容)
|
|
300
|
+
const savedMessages = loadMessages(user.uid);
|
|
301
|
+
if (savedMessages && savedMessages.length > 0) {
|
|
302
|
+
setMessages(savedMessages);
|
|
303
|
+
log(`🔄 Restored ${savedMessages.length} messages from localStorage`);
|
|
304
|
+
}
|
|
305
|
+
}, [user]); // 只在 user 變更時執行一次
|
|
306
|
+
|
|
307
|
+
// 💾 自動儲存 sessionId(每次變更時)
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (!user || !sessionId) return;
|
|
310
|
+
|
|
311
|
+
saveSessionId(user.uid, sessionId);
|
|
312
|
+
}, [user, sessionId]);
|
|
313
|
+
|
|
314
|
+
// 💾 自動儲存 messages(可選,debounce 避免過度寫入)
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (!user || messages.length === 0) return;
|
|
317
|
+
|
|
318
|
+
// 使用 setTimeout debounce(5 秒後才存)
|
|
319
|
+
const timeoutId = setTimeout(() => {
|
|
320
|
+
saveMessages(user.uid, messages);
|
|
321
|
+
}, 5000);
|
|
322
|
+
|
|
323
|
+
return () => clearTimeout(timeoutId);
|
|
324
|
+
}, [user, messages]);
|
|
325
|
+
|
|
326
|
+
// 歡迎訊息 hook
|
|
327
|
+
const { welcomeMessage, shouldShowWelcome } = useAutoWelcome(messages);
|
|
328
|
+
|
|
329
|
+
// 智慧捲動 hook
|
|
330
|
+
const { isAtBottom, scrollToBottom, handleScroll, checkIsAtBottom } = useScrollToBottom({
|
|
331
|
+
scrollContainerRef: scrollAreaRef,
|
|
332
|
+
scrollTargetRef: messagesEndRef,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// 🎯 取消自動捲動功能 - 使用者需手動滾動查看新內容
|
|
336
|
+
// 保留浮動按鈕,讓使用者可以隨時跳回底部
|
|
337
|
+
// useEffect(() => {
|
|
338
|
+
// if (isAtBottom) {
|
|
339
|
+
// scrollToBottom();
|
|
340
|
+
// }
|
|
341
|
+
// }, [messages, isAtBottom, scrollToBottom]);
|
|
342
|
+
|
|
343
|
+
// 🎯 當內容變化時,檢查是否需要顯示浮動按鈕
|
|
344
|
+
// 當 AI 生成內容超過視窗高度時,自動顯示按鈕提醒使用者
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
// 多次檢查,確保捕捉到內容超過視窗的時刻
|
|
347
|
+
// 因為 DOM 更新和渲染需要時間,單次檢查可能不準確
|
|
348
|
+
const timers = [
|
|
349
|
+
setTimeout(() => checkIsAtBottom(), 50), // 第一次快速檢查
|
|
350
|
+
setTimeout(() => checkIsAtBottom(), 150), // 第二次檢查
|
|
351
|
+
setTimeout(() => checkIsAtBottom(), 300), // 第三次檢查(確保 smooth scroll 完成)
|
|
352
|
+
];
|
|
353
|
+
return () => timers.forEach(timer => clearTimeout(timer));
|
|
354
|
+
}, [messages, checkIsAtBottom]);
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
const handleStopResponse = () => {
|
|
358
|
+
log("🛑 User requested to stop the response");
|
|
359
|
+
setIsStopping(true);
|
|
360
|
+
|
|
361
|
+
if (abortControllerRef.current) {
|
|
362
|
+
abortControllerRef.current.abort();
|
|
363
|
+
log("🛑 AbortController signal sent");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Reset states after a short delay to allow cleanup
|
|
367
|
+
setTimeout(() => {
|
|
368
|
+
setIsLoading(false);
|
|
369
|
+
setIsStopping(false);
|
|
370
|
+
abortControllerRef.current = null;
|
|
371
|
+
}, 500);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// 技能編輯處理函數
|
|
375
|
+
const handleEditSkill = (skill: { id: string; name: string; description: string; icon: string; source: "official" | "user"; editable: boolean }) => {
|
|
376
|
+
setSkillToEdit(skill);
|
|
377
|
+
setIsSkillsDrawerOpen(false);
|
|
378
|
+
setSkillsEditorOpen(true);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const handleCreateSkill = () => {
|
|
382
|
+
setSkillToEdit(null);
|
|
383
|
+
setIsSkillsDrawerOpen(false);
|
|
384
|
+
setSkillsEditorOpen(true);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const handleSkillsChange = () => {
|
|
388
|
+
// Skills 變更後的回呼(目前僅關閉 drawer 即可)
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// 清除檔案功能
|
|
392
|
+
const formatFileSize = (bytes: number) => {
|
|
393
|
+
if (bytes === 0) return '0 B';
|
|
394
|
+
const k = 1024;
|
|
395
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
396
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
397
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const handleOpenClearFilesDialog = async () => {
|
|
401
|
+
if (!user) return;
|
|
402
|
+
|
|
403
|
+
setIsLoadingStats(true);
|
|
404
|
+
setClearFilesDialogOpen(true);
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const token = await user.getIdToken();
|
|
408
|
+
const apiUrl = getApiUrl();
|
|
409
|
+
const response = await fetch(`${apiUrl}/api/files/stats?projectId=default`, {
|
|
410
|
+
headers: {
|
|
411
|
+
'Authorization': `Bearer ${token}`,
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (!response.ok) {
|
|
416
|
+
throw new Error('Failed to get file stats');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const data = await response.json();
|
|
420
|
+
setFileStats(data.stats);
|
|
421
|
+
log('📊 File stats loaded:', data.stats);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.error('Failed to load file stats:', error);
|
|
424
|
+
toast({
|
|
425
|
+
title: "載入失敗",
|
|
426
|
+
description: "無法取得檔案統計資訊",
|
|
427
|
+
variant: "destructive",
|
|
428
|
+
});
|
|
429
|
+
setClearFilesDialogOpen(false);
|
|
430
|
+
} finally {
|
|
431
|
+
setIsLoadingStats(false);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const handleClearFiles = async () => {
|
|
436
|
+
if (!user) return;
|
|
437
|
+
|
|
438
|
+
setIsClearingFiles(true);
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const token = await user.getIdToken();
|
|
442
|
+
const apiUrl = getApiUrl();
|
|
443
|
+
const response = await fetch(`${apiUrl}/api/files/clear?projectId=default`, {
|
|
444
|
+
method: 'DELETE',
|
|
445
|
+
headers: {
|
|
446
|
+
'Authorization': `Bearer ${token}`,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
throw new Error('Failed to clear files');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const data = await response.json();
|
|
455
|
+
log('🗑️ Files cleared:', data);
|
|
456
|
+
|
|
457
|
+
toast({
|
|
458
|
+
title: "清除完成",
|
|
459
|
+
description: data.message || `已清除 ${data.totalDeleted} 個檔案`,
|
|
460
|
+
duration: 2000,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
setClearFilesDialogOpen(false);
|
|
464
|
+
setFileStats(null);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error('Failed to clear files:', error);
|
|
467
|
+
toast({
|
|
468
|
+
title: "清除失敗",
|
|
469
|
+
description: "無法清除檔案,請稍後再試",
|
|
470
|
+
variant: "destructive",
|
|
471
|
+
});
|
|
472
|
+
} finally {
|
|
473
|
+
setIsClearingFiles(false);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const handleNewChat = () => {
|
|
478
|
+
// 1. 清空 sessionId(讓下次請求建立新 session)
|
|
479
|
+
setSessionId(undefined);
|
|
480
|
+
|
|
481
|
+
// 2. 清空所有訊息
|
|
482
|
+
setMessages([]);
|
|
483
|
+
|
|
484
|
+
// 3. 清空 localStorage(避免恢復舊對話)
|
|
485
|
+
if (user) {
|
|
486
|
+
clearSessionId(user.uid);
|
|
487
|
+
clearMessages(user.uid);
|
|
488
|
+
log('🗑️ Cleared localStorage for new chat');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 4. 重置其他狀態(保險起見)
|
|
492
|
+
setIsLoading(false);
|
|
493
|
+
setIsStopping(false);
|
|
494
|
+
abortControllerRef.current = null;
|
|
495
|
+
|
|
496
|
+
// 5. 清空當前對話 ID(歷史對話功能)
|
|
497
|
+
setCurrentConversationId(undefined);
|
|
498
|
+
|
|
499
|
+
// 6. 歡迎訊息會自動重新顯示(因為 shouldShowWelcome = messages.length === 0)
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// 🗂️ 選擇歷史對話
|
|
503
|
+
const handleSelectConversation = (conversation: Conversation) => {
|
|
504
|
+
log(`🗂️ Selected conversation: ${conversation.id} - ${conversation.title}`);
|
|
505
|
+
|
|
506
|
+
// 設定當前對話 ID(觸發 useConversation hook 載入訊息)
|
|
507
|
+
setCurrentConversationId(conversation.id);
|
|
508
|
+
|
|
509
|
+
// 設定 sessionId 以便繼續對話
|
|
510
|
+
if (conversation.sessionId) {
|
|
511
|
+
setSessionId(conversation.sessionId);
|
|
512
|
+
if (user) {
|
|
513
|
+
saveSessionId(user.uid, conversation.sessionId);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// 🗂️ 當選擇的對話訊息載入完成後,更新 messages 狀態
|
|
519
|
+
useEffect(() => {
|
|
520
|
+
if (selectedMessages && selectedMessages.length > 0 && currentConversationId) {
|
|
521
|
+
// 將 API 回傳的訊息轉換為本地 Message 格式
|
|
522
|
+
const convertedMessages: Message[] = selectedMessages.map((msg) => ({
|
|
523
|
+
id: msg.id,
|
|
524
|
+
role: msg.role,
|
|
525
|
+
content: msg.content,
|
|
526
|
+
timestamp: new Date(msg.timestamp),
|
|
527
|
+
toolUses: msg.toolUses?.map((tu) => ({
|
|
528
|
+
toolId: tu.id,
|
|
529
|
+
toolName: tu.name,
|
|
530
|
+
toolInput: tu.input,
|
|
531
|
+
toolOutput: tu.output,
|
|
532
|
+
})),
|
|
533
|
+
}));
|
|
534
|
+
|
|
535
|
+
setMessages(convertedMessages);
|
|
536
|
+
log(`🗂️ Loaded ${convertedMessages.length} messages from conversation ${currentConversationId}`);
|
|
537
|
+
|
|
538
|
+
// 同時更新 localStorage
|
|
539
|
+
if (user) {
|
|
540
|
+
saveMessages(user.uid, convertedMessages);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}, [selectedMessages, currentConversationId, user]);
|
|
544
|
+
|
|
545
|
+
const handleSignOut = async () => {
|
|
546
|
+
// 登出前清除所有使用者資料
|
|
547
|
+
if (user) {
|
|
548
|
+
clearAllUserData(user.uid);
|
|
549
|
+
log('🗑️ Cleared all user data before sign out');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 執行 Firebase 登出
|
|
553
|
+
await signOut();
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const handleCopyAllMessages = async () => {
|
|
557
|
+
if (messages.length === 0) return;
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
await copyAllMessages(messages);
|
|
561
|
+
setIsCopiedAll(true);
|
|
562
|
+
setTimeout(() => setIsCopiedAll(false), 2000);
|
|
563
|
+
console.log('✅ All messages copied to clipboard (plain text + HTML)');
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.error('❌ Copy all failed:', error);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const handleShareToEmail = async () => {
|
|
570
|
+
if (messages.length === 0) {
|
|
571
|
+
toast({
|
|
572
|
+
title: "無對話記錄",
|
|
573
|
+
description: "目前沒有對話可以分享",
|
|
574
|
+
variant: "destructive",
|
|
575
|
+
});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!user) {
|
|
580
|
+
toast({
|
|
581
|
+
title: "請先登入",
|
|
582
|
+
description: "需要登入才能使用 Email 分享功能",
|
|
583
|
+
variant: "destructive",
|
|
584
|
+
});
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
setIsSharingEmail(true);
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
// 取得 Firebase Auth Token
|
|
592
|
+
const token = await user.getIdToken();
|
|
593
|
+
|
|
594
|
+
// 準備 API 請求資料
|
|
595
|
+
const apiUrl = getApiUrl();
|
|
596
|
+
const response = await fetch(`${apiUrl}/api/share-conversation`, {
|
|
597
|
+
method: 'POST',
|
|
598
|
+
headers: {
|
|
599
|
+
'Authorization': `Bearer ${token}`,
|
|
600
|
+
'Content-Type': 'application/json',
|
|
601
|
+
},
|
|
602
|
+
body: JSON.stringify({
|
|
603
|
+
messages: messages.map(msg => ({
|
|
604
|
+
role: msg.role,
|
|
605
|
+
content: msg.content,
|
|
606
|
+
})),
|
|
607
|
+
title: `波特人對話記錄 - ${new Date().toLocaleDateString('zh-TW')}`,
|
|
608
|
+
}),
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const data = await response.json();
|
|
612
|
+
|
|
613
|
+
if (!response.ok) {
|
|
614
|
+
throw new Error(data.error || '發送失敗');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 成功 - 0.8 秒後自動消失
|
|
618
|
+
toast({
|
|
619
|
+
title: "📧 Email 已發送",
|
|
620
|
+
description: `對話記錄已發送到您的信箱 (${user.email})`,
|
|
621
|
+
duration: 800,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
log('✅ Conversation shared to email successfully', { email: user.email });
|
|
625
|
+
} catch (error) {
|
|
626
|
+
console.error('❌ Share to email failed:', error);
|
|
627
|
+
toast({
|
|
628
|
+
title: "發送失敗",
|
|
629
|
+
description: error instanceof Error ? error.message : '無法發送 Email,請稍後再試',
|
|
630
|
+
variant: "destructive",
|
|
631
|
+
});
|
|
632
|
+
} finally {
|
|
633
|
+
setIsSharingEmail(false);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const handleOpenUrl = async (url: string, logPrefix: string) => {
|
|
638
|
+
log(`${logPrefix} Opening: ${url}`);
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
// 判斷是否為 native 平台(iOS/Android)
|
|
642
|
+
const isNative = (window as any).Capacitor?.isNativePlatform?.() || false;
|
|
643
|
+
|
|
644
|
+
if (isNative) {
|
|
645
|
+
// Native 環境:使用 _system 在外部瀏覽器開啟
|
|
646
|
+
// Capacitor 的 WebView 會自動處理 _system target
|
|
647
|
+
window.open(url, '_system');
|
|
648
|
+
log("✅ Opened URL in external browser (Native)");
|
|
649
|
+
} else {
|
|
650
|
+
// 網頁環境:在新分頁開啟
|
|
651
|
+
window.open(url, '_blank');
|
|
652
|
+
log("✅ Opened URL in new tab (Web)");
|
|
653
|
+
}
|
|
654
|
+
} catch (error) {
|
|
655
|
+
log(`❌ Error opening URL:`, error);
|
|
656
|
+
console.error("Error opening URL:", error);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const handleFeatures = async () => {
|
|
661
|
+
await handleOpenUrl('https://app.botrun.ai/intro/', '✨');
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const handleAbout = async () => {
|
|
665
|
+
await handleOpenUrl('https://app.botrun.ai/intro/index.html#contact', 'ℹ️');
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const handleDeleteAccount = async () => {
|
|
669
|
+
await handleOpenUrl('https://app.botrun.ai/delete-account/', '🗑️');
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const handleSendMessage = async (content: string) => {
|
|
673
|
+
if (!content.trim()) return;
|
|
674
|
+
|
|
675
|
+
const requestId = `req-${Date.now()}`;
|
|
676
|
+
log(`📤 [${requestId}] Sending message:`, { content, sessionId });
|
|
677
|
+
|
|
678
|
+
const userMessage: Message = {
|
|
679
|
+
id: `user-${Date.now()}`,
|
|
680
|
+
role: "user",
|
|
681
|
+
content,
|
|
682
|
+
timestamp: new Date(),
|
|
683
|
+
};
|
|
684
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
685
|
+
setIsLoading(true);
|
|
686
|
+
|
|
687
|
+
// ✨ 自動重試機制:最多重試 1 次,使用者無需知道重試過程
|
|
688
|
+
const MAX_RETRIES = 1;
|
|
689
|
+
let lastError: Error | null = null;
|
|
690
|
+
|
|
691
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
692
|
+
// 重試時記錄 log(使用者看不到)
|
|
693
|
+
if (attempt > 0) {
|
|
694
|
+
log(`🔄 [${requestId}] Auto-retry attempt ${attempt}/${MAX_RETRIES}...`);
|
|
695
|
+
console.warn(`🔄 [${requestId}] Auto-retrying after transient error:`, lastError?.message);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
const assistantMessageId = `assistant-${Date.now()}`;
|
|
700
|
+
const assistantMessage: Message = {
|
|
701
|
+
id: assistantMessageId,
|
|
702
|
+
role: "assistant",
|
|
703
|
+
content: "",
|
|
704
|
+
timestamp: new Date(),
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// 重試時需要先移除上一次失敗的空 assistant message
|
|
708
|
+
if (attempt > 0) {
|
|
709
|
+
setMessages((prev) => {
|
|
710
|
+
// 移除最後一個空的 assistant message(如果有的話)
|
|
711
|
+
const lastMsg = prev[prev.length - 1];
|
|
712
|
+
if (lastMsg && lastMsg.role === 'assistant' && !lastMsg.content) {
|
|
713
|
+
return [...prev.slice(0, -1), assistantMessage];
|
|
714
|
+
}
|
|
715
|
+
return [...prev, assistantMessage];
|
|
716
|
+
});
|
|
717
|
+
} else {
|
|
718
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
719
|
+
}
|
|
720
|
+
setStreamingMessageId(assistantMessageId); // 開始串流
|
|
721
|
+
|
|
722
|
+
// Create new AbortController for this request
|
|
723
|
+
const abortController = new AbortController();
|
|
724
|
+
abortControllerRef.current = abortController;
|
|
725
|
+
|
|
726
|
+
log(`📡 [${requestId}] Making API request to ${API_URL}/api/chat (attempt ${attempt + 1}/${MAX_RETRIES + 1})`);
|
|
727
|
+
|
|
728
|
+
// ✨ 檢查 URL 參數 ?local=true(LM Studio 物理隔離模式)
|
|
729
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
730
|
+
const useLMStudio = searchParams.get('local') === 'true';
|
|
731
|
+
|
|
732
|
+
// Get Firebase ID token for authentication
|
|
733
|
+
// 🔧 物理隔離模式:使用假 token,不需要網路
|
|
734
|
+
let idToken: string;
|
|
735
|
+
if (useLMStudio) {
|
|
736
|
+
idToken = 'local-mode-no-auth-needed';
|
|
737
|
+
log(`🔧 [${requestId}] Physical isolation mode - using fake token (no network required)`);
|
|
738
|
+
} else {
|
|
739
|
+
if (!user) {
|
|
740
|
+
throw new Error("User not authenticated");
|
|
741
|
+
}
|
|
742
|
+
idToken = await user.getIdToken();
|
|
743
|
+
log(`🔐 [${requestId}] ID Token obtained, length: ${idToken?.length || 0}`);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Debug: 顯示當前 URL 和模式
|
|
747
|
+
log(`🔍 [${requestId}] URL: ${window.location.href}`);
|
|
748
|
+
log(`🔍 [${requestId}] URL params:`, Object.fromEntries(searchParams.entries()));
|
|
749
|
+
|
|
750
|
+
if (useLMStudio) {
|
|
751
|
+
log(`🔧 [${requestId}] ✅ LM Studio mode enabled (URL: ?local=true)`);
|
|
752
|
+
} else {
|
|
753
|
+
log(`🤖 [${requestId}] ✅ Claude Agent SDK mode (default)`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const response = await fetch(`${API_URL}/api/chat`, {
|
|
757
|
+
method: "POST",
|
|
758
|
+
headers: {
|
|
759
|
+
"Content-Type": "application/json",
|
|
760
|
+
"Authorization": `Bearer ${idToken}`,
|
|
761
|
+
},
|
|
762
|
+
body: JSON.stringify({
|
|
763
|
+
message: content,
|
|
764
|
+
sessionId,
|
|
765
|
+
useLMStudio, // ✨ 傳遞給後端
|
|
766
|
+
claudeModel, // 🧠 使用者選擇的 Claude 模型
|
|
767
|
+
}),
|
|
768
|
+
signal: abortController.signal,
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
log(`✅ [${requestId}] Response received, status: ${response.status}`);
|
|
772
|
+
|
|
773
|
+
// Handle Rate Limit Error (429) - 不重試,直接顯示錯誤
|
|
774
|
+
if (response.status === 429) {
|
|
775
|
+
const errorData = await response.json();
|
|
776
|
+
log(`⚠️ [${requestId}] Rate limit exceeded:`, errorData);
|
|
777
|
+
|
|
778
|
+
// 直接使用後端提供的完整錯誤訊息(包含所有詳細資訊和升級方案)
|
|
779
|
+
const errorMessage = errorData.message || '請求過於頻繁,請稍後再試';
|
|
780
|
+
|
|
781
|
+
// 在聊天中顯示錯誤訊息
|
|
782
|
+
const errorMessageObj: Message = {
|
|
783
|
+
id: `error-${Date.now()}`,
|
|
784
|
+
role: "assistant",
|
|
785
|
+
content: errorMessage, // 直接使用後端組合好的完整訊息
|
|
786
|
+
timestamp: new Date(),
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
setMessages((prev) => [...prev.slice(0, -1), errorMessageObj]); // 移除空的 assistant message
|
|
790
|
+
setIsLoading(false);
|
|
791
|
+
setStreamingMessageId(null);
|
|
792
|
+
return; // 429 錯誤不重試
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (!response.ok) {
|
|
796
|
+
log(`❌ [${requestId}] HTTP error! status: ${response.status}`);
|
|
797
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const reader = response.body?.getReader();
|
|
801
|
+
const decoder = new TextDecoder();
|
|
802
|
+
|
|
803
|
+
if (!reader) {
|
|
804
|
+
log(`❌ [${requestId}] No reader available`);
|
|
805
|
+
throw new Error("No reader available");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
log(`🔄 [${requestId}] Starting to read SSE stream...`);
|
|
809
|
+
|
|
810
|
+
let buffer = "";
|
|
811
|
+
let assistantContent = "";
|
|
812
|
+
let eventCount = 0;
|
|
813
|
+
let textChunkCount = 0;
|
|
814
|
+
|
|
815
|
+
while (true) {
|
|
816
|
+
const { done, value } = await reader.read();
|
|
817
|
+
|
|
818
|
+
if (done) {
|
|
819
|
+
// Removed: log(`🏁 [${requestId}] Stream finished - Events: ${eventCount}, Text chunks: ${textChunkCount}, Total chars: ${assistantContent.length}`);
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
buffer += decoder.decode(value, { stream: true });
|
|
824
|
+
const lines = buffer.split("\n");
|
|
825
|
+
buffer = lines.pop() || "";
|
|
826
|
+
|
|
827
|
+
for (const line of lines) {
|
|
828
|
+
if (line.startsWith("data: ")) {
|
|
829
|
+
eventCount++;
|
|
830
|
+
try {
|
|
831
|
+
const data = JSON.parse(line.slice(6));
|
|
832
|
+
|
|
833
|
+
if (data.sessionId) {
|
|
834
|
+
log(`🆔 [${requestId}] Got session ID: ${data.sessionId}`);
|
|
835
|
+
setSessionId(data.sessionId);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (data.text) {
|
|
839
|
+
assistantContent += data.text;
|
|
840
|
+
textChunkCount++;
|
|
841
|
+
setMessages((prev) =>
|
|
842
|
+
prev.map((msg) =>
|
|
843
|
+
msg.id === assistantMessageId
|
|
844
|
+
? { ...msg, content: assistantContent }
|
|
845
|
+
: msg
|
|
846
|
+
)
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (data.toolName && data.toolId) {
|
|
851
|
+
log(`🔧 [${requestId}] Tool usage: ${data.toolName} (${data.toolId})`);
|
|
852
|
+
setMessages((prev) =>
|
|
853
|
+
prev.map((msg) =>
|
|
854
|
+
msg.id === assistantMessageId
|
|
855
|
+
? {
|
|
856
|
+
...msg,
|
|
857
|
+
toolUses: [
|
|
858
|
+
...(msg.toolUses || []),
|
|
859
|
+
{
|
|
860
|
+
toolId: data.toolId,
|
|
861
|
+
toolName: data.toolName,
|
|
862
|
+
toolInput: data.toolInput,
|
|
863
|
+
},
|
|
864
|
+
],
|
|
865
|
+
}
|
|
866
|
+
: msg
|
|
867
|
+
)
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (data.toolOutput !== undefined && data.toolId) {
|
|
872
|
+
log(`📤 [${requestId}] Tool output received for ${data.toolId}`);
|
|
873
|
+
setMessages((prev) =>
|
|
874
|
+
prev.map((msg) =>
|
|
875
|
+
msg.id === assistantMessageId && msg.toolUses
|
|
876
|
+
? {
|
|
877
|
+
...msg,
|
|
878
|
+
toolUses: msg.toolUses.map((tu) =>
|
|
879
|
+
tu.toolId === data.toolId
|
|
880
|
+
? { ...tu, toolOutput: data.toolOutput, isError: data.isError }
|
|
881
|
+
: tu
|
|
882
|
+
),
|
|
883
|
+
}
|
|
884
|
+
: msg
|
|
885
|
+
)
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (data.token && data.fileName) {
|
|
890
|
+
log(`📥 [${requestId}] Download ready: ${data.fileName}`);
|
|
891
|
+
setMessages((prev) =>
|
|
892
|
+
prev.map((msg) =>
|
|
893
|
+
msg.id === assistantMessageId
|
|
894
|
+
? {
|
|
895
|
+
...msg,
|
|
896
|
+
downloadToken: data.token,
|
|
897
|
+
downloadFileName: data.fileName,
|
|
898
|
+
}
|
|
899
|
+
: msg
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (data.success) {
|
|
905
|
+
log(`✅ [${requestId}] Conversation completed`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (data.error) {
|
|
909
|
+
log(`❌ [${requestId}] Error: ${data.error}`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// 🔍 Handle debug info from backend (removed to reduce console noise)
|
|
913
|
+
// if (data.type === 'debug') {
|
|
914
|
+
// console.log(`🔍 [Backend Debug] ${data.message}`, data.details);
|
|
915
|
+
// }
|
|
916
|
+
} catch (e) {
|
|
917
|
+
log(`❌ [${requestId}] Error parsing SSE data:`, e);
|
|
918
|
+
console.error("Error parsing SSE data:", e);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ✅ 成功完成,執行 cleanup 並結束
|
|
925
|
+
if (attempt > 0) {
|
|
926
|
+
log(`✅ [${requestId}] Auto-retry succeeded on attempt ${attempt + 1}`);
|
|
927
|
+
}
|
|
928
|
+
setIsLoading(false);
|
|
929
|
+
setIsStopping(false);
|
|
930
|
+
setStreamingMessageId(null);
|
|
931
|
+
abortControllerRef.current = null;
|
|
932
|
+
return; // 成功,結束函數
|
|
933
|
+
|
|
934
|
+
} catch (error) {
|
|
935
|
+
// Check if error is due to abort - 不重試
|
|
936
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
937
|
+
log(`⏹️ [${requestId}] Request was stopped by user`);
|
|
938
|
+
setMessages((prev) => [
|
|
939
|
+
...prev,
|
|
940
|
+
{
|
|
941
|
+
id: `info-${Date.now()}`,
|
|
942
|
+
role: "assistant",
|
|
943
|
+
content: "⏹️ Response was stopped by user.",
|
|
944
|
+
timestamp: new Date(),
|
|
945
|
+
},
|
|
946
|
+
]);
|
|
947
|
+
setIsLoading(false);
|
|
948
|
+
setIsStopping(false);
|
|
949
|
+
setStreamingMessageId(null);
|
|
950
|
+
abortControllerRef.current = null;
|
|
951
|
+
return; // AbortError 不重試
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// 記錄錯誤(供開發者除錯)
|
|
955
|
+
log(`❌ [${requestId}] Error on attempt ${attempt + 1}/${MAX_RETRIES + 1}:`, error);
|
|
956
|
+
if (error instanceof Error) {
|
|
957
|
+
console.error(`❌ [${requestId}] Error name:`, error.name);
|
|
958
|
+
console.error(`❌ [${requestId}] Error message:`, error.message);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
962
|
+
|
|
963
|
+
// 如果還有重試次數,繼續迴圈(不顯示錯誤給使用者)
|
|
964
|
+
if (attempt < MAX_RETRIES) {
|
|
965
|
+
log(`🔄 [${requestId}] Will retry... (${MAX_RETRIES - attempt} attempts remaining)`);
|
|
966
|
+
// 短暫延遲後重試(避免立即重試)
|
|
967
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// 所有重試都失敗了,顯示錯誤給使用者
|
|
972
|
+
log(`❌ [${requestId}] All retry attempts exhausted. Showing error to user.`);
|
|
973
|
+
console.error("❌ [ChatPage] Full error after all retries:", lastError);
|
|
974
|
+
if (lastError) {
|
|
975
|
+
console.error('❌ Error stack:', lastError.stack);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
setMessages((prev) => [
|
|
979
|
+
...prev,
|
|
980
|
+
{
|
|
981
|
+
id: `error-${Date.now()}`,
|
|
982
|
+
role: "assistant",
|
|
983
|
+
content: "連線出現問題,請聯絡客服夥伴",
|
|
984
|
+
timestamp: new Date(),
|
|
985
|
+
},
|
|
986
|
+
]);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// finally 邏輯移到這裡(確保無論成功或失敗都會執行)
|
|
991
|
+
setIsLoading(false);
|
|
992
|
+
setIsStopping(false);
|
|
993
|
+
setStreamingMessageId(null); // 結束串流
|
|
994
|
+
abortControllerRef.current = null;
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
if (!user) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return (
|
|
1002
|
+
<div
|
|
1003
|
+
className="flex w-full flex-col bg-background"
|
|
1004
|
+
style={isCapacitor ? {
|
|
1005
|
+
position: 'fixed',
|
|
1006
|
+
top: 0,
|
|
1007
|
+
left: 0,
|
|
1008
|
+
right: 0,
|
|
1009
|
+
bottom: 0,
|
|
1010
|
+
height: '100vh',
|
|
1011
|
+
overflow: 'hidden'
|
|
1012
|
+
} : {
|
|
1013
|
+
height: '100dvh',
|
|
1014
|
+
minHeight: '-webkit-fill-available'
|
|
1015
|
+
}}
|
|
1016
|
+
>
|
|
1017
|
+
{/* Capacitor App: Status Bar 背景遮蔽 - 解決 Android 13+ setBackgroundColor 失效問題 */}
|
|
1018
|
+
{isCapacitor && (
|
|
1019
|
+
<div
|
|
1020
|
+
style={{
|
|
1021
|
+
position: 'fixed',
|
|
1022
|
+
top: 0,
|
|
1023
|
+
left: 0,
|
|
1024
|
+
right: 0,
|
|
1025
|
+
height: 'var(--safe-area-inset-top)',
|
|
1026
|
+
backgroundColor: '#F0EBE3', // 榻榻米色
|
|
1027
|
+
zIndex: 40, // 低於選單按鈕 (z-50),高於內容
|
|
1028
|
+
pointerEvents: 'none', // 讓點擊事件穿透,不擋到按鈕
|
|
1029
|
+
}}
|
|
1030
|
+
/>
|
|
1031
|
+
)}
|
|
1032
|
+
|
|
1033
|
+
{/* Floating Dots Menu */}
|
|
1034
|
+
<DropdownMenu onOpenChange={(open) => {
|
|
1035
|
+
if (open) {
|
|
1036
|
+
// 🔴 開啟選單時,標記使用者已看過更新通知
|
|
1037
|
+
if (updateAvailable && !hasSeenUpdateNotification) {
|
|
1038
|
+
console.log('👁️ [ChatPage] Menu opened, marking update as seen');
|
|
1039
|
+
markUpdateAsSeen();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// 🎯 重新查詢使用者方案(帶 3 分鐘快取)
|
|
1043
|
+
refetchUserPlan();
|
|
1044
|
+
}
|
|
1045
|
+
}}>
|
|
1046
|
+
<DropdownMenuTrigger asChild>
|
|
1047
|
+
<div className="fixed right-4 z-50" style={{
|
|
1048
|
+
top: isCapacitor
|
|
1049
|
+
? 'calc(var(--safe-area-inset-top) + 1rem)'
|
|
1050
|
+
: '1rem'
|
|
1051
|
+
}}>
|
|
1052
|
+
<Button
|
|
1053
|
+
variant="ghost"
|
|
1054
|
+
size="icon"
|
|
1055
|
+
className="h-10 w-10 rounded-full bg-background/80 backdrop-blur-sm shadow-md hover:bg-background/95 relative"
|
|
1056
|
+
>
|
|
1057
|
+
<MoreVertical className="h-5 w-5" />
|
|
1058
|
+
{/* 🔴 紅點提示:有新版本且尚未查看 */}
|
|
1059
|
+
{updateAvailable && !hasSeenUpdateNotification && (
|
|
1060
|
+
<span className="absolute top-0 right-0 h-3 w-3 rounded-full bg-red-500 border-2 border-background" />
|
|
1061
|
+
)}
|
|
1062
|
+
<span className="sr-only">More options</span>
|
|
1063
|
+
</Button>
|
|
1064
|
+
</div>
|
|
1065
|
+
</DropdownMenuTrigger>
|
|
1066
|
+
<DropdownMenuContent align="end" className="w-[min(90vw,384px)] max-h-[80vh] overflow-y-auto">
|
|
1067
|
+
{/* 使用者資訊區塊 */}
|
|
1068
|
+
<div className="flex items-center gap-3 px-2 py-3">
|
|
1069
|
+
{/* 使用者頭像 */}
|
|
1070
|
+
<div className="flex-shrink-0">
|
|
1071
|
+
{user?.photoURL ? (
|
|
1072
|
+
<Image
|
|
1073
|
+
src={user.photoURL}
|
|
1074
|
+
alt={user.displayName || 'User'}
|
|
1075
|
+
width={40}
|
|
1076
|
+
height={40}
|
|
1077
|
+
className="rounded-full"
|
|
1078
|
+
/>
|
|
1079
|
+
) : (
|
|
1080
|
+
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
|
|
1081
|
+
<User className="h-5 w-5 text-muted-foreground" />
|
|
1082
|
+
</div>
|
|
1083
|
+
)}
|
|
1084
|
+
</div>
|
|
1085
|
+
|
|
1086
|
+
{/* 使用者名稱和 Email */}
|
|
1087
|
+
<div className="flex-1 min-w-0">
|
|
1088
|
+
<p className="text-sm font-medium truncate">
|
|
1089
|
+
{user?.displayName || user?.email?.split('@')[0] || '使用者'}
|
|
1090
|
+
{/* 🎯 顯示方案名稱(如果有) */}
|
|
1091
|
+
{planName && <span className="text-muted-foreground font-normal"> ({planName})</span>}
|
|
1092
|
+
</p>
|
|
1093
|
+
{user?.email && (
|
|
1094
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
1095
|
+
{user.email}
|
|
1096
|
+
</p>
|
|
1097
|
+
)}
|
|
1098
|
+
</div>
|
|
1099
|
+
</div>
|
|
1100
|
+
<DropdownMenuSeparator />
|
|
1101
|
+
|
|
1102
|
+
{/* 1️⃣ 對話 */}
|
|
1103
|
+
<Collapsible
|
|
1104
|
+
open={menuSections.chat}
|
|
1105
|
+
onOpenChange={(open) => setMenuSections(prev => ({ ...prev, chat: open }))}
|
|
1106
|
+
>
|
|
1107
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full px-2 py-2 text-sm hover:bg-accent rounded-sm">
|
|
1108
|
+
<span className="font-medium">對話</span>
|
|
1109
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${menuSections.chat ? 'rotate-180' : ''}`} />
|
|
1110
|
+
</CollapsibleTrigger>
|
|
1111
|
+
<CollapsibleContent className="pl-2">
|
|
1112
|
+
<DropdownMenuItem onClick={handleNewChat} className="cursor-pointer gap-2">
|
|
1113
|
+
<MessageSquarePlus className="h-4 w-4" />
|
|
1114
|
+
<span>新對話</span>
|
|
1115
|
+
</DropdownMenuItem>
|
|
1116
|
+
<DropdownMenuItem
|
|
1117
|
+
onClick={() => {
|
|
1118
|
+
refetchConversations();
|
|
1119
|
+
setIsConversationListOpen(true);
|
|
1120
|
+
}}
|
|
1121
|
+
className="cursor-pointer gap-2"
|
|
1122
|
+
>
|
|
1123
|
+
<History className="h-4 w-4" />
|
|
1124
|
+
<span>歷史對話</span>
|
|
1125
|
+
</DropdownMenuItem>
|
|
1126
|
+
<DropdownMenuItem
|
|
1127
|
+
onClick={handleShareToEmail}
|
|
1128
|
+
className="cursor-pointer gap-2"
|
|
1129
|
+
disabled={isSharingEmail || messages.length === 0}
|
|
1130
|
+
>
|
|
1131
|
+
<Mail className="h-4 w-4" />
|
|
1132
|
+
<span>{isSharingEmail ? '發送中...' : '分享 Email'}</span>
|
|
1133
|
+
</DropdownMenuItem>
|
|
1134
|
+
<DropdownMenuItem
|
|
1135
|
+
onClick={handleCopyAllMessages}
|
|
1136
|
+
className="cursor-pointer gap-2"
|
|
1137
|
+
disabled={messages.length === 0}
|
|
1138
|
+
>
|
|
1139
|
+
{isCopiedAll ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
|
1140
|
+
<span>{isCopiedAll ? '已複製' : '複製對話'}</span>
|
|
1141
|
+
</DropdownMenuItem>
|
|
1142
|
+
<DropdownMenuItem onClick={handleOpenClearFilesDialog} className="cursor-pointer gap-2">
|
|
1143
|
+
<Trash2 className="h-4 w-4" />
|
|
1144
|
+
<span>清除檔案</span>
|
|
1145
|
+
</DropdownMenuItem>
|
|
1146
|
+
</CollapsibleContent>
|
|
1147
|
+
</Collapsible>
|
|
1148
|
+
|
|
1149
|
+
{/* 2️⃣ 孵化器 */}
|
|
1150
|
+
<Collapsible
|
|
1151
|
+
open={menuSections.incubator}
|
|
1152
|
+
onOpenChange={(open) => setMenuSections(prev => ({ ...prev, incubator: open }))}
|
|
1153
|
+
>
|
|
1154
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full px-2 py-2 text-sm hover:bg-accent rounded-sm">
|
|
1155
|
+
<span className="font-medium">孵化器</span>
|
|
1156
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${menuSections.incubator ? 'rotate-180' : ''}`} />
|
|
1157
|
+
</CollapsibleTrigger>
|
|
1158
|
+
<CollapsibleContent className="pl-2">
|
|
1159
|
+
<DropdownMenuItem onClick={() => setIsIncubatorOpen(true)} className="cursor-pointer gap-2">
|
|
1160
|
+
<Sparkles className="h-4 w-4" />
|
|
1161
|
+
<span>指揮家</span>
|
|
1162
|
+
</DropdownMenuItem>
|
|
1163
|
+
<DropdownMenuItem onClick={() => setIsSkillsDrawerOpen(true)} className="cursor-pointer gap-2">
|
|
1164
|
+
<Hammer className="h-4 w-4" />
|
|
1165
|
+
<span>技能</span>
|
|
1166
|
+
</DropdownMenuItem>
|
|
1167
|
+
</CollapsibleContent>
|
|
1168
|
+
</Collapsible>
|
|
1169
|
+
|
|
1170
|
+
{/* 3️⃣ GDrive */}
|
|
1171
|
+
<Collapsible
|
|
1172
|
+
open={menuSections.gdrive}
|
|
1173
|
+
onOpenChange={(open) => setMenuSections(prev => ({ ...prev, gdrive: open }))}
|
|
1174
|
+
>
|
|
1175
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full px-2 py-2 text-sm hover:bg-accent rounded-sm">
|
|
1176
|
+
<div className="flex items-center gap-2">
|
|
1177
|
+
<span className="font-medium">GDrive</span>
|
|
1178
|
+
{gdriveFolders.length > 0 && (
|
|
1179
|
+
<span className="text-xs text-muted-foreground">{gdriveFolders.length} 個</span>
|
|
1180
|
+
)}
|
|
1181
|
+
</div>
|
|
1182
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${menuSections.gdrive ? 'rotate-180' : ''}`} />
|
|
1183
|
+
</CollapsibleTrigger>
|
|
1184
|
+
<CollapsibleContent className="pl-2">
|
|
1185
|
+
{/* 資料夾列表 */}
|
|
1186
|
+
{isGdriveFoldersLoading ? (
|
|
1187
|
+
<div className="px-2 py-2 text-xs text-muted-foreground">載入中...</div>
|
|
1188
|
+
) : gdriveFolders.length === 0 ? (
|
|
1189
|
+
<div className="px-2 py-2 text-xs text-muted-foreground">尚無資料夾</div>
|
|
1190
|
+
) : (
|
|
1191
|
+
gdriveFolders.slice(0, 3).map((folder) => (
|
|
1192
|
+
<DropdownMenuItem
|
|
1193
|
+
key={folder.id}
|
|
1194
|
+
onClick={() => toggleGdriveSelect(folder.id)}
|
|
1195
|
+
className="cursor-pointer gap-2"
|
|
1196
|
+
>
|
|
1197
|
+
<div className={`w-3 h-3 rounded border flex items-center justify-center ${
|
|
1198
|
+
folder.selected ? 'bg-primary border-primary' : 'border-input'
|
|
1199
|
+
}`}>
|
|
1200
|
+
{folder.selected && <Check className="h-2 w-2 text-primary-foreground" />}
|
|
1201
|
+
</div>
|
|
1202
|
+
<span className="truncate flex-1">{folder.alias}</span>
|
|
1203
|
+
</DropdownMenuItem>
|
|
1204
|
+
))
|
|
1205
|
+
)}
|
|
1206
|
+
{gdriveFolders.length > 3 && (
|
|
1207
|
+
<div className="px-2 py-1 text-xs text-muted-foreground">
|
|
1208
|
+
+{gdriveFolders.length - 3} 個資料夾
|
|
1209
|
+
</div>
|
|
1210
|
+
)}
|
|
1211
|
+
<DropdownMenuSeparator />
|
|
1212
|
+
<DropdownMenuItem
|
|
1213
|
+
onClick={async () => {
|
|
1214
|
+
try {
|
|
1215
|
+
await syncAllGdrive();
|
|
1216
|
+
await refetchGdriveFolders();
|
|
1217
|
+
toast({ title: "同步完成" });
|
|
1218
|
+
} catch (err: any) {
|
|
1219
|
+
toast({ title: "同步失敗", description: err.message, variant: "destructive" });
|
|
1220
|
+
}
|
|
1221
|
+
}}
|
|
1222
|
+
className="cursor-pointer gap-2"
|
|
1223
|
+
disabled={isGdriveSyncing || gdriveFolders.length === 0}
|
|
1224
|
+
>
|
|
1225
|
+
<RefreshCw className={`h-4 w-4 ${isGdriveSyncing ? 'animate-spin' : ''}`} />
|
|
1226
|
+
<span>{isGdriveSyncing ? '同步中...' : '同步全部'}</span>
|
|
1227
|
+
</DropdownMenuItem>
|
|
1228
|
+
<DropdownMenuItem
|
|
1229
|
+
onClick={() => setIsGdriveAddDialogOpen(true)}
|
|
1230
|
+
className="cursor-pointer gap-2"
|
|
1231
|
+
>
|
|
1232
|
+
<Plus className="h-4 w-4" />
|
|
1233
|
+
<span>新增資料夾</span>
|
|
1234
|
+
</DropdownMenuItem>
|
|
1235
|
+
</CollapsibleContent>
|
|
1236
|
+
</Collapsible>
|
|
1237
|
+
|
|
1238
|
+
{/* 4️⃣ GitHub */}
|
|
1239
|
+
<Collapsible
|
|
1240
|
+
open={menuSections.github}
|
|
1241
|
+
onOpenChange={(open) => setMenuSections(prev => ({ ...prev, github: open }))}
|
|
1242
|
+
>
|
|
1243
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full px-2 py-2 text-sm hover:bg-accent rounded-sm">
|
|
1244
|
+
<div className="flex items-center gap-2">
|
|
1245
|
+
<span className="font-medium">GitHub</span>
|
|
1246
|
+
{isGitHubConnected && connectedRepos.length > 0 && (
|
|
1247
|
+
<span className="text-xs text-muted-foreground">{connectedRepos.length} 個</span>
|
|
1248
|
+
)}
|
|
1249
|
+
</div>
|
|
1250
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${menuSections.github ? 'rotate-180' : ''}`} />
|
|
1251
|
+
</CollapsibleTrigger>
|
|
1252
|
+
<CollapsibleContent className="pl-2">
|
|
1253
|
+
{isGitHubLoading ? (
|
|
1254
|
+
<div className="px-2 py-2 text-xs text-muted-foreground">載入中...</div>
|
|
1255
|
+
) : !isGitHubConnected ? (
|
|
1256
|
+
<DropdownMenuItem
|
|
1257
|
+
onClick={() => connectGitHub()}
|
|
1258
|
+
className="cursor-pointer gap-2"
|
|
1259
|
+
>
|
|
1260
|
+
<Link2 className="h-4 w-4" />
|
|
1261
|
+
<span>連結 GitHub</span>
|
|
1262
|
+
</DropdownMenuItem>
|
|
1263
|
+
) : (
|
|
1264
|
+
<>
|
|
1265
|
+
{/* 已下載的 Repo 列表(點擊同步,右側可刪除) */}
|
|
1266
|
+
{connectedRepos.length > 0 && (
|
|
1267
|
+
<>
|
|
1268
|
+
<div className="px-2 py-1 text-xs text-muted-foreground font-medium">已下載</div>
|
|
1269
|
+
{connectedRepos.slice(0, 3).map((repo) => {
|
|
1270
|
+
const isSyncing = syncingRepoName === repo.repoFullName;
|
|
1271
|
+
const isDeleting = deletingRepoName === repo.repoFullName;
|
|
1272
|
+
const isBusy = syncingRepoName !== null || deletingRepoName !== null;
|
|
1273
|
+
return (
|
|
1274
|
+
<div key={repo.id} className="flex items-center gap-1 px-2 py-1.5 text-sm hover:bg-accent rounded-sm group">
|
|
1275
|
+
<button
|
|
1276
|
+
onClick={async () => {
|
|
1277
|
+
try {
|
|
1278
|
+
setSyncingRepoName(repo.repoFullName);
|
|
1279
|
+
await syncGitHubRepo(repo.repoFullName);
|
|
1280
|
+
toast({ title: "同步完成", description: `${repo.repoFullName} 已更新至最新版本` });
|
|
1281
|
+
} catch (err: any) {
|
|
1282
|
+
toast({ title: "同步失敗", description: err.message, variant: "destructive" });
|
|
1283
|
+
} finally {
|
|
1284
|
+
setSyncingRepoName(null);
|
|
1285
|
+
}
|
|
1286
|
+
}}
|
|
1287
|
+
className="flex items-center gap-2 flex-1 min-w-0 cursor-pointer disabled:opacity-50"
|
|
1288
|
+
disabled={isBusy}
|
|
1289
|
+
title="點擊同步最新版本"
|
|
1290
|
+
>
|
|
1291
|
+
{isSyncing || repo.status === 'syncing' ? (
|
|
1292
|
+
<RefreshCw className="h-4 w-4 shrink-0 animate-spin text-blue-500" />
|
|
1293
|
+
) : repo.status === 'error' ? (
|
|
1294
|
+
<AlertCircle className="h-4 w-4 shrink-0 text-destructive" />
|
|
1295
|
+
) : (
|
|
1296
|
+
<CheckCircle2 className="h-4 w-4 shrink-0 text-green-600" />
|
|
1297
|
+
)}
|
|
1298
|
+
<span className="truncate">{repo.repoFullName}</span>
|
|
1299
|
+
{(isSyncing || repo.status === 'syncing') && <span className="text-xs text-blue-500 shrink-0">同步中</span>}
|
|
1300
|
+
{repo.status === 'error' && !isSyncing && <span className="text-xs text-destructive shrink-0">錯誤</span>}
|
|
1301
|
+
</button>
|
|
1302
|
+
<button
|
|
1303
|
+
onClick={async (e) => {
|
|
1304
|
+
e.stopPropagation();
|
|
1305
|
+
if (!confirm(`確定要移除 ${repo.repoFullName} 嗎?\n\n這會刪除本地下載的檔案,但不會影響 GitHub 上的原始倉庫。`)) return;
|
|
1306
|
+
try {
|
|
1307
|
+
setDeletingRepoName(repo.repoFullName);
|
|
1308
|
+
await deleteGitHubRepo(repo.repoFullName);
|
|
1309
|
+
toast({ title: "已移除", description: `${repo.repoFullName} 已從本地刪除` });
|
|
1310
|
+
} catch (err: any) {
|
|
1311
|
+
toast({ title: "移除失敗", description: err.message, variant: "destructive" });
|
|
1312
|
+
} finally {
|
|
1313
|
+
setDeletingRepoName(null);
|
|
1314
|
+
}
|
|
1315
|
+
}}
|
|
1316
|
+
className="p-1 opacity-0 group-hover:opacity-100 hover:bg-destructive/10 hover:text-destructive rounded transition-opacity disabled:opacity-50"
|
|
1317
|
+
disabled={isBusy}
|
|
1318
|
+
title="移除此 Repository"
|
|
1319
|
+
>
|
|
1320
|
+
{isDeleting ? <RefreshCw className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />}
|
|
1321
|
+
</button>
|
|
1322
|
+
</div>
|
|
1323
|
+
);
|
|
1324
|
+
})}
|
|
1325
|
+
{connectedRepos.length > 3 && (
|
|
1326
|
+
<div className="px-2 py-1 text-xs text-muted-foreground">
|
|
1327
|
+
+{connectedRepos.length - 3} 個 Repo
|
|
1328
|
+
</div>
|
|
1329
|
+
)}
|
|
1330
|
+
</>
|
|
1331
|
+
)}
|
|
1332
|
+
|
|
1333
|
+
{/* 尚未下載的 Repo 列表(點擊下載) */}
|
|
1334
|
+
{availableRepos.length > 0 && (
|
|
1335
|
+
<>
|
|
1336
|
+
{connectedRepos.length > 0 && <DropdownMenuSeparator />}
|
|
1337
|
+
<div className="px-2 py-1 text-xs text-muted-foreground font-medium">尚未下載 ({availableRepos.length})</div>
|
|
1338
|
+
{availableRepos.slice(0, 3).map((repo) => {
|
|
1339
|
+
const isCloning = cloningRepoName === repo.fullName;
|
|
1340
|
+
const isBusy = cloningRepoName !== null;
|
|
1341
|
+
return (
|
|
1342
|
+
<DropdownMenuItem
|
|
1343
|
+
key={repo.id}
|
|
1344
|
+
onClick={async () => {
|
|
1345
|
+
try {
|
|
1346
|
+
setCloningRepoName(repo.fullName);
|
|
1347
|
+
toast({ title: "下載中...", description: `正在下載 ${repo.fullName}` });
|
|
1348
|
+
await cloneGitHubRepo(repo.fullName);
|
|
1349
|
+
toast({ title: "下載完成", description: `${repo.fullName} 已可在對話中使用` });
|
|
1350
|
+
} catch (err: any) {
|
|
1351
|
+
toast({ title: "下載失敗", description: err.message, variant: "destructive" });
|
|
1352
|
+
} finally {
|
|
1353
|
+
setCloningRepoName(null);
|
|
1354
|
+
}
|
|
1355
|
+
}}
|
|
1356
|
+
className="cursor-pointer gap-2"
|
|
1357
|
+
disabled={isBusy}
|
|
1358
|
+
>
|
|
1359
|
+
{isCloning ? (
|
|
1360
|
+
<RefreshCw className="h-4 w-4 animate-spin text-blue-500" />
|
|
1361
|
+
) : (
|
|
1362
|
+
<Download className="h-4 w-4 text-muted-foreground" />
|
|
1363
|
+
)}
|
|
1364
|
+
<span className="truncate flex-1">{repo.fullName}</span>
|
|
1365
|
+
</DropdownMenuItem>
|
|
1366
|
+
);
|
|
1367
|
+
})}
|
|
1368
|
+
{availableRepos.length > 3 && (
|
|
1369
|
+
<DropdownMenuItem
|
|
1370
|
+
onClick={() => setShowGitHubReposModal(true)}
|
|
1371
|
+
className="cursor-pointer gap-2 text-primary"
|
|
1372
|
+
>
|
|
1373
|
+
<FolderGit2 className="h-4 w-4" />
|
|
1374
|
+
<span>查看全部 {availableRepos.length} 個</span>
|
|
1375
|
+
</DropdownMenuItem>
|
|
1376
|
+
)}
|
|
1377
|
+
</>
|
|
1378
|
+
)}
|
|
1379
|
+
|
|
1380
|
+
{/* 無任何 Repo 時顯示 */}
|
|
1381
|
+
{connectedRepos.length === 0 && availableRepos.length === 0 && (
|
|
1382
|
+
<div className="px-2 py-2 text-xs text-muted-foreground">尚無 Repository</div>
|
|
1383
|
+
)}
|
|
1384
|
+
|
|
1385
|
+
<DropdownMenuSeparator />
|
|
1386
|
+
<DropdownMenuItem
|
|
1387
|
+
onClick={() => connectGitHub()}
|
|
1388
|
+
className="cursor-pointer gap-2"
|
|
1389
|
+
>
|
|
1390
|
+
<Plus className="h-4 w-4" />
|
|
1391
|
+
<span>新增 Repository</span>
|
|
1392
|
+
</DropdownMenuItem>
|
|
1393
|
+
<DropdownMenuItem
|
|
1394
|
+
onClick={() => disconnectGitHub()}
|
|
1395
|
+
className="cursor-pointer gap-2 text-destructive focus:text-destructive"
|
|
1396
|
+
>
|
|
1397
|
+
<Unlink className="h-4 w-4" />
|
|
1398
|
+
<span>中斷連結</span>
|
|
1399
|
+
</DropdownMenuItem>
|
|
1400
|
+
</>
|
|
1401
|
+
)}
|
|
1402
|
+
</CollapsibleContent>
|
|
1403
|
+
</Collapsible>
|
|
1404
|
+
|
|
1405
|
+
{/* 5️⃣ AI 模型 */}
|
|
1406
|
+
<Collapsible
|
|
1407
|
+
open={menuSections.model}
|
|
1408
|
+
onOpenChange={(open) => setMenuSections(prev => ({ ...prev, model: open }))}
|
|
1409
|
+
>
|
|
1410
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full px-2 py-2 text-sm hover:bg-accent rounded-sm">
|
|
1411
|
+
<div className="flex items-center gap-2">
|
|
1412
|
+
<span className="font-medium">AI 模型</span>
|
|
1413
|
+
<span className="text-xs text-muted-foreground">{getModelInfo()?.name}</span>
|
|
1414
|
+
</div>
|
|
1415
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${menuSections.model ? 'rotate-180' : ''}`} />
|
|
1416
|
+
</CollapsibleTrigger>
|
|
1417
|
+
<CollapsibleContent className="px-2 pb-2">
|
|
1418
|
+
<div className="flex gap-1">
|
|
1419
|
+
{claudeModels.map((model) => (
|
|
1420
|
+
<button
|
|
1421
|
+
key={model.id}
|
|
1422
|
+
onClick={() => setClaudeModel(model.id)}
|
|
1423
|
+
className={`flex-1 px-2 py-1.5 text-xs rounded-md transition-colors ${
|
|
1424
|
+
claudeModel === model.id
|
|
1425
|
+
? 'bg-primary text-primary-foreground'
|
|
1426
|
+
: 'bg-muted hover:bg-muted/80'
|
|
1427
|
+
}`}
|
|
1428
|
+
title={model.description}
|
|
1429
|
+
>
|
|
1430
|
+
{model.name}
|
|
1431
|
+
</button>
|
|
1432
|
+
))}
|
|
1433
|
+
</div>
|
|
1434
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
1435
|
+
{getModelInfo()?.description}
|
|
1436
|
+
</p>
|
|
1437
|
+
</CollapsibleContent>
|
|
1438
|
+
</Collapsible>
|
|
1439
|
+
|
|
1440
|
+
{/* 5️⃣ 關於 */}
|
|
1441
|
+
<Collapsible
|
|
1442
|
+
open={menuSections.about}
|
|
1443
|
+
onOpenChange={(open) => setMenuSections(prev => ({ ...prev, about: open }))}
|
|
1444
|
+
>
|
|
1445
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full px-2 py-2 text-sm hover:bg-accent rounded-sm">
|
|
1446
|
+
<span className="font-medium">關於</span>
|
|
1447
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${menuSections.about ? 'rotate-180' : ''}`} />
|
|
1448
|
+
</CollapsibleTrigger>
|
|
1449
|
+
<CollapsibleContent className="pl-2">
|
|
1450
|
+
<DropdownMenuItem onClick={handleFeatures} className="cursor-pointer gap-2">
|
|
1451
|
+
<Star className="h-4 w-4" />
|
|
1452
|
+
<span>特色介紹</span>
|
|
1453
|
+
</DropdownMenuItem>
|
|
1454
|
+
<DropdownMenuItem onClick={handleAbout} className="cursor-pointer gap-2">
|
|
1455
|
+
<Info className="h-4 w-4" />
|
|
1456
|
+
<span>聯絡我們</span>
|
|
1457
|
+
</DropdownMenuItem>
|
|
1458
|
+
{/* 版本更新 - 僅在有新版本時顯示 */}
|
|
1459
|
+
{updateAvailable && (
|
|
1460
|
+
<DropdownMenuItem
|
|
1461
|
+
onClick={async () => { await openAppStore(); }}
|
|
1462
|
+
className="cursor-pointer gap-2 text-blue-600 dark:text-blue-400"
|
|
1463
|
+
>
|
|
1464
|
+
<RefreshCw className="h-4 w-4" />
|
|
1465
|
+
<span>{latestVersion ? `更新 v${latestVersion}` : '版本更新'}</span>
|
|
1466
|
+
</DropdownMenuItem>
|
|
1467
|
+
)}
|
|
1468
|
+
</CollapsibleContent>
|
|
1469
|
+
</Collapsible>
|
|
1470
|
+
|
|
1471
|
+
<DropdownMenuSeparator />
|
|
1472
|
+
|
|
1473
|
+
{/* 刪除帳號 - 僅在 iOS/Android App 中顯示 */}
|
|
1474
|
+
{isCapacitor && (
|
|
1475
|
+
<DropdownMenuItem onClick={handleDeleteAccount} className="cursor-pointer gap-2 text-destructive focus:text-destructive">
|
|
1476
|
+
<UserX className="h-4 w-4" />
|
|
1477
|
+
<span>刪除帳號</span>
|
|
1478
|
+
</DropdownMenuItem>
|
|
1479
|
+
)}
|
|
1480
|
+
<DropdownMenuItem onClick={handleSignOut} className="cursor-pointer gap-2">
|
|
1481
|
+
<LogOut className="h-4 w-4" />
|
|
1482
|
+
<span>登出</span>
|
|
1483
|
+
</DropdownMenuItem>
|
|
1484
|
+
</DropdownMenuContent>
|
|
1485
|
+
</DropdownMenu>
|
|
1486
|
+
|
|
1487
|
+
<ScrollArea
|
|
1488
|
+
ref={scrollAreaRef}
|
|
1489
|
+
className="flex-1"
|
|
1490
|
+
onScroll={handleScroll}
|
|
1491
|
+
style={{
|
|
1492
|
+
overflow: 'auto',
|
|
1493
|
+
WebkitOverflowScrolling: 'touch',
|
|
1494
|
+
// Capacitor App: 確保內容從 status bar 下方開始
|
|
1495
|
+
...(isCapacitor && {
|
|
1496
|
+
paddingTop: 'var(--safe-area-inset-top)'
|
|
1497
|
+
})
|
|
1498
|
+
}}
|
|
1499
|
+
>
|
|
1500
|
+
<div className="mx-auto w-full max-w-6xl px-4 md:px-6" style={{
|
|
1501
|
+
paddingTop: isCapacitor ? '1rem' : 'calc(var(--safe-area-inset-top) + 1rem)',
|
|
1502
|
+
paddingBottom: isCapacitor ? '160px' : '120px' // 預留輸入框高度空間
|
|
1503
|
+
}}>
|
|
1504
|
+
{/* 自動顯示歡迎訊息(僅在初次對話前) */}
|
|
1505
|
+
{shouldShowWelcome && messages.length === 0 && (
|
|
1506
|
+
<ChatMessage
|
|
1507
|
+
key={welcomeMessage.id}
|
|
1508
|
+
message={welcomeMessage}
|
|
1509
|
+
onSendMessage={handleSendMessage}
|
|
1510
|
+
isStreaming={false}
|
|
1511
|
+
/>
|
|
1512
|
+
)}
|
|
1513
|
+
|
|
1514
|
+
{/* 一般對話訊息 */}
|
|
1515
|
+
{messages.map((message, index) => (
|
|
1516
|
+
<ChatMessage
|
|
1517
|
+
key={message.id}
|
|
1518
|
+
message={message}
|
|
1519
|
+
onSendMessage={handleSendMessage}
|
|
1520
|
+
isStreaming={message.id === streamingMessageId}
|
|
1521
|
+
previousMessage={index > 0 ? messages[index - 1] : undefined}
|
|
1522
|
+
/>
|
|
1523
|
+
))}
|
|
1524
|
+
<div ref={messagesEndRef} />
|
|
1525
|
+
</div>
|
|
1526
|
+
</ScrollArea>
|
|
1527
|
+
|
|
1528
|
+
{/* 浮動「回到底部」按鈕 */}
|
|
1529
|
+
<ScrollToBottomButton
|
|
1530
|
+
visible={!isAtBottom}
|
|
1531
|
+
onClick={scrollToBottom}
|
|
1532
|
+
/>
|
|
1533
|
+
<div className="border-t flex-shrink-0 bg-background" style={{
|
|
1534
|
+
paddingBottom: isCapacitor
|
|
1535
|
+
? isAndroid
|
|
1536
|
+
? 'max(var(--safe-area-inset-bottom), var(--android-nav-bar-height))' // Android: 確保至少 48px
|
|
1537
|
+
: 'var(--safe-area-inset-bottom)' // iOS: 使用系統提供的 safe area
|
|
1538
|
+
: 'var(--safe-area-inset-bottom)'
|
|
1539
|
+
}}>
|
|
1540
|
+
<div className="mx-auto w-full max-w-6xl px-4 md:px-6">
|
|
1541
|
+
<ChatInput
|
|
1542
|
+
onSend={handleSendMessage}
|
|
1543
|
+
onStop={handleStopResponse}
|
|
1544
|
+
disabled={isLoading}
|
|
1545
|
+
isLoading={isLoading}
|
|
1546
|
+
isStopping={isStopping}
|
|
1547
|
+
placeholder=""
|
|
1548
|
+
transcriptionModel={transcriptionModel}
|
|
1549
|
+
/>
|
|
1550
|
+
</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
|
|
1553
|
+
{/* 指揮家孵化器 Modal */}
|
|
1554
|
+
<BotrunIncubator
|
|
1555
|
+
open={isIncubatorOpen}
|
|
1556
|
+
onOpenChange={setIsIncubatorOpen}
|
|
1557
|
+
onNewChat={handleNewChat}
|
|
1558
|
+
/>
|
|
1559
|
+
|
|
1560
|
+
{/* 技能孵化器 Drawer */}
|
|
1561
|
+
<SkillsManagerDrawer
|
|
1562
|
+
open={isSkillsDrawerOpen}
|
|
1563
|
+
onOpenChange={setIsSkillsDrawerOpen}
|
|
1564
|
+
onEdit={handleEditSkill}
|
|
1565
|
+
onCreate={handleCreateSkill}
|
|
1566
|
+
onSkillsChange={handleSkillsChange}
|
|
1567
|
+
/>
|
|
1568
|
+
|
|
1569
|
+
{/* 技能編輯器 */}
|
|
1570
|
+
<SkillsEditor
|
|
1571
|
+
open={skillsEditorOpen}
|
|
1572
|
+
onOpenChange={setSkillsEditorOpen}
|
|
1573
|
+
skill={skillToEdit}
|
|
1574
|
+
onSaved={handleSkillsChange}
|
|
1575
|
+
/>
|
|
1576
|
+
|
|
1577
|
+
{/* Google Drive 新增資料夾對話框 */}
|
|
1578
|
+
<GdriveAddDialog
|
|
1579
|
+
open={isGdriveAddDialogOpen}
|
|
1580
|
+
onOpenChange={setIsGdriveAddDialogOpen}
|
|
1581
|
+
onAdd={async (url, alias, type) => {
|
|
1582
|
+
try {
|
|
1583
|
+
await addGdriveFolder(url, alias, type);
|
|
1584
|
+
toast({ title: "資料夾已新增", description: "請點擊同步按鈕開始下載" });
|
|
1585
|
+
setIsGdriveAddDialogOpen(false);
|
|
1586
|
+
} catch (err: any) {
|
|
1587
|
+
toast({ title: "新增失敗", description: err.message, variant: "destructive" });
|
|
1588
|
+
}
|
|
1589
|
+
}}
|
|
1590
|
+
history={gdriveHistory}
|
|
1591
|
+
/>
|
|
1592
|
+
|
|
1593
|
+
{/* GitHub Repos Modal */}
|
|
1594
|
+
{showGitHubReposModal && (
|
|
1595
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
1596
|
+
{/* 背景遮罩 */}
|
|
1597
|
+
<div
|
|
1598
|
+
className="fixed inset-0 bg-black/50"
|
|
1599
|
+
onClick={() => setShowGitHubReposModal(false)}
|
|
1600
|
+
/>
|
|
1601
|
+
{/* Modal 內容 */}
|
|
1602
|
+
<div className="relative z-50 w-full max-w-md mx-4 bg-background rounded-lg shadow-lg border max-h-[80vh] flex flex-col">
|
|
1603
|
+
{/* 標題列 */}
|
|
1604
|
+
<div className="flex items-center justify-between p-4 border-b">
|
|
1605
|
+
<div className="flex items-center gap-2">
|
|
1606
|
+
<FolderGit2 className="h-5 w-5" />
|
|
1607
|
+
<h2 className="text-lg font-semibold">GitHub Repositories</h2>
|
|
1608
|
+
</div>
|
|
1609
|
+
<button
|
|
1610
|
+
onClick={() => setShowGitHubReposModal(false)}
|
|
1611
|
+
className="p-1 rounded-md hover:bg-accent"
|
|
1612
|
+
>
|
|
1613
|
+
<X className="h-5 w-5" />
|
|
1614
|
+
</button>
|
|
1615
|
+
</div>
|
|
1616
|
+
{/* 搜尋框 */}
|
|
1617
|
+
<div className="p-3 border-b">
|
|
1618
|
+
<div className="relative">
|
|
1619
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
1620
|
+
<input
|
|
1621
|
+
type="text"
|
|
1622
|
+
placeholder="搜尋 Repository..."
|
|
1623
|
+
className="w-full pl-9 pr-4 py-2 text-sm border rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1624
|
+
id="github-repos-search"
|
|
1625
|
+
onChange={(e) => {
|
|
1626
|
+
const searchValue = e.target.value.toLowerCase();
|
|
1627
|
+
const items = document.querySelectorAll('[data-github-repo]');
|
|
1628
|
+
items.forEach((item) => {
|
|
1629
|
+
const repoName = item.getAttribute('data-github-repo')?.toLowerCase() || '';
|
|
1630
|
+
(item as HTMLElement).style.display = repoName.includes(searchValue) ? 'flex' : 'none';
|
|
1631
|
+
});
|
|
1632
|
+
}}
|
|
1633
|
+
/>
|
|
1634
|
+
</div>
|
|
1635
|
+
</div>
|
|
1636
|
+
{/* Repo 列表 */}
|
|
1637
|
+
<div className="flex-1 overflow-y-auto p-2">
|
|
1638
|
+
{availableRepos.map((repo) => (
|
|
1639
|
+
<button
|
|
1640
|
+
key={repo.id}
|
|
1641
|
+
data-github-repo={repo.fullName}
|
|
1642
|
+
onClick={async () => {
|
|
1643
|
+
try {
|
|
1644
|
+
setIsGitHubCloning(true);
|
|
1645
|
+
await cloneGitHubRepo(repo.fullName);
|
|
1646
|
+
toast({ title: `已 Clone ${repo.fullName}` });
|
|
1647
|
+
setShowGitHubReposModal(false);
|
|
1648
|
+
} catch (err: any) {
|
|
1649
|
+
toast({ title: "Clone 失敗", description: err.message, variant: "destructive" });
|
|
1650
|
+
} finally {
|
|
1651
|
+
setIsGitHubCloning(false);
|
|
1652
|
+
}
|
|
1653
|
+
}}
|
|
1654
|
+
disabled={isGitHubCloning}
|
|
1655
|
+
className="flex items-center gap-3 w-full p-3 rounded-md hover:bg-accent text-left disabled:opacity-50"
|
|
1656
|
+
>
|
|
1657
|
+
<Github className="h-4 w-4 flex-shrink-0" />
|
|
1658
|
+
<div className="flex-1 min-w-0">
|
|
1659
|
+
<div className="font-medium truncate">{repo.name}</div>
|
|
1660
|
+
<div className="text-xs text-muted-foreground truncate">{repo.fullName}</div>
|
|
1661
|
+
</div>
|
|
1662
|
+
<Plus className="h-4 w-4 flex-shrink-0 text-primary" />
|
|
1663
|
+
</button>
|
|
1664
|
+
))}
|
|
1665
|
+
</div>
|
|
1666
|
+
{/* 底部資訊 */}
|
|
1667
|
+
<div className="p-3 border-t text-xs text-muted-foreground text-center">
|
|
1668
|
+
共 {availableRepos.length} 個可連結的 Repository
|
|
1669
|
+
</div>
|
|
1670
|
+
</div>
|
|
1671
|
+
</div>
|
|
1672
|
+
)}
|
|
1673
|
+
|
|
1674
|
+
{/* 歷史對話列表 */}
|
|
1675
|
+
<ConversationList
|
|
1676
|
+
open={isConversationListOpen}
|
|
1677
|
+
onOpenChange={setIsConversationListOpen}
|
|
1678
|
+
conversations={conversations}
|
|
1679
|
+
currentConversationId={currentConversationId}
|
|
1680
|
+
isLoading={isConversationsLoading}
|
|
1681
|
+
error={conversationsError}
|
|
1682
|
+
onSelect={handleSelectConversation}
|
|
1683
|
+
onNewChat={handleNewChat}
|
|
1684
|
+
onUpdateTitle={updateConversationTitle}
|
|
1685
|
+
onDelete={deleteConversation}
|
|
1686
|
+
/>
|
|
1687
|
+
|
|
1688
|
+
{/* 清除檔案確認對話框 */}
|
|
1689
|
+
<AlertDialog open={clearFilesDialogOpen} onOpenChange={setClearFilesDialogOpen}>
|
|
1690
|
+
<AlertDialogContent>
|
|
1691
|
+
<AlertDialogHeader>
|
|
1692
|
+
<AlertDialogTitle>清除上傳檔案</AlertDialogTitle>
|
|
1693
|
+
<AlertDialogDescription asChild>
|
|
1694
|
+
<div>
|
|
1695
|
+
{isLoadingStats ? (
|
|
1696
|
+
<span>載入中...</span>
|
|
1697
|
+
) : fileStats ? (
|
|
1698
|
+
<div className="space-y-2">
|
|
1699
|
+
<p>即將清除以下檔案,此操作無法復原:</p>
|
|
1700
|
+
<ul className="list-disc list-inside text-sm space-y-1 ml-2">
|
|
1701
|
+
{fileStats.uploads.count > 0 && (
|
|
1702
|
+
<li>上傳檔案:{fileStats.uploads.count} 個 ({formatFileSize(fileStats.uploads.size)})</li>
|
|
1703
|
+
)}
|
|
1704
|
+
{fileStats.pdfPages.count > 0 && (
|
|
1705
|
+
<li>PDF 拆解頁面:{fileStats.pdfPages.count} 個 ({formatFileSize(fileStats.pdfPages.size)})</li>
|
|
1706
|
+
)}
|
|
1707
|
+
</ul>
|
|
1708
|
+
{fileStats.total.count > 0 ? (
|
|
1709
|
+
<p className="font-medium mt-2">
|
|
1710
|
+
總計:{fileStats.total.count} 個檔案 ({formatFileSize(fileStats.total.size)})
|
|
1711
|
+
</p>
|
|
1712
|
+
) : (
|
|
1713
|
+
<p className="text-muted-foreground">目前沒有可清除的檔案</p>
|
|
1714
|
+
)}
|
|
1715
|
+
</div>
|
|
1716
|
+
) : (
|
|
1717
|
+
<span>無法載入檔案資訊</span>
|
|
1718
|
+
)}
|
|
1719
|
+
</div>
|
|
1720
|
+
</AlertDialogDescription>
|
|
1721
|
+
</AlertDialogHeader>
|
|
1722
|
+
<AlertDialogFooter>
|
|
1723
|
+
<AlertDialogCancel disabled={isClearingFiles}>取消</AlertDialogCancel>
|
|
1724
|
+
<AlertDialogAction
|
|
1725
|
+
onClick={handleClearFiles}
|
|
1726
|
+
disabled={isClearingFiles || isLoadingStats || !fileStats || fileStats.total.count === 0}
|
|
1727
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
1728
|
+
>
|
|
1729
|
+
{isClearingFiles ? '清除中...' : '確認清除'}
|
|
1730
|
+
</AlertDialogAction>
|
|
1731
|
+
</AlertDialogFooter>
|
|
1732
|
+
</AlertDialogContent>
|
|
1733
|
+
</AlertDialog>
|
|
1734
|
+
</div>
|
|
1735
|
+
);
|
|
1736
|
+
}
|