fix: imporove edit message with attachment image

This commit is contained in:
Faisal Amir 2025-09-15 21:48:01 +07:00
parent e80a865def
commit 3b22f0b7c0
3 changed files with 93 additions and 23 deletions

View File

@ -71,7 +71,7 @@ export const ThreadContent = memo(
streamTools?: any
contextOverflowModal?: React.ReactNode | null
updateMessage?: (item: ThreadMessage, message: string) => void
updateMessage?: (item: ThreadMessage, message: string, imageUrls?: string[]) => void
}
) => {
const { t } = useTranslation()
@ -276,9 +276,10 @@ export const ThreadContent = memo(
item.content?.find((c) => c.type === 'text')?.text?.value ||
''
}
onSave={(message) => {
imageUrls={item.content?.filter((c) => c.type === 'image_url' && c.image_url?.url).map((c) => c.image_url!.url)}
onSave={(message, imageUrls) => {
if (item.updateMessage) {
item.updateMessage(item, message)
item.updateMessage(item, message, imageUrls)
}
}}
/>

View File

@ -11,7 +11,7 @@ import {
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { IconPencil } from '@tabler/icons-react'
import { IconPencil, IconX } from '@tabler/icons-react'
import {
Tooltip,
TooltipContent,
@ -20,23 +20,27 @@ import {
interface EditMessageDialogProps {
message: string
onSave: (message: string) => void
imageUrls?: string[]
onSave: (message: string, imageUrls?: string[]) => void
triggerElement?: React.ReactNode
}
export function EditMessageDialog({
message,
imageUrls,
onSave,
triggerElement,
}: EditMessageDialogProps) {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const [draft, setDraft] = useState(message)
const [keptImages, setKeptImages] = useState<string[]>(imageUrls || [])
const textareaRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
setDraft(message)
}, [message])
setKeptImages(imageUrls || [])
}, [message, imageUrls])
useEffect(() => {
if (isOpen && textareaRef.current) {
@ -48,8 +52,15 @@ export function EditMessageDialog({
}, [isOpen])
const handleSave = () => {
if (draft !== message && draft.trim()) {
onSave(draft)
const hasTextChanged = draft !== message && draft.trim()
const hasImageChanged =
JSON.stringify(imageUrls || []) !== JSON.stringify(keptImages)
if (hasTextChanged || hasImageChanged) {
onSave(
draft.trim() || message,
keptImages.length > 0 ? keptImages : undefined
)
setIsOpen(false)
}
}
@ -64,7 +75,7 @@ export function EditMessageDialog({
const defaultTrigger = (
<Tooltip>
<TooltipTrigger asChild>
<div
<div
className="flex outline-0 items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
role="button"
tabIndex={0}
@ -90,6 +101,34 @@ export function EditMessageDialog({
<DialogContent>
<DialogHeader>
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
{keptImages.length > 0 && (
<div className="mt-2 space-y-2">
<div className="flex gap-3 flex-wrap">
{keptImages.map((imageUrl, index) => (
<div
key={index}
className="relative border border-main-view-fg/5 rounded-lg size-14"
>
<img
className="object-cover w-full h-full rounded-lg"
src={imageUrl}
alt={`Attached image ${index + 1}`}
/>
<div
className="absolute -top-1 -right-2.5 bg-destructive size-5 flex rounded-full items-center justify-center cursor-pointer"
onClick={() =>
setKeptImages((prev) =>
prev.filter((_, i) => i !== index)
)
}
>
<IconX className="text-destructive-fg" size={16} />
</div>
</div>
))}
</div>
</div>
)}
<Textarea
ref={textareaRef}
value={draft}
@ -106,7 +145,12 @@ export function EditMessageDialog({
</Button>
</DialogClose>
<Button
disabled={draft === message || !draft.trim()}
disabled={
draft === message &&
JSON.stringify(imageUrls || []) ===
JSON.stringify(keptImages) &&
!draft.trim()
}
onClick={handleSave}
size="sm"
className="w-full sm:w-auto"

View File

@ -87,12 +87,15 @@ function ThreadDetail() {
}, [threadId, currentThreadId, assistants])
useEffect(() => {
serviceHub.messages().fetchMessages(threadId).then((fetchedMessages) => {
if (fetchedMessages) {
// Update the messages in the store
setMessages(threadId, fetchedMessages)
}
})
serviceHub
.messages()
.fetchMessages(threadId)
.then((fetchedMessages) => {
if (fetchedMessages) {
// Update the messages in the store
setMessages(threadId, fetchedMessages)
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [threadId, serviceHub])
@ -137,17 +140,21 @@ function ThreadDetail() {
useEffect(() => {
// Track streaming state changes
const isCurrentlyStreaming = !!streamingContent
const justFinishedStreaming = wasStreamingRef.current && !isCurrentlyStreaming
const justFinishedStreaming =
wasStreamingRef.current && !isCurrentlyStreaming
wasStreamingRef.current = isCurrentlyStreaming
// If streaming just finished and user had an intended position, restore it
if (justFinishedStreaming && userIntendedPositionRef.current !== null) {
// Small delay to ensure DOM has updated
setTimeout(() => {
if (scrollContainerRef.current && userIntendedPositionRef.current !== null) {
if (
scrollContainerRef.current &&
userIntendedPositionRef.current !== null
) {
scrollContainerRef.current.scrollTo({
top: userIntendedPositionRef.current,
behavior: 'smooth'
behavior: 'smooth',
})
userIntendedPositionRef.current = null
setIsUserScrolling(false)
@ -196,7 +203,7 @@ function ThreadDetail() {
// Detect if this is a user-initiated scroll
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
setIsUserScrolling(!isBottom)
// If user scrolls during streaming and moves away from bottom, record their intended position
if (streamingContent && !isBottom) {
userIntendedPositionRef.current = scrollTop
@ -218,7 +225,7 @@ function ThreadDetail() {
// Detect if this is a user-initiated scroll
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
setIsUserScrolling(!isBottom)
// If user scrolls during streaming and moves away from bottom, record their intended position
if (streamingContent && !isBottom) {
userIntendedPositionRef.current = scrollTop
@ -229,11 +236,15 @@ function ThreadDetail() {
lastScrollTopRef.current = scrollTop
}
const updateMessage = (item: ThreadMessage, message: string) => {
const updateMessage = (
item: ThreadMessage,
message: string,
imageUrls?: string[]
) => {
const newMessages: ThreadMessage[] = messages.map((m) => {
if (m.id === item.id) {
const msg: ThreadMessage = cloneDeep(m)
msg.content = [
const newContent = [
{
type: ContentType.Text,
text: {
@ -242,6 +253,20 @@ function ThreadDetail() {
},
},
]
// Add image content if imageUrls are provided
if (imageUrls && imageUrls.length > 0) {
imageUrls.forEach((url) => {
newContent.push({
type: 'image_url' as ContentType,
image_url: {
url: url,
},
} as any)
})
}
msg.content = newContent
return msg
}
return m