@lastbrain/module-ai 2.0.26 → 2.0.30

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 (365) hide show
  1. package/README.md +52 -1
  2. package/dist/ai.build.config.d.ts.map +1 -1
  3. package/dist/ai.build.config.js +508 -9
  4. package/dist/api/admin/ai-provider-models/[id].d.ts +18 -0
  5. package/dist/api/admin/ai-provider-models/[id].d.ts.map +1 -0
  6. package/dist/api/admin/ai-provider-models/[id].js +58 -0
  7. package/dist/api/admin/ai-provider-models.d.ts +20 -0
  8. package/dist/api/admin/ai-provider-models.d.ts.map +1 -0
  9. package/dist/api/admin/ai-provider-models.js +26 -0
  10. package/dist/api/admin/ai-providers/[key].d.ts +18 -0
  11. package/dist/api/admin/ai-providers/[key].d.ts.map +1 -0
  12. package/dist/api/admin/ai-providers/[key].js +55 -0
  13. package/dist/api/admin/ai-providers.d.ts +20 -0
  14. package/dist/api/admin/ai-providers.d.ts.map +1 -0
  15. package/dist/api/admin/ai-providers.js +26 -0
  16. package/dist/api/admin/billing-analytics.d.ts +43 -0
  17. package/dist/api/admin/billing-analytics.d.ts.map +1 -0
  18. package/dist/api/admin/billing-analytics.js +144 -0
  19. package/dist/api/admin/global-ai-settings.d.ts +14 -0
  20. package/dist/api/admin/global-ai-settings.d.ts.map +1 -0
  21. package/dist/api/admin/global-ai-settings.js +63 -0
  22. package/dist/api/admin/token-packs/[id].d.ts.map +1 -1
  23. package/dist/api/admin/token-packs/[id].js +3 -2
  24. package/dist/api/admin/token-packs.d.ts.map +1 -1
  25. package/dist/api/admin/token-packs.js +3 -2
  26. package/dist/api/admin/user-monthly-details.d.ts +49 -0
  27. package/dist/api/admin/user-monthly-details.d.ts.map +1 -0
  28. package/dist/api/admin/user-monthly-details.js +140 -0
  29. package/dist/api/admin/user-quota.d.ts +21 -0
  30. package/dist/api/admin/user-quota.d.ts.map +1 -0
  31. package/dist/api/admin/user-quota.js +59 -0
  32. package/dist/api/admin/user-token/[id].d.ts.map +1 -1
  33. package/dist/api/admin/user-token/[id].js +2 -1
  34. package/dist/api/admin/user-token.d.ts +5 -2
  35. package/dist/api/admin/user-token.d.ts.map +1 -1
  36. package/dist/api/admin/user-token.js +91 -17
  37. package/dist/api/admin/user-usage-by-model.d.ts +22 -0
  38. package/dist/api/admin/user-usage-by-model.d.ts.map +1 -0
  39. package/dist/api/admin/user-usage-by-model.js +78 -0
  40. package/dist/api/admin/user-wallet-analytics.d.ts +15 -0
  41. package/dist/api/admin/user-wallet-analytics.d.ts.map +1 -0
  42. package/dist/api/admin/user-wallet-analytics.js +67 -0
  43. package/dist/api/admin/wallet-repair/route.d.ts +30 -0
  44. package/dist/api/admin/wallet-repair/route.d.ts.map +1 -0
  45. package/dist/api/admin/wallet-repair/route.js +63 -0
  46. package/dist/api/auth/ai-model-settings.d.ts +21 -0
  47. package/dist/api/auth/ai-model-settings.d.ts.map +1 -0
  48. package/dist/api/auth/ai-model-settings.js +86 -0
  49. package/dist/api/auth/ai-settings.d.ts +17 -0
  50. package/dist/api/auth/ai-settings.d.ts.map +1 -0
  51. package/dist/api/auth/ai-settings.js +87 -0
  52. package/dist/api/auth/api-keys/[id].d.ts +17 -0
  53. package/dist/api/auth/api-keys/[id].d.ts.map +1 -0
  54. package/dist/api/auth/api-keys/[id].js +66 -0
  55. package/dist/api/auth/api-keys.d.ts +19 -0
  56. package/dist/api/auth/api-keys.d.ts.map +1 -0
  57. package/dist/api/auth/api-keys.js +94 -0
  58. package/dist/api/auth/create-checkout.d.ts +1 -1
  59. package/dist/api/auth/create-checkout.d.ts.map +1 -1
  60. package/dist/api/auth/create-checkout.js +8 -6
  61. package/dist/api/auth/generate-image.d.ts +2 -2
  62. package/dist/api/auth/generate-image.d.ts.map +1 -1
  63. package/dist/api/auth/generate-image.js +404 -104
  64. package/dist/api/auth/generate-text.d.ts +3 -2
  65. package/dist/api/auth/generate-text.d.ts.map +1 -1
  66. package/dist/api/auth/generate-text.js +130 -58
  67. package/dist/api/auth/process-ocr.d.ts +9 -0
  68. package/dist/api/auth/process-ocr.d.ts.map +1 -0
  69. package/dist/api/auth/process-ocr.js +43 -0
  70. package/dist/api/auth/prompts/stats.d.ts +14 -0
  71. package/dist/api/auth/prompts/stats.d.ts.map +1 -0
  72. package/dist/api/auth/prompts/stats.js +46 -0
  73. package/dist/api/auth/prompts.d.ts +26 -0
  74. package/dist/api/auth/prompts.d.ts.map +1 -0
  75. package/dist/api/auth/prompts.js +175 -0
  76. package/dist/api/auth/token-balance.d.ts +26 -0
  77. package/dist/api/auth/token-balance.d.ts.map +1 -0
  78. package/dist/api/auth/token-balance.js +47 -0
  79. package/dist/api/auth/token-checkout.d.ts.map +1 -1
  80. package/dist/api/auth/token-checkout.js +22 -4
  81. package/dist/api/auth/token-packs.d.ts.map +1 -1
  82. package/dist/api/auth/token-packs.js +2 -1
  83. package/dist/api/auth/usage-by-model.d.ts +25 -0
  84. package/dist/api/auth/usage-by-model.d.ts.map +1 -0
  85. package/dist/api/auth/usage-by-model.js +95 -0
  86. package/dist/api/auth/usage.d.ts +26 -0
  87. package/dist/api/auth/usage.d.ts.map +1 -0
  88. package/dist/api/auth/usage.js +127 -0
  89. package/dist/api/auth/user-tokens.d.ts.map +1 -1
  90. package/dist/api/auth/user-tokens.js +36 -2
  91. package/dist/api/auth/wallet/route.d.ts +17 -0
  92. package/dist/api/auth/wallet/route.d.ts.map +1 -0
  93. package/dist/api/auth/wallet/route.js +68 -0
  94. package/dist/api/auth/wallet.d.ts +16 -0
  95. package/dist/api/auth/wallet.d.ts.map +1 -0
  96. package/dist/api/auth/wallet.js +71 -0
  97. package/dist/api/public/gateway-models.d.ts +25 -0
  98. package/dist/api/public/gateway-models.d.ts.map +1 -0
  99. package/dist/api/public/gateway-models.js +49 -0
  100. package/dist/api/public/pricing-summary.d.ts +46 -0
  101. package/dist/api/public/pricing-summary.d.ts.map +1 -0
  102. package/dist/api/public/pricing-summary.js +70 -0
  103. package/dist/api/public/prompts/[id]/stats.d.ts +15 -0
  104. package/dist/api/public/prompts/[id]/stats.d.ts.map +1 -0
  105. package/dist/api/public/prompts/[id]/stats.js +37 -0
  106. package/dist/api/public/prompts/[id]/view.d.ts +15 -0
  107. package/dist/api/public/prompts/[id]/view.d.ts.map +1 -0
  108. package/dist/api/public/prompts/[id]/view.js +41 -0
  109. package/dist/api/public/prompts/slug/[slug].d.ts +15 -0
  110. package/dist/api/public/prompts/slug/[slug].d.ts.map +1 -0
  111. package/dist/api/public/prompts/slug/[slug].js +40 -0
  112. package/dist/api/public/prompts/user/[username].d.ts +17 -0
  113. package/dist/api/public/prompts/user/[username].d.ts.map +1 -0
  114. package/dist/api/public/prompts/user/[username].js +51 -0
  115. package/dist/api/public/prompts.d.ts +15 -0
  116. package/dist/api/public/prompts.d.ts.map +1 -0
  117. package/dist/api/public/prompts.js +45 -0
  118. package/dist/api/public/token-packs.d.ts +11 -0
  119. package/dist/api/public/token-packs.d.ts.map +1 -0
  120. package/dist/api/public/token-packs.js +25 -0
  121. package/dist/api/public/token-pricing.d.ts +44 -0
  122. package/dist/api/public/token-pricing.d.ts.map +1 -0
  123. package/dist/api/public/token-pricing.js +168 -0
  124. package/dist/api/public/v1/_lib/artifacts.d.ts +52 -0
  125. package/dist/api/public/v1/_lib/artifacts.d.ts.map +1 -0
  126. package/dist/api/public/v1/_lib/artifacts.js +217 -0
  127. package/dist/api/public/v1/_lib/auth.d.ts +43 -0
  128. package/dist/api/public/v1/_lib/auth.d.ts.map +1 -0
  129. package/dist/api/public/v1/_lib/auth.js +108 -0
  130. package/dist/api/public/v1/_lib/errors.d.ts +17 -0
  131. package/dist/api/public/v1/_lib/errors.d.ts.map +1 -0
  132. package/dist/api/public/v1/_lib/errors.js +16 -0
  133. package/dist/api/public/v1/_lib/log.d.ts +29 -0
  134. package/dist/api/public/v1/_lib/log.d.ts.map +1 -0
  135. package/dist/api/public/v1/_lib/log.js +68 -0
  136. package/dist/api/public/v1/_lib/quota.d.ts +24 -0
  137. package/dist/api/public/v1/_lib/quota.d.ts.map +1 -0
  138. package/dist/api/public/v1/_lib/quota.js +118 -0
  139. package/dist/api/public/v1/_lib/router.d.ts +54 -0
  140. package/dist/api/public/v1/_lib/router.d.ts.map +1 -0
  141. package/dist/api/public/v1/_lib/router.js +119 -0
  142. package/dist/api/public/v1/connect.d.ts +20 -0
  143. package/dist/api/public/v1/connect.d.ts.map +1 -0
  144. package/dist/api/public/v1/connect.js +119 -0
  145. package/dist/api/public/v1/doc.d.ts +239 -0
  146. package/dist/api/public/v1/doc.d.ts.map +1 -0
  147. package/dist/api/public/v1/doc.js +253 -0
  148. package/dist/api/public/v1/history/[id].d.ts +92 -0
  149. package/dist/api/public/v1/history/[id].d.ts.map +1 -0
  150. package/dist/api/public/v1/history/[id].js +176 -0
  151. package/dist/api/public/v1/history.d.ts +30 -0
  152. package/dist/api/public/v1/history.d.ts.map +1 -0
  153. package/dist/api/public/v1/history.js +142 -0
  154. package/dist/api/public/v1/image-ai.d.ts +24 -0
  155. package/dist/api/public/v1/image-ai.d.ts.map +1 -0
  156. package/dist/api/public/v1/image-ai.js +233 -0
  157. package/dist/api/public/v1/prompts.d.ts +19 -0
  158. package/dist/api/public/v1/prompts.d.ts.map +1 -0
  159. package/dist/api/public/v1/prompts.js +107 -0
  160. package/dist/api/public/v1/provider.d.ts +16 -0
  161. package/dist/api/public/v1/provider.d.ts.map +1 -0
  162. package/dist/api/public/v1/provider.js +130 -0
  163. package/dist/api/public/v1/purchase.d.ts +11 -0
  164. package/dist/api/public/v1/purchase.d.ts.map +1 -0
  165. package/dist/api/public/v1/purchase.js +18 -0
  166. package/dist/api/public/v1/status.d.ts +35 -0
  167. package/dist/api/public/v1/status.d.ts.map +1 -0
  168. package/dist/api/public/v1/status.js +163 -0
  169. package/dist/api/public/v1/text-ai.d.ts +26 -0
  170. package/dist/api/public/v1/text-ai.d.ts.map +1 -0
  171. package/dist/api/public/v1/text-ai.js +239 -0
  172. package/dist/api/public/webhook.d.ts.map +1 -1
  173. package/dist/api/public/webhook.js +50 -39
  174. package/dist/api/track-usage.d.ts +12 -0
  175. package/dist/api/track-usage.d.ts.map +1 -0
  176. package/dist/api/track-usage.js +37 -0
  177. package/dist/components/Doc.d.ts.map +1 -1
  178. package/dist/components/Doc.js +1 -1
  179. package/dist/components/DocUsageCustom.js +6 -6
  180. package/dist/components/admin/UserTokenTab.d.ts.map +1 -1
  181. package/dist/components/admin/UserTokenTab.js +170 -23
  182. package/dist/components/auth/AuthDashboardAi.d.ts +2 -0
  183. package/dist/components/auth/AuthDashboardAi.d.ts.map +1 -0
  184. package/dist/components/auth/AuthDashboardAi.js +53 -0
  185. package/dist/docs/REFACTORING_BILLING_GUIDE.d.ts +277 -0
  186. package/dist/docs/REFACTORING_BILLING_GUIDE.d.ts.map +1 -0
  187. package/dist/docs/REFACTORING_BILLING_GUIDE.js +276 -0
  188. package/dist/index.d.ts +15 -1
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +18 -1
  191. package/dist/scripts/migrate-tokens-to-wallet.d.ts +13 -0
  192. package/dist/scripts/migrate-tokens-to-wallet.d.ts.map +1 -0
  193. package/dist/scripts/migrate-tokens-to-wallet.js +165 -0
  194. package/dist/server/__tests__/billing.test.d.ts +5 -0
  195. package/dist/server/__tests__/billing.test.d.ts.map +1 -0
  196. package/dist/server/__tests__/billing.test.js +223 -0
  197. package/dist/server/ai-client.d.ts +59 -0
  198. package/dist/server/ai-client.d.ts.map +1 -0
  199. package/dist/server/ai-client.js +111 -0
  200. package/dist/server/ai-generation-service.d.ts +66 -0
  201. package/dist/server/ai-generation-service.d.ts.map +1 -0
  202. package/dist/server/ai-generation-service.js +274 -0
  203. package/dist/server/billing.d.ts +200 -0
  204. package/dist/server/billing.d.ts.map +1 -0
  205. package/dist/server/billing.js +488 -0
  206. package/dist/server/gateway-service.d.ts +13 -0
  207. package/dist/server/gateway-service.d.ts.map +1 -0
  208. package/dist/server/gateway-service.js +161 -0
  209. package/dist/server/global-settings.d.ts +16 -0
  210. package/dist/server/global-settings.d.ts.map +1 -0
  211. package/dist/server/global-settings.js +42 -0
  212. package/dist/server/model-filter.d.ts +25 -0
  213. package/dist/server/model-filter.d.ts.map +1 -0
  214. package/dist/server/model-filter.js +240 -0
  215. package/dist/server/ocr.d.ts +39 -0
  216. package/dist/server/ocr.d.ts.map +1 -0
  217. package/dist/server/ocr.js +280 -0
  218. package/dist/server/openai-client.d.ts +19 -0
  219. package/dist/server/openai-client.d.ts.map +1 -0
  220. package/dist/server/openai-client.js +26 -0
  221. package/dist/server/pricing-config.d.ts +18 -0
  222. package/dist/server/pricing-config.d.ts.map +1 -0
  223. package/dist/server/pricing-config.js +94 -0
  224. package/dist/server/pricing-validator.d.ts +41 -0
  225. package/dist/server/pricing-validator.d.ts.map +1 -0
  226. package/dist/server/pricing-validator.js +113 -0
  227. package/dist/server/pricing.d.ts +121 -0
  228. package/dist/server/pricing.d.ts.map +1 -0
  229. package/dist/server/pricing.js +225 -0
  230. package/dist/server/quota.d.ts +66 -0
  231. package/dist/server/quota.d.ts.map +1 -0
  232. package/dist/server/quota.js +538 -0
  233. package/dist/server/wallet-repair.d.ts +32 -0
  234. package/dist/server/wallet-repair.d.ts.map +1 -0
  235. package/dist/server/wallet-repair.js +189 -0
  236. package/dist/server.d.ts +13 -1
  237. package/dist/server.d.ts.map +1 -1
  238. package/dist/server.js +87 -16
  239. package/dist/sitemap/handlers/prompts.d.ts +6 -0
  240. package/dist/sitemap/handlers/prompts.d.ts.map +1 -0
  241. package/dist/sitemap/handlers/prompts.js +72 -0
  242. package/dist/sitemap/handlers/users.d.ts +6 -0
  243. package/dist/sitemap/handlers/users.d.ts.map +1 -0
  244. package/dist/sitemap/handlers/users.js +80 -0
  245. package/dist/sitemap/manifest.d.ts +8 -0
  246. package/dist/sitemap/manifest.d.ts.map +1 -0
  247. package/dist/sitemap/manifest.js +27 -0
  248. package/dist/types/gateway.d.ts +40 -0
  249. package/dist/types/gateway.d.ts.map +1 -0
  250. package/dist/types/gateway.js +4 -0
  251. package/dist/types/quota.d.ts +74 -0
  252. package/dist/types/quota.d.ts.map +1 -0
  253. package/dist/types/quota.js +11 -0
  254. package/dist/utils/date.d.ts +7 -0
  255. package/dist/utils/date.d.ts.map +1 -0
  256. package/dist/utils/date.js +17 -0
  257. package/dist/web/admin/AdminTokenPacksPage.js +1 -1
  258. package/dist/web/admin/BillingAnalyticsPage.d.ts +2 -0
  259. package/dist/web/admin/BillingAnalyticsPage.d.ts.map +1 -0
  260. package/dist/web/admin/BillingAnalyticsPage.js +141 -0
  261. package/dist/web/admin/GlobalAISettingsPage.d.ts +2 -0
  262. package/dist/web/admin/GlobalAISettingsPage.d.ts.map +1 -0
  263. package/dist/web/admin/GlobalAISettingsPage.js +93 -0
  264. package/dist/web/admin/UserTokenPage.d.ts.map +1 -1
  265. package/dist/web/admin/UserTokenPage.js +20 -7
  266. package/dist/web/auth/AISettingsPage.d.ts +2 -0
  267. package/dist/web/auth/AISettingsPage.d.ts.map +1 -0
  268. package/dist/web/auth/AISettingsPage.js +258 -0
  269. package/dist/web/auth/APIKeysPage.d.ts +2 -0
  270. package/dist/web/auth/APIKeysPage.d.ts.map +1 -0
  271. package/dist/web/auth/APIKeysPage.js +154 -0
  272. package/dist/web/auth/HistoryPage.d.ts +2 -0
  273. package/dist/web/auth/HistoryPage.d.ts.map +1 -0
  274. package/dist/web/auth/HistoryPage.js +279 -0
  275. package/dist/web/auth/PromptsPage.d.ts +5 -0
  276. package/dist/web/auth/PromptsPage.d.ts.map +1 -0
  277. package/dist/web/auth/PromptsPage.js +137 -0
  278. package/dist/web/auth/TokenPage.d.ts.map +1 -1
  279. package/dist/web/auth/TokenPage.js +88 -31
  280. package/dist/web/auth/UsageAndTokensPage.d.ts +2 -0
  281. package/dist/web/auth/UsageAndTokensPage.d.ts.map +1 -0
  282. package/dist/web/auth/UsageAndTokensPage.js +157 -0
  283. package/dist/web/auth/UsagePage.d.ts +2 -0
  284. package/dist/web/auth/UsagePage.d.ts.map +1 -0
  285. package/dist/web/auth/UsagePage.js +62 -0
  286. package/dist/web/auth/components/ApiKeyFilterSelect.d.ts +13 -0
  287. package/dist/web/auth/components/ApiKeyFilterSelect.d.ts.map +1 -0
  288. package/dist/web/auth/components/ApiKeyFilterSelect.js +16 -0
  289. package/dist/web/auth/components/ModelUsageTable.d.ts +19 -0
  290. package/dist/web/auth/components/ModelUsageTable.d.ts.map +1 -0
  291. package/dist/web/auth/components/ModelUsageTable.js +37 -0
  292. package/dist/web/auth/components/PurchaseButton.d.ts +7 -0
  293. package/dist/web/auth/components/PurchaseButton.d.ts.map +1 -0
  294. package/dist/web/auth/components/PurchaseButton.js +13 -0
  295. package/dist/web/auth/components/TokenHistoryCard.d.ts +20 -0
  296. package/dist/web/auth/components/TokenHistoryCard.d.ts.map +1 -0
  297. package/dist/web/auth/components/TokenHistoryCard.js +76 -0
  298. package/dist/web/auth/components/TokenKpiGrid.d.ts +24 -0
  299. package/dist/web/auth/components/TokenKpiGrid.d.ts.map +1 -0
  300. package/dist/web/auth/components/TokenKpiGrid.js +38 -0
  301. package/dist/web/auth/components/UsageByDayChart.d.ts +11 -0
  302. package/dist/web/auth/components/UsageByDayChart.d.ts.map +1 -0
  303. package/dist/web/auth/components/UsageByDayChart.js +32 -0
  304. package/dist/web/auth/components/UsageByModelBarChart.d.ts +12 -0
  305. package/dist/web/auth/components/UsageByModelBarChart.d.ts.map +1 -0
  306. package/dist/web/auth/components/UsageByModelBarChart.js +32 -0
  307. package/dist/web/auth/components/WalletStatusCard.d.ts +9 -0
  308. package/dist/web/auth/components/WalletStatusCard.d.ts.map +1 -0
  309. package/dist/web/auth/components/WalletStatusCard.js +50 -0
  310. package/dist/web/components/ImageGenerative.d.ts +3 -1
  311. package/dist/web/components/ImageGenerative.d.ts.map +1 -1
  312. package/dist/web/components/ImageGenerative.js +139 -52
  313. package/dist/web/components/TextareaGenerative.d.ts +3 -1
  314. package/dist/web/components/TextareaGenerative.d.ts.map +1 -1
  315. package/dist/web/components/TextareaGenerative.js +10 -5
  316. package/dist/web/public/PromptDetailPage.d.ts +25 -0
  317. package/dist/web/public/PromptDetailPage.d.ts.map +1 -0
  318. package/dist/web/public/PromptDetailPage.js +71 -0
  319. package/dist/web/public/PromptDetailPageServer.d.ts +15 -0
  320. package/dist/web/public/PromptDetailPageServer.d.ts.map +1 -0
  321. package/dist/web/public/PromptDetailPageServer.js +68 -0
  322. package/dist/web/public/PublicPromptsPage.d.ts +5 -0
  323. package/dist/web/public/PublicPromptsPage.d.ts.map +1 -0
  324. package/dist/web/public/PublicPromptsPage.js +110 -0
  325. package/dist/web/public/PurchaseTokensPage.d.ts +2 -0
  326. package/dist/web/public/PurchaseTokensPage.d.ts.map +1 -0
  327. package/dist/web/public/PurchaseTokensPage.js +98 -0
  328. package/dist/web/public/UserAvatar.d.ts +13 -0
  329. package/dist/web/public/UserAvatar.d.ts.map +1 -0
  330. package/dist/web/public/UserAvatar.js +13 -0
  331. package/dist/web/public/UserDetailPageServer.d.ts +15 -0
  332. package/dist/web/public/UserDetailPageServer.d.ts.map +1 -0
  333. package/dist/web/public/UserDetailPageServer.js +31 -0
  334. package/dist/web/public/UserPromptsPage.d.ts +9 -0
  335. package/dist/web/public/UserPromptsPage.d.ts.map +1 -0
  336. package/dist/web/public/UserPromptsPage.js +112 -0
  337. package/dist/web/public/UserPromptsPageServer.d.ts +15 -0
  338. package/dist/web/public/UserPromptsPageServer.d.ts.map +1 -0
  339. package/dist/web/public/UserPromptsPageServer.js +31 -0
  340. package/package.json +18 -9
  341. package/supabase/migrations/20251125000000_ai_tokens.sql +7 -0
  342. package/supabase/migrations/20260123100002_user_token_quota_monthly copy.sql +173 -0
  343. package/supabase/migrations/20260128100003_update_and_add_table.sql +368 -0
  344. package/supabase/migrations/20260128120000_seed_providers_models.sql +78 -0
  345. package/supabase/migrations/20260128131405_add_api_key_id_to_ledgers.sql +41 -0
  346. package/supabase/migrations/20260128140000_ai_artifacts_storage.sql +99 -0
  347. package/supabase/migrations/20260128140002_ai_user_settings.sql +57 -0
  348. package/supabase/migrations/20260128150000_drop_ai_user_settings.sql +21 -0
  349. package/supabase/migrations/20260128160000_wallet_billing_system.sql +192 -0
  350. package/supabase/migrations/20260128160001_wallet_rpc_functions.sql +165 -0
  351. package/supabase/migrations/20260128170000_add_pack_coef_to_token_packs.sql +30 -0
  352. package/supabase/migrations/20260129120000_wallet_view_rpc.sql +41 -0
  353. package/supabase/migrations/20260129220003_update_pack_margins.sql +31 -0
  354. package/supabase/migrations/20260129330004_ai_user_prompts.sql +151 -0
  355. package/supabase/migrations/20260129330005_ai_prompts_ip_tracking.sql +92 -0
  356. package/supabase/migrations/20260129330006_ai_prompts_slug.sql +64 -0
  357. package/supabase/migrations/20260129330007_ai_prompts_view_slug.sql +26 -0
  358. package/supabase/migrations/20260129440000_ai_prompts_view_username.sql +33 -0
  359. package/supabase/migrations/20260129450000_ai_prompts_add_lang.sql +40 -0
  360. package/supabase/migrations/20260131000000_extract_model_prompt_in_ledger.sql +92 -0
  361. package/supabase/migrations/20260131140000_fix_duplicate_purchases.sql +64 -0
  362. package/supabase/migrations/20260201120000_module-ai_default_models.sql +63 -0
  363. package/supabase/migrations/20260201130000_module-ai_remove_provider_tables.sql +17 -0
  364. package/supabase/migrations-down/20251217120000_user_token_quota_monthly.sql +34 -0
  365. package/supabase/migrations-down/20260128131405_add_api_key_id_to_ledgers.sql +25 -0
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from "react";
4
- import { Card, CardBody, CardHeader, Button, Input, Spinner, addToast, Chip, } from "@lastbrain/ui";
5
- import { Plus, Minus, History, Coins } from "lucide-react";
6
- import { useModuleTranslation } from "@lastbrain/core";
4
+ import { Card, CardBody, CardHeader, Button, Input, Spinner, addToast, Select, SelectItem, } from "@lastbrain/ui";
5
+ import { Plus, Coins, DollarSign, TrendingDown, TrendingUp, Wallet, TrendingUp as UsageIcon, Calendar, } from "lucide-react";
6
+ import { logger, useModuleTranslation } from "@lastbrain/core";
7
+ import { formatDateFr } from "../../utils/date";
7
8
  export function UserTokenTab({ userId }) {
8
9
  const t = useModuleTranslation("ai");
9
10
  const [loading, setLoading] = useState(true);
@@ -12,15 +13,65 @@ export function UserTokenTab({ userId }) {
12
13
  const [adjustmentAmount, setAdjustmentAmount] = useState("");
13
14
  const [adjustmentDescription, setAdjustmentDescription] = useState("");
14
15
  const [processing, setProcessing] = useState(false);
15
- // Charger le solde et l'historique
16
+ const [walletAnalytics, setWalletAnalytics] = useState(null);
17
+ const [modelUsage, setModelUsage] = useState([]);
18
+ const [loadingUsage, setLoadingUsage] = useState(false);
19
+ const [selectedMonth, setSelectedMonth] = useState("all");
20
+ // Quota state
21
+ const [quotaStatus, setQuotaStatus] = useState(null);
22
+ const [customQuotaAmount, setCustomQuotaAmount] = useState("");
23
+ const [editingQuota, setEditingQuota] = useState(false);
24
+ // Charger analytics wallet
25
+ const fetchWalletAnalytics = async () => {
26
+ try {
27
+ const response = await fetch(`/api/ai/admin/user-wallet-analytics?userId=${userId}`);
28
+ if (response.ok) {
29
+ const data = await response.json();
30
+ setWalletAnalytics(data);
31
+ }
32
+ }
33
+ catch (error) {
34
+ logger.error("Error loading wallet analytics:", error);
35
+ }
36
+ };
37
+ // Charger usage par modèle
38
+ const fetchModelUsage = async () => {
39
+ try {
40
+ setLoadingUsage(true);
41
+ const params = new URLSearchParams({ userId });
42
+ if (selectedMonth !== "all") {
43
+ params.append("month", selectedMonth);
44
+ }
45
+ const response = await fetch(`/api/ai/admin/user-usage-by-model?${params.toString()}`);
46
+ if (response.ok) {
47
+ const data = await response.json();
48
+ setModelUsage(data.data || []);
49
+ }
50
+ }
51
+ catch (error) {
52
+ logger.error("Error loading model usage:", error);
53
+ }
54
+ finally {
55
+ setLoadingUsage(false);
56
+ }
57
+ };
58
+ // Charger le solde, l'historique et le quota
16
59
  const fetchTokenData = async () => {
17
60
  try {
18
61
  setLoading(true);
19
- const res = await fetch(`/api/ai/admin/user-token/${userId}`);
20
- if (!res.ok)
62
+ // Fetch tokens, quota, and analytics in parallel
63
+ const [tokenRes, quotaRes] = await Promise.all([
64
+ // Request a large limit so the UI can display the full history (credit & debit)
65
+ fetch(`/api/ai/admin/user-token/${userId}?limit=1000&offset=0`),
66
+ fetch(`/api/ai/admin/user-quota?userId=${userId}`),
67
+ ]);
68
+ // Fetch analytics separately (non-blocking)
69
+ fetchWalletAnalytics();
70
+ if (!tokenRes.ok) {
21
71
  throw new Error(t("admin.user_token.error.loading") ||
22
72
  "Échec chargement détails tokens");
23
- const data = await res.json();
73
+ }
74
+ const data = await tokenRes.json();
24
75
  const currentBalance = data.balance || 0;
25
76
  setBalance(currentBalance);
26
77
  const history = data.history || data.data || [];
@@ -29,10 +80,13 @@ export function UserTokenTab({ userId }) {
29
80
  const computed = history.map((entry) => {
30
81
  const balanceAfter = running;
31
82
  running -= entry.amount; // préparer balance pour l'entrée suivante
83
+ // Utiliser les montants USD si disponibles, sinon tokens (pour backward compat)
84
+ const displayAmount = entry.meta?.sell_usd || entry.meta?.sellUsd || entry.amount || 0;
85
+ const displayBalance = walletAnalytics?.walletSellValueUsd || balanceAfter;
32
86
  return {
33
87
  ...entry,
34
- balance_after: balanceAfter,
35
- display_amount: entry.amount,
88
+ balance_after: displayBalance,
89
+ display_amount: displayAmount,
36
90
  display_description: entry.meta?.reason ||
37
91
  entry.description ||
38
92
  entry.type ||
@@ -41,9 +95,14 @@ export function UserTokenTab({ userId }) {
41
95
  };
42
96
  });
43
97
  setLedger(computed);
98
+ // Load quota
99
+ if (quotaRes.ok) {
100
+ const quotaData = await quotaRes.json();
101
+ setQuotaStatus(quotaData.quota);
102
+ }
44
103
  }
45
104
  catch (error) {
46
- console.error(t("admin.user_token.error.loading_tokens") ||
105
+ logger.error(t("admin.user_token.error.loading_tokens") ||
47
106
  "Erreur lors du chargement des tokens:", error);
48
107
  addToast({
49
108
  color: "danger",
@@ -58,9 +117,13 @@ export function UserTokenTab({ userId }) {
58
117
  useEffect(() => {
59
118
  fetchTokenData();
60
119
  }, [userId]);
61
- // Ajuster le solde (crédit ou débit)
120
+ // Charger model usage quand le mois change
121
+ useEffect(() => {
122
+ fetchModelUsage();
123
+ }, [userId, selectedMonth]);
124
+ // Ajuster le solde (crédit seulement pour l'instant)
62
125
  const handleAdjustment = async (type) => {
63
- const amount = parseInt(adjustmentAmount);
126
+ const amount = parseFloat(adjustmentAmount);
64
127
  if (isNaN(amount) || amount <= 0) {
65
128
  addToast({
66
129
  color: "danger",
@@ -76,6 +139,13 @@ export function UserTokenTab({ userId }) {
76
139
  });
77
140
  return;
78
141
  }
142
+ if (type === "debit") {
143
+ addToast({
144
+ color: "danger",
145
+ title: "Le débit de crédits n'est pas encore supporté",
146
+ });
147
+ return;
148
+ }
79
149
  try {
80
150
  setProcessing(true);
81
151
  const response = await fetch(`/api/ai/admin/user-token`, {
@@ -83,8 +153,7 @@ export function UserTokenTab({ userId }) {
83
153
  headers: { "Content-Type": "application/json" },
84
154
  body: JSON.stringify({
85
155
  userId,
86
- amount: type === "credit" ? amount : -amount,
87
- type: "adjust",
156
+ amount: amount, // USD amount
88
157
  reason: adjustmentDescription,
89
158
  }),
90
159
  });
@@ -94,18 +163,18 @@ export function UserTokenTab({ userId }) {
94
163
  }
95
164
  addToast({
96
165
  color: "success",
97
- title: type === "credit"
98
- ? t("admin.user_token.success.credited") || "Tokens crédités"
99
- : t("admin.user_token.success.debited") || "Tokens débités",
166
+ title: `$${amount.toFixed(2)} de crédits IA ajoutés`,
100
167
  });
101
168
  // Reset
102
169
  setAdjustmentAmount("");
103
170
  setAdjustmentDescription("");
104
171
  // Recharger les données
105
172
  await fetchTokenData();
173
+ await fetchWalletAnalytics();
174
+ await fetchModelUsage();
106
175
  }
107
176
  catch (error) {
108
- console.error(t("admin.user_token.error.adjustment") || "Erreur:", error);
177
+ logger.error(t("admin.user_token.error.adjustment") || "Erreur:", error);
109
178
  addToast({
110
179
  color: "danger",
111
180
  title: t("admin.user_token.error.adjustment_error") ||
@@ -116,12 +185,90 @@ export function UserTokenTab({ userId }) {
116
185
  setProcessing(false);
117
186
  }
118
187
  };
188
+ // Update custom quota
189
+ const handleUpdateCustomQuota = async () => {
190
+ const amount = customQuotaAmount.trim() === "" ? null : parseInt(customQuotaAmount);
191
+ if (amount !== null && (isNaN(amount) || amount < 0)) {
192
+ addToast({
193
+ color: "danger",
194
+ title: "Montant invalide",
195
+ });
196
+ return;
197
+ }
198
+ try {
199
+ setProcessing(true);
200
+ const response = await fetch(`/api/ai/admin/user-quota`, {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/json" },
203
+ body: JSON.stringify({
204
+ userId,
205
+ customTokens: amount,
206
+ }),
207
+ });
208
+ if (!response.ok) {
209
+ throw new Error("Échec de la mise à jour du quota");
210
+ }
211
+ const data = await response.json();
212
+ setQuotaStatus(data.quota);
213
+ setEditingQuota(false);
214
+ setCustomQuotaAmount("");
215
+ addToast({
216
+ color: "success",
217
+ title: amount === null
218
+ ? "Quota réinitialisé"
219
+ : "Quota personnalisé mis à jour",
220
+ });
221
+ }
222
+ catch (error) {
223
+ logger.error("Erreur lors de la mise à jour du quota:", error);
224
+ addToast({
225
+ color: "danger",
226
+ title: "Erreur lors de la mise à jour du quota",
227
+ });
228
+ }
229
+ finally {
230
+ setProcessing(false);
231
+ }
232
+ };
119
233
  if (loading) {
120
234
  return (_jsx("div", { className: "flex justify-center items-center min-h-64", children: _jsx(Spinner, { size: "lg" }) }));
121
235
  }
122
- return (_jsxs("div", { className: "w-full space-y-6 mt-4", children: [_jsxs("div", { className: "w-full flex flex-col md:flex-row gap-6", children: [_jsxs(Card, { className: "min-w-72", children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Coins, { size: 20 }), _jsx("h3", { className: "text-lg font-semibold", children: t("admin.user_token.balance.title") || "Solde de tokens" })] }) }), _jsx(CardBody, { className: "flex items-center justify-center", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-4xl font-bold text-primary", children: balance.toLocaleString() }), _jsx("p", { className: "text-sm text-gray-500 mt-1", children: t("admin.user_token.balance.available") ||
123
- "tokens disponibles" })] }) })] }), _jsxs(Card, { className: "flex-1 ", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: t("admin.user_token.adjustment.title") || "Ajuster le solde" }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsx(Input, { type: "number", label: t("admin.user_token.adjustment.amount") || "Montant", placeholder: t("admin.user_token.adjustment.amount_placeholder") || "1000", value: adjustmentAmount, onChange: (e) => setAdjustmentAmount(e.target.value), min: "1", endContent: _jsx("span", { className: "text-sm text-gray-500", children: t("chip.tokens") || "tokens" }) }), _jsx(Input, { label: t("admin.user_token.adjustment.description") || "Description", placeholder: t("admin.user_token.adjustment.description_placeholder") ||
124
- "Raison de l'ajustement...", value: adjustmentDescription, onChange: (e) => setAdjustmentDescription(e.target.value), maxLength: 200 }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { color: "success", startContent: _jsx(Plus, { size: 16 }), onPress: () => handleAdjustment("credit"), isLoading: processing, isDisabled: !adjustmentAmount || !adjustmentDescription.trim(), children: t("admin.user_token.adjustment.credit_button") || "Créditer" }), _jsx(Button, { color: "danger", variant: "bordered", startContent: _jsx(Minus, { size: 16 }), onPress: () => handleAdjustment("debit"), isLoading: processing, isDisabled: !adjustmentAmount || !adjustmentDescription.trim(), children: t("admin.user_token.adjustment.debit_button") || "Débiter" })] })] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(History, { size: 20 }), _jsx("h3", { className: "text-lg font-semibold", children: t("admin.user_token.history.title") ||
125
- "Historique des transactions" })] }) }), _jsx(CardBody, { children: ledger.length === 0 ? (_jsx("p", { className: "text-center text-gray-500 py-4", children: t("admin.user_token.history.no_transactions") ||
126
- "Aucune transaction" })) : (_jsx("div", { className: "space-y-2", children: ledger.map((entry) => (_jsxs("div", { className: "flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg", children: [_jsxs("div", { className: "flex-1", children: [_jsx("p", { className: "text-sm font-medium", children: entry.display_description }), _jsx("p", { className: "text-xs text-gray-500", children: new Date(entry.created_at).toLocaleString() })] }), _jsxs("div", { className: "text-right", children: [_jsxs(Chip, { size: "sm", color: entry.display_amount > 0 ? "success" : "danger", variant: "flat", children: [entry.display_amount > 0 ? "+" : "", entry.display_amount.toLocaleString()] }), _jsxs("p", { className: "text-xs text-gray-500 mt-1", children: [t("admin.user_token.history.balance_label") || "Solde", ":", " ", entry.balance_after.toLocaleString()] })] })] }, entry.id))) })) })] })] }));
236
+ return (_jsxs("div", { className: "w-full space-y-6 mt-4", children: [walletAnalytics && (_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-4 gap-4 mb-6", children: [_jsx(Card, { className: "border-2 border-success", children: _jsx(CardBody, { children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(DollarSign, { size: 42, className: "text-success" }), _jsxs("div", { children: [_jsx("p", { className: "text-sm text-default-500", children: "CA Total (USD)" }), _jsxs("p", { className: "text-2xl font-bold text-success", children: ["$", walletAnalytics.totalSellUsd.toFixed(2)] })] })] }) }) }), _jsx(Card, { className: "border-2 border-warning", children: _jsx(CardBody, { children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(TrendingDown, { size: 42, className: "text-warning" }), _jsxs("div", { children: [_jsx("p", { className: "text-sm text-default-500", children: "Co\u00FBt Fournisseur (USD)" }), _jsxs("p", { className: "text-2xl font-bold text-warning", children: ["$", walletAnalytics.totalProviderCostUsd.toFixed(2)] })] })] }) }) }), _jsx(Card, { className: "border-2 border-secondary", children: _jsx(CardBody, { children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(TrendingUp, { size: 42, className: "text-secondary" }), _jsxs("div", { children: [_jsx("p", { className: "text-sm text-default-500", children: "Marge Totale (USD)" }), _jsxs("p", { className: "text-2xl font-bold text-secondary", children: ["$", walletAnalytics.totalMarginUsd.toFixed(2)] }), _jsxs("p", { className: "text-xs text-default-400", children: ["\u2248 ", walletAnalytics.marginPercent.toFixed(1), "% moyen"] })] })] }) }) }), _jsx(Card, { className: "border-2 border-primary", children: _jsx(CardBody, { children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Wallet, { size: 42, className: "text-primary" }), _jsxs("div", { children: [_jsx("p", { className: "text-sm text-default-500", children: "Wallet Budget" }), _jsxs("p", { className: "text-2xl font-bold text-primary", children: ["$", walletAnalytics.walletProviderBudgetUsd.toFixed(4)] }), _jsxs("p", { className: "text-xs text-default-400", children: ["Tokens:", " ", walletAnalytics.tokensAdded - walletAnalytics.tokensUsed] })] })] }) }) })] })), quotaStatus && quotaStatus.hasQuota && (_jsxs(Card, { className: "border-2 border-primary", children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center justify-between w-full", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Coins, { size: 20, className: "text-primary" }), _jsxs("h3", { className: "text-lg font-semibold", children: ["Quota mensuel inclus (", quotaStatus.plan.toUpperCase(), ")"] })] }), !editingQuota && (_jsx(Button, { size: "sm", color: "primary", variant: "flat", onPress: () => setEditingQuota(true), children: "Personnaliser" }))] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4", children: [_jsxs("div", { className: "text-center p-4 bg-primary-50 dark:bg-primary-900/20 rounded-lg", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-1", children: "Quota effectif" }), _jsx("p", { className: "text-3xl font-bold text-primary", children: quotaStatus.effectiveQuota.toLocaleString() })] }), _jsxs("div", { className: "text-center p-4 bg-warning-50 dark:bg-warning-900/20 rounded-lg", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-1", children: "Utilis\u00E9s" }), _jsx("p", { className: "text-3xl font-bold text-warning", children: quotaStatus.usedQuota.toLocaleString() })] }), _jsxs("div", { className: "text-center p-4 bg-success-50 dark:bg-success-900/20 rounded-lg", children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-1", children: "Restants" }), _jsx("p", { className: "text-3xl font-bold text-success", children: quotaStatus.remainingQuota.toLocaleString() })] })] }), quotaStatus.periodStart && quotaStatus.periodEnd && (_jsxs("div", { className: "text-center text-sm text-gray-500", children: [_jsxs("p", { children: ["P\u00E9riode: ", formatDateFr(quotaStatus.periodStart), " - ", formatDateFr(quotaStatus.periodEnd)] }), _jsx("p", { className: `mt-1 ${quotaStatus.isActive ? "text-success" : "text-danger"}`, children: quotaStatus.isActive ? "✓ Période active" : "✗ Hors période" })] })), editingQuota && (_jsxs("div", { className: "space-y-4 pt-4 border-t", children: [_jsx("h4", { className: "font-semibold", children: "Personnaliser le quota mensuel" }), _jsx(Input, { type: "number", label: "Quota personnalis\u00E9", placeholder: "Laisser vide pour r\u00E9initialiser au d\u00E9faut du plan", value: customQuotaAmount, onChange: (e) => setCustomQuotaAmount(e.target.value), min: "0", endContent: _jsx("span", { className: "text-sm text-gray-500", children: "tokens/mois" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { color: "primary", onPress: handleUpdateCustomQuota, isLoading: processing, children: "Appliquer" }), _jsx(Button, { variant: "flat", onPress: () => {
237
+ setEditingQuota(false);
238
+ setCustomQuotaAmount("");
239
+ }, children: "Annuler" }), _jsx(Button, { color: "warning", variant: "flat", onPress: () => {
240
+ setCustomQuotaAmount("");
241
+ handleUpdateCustomQuota();
242
+ }, isLoading: processing, children: "R\u00E9initialiser au d\u00E9faut" })] }), _jsx("p", { className: "text-xs text-gray-500", children: "Note: La modification s'appliquera imm\u00E9diatement. Le quota sera r\u00E9initialis\u00E9 au prochain renouvellement de p\u00E9riode." })] }))] })] })), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(UsageIcon, { size: 20 }), _jsx("h3", { className: "text-lg font-semibold", children: "Utilisation par mod\u00E8le" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Calendar, { size: 16, className: "text-gray-500" }), _jsx(Select, { size: "sm", label: "", placeholder: "P\u00E9riode", selectedKeys: [selectedMonth], onSelectionChange: (keys) => {
243
+ const key = Array.from(keys)[0];
244
+ setSelectedMonth(key);
245
+ }, className: "min-w-[180px]", children: [
246
+ { value: "all", label: "Tous les mois" },
247
+ ...(() => {
248
+ const months = [];
249
+ const now = new Date();
250
+ for (let i = 0; i < 12; i++) {
251
+ const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
252
+ const value = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
253
+ const label = date.toLocaleDateString("fr-FR", {
254
+ year: "numeric",
255
+ month: "long",
256
+ });
257
+ months.push({ value, label });
258
+ }
259
+ return months;
260
+ })(),
261
+ ].map((month) => (_jsx(SelectItem, { children: month.label }, month.value))) })] })] }), _jsx(CardBody, { children: loadingUsage ? (_jsx("div", { className: "flex justify-center items-center py-12", children: _jsx(Spinner, { size: "lg" }) })) : modelUsage.length === 0 ? (_jsx("div", { className: "text-center py-12 text-gray-500", children: "Aucune utilisation enregistr\u00E9e" })) : (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800", children: _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: "Total d\u00E9pens\u00E9 (p\u00E9riode)" }), _jsxs("p", { className: "text-2xl font-bold text-blue-600 dark:text-blue-400", children: ["$", modelUsage
262
+ .reduce((sum, item) => sum + item.total_sell_usd, 0)
263
+ .toFixed(2)] })] }), _jsxs("div", { children: [_jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: "Cr\u00E9dit restant" }), _jsxs("p", { className: "text-2xl font-bold text-green-600 dark:text-green-400", children: ["$", (walletAnalytics?.walletSellValueUsd || 0).toFixed(2)] })] })] }) }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-gray-200 dark:border-gray-700", children: [_jsx("th", { className: "text-left py-3 px-2 text-sm font-semibold text-gray-700 dark:text-gray-300", children: "Mod\u00E8le" }), _jsx("th", { className: "text-left py-3 px-2 text-sm font-semibold text-gray-700 dark:text-gray-300", children: "Provider" }), _jsx("th", { className: "text-right py-3 px-2 text-sm font-semibold text-gray-700 dark:text-gray-300", children: "Co\u00FBt total" }), _jsx("th", { className: "text-right py-3 px-2 text-sm font-semibold text-gray-700 dark:text-gray-300", children: "Appels" }), _jsx("th", { className: "text-right py-3 px-2 text-sm font-semibold text-gray-700 dark:text-gray-300", children: "Co\u00FBt/appel" })] }) }), _jsx("tbody", { children: modelUsage.map((item, index) => {
264
+ const totalSpent = modelUsage.reduce((sum, i) => sum + i.total_sell_usd, 0);
265
+ const percentOfTotal = totalSpent > 0
266
+ ? (item.total_sell_usd / totalSpent) * 100
267
+ : 0;
268
+ return (_jsxs("tr", { className: "border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors", children: [_jsx("td", { className: "py-3 px-2", children: _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium text-sm", children: item.model }), item.endpoint && (_jsx("span", { className: "text-xs text-gray-500", children: item.endpoint }))] }) }), _jsx("td", { className: "py-3 px-2", children: _jsx("span", { className: "text-sm text-gray-600 dark:text-gray-400 capitalize", children: item.provider || "—" }) }), _jsx("td", { className: "py-3 px-2 text-right", children: _jsxs("div", { className: "flex flex-col items-end", children: [_jsxs("span", { className: "font-semibold text-sm", children: ["$", item.total_sell_usd.toFixed(2)] }), _jsxs("span", { className: "text-xs text-gray-500", children: [percentOfTotal.toFixed(1), "%"] })] }) }), _jsx("td", { className: "py-3 px-2 text-right", children: _jsx("span", { className: "text-sm font-medium", children: item.call_count.toLocaleString() }) }), _jsx("td", { className: "py-3 px-2 text-right", children: _jsxs("span", { className: "text-sm text-gray-600 dark:text-gray-400", children: ["$", item.avg_cost_per_call.toFixed(4)] }) })] }, `${item.model}-${index}`));
269
+ }) })] }) }), _jsx("div", { className: "text-xs text-gray-500 pt-2", children: _jsx("p", { children: "\uD83D\uDCA1 Les co\u00FBts affich\u00E9s sont les montants d\u00E9bit\u00E9s du cr\u00E9dit IA (incluant marge et infrastructure)." }) })] })) })] }), _jsxs("div", { className: "w-full flex flex-col md:flex-row gap-6", children: [_jsxs(Card, { className: "min-w-72", children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(DollarSign, { size: 20 }), _jsx("h3", { className: "text-lg font-semibold", children: t("admin.user_token.balance.title") || "Crédits IA" })] }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-xs text-gray-500 uppercase tracking-wide mb-1", children: "Valeur offerte (100%)" }), _jsxs("p", { className: "text-3xl font-bold text-primary", children: ["$", walletAnalytics?.walletSellValueUsd?.toFixed(2) ||
270
+ balance.toFixed(2)] })] }), _jsx("div", { className: "border-t pt-4", children: _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-xs text-gray-500 uppercase tracking-wide mb-1", children: "Budget r\u00E9el de consommation (40%)" }), _jsxs("p", { className: "text-3xl font-bold text-success", children: ["$", walletAnalytics?.walletProviderBudgetUsd?.toFixed(2) ||
271
+ "0.00"] }), _jsx("p", { className: "text-xs text-gray-500 mt-1", children: "\uD83D\uDCA1 C'est ce montant qui sera d\u00E9bit\u00E9 des providers" })] }) })] })] }), _jsxs(Card, { className: "flex-1 ", children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: t("admin.user_token.adjustment.title") || "Ajuster le solde" }) }), _jsxs(CardBody, { className: "space-y-4", children: [_jsxs("div", { className: "bg-blue-50 dark:bg-blue-950/20 p-3 rounded-lg border border-blue-200 dark:border-blue-800", children: [_jsxs("p", { className: "text-xs text-gray-700 dark:text-gray-300", children: ["\uD83D\uDCA1 ", _jsx("strong", { children: "Important :" }), " Quand vous offrez des cr\u00E9dits, l'utilisateur re\u00E7oit :"] }), _jsxs("ul", { className: "text-xs text-gray-600 dark:text-gray-400 mt-2 space-y-1 pl-4", children: [_jsxs("li", { children: ["\u2022 ", _jsx("strong", { children: "100%" }), " affich\u00E9 comme \"valeur offerte\""] }), _jsxs("li", { children: ["\u2022 ", _jsx("strong", { children: "40%" }), " comme budget r\u00E9el de consommation"] }), _jsxs("li", { children: ["\u2022 ", _jsx("strong", { children: "60%" }), " sera la marge lors de l'utilisation"] })] }), _jsx("p", { className: "text-xs text-gray-600 dark:text-gray-400 mt-2", children: "Exemple : Offrir $5 = $5 affich\u00E9s, mais $2 de budget r\u00E9el" })] }), _jsx(Input, { type: "number", label: t("admin.user_token.adjustment.amount") || "Montant (USD)", placeholder: t("admin.user_token.adjustment.amount_placeholder") || "5.00", value: adjustmentAmount, onChange: (e) => setAdjustmentAmount(e.target.value), min: "0.01", max: "1000", step: "0.01", endContent: _jsx("span", { className: "text-sm text-gray-500", children: t("chip.usd") || "$" }) }), _jsx(Input, { label: t("admin.user_token.adjustment.description") || "Description", placeholder: t("admin.user_token.adjustment.description_placeholder") ||
272
+ "Raison de l'ajustement...", value: adjustmentDescription, onChange: (e) => setAdjustmentDescription(e.target.value), maxLength: 200 }), _jsx("div", { className: "flex justify-end gap-2", children: _jsx(Button, { color: "success", startContent: _jsx(Plus, { size: 16 }), onPress: () => handleAdjustment("credit"), isLoading: processing, isDisabled: !adjustmentAmount || !adjustmentDescription.trim(), children: t("admin.user_token.adjustment.credit_button") ||
273
+ "Offrir des crédits" }) })] })] })] })] }));
127
274
  }
@@ -0,0 +1,2 @@
1
+ export declare function AuthDashboardAi(): import("react/jsx-runtime").JSX.Element | null;
2
+ //# sourceMappingURL=AuthDashboardAi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthDashboardAi.d.ts","sourceRoot":"","sources":["../../../src/components/auth/AuthDashboardAi.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,eAAe,mDAkI9B"}
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ import { Card, CardBody, Progress, Spinner } from "@lastbrain/ui";
5
+ import { TrendingDown, Wallet, Gauge } from "lucide-react";
6
+ import { useAuth, useModuleTranslation, logger } from "@lastbrain/core";
7
+ export function AuthDashboardAi() {
8
+ const t = useModuleTranslation("ai");
9
+ const { user } = useAuth();
10
+ const [balance, setBalance] = useState(null);
11
+ const [loading, setLoading] = useState(true);
12
+ useEffect(() => {
13
+ if (user) {
14
+ fetchBalance();
15
+ }
16
+ }, [user]);
17
+ const fetchBalance = async () => {
18
+ try {
19
+ setLoading(true);
20
+ const response = await fetch("/api/ai/auth/wallet");
21
+ if (response.ok) {
22
+ const data = await response.json();
23
+ setBalance(data);
24
+ }
25
+ }
26
+ catch (error) {
27
+ logger.error("Failed to fetch wallet balance", error);
28
+ }
29
+ finally {
30
+ setLoading(false);
31
+ }
32
+ };
33
+ const formatUsd = (amount) => {
34
+ if (amount === undefined || amount === null)
35
+ return "$0.00";
36
+ return `$${amount.toFixed(2)}`;
37
+ };
38
+ if (loading) {
39
+ return (_jsx("div", { className: "flex justify-center py-8", children: _jsx(Spinner, {}) }));
40
+ }
41
+ if (!balance)
42
+ return null;
43
+ const usagePercentage = balance.percentage || 0;
44
+ const getUsageColor = (percentage) => {
45
+ if (percentage > 90)
46
+ return "danger";
47
+ if (percentage >= 80)
48
+ return "warning";
49
+ return "success";
50
+ };
51
+ const usageColor = getUsageColor(usagePercentage);
52
+ return (_jsxs("div", { className: "max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4", children: [_jsx(Card, { className: "border-2 border-primary", children: _jsxs(CardBody, { className: "p-6", children: [_jsxs("div", { className: "flex items-center gap-3 mb-4", children: [_jsx("div", { className: "p-2 rounded-lg bg-primary/10", children: _jsx(Wallet, { size: 24, className: "text-primary" }) }), _jsx("div", { className: "flex-1", children: _jsx("p", { className: "text-sm text-default-800", children: t("wallet.balance.current") || "Crédits disponibles" }) })] }), _jsx("p", { className: "text-3xl font-bold text-primary", children: formatUsd(balance.walletSellValueUsd) }), _jsx("p", { className: "text-xs text-default-700 mt-1", children: t("wallet.balance.available") || "Solde disponible" })] }) }), _jsx(Card, { className: "border-2 border-danger", children: _jsxs(CardBody, { className: "p-6", children: [_jsxs("div", { className: "flex items-center gap-3 mb-4", children: [_jsx("div", { className: "p-2 rounded-lg bg-danger/10", children: _jsx(TrendingDown, { size: 24, className: "text-danger" }) }), _jsx("div", { className: "flex-1", children: _jsx("p", { className: "text-sm text-default-800", children: t("wallet.balance.total_used") || "Total dépensé" }) })] }), _jsx("p", { className: "text-3xl font-bold text-danger", children: formatUsd(balance.totalUsed) }), _jsx("p", { className: "text-xs text-default-700 mt-1", children: t("wallet.balance.consumed") || "Consommé" })] }) }), _jsx(Card, { className: `border-2 border-${usageColor}`, children: _jsxs(CardBody, { className: "p-6", children: [_jsxs("div", { className: "flex items-center gap-3 mb-4", children: [_jsx("div", { className: `p-2 rounded-lg bg-${usageColor}/10`, children: _jsx(Gauge, { size: 24, className: `text-${usageColor}` }) }), _jsx("div", { className: "flex-1", children: _jsx("p", { className: "text-sm text-default-800", children: t("wallet.usage.percentage") || "Utilisation" }) })] }), _jsxs("p", { className: `text-3xl font-bold text-${usageColor}`, children: [usagePercentage.toFixed(1), "%"] }), _jsx("div", { className: "mt-3", children: _jsx(Progress, { value: usagePercentage, color: usageColor, className: "h-2" }) }), _jsxs("p", { className: "text-xs text-default-700 mt-2", children: [formatUsd(balance.totalUsed), " / ", formatUsd(balance.totalAdded)] })] }) })] }));
53
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * REFACTORING BILLING IA - GUIDE D'IMPLÉMENTATION
3
+ * =================================================
4
+ *
5
+ * Ce fichier documente les changements nécessaires pour centraliser
6
+ * la logique de billing IA et passer à un modèle "Crédits IA" (USD).
7
+ *
8
+ * ✅ DÉJÀ FAIT :
9
+ * ==============
10
+ * 1. `packages/module-ai/src/server/billing.ts`
11
+ * - Ajout documentation règles économiques
12
+ * - Création fonction `computeAndDebitAIUsage()` centralisée
13
+ * - Configuration minimums par action (ACTION_MIN_SELL_USD)
14
+ * - Logging anonyme de chaque débit
15
+ *
16
+ * 2. `packages/module-ai/src/api/public/pricing-summary.ts`
17
+ * - Endpoint GET pour afficher estimations selon nouveau modèle
18
+ *
19
+ * ⚠️ À FAIRE :
20
+ * ============
21
+ *
22
+ * A) Modifier tous les endpoints pour utiliser `computeAndDebitAIUsage()`
23
+ * -------------------------------------------------------------------
24
+ *
25
+ * Fichiers à modifier :
26
+ * - `packages/module-ai/src/api/auth/generate-text.ts`
27
+ * - `packages/module-ai/src/api/auth/generate-image.ts`
28
+ * - `packages/module-recipes-pro/src/api/auth/recipes-bilingual.ts`
29
+ * - Tous les endpoints de suggestions UI (autocomplete, etc.)
30
+ *
31
+ * Pattern de remplacement :
32
+ *
33
+ * ```typescript
34
+ * // AVANT (ancien système avec recordConsumption)
35
+ * const consumeResult = await recordConsumption(user.id, usage, pricing, metadata);
36
+ *
37
+ * // APRÈS (nouveau système centralisé)
38
+ * import { computeAndDebitAIUsage, computeProviderCostUsd } from "@lastbrain/module-ai/server/billing";
39
+ * import { getWalletState } from "@lastbrain/module-ai/server/billing";
40
+ *
41
+ * // 1. Calculer le coût provider
42
+ * const providerCostUsd = computeProviderCostUsd(usage, pricing);
43
+ *
44
+ * // 2. Récupérer wallet
45
+ * const wallet = await getWalletState(user.id);
46
+ *
47
+ * // 3. Compute & debit avec actionType approprié
48
+ * const billingResult = computeAndDebitAIUsage({
49
+ * userId: user.id,
50
+ * actionType: "generate-recipe-text", // ou "generate-image", "generate-text", etc.
51
+ * providerCostUsd,
52
+ * wallet,
53
+ * metadata: {
54
+ * model: modelName,
55
+ * provider,
56
+ * endpoint: "generate-text",
57
+ * prompt: prompt.substring(0, 32),
58
+ * generatedText: generatedText.substring(0, 500),
59
+ * },
60
+ * });
61
+ *
62
+ * if (!billingResult.success) {
63
+ * return NextResponse.json(
64
+ * { error: "Crédits insuffisants" },
65
+ * { status: 402 } // Payment Required
66
+ * );
67
+ * }
68
+ *
69
+ * // 4. Enregistrer dans le ledger via RPC
70
+ * const { error: ledgerError } = await supabase.rpc("record_token_consumption", {
71
+ * p_user_id: user.id,
72
+ * p_debit_tokens: billingResult.tokensDebited || 0,
73
+ * p_provider_cost_usd: providerCostUsd,
74
+ * p_sell_usd: billingResult.sellCostUsd,
75
+ * p_margin_usd: billingResult.marginUsd,
76
+ * p_meta: metadata,
77
+ * });
78
+ * ```
79
+ *
80
+ *
81
+ * B) Refactorer PurchaseTokensPage.tsx
82
+ * ------------------------------------
83
+ *
84
+ * Modifications UI :
85
+ *
86
+ * 1. Remplacer tous "Tokens" par "Crédits IA"
87
+ *
88
+ * 2. Afficher pour chaque pack :
89
+ * ```tsx
90
+ * <div className="mb-6">
91
+ * <p className="text-4xl font-bold text-primary mb-1">
92
+ * {formatPrice(pack.price_cents, pack.currency)}
93
+ * </p>
94
+ * <p className="text-sm text-default-700">Crédit IA</p>
95
+ * </div>
96
+ *
97
+ * <div className="mb-4 p-3 bg-blue-50 rounded-lg">
98
+ * <p className="text-xs text-gray-600 mb-1">
99
+ * Budget fournisseur inclus
100
+ * </p>
101
+ * <p className="text-sm font-semibold text-blue-600">
102
+ * ${((pack.price_cents / 100) / 2.5).toFixed(2)}
103
+ * </p>
104
+ * <p className="text-xs text-gray-500 mt-1">
105
+ * (marge et infrastructure incluses)
106
+ * </p>
107
+ * </div>
108
+ *
109
+ * <div className="mb-6 text-xs text-gray-600">
110
+ * <p>· Recette complète : à partir de 0.20$</p>
111
+ * <p>· Image : à partir de 0.05$</p>
112
+ * <p className="mt-2 font-semibold text-green-600">
113
+ * ≈ {Math.floor((pack.price_cents / 100) / 0.20)} recettes max
114
+ * </p>
115
+ * </div>
116
+ * ```
117
+ *
118
+ * 3. Supprimer/simplifier la section "Models Pricing"
119
+ * - Ne plus afficher prix token/million tokens
120
+ * - Afficher coûts estimés par action en USD
121
+ * - Utiliser endpoint `/api/ai/public/pricing-summary`
122
+ *
123
+ *
124
+ * C) Refactorer UsageAndTokensPage.tsx
125
+ * ------------------------------------
126
+ *
127
+ * Modifications UI :
128
+ *
129
+ * 1. Remplacer TokenKpiGrid par une barre de progression simple :
130
+ * ```tsx
131
+ * <Card>
132
+ * <CardHeader>
133
+ * <h3>Crédits IA restants</h3>
134
+ * </CardHeader>
135
+ * <CardBody>
136
+ * <div className="mb-2">
137
+ * <div className="flex justify-between mb-1">
138
+ * <span className="text-2xl font-bold">
139
+ * {wallet ? `$${wallet.walletSellValueUsd.toFixed(2)}` : "—"}
140
+ * </span>
141
+ * <span className={`text-sm ${getColorClass(percentRemaining)}`}>
142
+ * {percentRemaining.toFixed(1)}%
143
+ * </span>
144
+ * </div>
145
+ * <div className="w-full bg-gray-200 rounded-full h-3">
146
+ * <div
147
+ * className={`h-3 rounded-full ${getProgressBarColor(percentRemaining)}`}
148
+ * style={{ width: `${percentRemaining}%` }}
149
+ * />
150
+ * </div>
151
+ * </div>
152
+ *
153
+ * {percentRemaining < 25 && (
154
+ * <Alert color="warning" className="mt-4">
155
+ * <AlertCircle size={20} />
156
+ * <p>Vos crédits sont faibles. Rechargez maintenant.</p>
157
+ * <PurchaseButton />
158
+ * </Alert>
159
+ * )}
160
+ * </CardBody>
161
+ * </Card>
162
+ * ```
163
+ *
164
+ * 2. Modifier UsageByModelBarChart pour afficher :
165
+ * - Modèle
166
+ * - Nombre d'appels
167
+ * - Total facturé (sellUsd)
168
+ * - Ne PAS afficher tokens
169
+ *
170
+ * 3. Seuils couleurs :
171
+ * ```typescript
172
+ * function getColorClass(percent: number): string {
173
+ * if (percent > 50) return "text-green-600";
174
+ * if (percent > 25) return "text-orange-500";
175
+ * return "text-red-600";
176
+ * }
177
+ *
178
+ * function getProgressBarColor(percent: number): string {
179
+ * if (percent > 50) return "bg-green-500";
180
+ * if (percent > 25) return "bg-orange-500";
181
+ * return "bg-red-500";
182
+ * }
183
+ * ```
184
+ *
185
+ *
186
+ * D) Endpoint wallet
187
+ * ------------------
188
+ *
189
+ * Créer/modifier `/api/ai/auth/wallet` pour retourner :
190
+ * ```typescript
191
+ * {
192
+ * walletSellValueUsd: number, // Crédit restant
193
+ * walletProviderBudgetUsd: number, // Budget provider restant
194
+ * percentRemaining: number, // % restant
195
+ * tokenBalance: number, // Pour compatibilité (ne pas afficher en UI)
196
+ * }
197
+ * ```
198
+ *
199
+ *
200
+ * E) Tests à ajouter
201
+ * ------------------
202
+ *
203
+ * Créer `packages/module-ai/src/server/__tests__/billing.test.ts` :
204
+ *
205
+ * ```typescript
206
+ * import { computeAndDebitAIUsage } from "../billing";
207
+ *
208
+ * describe("Billing centralisé", () => {
209
+ * test("Pack 5$ : recette doit débiter min 0.20$", () => {
210
+ * const result = computeAndDebitAIUsage({
211
+ * userId: "test-user",
212
+ * actionType: "generate-recipe-text",
213
+ * providerCostUsd: 0.001, // Très bas
214
+ * wallet: {
215
+ * tokenBalance: 100000,
216
+ * walletProviderBudgetUsd: 2.0,
217
+ * walletSellValueUsd: 5.0,
218
+ * },
219
+ * metadata: { model: "gpt-4o-mini" },
220
+ * });
221
+ *
222
+ * expect(result.success).toBe(true);
223
+ * expect(result.sellCostUsd).toBeGreaterThanOrEqual(0.20);
224
+ * });
225
+ *
226
+ * test("Budget provider insuffisant => refus", () => {
227
+ * const result = computeAndDebitAIUsage({
228
+ * userId: "test-user",
229
+ * actionType: "generate-recipe-text",
230
+ * providerCostUsd: 0.15,
231
+ * wallet: {
232
+ * tokenBalance: 100000,
233
+ * walletProviderBudgetUsd: 0.10, // Insuffisant
234
+ * walletSellValueUsd: 5.0,
235
+ * },
236
+ * metadata: { model: "gpt-4o-mini" },
237
+ * });
238
+ *
239
+ * expect(result.success).toBe(false);
240
+ * expect(result.error).toBe("insufficient_credits");
241
+ * });
242
+ * });
243
+ * ```
244
+ *
245
+ *
246
+ * F) Migration base de données
247
+ * ----------------------------
248
+ *
249
+ * Aucune migration nécessaire - le système est rétrocompatible.
250
+ * Les anciennes données continuent de fonctionner avec les nouveaux calculs.
251
+ *
252
+ *
253
+ * G) Documentation utilisateur
254
+ * ----------------------------
255
+ *
256
+ * Mettre à jour la doc pour expliquer :
257
+ * - Le système de Crédits IA (USD) remplace les tokens
258
+ * - Coûts minimum par action garantissent qualité service
259
+ * - Budget provider séparé évite les pertes
260
+ * - Marge de 60% permet maintenance infrastructure
261
+ *
262
+ *
263
+ * VALIDATION FINALE :
264
+ * ==================
265
+ *
266
+ * Avant mise en prod, vérifier :
267
+ *
268
+ * ✅ Tous les endpoints appellent computeAndDebitAIUsage()
269
+ * ✅ UI affiche "Crédits IA" au lieu de "tokens"
270
+ * ✅ Minimums appliqués : recette ≥ 0.20$, image ≥ 0.05$
271
+ * ✅ walletProviderBudgetUsd ne passe jamais sous 0
272
+ * ✅ Logs anonymes apparaissent à chaque débit
273
+ * ✅ Tests passent
274
+ * ✅ Pas de régression pour utilisateurs existants
275
+ */
276
+ export {};
277
+ //# sourceMappingURL=REFACTORING_BILLING_GUIDE.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"REFACTORING_BILLING_GUIDE.d.ts","sourceRoot":"","sources":["../../src/docs/REFACTORING_BILLING_GUIDE.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkRG;AAGH,OAAO,EAAE,CAAC"}