@nexttylabs/echo 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (579) hide show
  1. package/.changeset/README.md +21 -0
  2. package/.changeset/config.json +11 -0
  3. package/.changeset/cozy-ghosts-care.md +5 -0
  4. package/.changeset/sharp-lines-stand.md +5 -0
  5. package/.changeset/sour-doodles-eat.md +5 -0
  6. package/.changeset/tender-moose-shop.md +5 -0
  7. package/.github/pull_request_template.md +13 -0
  8. package/.github/workflows/ci.yml +41 -0
  9. package/.github/workflows/publish.yml +44 -0
  10. package/.github/workflows/release.yml +73 -0
  11. package/AGENTS.md +92 -0
  12. package/CHANGELOG.md +13 -0
  13. package/Dockerfile +57 -0
  14. package/LICENSE +661 -0
  15. package/Makefile +77 -0
  16. package/README.md +198 -0
  17. package/app/(auth)/login/page.tsx +53 -0
  18. package/app/(auth)/register/page.tsx +48 -0
  19. package/app/(auth)/sign-in/page.tsx +22 -0
  20. package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +103 -0
  21. package/app/(dashboard)/admin/feedback/[id]/page.tsx +154 -0
  22. package/app/(dashboard)/admin/feedback/new/page.tsx +91 -0
  23. package/app/(dashboard)/admin/feedback/page.tsx +81 -0
  24. package/app/(dashboard)/admin/layout.tsx +48 -0
  25. package/app/(dashboard)/analytics/portal/page.tsx +30 -0
  26. package/app/(dashboard)/dashboard/page.tsx +133 -0
  27. package/app/(dashboard)/layout.tsx +69 -0
  28. package/app/(dashboard)/no-access/page.tsx +45 -0
  29. package/app/(dashboard)/settings/access/page.tsx +56 -0
  30. package/app/(dashboard)/settings/api-keys/page.tsx +55 -0
  31. package/app/(dashboard)/settings/appearance/page.tsx +40 -0
  32. package/app/(dashboard)/settings/branding/page.tsx +62 -0
  33. package/app/(dashboard)/settings/changelog/page.tsx +51 -0
  34. package/app/(dashboard)/settings/danger-zone/page.tsx +92 -0
  35. package/app/(dashboard)/settings/feedback/page.tsx +63 -0
  36. package/app/(dashboard)/settings/integrations/page.tsx +94 -0
  37. package/app/(dashboard)/settings/layout.tsx +43 -0
  38. package/app/(dashboard)/settings/modules/page.tsx +54 -0
  39. package/app/(dashboard)/settings/notifications/page.tsx +48 -0
  40. package/app/(dashboard)/settings/organization/page.tsx +104 -0
  41. package/app/(dashboard)/settings/organization/portal/access/page.tsx +22 -0
  42. package/app/(dashboard)/settings/organization/portal/experience/page.tsx +22 -0
  43. package/app/(dashboard)/settings/organization/portal/growth/page.tsx +22 -0
  44. package/app/(dashboard)/settings/organization/portal/layout.tsx +24 -0
  45. package/app/(dashboard)/settings/organization/portal/page.tsx +22 -0
  46. package/app/(dashboard)/settings/organizations/[orgId]/members/page.tsx +69 -0
  47. package/app/(dashboard)/settings/organizations/new/page.tsx +36 -0
  48. package/app/(dashboard)/settings/page.tsx +22 -0
  49. package/app/(dashboard)/settings/portal-access/page.tsx +53 -0
  50. package/app/(dashboard)/settings/portal-branding/page.tsx +59 -0
  51. package/app/(dashboard)/settings/portal-growth/page.tsx +57 -0
  52. package/app/(dashboard)/settings/portal-modules/page.tsx +49 -0
  53. package/app/(dashboard)/settings/portal-resources/page.tsx +66 -0
  54. package/app/(dashboard)/settings/profile/page.tsx +48 -0
  55. package/app/(dashboard)/settings/widgets/page.tsx +63 -0
  56. package/app/(public)/[organizationSlug]/changelog/page.tsx +109 -0
  57. package/app/(public)/[organizationSlug]/feedback/[id]/page.tsx +146 -0
  58. package/app/(public)/[organizationSlug]/page.tsx +160 -0
  59. package/app/(public)/[organizationSlug]/roadmap/page.tsx +142 -0
  60. package/app/(public)/docs/page.tsx +48 -0
  61. package/app/(public)/feedback/[id]/not-found.tsx +33 -0
  62. package/app/(public)/feedback/[id]/page.tsx +102 -0
  63. package/app/(public)/invite/[token]/page.tsx +121 -0
  64. package/app/(public)/page.tsx +22 -0
  65. package/app/(public)/widget/[organizationId]/page.tsx +122 -0
  66. package/app/api/_utils.ts +29 -0
  67. package/app/api/admin/backup/route.ts +72 -0
  68. package/app/api/api-keys/[keyId]/route.ts +92 -0
  69. package/app/api/api-keys/route.ts +116 -0
  70. package/app/api/auth/[...all]/route.ts +21 -0
  71. package/app/api/auth/clear-session/route.ts +43 -0
  72. package/app/api/auth/register/handler.ts +176 -0
  73. package/app/api/auth/register/route.ts +26 -0
  74. package/app/api/docs/route.ts +28 -0
  75. package/app/api/feedback/[id]/comments/[commentId]/route.ts +105 -0
  76. package/app/api/feedback/[id]/comments/route.ts +421 -0
  77. package/app/api/feedback/[id]/duplicates/route.ts +285 -0
  78. package/app/api/feedback/[id]/handler.ts +91 -0
  79. package/app/api/feedback/[id]/processing-status/route.ts +199 -0
  80. package/app/api/feedback/[id]/reclassify/route.ts +145 -0
  81. package/app/api/feedback/[id]/route.ts +511 -0
  82. package/app/api/feedback/[id]/suggest-tags/route.ts +227 -0
  83. package/app/api/feedback/[id]/sync-github/route.ts +52 -0
  84. package/app/api/feedback/[id]/vote/route.ts +431 -0
  85. package/app/api/feedback/bulk/route.ts +212 -0
  86. package/app/api/feedback/handler.ts +138 -0
  87. package/app/api/feedback/route.ts +298 -0
  88. package/app/api/feedback/similar/route.ts +100 -0
  89. package/app/api/health/route.test.ts +64 -0
  90. package/app/api/health/route.ts +92 -0
  91. package/app/api/identify/jwt/route.ts +29 -0
  92. package/app/api/integrations/github/route.ts +196 -0
  93. package/app/api/internal/domain-lookup/route.ts +67 -0
  94. package/app/api/invitations/accept/handler.ts +101 -0
  95. package/app/api/invitations/accept/route.ts +29 -0
  96. package/app/api/notifications/preferences/route.ts +109 -0
  97. package/app/api/organizations/[orgId]/handler.ts +123 -0
  98. package/app/api/organizations/[orgId]/invitations/handler.ts +121 -0
  99. package/app/api/organizations/[orgId]/invitations/route.ts +29 -0
  100. package/app/api/organizations/[orgId]/members/[memberId]/handler.ts +208 -0
  101. package/app/api/organizations/[orgId]/members/[memberId]/route.ts +30 -0
  102. package/app/api/organizations/[orgId]/members/handler.ts +77 -0
  103. package/app/api/organizations/[orgId]/members/route.ts +29 -0
  104. package/app/api/organizations/[orgId]/route.ts +30 -0
  105. package/app/api/organizations/handler.ts +97 -0
  106. package/app/api/organizations/route.ts +29 -0
  107. package/app/api/tags/sync/route.ts +88 -0
  108. package/app/api/upload/handler.ts +79 -0
  109. package/app/api/upload/route.ts +37 -0
  110. package/app/api/v1/feedback/[id]/route.ts +276 -0
  111. package/app/api/v1/feedback/route.ts +250 -0
  112. package/app/api/v1/spec/route.ts +356 -0
  113. package/app/api/webhooks/[webhookId]/route.ts +213 -0
  114. package/app/api/webhooks/github/route.ts +158 -0
  115. package/app/api/webhooks/route.ts +143 -0
  116. package/app/favicon.ico +0 -0
  117. package/app/globals.css +139 -0
  118. package/app/health/route.ts +108 -0
  119. package/app/layout.tsx +60 -0
  120. package/bun.lock +2503 -0
  121. package/components/api/rate-limit-info.tsx +86 -0
  122. package/components/api-keys/api-key-manager.tsx +262 -0
  123. package/components/auth/login-form.tsx +207 -0
  124. package/components/auth/register-form.tsx +230 -0
  125. package/components/comment/comment-form.tsx +111 -0
  126. package/components/comment/internal-notes.tsx +219 -0
  127. package/components/comment/public-comments.tsx +387 -0
  128. package/components/component-example-client-only.tsx +29 -0
  129. package/components/component-example.tsx +519 -0
  130. package/components/dashboard/index.ts +22 -0
  131. package/components/dashboard/organization-switcher.tsx +96 -0
  132. package/components/dashboard/quick-actions.tsx +57 -0
  133. package/components/dashboard/recent-feedback-list.tsx +152 -0
  134. package/components/dashboard/stats-cards.tsx +88 -0
  135. package/components/dashboard/status-chart.tsx +106 -0
  136. package/components/example.tsx +70 -0
  137. package/components/feedback/attachment-list.tsx +103 -0
  138. package/components/feedback/auto-classification-badge.tsx +92 -0
  139. package/components/feedback/classification-override.tsx +64 -0
  140. package/components/feedback/duplicate-suggestions-inline.tsx +158 -0
  141. package/components/feedback/duplicate-suggestions.tsx +188 -0
  142. package/components/feedback/embedded-feedback-form.tsx +439 -0
  143. package/components/feedback/feedback-actions.tsx +160 -0
  144. package/components/feedback/feedback-bulk-actions.tsx +184 -0
  145. package/components/feedback/feedback-detail-view.tsx +321 -0
  146. package/components/feedback/feedback-detail.tsx +305 -0
  147. package/components/feedback/feedback-edit-form.tsx +131 -0
  148. package/components/feedback/feedback-filters.tsx +222 -0
  149. package/components/feedback/feedback-list-controls.tsx +433 -0
  150. package/components/feedback/feedback-list-item.tsx +298 -0
  151. package/components/feedback/feedback-list-skeleton.tsx +49 -0
  152. package/components/feedback/feedback-list.tsx +523 -0
  153. package/components/feedback/feedback-sorter.tsx +117 -0
  154. package/components/feedback/feedback-stats.tsx +124 -0
  155. package/components/feedback/file-upload.tsx +289 -0
  156. package/components/feedback/processing-status.tsx +161 -0
  157. package/components/feedback/status-history.tsx +134 -0
  158. package/components/feedback/status-selector.tsx +153 -0
  159. package/components/feedback/submit-on-behalf-form.tsx +403 -0
  160. package/components/feedback/tag-suggestions.tsx +212 -0
  161. package/components/feedback/vote-button.tsx +113 -0
  162. package/components/feedback/vote-list.tsx +108 -0
  163. package/components/integrations/github-config.tsx +200 -0
  164. package/components/landing/hero.tsx +150 -0
  165. package/components/layout/dashboard-layout.tsx +59 -0
  166. package/components/layout/index.ts +20 -0
  167. package/components/layout/language-switcher.tsx +129 -0
  168. package/components/layout/mobile-sidebar.tsx +66 -0
  169. package/components/layout/sidebar.tsx +279 -0
  170. package/components/portal/changelog-entry.tsx +132 -0
  171. package/components/portal/changelog-list.tsx +85 -0
  172. package/components/portal/contributor-badge.tsx +29 -0
  173. package/components/portal/contributors-sidebar.tsx +98 -0
  174. package/components/portal/create-post-dialog.tsx +247 -0
  175. package/components/portal/feedback-board.tsx +205 -0
  176. package/components/portal/feedback-post-card.tsx +198 -0
  177. package/components/portal/help-center.tsx +169 -0
  178. package/components/portal/leaderboard.tsx +29 -0
  179. package/components/portal/portal-header.tsx +153 -0
  180. package/components/portal/portal-layout.tsx +62 -0
  181. package/components/portal/portal-modules-panel.tsx +118 -0
  182. package/components/portal/portal-nav.tsx +59 -0
  183. package/components/portal/portal-overview.tsx +174 -0
  184. package/components/portal/portal-settings-nav.tsx +62 -0
  185. package/components/portal/portal-settings-shell.tsx +71 -0
  186. package/components/portal/portal-shell.tsx +62 -0
  187. package/components/portal/portal-tab-nav.tsx +77 -0
  188. package/components/portal/project-switcher.tsx +20 -0
  189. package/components/portal/roadmap-board.tsx +82 -0
  190. package/components/portal/roadmap-card.tsx +76 -0
  191. package/components/portal/roadmap-column.tsx +78 -0
  192. package/components/portal/settings-forms/access-form.tsx +194 -0
  193. package/components/portal/settings-forms/copy-form.tsx +95 -0
  194. package/components/portal/settings-forms/index.ts +23 -0
  195. package/components/portal/settings-forms/languages-form.tsx +223 -0
  196. package/components/portal/settings-forms/seo-form.tsx +156 -0
  197. package/components/portal/settings-forms/sharing-form.tsx +155 -0
  198. package/components/portal/settings-forms/theme-form.tsx +104 -0
  199. package/components/settings/api-keys-list.tsx +167 -0
  200. package/components/settings/appearance-form.tsx +71 -0
  201. package/components/settings/index.ts +25 -0
  202. package/components/settings/invite-member-form.tsx +119 -0
  203. package/components/settings/notification-preferences.tsx +174 -0
  204. package/components/settings/organization-form.tsx +165 -0
  205. package/components/settings/organization-members-list.tsx +197 -0
  206. package/components/settings/profile-form.tsx +124 -0
  207. package/components/settings/role-selector.tsx +57 -0
  208. package/components/settings/settings-sidebar.tsx +115 -0
  209. package/components/shared/pagination.tsx +215 -0
  210. package/components/ui/alert-dialog.tsx +201 -0
  211. package/components/ui/alert.tsx +75 -0
  212. package/components/ui/avatar.tsx +126 -0
  213. package/components/ui/badge.tsx +62 -0
  214. package/components/ui/button.tsx +77 -0
  215. package/components/ui/card.tsx +111 -0
  216. package/components/ui/combobox.tsx +311 -0
  217. package/components/ui/dialog.tsx +158 -0
  218. package/components/ui/dropdown-menu.tsx +272 -0
  219. package/components/ui/field.tsx +256 -0
  220. package/components/ui/input-group.tsx +164 -0
  221. package/components/ui/input.tsx +36 -0
  222. package/components/ui/label.tsx +41 -0
  223. package/components/ui/pagination.tsx +142 -0
  224. package/components/ui/select.tsx +202 -0
  225. package/components/ui/separator.tsx +45 -0
  226. package/components/ui/sheet.tsx +151 -0
  227. package/components/ui/skeleton.tsx +32 -0
  228. package/components/ui/switch.tsx +49 -0
  229. package/components/ui/table.tsx +118 -0
  230. package/components/ui/tabs.tsx +107 -0
  231. package/components/ui/textarea.tsx +35 -0
  232. package/components/ui/tooltip.tsx +78 -0
  233. package/components/widget/widget-form.tsx +439 -0
  234. package/components.json +24 -0
  235. package/db/init/01-init.sql +13 -0
  236. package/docker-compose.dev.yml +26 -0
  237. package/docker-compose.yml +98 -0
  238. package/docs/architecture.md +259 -0
  239. package/docs/component-inventory.md +261 -0
  240. package/docs/database-migrations.md +76 -0
  241. package/docs/development-guide.md +209 -0
  242. package/docs/e2e-user-flows.csv +31 -0
  243. package/docs/er-diagram-feedback.mmd +138 -0
  244. package/docs/er-diagram.mmd +281 -0
  245. package/docs/i18n-check-report.md +296 -0
  246. package/docs/index.md +214 -0
  247. package/docs/logic-chain.md +94 -0
  248. package/docs/plans/2026-01-02-database-migration-scripts.md +496 -0
  249. package/docs/plans/2026-01-02-user-login-design.md +37 -0
  250. package/docs/plans/2026-01-02-user-login.md +437 -0
  251. package/docs/plans/2026-01-02-user-registration-design.md +47 -0
  252. package/docs/plans/2026-01-02-user-registration.md +628 -0
  253. package/docs/plans/2026-01-03-roles-permissions-design.md +20 -0
  254. package/docs/plans/2026-01-03-roles-permissions.md +266 -0
  255. package/docs/plans/2026-01-05-authentication-middleware.md +207 -0
  256. package/docs/plans/2026-01-05-member-removal.md +186 -0
  257. package/docs/plans/2026-01-05-organization-creation.md +374 -0
  258. package/docs/plans/2026-01-05-rbac-middleware.md +112 -0
  259. package/docs/plans/2026-01-05-role-configuration.md +441 -0
  260. package/docs/plans/2026-01-06-file-upload-support.md +804 -0
  261. package/docs/plans/2026-01-06-permission-check-hook.md +155 -0
  262. package/docs/plans/2026-01-06-resource-ownership-check.md +231 -0
  263. package/docs/plans/2026-01-07-feedback-tracking-link.md +459 -0
  264. package/docs/plans/2026-01-09-logout-redirect-design.md +52 -0
  265. package/docs/plans/2026-01-09-phase2-3-plan.md +654 -0
  266. package/docs/plans/2026-01-09-portal-execution-plan.md +408 -0
  267. package/docs/plans/2026-01-09-project-delete-feature-design.md +163 -0
  268. package/docs/plans/2026-01-09-project-delete-implementation.md +451 -0
  269. package/docs/plans/2026-01-09-project-edit-delete-design.md +52 -0
  270. package/docs/plans/2026-01-09-settings-center-design.md +114 -0
  271. package/docs/plans/2026-01-09-settings-center.md +948 -0
  272. package/docs/plans/2026-01-10-organization-only-design.md +66 -0
  273. package/docs/plans/2026-01-10-organization-only-implementation.md +433 -0
  274. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +18 -0
  275. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +296 -0
  276. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +173 -0
  277. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +82 -0
  278. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +521 -0
  279. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +75 -0
  280. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +293 -0
  281. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +180 -0
  282. package/docs/plans/2026-01-16-e2e-test-fixes.md +158 -0
  283. package/docs/plans/2026-01-17-admin-feedback-filters.md +214 -0
  284. package/docs/plans/2026-01-17-admin-feedback-improvements.md +453 -0
  285. package/docs/plans/2026-01-18-changesets-design.md +40 -0
  286. package/docs/product_changes.md +37 -0
  287. package/docs/project-overview.md +159 -0
  288. package/docs/project-scan-report.json +104 -0
  289. package/docs/route-role-visibility.md +51 -0
  290. package/docs/source-tree-analysis.md +150 -0
  291. package/docs/testing/delete-project-manual-tests.md +18 -0
  292. package/docs/user-story-tracking.md +191 -0
  293. package/drizzle.config.ts +32 -0
  294. package/eslint.config.mjs +19 -0
  295. package/hooks/use-permissions.ts +56 -0
  296. package/i18n/config.ts +45 -0
  297. package/i18n/request.ts +28 -0
  298. package/i18n/resolve-locale.ts +38 -0
  299. package/lib/api/errors.ts +62 -0
  300. package/lib/auth/cli-config.ts +35 -0
  301. package/lib/auth/client.ts +20 -0
  302. package/lib/auth/config.ts +55 -0
  303. package/lib/auth/jwt-identity.ts +21 -0
  304. package/lib/auth/org-context.ts +71 -0
  305. package/lib/auth/organization.ts +107 -0
  306. package/lib/auth/permissions.ts +87 -0
  307. package/lib/auth/session.ts +23 -0
  308. package/lib/config/rate-limits.ts +64 -0
  309. package/lib/dashboard/get-dashboard-stats.ts +136 -0
  310. package/lib/db/index.ts +41 -0
  311. package/lib/db/migrate.test.ts +49 -0
  312. package/lib/db/migrate.ts +62 -0
  313. package/lib/db/migrations/.gitkeep +0 -0
  314. package/lib/db/migrations/0000_cynical_gladiator.sql +53 -0
  315. package/lib/db/migrations/0001_wandering_sunfire.sql +27 -0
  316. package/lib/db/migrations/0002_shallow_speedball.sql +1 -0
  317. package/lib/db/migrations/0003_add_org_description.sql +1 -0
  318. package/lib/db/migrations/0003_boring_wild_pack.sql +13 -0
  319. package/lib/db/migrations/0004_windy_tyrannus.sql +27 -0
  320. package/lib/db/migrations/0005_perpetual_doorman.sql +5 -0
  321. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +13 -0
  322. package/lib/db/migrations/0007_clever_captain_cross.sql +14 -0
  323. package/lib/db/migrations/0008_sparkling_pandemic.sql +2 -0
  324. package/lib/db/migrations/0009_happy_black_tom.sql +29 -0
  325. package/lib/db/migrations/0010_kind_junta.sql +8 -0
  326. package/lib/db/migrations/0011_mute_squadron_supreme.sql +25 -0
  327. package/lib/db/migrations/0012_giant_power_man.sql +24 -0
  328. package/lib/db/migrations/0013_damp_titanium_man.sql +17 -0
  329. package/lib/db/migrations/0014_blue_alice.sql +18 -0
  330. package/lib/db/migrations/0015_webhook_tables.sql +41 -0
  331. package/lib/db/migrations/0016_github_integration.sql +30 -0
  332. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +22 -0
  333. package/lib/db/migrations/0017_slimy_inhumans.sql +6 -0
  334. package/lib/db/migrations/0018_same_spitfire.sql +1 -0
  335. package/lib/db/migrations/0019_jittery_loners.sql +16 -0
  336. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +14 -0
  337. package/lib/db/migrations/meta/0000_snapshot.json +374 -0
  338. package/lib/db/migrations/meta/0001_snapshot.json +553 -0
  339. package/lib/db/migrations/meta/0002_snapshot.json +560 -0
  340. package/lib/db/migrations/meta/0003_snapshot.json +650 -0
  341. package/lib/db/migrations/meta/0004_snapshot.json +852 -0
  342. package/lib/db/migrations/meta/0005_snapshot.json +900 -0
  343. package/lib/db/migrations/meta/0006_snapshot.json +1011 -0
  344. package/lib/db/migrations/meta/0007_snapshot.json +1125 -0
  345. package/lib/db/migrations/meta/0008_snapshot.json +1146 -0
  346. package/lib/db/migrations/meta/0009_snapshot.json +1386 -0
  347. package/lib/db/migrations/meta/0010_snapshot.json +1419 -0
  348. package/lib/db/migrations/meta/0011_snapshot.json +1615 -0
  349. package/lib/db/migrations/meta/0012_snapshot.json +1805 -0
  350. package/lib/db/migrations/meta/0013_snapshot.json +1948 -0
  351. package/lib/db/migrations/meta/0014_snapshot.json +2082 -0
  352. package/lib/db/migrations/meta/0015_snapshot.json +2476 -0
  353. package/lib/db/migrations/meta/0016_snapshot.json +2633 -0
  354. package/lib/db/migrations/meta/0017_snapshot.json +2680 -0
  355. package/lib/db/migrations/meta/0018_snapshot.json +2686 -0
  356. package/lib/db/migrations/meta/0019_snapshot.json +2741 -0
  357. package/lib/db/migrations/meta/_journal.json +146 -0
  358. package/lib/db/schema/ai-processing.ts +90 -0
  359. package/lib/db/schema/api-keys.ts +61 -0
  360. package/lib/db/schema/attachments.ts +48 -0
  361. package/lib/db/schema/auth.ts +111 -0
  362. package/lib/db/schema/comments.ts +74 -0
  363. package/lib/db/schema/duplicates.ts +80 -0
  364. package/lib/db/schema/feedback.ts +88 -0
  365. package/lib/db/schema/github-integrations.ts +66 -0
  366. package/lib/db/schema/index.ts +35 -0
  367. package/lib/db/schema/invitations.ts +32 -0
  368. package/lib/db/schema/notifications.ts +85 -0
  369. package/lib/db/schema/organization-members.ts +37 -0
  370. package/lib/db/schema/organization-settings.ts +134 -0
  371. package/lib/db/schema/organizations.ts +30 -0
  372. package/lib/db/schema/projects.ts +145 -0
  373. package/lib/db/schema/status-history.ts +63 -0
  374. package/lib/db/schema/tags.ts +194 -0
  375. package/lib/db/schema/user-profiles.ts +31 -0
  376. package/lib/db/schema/votes.ts +60 -0
  377. package/lib/db/schema/webhooks.ts +106 -0
  378. package/lib/feedback/filters.ts +28 -0
  379. package/lib/feedback/find-similar.ts +49 -0
  380. package/lib/feedback/get-feedback-by-id.ts +159 -0
  381. package/lib/feedback/prefill.ts +51 -0
  382. package/lib/http/get-request-url.ts +28 -0
  383. package/lib/integrations/github.ts +159 -0
  384. package/lib/invitations.ts +22 -0
  385. package/lib/logger.test.ts +31 -0
  386. package/lib/logger.ts +58 -0
  387. package/lib/middleware/api-key.ts +126 -0
  388. package/lib/middleware/rate-limit-keys.ts +47 -0
  389. package/lib/middleware/rate-limit.ts +148 -0
  390. package/lib/middleware/rbac.ts +39 -0
  391. package/lib/middleware/request-id.test.ts +28 -0
  392. package/lib/middleware/request-id.ts +30 -0
  393. package/lib/middleware/request-logger.test.ts +36 -0
  394. package/lib/middleware/request-logger.ts +41 -0
  395. package/lib/middleware/with-rate-limit.ts +33 -0
  396. package/lib/portal/analytics.ts +20 -0
  397. package/lib/portal/contributors.ts +27 -0
  398. package/lib/portal/i18n.ts +20 -0
  399. package/lib/portal/leaderboard-settings.ts +20 -0
  400. package/lib/portal/modules.ts +20 -0
  401. package/lib/portal/portal-copy.ts +20 -0
  402. package/lib/portal/public-context.tsx +110 -0
  403. package/lib/portal/seo.ts +20 -0
  404. package/lib/portal/settings-context.ts +56 -0
  405. package/lib/portal/sharing.ts +20 -0
  406. package/lib/portal/sorting.ts +20 -0
  407. package/lib/portal/theme.ts +20 -0
  408. package/lib/services/ai/classifier.ts +296 -0
  409. package/lib/services/ai/duplicate-detector.ts +255 -0
  410. package/lib/services/ai/tag-suggester.ts +108 -0
  411. package/lib/services/api-keys.ts +164 -0
  412. package/lib/services/backup.ts +173 -0
  413. package/lib/services/email/templates.ts +158 -0
  414. package/lib/services/email.ts +68 -0
  415. package/lib/services/github-sync.ts +205 -0
  416. package/lib/services/notifications/index.ts +224 -0
  417. package/lib/services/portal-settings.ts +157 -0
  418. package/lib/swagger/config.ts +296 -0
  419. package/lib/swagger/generate.ts +400 -0
  420. package/lib/upload/file-validator.ts +52 -0
  421. package/lib/upload/storage.ts +59 -0
  422. package/lib/utils/format.ts +26 -0
  423. package/lib/utils/slug.ts +28 -0
  424. package/lib/utils.ts +23 -0
  425. package/lib/validations/auth.ts +56 -0
  426. package/lib/validations/comment.ts +44 -0
  427. package/lib/validations/feedback.ts +51 -0
  428. package/lib/validations/invitations.ts +23 -0
  429. package/lib/validations/organizations.ts +34 -0
  430. package/lib/validations/projects.ts +49 -0
  431. package/lib/validators/feedback.ts +57 -0
  432. package/lib/validators/index.ts +18 -0
  433. package/lib/webhooks/events.ts +73 -0
  434. package/lib/webhooks/index.ts +21 -0
  435. package/lib/webhooks/retry.ts +188 -0
  436. package/lib/webhooks/sender.ts +183 -0
  437. package/lib/webhooks/verify.ts +37 -0
  438. package/lib/workers/feedback-processor.ts +255 -0
  439. package/messages/en.json +965 -0
  440. package/messages/jp.json +862 -0
  441. package/messages/zh-CN.json +855 -0
  442. package/next-env.d.ts +6 -0
  443. package/next.config.ts +66 -0
  444. package/package.json +84 -0
  445. package/playwright.config.ts +44 -0
  446. package/postcss.config.mjs +7 -0
  447. package/proxy.test.ts +131 -0
  448. package/proxy.ts +190 -0
  449. package/public/file.svg +1 -0
  450. package/public/globe.svg +1 -0
  451. package/public/logo-64.svg +5 -0
  452. package/public/logo.svg +5 -0
  453. package/public/next.svg +1 -0
  454. package/public/openapi.json +673 -0
  455. package/public/uploads/.gitkeep +0 -0
  456. package/public/uploads/02695701-ded0-4c81-8a21-9326c1d65448.pdf +1 -0
  457. package/public/uploads/178843ea-2780-48ef-8988-f4cba442e4cb.pdf +1 -0
  458. package/public/uploads/24b0a9ef-da93-49da-934f-637f89c7871d.pdf +1 -0
  459. package/public/uploads/7a11626d-a8e4-4b91-a8eb-20b6213b0a5a.pdf +1 -0
  460. package/public/uploads/b0703f4d-6e7b-4aab-8191-1a7b15f1b8ee.pdf +1 -0
  461. package/public/uploads/c8de0aed-4d3a-44aa-83bb-6594b7a2ddb3.pdf +1 -0
  462. package/public/uploads/e4cce295-0d85-4525-a1b0-a61c45722e26.pdf +1 -0
  463. package/public/uploads/eb4df45e-563c-48b8-9c68-c18212312426.pdf +1 -0
  464. package/public/vercel.svg +1 -0
  465. package/public/widget/embed.js +249 -0
  466. package/public/window.svg +1 -0
  467. package/scripts/backup-db.sh +57 -0
  468. package/scripts/backup-db.ts +24 -0
  469. package/scripts/generate-openapi.ts +22 -0
  470. package/scripts/migration-helper.ts +39 -0
  471. package/scripts/pre-deploy.ts +75 -0
  472. package/scripts/restore-db.sh +60 -0
  473. package/scripts/rollback.ts +72 -0
  474. package/scripts/seed-tags.ts +48 -0
  475. package/tests/api/feedback-bulk.test.ts +47 -0
  476. package/tests/api/feedback-by-id.test.ts +67 -0
  477. package/tests/api/feedback-comments-route-import.test.ts +26 -0
  478. package/tests/api/feedback-create.test.ts +71 -0
  479. package/tests/api/feedback-delete.test.ts +160 -0
  480. package/tests/api/feedback-filter.test.ts +250 -0
  481. package/tests/api/feedback-list.test.ts +234 -0
  482. package/tests/api/feedback-route-assignee-condition.test.ts +32 -0
  483. package/tests/api/feedback-similar.test.ts +46 -0
  484. package/tests/api/feedback-sort.test.ts +261 -0
  485. package/tests/api/feedback-status-enum.test.ts +49 -0
  486. package/tests/api/feedback-status-filter.test.ts +117 -0
  487. package/tests/api/feedback-submit-on-behalf.test.ts +269 -0
  488. package/tests/api/feedback.test.ts +175 -0
  489. package/tests/api/identify-jwt.test.ts +25 -0
  490. package/tests/api/invitation-accept.test.ts +213 -0
  491. package/tests/api/organization-invitations.test.ts +186 -0
  492. package/tests/api/organization-members-list.test.ts +79 -0
  493. package/tests/api/organization-members.test.ts +340 -0
  494. package/tests/api/organizations.test.ts +149 -0
  495. package/tests/api/register.test.ts +112 -0
  496. package/tests/api/upload.test.ts +103 -0
  497. package/tests/api/vote.test.ts +82 -0
  498. package/tests/app/admin-feedback-detail-page.test.tsx +25 -0
  499. package/tests/app/admin-feedback-list-page.test.tsx +25 -0
  500. package/tests/app/admin-feedback-new-page.test.tsx +25 -0
  501. package/tests/app/health-route-helpers.test.ts +27 -0
  502. package/tests/app/login-page.test.ts +26 -0
  503. package/tests/app/portal-page.test.ts +29 -0
  504. package/tests/app/project-portal-overview.test.tsx +25 -0
  505. package/tests/app/widget-page-import.test.ts +25 -0
  506. package/tests/components/create-post-dialog-defaults.test.ts +43 -0
  507. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +27 -0
  508. package/tests/components/feedback/embedded-feedback-form.test.tsx +96 -0
  509. package/tests/components/feedback/feedback-detail.test.tsx +25 -0
  510. package/tests/components/feedback/feedback-stats.test.tsx +49 -0
  511. package/tests/components/feedback-bulk-actions.test.tsx +39 -0
  512. package/tests/components/feedback-i18n-keys.test.ts +70 -0
  513. package/tests/components/feedback-list-controls-compile.test.ts +25 -0
  514. package/tests/components/feedback-list-controls.test.tsx +204 -0
  515. package/tests/components/feedback-list-item.test.tsx +67 -0
  516. package/tests/components/landing/hero.test.tsx +46 -0
  517. package/tests/components/layout/language-switcher.test.tsx +25 -0
  518. package/tests/components/layout/sidebar.test.tsx +157 -0
  519. package/tests/components/login-form.test.ts +25 -0
  520. package/tests/components/organization-form.test.ts +32 -0
  521. package/tests/components/organization-switcher.test.ts +25 -0
  522. package/tests/components/pagination.test.tsx +43 -0
  523. package/tests/components/portal-overview.test.tsx +25 -0
  524. package/tests/components/profile-form.test.tsx +139 -0
  525. package/tests/components/role-selector.test.ts +31 -0
  526. package/tests/components/status-chart.test.tsx +90 -0
  527. package/tests/e2e/auth.e2e.ts +323 -0
  528. package/tests/e2e/feedback-actions.e2e.ts +471 -0
  529. package/tests/e2e/feedback-attachment.e2e.ts +168 -0
  530. package/tests/e2e/feedback-customer.e2e.ts +226 -0
  531. package/tests/e2e/feedback-management.e2e.ts +565 -0
  532. package/tests/e2e/feedback-submit.e2e.ts +133 -0
  533. package/tests/e2e/feedback-view.e2e.ts +297 -0
  534. package/tests/e2e/fixtures/test-data.ts +235 -0
  535. package/tests/e2e/health-check.e2e.ts +230 -0
  536. package/tests/e2e/helpers/test-utils-helpers.test.ts +43 -0
  537. package/tests/e2e/helpers/test-utils.ts +298 -0
  538. package/tests/e2e/integration-placeholders.e2e.ts +199 -0
  539. package/tests/e2e/organization.e2e.ts +292 -0
  540. package/tests/e2e/permissions.e2e.ts +424 -0
  541. package/tests/e2e/project-widget.e2e.ts +63 -0
  542. package/tests/feedback/filters.test.ts +29 -0
  543. package/tests/hooks/use-permissions.test.ts +52 -0
  544. package/tests/lib/ai/classifier.test.ts +104 -0
  545. package/tests/lib/ai/duplicate-detector.test.ts +234 -0
  546. package/tests/lib/attachments-schema.test.ts +30 -0
  547. package/tests/lib/auth/session.test.ts +49 -0
  548. package/tests/lib/auth-client.test.ts +37 -0
  549. package/tests/lib/auth-config.test.ts +26 -0
  550. package/tests/lib/feedback-prefill.test.ts +52 -0
  551. package/tests/lib/feedback-processor.test.ts +41 -0
  552. package/tests/lib/feedback-schema.test.ts +33 -0
  553. package/tests/lib/file-validator.test.ts +48 -0
  554. package/tests/lib/get-feedback-by-id.test.ts +37 -0
  555. package/tests/lib/invitations.test.ts +35 -0
  556. package/tests/lib/login-schema.test.ts +36 -0
  557. package/tests/lib/org-context.test.ts +95 -0
  558. package/tests/lib/organization-access.test.ts +44 -0
  559. package/tests/lib/organization-member-role-schema.test.ts +41 -0
  560. package/tests/lib/permissions.test.ts +88 -0
  561. package/tests/lib/portal-analytics.test.ts +25 -0
  562. package/tests/lib/portal-contributors.test.ts +25 -0
  563. package/tests/lib/portal-copy.test.ts +27 -0
  564. package/tests/lib/portal-i18n.test.ts +30 -0
  565. package/tests/lib/portal-leaderboard-settings.test.ts +25 -0
  566. package/tests/lib/portal-modules.test.ts +25 -0
  567. package/tests/lib/portal-seo.test.ts +25 -0
  568. package/tests/lib/portal-sharing.test.ts +25 -0
  569. package/tests/lib/portal-sorting.test.ts +25 -0
  570. package/tests/lib/portal-theme.test.ts +25 -0
  571. package/tests/lib/rate-limit.test.ts +142 -0
  572. package/tests/lib/resolve-locale.test.ts +34 -0
  573. package/tests/lib/services/backup.test.ts +145 -0
  574. package/tests/lib/user-organizations.test.ts +42 -0
  575. package/tests/lib/user-role-schema.test.ts +33 -0
  576. package/tests/lib/user-schema.test.ts +25 -0
  577. package/tests/setup.ts +74 -0
  578. package/tsconfig.json +34 -0
  579. package/types/bun-test.d.ts +31 -0
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+
4
+ /*
5
+ * Copyright (c) 2026 Echo Team
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU Affero General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU Affero General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ import { useEffect, useState } from "react";
22
+
23
+ interface StatCardProps {
24
+ title: string;
25
+ value: number;
26
+ colorClass: string;
27
+ }
28
+
29
+ function StatCard({ title, value, colorClass }: StatCardProps) {
30
+ return (
31
+ <div className={`p-4 rounded-lg border ${colorClass}`}>
32
+ <p className="text-sm text-muted-foreground">{title}</p>
33
+ <p className="text-2xl font-bold mt-1">{value}</p>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ interface FeedbackStatsProps {
39
+ organizationId: string;
40
+ }
41
+
42
+ interface FeedbackItem {
43
+ status: string;
44
+ type: string;
45
+ }
46
+
47
+ export function FeedbackStats({ organizationId }: FeedbackStatsProps) {
48
+ const [stats, setStats] = useState<{
49
+ total: number;
50
+ byStatus: Record<string, number>;
51
+ byType: Record<string, number>;
52
+ } | null>(null);
53
+
54
+ useEffect(() => {
55
+ const fetchStats = async () => {
56
+ try {
57
+ const params = new URLSearchParams({
58
+ organizationId,
59
+ pageSize: "100",
60
+ });
61
+
62
+ const response = await fetch(`/api/feedback?${params}`);
63
+ if (response.ok) {
64
+ const result = await response.json();
65
+ const byStatus: Record<string, number> = {};
66
+ const byType: Record<string, number> = {};
67
+
68
+ result.data.forEach((f: FeedbackItem) => {
69
+ byStatus[f.status] = (byStatus[f.status] || 0) + 1;
70
+ byType[f.type] = (byType[f.type] || 0) + 1;
71
+ });
72
+
73
+ setStats({
74
+ total: result.total,
75
+ byStatus,
76
+ byType,
77
+ });
78
+ }
79
+ } catch (error) {
80
+ console.error("Failed to fetch stats:", error);
81
+ }
82
+ };
83
+
84
+ fetchStats();
85
+ }, [organizationId]);
86
+
87
+ if (!stats) {
88
+ return (
89
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
90
+ {[1, 2, 3, 4].map((i) => (
91
+ <div
92
+ key={i}
93
+ className="p-4 rounded-lg border animate-pulse bg-muted h-20"
94
+ />
95
+ ))}
96
+ </div>
97
+ );
98
+ }
99
+
100
+ return (
101
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
102
+ <StatCard
103
+ title="总反馈数"
104
+ value={stats.total}
105
+ colorClass="border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950"
106
+ />
107
+ <StatCard
108
+ title="新接收"
109
+ value={stats.byStatus["new"] || 0}
110
+ colorClass="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950"
111
+ />
112
+ <StatCard
113
+ title="处理中"
114
+ value={stats.byStatus["in-progress"] || 0}
115
+ colorClass="border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950"
116
+ />
117
+ <StatCard
118
+ title="Bug"
119
+ value={stats.byType["bug"] || 0}
120
+ colorClass="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950"
121
+ />
122
+ </div>
123
+ );
124
+ }
@@ -0,0 +1,289 @@
1
+ "use client";
2
+
3
+
4
+ /*
5
+ * Copyright (c) 2026 Echo Team
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU Affero General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU Affero General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ import { useCallback, useEffect, useMemo, useState } from "react";
22
+ import Image from "next/image";
23
+ import { useTranslations } from "next-intl";
24
+ import { Upload, File as FileIcon, X } from "lucide-react";
25
+ import { Button } from "@/components/ui/button";
26
+ import { cn } from "@/lib/utils";
27
+
28
+ type UploadStatus = "pending" | "uploading" | "success" | "error";
29
+
30
+ interface UploadedFile {
31
+ file: File;
32
+ preview?: string;
33
+ status: UploadStatus;
34
+ attachmentId?: number;
35
+ }
36
+
37
+ interface FileUploadProps {
38
+ feedbackId?: number;
39
+ onFilesSelected?: (count: number) => void;
40
+ onUploaded?: (ids: number[]) => void;
41
+ maxSize?: number;
42
+ accept?: string;
43
+ }
44
+
45
+ export function FileUpload({
46
+ feedbackId,
47
+ onFilesSelected,
48
+ onUploaded,
49
+ maxSize = 5 * 1024 * 1024,
50
+ accept = "image/png,image/jpeg,image/gif,application/pdf",
51
+ }: FileUploadProps) {
52
+ const t = useTranslations("feedback.fileUpload");
53
+ const [files, setFiles] = useState<UploadedFile[]>([]);
54
+ const [isDragging, setIsDragging] = useState(false);
55
+
56
+ const allowedTypes = useMemo(() => accept.split(","), [accept]);
57
+ const maxSizeMb = useMemo(() => Math.round(maxSize / 1024 / 1024), [maxSize]);
58
+
59
+ const notifySelection = useCallback(
60
+ (nextFiles: UploadedFile[]) => {
61
+ onFilesSelected?.(nextFiles.length);
62
+ },
63
+ [onFilesSelected],
64
+ );
65
+
66
+ const validateFile = useCallback(
67
+ (file: File): string | null => {
68
+ if (file.size > maxSize) {
69
+ return t("errors.size", { maxMb: maxSizeMb });
70
+ }
71
+ if (!allowedTypes.includes(file.type)) {
72
+ return t("errors.type");
73
+ }
74
+ return null;
75
+ },
76
+ [allowedTypes, maxSize, maxSizeMb, t],
77
+ );
78
+
79
+ const handleFiles = useCallback(
80
+ (newFiles: File[]) => {
81
+ const validFiles: UploadedFile[] = [];
82
+
83
+ for (const file of newFiles) {
84
+ const error = validateFile(file);
85
+ if (error) {
86
+ alert(error);
87
+ continue;
88
+ }
89
+
90
+ const preview = file.type.startsWith("image/")
91
+ ? URL.createObjectURL(file)
92
+ : undefined;
93
+
94
+ validFiles.push({ file, preview, status: "pending" });
95
+ }
96
+
97
+ setFiles((prev) => {
98
+ const nextFiles = [...prev, ...validFiles];
99
+ notifySelection(nextFiles);
100
+ return nextFiles;
101
+ });
102
+ },
103
+ [notifySelection, validateFile],
104
+ );
105
+
106
+ useEffect(() => {
107
+ return () => {
108
+ files.forEach((file) => {
109
+ if (file.preview) {
110
+ URL.revokeObjectURL(file.preview);
111
+ }
112
+ });
113
+ };
114
+ }, [files]);
115
+
116
+ const uploadFiles = async () => {
117
+ if (!feedbackId) {
118
+ alert(t("errors.missingFeedbackId"));
119
+ return;
120
+ }
121
+
122
+ const pending = files.filter((file) => file.status === "pending");
123
+
124
+ for (const fileItem of pending) {
125
+ setFiles((prev) =>
126
+ prev.map((item) =>
127
+ item === fileItem ? { ...item, status: "uploading" } : item,
128
+ ),
129
+ );
130
+
131
+ try {
132
+ const formData = new FormData();
133
+ formData.append("files", fileItem.file);
134
+ formData.append("feedbackId", String(feedbackId));
135
+
136
+ const response = await fetch("/api/upload", {
137
+ method: "POST",
138
+ body: formData,
139
+ });
140
+
141
+ if (!response.ok) {
142
+ setFiles((prev) =>
143
+ prev.map((item) =>
144
+ item === fileItem ? { ...item, status: "error" } : item,
145
+ ),
146
+ );
147
+ continue;
148
+ }
149
+
150
+ const result = await response.json();
151
+ const attachmentId = result.data?.[0]?.attachmentId as number | undefined;
152
+
153
+ setFiles((prev) =>
154
+ prev.map((item) =>
155
+ item === fileItem
156
+ ? { ...item, status: "success", attachmentId }
157
+ : item,
158
+ ),
159
+ );
160
+ } catch {
161
+ setFiles((prev) =>
162
+ prev.map((item) =>
163
+ item === fileItem ? { ...item, status: "error" } : item,
164
+ ),
165
+ );
166
+ }
167
+ }
168
+
169
+ const uploadedIds = files
170
+ .filter((file) => file.attachmentId !== undefined)
171
+ .map((file) => file.attachmentId as number);
172
+
173
+ onUploaded?.(uploadedIds);
174
+ };
175
+
176
+ const removeFile = (fileItem: UploadedFile) => {
177
+ setFiles((prev) => {
178
+ const nextFiles = prev.filter((file) => file !== fileItem);
179
+ notifySelection(nextFiles);
180
+ return nextFiles;
181
+ });
182
+ if (fileItem.preview) {
183
+ URL.revokeObjectURL(fileItem.preview);
184
+ }
185
+ };
186
+
187
+ return (
188
+ <div className="space-y-4">
189
+ <div
190
+ onDragOver={(event) => {
191
+ event.preventDefault();
192
+ setIsDragging(true);
193
+ }}
194
+ onDragLeave={() => setIsDragging(false)}
195
+ onDrop={(event) => {
196
+ event.preventDefault();
197
+ setIsDragging(false);
198
+ handleFiles(Array.from(event.dataTransfer.files));
199
+ }}
200
+ className={cn(
201
+ "border-2 border-dashed rounded-lg p-6 text-center transition-colors",
202
+ isDragging
203
+ ? "border-primary bg-primary/5"
204
+ : "border-muted-foreground/25 hover:border-muted-foreground/50",
205
+ )}
206
+ >
207
+ <input
208
+ id="file-upload"
209
+ type="file"
210
+ multiple
211
+ accept={accept}
212
+ onChange={(event) =>
213
+ handleFiles(Array.from(event.target.files ?? []))
214
+ }
215
+ className="hidden"
216
+ />
217
+ <label htmlFor="file-upload" className="cursor-pointer">
218
+ <Upload className="mx-auto h-10 w-10 text-muted-foreground mb-2" />
219
+ <p className="text-sm text-muted-foreground">
220
+ {t("dropzone.title")}
221
+ </p>
222
+ <p className="text-xs text-muted-foreground mt-1">
223
+ {t("dropzone.subtitle", { maxMb: maxSizeMb })}
224
+ </p>
225
+ </label>
226
+ </div>
227
+
228
+ {files.length > 0 && (
229
+ <div className="space-y-2">
230
+ {files.map((fileItem) => (
231
+ <div
232
+ key={`${fileItem.file.name}-${fileItem.file.size}`}
233
+ className="flex items-center gap-3 p-3 border rounded-lg"
234
+ >
235
+ {fileItem.preview ? (
236
+ <Image
237
+ src={fileItem.preview}
238
+ alt={fileItem.file.name}
239
+ width={48}
240
+ height={48}
241
+ unoptimized
242
+ className="w-12 h-12 object-cover rounded"
243
+ />
244
+ ) : (
245
+ <div className="w-12 h-12 bg-muted rounded flex items-center justify-center">
246
+ <FileIcon className="w-6 h-6 text-muted-foreground" />
247
+ </div>
248
+ )}
249
+
250
+ <div className="flex-1 min-w-0">
251
+ <p className="text-sm font-medium truncate">
252
+ {fileItem.file.name}
253
+ </p>
254
+ <p className="text-xs text-muted-foreground">
255
+ {(fileItem.file.size / 1024).toFixed(1)} KB
256
+ </p>
257
+ </div>
258
+
259
+ <div className="text-xs">
260
+ {fileItem.status === "pending" && t("status.pending")}
261
+ {fileItem.status === "uploading" && t("status.uploading")}
262
+ {fileItem.status === "success" && t("status.success")}
263
+ {fileItem.status === "error" && t("status.error")}
264
+ </div>
265
+
266
+ <Button
267
+ type="button"
268
+ variant="ghost"
269
+ size="icon"
270
+ onClick={() => removeFile(fileItem)}
271
+ >
272
+ <X className="w-4 h-4" />
273
+ </Button>
274
+ </div>
275
+ ))}
276
+
277
+ <Button
278
+ type="button"
279
+ onClick={uploadFiles}
280
+ disabled={!feedbackId || files.every((file) => file.status !== "pending")}
281
+ className="w-full"
282
+ >
283
+ {t("buttons.upload")}
284
+ </Button>
285
+ </div>
286
+ )}
287
+ </div>
288
+ );
289
+ }
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+
4
+ /*
5
+ * Copyright (c) 2026 Echo Team
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU Affero General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU Affero General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ import { useEffect, useState, useCallback } from "react";
22
+ import { Button } from "@/components/ui/button";
23
+ import { Loader2, CheckCircle2, XCircle, RefreshCw, Bot } from "lucide-react";
24
+ import { useTranslations } from "next-intl";
25
+
26
+ interface ProcessingResult {
27
+ classification?: {
28
+ type: string;
29
+ priority: string;
30
+ confidence: number;
31
+ reasons: string[];
32
+ };
33
+ tagSuggestions?: Array<{
34
+ name: string;
35
+ slug: string;
36
+ confidence: number;
37
+ }>;
38
+ duplicateCandidates?: Array<{
39
+ feedbackId: number;
40
+ similarity: number;
41
+ }>;
42
+ processingTime?: number;
43
+ status: string;
44
+ errorMessage?: string;
45
+ }
46
+
47
+ interface ProcessingStatusProps {
48
+ feedbackId: number;
49
+ onCompleted?: (result: ProcessingResult) => void;
50
+ }
51
+
52
+ export default function ProcessingStatus({
53
+ feedbackId,
54
+ onCompleted,
55
+ }: ProcessingStatusProps) {
56
+ const t = useTranslations("processing");
57
+ const [status, setStatus] = useState<"pending" | "processing" | "completed" | "failed">("pending");
58
+ const [result, setResult] = useState<ProcessingResult | null>(null);
59
+ const [isRetrying, setIsRetrying] = useState(false);
60
+
61
+ const checkStatus = useCallback(async () => {
62
+ try {
63
+ const response = await fetch(
64
+ `/api/feedback/${feedbackId}/processing-status`,
65
+ );
66
+ if (response.ok) {
67
+ const data = await response.json();
68
+ setStatus(data.status);
69
+
70
+ if (data.status === "completed" && data.result) {
71
+ setResult(data.result);
72
+ onCompleted?.(data.result);
73
+ }
74
+ }
75
+ } catch (error) {
76
+ console.error("Failed to check processing status:", error);
77
+ }
78
+ }, [feedbackId, onCompleted]);
79
+
80
+ useEffect(() => {
81
+ if (status !== "completed" && status !== "failed") {
82
+ const interval = setInterval(checkStatus, 2000);
83
+ checkStatus();
84
+ return () => clearInterval(interval);
85
+ }
86
+ }, [status, checkStatus]);
87
+
88
+ async function retry() {
89
+ setIsRetrying(true);
90
+ try {
91
+ const response = await fetch(
92
+ `/api/feedback/${feedbackId}/processing-status`,
93
+ { method: "POST" },
94
+ );
95
+ if (response.ok) {
96
+ setStatus("pending");
97
+ setResult(null);
98
+ }
99
+ } finally {
100
+ setIsRetrying(false);
101
+ }
102
+ }
103
+
104
+ if (status === "pending") {
105
+ return (
106
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
107
+ <Loader2 className="h-4 w-4 animate-spin" />
108
+ <span>{t("queued")}</span>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ if (status === "processing") {
114
+ return (
115
+ <div className="flex items-center gap-2 text-sm text-primary">
116
+ <Bot className="h-4 w-4 animate-pulse" />
117
+ <Loader2 className="h-4 w-4 animate-spin" />
118
+ <span>{t("inProgress")}</span>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ if (status === "completed") {
124
+ return (
125
+ <div className="flex items-center gap-2 text-sm text-green-600">
126
+ <CheckCircle2 className="h-4 w-4" />
127
+ <span>{t("completed")}</span>
128
+ {result?.processingTime && (
129
+ <span className="text-muted-foreground">
130
+ ({result.processingTime}ms)
131
+ </span>
132
+ )}
133
+ </div>
134
+ );
135
+ }
136
+
137
+ if (status === "failed") {
138
+ return (
139
+ <div className="flex items-center gap-2 text-sm text-destructive">
140
+ <XCircle className="h-4 w-4" />
141
+ <span>{t("failed")}</span>
142
+ <Button
143
+ size="sm"
144
+ variant="ghost"
145
+ className="h-6 px-2"
146
+ onClick={retry}
147
+ disabled={isRetrying}
148
+ >
149
+ {isRetrying ? (
150
+ <Loader2 className="h-3 w-3 animate-spin" />
151
+ ) : (
152
+ <RefreshCw className="h-3 w-3" />
153
+ )}
154
+ {isRetrying ? t("retrying") : t("retry")}
155
+ </Button>
156
+ </div>
157
+ );
158
+ }
159
+
160
+ return null;
161
+ }
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+
4
+ /*
5
+ * Copyright (c) 2026 Echo Team
6
+ *
7
+ * This program is free software: you can redistribute it and/or modify
8
+ * it under the terms of the GNU Affero General Public License as published by
9
+ * the Free Software Foundation, either version 3 of the License, or
10
+ * (at your option) any later version.
11
+ *
12
+ * This program is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ * GNU Affero General Public License for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ import { formatDistanceToNow } from "date-fns";
22
+ import { zhCN } from "date-fns/locale";
23
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
24
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
25
+ import { Badge } from "@/components/ui/badge";
26
+ import { Clock } from "lucide-react";
27
+ import { cn } from "@/lib/utils";
28
+ import { STATUS_LABELS, STATUS_COLORS } from "@/lib/validations/feedback";
29
+
30
+ interface StatusHistoryEntry {
31
+ historyId: number;
32
+ oldStatus: string;
33
+ newStatus: string;
34
+ changedBy: {
35
+ id: string;
36
+ name: string | null;
37
+ email: string;
38
+ } | null;
39
+ changedAt: string;
40
+ comment: string | null;
41
+ }
42
+
43
+ interface StatusHistoryProps {
44
+ history: StatusHistoryEntry[];
45
+ className?: string;
46
+ }
47
+
48
+ export function StatusHistory({ history, className }: StatusHistoryProps) {
49
+ if (history.length === 0) {
50
+ return null;
51
+ }
52
+
53
+ const getInitials = (name: string | null, email: string) => {
54
+ if (name) {
55
+ return name
56
+ .split(" ")
57
+ .map((n) => n[0])
58
+ .join("")
59
+ .toUpperCase()
60
+ .slice(0, 2);
61
+ }
62
+ return email[0].toUpperCase();
63
+ };
64
+
65
+ return (
66
+ <Card className={className}>
67
+ <CardHeader>
68
+ <CardTitle className="text-lg flex items-center gap-2">
69
+ <Clock className="w-5 h-5" />
70
+ 状态历史
71
+ </CardTitle>
72
+ </CardHeader>
73
+ <CardContent>
74
+ <div className="space-y-4">
75
+ {history.map((entry) => (
76
+ <div key={entry.historyId} className="flex gap-4">
77
+ <div className="relative shrink-0">
78
+ <Avatar className="w-10 h-10 border-2 border-background">
79
+ <AvatarFallback className="text-xs">
80
+ {entry.changedBy
81
+ ? getInitials(entry.changedBy.name, entry.changedBy.email)
82
+ : "SYS"}
83
+ </AvatarFallback>
84
+ </Avatar>
85
+ </div>
86
+
87
+ <div className="flex-1 pb-4 border-b border-border last:border-b-0 last:pb-0">
88
+ <div className="flex items-center gap-2 mb-1">
89
+ <span className="font-medium text-sm">
90
+ {entry.changedBy?.name || entry.changedBy?.email || "System"}
91
+ </span>
92
+ <span className="text-xs text-muted-foreground">
93
+ {formatDistanceToNow(new Date(entry.changedAt), {
94
+ addSuffix: true,
95
+ locale: zhCN,
96
+ })}
97
+ </span>
98
+ </div>
99
+
100
+ <div className="flex items-center gap-2 flex-wrap">
101
+ <Badge
102
+ className={cn(
103
+ "text-xs",
104
+ STATUS_COLORS[entry.oldStatus as keyof typeof STATUS_COLORS],
105
+ )}
106
+ >
107
+ {STATUS_LABELS[entry.oldStatus as keyof typeof STATUS_LABELS] ??
108
+ entry.oldStatus}
109
+ </Badge>
110
+ <span className="text-muted-foreground">→</span>
111
+ <Badge
112
+ className={cn(
113
+ "text-xs",
114
+ STATUS_COLORS[entry.newStatus as keyof typeof STATUS_COLORS],
115
+ )}
116
+ >
117
+ {STATUS_LABELS[entry.newStatus as keyof typeof STATUS_LABELS] ??
118
+ entry.newStatus}
119
+ </Badge>
120
+ </div>
121
+
122
+ {entry.comment && (
123
+ <p className="text-sm text-muted-foreground mt-2 italic">
124
+ &quot;{entry.comment}&quot;
125
+ </p>
126
+ )}
127
+ </div>
128
+ </div>
129
+ ))}
130
+ </div>
131
+ </CardContent>
132
+ </Card>
133
+ );
134
+ }