@hed-hog/core 0.0.349 → 0.0.351

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 (350) hide show
  1. package/dist/access-log/access-log.controller.d.ts +16 -0
  2. package/dist/access-log/access-log.controller.d.ts.map +1 -0
  3. package/dist/access-log/access-log.controller.js +44 -0
  4. package/dist/access-log/access-log.controller.js.map +1 -0
  5. package/dist/access-log/access-log.interceptor.d.ts +9 -0
  6. package/dist/access-log/access-log.interceptor.d.ts.map +1 -0
  7. package/dist/access-log/access-log.interceptor.js +46 -0
  8. package/dist/access-log/access-log.interceptor.js.map +1 -0
  9. package/dist/access-log/access-log.module.d.ts +3 -0
  10. package/dist/access-log/access-log.module.d.ts.map +1 -0
  11. package/dist/access-log/access-log.module.js +29 -0
  12. package/dist/access-log/access-log.module.js.map +1 -0
  13. package/dist/access-log/access-log.service.d.ts +47 -0
  14. package/dist/access-log/access-log.service.d.ts.map +1 -0
  15. package/dist/access-log/access-log.service.js +113 -0
  16. package/dist/access-log/access-log.service.js.map +1 -0
  17. package/dist/access-log/dto/query-access-log.dto.d.ts +8 -0
  18. package/dist/access-log/dto/query-access-log.dto.d.ts.map +1 -0
  19. package/dist/access-log/dto/query-access-log.dto.js +42 -0
  20. package/dist/access-log/dto/query-access-log.dto.js.map +1 -0
  21. package/dist/auth/auth.controller.d.ts +12 -12
  22. package/dist/auth/auth.service.d.ts +27 -27
  23. package/dist/auth/auth.service.d.ts.map +1 -1
  24. package/dist/auth/auth.service.js +0 -4
  25. package/dist/auth/auth.service.js.map +1 -1
  26. package/dist/challenge/challenge.service.d.ts +2 -2
  27. package/dist/challenge/challenge.service.d.ts.map +1 -1
  28. package/dist/challenge/challenge.service.js +0 -1
  29. package/dist/challenge/challenge.service.js.map +1 -1
  30. package/dist/core/system.module.d.ts.map +1 -1
  31. package/dist/core/system.module.js +1 -0
  32. package/dist/core/system.module.js.map +1 -1
  33. package/dist/core.module.d.ts.map +1 -1
  34. package/dist/core.module.js +15 -0
  35. package/dist/core.module.js.map +1 -1
  36. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  37. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  38. package/dist/dashboard/dashboard.module.d.ts.map +1 -1
  39. package/dist/dashboard/dashboard.module.js +4 -1
  40. package/dist/dashboard/dashboard.module.js.map +1 -1
  41. package/dist/file/file.controller.js +0 -1
  42. package/dist/file/file.controller.js.map +1 -1
  43. package/dist/file/file.service.d.ts +65 -0
  44. package/dist/file/file.service.d.ts.map +1 -1
  45. package/dist/file/file.service.js +151 -3
  46. package/dist/file/file.service.js.map +1 -1
  47. package/dist/file/provider/abstract.provider.d.ts +8 -0
  48. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  49. package/dist/file/provider/abstract.provider.js.map +1 -1
  50. package/dist/file/provider/azure.provider.d.ts +4 -0
  51. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  52. package/dist/file/provider/azure.provider.js +14 -0
  53. package/dist/file/provider/azure.provider.js.map +1 -1
  54. package/dist/file/provider/gcs.provider.d.ts +4 -0
  55. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  56. package/dist/file/provider/gcs.provider.js +14 -0
  57. package/dist/file/provider/gcs.provider.js.map +1 -1
  58. package/dist/file/provider/local.provider.d.ts +4 -0
  59. package/dist/file/provider/local.provider.d.ts.map +1 -1
  60. package/dist/file/provider/local.provider.js +19 -0
  61. package/dist/file/provider/local.provider.js.map +1 -1
  62. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  63. package/dist/file/provider/provider.factory.js +3 -0
  64. package/dist/file/provider/provider.factory.js.map +1 -1
  65. package/dist/file/provider/s3.provider.d.ts +4 -0
  66. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  67. package/dist/file/provider/s3.provider.js +17 -0
  68. package/dist/file/provider/s3.provider.js.map +1 -1
  69. package/dist/index.d.ts +3 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +4 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/mcp/guards/mcp-auth.guard.d.ts +6 -1
  74. package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -1
  75. package/dist/mcp/guards/mcp-auth.guard.js +29 -2
  76. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
  77. package/dist/mcp/mcp-api-key.controller.d.ts +32 -0
  78. package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -0
  79. package/dist/mcp/mcp-api-key.controller.js +64 -0
  80. package/dist/mcp/mcp-api-key.controller.js.map +1 -0
  81. package/dist/mcp/mcp-api-key.service.d.ts +27 -0
  82. package/dist/mcp/mcp-api-key.service.d.ts.map +1 -0
  83. package/dist/mcp/mcp-api-key.service.js +74 -0
  84. package/dist/mcp/mcp-api-key.service.js.map +1 -0
  85. package/dist/mcp/mcp-audit.service.d.ts +3 -1
  86. package/dist/mcp/mcp-audit.service.d.ts.map +1 -1
  87. package/dist/mcp/mcp-audit.service.js +14 -2
  88. package/dist/mcp/mcp-audit.service.js.map +1 -1
  89. package/dist/mcp/mcp-registry.service.d.ts +1 -2
  90. package/dist/mcp/mcp-registry.service.d.ts.map +1 -1
  91. package/dist/mcp/mcp-registry.service.js +6 -2
  92. package/dist/mcp/mcp-registry.service.js.map +1 -1
  93. package/dist/mcp/mcp.controller.d.ts +3 -1
  94. package/dist/mcp/mcp.controller.d.ts.map +1 -1
  95. package/dist/mcp/mcp.controller.js +10 -2
  96. package/dist/mcp/mcp.controller.js.map +1 -1
  97. package/dist/mcp/mcp.module.d.ts.map +1 -1
  98. package/dist/mcp/mcp.module.js +80 -1
  99. package/dist/mcp/mcp.module.js.map +1 -1
  100. package/dist/mcp/tools/core-ai.mcp-tools.d.ts +45 -0
  101. package/dist/mcp/tools/core-ai.mcp-tools.d.ts.map +1 -0
  102. package/dist/mcp/tools/core-ai.mcp-tools.js +214 -0
  103. package/dist/mcp/tools/core-ai.mcp-tools.js.map +1 -0
  104. package/dist/mcp/tools/core-dashboard-items.mcp-tools.d.ts +21 -0
  105. package/dist/mcp/tools/core-dashboard-items.mcp-tools.d.ts.map +1 -0
  106. package/dist/mcp/tools/core-dashboard-items.mcp-tools.js +89 -0
  107. package/dist/mcp/tools/core-dashboard-items.mcp-tools.js.map +1 -0
  108. package/dist/mcp/tools/core-dashboard.mcp-tools.d.ts +29 -0
  109. package/dist/mcp/tools/core-dashboard.mcp-tools.d.ts.map +1 -0
  110. package/dist/mcp/tools/core-dashboard.mcp-tools.js +149 -0
  111. package/dist/mcp/tools/core-dashboard.mcp-tools.js.map +1 -0
  112. package/dist/mcp/tools/core-event-webhooks.mcp-tools.d.ts +30 -0
  113. package/dist/mcp/tools/core-event-webhooks.mcp-tools.d.ts.map +1 -0
  114. package/dist/mcp/tools/core-event-webhooks.mcp-tools.js +185 -0
  115. package/dist/mcp/tools/core-event-webhooks.mcp-tools.js.map +1 -0
  116. package/dist/mcp/tools/core-files.mcp-tools.d.ts +26 -0
  117. package/dist/mcp/tools/core-files.mcp-tools.d.ts.map +1 -0
  118. package/dist/mcp/tools/core-files.mcp-tools.js +140 -0
  119. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -0
  120. package/dist/mcp/tools/core-integration-events.mcp-tools.d.ts +26 -0
  121. package/dist/mcp/tools/core-integration-events.mcp-tools.d.ts.map +1 -0
  122. package/dist/mcp/tools/core-integration-events.mcp-tools.js +168 -0
  123. package/dist/mcp/tools/core-integration-events.mcp-tools.js.map +1 -0
  124. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +32 -0
  125. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +1 -0
  126. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +178 -0
  127. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +1 -0
  128. package/dist/mcp/tools/core-mail-sent.mcp-tools.d.ts +14 -0
  129. package/dist/mcp/tools/core-mail-sent.mcp-tools.d.ts.map +1 -0
  130. package/dist/mcp/tools/core-mail-sent.mcp-tools.js +106 -0
  131. package/dist/mcp/tools/core-mail-sent.mcp-tools.js.map +1 -0
  132. package/dist/mcp/tools/core-mail.mcp-tools.d.ts +36 -0
  133. package/dist/mcp/tools/core-mail.mcp-tools.d.ts.map +1 -0
  134. package/dist/mcp/tools/core-mail.mcp-tools.js +184 -0
  135. package/dist/mcp/tools/core-mail.mcp-tools.js.map +1 -0
  136. package/dist/mcp/tools/core-mcp-chat.mcp-tools.d.ts +21 -0
  137. package/dist/mcp/tools/core-mcp-chat.mcp-tools.d.ts.map +1 -0
  138. package/dist/mcp/tools/core-mcp-chat.mcp-tools.js +118 -0
  139. package/dist/mcp/tools/core-mcp-chat.mcp-tools.js.map +1 -0
  140. package/dist/mcp/tools/core-menus.mcp-tools.d.ts +40 -0
  141. package/dist/mcp/tools/core-menus.mcp-tools.d.ts.map +1 -0
  142. package/dist/mcp/tools/core-menus.mcp-tools.js +268 -0
  143. package/dist/mcp/tools/core-menus.mcp-tools.js.map +1 -0
  144. package/dist/mcp/tools/core-notifications.mcp-tools.d.ts +31 -0
  145. package/dist/mcp/tools/core-notifications.mcp-tools.d.ts.map +1 -0
  146. package/dist/mcp/tools/core-notifications.mcp-tools.js +178 -0
  147. package/dist/mcp/tools/core-notifications.mcp-tools.js.map +1 -0
  148. package/dist/mcp/tools/core-profile.mcp-tools.d.ts +20 -0
  149. package/dist/mcp/tools/core-profile.mcp-tools.d.ts.map +1 -0
  150. package/dist/mcp/tools/core-profile.mcp-tools.js +111 -0
  151. package/dist/mcp/tools/core-profile.mcp-tools.js.map +1 -0
  152. package/dist/mcp/tools/core-roles.mcp-tools.d.ts +70 -0
  153. package/dist/mcp/tools/core-roles.mcp-tools.d.ts.map +1 -0
  154. package/dist/mcp/tools/core-roles.mcp-tools.js +355 -0
  155. package/dist/mcp/tools/core-roles.mcp-tools.js.map +1 -0
  156. package/dist/mcp/tools/core-routes.mcp-tools.d.ts +52 -0
  157. package/dist/mcp/tools/core-routes.mcp-tools.d.ts.map +1 -0
  158. package/dist/mcp/tools/core-routes.mcp-tools.js +254 -0
  159. package/dist/mcp/tools/core-routes.mcp-tools.js.map +1 -0
  160. package/dist/mcp/tools/core-screens.mcp-tools.d.ts +47 -0
  161. package/dist/mcp/tools/core-screens.mcp-tools.d.ts.map +1 -0
  162. package/dist/mcp/tools/core-screens.mcp-tools.js +250 -0
  163. package/dist/mcp/tools/core-screens.mcp-tools.js.map +1 -0
  164. package/dist/mcp/tools/core-sessions.mcp-tools.d.ts +20 -0
  165. package/dist/mcp/tools/core-sessions.mcp-tools.d.ts.map +1 -0
  166. package/dist/mcp/tools/core-sessions.mcp-tools.js +128 -0
  167. package/dist/mcp/tools/core-sessions.mcp-tools.js.map +1 -0
  168. package/dist/mcp/tools/core-settings.mcp-tools.d.ts +25 -0
  169. package/dist/mcp/tools/core-settings.mcp-tools.d.ts.map +1 -1
  170. package/dist/mcp/tools/core-settings.mcp-tools.js +138 -0
  171. package/dist/mcp/tools/core-settings.mcp-tools.js.map +1 -1
  172. package/dist/mcp/tools/core-system.mcp-tools.d.ts +8 -0
  173. package/dist/mcp/tools/core-system.mcp-tools.d.ts.map +1 -0
  174. package/dist/mcp/tools/core-system.mcp-tools.js +40 -0
  175. package/dist/mcp/tools/core-system.mcp-tools.js.map +1 -0
  176. package/dist/mcp/tools/core-webhooks.mcp-tools.d.ts +46 -0
  177. package/dist/mcp/tools/core-webhooks.mcp-tools.d.ts.map +1 -0
  178. package/dist/mcp/tools/core-webhooks.mcp-tools.js +266 -0
  179. package/dist/mcp/tools/core-webhooks.mcp-tools.js.map +1 -0
  180. package/dist/mcp-chat/mcp-chat.controller.d.ts +5 -1
  181. package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -1
  182. package/dist/mcp-chat/mcp-chat.controller.js +46 -0
  183. package/dist/mcp-chat/mcp-chat.controller.js.map +1 -1
  184. package/dist/mcp-chat/mcp-chat.service.d.ts +31 -3
  185. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  186. package/dist/mcp-chat/mcp-chat.service.js +776 -38
  187. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  188. package/dist/notification/dto/create-notification.dto.d.ts +26 -0
  189. package/dist/notification/dto/create-notification.dto.d.ts.map +1 -0
  190. package/dist/notification/dto/create-notification.dto.js +86 -0
  191. package/dist/notification/dto/create-notification.dto.js.map +1 -0
  192. package/dist/notification/dto/update-progress.dto.d.ts +6 -0
  193. package/dist/notification/dto/update-progress.dto.d.ts.map +1 -0
  194. package/dist/notification/dto/update-progress.dto.js +33 -0
  195. package/dist/notification/dto/update-progress.dto.js.map +1 -0
  196. package/dist/notification/notification.controller.d.ts +112 -0
  197. package/dist/notification/notification.controller.d.ts.map +1 -0
  198. package/dist/notification/notification.controller.js +130 -0
  199. package/dist/notification/notification.controller.js.map +1 -0
  200. package/dist/notification/notification.module.d.ts +3 -0
  201. package/dist/notification/notification.module.d.ts.map +1 -0
  202. package/dist/notification/notification.module.js +29 -0
  203. package/dist/notification/notification.module.js.map +1 -0
  204. package/dist/notification/notification.service.d.ts +112 -0
  205. package/dist/notification/notification.service.d.ts.map +1 -0
  206. package/dist/notification/notification.service.js +269 -0
  207. package/dist/notification/notification.service.js.map +1 -0
  208. package/dist/pdf/pdf.service.js +1 -1
  209. package/dist/pdf/pdf.service.js.map +1 -1
  210. package/dist/profile/profile.controller.d.ts +4 -4
  211. package/dist/profile/profile.controller.js +0 -1
  212. package/dist/profile/profile.controller.js.map +1 -1
  213. package/dist/profile/profile.module.d.ts.map +1 -1
  214. package/dist/profile/profile.module.js +1 -0
  215. package/dist/profile/profile.module.js.map +1 -1
  216. package/dist/profile/profile.service.d.ts +4 -4
  217. package/dist/session/session.controller.d.ts +3 -3
  218. package/dist/session/session.service.d.ts +9 -9
  219. package/dist/setting/dto/update-setting-list.dto.d.ts +9 -0
  220. package/dist/setting/dto/update-setting-list.dto.d.ts.map +1 -0
  221. package/dist/setting/dto/update-setting-list.dto.js +44 -0
  222. package/dist/setting/dto/update-setting-list.dto.js.map +1 -0
  223. package/dist/setting/setting.controller.d.ts +20 -1
  224. package/dist/setting/setting.controller.d.ts.map +1 -1
  225. package/dist/setting/setting.controller.js +12 -0
  226. package/dist/setting/setting.controller.js.map +1 -1
  227. package/dist/setting/setting.service.d.ts +20 -1
  228. package/dist/setting/setting.service.d.ts.map +1 -1
  229. package/dist/setting/setting.service.js +80 -4
  230. package/dist/setting/setting.service.js.map +1 -1
  231. package/dist/task/task.service.d.ts +13 -0
  232. package/dist/task/task.service.d.ts.map +1 -1
  233. package/dist/task/task.service.js +85 -0
  234. package/dist/task/task.service.js.map +1 -1
  235. package/dist/token/token.service.d.ts.map +1 -1
  236. package/dist/token/token.service.js +0 -1
  237. package/dist/token/token.service.js.map +1 -1
  238. package/dist/user/user.controller.d.ts +11 -11
  239. package/dist/user/user.service.d.ts +27 -27
  240. package/dist/webhook-integration/dto/webhook-action.dto.d.ts +22 -0
  241. package/dist/webhook-integration/dto/webhook-action.dto.d.ts.map +1 -0
  242. package/dist/webhook-integration/dto/webhook-action.dto.js +120 -0
  243. package/dist/webhook-integration/dto/webhook-action.dto.js.map +1 -0
  244. package/dist/webhook-integration/webhook-integration.controller.d.ts +7 -5
  245. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  246. package/dist/webhook-integration/webhook-integration.controller.js +18 -5
  247. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  248. package/dist/webhook-integration/webhook-integration.service.d.ts +18 -5
  249. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  250. package/dist/webhook-integration/webhook-integration.service.js +342 -59
  251. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  252. package/dist/webhook-integration/webhook-utils.d.ts +1 -0
  253. package/dist/webhook-integration/webhook-utils.d.ts.map +1 -1
  254. package/dist/webhook-integration/webhook-utils.js +38 -0
  255. package/dist/webhook-integration/webhook-utils.js.map +1 -1
  256. package/hedhog/data/menu.yaml +63 -4
  257. package/hedhog/data/route.yaml +1437 -0
  258. package/hedhog/data/setting_group.yaml +112 -0
  259. package/hedhog/data/setting_subgroup.yaml +32 -0
  260. package/hedhog/frontend/app/access-log/page.tsx.ejs +772 -0
  261. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +285 -0
  262. package/hedhog/frontend/app/account/layout.tsx.ejs +32 -55
  263. package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +5 -0
  264. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +10 -3
  265. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +33 -13
  266. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +12 -5
  267. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +12 -5
  268. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +3559 -1835
  269. package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +633 -0
  270. package/hedhog/frontend/app/mcp_chat/chat-types.ts.ejs +16 -0
  271. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +583 -384
  272. package/hedhog/frontend/app/menu/page.tsx.ejs +9 -3
  273. package/hedhog/frontend/app/roles/page.tsx.ejs +18 -6
  274. package/hedhog/frontend/app/routes/page.tsx.ejs +9 -3
  275. package/hedhog/frontend/app/setting-list/page.tsx.ejs +822 -0
  276. package/hedhog/frontend/app/users/page.tsx.ejs +445 -203
  277. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +310 -304
  278. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +41 -44
  279. package/hedhog/frontend/messages/en.json +343 -3
  280. package/hedhog/frontend/messages/pt.json +343 -3
  281. package/hedhog/table/access_log.yaml +31 -0
  282. package/hedhog/table/mcp_api_key.yaml +30 -0
  283. package/hedhog/table/notification.yaml +72 -0
  284. package/hedhog/table/webhook_action_log.yaml +49 -0
  285. package/package.json +5 -4
  286. package/src/access-log/access-log.controller.ts +29 -0
  287. package/src/access-log/access-log.interceptor.ts +38 -0
  288. package/src/access-log/access-log.module.ts +16 -0
  289. package/src/access-log/access-log.service.ts +129 -0
  290. package/src/access-log/dto/query-access-log.dto.ts +23 -0
  291. package/src/auth/auth.service.ts +0 -7
  292. package/src/challenge/challenge.service.ts +0 -2
  293. package/src/core/system.module.ts +1 -0
  294. package/src/core.module.ts +15 -0
  295. package/src/dashboard/dashboard.module.ts +4 -1
  296. package/src/file/file.controller.ts +14 -14
  297. package/src/file/file.service.ts +198 -4
  298. package/src/file/provider/abstract.provider.ts +10 -0
  299. package/src/file/provider/azure.provider.ts +24 -0
  300. package/src/file/provider/gcs.provider.ts +20 -0
  301. package/src/file/provider/local.provider.ts +30 -2
  302. package/src/file/provider/provider.factory.ts +3 -0
  303. package/src/file/provider/s3.provider.ts +22 -0
  304. package/src/index.ts +5 -0
  305. package/src/mcp/guards/mcp-auth.guard.ts +64 -32
  306. package/src/mcp/mcp-api-key.controller.ts +42 -0
  307. package/src/mcp/mcp-api-key.service.ts +66 -0
  308. package/src/mcp/mcp-audit.service.ts +47 -36
  309. package/src/mcp/mcp-registry.service.ts +75 -69
  310. package/src/mcp/mcp.controller.ts +40 -30
  311. package/src/mcp/mcp.module.ts +115 -36
  312. package/src/mcp/tools/core-ai.mcp-tools.ts +156 -0
  313. package/src/mcp/tools/core-dashboard-items.mcp-tools.ts +63 -0
  314. package/src/mcp/tools/core-dashboard.mcp-tools.ts +103 -0
  315. package/src/mcp/tools/core-event-webhooks.mcp-tools.ts +134 -0
  316. package/src/mcp/tools/core-files.mcp-tools.ts +105 -0
  317. package/src/mcp/tools/core-integration-events.mcp-tools.ts +118 -0
  318. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +128 -0
  319. package/src/mcp/tools/core-mail-sent.mcp-tools.ts +76 -0
  320. package/src/mcp/tools/core-mail.mcp-tools.ts +130 -0
  321. package/src/mcp/tools/core-mcp-chat.mcp-tools.ts +81 -0
  322. package/src/mcp/tools/core-menus.mcp-tools.ts +197 -0
  323. package/src/mcp/tools/core-notifications.mcp-tools.ts +128 -0
  324. package/src/mcp/tools/core-profile.mcp-tools.ts +74 -0
  325. package/src/mcp/tools/core-roles.mcp-tools.ts +288 -0
  326. package/src/mcp/tools/core-routes.mcp-tools.ts +196 -0
  327. package/src/mcp/tools/core-screens.mcp-tools.ts +186 -0
  328. package/src/mcp/tools/core-sessions.mcp-tools.ts +97 -0
  329. package/src/mcp/tools/core-settings.mcp-tools.ts +171 -53
  330. package/src/mcp/tools/core-system.mcp-tools.ts +19 -0
  331. package/src/mcp/tools/core-webhooks.mcp-tools.ts +199 -0
  332. package/src/mcp-chat/mcp-chat.controller.ts +113 -60
  333. package/src/mcp-chat/mcp-chat.service.ts +1343 -332
  334. package/src/notification/dto/create-notification.dto.ts +71 -0
  335. package/src/notification/dto/update-progress.dto.ts +16 -0
  336. package/src/notification/notification.controller.ts +83 -0
  337. package/src/notification/notification.module.ts +16 -0
  338. package/src/notification/notification.service.ts +324 -0
  339. package/src/pdf/pdf.service.ts +1 -1
  340. package/src/profile/profile.controller.ts +1 -1
  341. package/src/profile/profile.module.ts +1 -0
  342. package/src/setting/dto/update-setting-list.dto.ts +32 -0
  343. package/src/setting/setting.controller.ts +9 -0
  344. package/src/setting/setting.service.ts +99 -11
  345. package/src/task/task.service.ts +103 -0
  346. package/src/token/token.service.ts +0 -2
  347. package/src/webhook-integration/dto/webhook-action.dto.ts +96 -0
  348. package/src/webhook-integration/webhook-integration.controller.ts +124 -103
  349. package/src/webhook-integration/webhook-integration.service.ts +1137 -735
  350. package/src/webhook-integration/webhook-utils.ts +136 -79
@@ -12,232 +12,71 @@ import {
12
12
  } from '@/components/ui/alert-dialog';
13
13
  import { Button } from '@/components/ui/button';
14
14
  import { ScrollArea } from '@/components/ui/scroll-area';
15
- import { Textarea } from '@/components/ui/textarea';
16
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
17
15
  import {
18
- Bot,
19
- ChevronDown,
20
- ChevronRight,
21
- MessageSquare,
22
- Plus,
23
- Send,
24
- Trash2,
25
- Wrench,
26
- } from 'lucide-react';
16
+ Sheet,
17
+ SheetDescription,
18
+ SheetHeader,
19
+ SheetTitle,
20
+ } from '@/components/ui/sheet';
21
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
22
+ import { SidebarTrigger } from '@/components/ui/sidebar';
23
+ import { useFormDraft } from '@/hooks/use-form-draft';
24
+ import { useIsMobile } from '@/hooks/use-mobile';
25
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
26
+ import { Bot, MessageSquare } from 'lucide-react';
27
27
  import { useTranslations } from 'next-intl';
28
- import { useEffect, useRef, useState } from 'react';
29
- import ReactMarkdown from 'react-markdown';
30
- import remarkGfm from 'remark-gfm';
28
+ import { useEffect, useMemo, useRef, useState } from 'react';
31
29
  import { toast } from 'sonner';
32
-
33
- type Conversation = {
34
- id: number;
35
- title: string | null;
36
- created_at: string;
37
- updated_at: string;
38
- };
39
-
40
- type Message = {
41
- id: number;
42
- conversation_id: number;
43
- role: 'user' | 'assistant' | 'tool_call' | 'tool_result';
44
- content: string;
45
- tool_name: string | null;
46
- tool_call_id: string | null;
47
- created_at: string;
48
- };
49
-
50
- type MarkdownComponentProps = {
51
- children?: React.ReactNode;
52
- className?: string;
53
- href?: string;
54
- };
55
-
56
- const markdownComponents: Record<
57
- string,
58
- (props: MarkdownComponentProps) => React.ReactNode
59
- > = {
60
- p({ children }) {
61
- return <p className="mb-2 last:mb-0 leading-relaxed">{children}</p>;
62
- },
63
- ul({ children }) {
64
- return <ul className="mb-2 list-disc pl-4 space-y-1">{children}</ul>;
65
- },
66
- ol({ children }) {
67
- return <ol className="mb-2 list-decimal pl-4 space-y-1">{children}</ol>;
68
- },
69
- li({ children }) {
70
- return <li className="leading-relaxed">{children}</li>;
71
- },
72
- strong({ children }) {
73
- return <strong className="font-semibold">{children}</strong>;
74
- },
75
- em({ children }) {
76
- return <em className="italic">{children}</em>;
77
- },
78
- code({ children, className }) {
79
- const isBlock = className?.includes('language-');
80
- return isBlock ? (
81
- <code className="block rounded-md bg-black/10 dark:bg-white/10 px-3 py-2 text-xs font-mono whitespace-pre-wrap my-2">
82
- {children}
83
- </code>
84
- ) : (
85
- <code className="rounded bg-black/10 dark:bg-white/10 px-1 py-0.5 text-xs font-mono">
86
- {children}
87
- </code>
88
- );
89
- },
90
- pre({ children }) {
91
- return <pre className="my-2 overflow-x-auto rounded-md">{children}</pre>;
92
- },
93
- blockquote({ children }) {
94
- return (
95
- <blockquote className="border-l-2 border-current/30 pl-3 italic my-2 text-current/70">
96
- {children}
97
- </blockquote>
98
- );
99
- },
100
- h1({ children }) {
101
- return (
102
- <h1 className="text-base font-bold mb-2 mt-3 first:mt-0">{children}</h1>
103
- );
104
- },
105
- h2({ children }) {
106
- return (
107
- <h2 className="text-sm font-bold mb-2 mt-3 first:mt-0">{children}</h2>
108
- );
109
- },
110
- h3({ children }) {
111
- return (
112
- <h3 className="text-sm font-semibold mb-1 mt-2 first:mt-0">{children}</h3>
113
- );
114
- },
115
- a({ href, children }) {
116
- return (
117
- <a
118
- href={href}
119
- target="_blank"
120
- rel="noopener noreferrer"
121
- className="underline underline-offset-2 hover:opacity-80"
122
- >
123
- {children}
124
- </a>
125
- );
126
- },
127
- hr() {
128
- return <hr className="my-3 border-current/20" />;
129
- },
130
- table({ children }) {
131
- return (
132
- <div className="my-2 overflow-x-auto">
133
- <table className="text-xs border-collapse w-full">{children}</table>
134
- </div>
135
- );
136
- },
137
- th({ children }) {
138
- return (
139
- <th className="border border-current/20 px-2 py-1 font-semibold text-left bg-current/5">
140
- {children}
141
- </th>
142
- );
143
- },
144
- td({ children }) {
145
- return <td className="border border-current/20 px-2 py-1">{children}</td>;
146
- },
147
- };
148
-
149
- function ToolBadge({
150
- message,
151
- variant,
152
- }: {
153
- message: Message;
154
- variant: 'call' | 'result';
155
- }) {
156
- const [expanded, setExpanded] = useState(false);
157
- let parsed: unknown;
158
- try {
159
- parsed = JSON.parse(message.content);
160
- } catch {
161
- parsed = message.content;
162
- }
163
-
164
- const colorClass =
165
- variant === 'call'
166
- ? 'border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-800 dark:bg-violet-950 dark:text-violet-300'
167
- : 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300';
168
-
169
- const preClass =
170
- variant === 'call'
171
- ? 'border-violet-100 bg-violet-50/50 text-violet-800 dark:border-violet-900 dark:bg-violet-950/50 dark:text-violet-200'
172
- : 'border-emerald-100 bg-emerald-50/50 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/50 dark:text-emerald-200';
173
-
174
- return (
175
- <div className="my-1 flex flex-col items-start">
176
- <button
177
- type="button"
178
- className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-colors hover:opacity-80 ${colorClass}`}
179
- onClick={() => setExpanded((v) => !v)}
180
- >
181
- <Wrench className="h-3 w-3 shrink-0" />
182
- <span className="font-mono">{message.tool_name}</span>
183
- {expanded ? (
184
- <ChevronDown className="h-3 w-3 shrink-0" />
185
- ) : (
186
- <ChevronRight className="h-3 w-3 shrink-0" />
187
- )}
188
- </button>
189
- {expanded && (
190
- <pre
191
- className={`mt-1.5 w-full max-w-lg rounded-lg border px-3 py-2 text-xs font-mono overflow-x-auto whitespace-pre-wrap ${preClass}`}
192
- >
193
- {typeof parsed === 'string'
194
- ? parsed
195
- : JSON.stringify(parsed, null, 2)}
196
- </pre>
197
- )}
198
- </div>
199
- );
200
- }
201
-
202
- function TypingIndicator() {
203
- return (
204
- <div className="flex items-start gap-3">
205
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted ring-2 ring-background">
206
- <Bot className="h-4 w-4 text-muted-foreground" />
207
- </div>
208
- <div className="flex items-center gap-1 rounded-2xl rounded-tl-sm bg-muted px-4 py-3 shadow-sm">
209
- <span
210
- className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60 animate-bounce"
211
- style={{ animationDelay: '0ms' }}
212
- />
213
- <span
214
- className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60 animate-bounce"
215
- style={{ animationDelay: '150ms' }}
216
- />
217
- <span
218
- className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60 animate-bounce"
219
- style={{ animationDelay: '300ms' }}
220
- />
221
- </div>
222
- </div>
223
- );
224
- }
30
+ import { ChatInput, ConversationSidebar, MessageFeed } from './chat-components';
31
+ import { Conversation, Message } from './chat-types';
225
32
 
226
33
  export default function McpChatPage() {
227
- const { request, currentLocaleCode, refetchUser } = useApp();
34
+ const { request, currentLocaleCode, refetchUser, accessToken, getSettingValue } = useApp();
228
35
  const t = useTranslations('core.McpChatPage');
36
+ const mcpEnabled = getSettingValue('mcp-enabled') !== false;
37
+ const isMobile = useIsMobile();
229
38
 
230
39
  const [selectedConvId, setSelectedConvId] = useState<number | null>(null);
231
40
  const [messages, setMessages] = useState<Message[]>([]);
232
41
  const [input, setInput] = useState('');
42
+ const [attachments, setAttachments] = useState<
43
+ Array<{ id: number; name: string }>
44
+ >([]);
45
+ const [isUploadingFile, setIsUploadingFile] = useState(false);
233
46
  const [isSending, setIsSending] = useState(false);
47
+ const [isStreamingResponse, setIsStreamingResponse] = useState(false);
48
+ const [isLoadingMessages, setIsLoadingMessages] = useState(false);
49
+ const [isMobileConversationsOpen, setIsMobileConversationsOpen] =
50
+ useState(false);
234
51
  const [convToDelete, setConvToDelete] = useState<Conversation | null>(null);
235
52
  const messagesEndRef = useRef<HTMLDivElement>(null);
236
53
  const textareaRef = useRef<HTMLTextAreaElement>(null);
54
+ const streamingTimeoutRef = useRef<number | null>(null);
55
+ const streamingRunRef = useRef(0);
56
+ const streamAbortRef = useRef<AbortController | null>(null);
237
57
 
238
- const { data: conversations, refetch: refetchConvs } = useQuery<
239
- Conversation[]
240
- >({
58
+ const selectedConversationDraft = useFormDraft<number | null>({
59
+ storageKey: 'mcp-chat-selected-conversation-draft',
60
+ value: selectedConvId,
61
+ hasData: selectedConvId !== null,
62
+ debounceMs: 250,
63
+ });
64
+
65
+ const {
66
+ clearDraft: clearMessageInputDraft,
67
+ loadDraft: loadMessageInputDraft,
68
+ } = useFormDraft<string>({
69
+ storageKey: `mcp-chat-input-${selectedConvId ?? 'new'}-draft`,
70
+ value: input,
71
+ hasData: input.trim().length > 0,
72
+ debounceMs: 300,
73
+ });
74
+
75
+ const {
76
+ data: conversations,
77
+ refetch: refetchConvs,
78
+ isLoading: isLoadingConversations,
79
+ } = useQuery<Conversation[]>({
241
80
  queryKey: ['mcp-chat-conversations', currentLocaleCode],
242
81
  queryFn: async () => {
243
82
  const res = await request<Conversation[]>({
@@ -246,35 +85,312 @@ export default function McpChatPage() {
246
85
  });
247
86
  return res.data;
248
87
  },
88
+ enabled: mcpEnabled,
249
89
  });
250
90
 
251
- const convList = conversations ?? [];
91
+ const convList = useMemo(() => conversations ?? [], [conversations]);
252
92
  const selectedConv = convList.find((c) => c.id === selectedConvId) ?? null;
93
+ const activeMessageConversationId = mcpEnabled ? selectedConvId : null;
94
+
95
+ useEffect(() => {
96
+ if (selectedConvId !== null || convList.length === 0) return;
97
+ const draft = selectedConversationDraft.loadDraft();
98
+ const draftConvId = draft?.payload;
99
+ if (typeof draftConvId !== 'number') return;
100
+
101
+ if (convList.some((conv) => conv.id === draftConvId)) {
102
+ setSelectedConvId(draftConvId);
103
+ return;
104
+ }
105
+
106
+ selectedConversationDraft.clearDraft();
107
+ }, [convList, selectedConvId, selectedConversationDraft]);
253
108
 
254
109
  useEffect(() => {
255
- if (!selectedConvId) return;
110
+ const draft = loadMessageInputDraft();
111
+ if (typeof draft?.payload === 'string') {
112
+ setInput(draft.payload);
113
+ return;
114
+ }
115
+
116
+ setInput('');
117
+ }, [selectedConvId, loadMessageInputDraft]);
118
+
119
+ useEffect(() => {
120
+ if (!activeMessageConversationId) {
121
+ setMessages([]);
122
+ setIsLoadingMessages(false);
123
+ return;
124
+ }
125
+
126
+ let isMounted = true;
127
+ setIsLoadingMessages(true);
128
+
256
129
  request<Message[]>({
257
- url: `/mcp-chat/${selectedConvId}/messages`,
130
+ url: `/mcp-chat/${activeMessageConversationId}/messages`,
258
131
  method: 'GET',
259
132
  })
260
- .then((res) => setMessages(res.data))
261
- .catch(() => setMessages([]));
262
- }, [selectedConvId]);
133
+ .then((res) => {
134
+ if (isMounted) {
135
+ setMessages(res.data);
136
+ }
137
+ })
138
+ .catch(() => {
139
+ if (isMounted) {
140
+ setMessages([]);
141
+ toast.error(t('loadMessagesError'));
142
+ }
143
+ })
144
+ .finally(() => {
145
+ if (isMounted) {
146
+ setIsLoadingMessages(false);
147
+ }
148
+ });
149
+
150
+ return () => {
151
+ isMounted = false;
152
+ };
153
+ }, [request, activeMessageConversationId, t]);
263
154
 
264
155
  useEffect(() => {
265
156
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
266
- }, [messages, isSending]);
157
+ }, [messages, isSending, isLoadingMessages, isStreamingResponse]);
158
+
159
+ function stopStreaming() {
160
+ streamingRunRef.current += 1;
161
+ if (streamAbortRef.current) {
162
+ streamAbortRef.current.abort();
163
+ streamAbortRef.current = null;
164
+ }
165
+ if (streamingTimeoutRef.current !== null) {
166
+ window.clearTimeout(streamingTimeoutRef.current);
167
+ streamingTimeoutRef.current = null;
168
+ }
169
+ setIsStreamingResponse(false);
170
+ }
171
+
172
+ useEffect(() => {
173
+ return () => {
174
+ if (streamAbortRef.current) {
175
+ streamAbortRef.current.abort();
176
+ }
177
+ if (streamingTimeoutRef.current !== null) {
178
+ window.clearTimeout(streamingTimeoutRef.current);
179
+ }
180
+ };
181
+ }, []);
182
+
183
+ async function streamAssistantMessage(responseMessages: Message[]) {
184
+ const lastAssistantIdx = [...responseMessages]
185
+ .map((msg, idx) => ({ msg, idx }))
186
+ .reverse()
187
+ .find(({ msg }) => msg.role === 'assistant')?.idx;
188
+
189
+ if (lastAssistantIdx === undefined) {
190
+ setMessages(responseMessages);
191
+ return;
192
+ }
193
+
194
+ const target = responseMessages[lastAssistantIdx] as Message;
195
+ const fullContent = target.content ?? '';
196
+
197
+ if (fullContent.length < 24) {
198
+ setMessages(responseMessages);
199
+ return;
200
+ }
201
+
202
+ stopStreaming();
203
+ const runId = ++streamingRunRef.current;
204
+ setIsStreamingResponse(true);
205
+
206
+ const baseMessages: Message[] = responseMessages.map(
207
+ (msg, idx): Message =>
208
+ idx === lastAssistantIdx ? { ...msg, content: '' } : msg
209
+ );
210
+ setMessages(baseMessages);
211
+
212
+ await new Promise<void>((resolve) => {
213
+ let cursor = 0;
214
+ const chunkSize = Math.max(
215
+ 8,
216
+ Math.min(32, Math.ceil(fullContent.length / 80))
217
+ );
218
+
219
+ const tick = () => {
220
+ if (streamingRunRef.current !== runId) {
221
+ resolve();
222
+ return;
223
+ }
224
+
225
+ cursor = Math.min(fullContent.length, cursor + chunkSize);
226
+ const nextMessages: Message[] = responseMessages.map(
227
+ (msg, idx): Message =>
228
+ idx === lastAssistantIdx
229
+ ? { ...target, content: fullContent.slice(0, cursor) }
230
+ : msg
231
+ );
232
+ setMessages(nextMessages);
233
+
234
+ if (cursor >= fullContent.length) {
235
+ resolve();
236
+ return;
237
+ }
238
+
239
+ streamingTimeoutRef.current = window.setTimeout(tick, 16);
240
+ };
241
+
242
+ tick();
243
+ });
244
+
245
+ if (streamingRunRef.current === runId) {
246
+ setMessages(responseMessages);
247
+ setIsStreamingResponse(false);
248
+ streamingTimeoutRef.current = null;
249
+ }
250
+ }
251
+
252
+ async function sendMessageViaStream(
253
+ conversationId: number,
254
+ message: string,
255
+ tempAssistantId: number
256
+ ): Promise<Message[] | null> {
257
+ const rawBase = process.env.NEXT_PUBLIC_API_BASE_URL || '/api';
258
+ const apiBase = rawBase.replace(/\/$/, '');
259
+ const url = `${apiBase}/mcp-chat/${conversationId}/messages/stream`;
260
+
261
+ const controller = new AbortController();
262
+ streamAbortRef.current = controller;
263
+ setIsStreamingResponse(true);
264
+
265
+ const headers: Record<string, string> = {
266
+ 'Content-Type': 'application/json',
267
+ Accept: 'text/event-stream',
268
+ 'Accept-Language': currentLocaleCode,
269
+ };
270
+
271
+ if (accessToken) {
272
+ headers.Authorization = `Bearer ${accessToken}`;
273
+ }
274
+
275
+ const response = await fetch(url, {
276
+ method: 'POST',
277
+ headers,
278
+ body: JSON.stringify({ message }),
279
+ signal: controller.signal,
280
+ });
281
+
282
+ if (!response.ok || !response.body) {
283
+ throw new Error(`stream-unavailable:${response.status}`);
284
+ }
285
+
286
+ let accumulated = '';
287
+ setMessages((prev) => [
288
+ ...prev,
289
+ {
290
+ id: tempAssistantId,
291
+ conversation_id: conversationId,
292
+ role: 'assistant',
293
+ content: '',
294
+ tool_name: null,
295
+ tool_call_id: null,
296
+ created_at: new Date().toISOString(),
297
+ },
298
+ ]);
299
+
300
+ const reader = response.body.getReader();
301
+ const decoder = new TextDecoder();
302
+ let buffer = '';
303
+ let doneMessages: Message[] | null = null;
304
+
305
+ while (true) {
306
+ const { value, done } = await reader.read();
307
+ if (done) break;
308
+
309
+ buffer += decoder.decode(value, { stream: true });
310
+ const events = buffer.split('\n\n');
311
+ buffer = events.pop() ?? '';
312
+
313
+ for (const eventBlock of events) {
314
+ const dataLines = eventBlock
315
+ .split('\n')
316
+ .filter((line) => line.startsWith('data:'))
317
+ .map((line) => line.slice(5).trim());
318
+
319
+ if (dataLines.length === 0) continue;
320
+ const payloadText = dataLines.join('\n');
321
+
322
+ let payload: {
323
+ type?: 'chunk' | 'done' | 'error';
324
+ content?: string;
325
+ message?: string;
326
+ messages?: Message[];
327
+ };
328
+ try {
329
+ payload = JSON.parse(payloadText);
330
+ } catch {
331
+ continue;
332
+ }
333
+
334
+ if (payload.type === 'chunk') {
335
+ const chunk = payload.content ?? '';
336
+ if (!chunk) continue;
337
+ accumulated += chunk;
338
+ setMessages((prev) =>
339
+ prev.map((msg) =>
340
+ msg.id === tempAssistantId
341
+ ? { ...msg, content: accumulated }
342
+ : msg
343
+ )
344
+ );
345
+ }
346
+
347
+ if (payload.type === 'done' && Array.isArray(payload.messages)) {
348
+ doneMessages = payload.messages;
349
+ }
350
+
351
+ if (payload.type === 'error') {
352
+ throw new Error(payload.message || 'stream-error');
353
+ }
354
+ }
355
+ }
356
+
357
+ return doneMessages;
358
+ }
267
359
 
268
360
  // Auto-resize textarea
269
361
  useEffect(() => {
270
- if (textareaRef.current) {
271
- textareaRef.current.style.height = 'auto';
272
- textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 300)}px`;
362
+ const textarea = textareaRef.current;
363
+ if (!textarea) {
364
+ return;
365
+ }
366
+
367
+ textarea.style.height = 'auto';
368
+ const maxHeight = isMobile ? 160 : 300;
369
+ const nextHeight = Math.min(textarea.scrollHeight, maxHeight);
370
+
371
+ const frameId = window.requestAnimationFrame(() => {
372
+ textarea.style.height = `${nextHeight}px`;
373
+ });
374
+
375
+ return () => {
376
+ window.cancelAnimationFrame(frameId);
377
+ };
378
+ }, [input, isMobile]);
379
+
380
+ function handleSelectConversation(convId: number) {
381
+ stopStreaming();
382
+ setSelectedConvId(convId);
383
+ setMessages([]);
384
+ if (isMobile) {
385
+ setIsMobileConversationsOpen(false);
273
386
  }
274
- }, [input]);
387
+ }
275
388
 
276
389
  async function handleNewConversation() {
390
+ if (!mcpEnabled) return null;
391
+
277
392
  try {
393
+ stopStreaming();
278
394
  const res = await request<Conversation>({
279
395
  url: '/mcp-chat',
280
396
  method: 'POST',
@@ -283,15 +399,25 @@ export default function McpChatPage() {
283
399
  await refetchConvs();
284
400
  setSelectedConvId(res.data.id);
285
401
  setMessages([]);
402
+ setInput('');
403
+ if (isMobile) {
404
+ setIsMobileConversationsOpen(false);
405
+ }
286
406
  return res.data.id;
287
407
  } catch {
288
- toast.error('Failed to create conversation');
408
+ toast.error(t('createConversationError'));
289
409
  return null;
290
410
  }
291
411
  }
292
412
 
293
413
  async function handleSend() {
294
- if (!input.trim() || isSending) return;
414
+ if (
415
+ !mcpEnabled ||
416
+ (!input.trim() && attachments.length === 0) ||
417
+ isSending ||
418
+ isUploadingFile
419
+ )
420
+ return;
295
421
  let activeConvId = selectedConvId;
296
422
  if (!activeConvId) {
297
423
  activeConvId = await handleNewConversation();
@@ -299,15 +425,27 @@ export default function McpChatPage() {
299
425
  }
300
426
  setIsSending(true);
301
427
  const currentInput = input;
428
+ const currentAttachments = attachments;
429
+ const tempId = -Date.now();
430
+ const attachmentsBlock =
431
+ currentAttachments.length > 0
432
+ ? `\n\n${t('attachedFilesPrefix')}\n${currentAttachments
433
+ .map((file) => `- ${file.name} (file_id: ${file.id})`)
434
+ .join('\n')}`
435
+ : '';
436
+ const payloadMessage = `${currentInput.trim()}${attachmentsBlock}`.trim();
437
+
302
438
  setInput('');
439
+ setAttachments([]);
440
+ clearMessageInputDraft();
303
441
 
304
442
  setMessages((prev) => [
305
443
  ...prev,
306
444
  {
307
- id: -Date.now(),
445
+ id: tempId,
308
446
  conversation_id: activeConvId!,
309
447
  role: 'user',
310
- content: currentInput,
448
+ content: payloadMessage,
311
449
  tool_name: null,
312
450
  tool_call_id: null,
313
451
  created_at: new Date().toISOString(),
@@ -315,25 +453,101 @@ export default function McpChatPage() {
315
453
  ]);
316
454
 
317
455
  try {
318
- const res = await request<Message[]>({
319
- url: `/mcp-chat/${activeConvId}/messages`,
320
- method: 'POST',
321
- data: { message: currentInput },
322
- });
323
- setMessages(res.data);
456
+ stopStreaming();
457
+ const tempAssistantId = -(Date.now() + 1);
458
+ let finalMessages: Message[] | null = null;
459
+
460
+ try {
461
+ finalMessages = await sendMessageViaStream(
462
+ activeConvId,
463
+ payloadMessage,
464
+ tempAssistantId
465
+ );
466
+ } catch {
467
+ setMessages((prev) => prev.filter((m) => m.id !== tempAssistantId));
468
+
469
+ const fallbackRes = await request<Message[]>({
470
+ url: `/mcp-chat/${activeConvId}/messages`,
471
+ method: 'POST',
472
+ data: { message: payloadMessage },
473
+ });
474
+ finalMessages = fallbackRes.data;
475
+ await streamAssistantMessage(finalMessages);
476
+ }
477
+
478
+ if (finalMessages) {
479
+ setMessages(finalMessages);
480
+ }
481
+
324
482
  await refetchConvs();
325
- const hadToolUse = res.data.some(
483
+ const hadToolUse = (finalMessages ?? []).some(
326
484
  (m) => m.role === 'tool_call' || m.role === 'tool_result'
327
485
  );
328
486
  if (hadToolUse) await refetchUser();
329
487
  } catch {
330
- toast.error('Failed to send message');
331
- setMessages((prev) => prev.filter((m) => m.id !== -Date.now()));
488
+ toast.error(t('sendMessageError'));
489
+ setMessages((prev) => prev.filter((m) => m.id !== tempId));
490
+ setInput(currentInput);
491
+ setAttachments(currentAttachments);
332
492
  } finally {
493
+ streamAbortRef.current = null;
494
+ setIsStreamingResponse(false);
333
495
  setIsSending(false);
334
496
  }
335
497
  }
336
498
 
499
+ const uploadAttachment = async (file: File) => {
500
+ const existingNames = new Set(attachments.map((item) => item.name));
501
+ if (existingNames.has(file.name)) {
502
+ return;
503
+ }
504
+
505
+ setIsUploadingFile(true);
506
+ try {
507
+ const formData = new FormData();
508
+ formData.append('file', file);
509
+ formData.append('destination', 'mcp-chat/attachments');
510
+
511
+ const { data } = await request<{ id: number; filename?: string }>({
512
+ url: '/file',
513
+ method: 'POST',
514
+ data: formData,
515
+ headers: {
516
+ 'Content-Type': 'multipart/form-data',
517
+ },
518
+ });
519
+
520
+ if (!data?.id) {
521
+ throw new Error('invalid-file-id');
522
+ }
523
+
524
+ setAttachments((prev) => [
525
+ ...prev,
526
+ {
527
+ id: data.id,
528
+ name: data.filename || file.name,
529
+ },
530
+ ]);
531
+ } catch {
532
+ toast.error(t('uploadFileError'));
533
+ } finally {
534
+ setIsUploadingFile(false);
535
+ }
536
+ };
537
+
538
+ const handleUploadFiles = (files: FileList) => {
539
+ const queue = Array.from(files);
540
+ void (async () => {
541
+ for (const file of queue) {
542
+ await uploadAttachment(file);
543
+ }
544
+ })();
545
+ };
546
+
547
+ const handleRemoveAttachment = (id: number) => {
548
+ setAttachments((prev) => prev.filter((item) => item.id !== id));
549
+ };
550
+
337
551
  async function handleDeleteConversation() {
338
552
  if (!convToDelete) return;
339
553
  try {
@@ -342,121 +556,103 @@ export default function McpChatPage() {
342
556
  method: 'DELETE',
343
557
  data: { ids: [convToDelete.id] },
344
558
  });
345
- toast.success('Conversation deleted');
559
+ toast.success(t('deleteConversationSuccess'));
346
560
  if (selectedConvId === convToDelete.id) {
347
561
  setSelectedConvId(null);
348
562
  setMessages([]);
563
+ setInput('');
349
564
  }
350
565
  setConvToDelete(null);
351
566
  await refetchConvs();
352
567
  } catch {
353
- toast.error('Failed to delete conversation');
568
+ toast.error(t('deleteConversationError'));
354
569
  }
355
570
  }
356
571
 
357
- const inputArea = (
358
- <div className="relative flex items-end gap-2 rounded-2xl bg-background px-5 py-4">
359
- <Textarea
360
- ref={textareaRef}
361
- className="resize-none flex-1 border-0 shadow-none focus-visible:ring-0 p-3 min-h-6 max-h-75 overflow-y-auto text-sm leading-relaxed bg-transparent"
362
- placeholder={t('placeholder')}
363
- value={input}
364
- disabled={isSending}
365
- onChange={(e) => setInput(e.target.value)}
366
- onKeyDown={(e) => {
367
- if (e.key === 'Enter' && !e.shiftKey) {
368
- e.preventDefault();
369
- void handleSend();
370
- }
371
- }}
372
- rows={1}
373
- />
374
- <Button
375
- size="icon"
376
- className="shrink-0 h-8 w-8 rounded-xl cursor-pointer"
377
- disabled={isSending || !input.trim()}
378
- onClick={() => void handleSend()}
379
- >
380
- <Send className="h-3.5 w-3.5" />
381
- </Button>
382
- </div>
572
+ const sidebarContent = (
573
+ <ConversationSidebar
574
+ conversations={convList}
575
+ selectedConvId={selectedConvId}
576
+ isLoading={isLoadingConversations}
577
+ emptyLabel={t('emptyConversations')}
578
+ conversationsLabel={t('conversations')}
579
+ newConversationLabel={t('newConversation')}
580
+ onCreate={() => {
581
+ void handleNewConversation();
582
+ }}
583
+ onSelect={handleSelectConversation}
584
+ onDelete={setConvToDelete}
585
+ />
383
586
  );
384
587
 
588
+ if (!mcpEnabled) {
589
+ return (
590
+ <div className="flex h-dvh min-h-0 items-center justify-center bg-background px-4">
591
+ <div className="flex max-w-md flex-col items-center gap-3 text-center">
592
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-muted text-muted-foreground ring-1 ring-border">
593
+ <Bot className="h-7 w-7" />
594
+ </div>
595
+ <h1 className="text-xl font-semibold tracking-tight">
596
+ {t('title')}
597
+ </h1>
598
+ <p className="text-sm text-muted-foreground">{t('mcpDisabled')}</p>
599
+ </div>
600
+ </div>
601
+ );
602
+ }
603
+
385
604
  return (
386
- <div className="flex h-screen overflow-hidden bg-background">
605
+ <div className="flex h-dvh min-h-0 overflow-hidden bg-background">
387
606
  {/* Sidebar */}
388
- <aside className="w-64 shrink-0 flex flex-col border-r bg-muted/30">
389
- <div className="p-3 border-b bg-background/50">
390
- <Button
391
- variant="outline"
392
- className="w-full justify-start gap-2 cursor-pointer rounded-xl h-9 text-sm font-medium"
393
- onClick={handleNewConversation}
394
- >
395
- <Plus className="h-4 w-4" />
396
- {t('newConversation')}
397
- </Button>
398
- </div>
399
- <div className="px-4 pt-4 pb-1.5">
400
- <p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
401
- {t('conversations')}
402
- </p>
403
- </div>
404
- <ScrollArea className="flex-1 px-2 pb-2">
405
- {convList.length === 0 ? (
406
- <div className="flex flex-col items-center gap-2 py-8 px-4 text-center">
407
- <MessageSquare className="h-8 w-8 text-muted-foreground/40" />
408
- <p className="text-xs text-muted-foreground">
409
- {t('emptyConversations')}
410
- </p>
411
- </div>
412
- ) : (
413
- <div className="space-y-0.5 pt-1">
414
- {convList.map((conv) => (
415
- <div
416
- key={conv.id}
417
- className={`group flex items-center justify-between rounded-xl px-3 py-2 cursor-pointer transition-colors text-sm ${
418
- selectedConvId === conv.id
419
- ? 'bg-primary text-primary-foreground'
420
- : 'hover:bg-accent text-foreground'
421
- }`}
422
- onClick={() => {
423
- setSelectedConvId(conv.id);
424
- setMessages([]);
425
- }}
426
- >
427
- <div className="flex items-center gap-2 min-w-0 flex-1">
428
- <MessageSquare className="h-3.5 w-3.5 shrink-0 opacity-60" />
429
- <span className="truncate text-xs font-medium">
430
- {conv.title || `#${conv.id}`}
431
- </span>
432
- </div>
433
- <Button
434
- variant="ghost"
435
- size="icon"
436
- className={`h-6 w-6 opacity-0 group-hover:opacity-100 shrink-0 cursor-pointer transition-opacity ${
437
- selectedConvId === conv.id
438
- ? 'hover:bg-primary-foreground/20 text-primary-foreground'
439
- : 'hover:bg-destructive/10 hover:text-destructive'
440
- }`}
441
- onClick={(e) => {
442
- e.stopPropagation();
443
- setConvToDelete(conv);
444
- }}
445
- >
446
- <Trash2 className="h-3 w-3" />
447
- </Button>
448
- </div>
449
- ))}
450
- </div>
451
- )}
452
- </ScrollArea>
607
+ <aside className="hidden md:flex w-64 shrink-0 flex-col border-r bg-muted/30">
608
+ {sidebarContent}
453
609
  </aside>
454
610
 
611
+ <Sheet
612
+ open={isMobileConversationsOpen}
613
+ onOpenChange={setIsMobileConversationsOpen}
614
+ >
615
+ <ResizableSheetContent
616
+ sheetId="core-mcp-chat-mobile-conversations-sheet"
617
+ defaultWidth={320}
618
+ minWidth={280}
619
+ maxWidth={560}
620
+ side="left"
621
+ className="w-[85vw] max-w-[320px] p-0 [&>button]:top-2 [&>button]:right-2 [&>button]:z-20"
622
+ >
623
+ <SheetHeader className="sr-only">
624
+ <SheetTitle>{t('conversations')}</SheetTitle>
625
+ <SheetDescription>{t('openConversations')}</SheetDescription>
626
+ </SheetHeader>
627
+ <div className="flex h-full flex-col bg-muted/30 pt-11">
628
+ {sidebarContent}
629
+ </div>
630
+ </ResizableSheetContent>
631
+ </Sheet>
632
+
455
633
  {/* Main area */}
456
- <main className="flex-1 flex flex-col overflow-hidden min-w-0">
634
+ <main className="flex-1 flex min-h-0 min-w-0 flex-col overflow-hidden">
635
+ {isMobile && (
636
+ <header className="h-12 shrink-0 border-b flex items-center px-3 gap-2 bg-background/90 backdrop-blur-sm md:hidden">
637
+ <SidebarTrigger className="h-8 w-8 cursor-pointer" />
638
+ <Button
639
+ variant="ghost"
640
+ size="icon"
641
+ className="h-8 w-8 cursor-pointer"
642
+ onClick={() => setIsMobileConversationsOpen(true)}
643
+ aria-label={t('openConversations')}
644
+ >
645
+ <MessageSquare className="h-4 w-4" />
646
+ </Button>
647
+ <span className="text-sm font-medium truncate">
648
+ {selectedConv?.title || t('title')}
649
+ </span>
650
+ </header>
651
+ )}
652
+
457
653
  {!selectedConvId ? (
458
654
  /* Empty state */
459
- <div className="flex flex-1 flex-col items-center justify-center gap-8 px-6">
655
+ <div className="flex flex-1 flex-col items-center justify-center gap-6 px-4 md:gap-8 md:px-6">
460
656
  <div className="flex flex-col items-center gap-3 text-center">
461
657
  <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10 ring-1 ring-primary/20">
462
658
  <Bot className="h-8 w-8 text-primary" />
@@ -468,13 +664,35 @@ export default function McpChatPage() {
468
664
  {t('description')}
469
665
  </p>
470
666
  </div>
471
- <div className="w-full max-w-2xl">{inputArea}</div>
667
+ <div className="w-full max-w-2xl px-1 pb-[max(0.5rem,env(safe-area-inset-bottom))] md:px-0 md:pb-0">
668
+ <ChatInput
669
+ textareaRef={textareaRef}
670
+ submitOnEnter={!isMobile}
671
+ input={input}
672
+ isSending={isSending}
673
+ isUploadingFile={isUploadingFile}
674
+ disabled={!mcpEnabled}
675
+ attachments={attachments}
676
+ placeholder={t('placeholder')}
677
+ sendLabel={t('send')}
678
+ sendingLabel={t('sending')}
679
+ attachLabel={t('attachFile')}
680
+ uploadingFileLabel={t('uploadingFile')}
681
+ removeAttachmentLabel={t('removeAttachment')}
682
+ onInputChange={setInput}
683
+ onSend={() => {
684
+ void handleSend();
685
+ }}
686
+ onUploadFiles={handleUploadFiles}
687
+ onRemoveAttachment={handleRemoveAttachment}
688
+ />
689
+ </div>
472
690
  </div>
473
691
  ) : (
474
692
  /* Chat state */
475
693
  <>
476
694
  {/* Header */}
477
- <header className="h-14 shrink-0 border-b flex items-center px-6 gap-3 bg-background/80 backdrop-blur-sm">
695
+ <header className="h-14 shrink-0 border-b items-center px-4 md:px-6 gap-3 bg-background/80 backdrop-blur-sm hidden md:flex">
478
696
  <div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
479
697
  <Bot className="h-4 w-4 text-primary" />
480
698
  </div>
@@ -489,60 +707,41 @@ export default function McpChatPage() {
489
707
  </header>
490
708
 
491
709
  {/* Messages */}
492
- <ScrollArea className="flex-1 min-h-0 px-4 py-6">
493
- <div className="flex flex-col gap-4 max-w-3xl mx-auto">
494
- {messages.map((msg, idx) => {
495
- if (msg.role === 'tool_call') {
496
- return (
497
- <ToolBadge
498
- key={msg.id ?? idx}
499
- message={msg}
500
- variant="call"
501
- />
502
- );
503
- }
504
- if (msg.role === 'tool_result') {
505
- return (
506
- <ToolBadge
507
- key={msg.id ?? idx}
508
- message={msg}
509
- variant="result"
510
- />
511
- );
512
- }
513
- if (msg.role === 'user') {
514
- return (
515
- <div key={msg.id ?? idx} className="flex justify-end">
516
- <div className="max-w-[75%] rounded-2xl rounded-tr-md bg-primary px-4 py-2.5 text-sm text-primary-foreground shadow-sm">
517
- {msg.content}
518
- </div>
519
- </div>
520
- );
521
- }
522
- return (
523
- <div key={msg.id ?? idx} className="flex items-start gap-3">
524
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted ring-2 ring-background shadow-sm">
525
- <Bot className="h-4 w-4 text-muted-foreground" />
526
- </div>
527
- <div className="max-w-[75%] rounded-2xl rounded-tl-md bg-muted px-4 py-2.5 text-sm shadow-sm prose-sm">
528
- <ReactMarkdown
529
- remarkPlugins={[remarkGfm]}
530
- components={markdownComponents as never}
531
- >
532
- {msg.content}
533
- </ReactMarkdown>
534
- </div>
535
- </div>
536
- );
537
- })}
538
- {isSending && <TypingIndicator />}
539
- <div ref={messagesEndRef} />
540
- </div>
710
+ <ScrollArea className="min-h-0 flex-1 px-3 py-4 md:px-4 md:py-6">
711
+ <MessageFeed
712
+ messages={messages}
713
+ isLoadingMessages={isLoadingMessages}
714
+ isSending={isSending}
715
+ isStreamingResponse={isStreamingResponse}
716
+ messagesEndRef={messagesEndRef}
717
+ />
541
718
  </ScrollArea>
542
719
 
543
720
  {/* Input */}
544
- <div className="shrink-0 border-t bg-background/80 backdrop-blur-sm px-6 py-4">
545
- <div className="max-w-3xl mx-auto">{inputArea}</div>
721
+ <div className="shrink-0 border-t bg-background/80 px-3 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] backdrop-blur-sm md:px-6 md:py-4">
722
+ <div className="max-w-3xl mx-auto">
723
+ <ChatInput
724
+ textareaRef={textareaRef}
725
+ submitOnEnter={!isMobile}
726
+ input={input}
727
+ isSending={isSending}
728
+ isUploadingFile={isUploadingFile}
729
+ disabled={!mcpEnabled}
730
+ attachments={attachments}
731
+ placeholder={t('placeholder')}
732
+ sendLabel={t('send')}
733
+ sendingLabel={t('sending')}
734
+ attachLabel={t('attachFile')}
735
+ uploadingFileLabel={t('uploadingFile')}
736
+ removeAttachmentLabel={t('removeAttachment')}
737
+ onInputChange={setInput}
738
+ onSend={() => {
739
+ void handleSend();
740
+ }}
741
+ onUploadFiles={handleUploadFiles}
742
+ onRemoveAttachment={handleRemoveAttachment}
743
+ />
744
+ </div>
546
745
  </div>
547
746
  </>
548
747
  )}