What Smart Doc is

Smart Doc (internally "SparkDoc") is Allego's AI-powered document authoring tool. It shares the same Vite SPA as the lesson builder — both live under webapp/sco/lesson-builder/ — but forks at the top: AllegoLesson.tsx for lessons, AllegoDocument.tsx for documents.

The critical architectural difference: Smart Doc uses a two-layer model that separates structure from content. Lessons use a flat array. This separation is what makes block-level AI generation straightforward in Smart Doc.

The two architectures side by side

Lesson — flat block array

LessonContext
blocks: Block[]
{ id, blockType, title, bodyText, … }
{ id, blockType, questionText, answers, … }
{ id, blockType, htmlText, listItems, … }

Each block is self-contained — the block object is its own content. No separate spec layer.

Smart Doc — layout + spec

DocumentLayout (structure)
Page → Row → Column →
BlockContent { uniqueId, blockTemplate }
DocumentSpec[] (content)
{ uniqueid, blocktemplate, htmlSnippet, … }
{ uniqueid, blocktemplate, dataMatrix, … }

Layout holds the grid. Spec holds the content. Linked by uniqueId. Either layer changes independently.


Smart Doc's three update mechanisms

Smart Doc never does full-document replacement after initial generation. Every subsequent change uses one of three targeted mechanisms.

1. User field edits — Froala → single spec field

When a user edits a block via the Froala WYSIWYG editor, the change touches exactly one field on one spec item.

User typesFroala editor
→
Context callonDocumentBlockFieldChange(uniqueId, fieldKey, value)
→
State patchCopy-on-write: only spec[idx][fieldKey] changes

The fieldKey is typed to one of five values: htmlContent, listText, chartConfig, image, or heading. Everything else in the document is untouched.

2. AI block-level generation — single-block LLM request

The per-block sparkle button sends only that one block's metadata to the LLM. The response replaces only that block's spec item.

User clicksBlock AI button
→
BuildbuildBlockRequest(uniqueId, template, heading)
→
APIPOST /rest/document/create
→
ApplyreplaceSpecItem(uniqueId, newItem)

The request payload includes a blockTemplateDictionary entry for just the target block's template type, telling the LLM exactly which structured fields to return. On response, replaceSpecItem preserves user-selected images and headings — it merges, it doesn't blindly overwrite.

3. Layout structural operations — block CRUD

The layout reducer handles add, remove, move, and replace as immutable tree transforms. Spec items are automatically created or pruned by useEffect hooks that watch the layout state.

ActionADD_BLOCK_AT_EDGE
→
ReducerDeep-clone layout, insert block
→
Side effectAppend default spec item for new uniqueId

Other layout actions: REMOVE_BLOCK (auto-prunes orphaned spec), MOVE_BLOCK (drag-and-drop), REPLACE_BLOCK (swap template type).


What lessons do today

The lesson editor has block-level manual editing (add, remove, move, clone, edit fields) but no block-level AI editing. Generation is all-or-nothing.

User submitsAI generation form
→
APIPOST /rest/lesson/outline + /rest/lesson/create
→
ResponseAIBlock[] — full lesson spec
→
ApplycreateFromLessonSpect() — replaces all blocks

createFromLessonSpect iterates the LLM's AIBlock[] array, calls createBlock(type, aiBlock) for each, and sets this.lesson.blocks = blocks. Any blocks the user had — manual edits, image choices, quiz adjustments — are wiped.

The individual block editing infrastructure is solid. onBlockFieldChange patches a single field on a single block. BlockActionType handles Move, Clone, Remove. What's missing is the bridge: telling the LLM "regenerate this one block and slot the result back in."


Gap analysis

Capability Smart Doc Lessons
Data model Two-layer: layout (structure) + spec (content), linked by uniqueId Flat Block[] — each block is its own content. Identified by id
Per-block AI generation buildBlockRequest() sends one block to LLM → replaceSpecItem() slots it back Not implemented — LLM always returns full lesson
Block template dictionary 17 templates with structured field definitions the LLM uses to shape its output Block types exist but no LLM-facing dictionary that describes expected output fields
User edit preservation replaceSpecItem() preserves images and headings; htmlContent (user edits) overrides htmlSnippet (generated) No distinction between user-edited and AI-generated content within a block
Block CRUD Layout reducer: ADD_BLOCK_AT_EDGE, REMOVE_BLOCK, MOVE_BLOCK, REPLACE_BLOCK Lesson context reducer: ADD_BLOCK, DELETE_BLOCK, MOVE_BLOCK, CLONE_BLOCK
Dual-mode AI form GenerateAIDocumentForm handles both full-doc and per-block via isBlockMode GenerateAILessonForm handles full-lesson only
Backend endpoint Same POST /rest/document/create for both modes — payload determines scope POST /rest/lesson/create returns full lesson only

What we can reuse

1

Per-block request/response pattern

Smart Doc's buildBlockRequest() sends a focused payload with just the target block's metadata and its template dictionary entry. On response, replaceSpecItem() merges the new content while preserving user selections. Lessons can follow the exact same pattern: build a single-block payload from the lesson block's id and blockType, send it through a new or extended API endpoint, and apply the result with a merge function that respects user edits.

High reuse value Medium effort
2

Block template dictionary concept

Smart Doc's documentBlockTemplateDictionary tells the LLM exactly what structured fields to return per block template (e.g., paragraphs → htmlSnippet, attribute-table → dataMatrix). Lessons need an equivalent lessonBlockTemplateDictionary mapping block types like text-1-col → { bodyText }, question-choose-one → { questionText, answers[] }, etc. The concept maps directly; the field names differ.

High reuse value Medium effort
3

Dual-mode AI form

GenerateAIDocumentForm switches between full-document and single-block generation with a single isBlockMode flag. The form narrows its UI to show only the target block's heading, guidelines, and source material. GenerateAILessonForm could adopt the same pattern — add an isBlockMode prop, limit the form to one block's parameters, and wire a new onBlockSuccess callback alongside the existing onLessonSpecSuccess.

High reuse value Low effort
4

Merge-not-replace application logic

Smart Doc's handleReplaceSpecItem() in document-context.tsx shows the exact merge logic: find the existing item by uniqueId, preserve user-selected images (via BLOCKS_WITH_PRESERVED_IMAGES), always keep the existing heading. For lessons, a parallel replaceLessonBlock() would find the block by id, preserve user-uploaded images (base64Image, fileName), user-set titles, and quiz scoring state while accepting the new AI-generated text content.

High reuse value Low effort
5

User-edit vs. AI-generated content distinction

Smart Doc distinguishes htmlSnippet (AI-generated) from htmlContent (user Froala edits). When the user has edited, htmlContent takes precedence in rendering. When AI regenerates a block, it replaces htmlSnippet and clears htmlContent. Lessons currently have no such distinction — all content fields are treated identically regardless of origin. Adding a lightweight "dirty" or "user-modified" flag per field would let the system warn before overwriting manual edits.

Medium reuse value Medium effort
6

Same-endpoint dual-mode API

Smart Doc's backend reuses POST /rest/document/create for both full-document and single-block generation — the payload shape determines which mode runs. The lesson backend (POST /rest/lesson/create) could follow the same approach: when the request includes a block field (single block metadata), the apiserver generates only that block and returns a single AIBlock instead of a full lesson spec. No new endpoint needed.

Medium reuse value High effort

Key files to study

Smart Doc (the model to follow)

Lesson side (what we're extending)

Backend


Bottom line

Smart Doc already solved the hard design problem — how to scope an AI generation request to a single block and merge the result without losing user work. The lesson editor doesn't need Smart Doc's two-layer model (lessons are simpler; the flat array is fine), but it can directly adopt the patterns that sit on top of it:

The largest open question for the Decide phase is whether the backend API can be extended to accept single-block lesson requests or whether a new prompt type is needed in the apiserver. That's where the bulk of the effort sits — the frontend patterns are well-proven and map cleanly.