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.
269 lines
6.4 KiB
Markdown
269 lines
6.4 KiB
Markdown
# 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`:
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```html
|
|
<!-- 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:
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```css
|
|
/* 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
|
|
|
|
```mermaid
|
|
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) |