mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-22 16:44:21 +01:00
Another take on chat timestamps (#2214)
Co-authored-by: Nahiyan Khan <nahiyan@squareup.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef } from 'react';
|
|||||||
import LinkPreview from './LinkPreview';
|
import LinkPreview from './LinkPreview';
|
||||||
import GooseResponseForm from './GooseResponseForm';
|
import GooseResponseForm from './GooseResponseForm';
|
||||||
import { extractUrls } from '../utils/urlUtils';
|
import { extractUrls } from '../utils/urlUtils';
|
||||||
|
import { formatMessageTimestamp } from '../utils/timeUtils';
|
||||||
import MarkdownContent from './MarkdownContent';
|
import MarkdownContent from './MarkdownContent';
|
||||||
import ToolCallWithResponse from './ToolCallWithResponse';
|
import ToolCallWithResponse from './ToolCallWithResponse';
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +40,9 @@ export default function GooseMessage({
|
|||||||
// Extract text content from the message
|
// Extract text content from the message
|
||||||
let textContent = getTextContent(message);
|
let textContent = getTextContent(message);
|
||||||
|
|
||||||
|
// Memoize the timestamp
|
||||||
|
const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]);
|
||||||
|
|
||||||
// Get tool requests from the message
|
// Get tool requests from the message
|
||||||
const toolRequests = getToolRequests(message);
|
const toolRequests = getToolRequests(message);
|
||||||
|
|
||||||
@@ -116,35 +120,47 @@ export default function GooseMessage({
|
|||||||
{textContent && (
|
{textContent && (
|
||||||
<div className="flex flex-col group">
|
<div className="flex flex-col group">
|
||||||
<div
|
<div
|
||||||
className={`goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 ${toolRequests.length > 0 ? 'rounded-b-none' : ''}`}
|
className={`goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 ${toolRequests.length > 0 ? 'rounded-b-none' : 'rounded-bl-none'}`}
|
||||||
>
|
>
|
||||||
<div ref={contentRef}>{<MarkdownContent content={textContent} />}</div>
|
<div ref={contentRef}>{<MarkdownContent content={textContent} />}</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Only show MessageCopyLink if there's text content and no tool requests/responses */}
|
{/* Only show MessageCopyLink if there's text content and no tool requests/responses */}
|
||||||
{textContent && message.content.every((content) => content.type === 'text') && (
|
<div className="relative flex justify-end z-[-1]">
|
||||||
<div className="flex justify-end mr-2">
|
{toolRequests.length === 0 && (
|
||||||
<MessageCopyLink text={textContent} contentRef={contentRef} />
|
<div className="absolute left-0 text-xs text-textSubtle pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
|
||||||
</div>
|
{timestamp}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
{textContent && message.content.every((content) => content.type === 'text') && (
|
||||||
|
<div className="absolute left-0 pt-1">
|
||||||
|
<MessageCopyLink text={textContent} contentRef={contentRef} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{toolRequests.length > 0 && (
|
{toolRequests.length > 0 && (
|
||||||
<div
|
<div className="relative flex flex-col w-full">
|
||||||
className={`goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 ${textContent ? '' : 'rounded-t-2xl'} rounded-b-2xl px-4 pt-4 pb-2`}
|
<div
|
||||||
>
|
className={`goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 ${textContent ? '' : 'rounded-t-2xl'} rounded-b-2xl rounded-bl-none px-4 pt-4 pb-2`}
|
||||||
{toolRequests.map((toolRequest) => (
|
>
|
||||||
<ToolCallWithResponse
|
{toolRequests.map((toolRequest) => (
|
||||||
// If the message is resumed and not matched tool response, it means the tool is broken or cancelled.
|
<ToolCallWithResponse
|
||||||
isCancelledMessage={
|
// If the message is resumed and not matched tool response, it means the tool is broken or cancelled.
|
||||||
messageIndex < messageHistoryIndex &&
|
isCancelledMessage={
|
||||||
toolResponsesMap.get(toolRequest.id) == undefined
|
messageIndex < messageHistoryIndex &&
|
||||||
}
|
toolResponsesMap.get(toolRequest.id) == undefined
|
||||||
key={toolRequest.id}
|
}
|
||||||
toolRequest={toolRequest}
|
key={toolRequest.id}
|
||||||
toolResponse={toolResponsesMap.get(toolRequest.id)}
|
toolRequest={toolRequest}
|
||||||
/>
|
toolResponse={toolResponsesMap.get(toolRequest.id)}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-textSubtle pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
|
||||||
|
{timestamp}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function MessageCopyLink({ text, contentRef }: MessageCopyLinkPro
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="flex items-center gap-1 text-xs text-textSubtle hover:text-textProminent transition-all duration-200 mt-1 opacity-0 transform -translate-y-2 group-hover:translate-y-0 group-hover:opacity-100"
|
className="flex items-center gap-1 text-xs text-textSubtle hover:cursor-pointer hover:text-textProminent transition-all duration-200 opacity-0 group-hover:opacity-100 -translate-y-4 group-hover:translate-y-0"
|
||||||
>
|
>
|
||||||
<Copy className="h-3 w-3" />
|
<Copy className="h-3 w-3" />
|
||||||
<span>{copied ? 'Copied!' : 'Copy'}</span>
|
<span>{copied ? 'Copied!' : 'Copy'}</span>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef, useMemo } from 'react';
|
||||||
import LinkPreview from './LinkPreview';
|
import LinkPreview from './LinkPreview';
|
||||||
import { extractUrls } from '../utils/urlUtils';
|
import { extractUrls } from '../utils/urlUtils';
|
||||||
import MarkdownContent from './MarkdownContent';
|
import MarkdownContent from './MarkdownContent';
|
||||||
import { Message, getTextContent } from '../types/message';
|
import { Message, getTextContent } from '../types/message';
|
||||||
import MessageCopyLink from './MessageCopyLink';
|
import MessageCopyLink from './MessageCopyLink';
|
||||||
|
import { formatMessageTimestamp } from '../utils/timeUtils';
|
||||||
|
|
||||||
interface UserMessageProps {
|
interface UserMessageProps {
|
||||||
message: Message;
|
message: Message;
|
||||||
@@ -15,6 +16,9 @@ export default function UserMessage({ message }: UserMessageProps) {
|
|||||||
// Extract text content from the message
|
// Extract text content from the message
|
||||||
const textContent = getTextContent(message);
|
const textContent = getTextContent(message);
|
||||||
|
|
||||||
|
// Memoize the timestamp
|
||||||
|
const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]);
|
||||||
|
|
||||||
// Extract URLs which explicitly contain the http:// or https:// protocol
|
// Extract URLs which explicitly contain the http:// or https:// protocol
|
||||||
const urls = extractUrls(textContent, []);
|
const urls = extractUrls(textContent, []);
|
||||||
|
|
||||||
@@ -30,8 +34,13 @@ export default function UserMessage({ message }: UserMessageProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="relative h-[22px] flex justify-end">
|
||||||
<MessageCopyLink text={textContent} contentRef={contentRef} />
|
<div className="absolute right-0 text-xs text-textSubtle pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
|
||||||
|
{timestamp}
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-0 pt-1">
|
||||||
|
<MessageCopyLink text={textContent} contentRef={contentRef} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { type SessionDetails } from '../../sessions';
|
import { type SessionDetails } from '../../sessions';
|
||||||
import { SessionHeaderCard, SessionMessages, formatDate } from './SessionViewComponents';
|
import { SessionHeaderCard, SessionMessages } from './SessionViewComponents';
|
||||||
|
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||||
import { createSharedSession } from '../../sharedSessions';
|
import { createSharedSession } from '../../sharedSessions';
|
||||||
import { Modal, ModalContent } from '../ui/modal';
|
import { Modal, ModalContent } from '../ui/modal';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
@@ -120,7 +121,7 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
|||||||
<div className="flex items-center text-sm text-textSubtle mt-1 space-x-5">
|
<div className="flex items-center text-sm text-textSubtle mt-1 space-x-5">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Calendar className="w-4 h-4 mr-1" />
|
<Calendar className="w-4 h-4 mr-1" />
|
||||||
{formatDate(session.messages[0]?.created)}
|
{formatMessageTimestamp(session.messages[0]?.created)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<MessageSquareText className="w-4 h-4 mr-1" />
|
<MessageSquareText className="w-4 h-4 mr-1" />
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Button } from '../ui/button';
|
|||||||
import BackButton from '../ui/BackButton';
|
import BackButton from '../ui/BackButton';
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
import { View, ViewOptions } from '../../App';
|
import { View, ViewOptions } from '../../App';
|
||||||
|
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||||
|
|
||||||
interface SessionListViewProps {
|
interface SessionListViewProps {
|
||||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||||
@@ -45,29 +46,6 @@ const SessionListView: React.FC<SessionListViewProps> = ({ setView, onSelectSess
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format date to be more readable
|
|
||||||
// eg. "10:39 PM, Feb 28, 2025"
|
|
||||||
const formatDateString = (dateString: string) => {
|
|
||||||
try {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
const time = new Intl.DateTimeFormat('en-US', {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
hour12: true,
|
|
||||||
}).format(date);
|
|
||||||
|
|
||||||
const dateStr = new Intl.DateTimeFormat('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
}).format(date);
|
|
||||||
|
|
||||||
return `${time}, ${dateStr}`;
|
|
||||||
} catch (e) {
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full">
|
<div className="h-screen w-full">
|
||||||
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>
|
||||||
@@ -115,7 +93,9 @@ const SessionListView: React.FC<SessionListViewProps> = ({ setView, onSelectSess
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<div className="flex items-center text-textSubtle text-sm">
|
<div className="flex items-center text-textSubtle text-sm">
|
||||||
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
|
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||||
<span className="truncate">{formatDateString(session.modified)}</span>
|
<span className="truncate">
|
||||||
|
{formatMessageTimestamp(Date.parse(session.modified) / 1000)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-textSubtle text-sm">
|
<div className="flex items-center text-textSubtle text-sm">
|
||||||
<Folder className="w-3 h-3 mr-1 flex-shrink-0" />
|
<Folder className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||||
|
|||||||
@@ -8,31 +8,7 @@ import MarkdownContent from '../MarkdownContent';
|
|||||||
import ToolCallWithResponse from '../ToolCallWithResponse';
|
import ToolCallWithResponse from '../ToolCallWithResponse';
|
||||||
import { ToolRequestMessageContent, ToolResponseMessageContent } from '../../types/message';
|
import { ToolRequestMessageContent, ToolResponseMessageContent } from '../../types/message';
|
||||||
import { type Message } from '../../types/message';
|
import { type Message } from '../../types/message';
|
||||||
|
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||||
/**
|
|
||||||
* Format a timestamp into a human-readable date string
|
|
||||||
*/
|
|
||||||
export const formatDate = (timestamp: number) => {
|
|
||||||
const date = new Date(timestamp * 1000);
|
|
||||||
|
|
||||||
const getOrdinal = (n: number) => {
|
|
||||||
const s = ['th', 'st', 'nd', 'rd'];
|
|
||||||
const v = n % 100;
|
|
||||||
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hours = date.toLocaleTimeString('en-US', {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const month = date.toLocaleString('en-US', { month: 'short' });
|
|
||||||
const day = getOrdinal(date.getDate());
|
|
||||||
const year = date.getFullYear();
|
|
||||||
|
|
||||||
return `${hours}, ${month} ${day}, ${year}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get tool responses map from messages
|
* Get tool responses map from messages
|
||||||
@@ -166,7 +142,7 @@ export const SessionMessages: React.FC<SessionMessagesProps> = ({
|
|||||||
{message.role === 'user' ? 'You' : 'Goose'}
|
{message.role === 'user' ? 'You' : 'Goose'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-textSubtle">
|
<span className="text-xs text-textSubtle">
|
||||||
{new Date(message.created * 1000).toLocaleTimeString()}
|
{formatMessageTimestamp(message.created)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Calendar, MessageSquareText, Folder, Target } from 'lucide-react';
|
import { Calendar, MessageSquareText, Folder, Target } from 'lucide-react';
|
||||||
import { type SharedSessionDetails } from '../../sharedSessions';
|
import { type SharedSessionDetails } from '../../sharedSessions';
|
||||||
import { SessionHeaderCard, SessionMessages, formatDate } from './SessionViewComponents';
|
import { SessionHeaderCard, SessionMessages } from './SessionViewComponents';
|
||||||
|
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||||
|
|
||||||
interface SharedSessionViewProps {
|
interface SharedSessionViewProps {
|
||||||
session: SharedSessionDetails | null;
|
session: SharedSessionDetails | null;
|
||||||
@@ -32,7 +33,7 @@ const SharedSessionView: React.FC<SharedSessionViewProps> = ({
|
|||||||
<div className="flex items-center text-sm text-textSubtle mt-1 space-x-5">
|
<div className="flex items-center text-sm text-textSubtle mt-1 space-x-5">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Calendar className="w-4 h-4 mr-1" />
|
<Calendar className="w-4 h-4 mr-1" />
|
||||||
{formatDate(session.messages[0]?.created)}
|
{formatMessageTimestamp(session.messages[0]?.created)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<MessageSquareText className="w-4 h-4 mr-1" />
|
<MessageSquareText className="w-4 h-4 mr-1" />
|
||||||
|
|||||||
30
ui/desktop/src/utils/timeUtils.ts
Normal file
30
ui/desktop/src/utils/timeUtils.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export function formatMessageTimestamp(timestamp: number): string {
|
||||||
|
// Convert from Unix timestamp (seconds) to milliseconds
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Format time as HH:MM AM/PM
|
||||||
|
const timeStr = date.toLocaleTimeString('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the message is from today
|
||||||
|
if (
|
||||||
|
date.getDate() === now.getDate() &&
|
||||||
|
date.getMonth() === now.getMonth() &&
|
||||||
|
date.getFullYear() === now.getFullYear()
|
||||||
|
) {
|
||||||
|
return timeStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not today, format as MM/DD/YYYY HH:MM AM/PM
|
||||||
|
const dateStr = date.toLocaleDateString('en-US', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
return `${dateStr} ${timeStr}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user