From 3bc4b525f382e964e9e2d0945e4d79e67cdcb5ae Mon Sep 17 00:00:00 2001 From: John Carroll <13478618+john-carroll-sw@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:16:21 -0400 Subject: [PATCH 1/3] feat: Complete chat history panel integration - Added chat history functionality to display transcriptions from both model responses and user inputs. - Modified App.tsx, types.ts, and history-panel.tsx to support the history panel logic. - Updated translation.json for localized strings related to the chat history. This update enhances the user experience by providing a comprehensive view of the conversation history. --- app/frontend/src/App.tsx | 46 ++++++++++- .../src/components/ui/history-panel.tsx | 80 ++++++++++++++----- app/frontend/src/locales/en/translation.json | 1 + app/frontend/src/locales/es/translation.json | 1 + app/frontend/src/locales/fr/translation.json | 1 + app/frontend/src/locales/ja/translation.json | 1 + app/frontend/src/types.ts | 21 ++++- 7 files changed, 127 insertions(+), 24 deletions(-) diff --git a/app/frontend/src/App.tsx b/app/frontend/src/App.tsx index ebfde279..599d8bcf 100644 --- a/app/frontend/src/App.tsx +++ b/app/frontend/src/App.tsx @@ -6,12 +6,13 @@ import { Button } from "@/components/ui/button"; import { GroundingFiles } from "@/components/ui/grounding-files"; import GroundingFileView from "@/components/ui/grounding-file-view"; import StatusMessage from "@/components/ui/status-message"; +import HistoryPanel from "@/components/ui/history-panel"; import useRealTime from "@/hooks/useRealtime"; import useAudioRecorder from "@/hooks/useAudioRecorder"; import useAudioPlayer from "@/hooks/useAudioPlayer"; -import { GroundingFile, ToolResult } from "./types"; +import { GroundingFile, HistoryItem, ToolResult } from "./types"; import logo from "./assets/logo.svg"; @@ -19,8 +20,12 @@ function App() { const [isRecording, setIsRecording] = useState(false); const [groundingFiles, setGroundingFiles] = useState([]); const [selectedFile, setSelectedFile] = useState(null); + const [assistantGroundingFiles, setAssistantGroundingFiles] = useState([]); // New state for assistant grounding files + const [showHistory, setShowHistory] = useState(false); + const [history, setHistory] = useState([]); const { startSession, addUserAudio, inputAudioBufferClear } = useRealTime({ + enableInputAudioTranscription: true, // Enable input audio transcription from the user to show in the history onWebSocketOpen: () => console.log("WebSocket connection opened"), onWebSocketClose: () => console.log("WebSocket connection closed"), onWebSocketError: event => console.error("WebSocket error:", event), @@ -33,12 +38,40 @@ function App() { }, onReceivedExtensionMiddleTierToolResponse: message => { const result: ToolResult = JSON.parse(message.tool_result); - const files: GroundingFile[] = result.sources.map(x => { return { id: x.chunk_id, name: x.title, content: x.chunk }; }); - setGroundingFiles(prev => [...prev, ...files]); + setGroundingFiles(prev => [...prev, ...files]); // Keep track of all files used in the session + setAssistantGroundingFiles(files); // Store the grounding files for the assistant + }, + onReceivedInputAudioTranscriptionCompleted: message => { + // Update history with input audio transcription when completed + const newHistoryItem: HistoryItem = { + id: message.event_id, + transcript: message.transcript, + groundingFiles: [], // Assuming no grounding files are associated with the transcription completed + sender: "user", // Indicate that this message is from the user + timestamp: new Date() // Add timestamp + }; + setHistory(prev => [...prev, newHistoryItem]); + }, + onReceivedResponseDone: message => { + const transcript = message.response.output.map(output => output.content?.map(content => content.transcript).join(" ")).join(" "); + if (!transcript) { + return; // Skip adding the history item if the transcript is null or empty + } + + // Update history with response done + const newHistoryItem: HistoryItem = { + id: message.event_id, + transcript: transcript, + groundingFiles: assistantGroundingFiles, // Include the assistant's grounding files + sender: "assistant", // Indicate that this message is from the assistant + timestamp: new Date() // Add timestamp + }; + setHistory(prev => [...prev, newHistoryItem]); + setAssistantGroundingFiles([]); // Clear the assistant grounding files after use } }); @@ -92,6 +125,11 @@ function App() { +
+ +
@@ -99,6 +137,8 @@ function App() {
setSelectedFile(null)} /> + + setShowHistory(false)} onSelectedGroundingFile={setSelectedFile} /> ); } diff --git a/app/frontend/src/components/ui/history-panel.tsx b/app/frontend/src/components/ui/history-panel.tsx index ccb4ec69..c5e2aac9 100644 --- a/app/frontend/src/components/ui/history-panel.tsx +++ b/app/frontend/src/components/ui/history-panel.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { X } from "lucide-react"; @@ -17,6 +18,30 @@ type Properties = { export default function HistoryPanel({ show, history, onClosed, onSelectedGroundingFile }: Properties) { const { t } = useTranslation(); + const historyEndRef = useRef(null); + + // Scroll to the bottom whenever the history changes + useEffect(() => { + if (historyEndRef.current) { + historyEndRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [history]); + + const formatTimestamp = (timestamp: Date) => { + const hours = timestamp.getHours(); + const minutes = timestamp.getMinutes(); + const ampm = hours >= 12 ? "PM" : "AM"; + const formattedHours = hours % 12 || 12; + const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes; + return `${formattedHours}:${formattedMinutes} ${ampm}`; + }; + + const shouldShowTimestamp = (current: Date, next?: Date, isLast?: boolean) => { + if (isLast) return false; // Do not show timestamp for the last message + if (!next) return true; + const diff = (next.getTime() - current.getTime()) / 1000; // Difference in seconds + return diff > 60; // Show timestamp if more than 30 seconds have passed + }; return ( @@ -28,27 +53,44 @@ export default function HistoryPanel({ show, history, onClosed, onSelectedGround transition={{ type: "spring", stiffness: 300, damping: 30 }} className="fixed inset-y-0 right-0 z-40 w-full overflow-y-auto bg-white shadow-lg sm:w-96" > +
+

{t("history.answerHistory")}

+ +
-
-

{t("history.answerHistory")}

- -
{history.length > 0 ? ( - history.map((item, index) => ( -
-

{item.id}

-
-                                        {item.transcript}
-                                    
-
- {item.groundingFiles.map((file, index) => ( - onSelectedGroundingFile(file)} /> - ))} -
-
- )) +
+ {history.map((item, index) => { + const nextItem = history[index + 1]; + const isLast = index === history.length - 1; + const showTimestamp = shouldShowTimestamp( + new Date(item.timestamp), + nextItem ? new Date(nextItem.timestamp) : undefined, + isLast + ); + return ( +
+
+

{item.transcript}

+
+ {item.groundingFiles.map((file, index) => ( + onSelectedGroundingFile(file)} /> + ))} +
+
+ {showTimestamp && ( +
{formatTimestamp(new Date(item.timestamp))}
+ )} +
+ ); + })} +
+
) : (

{t("history.noHistory")}

)} diff --git a/app/frontend/src/locales/en/translation.json b/app/frontend/src/locales/en/translation.json index 58524d62..b6e189e1 100644 --- a/app/frontend/src/locales/en/translation.json +++ b/app/frontend/src/locales/en/translation.json @@ -2,6 +2,7 @@ "app": { "title": "Talk to your data", "footer": "Built with Azure AI Search + Azure OpenAI", + "showHistory": "Show Chat History", "stopRecording": "Stop recording", "startRecording": "Start recording", "stopConversation": "Stop conversation" diff --git a/app/frontend/src/locales/es/translation.json b/app/frontend/src/locales/es/translation.json index b0a51c11..15b2f62a 100644 --- a/app/frontend/src/locales/es/translation.json +++ b/app/frontend/src/locales/es/translation.json @@ -2,6 +2,7 @@ "app": { "title": "Habla con tus datos", "footer": "Creado con Azure AI Search + Azure OpenAI", + "showHistory": "Mostrar historial de chat", "stopRecording": "Detener grabación", "startRecording": "Comenzar grabación", "stopConversation": "Detener conversación" diff --git a/app/frontend/src/locales/fr/translation.json b/app/frontend/src/locales/fr/translation.json index 3e381b62..3c594206 100644 --- a/app/frontend/src/locales/fr/translation.json +++ b/app/frontend/src/locales/fr/translation.json @@ -2,6 +2,7 @@ "app": { "title": "Parlez à vos données", "footer": "Créée avec Azure AI Search + Azure OpenAI", + "showHistory": "Afficher l'historique du chat", "stopRecording": "Arrêter l'enregistrement", "startRecording": "Commencer l'enregistrement", "stopConversation": "Arrêter la conversation" diff --git a/app/frontend/src/locales/ja/translation.json b/app/frontend/src/locales/ja/translation.json index 0f332e02..8297e65d 100644 --- a/app/frontend/src/locales/ja/translation.json +++ b/app/frontend/src/locales/ja/translation.json @@ -2,6 +2,7 @@ "app": { "title": "データと話す", "footer": "Azure AI Search + Azure OpenAI で構築", + "showHistory": "チャット履歴を表示", "stopRecording": "録音を停止", "startRecording": "録音を開始", "stopConversation": "会話を停止" diff --git a/app/frontend/src/types.ts b/app/frontend/src/types.ts index bef21a02..16e5960d 100644 --- a/app/frontend/src/types.ts +++ b/app/frontend/src/types.ts @@ -1,20 +1,28 @@ +// Represents a grounding file export type GroundingFile = { id: string; name: string; content: string; }; +// Represents an item in the history export type HistoryItem = { id: string; transcript: string; groundingFiles: GroundingFile[]; + sender: "user" | "assistant"; // Add sender field + timestamp: Date; // Add timestamp field }; +// Represents a command to update the session export type SessionUpdateCommand = { type: "session.update"; session: { turn_detection?: { type: "server_vad" | "none"; + threshold?: number; + prefix_padding_ms?: number; + silence_duration_ms?: number; }; input_audio_transcription?: { model: "whisper-1"; @@ -22,29 +30,35 @@ export type SessionUpdateCommand = { }; }; +// Represents a command to append audio to the input buffer export type InputAudioBufferAppendCommand = { type: "input_audio_buffer.append"; - audio: string; + audio: string; // Ensure this is a valid base64-encoded string }; +// Represents a command to clear the input audio buffer export type InputAudioBufferClearCommand = { type: "input_audio_buffer.clear"; }; +// Represents a generic message export type Message = { type: string; }; +// Represents a response containing an audio delta export type ResponseAudioDelta = { type: "response.audio.delta"; - delta: string; + delta: string; // Ensure this is a valid base64-encoded string }; +// Represents a response containing an audio transcript delta export type ResponseAudioTranscriptDelta = { type: "response.audio_transcript.delta"; delta: string; }; +// Represents a response indicating that input audio transcription is completed export type ResponseInputAudioTranscriptionCompleted = { type: "conversation.item.input_audio_transcription.completed"; event_id: string; @@ -53,6 +67,7 @@ export type ResponseInputAudioTranscriptionCompleted = { transcript: string; }; +// Represents a response indicating that the response is done export type ResponseDone = { type: "response.done"; event_id: string; @@ -62,6 +77,7 @@ export type ResponseDone = { }; }; +// Represents a response from an extension middle tier tool export type ExtensionMiddleTierToolResponse = { type: "extension.middle_tier_tool.response"; previous_item_id: string; @@ -69,6 +85,7 @@ export type ExtensionMiddleTierToolResponse = { tool_result: string; // JSON string that needs to be parsed into ToolResult }; +// Represents the result from a tool export type ToolResult = { sources: { chunk_id: string; title: string; chunk: string }[]; }; From 362114a89f3b254c187e76884173d4cda98fa464 Mon Sep 17 00:00:00 2001 From: John Carroll <13478618+john-carroll-sw@users.noreply.github.com> Date: Fri, 1 Nov 2024 18:27:55 -0400 Subject: [PATCH 2/3] Refactored frontend for code clarity and globalization: - Removed redundant comments in `App.tsx`, `history-panel.tsx`, and `types.ts`. - Optimized date formatting for `history-panel.tsx` with global considerations. - Corrected time duration in `history-panel.tsx` comment to match code. - Improved timestamp handling in `history-panel.tsx` by reusing existing Date object. - Updated `translation.json` to simplify phrases and avoid uppercase. - Enhanced usage and optional parameters for `groundingFiles` in `types.ts`. --- app/frontend/src/App.tsx | 34 +++++++++---------- .../src/components/ui/history-panel.tsx | 18 ++++------ app/frontend/src/locales/en/translation.json | 4 +-- app/frontend/src/locales/es/translation.json | 4 +-- app/frontend/src/locales/fr/translation.json | 4 +-- app/frontend/src/locales/ja/translation.json | 4 +-- app/frontend/src/types.ts | 6 ++-- 7 files changed, 34 insertions(+), 40 deletions(-) diff --git a/app/frontend/src/App.tsx b/app/frontend/src/App.tsx index 599d8bcf..56904720 100644 --- a/app/frontend/src/App.tsx +++ b/app/frontend/src/App.tsx @@ -3,7 +3,6 @@ import { Mic, MicOff } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; -import { GroundingFiles } from "@/components/ui/grounding-files"; import GroundingFileView from "@/components/ui/grounding-file-view"; import StatusMessage from "@/components/ui/status-message"; import HistoryPanel from "@/components/ui/history-panel"; @@ -18,10 +17,9 @@ import logo from "./assets/logo.svg"; function App() { const [isRecording, setIsRecording] = useState(false); - const [groundingFiles, setGroundingFiles] = useState([]); const [selectedFile, setSelectedFile] = useState(null); - const [assistantGroundingFiles, setAssistantGroundingFiles] = useState([]); // New state for assistant grounding files - const [showHistory, setShowHistory] = useState(false); + const [groundingFiles, setGroundingFiles] = useState([]); + const [showTranscript, setShowTranscript] = useState(false); const [history, setHistory] = useState([]); const { startSession, addUserAudio, inputAudioBufferClear } = useRealTime({ @@ -42,16 +40,15 @@ function App() { return { id: x.chunk_id, name: x.title, content: x.chunk }; }); - setGroundingFiles(prev => [...prev, ...files]); // Keep track of all files used in the session - setAssistantGroundingFiles(files); // Store the grounding files for the assistant + setGroundingFiles(files); // Store the grounding files for the assistant }, onReceivedInputAudioTranscriptionCompleted: message => { // Update history with input audio transcription when completed const newHistoryItem: HistoryItem = { id: message.event_id, transcript: message.transcript, - groundingFiles: [], // Assuming no grounding files are associated with the transcription completed - sender: "user", // Indicate that this message is from the user + groundingFiles: [], + sender: "user", timestamp: new Date() // Add timestamp }; setHistory(prev => [...prev, newHistoryItem]); @@ -59,19 +56,19 @@ function App() { onReceivedResponseDone: message => { const transcript = message.response.output.map(output => output.content?.map(content => content.transcript).join(" ")).join(" "); if (!transcript) { - return; // Skip adding the history item if the transcript is null or empty + return; } // Update history with response done const newHistoryItem: HistoryItem = { id: message.event_id, transcript: transcript, - groundingFiles: assistantGroundingFiles, // Include the assistant's grounding files - sender: "assistant", // Indicate that this message is from the assistant + groundingFiles: groundingFiles, + sender: "assistant", timestamp: new Date() // Add timestamp }; setHistory(prev => [...prev, newHistoryItem]); - setAssistantGroundingFiles([]); // Clear the assistant grounding files after use + setGroundingFiles([]); // Clear the assistant grounding files after use } }); @@ -124,11 +121,14 @@ function App() {
-
- +
@@ -138,7 +138,7 @@ function App() { setSelectedFile(null)} /> - setShowHistory(false)} onSelectedGroundingFile={setSelectedFile} /> + setShowTranscript(false)} onSelectedGroundingFile={setSelectedFile} />
); } diff --git a/app/frontend/src/components/ui/history-panel.tsx b/app/frontend/src/components/ui/history-panel.tsx index c5e2aac9..f2eb7cef 100644 --- a/app/frontend/src/components/ui/history-panel.tsx +++ b/app/frontend/src/components/ui/history-panel.tsx @@ -40,7 +40,7 @@ export default function HistoryPanel({ show, history, onClosed, onSelectedGround if (isLast) return false; // Do not show timestamp for the last message if (!next) return true; const diff = (next.getTime() - current.getTime()) / 1000; // Difference in seconds - return diff > 60; // Show timestamp if more than 30 seconds have passed + return diff > 60; // Show timestamp if more than 60 seconds have passed }; return ( @@ -54,7 +54,7 @@ export default function HistoryPanel({ show, history, onClosed, onSelectedGround className="fixed inset-y-0 right-0 z-40 w-full overflow-y-auto bg-white shadow-lg sm:w-96" >
-

{t("history.answerHistory")}

+

{t("history.transcriptHistory")}

@@ -65,27 +65,21 @@ export default function HistoryPanel({ show, history, onClosed, onSelectedGround {history.map((item, index) => { const nextItem = history[index + 1]; const isLast = index === history.length - 1; - const showTimestamp = shouldShowTimestamp( - new Date(item.timestamp), - nextItem ? new Date(nextItem.timestamp) : undefined, - isLast - ); + const showTimestamp = shouldShowTimestamp(item.timestamp, nextItem ? nextItem.timestamp : undefined, isLast); return (

{item.transcript}

- {item.groundingFiles.map((file, index) => ( + {item.groundingFiles?.map((file, index) => ( onSelectedGroundingFile(file)} /> ))}
- {showTimestamp && ( -
{formatTimestamp(new Date(item.timestamp))}
- )} + {showTimestamp &&
{formatTimestamp(item.timestamp)}
}
); })} diff --git a/app/frontend/src/locales/en/translation.json b/app/frontend/src/locales/en/translation.json index b6e189e1..b988029e 100644 --- a/app/frontend/src/locales/en/translation.json +++ b/app/frontend/src/locales/en/translation.json @@ -2,7 +2,7 @@ "app": { "title": "Talk to your data", "footer": "Built with Azure AI Search + Azure OpenAI", - "showHistory": "Show Chat History", + "showTranscript": "Show transcript", "stopRecording": "Stop recording", "startRecording": "Start recording", "stopConversation": "Stop conversation" @@ -12,7 +12,7 @@ "conversationInProgress": "Conversation in progress" }, "history": { - "answerHistory": "Answer history", + "transcriptHistory": "Transcript history", "noHistory": "No history yet." }, "groundingFiles": { diff --git a/app/frontend/src/locales/es/translation.json b/app/frontend/src/locales/es/translation.json index 15b2f62a..1955f94d 100644 --- a/app/frontend/src/locales/es/translation.json +++ b/app/frontend/src/locales/es/translation.json @@ -2,7 +2,7 @@ "app": { "title": "Habla con tus datos", "footer": "Creado con Azure AI Search + Azure OpenAI", - "showHistory": "Mostrar historial de chat", + "showTranscript": "Mostrar transcripción", "stopRecording": "Detener grabación", "startRecording": "Comenzar grabación", "stopConversation": "Detener conversación" @@ -12,7 +12,7 @@ "conversationInProgress": "Conversación en progreso" }, "history": { - "answerHistory": "Historial de respuestas", + "transcriptHistory": "Historial de transcripciones", "noHistory": "Aún no hay historial." }, "groundingFiles": { diff --git a/app/frontend/src/locales/fr/translation.json b/app/frontend/src/locales/fr/translation.json index 3c594206..76c47e0f 100644 --- a/app/frontend/src/locales/fr/translation.json +++ b/app/frontend/src/locales/fr/translation.json @@ -2,7 +2,7 @@ "app": { "title": "Parlez à vos données", "footer": "Créée avec Azure AI Search + Azure OpenAI", - "showHistory": "Afficher l'historique du chat", + "showTranscript": "Afficher la transcription", "stopRecording": "Arrêter l'enregistrement", "startRecording": "Commencer l'enregistrement", "stopConversation": "Arrêter la conversation" @@ -12,7 +12,7 @@ "conversationInProgress": "Conversation en cours" }, "history": { - "answerHistory": "Historique des réponses", + "transcriptHistory": "Historique de la transcription", "noHistory": "Pas encore d'historique." }, "groundingFiles": { diff --git a/app/frontend/src/locales/ja/translation.json b/app/frontend/src/locales/ja/translation.json index 8297e65d..8ba22730 100644 --- a/app/frontend/src/locales/ja/translation.json +++ b/app/frontend/src/locales/ja/translation.json @@ -2,7 +2,7 @@ "app": { "title": "データと話す", "footer": "Azure AI Search + Azure OpenAI で構築", - "showHistory": "チャット履歴を表示", + "showTranscript": "トランスクリプトを表示", "stopRecording": "録音を停止", "startRecording": "録音を開始", "stopConversation": "会話を停止" @@ -12,7 +12,7 @@ "conversationInProgress": "会話が進行中" }, "history": { - "answerHistory": "回答履歴", + "transcriptHistory": "トランスクリプト履歴", "noHistory": "まだ履歴はありません。" }, "groundingFiles": { diff --git a/app/frontend/src/types.ts b/app/frontend/src/types.ts index 16e5960d..2c6aaa53 100644 --- a/app/frontend/src/types.ts +++ b/app/frontend/src/types.ts @@ -9,8 +9,8 @@ export type GroundingFile = { export type HistoryItem = { id: string; transcript: string; - groundingFiles: GroundingFile[]; - sender: "user" | "assistant"; // Add sender field + groundingFiles?: GroundingFile[]; + sender: "user" | "assistant"; timestamp: Date; // Add timestamp field }; @@ -49,7 +49,7 @@ export type Message = { // Represents a response containing an audio delta export type ResponseAudioDelta = { type: "response.audio.delta"; - delta: string; // Ensure this is a valid base64-encoded string + delta: string; }; // Represents a response containing an audio transcript delta From 27c6d01b8b04f7b8abfb55befd9ced298b905745 Mon Sep 17 00:00:00 2001 From: John Carroll <13478618+john-carroll-sw@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:22:39 -0400 Subject: [PATCH 3/3] feat: Add globalization support for timestamp formatting and optimize rendering - Use `Intl.DateTimeFormat` to format timestamps according to the user's locale. - Utilize `navigator.language` to determine the user's preferred language. - Ensure that timestamps are displayed in a format familiar to the user based on their language and regional settings. - For a user with the locale set to `en-US` (United States), the timestamp might be formatted as `2:30 PM`. - For a user with the locale set to `fr-FR` (France), the timestamp might be formatted as `14:30`. - Update the current time every second to dynamically show the timestamp after 60 seconds have passed. - Use `React.memo` to prevent unnecessary re-renders of child components and ensure efficient rendering logic. This change improves the user experience by providing localized timestamp formatting and optimizing component rendering. --- .../src/components/ui/history-panel.tsx | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/app/frontend/src/components/ui/history-panel.tsx b/app/frontend/src/components/ui/history-panel.tsx index f2eb7cef..d4c12c54 100644 --- a/app/frontend/src/components/ui/history-panel.tsx +++ b/app/frontend/src/components/ui/history-panel.tsx @@ -1,14 +1,13 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState, memo } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { X } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Button } from "./button"; import GroundingFile from "./grounding-file"; import { GroundingFile as GroundingFileType, HistoryItem } from "@/types"; -import { useTranslation } from "react-i18next"; - type Properties = { history: HistoryItem[]; show: boolean; @@ -16,9 +15,10 @@ type Properties = { onSelectedGroundingFile: (file: GroundingFileType) => void; }; -export default function HistoryPanel({ show, history, onClosed, onSelectedGroundingFile }: Properties) { +const HistoryPanel = ({ show, history, onClosed, onSelectedGroundingFile }: Properties) => { const { t } = useTranslation(); const historyEndRef = useRef(null); + const [currentTime, setCurrentTime] = useState(new Date()); // Scroll to the bottom whenever the history changes useEffect(() => { @@ -27,22 +27,32 @@ export default function HistoryPanel({ show, history, onClosed, onSelectedGround } }, [history]); + // Update current time every second + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(new Date()); + }, 1000); + return () => clearInterval(interval); + }, []); + const formatTimestamp = (timestamp: Date) => { - const hours = timestamp.getHours(); - const minutes = timestamp.getMinutes(); - const ampm = hours >= 12 ? "PM" : "AM"; - const formattedHours = hours % 12 || 12; - const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes; - return `${formattedHours}:${formattedMinutes} ${ampm}`; + const options: Intl.DateTimeFormatOptions = { + hour: "numeric", + minute: "numeric", + hour12: true + }; + return new Intl.DateTimeFormat(navigator.language, options).format(timestamp); }; - const shouldShowTimestamp = (current: Date, next?: Date, isLast?: boolean) => { - if (isLast) return false; // Do not show timestamp for the last message - if (!next) return true; - const diff = (next.getTime() - current.getTime()) / 1000; // Difference in seconds + const shouldShowTimestamp = (current: Date, next?: Date) => { + const nextTime = next ? next.getTime() : currentTime.getTime(); + const diff = (nextTime - current.getTime()) / 1000; // Difference in seconds + return diff > 60; // Show timestamp if more than 60 seconds have passed }; + const MemoizedGroundingFile = memo(GroundingFile); + return ( {show && ( @@ -64,8 +74,7 @@ export default function HistoryPanel({ show, history, onClosed, onSelectedGround
{history.map((item, index) => { const nextItem = history[index + 1]; - const isLast = index === history.length - 1; - const showTimestamp = shouldShowTimestamp(item.timestamp, nextItem ? nextItem.timestamp : undefined, isLast); + const showTimestamp = shouldShowTimestamp(item.timestamp, nextItem ? nextItem.timestamp : undefined); return (
{item.transcript}

{item.groundingFiles?.map((file, index) => ( - onSelectedGroundingFile(file)} /> + onSelectedGroundingFile(file)} /> ))}
@@ -93,4 +102,6 @@ export default function HistoryPanel({ show, history, onClosed, onSelectedGround )} ); -} +}; + +export default HistoryPanel;