diff --git a/services/platform/app/(app)/dashboard/[id]/chat/[threadId]/page.tsx b/services/platform/app/(app)/dashboard/[id]/chat/[threadId]/page.tsx index 4edd95c66..7a139dce0 100644 --- a/services/platform/app/(app)/dashboard/[id]/chat/[threadId]/page.tsx +++ b/services/platform/app/(app)/dashboard/[id]/chat/[threadId]/page.tsx @@ -45,7 +45,7 @@ function ChatSkeleton() {
{/* Messages area with conversation skeleton */} -
+
{/* User message */}
diff --git a/services/platform/app/(app)/dashboard/[id]/chat/components/chat-actions.tsx b/services/platform/app/(app)/dashboard/[id]/chat/components/chat-actions.tsx index c292a04ce..366d72425 100644 --- a/services/platform/app/(app)/dashboard/[id]/chat/components/chat-actions.tsx +++ b/services/platform/app/(app)/dashboard/[id]/chat/components/chat-actions.tsx @@ -75,7 +75,7 @@ export function ChatActions({ - {tCommon('actions.rename')} + + {tCommon('actions.rename')} + @@ -98,7 +100,9 @@ export function ChatActions({ - {tCommon('actions.delete')} + + {tCommon('actions.delete')} + @@ -111,7 +115,8 @@ export function ChatActions({ description={ <> {tChat('deleteConfirmation', { title: chat.title })} -

+
+
{tChat('deleteArchiveMessage')} diff --git a/services/platform/app/(app)/dashboard/[id]/chat/components/chat-history-sidebar.tsx b/services/platform/app/(app)/dashboard/[id]/chat/components/chat-history-sidebar.tsx index 3628e81f5..d31637843 100644 --- a/services/platform/app/(app)/dashboard/[id]/chat/components/chat-history-sidebar.tsx +++ b/services/platform/app/(app)/dashboard/[id]/chat/components/chat-history-sidebar.tsx @@ -39,6 +39,7 @@ export function ChatHistorySidebar({ const [editingChatId, setEditingChatId] = useState(null); const [editValue, setEditValue] = useState(''); const inputRef = useRef(null); + const clickTimeoutRef = useRef | null>(null); // Load chat threads for current user const threadsData = useQuery(api.threads.listThreads, {}); @@ -149,12 +150,12 @@ export function ChatHistorySidebar({ return ( -
- {t('history.recent')} -
{!chats ? (
@@ -180,31 +181,50 @@ export function ChatHistorySidebar({ )} > {isEditing ? ( - setEditValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleSaveRename(chat._id); - } else if (e.key === 'Escape') { - e.preventDefault(); - handleCancelRename(); - } +
handleInputBlur(chat._id)} - className="flex-1 h-6 text-sm px-1.5 py-0 leading-none -mx-1.5" - /> + > + setEditValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSaveRename(chat._id); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleCancelRename(); + } + }} + onBlur={() => handleInputBlur(chat._id)} + className="w-full h-6 px-0 py-0 leading-none focus:border-0 ring-0 outline-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:ring-0 focus:outline-none shadow-none" + /> +
) : ( <> -
+
(null); + + // Update timestamp when optimistic message is set + useEffect(() => { + if (optimisticMessage?.content) { + optimisticMessageTimestampRef.current = Date.now(); + } + }, [optimisticMessage?.content]); + useEffect(() => { if ( optimisticMessage?.content && uiMessages !== undefined && - threadMessages?.some((m) => { + optimisticMessageTimestampRef.current + ) { + // Find user messages created after the optimistic message was set + // Use a small buffer (500ms before) to account for timing differences + const searchStartTime = optimisticMessageTimestampRef.current - 500; + + const matchingMessage = threadMessages?.find((m) => { if (m.role !== 'user') return false; + // Only consider messages created around or after the optimistic message + const messageTime = m._creationTime || m.timestamp.getTime(); + if (messageTime < searchStartTime) return false; // Check for exact match OR if the message starts with the optimistic content // (handles case where images are appended as markdown) return ( m.content === optimisticMessage.content || m.content.startsWith(optimisticMessage.content) ); - }) - ) { - setOptimisticMessage(null); + }); + + if (matchingMessage) { + setOptimisticMessage(null); + optimisticMessageTimestampRef.current = null; + } } }, [ uiMessages, @@ -684,7 +706,7 @@ export function ChatInterface({
- {((isPending && userDraftMessage && !streamingMessage) || + {((isPending && !streamingMessage) || + (streamingMessage && !streamingMessage.text) || hasActiveTools) && ( >; /** Additional CSS class */ className?: string; + /** Whether to show the typing cursor */ + showCursor?: boolean; } // ============================================================================ @@ -109,6 +111,19 @@ const StableMarkdown = memo( // STREAMING MARKDOWN COMPONENT // ============================================================================ +/** + * Animated cursor that appears during typing. + * Uses CSS animation for smooth blinking without JS overhead. + */ +const TypewriterCursor = memo(function TypewriterCursor() { + return ( +