fix: imporove edit message with attachment image
This commit is contained in:
parent
e80a865def
commit
3b22f0b7c0
@ -71,7 +71,7 @@ export const ThreadContent = memo(
|
|||||||
|
|
||||||
streamTools?: any
|
streamTools?: any
|
||||||
contextOverflowModal?: React.ReactNode | null
|
contextOverflowModal?: React.ReactNode | null
|
||||||
updateMessage?: (item: ThreadMessage, message: string) => void
|
updateMessage?: (item: ThreadMessage, message: string, imageUrls?: string[]) => void
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -276,9 +276,10 @@ export const ThreadContent = memo(
|
|||||||
item.content?.find((c) => c.type === 'text')?.text?.value ||
|
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) {
|
if (item.updateMessage) {
|
||||||
item.updateMessage(item, message)
|
item.updateMessage(item, message, imageUrls)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { IconPencil } from '@tabler/icons-react'
|
import { IconPencil, IconX } from '@tabler/icons-react'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@ -20,23 +20,27 @@ import {
|
|||||||
|
|
||||||
interface EditMessageDialogProps {
|
interface EditMessageDialogProps {
|
||||||
message: string
|
message: string
|
||||||
onSave: (message: string) => void
|
imageUrls?: string[]
|
||||||
|
onSave: (message: string, imageUrls?: string[]) => void
|
||||||
triggerElement?: React.ReactNode
|
triggerElement?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditMessageDialog({
|
export function EditMessageDialog({
|
||||||
message,
|
message,
|
||||||
|
imageUrls,
|
||||||
onSave,
|
onSave,
|
||||||
triggerElement,
|
triggerElement,
|
||||||
}: EditMessageDialogProps) {
|
}: EditMessageDialogProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [draft, setDraft] = useState(message)
|
const [draft, setDraft] = useState(message)
|
||||||
|
const [keptImages, setKeptImages] = useState<string[]>(imageUrls || [])
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDraft(message)
|
setDraft(message)
|
||||||
}, [message])
|
setKeptImages(imageUrls || [])
|
||||||
|
}, [message, imageUrls])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && textareaRef.current) {
|
if (isOpen && textareaRef.current) {
|
||||||
@ -48,8 +52,15 @@ export function EditMessageDialog({
|
|||||||
}, [isOpen])
|
}, [isOpen])
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (draft !== message && draft.trim()) {
|
const hasTextChanged = draft !== message && draft.trim()
|
||||||
onSave(draft)
|
const hasImageChanged =
|
||||||
|
JSON.stringify(imageUrls || []) !== JSON.stringify(keptImages)
|
||||||
|
|
||||||
|
if (hasTextChanged || hasImageChanged) {
|
||||||
|
onSave(
|
||||||
|
draft.trim() || message,
|
||||||
|
keptImages.length > 0 ? keptImages : undefined
|
||||||
|
)
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,6 +101,34 @@ export function EditMessageDialog({
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
|
<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
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={draft}
|
value={draft}
|
||||||
@ -106,7 +145,12 @@ export function EditMessageDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button
|
<Button
|
||||||
disabled={draft === message || !draft.trim()}
|
disabled={
|
||||||
|
draft === message &&
|
||||||
|
JSON.stringify(imageUrls || []) ===
|
||||||
|
JSON.stringify(keptImages) &&
|
||||||
|
!draft.trim()
|
||||||
|
}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
|
|||||||
@ -87,12 +87,15 @@ function ThreadDetail() {
|
|||||||
}, [threadId, currentThreadId, assistants])
|
}, [threadId, currentThreadId, assistants])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
serviceHub.messages().fetchMessages(threadId).then((fetchedMessages) => {
|
serviceHub
|
||||||
if (fetchedMessages) {
|
.messages()
|
||||||
// Update the messages in the store
|
.fetchMessages(threadId)
|
||||||
setMessages(threadId, fetchedMessages)
|
.then((fetchedMessages) => {
|
||||||
}
|
if (fetchedMessages) {
|
||||||
})
|
// Update the messages in the store
|
||||||
|
setMessages(threadId, fetchedMessages)
|
||||||
|
}
|
||||||
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [threadId, serviceHub])
|
}, [threadId, serviceHub])
|
||||||
|
|
||||||
@ -137,17 +140,21 @@ function ThreadDetail() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Track streaming state changes
|
// Track streaming state changes
|
||||||
const isCurrentlyStreaming = !!streamingContent
|
const isCurrentlyStreaming = !!streamingContent
|
||||||
const justFinishedStreaming = wasStreamingRef.current && !isCurrentlyStreaming
|
const justFinishedStreaming =
|
||||||
|
wasStreamingRef.current && !isCurrentlyStreaming
|
||||||
wasStreamingRef.current = isCurrentlyStreaming
|
wasStreamingRef.current = isCurrentlyStreaming
|
||||||
|
|
||||||
// If streaming just finished and user had an intended position, restore it
|
// If streaming just finished and user had an intended position, restore it
|
||||||
if (justFinishedStreaming && userIntendedPositionRef.current !== null) {
|
if (justFinishedStreaming && userIntendedPositionRef.current !== null) {
|
||||||
// Small delay to ensure DOM has updated
|
// Small delay to ensure DOM has updated
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (scrollContainerRef.current && userIntendedPositionRef.current !== null) {
|
if (
|
||||||
|
scrollContainerRef.current &&
|
||||||
|
userIntendedPositionRef.current !== null
|
||||||
|
) {
|
||||||
scrollContainerRef.current.scrollTo({
|
scrollContainerRef.current.scrollTo({
|
||||||
top: userIntendedPositionRef.current,
|
top: userIntendedPositionRef.current,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth',
|
||||||
})
|
})
|
||||||
userIntendedPositionRef.current = null
|
userIntendedPositionRef.current = null
|
||||||
setIsUserScrolling(false)
|
setIsUserScrolling(false)
|
||||||
@ -229,11 +236,15 @@ function ThreadDetail() {
|
|||||||
lastScrollTopRef.current = scrollTop
|
lastScrollTopRef.current = scrollTop
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateMessage = (item: ThreadMessage, message: string) => {
|
const updateMessage = (
|
||||||
|
item: ThreadMessage,
|
||||||
|
message: string,
|
||||||
|
imageUrls?: string[]
|
||||||
|
) => {
|
||||||
const newMessages: ThreadMessage[] = messages.map((m) => {
|
const newMessages: ThreadMessage[] = messages.map((m) => {
|
||||||
if (m.id === item.id) {
|
if (m.id === item.id) {
|
||||||
const msg: ThreadMessage = cloneDeep(m)
|
const msg: ThreadMessage = cloneDeep(m)
|
||||||
msg.content = [
|
const newContent = [
|
||||||
{
|
{
|
||||||
type: ContentType.Text,
|
type: ContentType.Text,
|
||||||
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 msg
|
||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user