@tribe-nest/forge 3.59.0 → 3.61.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.
- package/package.json +2 -1
- package/src/_tests/testCommunity.tsx +28 -0
- package/src/contexts/CartContext.tsx +34 -1
- package/src/contexts/CommunityContext.tsx +192 -0
- package/src/contexts/PublicAuthContext.tsx +15 -3
- package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
- package/src/data/queries/useAccount.ts +33 -2
- package/src/data/queries/useAnalytics.ts +6 -6
- package/src/data/queries/useCallChat.ts +2 -2
- package/src/data/queries/useChat.ts +52 -40
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +345 -0
- package/src/data/queries/useCommunity.ts +450 -102
- package/src/data/queries/useCommunityAgent.ts +247 -0
- package/src/data/queries/useCommunityCalendar.ts +150 -0
- package/src/data/queries/useCommunityClassroom.ts +75 -0
- package/src/data/queries/useCommunityMembers.ts +108 -0
- package/src/data/queries/useCommunityModeration.ts +327 -0
- package/src/data/queries/useCommunityWiki.ts +137 -0
- package/src/data/queries/useCourses.ts +2 -0
- package/src/data/queries/useEmailSignIn.ts +79 -0
- package/src/data/queries/useEvents.ts +6 -1
- package/src/data/queries/useForms.ts +5 -1
- package/src/data/queries/useLeadMagnet.ts +6 -2
- package/src/data/queries/useMembership.ts +32 -7
- package/src/data/queries/useMembershipCalls.ts +116 -7
- package/src/data/queries/useMyBookings.ts +24 -1
- package/src/data/queries/useMyTickets.ts +24 -1
- package/src/data/queries/useNotifications.ts +6 -2
- package/src/data/queries/useOrders.ts +2 -1
- package/src/data/queries/usePWA.ts +111 -35
- package/src/data/queries/usePostCollections.ts +28 -16
- package/src/data/queries/usePosts.ts +16 -14
- package/src/data/queries/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +28 -9
- package/src/i18n/de.json +891 -2
- package/src/i18n/en.json +891 -2
- package/src/index.ts +36 -7
- package/src/runtime/pages.spec.ts +36 -0
- package/src/runtime/pages.ts +52 -27
- package/src/server/_tests/buildWebManifest.spec.ts +1 -1
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +64 -34
- package/src/ui/analytics/PageMetaPixel.tsx +23 -8
- package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
- package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
- package/src/ui/analytics/useMetaEvents.ts +78 -0
- package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
- package/src/ui/format/membershipGate.ts +19 -15
- package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
- package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
- package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
- package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
- package/src/ui/headless/_tests/communityHarness.tsx +212 -0
- package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
- package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
- package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
- package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
- package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
- package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
- package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
- package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
- package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +596 -0
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +179 -0
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +295 -0
- package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
- package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
- package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
- package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
- package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
- package/src/ui/headless/auth/useSignupForm.ts +30 -3
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/CommunityGate.tsx +67 -0
- package/src/ui/headless/communities/index.ts +96 -0
- package/src/ui/headless/communities/paths.ts +36 -0
- package/src/ui/headless/communities/useCommunity.ts +140 -0
- package/src/ui/headless/communities/useCommunityAbout.ts +71 -0
- package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
- package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
- package/src/ui/headless/communities/useCommunityHome.ts +91 -0
- package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +494 -0
- package/src/ui/headless/communities/useCommunityMembership.ts +182 -0
- package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
- package/src/ui/headless/communities/useMyMemberships.ts +59 -0
- package/src/ui/headless/community/communityPoll.ts +297 -0
- package/src/ui/headless/community/countries.ts +123 -0
- package/src/ui/headless/community/index.ts +81 -1
- package/src/ui/headless/community/linkPreview.ts +71 -0
- package/src/ui/headless/community/richLexical.ts +27 -2
- package/src/ui/headless/community/useCommunityAgent.ts +285 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
- package/src/ui/headless/community/useCommunityMembers.ts +175 -0
- package/src/ui/headless/community/useCommunityModeration.ts +300 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
- package/src/ui/headless/community/useCommunityThread.ts +16 -3
- package/src/ui/headless/community/useMentionComposer.ts +17 -9
- package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
- package/src/ui/headless/downloads/index.ts +8 -0
- package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
- package/src/ui/headless/index.ts +21 -4
- package/src/ui/headless/memberHome/index.ts +11 -1
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +84 -44
- package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +8 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +5 -4
- package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +1 -1
- package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +1 -1
- package/src/ui/headless/membership/useMembershipCheckout.ts +89 -22
- package/src/ui/headless/membership/useMembershipGateNotice.ts +25 -7
- package/src/ui/headless/portal/index.ts +57 -0
- package/src/ui/headless/portal/usePortal.ts +274 -0
- package/src/ui/headless/portal/usePortalFiles.ts +68 -0
- package/src/ui/headless/portal/usePortalMessages.ts +189 -0
- package/src/ui/headless/portal/usePortalPreview.ts +142 -0
- package/src/ui/headless/posts/upsell.ts +54 -8
- package/src/ui/headless/posts/useMemberPostsFeed.ts +6 -5
- package/src/ui/headless/posts/usePostCollectionDetail.ts +7 -4
- package/src/ui/headless/posts/usePostCollectionsList.ts +6 -5
- package/src/ui/headless/posts/usePostItem.ts +6 -5
- package/src/ui/headless/siteApp/appBar.ts +102 -0
- package/src/ui/headless/siteApp/appHome.ts +344 -0
- package/src/ui/headless/siteApp/appPrompts.ts +229 -0
- package/src/ui/headless/siteApp/index.ts +58 -0
- package/src/ui/headless/siteApp/useAppHome.ts +210 -0
- package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
- package/src/ui/headless/work/index.ts +2 -0
- package/src/ui/headless/work/useWorkPortal.ts +37 -26
- package/src/ui/index.ts +28 -12
- package/src/ui/styled/AccountDashboard.tsx +754 -602
- package/src/ui/styled/AiAgentWidget.tsx +13 -1
- package/src/ui/styled/ChatRoom.tsx +126 -16
- package/src/ui/styled/Checkout.tsx +8 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingDetail.tsx +2 -0
- package/src/ui/styled/CourseConfirmation.tsx +5 -0
- package/src/ui/styled/CourseDetail.tsx +2 -0
- package/src/ui/styled/DownloadPage.tsx +263 -0
- package/src/ui/styled/EventConfirmation.tsx +10 -0
- package/src/ui/styled/EventDetail.tsx +10 -0
- package/src/ui/styled/FilmLibrary.tsx +22 -9
- package/src/ui/styled/MembershipCheckout.tsx +37 -17
- package/src/ui/styled/MembershipTierCallout.tsx +20 -1
- package/src/ui/styled/MembershipTiers.tsx +49 -11
- package/src/ui/styled/MusicLinkPage.tsx +5 -5
- package/src/ui/styled/Paywall.tsx +1 -1
- package/src/ui/styled/ProductDetail.tsx +12 -0
- package/src/ui/styled/PwaRegistration.tsx +5 -1
- package/src/ui/styled/SignupForm.tsx +18 -3
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +6 -6
- package/src/ui/styled/_tests/AccountDashboardMemberships.spec.tsx +151 -0
- package/src/ui/styled/_tests/AccountDashboardRentals.spec.tsx +1 -1
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +54 -11
- package/src/ui/styled/_tests/membershipTierCallout.spec.tsx +1 -1
- package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +1 -1
- package/src/ui/styled/app/AppBar.tsx +298 -0
- package/src/ui/styled/app/AppHome.tsx +1139 -0
- package/src/ui/styled/app/AppHomeMark.tsx +66 -0
- package/src/ui/styled/app/AppPrompts.tsx +464 -0
- package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
- package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
- package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
- package/src/ui/styled/app/index.ts +7 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
- package/src/ui/styled/chat/MessageBubble.tsx +43 -6
- package/src/ui/styled/communities/CommunitiesList.tsx +88 -0
- package/src/ui/styled/communities/CommunityAbout.tsx +145 -0
- package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
- package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
- package/src/ui/styled/communities/CommunityHome.tsx +298 -0
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +544 -0
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +249 -0
- package/src/ui/styled/communities/CommunityPending.tsx +49 -0
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +115 -0
- package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
- package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
- package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
- package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
- package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +19 -0
- package/src/ui/styled/communities/shared.tsx +177 -0
- package/src/ui/styled/community/CommunityAgent.tsx +286 -0
- package/src/ui/styled/community/CommunityComposer.tsx +266 -15
- package/src/ui/styled/community/CommunityFeed.tsx +50 -1
- package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
- package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
- package/src/ui/styled/community/CommunityMembers.tsx +387 -0
- package/src/ui/styled/community/CommunityModeration.tsx +587 -0
- package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
- package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
- package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
- package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
- package/src/ui/styled/community/CommunityRichText.tsx +177 -32
- package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
- package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
- package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
- package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
- package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
- package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
- package/src/ui/styled/community/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +253 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +41 -54
- package/src/ui/styled/members/MemberHomeLayout.tsx +327 -29
- package/src/ui/styled/members/MemberPostCard.tsx +9 -5
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- package/src/ui/styled/members/PostCollectionDetail.tsx +18 -10
- package/src/ui/styled/members/PostCollectionsGrid.tsx +24 -10
- package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +167 -8
- package/src/ui/styled/members/callTranscriptText.ts +67 -0
- package/src/ui/styled/members/index.ts +8 -0
- package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
- package/src/ui/styled/portal/PortalActivity.tsx +167 -0
- package/src/ui/styled/portal/PortalBilling.tsx +197 -0
- package/src/ui/styled/portal/PortalFiles.tsx +279 -0
- package/src/ui/styled/portal/PortalHome.tsx +401 -0
- package/src/ui/styled/portal/PortalMessages.tsx +300 -0
- package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
- package/src/ui/styled/portal/PortalProjects.tsx +407 -0
- package/src/ui/styled/portal/PortalShell.tsx +397 -0
- package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
- package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
- package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
- package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
- package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
- package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
- package/src/ui/styled/portal/index.ts +13 -0
- package/src/ui/styled/portal/portalNav.ts +260 -0
- package/src/ui/styled/portal/shared.tsx +206 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
- package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
- package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
- package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
- package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
- package/src/ui/styled/work/WorkReportBody.tsx +223 -248
- package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
- package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
- package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
- package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
- package/src/ui/styled/work/index.ts +1 -1
- package/src/utils/_tests/metaEvents.spec.ts +264 -0
- package/src/utils/_tests/safeRedirect.spec.ts +2 -2
- package/src/utils/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
- package/src/ui/headless/membership/MembershipGate.tsx +0 -44
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +0 -149
- package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +0 -105
|
@@ -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
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { useEffect, useId, useRef, useState } from "react";
|
|
2
|
-
import { Bold, Italic, Underline, List, ListOrdered, Link as LinkIcon, PenLine } from "lucide-react";
|
|
2
|
+
import { BarChart3, Bold, Italic, Underline, List, ListOrdered, Link as LinkIcon, PenLine, Plus, X } from "lucide-react";
|
|
3
|
+
import { useForgeT } from "../../../i18n";
|
|
3
4
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
4
5
|
import { formatBytes } from "../../headless/chat/attachmentKind";
|
|
5
6
|
import { useCommunityAttachmentUpload } from "../../headless/community/useCommunityAttachmentUpload";
|
|
6
7
|
import { useRichMentionComposer, type RichCommand } from "../../headless/community/useRichMentionComposer";
|
|
8
|
+
import {
|
|
9
|
+
COMMUNITY_POLL_LIMITS,
|
|
10
|
+
buildPollPayload,
|
|
11
|
+
useCommunityPollDraft,
|
|
12
|
+
} from "../../headless/community/communityPoll";
|
|
7
13
|
// Forge's own `?` disclosure. It is named for the call screen it was first built
|
|
8
14
|
// for, but it is a plain button + panel over the theme tokens with no media
|
|
9
15
|
// dependency, so every styled surface uses it rather than growing a second one.
|
|
@@ -11,18 +17,50 @@ import { useRichMentionComposer, type RichCommand } from "../../headless/communi
|
|
|
11
17
|
// is not a Forge dependency and would not resolve on a real creator site.
|
|
12
18
|
import { CallHelpHint } from "../../media/CallHelpHint";
|
|
13
19
|
import { MemberAvatar } from "./MemberAvatar";
|
|
14
|
-
import
|
|
20
|
+
import { CommunityAgentAvatar, CommunityAgentName } from "./CommunityAgent";
|
|
21
|
+
import type {
|
|
22
|
+
CommunityAttachmentInput,
|
|
23
|
+
CommunityPollInput,
|
|
24
|
+
CommunityPollResultsVisibility,
|
|
25
|
+
} from "../../../data/queries/useCommunity";
|
|
15
26
|
|
|
16
27
|
export interface CommunityComposerSubmit {
|
|
17
28
|
title?: string;
|
|
18
29
|
body: unknown;
|
|
19
30
|
attachments: CommunityAttachmentInput[];
|
|
20
31
|
mentionedAccountIds: string[];
|
|
32
|
+
/** Present when the member attached a poll. `title` is then its question. */
|
|
33
|
+
poll?: CommunityPollInput;
|
|
21
34
|
}
|
|
22
35
|
|
|
36
|
+
const POLL_ERROR_KEY: Record<string, string> = {
|
|
37
|
+
question_required: "forge.community_composer.poll_question_required",
|
|
38
|
+
question_too_long: "forge.community_composer.poll_question_too_long",
|
|
39
|
+
option_empty: "forge.community_composer.poll_option_empty",
|
|
40
|
+
option_too_long: "forge.community_composer.poll_option_too_long",
|
|
41
|
+
option_duplicate: "forge.community_composer.poll_option_duplicate",
|
|
42
|
+
options_too_few: "forge.community_composer.poll_options_too_few",
|
|
43
|
+
options_too_many: "forge.community_composer.poll_options_too_many",
|
|
44
|
+
closes_invalid: "forge.community_composer.poll_closes_invalid",
|
|
45
|
+
closes_in_past: "forge.community_composer.poll_closes_in_past",
|
|
46
|
+
closes_required: "forge.community_composer.poll_closes_required",
|
|
47
|
+
visibility_invalid: "forge.community_composer.poll_visibility_invalid",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const VISIBILITY_KEY: Record<CommunityPollResultsVisibility, string> = {
|
|
51
|
+
always: "forge.community_composer.poll_visibility_always",
|
|
52
|
+
after_vote: "forge.community_composer.poll_visibility_after_vote",
|
|
53
|
+
after_close: "forge.community_composer.poll_visibility_after_close",
|
|
54
|
+
};
|
|
55
|
+
|
|
23
56
|
export interface CommunityComposerProps {
|
|
24
57
|
/** Show the title input (forum spaces). */
|
|
25
58
|
withTitle?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Offer a Poll toggle (the space feed's post box). The poll's question is
|
|
61
|
+
* the post title, so turning it on shows the title input whatever `withTitle` says.
|
|
62
|
+
*/
|
|
63
|
+
allowPoll?: boolean;
|
|
26
64
|
placeholder?: string;
|
|
27
65
|
submitLabel?: string;
|
|
28
66
|
autoFocus?: boolean;
|
|
@@ -72,6 +110,7 @@ export interface CommunityComposerProps {
|
|
|
72
110
|
*/
|
|
73
111
|
export function CommunityComposer({
|
|
74
112
|
withTitle = false,
|
|
113
|
+
allowPoll = false,
|
|
75
114
|
placeholder = "Write something…",
|
|
76
115
|
submitLabel = "Post",
|
|
77
116
|
autoFocus = false,
|
|
@@ -84,7 +123,13 @@ export function CommunityComposer({
|
|
|
84
123
|
onCancel,
|
|
85
124
|
}: CommunityComposerProps) {
|
|
86
125
|
const t = useThemeTokens();
|
|
126
|
+
const tr = useForgeT();
|
|
87
127
|
const composer = useRichMentionComposer();
|
|
128
|
+
const poll = useCommunityPollDraft();
|
|
129
|
+
const [pollOn, setPollOn] = useState(false);
|
|
130
|
+
const withPoll = allowPoll && pollOn;
|
|
131
|
+
const showTitle = withTitle || withPoll;
|
|
132
|
+
const pollFieldId = useId();
|
|
88
133
|
const upload = useCommunityAttachmentUpload();
|
|
89
134
|
const [title, setTitle] = useState("");
|
|
90
135
|
const [sending, setSending] = useState(false);
|
|
@@ -105,15 +150,18 @@ export function CommunityComposer({
|
|
|
105
150
|
const [confirmingDiscard, setConfirmingDiscard] = useState(false);
|
|
106
151
|
|
|
107
152
|
const border = `1px solid ${t.border}`;
|
|
108
|
-
|
|
109
|
-
|
|
153
|
+
// A poll may go out with no body: the question and options are the post. Its
|
|
154
|
+
// own checks run on submit, so the button stays live and a click shows why.
|
|
155
|
+
const canSubmit = withPoll
|
|
156
|
+
? !sending
|
|
157
|
+
: (!composer.isEmpty || upload.hasPending) && !sending && (!withTitle || title.trim().length > 0);
|
|
110
158
|
/** Is there work in here that closing would otherwise have thrown away? */
|
|
111
159
|
const hasDraft = !composer.isEmpty || upload.hasPending || title.trim().length > 0;
|
|
112
160
|
|
|
113
161
|
/** Put the caret where typing should carry on: the title first on a forum
|
|
114
162
|
* post, otherwise the end of whatever is already written. */
|
|
115
163
|
const focusEditor = () => {
|
|
116
|
-
if (
|
|
164
|
+
if (showTitle && titleRef.current) {
|
|
117
165
|
titleRef.current.focus();
|
|
118
166
|
return;
|
|
119
167
|
}
|
|
@@ -142,25 +190,37 @@ export function CommunityComposer({
|
|
|
142
190
|
composer.reset();
|
|
143
191
|
setTitle("");
|
|
144
192
|
upload.clear();
|
|
193
|
+
poll.reset();
|
|
194
|
+
setPollOn(false);
|
|
145
195
|
setError(null);
|
|
146
196
|
setConfirmingDiscard(false);
|
|
147
197
|
};
|
|
148
198
|
|
|
199
|
+
const changeTitle = (value: string) => {
|
|
200
|
+
setTitle(value);
|
|
201
|
+
poll.setQuestion(value);
|
|
202
|
+
};
|
|
203
|
+
|
|
149
204
|
const submit = async () => {
|
|
150
205
|
if (!canSubmit) return;
|
|
206
|
+
if (withPoll && !poll.attempt()) return;
|
|
151
207
|
setSending(true);
|
|
152
208
|
setError(null);
|
|
153
209
|
try {
|
|
154
210
|
const attachments = upload.hasPending ? await upload.uploadAll() : [];
|
|
211
|
+
const pollPayload = withPoll ? buildPollPayload(poll.draft) : null;
|
|
155
212
|
await onSubmit({
|
|
156
|
-
title: withTitle ? title.trim() : undefined,
|
|
213
|
+
title: pollPayload ? pollPayload.title : withTitle ? title.trim() : undefined,
|
|
157
214
|
body: composer.getBody(),
|
|
158
215
|
attachments,
|
|
159
216
|
mentionedAccountIds: composer.getMentionedAccountIds(),
|
|
217
|
+
...(pollPayload ? { poll: pollPayload.poll } : {}),
|
|
160
218
|
});
|
|
161
219
|
composer.reset();
|
|
162
220
|
setTitle("");
|
|
163
221
|
upload.clear();
|
|
222
|
+
poll.reset();
|
|
223
|
+
setPollOn(false);
|
|
164
224
|
// Posted, so the box has served its purpose and folds away. There is no
|
|
165
225
|
// draft left to lose: reset() ran a line above.
|
|
166
226
|
if (collapsible) setOpen(false);
|
|
@@ -285,13 +345,15 @@ export function CommunityComposer({
|
|
|
285
345
|
id={bodyId}
|
|
286
346
|
style={{ display: isOpen ? "flex" : "none", flexDirection: "column", gap: 8 }}
|
|
287
347
|
>
|
|
288
|
-
{
|
|
348
|
+
{showTitle && (
|
|
289
349
|
<input
|
|
290
350
|
ref={titleRef}
|
|
291
351
|
value={title}
|
|
292
|
-
onChange={(e) =>
|
|
293
|
-
placeholder="Title"
|
|
294
|
-
|
|
352
|
+
onChange={(e) => changeTitle(e.target.value)}
|
|
353
|
+
placeholder={withPoll ? tr("forge.community_composer.poll_question_placeholder") : "Title"}
|
|
354
|
+
aria-label={withPoll ? tr("forge.community_composer.poll_question_label") : undefined}
|
|
355
|
+
aria-invalid={withPoll && poll.showErrors && !!poll.errors.question}
|
|
356
|
+
maxLength={COMMUNITY_POLL_LIMITS.questionMaxLength}
|
|
295
357
|
style={{
|
|
296
358
|
padding: "8px 10px",
|
|
297
359
|
borderRadius: t.cornerRadius,
|
|
@@ -302,6 +364,11 @@ export function CommunityComposer({
|
|
|
302
364
|
}}
|
|
303
365
|
/>
|
|
304
366
|
)}
|
|
367
|
+
{withPoll && poll.showErrors && poll.errors.question && (
|
|
368
|
+
<div role="alert" style={{ fontSize: 12, color: "#dc2626" }}>
|
|
369
|
+
{tr(POLL_ERROR_KEY[poll.errors.question])}
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
305
372
|
|
|
306
373
|
{/* Formatting toolbar */}
|
|
307
374
|
<div style={{ display: "flex", alignItems: "center", gap: 4, flexWrap: "wrap" }}>
|
|
@@ -358,7 +425,8 @@ export function CommunityComposer({
|
|
|
358
425
|
color: ${t.muted};
|
|
359
426
|
pointer-events: none;
|
|
360
427
|
}
|
|
361
|
-
.forge-community-composer-editable [data-account-id]
|
|
428
|
+
.forge-community-composer-editable [data-account-id],
|
|
429
|
+
.forge-community-composer-editable [data-agent-id] {
|
|
362
430
|
color: ${t.primary};
|
|
363
431
|
font-weight: 600;
|
|
364
432
|
background: ${t.primary}1a;
|
|
@@ -386,7 +454,8 @@ export function CommunityComposer({
|
|
|
386
454
|
>
|
|
387
455
|
{composer.suggestions.map((s) => (
|
|
388
456
|
<button
|
|
389
|
-
key={s.accountId}
|
|
457
|
+
key={s.agentId ? `agent:${s.agentId}` : s.accountId}
|
|
458
|
+
data-testid={s.agentId ? "community-mention-agent" : "community-mention-member"}
|
|
390
459
|
type="button"
|
|
391
460
|
onMouseDown={(e) => {
|
|
392
461
|
// mousedown so the editor keeps focus.
|
|
@@ -407,14 +476,173 @@ export function CommunityComposer({
|
|
|
407
476
|
fontSize: 14,
|
|
408
477
|
}}
|
|
409
478
|
>
|
|
410
|
-
|
|
411
|
-
|
|
479
|
+
{s.agentId ? (
|
|
480
|
+
<>
|
|
481
|
+
<CommunityAgentAvatar agent={s} size={24} />
|
|
482
|
+
<CommunityAgentName agent={s} fontWeight={500} />
|
|
483
|
+
</>
|
|
484
|
+
) : (
|
|
485
|
+
<>
|
|
486
|
+
<MemberAvatar member={{ displayName: s.name, avatarUrl: s.avatarUrl }} size={24} />
|
|
487
|
+
<span style={{ fontWeight: 500 }}>{s.name}</span>
|
|
488
|
+
</>
|
|
489
|
+
)}
|
|
412
490
|
</button>
|
|
413
491
|
))}
|
|
414
492
|
</div>
|
|
415
493
|
)}
|
|
416
494
|
</div>
|
|
417
495
|
|
|
496
|
+
{withPoll && (
|
|
497
|
+
<fieldset
|
|
498
|
+
data-testid="community-composer-poll"
|
|
499
|
+
style={{ border, borderRadius: t.cornerRadius, padding: 10, margin: 0, minWidth: 0 }}
|
|
500
|
+
>
|
|
501
|
+
<legend style={{ padding: "0 4px", fontSize: 13, fontWeight: 600, color: t.text }}>
|
|
502
|
+
{tr("forge.community_composer.poll_options_legend")}
|
|
503
|
+
</legend>
|
|
504
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
|
505
|
+
{poll.draft.options.map((option, index) => {
|
|
506
|
+
const optionError = poll.showErrors ? poll.errors.options[index] : null;
|
|
507
|
+
const errorId = `${pollFieldId}-option-${index}-error`;
|
|
508
|
+
return (
|
|
509
|
+
<div key={index} style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
510
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
511
|
+
<input
|
|
512
|
+
value={option}
|
|
513
|
+
onChange={(e) => poll.setOption(index, e.target.value)}
|
|
514
|
+
placeholder={tr("forge.community_composer.poll_option_placeholder", { number: index + 1 })}
|
|
515
|
+
aria-label={tr("forge.community_composer.poll_option_label", { number: index + 1 })}
|
|
516
|
+
aria-invalid={!!optionError}
|
|
517
|
+
aria-describedby={optionError ? errorId : undefined}
|
|
518
|
+
maxLength={COMMUNITY_POLL_LIMITS.optionMaxLength}
|
|
519
|
+
style={{
|
|
520
|
+
flex: 1,
|
|
521
|
+
minWidth: 0,
|
|
522
|
+
padding: "6px 10px",
|
|
523
|
+
borderRadius: t.cornerRadius,
|
|
524
|
+
border: `1px solid ${optionError ? "#dc2626" : t.border}`,
|
|
525
|
+
background: t.background,
|
|
526
|
+
color: t.text,
|
|
527
|
+
fontSize: 14,
|
|
528
|
+
}}
|
|
529
|
+
/>
|
|
530
|
+
<button
|
|
531
|
+
type="button"
|
|
532
|
+
onClick={() => poll.removeOption(index)}
|
|
533
|
+
disabled={!poll.canRemoveOption}
|
|
534
|
+
aria-label={tr("forge.community_composer.poll_remove_option", { number: index + 1 })}
|
|
535
|
+
style={{
|
|
536
|
+
...ghostButton,
|
|
537
|
+
padding: "4px 8px",
|
|
538
|
+
display: "inline-flex",
|
|
539
|
+
alignItems: "center",
|
|
540
|
+
cursor: poll.canRemoveOption ? "pointer" : "not-allowed",
|
|
541
|
+
opacity: poll.canRemoveOption ? 1 : 0.4,
|
|
542
|
+
}}
|
|
543
|
+
>
|
|
544
|
+
<X size={14} aria-hidden />
|
|
545
|
+
</button>
|
|
546
|
+
</div>
|
|
547
|
+
{optionError && (
|
|
548
|
+
<span id={errorId} role="alert" style={{ fontSize: 12, color: "#dc2626" }}>
|
|
549
|
+
{tr(POLL_ERROR_KEY[optionError])}
|
|
550
|
+
</span>
|
|
551
|
+
)}
|
|
552
|
+
</div>
|
|
553
|
+
);
|
|
554
|
+
})}
|
|
555
|
+
{poll.showErrors && poll.errors.optionsList && (
|
|
556
|
+
<span role="alert" style={{ fontSize: 12, color: "#dc2626" }}>
|
|
557
|
+
{tr(POLL_ERROR_KEY[poll.errors.optionsList])}
|
|
558
|
+
</span>
|
|
559
|
+
)}
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
onClick={poll.addOption}
|
|
563
|
+
disabled={!poll.canAddOption}
|
|
564
|
+
style={{
|
|
565
|
+
...ghostButton,
|
|
566
|
+
alignSelf: "flex-start",
|
|
567
|
+
display: "inline-flex",
|
|
568
|
+
alignItems: "center",
|
|
569
|
+
gap: 4,
|
|
570
|
+
cursor: poll.canAddOption ? "pointer" : "not-allowed",
|
|
571
|
+
opacity: poll.canAddOption ? 1 : 0.5,
|
|
572
|
+
}}
|
|
573
|
+
>
|
|
574
|
+
<Plus size={14} aria-hidden />
|
|
575
|
+
{tr("forge.community_composer.poll_add_option")}
|
|
576
|
+
</button>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<label
|
|
580
|
+
style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 10, fontSize: 13, color: t.text }}
|
|
581
|
+
>
|
|
582
|
+
<input
|
|
583
|
+
type="checkbox"
|
|
584
|
+
role="switch"
|
|
585
|
+
checked={poll.draft.multipleChoice}
|
|
586
|
+
onChange={(e) => poll.setMultipleChoice(e.target.checked)}
|
|
587
|
+
style={{ accentColor: t.primary, margin: 0 }}
|
|
588
|
+
/>
|
|
589
|
+
{tr("forge.community_composer.poll_multiple_choice")}
|
|
590
|
+
</label>
|
|
591
|
+
|
|
592
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginTop: 10 }}>
|
|
593
|
+
<label style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 13, color: t.text }}>
|
|
594
|
+
{tr("forge.community_composer.poll_closes_label")}
|
|
595
|
+
<input
|
|
596
|
+
type="datetime-local"
|
|
597
|
+
value={poll.draft.closesAt}
|
|
598
|
+
onChange={(e) => poll.setClosesAt(e.target.value)}
|
|
599
|
+
aria-invalid={poll.showErrors && !!poll.errors.closesAt}
|
|
600
|
+
style={{
|
|
601
|
+
padding: "6px 8px",
|
|
602
|
+
borderRadius: t.cornerRadius,
|
|
603
|
+
border: `1px solid ${poll.showErrors && poll.errors.closesAt ? "#dc2626" : t.border}`,
|
|
604
|
+
background: t.background,
|
|
605
|
+
color: t.text,
|
|
606
|
+
fontSize: 13,
|
|
607
|
+
}}
|
|
608
|
+
/>
|
|
609
|
+
</label>
|
|
610
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 4, fontSize: 13, color: t.text }}>
|
|
611
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
|
|
612
|
+
<label htmlFor={`${pollFieldId}-visibility`}>{tr("forge.community_composer.poll_visibility_label")}</label>
|
|
613
|
+
<CallHelpHint label={tr("forge.community_composer.poll_visibility_help_label")}>
|
|
614
|
+
{tr("forge.community_composer.poll_visibility_help")}
|
|
615
|
+
</CallHelpHint>
|
|
616
|
+
</span>
|
|
617
|
+
<select
|
|
618
|
+
id={`${pollFieldId}-visibility`}
|
|
619
|
+
value={poll.draft.resultsVisibility}
|
|
620
|
+
onChange={(e) => poll.setResultsVisibility(e.target.value as CommunityPollResultsVisibility)}
|
|
621
|
+
style={{
|
|
622
|
+
padding: "6px 8px",
|
|
623
|
+
borderRadius: t.cornerRadius,
|
|
624
|
+
border,
|
|
625
|
+
background: t.background,
|
|
626
|
+
color: t.text,
|
|
627
|
+
fontSize: 13,
|
|
628
|
+
}}
|
|
629
|
+
>
|
|
630
|
+
{(Object.keys(VISIBILITY_KEY) as CommunityPollResultsVisibility[]).map((value) => (
|
|
631
|
+
<option key={value} value={value}>
|
|
632
|
+
{tr(VISIBILITY_KEY[value])}
|
|
633
|
+
</option>
|
|
634
|
+
))}
|
|
635
|
+
</select>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
{poll.showErrors && poll.errors.closesAt && (
|
|
639
|
+
<div role="alert" style={{ fontSize: 12, color: "#dc2626", marginTop: 4 }}>
|
|
640
|
+
{tr(POLL_ERROR_KEY[poll.errors.closesAt])}
|
|
641
|
+
</div>
|
|
642
|
+
)}
|
|
643
|
+
</fieldset>
|
|
644
|
+
)}
|
|
645
|
+
|
|
418
646
|
{upload.error && <div style={{ fontSize: 12, color: "#dc2626" }}>{upload.error}</div>}
|
|
419
647
|
{error && <div style={{ fontSize: 12, color: "#dc2626" }}>{error}</div>}
|
|
420
648
|
|
|
@@ -485,7 +713,30 @@ export function CommunityComposer({
|
|
|
485
713
|
>
|
|
486
714
|
📎
|
|
487
715
|
</button>
|
|
488
|
-
|
|
716
|
+
{allowPoll && (
|
|
717
|
+
<button
|
|
718
|
+
type="button"
|
|
719
|
+
onClick={() => setPollOn((on) => !on)}
|
|
720
|
+
aria-pressed={pollOn}
|
|
721
|
+
style={{
|
|
722
|
+
...ghostButton,
|
|
723
|
+
padding: "4px 10px",
|
|
724
|
+
display: "inline-flex",
|
|
725
|
+
alignItems: "center",
|
|
726
|
+
gap: 4,
|
|
727
|
+
background: pollOn ? `${t.primary}1a` : "none",
|
|
728
|
+
color: pollOn ? t.primary : t.text,
|
|
729
|
+
}}
|
|
730
|
+
>
|
|
731
|
+
<BarChart3 size={14} aria-hidden />
|
|
732
|
+
{tr("forge.community_composer.poll_toggle")}
|
|
733
|
+
</button>
|
|
734
|
+
)}
|
|
735
|
+
<span style={{ flex: 1, fontSize: 11, color: t.muted }}>
|
|
736
|
+
{composer.agent
|
|
737
|
+
? tr("forge.community_composer.mention_hint_agent", { agent_name: composer.agent.name })
|
|
738
|
+
: tr("forge.community_composer.mention_hint")}
|
|
739
|
+
</span>
|
|
489
740
|
{collapsible && (
|
|
490
741
|
<>
|
|
491
742
|
<CallHelpHint label="What happens to what I have typed?">
|