fix: extract images from rich text HTML to enable image description quick action
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 34s

This commit is contained in:
2026-05-02 23:41:46 +02:00
parent 01cf5ccad7
commit d0387cd9a0
4 changed files with 42 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect, useMemo } from 'react'
import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata, NoteType } from '@/lib/types' import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata, NoteType } from '@/lib/types'
import { import {
Dialog, Dialog,
@@ -28,7 +28,7 @@ import { RichTextEditor } from '@/components/rich-text-editor'
import { X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles, Maximize2, Copy, Wand2, LogOut } from 'lucide-react' import { X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles, Maximize2, Copy, Wand2, LogOut } from 'lucide-react'
import { updateNote, createNote, cleanupOrphanedImages, leaveSharedNote } from '@/app/actions/notes' import { updateNote, createNote, cleanupOrphanedImages, leaveSharedNote } from '@/app/actions/notes'
import { fetchLinkMetadata } from '@/app/actions/scrape' import { fetchLinkMetadata } from '@/app/actions/scrape'
import { cn } from '@/lib/utils' import { cn, extractImagesFromHTML } from '@/lib/utils'
import { toast } from 'sonner' import { toast } from 'sonner'
import { MarkdownContent } from './markdown-content' import { MarkdownContent } from './markdown-content'
import { LabelManager } from './label-manager' import { LabelManager } from './label-manager'
@@ -278,12 +278,21 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
const handleGenerateTitles = async () => { const handleGenerateTitles = async () => {
// Combine content and link metadata for AI // Combine content and link metadata for AI
const fullContent = [ const fullContentForAI = [
content, content,
...links.map(l => `${l.title || ''} ${l.description || ''}`) ...links.map(l => `${l.title || ''} ${l.description || ''}`)
].join(' ').trim() ]
.join(' ')
.trim()
const wordCount = fullContent.split(/\s+/).filter(word => word.length > 0).length const allImages = useMemo(() => {
const extracted = noteType === 'richtext' ? extractImagesFromHTML(content) : [];
return Array.from(new Set([...images, ...extracted]));
}, [images, content, noteType]);
const wordCount = fullContentForAI.split(/\s+/).filter(word => word.length > 0).length
if (wordCount < 10) { if (wordCount < 10) {
toast.error(t('ai.titleGenerationMinWords', { count: wordCount })) toast.error(t('ai.titleGenerationMinWords', { count: wordCount }))
@@ -1006,7 +1015,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
onClose={() => setAiOpen(false)} onClose={() => setAiOpen(false)}
noteTitle={title} noteTitle={title}
noteContent={content} noteContent={content}
noteImages={images} noteImages={allImages}
onApplyToNote={(newContent) => { onApplyToNote={(newContent) => {
setPreviousContentForCopilot(content) setPreviousContentForCopilot(content)
setContent(newContent) setContent(newContent)

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useState, useEffect, useRef, useCallback, useTransition } from 'react' import { useState, useEffect, useRef, useCallback, useTransition, useMemo } from 'react'
import { Note, CheckItem, NOTE_COLORS, NoteColor, NoteType } from '@/lib/types' import { Note, CheckItem, NOTE_COLORS, NoteColor, NoteType } from '@/lib/types'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
@@ -17,7 +17,7 @@ import { ComparisonModal } from '@/components/comparison-modal'
import { NoteTypeSelector } from '@/components/note-type-selector' import { NoteTypeSelector } from '@/components/note-type-selector'
import { RichTextEditor } from '@/components/rich-text-editor' import { RichTextEditor } from '@/components/rich-text-editor'
import { useLanguage } from '@/lib/i18n' import { useLanguage } from '@/lib/i18n'
import { cn } from '@/lib/utils' import { cn, extractImagesFromHTML } from '@/lib/utils'
import { import {
updateNote, updateNote,
toggleArchive, toggleArchive,
@@ -130,6 +130,12 @@ export function NoteInlineEditor({
const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || []) const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
const [noteType, setNoteType] = useState<NoteType>(note.type) const [noteType, setNoteType] = useState<NoteType>(note.type)
const isMarkdown = noteType === 'markdown' const isMarkdown = noteType === 'markdown'
const allImages = useMemo(() => {
const extracted = noteType === 'richtext' ? extractImagesFromHTML(content) : [];
return Array.from(new Set([...(note.images || []), ...extracted]));
}, [note.images, content, noteType]);
const [showMarkdownPreview, setShowMarkdownPreview] = useState( const [showMarkdownPreview, setShowMarkdownPreview] = useState(
defaultPreviewMode && (note.isMarkdown || false) defaultPreviewMode && (note.isMarkdown || false)
) )
@@ -911,7 +917,7 @@ export function NoteInlineEditor({
onClose={() => setAiOpen(false)} onClose={() => setAiOpen(false)}
noteTitle={title} noteTitle={title}
noteContent={content} noteContent={content}
noteImages={note.images || undefined} noteImages={allImages}
onApplyToNote={(newContent) => { onApplyToNote={(newContent) => {
setPreviousContent(content) setPreviousContent(content)
changeContent(newContent) changeContent(newContent)
@@ -929,3 +935,6 @@ export function NoteInlineEditor({
</div> </div>
) )
} }

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect, useMemo } from 'react'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
@@ -37,7 +37,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils' import { cn, extractImagesFromHTML } from '@/lib/utils'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { NoteTypeSelector } from '@/components/note-type-selector' import { NoteTypeSelector } from '@/components/note-type-selector'
@@ -145,6 +145,11 @@ export function NoteInput({
...links.map(l => `${l.title || ''} ${l.description || ''}`) ...links.map(l => `${l.title || ''} ${l.description || ''}`)
].join(' ').trim(); ].join(' ').trim();
const allImages = useMemo(() => {
const extracted = type === 'richtext' ? extractImagesFromHTML(content) : [];
return Array.from(new Set([...images, ...extracted]));
}, [images, content, type]);
// Auto-tagging hook // Auto-tagging hook
const { suggestions, isAnalyzing } = useAutoTagging({ const { suggestions, isAnalyzing } = useAutoTagging({
content: type !== 'checklist' ? fullContentForAI : '', content: type !== 'checklist' ? fullContentForAI : '',
@@ -1036,7 +1041,7 @@ export function NoteInput({
onClose={() => setAiOpen(false)} onClose={() => setAiOpen(false)}
noteTitle={title} noteTitle={title}
noteContent={content} noteContent={content}
noteImages={images} noteImages={allImages}
onApplyToNote={(newContent) => setContent(newContent)} onApplyToNote={(newContent) => setContent(newContent)}
lastActionApplied={false} lastActionApplied={false}
notebooks={notebooks.map(nb => ({ id: nb.id, name: nb.name }))} notebooks={notebooks.map(nb => ({ id: nb.id, name: nb.name }))}

View File

@@ -6,6 +6,12 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export function extractImagesFromHTML(html: string): string[] {
if (!html) return [];
const matches = html.matchAll(/<img[^>]+src="([^">]+)"/gi);
return Array.from(matches).map(m => m[1]);
}
/** /**
* Deep equality check for two values * Deep equality check for two values
* More performant than JSON.stringify for comparison * More performant than JSON.stringify for comparison