@@ -41,3 +41,4 @@ export const Modal: FC = ({ open, onClose, children, paperSx, wide = ) } + \ No newline at end of file @@ -3,3 +3,116 @@ border: 0; cursor: pointer; } + +// Обёртка сообщения +.messageWrapper { + display: flex; + align-items: center; + margin-top: 16px; + margin-bottom: 16px; +} + +// Аватар +.avatar { + margin-right: 8px; +} + +// Контейнер контента +.contentContainer { + padding-left: 0; + width: fit-content; + max-width: 68%; + margin-right: auto; + + @media (max-width: 768px) { + max-width: 95%; + } +} + +// Пузырь сообщения +.bubble { + overflow-y: auto; + position: relative; + padding: 15px 23px; + border: 1px solid #303035; + color: #a6a5a5; + box-shadow: none; + line-height: 22.5px; + font-size: 15px; + margin-top: 4px; + text-align: left; + font-family: Raleway, sans-serif; + border-radius: 13px; + + p { + color: inherit; + font-style: inherit; + } +} + +// Пузырь с файлом +.bubbleFile { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + color: #7f7df3 !important; + font-weight: 600 !important; + line-height: 140%; + letter-spacing: 0.2px; + cursor: pointer; +} + +// Кнопка меню +.menuButton { + position: sticky; + top: 20px; +} + +// Меню +.menu { + :global(.MuiMenu-list) { + background-color: #303035; + color: #8280ff; + border-radius: 15px; + } + + :global(.MuiPopover-paper) { + background-color: #303035; + border-radius: 15px; + } +} + +// Пункт меню +.menuItem { + font-size: 15px; + font-weight: 500; + display: flex; + align-items: center; + gap: 10px; +} + +.menuItemDelete { + font-size: 15px; + font-weight: 500; + display: flex; + align-items: center; + gap: 5px; +} + +// Имя модели и время +.metaText { + color: #a6a5a5; + line-height: 16.8px; + font-size: 13px; + font-weight: 600; + margin-right: 16px; +} + +.metaTime { + color: #a6a5a5; + line-height: 19.6px; + font-size: 13px; + font-weight: 600; + margin-right: 32px; +} @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { Box, Menu, MenuItem, Slide, Typography } from '@mui/material' +import { Box, Menu, MenuItem, Typography } from '@mui/material' import Stack from '@mui/material/Stack' import Image from 'next/image' @@ -12,7 +12,6 @@ import { FullscreenIcon } from './icons/fullscreen-icon' import { FullscreenMessageModal } from './fullscreen-message-modal' export function BotMessage(props: any) { - // const LazyCode = dynamic(() => import('src/widgets/chat-gpt-field/ui/code')) const [anchorEl, setAnchorEl] = React.useState(null) const open = Boolean(anchorEl) const [markdownMessage, setMarkDownMessage] = React.useState('') @@ -21,9 +20,11 @@ export function BotMessage(props: any) { const copy = (text: string) => { navigator.clipboard.writeText(text) } + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget) } + const handleClose = () => { setAnchorEl(null) } @@ -46,100 +47,41 @@ export function BotMessage(props: any) { } }, [props]) - const flexStyle = props.message.file - ? { - display: 'flex', - alignItems: 'center', - justifyContent: 'start', - gap: '12px', - color: '#7f7df3 !important', - fontWeight: '600 !important', - lineHeight: '140%', - letterSpacing: '0.2px', - cursor: 'pointer', - } - : {} - return ( - // - + {/* Обёртка сообщения */} + + + {/* Аватар (только десктоп) */} {props.desktop && ( - + {''} )} - + + {/* Контейнер контента */} + - + {props.modelTitle} - + {formatDate(props.message.created_at, { hour: 'numeric', minute: 'numeric', })} - + + + {/* Пузырь сообщения */} { if (props.message.file) { window.open(props.message.file, '_blank') } }} - className='smallScroll' - sx={{ - overflowY: 'auto', - position: 'relative', - padding: '15px 23px', - border: `1px solid #303035`, - color: '#A6A5A5', - boxShadow: 'none', - lineHeight: '22.5px', - fontSize: '15px', - marginTop: 0.5, - textAlign: 'left', - fontFamily: 'Raleway,sans-serif', - borderRadius: '13px', - '& p': { - color: 'inherit', - fontStyle: 'inherit', - }, - ...flexStyle, - }} + className={`smallScroll ${styles.bubble} ${props.message.file ? styles.bubbleFile : ''}`} > {props.message.file ? ( @@ -150,20 +92,17 @@ export function BotMessage(props: any) { fill='#7f7df3' /> - ) : ( - '' - )} + ) : null} + + {/* Кнопка меню */} + { - setFullscreenModalOpen(true) - }} + className={styles.menuItem} + onClick={() => setFullscreenModalOpen(true)} > На весь экран + { - copy(props.message.content ? props.message.content : props.message.file.split('/')[4].split('?')[0]) - }} + className={styles.menuItem} + onClick={() => copy(props.message.content ? props.message.content : props.message.file.split('/')[4].split('?')[0])} > - - - + + + Копировать + { - props.deleteMessage(props.message.uid) - }} + className={styles.menuItemDelete} + onClick={() => props.deleteMessage(props.message.uid)} > + setFullscreenModalOpen(false)} @@ -12,6 +12,8 @@ import { PreviewView } from '#/widgets/messages' import { IsNextDay } from '#/widgets/messages/ui/is-next-day' import { makeThinScrollbar } from '#/shared/lib/constants/styles' +import styles from './chat-messages.module.scss' + interface IMessagesList { device: 'mobile' | 'desktop' modelType: string @@ -130,16 +132,11 @@ export const ChatMessagesList: React.FC = memo( > {scrollBottom > 500 && ( { //@ts-ignore const block = paginationScroll.current @@ -156,22 +153,16 @@ export const ChatMessagesList: React.FC = memo( {loading && ( )} @@ -0,0 +1,20 @@ +.scrollDownButton { + position: absolute; + width: fit-content; + cursor: pointer; + margin: 0 auto; + bottom: 150px; + z-index: 10; +} + +.loadingIndicator { + position: absolute; + width: fit-content; + margin: 0 auto; + top: 10px; + z-index: 10; +} + +.circularProgress { + color: #7F7DF3; +} \ No newline at end of file @@ -1,16 +1,16 @@ .closeIcon { cursor: pointer; position: absolute; - top: 22px; + top: 45px; right: 25px; @media (min-width: 400px) and (max-width: 550px) { - top: 20px; + top: 35px; right: 20px; } @media (max-width: 768px) { - top: 20px; + top: 30px; right: 20px; } } @@ -53,10 +53,48 @@ } .markdownContent { + font-weight: 400; + font-style: normal; + color: #ffffff; + text-align: left; + + p, li, td, blockquote { + font-size: var(--fs-base) !important; + } + + h1 { + font-size: var(--fs-h1) !important; + font-weight: 700; + margin-top: 16px; + margin-bottom: 6px; + } + + h2 { + font-size: var(--fs-h2) !important; + font-weight: 700; + margin-top: 14px; + margin-bottom: 6px; + } + + h3 { + font-size: var(--fs-h3) !important; + font-weight: 600; + margin-top: 12px; + margin-bottom: 5px; + } + + strong { + font-weight: 700; + } + + code { + font-size: var(--fs-code) !important; + } + ul, ol { - padding-left: 1.5em; - margin: 0.5em 0; + padding-left: 24px; + margin: 8px 0; } ul { @@ -68,38 +106,29 @@ } li { - margin: 0.25em 0; + margin: 4px 0; } } .content { - height: 500px; overflow-y: auto; - margin-bottom: 20px; flex: 1; min-height: 0; + padding-top: 20px; + padding-right: 8px; + padding-left: 20px; @media (min-width: 769px) and (max-height: 767px) { - height: auto; flex: 1; - margin-bottom: 15px; + padding-top: 15px; + padding-left: 15px; } @media (max-width: 768px) { - height: calc(100vh - 150px); - flex: 0 0 auto; - margin-bottom: 15px; - } - - &.macos { - @media (max-width: 768px) { - @media (max-width: 450px) { - height: calc(100vh - 200px); - margin-bottom: 15px; - } - } + flex: 1; + padding-top: 15px; + padding-left: 15px; } - } .footer { @@ -108,31 +137,30 @@ display: flex; align-items: center; gap: 10px; - margin: auto 0 0 -30px; - padding: 0 30px; flex-shrink: 0; + margin: auto -30px 0; + padding: 0 30px; @media (min-width: 769px) and (max-height: 767px) { height: 70px; - margin: auto 0 0 -25px; + margin: auto -25px 0; padding: 0 25px; } @media (max-width: 768px) { - height: 50px; + height: 60px; + margin: auto -20px 0; padding: 0 20px; gap: 8px; - margin: 0; - - @media (max-width: 450px) { - flex-direction: column; - height: auto; - margin: 0; - padding: 5px; - gap: 15px; - align-items: stretch; - } - } + + @media (max-width: 450px) { + flex-direction: column; + height: auto; + padding: 12px 20px; + gap: 12px; + align-items: stretch; + } + } } .zoomButtonsContainer { @@ -262,4 +290,4 @@ @media (max-width: 768px) { gap: 8px; } -} +} \ No newline at end of file @@ -27,10 +27,9 @@ export const FullscreenMessageModal: React.FC = ({ const [fontSize, setFontSize] = useState(16) const contentRef = useRef(null) const markdownRef = useRef(null) - - // Проверка на macOS - const isMacOS = typeof navigator !== 'undefined' && - (navigator.platform.toUpperCase().indexOf('MAC') >= 0 || + + const isMacOS = typeof navigator !== 'undefined' && + (navigator.platform.toUpperCase().indexOf('MAC') >= 0 || (navigator.userAgent.includes('Mac') && !navigator.userAgent.includes('iPhone') && !navigator.userAgent.includes('iPad'))) const handleFontChange = (event: SelectChangeEvent) => { @@ -47,26 +46,21 @@ export const FullscreenMessageModal: React.FC = ({ const copyIconRef = useRef(null) - // Функция для создания отформатированного HTML для Word const createFormattedHtml = useCallback((htmlContent: string) => { - // Создаем временный контейнер для обработки HTML const tempDiv = document.createElement('div') tempDiv.innerHTML = htmlContent - - // Добавляем inline стили ко всем элементам через setAttribute + const allElements = tempDiv.querySelectorAll('*') allElements.forEach((el) => { const htmlEl = el as HTMLElement - // Получаем текущий style или создаем новый const currentStyle = htmlEl.getAttribute('style') || '' const newStyle = `${currentStyle ? currentStyle + '; ' : ''}font-family: ${fontFamily}; font-size: ${fontSize}pt;` htmlEl.setAttribute('style', newStyle) }) - - // Также применяем к самому контейнеру + const containerStyle = `font-family: ${fontFamily}; font-size: ${fontSize}pt;` tempDiv.setAttribute('style', containerStyle) - + return ` @@ -83,7 +77,6 @@ export const FullscreenMessageModal: React.FC = ({ ` }, [fontFamily, fontSize]) - // Функция для смены иконки на галочку const showCopySuccess = () => { const svg = copyIconRef.current if (!svg) return @@ -100,8 +93,7 @@ export const FullscreenMessageModal: React.FC = ({ /> ` svg.style.animation = 'pulse 0.3s ease' - - // Добавляем анимацию (только один раз) + if (!document.querySelector('#copy-icon-animation')) { const style = document.createElement('style') style.id = 'copy-icon-animation' @@ -113,8 +105,7 @@ export const FullscreenMessageModal: React.FC = ({ ` document.head.appendChild(style) } - - // Возвращаем исходную иконку через 1.5 секунды + setTimeout(() => { if (svg) { svg.innerHTML = originalHTML @@ -123,7 +114,6 @@ export const FullscreenMessageModal: React.FC = ({ }, 1500) } - // Обработка копирования Ctrl+C только когда модалка открыта и выделение внутри неё useEffect(() => { if (!open) return @@ -147,7 +137,6 @@ export const FullscreenMessageModal: React.FC = ({ return () => document.removeEventListener('copy', handleCopy) }, [open, createFormattedHtml]) - // Обработка копирования по клику на иконку const handleCopy = async (e?: React.MouseEvent) => { if (e) { e.preventDefault() @@ -161,10 +150,9 @@ export const FullscreenMessageModal: React.FC = ({ const formattedHtml = createFormattedHtml(htmlContent) try { - // Используем правильную кодировку UTF-8 для Blob const htmlBlob = new Blob([formattedHtml], { type: 'text/html;charset=utf-8' }) const textBlob = new Blob([textContent], { type: 'text/plain;charset=utf-8' }) - + const clipboardItem = new ClipboardItem({ 'text/html': htmlBlob, 'text/plain': textBlob, @@ -172,9 +160,7 @@ export const FullscreenMessageModal: React.FC = ({ await navigator.clipboard.write([clipboardItem]) showCopySuccess() } catch { - // Fallback для старых браузеров и Windows - используем метод через execCommand с правильно форматированным HTML try { - // Создаем временный div с полным HTML документом для Word const tempDiv = document.createElement('div') tempDiv.style.position = 'absolute' tempDiv.style.left = '-9999px' @@ -188,13 +174,12 @@ export const FullscreenMessageModal: React.FC = ({ if (selection) { selection.removeAllRanges() selection.addRange(range) - - // Используем execCommand для копирования + const success = document.execCommand('copy') - + selection.removeAllRanges() document.body.removeChild(tempDiv) - + if (success) { showCopySuccess() } @@ -202,12 +187,10 @@ export const FullscreenMessageModal: React.FC = ({ document.body.removeChild(tempDiv) } } catch { - // Последняя попытка - просто скопировать текст без форматирования try { await navigator.clipboard.writeText(textContent) showCopySuccess() } catch { - // Игнорируем ошибки } } } @@ -287,16 +270,14 @@ export const FullscreenMessageModal: React.FC = ({ @@ -1,4 +1,4 @@ - +desktop.ini /node_modules /.pnp .pnp.js