import { MutableRefObject } from 'react'; import { SuggestionCardSource } from '@brainfish-ai/components'; import { Heading } from './lib/getHeadings'; export type { Heading }; export type EditorTheme = 'light' | 'dark' | 'system'; export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected'; /** * Upload handler for editor media (paste/drop, video slash, ImageBlock). * `onProgress` is optional; when provided, report 0–1 as upload bytes progress (best-effort). */ export type MediaUploadHandler = (file: File, onProgress?: (fractionComplete: number) => void) => Promise; /** * Search result for link search functionality */ export type SearchResult = { title: string; subtitle?: string; url: string; }; export interface CollabUser { /** * Unique identifier for the user. */ id: string; /** * Display name for the user (shown on cursors). */ name: string; } export interface BrainfishEditorProps { /** * The initial content of the editor. * For collaborative editing, this is only applied if the Y.Doc is empty. */ markdown: string; /** * Optional class name for the container. */ className?: string; /** * Whether the editor is read-only. */ readOnly?: boolean; /** * Tailwind top offset class for the sticky review suggestion rail. * Defaults to `top-0`. */ reviewSuggestionRailTopClassName?: string; /** * Theme for the editor. 'system' follows OS preference. * @default 'system' */ theme?: EditorTheme; /** * Called when serialized markdown changes (collab or standalone). * Use for dirty tracking and syncing draft state (e.g. document.draftText) so publish button state is correct. */ onContentChange?: (markdown: string) => void; /** * Configuration for real-time collaboration. * When provided with `url` and `room`, enables collaborative editing. */ collabConfig?: CollabConfig; /** * Callback when user searches for documents from link insert interface. * If not provided, only URL input is shown (no search functionality). */ onSearchLink?: (term: string) => Promise; /** * Callback to check and correct grammar/spelling for the given text. * The callback is responsible for: * - Making the API call * - Showing loading/success/error toasts * - Handling errors * * @param text - The selected text to check (already normalized) * @returns The corrected text, or "OK" if no changes are needed * @throws Error if the check fails (editor will not replace text) */ onGrammarCheck?: (text: string) => Promise; /** * Callback to upload an image file and return its URL. * Used by ImageBlock component when user selects/pastes an image. * * @param file - The image file to upload * @param onProgress - Optional 0–1 upload progress (ImageBlock may not invoke this) */ onUploadImage?: MediaUploadHandler; /** * Callback to upload a video file and return its URL. * Used by video slash command when user selects a video file. * * @param file - The video file to upload * @param onProgress - Optional 0–1 upload progress */ onUploadVideo?: MediaUploadHandler; /** * Callback to upload a file attachment (PDF, Word, Excel, PowerPoint) and return its URL. * Used by the File slash command when user selects a document. * * @param file - The file to upload * @param onProgress - Optional 0–1 upload progress */ onUploadFile?: MediaUploadHandler; /** * URL or path for the image shown when an image fails to load. * @default "../assets/image-error.svg" (relative to bundle) */ imageErrorFallbackUrl?: string; /** * Called when document headings change. * Useful for rendering a Table of Contents. * Fires on initial load/sync and whenever headings are added, removed, or modified. */ onHeadingsChange?: (headings: Heading[]) => void; /** * Document issues from health check (e.g. broken links, inappropriate content). * Used to decorate in-editor ranges/nodes. action should match health check action * strings (e.g. "Broken article links", "Broken image links", "Inappropriate content"). */ issues?: DocumentIssue[]; /** * Loader for review suggestions. The editor owns the loaded list, resolves it * against the live ProseMirror document, and only reports resolution/action * results back to the host. */ reviewSuggestionsLoader?: ReviewSuggestionsLoader; /** * Called after the editor applies the selected suggestion to the live * document. Hosts should use this to persist suggestion status; document * content changes continue to flow through `onContentChange`. */ onReviewSuggestionAccept?: (suggestionId: string) => Promise | void; /** * Called when the user rejects a suggestion. Hosts should use this to mark * the suggestion rejected. */ onReviewSuggestionReject?: (suggestionId: string) => Promise | void; /** * Called when the user clicks a review-suggestion source that has a * `details.sourceId` (session recording). Hosts should open the player * with this id. Safe to handle while the editor is read-only. */ onReviewSuggestionSourceClick?: (sourceId: string) => void; /** * Called once per suggestion each time the suggestion list is resolved * against the live document, with the resolution outcome. Intended for * low-cardinality telemetry (e.g. emitting a counter keyed by `outcome`) * so producers can detect anchors that consistently fail to resolve. */ onReviewSuggestionResolve?: (suggestionId: string, outcome: ReviewSuggestionResolveOutcome) => void; /** * Called once per resolver pass with aggregate resolution data. Hosts can use * this to persist stale suggestions and display the count that actually * resolved in the editor without subscribing to editor state. */ onReviewSuggestionsResolved?: (summary: ReviewSuggestionsResolveSummary) => void; /** * Optional imperative bridge for hosts that render suggestion controls outside * the editor. When set, the editor writes live bulk actions for the mounted * ProseMirror view into the ref and clears it on teardown. */ reviewSuggestionBulkActionsRef?: MutableRefObject; /** * Help Center channels available for variable-content targeting. * Hosts typically pass `{ id, name }` from the team's help_center channels. */ channels?: Array<{ id: string; name: string; }>; /** * When set, variable content resolves to the matching channel variant (exact match, else default). * Used by Help Center read-only views and extensible for Platform single-channel editing. * When unset, all variants are shown. */ selectedChannelId?: string; } export type ReviewSuggestionBulkActions = { applyAllReviewSuggestions: () => string[]; rejectAllReviewSuggestions: () => string[]; getResolvedReviewSuggestionCount: () => number; /** * Serializes the live editor document. Hosts can call this immediately after * bulk apply to persist the exact post-transaction markdown without waiting * for the editor's async content-change listener. */ getMarkdown?: () => string; }; export type ReviewSuggestionsEditorData = { reviewSuggestions: ReviewSuggestion[]; }; export type ReviewSuggestionsLoader = { fetch: () => Promise; map: (raw: TRaw) => ReviewSuggestionsEditorData; }; /** * Per-suggestion display details surfaced in the review-suggestion card. * The host provides whichever fields it has — both are optional. */ export type ReviewSuggestionDetails = { source?: SuggestionCardSource; reason?: string; /** Analytics session id. Set only for replay_session sources that can open the player. */ sourceId?: string; }; export type ReviewSuggestionOpType = 'insert' | 'delete' | 'replace'; /** * Outcome of resolving a single review suggestion against the live PM doc. * Emitted via `onReviewSuggestionResolve` so hosts can wire up telemetry * (e.g. surface ambiguous anchors back to the producer). * * - `resolved`: strict `prefix + original + suffix` needle found uniquely. * - `resolved_fallback`: strict needle missed, but a one-sided fallback * (`original + suffix` or `prefix + original`) resolved uniquely. Indicates * the user has edited near the suggestion since it was generated; producer * anchors are slightly stale but still salvageable. * - `not_found`: neither strict nor fallback needles match. Suggestion is * silently dropped. * - `ambiguous`: a candidate needle is present more than once, or both * one-sided fallbacks resolve to different ranges. Resolver fails closed. * - `empty_anchor`: combined needle is the empty string; nothing to anchor on. * * Inserts (`originalText === ''`) never use fallback — matching on prefix or * suffix alone would almost always be ambiguous. */ export type ReviewSuggestionResolveOutcome = 'resolved' | 'resolved_fallback' | 'not_found' | 'ambiguous' | 'empty_anchor'; export type ReviewSuggestionUnresolvedOutcome = Exclude; export type UnresolvedReviewSuggestion = { id: string; outcome: ReviewSuggestionUnresolvedOutcome; reason: string; }; export type ReviewSuggestionsResolveSummary = { totalCount: number; resolvedCount: number; unresolvedCount: number; unresolved: UnresolvedReviewSuggestion[]; }; /** * A single review suggestion rendered inline in the editor. * * Anchor fields may contain markdown; `normalizeAnchors` strips markdown * and aligns punctuation before matching against the resolver's flattened doc text. * Replace/delete: range is located inside `originalText` between context strings. * Inserts: `originalText` is `''`; the resolver finds the boundary between * `contextPrefix` and `contextSuffix` (including block-boundary-tolerant matching). * * `proposedText` may be markdown (rendered in the chip / parsed on apply); * empty string for delete. */ export type ReviewSuggestion = { id: string; opType: ReviewSuggestionOpType; /** Slice the suggestion replaces (markdown allowed; normalized for matching). Empty for inserts. */ originalText: string; /** Context before `originalText` (markdown allowed; normalized for matching). */ contextPrefix: string; /** Context after `originalText` (markdown allowed; normalized for matching). */ contextSuffix: string; /** Replacement or insertion body; may be markdown. Empty for delete. */ proposedText: string; /** Optional card details displayed alongside this suggestion. */ details?: ReviewSuggestionDetails; }; /** * Document issue from health check. Compatible with Platform's DocumentIssue. * action values: "Broken article links", "Broken image links", "Inappropriate content", * "Grammatical errors", "Stale articles", "Similar documents". */ export type DocumentIssue = { id: string; action: string; issue: string; reason?: string; timeToFix?: number; }; export interface CollabConfig { /** * WebSocket URL for the collaboration server. * @example "wss://collab.example.com/collaboration" */ url: string; /** * Room/document identifier for collaboration. * Typically the document ID. */ room: string; /** * Authentication token for the collaboration server. */ token?: string; /** * Current user information for cursor display. */ user?: CollabUser; /** * Called when the connection status changes. */ onConnectionChange?: (status: ConnectionStatus) => void; /** * Called when the document is synced with the server. * This indicates the editor is ready for use. * @param helpers - Object containing helper functions for editor manipulation * @param helpers.replaceContent - Function to replace the entire editor content with new markdown * @param helpers.refreshHeadings - Function to manually trigger headings extraction for TOC */ onSynced?: (helpers: { replaceContent: (markdown: string) => void; refreshHeadings: () => void; }) => void; /** * Called when awareness states change (other users' cursors, selections, etc.). */ onAwarenessChange?: (states: Map>) => void; /** * Called when the collaboration server refuses the connection (permission * denied). The provider does not reconnect on its own after this, so the * host should decide what to do — e.g. refetch the document and reconnect * to a new room. * @param payload.reason - Reason string forwarded by the server * @param payload.hasUnsyncedChanges - True when local edits had not yet * reached the server and will be lost if the session is abandoned */ onAuthenticationFailed?: (payload: { reason: string; hasUnsyncedChanges: boolean; }) => void; }