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
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 34s
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 }))}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user