@tomako/tools-runtime 0.1.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 (531) hide show
  1. package/README.md +12 -0
  2. package/package.json +629 -0
  3. package/src/assets/icons/brands/social/bluesky-icon.tsx +9 -0
  4. package/src/assets/icons/brands/social/facebook-icon.tsx +9 -0
  5. package/src/assets/icons/brands/social/index.ts +8 -0
  6. package/src/assets/icons/brands/social/instagram-icon.tsx +9 -0
  7. package/src/assets/icons/brands/social/linkedin-icon.tsx +9 -0
  8. package/src/assets/icons/brands/social/threads-icon.tsx +9 -0
  9. package/src/assets/icons/brands/social/tiktok-icon.tsx +10 -0
  10. package/src/assets/icons/brands/social/x-icon.tsx +9 -0
  11. package/src/assets/icons/brands/social/youtube-icon.tsx +9 -0
  12. package/src/assets/icons/social-profile/facebook-icon.tsx +12 -0
  13. package/src/assets/icons/social-profile/index.ts +29 -0
  14. package/src/assets/icons/social-profile/instagram-icon.tsx +18 -0
  15. package/src/assets/icons/social-profile/linkedin-icon.tsx +12 -0
  16. package/src/assets/icons/social-profile/tiktok-icon.tsx +12 -0
  17. package/src/assets/icons/social-profile/x-icon.tsx +13 -0
  18. package/src/assets/icons/social-profile/youtube-icon.tsx +12 -0
  19. package/src/components/tools/engine/form-builder.tsx +219 -0
  20. package/src/components/tools/engine/package-widget-loader.tsx +38 -0
  21. package/src/components/tools/engine/package-widget-runtime.tsx +409 -0
  22. package/src/components/tools/engine/package-widget.tsx +77 -0
  23. package/src/components/tools/engine/result-view.tsx +596 -0
  24. package/src/components/tools/engine/use-legacy-llm-task.ts +64 -0
  25. package/src/components/tools/engine/use-tool-task.ts +195 -0
  26. package/src/components/tools/market-forecast-report.tsx +1150 -0
  27. package/src/components/tools/registry.ts +13 -0
  28. package/src/components/tools/workspace/index.ts +39 -0
  29. package/src/components/tools/workspace/tool-action-bar.tsx +99 -0
  30. package/src/components/tools/workspace/tool-empty-panel.tsx +32 -0
  31. package/src/components/tools/workspace/tool-error-panel.tsx +40 -0
  32. package/src/components/tools/workspace/tool-field.tsx +124 -0
  33. package/src/components/tools/workspace/tool-file-upload.tsx +112 -0
  34. package/src/components/tools/workspace/tool-form-section.tsx +21 -0
  35. package/src/components/tools/workspace/tool-form-shell.tsx +36 -0
  36. package/src/components/tools/workspace/tool-loading-panel.tsx +269 -0
  37. package/src/components/tools/workspace/tool-option-row.tsx +97 -0
  38. package/src/components/tools/workspace/tool-preview-panel.tsx +31 -0
  39. package/src/components/tools/workspace/tool-result-actions.tsx +67 -0
  40. package/src/components/tools/workspace/tool-result-file-actions.ts +31 -0
  41. package/src/components/tools/workspace/tool-result-header.tsx +39 -0
  42. package/src/components/tools/workspace/tool-status.tsx +13 -0
  43. package/src/components/tools/workspace/tool-workspace-layout.tsx +226 -0
  44. package/src/constants/LKS.ts +29 -0
  45. package/src/constants/related-tools.ts +18 -0
  46. package/src/constants/social-profile-kit.ts +284 -0
  47. package/src/constants/tool-assets.ts +7 -0
  48. package/src/env.d.ts +3 -0
  49. package/src/features/tools/_templates/standard-tool.container.tsx +90 -0
  50. package/src/features/tools/app-icon-resizer/app-icon-resizer.container.tsx +11 -0
  51. package/src/features/tools/app-icon-resizer/app-icon-resizer.spec.ts +15 -0
  52. package/src/features/tools/app-icon-resizer/widget/app-icon-resizer.tsx +444 -0
  53. package/src/features/tools/app-icon-resizer/widget/constants.ts +19 -0
  54. package/src/features/tools/app-icon-resizer/widget/helpers.ts +314 -0
  55. package/src/features/tools/app-icon-resizer/widget/index.ts +7 -0
  56. package/src/features/tools/app-icon-resizer/widget/platform-icons.tsx +59 -0
  57. package/src/features/tools/app-icon-resizer/widget/preview-dialogs.tsx +139 -0
  58. package/src/features/tools/app-icon-resizer/widget/result-shell.tsx +165 -0
  59. package/src/features/tools/app-icon-resizer/widget/types.ts +36 -0
  60. package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.container.tsx +108 -0
  61. package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.spec.ts +14 -0
  62. package/src/features/tools/app-store-screenshot-generator/config.ts +3 -0
  63. package/src/features/tools/app-store-screenshot-generator/widget/app-store-screenshot-generator.tsx +1250 -0
  64. package/src/features/tools/app-store-screenshot-generator/widget/asset-slots.ts +76 -0
  65. package/src/features/tools/app-store-screenshot-generator/widget/constants.ts +43 -0
  66. package/src/features/tools/app-store-screenshot-generator/widget/form-controls.tsx +214 -0
  67. package/src/features/tools/app-store-screenshot-generator/widget/generation.ts +528 -0
  68. package/src/features/tools/app-store-screenshot-generator/widget/index.ts +7 -0
  69. package/src/features/tools/app-store-screenshot-generator/widget/input-media.ts +175 -0
  70. package/src/features/tools/app-store-screenshot-generator/widget/types.ts +60 -0
  71. package/src/features/tools/app-store-screenshot-generator/widget/zip.ts +119 -0
  72. package/src/features/tools/build-in-public-story-generator/build-in-public-story-generator.container.tsx +135 -0
  73. package/src/features/tools/build-in-public-story-generator/build-in-public-story-generator.spec.ts +15 -0
  74. package/src/features/tools/build-in-public-story-generator/widget/build-in-public-story-generator.tsx +314 -0
  75. package/src/features/tools/build-in-public-story-generator/widget/constants.ts +3 -0
  76. package/src/features/tools/build-in-public-story-generator/widget/helpers.ts +25 -0
  77. package/src/features/tools/build-in-public-story-generator/widget/index.ts +7 -0
  78. package/src/features/tools/build-in-public-story-generator/widget/preview-panels.tsx +58 -0
  79. package/src/features/tools/build-in-public-story-generator/widget/result-shell.tsx +164 -0
  80. package/src/features/tools/build-in-public-story-generator/widget/types.ts +6 -0
  81. package/src/features/tools/cold-start-channel-selector/cold-start-channel-selector.container.tsx +30 -0
  82. package/src/features/tools/cold-start-channel-selector/cold-start-channel-selector.spec.ts +15 -0
  83. package/src/features/tools/cold-start-channel-selector/widget/cold-start-channel-selector.tsx +417 -0
  84. package/src/features/tools/cold-start-channel-selector/widget/constants.ts +46 -0
  85. package/src/features/tools/cold-start-channel-selector/widget/form-controls.tsx +41 -0
  86. package/src/features/tools/cold-start-channel-selector/widget/helpers.ts +75 -0
  87. package/src/features/tools/cold-start-channel-selector/widget/index.ts +7 -0
  88. package/src/features/tools/cold-start-channel-selector/widget/preview-panels.tsx +250 -0
  89. package/src/features/tools/cold-start-channel-selector/widget/result-shell.tsx +199 -0
  90. package/src/features/tools/cold-start-channel-selector/widget/types.ts +21 -0
  91. package/src/features/tools/commercial-bp-generator/commercial-bp-generator.container.tsx +102 -0
  92. package/src/features/tools/commercial-bp-generator/commercial-bp-generator.spec.ts +14 -0
  93. package/src/features/tools/commercial-bp-generator/widget/commercial-bp-generator.tsx +676 -0
  94. package/src/features/tools/commercial-bp-generator/widget/constants.ts +32 -0
  95. package/src/features/tools/commercial-bp-generator/widget/helpers.ts +147 -0
  96. package/src/features/tools/commercial-bp-generator/widget/index.ts +7 -0
  97. package/src/features/tools/commercial-bp-generator/widget/result-shell.tsx +271 -0
  98. package/src/features/tools/commercial-bp-generator/widget/style-option.tsx +54 -0
  99. package/src/features/tools/commercial-bp-generator/widget/types.ts +33 -0
  100. package/src/features/tools/community-feedback-inbox/community-feedback-inbox.container.tsx +126 -0
  101. package/src/features/tools/community-feedback-inbox/community-feedback-inbox.spec.ts +15 -0
  102. package/src/features/tools/community-feedback-inbox/widget/community-feedback-inbox.tsx +271 -0
  103. package/src/features/tools/community-feedback-inbox/widget/constants.ts +3 -0
  104. package/src/features/tools/community-feedback-inbox/widget/helpers.ts +28 -0
  105. package/src/features/tools/community-feedback-inbox/widget/index.ts +7 -0
  106. package/src/features/tools/community-feedback-inbox/widget/preview-panels.tsx +92 -0
  107. package/src/features/tools/community-feedback-inbox/widget/result-shell.tsx +266 -0
  108. package/src/features/tools/community-feedback-inbox/widget/types.ts +6 -0
  109. package/src/features/tools/community-pain-point-extractor/community-pain-point-extractor.container.tsx +124 -0
  110. package/src/features/tools/community-pain-point-extractor/community-pain-point-extractor.spec.ts +15 -0
  111. package/src/features/tools/community-pain-point-extractor/widget/community-pain-point-extractor.tsx +404 -0
  112. package/src/features/tools/community-pain-point-extractor/widget/constants.ts +41 -0
  113. package/src/features/tools/community-pain-point-extractor/widget/helpers.ts +76 -0
  114. package/src/features/tools/community-pain-point-extractor/widget/index.ts +8 -0
  115. package/src/features/tools/community-pain-point-extractor/widget/progress-panels.tsx +251 -0
  116. package/src/features/tools/community-pain-point-extractor/widget/result-shell.tsx +478 -0
  117. package/src/features/tools/community-pain-point-extractor/widget/types.ts +33 -0
  118. package/src/features/tools/community-post-risk-checker/community-post-risk-checker.package.ts +792 -0
  119. package/src/features/tools/competitor-analysis/competitor-analysis.container.tsx +136 -0
  120. package/src/features/tools/competitor-analysis/competitor-analysis.spec.ts +15 -0
  121. package/src/features/tools/competitor-analysis/widget/competitor-analysis.tsx +241 -0
  122. package/src/features/tools/competitor-analysis/widget/constants.ts +9 -0
  123. package/src/features/tools/competitor-analysis/widget/index.ts +7 -0
  124. package/src/features/tools/competitor-analysis/widget/preview-panels.tsx +60 -0
  125. package/src/features/tools/competitor-analysis/widget/result-shell.tsx +418 -0
  126. package/src/features/tools/competitor-analysis/widget/types.ts +13 -0
  127. package/src/features/tools/disclaimer-generator/disclaimer-generator.package.ts +339 -0
  128. package/src/features/tools/email-template-generator/email-template-generator.container.tsx +224 -0
  129. package/src/features/tools/email-template-generator/email-template-generator.spec.ts +14 -0
  130. package/src/features/tools/email-template-generator/widget/email-template-generator.tsx +358 -0
  131. package/src/features/tools/email-template-generator/widget/helpers.ts +39 -0
  132. package/src/features/tools/email-template-generator/widget/index.ts +7 -0
  133. package/src/features/tools/email-template-generator/widget/preview-panel.tsx +311 -0
  134. package/src/features/tools/email-template-generator/widget/result-text-block.tsx +7 -0
  135. package/src/features/tools/email-template-generator/widget/types.ts +24 -0
  136. package/src/features/tools/gtm-readiness-checklist/gtm-readiness-checklist.container.tsx +75 -0
  137. package/src/features/tools/gtm-readiness-checklist/gtm-readiness-checklist.spec.ts +15 -0
  138. package/src/features/tools/gtm-readiness-checklist/widget/gtm-readiness-checklist.tsx +122 -0
  139. package/src/features/tools/gtm-readiness-checklist/widget/index.ts +7 -0
  140. package/src/features/tools/investor-matching-planner/investor-matching-planner.package.ts +869 -0
  141. package/src/features/tools/koko-codex-assistant/koko-codex-assistant.container.tsx +151 -0
  142. package/src/features/tools/koko-codex-assistant/koko-codex-assistant.spec.ts +14 -0
  143. package/src/features/tools/koko-codex-assistant/widget/index.ts +7 -0
  144. package/src/features/tools/koko-codex-assistant/widget/koko-codex-assistant-download.tsx +192 -0
  145. package/src/features/tools/logo-generator/logo-generator.container.tsx +112 -0
  146. package/src/features/tools/logo-generator/logo-generator.spec.ts +15 -0
  147. package/src/features/tools/logo-generator/widget/index.ts +7 -0
  148. package/src/features/tools/logo-generator/widget/logo-generator.tsx +1244 -0
  149. package/src/features/tools/market-forecast-report/market-forecast-report.container.tsx +73 -0
  150. package/src/features/tools/market-forecast-report/market-forecast-report.spec.ts +15 -0
  151. package/src/features/tools/million-user-growth-test/million-user-growth-test.container.tsx +123 -0
  152. package/src/features/tools/million-user-growth-test/million-user-growth-test.spec.ts +14 -0
  153. package/src/features/tools/million-user-growth-test/widget/index.ts +7 -0
  154. package/src/features/tools/million-user-growth-test/widget/million-user-growth-test.tsx +433 -0
  155. package/src/features/tools/module-loader.ts +107 -0
  156. package/src/features/tools/og-image-generator/og-image-generator.container.tsx +86 -0
  157. package/src/features/tools/og-image-generator/og-image-generator.spec.ts +14 -0
  158. package/src/features/tools/og-image-generator/widget/constants.ts +38 -0
  159. package/src/features/tools/og-image-generator/widget/form-fields.tsx +240 -0
  160. package/src/features/tools/og-image-generator/widget/index.ts +7 -0
  161. package/src/features/tools/og-image-generator/widget/og-image-generator.tsx +742 -0
  162. package/src/features/tools/og-image-generator/widget/preview-panel.tsx +305 -0
  163. package/src/features/tools/og-image-generator/widget/types.ts +1 -0
  164. package/src/features/tools/package/direct-models.ts +20 -0
  165. package/src/features/tools/package/form.ts +38 -0
  166. package/src/features/tools/package/l10n.ts +22 -0
  167. package/src/features/tools/package/package-container.tsx +285 -0
  168. package/src/features/tools/package/package-loaders.ts +22 -0
  169. package/src/features/tools/package/package-registry.ts +36 -0
  170. package/src/features/tools/package/result-markdown.ts +142 -0
  171. package/src/features/tools/package/schema.ts +379 -0
  172. package/src/features/tools/package/to-tool-module.ts +20 -0
  173. package/src/features/tools/package/to-tool-spec.ts +52 -0
  174. package/src/features/tools/package/types.ts +381 -0
  175. package/src/features/tools/pitch-deck-generator/pitch-deck-generator.container.tsx +175 -0
  176. package/src/features/tools/pitch-deck-generator/pitch-deck-generator.spec.ts +14 -0
  177. package/src/features/tools/pitch-deck-generator/pitch-deck-library-data.ts +476 -0
  178. package/src/features/tools/pitch-deck-generator/pitch-deck-library.tsx +159 -0
  179. package/src/features/tools/pitch-deck-generator/widget/ceremony-line.tsx +27 -0
  180. package/src/features/tools/pitch-deck-generator/widget/constants.ts +27 -0
  181. package/src/features/tools/pitch-deck-generator/widget/index.ts +7 -0
  182. package/src/features/tools/pitch-deck-generator/widget/pitch-deck-generator.tsx +763 -0
  183. package/src/features/tools/pitch-deck-generator/widget/result-helpers.ts +102 -0
  184. package/src/features/tools/pitch-deck-generator/widget/result-shell.tsx +263 -0
  185. package/src/features/tools/pitch-deck-generator/widget/slide-frame.tsx +106 -0
  186. package/src/features/tools/pitch-deck-generator/widget/slide-lightbox.tsx +149 -0
  187. package/src/features/tools/pitch-deck-generator/widget/style-option.tsx +54 -0
  188. package/src/features/tools/pitch-deck-generator/widget/types.ts +44 -0
  189. package/src/features/tools/product-agreement-generator/product-agreement-generator.container.tsx +137 -0
  190. package/src/features/tools/product-agreement-generator/product-agreement-generator.spec.ts +14 -0
  191. package/src/features/tools/product-agreement-generator/widget/constants.ts +27 -0
  192. package/src/features/tools/product-agreement-generator/widget/form-controls.tsx +218 -0
  193. package/src/features/tools/product-agreement-generator/widget/helpers.ts +68 -0
  194. package/src/features/tools/product-agreement-generator/widget/index.ts +7 -0
  195. package/src/features/tools/product-agreement-generator/widget/product-agreement-generator.tsx +671 -0
  196. package/src/features/tools/product-agreement-generator/widget/result-shell.tsx +473 -0
  197. package/src/features/tools/product-agreement-generator/widget/types.ts +89 -0
  198. package/src/features/tools/product-hunt-launch-planner/product-hunt-launch-planner.container.tsx +122 -0
  199. package/src/features/tools/product-hunt-launch-planner/product-hunt-launch-planner.spec.ts +15 -0
  200. package/src/features/tools/product-hunt-launch-planner/widget/constants.ts +5 -0
  201. package/src/features/tools/product-hunt-launch-planner/widget/helpers.ts +44 -0
  202. package/src/features/tools/product-hunt-launch-planner/widget/index.ts +7 -0
  203. package/src/features/tools/product-hunt-launch-planner/widget/preview-panel.tsx +143 -0
  204. package/src/features/tools/product-hunt-launch-planner/widget/product-hunt-launch-planner.tsx +504 -0
  205. package/src/features/tools/product-hunt-launch-planner/widget/result-shell.tsx +176 -0
  206. package/src/features/tools/product-hunt-launch-planner/widget/types.ts +20 -0
  207. package/src/features/tools/product-name-generator/product-name-generator.container.tsx +14 -0
  208. package/src/features/tools/product-name-generator/product-name-generator.spec.ts +15 -0
  209. package/src/features/tools/product-name-generator/widget/candidate-card.tsx +273 -0
  210. package/src/features/tools/product-name-generator/widget/constants.ts +21 -0
  211. package/src/features/tools/product-name-generator/widget/form-controls.tsx +136 -0
  212. package/src/features/tools/product-name-generator/widget/helpers.ts +41 -0
  213. package/src/features/tools/product-name-generator/widget/index.ts +7 -0
  214. package/src/features/tools/product-name-generator/widget/product-name-generator.tsx +776 -0
  215. package/src/features/tools/product-name-generator/widget/types.ts +36 -0
  216. package/src/features/tools/product-positioning-analyzer/product-positioning-analyzer.package.ts +801 -0
  217. package/src/features/tools/product-poster-generator/product-poster-generator.container.tsx +14 -0
  218. package/src/features/tools/product-poster-generator/product-poster-generator.spec.ts +15 -0
  219. package/src/features/tools/product-poster-generator/widget/constants.ts +125 -0
  220. package/src/features/tools/product-poster-generator/widget/form-controls.tsx +204 -0
  221. package/src/features/tools/product-poster-generator/widget/helpers.ts +155 -0
  222. package/src/features/tools/product-poster-generator/widget/index.ts +7 -0
  223. package/src/features/tools/product-poster-generator/widget/product-poster-generator.tsx +663 -0
  224. package/src/features/tools/product-poster-generator/widget/result-shell.tsx +12 -0
  225. package/src/features/tools/product-poster-generator/widget/types.ts +83 -0
  226. package/src/features/tools/product-ui-generator/product-ui-generator.container.tsx +116 -0
  227. package/src/features/tools/product-ui-generator/product-ui-generator.spec.ts +14 -0
  228. package/src/features/tools/product-ui-generator/widget/constants.ts +18 -0
  229. package/src/features/tools/product-ui-generator/widget/helpers.ts +11 -0
  230. package/src/features/tools/product-ui-generator/widget/index.ts +7 -0
  231. package/src/features/tools/product-ui-generator/widget/preview-panels.tsx +73 -0
  232. package/src/features/tools/product-ui-generator/widget/product-ui-generator.tsx +304 -0
  233. package/src/features/tools/product-ui-generator/widget/result-shell.tsx +118 -0
  234. package/src/features/tools/product-ui-generator/widget/types.ts +9 -0
  235. package/src/features/tools/reddit-pain-point-finder/reddit-pain-point-finder.container.tsx +113 -0
  236. package/src/features/tools/reddit-pain-point-finder/reddit-pain-point-finder.spec.ts +15 -0
  237. package/src/features/tools/reddit-pain-point-finder/widget/constants.ts +5 -0
  238. package/src/features/tools/reddit-pain-point-finder/widget/index.ts +7 -0
  239. package/src/features/tools/reddit-pain-point-finder/widget/preview-panels.tsx +73 -0
  240. package/src/features/tools/reddit-pain-point-finder/widget/reddit-pain-point-finder.tsx +281 -0
  241. package/src/features/tools/reddit-pain-point-finder/widget/result-shell.tsx +203 -0
  242. package/src/features/tools/reddit-pain-point-finder/widget/types.ts +15 -0
  243. package/src/features/tools/registry.ts +288 -0
  244. package/src/features/tools/runtime-registry.ts +186 -0
  245. package/src/features/tools/shared/create-guide-stack-container.tsx +176 -0
  246. package/src/features/tools/shared/create-standard-landing-container.tsx +163 -0
  247. package/src/features/tools/shared/tool-content-sections.tsx +659 -0
  248. package/src/features/tools/shared/tool-cta.tsx +66 -0
  249. package/src/features/tools/shared/tool-draft-view.tsx +53 -0
  250. package/src/features/tools/shared/tool-guide-section.tsx +191 -0
  251. package/src/features/tools/shared/tool-handoff.tsx +38 -0
  252. package/src/features/tools/shared/tool-mcp-badge.tsx +23 -0
  253. package/src/features/tools/shared/tool-message-translator.ts +37 -0
  254. package/src/features/tools/shared/tool-page-shell.tsx +348 -0
  255. package/src/features/tools/shared/tool-standard-generation-steps.tsx +52 -0
  256. package/src/features/tools/shared/tool-standard-landing-sections.tsx +447 -0
  257. package/src/features/tools/shared/tool-ui-registry.ts +237 -0
  258. package/src/features/tools/shared/tool-widget-slot.tsx +29 -0
  259. package/src/features/tools/slogan-generator/slogan-generator.package.ts +641 -0
  260. package/src/features/tools/social-listening-research-report/social-listening-research-report.container.tsx +124 -0
  261. package/src/features/tools/social-listening-research-report/social-listening-research-report.spec.ts +15 -0
  262. package/src/features/tools/social-listening-research-report/widget/constants.ts +4 -0
  263. package/src/features/tools/social-listening-research-report/widget/index.ts +7 -0
  264. package/src/features/tools/social-listening-research-report/widget/preview-panels.tsx +73 -0
  265. package/src/features/tools/social-listening-research-report/widget/result-shell.tsx +216 -0
  266. package/src/features/tools/social-listening-research-report/widget/social-listening-research-report.tsx +303 -0
  267. package/src/features/tools/social-listening-research-report/widget/types.ts +15 -0
  268. package/src/features/tools/social-platform-asset-adapter/social-platform-asset-adapter.container.tsx +123 -0
  269. package/src/features/tools/social-platform-asset-adapter/social-platform-asset-adapter.spec.ts +15 -0
  270. package/src/features/tools/social-platform-asset-adapter/widget/constants.ts +43 -0
  271. package/src/features/tools/social-platform-asset-adapter/widget/helpers.ts +249 -0
  272. package/src/features/tools/social-platform-asset-adapter/widget/index.ts +7 -0
  273. package/src/features/tools/social-platform-asset-adapter/widget/result-shell.tsx +350 -0
  274. package/src/features/tools/social-platform-asset-adapter/widget/section-heading.tsx +18 -0
  275. package/src/features/tools/social-platform-asset-adapter/widget/social-platform-asset-adapter.tsx +641 -0
  276. package/src/features/tools/social-platform-asset-adapter/widget/types.ts +40 -0
  277. package/src/features/tools/social-profile-kit/social-profile-kit.container.tsx +177 -0
  278. package/src/features/tools/social-profile-kit/social-profile-kit.spec.ts +15 -0
  279. package/src/features/tools/social-profile-kit/widget/context-editor.tsx +171 -0
  280. package/src/features/tools/social-profile-kit/widget/generation-runtime.ts +281 -0
  281. package/src/features/tools/social-profile-kit/widget/index.ts +7 -0
  282. package/src/features/tools/social-profile-kit/widget/input-panel.tsx +294 -0
  283. package/src/features/tools/social-profile-kit/widget/platform-dock.tsx +115 -0
  284. package/src/features/tools/social-profile-kit/widget/profile-canvas.tsx +318 -0
  285. package/src/features/tools/social-profile-kit/widget/profile-preview/bluesky-preview.tsx +188 -0
  286. package/src/features/tools/social-profile-kit/widget/profile-preview/facebook-preview.tsx +168 -0
  287. package/src/features/tools/social-profile-kit/widget/profile-preview/index.ts +1 -0
  288. package/src/features/tools/social-profile-kit/widget/profile-preview/instagram-preview.tsx +278 -0
  289. package/src/features/tools/social-profile-kit/widget/profile-preview/linkedin-preview.tsx +358 -0
  290. package/src/features/tools/social-profile-kit/widget/profile-preview/profile-preview.tsx +348 -0
  291. package/src/features/tools/social-profile-kit/widget/profile-preview/shared.tsx +391 -0
  292. package/src/features/tools/social-profile-kit/widget/profile-preview/threads-preview.tsx +148 -0
  293. package/src/features/tools/social-profile-kit/widget/profile-preview/tiktok-preview.tsx +319 -0
  294. package/src/features/tools/social-profile-kit/widget/profile-preview/x-preview.tsx +341 -0
  295. package/src/features/tools/social-profile-kit/widget/profile-preview/youtube-preview.tsx +297 -0
  296. package/src/features/tools/social-profile-kit/widget/social-profile-kit-generator.tsx +1059 -0
  297. package/src/features/tools/social-profile-kit/widget/types.ts +104 -0
  298. package/src/features/tools/spec-registry.ts +158 -0
  299. package/src/features/tools/startup-idea-research/startup-idea-research.container.tsx +111 -0
  300. package/src/features/tools/startup-idea-research/startup-idea-research.spec.ts +15 -0
  301. package/src/features/tools/startup-idea-research/widget/constants.ts +11 -0
  302. package/src/features/tools/startup-idea-research/widget/index.ts +7 -0
  303. package/src/features/tools/startup-idea-research/widget/preview-panels.tsx +67 -0
  304. package/src/features/tools/startup-idea-research/widget/result-shell.tsx +323 -0
  305. package/src/features/tools/startup-idea-research/widget/startup-idea-research.tsx +272 -0
  306. package/src/features/tools/startup-idea-research/widget/types.ts +21 -0
  307. package/src/features/tools/tarot-reading/tarot-reading.container.tsx +412 -0
  308. package/src/features/tools/tarot-reading/tarot-reading.spec.ts +15 -0
  309. package/src/features/tools/tarot-reading/widget/cards.tsx +152 -0
  310. package/src/features/tools/tarot-reading/widget/constants.ts +69 -0
  311. package/src/features/tools/tarot-reading/widget/deck-view.tsx +86 -0
  312. package/src/features/tools/tarot-reading/widget/deck.ts +86 -0
  313. package/src/features/tools/tarot-reading/widget/index.ts +7 -0
  314. package/src/features/tools/tarot-reading/widget/landing-view.tsx +209 -0
  315. package/src/features/tools/tarot-reading/widget/reading-copy.tsx +138 -0
  316. package/src/features/tools/tarot-reading/widget/reading-view.tsx +189 -0
  317. package/src/features/tools/tarot-reading/widget/reveal-view.tsx +50 -0
  318. package/src/features/tools/tarot-reading/widget/ritual-view.tsx +276 -0
  319. package/src/features/tools/tarot-reading/widget/tarot-reading.tsx +262 -0
  320. package/src/features/tools/tarot-reading/widget/types.ts +22 -0
  321. package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.container.tsx +114 -0
  322. package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.spec.ts +14 -0
  323. package/src/features/tools/twitter-gif-downloader/widget/index.ts +7 -0
  324. package/src/features/tools/twitter-gif-downloader/widget/twitter-gif-downloader.tsx +351 -0
  325. package/src/features/tools/types.ts +68 -0
  326. package/src/features/tools/user-interview-guide-generator/user-interview-guide-generator.package.ts +774 -0
  327. package/src/features/tools/user-interview-insight-report-generator/user-interview-insight-report-generator.container.tsx +23 -0
  328. package/src/features/tools/user-interview-insight-report-generator/user-interview-insight-report-generator.spec.ts +14 -0
  329. package/src/features/tools/user-interview-insight-report-generator/widget/constants.ts +8 -0
  330. package/src/features/tools/user-interview-insight-report-generator/widget/helpers.ts +4 -0
  331. package/src/features/tools/user-interview-insight-report-generator/widget/index.ts +7 -0
  332. package/src/features/tools/user-interview-insight-report-generator/widget/preview-panels.tsx +55 -0
  333. package/src/features/tools/user-interview-insight-report-generator/widget/result-shell.tsx +122 -0
  334. package/src/features/tools/user-interview-insight-report-generator/widget/types.ts +16 -0
  335. package/src/features/tools/user-interview-insight-report-generator/widget/user-interview-insight-report-generator.tsx +293 -0
  336. package/src/features/tools/user-interview-record-generator/user-interview-record-generator.container.tsx +18 -0
  337. package/src/features/tools/user-interview-record-generator/user-interview-record-generator.spec.ts +14 -0
  338. package/src/features/tools/user-interview-record-generator/widget/constants.ts +9 -0
  339. package/src/features/tools/user-interview-record-generator/widget/helpers.ts +4 -0
  340. package/src/features/tools/user-interview-record-generator/widget/index.ts +7 -0
  341. package/src/features/tools/user-interview-record-generator/widget/preview-panels.tsx +48 -0
  342. package/src/features/tools/user-interview-record-generator/widget/result-shell.tsx +128 -0
  343. package/src/features/tools/user-interview-record-generator/widget/types.ts +16 -0
  344. package/src/features/tools/user-interview-record-generator/widget/user-interview-record-generator.tsx +305 -0
  345. package/src/features/tools/utm-builder/utm-builder.package.ts +846 -0
  346. package/src/features/tools/widget-registry.ts +73 -0
  347. package/src/generated/mcp-tools-registry.json +17 -0
  348. package/src/host/client-proxy.test.ts +33 -0
  349. package/src/host/client.ts +678 -0
  350. package/src/host/server.ts +119 -0
  351. package/src/i18n/client.ts +11 -0
  352. package/src/i18n/locale-fallback.ts +38 -0
  353. package/src/i18n/locale.ts +18 -0
  354. package/src/i18n/messages/en/page/tools.ts +124 -0
  355. package/src/i18n/messages/en/tools/app-icon-resizer.ts +244 -0
  356. package/src/i18n/messages/en/tools/app-store-screenshot-generator.ts +145 -0
  357. package/src/i18n/messages/en/tools/build-in-public-story-generator.ts +229 -0
  358. package/src/i18n/messages/en/tools/cold-start-channel-selector.ts +423 -0
  359. package/src/i18n/messages/en/tools/commercial-bp-generator.ts +304 -0
  360. package/src/i18n/messages/en/tools/community-feedback-inbox.ts +218 -0
  361. package/src/i18n/messages/en/tools/community-pain-point-extractor.ts +230 -0
  362. package/src/i18n/messages/en/tools/community-post-risk-checker.ts +209 -0
  363. package/src/i18n/messages/en/tools/competitor-analysis.ts +197 -0
  364. package/src/i18n/messages/en/tools/email-template-generator.ts +302 -0
  365. package/src/i18n/messages/en/tools/engine.ts +69 -0
  366. package/src/i18n/messages/en/tools/gtm-readiness-checklist.ts +166 -0
  367. package/src/i18n/messages/en/tools/index.ts +73 -0
  368. package/src/i18n/messages/en/tools/koko-codex-assistant.ts +171 -0
  369. package/src/i18n/messages/en/tools/logo-generator.ts +173 -0
  370. package/src/i18n/messages/en/tools/market-forecast-report.ts +228 -0
  371. package/src/i18n/messages/en/tools/million-user-growth-test.ts +386 -0
  372. package/src/i18n/messages/en/tools/og-image-generator.ts +172 -0
  373. package/src/i18n/messages/en/tools/pitch-deck-generator.ts +358 -0
  374. package/src/i18n/messages/en/tools/product-agreement-generator.ts +411 -0
  375. package/src/i18n/messages/en/tools/product-hunt-launch-planner.ts +272 -0
  376. package/src/i18n/messages/en/tools/product-name-generator.ts +383 -0
  377. package/src/i18n/messages/en/tools/product-poster-generator.ts +294 -0
  378. package/src/i18n/messages/en/tools/product-ui-generator.ts +234 -0
  379. package/src/i18n/messages/en/tools/reddit-pain-point-finder.ts +163 -0
  380. package/src/i18n/messages/en/tools/shared.ts +24 -0
  381. package/src/i18n/messages/en/tools/social-listening-research-report.ts +185 -0
  382. package/src/i18n/messages/en/tools/social-media-profile-generator.ts +22 -0
  383. package/src/i18n/messages/en/tools/social-platform-asset-adapter.ts +287 -0
  384. package/src/i18n/messages/en/tools/social-profile-kit.ts +150 -0
  385. package/src/i18n/messages/en/tools/startup-idea-research.ts +170 -0
  386. package/src/i18n/messages/en/tools/tarot-reading.ts +230 -0
  387. package/src/i18n/messages/en/tools/twitter-gif-downloader.ts +145 -0
  388. package/src/i18n/messages/en/tools/user-interview-insight-report-generator.ts +242 -0
  389. package/src/i18n/messages/en/tools/user-interview-record-generator.ts +244 -0
  390. package/src/i18n/messages/en/tools-ui.ts +704 -0
  391. package/src/i18n/messages/zh/page/tools.ts +124 -0
  392. package/src/i18n/messages/zh/tools/app-icon-resizer.ts +242 -0
  393. package/src/i18n/messages/zh/tools/app-store-screenshot-generator.ts +145 -0
  394. package/src/i18n/messages/zh/tools/build-in-public-story-generator.ts +230 -0
  395. package/src/i18n/messages/zh/tools/cold-start-channel-selector.ts +422 -0
  396. package/src/i18n/messages/zh/tools/commercial-bp-generator.ts +302 -0
  397. package/src/i18n/messages/zh/tools/community-feedback-inbox.ts +217 -0
  398. package/src/i18n/messages/zh/tools/community-pain-point-extractor.ts +230 -0
  399. package/src/i18n/messages/zh/tools/community-post-risk-checker.ts +200 -0
  400. package/src/i18n/messages/zh/tools/competitor-analysis.ts +192 -0
  401. package/src/i18n/messages/zh/tools/email-template-generator.ts +302 -0
  402. package/src/i18n/messages/zh/tools/engine.ts +68 -0
  403. package/src/i18n/messages/zh/tools/gtm-readiness-checklist.ts +166 -0
  404. package/src/i18n/messages/zh/tools/index.ts +73 -0
  405. package/src/i18n/messages/zh/tools/koko-codex-assistant.ts +170 -0
  406. package/src/i18n/messages/zh/tools/logo-generator.ts +172 -0
  407. package/src/i18n/messages/zh/tools/market-forecast-report.ts +228 -0
  408. package/src/i18n/messages/zh/tools/million-user-growth-test.ts +385 -0
  409. package/src/i18n/messages/zh/tools/og-image-generator.ts +173 -0
  410. package/src/i18n/messages/zh/tools/pitch-deck-generator.ts +358 -0
  411. package/src/i18n/messages/zh/tools/product-agreement-generator.ts +401 -0
  412. package/src/i18n/messages/zh/tools/product-hunt-launch-planner.ts +267 -0
  413. package/src/i18n/messages/zh/tools/product-name-generator.ts +383 -0
  414. package/src/i18n/messages/zh/tools/product-poster-generator.ts +282 -0
  415. package/src/i18n/messages/zh/tools/product-ui-generator.ts +234 -0
  416. package/src/i18n/messages/zh/tools/reddit-pain-point-finder.ts +162 -0
  417. package/src/i18n/messages/zh/tools/shared.ts +24 -0
  418. package/src/i18n/messages/zh/tools/social-listening-research-report.ts +181 -0
  419. package/src/i18n/messages/zh/tools/social-media-profile-generator.ts +22 -0
  420. package/src/i18n/messages/zh/tools/social-platform-asset-adapter.ts +284 -0
  421. package/src/i18n/messages/zh/tools/social-profile-kit.ts +150 -0
  422. package/src/i18n/messages/zh/tools/startup-idea-research.ts +165 -0
  423. package/src/i18n/messages/zh/tools/tarot-reading.ts +228 -0
  424. package/src/i18n/messages/zh/tools/twitter-gif-downloader.ts +138 -0
  425. package/src/i18n/messages/zh/tools/user-interview-insight-report-generator.ts +224 -0
  426. package/src/i18n/messages/zh/tools/user-interview-record-generator.ts +226 -0
  427. package/src/i18n/messages/zh/tools-ui.ts +684 -0
  428. package/src/i18n/messages/zh-tw/page/tools.ts +115 -0
  429. package/src/i18n/messages/zh-tw/tools/app-icon-resizer.ts +242 -0
  430. package/src/i18n/messages/zh-tw/tools/app-store-screenshot-generator.ts +145 -0
  431. package/src/i18n/messages/zh-tw/tools/build-in-public-story-generator.ts +230 -0
  432. package/src/i18n/messages/zh-tw/tools/cold-start-channel-selector.ts +440 -0
  433. package/src/i18n/messages/zh-tw/tools/commercial-bp-generator.ts +302 -0
  434. package/src/i18n/messages/zh-tw/tools/community-feedback-inbox.ts +217 -0
  435. package/src/i18n/messages/zh-tw/tools/community-pain-point-extractor.ts +230 -0
  436. package/src/i18n/messages/zh-tw/tools/community-post-risk-checker.ts +200 -0
  437. package/src/i18n/messages/zh-tw/tools/competitor-analysis.ts +192 -0
  438. package/src/i18n/messages/zh-tw/tools/email-template-generator.ts +302 -0
  439. package/src/i18n/messages/zh-tw/tools/engine.ts +68 -0
  440. package/src/i18n/messages/zh-tw/tools/gtm-readiness-checklist.ts +166 -0
  441. package/src/i18n/messages/zh-tw/tools/index.ts +73 -0
  442. package/src/i18n/messages/zh-tw/tools/koko-codex-assistant.ts +170 -0
  443. package/src/i18n/messages/zh-tw/tools/logo-generator.ts +71 -0
  444. package/src/i18n/messages/zh-tw/tools/market-forecast-report.ts +228 -0
  445. package/src/i18n/messages/zh-tw/tools/million-user-growth-test.ts +385 -0
  446. package/src/i18n/messages/zh-tw/tools/og-image-generator.ts +173 -0
  447. package/src/i18n/messages/zh-tw/tools/pitch-deck-generator.ts +358 -0
  448. package/src/i18n/messages/zh-tw/tools/product-agreement-generator.ts +401 -0
  449. package/src/i18n/messages/zh-tw/tools/product-hunt-launch-planner.ts +267 -0
  450. package/src/i18n/messages/zh-tw/tools/product-name-generator.ts +383 -0
  451. package/src/i18n/messages/zh-tw/tools/product-poster-generator.ts +282 -0
  452. package/src/i18n/messages/zh-tw/tools/product-ui-generator.ts +234 -0
  453. package/src/i18n/messages/zh-tw/tools/reddit-pain-point-finder.ts +162 -0
  454. package/src/i18n/messages/zh-tw/tools/shared.ts +24 -0
  455. package/src/i18n/messages/zh-tw/tools/social-listening-research-report.ts +181 -0
  456. package/src/i18n/messages/zh-tw/tools/social-media-profile-generator.ts +22 -0
  457. package/src/i18n/messages/zh-tw/tools/social-platform-asset-adapter.ts +284 -0
  458. package/src/i18n/messages/zh-tw/tools/social-profile-kit.ts +150 -0
  459. package/src/i18n/messages/zh-tw/tools/startup-idea-research.ts +165 -0
  460. package/src/i18n/messages/zh-tw/tools/tarot-reading.ts +228 -0
  461. package/src/i18n/messages/zh-tw/tools/twitter-gif-downloader.ts +138 -0
  462. package/src/i18n/messages/zh-tw/tools/user-interview-insight-report-generator.ts +224 -0
  463. package/src/i18n/messages/zh-tw/tools/user-interview-record-generator.ts +226 -0
  464. package/src/i18n/messages/zh-tw/tools-ui.ts +525 -0
  465. package/src/index.ts +15 -0
  466. package/src/lib/activity-task-handoff.ts +404 -0
  467. package/src/lib/api/errors.ts +39 -0
  468. package/src/lib/app-origin.ts +30 -0
  469. package/src/lib/fact-claim-contract.ts +68 -0
  470. package/src/lib/fake-delay.ts +3 -0
  471. package/src/lib/file/download.ts +10 -0
  472. package/src/lib/routing/locale-paths.ts +27 -0
  473. package/src/lib/seo-meta.ts +3 -0
  474. package/src/lib/tools/app-icon-resizer-schema.ts +191 -0
  475. package/src/lib/tools/app-store-screenshot-generator-schema.ts +185 -0
  476. package/src/lib/tools/build-in-public-story-generator-schema.ts +67 -0
  477. package/src/lib/tools/build-in-public-story-generator.format.ts +507 -0
  478. package/src/lib/tools/cold-start-channel-selector-schema.ts +147 -0
  479. package/src/lib/tools/commercial-bp-generator-schema.ts +222 -0
  480. package/src/lib/tools/community-feedback-inbox-schema.ts +125 -0
  481. package/src/lib/tools/community-feedback-inbox.format.ts +764 -0
  482. package/src/lib/tools/community-pain-point-extractor-schema.ts +166 -0
  483. package/src/lib/tools/community-post-risk-checker-schema.ts +99 -0
  484. package/src/lib/tools/community-post-risk-checker.format.ts +557 -0
  485. package/src/lib/tools/competitor-analysis-schema.ts +144 -0
  486. package/src/lib/tools/draft-preview-access.ts +3 -0
  487. package/src/lib/tools/draft-store.ts +69 -0
  488. package/src/lib/tools/email-template-generator-schema.ts +124 -0
  489. package/src/lib/tools/gtm-checklist-copy.ts +57 -0
  490. package/src/lib/tools/gtm-checklist-structure.ts +37 -0
  491. package/src/lib/tools/image-download.ts +68 -0
  492. package/src/lib/tools/input-quality.ts +97 -0
  493. package/src/lib/tools/logo-generator-schema.ts +224 -0
  494. package/src/lib/tools/market-forecast-report-schema.ts +299 -0
  495. package/src/lib/tools/mcp-registry.ts +16 -0
  496. package/src/lib/tools/mcp-types.ts +18 -0
  497. package/src/lib/tools/million-user-growth-test-schema.ts +49 -0
  498. package/src/lib/tools/million-user-growth-test.ts +181 -0
  499. package/src/lib/tools/og-image-generator-render.ts +448 -0
  500. package/src/lib/tools/og-image-generator-schema.ts +140 -0
  501. package/src/lib/tools/ops-catalog.ts +102 -0
  502. package/src/lib/tools/pitch-deck-generator-schema.ts +182 -0
  503. package/src/lib/tools/pitch-deck-package.ts +0 -0
  504. package/src/lib/tools/product-agreement-generator-schema.ts +168 -0
  505. package/src/lib/tools/product-hunt-launch-planner-schema.ts +136 -0
  506. package/src/lib/tools/product-hunt-launch-planner.format.ts +72 -0
  507. package/src/lib/tools/product-name-generator-schema.ts +563 -0
  508. package/src/lib/tools/product-poster-generator-schema.ts +215 -0
  509. package/src/lib/tools/product-ui-generator-schema.ts +157 -0
  510. package/src/lib/tools/reddit-pain-point-finder-schema.ts +88 -0
  511. package/src/lib/tools/resolve-tool-locale-copy.ts +75 -0
  512. package/src/lib/tools/slug.ts +60 -0
  513. package/src/lib/tools/social-listening-research-report-schema.ts +105 -0
  514. package/src/lib/tools/social-platform-asset-adapter-schema.ts +152 -0
  515. package/src/lib/tools/social-profile-kit.constants.ts +12 -0
  516. package/src/lib/tools/social-profile-kit.generation.ts +257 -0
  517. package/src/lib/tools/social-profile-kit.input.ts +290 -0
  518. package/src/lib/tools/social-profile-kit.manual.ts +481 -0
  519. package/src/lib/tools/social-profile-kit.package.ts +220 -0
  520. package/src/lib/tools/social-profile-kit.schema.ts +328 -0
  521. package/src/lib/tools/startup-idea-research-schema.ts +107 -0
  522. package/src/lib/tools/tarot-imagery.ts +89 -0
  523. package/src/lib/tools/tarot-reading.ts +1044 -0
  524. package/src/lib/tools/tool-message-keys.ts +18 -0
  525. package/src/lib/tools/types.ts +32 -0
  526. package/src/lib/tools/user-interview.format.ts +147 -0
  527. package/src/lib/tools/user-interview.generation.ts +670 -0
  528. package/src/lib/tools/user-interview.schema.ts +192 -0
  529. package/src/lib/zip.ts +106 -0
  530. package/src/services/tools/handlers/gtm-readiness-checklist.ts +71 -0
  531. package/src/services/tools/handlers/types.ts +7 -0
@@ -0,0 +1,1150 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { Copy, ExternalLink, Loader2, RefreshCw } from "lucide-react";
5
+
6
+ import { Button } from "@tomako/ui/button";
7
+ import { Field, Input, Textarea } from "@tomako/ui/input";
8
+ import { NativeSelect, NativeSelectOption } from "@tomako/ui/native-select";
9
+ import { useLegacyLlmTaskWatch } from "../../components/tools/engine/use-legacy-llm-task";
10
+ import { useLocale, useTranslations } from "../../i18n/client";
11
+ import type { AppLocale } from "../../i18n/locale";
12
+ import {
13
+ marketForecastReportInputSchema,
14
+ type MarketForecastReportResult,
15
+ } from "../../lib/tools/market-forecast-report-schema";
16
+ import {
17
+ fetchMarketForecastReportResult,
18
+ fetchMarketForecastReportTask,
19
+ submitMarketForecastReport,
20
+ } from "../../host/client";
21
+
22
+ type ErrorCategory = "submit" | "task" | "timeout" | "schema" | "invalid";
23
+ type ForecastHorizon = "one_year" | "three_year" | "five_year";
24
+ type WidgetState =
25
+ | { phase: "idle" }
26
+ | { phase: "submitting" }
27
+ | { phase: "running"; status: string; partial?: MarketForecastReportResult }
28
+ | { phase: "done"; data: MarketForecastReportResult; taskId: string }
29
+ | { phase: "error"; category: ErrorCategory; partial?: MarketForecastReportResult };
30
+
31
+ type ProgressItem = {
32
+ id: string;
33
+ text: string;
34
+ };
35
+
36
+ const POLL_INTERVAL_MS = 4000;
37
+ const PROGRESS_INTERVAL_MS = 15_000;
38
+ const GRACE_WINDOW_MS = 720_000;
39
+
40
+ export function MarketForecastReport() {
41
+ const t = useTranslations("toolsPages.marketForecastReport.widget");
42
+ const locale = useLocale() as AppLocale;
43
+
44
+ const [productContext, setProductContext] = useState("");
45
+ const [targetUsers, setTargetUsers] = useState("");
46
+ const [market, setMarket] = useState("");
47
+ const [currentUsers, setCurrentUsers] = useState("");
48
+ const [metrics, setMetrics] = useState("");
49
+ const [forecastHorizon, setForecastHorizon] = useState<ForecastHorizon>("three_year");
50
+ const [fieldError, setFieldError] = useState<string | null>(null);
51
+ const [state, setState] = useState<WidgetState>({ phase: "idle" });
52
+ const [progressItems, setProgressItems] = useState<ProgressItem[]>([]);
53
+ const {
54
+ start: startTaskWatch,
55
+ stop: stopTaskWatch,
56
+ } = useLegacyLlmTaskWatch<MarketForecastReportResult>();
57
+
58
+ const resultRef = useRef<HTMLDivElement | null>(null);
59
+ const cancelledRef = useRef(false);
60
+ const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
61
+ const progressStepIndexRef = useRef(0);
62
+ const settledRef = useRef(false);
63
+ const currentPartialRef = useRef<MarketForecastReportResult | undefined>(undefined);
64
+ const progressMessages = useMemo(() => t.raw("progressSteps") as string[], [t]);
65
+
66
+ function cleanup() {
67
+ stopTaskWatch();
68
+ if (progressTimerRef.current) clearInterval(progressTimerRef.current);
69
+ progressTimerRef.current = null;
70
+ }
71
+
72
+ function pushProgressStep(text: string) {
73
+ setProgressItems((items) => [
74
+ ...items.slice(-5),
75
+ { id: `${Date.now()}-${items.length}`, text },
76
+ ]);
77
+ }
78
+
79
+ function startProgressTicker() {
80
+ setProgressItems([]);
81
+ progressStepIndexRef.current = 0;
82
+ if (progressMessages[0]) pushProgressStep(progressMessages[0]);
83
+ progressTimerRef.current = setInterval(() => {
84
+ progressStepIndexRef.current = Math.min(
85
+ progressStepIndexRef.current + 1,
86
+ progressMessages.length - 1,
87
+ );
88
+ const nextStep = progressMessages[progressStepIndexRef.current];
89
+ if (nextStep) pushProgressStep(nextStep);
90
+ if (progressStepIndexRef.current >= progressMessages.length - 1) {
91
+ if (progressTimerRef.current) clearInterval(progressTimerRef.current);
92
+ progressTimerRef.current = null;
93
+ }
94
+ }, PROGRESS_INTERVAL_MS);
95
+ }
96
+
97
+ useEffect(() => {
98
+ return () => {
99
+ cancelledRef.current = true;
100
+ stopTaskWatch();
101
+ if (progressTimerRef.current) clearInterval(progressTimerRef.current);
102
+ };
103
+ }, [stopTaskWatch]);
104
+
105
+ function updatePartial(data: MarketForecastReportResult) {
106
+ if (cancelledRef.current || settledRef.current) return;
107
+ currentPartialRef.current = data;
108
+ setState((current) => {
109
+ if (current.phase === "running") return { ...current, partial: data };
110
+ return { phase: "running", status: "AWAITING_RESULT", partial: data };
111
+ });
112
+ }
113
+
114
+ function finishDone(data: MarketForecastReportResult, taskId: string) {
115
+ if (cancelledRef.current || settledRef.current) return;
116
+ settledRef.current = true;
117
+ cleanup();
118
+ setState({ phase: "done", data, taskId });
119
+ }
120
+
121
+ function finishError(category: ErrorCategory) {
122
+ if (cancelledRef.current || settledRef.current) return;
123
+ settledRef.current = true;
124
+ cleanup();
125
+ setState({ phase: "error", category, partial: currentPartialRef.current });
126
+ }
127
+
128
+ async function pollStatusOnce(taskId: string) {
129
+ const [task, result] = await Promise.all([
130
+ fetchMarketForecastReportTask(taskId),
131
+ fetchMarketForecastReportResult(taskId),
132
+ ]);
133
+ if (cancelledRef.current || settledRef.current) {
134
+ return { settled: false as const };
135
+ }
136
+ if (result.data) {
137
+ if (result.data.reportStatus === "final") {
138
+ return {
139
+ settled: true as const,
140
+ kind: "success" as const,
141
+ data: result.data,
142
+ };
143
+ }
144
+ updatePartial(result.data);
145
+ }
146
+ if (result.status === "RESULT_INVALID") {
147
+ return {
148
+ settled: true as const,
149
+ kind: "error" as const,
150
+ category: "schema" as const,
151
+ };
152
+ }
153
+ if (task.status === "FAILED") {
154
+ return {
155
+ settled: true as const,
156
+ kind: "error" as const,
157
+ category: "task" as const,
158
+ };
159
+ }
160
+ setState((current) => ({
161
+ phase: "running",
162
+ status: task.status,
163
+ partial: current.phase === "running" ? current.partial : currentPartialRef.current,
164
+ }));
165
+ return { settled: false as const, pendingWriteback: true };
166
+ }
167
+
168
+ async function startResearch() {
169
+ const parsed = marketForecastReportInputSchema.safeParse({
170
+ productContext,
171
+ targetUsers,
172
+ market: market || undefined,
173
+ currentUsers: currentUsers || undefined,
174
+ currentRevenue: metrics || undefined,
175
+ pricing: metrics || undefined,
176
+ paidConversionRate: metrics || undefined,
177
+ growthHistory: metrics || undefined,
178
+ communityKeywords: targetUsers || undefined,
179
+ forecastHorizon,
180
+ });
181
+
182
+ if (!parsed.success) {
183
+ setFieldError(t("errors.invalid"));
184
+ setState({ phase: "error", category: "invalid" });
185
+ return;
186
+ }
187
+
188
+ setFieldError(null);
189
+ currentPartialRef.current = undefined;
190
+ cancelledRef.current = false;
191
+ settledRef.current = false;
192
+ cleanup();
193
+ setState({ phase: "submitting" });
194
+ startProgressTicker();
195
+ resultRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
196
+
197
+ let taskId: string;
198
+ try {
199
+ const submitted = await submitMarketForecastReport(parsed.data, locale);
200
+ taskId = submitted.taskId;
201
+ } catch {
202
+ finishError("submit");
203
+ return;
204
+ }
205
+
206
+ if (cancelledRef.current) return;
207
+ setState({ phase: "running", status: "PENDING" });
208
+
209
+ startTaskWatch(
210
+ taskId,
211
+ pollStatusOnce,
212
+ {
213
+ onStatus: (status) => {
214
+ if (!cancelledRef.current && !settledRef.current) {
215
+ setState((current) => ({
216
+ phase: "running",
217
+ status,
218
+ partial:
219
+ current.phase === "running"
220
+ ? current.partial
221
+ : currentPartialRef.current,
222
+ }));
223
+ }
224
+ },
225
+ onSuccess: (data) => {
226
+ pushProgressStep(t("progressResultReady"));
227
+ finishDone(data, taskId);
228
+ },
229
+ onError: (category) => {
230
+ finishError(category);
231
+ },
232
+ },
233
+ {
234
+ pollIntervalMs: POLL_INTERVAL_MS,
235
+ graceWindowMs: GRACE_WINDOW_MS,
236
+ },
237
+ );
238
+ }
239
+
240
+ function reset() {
241
+ cancelledRef.current = false;
242
+ settledRef.current = false;
243
+ currentPartialRef.current = undefined;
244
+ cleanup();
245
+ setProgressItems([]);
246
+ setState({ phase: "idle" });
247
+ }
248
+
249
+ const busy = state.phase === "submitting" || state.phase === "running";
250
+ const partial =
251
+ state.phase === "running" || state.phase === "error" ? state.partial : undefined;
252
+ const statusLabel =
253
+ state.phase === "running"
254
+ ? t.has(`status.${state.status}`)
255
+ ? t(`status.${state.status}`)
256
+ : t("status.default")
257
+ : "";
258
+
259
+ return (
260
+ <div className="grid gap-8 xl:grid-cols-[0.42fr_0.58fr] xl:items-start">
261
+ <section className="rounded-[1.25rem] border border-[#D9DEE7] bg-white p-5 shadow-[0_16px_44px_rgba(32,41,58,0.07)] md:p-6">
262
+ <Field label={t("productLabel")} error={fieldError || undefined}>
263
+ <Textarea
264
+ rows={6}
265
+ placeholder={t("productPlaceholder")}
266
+ value={productContext}
267
+ onChange={(event) => setProductContext(event.target.value)}
268
+ disabled={busy}
269
+ aria-invalid={fieldError ? true : undefined}
270
+ />
271
+ </Field>
272
+ <div className="mt-4 grid gap-4 sm:grid-cols-2">
273
+ <Field label={t("targetUsersLabel")}>
274
+ <Input
275
+ placeholder={t("targetUsersPlaceholder")}
276
+ value={targetUsers}
277
+ onChange={(event) => setTargetUsers(event.target.value)}
278
+ disabled={busy}
279
+ />
280
+ </Field>
281
+ <Field label={t("marketLabel")}>
282
+ <Input
283
+ placeholder={t("marketPlaceholder")}
284
+ value={market}
285
+ onChange={(event) => setMarket(event.target.value)}
286
+ disabled={busy}
287
+ />
288
+ </Field>
289
+ <Field label={t("currentUsersLabel")}>
290
+ <Input
291
+ placeholder={t("currentUsersPlaceholder")}
292
+ value={currentUsers}
293
+ onChange={(event) => setCurrentUsers(event.target.value)}
294
+ disabled={busy}
295
+ />
296
+ </Field>
297
+ <Field label={t("horizonLabel")}>
298
+ <NativeSelect
299
+ className="w-full bg-white"
300
+ value={forecastHorizon}
301
+ onChange={(event) => setForecastHorizon(event.target.value as ForecastHorizon)}
302
+ disabled={busy}
303
+ >
304
+ <NativeSelectOption value="one_year">{t("horizon.oneYear")}</NativeSelectOption>
305
+ <NativeSelectOption value="three_year">{t("horizon.threeYear")}</NativeSelectOption>
306
+ <NativeSelectOption value="five_year">{t("horizon.fiveYear")}</NativeSelectOption>
307
+ </NativeSelect>
308
+ </Field>
309
+ </div>
310
+ <div className="mt-4">
311
+ <Field label={t("metricsLabel")} hint={t("metricsHint")}>
312
+ <Textarea
313
+ rows={4}
314
+ placeholder={t("metricsPlaceholder")}
315
+ value={metrics}
316
+ onChange={(event) => setMetrics(event.target.value)}
317
+ disabled={busy}
318
+ />
319
+ </Field>
320
+ </div>
321
+ <Button className="mt-5 w-full" onClick={() => void startResearch()} disabled={busy}>
322
+ {busy ? (
323
+ <>
324
+ <Loader2 className="size-4 animate-spin" aria-hidden />
325
+ {state.phase === "submitting" ? t("submitting") : t("running")}
326
+ </>
327
+ ) : (
328
+ t("submit")
329
+ )}
330
+ </Button>
331
+ <p className="mt-3 text-xs leading-5 text-[#666666]">{t("inputHint")}</p>
332
+ </section>
333
+
334
+ <section ref={resultRef} className="min-h-[34rem]">
335
+ {state.phase === "idle" ? <EmptyPanel title={t("emptyTitle")} hint={t("emptyHint")} /> : null}
336
+ {busy ? (
337
+ <RunningPanel
338
+ title={t("runningTitle")}
339
+ hint={t("runningHint")}
340
+ statusLabel={statusLabel}
341
+ progressItems={progressItems}
342
+ progressTitle={t("progressTitle")}
343
+ />
344
+ ) : null}
345
+ {partial ? <ForecastReport data={partial} t={t} variant="partial" onReset={reset} /> : null}
346
+ {state.phase === "error" ? (
347
+ <ErrorPanel
348
+ message={t(`errors.${state.category}`)}
349
+ retryLabel={t("retry")}
350
+ onRetry={() => void startResearch()}
351
+ />
352
+ ) : null}
353
+ {state.phase === "done" ? (
354
+ <ForecastReport data={state.data} taskId={state.taskId} t={t} variant="final" onReset={reset} />
355
+ ) : null}
356
+ </section>
357
+ </div>
358
+ );
359
+ }
360
+
361
+ function EmptyPanel({ title, hint }: { title: string; hint: string }) {
362
+ return (
363
+ <div className="flex min-h-[34rem] flex-col justify-center rounded-[1.25rem] border border-dashed border-[#CDD4DE] bg-[#FAFAF7] p-8">
364
+ <p className="text-xl font-semibold text-[#111111]">{title}</p>
365
+ <p className="mt-3 max-w-xl text-sm leading-6 text-[#666666]">{hint}</p>
366
+ </div>
367
+ );
368
+ }
369
+
370
+ function RunningPanel({
371
+ title,
372
+ hint,
373
+ statusLabel,
374
+ progressItems,
375
+ progressTitle,
376
+ }: {
377
+ title: string;
378
+ hint: string;
379
+ statusLabel: string;
380
+ progressItems: ProgressItem[];
381
+ progressTitle: string;
382
+ }) {
383
+ return (
384
+ <div className="mb-5 rounded-[1.25rem] border border-[#D9DEE7] bg-white p-6 shadow-[0_16px_44px_rgba(32,41,58,0.07)]">
385
+ <div className="flex items-start justify-between gap-4">
386
+ <div>
387
+ <p className="text-xl font-semibold text-[#111111]">{title}</p>
388
+ <p className="mt-2 text-sm leading-6 text-[#666666]">{hint}</p>
389
+ </div>
390
+ <span className="rounded-full bg-[#EEF2F6] px-3 py-1 text-xs font-semibold text-[#2F3A48]">
391
+ {statusLabel}
392
+ </span>
393
+ </div>
394
+ <div className="mt-6">
395
+ <p className="text-xs font-semibold uppercase tracking-[0.08em] text-[#667085]">
396
+ {progressTitle}
397
+ </p>
398
+ <div className="mt-3 space-y-3">
399
+ {progressItems.map((item) => (
400
+ <div key={item.id} className="rounded-md bg-[#F6F7F8] px-4 py-3 text-sm leading-6 text-[#333333]">
401
+ {item.text}
402
+ </div>
403
+ ))}
404
+ </div>
405
+ </div>
406
+ </div>
407
+ );
408
+ }
409
+
410
+ function ErrorPanel({
411
+ message,
412
+ retryLabel,
413
+ onRetry,
414
+ }: {
415
+ message: string;
416
+ retryLabel: string;
417
+ onRetry: () => void;
418
+ }) {
419
+ return (
420
+ <div className="mt-5 rounded-[1.25rem] border border-[#F2C7C7] bg-[#FFF7F7] p-6">
421
+ <p className="text-base font-semibold text-[#8A1F1F]">{message}</p>
422
+ <Button className="mt-4" variant="secondary" onClick={onRetry}>
423
+ <RefreshCw className="size-4" aria-hidden />
424
+ {retryLabel}
425
+ </Button>
426
+ </div>
427
+ );
428
+ }
429
+
430
+ function ForecastReport({
431
+ data,
432
+ taskId,
433
+ t,
434
+ variant,
435
+ onReset,
436
+ }: {
437
+ data: MarketForecastReportResult;
438
+ taskId?: string;
439
+ t: ReturnType<typeof useTranslations>;
440
+ variant: "partial" | "final";
441
+ onReset: () => void;
442
+ }) {
443
+ const [copied, setCopied] = useState(false);
444
+ const marketDefinition = data.marketDefinition;
445
+ const evidenceBreakdown = data.evidenceStrengthBreakdown.length
446
+ ? data.evidenceStrengthBreakdown
447
+ : buildEvidenceBreakdown(data);
448
+
449
+ async function copyReport() {
450
+ const lines = [
451
+ data.reportTitle,
452
+ data.executiveSummary,
453
+ "",
454
+ t("sections.forecast"),
455
+ ...data.forecastScenarios.map(
456
+ (item) =>
457
+ `${item.scenario}: ${item.users}, ${item.paidUsers}, ${item.paidConversionRate}, ARR ${item.arr}`,
458
+ ),
459
+ "",
460
+ t("sections.evidence"),
461
+ ...data.evidenceItems.map((item) => `${item.claim} | ${item.sourceLabel}`),
462
+ "",
463
+ data.boundaryNote,
464
+ data.disclaimer,
465
+ ];
466
+ await navigator.clipboard.writeText(lines.filter(Boolean).join("\n"));
467
+ setCopied(true);
468
+ window.setTimeout(() => setCopied(false), 1600);
469
+ }
470
+
471
+ return (
472
+ <div className="rounded-[1.25rem] border border-[#D9DEE7] bg-white p-5 shadow-[0_16px_44px_rgba(32,41,58,0.07)] md:p-6">
473
+ <div className="flex flex-col justify-between gap-4 md:flex-row md:items-start">
474
+ <div>
475
+ <p className="text-xs font-semibold uppercase tracking-[0.08em] text-[#47647C]">
476
+ {variant === "partial" ? t("report.partialKicker") : t("report.kicker")}
477
+ </p>
478
+ <h3 className="mt-2 text-2xl font-semibold leading-tight text-[#111111]">
479
+ {data.reportTitle}
480
+ </h3>
481
+ <p className="mt-3 max-w-3xl text-sm leading-6 text-[#555555]">
482
+ {data.executiveSummary || t("report.partialSummary")}
483
+ </p>
484
+ </div>
485
+ <div className="flex flex-wrap gap-2">
486
+ <Button variant="secondary" onClick={() => void copyReport()} disabled={variant === "partial" && !data.forecastScenarios.length}>
487
+ <Copy className="size-4" aria-hidden />
488
+ {copied ? t("copied") : t("copy")}
489
+ </Button>
490
+ <Button variant="secondary" onClick={onReset}>
491
+ <RefreshCw className="size-4" aria-hidden />
492
+ {t("analyzeAnother")}
493
+ </Button>
494
+ </div>
495
+ </div>
496
+
497
+ <div className="mt-6 grid gap-3 md:grid-cols-4">
498
+ <Metric label={t("report.confidence")} value={t(`confidence.${data.confidence}`)} />
499
+ <Metric label={t("report.scenarios")} value={String(data.forecastScenarios.length)} />
500
+ <Metric label={t("report.sources")} value={String(data.sources.length || data.evidenceItems.length)} />
501
+ <Metric label={t("report.taskId")} value={taskId || t("report.inProgress")} />
502
+ </div>
503
+
504
+ {(data.researchProtocol || data.stabilityCheck) ? (
505
+ <ProtocolAndStability data={data} t={t} />
506
+ ) : null}
507
+
508
+ {evidenceBreakdown.length ? (
509
+ <EvidenceStrengthSummary
510
+ title={t("report.evidenceStrength")}
511
+ items={evidenceBreakdown}
512
+ t={t}
513
+ />
514
+ ) : null}
515
+
516
+ {marketDefinition ? (
517
+ <ReportSection title={t("sections.market")}>
518
+ <div className="grid gap-4 md:grid-cols-3">
519
+ <EstimateCard title="TAM" item={marketDefinition.tam} />
520
+ <EstimateCard title="SAM" item={marketDefinition.sam} />
521
+ <EstimateCard title="SOM" item={marketDefinition.som} />
522
+ </div>
523
+ <p className="mt-4 text-sm leading-6 text-[#555555]">
524
+ {marketDefinition.category} / {marketDefinition.targetSegment}
525
+ {marketDefinition.region ? ` / ${marketDefinition.region}` : ""}
526
+ </p>
527
+ </ReportSection>
528
+ ) : null}
529
+
530
+ {data.forecastScenarios.length ? (
531
+ <ReportSection title={t("sections.forecast")}>
532
+ <div className="grid gap-4 lg:grid-cols-3">
533
+ {data.forecastScenarios.map((scenario) => (
534
+ <article key={`${scenario.scenario}-${scenario.horizon}`} className="rounded-[1rem] border border-[#E1E5EA] p-5">
535
+ <div className="flex items-center justify-between gap-3">
536
+ <h4 className="text-base font-semibold text-[#111111]">
537
+ {t(`scenario.${scenario.scenario}`)}
538
+ </h4>
539
+ <span className="rounded-full bg-[#EEF2F6] px-3 py-1 text-xs font-semibold text-[#405266]">
540
+ {t(`confidence.${scenario.confidence}`)}
541
+ </span>
542
+ </div>
543
+ <div className="mt-4 grid gap-3">
544
+ <Metric label={t("forecast.users")} value={scenario.users} />
545
+ <Metric label={t("forecast.paidUsers")} value={scenario.paidUsers} />
546
+ <Metric label={t("forecast.conversion")} value={scenario.paidConversionRate} />
547
+ <Metric label={t("forecast.arr")} value={scenario.arr} />
548
+ </div>
549
+ {scenario.formula ? (
550
+ <FormulaBlock title={t("forecast.formula")} formula={scenario.formula} />
551
+ ) : null}
552
+ <ListBlock title={t("forecast.assumptions")} items={scenario.assumptions} compact />
553
+ </article>
554
+ ))}
555
+ </div>
556
+ </ReportSection>
557
+ ) : null}
558
+
559
+ {data.dataVisuals.length ? (
560
+ <ReportSection title={t("sections.dataVisuals")}>
561
+ <div className="grid gap-4 md:grid-cols-2">
562
+ {data.dataVisuals.map((visual) => (
563
+ <DataVisualCard key={visual.title} visual={visual} t={t} />
564
+ ))}
565
+ </div>
566
+ </ReportSection>
567
+ ) : null}
568
+
569
+ {data.evidenceItems.length ? (
570
+ <ReportSection title={t("sections.evidence")}>
571
+ <div className="space-y-3">
572
+ {data.evidenceItems.slice(0, 8).map((item) => (
573
+ <article key={`${item.claim}-${item.sourceLabel}`} className="rounded-[1rem] border border-[#E1E5EA] p-4">
574
+ <div className="flex flex-col justify-between gap-2 md:flex-row md:items-start">
575
+ <p className="text-sm font-semibold leading-6 text-[#111111]">{item.claim}</p>
576
+ <span className="shrink-0 rounded-full bg-[#F7F4EC] px-3 py-1 text-xs font-semibold text-[#6E5636]">
577
+ {item.sourceType}
578
+ </span>
579
+ </div>
580
+ {item.metric ? <p className="mt-2 text-sm text-[#405266]">{item.metric}</p> : null}
581
+ <p className="mt-2 text-xs leading-5 text-[#666666]">
582
+ {item.sourceUrl ? (
583
+ <a className="font-semibold underline-offset-4 hover:underline" href={item.sourceUrl} target="_blank" rel="noreferrer">
584
+ {item.sourceLabel}
585
+ </a>
586
+ ) : (
587
+ item.sourceLabel
588
+ )}
589
+ {" | "}
590
+ {item.relevance}
591
+ </p>
592
+ </article>
593
+ ))}
594
+ </div>
595
+ </ReportSection>
596
+ ) : null}
597
+
598
+ {(data.communitySignals.length || data.policyTrends.length) ? (
599
+ <ReportSection title={t("sections.signals")}>
600
+ <div className="grid gap-4 md:grid-cols-2">
601
+ <CommunitySignalsCard title={t("report.community")} items={data.communitySignals} t={t} />
602
+ <PolicyTrendsCard title={t("report.policy")} items={data.policyTrends} t={t} />
603
+ </div>
604
+ </ReportSection>
605
+ ) : null}
606
+
607
+ {data.comparableCases.length ? (
608
+ <ReportSection title={t("sections.comparables")}>
609
+ <div className="grid gap-4 md:grid-cols-2">
610
+ {data.comparableCases.map((item) => (
611
+ <ComparableCaseCard key={item.name} item={item} t={t} />
612
+ ))}
613
+ </div>
614
+ </ReportSection>
615
+ ) : null}
616
+
617
+ <ReportSection title={t("sections.diligence")}>
618
+ <div className="grid gap-4 md:grid-cols-2">
619
+ <ListBlock
620
+ title={t("report.sensitivity")}
621
+ items={data.sensitivityAnalysis.map(
622
+ (item) => `${item.variable}: ${item.whyItMatters}`,
623
+ )}
624
+ />
625
+ <ListBlock title={t("report.checklist")} items={data.investorDiligenceChecklist} />
626
+ <ListBlock title={t("report.missingEvidence")} items={data.missingEvidence} />
627
+ <SourceList title={t("report.sources")} items={data.sources} />
628
+ </div>
629
+ </ReportSection>
630
+
631
+ <p className="mt-6 rounded-md bg-[#F6F7F8] p-4 text-xs leading-5 text-[#666666]">
632
+ {data.boundaryNote || data.disclaimer || t("boundaryFallback")}
633
+ </p>
634
+ </div>
635
+ );
636
+ }
637
+
638
+ function ProtocolAndStability({
639
+ data,
640
+ t,
641
+ }: {
642
+ data: MarketForecastReportResult;
643
+ t: ReturnType<typeof useTranslations>;
644
+ }) {
645
+ const protocol = data.researchProtocol;
646
+ const stability = data.stabilityCheck;
647
+ return (
648
+ <section className="mt-6 grid gap-4 md:grid-cols-2">
649
+ {protocol ? (
650
+ <article className="rounded-[1rem] border border-[#E1E5EA] p-5">
651
+ <h4 className="text-base font-semibold text-[#111111]">{t("report.researchProtocol")}</h4>
652
+ <div className="mt-3 space-y-3 text-sm leading-6 text-[#555555]">
653
+ {protocol.primaryMarketDefinition ? (
654
+ <p>
655
+ <span className="font-semibold text-[#111111]">{t("protocol.market")}:</span>{" "}
656
+ {protocol.primaryMarketDefinition}
657
+ </p>
658
+ ) : null}
659
+ {protocol.marketBoundary ? (
660
+ <p>
661
+ <span className="font-semibold text-[#111111]">{t("protocol.boundary")}:</span>{" "}
662
+ {protocol.marketBoundary}
663
+ </p>
664
+ ) : null}
665
+ {protocol.recencyRule ? (
666
+ <p>
667
+ <span className="font-semibold text-[#111111]">{t("protocol.recency")}:</span>{" "}
668
+ {protocol.recencyRule}
669
+ </p>
670
+ ) : null}
671
+ {protocol.forecastFormula ? (
672
+ <p>
673
+ <span className="font-semibold text-[#111111]">{t("protocol.formula")}:</span>{" "}
674
+ {protocol.forecastFormula}
675
+ </p>
676
+ ) : null}
677
+ {protocol.sourceBucketsCovered.length ? (
678
+ <div className="flex flex-wrap gap-2">
679
+ {protocol.sourceBucketsCovered.map((item) => (
680
+ <span key={item} className="rounded-full bg-[#F6F7F8] px-3 py-1 text-xs font-semibold text-[#405266]">
681
+ {item}
682
+ </span>
683
+ ))}
684
+ </div>
685
+ ) : null}
686
+ {protocol.evidenceChannelPlan ? (
687
+ <ChannelPlan plan={protocol.evidenceChannelPlan} t={t} />
688
+ ) : null}
689
+ </div>
690
+ </article>
691
+ ) : null}
692
+ {stability ? (
693
+ <article className="rounded-[1rem] border border-[#E1E5EA] p-5">
694
+ <div className="flex items-start justify-between gap-3">
695
+ <h4 className="text-base font-semibold text-[#111111]">{t("report.stabilityCheck")}</h4>
696
+ <span className="rounded-full bg-[#EEF2F6] px-3 py-1 text-xs font-semibold text-[#405266]">
697
+ {t(`volatility.${stability.volatilityRisk}`)}
698
+ </span>
699
+ </div>
700
+ {stability.conclusionBand ? (
701
+ <p className="mt-3 text-sm leading-6 text-[#555555]">{stability.conclusionBand}</p>
702
+ ) : null}
703
+ {stability.likelyVarianceDrivers.length ? (
704
+ <ListBlock title={t("report.varianceDrivers")} items={stability.likelyVarianceDrivers} compact />
705
+ ) : null}
706
+ {stability.sanityChecks.length ? (
707
+ <div className="mt-4 space-y-2">
708
+ {stability.sanityChecks.slice(0, 6).map((check) => (
709
+ <div key={check.name} className="rounded-md bg-[#FAFAF7] p-3 text-xs leading-5 text-[#555555]">
710
+ <span className="font-semibold text-[#111111]">{check.name}</span>
711
+ <span className="ml-2 rounded-full bg-white px-2 py-0.5 font-semibold text-[#405266]">
712
+ {t(`checkStatus.${check.status}`)}
713
+ </span>
714
+ <p className="mt-1">{check.note}</p>
715
+ </div>
716
+ ))}
717
+ </div>
718
+ ) : null}
719
+ </article>
720
+ ) : null}
721
+ </section>
722
+ );
723
+ }
724
+
725
+ function ChannelPlan({
726
+ plan,
727
+ t,
728
+ }: {
729
+ plan: NonNullable<NonNullable<MarketForecastReportResult["researchProtocol"]>["evidenceChannelPlan"]>;
730
+ t: ReturnType<typeof useTranslations>;
731
+ }) {
732
+ return (
733
+ <div className="mt-4 rounded-md bg-[#FAFAF7] p-4">
734
+ <p className="text-xs font-semibold uppercase tracking-[0.08em] text-[#667085]">
735
+ {t("protocol.channelPlan")}
736
+ </p>
737
+ {plan.channelSelectionRationale ? (
738
+ <p className="mt-2 text-xs leading-5 text-[#666666]">{plan.channelSelectionRationale}</p>
739
+ ) : null}
740
+ <div className="mt-3 flex flex-wrap gap-2 text-xs">
741
+ {plan.marketArchetype ? <SignalBadge label={t("protocol.archetype")} value={plan.marketArchetype} /> : null}
742
+ {plan.geo ? <SignalBadge label={t("protocol.geo")} value={plan.geo} /> : null}
743
+ {plan.language ? <SignalBadge label={t("protocol.language")} value={plan.language} /> : null}
744
+ </div>
745
+ {plan.channels.length ? (
746
+ <div className="mt-4 space-y-2">
747
+ {plan.channels.slice(0, 10).map((channel) => (
748
+ <div key={`${channel.name}-${channel.role}`} className="rounded-md bg-white p-3 text-xs leading-5 text-[#555555]">
749
+ <div className="flex items-start justify-between gap-3">
750
+ <span className="font-semibold text-[#111111]">{channel.name}</span>
751
+ <span className="shrink-0 rounded-full bg-[#EEF2F6] px-2 py-0.5 font-semibold text-[#405266]">
752
+ {t(`channelRole.${channel.role}`)}
753
+ </span>
754
+ </div>
755
+ <p className="mt-1">{channel.reason}</p>
756
+ <p className="mt-1 text-[#777777]">{channel.evidenceUse}</p>
757
+ </div>
758
+ ))}
759
+ </div>
760
+ ) : null}
761
+ </div>
762
+ );
763
+ }
764
+
765
+ function buildEvidenceBreakdown(data: MarketForecastReportResult) {
766
+ const counts = new Map<string, number>();
767
+ for (const item of data.evidenceItems) {
768
+ counts.set(item.sourceType, (counts.get(item.sourceType) ?? 0) + 1);
769
+ }
770
+ for (const item of data.sources) {
771
+ counts.set(item.type, Math.max(counts.get(item.type) ?? 0, 1));
772
+ }
773
+ return Array.from(counts.entries()).map(([sourceType, evidenceCount]) => ({
774
+ sourceType: sourceType as MarketForecastReportResult["evidenceItems"][number]["sourceType"],
775
+ evidenceCount,
776
+ strength: evidenceCount >= 3 ? ("strong" as const) : evidenceCount >= 2 ? ("mixed" as const) : ("weak" as const),
777
+ note: undefined,
778
+ }));
779
+ }
780
+
781
+ function ReportSection({
782
+ title,
783
+ children,
784
+ }: {
785
+ title: string;
786
+ children: React.ReactNode;
787
+ }) {
788
+ return (
789
+ <section className="mt-8 border-t border-[#E5E8EC] pt-6">
790
+ <h3 className="text-xl font-semibold text-[#111111]">{title}</h3>
791
+ <div className="mt-4">{children}</div>
792
+ </section>
793
+ );
794
+ }
795
+
796
+ function Metric({ label, value }: { label: string; value: string }) {
797
+ return (
798
+ <div className="rounded-md bg-[#F6F7F8] p-4">
799
+ <p className="text-xs font-semibold uppercase tracking-[0.08em] text-[#667085]">{label}</p>
800
+ <p className="mt-2 break-words text-sm font-semibold text-[#111111]">{value}</p>
801
+ </div>
802
+ );
803
+ }
804
+
805
+ function EvidenceStrengthSummary({
806
+ title,
807
+ items,
808
+ t,
809
+ }: {
810
+ title: string;
811
+ items: MarketForecastReportResult["evidenceStrengthBreakdown"];
812
+ t: ReturnType<typeof useTranslations>;
813
+ }) {
814
+ const max = Math.max(...items.map((item) => item.evidenceCount), 1);
815
+ return (
816
+ <section className="mt-6 rounded-[1rem] border border-[#E1E5EA] p-5">
817
+ <h4 className="text-base font-semibold text-[#111111]">{title}</h4>
818
+ <div className="mt-4 space-y-3">
819
+ {items.map((item) => (
820
+ <div key={item.sourceType}>
821
+ <div className="flex items-center justify-between gap-4 text-xs font-semibold text-[#667085]">
822
+ <span>{item.sourceType}</span>
823
+ <span>
824
+ {item.evidenceCount} / {t(`strength.${item.strength}`)}
825
+ </span>
826
+ </div>
827
+ <div className="mt-2 h-2 rounded-full bg-[#EEF2F6]">
828
+ <div
829
+ className="h-2 rounded-full bg-[#47647C]"
830
+ style={{ width: `${Math.max(10, (item.evidenceCount / max) * 100)}%` }}
831
+ />
832
+ </div>
833
+ {item.note ? <p className="mt-1 text-xs leading-5 text-[#777777]">{item.note}</p> : null}
834
+ </div>
835
+ ))}
836
+ </div>
837
+ </section>
838
+ );
839
+ }
840
+
841
+ function FormulaBlock({
842
+ title,
843
+ formula,
844
+ }: {
845
+ title: string;
846
+ formula: NonNullable<MarketForecastReportResult["forecastScenarios"][number]["formula"]>;
847
+ }) {
848
+ const rows = [
849
+ ["Reachable", formula.reachableUsers],
850
+ ["Acquire", formula.acquisitionRate],
851
+ ["Active", formula.activeUsers],
852
+ ["Paid", formula.paidConversionRate],
853
+ ["ARPU/ACV", formula.arpuOrAcv],
854
+ ].filter((row): row is [string, string] => Boolean(row[1]));
855
+ return (
856
+ <div className="mt-4 rounded-md bg-[#FAFAF7] p-4">
857
+ <p className="text-xs font-semibold uppercase tracking-[0.08em] text-[#667085]">{title}</p>
858
+ {formula.arrFormula ? (
859
+ <p className="mt-2 text-sm font-semibold leading-6 text-[#111111]">{formula.arrFormula}</p>
860
+ ) : null}
861
+ {rows.length ? (
862
+ <dl className="mt-3 grid gap-2 text-xs text-[#555555]">
863
+ {rows.map(([label, value]) => (
864
+ <div key={label} className="flex justify-between gap-3">
865
+ <dt className="text-[#667085]">{label}</dt>
866
+ <dd className="text-right font-semibold text-[#333333]">{value}</dd>
867
+ </div>
868
+ ))}
869
+ </dl>
870
+ ) : null}
871
+ {formula.rationale ? <p className="mt-3 text-xs leading-5 text-[#666666]">{formula.rationale}</p> : null}
872
+ </div>
873
+ );
874
+ }
875
+
876
+ function DataVisualCard({
877
+ visual,
878
+ t,
879
+ }: {
880
+ visual: MarketForecastReportResult["dataVisuals"][number];
881
+ t: ReturnType<typeof useTranslations>;
882
+ }) {
883
+ const max = Math.max(...visual.dataPoints.map((point) => parseLooseNumber(point.value)), 1);
884
+ return (
885
+ <article className="rounded-[1rem] border border-[#E1E5EA] p-5">
886
+ <div className="flex items-start justify-between gap-3">
887
+ <div>
888
+ <h4 className="text-base font-semibold text-[#111111]">{visual.title}</h4>
889
+ <p className="mt-2 text-sm leading-6 text-[#555555]">{visual.summary}</p>
890
+ </div>
891
+ <span className="shrink-0 rounded-full bg-[#EEF2F6] px-3 py-1 text-xs font-semibold text-[#405266]">
892
+ {t(`confidence.${visual.confidence}`)}
893
+ </span>
894
+ </div>
895
+ <div className="mt-5 space-y-4">
896
+ {visual.dataPoints.map((point) => {
897
+ const numeric = parseLooseNumber(point.value);
898
+ return (
899
+ <div key={`${visual.title}-${point.label}`}>
900
+ <div className="flex items-center justify-between gap-4 text-sm">
901
+ <span className="font-semibold text-[#111111]">{point.label}</span>
902
+ <span className="text-[#405266]">{point.value}</span>
903
+ </div>
904
+ <div className="mt-2 h-2 rounded-full bg-[#EEF2F6]">
905
+ <div
906
+ className="h-2 rounded-full bg-[#6E5636]"
907
+ style={{ width: `${Math.max(8, (numeric / max) * 100)}%` }}
908
+ />
909
+ </div>
910
+ {point.note || point.sourceLabel ? (
911
+ <p className="mt-1 text-xs leading-5 text-[#777777]">
912
+ {point.note}
913
+ {point.sourceLabel ? (
914
+ <>
915
+ {" "}
916
+ <SourceLink label={point.sourceLabel} url={point.sourceUrl} />
917
+ </>
918
+ ) : null}
919
+ </p>
920
+ ) : null}
921
+ </div>
922
+ );
923
+ })}
924
+ </div>
925
+ </article>
926
+ );
927
+ }
928
+
929
+ function parseLooseNumber(value: string) {
930
+ const text = value.toLowerCase().replace(/,/g, "");
931
+ const match = text.match(/[\d.]+/);
932
+ if (!match) return 1;
933
+ const base = Number(match[0]) || 1;
934
+ if (text.includes("b") || text.includes("亿")) return base * 1_000;
935
+ if (text.includes("m") || text.includes("百万") || text.includes("万")) return base;
936
+ return base;
937
+ }
938
+
939
+ function CommunitySignalsCard({
940
+ title,
941
+ items,
942
+ t,
943
+ }: {
944
+ title: string;
945
+ items: MarketForecastReportResult["communitySignals"];
946
+ t: ReturnType<typeof useTranslations>;
947
+ }) {
948
+ return (
949
+ <div className="rounded-[1rem] border border-[#E1E5EA] p-5">
950
+ <p className="text-sm font-semibold text-[#111111]">{title}</p>
951
+ <div className="mt-4 space-y-4">
952
+ {items.length ? (
953
+ items.map((item) => (
954
+ <article key={`${item.platform}-${item.communityOrQuery}`} className="rounded-md bg-[#FAFAF7] p-4">
955
+ <div className="flex flex-col justify-between gap-2 sm:flex-row sm:items-start">
956
+ <div>
957
+ <h4 className="text-sm font-semibold text-[#111111]">{item.platform}</h4>
958
+ <p className="mt-1 text-xs leading-5 text-[#667085]">{item.communityOrQuery}</p>
959
+ </div>
960
+ <span className="rounded-full bg-white px-3 py-1 text-xs font-semibold text-[#405266]">
961
+ {t(`intent.${item.buyingIntent ?? "unknown"}`)}
962
+ </span>
963
+ </div>
964
+ <p className="mt-3 text-sm leading-6 text-[#333333]">{item.activeSignal}</p>
965
+ {item.repeatedPain ? <p className="mt-2 text-sm leading-6 text-[#555555]">{item.repeatedPain}</p> : null}
966
+ <div className="mt-3 flex flex-wrap gap-2">
967
+ {item.observedMetric ? <SignalBadge label={t("report.observedMetric")} value={item.observedMetric} /> : null}
968
+ {item.observedPeriod ? <SignalBadge label={t("report.observedPeriod")} value={item.observedPeriod} /> : null}
969
+ <SignalBadge label={t("report.sourceStatus")} value={t(`sourceStatus.${item.evidenceStatus}`)} />
970
+ </div>
971
+ <div className="mt-3 space-y-1 text-xs leading-5 text-[#666666]">
972
+ {item.sourceLabel ? <SourceLink label={item.sourceLabel} url={item.sourceUrl} /> : null}
973
+ {item.sampleLinks.length ? (
974
+ <div className="flex flex-wrap gap-x-3 gap-y-1">
975
+ {item.sampleLinks.slice(0, 4).map((link) => (
976
+ <SourceLink key={`${link.label}-${link.url}`} label={link.label} url={link.url} />
977
+ ))}
978
+ </div>
979
+ ) : null}
980
+ </div>
981
+ </article>
982
+ ))
983
+ ) : (
984
+ <p className="text-sm text-[#777777]">-</p>
985
+ )}
986
+ </div>
987
+ </div>
988
+ );
989
+ }
990
+
991
+ function PolicyTrendsCard({
992
+ title,
993
+ items,
994
+ t,
995
+ }: {
996
+ title: string;
997
+ items: MarketForecastReportResult["policyTrends"];
998
+ t: ReturnType<typeof useTranslations>;
999
+ }) {
1000
+ return (
1001
+ <div className="rounded-[1rem] border border-[#E1E5EA] p-5">
1002
+ <p className="text-sm font-semibold text-[#111111]">{title}</p>
1003
+ <div className="mt-4 space-y-4">
1004
+ {items.length ? (
1005
+ items.map((item) => (
1006
+ <article key={item.title} className="rounded-md bg-[#FAFAF7] p-4">
1007
+ <div className="flex items-start justify-between gap-3">
1008
+ <h4 className="text-sm font-semibold leading-6 text-[#111111]">{item.title}</h4>
1009
+ <span className="shrink-0 rounded-full bg-white px-3 py-1 text-xs font-semibold text-[#405266]">
1010
+ {t(`policy.${item.direction}`)}
1011
+ </span>
1012
+ </div>
1013
+ <p className="mt-2 text-sm leading-6 text-[#555555]">{item.impactOnAdoption}</p>
1014
+ {item.impactOnCostOrSalesCycle ? (
1015
+ <p className="mt-2 text-xs leading-5 text-[#666666]">{item.impactOnCostOrSalesCycle}</p>
1016
+ ) : null}
1017
+ {item.sourceLabel ? (
1018
+ <p className="mt-2 text-xs leading-5 text-[#666666]">
1019
+ <SourceLink label={item.sourceLabel} url={item.sourceUrl} />
1020
+ </p>
1021
+ ) : null}
1022
+ </article>
1023
+ ))
1024
+ ) : (
1025
+ <p className="text-sm text-[#777777]">-</p>
1026
+ )}
1027
+ </div>
1028
+ </div>
1029
+ );
1030
+ }
1031
+
1032
+ function ComparableCaseCard({
1033
+ item,
1034
+ t,
1035
+ }: {
1036
+ item: MarketForecastReportResult["comparableCases"][number];
1037
+ t: ReturnType<typeof useTranslations>;
1038
+ }) {
1039
+ return (
1040
+ <article className="rounded-[1rem] border border-[#E1E5EA] p-5">
1041
+ <div className="flex items-start justify-between gap-3">
1042
+ <h4 className="text-base font-semibold text-[#111111]">{item.name}</h4>
1043
+ <span className="rounded-full bg-[#EEF2F6] px-3 py-1 text-xs font-semibold text-[#405266]">
1044
+ {t(`confidence.${item.confidence}`)}
1045
+ </span>
1046
+ </div>
1047
+ <p className="mt-3 text-xs font-semibold uppercase tracking-[0.08em] text-[#667085]">
1048
+ {item.comparableDimension}
1049
+ </p>
1050
+ <p className="mt-2 text-sm leading-6 text-[#333333]">{item.relevance}</p>
1051
+ <p className="mt-3 rounded-md bg-[#FAFAF7] p-3 text-xs leading-5 text-[#666666]">
1052
+ {item.limitation}
1053
+ </p>
1054
+ {item.sourceLabel ? (
1055
+ <p className="mt-3 text-xs leading-5 text-[#666666]">
1056
+ <SourceLink label={item.sourceLabel} url={item.sourceUrl} />
1057
+ </p>
1058
+ ) : null}
1059
+ </article>
1060
+ );
1061
+ }
1062
+
1063
+ function SignalBadge({ label, value }: { label: string; value: string }) {
1064
+ return (
1065
+ <span className="rounded-full bg-white px-3 py-1 text-xs font-semibold text-[#405266]">
1066
+ {label}: {value}
1067
+ </span>
1068
+ );
1069
+ }
1070
+
1071
+ function SourceList({
1072
+ title,
1073
+ items,
1074
+ }: {
1075
+ title: string;
1076
+ items: MarketForecastReportResult["sources"];
1077
+ }) {
1078
+ return (
1079
+ <div className="rounded-[1rem] border border-[#E1E5EA] p-5">
1080
+ <p className="text-sm font-semibold text-[#111111]">{title}</p>
1081
+ <ul className="mt-3 space-y-2 text-sm leading-6 text-[#555555]">
1082
+ {items.length ? (
1083
+ items.map((item) => (
1084
+ <li key={`${item.label}-${item.url ?? item.type}`}>
1085
+ <SourceLink label={item.label} url={item.url} />
1086
+ <span className="text-[#777777]"> / {item.type}</span>
1087
+ </li>
1088
+ ))
1089
+ ) : (
1090
+ <li className="text-[#777777]">-</li>
1091
+ )}
1092
+ </ul>
1093
+ </div>
1094
+ );
1095
+ }
1096
+
1097
+ function SourceLink({ label, url }: { label: string; url?: string }) {
1098
+ if (!url) return <span>{label}</span>;
1099
+ return (
1100
+ <a
1101
+ className="inline-flex items-center gap-1 font-semibold text-[#2F516B] underline-offset-4 hover:underline"
1102
+ href={url}
1103
+ target="_blank"
1104
+ rel="noreferrer"
1105
+ >
1106
+ {label}
1107
+ <ExternalLink className="size-3" aria-hidden />
1108
+ </a>
1109
+ );
1110
+ }
1111
+
1112
+ function EstimateCard({
1113
+ title,
1114
+ item,
1115
+ }: {
1116
+ title: string;
1117
+ item: { estimate: string; rationale: string; confidence: "low" | "medium" | "high" };
1118
+ }) {
1119
+ return (
1120
+ <article className="rounded-[1rem] border border-[#E1E5EA] p-5">
1121
+ <div className="flex items-center justify-between gap-3">
1122
+ <h4 className="text-base font-semibold text-[#111111]">{title}</h4>
1123
+ <span className="rounded-full bg-[#EEF2F6] px-3 py-1 text-xs font-semibold text-[#405266]">
1124
+ {item.confidence}
1125
+ </span>
1126
+ </div>
1127
+ <p className="mt-3 text-lg font-semibold text-[#111111]">{item.estimate}</p>
1128
+ <p className="mt-2 text-sm leading-6 text-[#555555]">{item.rationale}</p>
1129
+ </article>
1130
+ );
1131
+ }
1132
+
1133
+ function ListBlock({
1134
+ title,
1135
+ items,
1136
+ compact = false,
1137
+ }: {
1138
+ title: string;
1139
+ items: string[];
1140
+ compact?: boolean;
1141
+ }) {
1142
+ return (
1143
+ <div className={compact ? "mt-4" : "rounded-[1rem] border border-[#E1E5EA] p-5"}>
1144
+ <p className="text-sm font-semibold text-[#111111]">{title}</p>
1145
+ <ul className="mt-3 space-y-2 text-sm leading-6 text-[#555555]">
1146
+ {items.length ? items.map((item) => <li key={item}>{item}</li>) : <li className="text-[#777777]">-</li>}
1147
+ </ul>
1148
+ </div>
1149
+ );
1150
+ }