123 lines
3.7 KiB
TypeScript
123 lines
3.7 KiB
TypeScript
import React, { memo } from 'react';
|
|
import { Box, Text, Static } from 'ink';
|
|
import type { Message } from '../../../models/types.js';
|
|
import { getMessageText } from '../../../models/media.js';
|
|
import { renderMarkdown } from '../markdown.js';
|
|
import { getBannerLines } from '../banner.js';
|
|
|
|
export interface MessageListProps {
|
|
messages: Message[];
|
|
scrollOffset?: number;
|
|
streamingContent?: string;
|
|
verbose?: boolean;
|
|
}
|
|
|
|
// Helper to format timestamp in human-readable way
|
|
function formatTimestamp(timestamp: number): string {
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const isSameDay = date.toDateString() === now.toDateString();
|
|
|
|
if (isSameDay) {
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
// Individual message component
|
|
const MessageItem = memo(function MessageItem({
|
|
message,
|
|
index,
|
|
}: {
|
|
message: Message;
|
|
index: number;
|
|
}): React.ReactElement {
|
|
const isUser = message.role === 'user';
|
|
const accentColor = isUser ? 'blue' : '#ff8c00';
|
|
const timestampText = message.timestamp ? formatTimestamp(message.timestamp) : 'unknown time';
|
|
|
|
return (
|
|
<Box
|
|
key={index}
|
|
marginBottom={1}
|
|
flexDirection="row"
|
|
>
|
|
<Box minWidth={1} backgroundColor={accentColor} />
|
|
<Box
|
|
flexDirection="column"
|
|
flexGrow={1}
|
|
paddingX={2}
|
|
paddingY={1}
|
|
>
|
|
{/* Author line */}
|
|
<Box marginBottom={1} justifyContent="space-between" flexDirection="row">
|
|
<Text color={accentColor} bold>
|
|
{isUser ? 'You' : 'Flynn'}
|
|
</Text>
|
|
<Text color="gray">| {timestampText}</Text>
|
|
</Box>
|
|
|
|
{/* Content */}
|
|
<Text wrap="wrap">
|
|
{message.role === 'assistant'
|
|
? renderMarkdown(getMessageText(message))
|
|
: getMessageText(message)}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
});
|
|
|
|
export const MessageList = memo(function MessageList({
|
|
messages,
|
|
scrollOffset = 0,
|
|
streamingContent,
|
|
verbose = false,
|
|
}: MessageListProps): React.ReactElement {
|
|
const visibleMessages = messages.slice(scrollOffset);
|
|
|
|
return (
|
|
<Box flexDirection="column" flexGrow={1} paddingX={1} overflowY="hidden">
|
|
{visibleMessages.length === 0 && !streamingContent ? (
|
|
<Box flexDirection="column" alignItems="center" justifyContent="center" flexGrow={1}>
|
|
{getBannerLines().map((line, i) => (
|
|
<Text key={i} color="#ff8c00">{line}</Text>
|
|
))}
|
|
<Text color="gray">{'\n'}Start typing to chat with Flynn.</Text>
|
|
</Box>
|
|
) : (
|
|
<>
|
|
<Static items={visibleMessages}>
|
|
{(message, index) => (
|
|
<MessageItem key={index} message={message} index={index} />
|
|
)}
|
|
</Static>
|
|
{streamingContent && (
|
|
<Box marginBottom={1} flexDirection="row">
|
|
<Box minWidth={1} backgroundColor="#ff8c00" />
|
|
<Box
|
|
flexDirection="column"
|
|
flexGrow={1}
|
|
paddingX={2}
|
|
paddingY={1}
|
|
>
|
|
<Box marginBottom={1}>
|
|
<Text color="#ff8c00" bold>Flynn</Text>
|
|
</Box>
|
|
<Text wrap="wrap">{verbose ? streamingContent : renderMarkdown(streamingContent)}</Text>
|
|
<Text color="yellow">▌</Text>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</>
|
|
)}
|
|
{messages.length > 0 && scrollOffset > 0 && (
|
|
<Box position="absolute" marginTop={-1}>
|
|
<Text color="gray">{scrollOffset} more above</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
});
|