Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2197,5 +2197,6 @@
"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 <servicesLink>Business Solutions</servicesLink> 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 a 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."
}
86 changes: 64 additions & 22 deletions front_end/src/app/(main)/questions/components/notebook_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,78 @@ 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.
const TILE_PREVIEW_WIDTH = 900;
const TILE_PREVIEW_HEIGHT = 80;
const MAX_SOURCE_CHARS_TO_SCAN = 2000;

// Patterns whose presence in the *rendered* preview would surface 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 <a> tags via the tile's read-mode MarkdownEditor.
const NOISY_TILE_PREVIEW_PATTERNS: RegExp[] = [
/<[a-zA-Z][^>]*>/, // raw HTML
/\|[^|\n]*\|[^|\n]*\|/, // 3+ pipes on a single line (table-ish)
];

const hasNoisyTilePreview = (markdown: string | undefined): boolean => {
if (!markdown) return false;
const preview = getMarkdownSummary({
markdown: markdown.slice(0, MAX_SOURCE_CHARS_TO_SCAN),
width: TILE_PREVIEW_WIDTH,
height: TILE_PREVIEW_HEIGHT,
});
return NOISY_TILE_PREVIEW_PATTERNS.some((pattern) => pattern.test(preview));
};

const createNotebookSchema = (t: ReturnType<typeof useTranslations>) => {
return z.object({
title: z
.string()
.min(4, {
message: t("errorMinLength", { field: "String", minLength: 4 }),
})
.max(200, {
message: t("errorMaxLength", { field: "String", maxLength: 200 }),
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 }),
}),
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 }),
}),
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<ReturnType<typeof createNotebookSchema>>;

Expand Down
Loading