mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-23 00:54:22 +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 GooseResponseForm from './GooseResponseForm';
|
||||
import { extractUrls } from '../utils/urlUtils';
|
||||
import { formatMessageTimestamp } from '../utils/timeUtils';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
import ToolCallWithResponse from './ToolCallWithResponse';
|
||||
import {
|
||||
@@ -39,6 +40,9 @@ export default function GooseMessage({
|
||||
// Extract text content from the message
|
||||
let textContent = getTextContent(message);
|
||||
|
||||
// Memoize the timestamp
|
||||
const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]);
|
||||
|
||||
// Get tool requests from the message
|
||||
const toolRequests = getToolRequests(message);
|
||||
|
||||
@@ -116,22 +120,30 @@ export default function GooseMessage({
|
||||
{textContent && (
|
||||
<div className="flex flex-col group">
|
||||
<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>
|
||||
{/* Only show MessageCopyLink if there's text content and no tool requests/responses */}
|
||||
<div className="relative flex justify-end z-[-1]">
|
||||
{toolRequests.length === 0 && (
|
||||
<div className="absolute left-0 text-xs text-textSubtle pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
|
||||
{timestamp}
|
||||
</div>
|
||||
)}
|
||||
{textContent && message.content.every((content) => content.type === 'text') && (
|
||||
<div className="flex justify-end mr-2">
|
||||
<div className="absolute left-0 pt-1">
|
||||
<MessageCopyLink text={textContent} contentRef={contentRef} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toolRequests.length > 0 && (
|
||||
<div className="relative flex flex-col w-full">
|
||||
<div
|
||||
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`}
|
||||
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
|
||||
@@ -146,6 +158,10 @@ export default function GooseMessage({
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{hasToolConfirmation && (
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function MessageCopyLink({ text, contentRef }: MessageCopyLinkPro
|
||||
return (
|
||||
<button
|
||||
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" />
|
||||
<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 { extractUrls } from '../utils/urlUtils';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
import { Message, getTextContent } from '../types/message';
|
||||
import MessageCopyLink from './MessageCopyLink';
|
||||
import { formatMessageTimestamp } from '../utils/timeUtils';
|
||||
|
||||
interface UserMessageProps {
|
||||
message: Message;
|
||||
@@ -15,6 +16,9 @@ export default function UserMessage({ message }: UserMessageProps) {
|
||||
// Extract text content from the 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
|
||||
const urls = extractUrls(textContent, []);
|
||||
|
||||
@@ -30,10 +34,15 @@ export default function UserMessage({ message }: UserMessageProps) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div className="relative h-[22px] flex justify-end">
|
||||
<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>
|
||||
|
||||
{/* TODO(alexhancock): Re-enable link previews once styled well again */}
|
||||
{false && urls.length > 0 && (
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
LoaderCircle,
|
||||
} from 'lucide-react';
|
||||
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 { Modal, ModalContent } from '../ui/modal';
|
||||
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">
|
||||
<span className="flex items-center">
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
{formatDate(session.messages[0]?.created)}
|
||||
{formatMessageTimestamp(session.messages[0]?.created)}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<MessageSquareText className="w-4 h-4 mr-1" />
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Button } from '../ui/button';
|
||||
import BackButton from '../ui/BackButton';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { View, ViewOptions } from '../../App';
|
||||
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||
|
||||
interface SessionListViewProps {
|
||||
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 (
|
||||
<div className="h-screen w-full">
|
||||
<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 items-center text-textSubtle text-sm">
|
||||
<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 className="flex items-center text-textSubtle text-sm">
|
||||
<Folder className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
|
||||
@@ -8,31 +8,7 @@ import MarkdownContent from '../MarkdownContent';
|
||||
import ToolCallWithResponse from '../ToolCallWithResponse';
|
||||
import { ToolRequestMessageContent, ToolResponseMessageContent } from '../../types/message';
|
||||
import { type Message } from '../../types/message';
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
};
|
||||
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||
|
||||
/**
|
||||
* Get tool responses map from messages
|
||||
@@ -166,7 +142,7 @@ export const SessionMessages: React.FC<SessionMessagesProps> = ({
|
||||
{message.role === 'user' ? 'You' : 'Goose'}
|
||||
</span>
|
||||
<span className="text-xs text-textSubtle">
|
||||
{new Date(message.created * 1000).toLocaleTimeString()}
|
||||
{formatMessageTimestamp(message.created)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Calendar, MessageSquareText, Folder, Target } from 'lucide-react';
|
||||
import { type SharedSessionDetails } from '../../sharedSessions';
|
||||
import { SessionHeaderCard, SessionMessages, formatDate } from './SessionViewComponents';
|
||||
import { SessionHeaderCard, SessionMessages } from './SessionViewComponents';
|
||||
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||
|
||||
interface SharedSessionViewProps {
|
||||
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">
|
||||
<span className="flex items-center">
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
{formatDate(session.messages[0]?.created)}
|
||||
{formatMessageTimestamp(session.messages[0]?.created)}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<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