Files
agent-desk-ng/web/app/(dashboard)/dashboard/conversations/_components/agent-message-editor.tsx
mono 63e0457886 fix: harden identity, live access, drafts and setup (review only)
Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
2026-10-16 12:15:20 +00:00

127 lines
3.7 KiB
TypeScript

// Modified in the independent hardening fork; see docs/hardening/README.md.
// Modified for AgentDeskFixes: retain operator drafts and forward pending-send state.
"use client"
import { useEffect, useRef, useState } from "react"
import { toast } from "sonner"
import {
SharedMessageEditor,
type UploadedMessageEditorImage,
} from "@/components/chat/shared-message-editor"
import {
sendOperatorPresence,
useAgentConversationsStore,
} from "@/hooks/use-agent-conversation-realtime"
import type { MessageEditorDraftStore } from "@/lib/message-editor-drafts"
import { useI18n } from "@/i18n/provider"
import { fetchQuickReplyListAll, type AdminQuickReply } from "@/lib/api/admin"
import { suggestAgentReply } from "@/lib/api/agent"
type AgentMessageEditorProps = {
draftStore: MessageEditorDraftStore
sending?: boolean
disabled?: boolean
uploadingAsset?: boolean
onSend: (html: string) => Promise<void>
onUploadImage: (file: File) => Promise<UploadedMessageEditorImage | null>
onSendAttachment: (file: File) => Promise<void>
}
/**
* Agent message editor for dashboard conversations.
* Supports quick replies picker and slash command (/) autocomplete.
*/
export function AgentMessageEditor({
draftStore,
sending = false,
disabled = false,
uploadingAsset = false,
onSend,
onUploadImage,
onSendAttachment,
}: AgentMessageEditorProps) {
const t = useI18n()
const selectedConversationId = useAgentConversationsStore(
(state) => state.selectedConversationId
)
const lastTypingSentAtRef = useRef<number>(0)
const [quickReplies, setQuickReplies] = useState<AdminQuickReply[]>([])
const [loadingQuickReplies, setLoadingQuickReplies] = useState(true)
const [quickReplyPickerOpen, setQuickReplyPickerOpen] = useState(false)
const handleTyping = () => {
if (!selectedConversationId) return
const now = Date.now()
if (now - lastTypingSentAtRef.current >= 3000) {
lastTypingSentAtRef.current = now
sendOperatorPresence(selectedConversationId, "typing")
}
}
useEffect(() => {
let cancelled = false
void fetchQuickReplyListAll()
.then((list) => {
if (!cancelled) {
setQuickReplies(list)
}
})
.catch((error) => {
if (!cancelled) {
toast.error(error instanceof Error ? error.message : t("conversation.loadQuickRepliesFailed"))
}
})
.finally(() => {
if (!cancelled) {
setLoadingQuickReplies(false)
}
})
return () => {
cancelled = true
}
}, [t])
return (
<SharedMessageEditor
variant="agent"
conversationId={selectedConversationId}
draftStore={draftStore}
sending={sending}
disabled={disabled}
uploadingAsset={uploadingAsset}
quickReplies={{
open: quickReplyPickerOpen,
loading: loadingQuickReplies,
items: quickReplies,
onOpenChange: setQuickReplyPickerOpen,
}}
aiCopilot={
selectedConversationId
? {
onSuggest: async () => {
try {
const text = await suggestAgentReply(selectedConversationId)
if (!text) {
toast.info(t("aiCopilot.errorGenerating"))
return null
}
return text
} catch (error) {
toast.error(
error instanceof Error ? error.message : t("aiCopilot.errorGenerating")
)
return null
}
},
}
: undefined
}
onSend={onSend}
onUploadImage={onUploadImage}
onSendAttachment={onSendAttachment}
onTyping={handleTyping}
/>
)
}