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

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)