@@ -46,7 +46,7 @@ const Home: NextPageWithLayout = () => { title: 'ChatGPT', image: '/images/promo-cards/gpt.png', description: 'Чат-бот, при помощи которого вы сможете найти любой ответ на ваш запрос.', - routerlink: '/chat-bot/chatgpt_5_5', + routerlink: '/chat-bot/chatgpt', buttonText: 'Попробовать', }, { @@ -0,0 +1,38 @@ +export interface ParsedThinkContent { + thinkContents: string[] + mainContent: string + hasIncompleteThink: boolean +} + +const COMPLETE_THINK_REGEX = /([\s\S]*?)<\/think>/gi +const INCOMPLETE_THINK_REGEX = /([\s\S]*)$/i + +export function parseThinkBlocks(content: string): ParsedThinkContent { + if (!content) { + return { thinkContents: [], mainContent: '', hasIncompleteThink: false } + } + + const thinkContents: string[] = [] + let remaining = content.replace(COMPLETE_THINK_REGEX, (_, thinkContent: string) => { + const trimmed = thinkContent.trim() + if (trimmed) { + thinkContents.push(trimmed) + } + return '' + }) + + const incompleteMatch = remaining.match(INCOMPLETE_THINK_REGEX) + if (incompleteMatch) { + const trimmed = incompleteMatch[1].trim() + if (trimmed) { + thinkContents.push(trimmed) + } + remaining = remaining.slice(0, incompleteMatch.index) + } + + return { + thinkContents, + mainContent: remaining.trim(), + hasIncompleteThink: Boolean(incompleteMatch), + } +} @@ -6,10 +6,12 @@ import Image from 'next/image' import { TooltipCustom } from '#/shared' import { formatDate } from '#/shared/lib/helpers' import { Markdown } from '../../lib/markdown/markdown' +import { parseThinkBlocks } from '../../lib/parse-think-blocks' import styles from './bot-message.module.scss' import { FullscreenIcon } from './icons/fullscreen-icon' import { FullscreenMessageModal } from './fullscreen-message-modal' +import { ThinkBlock } from './think-block' export function BotMessage(props: any) { const [anchorEl, setAnchorEl] = React.useState(null) @@ -59,6 +61,13 @@ export function BotMessage(props: any) { } : {} + const rawContent = props.message.file + ? props.message.file.match(/(.+)\/(.+)\?/)[2] + : props.message.content + const { thinkContents, mainContent, hasIncompleteThink } = parseThinkBlocks( + props.message.file ? '' : props.message.content || '', + ) + return ( + {props.message.file ? ( + + ) : ( + <> + {thinkContents.map((thinkContent, index) => ( + + ))} + {mainContent ? ( + + ) : null} + + )} { - copy(props.message.content ? props.message.content : props.message.file.split('/')[4].split('?')[0]) + copy( + props.message.file + ? props.message.file.split('/')[4].split('?')[0] + : mainContent, + ) }} > @@ -4,9 +4,11 @@ import Dialog from '@mui/material/Dialog' import Image from 'next/image' import { Markdown } from '../../lib/markdown/markdown' +import { parseThinkBlocks } from '../../lib/parse-think-blocks' import { ZoomOutIcon as ZoomInIcon } from './icons/zoom-in-icon' import { ZoomOutIcon } from './icons/zoom-out-icon' +import { ThinkBlock } from './think-block' import styles from './fullscreen-message-modal.module.scss' @@ -47,6 +49,8 @@ export const FullscreenMessageModal: React.FC = ({ const copyIconRef = useRef(null) + const { thinkContents, mainContent } = parseThinkBlocks(messageContent || '') + // Функция для создания отформатированного HTML для Word const createFormattedHtml = useCallback((htmlContent: string) => { // Создаем временный контейнер для обработки HTML @@ -284,22 +288,29 @@ export const FullscreenMessageModal: React.FC = ({ {messageContent && ( - - - + <> + {thinkContents.map((thinkContent, index) => ( + + ))} + {mainContent ? ( + + + + ) : null} + )} @@ -4,3 +4,4 @@ export * from './user-message' export * from './bot-message' export * from './preview-view' export * from './fullscreen-message-modal' +export * from './think-block' @@ -0,0 +1,48 @@ +.thinkBlock { + margin: 12px 0; + border-left: 2px solid #4a4a52; + padding-left: 12px; +} + +.summary { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; + list-style: none; + color: #a6a5a5; + font-size: 13px; + font-weight: 600; + line-height: 1.4; + font-family: Inter, sans-serif; + + &::-webkit-details-marker { + display: none; + } +} + +.summaryLabel { + letter-spacing: 0.2px; +} + +.chevron { + flex-shrink: 0; + transition: transform 0.2s ease; +} + +.thinkBlock[open] .chevron { + transform: rotate(180deg); +} + +.content { + margin-top: 8px; + color: #b0b0b8; + font-size: 14px; + line-height: 1.5; + font-family: Inter, sans-serif; + + > div { + margin-top: 0; + } +} @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react' + +import { Markdown } from '../../lib/markdown/markdown' + +import styles from './think-block.module.scss' + +interface ThinkBlockProps { + content: string + streaming?: boolean + defaultOpen?: boolean +} + +export function ThinkBlock({ content, streaming = false, defaultOpen = false }: ThinkBlockProps) { + const [isOpen, setIsOpen] = useState(defaultOpen || streaming) + + useEffect(() => { + if (streaming) { + setIsOpen(true) + } + }, [streaming]) + + if (!content) { + return null + } + + return ( +
{ + setIsOpen((event.target as HTMLDetailsElement).open) + }} + > + +
Размышления + + + + +
+ +
+ + ) +}