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

@@ -7,7 +7,7 @@ import type { UIMessage } from 'ai'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
X, Bot, Sparkles, Send, Loader2,
X, Bot, Sparkles, Send, Loader2, Square,
Briefcase, Palette, GraduationCap, Coffee,
Lightbulb, Minimize2, AlignLeft, Wand2,
Globe, BookOpen, FileText, RotateCcw, Check,
@@ -70,6 +70,7 @@ interface ContextualAIChatProps {
onClose: () => void
noteTitle?: string
noteContent?: string
noteImages?: string[]
/** Called when an action result should be injected into the note */
onApplyToNote?: (newContent: string) => void
/** Called when the user wants to undo the last injected action */
@@ -88,6 +89,7 @@ export function ContextualAIChat({
onClose,
noteTitle,
noteContent,
noteImages,
onApplyToNote,
onUndoLastAction,
lastActionApplied = false,
@@ -115,7 +117,12 @@ export function ContextualAIChat({
const buildChatBody = () => {
const body: Record<string, any> = { language, webSearch }
if (chatScope === 'note') {
body.noteContext = { title: noteTitle || '', content: noteContent || '', tone: selectedTone }
body.noteContext = {
title: noteTitle || '',
content: noteContent || '',
tone: selectedTone,
images: noteImages || [],
}
} else if (chatScope !== 'all') {
// scope is a notebook ID
body.notebookId = chatScope
@@ -123,7 +130,7 @@ export function ContextualAIChat({
return body
}
const { messages, sendMessage, status } = useChat({ transport })
const { messages, sendMessage, status, stop } = useChat({ transport })
const isLoading = status === 'submitted' || status === 'streaming'
@@ -273,6 +280,8 @@ export function ContextualAIChat({
{messages.map((msg: UIMessage) => {
const content = getMessageContent(msg)
// Skip empty assistant messages (thinking/reasoning phase)
if (msg.role === 'assistant' && !content) return null
return (
<div key={msg.id} className={cn('flex gap-2', msg.role === 'user' && 'flex-row-reverse')}>
<div className={cn(
@@ -414,14 +423,24 @@ export function ContextualAIChat({
</button>
)}
</div>
<Button
size="icon"
className="h-7 w-7 rounded-lg bg-primary text-primary-foreground shadow-sm disabled:opacity-50"
onClick={handleSend}
disabled={isLoading || !input.trim()}
>
{isLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5 ml-0.5" />}
</Button>
{isLoading ? (
<Button
size="icon"
className="h-7 w-7 rounded-lg bg-red-500 text-white shadow-sm hover:bg-red-600"
onClick={() => stop()}
>
<Square className="h-3.5 w-3.5" />
</Button>
) : (
<Button
size="icon"
className="h-7 w-7 rounded-lg bg-primary text-primary-foreground shadow-sm disabled:opacity-50"
onClick={handleSend}
disabled={!input.trim()}
>
<Send className="h-3.5 w-3.5 ml-0.5" />
</Button>
)}
</div>
</div>
<p className="text-[9px] text-muted-foreground/40 text-center mt-1">{t('ai.newLineHint')}</p>