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.
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
-
Reuse existing logic: The
onUploadcallback logic forCrepe.Feature.ImageBlockshould be reused for local file uploads to maintain consistency with OCR processing. -
ProseMirror API: Use
schema.nodes.image.create()andreplaceSelectionWith()to insert images at cursor position. -
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.
-
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)