gpt_markdown

repository·main·Indexed 41 days ago

https://github.com/infinitix-llc/gpt_markdown

A production-grade Flutter renderer for AI-generated content. It supports complex Markdown, LaTeX math, streaming text updates, and custom inline UI patterns. The package provides high-performance rendering for AI chat interfaces, featuring split-document caching for streaming responses and a flexible customization system using Style objects for appearance and Builders for structural changes.

Tokens
26.2K
Snippets
82
Records
102
Agent score
51%

What's inside gpt_markdown

  1. Customize gpt_markdown appearance and structure

    main

    You can customize gpt_markdown using two distinct mechanisms that do not overlap:

    1. Style objects: Use these to change the appearance (colors, sizes, padding, fonts) of existing components. This is the preferred method for visual tweaks as it preserves the default component structure and future improvements.
    2. Builders: Use these to change the structure by replacing a component's widget entirely. Use a builder only when you need to fundamentally change how a component is laid out or behaves.

    Every component (headings, links, code blocks, etc.) supports both styles and builders.

    // Example: Using a Style object for appearance
    GptMarkdown(
      text,
      styleSheet: const GptMarkdownStyleSheet(
        blockQuote: BlockQuoteStyle(barWidth: 4),
      ),
    )
    
    // Example: Using a Builder for structure
    GptMarkdown(
      text,
      blockQuoteBuilder: (context, content, style) => Card(
        child: content,
      ),
    )
  2. Manage Markdown Scopes for InlinePatterns

    main

    Markdown is hierarchical. InlinePattern allows you to restrict where a pattern is applied using scopes to prevent rendering bugs and unintended matches.

    Available Scopes

    • content: Ordinary document and inline text.
    • linkLabel: Inside the label of a [label](url) link.
    • tableCell: Inside a table cell.
    • heading: Inside a # heading.

    By default, InlinePattern uses MarkdownComponent.allScopesExceptLinkLabel.

    Do not return a WidgetSpan inside a linkLabel scope. On iOS, returning a WidgetSpan inside a link label (which is already wrapped in a WidgetSpan) creates a nested placeholder that is invisible. If your pattern needs to work inside link labels, ensure your builder returns a TextSpan.

    // Safe for nesting in link labels
    InlinePattern(
      pattern: RegExp(r'GH-\d+'),
      builder: (context, match, style) => TextSpan(text: match.group(0)!),
      scopes: MarkdownComponent.allScopes,
    )
    
    // Restricted to prose only
    InlinePattern(
      pattern: RegExp(r'#token'),
      builder: (context, match, style) => TextSpan(text: match.group(0)!),
      scopes: const {MarkdownScope.content},
    )
  3. Streaming limitations and accessibility

    main

    Limitations

    • Selection: Text selection is unavailable while the reply is actively revealing because the 'tail' is rebuilt every frame. Selection becomes stable once the reply settles.
    • Fade Edge: The fade animation occurs at the reveal edge (the point where new text appears), not per individual character/glyph.

    Accessibility

    • The widget honors the system's reduced motion settings. If MediaQuery.disableAnimationsOf is true, the reveal is skipped and the document renders immediately without a ticker.
  4. Configure component lists in GptMarkdown

    main

    When providing components (for blocks) or inlineComponents (for inline syntax) to GptMarkdown, providing a list replaces the default behavior. To keep built-in support (like bold, links, and tables), you must spread the default components into your list.

    Order matters: The list order determines which regex matches first and which handler claims the match. To override a default component, prepend your custom component to the list.

    Performance tip: Component lists are compared by identity. Do not build the list inside a build method; instead, cache it as a late final variable to avoid regenerating spans every frame.

    // Correct way to add a custom component while preserving defaults
    GptMarkdown(
      text,
      components: [...],
      inlineComponents: [MyComponent(), ...MarkdownComponent.inlineComponents],
    )
  5. Configure the isStreaming parameter

    main

    The isStreaming boolean is required for correct behavior:

    • isStreaming: true: The widget uses an optimized 'split' path. It caches the 'settled prefix' (everything before the last safe blank line) and only rebuilds the 'live tail'. This makes streaming much faster than static rendering for long documents.
    • isStreaming: false: The widget renders statically. Use this for history messages or once a stream has completed. This avoids all animation and ticker overhead.

    Warning: Always set isStreaming to false when the stream finishes or encounters an error. Leaving it true causes the ticker to run indefinitely, wasting resources.

    GptMarkdown(
      message.text,
      animation: GptMarkdownAnimation.fade,
      isStreaming: message.isGenerating, // false for history or finished messages
    )
  6. How to customize appearance vs structure in gpt_markdown

    main

    When customizing the rendering, you must choose between two distinct mechanisms that do not overlap. Using the wrong one can lead to unnecessary complexity or loss of default functionality.

    1. Style objects: Use these for Appearance changes such as colors, sizes, padding, and fonts. This is the preferred method for visual tweaks.
    2. Builders: Use these for Structure changes when you need to replace a component's widget entirely.

    Best Practice: If you only want to change a color or a font, use a Style object. Using a Builder for simple visual changes causes the component to lose its default structure and any future improvements made to that component's default implementation.

    // Example of using a Style object for appearance
    BlockQuoteStyle(barWidth: 4)
    
    // Example of using a Builder for structural replacement
    blockQuoteBuilder: (context, content) => MyCustomWidget(content)
  7. Enable text selection and RTL support

    main

    Text Selection

    To allow users to select and copy text, wrap the GptMarkdown widget in a SelectionArea.

    Right-to-Left (RTL)

    For RTL languages, use the textDirection property. GptMarkdown correctly handles the visual ordering of inline widgets (math, images, links) in mixed-direction paragraphs.

    // Enable selection
    SelectionArea(child: GptMarkdown(reply))
    
    // Enable RTL
    GptMarkdown(reply, textDirection: TextDirection.rtl)
  8. Render Markdown with GptMarkdown

    main

    Use the GptMarkdown widget to render AI-generated markdown.

    Important: The widget sizes itself to its content and does not scroll. To prevent long replies from overflowing, wrap the widget in a scrollable container like SingleChildScrollView or use it within a ListView.builder.

    // Basic usage
    GptMarkdown('# Hello\n\nSome **bold** text and `inline code`.')
    
    // Recommended: Wrap in a scrollable widget to prevent overflow
    SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: GptMarkdown(reply),
    )
    
    // Usage in a chat list
    ListView.builder(
      itemCount: messages.length,
      itemBuilder: (context, i) => Padding(
        padding: const EdgeInsets.symmetric(vertical: 8),
        child: GptMarkdown(messages[i].text),
      ),
    )
  9. Prevent double text scaling in custom inline widgets

    main

    In v1.2.0, text scaling is now proportional and exact. If you are building custom inline widgets using WidgetSpan, ensure they do not scale twice by wrapping the child in MediaQuery.withNoTextScaling.

    WidgetSpan(child: MediaQuery.withNoTextScaling(child: MyChip()))
  10. Inspect components using the widgetbook toolbar

    main

    The widgetbook provides a toolbar to test how components respond to various environmental factors. Available addons include:

    • Text scale: Adjust from 0.85x to 3.0x (simulating OS settings).
    • Theme: Switch between Light and Dark modes (both use GptMarkdownThemeData).
    • Viewport: Simulate different devices like iPhone SE, iPhone 13, iPhone 13 Pro Max, iPad Air, Galaxy S20, and OnePlus 8 Pro.
    • Alignment: Change where the use case sits within the frame.
    • Inspector: View widget bounds overlay.
    • Grid: Show an alignment grid.

    Additionally, some use cases include custom knobs (e.g., sliders for font size factor, border width, radius, and padding).

  11. Optimize performance for non-animated streaming

    main

    If you want the performance benefits of the 'split' path (caching the prefix) but do not want a visible reveal animation, do not use animation: none. Using none forces the widget to rebuild the entire document on every token.

    Instead, use GptMarkdownAnimation.fade with a very high charactersPerSecond value. This allows the reveal to complete within a single frame while still utilizing the optimized split path.

    GptMarkdown(
      reply,
      animation: GptMarkdownAnimation.fade,
      isStreaming: generating,
      charactersPerSecond: 100000, // Completes within a frame
    )