diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 9f54464212..3f51174576 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -41,6 +41,7 @@ "hostPrivateInstancesDescription": "Surface insights from within your organization", "questionFeed": "Question Feed", "feed": "Feed", + "newCommentsSortNote": "New Comments only shows questions you have viewed before and that have unread comments.", "collectiveForecastsForPublicGood": "Collective forecasts for the public good", "followImportantTopics": "Follow important topics", "followImportantTopicsDescription": "AI, Geopolitics, Biosecurity, and more", @@ -235,6 +236,7 @@ "metaculus_prediction": "Metaculus Prediction", "metaculus": "Metaculus", "community": "community", + "enterProbability": "Enter probability", "communityPrediction": "Community Prediction", "unweightedAggregate": "Unweighted Aggregate", "firstQuartile": "lower 25%", @@ -272,6 +274,8 @@ "commentsReadGuidelines": "Read our guidelines", "commentsLearnMarkdown": "Learn about our Markdown", "commentDeleted": "Comment deleted", + "deleteCommentConfirmTitle": "Delete comment?", + "deleteCommentConfirmDescription": "This comment will be removed. This can't be undone.", "commentsReportCommentHeading": "Report this comment", "commentsReportCommentDescription": "Is there something wrong with this comment? Please help us keep our community great by reporting it.", "commentsReportSpam": "Report as spam", @@ -327,6 +331,8 @@ "hideCommunityPrediction": "Hide Community Prediction", "viewMetaculusPrediction": "View Metaculus Prediction", "questionSearchPlaceholder": "search questions...", + "feedSearchResultsFor": "{count} results for \"{search}\"", + "feedSearchResultsForWithoutCount": "Results for \"{search}\"", "articlesSearchPlaceholder": "search articles...", "tagSearchPlaceholder": "search tags...", "search": "Search", @@ -351,6 +357,7 @@ "searchOptionActivePrediction": "Active Prediction", "searchOptionWithdrawnPrediction": "Withdrawn", "searchOptionNotPredicted": "Not Predicted", + "searchOptionCommented": "Commented", "searchOptionAuthored": "Authored", "searchOptionUpvoted": "Upvoted", "searchOptionPrivate": "Private", @@ -375,6 +382,7 @@ "estimatedReadingTime": "{minutes} min read", "predictions": "Predictions", "predictors": "Predictors", + "predictorsMentionWarning": "Only curators and admins can notify @predictors. Your mention will not send notifications.", "relativeLog": "Relative Log", "unreadAll": "all unread", "questionsLeft": "{count, plural, =1 {1 question left} other {{count} questions left} }", @@ -384,6 +392,7 @@ "unreadWithTotalCount": "({unread_count_formatted} unread) {total_count_formatted} total", "unreadWithTotalCountXs": "({unread_count_formatted}) {total_count_formatted}", "forecastersWithCount": "{count_formatted} {count, plural, =1 {forecaster} other {forecasters}}", + "histogramBinShare": "{share} weight of forecasts", "or": "or", "signupInviteUsers": "Invite Users", "signupInviteUsersDescription": "Enter the email addresses of users you would like to invite, separated by newlines. They will receive an invitation to sign up by email.", @@ -489,6 +498,7 @@ "reminderErrorPercentLifetimeRequired": "Please set a percent of lifetime", "reminderErrorPercentLifetimeCannotHappen": "This would result in no notification, please select a lower value", "reminderErrorPercentLifetimeWouldNotRepeat": "This would result in no repetition. Consider switching to \"Doesn't Repeat\"", + "reminderErrorDuplicateDate": "This date is already selected. Please choose a different date.", "reminderNotePlaceholder": "Write yourself a note...", "reminderCPChangesDescription1": "Notify me when the forecast changes by more than 10%", "reminderCPChangesDescription2": "For continuous questions, this is measured by a Kolmogorov–Smirnov test.", @@ -601,6 +611,7 @@ "toggleAllTopics": "Toggle all topics", "toggleNewsAnnotations": "Toggle news annotations", "Filter": "Filter", + "sort": "Sort", "done": "done", "clear": "clear", "predicted": "Predicted", @@ -778,6 +789,7 @@ "aboutMetaculusTitle": "About Metaculus", "aboutMetaculusDescription": "Metaculus is an online forecasting platform and aggregation engine working to improve human reasoning and coordination on topics of global importance.", "more": "More", + "nMore": "{count} more", "moreLikely": "percentage points higher", "afterElection": "under {winner}", "ifCandidateElected": "if {candidate} elected", @@ -845,6 +857,7 @@ "userProfile": "{username}'s profile", "unknownUserProfile": "User's profile", "userMedals": "{username}'s medals", + "userContributions": "{username}'s contributions", "unknownUserMedals": "User's medals", "noMedals": "No medals yet", "leaderboard": "Leaderboard", @@ -910,6 +923,7 @@ "parentResolutionCriteria": "Parent Resolution Criteria", "childResolutionCriteria": "Child Resolution Criteria", "loadMoreComments": "Load more comments", + "linkedComment": "Linked comment", "followers": "Followers", "followed": "Followed", "followButton": "Follow", @@ -994,7 +1008,7 @@ "settingsNewEmail": "New Email", "settingsEmailChangeDescription": "Enter a new email address and a password, and we'll send you a link to change your email.", "settingsChangeEmailAddress": "Change email address", - "settingsChangeEmailAddressSuccess": "A confirmation email has been sent to your new address. Please follow the link inside to activate it.", + "settingsChangeEmailAddressSuccess": "A confirmation email has been sent to your old address. Please follow the link inside to activate it.", "emailChangeErrorMessage": "Failed to change email", "emailChangeSuccessMessage": "Your email address has been changed successfully", "socialAccountNoPasswordBanner": "Your account doesn't have a password yet. Set one below to change your email or log in with email and password.", @@ -1093,6 +1107,7 @@ "choicesSeparatedBy": "Choices (separated by ,)", "choicesLockedHelp": "Options can only be changed through the admin panel once forecasting has started.", "projects": "projects", + "projectsTags": "Projects / Tags", "FABPrizePool": "PRIZE POOL", "FABPrizeValue": "$30,000", "FABStartDate": "START DATE", @@ -1210,6 +1225,7 @@ "deselectAll": "deselect all", "forecastDataIsEmpty": "Forecast data is empty", "noForecastsYet": "No forecasts yet", + "noHistogramData": "No histogram data available", "RevealTemporarily": "Reveal Temporarily", "CPIsHidden": "Community Prediction is hidden", "createdByUserOnDate": "Created by {user} on {date}", @@ -1309,6 +1325,9 @@ "youArePostingAPrivateComment": "You are posting a private comment", "unread": "unread", "contentTranslatedHeaderText": "Some content on this page is automatically translated, and may be inaccurate.", + "translatedBy": "translated by", + "listLayout": "List layout", + "gridLayout": "Grid layout", "showOriginalContent": "Show original", "translated_by": "translated by", "nextQuestion": "Next Question", @@ -1323,6 +1342,8 @@ "keyFactor": "Key Factor", "scrollToKeyFactor": "Scroll to key factor", "keyFactors": "Key Factors", + "keyFactorsFor": "Key factors for", + "goToKeyFactor": "Go to key factor {number}", "topKeyFactors": "Top Key Factors", "notAvailable": "Not available", "indexesTitle": "Project", @@ -1383,6 +1404,7 @@ "selectChildQuestion": "Select Child Question", "pickQuestion": "Pick Question", "viewQuestions": "View Questions({count})", + "includingSubquestions": "Including Subquestions: {count}", "predictionFlow": "Prediction Flow", "started": "Started", "starts": "Starts", @@ -1781,6 +1803,7 @@ "unknown": "Unknown", "lowest": "Lowest", "highest": "Highest", + "fanChart": "Fan Chart", "timeline": "Timeline", "inNews": "In the News", "info": "Question Info", @@ -1965,6 +1988,7 @@ "copyQuestionLinkSuccess": "Question link successfully created! You can see them under the Question Links section for each question.", "viewQuestionLinks": "View Question Links", "moreActions": "More actions", + "openOnMetaculus": "Open on Metaculus", "copyQuestionLinkImpactPrefix": "has a", "copyQuestionLinkImpactSuffix": "causal impact on:", "copyQuestionLinkStrengthLabel": "The strength of this impact is:", @@ -1980,6 +2004,7 @@ "loadMore": "Load More", "noPrivateNotes": "No private notes yet", "privateNotes": "Private Notes", + "questionLinks": "Question Links", "justNow": "just now", "cmmButtonShort": "Mind", "myForecastingBots": "My Forecasting Bots", @@ -2026,6 +2051,7 @@ "tournamentTimelineStarts": "Starts {when}", "tournamentTimelineEnds": "Ends {when}", "tournamentTimelineClosed": "Pending resolutions", + "tournamentTimelinePendingWinners": "Pending winners announcement", "tournamentTimelineAllResolved": "All questions resolved", "tournamentRelativeSoon": "soon", "tournamentRelativeUnderMinute": "in under a minute", @@ -2084,7 +2110,7 @@ "readFullReport": "Read full report", "feedTileNewTournament": "New Tournament", "feedTileForecastNewQuestions": "Forecast {count, plural, one {# new question} other {# new questions}}", - "feedTileQuestionsResolved": "{count, plural, one {# question resolved} other {# questions resolved}}", + "feedTileQuestionsRecentlyResolved": "{count, plural, one {# question recently resolved} other {# questions recently resolved}}", "feedTileCheckLeaderboards": "Check out the final leaderboards", "feedTilePopularTournament": "Popular Tournament", "feedTileClosesIn": "Closes in {when}", @@ -2197,5 +2223,139 @@ "whyTrustMetaculusLessNoiseTitle": "Less noise, more truth", "whyTrustMetaculusLessNoise": "Metaculus's crowd-powered forecasts cut through the noise by grounding every prediction in transparent evidence, accountable scoring, and a decade of demonstrated accuracy. Metaculus equips policymakers, researchers, journalists, and corporate organizations with evidence-based forecasts that surface clear, quantifiable insight into the world's most critical uncertainties. Explore our suite of Business Solutions to learn more about how Metaculus can improve your organization's decision-making.", "publishTimeLockedDescription": "Publish time cannot be changed after creation.", - "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)" + "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)", + "feedTileSummaryRequiredForFormattedContent": "Feed tile summary is required because the visible start of your notebook contains formatting (a list, blockquote, code block, table, or raw HTML) that wouldn't render cleanly on a feed tile. Add a short plain-text summary so readers see a clean preview.", + "laborHubJobsBreadcrumb": "All Jobs", + "laborHubJobsVisitCta": "Visit Jobs", + "laborHubJobsPageTitle": "All Jobs · Labor Automation Forecasting Hub | Metaculus", + "laborHubJobsPageDescription": "Browse 15 US occupations forecasted by the Metaculus community through 2035. See how AI may reshape each one.", + "laborHubJobsHeroTitle": "Which jobs will survive AI?", + "laborHubJobsHeroLead": "Metaculus forecasters track 15 representative US occupations through 2035. Pick a job to see the forecast trajectory, the why behind the numbers, and what comes next.", + "laborHubJobDetailTitle": "Will AI replace {name}? · Metaculus Labor Hub forecast", + "laborHubJobDetailDescription": "Community forecasts for {name} employment, wages and AI exposure through 2035. Live from the Metaculus Labor Automation Forecasting Hub.", + "laborHubJobDetailSubtitle": "Community forecast of employment change versus 2025, across 2027, 2030, and 2035.", + "laborHubJobsClickTileSubtitle": "Click any tile for the full forecast & forecaster commentary", + "laborHubJobsYearToggleLabel": "Forecast year", + "laborHubJobsExploreCta": "Explore the full Labor Hub Dashboard", + "laborHubJobsExploreCtaSub": "Occupations and wages · economy-wide forecasts · forecaster commentary", + "laborHubJobsByYear": "By {year}", + "laborHubJobsJumpToLabel": "Jump to:", + "laborHubJobsFeltenLabel": "Felten et al. exposure", + "laborHubJobsFeltenTooltip": "Academic measure of how much a job's required abilities overlap with current AI capabilities.", + "laborHubJobsMnaLabel": "M&A vulnerability", + "laborHubJobsMnaTooltip": "Academic measure combining a job's AI exposure with how well its workers can adapt.", + "laborHubJobsAoeLabel": "Anthropic Exposure", + "laborHubJobsAoeTooltip": "Grounded in real, anonymized Claude usage — the share of this job's tasks already being done with AI.", + "laborHubJobsRingHigh": "HIGH", + "laborHubJobsRingMed": "MED", + "laborHubJobsRingLow": "LOW", + "laborHubJobsCuratedInsightsTitle": "Curated insights", + "laborHubJobsCuratedInsightsEmpty": "No insights surfaced for {name} yet — check the full Labor Hub for active discussion.", + "laborHubJobsInsightSourceFromAnotherPost": "from a related question", + "laborHubJobsWagesTitle": "Median wage by 2035", + "laborHubJobsWagesSub": "Real change vs 2025", + "laborHubJobsWagesUnavailable": "Per-occupation wage forecast not yet tracked", + "laborHubJobsHoursTitle": "Hours / week · economy-wide", + "laborHubJobsHoursEconomyWideSub": "By 2035, vs {from} hrs/week in 2025", + "laborHubJobsShareTitle": "Share this forecast", + "laborHubJobsShareLead": "Download a share card and share this forecast with your network.", + "laborHubJobsShareYearLabel": "Headline year", + "laborHubJobsShareYearOption": "By {year}", + "laborHubJobsShareSave": "Save Image", + "laborHubJobsShareTweet": "Share on X", + "laborHubJobsShareLinkedin": "Share on LinkedIn", + "laborHubJobsMetricSeeAll": "Click to compare this to other jobs →", + "laborHubJobsMetricPaperLink": "Read the paper →", + "laborHubJobsProForecasterTitle": "Pro Forecaster", + "laborHubJobsMetricChartHead": "How compares", + "laborHubJobsMetricLower": "lower", + "laborHubJobsMetricHigher": "higher", + "laborHubJobsFeltenSource": "Felten, Raj & Seamans — Language modeling AIOE", + "laborHubJobsFeltenAxisLabel": "Felten exposure score (standardized)", + "laborHubJobsFeltenNature1": "The AI Occupational Exposure (AIOE) measures the overlap between AI capabilities and the abilities an occupation requires, aggregated from O*NET ability data.", + "laborHubJobsFeltenNature3": "Higher exposure does not by itself mean a job will be automated — it can equally signal that AI augments the work.", + "laborHubJobsFeltenBounds": "Scale: standardized z-score · the average occupation sits near 0; positive values are more exposed than average, negative values less.", + "laborHubJobsMnaSource": "Manning & Aguirre — Vulnerability score", + "laborHubJobsMnaAxisLabel": "Manning & Aguirre vulnerability (0–1)", + "laborHubJobsMnaNature1": "Academic measure combining exposure to AI with workers' adaptive capacity.", + "laborHubJobsMnaNature3": "Vulnerability scores are related both to the likelihood of displacement (through AI exposure) and its burden (through adaptive capacity).", + "laborHubJobsMnaBounds": "Scale: 0 to 1 · highest in occupations that are both heavily exposed and their workers are poorly cushioned against displacement.", + "laborHubJobsAoeSource": "Massenkoff & McCrory — Observed Exposure (AOE)", + "laborHubJobsAoeAxisLabel": "Observed AI exposure (0–100%)", + "laborHubJobsAoeNature1": "Unlike purely theoretical measures, this is also grounded in real, anonymized Claude usage: what people are actually doing with AI right now, mapped to O*NET occupation tasks.", + "laborHubJobsAoeNature3": "AOE captures observed adoption, not potential — a low value may mean the work simply isn't being brought to Claude yet, not that AI couldn't do it.", + "laborHubJobsAoeBounds": "Scale: 0% to 100% · highest in occupations whose tasks AI can theoretically do and is already being used for.", + "laborHubJobsChartA11y": "Employment forecast by year", + "laborHubJobsChartBaseline": "Baseline", + "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", + "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubChamberSenate": "Senate", + "midtermsHubChamberHouse": "House", + "midtermsHubChamberGovernor": "Governor", + "midtermsHubChamberControl": "Chamber control", + "midtermsHubCongressForecast": "Congressional Control", + "midtermsHubChamberCurrent": "Current:", + "midtermsHubChamberForecast": "Forecast:", + "midtermsHubChamberTooltipBody": "Forecasters give {party} a {pct}% chance of holding the most {chamber} seats.", + "midtermsHubChamberTooltipDisclaimer": "Click to view forecasting question", + "midtermsHubPartyDemocrats": "Democrats", + "midtermsHubPartyRepublicans": "Republicans", + "midtermsHubOutcomeRepRep": "Rep House / Rep Senate", + "midtermsHubOutcomeRepDem": "Dem House / Rep Senate", + "midtermsHubOutcomeDemRep": "Rep House / Dem Senate", + "midtermsHubOutcomeDemDem": "Dem House / Dem Senate", + "midtermsHubCongressSummary": "Forecasters' current views on Congress control are reflected in the probabilities above.", + "midtermsHubThingsToWatch": "Key Drivers", + "midtermsHubAbortionAmendment": "Missouri abortion-rights repeal", + "midtermsHubAbortionAmendmentContext": "Missouri voters decide whether to repeal the abortion-rights protections they enshrined in 2024.", + "midtermsHubElectoralConsequences": "Electoral Consequences", + "midtermsHubConsequenceQuestion": "Question", + "midtermsHubConsequenceIfRep": "if Rep Congress", + "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceHeaderRepTitle": "Republican Congress", + "midtermsHubConsequenceHeaderRepSubtitle": "If GOP controls", + "midtermsHubConsequenceHeaderSplitTitle": "Split Congress", + "midtermsHubConsequenceHeaderSplitSubtitle": "If control is split", + "midtermsHubConsequenceIfSplit": "if Split Congress", + "midtermsHubMailInBallots": "Mail-in ballot court challenge", + "midtermsHubMailInBallotsContext": "New USPS restrictions could reshape mail-in voting access, but pending lawsuits may block them before November.", + "midtermsHubElectionEmergency": "Election-integrity national emergency", + "midtermsHubElectionEmergencyContext": "A national emergency declaration could expand federal authority over the election.", + "midtermsHubConsequenceHeaderDemTitle": "Democratic Congress", + "midtermsHubConsequenceHeaderDemSubtitle": "If Dems control", + "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", + "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", + "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", + "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", + "midtermsHubCommunityInsights": "Community Insights", + "midtermsHubScrollLeft": "Scroll insights left", + "midtermsHubScrollRight": "Scroll insights right", + "midtermsHubDemocrat": "Democrat", + "midtermsHubRepublican": "Republican", + "midtermsHubNotContested": "Not contested", + "midtermsHubDemPct": "{pct}% Democrat", + "midtermsHubRepPct": "{pct}% Republican", + "midtermsHubNoForecast": "No forecast", + "midtermsHubFooterDisclaimer": "Not affiliated with any political party.", + "midtermsHubHeroTitleLine1": "2026 US", + "midtermsHubHeroTitleLine2": "Midterm Elections", + "midtermsHubHeroSubtitle": "Forecasts from the Metaculus community on contested races and the policies they will shape.", + "midtermsHubHeroSubtitleMobile": "Real-time forecasts from the Metaculus community.", + "midtermsHubConsequencesSubtitle": "How forecasted outcomes shift depending on which party holds Congress.", + "midtermsHubUpdatedRealtime": "Updated in real time", + "midtermsHubMetaculusUser": "Metaculus User", + "midtermsHubComingSoon": "Coming Soon", + "midtermsHubClickToView": "Click to view question", + "midtermsHubSeatDistributionsTitle": "Seat Distributions", + "midtermsHubSeatDistributionsSubtitle": "The community's forecasts for the final seat advantage in each chamber.", + "midtermsHubSenateSeats": "Senate", + "midtermsHubHouseSeats": "House", + "midtermsHubDemSeatAdvantage": "Democratic Seat Advantage", + "midtermsHubRepSeatAdvantage": "Republican Seat Advantage", + "midtermsHubEven": "EVEN", + "midtermsHubForecastUnavailable": "Forecast unavailable", + "midtermsHubSeatAdvantageTooltip": "{count} seat advantage", + "midtermsHubSeatAdvantageOverTooltip": ">{count} seat advantage", + "midtermsHubProbabilityTooltip": "{value}% probability", + "midtermsHubEvenTooltip": "Even" } diff --git a/front_end/src/app/(main)/questions/components/notebook_form.tsx b/front_end/src/app/(main)/questions/components/notebook_form.tsx index 9d3993ccf6..5f55597542 100644 --- a/front_end/src/app/(main)/questions/components/notebook_form.tsx +++ b/front_end/src/app/(main)/questions/components/notebook_form.tsx @@ -33,36 +33,100 @@ import { getQuestionDraft, saveQuestionDraft, } from "@/utils/drafts/questionForm"; +import { getMarkdownSummary } from "@/utils/markdown"; import { getPostLink } from "@/utils/navigation"; import BacktoCreate from "./back_to_create"; import CategoryPicker from "./category_picker"; import { createQuestionPost, updatePost } from "../actions"; +// Run the same strip-markdown pipeline the feed tile uses (getMarkdownSummary) +// to scan exactly what a reader would see. Sized to the widest realistic tile +// (~900px → ~450 rendered chars); anything past that point is past every tile +// size and doesn't need a summary. MAX_SOURCE_CHARS_TO_SCAN bounds the remark +// parse cost on long notebooks — 2000 source chars maps to well over the +// rendered budget even with markdown-heavy input. TILE_VISIBLE_SOURCE_CHARS +// is a tighter window used for raw-source checks (lists, blockquotes, code +// fences): their markers are stripped before render, so we can't see them in +// the preview output and instead approximate "could appear on the tile" by +// scanning the first ~600 source chars. +const TILE_PREVIEW_WIDTH = 900; +const TILE_PREVIEW_HEIGHT = 80; +const MAX_SOURCE_CHARS_TO_SCAN = 2000; +const TILE_VISIBLE_SOURCE_CHARS = 600; + +// Patterns whose presence in the *rendered* preview surfaces visibly on the +// tile. strip-markdown leaves these in: raw HTML tags (iframes, divs) and +// pipes from table-like content MDXEditor didn't promote to a real table. +// Plain text and `[text](url)` link syntax are accepted: links re-render as +// real tags via the tile's read-mode MarkdownEditor. +const NOISY_RENDERED_PATTERNS: RegExp[] = [ + /<[a-zA-Z][^>]*>/, // raw HTML + /\|[^|\n]*\|[^|\n]*\|/, // 3+ pipes on a single line (table-ish) +]; + +// Patterns whose markers strip-markdown removes, but whose visual semantics +// (bullets, numbering, indentation, monospace) are part of the author's +// intent. Without them the tile shows the content as run-on prose, which +// silently strips meaning rather than displaying syntax. Checked against the +// raw source within the tile-visible window. The `{0,3}` allowance matches +// CommonMark's leading-space tolerance for these block constructs. +const NOISY_RAW_SOURCE_PATTERNS: RegExp[] = [ + /^[ \t]{0,3}[-*+][ \t]+\S/m, // bullet list item + /^[ \t]{0,3}\d+[.)][ \t]+\S/m, // ordered list item + /^[ \t]{0,3}>[ \t]?/m, // blockquote + /^[ \t]{0,3}```/m, // fenced code block +]; + +const hasNoisyTilePreview = (markdown: string | undefined): boolean => { + if (!markdown) return false; + const rawSource = markdown.slice(0, TILE_VISIBLE_SOURCE_CHARS); + if (NOISY_RAW_SOURCE_PATTERNS.some((pattern) => pattern.test(rawSource))) { + return true; + } + const preview = getMarkdownSummary({ + markdown: markdown.slice(0, MAX_SOURCE_CHARS_TO_SCAN), + width: TILE_PREVIEW_WIDTH, + height: TILE_PREVIEW_HEIGHT, + }); + return NOISY_RENDERED_PATTERNS.some((pattern) => pattern.test(preview)); +}; + const createNotebookSchema = (t: ReturnType) => { - return z.object({ - title: z - .string() - .min(4, { - message: t("errorMinLength", { field: "String", minLength: 4 }), - }) - .max(200, { - message: t("errorMaxLength", { field: "String", maxLength: 200 }), - }), - short_title: z - .string() - .min(4, { - message: t("errorMinLength", { field: "String", minLength: 4 }), - }) - .max(80, { - message: t("errorMaxLength", { field: "String", maxLength: 80 }), + return z + .object({ + title: z + .string() + .min(4, { + message: t("errorMinLength", { field: "String", minLength: 4 }), + }) + .max(200, { + message: t("errorMaxLength", { field: "String", maxLength: 200 }), + }), + short_title: z + .string() + .min(4, { + message: t("errorMinLength", { field: "String", minLength: 4 }), + }) + .max(80, { + message: t("errorMaxLength", { field: "String", maxLength: 80 }), + }), + default_project: z.number(), + markdown: z.string().min(1, { + message: t("errorMinLength", { field: "String", minLength: 1 }), }), - default_project: z.number(), - markdown: z.string().min(1, { - message: t("errorMinLength", { field: "String", minLength: 1 }), - }), - feed_tile_summary: z.string().optional(), - }); + feed_tile_summary: z.string().optional(), + }) + .superRefine((data, ctx) => { + const summary = data.feed_tile_summary?.trim() ?? ""; + if (summary.length === 0 && hasNoisyTilePreview(data.markdown)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t("feedTileSummaryRequiredForFormattedContent"), + path: ["feed_tile_summary"], + }); + } + }); }; type FormData = z.infer>;