Wire event attachments into chat composer and messages

This commit is contained in:
Josh Hawkins 2026-04-08 17:07:33 -05:00
parent d62ae17fcc
commit 01519607de

View File

@ -3,16 +3,20 @@ import { Input } from "@/components/ui/input";
import { FaArrowUpLong } from "react-icons/fa6"; import { FaArrowUpLong } from "react-icons/fa6";
import { LuCircleAlert } from "react-icons/lu"; import { LuCircleAlert } from "react-icons/lu";
import { useTranslation } from "react-i18next"; 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 axios from "axios";
import { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow"; import { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow";
import { MessageBubble } from "@/components/chat/ChatMessage"; import { MessageBubble } from "@/components/chat/ChatMessage";
import { ToolCallsGroup } from "@/components/chat/ToolCallsGroup"; import { ToolCallsGroup } from "@/components/chat/ToolCallsGroup";
import { ChatStartingState } from "@/components/chat/ChatStartingState"; 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 type { ChatMessage } from "@/types/chat";
import { import {
getEventIdsFromSearchObjectsToolCalls, getEventIdsFromSearchObjectsToolCalls,
getFindSimilarObjectsFromToolCalls, getFindSimilarObjectsFromToolCalls,
prependAttachment,
streamChatCompletion, streamChatCompletion,
} from "@/utils/chatUtil"; } from "@/utils/chatUtil";
@ -22,6 +26,7 @@ export default function ChatPage() {
const [messages, setMessages] = useState<ChatMessage[]>([]); const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [attachedEventId, setAttachedEventId] = useState<string | null>(null);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
@ -75,12 +80,31 @@ export default function ChatPage() {
[isLoading, t], [isLoading, t],
); );
const sendMessage = useCallback(() => { const recentEventIds = useMemo(() => {
const text = input.trim(); for (let i = messages.length - 1; i >= 0; i--) {
if (!text || isLoading) return; const msg = messages[i];
setInput(""); if (msg.role !== "assistant" || !msg.toolCalls) continue;
submitConversation([...messages, { role: "user", content: text }]); const similar = getFindSimilarObjectsFromToolCalls(msg.toolCalls);
}, [input, isLoading, messages, submitConversation]); 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( const handleEditSubmit = useCallback(
(messageIndex: number, newContent: string) => { (messageIndex: number, newContent: string) => {
@ -93,6 +117,10 @@ export default function ChatPage() {
[messages, submitConversation], [messages, submitConversation],
); );
const handleClearAttachment = useCallback(() => {
setAttachedEventId(null);
}, []);
return ( return (
<div className="flex size-full justify-center p-2 md:p-4"> <div className="flex size-full justify-center p-2 md:p-4">
<div className="flex size-full flex-col xl:w-[50%] 3xl:w-[35%]"> <div className="flex size-full flex-col xl:w-[50%] 3xl:w-[35%]">
@ -170,13 +198,19 @@ export default function ChatPage() {
<ChatEventThumbnailsRow <ChatEventThumbnailsRow
events={similar.results} events={similar.results}
anchor={similar.anchor} anchor={similar.anchor}
onAttach={setAttachedEventId}
/> />
); );
} }
const events = getEventIdsFromSearchObjectsToolCalls( const events = getEventIdsFromSearchObjectsToolCalls(
msg.toolCalls, msg.toolCalls,
); );
return <ChatEventThumbnailsRow events={events} />; return (
<ChatEventThumbnailsRow
events={events}
onAttach={setAttachedEventId}
/>
);
})()} })()}
</div> </div>
); );
@ -200,6 +234,10 @@ export default function ChatPage() {
sendMessage={sendMessage} sendMessage={sendMessage}
isLoading={isLoading} isLoading={isLoading}
placeholder={t("placeholder")} placeholder={t("placeholder")}
attachedEventId={attachedEventId}
onClearAttachment={handleClearAttachment}
onAttach={setAttachedEventId}
recentEventIds={recentEventIds}
/> />
)} )}
</div> </div>
@ -210,9 +248,13 @@ export default function ChatPage() {
type ChatEntryProps = { type ChatEntryProps = {
input: string; input: string;
setInput: (value: string) => void; setInput: (value: string) => void;
sendMessage: () => void; sendMessage: (textOverride?: string) => void;
isLoading: boolean; isLoading: boolean;
placeholder: string; placeholder: string;
attachedEventId: string | null;
onClearAttachment: () => void;
onAttach: (eventId: string) => void;
recentEventIds: string[];
}; };
function ChatEntry({ function ChatEntry({
@ -221,6 +263,10 @@ function ChatEntry({
sendMessage, sendMessage,
isLoading, isLoading,
placeholder, placeholder,
attachedEventId,
onClearAttachment,
onAttach,
recentEventIds,
}: ChatEntryProps) { }: ChatEntryProps) {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
@ -230,8 +276,28 @@ function ChatEntry({
}; };
return ( return (
<div className="mt-2 flex w-full flex-col items-center justify-center rounded-xl bg-secondary p-3"> <div className="mt-2 flex w-full flex-col items-stretch justify-center gap-2 rounded-xl bg-secondary p-3">
{attachedEventId && (
<div className="flex items-center">
<ChatAttachmentChip
eventId={attachedEventId}
mode="composer"
onRemove={onClearAttachment}
/>
</div>
)}
{attachedEventId && (
<ChatQuickReplies
onSend={(text) => sendMessage(text)}
disabled={isLoading}
/>
)}
<div className="flex w-full flex-row items-center gap-2"> <div className="flex w-full flex-row items-center gap-2">
<ChatPaperclipButton
recentEventIds={recentEventIds}
onAttach={onAttach}
disabled={isLoading || attachedEventId != null}
/>
<Input <Input
className="w-full flex-1 border-transparent bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent" className="w-full flex-1 border-transparent bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent"
placeholder={placeholder} placeholder={placeholder}
@ -244,7 +310,7 @@ function ChatEntry({
variant="select" variant="select"
className="size-10 shrink-0 rounded-full" className="size-10 shrink-0 rounded-full"
disabled={!input.trim() || isLoading} disabled={!input.trim() || isLoading}
onClick={sendMessage} onClick={() => sendMessage()}
> >
<FaArrowUpLong size="16" /> <FaArrowUpLong size="16" />
</Button> </Button>