From 01519607dee736d2c2621fc1cb1cd5fa415af309 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:07:33 -0500 Subject: [PATCH] Wire event attachments into chat composer and messages --- web/src/pages/Chat.tsx | 88 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/web/src/pages/Chat.tsx b/web/src/pages/Chat.tsx index bab238fa1b..5399f10bbe 100644 --- a/web/src/pages/Chat.tsx +++ b/web/src/pages/Chat.tsx @@ -3,16 +3,20 @@ import { Input } from "@/components/ui/input"; import { FaArrowUpLong } from "react-icons/fa6"; import { LuCircleAlert } from "react-icons/lu"; import { useTranslation } from "react-i18next"; -import { useState, useCallback, useRef, useEffect } from "react"; +import { useState, useCallback, useRef, useEffect, useMemo } from "react"; import axios from "axios"; import { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow"; import { MessageBubble } from "@/components/chat/ChatMessage"; import { ToolCallsGroup } from "@/components/chat/ToolCallsGroup"; import { ChatStartingState } from "@/components/chat/ChatStartingState"; +import { ChatAttachmentChip } from "@/components/chat/ChatAttachmentChip"; +import { ChatQuickReplies } from "@/components/chat/ChatQuickReplies"; +import { ChatPaperclipButton } from "@/components/chat/ChatPaperclipButton"; import type { ChatMessage } from "@/types/chat"; import { getEventIdsFromSearchObjectsToolCalls, getFindSimilarObjectsFromToolCalls, + prependAttachment, streamChatCompletion, } from "@/utils/chatUtil"; @@ -22,6 +26,7 @@ export default function ChatPage() { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const [attachedEventId, setAttachedEventId] = useState(null); const scrollRef = useRef(null); useEffect(() => { @@ -75,12 +80,31 @@ export default function ChatPage() { [isLoading, t], ); - const sendMessage = useCallback(() => { - const text = input.trim(); - if (!text || isLoading) return; - setInput(""); - submitConversation([...messages, { role: "user", content: text }]); - }, [input, isLoading, messages, submitConversation]); + const recentEventIds = useMemo(() => { + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]; + if (msg.role !== "assistant" || !msg.toolCalls) continue; + const similar = getFindSimilarObjectsFromToolCalls(msg.toolCalls); + if (similar) return similar.results.map((e) => e.id); + const events = getEventIdsFromSearchObjectsToolCalls(msg.toolCalls); + if (events.length > 0) return events.map((e) => e.id); + } + return []; + }, [messages]); + + const sendMessage = useCallback( + (textOverride?: string) => { + const text = (textOverride ?? input).trim(); + if (!text || isLoading) return; + const wireText = attachedEventId + ? prependAttachment(text, attachedEventId) + : text; + setInput(""); + setAttachedEventId(null); + submitConversation([...messages, { role: "user", content: wireText }]); + }, + [attachedEventId, input, isLoading, messages, submitConversation], + ); const handleEditSubmit = useCallback( (messageIndex: number, newContent: string) => { @@ -93,6 +117,10 @@ export default function ChatPage() { [messages, submitConversation], ); + const handleClearAttachment = useCallback(() => { + setAttachedEventId(null); + }, []); + return (
@@ -170,13 +198,19 @@ export default function ChatPage() { ); } const events = getEventIdsFromSearchObjectsToolCalls( msg.toolCalls, ); - return ; + return ( + + ); })()}
); @@ -200,6 +234,10 @@ export default function ChatPage() { sendMessage={sendMessage} isLoading={isLoading} placeholder={t("placeholder")} + attachedEventId={attachedEventId} + onClearAttachment={handleClearAttachment} + onAttach={setAttachedEventId} + recentEventIds={recentEventIds} /> )}
@@ -210,9 +248,13 @@ export default function ChatPage() { type ChatEntryProps = { input: string; setInput: (value: string) => void; - sendMessage: () => void; + sendMessage: (textOverride?: string) => void; isLoading: boolean; placeholder: string; + attachedEventId: string | null; + onClearAttachment: () => void; + onAttach: (eventId: string) => void; + recentEventIds: string[]; }; function ChatEntry({ @@ -221,6 +263,10 @@ function ChatEntry({ sendMessage, isLoading, placeholder, + attachedEventId, + onClearAttachment, + onAttach, + recentEventIds, }: ChatEntryProps) { const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { @@ -230,8 +276,28 @@ function ChatEntry({ }; return ( -
+
+ {attachedEventId && ( +
+ +
+ )} + {attachedEventId && ( + sendMessage(text)} + disabled={isLoading} + /> + )}
+ sendMessage()} >