diff --git a/webapp/src/components/MessageList.css b/webapp/src/components/MessageList.css new file mode 100644 index 00000000..281e20d0 --- /dev/null +++ b/webapp/src/components/MessageList.css @@ -0,0 +1,180 @@ +.message-list-container { + flex: 1; + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + scroll-behavior: smooth; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: inherit; + background-color: var(--theme-background, #ffffff); + color: var(--theme-text, #000000); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.message-list-container[data-testid] { + outline: none; +} + +.message-list-container::-webkit-scrollbar { + width: 10px; +} + +.message-list-container::-webkit-scrollbar-track { + background: var(--theme-background); + border-radius: 4px; +} + +.message-list-container::-webkit-scrollbar-thumb { + background: var(--theme-primary); + border-radius: 4px; + border: 2px solid var(--theme-background); +} + +.message-list-container::-webkit-scrollbar-thumb:hover { + background: var(--theme-primary-dark); +} + +.message-content { + color: inherit; + background: transparent; + transition: color 0.3s ease, background-color 0.3s ease; +} + +.message-content pre[class*="language-"], +.message-content code[class*="language-"] { + background: var(--theme-surface, #f5f5f5); + color: var(--theme-text, #000000); + font-family: var(--theme-code-font); + border: 1px solid var(--theme-border, #e0e0e0); + border-radius: 4px; +} + +.message-content .href-link, +.message-content .play-button, +.message-content .regen-button, +.message-content .cancel-button, +.message-content .text-submit-button { + cursor: pointer; + user-select: none; + display: inline-block; + margin: 2px; + border-radius: 4px; + background-color: var(--theme-surface); + color: var(--theme-text); + transition: all var(--transition-duration) var(--transition-timing), + transform 0.2s ease-in-out; +} + +.message-content .href-link:hover, +.message-content .play-button:hover, +.message-content .regen-button:hover, +.message-content .cancel-button:hover, +.message-content .text-submit-button:hover { + opacity: 0.8; + background-color: var(--theme-primary); + color: var(--theme-bg); + transform: translateY(-1px); +} + +.message-content .referenced-message { + cursor: pointer; + padding: 4px; + margin: 4px 0; + border-left: 3px solid var(--theme-border); + transition: all 0.3s ease; +} + +.message-content .referenced-message.expanded { + background-color: var(--theme-surface); +} + +.message-item { + padding: 1rem; + border-radius: 12px; + max-width: 80%; + box-shadow: var(--theme-shadow-medium, 0 2px 4px rgba(0,0,0,0.1)); + transform: translate3d(0, 0, 0); + transition: transform 0.2s cubic-bezier(0.2, 0, 0.2, 1); + position: relative; + overflow: visible; + backface-visibility: hidden; + perspective: inherit; + border: 1px solid var(--theme-border, #e0e0e0); +} + +.message-item:hover { + transform: translate3d(0, -3px, 0); + box-shadow: var(--theme-shadow-large, 0 4px 8px rgba(0,0,0,0.15)); +} + +.message-item.user { + align-self: flex-end; + background-color: var(--theme-primary, #007AFF); + color: var(--theme-text-on-primary, #ffffff); +} + +.message-item.system { + align-self: flex-start; + background-color: var(--theme-secondary, #5856D6); + color: var(--theme-text-on-secondary, #ffffff); +} + +.message-item.error { + align-self: flex-start; + background: linear-gradient(135deg, var(--theme-error, #FF3B30), var(--theme-warning, #FF9500)); + color: var(--theme-text-on-error, #ffffff); +} + +.message-item.loading, +.message-item.assistant, +.message-item.reference { + align-self: flex-start; + background-color: var(--theme-surface, #f5f5f5); + color: var(--theme-text, #000000); + transition: background-color 0.3s ease, color 0.3s ease; +} +/* Reply form styling */ +.reply-form { + display: flex; + gap: 8px; + margin-top: 8px; + padding: 8px; + background: var(--theme-surface-light, #fafafa); + border-radius: 8px; +} +.reply-input { + flex: 1; + min-height: 36px; + padding: 8px 12px; + border: 1px solid var(--theme-border, #e0e0e0); + border-radius: 4px; + background: var(--theme-background, #ffffff); + color: var(--theme-text, #000000); + font-family: inherit; + resize: vertical; + transition: border-color 0.2s ease; +} +.reply-input:focus { + outline: none; + border-color: var(--theme-primary, #007AFF); +} +.text-submit-button { + padding: 8px 16px; + background-color: var(--theme-primary, #007AFF); + color: var(--theme-text-on-primary, #ffffff); + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s ease, transform 0.1s ease; +} +.text-submit-button:hover { + background-color: var(--theme-primary-dark, #0056b3); + transform: translateY(-1px); +} +.text-submit-button:active { + transform: translateY(0); +} \ No newline at end of file diff --git a/webapp/src/components/MessageList.tsx b/webapp/src/components/MessageList.tsx index 1b8fbbd3..aa556788 100644 --- a/webapp/src/components/MessageList.tsx +++ b/webapp/src/components/MessageList.tsx @@ -3,124 +3,16 @@ import {useSelector} from 'react-redux'; import {useTheme} from '../hooks/useTheme'; import {RootState} from '../store'; import {isArchive} from '../services/appConfig'; -import styled from 'styled-components'; import {debounce, resetTabState, updateTabs} from '../utils/tabHandling'; import WebSocketService from "../services/websocket"; import Prism from 'prismjs'; -import {Message, MessageType} from "../types/messages"; +import {Message} from "../types/messages"; +import './MessageList.css'; const VERBOSE_LOGGING = false && process.env.NODE_ENV === 'development'; const CONTAINER_ID = 'message-list-' + Math.random().toString(36).substr(2, 9); -const MessageListContainer = styled.div` - flex: 1; - overflow-y: auto; - padding: 1rem; - /* Add test id */ - - &[data-testid] { - outline: none; - } - - display: flex; - flex-direction: column; - gap: 1rem; - scroll-behavior: smooth; - background-color: ${({theme}) => theme.colors.background}; - /* Optimize composite layers */ - transform: translate3d(0, 0, 0); - backface-visibility: hidden; - perspective: inherit; - - &::-webkit-scrollbar { - width: 10px; - } - - &::-webkit-scrollbar-track { - background: ${({theme}) => theme.colors.surface}; - border-radius: 4px; - } - - &::-webkit-scrollbar-thumb { - background: ${({theme}) => theme.colors.primary}; - border-radius: 4px; - border: 2px solid ${({theme}) => theme.colors.surface}; - - &:hover { - background: ${({theme}) => theme.colors.primaryDark}; - } - } -`; - -const MessageContent = styled.div` - /* Theme variables for consistent styling */ - color: var(--theme-text); - background: var(--theme-bg); - - pre[class*="language-"], - code[class*="language-"] { - background: var(--theme-surface); - color: var(--theme-text); - font-family: var(--theme-code-font); - } - - .href-link, .play-button, .regen-button, .cancel-button, .text-submit-button { - cursor: pointer; - user-select: none; - display: inline-block; - margin: 2px; - border-radius: 4px; - background-color: var(--theme-surface); - color: var(--theme-text); - transition: all var(--transition-duration) var(--transition-timing), - transform 0.2s ease-in-out; - - &:hover { - opacity: 0.8; - background-color: var(--theme-primary); - color: var(--theme-bg); - transform: translateY(-1px); - } - } - - .referenced-message { - cursor: pointer; - padding: 4px; - margin: 4px 0; - border-left: 3px solid ${({theme}) => theme.colors.border}; - transition: all 0.3s ease; - - &.expanded { - background-color: ${({theme}) => theme.colors.surface}; - } - } - - pre[class*="language-"] { - background: ${({theme}) => theme.colors.surface}; - margin: 1em 0; - padding: 1em; - border-radius: ${({theme}) => theme.sizing.borderRadius.md}; - transition: all var(--transition-duration) var(--transition-timing); - box-shadow: ${({theme}) => theme.shadows.medium}; - } - - code[class*="language-"] { - color: ${({theme}) => theme.colors.text.primary}; - text-shadow: none; - transition: all 0.3s ease; - font-family: ${({theme}) => theme.typography.console.fontFamily}; - } - - :not(pre) > code { - background: ${({theme}) => theme.colors.surface}; - color: ${({theme}) => theme.colors.text.primary}; - padding: 0.2em 0.4em; - border-radius: ${({theme}) => theme.sizing.borderRadius.sm}; - font-size: 0.9em; - transition: all 0.3s ease; - } -`; /** * Extracts message ID and action from clicked elements * Supports both data attributes and class-based detection @@ -143,50 +35,6 @@ const extractMessageAction = (target: HTMLElement): { messageId: string | undefi return {messageId, action}; }; -const MessageItem = styled.div<{ type: MessageType }>` - padding: 1rem; - border-radius: 12px; - align-self: ${({type}) => type === 'user' ? 'flex-end' : 'flex-start'}; - max-width: 80%; - box-shadow: ${({theme}) => theme.shadows.medium}; - /* Use hardware-accelerated properties */ - transform: translate3d(0, 0, 0); - transition: transform 0.2s cubic-bezier(0.2, 0, 0.2, 1); - position: relative; - overflow: visible; - backface-visibility: hidden; - perspective: inherit; - - background-color: ${({type}) => { - switch (type) { - case 'user': - return ({theme}) => theme.colors.primary; - case 'system': - return ({theme}) => theme.colors.secondary; - case 'error': - return ({theme}) => `linear-gradient(135deg, ${theme.colors.error}, ${theme.colors.warning})`; - case 'loading': - return ({theme}) => theme.colors.surface; - case 'assistant': - return ({theme}) => theme.colors.surface; - case 'reference': - return ({theme}) => theme.colors.surface; - default: - return ({theme}) => theme.colors.surface; - } - }}; - color: ${({type, theme}) => - type === 'user' || type === 'system' || type === 'error' - ? '#fff' - : theme.colors.text.primary}; - - &:hover { - transform: translate3d(0, -3px, 0); - box-shadow: ${({theme}) => theme.shadows.large}; - } - -`; - const handleClick = (e: React.MouseEvent) => { const target = e.target as HTMLElement; const {messageId, action} = extractMessageAction(target); @@ -204,9 +52,10 @@ export const handleMessageAction = (messageId: string, action: string) => { } if (action === 'text-submit') { - const input = document.querySelector(`.reply-input[data-message-id="${messageId}"]`) as HTMLTextAreaElement; + const input = document.querySelector(`.reply-input[data-id="${messageId}"]`) as HTMLTextAreaElement; if (input) { const text = input.value; + if (!text.trim()) return; // Don't send empty messages const escapedText = encodeURIComponent(text); const message = `!${messageId},userTxt,${escapedText}`; WebSocketService.send(message); @@ -218,6 +67,8 @@ export const handleMessageAction = (messageId: string, action: string) => { }); } input.value = ''; + // Optional: Add visual feedback + input.style.height = 'auto'; } return; } @@ -284,7 +135,14 @@ export const expandMessageReferences = (content: string, messages: Message[]): s const MessageList: React.FC = ({messages: propMessages}) => { // Add archive mode class to container in archive mode - const containerClassName = `message-list-container${isArchive ? ' archive-mode' : ''}`; + const currentTheme = useSelector((state: RootState) => state.ui.theme); + const containerClassName = `message-list-container${isArchive ? ' archive-mode' : ''} theme-${currentTheme}`; + // Apply theme class to container + React.useEffect(() => { + if (messageListRef.current) { + messageListRef.current.setAttribute('data-theme', currentTheme); + } + }, [currentTheme]); // Memoize processMessages function const processMessages = React.useCallback((msgs: Message[]) => { return msgs @@ -386,10 +244,11 @@ const MessageList: React.FC = ({messages: propMessages}) => { }, [finalMessages]); return ( - {finalMessages.map((message) => { console.debug('MessageList - Rendering message', { @@ -397,24 +256,46 @@ const MessageList: React.FC = ({messages: propMessages}) => { type: message.type, timestamp: message.timestamp, contentLength: message.content?.length || 0 - }); - return - {} - ; + {message.type === 'assistant' && ( +
+