-- allium: 1 -- bDS Editor Views -- Scope: UI content area (all waves + extensions) -- Distilled from: PostEditor.tsx, MediaEditor.tsx, Editor.tsx (Dashboard), -- SettingsView.tsx, StyleView.tsx, ChatPanel.tsx, TagsView, -- ScriptEditor.tsx, TemplateEditor.tsx, MenuEditorView.tsx, -- MetadataDiffPanel.tsx, ImportAnalysisView.tsx -- Describes the layout and behaviour of each editor view rendered in -- the main content area when a tab is active (or dashboard when no tab). -- Tab routing is in tabs.allium. Sidebar navigation is in sidebar_views.allium. use "./tabs.allium" as tabs use "./post.allium" as post use "./media.allium" as media use "./i18n.allium" as i18n -- ─── External surfaces ────────────────────────────────────── -- The user is the external actor who initiates all editor interactions -- via clicks, keyboard shortcuts, and drag-and-drop. This surface -- provides the triggers that editor action rules listen for. surface User { provides: PostAIAnalysisRequested(post_id) provides: PostTranslateRequested(post_id, target_language) provides: PostSaved(post_id) provides: PostPublishRequested(post_id) provides: PostDiscardRequested(post_id) provides: PostDeleteRequested(post_id) provides: PostInsertLinkRequested(post_id) provides: PostInsertMediaRequested(post_id) provides: PostGalleryRequested(post_id) provides: ImageDroppedOnEditor(post_id, file_path) provides: PostLanguageDetectRequested(post_id) provides: MediaAIImageAnalysisRequested(media_id) provides: MediaDetectLanguageRequested(media_id) provides: MediaTranslateMetadataRequested(media_id, target_language) provides: MediaReplaceFileRequested(media_id) provides: MediaDeleteRequested(media_id) provides: MediaLinkToPostRequested(media_id) provides: MediaTranslationEditClicked(media_id, language) provides: MediaTranslationRefreshClicked(media_id, language) provides: MediaTranslationDeleteClicked(media_id, language) provides: StyleThemeSelected(theme_name) provides: StyleApplyRequested(theme_name) provides: ScriptSaveRequested(script_id) provides: ScriptRunRequested(script_id) provides: ScriptDeleteRequested(script_id) provides: TemplateSaveRequested(template_id) provides: TemplateDeleteRequested(template_id) provides: ImportAnalyzeRequested(definition_id, file_path) provides: ImportExecuteRequested(definition_id) } -- ─── Dashboard (no tab active) ─────────────────────────────── -- Shown as default/welcome view when no entity tab is active. -- Single centered column. value Dashboard { title: String subtitle: String stats: DashboardStats timeline: DashboardTimeline tag_cloud: DashboardTagCloud category_cloud: DashboardCategoryCloud recent_posts: List } value DashboardStats { -- Three stat cards side by side: total_posts: Integer published_count: Integer draft_count: Integer archived_count: Integer -- shown only if > 0 media_count: Integer image_count: Integer total_media_size: String -- formatted B/KB/MB/GB tag_count: Integer category_count: Integer } value DashboardTimeline { -- Bar chart of posts over time -- Last 12 months that have data -- Each bar: count label on top, month abbreviation + year below -- Bar height proportional to max count months: List } value DashboardTimelineMonth { label: String -- month abbreviation year: Integer count: Integer } value DashboardTagCloud { -- Up to 40 tags, sorted alphabetically -- Font size scaled 11px-22px based on post count -- Tags with colours get coloured background with contrast text -- Hover title shows post count -- "and N more" text if > 40 tags tags: List overflow_count: Integer? } value DashboardTag { name: String count: Integer color: String? } value DashboardCategoryCloud { -- All categories as badge-like tags -- Each shows category name + count categories: List } value DashboardCategory { name: String count: Integer } value DashboardRecentPost { -- Last 5 posts by updatedAt descending -- Each row: title (or "Untitled"), status badge (draft/published), date -- Single-click: preview tab, double-click: pin tab post_id: String title: String status: String date: String } config { dashboard_max_tags: Integer = 40 dashboard_tag_min_font: Integer = 11 dashboard_tag_max_font: Integer = 22 dashboard_recent_count: Integer = 5 dashboard_timeline_months: Integer = 12 } -- ─── Post editor ────────────────────────────────────────────── -- Single vertical column. Three regions: header, body, footer. -- Header bar: -- Left: post title (or "Untitled") with dirty indicator dot -- Right: status badge, auto-save indicator, Quick Actions dropdown, Publish/Delete buttons -- Quick Actions: -- AI Analysis (suggests title, excerpt, slug) -- Translate Post (opens translation modal) -- Collapsible Metadata section (starts expanded when title is empty): -- Two-column layout. Left column: metadata fields. Right column: linked media panel. -- Left column fields: -- Title (text input), Tags (autocomplete chip input), Author (text input), -- Language (select + AI detect button), Do Not Translate (checkbox), -- Slug (read-only), Categories (chip input), Template (select, if templates exist), -- PostLinks (backlinks and outlinks) -- Right column: -- LinkedMediaPanel (media items linked to this post) -- Language flags bar (inline with metadata toggle): -- Row of flag emoji buttons for canonical language + each translation -- Styles: status class (draft/published), active indicator -- Clicking switches editor content to that language's draft -- Collapsible Excerpt section: -- Excerpt textarea (4 rows) -- Editor Body: -- Toolbar: "Content" label | mode toggle (Visual/Markdown/Preview) | action buttons -- Action buttons (markdown mode only): Gallery, Insert Post Link, Insert Media -- Visual mode: rich-text WYSIWYG editor -- Markdown mode: code editor with markdown-with-macros language -- Highlights [[macro ...]] double-bracket syntax -- Word wrap on, minimap off, 14px font -- Preview mode: iframe showing rendered preview -- Supports drag-and-drop of images (imports media, inserts markdown) -- Footer: -- Three date stamps: Created, Updated, Published (published only if publishedAt exists) -- Locale-formatted dates -- Modals: -- InsertModal for link search (Ctrl+K) and media search -- AISuggestionsModal for AI-generated title/excerpt/slug -- Translation modal: select target language, shows existing status -- Lightbox for content images value PostEditorView { post_id: String header: PostEditorHeader metadata_expanded: Boolean excerpt_expanded: Boolean editor_mode: String -- visual | markdown | preview } value PostEditorHeader { title: String -- post title or "Untitled" is_dirty: Boolean status: String -- draft | published | archived is_auto_saving: Boolean } config { post_auto_save_delay: Integer = 3000 -- milliseconds of idle before auto-save triggers } invariant PostAutoSave { -- Auto-saves after 3 seconds of idle in the editor -- Also auto-saves on unmount/tab switch -- Ctrl/Cmd+S triggers immediate save } invariant PostDirtyTracking { -- Compares canonical draft + translation drafts against saved state -- Dirty indicator shown in header and tab bar } -- ─── Post editor actions ──────────────────────────────────── -- Shared modal for presenting AI suggestions with per-field accept/reject. -- Used by PostAIAnalysis and MediaAIImageAnalysis. value AISuggestionsModal { fields: List -- Layout: title bar ("AI Suggestions"), scrollable field list, button row -- Each field rendered as a row: -- Left: checkbox (accept/reject), label -- Center: current value (read-only, muted), arrow, suggested value (highlighted) -- Special: slug field checkbox disabled if post was ever published -- Buttons: Cancel (secondary), Apply Selected (primary) -- Cancel discards all; Apply writes only accepted fields to entity } value AISuggestionField { label: String current_value: String suggested_value: String accepted: Boolean -- checkbox, default true locked: Boolean -- if true, checkbox disabled (e.g. published slug) } -- ─── Modals and overlays ──────────────────────────────────── -- All modals rendered as centered overlay with backdrop dimming. -- ESC key or backdrop click closes modal (cancel semantics). -- Overlays (PostPicker, ColourPicker) are positioned inline near trigger. value InsertPostLinkModal { -- Two-tab modal opened by Ctrl/Cmd+K in post editor (markdown mode). -- Tab 1 - Internal: -- Search input (debounced 300ms, queries post titles) -- Results list: post title + status badge (draft/published/archived) -- If semantic similarity enabled: results ranked by vector similarity -- Click result: inserts [title](/YYYY/MM/DD/slug) at cursor, closes modal -- "Create Post" row at bottom of results: -- Creates new post with search query as title, inserts link to it -- Tab 2 - External: -- URL input field (required) -- Display text input field (optional) -- Insert button: inserts [text](url) or bare url if no text, closes modal active_tab: String -- internal | external search_query: String results: List } value InsertLinkResult { post_id: String title: String status: String -- draft | published | archived canonical_url: String -- /YYYY/MM/DD/slug } value InsertMediaModal { -- Grid modal for inserting media references into post content. -- Search input filtering by media title and original filename. -- Grid of media items: bds-thumb:// thumbnail (medium 400px), title below. -- Click item: -- Images: inserts ![alt](bds-media://id) at cursor -- Non-images: inserts [originalName](bds-media://id) at cursor -- Closes modal after insertion. search_query: String results: List } value InsertMediaResult { media_id: String title: String original_name: String is_image: Boolean thumbnail_url: String? -- bds-thumb:// for images, null for others } value LanguagePickerModal { -- Shown for Translate Post and Translate Media Metadata actions. -- Lists all configured blogLanguages except source language. -- Each row: flag emoji, language name, status badge if translation exists. -- Existing translations show "(draft)" or "(published)" badge. -- Click selects target language and initiates translation flow. -- Cancel closes without action. source_language: String available_targets: List } value LanguageTarget { code: String name: String flag_emoji: String has_existing_translation: Boolean existing_status: String? -- draft | published, if translation exists } value ConfirmDeleteModal { -- Custom styled modal for destructive operations with reference info. -- Used by: MediaDelete (shows linked posts), TagDelete (shows post count). -- Layout: warning icon, title, entity name, reference section, buttons. -- Reference section: "This item is referenced by:" + bulleted list. -- Buttons: Cancel (secondary), Delete (destructive red). entity_name: String entity_type: String -- media | tag reference_count: Integer reference_list: List -- titles of referencing entities } value ConfirmDialog { -- Custom styled modal for non-delete confirmations. -- Used by: TagMerge ("Merge N tags into {target}? Cannot be undone."). -- Layout: title, descriptive message, buttons. -- Buttons: Cancel (secondary), Confirm (primary). title: String message: String } -- Native confirm dialogs (via rfd crate) are NOT modelled as values. -- They are simple yes/no system dialogs with a message string. -- Used by: PostDelete, PostDiscard, TemplateDelete (with references). value GalleryOverlay { -- Full-screen overlay showing all media linked to a post. -- Opened from Gallery button in post editor toolbar (markdown mode). -- Image grid: bds-thumb:// thumbnails (medium 400px), 3-4 columns. -- Click image: opens LightboxView for that image. -- Close: X button or ESC key. post_id: String images: List } value GalleryImage { media_id: String thumbnail_url: String -- bds-thumb://media_id alt_text: String? } value LightboxView { -- Full-screen image viewer, sub-view of GalleryOverlay. -- Shows single image at full resolution via bds-media:// protocol. -- Navigation: left/right arrow buttons, keyboard left/right arrow keys. -- Close: X button, ESC key, or click outside image area. -- Header: image title or filename, index counter "3 of 12". current_index: Integer total_count: Integer media_id: String image_url: String -- bds-media://media_id alt_text: String? } value ColourPickerPopover { -- Inline popover positioned below tag colour field (tags view manage). -- Not a modal — does not dim backdrop, no ESC/backdrop close. -- Closes when clicking outside popover or selecting a colour. -- Grid of 17 preset colour swatches (4x4 + 1). -- Below grid: custom hex input field (#RRGGBB). -- Selection is immediate — no confirm/cancel buttons. -- Choosing a colour updates the tag form's colour field live. presets: List -- 17 hex colour values custom_hex: String? selected: String? } config { colour_picker_preset_count: Integer = 17 } value PostPickerOverlay { -- Inline overlay positioned near "Link to Post" button (media editor). -- Not a modal — positioned as dropdown below trigger button. -- Search input filtering posts by title (only unlinked posts shown). -- Up to 10 results displayed. "and N more" text if total exceeds 10. -- Click result: links media to selected post, closes overlay. search_query: String results: List overflow_count: Integer? -- shown as "and N more" if > 0 } value PostPickerResult { post_id: String title: String } value ModelSelectorDropdown { -- Dropdown in chat panel header for per-conversation model override. -- Groups models by provider using section headers. -- Each entry: model display name. -- Expandable details per model: context window, max output tokens, pricing. -- Selected model persisted with conversation record. -- Changing model mid-conversation applies to subsequent messages only. groups: List selected_model_id: String? } value ModelProviderGroup { provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio" models: List } value ModelEntry { model_id: String display_name: String context_window: Integer max_output_tokens: Integer } rule PostAIAnalysis { when: PostAIAnalysisRequested(post_id) -- Gate: airplane mode check (see flows.allium AIOperationGating) -- Uses title model (not default chat model) -- Input: post title + excerpt + content (first 2000 chars) -- Response: suggested title, excerpt, slug -- Opens AISuggestionsModal with 3 fields: -- Each field: current value, suggested value, accept checkbox -- Slug field locked (no accept checkbox) if post was ever published -- On confirm: applies only checked fields, triggers auto-save } rule PostTranslateAction { when: PostTranslateRequested(post_id, target_language) -- Gate: airplane mode check -- Opens language picker modal: -- Available target languages from project blogLanguages -- Existing translations shown with status badge (draft/published) -- Two sequential AI calls via title model: -- 1. Translate metadata (title, excerpt) to target language -- 2. Translate content (full markdown body) to target language -- Creates/updates translation record in DB -- If source post is published: transitions source to draft -- (copies file content back to DB so it can be edited) } rule PostAutoTranslateOnSave { when: PostSaved(post_id) -- Gate: airplane mode check + auto_translate not disabled (doNotTranslate=false) -- For each configured blog language missing a translation: -- Enqueue background translation task (title model) -- Each task: translate metadata + content, create translation record -- Cascades to linked media: for each linked media item, -- translate media metadata for missing languages -- See flows.allium AutoTranslationChain for full chain } rule PostPublishAction { when: PostPublishRequested(post_id) -- Implicit save first (awaited) if post is dirty -- Then calls engine publish (see flows.allium PublishPostSideEffects) -- Also publishes all translations whose source language is published -- UI updates: status badge -> published, sidebar section move } rule PostDiscardChanges { when: PostDiscardRequested(post_id) -- Only available for published posts with pending draft changes -- Native confirm dialog (rfd): "Discard changes to this post?" -- On confirm: reads published version from .md file, -- restores DB to published state (content=null, status=published) -- Editor reloads with restored content } rule PostDeleteAction { when: PostDeleteRequested(post_id) -- Native confirm dialog (rfd): "Delete this post?" -- If published: also deletes .md file and all translation files -- If never published: only deletes DB record -- Removes from DB, closes tab, sidebar removes item -- See flows.allium DeletePostSideEffects } rule PostInsertLink { when: PostInsertLinkRequested(post_id) -- Keyboard shortcut: Ctrl/Cmd+K -- Opens InsertModal with two tabs: Internal, External -- Internal tab: -- Search input (debounced, queries post titles) -- Results list: title + status badge (draft/published) -- If semantic similarity enabled: results ranked by similarity -- Click inserts markdown link: [title](/YYYY/MM/DD/slug) -- "Create Post" option at bottom of search results: -- Creates new post with search query as title -- Inserts link to newly created post -- External tab: -- URL input + optional display text input -- Inserts: [text](url) or bare url if no display text } rule PostInsertMedia { when: PostInsertMediaRequested(post_id) -- Opens InsertModal (media search variant) -- Search input, grid of media items with bds-thumb:// thumbnails -- Click inserts markdown: -- Images: ![alt](bds-media://id) -- Non-images: [originalName](bds-media://id) } rule PostGalleryAction { when: PostGalleryRequested(post_id) -- Opens gallery overlay showing all media linked to this post -- Image grid with bds-thumb:// thumbnails -- Click on image opens lightbox (full-size bds-media:// preview) -- Lightbox: left/right arrow navigation, close button, ESC to close } rule PostDragDropImage { when: ImageDroppedOnEditor(post_id, file_path) -- Chain of operations (see flows.allium DragDropImageChain): -- 1. Import media file -> media record + file copy + sidecar -- 2. Generate thumbnails (async: small/medium/large/ai) -- 3. Link media to post (update sidecar linkedPostIds) -- 4. Insert markdown image at cursor: ![](bds-media://id) -- 5. If AI available: AI image analysis (async, auto-applied, no modal) -- 6. If auto-translate enabled: cascade translate media metadata -- Steps 1-4 synchronous. Steps 5-6 background tasks. } rule PostLanguageDetect { when: PostLanguageDetectRequested(post_id) -- Gate: airplane mode check -- Sends content sample to title model -- Auto-sets post language field (no modal) -- Triggers auto-save } -- Post editor mode switching: -- Visual mode: rich-text WYSIWYG (TipTap in TS, not yet in Rust) -- Markdown mode: code editor with markdown-with-macros highlighting -- Preview mode: iframe with rendered HTML via preview server -- Mode persists per-session, default from editor settings -- Language tab switching (translation editing): -- Flag emoji buttons in metadata bar for canonical + each translation -- Clicking a flag loads that language's draft content into editor -- Each flag shows status (draft/published) via CSS class -- Active flag highlighted -- LinkedMediaPanel (right column of metadata): -- Shows media items linked to this post -- Actions: -- Import & Link: native file dialog, runs full import + link chain -- Link Existing: opens media picker modal (search + grid) -- Unlink: removes media-post link from sidecar (no confirmation) -- Reorder: drag handle on each item -- Click: opens media item in its own tab (preview intent) -- Import & Link triggers: import -> thumbnail -> link -> optional AI analysis -- ─── Media editor ───────────────────────────────────────────── -- Single column: preview area on top, metadata form below. -- Header bar: -- Media display name -- Actions: Quick Actions dropdown, Replace File, Save, Delete -- Quick Actions: -- AI Image Analysis (image/* types only) - suggests title, alt, caption -- Detect Language (from metadata text) -- Translate Metadata (opens translation target modal) -- Preview area: -- Images: rendered via bds-media:// protocol with cache-busting timestamp -- Non-images: SVG file icon placeholder + original filename text -- Metadata form fields: -- File Name (read-only), MIME Type (read-only), -- Size (formatted as KB) + Dimensions (W x H, only if width/height exist), -- Title (editable), Alt Text (editable), Caption (textarea 3 rows), -- Tags (comma-separated text input), Author, Language (select) -- Translations section (shown only when language is set): -- List of existing translations: flag + language name + title -- Per-translation: click to edit, refresh button, delete button -- "No translations" message when empty -- Linked Posts section: -- "Link to Post" button opens post picker overlay -- Post picker: search input, up to 10 unlinked posts, "and N more" if >10 -- List of currently linked posts (clickable -> navigates), unlink button -- "Not linked to any posts" when empty value MediaEditorView { media_id: String is_image: Boolean file_name: String -- originalName, read-only mime_type: String file_size: String -- formatted dimensions: String? -- "W x H" if available title: String? alt_text: String? caption: String? tags: String? -- comma-separated author: String? language: String? translations: List linked_posts: List } value MediaTranslationItem { language: String title: String? } value LinkedPostItem { post_id: String title: String } -- No auto-save; explicit Save button required. -- Replace File opens a native file dialog. -- Delete shows confirmation with references (linked posts). -- ─── Media editor actions ───────────────────────────────────── rule MediaAIImageAnalysis { when: MediaAIImageAnalysisRequested(media_id) -- Gate: airplane mode check (see flows.allium AIOperationGating) -- Only available for image/* MIME types (button hidden for non-images) -- Uses image analysis model (vision-capable, not title model) -- Input: AI-optimized JPEG thumbnail (448x448, generated on import) -- Response: suggested title, alt text, caption -- Opens AISuggestionsModal with 3 fields (title, alt, caption) -- On confirm: applies checked fields, triggers explicit save } rule MediaDetectLanguage { when: MediaDetectLanguageRequested(media_id) -- Gate: airplane mode check -- Input: concatenation of title + alt + caption text -- Response: detected language code -- Immediately persists to media record (no modal, no confirmation) -- Triggers sidecar rewrite } rule MediaTranslateMetadata { when: MediaTranslateMetadataRequested(media_id, target_language) -- Gate: airplane mode check -- Opens language picker modal (same pattern as post translate) -- Two-step process: -- 1. If source language not set: detect it first (auto-persist) -- 2. Translate title, alt, caption to target language via title model -- Creates/updates media translation record -- Writes translated sidecar file: {path}.{lang}.meta } rule MediaReplaceFile { when: MediaReplaceFileRequested(media_id) -- Opens native file dialog (no MIME type filter) -- Copies selected file over existing media file path -- If image: regenerates thumbnails synchronously (awaited) -- Preview area updates with cache-busting timestamp query param } rule MediaDeleteAction { when: MediaDeleteRequested(media_id) -- Opens ConfirmDeleteModal (custom modal, not native dialog) -- Shows: media display name, linked posts count and list -- Two buttons: Cancel, Delete (destructive red style) -- On confirm: deletes file, sidecar, thumbnails, all translations, -- post-media links, FTS index entry -- Closes media tab, sidebar removes item -- See flows.allium DeleteMediaSideEffects } rule MediaLinkToPost { when: MediaLinkToPostRequested(media_id) -- Opens inline post picker overlay (not a modal, positioned near button) -- Search input filtering unlinked posts by title -- Up to 10 results shown, "and N more" text if results exceed 10 -- Click links media to selected post (updates sidecar linkedPostIds) -- Linked posts list refreshes immediately } rule MediaTranslationEdit { when: MediaTranslationEditClicked(media_id, language) -- Loads translation fields inline (title, alt, caption) for that language -- Edit in place, save persists to translated sidecar {path}.{lang}.meta } rule MediaTranslationRefresh { when: MediaTranslationRefreshClicked(media_id, language) -- Gate: airplane mode check -- Re-translates from source language to target via title model -- Overwrites existing translation fields -- Rewrites translated sidecar file } rule MediaTranslationDelete { when: MediaTranslationDeleteClicked(media_id, language) -- Deletes translation record from DB -- Deletes translated sidecar file: {path}.{lang}.meta -- No confirmation dialog } -- ─── Settings view ──────────────────────────────────────────── -- VS Code-style settings page with header + search bar + scrollable sections. -- Search filters sections by keyword match. "No results" with clear button. -- 8 sections (style is a separate tab, not a settings section): -- 1. Project Settings: -- Project Name (text), Project Description (textarea 3 rows), -- Data Path (text + Browse + Reset), Public URL (url), -- Main Language (select), Blog Languages (checkboxes, main disabled), -- Default Author (text), Max Posts Per Page (number 1-500 default 50), -- Blogmark Category (select), Blogmark Bookmarklet (copy button), Save -- 2. Editor Settings: -- Default Editor Mode (select: WYSIWYG/Markdown/Preview), -- Diff View Style (select: Inline/Side-by-side), -- Wrap Long Lines (checkbox), Hide Unchanged Regions (checkbox) -- 3. Content Settings (Categories): -- Table: Category | Title | Render in Lists | Show Titles | Post Template | List Template | Remove -- Protected categories: article, aside, page, picture (cannot be deleted) -- Add Category: text input + Add button. Reset to Defaults button -- 4. AI Assistant Settings: -- API keys: OpenCode, Mistral (masked display + Change, or password input + Save) -- Local providers: Ollama enable checkbox + capabilities table, -- LM Studio enable checkbox + capabilities table -- Offline mode: enable checkbox (disabled if no local providers), -- selectors for chat/title/image models (local only) -- Default Model (select with optgroup by provider + refresh), -- shows: max output tokens, context window, pricing -- Title Model (select), Image Analysis Model (vision-capable only) -- System Prompt (textarea 12 rows + Save + Reset to Default) -- 5. Technology Settings: -- Python Runtime Mode (select: Web Worker/Main Thread) -- Semantic Similarity (checkbox) -- 6. Publishing Settings (SSH): -- Info: SSH key auth only -- SSH Mode (select: scp/rsync), SSH Host, SSH Username, SSH Remote Path -- Save + Clear buttons -- 7. Data Maintenance: -- Rebuild buttons (6): Posts from Files, Media from Files, Scripts from Files, -- Templates from Files, Links, Regenerate Missing Thumbnails -- Open Data Folder button -- 8. MCP Server Settings: -- Status badge (port or "Not running") -- Per-agent rows: Claude Code, Claude Desktop, GitHub Copilot, Gemini CLI, -- OpenCode, Mistral Vibe, OpenAI Codex — each with Add/Remove toggle -- ─── Settings view actions ────────────────────────────────── -- API key validation: -- On Save: test API call to provider endpoint before persisting -- On validation failure: toast error message, key not saved -- On success: key saved, masked display shown -- Local provider enable/disable: -- Enabling Ollama/LM Studio: fetches model list, shows capabilities table -- Capabilities table columns: model name, chat, title, image (checkmarks) -- Models auto-fetched on enable; manual refresh via Refresh Models button -- Model catalog refresh: -- Refresh button next to Default Model selector -- Fetches available models from all enabled providers -- Updates optgroup dropdown with provider groupings -- Per-model info: max output tokens, context window, pricing -- Rebuild operations (Data Maintenance section): -- 6 buttons, each executes immediately (no confirmation dialog) -- Runs as background task with progress visible in Tasks panel -- On completion: wholesale replaces store data for that entity type -- Sidebar and editors re-render with fresh data -- Category management (Content Settings): -- Protected categories: article, aside, page, picture (Remove button disabled) -- Add Category: text input + Add button, validates non-empty and unique -- Per-category settings: editable inline in table row -- "Reset to Defaults" restores default categories set -- MCP per-agent config: -- Add/Remove toggle per agent row -- Status badge: shows port number when running, "Not running" otherwise -- Bookmarklet (Project Settings): -- Copy button copies bookmarklet JavaScript to clipboard -- Bookmarklet uses project's publicUrl to construct POST endpoint -- ─── Style view ─────────────────────────────────────────────── -- Simple vertical layout: header, theme picker, controls row, preview. -- Theme Picker: grid of theme buttons (one per Pico CSS theme). -- Each button: swatch with 3 colour tones (accent, light bg, dark bg), theme name. -- Selected theme: highlighted with aria-pressed. -- Controls row: Preview Mode dropdown (Auto/Light/Dark), Apply Theme button. -- Preview: iframe of style preview page at 127.0.0.1:4123/__style-preview -- with theme and mode query params. Updates live on selection change. -- Apply Theme persists to project metadata. -- ─── Style view actions ───────────────────────────────────── rule StyleThemeSelect { when: StyleThemeSelected(theme_name) -- Updates iframe preview immediately (query param change) -- Does NOT persist until Apply is clicked } rule StyleApplyTheme { when: StyleApplyRequested(theme_name) -- Persists picoTheme to project metadata (meta/project.json) -- See flows.allium UpdateProjectMetadataSideEffects } -- Preview mode dropdown (Auto/Light/Dark): -- Controls iframe query param, live update on change -- Does not persist — local UI state only -- ─── Tags view ──────────────────────────────────────────────── -- Three sections controlled by sidebar TagsNav: -- 1. Cloud: tag cloud visualization. Tags sized by post count, -- coloured by tag colour. Clickable to select for editing. -- 2. Manage (Create/Edit): tag name input, colour picker, template select. -- Edit existing tag or create new. Delete button with confirmation. -- 3. Merge: source tag select, target tag select, Merge button. -- Merges all posts from source tag into target, removes source. -- ─── Tags view actions ────────────────────────────────────── -- Cloud section: -- Tags sized by post count (same 11-22px scaling as dashboard) -- Tags with colours get coloured background -- Click selects tag: switches to Manage section, loads tag data -- Manage section: -- Create new: empty form, Save creates tag + writes tags.json -- Edit existing: loads tag data into form, Save updates -- Tag name input, colour picker, template select (optional) -- Colour picker: 17 preset colour swatches + custom hex input field -- Delete: opens ConfirmDialog showing post count -- "This tag is used in N posts. Delete anyway?" -- On confirm: background task removes tag from all posts, -- rewrites published .md files, deletes tag, writes tags.json -- Merge section: -- Source tag multi-select, target tag single-select -- Merge button: opens ConfirmDialog -- "Merge N tags into {target}? This cannot be undone." -- On confirm: background task updates all affected posts, -- rewrites published .md files, deletes source tags, writes tags.json -- Sync (maintenance action): -- Rewrites tags.json from current DB state -- ─── Chat panel ─────────────────────────────────────────────── -- Three-region vertical layout: header, message area, input area. -- API Key Required screen (shown when no API key): -- Key icon, title, description, "Open Settings" button -- Header: -- Left: conversation title (or "New Chat"), CSS ellipsis on overflow -- Right: model selector button + dropdown. Groups models by provider. -- Message area: -- Welcome screen (no messages, not streaming): robot icon, tips -- Message list: each has avatar (user=person, assistant=robot), role label, text -- User messages: plain text -- Assistant messages: rendered as GFM Markdown -- External images blocked (CSP), shown as links -- Tool markers: checkmark/dot, tool name, truncated args (30 char strings) -- Streaming: accumulating markdown + tool markers, thinking dots animation -- Auto-scrolls to bottom on new messages -- Input area: -- Abort/Stop button (only during streaming, square stop icon) -- Auto-growing textarea (max 200px). Enter sends, Shift+Enter newline -- Send button (up-arrow icon), disabled when empty or streaming -- Token usage tracked per conversation (displayed in status bar, not chat panel). -- Per-conversation model override via header selector. config { chat_tool_args_max_length: Integer = 30 chat_input_max_height: Integer = 200 } -- ─── Chat panel actions ───────────────────────────────────── -- Model selector: -- Dropdown in header, groups models by provider (optgroup) -- Selection is per-conversation override, persisted with conversation -- Changing model mid-conversation applies to subsequent messages only -- Streaming behaviour: -- Messages accumulate in real-time (markdown rendered incrementally) -- Tool call markers: checkmark icon (done) or dot (in-progress), -- tool name, args truncated to 30 chars for string values -- Thinking animation (three dots) during LLM processing -- Stop/Abort: -- Square stop icon button, visible only during active streaming -- Stops current generation, partial message preserved as-is -- Assistant action dispatch: -- Assistant tool calls can trigger navigation actions: -- open_post(id), open_media(id), open_settings(), etc. -- Actions dispatched through store, same as user clicks -- Navigation actions open tabs with pin intent -- ─── Git diff view ──────────────────────────────────────────── -- Renders diff for a file (working tree vs HEAD) or a commit. -- File diff: id = "git-diff:{filePath}" -- Commit diff: id = "git-diff:commit:{commitHash}" -- Supports inline and side-by-side diff display modes (from editor settings). -- No actions beyond viewing — changes are managed via git sidebar. -- ─── Menu editor view ──────────────────────────────────────── -- Visual editor for the OPML navigation menu (meta/menu.opml). -- See menu.allium for data model. -- Layout: toolbar at top, tree view of menu items below. -- Each item row: drag handle, icon, label, type badge, action buttons. -- Nested items indented to show hierarchy. -- ─── Menu editor actions ──────────────────────────────────── -- Add item (toolbar buttons): -- Page entry: opens lazy-loaded page picker (posts with "page" category) -- Category archive entry: opens lazy-loaded category picker -- Submenu: creates empty container node for nesting children -- Home entry: always available, maximum one allowed -- Save: serializes tree to OPML 2.0, writes meta/menu.opml -- Move operations (per-item buttons): -- Up/Down: reorder within same nesting level -- Indent: nest under previous sibling (becomes child) -- Unindent: move to parent's level (becomes next sibling of parent) -- Home item: protected — cannot be moved or deleted -- Drag-and-drop reorder: -- Drag handle on each item -- Auto-expand collapsed submenus on hover (450ms delay) -- Drop indicators show target position and nesting level -- Delete: removes item and all children, no confirmation dialog -- Exception: home item cannot be deleted (button hidden) -- ─── Metadata diff view ────────────────────────────────────── -- Shows DB vs filesystem differences for all entity types. -- See metadata_diff.allium for diff field definitions. -- Layout: scan button at top, results grouped by entity type below. -- Each diff entry: entity name, field name, DB value, file value, -- two sync buttons (DB->File, File->DB). -- ─── Metadata diff actions ────────────────────────────────── -- Scan: runs 4 parallel scans (posts, media, scripts, templates) -- Compares DB records against filesystem files field by field -- Results grouped by entity type, sorted by entity name -- Shows count of differences per entity type in section header -- Per-field sync: -- DB -> File button: overwrites file field with DB value -- File -> DB button: overwrites DB field with file value -- No confirmation dialog for individual field syncs -- Button disappears after successful sync (field now matches) -- Orphan file import: -- Files on disk with no matching DB record shown separately -- "Import" action creates DB record from file metadata -- Appears at bottom of each entity type section -- ─── Documentation views ───────────────────────────────────── -- documentation: renders DOCUMENTATION.md as styled HTML -- api_documentation: renders API.md as styled HTML -- ─── Validation views ──────────────────────────────────────── -- site_validation: checks generated site for broken links, missing assets -- translation_validation: checks translation completeness across languages -- ─── Validation view actions ──────────────────────────────── -- Site validation: -- Scans generated HTML for broken internal links, missing assets -- Results: list of issues with file path + line + description -- No auto-fix actions — informational only -- Translation validation: -- Checks translation completeness across configured blog languages -- Results: matrix of posts x languages, missing translations highlighted -- No auto-fix actions — informational only -- ─── Find duplicates view ──────────────────────────────────── -- Uses embedding vectors to find semantically similar posts. -- Shows candidate pairs with similarity score. -- See embedding.allium for vector details. -- ─── Find duplicates actions ──────────────────────────────── -- Uses HNSW index of post embedding vectors for similarity search -- Shows candidate pairs: post A title, post B title, score (0.0-1.0) -- Click on either post title opens it in a tab (preview intent) -- No auto-merge or auto-delete actions -- ─── Script editor view ────────────────────────────────────── -- Code editor for Lua scripts (macros, utilities, transforms). -- See script.allium for entity model. -- Layout: header bar, code editor, no preview. -- Header: script title (editable), kind badge, enabled toggle. -- Editor: code editor with Lua syntax highlighting. -- Toolbar: Run button, Save button, Delete button, entrypoint selector. -- ─── Script editor actions ────────────────────────────────── rule ScriptSave { when: ScriptSaveRequested(script_id) -- Lua syntax check first (parse validation) -- If syntax error: blocks save, shows error inline in editor gutter -- If valid: bumps version, saves to DB + rewrites .lua file -- See flows.allium for UpdateScriptSideEffects } rule ScriptRun { when: ScriptRunRequested(script_id) -- Executes script in Lua runtime -- Calls configured entrypoint function (default: render) -- stdout/stderr directed to Output panel tab -- Output panel auto-opens if not already visible -- Errors shown in Output panel with line numbers } rule ScriptDelete { when: ScriptDeleteRequested(script_id) -- No confirmation dialog -- Deletes DB record + .lua file on disk -- Closes script tab, sidebar removes item } -- ─── Template editor view ───────────────────────────────────── -- Code editor for Liquid templates (post, list, not_found, partial). -- See template.allium for entity model and Liquid subset. -- Layout: header bar, code editor, preview pane. -- Header: template title (editable), kind badge, enabled toggle, version display. -- Editor: code editor with Liquid syntax highlighting. -- Preview: rendered template output in iframe (uses sample post data). -- Toolbar: Save button, Delete button. -- ─── Template editor actions ──────────────────────────────── rule TemplateSave { when: TemplateSaveRequested(template_id) -- Liquid validation first (parse check for syntax errors) -- If invalid: blocks save, shows error inline in editor -- If valid: bumps version, saves to DB + rewrites .liquid file -- See flows.allium for UpdateTemplateSideEffects } rule TemplateDelete { when: TemplateDeleteRequested(template_id) -- Checks for references: posts using this template, tags with postTemplateSlug -- If references exist: native confirm dialog (rfd) -- "This template is used by N posts and M tags. Force delete?" -- Force delete: nulls templateSlug on referencing posts, -- nulls postTemplateSlug on referencing tags -- If no references: deletes without confirmation -- Deletes DB record + .liquid file on disk -- Closes template tab, sidebar removes item } -- ─── Import analysis view ─────────────────────────────────── -- Editor for WXR (WordPress eXtended RSS) import definitions. -- Keyed by import definition ID. Opened as always-pinned tab. -- Workflow: Select File -> Analyze -> Resolve Conflicts -> Execute -- Layout: header (definition name), file selector, analysis results, -- conflict resolution panel, taxonomy mapping, execute button. -- ─── Import analysis actions ──────────────────────────────── rule ImportSelectAndAnalyze { when: ImportAnalyzeRequested(definition_id, file_path) -- Parses WXR XML file -- Extracts: posts, pages, media, tags, categories, authors -- Shows summary counts per entity type -- Identifies conflicts: duplicate slugs, existing categories/tags } -- Conflict resolution: -- Per-item dropdown: Import (create new), Skip (ignore), Merge (combine) -- Default: Import for new items, Skip for existing matches -- Taxonomy mapping: -- Source categories/tags mapped to existing project categories/tags -- Dropdown per source taxonomy term -> target term or "Create New" -- AI taxonomy analysis (optional): -- Gate: airplane mode check -- Suggests mappings based on name similarity via title model rule ImportExecute { when: ImportExecuteRequested(definition_id) -- No confirmation dialog — executes immediately -- Processes items per conflict resolution settings -- Creates posts, media, tags, categories as needed -- Summary with counts shown in import view on completion -- Created entities appear in sidebar immediately (store updated) }