Files
llm-in-text/plans/image-button-plan.md
“ydy0615” 64cfa58376 feat(editor): add image insertion with OCR support and size limit handling
Add image button with dropdown menu for uploading local images or inserting from URL.
Integrate VLM-based OCR to extract text context from images and include in AI suggestions.
Implement document size limits to disable AI when exceeding threshold.
Refactor copilot plugin with per-view runtime state and OCR context injection.
Add OCR cache utility for managing image metadata.
Add code splitting configuration for optimized bundle size.
2026-02-14 18:28:37 +08:00

6.4 KiB

Image Button Implementation Plan

Overview

Add an image button to the MilkdownEditor that allows users to insert images at the cursor position. The button will provide a dropdown menu with two options: upload local file or input image URL.

Current Architecture Analysis

Existing Image Handling

The editor already has image support through @milkdown/crepe:

// From MilkdownEditor.vue lines 217-231
features: {
    [Crepe.Feature.Latex]: true,
    [Crepe.Feature.ImageBlock]: true,
},
featureConfigs: {
    [Crepe.Feature.ImageBlock]: {
        onUpload: (file) => {
            const objectUrl = URL.createObjectURL(file)
            objectUrls.add(objectUrl)
            performOCR(file, objectUrl)
            return objectUrl
        }
    }
}

Editor Access Pattern

The code uses editorViewCtx to access the ProseMirror editor view:

crepe.editor.action((ctx) => {
    const view = ctx.get(editorViewCtx)
    // manipulate editor state
})

Implementation Plan

1. Template Changes

Add new button with dropdown menu in the action-buttons section:

<!-- Image button with dropdown -->
<div class="image-btn-wrapper">
  <button
    type="button"
    class="action-btn"
    aria-label="Insert Image"
    title="Insert Image"
    @click="toggleImageDropdown"
  >
    <!-- Image SVG icon -->
    <svg>...</svg>
    <span class="btn-tooltip">Insert Image</span>
  </button>
  
  <!-- Dropdown menu -->
  <div v-if="showImageDropdown" class="image-dropdown">
    <button @click="triggerImageUpload">Upload Local Image</button>
    <button @click="showUrlDialog = true">Insert from URL</button>
  </div>
</div>

<!-- Hidden file input for image upload -->
<input type="file" ref="imageInputRef" @change="handleImageUpload" accept="image/*" style="display:none">

<!-- URL input dialog -->
<div v-if="showUrlDialog" class="url-dialog-overlay" @click.self="showUrlDialog = false">
  <div class="url-dialog">
    <input v-model="imageUrl" placeholder="Enter image URL" />
    <button @click="insertImageFromUrl">Insert</button>
    <button @click="showUrlDialog = false">Cancel</button>
  </div>
</div>

2. Script Changes

Add new refs and methods:

// New refs
const imageInputRef = ref(null)
const showImageDropdown = ref(false)
const showUrlDialog = ref(false)
const imageUrl = ref('')

// Toggle dropdown
const toggleImageDropdown = () => {
    showImageDropdown.value = !showImageDropdown.value
}

// Trigger file input
const triggerImageUpload = () => {
    showImageDropdown.value = false
    imageInputRef.value?.click()
}

// Handle file upload - reuse existing onUpload logic
const handleImageUpload = async (event) => {
    const file = event.target.files?.[0]
    if (!file) return
    
    const objectUrl = URL.createObjectURL(file)
    objectUrls.add(objectUrl)
    performOCR(file, objectUrl)
    
    // Insert image at cursor
    insertImageAtCursor(objectUrl)
    event.target.value = ''
}

// Insert image from URL
const insertImageFromUrl = () => {
    if (!imageUrl.value.trim()) return
    insertImageAtCursor(imageUrl.value.trim())
    imageUrl.value = ''
    showUrlDialog.value = false
}

// Core function: insert image at cursor position
const insertImageAtCursor = (src) => {
    if (!crepe) return
    
    crepe.editor.action((ctx) => {
        const view = ctx.get(editorViewCtx)
        const { state } = view
        const { selection, schema } = state
        
        // Get image node type from schema
        const imageType = schema.nodes.image
        if (!imageType) return
        
        // Create image node
        const imageNode = imageType.create({ src })
        
        // Create transaction to insert at cursor
        const tr = state.tr
        tr = tr.replaceSelectionWith(imageNode)
        
        view.dispatch(tr)
    })
}

3. Style Changes

Add styles for dropdown and dialog:

/* Image button wrapper */
.image-btn-wrapper {
    position: relative;
}

/* Dropdown menu */
.image-dropdown {
    position: absolute;
    bottom: 100%;
    right: 0;
    margin-bottom: 8px;
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    overflow: hidden;
    z-index: 10000;
    min-width: 160px;
}

.image-dropdown button {
    display: block;
    width: 100%;
    padding: 10px 16px;
    border: none;
    background: none;
    text-align: left;
    cursor: pointer;
    font-size: 14px;
    color: #333;
}

.image-dropdown button:hover {
    background: #f5f5f5;
}

/* URL dialog overlay */
.url-dialog-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0,0,0,0.3);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10001;
}

.url-dialog {
    background: #fff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.2);
}

.url-dialog input {
    width: 300px;
    padding: 8px 12px;
    border: 1px solid #ddd;
    border-radius: 4px;
    margin-bottom: 12px;
}

.url-dialog button {
    padding: 8px 16px;
    margin-right: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
    cursor: pointer;
}

Workflow Diagram

flowchart TD
    A[Click Image Button] --> B{Toggle Dropdown}
    B --> C[Show Dropdown Menu]
    C --> D{User Choice}
    D -->|Upload Local| E[Open File Picker]
    D -->|From URL| F[Show URL Dialog]
    E --> G[Select Image File]
    G --> H[Create Object URL]
    H --> I[Perform OCR]
    I --> J[Insert Image at Cursor]
    F --> K[Enter URL]
    K --> L[Click Insert]
    L --> J
    J --> M[Image Appears in Editor]

Key Implementation Notes

  1. Reuse existing logic: The onUpload callback logic for Crepe.Feature.ImageBlock should be reused for local file uploads to maintain consistency with OCR processing.

  2. ProseMirror API: Use schema.nodes.image.create() and replaceSelectionWith() to insert images at cursor position.

  3. Click outside to close: The dropdown should close when clicking outside. This can be achieved with a click-outside directive or by listening to document clicks.

  4. Accessibility: Ensure proper ARIA labels and keyboard navigation support.

Files to Modify

  • src/components/MilkdownEditor.vue - All changes will be in this single file

Dependencies

No new dependencies required. All functionality uses existing:

  • Vue 3 Composition API
  • Milkdown/ProseMirror APIs
  • Native browser APIs (URL.createObjectURL, FileReader)