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.
Files changed (574) hide show
  1. package/botrun-c/.claude/skills/DOCX/351/226/261/350/256/200/346/212/200/350/203/275/SKILL.md +103 -0
  2. 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
  3. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/SKILL.md +1093 -0
  4. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-branch.mjs +73 -0
  5. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-branches.mjs +77 -0
  6. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-checkout.mjs +72 -0
  7. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-clone.mjs +72 -0
  8. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-commit.mjs +75 -0
  9. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-delete-branch.mjs +75 -0
  10. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-delete-file.mjs +72 -0
  11. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-diff.mjs +80 -0
  12. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-list-tree.mjs +336 -0
  13. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-list.mjs +199 -0
  14. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-merge.mjs +86 -0
  15. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-move.mjs +75 -0
  16. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-pr-create.mjs +81 -0
  17. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-pr-list.mjs +74 -0
  18. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-pr-merge.mjs +83 -0
  19. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-pr-view.mjs +71 -0
  20. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-push.mjs +71 -0
  21. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-read.mjs +277 -0
  22. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-search.mjs +370 -0
  23. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-stash.mjs +116 -0
  24. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-sync.mjs +71 -0
  25. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/github-write.mjs +74 -0
  26. package/botrun-c/.claude/skills/GitHub/346/212/200/350/203/275/scripts/lib/path-validator.mjs +167 -0
  27. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/SKILL.md +605 -0
  28. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-create.mjs +127 -0
  29. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-delete.mjs +77 -0
  30. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-diff.mjs +87 -0
  31. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-history.mjs +99 -0
  32. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-list.mjs +174 -0
  33. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-read.mjs +214 -0
  34. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-restore.mjs +75 -0
  35. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-search.mjs +270 -0
  36. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-sync-back.mjs +155 -0
  37. package/botrun-c/.claude/skills/GoogleDrive/346/212/200/350/203/275/scripts/gdrive-update.mjs +100 -0
  38. package/botrun-c/.claude/skills/HTML/347/224/237/346/210/220/346/212/200/350/203/275/SKILL.md +414 -0
  39. package/botrun-c/.claude/skills/HTML/347/224/237/346/210/220/346/212/200/350/203/275/scripts/create-project.mjs +91 -0
  40. package/botrun-c/.claude/skills/HTML/347/224/237/346/210/220/346/212/200/350/203/275/scripts/finalize-project.mjs +162 -0
  41. package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/SKILL.md +206 -0
  42. package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/package.json +19 -0
  43. package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/pdf-analyze.mjs +309 -0
  44. package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/pdf-compress.mjs +315 -0
  45. package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/pdf-split.mjs +275 -0
  46. package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/pdf-to-text.mjs +336 -0
  47. package/botrun-c/.claude/skills/PDF/346/212/200/350/203/275/scripts/test-pdf-scripts.mjs +491 -0
  48. package/botrun-c/.claude/skills/docx-to-markdown/SKILL.md +76 -0
  49. package/botrun-c/.claude/skills/docx-to-markdown/scripts/convert_docx.py +183 -0
  50. package/botrun-c/.claude/skills/gcp-coord-ml-ocr/SKILL.md +133 -0
  51. package/botrun-c/.claude/skills/gcp-coord-ml-ocr/scripts/ocr_processor.py +381 -0
  52. package/botrun-c/.claude/skills/gemini-transcribe/SKILL.md +115 -0
  53. package/botrun-c/.claude/skills/gemini-transcribe/scripts/transcribe.py +499 -0
  54. package/botrun-c/.claude/skills/nchc-transcribe/SKILL.md +131 -0
  55. package/botrun-c/.claude/skills/nchc-transcribe/scripts/transcribe.py +522 -0
  56. package/botrun-c/.claude/skills/p320-moj-review/SKILL.md +200 -0
  57. package/botrun-c/.claude/skills/p320-moj-review/references/guideline.md +118 -0
  58. package/botrun-c/.claude/skills/pdf-multimodal-processor/SKILL.md +186 -0
  59. package/botrun-c/.claude/skills/pdf-multimodal-processor/scripts/process_pdf.py +515 -0
  60. package/botrun-c/.claude/skills/ripgrep/346/220/234/345/260/213/346/212/200/350/203/275/SKILL.md +81 -0
  61. package/botrun-c/.claude/skills/ripgrep/346/220/234/345/260/213/346/212/200/350/203/275/scripts/package.json +13 -0
  62. package/botrun-c/.claude/skills/ripgrep/346/220/234/345/260/213/346/212/200/350/203/275/scripts/secure-search.mjs +232 -0
  63. 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
  64. package/botrun-c/.claude/skills//345/216/273/350/255/230/345/210/245/346/212/200/350/203/275/SKILL.md +125 -0
  65. package/botrun-c/.claude/skills//345/233/236/346/206/266/346/212/200/350/203/275/SKILL.md +123 -0
  66. package/botrun-c/.claude/skills//345/233/236/346/206/266/346/212/200/350/203/275/scripts/recall.mjs +339 -0
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. 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
  80. package/botrun-c/.claude/skills//345/255/265/345/214/226/346/212/200/350/203/275/SKILL.md +131 -0
  81. package/botrun-c/.claude/skills//345/255/265/345/214/226/346/212/200/350/203/275/scripts/skill-manager.mjs +542 -0
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. package/botrun-c/.claude/skills//350/250/230/346/206/266/346/212/200/350/203/275/SKILL.md +114 -0
  95. package/botrun-c/.claude/skills//350/250/230/346/206/266/346/212/200/350/203/275/scripts/remember.mjs +159 -0
  96. package/botrun-c/.claude/skills//350/250/230/346/206/266/346/212/200/350/203/275/scripts/test-advanced.mjs +342 -0
  97. package/botrun-c/.claude/skills//350/250/230/346/206/266/346/212/200/350/203/275/scripts/test.mjs +430 -0
  98. 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
  99. 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
  100. 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
  101. 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
  102. package/botrun-c/.dockerignore +131 -0
  103. package/botrun-c/.dockeroptimize +13 -0
  104. package/botrun-c/.firebase/hosting.b3V0.cache +30 -0
  105. package/botrun-c/.firebaserc +5 -0
  106. package/botrun-c/.gcloudignore +56 -0
  107. package/botrun-c/.ruby-version +1 -0
  108. package/botrun-c/CHANGELOG.md +224 -0
  109. package/botrun-c/Dockerfile.gcp +365 -0
  110. package/botrun-c/Dockerfile.slim +195 -0
  111. package/botrun-c/Gemfile +8 -0
  112. package/botrun-c/Gemfile.lock +233 -0
  113. package/botrun-c/LOCAL-DEVELOPMENT-GUIDE.md +393 -0
  114. package/botrun-c/PLAN.md +131 -0
  115. package/botrun-c/README.md +129 -0
  116. package/botrun-c/TODO-CAPACITOR-INTEGRATION.md +482 -0
  117. package/botrun-c/VERIFICATION.md +121 -0
  118. package/botrun-c/admin/RATE_LIMIT_MANAGEMENT.md +312 -0
  119. package/botrun-c/admin/README.md +338 -0
  120. package/botrun-c/admin/botrun-billing-report-1765235244038.csv +22 -0
  121. package/botrun-c/admin/botrun-billing-report-1765235323089.csv +22 -0
  122. package/botrun-c/admin/botrun-dashboard-1765235244039.html +278 -0
  123. package/botrun-c/admin/botrun-dashboard-1765235323089.html +278 -0
  124. package/botrun-c/admin/botrun-timeseries-074xGzIKWyfKTBt1NMIj9lxi5mO2-1765235244039.html +161 -0
  125. package/botrun-c/admin/botrun-timeseries-074xGzIKWyfKTBt1NMIj9lxi5mO2-1765235323090.html +161 -0
  126. package/botrun-c/admin/check-billing.ts +48 -0
  127. package/botrun-c/admin/count-all-users.ts +65 -0
  128. package/botrun-c/admin/generate-analytics-report.ts +188 -0
  129. package/botrun-c/admin/jest.config.ts +43 -0
  130. package/botrun-c/admin/lib/__tests__/formatters.test.ts +264 -0
  131. package/botrun-c/admin/lib/__tests__/mocks/firebase.mock.ts +112 -0
  132. package/botrun-c/admin/lib/__tests__/services/UserService.test.ts +83 -0
  133. package/botrun-c/admin/lib/__tests__/user-resolver.test.ts +439 -0
  134. package/botrun-c/admin/lib/analyzers/BillingAnalyticsService.ts +242 -0
  135. package/botrun-c/admin/lib/analyzers/CsvExporter.ts +263 -0
  136. package/botrun-c/admin/lib/analyzers/HtmlChartGenerator.ts +530 -0
  137. package/botrun-c/admin/lib/analyzers/SpikeAnalyzer.ts +608 -0
  138. package/botrun-c/admin/lib/analyzers/TimeSeriesAnalyzer.ts +344 -0
  139. package/botrun-c/admin/lib/cli/CLIBootstrapper.ts +40 -0
  140. package/botrun-c/admin/lib/commands/AbstractCommand.ts +69 -0
  141. package/botrun-c/admin/lib/commands/AnalyticsCommand.ts +215 -0
  142. package/botrun-c/admin/lib/commands/ListCommand.ts +85 -0
  143. package/botrun-c/admin/lib/commands/QueryCommand.ts +81 -0
  144. package/botrun-c/admin/lib/commands/SpikeCommand.ts +83 -0
  145. package/botrun-c/admin/lib/domain/User.ts +38 -0
  146. package/botrun-c/admin/lib/firebase.ts +92 -0
  147. package/botrun-c/admin/lib/firestore-safe.ts +105 -0
  148. package/botrun-c/admin/lib/formatters.ts +154 -0
  149. package/botrun-c/admin/lib/rate-limit-detailed.ts +355 -0
  150. package/botrun-c/admin/lib/rate-limit.ts +286 -0
  151. package/botrun-c/admin/lib/repositories/UserRepository.ts +104 -0
  152. package/botrun-c/admin/lib/services/UserService.ts +108 -0
  153. package/botrun-c/admin/lib/types.ts +56 -0
  154. package/botrun-c/admin/lib/user-resolver.ts +275 -0
  155. package/botrun-c/admin/manage-auth-domains.js +178 -0
  156. package/botrun-c/admin/migrate-plan-free.sh +64 -0
  157. package/botrun-c/admin/package-lock.json +5656 -0
  158. package/botrun-c/admin/package.json +28 -0
  159. package/botrun-c/admin/rate-limit.sh +55 -0
  160. package/botrun-c/admin/remove-custom-limits.ts +75 -0
  161. package/botrun-c/admin/reset-rate.sh +70 -0
  162. package/botrun-c/admin/reset-trial.sh +68 -0
  163. package/botrun-c/admin/seed-trial-data.sh +38 -0
  164. package/botrun-c/admin/trial.sh +55 -0
  165. package/botrun-c/admin/tsconfig.json +20 -0
  166. package/botrun-c/admin/users-cli.py +298 -0
  167. package/botrun-c/admin/users.sh +12 -0
  168. package/botrun-c/admin/users.ts +1321 -0
  169. package/botrun-c/api/admin-tools/check-whitelist.mjs.migration-bak +74 -0
  170. package/botrun-c/api/admin-tools/create-user.mjs +92 -0
  171. package/botrun-c/api/admin-tools/create-user.mjs.migration-bak +92 -0
  172. package/botrun-c/api/admin-tools/firebase-auth-domains-v2.py +287 -0
  173. package/botrun-c/api/admin-tools/firebase-auth-domains-v2.py.migration-bak +287 -0
  174. package/botrun-c/api/admin-tools/init-firestore.mjs +75 -0
  175. package/botrun-c/api/admin-tools/init-firestore.mjs.migration-bak +101 -0
  176. package/botrun-c/api/admin-tools/list-users.mjs +43 -0
  177. package/botrun-c/api/admin-tools/list-users.mjs.migration-bak +43 -0
  178. package/botrun-c/api/admin-tools/manage-authorized-domains.ts +255 -0
  179. package/botrun-c/api/admin-tools/manage-authorized-domains.ts.migration-bak +255 -0
  180. package/botrun-c/api/admin-tools/setup-custom-claims.ts.migration-bak +155 -0
  181. package/botrun-c/api/admin-tools/test-claims.mjs +69 -0
  182. package/botrun-c/api/admin-tools/test-claims.mjs.migration-bak +69 -0
  183. package/botrun-c/api/admin-tools/update-service-urls-gcloud.mjs +72 -0
  184. package/botrun-c/api/admin-tools/update-service-urls-gcloud.mjs.migration-bak +72 -0
  185. package/botrun-c/api/admin-tools/update-service-urls-to-lb.mjs +159 -0
  186. package/botrun-c/api/admin-tools/update-service-urls-to-lb.mjs.migration-bak +159 -0
  187. package/botrun-c/api/admin-tools/update-service-urls.mjs +82 -0
  188. package/botrun-c/api/admin-tools/update-service-urls.mjs.migration-bak +82 -0
  189. package/botrun-c/api/package-lock.json +5031 -0
  190. package/botrun-c/api/package.json +63 -0
  191. package/botrun-c/api/pnpm-lock.yaml +4157 -0
  192. package/botrun-c/api/rate-limit-export-1763620678737.csv +79 -0
  193. package/botrun-c/api/scripts/README-migrate-plan-free.md +197 -0
  194. package/botrun-c/api/scripts/check-users.ts +45 -0
  195. package/botrun-c/api/scripts/delete-firestore-docs.js +43 -0
  196. package/botrun-c/api/scripts/dump-subscriptions.ts +395 -0
  197. package/botrun-c/api/scripts/list-all-users.ts +65 -0
  198. package/botrun-c/api/scripts/migrate-plan-free-rpd-16to32.ts +262 -0
  199. package/botrun-c/api/scripts/migrate-plan-names-to-lite-max.ts +269 -0
  200. package/botrun-c/api/scripts/query-user-info.ts +304 -0
  201. package/botrun-c/api/scripts/reset-rate-limit.ts +206 -0
  202. package/botrun-c/api/scripts/reset-trial-subscription.ts +166 -0
  203. package/botrun-c/api/scripts/rollback-plan-names-from-lite-max.ts +139 -0
  204. package/botrun-c/api/scripts/seed-rate-limit-test-data.ts +114 -0
  205. package/botrun-c/api/scripts/seed-subscription-test-data.ts +173 -0
  206. package/botrun-c/api/scripts/test-rate-limit-message.ts +108 -0
  207. package/botrun-c/api/test-billing-write.ts +103 -0
  208. package/botrun-c/api/test-billing.ts +81 -0
  209. package/botrun-c/api/test-chat-upload.sh +162 -0
  210. package/botrun-c/api/test-nchc-whisper.ts +87 -0
  211. package/botrun-c/api/test-skills-loading.js +77 -0
  212. package/botrun-c/api/test-skills-symlink.js +118 -0
  213. package/botrun-c/api/test-upload-multimodal.sh +167 -0
  214. package/botrun-c/api/tsconfig.json +27 -0
  215. package/botrun-c/api/verify-form-2-1.mjs +229 -0
  216. package/botrun-c/api/vitest.config.ts +9 -0
  217. package/botrun-c/appium-capabilities.json +20 -0
  218. package/botrun-c/cors.json +26 -0
  219. package/botrun-c/delete-old.sh +44 -0
  220. package/botrun-c/design/google-drive/01-architecture.md +186 -0
  221. package/botrun-c/design/google-drive/02-api-design.md +314 -0
  222. package/botrun-c/design/google-drive/03-frontend-design.md +278 -0
  223. package/botrun-c/design/google-drive/04-skill-design.md +384 -0
  224. package/botrun-c/design/google-drive/05-test-plan.md +507 -0
  225. package/botrun-c/design/google-drive/README.md +32 -0
  226. package/botrun-c/dev/mcp-token-calculator/README.md +92 -0
  227. package/botrun-c/dev/mcp-token-calculator/RESULTS.md +188 -0
  228. package/botrun-c/dev/mcp-token-calculator/calculate-current-tokens.ts +369 -0
  229. package/botrun-c/dev/mcp-token-calculator/calculate-mcp-tokens.ts +464 -0
  230. package/botrun-c/dev/mcp-token-calculator/optimization-proposals.ts +316 -0
  231. package/botrun-c/dev/mcp-token-calculator/package-lock.json +599 -0
  232. package/botrun-c/dev/mcp-token-calculator/package.json +22 -0
  233. package/botrun-c/dev/mcp-token-calculator/test-all-schemas.ts +314 -0
  234. package/botrun-c/dev/mcp-token-calculator/test-schema-correctness.ts +221 -0
  235. package/botrun-c/dev/mcp-token-calculator/verify-optimization.ts +159 -0
  236. package/botrun-c/fastlane/ANDROID-DEPLOYMENT-SETUP.md +825 -0
  237. package/botrun-c/fastlane/IOS-DEPLOYMENT-SETUP.md +431 -0
  238. package/botrun-c/fastlane/Pluginfile +5 -0
  239. package/botrun-c/fastlane/QUICK-START.md +133 -0
  240. package/botrun-c/fastlane/README.md +424 -0
  241. package/botrun-c/fastlane/RUBY-SETUP.md +449 -0
  242. package/botrun-c/fastlane/android/Appfile +11 -0
  243. package/botrun-c/fastlane/android/Fastfile +240 -0
  244. package/botrun-c/firebase.json +46 -0
  245. package/botrun-c/firestore-init-data.json +40 -0
  246. package/botrun-c/firestore.indexes.json +55 -0
  247. package/botrun-c/firestore.rules +53 -0
  248. package/botrun-c/fix-codex-permissions.sh +56 -0
  249. package/botrun-c/gcs-lifecycle-30day-retention.json +12 -0
  250. package/botrun-c/html-architecture/architecture/deployment.html +664 -0
  251. package/botrun-c/html-architecture/architecture/index.html +309 -0
  252. package/botrun-c/html-architecture/architecture/styles.css +500 -0
  253. package/botrun-c/html-architecture/architecture/system-components.html +538 -0
  254. package/botrun-c/html-architecture/architecture/user-flow.html +370 -0
  255. package/botrun-c/mobile-android/FIREBASE-AUTH-SETUP.md +302 -0
  256. package/botrun-c/mobile-android/WSL-ANDROID-SETUP.md +252 -0
  257. package/botrun-c/mobile-android/android/app/build.gradle +75 -0
  258. package/botrun-c/mobile-android/android/app/capacitor.build.gradle +31 -0
  259. package/botrun-c/mobile-android/android/app/proguard-rules.pro +21 -0
  260. package/botrun-c/mobile-android/android/build.gradle +29 -0
  261. package/botrun-c/mobile-android/android/capacitor.settings.gradle +42 -0
  262. package/botrun-c/mobile-android/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  263. package/botrun-c/mobile-android/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  264. package/botrun-c/mobile-android/android/gradle.properties +22 -0
  265. package/botrun-c/mobile-android/android/gradlew +252 -0
  266. package/botrun-c/mobile-android/android/gradlew.bat +94 -0
  267. package/botrun-c/mobile-android/android/settings.gradle +5 -0
  268. package/botrun-c/mobile-android/android/variables.gradle +16 -0
  269. package/botrun-c/mobile-android/capacitor.config.ts +44 -0
  270. package/botrun-c/mobile-android/fastlane/Appfile +11 -0
  271. package/botrun-c/mobile-android/fastlane/Fastfile +202 -0
  272. package/botrun-c/mobile-android/fastlane/README.md +96 -0
  273. package/botrun-c/mobile-android/google-services.json.template +39 -0
  274. package/botrun-c/mobile-android/package.json +32 -0
  275. package/botrun-c/mobile-android/resources/icon.png +0 -0
  276. package/botrun-c/mobile-android/tsconfig.json +15 -0
  277. package/botrun-c/mobile-ios/.ruby-version +1 -0
  278. package/botrun-c/mobile-ios/BUILD-LOG.md +82 -0
  279. package/botrun-c/mobile-ios/Gemfile +4 -0
  280. package/botrun-c/mobile-ios/Gemfile.lock +299 -0
  281. package/botrun-c/mobile-ios/GoogleService-Info.plist.template +34 -0
  282. package/botrun-c/mobile-ios/IOS-PERMISSIONS.md +80 -0
  283. package/botrun-c/mobile-ios/QUICK-START.md +189 -0
  284. package/botrun-c/mobile-ios/README.md +231 -0
  285. package/botrun-c/mobile-ios/capacitor.config.ts +46 -0
  286. package/botrun-c/mobile-ios/fastlane/Fastfile +326 -0
  287. package/botrun-c/mobile-ios/fastlane/README.md +88 -0
  288. package/botrun-c/mobile-ios/ios/App/App/App.entitlements +10 -0
  289. package/botrun-c/mobile-ios/ios/App/App/AppDelegate.swift +49 -0
  290. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png +0 -0
  291. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  292. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Contents.json +6 -0
  293. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json +56 -0
  294. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany-dark.png +0 -0
  295. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany.png +0 -0
  296. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany-dark.png +0 -0
  297. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany.png +0 -0
  298. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany-dark.png +0 -0
  299. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany.png +0 -0
  300. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png +0 -0
  301. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png +0 -0
  302. package/botrun-c/mobile-ios/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png +0 -0
  303. package/botrun-c/mobile-ios/ios/App/App/Base.lproj/LaunchScreen.storyboard +32 -0
  304. package/botrun-c/mobile-ios/ios/App/App/Base.lproj/Main.storyboard +19 -0
  305. package/botrun-c/mobile-ios/ios/App/App/Info.plist +72 -0
  306. package/botrun-c/mobile-ios/ios/App/App.xcodeproj/project.pbxproj +446 -0
  307. package/botrun-c/mobile-ios/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +78 -0
  308. package/botrun-c/mobile-ios/ios/App/App.xcworkspace/contents.xcworkspacedata +10 -0
  309. package/botrun-c/mobile-ios/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  310. package/botrun-c/mobile-ios/ios/App/Podfile +52 -0
  311. package/botrun-c/mobile-ios/ios/App/Podfile.lock +205 -0
  312. package/botrun-c/mobile-ios/ios/App/add_plist.rb +30 -0
  313. package/botrun-c/mobile-ios/ios/App/fix_plist_path.rb +22 -0
  314. package/botrun-c/mobile-ios/package.json +29 -0
  315. package/botrun-c/mobile-ios/resources/icon.png +0 -0
  316. package/botrun-c/mobile-ios/run-simulator.sh +34 -0
  317. package/botrun-c/mobile-ios/screenshot-after-clean-rebuild.png +0 -0
  318. package/botrun-c/mobile-ios/screenshot-final-version.png +0 -0
  319. package/botrun-c/mobile-ios/screenshot-simulator.png +0 -0
  320. package/botrun-c/mobile-ios/screenshot-version-1009.png +0 -0
  321. package/botrun-c/mobile-ios/screenshot-with-version.png +0 -0
  322. package/botrun-c/mobile-ios/screenshot-working.png +0 -0
  323. package/botrun-c/mobile-ios/setup-ios.sh +106 -0
  324. package/botrun-c/mobile-ios/tsconfig.json +16 -0
  325. package/botrun-c/org-policy-allow-all-users.yaml +3 -0
  326. package/botrun-c/package.json +68 -0
  327. package/botrun-c/pnpm-lock.yaml +10198 -0
  328. package/botrun-c/pnpm-workspace.yaml +6 -0
  329. package/botrun-c/prompts/BOTRUN-1.md +13 -0
  330. package/botrun-c/prompts/BOTRUN-2.md +70 -0
  331. package/botrun-c/prompts/BOTRUN-3.md +139 -0
  332. package/botrun-c/prototypes/firestore-gax-debug/package-lock.json +1204 -0
  333. package/botrun-c/prototypes/firestore-gax-debug/package.json +12 -0
  334. package/botrun-c/prototypes/firestore-gax-debug/pnpm-workspace.yaml +2 -0
  335. package/botrun-c/prototypes/firestore-gax-debug/test.js +98 -0
  336. package/botrun-c/prototypes/taiwan-haiku/README.md +93 -0
  337. package/botrun-c/prototypes/taiwan-haiku/index.ts +139 -0
  338. package/botrun-c/prototypes/taiwan-haiku/package.json +23 -0
  339. package/botrun-c/prototypes/taiwan-haiku/pnpm-lock.yaml +730 -0
  340. package/botrun-c/prototypes/taiwan-haiku/test-vertex-direct.ts +83 -0
  341. package/botrun-c/prototypes/taiwan-haiku/tsconfig.json +18 -0
  342. package/botrun-c/prototypes/web-search/FINDINGS-STAGE-1.md +48 -0
  343. package/botrun-c/prototypes/web-search/README.md +46 -0
  344. package/botrun-c/prototypes/web-search/VERIFICATION-REPORT.md +310 -0
  345. package/botrun-c/prototypes/web-search/package-lock.json +595 -0
  346. package/botrun-c/prototypes/web-search/package.json +18 -0
  347. package/botrun-c/prototypes/web-search/stage-1.1-basic-connection.js +123 -0
  348. package/botrun-c/prototypes/web-search/stage-1.2-with-websearch.js +154 -0
  349. package/botrun-c/prototypes/web-search/stage-1.3-search-query.js +192 -0
  350. package/botrun-c/prototypes/web-search/stage-2-agent-sdk-websearch.js +265 -0
  351. package/botrun-c/runtime-scripts/env-detect.sh +64 -0
  352. package/botrun-c/scripts/github-collaborator-manager.sh +297 -0
  353. package/botrun-c/scripts/github-collaborator-manager.ts +325 -0
  354. package/botrun-c/scripts/manage-collaborators.sh +91 -0
  355. package/botrun-c/scripts/setup-secrets.sh +202 -0
  356. package/botrun-c/scripts/stress-test-17-users.sh +113 -0
  357. package/botrun-c/scripts/stress-test-api.sh +119 -0
  358. package/botrun-c/scripts/stress-test-full-report.sh +385 -0
  359. package/botrun-c/specs/infra-vm/design.md +0 -0
  360. package/botrun-c/specs/share-skill/design.md +1146 -0
  361. package/botrun-c/specs/share-skill/tasks.md +444 -0
  362. package/botrun-c/stress-test-results/20260121-162630/config.json +7 -0
  363. package/botrun-c/stress-test-results/20260121-162630/user-1-status.txt +1 -0
  364. package/botrun-c/stress-test-results/20260121-162630/user-1-timing.txt +1 -0
  365. package/botrun-c/stress-test-results/20260121-162630/user-1.json +2 -0
  366. package/botrun-c/stress-test-results/20260121-162646/config.json +7 -0
  367. package/botrun-c/stress-test-results/20260121-162646/user-1-status.txt +1 -0
  368. package/botrun-c/stress-test-results/20260121-162646/user-1-timing.txt +1 -0
  369. package/botrun-c/stress-test-results/20260121-162646/user-1.json +2 -0
  370. package/botrun-c/stress-test-results/20260121-162646/user-10-status.txt +1 -0
  371. package/botrun-c/stress-test-results/20260121-162646/user-10-timing.txt +1 -0
  372. package/botrun-c/stress-test-results/20260121-162646/user-10.json +2 -0
  373. package/botrun-c/stress-test-results/20260121-162646/user-11-status.txt +1 -0
  374. package/botrun-c/stress-test-results/20260121-162646/user-11-timing.txt +1 -0
  375. package/botrun-c/stress-test-results/20260121-162646/user-11.json +2 -0
  376. package/botrun-c/stress-test-results/20260121-162646/user-12-status.txt +1 -0
  377. package/botrun-c/stress-test-results/20260121-162646/user-12-timing.txt +1 -0
  378. package/botrun-c/stress-test-results/20260121-162646/user-12.json +2 -0
  379. package/botrun-c/stress-test-results/20260121-162646/user-13-status.txt +1 -0
  380. package/botrun-c/stress-test-results/20260121-162646/user-13-timing.txt +1 -0
  381. package/botrun-c/stress-test-results/20260121-162646/user-13.json +2 -0
  382. package/botrun-c/stress-test-results/20260121-162646/user-14-status.txt +1 -0
  383. package/botrun-c/stress-test-results/20260121-162646/user-14-timing.txt +1 -0
  384. package/botrun-c/stress-test-results/20260121-162646/user-14.json +2 -0
  385. package/botrun-c/stress-test-results/20260121-162646/user-15-status.txt +1 -0
  386. package/botrun-c/stress-test-results/20260121-162646/user-15-timing.txt +1 -0
  387. package/botrun-c/stress-test-results/20260121-162646/user-15.json +2 -0
  388. package/botrun-c/stress-test-results/20260121-162646/user-16-status.txt +1 -0
  389. package/botrun-c/stress-test-results/20260121-162646/user-16-timing.txt +1 -0
  390. package/botrun-c/stress-test-results/20260121-162646/user-16.json +2 -0
  391. package/botrun-c/stress-test-results/20260121-162646/user-17-status.txt +1 -0
  392. package/botrun-c/stress-test-results/20260121-162646/user-17-timing.txt +1 -0
  393. package/botrun-c/stress-test-results/20260121-162646/user-17.json +2 -0
  394. package/botrun-c/stress-test-results/20260121-162646/user-2-status.txt +1 -0
  395. package/botrun-c/stress-test-results/20260121-162646/user-2-timing.txt +1 -0
  396. package/botrun-c/stress-test-results/20260121-162646/user-2.json +2 -0
  397. package/botrun-c/stress-test-results/20260121-162646/user-3-status.txt +1 -0
  398. package/botrun-c/stress-test-results/20260121-162646/user-3-timing.txt +1 -0
  399. package/botrun-c/stress-test-results/20260121-162646/user-3.json +2 -0
  400. package/botrun-c/stress-test-results/20260121-162646/user-4-status.txt +1 -0
  401. package/botrun-c/stress-test-results/20260121-162646/user-4-timing.txt +1 -0
  402. package/botrun-c/stress-test-results/20260121-162646/user-4.json +2 -0
  403. package/botrun-c/stress-test-results/20260121-162646/user-5-status.txt +1 -0
  404. package/botrun-c/stress-test-results/20260121-162646/user-5-timing.txt +1 -0
  405. package/botrun-c/stress-test-results/20260121-162646/user-5.json +2 -0
  406. package/botrun-c/stress-test-results/20260121-162646/user-6-status.txt +1 -0
  407. package/botrun-c/stress-test-results/20260121-162646/user-6-timing.txt +1 -0
  408. package/botrun-c/stress-test-results/20260121-162646/user-6.json +2 -0
  409. package/botrun-c/stress-test-results/20260121-162646/user-7-status.txt +1 -0
  410. package/botrun-c/stress-test-results/20260121-162646/user-7-timing.txt +1 -0
  411. package/botrun-c/stress-test-results/20260121-162646/user-7.json +2 -0
  412. package/botrun-c/stress-test-results/20260121-162646/user-8-status.txt +1 -0
  413. package/botrun-c/stress-test-results/20260121-162646/user-8-timing.txt +1 -0
  414. package/botrun-c/stress-test-results/20260121-162646/user-8.json +2 -0
  415. package/botrun-c/stress-test-results/20260121-162646/user-9-status.txt +1 -0
  416. package/botrun-c/stress-test-results/20260121-162646/user-9-timing.txt +1 -0
  417. package/botrun-c/stress-test-results/20260121-162646/user-9.json +2 -0
  418. package/botrun-c/stress-test-results/20260121-163148/REPORT.html +344 -0
  419. package/botrun-c/stress-test-results/20260121-163148/REPORT.md +93 -0
  420. package/botrun-c/stress-test-results/20260121-163148/REPORT.pdf +0 -0
  421. package/botrun-c/stress-test-results/20260121-163148/config.json +7 -0
  422. package/botrun-c/stress-test-results/20260121-163148/summary.json +26 -0
  423. package/botrun-c/stress-test-results/20260121-163148/user-1-status.txt +1 -0
  424. package/botrun-c/stress-test-results/20260121-163148/user-1-timing.txt +1 -0
  425. package/botrun-c/stress-test-results/20260121-163148/user-1.json +206 -0
  426. package/botrun-c/stress-test-results/20260121-163148/user-10-status.txt +1 -0
  427. package/botrun-c/stress-test-results/20260121-163148/user-10-timing.txt +1 -0
  428. package/botrun-c/stress-test-results/20260121-163148/user-10.json +110 -0
  429. package/botrun-c/stress-test-results/20260121-163148/user-11-status.txt +1 -0
  430. package/botrun-c/stress-test-results/20260121-163148/user-11-timing.txt +1 -0
  431. package/botrun-c/stress-test-results/20260121-163148/user-11.json +104 -0
  432. package/botrun-c/stress-test-results/20260121-163148/user-12-status.txt +1 -0
  433. package/botrun-c/stress-test-results/20260121-163148/user-12-timing.txt +1 -0
  434. package/botrun-c/stress-test-results/20260121-163148/user-12.json +98 -0
  435. package/botrun-c/stress-test-results/20260121-163148/user-13-status.txt +1 -0
  436. package/botrun-c/stress-test-results/20260121-163148/user-13-timing.txt +1 -0
  437. package/botrun-c/stress-test-results/20260121-163148/user-13.json +68 -0
  438. package/botrun-c/stress-test-results/20260121-163148/user-14-status.txt +1 -0
  439. package/botrun-c/stress-test-results/20260121-163148/user-14-timing.txt +1 -0
  440. package/botrun-c/stress-test-results/20260121-163148/user-14.json +116 -0
  441. package/botrun-c/stress-test-results/20260121-163148/user-15-status.txt +1 -0
  442. package/botrun-c/stress-test-results/20260121-163148/user-15-timing.txt +1 -0
  443. package/botrun-c/stress-test-results/20260121-163148/user-15.json +131 -0
  444. package/botrun-c/stress-test-results/20260121-163148/user-16-status.txt +1 -0
  445. package/botrun-c/stress-test-results/20260121-163148/user-16-timing.txt +1 -0
  446. package/botrun-c/stress-test-results/20260121-163148/user-16.json +110 -0
  447. package/botrun-c/stress-test-results/20260121-163148/user-17-status.txt +1 -0
  448. package/botrun-c/stress-test-results/20260121-163148/user-17-timing.txt +1 -0
  449. package/botrun-c/stress-test-results/20260121-163148/user-17.json +116 -0
  450. package/botrun-c/stress-test-results/20260121-163148/user-2-status.txt +1 -0
  451. package/botrun-c/stress-test-results/20260121-163148/user-2-timing.txt +1 -0
  452. package/botrun-c/stress-test-results/20260121-163148/user-2.json +98 -0
  453. package/botrun-c/stress-test-results/20260121-163148/user-3-status.txt +1 -0
  454. package/botrun-c/stress-test-results/20260121-163148/user-3-timing.txt +1 -0
  455. package/botrun-c/stress-test-results/20260121-163148/user-3.json +62 -0
  456. package/botrun-c/stress-test-results/20260121-163148/user-4-status.txt +1 -0
  457. package/botrun-c/stress-test-results/20260121-163148/user-4-timing.txt +1 -0
  458. package/botrun-c/stress-test-results/20260121-163148/user-4.json +113 -0
  459. package/botrun-c/stress-test-results/20260121-163148/user-5-status.txt +1 -0
  460. package/botrun-c/stress-test-results/20260121-163148/user-5-timing.txt +1 -0
  461. package/botrun-c/stress-test-results/20260121-163148/user-5.json +83 -0
  462. package/botrun-c/stress-test-results/20260121-163148/user-6-status.txt +1 -0
  463. package/botrun-c/stress-test-results/20260121-163148/user-6-timing.txt +1 -0
  464. package/botrun-c/stress-test-results/20260121-163148/user-6.json +125 -0
  465. package/botrun-c/stress-test-results/20260121-163148/user-7-status.txt +1 -0
  466. package/botrun-c/stress-test-results/20260121-163148/user-7-timing.txt +1 -0
  467. package/botrun-c/stress-test-results/20260121-163148/user-7.json +62 -0
  468. package/botrun-c/stress-test-results/20260121-163148/user-8-status.txt +1 -0
  469. package/botrun-c/stress-test-results/20260121-163148/user-8-timing.txt +1 -0
  470. package/botrun-c/stress-test-results/20260121-163148/user-8.json +41 -0
  471. package/botrun-c/stress-test-results/20260121-163148/user-9-status.txt +1 -0
  472. package/botrun-c/stress-test-results/20260121-163148/user-9-timing.txt +1 -0
  473. package/botrun-c/stress-test-results/20260121-163148/user-9.json +53 -0
  474. package/botrun-c/test-merge.md +1 -0
  475. package/botrun-c/version.txt +1 -0
  476. package/botrun-c/web/.version +1 -0
  477. package/botrun-c/web/ALLOW-PUBLIC-ACCESS.md +57 -0
  478. package/botrun-c/web/FINAL-SETUP-STEPS.md +71 -0
  479. package/botrun-c/web/FIREBASE-MANUAL-STEPS.md +41 -0
  480. package/botrun-c/web/QUICK-SETUP-HELPER.md +79 -0
  481. package/botrun-c/web/README.md +162 -0
  482. package/botrun-c/web/__tests__/image-extractor.test.ts +147 -0
  483. package/botrun-c/web/__tests__/useGitHub.test.ts +245 -0
  484. package/botrun-c/web/app/favicon.ico +0 -0
  485. package/botrun-c/web/app/globals.css +215 -0
  486. package/botrun-c/web/app/image-preview/page.tsx +213 -0
  487. package/botrun-c/web/app/layout.tsx +46 -0
  488. package/botrun-c/web/app/page.tsx +45 -0
  489. package/botrun-c/web/components/CapacitorProvider.tsx +43 -0
  490. package/botrun-c/web/components/ChatPage.tsx +1736 -0
  491. package/botrun-c/web/components/ConversationList.tsx +237 -0
  492. package/botrun-c/web/components/ConversationListItem.tsx +121 -0
  493. package/botrun-c/web/components/LoginPage.tsx +212 -0
  494. package/botrun-c/web/components/Providers.tsx +12 -0
  495. package/botrun-c/web/components/StatusBarProvider.tsx +16 -0
  496. package/botrun-c/web/components/attachment-preview.tsx +193 -0
  497. package/botrun-c/web/components/botrun-incubator/BotrunIncubator.tsx +266 -0
  498. package/botrun-c/web/components/botrun-incubator/index.ts +1 -0
  499. package/botrun-c/web/components/chat-input.tsx +1251 -0
  500. package/botrun-c/web/components/chat-message.tsx +598 -0
  501. package/botrun-c/web/components/drop-zone.tsx +143 -0
  502. package/botrun-c/web/components/gdrive/gdrive-add-dialog.tsx +254 -0
  503. package/botrun-c/web/components/gdrive/gdrive-drawer.tsx +236 -0
  504. package/botrun-c/web/components/gdrive/gdrive-selector.tsx +226 -0
  505. package/botrun-c/web/components/gdrive/index.ts +7 -0
  506. package/botrun-c/web/components/generated-image-card.tsx +363 -0
  507. package/botrun-c/web/components/github/GitHubConnect.tsx +366 -0
  508. package/botrun-c/web/components/github/index.ts +2 -0
  509. package/botrun-c/web/components/import-skill-dialog.tsx +208 -0
  510. package/botrun-c/web/components/scroll-to-bottom-button.tsx +90 -0
  511. package/botrun-c/web/components/share-skill-dialog.tsx +175 -0
  512. package/botrun-c/web/components/skills-editor.tsx +443 -0
  513. package/botrun-c/web/components/skills-manager-drawer.tsx +586 -0
  514. package/botrun-c/web/components/smart-button-group.tsx +132 -0
  515. package/botrun-c/web/config/smart-buttons.ts +295 -0
  516. package/botrun-c/web/config/welcome-messages.ts +29 -0
  517. package/botrun-c/web/contexts/AuthContext.tsx +220 -0
  518. package/botrun-c/web/hooks/use-toast.ts +187 -0
  519. package/botrun-c/web/hooks/useAppStoreUpdate.ts +356 -0
  520. package/botrun-c/web/hooks/useAppVersion.ts +113 -0
  521. package/botrun-c/web/hooks/useAttachments.ts +269 -0
  522. package/botrun-c/web/hooks/useAuthToken.ts +59 -0
  523. package/botrun-c/web/hooks/useAutoWelcome.ts +89 -0
  524. package/botrun-c/web/hooks/useCapacitorPlatform.ts +113 -0
  525. package/botrun-c/web/hooks/useCapawesomeLiveUpdate.ts +149 -0
  526. package/botrun-c/web/hooks/useClaudeModel.ts +118 -0
  527. package/botrun-c/web/hooks/useConversation.ts +115 -0
  528. package/botrun-c/web/hooks/useConversations.ts +235 -0
  529. package/botrun-c/web/hooks/useGdriveFolders.ts +199 -0
  530. package/botrun-c/web/hooks/useGdriveSync.ts +107 -0
  531. package/botrun-c/web/hooks/useGitHub.ts +409 -0
  532. package/botrun-c/web/hooks/useScrollToBottom.ts +150 -0
  533. package/botrun-c/web/hooks/useStatusBar.ts +42 -0
  534. package/botrun-c/web/hooks/useUserPlan.ts +153 -0
  535. package/botrun-c/web/lib/api-config.ts +162 -0
  536. package/botrun-c/web/lib/api-url.ts +62 -0
  537. package/botrun-c/web/lib/api.ts +105 -0
  538. package/botrun-c/web/lib/attachment-utils.ts +168 -0
  539. package/botrun-c/web/lib/build-version.ts +11 -0
  540. package/botrun-c/web/lib/clipboard-utils.ts +199 -0
  541. package/botrun-c/web/lib/constants.ts +62 -0
  542. package/botrun-c/web/lib/conversation-storage.ts +324 -0
  543. package/botrun-c/web/lib/firebase-capacitor.ts +135 -0
  544. package/botrun-c/web/lib/firebase.ts +137 -0
  545. package/botrun-c/web/lib/firestore-auth.ts +61 -0
  546. package/botrun-c/web/lib/firestore-auth.ts.migration-bak +319 -0
  547. package/botrun-c/web/lib/image-extractor.ts +156 -0
  548. package/botrun-c/web/lib/path-utils.ts +48 -0
  549. package/botrun-c/web/lib/rehype-del-to-mark.ts +19 -0
  550. package/botrun-c/web/lib/upload-service.ts +326 -0
  551. package/botrun-c/web/lib/utils.ts +6 -0
  552. package/botrun-c/web/next.config.js +49 -0
  553. package/botrun-c/web/package-lock.json +6711 -0
  554. package/botrun-c/web/package.json +63 -0
  555. package/botrun-c/web/postcss.config.js +6 -0
  556. package/botrun-c/web/public/apple_icon.png +0 -0
  557. package/botrun-c/web/public/google_icon.png +0 -0
  558. package/botrun-c/web/public/logo-48-small.png +0 -0
  559. package/botrun-c/web/public/logo-48.png +0 -0
  560. package/botrun-c/web/scripts/generate-version.js +36 -0
  561. package/botrun-c/web/scripts/update-build-version.js +41 -0
  562. package/botrun-c/web/tailwind.config.ts +80 -0
  563. package/botrun-c/web/test-welcome-message.mjs +115 -0
  564. package/botrun-c/web/tsconfig.json +42 -0
  565. package/botrun-c/web/types/agent.ts +6 -0
  566. package/botrun-c/web/types/attachment.ts +39 -0
  567. package/botrun-c/web/types/conversation.ts +68 -0
  568. package/botrun-c/web/types/flutter.d.ts +6 -0
  569. package/botrun-c/web/types/project.ts +41 -0
  570. package/botrun-c/web/types/skills.ts +13 -0
  571. 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
  572. package/lib/prompt/prompts/zero-framework/fullstack.md +17 -0
  573. package/lib/prompt/prompts/zero-framework/segment.md +13 -0
  574. package/package.json +1 -1
@@ -0,0 +1,1251 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+ import { Send, Loader2, Paperclip, StopCircle, Mic, Square, Download, AlertCircle, RefreshCw, X, Settings, Shield, User, Camera as CameraIcon, Folder, Video, Info } from "lucide-react";
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuLabel,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
+ } from "@/components/ui/dropdown-menu";
15
+ import { useAuth } from "@/contexts/AuthContext";
16
+ import { useCapacitorPlatform } from "@/hooks/useCapacitorPlatform";
17
+ import { SkillsManagerDrawer } from "./skills-manager-drawer";
18
+ import { SkillsEditor } from "./skills-editor";
19
+ import { getApiEndpoint } from "@/lib/api-url";
20
+ import { AudioRecorder } from '@capawesome-team/capacitor-audio-recorder';
21
+ import { SystemLimits, DEFAULT_LIMITS, formatFileSize } from "@/lib/constants";
22
+ import { Filesystem, Directory } from '@capacitor/filesystem';
23
+ import { Share } from '@capacitor/share';
24
+ import { ForegroundService } from '@capawesome-team/capacitor-android-foreground-service';
25
+ import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
26
+ import { uploadFile, uploadFiles } from "@/lib/upload-service";
27
+ import { convertToLocalPath } from "@/lib/path-utils";
28
+ import type { Skill } from "@/types/skills";
29
+ import { useAttachments } from "@/hooks/useAttachments";
30
+ import { AttachmentPreview } from "./attachment-preview";
31
+ import { DropZone } from "./drop-zone";
32
+
33
+ // API URL - 根據環境和當前路徑動態計算
34
+ const API_URL = (() => {
35
+ if (typeof window === 'undefined') return '';
36
+
37
+ // Capacitor (iOS/Android App)
38
+ if ((window as any).Capacitor) {
39
+ return process.env.NEXT_PUBLIC_API_URL || 'https://botrun-c-257949799705.asia-east1.run.app';
40
+ }
41
+
42
+ // Load Balancer 架構:檢查是否在個人後端 (/u/{hash}/)
43
+ const pathname = window.location.pathname;
44
+ if (pathname.startsWith('/u/')) {
45
+ const match = pathname.match(/^\/u\/[0-9a-f]{8}/);
46
+ if (match) {
47
+ return match[0];
48
+ }
49
+ }
50
+
51
+ // 使用環境變數設定的 API URL(如果有的話)
52
+ if (process.env.NEXT_PUBLIC_API_URL) {
53
+ return process.env.NEXT_PUBLIC_API_URL;
54
+ }
55
+
56
+ // 統一前端或其他情況:使用相對路徑(空字串)
57
+ return '';
58
+ })();
59
+
60
+ interface ChatInputProps {
61
+ onSend: (message: string) => void;
62
+ onStop?: () => void;
63
+ disabled?: boolean;
64
+ isLoading?: boolean;
65
+ isStopping?: boolean;
66
+ placeholder?: string;
67
+ transcriptionModel?: string;
68
+ }
69
+
70
+ export function ChatInput({
71
+ onSend,
72
+ onStop,
73
+ disabled = false,
74
+ isLoading = false,
75
+ isStopping = false,
76
+ placeholder = "Type your message...",
77
+ transcriptionModel = 'whisper-Breeze-ASR-25',
78
+ }: ChatInputProps) {
79
+ const { user } = useAuth();
80
+ const { isCapacitor, isAndroid, isIOS } = useCapacitorPlatform();
81
+
82
+ // 🔑 判斷是否為桌面 Web(非 App + 非行動裝置 UserAgent)
83
+ const isDesktopWeb = !isCapacitor &&
84
+ typeof window !== 'undefined' &&
85
+ !/iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
86
+
87
+
88
+ // 🔄 測試:改回受控組件 + composition 保護
89
+ const [message, setMessage] = useState("");
90
+ const [uploading, setUploading] = useState(false);
91
+ const [skills, setSkills] = useState<Skill[]>([]);
92
+ const [systemLimits, setSystemLimits] = useState<SystemLimits>(DEFAULT_LIMITS);
93
+ const [isRecording, setIsRecording] = useState(false);
94
+ const [transcribing, setTranscribing] = useState(false);
95
+ const [transcriptionStatus, setTranscriptionStatus] = useState("");
96
+ const [recordedAudio, setRecordedAudio] = useState<Blob | null>(null);
97
+ const [transcriptionError, setTranscriptionError] = useState<string | null>(null);
98
+ const [managerDrawerOpen, setManagerDrawerOpen] = useState(false);
99
+ const [editorOpen, setEditorOpen] = useState(false);
100
+ const [skillToEdit, setSkillToEdit] = useState<Skill | null>(null);
101
+ const [attachmentError, setAttachmentError] = useState<string | null>(null);
102
+ const isComposingRef = useRef(false); // 追蹤輸入法狀態
103
+
104
+ // 統一附件系統
105
+ const {
106
+ attachments,
107
+ addFiles,
108
+ removeAttachment,
109
+ clearAll: clearAllAttachments,
110
+ retryUpload,
111
+ uploadAll,
112
+ isUploading: isUploadingAttachments,
113
+ hasAttachments,
114
+ canAddMore,
115
+ count: attachmentCount,
116
+ } = useAttachments({
117
+ maxCount: 20,
118
+ maxFileSize: systemLimits.http.maxFileSize,
119
+ onError: (error) => setAttachmentError(error),
120
+ });
121
+ const inputRef = useRef<HTMLTextAreaElement>(null);
122
+ const fileInputRef = useRef<HTMLInputElement>(null);
123
+ const videoInputRef = useRef<HTMLInputElement>(null); // 錄影專用
124
+
125
+ // Load system limits
126
+ const loadSystemLimits = async () => {
127
+ try {
128
+ const url = getApiEndpoint("/api/config/limits");
129
+ const response = await fetch(url);
130
+ if (response.ok) {
131
+ const limits = await response.json();
132
+ setSystemLimits(limits);
133
+ // Removed: console.log('[ChatInput] System limits loaded:', limits);
134
+ }
135
+ } catch (error) {
136
+ console.error('[ChatInput] Failed to load system limits, using defaults:', error);
137
+ }
138
+ };
139
+
140
+ // Load skills
141
+ const loadSkills = async () => {
142
+ const url = getApiEndpoint("/api/skills");
143
+ // Removed: console.log('[ChatInput] Loading skills from:', url);
144
+
145
+ try {
146
+ // 如果已登入,帶上認證 token
147
+ const headers: HeadersInit = {};
148
+ if (user) {
149
+ const idToken = await user.getIdToken();
150
+ headers.Authorization = `Bearer ${idToken}`;
151
+ // Removed: console.log('[ChatInput] Loading with auth token');
152
+ }
153
+ // Removed: else { console.log('[ChatInput] Loading without auth (guest mode)'); }
154
+
155
+ const res = await fetch(url, { headers });
156
+ const data = await res.json();
157
+ // Removed: console.log('[ChatInput] Skills loaded:', data);
158
+
159
+ if (data.success && Array.isArray(data.skills)) {
160
+ setSkills(data.skills);
161
+ }
162
+ } catch (error) {
163
+ console.error("Failed to load skills:", error);
164
+ }
165
+ };
166
+
167
+ // 當 user 狀態變化時重新載入 skills(登入/登出時)
168
+ useEffect(() => {
169
+ loadSystemLimits();
170
+ loadSkills();
171
+ }, [user]);
172
+
173
+ useEffect(() => {
174
+ // 只在 Web 環境下,LLM 回傳完畢後自動 focus
175
+ // Mobile App 不 auto-focus,避免鍵盤自動彈出干擾閱讀
176
+ if (!disabled && !isCapacitor) {
177
+ inputRef.current?.focus();
178
+ }
179
+ }, [disabled, isCapacitor]);
180
+
181
+ // 🔄 受控組件版本:自動調整高度(只在非 composition 期間)
182
+ useEffect(() => {
183
+ // 在 composition 期間不調整高度,避免干擾 IME
184
+ if (isComposingRef.current) return;
185
+
186
+ const textarea = inputRef.current;
187
+ if (!textarea) return;
188
+
189
+ // 調整高度
190
+ textarea.style.height = 'auto';
191
+ const newHeight = Math.min(textarea.scrollHeight, 200);
192
+ textarea.style.height = `${newHeight}px`;
193
+ textarea.style.overflowY = textarea.scrollHeight > 200 ? 'auto' : 'hidden';
194
+ }, [message]); // message 改變時調整高度
195
+
196
+ const handleSubmit = async (e: React.FormEvent) => {
197
+ e.preventDefault();
198
+
199
+ const messageText = message.trim();
200
+
201
+ // 需要有訊息或附件才能送出
202
+ if (!messageText && !hasAttachments) return;
203
+ if (disabled) return;
204
+
205
+ // 如果有附件,需要先上傳
206
+ if (hasAttachments) {
207
+ if (!user) {
208
+ alert("請先登入");
209
+ return;
210
+ }
211
+
212
+ setUploading(true);
213
+ try {
214
+ const idToken = await user.getIdToken();
215
+ const results = await uploadAll(idToken, user.uid, 'default');
216
+
217
+ // 取得成功上傳的檔案路徑
218
+ const attachmentPaths = results
219
+ .filter(r => r.success)
220
+ .map(r => convertToLocalPath(r.displayPath));
221
+
222
+ // 組合最終訊息
223
+ const finalMessage = attachmentPaths.length > 0
224
+ ? `${attachmentPaths.join('\n')}\n\n${messageText}`
225
+ : messageText;
226
+
227
+ // 發送訊息
228
+ if (finalMessage.trim()) {
229
+ onSend(finalMessage);
230
+ }
231
+
232
+ // 清空
233
+ setMessage('');
234
+ clearAllAttachments();
235
+ setAttachmentError(null);
236
+
237
+ } catch (error) {
238
+ console.error('Send error:', error);
239
+ setAttachmentError('發送失敗,請重試');
240
+ } finally {
241
+ setUploading(false);
242
+ }
243
+ } else {
244
+ // 沒有附件,直接發送訊息
245
+ onSend(messageText);
246
+ setMessage('');
247
+ }
248
+ };
249
+
250
+ const handleSkillSelect = (skill: Skill) => {
251
+ // Send skill name directly to chat
252
+ onSend(skill.name);
253
+ };
254
+
255
+ // 開啟編輯器(編輯模式)
256
+ const handleEditSkill = (skill: Skill) => {
257
+ setSkillToEdit(skill);
258
+ setManagerDrawerOpen(false);
259
+ setEditorOpen(true);
260
+ };
261
+
262
+ // 開啟編輯器(新增模式)
263
+ const handleCreateSkill = () => {
264
+ setSkillToEdit(null);
265
+ setManagerDrawerOpen(false);
266
+ setEditorOpen(true);
267
+ };
268
+
269
+ // Skills 變更後重新載入
270
+ const handleSkillsChange = () => {
271
+ loadSkills();
272
+ };
273
+
274
+ const handleKeyDown = (e: React.KeyboardEvent) => {
275
+ // 忽略輸入法組合中的 Enter
276
+ if (e.nativeEvent.isComposing) return;
277
+
278
+ if (e.key === "Enter") {
279
+ if (isDesktopWeb) {
280
+ // 桌面 Web:Shift+Enter 換行、Enter 送出
281
+ if (!e.shiftKey) {
282
+ e.preventDefault();
283
+ handleSubmit(e);
284
+ }
285
+ // Shift+Enter 時不阻止,允許換行
286
+ }
287
+ // 其他情況(手機 Web、iOS App、Android App):Enter 換行,不處理
288
+ }
289
+ };
290
+
291
+ // 處理輸入法 composition events(中文、日文、韓文等)
292
+ const handleCompositionStart = () => {
293
+ isComposingRef.current = true;
294
+ };
295
+
296
+ const handleCompositionEnd = (e: React.CompositionEvent<HTMLTextAreaElement>) => {
297
+ isComposingRef.current = false;
298
+
299
+ // 確保最終值被更新
300
+ setMessage(e.currentTarget.value);
301
+ };
302
+
303
+ // 處理一般輸入
304
+ const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
305
+ setMessage(e.target.value);
306
+ };
307
+
308
+ // 處理檔案選擇(統一附件系統:只加入預覽,送出時才上傳)
309
+ const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
310
+ const selectedFiles = e.target.files;
311
+ if (!selectedFiles || selectedFiles.length === 0) return;
312
+
313
+ // 加入附件列表(延遲上傳)
314
+ addFiles(Array.from(selectedFiles));
315
+
316
+ // Reset input value to allow selecting the same file again
317
+ if (fileInputRef.current) {
318
+ fileInputRef.current.value = "";
319
+ }
320
+ }, [addFiles]);
321
+
322
+ // 處理貼上事件(支援截圖和圖片)
323
+ const handlePaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
324
+ const items = e.clipboardData?.items;
325
+ if (!items) return;
326
+
327
+ const files: File[] = [];
328
+ for (const item of items) {
329
+ if (item.kind === 'file') {
330
+ const file = item.getAsFile();
331
+ if (file) {
332
+ files.push(file);
333
+ }
334
+ }
335
+ }
336
+
337
+ // 如果有檔案,加入附件列表並阻止預設的文字貼上
338
+ if (files.length > 0) {
339
+ e.preventDefault();
340
+ addFiles(files);
341
+ }
342
+ // 如果沒有檔案(純文字),讓預設行為處理
343
+ }, [addFiles]);
344
+
345
+ // 處理拖放事件
346
+ const handleDrop = useCallback((files: File[]) => {
347
+ addFiles(files);
348
+ }, [addFiles]);
349
+
350
+ const handleFileButtonClick = () => {
351
+ fileInputRef.current?.click();
352
+ };
353
+
354
+ const handleVideoCaptureClick = () => {
355
+ videoInputRef.current?.click();
356
+ };
357
+
358
+ // 使用 Capacitor Camera API 處理拍照/錄影/相簿選取(統一附件系統)
359
+ const handleMediaCapture = async () => {
360
+ try {
361
+ // 使用 Camera.getPhoto 搭配 Prompt 模式
362
+ // 會顯示原生選單:拍照、從相簿選擇
363
+ const media = await Camera.getPhoto({
364
+ quality: 90,
365
+ allowEditing: false,
366
+ resultType: CameraResultType.Uri,
367
+ source: CameraSource.Camera, // 關鍵:不跳出選項,直接進入相機
368
+ saveToGallery: false, // 拍照後不自動存到相簿(避免重複)
369
+ });
370
+
371
+ if (!media.webPath) {
372
+ throw new Error("無法取得媒體檔案");
373
+ }
374
+
375
+ // 將 webPath 轉換為 Blob
376
+ const response = await fetch(media.webPath);
377
+ const blob = await response.blob();
378
+
379
+ // 根據 format 判斷檔案類型和副檔名
380
+ const format = media.format || 'jpg';
381
+ const mimeType = blob.type || (format.includes('mp4') || format.includes('mov') ? 'video/mp4' : 'image/jpeg');
382
+ const fileName = `capture_${Date.now()}.${format}`;
383
+ const file = new File([blob], fileName, { type: mimeType });
384
+
385
+ // 加入附件列表(延遲上傳)
386
+ addFiles([file]);
387
+
388
+ } catch (error: any) {
389
+ // 使用者取消操作不顯示錯誤
390
+ if (error.message === 'User cancelled photos app') {
391
+ console.log('📸 [Media Capture] 使用者取消操作');
392
+ } else {
393
+ console.error('❌ [Media Capture] 失敗:', error);
394
+ alert("媒體操作失敗: " + (error instanceof Error ? error.message : "未知錯誤"));
395
+ }
396
+ }
397
+ };
398
+
399
+ // Helper: 將 base64 字串轉換為 Blob
400
+ const base64ToBlob = (base64: string, mimeType: string): Blob => {
401
+ const byteCharacters = atob(base64);
402
+ const byteNumbers = new Array(byteCharacters.length);
403
+ for (let i = 0; i < byteCharacters.length; i++) {
404
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
405
+ }
406
+ const byteArray = new Uint8Array(byteNumbers);
407
+ return new Blob([byteArray], { type: mimeType });
408
+ };
409
+
410
+ // Helper: 記憶體監控
411
+ const logMemoryUsage = (label: string) => {
412
+ if ((performance as any).memory) {
413
+ const m = (performance as any).memory;
414
+ console.log(`💾 [Memory] ${label}:`, {
415
+ used: `${(m.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
416
+ total: `${(m.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
417
+ percent: `${((m.usedJSHeapSize / m.jsHeapSizeLimit) * 100).toFixed(1)}%`,
418
+ });
419
+ }
420
+ };
421
+
422
+ // 儲存錄音到 IndexedDB
423
+ const saveAudioToIndexedDB = async (audioBlob: Blob, timestamp: number) => {
424
+ try {
425
+ const dbName = 'AudioRecordings';
426
+ const storeName = 'recordings';
427
+
428
+ const request = indexedDB.open(dbName, 1);
429
+
430
+ request.onupgradeneeded = (event) => {
431
+ const db = (event.target as IDBOpenDBRequest).result;
432
+ if (!db.objectStoreNames.contains(storeName)) {
433
+ db.createObjectStore(storeName, { keyPath: 'timestamp' });
434
+ }
435
+ };
436
+
437
+ return new Promise<void>((resolve, reject) => {
438
+ request.onsuccess = () => {
439
+ const db = request.result;
440
+ const transaction = db.transaction([storeName], 'readwrite');
441
+ const store = transaction.objectStore(storeName);
442
+ store.put({ timestamp, audioBlob, createdAt: new Date().toISOString() });
443
+
444
+ transaction.oncomplete = () => {
445
+ console.log('✅ 錄音已儲存到 IndexedDB');
446
+ resolve();
447
+ };
448
+
449
+ transaction.onerror = () => reject(transaction.error);
450
+ };
451
+
452
+ request.onerror = () => reject(request.error);
453
+ });
454
+ } catch (error) {
455
+ console.error('儲存錄音失敗:', error);
456
+ }
457
+ };
458
+
459
+ // 從 IndexedDB 讀取錄音
460
+ const getAudioFromIndexedDB = async (timestamp: number): Promise<Blob | null> => {
461
+ try {
462
+ const dbName = 'AudioRecordings';
463
+ const storeName = 'recordings';
464
+
465
+ const request = indexedDB.open(dbName, 1);
466
+
467
+ return new Promise((resolve, reject) => {
468
+ request.onsuccess = () => {
469
+ const db = request.result;
470
+ const transaction = db.transaction([storeName], 'readonly');
471
+ const store = transaction.objectStore(storeName);
472
+ const getRequest = store.get(timestamp);
473
+
474
+ getRequest.onsuccess = () => {
475
+ const result = getRequest.result;
476
+ resolve(result ? result.audioBlob : null);
477
+ };
478
+
479
+ getRequest.onerror = () => reject(getRequest.error);
480
+ };
481
+
482
+ request.onerror = () => reject(request.error);
483
+ });
484
+ } catch (error) {
485
+ console.error('讀取錄音失敗:', error);
486
+ return null;
487
+ }
488
+ };
489
+
490
+ // 轉錄音訊(帶 retry 機制)- 使用 presigned URL 避免 413 錯誤
491
+ const transcribeAudio = async (audioBlob: Blob, timestamp: number, retryCount = 0): Promise<boolean> => {
492
+ const maxRetries = 3;
493
+
494
+ try {
495
+ setTranscribing(true);
496
+ setTranscriptionError(null);
497
+ setTranscriptionStatus(`🎤 轉錄中... ${retryCount > 0 ? `(重試 ${retryCount}/${maxRetries})` : ''}`);
498
+
499
+ if (!user) {
500
+ throw new Error("請先登入");
501
+ }
502
+
503
+ const idToken = await user.getIdToken();
504
+
505
+ const mimeType = audioBlob.type || 'audio/webm';
506
+ const extension = mimeType.includes('mp4') || mimeType.includes('m4a') ? 'm4a' :
507
+ mimeType.includes('aac') ? 'aac' :
508
+ mimeType.includes('mpeg') || mimeType.includes('mp3') ? 'mp3' : 'webm';
509
+ const fileName = `recording-${timestamp}.${extension}`;
510
+
511
+ const audioFile = new File([audioBlob], fileName, { type: mimeType });
512
+
513
+ // 🔍 記錄音訊檔案資訊
514
+ const fileSizeMB = (audioFile.size / 1024 / 1024).toFixed(2);
515
+ console.log(`🎤 [Transcribe] Audio file: ${fileSizeMB} MB (${audioFile.size} bytes), ${mimeType}`);
516
+
517
+ // 🚀 Step 1: 上傳音訊到 GCS(使用統一上傳服務)
518
+ setTranscriptionStatus(`📤 上傳音訊中...`);
519
+ const uploadStartTime = Date.now();
520
+ console.log(`📤 [Transcribe] Upload started at ${new Date(uploadStartTime).toLocaleTimeString()}`);
521
+
522
+ const uploadResult = await uploadFile({
523
+ file: audioFile,
524
+ idToken,
525
+ userId: user.uid,
526
+ projectId: 'default'
527
+ });
528
+
529
+ const uploadEndTime = Date.now();
530
+ const uploadDuration = ((uploadEndTime - uploadStartTime) / 1000).toFixed(2);
531
+
532
+ if (!uploadResult.success) {
533
+ console.error(`❌ [Transcribe] Upload failed after ${uploadDuration}s:`, uploadResult.error);
534
+ throw new Error(uploadResult.error || "音訊上傳失敗");
535
+ }
536
+
537
+ const gcsPath = uploadResult.displayPath;
538
+ const audioUploadId = uploadResult.audioUploadId;
539
+ console.log(`✅ [Transcribe] Upload completed in ${uploadDuration}s → ${gcsPath}`);
540
+ console.log(`🎫 [Transcribe] audioUploadId: ${audioUploadId}`);
541
+ logMemoryUsage('After upload completed');
542
+
543
+ // 🚀 Step 2: 呼叫轉錄 API(傳遞 GCS URI 和 audioUploadId)
544
+ setTranscriptionStatus(`🎤 轉錄中... ${retryCount > 0 ? `(重試 ${retryCount}/${maxRetries})` : ''}`);
545
+ const transcribeStartTime = Date.now();
546
+ console.log(`🎤 [Transcribe] Transcription started at ${new Date(transcribeStartTime).toLocaleTimeString()}`);
547
+
548
+ const response = await fetch(getApiEndpoint("/api/transcribe-audio"), {
549
+ method: "POST",
550
+ headers: {
551
+ "Content-Type": "application/json",
552
+ "Authorization": `Bearer ${idToken}`,
553
+ ...(audioUploadId && { "X-Audio-Upload-Id": audioUploadId }),
554
+ },
555
+ body: JSON.stringify({
556
+ gcsUri: gcsPath,
557
+ model: transcriptionModel,
558
+ }),
559
+ });
560
+
561
+ const transcribeEndTime = Date.now();
562
+ const transcribeDuration = ((transcribeEndTime - transcribeStartTime) / 1000).toFixed(2);
563
+
564
+ if (!response.ok) {
565
+ console.error(`❌ [Transcribe] API failed after ${transcribeDuration}s: HTTP ${response.status}`);
566
+ throw new Error(`HTTP error! status: ${response.status}`);
567
+ }
568
+
569
+ const result = await response.json();
570
+ console.log(`🔍 [Transcribe] API response received in ${transcribeDuration}s:`, {
571
+ success: result.success,
572
+ resultsCount: result.results?.length || 0,
573
+ hasResults: !!result.results,
574
+ firstResultKeys: result.results?.[0] ? Object.keys(result.results[0]) : [],
575
+ textLength: result.results?.[0]?.text?.length || 0
576
+ });
577
+
578
+ // 🔍 Debug: 詳細檢查條件
579
+ console.log(`🔍 [Transcribe] Condition check:`, {
580
+ 'response.ok': response.ok,
581
+ 'result.success': result.success,
582
+ 'result.results': !!result.results,
583
+ 'result.results.length': result.results?.length || 0,
584
+ 'condition met': response.ok && result.success && result.results && result.results.length > 0
585
+ });
586
+
587
+ if (response.ok && result.success && result.results && result.results.length > 0) {
588
+ // 轉錄成功
589
+ const transcripts = result.results.map((r: any) => r.text).join('\n\n');
590
+ const textLength = transcripts.length;
591
+ const totalDuration = ((transcribeEndTime - uploadStartTime) / 1000).toFixed(2);
592
+
593
+ console.log(`✅ [Transcribe] Success! Transcription completed in ${transcribeDuration}s`);
594
+ console.log(`📊 [Transcribe] Summary: Upload ${uploadDuration}s + Transcribe ${transcribeDuration}s = Total ${totalDuration}s`);
595
+ console.log(`📝 [Transcribe] Transcribed ${textLength} characters`);
596
+
597
+ // 檢查是否為空字串(未偵測到語音)
598
+ if (transcripts === '' || transcripts.trim() === '') {
599
+ console.log('ℹ️ [Transcribe] No speech detected in audio');
600
+ setTranscriptionStatus('no-speech');
601
+ setRecordedAudio(null);
602
+ setTranscriptionError(null);
603
+
604
+ // 5秒後清除狀態
605
+ setTimeout(() => {
606
+ setTranscriptionStatus("");
607
+ setTranscribing(false);
608
+ }, 5000);
609
+
610
+ return true;
611
+ }
612
+
613
+ // 有內容的正常轉錄,自動送出
614
+ onSend(transcripts);
615
+
616
+ setTranscriptionStatus(`✅ 轉錄成功!`);
617
+ setRecordedAudio(null);
618
+ setTranscriptionError(null);
619
+
620
+ // 3秒後清除狀態
621
+ setTimeout(() => {
622
+ setTranscriptionStatus("");
623
+ setTranscribing(false);
624
+ }, 3000);
625
+
626
+ return true;
627
+ } else {
628
+ console.error('❌ [Transcribe] Failed:', result.error || "轉錄失敗");
629
+ throw new Error(result.error || "轉錄失敗");
630
+ }
631
+ } catch (error) {
632
+ console.error(`轉錄錯誤 (嘗試 ${retryCount + 1}/${maxRetries + 1}):`, error);
633
+
634
+ // 如果還有重試機會,自動重試
635
+ if (retryCount < maxRetries) {
636
+ console.log(`⚠️ 將在 2 秒後重試...`);
637
+ await new Promise(resolve => setTimeout(resolve, 2000));
638
+ return transcribeAudio(audioBlob, timestamp, retryCount + 1);
639
+ } else {
640
+ // 已達最大重試次數
641
+ const errorMessage = error instanceof Error ? error.message : "未知錯誤";
642
+ setTranscriptionError(`轉錄失敗(已重試 ${maxRetries} 次): ${errorMessage}`);
643
+ setTranscriptionStatus("");
644
+ setTranscribing(false);
645
+ return false;
646
+ }
647
+ }
648
+ };
649
+
650
+ // 開始/停止錄音(使用 Capacitor Audio Recorder,跨平台支援)
651
+ const handleRecordingToggle = async () => {
652
+ if (isRecording) {
653
+ // 停止錄音
654
+ try {
655
+ const result = await AudioRecorder.stopRecording();
656
+ console.log('🎤 Recording stopped:', result);
657
+
658
+ // 🔥 Android: 關閉前景服務
659
+ if ((window as any).Capacitor?.getPlatform() === 'android') {
660
+ try {
661
+ await ForegroundService.stopForegroundService();
662
+ console.log('🔥 Foreground service stopped');
663
+ } catch (fgError) {
664
+ console.warn('⚠️ Failed to stop foreground service:', fgError);
665
+ }
666
+ }
667
+
668
+ const timestamp = Date.now();
669
+ let audioBlob: Blob | null = null;
670
+
671
+ // Web 平台返回 blob,Native 平台返回 uri
672
+ if (result.blob) {
673
+ // Web: 直接使用 blob
674
+ console.log('🎤 [Web] Using blob');
675
+ audioBlob = result.blob;
676
+ } else if (result.uri) {
677
+ // Native (iOS/Android): 從 uri 讀取檔案並轉換為 blob
678
+ try {
679
+ const fileData = await Filesystem.readFile({
680
+ path: result.uri
681
+ });
682
+
683
+ const base64Data = fileData.data as string;
684
+ const mimeType = 'audio/mp4';
685
+ audioBlob = base64ToBlob(base64Data, mimeType);
686
+
687
+ if (audioBlob.size === 0) {
688
+ throw new Error('Blob size is 0 - conversion failed');
689
+ }
690
+ } catch (fileError) {
691
+ console.error('Failed to read/convert file:', fileError);
692
+ throw new Error('無法讀取錄音檔案: ' + (fileError instanceof Error ? fileError.message : '未知錯誤'));
693
+ }
694
+ }
695
+
696
+ if (audioBlob) {
697
+ // 儲存到 IndexedDB
698
+ await saveAudioToIndexedDB(audioBlob, timestamp);
699
+
700
+ // 儲存到 state(用於 retry 和下載)
701
+ setRecordedAudio(audioBlob);
702
+
703
+ // 開始轉錄
704
+ await transcribeAudio(audioBlob, timestamp);
705
+ }
706
+
707
+ setIsRecording(false);
708
+ } catch (error) {
709
+ console.error('❌ Failed to stop recording:', error);
710
+ alert('停止錄音失敗: ' + (error instanceof Error ? error.message : '未知錯誤'));
711
+ setIsRecording(false);
712
+ }
713
+ } else {
714
+ // 開始錄音
715
+ try {
716
+ // 請求麥克風權限
717
+ const permissions = await AudioRecorder.requestPermissions();
718
+ console.log('🎤 Audio Permissions:', permissions);
719
+
720
+ if (permissions.recordAudio !== 'granted') {
721
+ alert('需要麥克風權限才能錄音');
722
+ return;
723
+ }
724
+
725
+ // 🔥 Android: 請求通知權限並啟動前景服務(讓錄音在背景/螢幕關閉時繼續)
726
+ if ((window as any).Capacitor?.getPlatform() === 'android') {
727
+ try {
728
+ // Android 13+ (API 33+) 需要明確請求通知權限
729
+ const notificationPermission = await ForegroundService.checkPermissions();
730
+ console.log('🔔 Notification Permission:', notificationPermission);
731
+
732
+ if (notificationPermission.display !== 'granted') {
733
+ console.log('🔔 Requesting notification permission...');
734
+ const result = await ForegroundService.requestPermissions();
735
+ console.log('🔔 Notification Permission Result:', result);
736
+
737
+ if (result.display !== 'granted') {
738
+ console.warn('⚠️ Notification permission denied, foreground service may not show notification');
739
+ // 繼續執行,但前景服務可能無法顯示通知
740
+ }
741
+ }
742
+
743
+ // 啟動前景服務
744
+ await ForegroundService.startForegroundService({
745
+ id: 1,
746
+ title: '正在錄音',
747
+ body: '點擊返回 App',
748
+ smallIcon: 'ic_launcher',
749
+ });
750
+ console.log('🔥 Foreground service started');
751
+ } catch (fgError) {
752
+ console.warn('⚠️ Failed to start foreground service:', fgError);
753
+ // 繼續錄音,即使前景服務啟動失敗
754
+ }
755
+ }
756
+
757
+ // 開始錄音(優化設定:極致壓縮 - 方案 1)
758
+ // 32 kbps + 16 kHz = ~240 KB/分鐘(相較原本 1.4 MB 減少 83%)
759
+ // 上傳速度從 ~1.1 秒加速到 ~0.2 秒(5.5 倍加速)
760
+ await AudioRecorder.startRecording({
761
+ bitRate: 32000, // 32 kbps(語音辨識足夠)
762
+ sampleRate: 16000, // 16 kHz(Whisper 最低支援,覆蓋所有語音頻率)
763
+ });
764
+ console.log('🎤 Recording started');
765
+
766
+ setIsRecording(true);
767
+ setTranscriptionStatus("🎤 錄音中...");
768
+ } catch (error) {
769
+ console.error('❌ Failed to start recording:', error);
770
+ alert('無法啟動錄音: ' + (error instanceof Error ? error.message : '未知錯誤'));
771
+ }
772
+ }
773
+ };
774
+
775
+ // 下載錄音檔案
776
+ const handleDownloadRecording = async () => {
777
+ if (!recordedAudio) return;
778
+
779
+ try {
780
+ // 檢測平台
781
+ const isNative = (window as any).Capacitor?.isNativePlatform() || false;
782
+
783
+ if (isNative) {
784
+ // Native 平台(iOS/Android):使用 Share API 觸發分享面板
785
+ console.log('🔽 [Download] Native platform detected, using Share API');
786
+
787
+ // 轉換 Blob 為 base64
788
+ const reader = new FileReader();
789
+ reader.onloadend = async () => {
790
+ try {
791
+ const base64Data = reader.result as string;
792
+ const base64String = base64Data.split(',')[1]; // 移除 data URL prefix
793
+
794
+ const fileName = `recording-${Date.now()}.${recordedAudio.type.includes('mp4') ? 'm4a' : 'webm'}`;
795
+
796
+ // 寫入檔案到 Cache 目錄(暫存)
797
+ const result = await Filesystem.writeFile({
798
+ path: fileName,
799
+ data: base64String,
800
+ directory: Directory.Cache,
801
+ });
802
+
803
+ console.log('✅ [Download] Temp file created:', result.uri);
804
+
805
+ // 使用 Share API 觸發系統分享面板
806
+ await Share.share({
807
+ title: '儲存錄音檔案',
808
+ text: `錄音檔案:${fileName}`,
809
+ url: result.uri, // iOS/Android 支援 file:// URLs
810
+ dialogTitle: '選擇儲存位置',
811
+ });
812
+
813
+ console.log('✅ [Download] Share dialog completed');
814
+ } catch (error) {
815
+ const errorMessage = error instanceof Error ? error.message : String(error);
816
+
817
+ // 如果使用者取消分享,不顯示錯誤訊息
818
+ if (errorMessage.toLowerCase().includes('cancel')) {
819
+ console.log('ℹ️ [Download] Share cancelled by user');
820
+ return;
821
+ }
822
+
823
+ // 其他錯誤顯示為下載失敗
824
+ console.error('❌ [Download] Failed to save file:', error);
825
+ alert('下載失敗: ' + errorMessage);
826
+ }
827
+ };
828
+ reader.readAsDataURL(recordedAudio);
829
+ } else {
830
+ // Web 平台:使用傳統下載方式
831
+ console.log('🔽 [Download] Web platform detected, using blob URL');
832
+
833
+ const url = URL.createObjectURL(recordedAudio);
834
+ const a = document.createElement('a');
835
+ a.href = url;
836
+ a.download = `recording-${Date.now()}.webm`;
837
+ document.body.appendChild(a);
838
+ a.click();
839
+ document.body.removeChild(a);
840
+ URL.revokeObjectURL(url);
841
+
842
+ console.log('✅ [Download] File downloaded');
843
+ }
844
+ } catch (error) {
845
+ console.error('❌ [Download] Error:', error);
846
+ alert('下載失敗: ' + (error instanceof Error ? error.message : '未知錯誤'));
847
+ }
848
+ };
849
+
850
+ // 重試轉錄
851
+ const handleRetryTranscription = async () => {
852
+ if (!recordedAudio) return;
853
+
854
+ const timestamp = Date.now();
855
+ await transcribeAudio(recordedAudio, timestamp);
856
+ };
857
+
858
+
859
+ return (
860
+ <DropZone onDrop={handleDrop} disabled={disabled || uploading || !canAddMore}>
861
+ <div className="w-full border-t bg-background">
862
+ {/* 錄音/轉錄狀態:只有異常時才顯示,正常狀態不凸顯(按鈕本身已表示狀態) */}
863
+
864
+ {/* 轉錄錯誤處理 */}
865
+ {transcriptionError && recordedAudio && (
866
+ <div className="relative flex items-start gap-3 p-4 pb-3 bg-red-50/50 dark:bg-red-950/30">
867
+ <div className="flex-shrink-0">
868
+ <AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
869
+ </div>
870
+ {/* [D-2] 加入 pr-8 預留 X 按鈕空間 */}
871
+ <div className="flex-1 pr-8">
872
+ <p className="text-sm font-medium text-red-900 dark:text-red-200 mb-2">
873
+ {transcriptionError}
874
+ </p>
875
+ {/* [D-1] 加入 flex-col sm:flex-row 實現 responsive */}
876
+ <div className="flex flex-col sm:flex-row gap-2">
877
+ <Button
878
+ type="button"
879
+ size="sm"
880
+ onClick={handleRetryTranscription}
881
+ disabled={transcribing}
882
+ variant="outline"
883
+ className="border-red-300 hover:bg-red-100 dark:border-red-700 dark:hover:bg-red-900"
884
+ >
885
+ {transcribing ? (
886
+ <>
887
+ <Loader2 className="mr-2 h-3 w-3 animate-spin" />
888
+ 重試中...
889
+ </>
890
+ ) : (
891
+ <>
892
+ <RefreshCw className="mr-2 h-3 w-3" />
893
+ 重試轉錄
894
+ </>
895
+ )}
896
+ </Button>
897
+ <Button
898
+ type="button"
899
+ size="sm"
900
+ onClick={handleDownloadRecording}
901
+ variant="outline"
902
+ className="border-red-300 hover:bg-red-100 dark:border-red-700 dark:hover:bg-red-900"
903
+ >
904
+ <Download className="mr-2 h-3 w-3" />
905
+ 下載錄音
906
+ </Button>
907
+ </div>
908
+ </div>
909
+ <div className="absolute top-2 right-2">
910
+ <Button
911
+ type="button"
912
+ size="icon"
913
+ variant="ghost"
914
+ onClick={() => setTranscriptionError(null)}
915
+ className="h-6 w-6 text-red-600 hover:bg-red-100 dark:text-red-400 dark:hover:bg-red-900"
916
+ aria-label="關閉錯誤訊息"
917
+ >
918
+ <X className="h-4 w-4" />
919
+ </Button>
920
+ </div>
921
+ </div>
922
+ )}
923
+
924
+ {/* 未偵測到語音訊息 */}
925
+ {transcriptionStatus === 'no-speech' && !transcriptionError && (
926
+ <div className="relative">
927
+ <div className="flex gap-3 p-4 pb-2 bg-blue-50 dark:bg-blue-950/50 border-l-4 border-blue-500">
928
+ <Info className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
929
+ <div className="flex-1 min-w-0">
930
+ <p className="text-sm font-medium text-blue-900 dark:text-blue-200">
931
+ 未偵測到語音內容
932
+ </p>
933
+ <p className="text-xs text-blue-700 dark:text-blue-400 mt-1">
934
+ 請確認麥克風權限已開啟,或嘗試重新錄音
935
+ </p>
936
+ </div>
937
+ </div>
938
+ <div className="absolute top-2 right-2">
939
+ <Button
940
+ type="button"
941
+ size="icon"
942
+ variant="ghost"
943
+ onClick={() => setTranscriptionStatus("")}
944
+ className="h-6 w-6 text-blue-600 hover:bg-blue-100 dark:text-blue-400 dark:hover:bg-blue-900"
945
+ aria-label="關閉訊息"
946
+ >
947
+ <X className="h-4 w-4" />
948
+ </Button>
949
+ </div>
950
+ </div>
951
+ )}
952
+
953
+ {/* 錄音/轉錄狀態 */}
954
+ {(transcribing || transcriptionStatus) && !transcriptionError && transcriptionStatus !== 'no-speech' && (
955
+ <div className="flex items-center gap-2 p-4 pb-2 bg-amber-50/50 dark:bg-amber-950/30">
956
+ <Loader2 className="h-4 w-4 animate-spin text-amber-600 dark:text-amber-500" />
957
+ <div className="flex-1">
958
+ <p className="text-sm font-medium text-amber-900 dark:text-amber-200">
959
+ {transcriptionStatus}
960
+ </p>
961
+ <p className="text-xs text-amber-700 dark:text-amber-400">
962
+ 轉錄完成後會自動送出(錄音已安全儲存)
963
+ </p>
964
+ </div>
965
+ </div>
966
+ )}
967
+
968
+ {/* 上傳中狀態 */}
969
+ {uploading && (
970
+ <div className="flex items-center gap-2 p-4 pb-2 bg-blue-50/50 dark:bg-blue-950/30">
971
+ <Loader2 className="h-4 w-4 animate-spin text-blue-600 dark:text-blue-500" />
972
+ <p className="text-sm font-medium text-blue-900 dark:text-blue-200">
973
+ 📤 正在自動上傳檔案...
974
+ </p>
975
+ </div>
976
+ )}
977
+
978
+ {/* 附件錯誤提示 */}
979
+ {attachmentError && (
980
+ <div className="relative flex items-start gap-3 p-4 pb-3 bg-red-50/50 dark:bg-red-950/30">
981
+ <AlertCircle className="h-5 w-5 flex-shrink-0 text-red-600 dark:text-red-400" />
982
+ <p className="text-sm font-medium text-red-900 dark:text-red-200 flex-1">
983
+ {attachmentError}
984
+ </p>
985
+ <Button
986
+ type="button"
987
+ size="icon"
988
+ variant="ghost"
989
+ onClick={() => setAttachmentError(null)}
990
+ className="h-6 w-6 text-red-600 hover:bg-red-100 dark:text-red-400 dark:hover:bg-red-900 flex-shrink-0"
991
+ aria-label="關閉錯誤訊息"
992
+ >
993
+ <X className="h-4 w-4" />
994
+ </Button>
995
+ </div>
996
+ )}
997
+
998
+ {/* 附件預覽區 */}
999
+ <AttachmentPreview
1000
+ attachments={attachments}
1001
+ maxCount={20}
1002
+ onRemove={removeAttachment}
1003
+ onRetry={retryUpload}
1004
+ onClearAll={clearAllAttachments}
1005
+ disabled={disabled || uploading}
1006
+ />
1007
+
1008
+ <form
1009
+ onSubmit={handleSubmit}
1010
+ className="flex w-full gap-2 p-3 sm:p-4"
1011
+ >
1012
+ {/* 檔案上傳 input (用於選擇本機檔案、多選等) */}
1013
+ <input
1014
+ ref={fileInputRef}
1015
+ type="file"
1016
+ multiple
1017
+ onChange={handleFileSelect}
1018
+ className="hidden"
1019
+ />
1020
+
1021
+ {/* 錄影專用 input */}
1022
+ <input
1023
+ ref={videoInputRef}
1024
+ type="file"
1025
+ capture="environment"
1026
+ onChange={handleFileSelect}
1027
+ className="hidden"
1028
+ />
1029
+
1030
+ {/* 上傳按鈕 - Web 直接上傳,App 顯示下拉選單 */}
1031
+ {isCapacitor ? (
1032
+ // Capacitor App:保留下拉選單(拍照、錄影)
1033
+ <DropdownMenu>
1034
+ <DropdownMenuTrigger asChild>
1035
+ <Button
1036
+ type="button"
1037
+ size="icon"
1038
+ variant="outline"
1039
+ disabled={disabled}
1040
+ className="shrink-0"
1041
+ title="上傳檔案"
1042
+ >
1043
+ <Paperclip className="h-4 w-4" />
1044
+ </Button>
1045
+ </DropdownMenuTrigger>
1046
+ <DropdownMenuContent align="start" className="w-64">
1047
+ {/* Capacitor App:拍照和錄影分離 */}
1048
+ {isCapacitor && (
1049
+ <>
1050
+ <DropdownMenuItem
1051
+ onClick={handleFileButtonClick}
1052
+ className="cursor-pointer hover:bg-accent font-medium"
1053
+ >
1054
+ <Folder className="mr-2 h-4 w-4" />
1055
+ <span>上傳檔案</span>
1056
+ </DropdownMenuItem>
1057
+ {/* 拍照和錄影功能僅在 Android 平台顯示 */}
1058
+ {isAndroid && (
1059
+ <>
1060
+ <DropdownMenuItem
1061
+ onClick={handleMediaCapture}
1062
+ className="cursor-pointer hover:bg-accent font-medium"
1063
+ >
1064
+ <CameraIcon className="mr-2 h-4 w-4" />
1065
+ <span>拍照</span>
1066
+ </DropdownMenuItem>
1067
+ <DropdownMenuItem
1068
+ onClick={handleVideoCaptureClick}
1069
+ className="cursor-pointer hover:bg-accent font-medium"
1070
+ >
1071
+ <Video className="mr-2 h-4 w-4" />
1072
+ <span>錄影</span>
1073
+ </DropdownMenuItem>
1074
+ </>
1075
+ )}
1076
+ </>
1077
+ )}
1078
+
1079
+ </DropdownMenuContent>
1080
+ </DropdownMenu>
1081
+ ) : (
1082
+ // Web 環境:直接點擊上傳
1083
+ <Button
1084
+ type="button"
1085
+ size="icon"
1086
+ variant="outline"
1087
+ disabled={disabled}
1088
+ className="shrink-0"
1089
+ onClick={handleFileButtonClick}
1090
+ title="上傳檔案"
1091
+ >
1092
+ <Paperclip className="h-4 w-4" />
1093
+ </Button>
1094
+ )}
1095
+
1096
+ {/* Input Field - 受控組件 + composition 保護 */}
1097
+ <Textarea
1098
+ ref={inputRef}
1099
+ value={message}
1100
+ onChange={handleChange}
1101
+ onKeyDown={handleKeyDown}
1102
+ onPaste={handlePaste}
1103
+ onCompositionStart={handleCompositionStart}
1104
+ onCompositionEnd={handleCompositionEnd}
1105
+ placeholder={hasAttachments ? '新增說明文字...' : placeholder}
1106
+ disabled={disabled}
1107
+ className="flex-1 text-base resize-none overflow-hidden min-h-[2.25rem] max-h-[200px] leading-normal py-2"
1108
+ autoComplete="off"
1109
+ autoFocus
1110
+ rows={1}
1111
+ inputMode="text"
1112
+ />
1113
+
1114
+ {/* 動態右側按鈕:Stop (AI) > Stop (錄音) > Send > Mic */}
1115
+ {isLoading ? (
1116
+ // 1. Stop AI response (最高優先級)
1117
+ // 旋轉圓環 + 停止方塊:表示 AI 思考中,可點擊停止
1118
+ <Button
1119
+ type="button"
1120
+ size="icon"
1121
+ variant="ghost"
1122
+ onClick={onStop}
1123
+ disabled={isStopping}
1124
+ className="shrink-0 relative hover:bg-destructive/10"
1125
+ title="AI 思考中,點擊停止"
1126
+ >
1127
+ {isStopping ? (
1128
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
1129
+ ) : (
1130
+ <div className="relative flex items-center justify-center w-6 h-6">
1131
+ {/* 外圈旋轉動畫 - 漸層弧線 */}
1132
+ <svg className="absolute w-6 h-6 animate-spin" viewBox="0 0 24 24">
1133
+ <circle
1134
+ className="opacity-20"
1135
+ cx="12"
1136
+ cy="12"
1137
+ r="10"
1138
+ stroke="currentColor"
1139
+ strokeWidth="2"
1140
+ fill="none"
1141
+ />
1142
+ <circle
1143
+ className="text-primary"
1144
+ cx="12"
1145
+ cy="12"
1146
+ r="10"
1147
+ stroke="currentColor"
1148
+ strokeWidth="2"
1149
+ fill="none"
1150
+ strokeDasharray="32"
1151
+ strokeDashoffset="12"
1152
+ strokeLinecap="round"
1153
+ />
1154
+ </svg>
1155
+ {/* 中心停止方塊 */}
1156
+ <Square className="h-2.5 w-2.5 text-primary fill-primary" />
1157
+ </div>
1158
+ )}
1159
+ </Button>
1160
+ ) : isRecording ? (
1161
+ // 2. Stop recording
1162
+ <Button
1163
+ type="button"
1164
+ size="icon"
1165
+ variant="destructive"
1166
+ onClick={handleRecordingToggle}
1167
+ disabled={disabled || transcribing}
1168
+ className="shrink-0"
1169
+ title="停止錄音"
1170
+ >
1171
+ <Square className="h-4 w-4" />
1172
+ </Button>
1173
+ ) : message.trim() ? (
1174
+ // 3. Send message (有文字訊息)
1175
+ <Button
1176
+ type="submit"
1177
+ size="icon"
1178
+ disabled={disabled || uploading}
1179
+ className="shrink-0"
1180
+ title="送出訊息"
1181
+ >
1182
+ {uploading ? (
1183
+ <Loader2 className="h-4 w-4 animate-spin" />
1184
+ ) : (
1185
+ <Send className="h-4 w-4" />
1186
+ )}
1187
+ </Button>
1188
+ ) : hasAttachments ? (
1189
+ // 4. 有附件但無文字:顯示錄音 + 發送雙按鈕
1190
+ <div className="flex items-center gap-1">
1191
+ <Button
1192
+ type="button"
1193
+ size="icon"
1194
+ variant="outline"
1195
+ onClick={handleRecordingToggle}
1196
+ disabled={disabled || transcribing}
1197
+ className="shrink-0"
1198
+ title="開始錄音"
1199
+ >
1200
+ <Mic className="h-4 w-4" />
1201
+ </Button>
1202
+ <Button
1203
+ type="submit"
1204
+ size="icon"
1205
+ disabled={disabled || uploading}
1206
+ className="shrink-0"
1207
+ title="送出附件"
1208
+ >
1209
+ {uploading ? (
1210
+ <Loader2 className="h-4 w-4 animate-spin" />
1211
+ ) : (
1212
+ <Send className="h-4 w-4" />
1213
+ )}
1214
+ </Button>
1215
+ </div>
1216
+ ) : (
1217
+ // 5. Start recording (預設,無附件無文字)
1218
+ <Button
1219
+ type="button"
1220
+ size="icon"
1221
+ variant="outline"
1222
+ onClick={handleRecordingToggle}
1223
+ disabled={disabled || transcribing}
1224
+ className="shrink-0"
1225
+ title="開始錄音"
1226
+ >
1227
+ <Mic className="h-4 w-4" />
1228
+ </Button>
1229
+ )}
1230
+ </form>
1231
+
1232
+ {/* Skills Manager Drawer */}
1233
+ <SkillsManagerDrawer
1234
+ open={managerDrawerOpen}
1235
+ onOpenChange={setManagerDrawerOpen}
1236
+ onEdit={handleEditSkill}
1237
+ onCreate={handleCreateSkill}
1238
+ onSkillsChange={handleSkillsChange}
1239
+ />
1240
+
1241
+ {/* Skills Editor */}
1242
+ <SkillsEditor
1243
+ open={editorOpen}
1244
+ onOpenChange={setEditorOpen}
1245
+ skill={skillToEdit}
1246
+ onSaved={handleSkillsChange}
1247
+ />
1248
+ </div>
1249
+ </DropZone>
1250
+ );
1251
+ }