@tribe-nest/forge 3.60.0 → 3.63.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 (220) hide show
  1. package/package.json +5 -2
  2. package/src/contexts/CartContext.tsx +34 -1
  3. package/src/contexts/PublicAuthContext.tsx +15 -3
  4. package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
  5. package/src/data/queries/useAccount.ts +33 -2
  6. package/src/data/queries/useAnalytics.ts +6 -6
  7. package/src/data/queries/useCallChat.ts +2 -2
  8. package/src/data/queries/useChat.ts +10 -1
  9. package/src/data/queries/useCheckouts.ts +2 -1
  10. package/src/data/queries/useCoachingAvailability.ts +10 -1
  11. package/src/data/queries/useCommunities.ts +2 -0
  12. package/src/data/queries/useCommunity.ts +362 -18
  13. package/src/data/queries/useCommunityAgent.ts +247 -0
  14. package/src/data/queries/useCommunityCalendar.ts +150 -0
  15. package/src/data/queries/useCommunityClassroom.ts +75 -0
  16. package/src/data/queries/useCommunityMembers.ts +108 -0
  17. package/src/data/queries/useCommunityModeration.ts +327 -0
  18. package/src/data/queries/useCommunityWiki.ts +137 -0
  19. package/src/data/queries/useCourses.ts +2 -0
  20. package/src/data/queries/useEmailSignIn.ts +79 -0
  21. package/src/data/queries/useEvents.ts +6 -1
  22. package/src/data/queries/useForms.ts +5 -1
  23. package/src/data/queries/useLeadMagnet.ts +6 -2
  24. package/src/data/queries/useMyBookings.ts +24 -1
  25. package/src/data/queries/useMyTickets.ts +24 -1
  26. package/src/data/queries/useNotifications.ts +6 -2
  27. package/src/data/queries/useOrders.ts +2 -1
  28. package/src/data/queries/usePWA.ts +111 -35
  29. package/src/data/queries/useSiteApp.ts +130 -0
  30. package/src/data/queries/useSubscriptions.ts +6 -1
  31. package/src/i18n/de.json +746 -1
  32. package/src/i18n/en.json +746 -1
  33. package/src/index.ts +20 -0
  34. package/src/runtime/pages.spec.ts +5 -0
  35. package/src/runtime/pages.ts +32 -6
  36. package/src/seo/_tests/seo.spec.ts +326 -0
  37. package/src/seo/applySeoToHead.ts +98 -0
  38. package/src/seo/index.ts +9 -0
  39. package/src/seo/resolve.ts +211 -0
  40. package/src/seo/seoHead.ts +61 -0
  41. package/src/seo/siteSeoHead.ts +97 -0
  42. package/src/seo/types.ts +99 -0
  43. package/src/server/_tests/siteBootstrap.spec.ts +65 -0
  44. package/src/server/index.ts +33 -4
  45. package/src/types/models.ts +11 -0
  46. package/src/ui/analytics/ForgeAnalytics.tsx +134 -34
  47. package/src/ui/analytics/PageMetaPixel.tsx +23 -8
  48. package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
  49. package/src/ui/analytics/_tests/ForgeAnalytics.webVitals.spec.tsx +129 -0
  50. package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
  51. package/src/ui/analytics/useMetaEvents.ts +78 -0
  52. package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
  53. package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
  54. package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
  55. package/src/ui/headless/_tests/communityHarness.tsx +10 -3
  56. package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
  57. package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
  58. package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
  59. package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
  60. package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
  61. package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
  62. package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
  63. package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +228 -14
  64. package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
  65. package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
  66. package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
  67. package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
  68. package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
  69. package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
  70. package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
  71. package/src/ui/headless/auth/useSignupForm.ts +20 -1
  72. package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
  73. package/src/ui/headless/chat/useChatRoom.ts +35 -1
  74. package/src/ui/headless/communities/index.ts +41 -0
  75. package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
  76. package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
  77. package/src/ui/headless/communities/useCommunityHome.ts +3 -0
  78. package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
  79. package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
  80. package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
  81. package/src/ui/headless/community/communityPoll.ts +297 -0
  82. package/src/ui/headless/community/countries.ts +123 -0
  83. package/src/ui/headless/community/index.ts +81 -1
  84. package/src/ui/headless/community/linkPreview.ts +71 -0
  85. package/src/ui/headless/community/richLexical.ts +27 -2
  86. package/src/ui/headless/community/useCommunityAgent.ts +285 -0
  87. package/src/ui/headless/community/useCommunityMembers.ts +175 -0
  88. package/src/ui/headless/community/useCommunityModeration.ts +300 -0
  89. package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
  90. package/src/ui/headless/community/useCommunityThread.ts +16 -3
  91. package/src/ui/headless/community/useMentionComposer.ts +17 -9
  92. package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
  93. package/src/ui/headless/downloads/index.ts +8 -0
  94. package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
  95. package/src/ui/headless/index.ts +13 -0
  96. package/src/ui/headless/memberHome/index.ts +10 -0
  97. package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
  98. package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
  99. package/src/ui/headless/portal/index.ts +57 -0
  100. package/src/ui/headless/portal/usePortal.ts +274 -0
  101. package/src/ui/headless/portal/usePortalFiles.ts +68 -0
  102. package/src/ui/headless/portal/usePortalMessages.ts +189 -0
  103. package/src/ui/headless/portal/usePortalPreview.ts +142 -0
  104. package/src/ui/headless/siteApp/appBar.ts +102 -0
  105. package/src/ui/headless/siteApp/appHome.ts +344 -0
  106. package/src/ui/headless/siteApp/appPrompts.ts +229 -0
  107. package/src/ui/headless/siteApp/index.ts +58 -0
  108. package/src/ui/headless/siteApp/useAppHome.ts +210 -0
  109. package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
  110. package/src/ui/headless/work/index.ts +2 -0
  111. package/src/ui/headless/work/useWorkPortal.ts +37 -26
  112. package/src/ui/index.ts +11 -0
  113. package/src/ui/styled/AccountDashboard.tsx +712 -352
  114. package/src/ui/styled/AiAgentWidget.tsx +13 -1
  115. package/src/ui/styled/ChatRoom.tsx +126 -16
  116. package/src/ui/styled/Checkout.tsx +8 -0
  117. package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
  118. package/src/ui/styled/CoachingConfirmation.tsx +8 -0
  119. package/src/ui/styled/CoachingDetail.tsx +2 -0
  120. package/src/ui/styled/CourseConfirmation.tsx +5 -0
  121. package/src/ui/styled/CourseDetail.tsx +2 -0
  122. package/src/ui/styled/DownloadPage.tsx +263 -0
  123. package/src/ui/styled/EventConfirmation.tsx +10 -0
  124. package/src/ui/styled/EventDetail.tsx +10 -0
  125. package/src/ui/styled/FilmLibrary.tsx +22 -9
  126. package/src/ui/styled/MusicLinkPage.tsx +5 -5
  127. package/src/ui/styled/ProductDetail.tsx +12 -0
  128. package/src/ui/styled/PwaRegistration.tsx +5 -1
  129. package/src/ui/styled/SignupForm.tsx +18 -3
  130. package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +5 -5
  131. package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
  132. package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
  133. package/src/ui/styled/app/AppBar.tsx +298 -0
  134. package/src/ui/styled/app/AppHome.tsx +1139 -0
  135. package/src/ui/styled/app/AppHomeMark.tsx +66 -0
  136. package/src/ui/styled/app/AppPrompts.tsx +464 -0
  137. package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
  138. package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
  139. package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
  140. package/src/ui/styled/app/index.ts +7 -0
  141. package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
  142. package/src/ui/styled/chat/MessageBubble.tsx +43 -6
  143. package/src/ui/styled/communities/CommunityAbout.tsx +6 -1
  144. package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
  145. package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
  146. package/src/ui/styled/communities/CommunityHome.tsx +134 -11
  147. package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
  148. package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
  149. package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
  150. package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
  151. package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
  152. package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
  153. package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
  154. package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
  155. package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
  156. package/src/ui/styled/communities/index.ts +9 -0
  157. package/src/ui/styled/communities/shared.tsx +39 -11
  158. package/src/ui/styled/community/CommunityAgent.tsx +286 -0
  159. package/src/ui/styled/community/CommunityComposer.tsx +266 -15
  160. package/src/ui/styled/community/CommunityFeed.tsx +50 -1
  161. package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
  162. package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
  163. package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
  164. package/src/ui/styled/community/CommunityMembers.tsx +387 -0
  165. package/src/ui/styled/community/CommunityModeration.tsx +587 -0
  166. package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
  167. package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
  168. package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
  169. package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
  170. package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
  171. package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
  172. package/src/ui/styled/community/CommunityRichText.tsx +177 -32
  173. package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
  174. package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
  175. package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
  176. package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
  177. package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
  178. package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
  179. package/src/ui/styled/community/index.ts +37 -2
  180. package/src/ui/styled/forge-utilities.css +234 -0
  181. package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
  182. package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
  183. package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
  184. package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
  185. package/src/ui/styled/members/index.ts +8 -0
  186. package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
  187. package/src/ui/styled/portal/PortalActivity.tsx +167 -0
  188. package/src/ui/styled/portal/PortalBilling.tsx +197 -0
  189. package/src/ui/styled/portal/PortalFiles.tsx +279 -0
  190. package/src/ui/styled/portal/PortalHome.tsx +401 -0
  191. package/src/ui/styled/portal/PortalMessages.tsx +300 -0
  192. package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
  193. package/src/ui/styled/portal/PortalProjects.tsx +407 -0
  194. package/src/ui/styled/portal/PortalShell.tsx +397 -0
  195. package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
  196. package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
  197. package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
  198. package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
  199. package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
  200. package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
  201. package/src/ui/styled/portal/index.ts +13 -0
  202. package/src/ui/styled/portal/portalNav.ts +260 -0
  203. package/src/ui/styled/portal/shared.tsx +206 -0
  204. package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
  205. package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
  206. package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
  207. package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
  208. package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
  209. package/src/ui/styled/work/WorkReportBody.tsx +223 -248
  210. package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
  211. package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
  212. package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
  213. package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
  214. package/src/ui/styled/work/index.ts +1 -1
  215. package/src/utils/_tests/landing.spec.ts +39 -0
  216. package/src/utils/_tests/metaEvents.spec.ts +264 -0
  217. package/src/utils/_tests/structuredDataSeo.spec.ts +247 -0
  218. package/src/utils/landing.ts +36 -10
  219. package/src/utils/metaEvents.ts +302 -0
  220. package/src/utils/structuredData.ts +339 -1
@@ -51,6 +51,12 @@ export function useCommunityStyles() {
51
51
  * Paystack popup is open (see `DialogRoot`). The hook that owns the flow must
52
52
  * live in the COMPONENT that renders this, not inside `children`: a Paystack
53
53
  * stand-down remounts the content.
54
+ *
55
+ * Below `lg` it is a bottom sheet: anchored to the bottom edge, full width,
56
+ * top corners rounded, a grab handle, scrolling inside, and clear of the
57
+ * phone's home indicator. From `lg` up it is the centred dialog it always was.
58
+ * The layout is breakpoint classes, so there is nothing to measure and nothing
59
+ * that renders differently on the server.
54
60
  */
55
61
  export function CommunitySheet({
56
62
  open,
@@ -71,12 +77,20 @@ export function CommunitySheet({
71
77
  return (
72
78
  <DialogRoot open={open} onOpenChange={onOpenChange}>
73
79
  <DialogPortal>
74
- <DialogOverlay style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 50 }} />
80
+ <DialogOverlay style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: COMMUNITY_SHEET_Z }} />
75
81
  <DialogContent
76
82
  data-testid={testId}
83
+ data-sheet="bottom-below-lg"
77
84
  aria-describedby={undefined}
85
+ className={COMMUNITY_SHEET_CLASSES}
78
86
  style={sheetStyle(t)}
79
87
  >
88
+ <div
89
+ aria-hidden
90
+ data-sheet-handle=""
91
+ className="fg:mx-auto fg:mb-3 fg:lg:hidden"
92
+ style={{ width: 40, height: 4, borderRadius: 2, background: t.border }}
93
+ />
80
94
  <div className="fg:flex fg:items-center fg:justify-between fg:gap-3" style={{ marginBottom: 12 }}>
81
95
  <DialogTitle style={{ fontSize: 18, fontWeight: 700, margin: 0, fontFamily: t.headingFontFamily }}>
82
96
  {title}
@@ -98,20 +112,34 @@ export function CommunitySheet({
98
112
  );
99
113
  }
100
114
 
115
+ /**
116
+ * The sheet's stacking order. It is modal, so it covers everything pinned to
117
+ * the screen: the site's chat bubble (`AiAgentWidget` defaults to 9999) and
118
+ * the member home's bottom bar.
119
+ */
120
+ export const COMMUNITY_SHEET_Z = 10000;
121
+
122
+ /**
123
+ * Where the sheet sits: a bottom sheet below `lg`, the centred dialog from
124
+ * `lg` up. The corner radius comes from the theme through `--cs-radius`.
125
+ */
126
+ export const COMMUNITY_SHEET_CLASSES = [
127
+ "fg:fixed fg:inset-x-0 fg:bottom-0 fg:w-full fg:max-h-[90dvh] fg:overflow-y-auto",
128
+ "fg:rounded-t-[var(--cs-radius)] fg:px-5 fg:pt-3",
129
+ "fg:lg:inset-x-auto fg:lg:bottom-auto fg:lg:top-1/2 fg:lg:left-1/2 fg:lg:-translate-x-1/2 fg:lg:-translate-y-1/2",
130
+ "fg:lg:w-[min(560px,calc(100vw-32px))] fg:lg:max-h-[88vh] fg:lg:rounded-[var(--cs-radius-lg)] fg:lg:p-6",
131
+ ].join(" ");
132
+
101
133
  function sheetStyle(t: ResolvedThemeTokens): CSSProperties {
102
134
  return {
103
- position: "fixed",
104
- top: "50%",
105
- left: "50%",
106
- transform: "translate(-50%, -50%)",
107
- width: "min(560px, calc(100vw - 32px))",
108
- maxHeight: "88vh",
109
- overflowY: "auto",
135
+ // A bottom sheet's top corners stay visibly round on a square theme.
136
+ ["--cs-radius" as string]: `${Math.max(t.cornerRadius, 12)}px`,
137
+ ["--cs-radius-lg" as string]: `${t.cornerRadius}px`,
138
+ // Clear of the phone's home indicator; the inset is 0 everywhere else.
139
+ paddingBottom: "calc(24px + env(safe-area-inset-bottom))",
110
140
  background: t.background,
111
141
  color: t.text,
112
- borderRadius: t.cornerRadius,
113
- padding: 24,
114
- zIndex: 51,
142
+ zIndex: COMMUNITY_SHEET_Z + 1,
115
143
  fontFamily: t.fontFamily,
116
144
  boxSizing: "border-box",
117
145
  };
@@ -0,0 +1,286 @@
1
+ import { Bot, Sparkles, ThumbsDown, ThumbsUp, X } from "lucide-react";
2
+ import { useForgeT } from "../../../i18n";
3
+ import { useThemeTokens } from "../../theme/ForgeThemeProvider";
4
+ import {
5
+ linkifyText,
6
+ useCommunityAgentAnswerFeedback,
7
+ useCommunityCatchUp,
8
+ } from "../../headless/community/useCommunityAgent";
9
+ import type { CommunityAgentFeedbackEntity } from "../../../data/queries/useCommunityAgent";
10
+
11
+ /** What an agent is drawn from: its name and avatar. */
12
+ export interface CommunityAgentIdentity {
13
+ name: string;
14
+ avatarUrl?: string | null;
15
+ }
16
+
17
+ /**
18
+ * The "AI" chip. Shown beside the community AI's name everywhere it appears,
19
+ * so it is never mistaken for a member or for the creator.
20
+ */
21
+ export function CommunityAgentBadge() {
22
+ const t = useThemeTokens();
23
+ const tr = useForgeT();
24
+ return (
25
+ <span
26
+ data-testid="community-agent-badge"
27
+ title={tr("forge.community_agent.badge_title")}
28
+ style={{
29
+ background: t.primary,
30
+ color: t.textPrimary,
31
+ borderRadius: 999,
32
+ fontSize: 10,
33
+ fontWeight: 700,
34
+ padding: "1px 6px",
35
+ letterSpacing: 0.4,
36
+ whiteSpace: "nowrap",
37
+ flexShrink: 0,
38
+ }}
39
+ >
40
+ {tr("forge.community_agent.badge")}
41
+ </span>
42
+ );
43
+ }
44
+
45
+ export interface CommunityAgentAvatarProps {
46
+ agent: CommunityAgentIdentity;
47
+ size?: number;
48
+ }
49
+
50
+ /** The agent's avatar, or a neutral AI glyph when the creator set none. */
51
+ export function CommunityAgentAvatar({ agent, size = 36 }: CommunityAgentAvatarProps) {
52
+ const t = useThemeTokens();
53
+ const style: React.CSSProperties = { width: size, height: size, borderRadius: "50%", flexShrink: 0, objectFit: "cover" };
54
+ if (agent.avatarUrl) return <img src={agent.avatarUrl} alt={agent.name} style={style} />;
55
+ return (
56
+ <div
57
+ role="img"
58
+ aria-label={agent.name}
59
+ data-testid="community-agent-glyph"
60
+ style={{
61
+ ...style,
62
+ display: "flex",
63
+ alignItems: "center",
64
+ justifyContent: "center",
65
+ background: `${t.primary}22`,
66
+ color: t.primary,
67
+ }}
68
+ >
69
+ <Bot size={Math.max(12, Math.round(size * 0.55))} aria-hidden />
70
+ </div>
71
+ );
72
+ }
73
+
74
+ export interface CommunityAgentNameProps {
75
+ agent: CommunityAgentIdentity;
76
+ fontSize?: number;
77
+ fontWeight?: number;
78
+ }
79
+
80
+ /** The agent's name with its "AI" badge, in place of a member's name. */
81
+ export function CommunityAgentName({ agent, fontSize = 14, fontWeight = 600 }: CommunityAgentNameProps) {
82
+ const t = useThemeTokens();
83
+ return (
84
+ <span style={{ display: "inline-flex", alignItems: "center", gap: 6, minWidth: 0 }}>
85
+ <span
86
+ style={{
87
+ color: t.text,
88
+ fontWeight,
89
+ fontSize,
90
+ whiteSpace: "nowrap",
91
+ overflow: "hidden",
92
+ textOverflow: "ellipsis",
93
+ }}
94
+ >
95
+ {agent.name}
96
+ </span>
97
+ <CommunityAgentBadge />
98
+ </span>
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Plain text with its http(s) addresses made clickable, line breaks kept. For
104
+ * the agent's answers, which end with a "Sources:" list of addresses.
105
+ */
106
+ export function CommunityLinkifiedText({ text }: { text: string }) {
107
+ const t = useThemeTokens();
108
+ return (
109
+ <span style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}>
110
+ {linkifyText(text).map((segment, i) =>
111
+ segment.kind === "link" ? (
112
+ <a
113
+ key={i}
114
+ href={segment.url}
115
+ target="_blank"
116
+ rel="noopener noreferrer"
117
+ style={{ color: "inherit", textDecoration: "underline", textDecorationColor: t.primary }}
118
+ >
119
+ {segment.text}
120
+ </a>
121
+ ) : (
122
+ <span key={i}>{segment.text}</span>
123
+ ),
124
+ )}
125
+ </span>
126
+ );
127
+ }
128
+
129
+ /** "<name> is thinking..." while the agent writes an answer. */
130
+ export function CommunityAgentThinking({ name }: { name?: string | null }) {
131
+ const t = useThemeTokens();
132
+ const tr = useForgeT();
133
+ return (
134
+ <div
135
+ role="status"
136
+ data-testid="community-agent-thinking"
137
+ style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12, color: t.muted }}
138
+ >
139
+ <Sparkles size={13} aria-hidden style={{ color: t.primary }} />
140
+ {name ? tr("forge.community_agent.thinking", { agent_name: name }) : tr("forge.community_agent.thinking_unnamed")}
141
+ </div>
142
+ );
143
+ }
144
+
145
+ export interface CommunityAgentFeedbackProps {
146
+ entityType: CommunityAgentFeedbackEntity;
147
+ entityId: string;
148
+ /** The member's saved rating, from `useCommunityAgentAnswerRatings`. */
149
+ saved?: 1 | -1 | null;
150
+ }
151
+
152
+ /**
153
+ * Thumbs up / down on one AI answer, for the member who asked (render it only
154
+ * where `useCommunityAgentAnswerRatings` says they may rate). Starts from the
155
+ * saved rating; optimistic; a second press on the active thumb clears it. A
156
+ * 404 hides it without a word.
157
+ */
158
+ export function CommunityAgentFeedback({ entityType, entityId, saved = null }: CommunityAgentFeedbackProps) {
159
+ const t = useThemeTokens();
160
+ const tr = useForgeT();
161
+ const feedback = useCommunityAgentAnswerFeedback(entityType, entityId, saved);
162
+ if (feedback.hidden) return null;
163
+
164
+ const thumb = (value: 1 | -1, label: string, icon: React.ReactNode) => {
165
+ const active = feedback.value === value;
166
+ return (
167
+ <button
168
+ type="button"
169
+ onClick={() => feedback.rate(value)}
170
+ aria-label={label}
171
+ aria-pressed={active}
172
+ title={label}
173
+ style={{
174
+ display: "inline-flex",
175
+ alignItems: "center",
176
+ background: active ? `${t.primary}1a` : "none",
177
+ border: "none",
178
+ borderRadius: t.cornerRadius,
179
+ cursor: "pointer",
180
+ color: active ? t.primary : t.muted,
181
+ padding: "2px 4px",
182
+ }}
183
+ >
184
+ {icon}
185
+ </button>
186
+ );
187
+ };
188
+
189
+ return (
190
+ <span data-testid="community-agent-feedback" style={{ display: "inline-flex", alignItems: "center", gap: 2 }}>
191
+ {thumb(1, tr("forge.community_agent.feedback_up"), <ThumbsUp size={13} aria-hidden />)}
192
+ {thumb(-1, tr("forge.community_agent.feedback_down"), <ThumbsDown size={13} aria-hidden />)}
193
+ </span>
194
+ );
195
+ }
196
+
197
+ export interface CommunityCatchMeUpProps {
198
+ postId: string;
199
+ agent: CommunityAgentIdentity;
200
+ }
201
+
202
+ /**
203
+ * "Catch me up" on a long thread. The summary opens in a private panel, marked
204
+ * as written by the AI, that only this member sees; nothing is posted. A
205
+ * reached daily limit or an unavailable AI is said plainly.
206
+ */
207
+ export function CommunityCatchMeUp({ postId, agent }: CommunityCatchMeUpProps) {
208
+ const t = useThemeTokens();
209
+ const tr = useForgeT();
210
+ const catchUp = useCommunityCatchUp(postId);
211
+ const border = `1px solid ${t.border}`;
212
+
213
+ const errorCopy =
214
+ catchUp.error === "daily_limit"
215
+ ? tr("forge.community_agent.catch_up_daily_limit")
216
+ : catchUp.error === "unavailable"
217
+ ? tr("forge.community_agent.catch_up_unavailable")
218
+ : catchUp.error
219
+ ? tr("forge.community_agent.catch_up_failed")
220
+ : null;
221
+
222
+ if (catchUp.summary) {
223
+ return (
224
+ <section
225
+ data-testid="community-catch-me-up-panel"
226
+ aria-label={tr("forge.community_agent.catch_up_panel_label", { agent_name: catchUp.summary.agentName })}
227
+ style={{ border, borderRadius: t.cornerRadius, background: `${t.primary}0d`, padding: 14 }}
228
+ >
229
+ <header style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
230
+ <CommunityAgentAvatar agent={agent} size={24} />
231
+ <span style={{ flex: 1, minWidth: 0, display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
232
+ <CommunityAgentName agent={{ ...agent, name: catchUp.summary.agentName || agent.name }} fontSize={13} />
233
+ <span style={{ fontSize: 12, color: t.muted }}>{tr("forge.community_agent.catch_up_private")}</span>
234
+ </span>
235
+ <button
236
+ type="button"
237
+ onClick={catchUp.dismiss}
238
+ aria-label={tr("forge.community_agent.catch_up_dismiss")}
239
+ style={{ background: "none", border: "none", cursor: "pointer", color: t.muted, padding: 2 }}
240
+ >
241
+ <X size={15} aria-hidden />
242
+ </button>
243
+ </header>
244
+ <div style={{ color: t.text, fontSize: 14, lineHeight: 1.55 }}>
245
+ <CommunityLinkifiedText text={catchUp.summary.summary} />
246
+ </div>
247
+ </section>
248
+ );
249
+ }
250
+
251
+ return (
252
+ <div style={{ display: "flex", flexDirection: "column", gap: 6, alignItems: "flex-start" }}>
253
+ <button
254
+ type="button"
255
+ onClick={catchUp.ask}
256
+ disabled={catchUp.isPending}
257
+ style={{
258
+ display: "inline-flex",
259
+ alignItems: "center",
260
+ gap: 6,
261
+ background: "none",
262
+ border,
263
+ borderRadius: t.cornerRadius,
264
+ padding: "6px 12px",
265
+ cursor: catchUp.isPending ? "wait" : "pointer",
266
+ color: t.text,
267
+ fontSize: 13,
268
+ fontWeight: 600,
269
+ opacity: catchUp.isPending ? 0.7 : 1,
270
+ }}
271
+ >
272
+ <Sparkles size={14} aria-hidden style={{ color: t.primary }} />
273
+ {catchUp.isPending ? tr("forge.community_agent.catch_up_pending") : tr("forge.community_agent.catch_up")}
274
+ </button>
275
+ {errorCopy && (
276
+ <div
277
+ role="alert"
278
+ data-testid="community-catch-me-up-error"
279
+ style={{ fontSize: 13, color: t.text, borderLeft: `3px solid ${t.primary}`, paddingLeft: 8 }}
280
+ >
281
+ {errorCopy}
282
+ </div>
283
+ )}
284
+ </div>
285
+ );
286
+ }