feat: chat stop button, image paste, vision AI, search fixes
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 43s

- Add stop button to all chat interfaces (floating, contextual, full-page)
- Add conversation sliding window (50 messages) to prevent context overflow
- Add chat timeout warning (30s toast)
- Force response language in chat system prompt (mandatory per-locale)
- Add image paste from clipboard in all note editors (card, list, input)
- Fix upload API to infer extension from MIME type for clipboard images
- Add image description support in note AI chat (base64 vision)
- Fix search regex crash on special characters (escape user input)
- Fix search case-insensitivity on PostgreSQL (mode: 'insensitive')
- Add try/catch around semantic search in chat route (prevent blocking)
- Add new chat button to floating AI assistant
- Fix empty thinking bubbles for reasoning models (filter non-text parts)
- Remove duplicate AI assistant toggle from note editor header
- Improve link metadata scraping (timeout, content-type check, relative URLs)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 22:34:07 +02:00
parent 3b8152c7c0
commit ae89f8a014
13 changed files with 354 additions and 127 deletions

View File

@@ -434,6 +434,38 @@ export function NoteInput({
e.target.value = ''
}
const uploadImageFile = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', { method: 'POST', body: formData })
if (!response.ok) throw new Error('Upload failed')
const data = await response.json()
return data.url
}
// Paste handler: upload clipboard images
useEffect(() => {
const handlePaste = async (e: ClipboardEvent) => {
const items = e.clipboardData?.items
if (!items) return
for (const item of Array.from(items)) {
if (item.type.startsWith('image/')) {
e.preventDefault()
const file = item.getAsFile()
if (!file) continue
try {
const url = await uploadImageFile(file)
setImages(prev => [...prev, url])
} catch {
toast.error(t('notes.uploadFailed', { filename: 'pasted image' }))
}
}
}
}
document.addEventListener('paste', handlePaste)
return () => document.removeEventListener('paste', handlePaste)
}, [t])
const handleAddLink = async () => {
if (!linkUrl) return
@@ -927,6 +959,7 @@ export function NoteInput({
onClose={() => setAiOpen(false)}
noteTitle={title}
noteContent={content}
noteImages={images}
onApplyToNote={(newContent) => setContent(newContent)}
lastActionApplied={false}
notebooks={notebooks.map(nb => ({ id: nb.id, name: nb.name }))}