@@ -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), + } +} @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { useSession } from 'next-auth/react' import { useAppDispatch } from '#/app/store/store' @@ -264,6 +264,7 @@ export function useChatModel( const { data } = useSession() const dispatch = useAppDispatch() const [messages, setMessages] = useState(null) + const [loadedChatUid, setLoadedChatUid] = useState(null) const [loading, setLoading] = useState(false) const [paginationLoading, setPaginationLoading] = useState(false) const [offset, setOffset] = useState(0) @@ -597,6 +598,10 @@ export function useChatModel( } }, []) + useLayoutEffect(() => { + setLoadedChatUid(null) + }, [currentChat]) + useEffect(() => { abortRef.current?.abort() abortRef.current = null @@ -606,6 +611,7 @@ export function useChatModel( if (!currentChat) { setMessages([]) + setLoadedChatUid(null) setOffset(0) reconnectAttemptedForChatRef.current = null return @@ -629,11 +635,13 @@ export function useChatModel( if (!Array.isArray(answer)) { showMessage('Ошибка загрузки чата') setMessages([]) + setLoadedChatUid(currentChat) return } const loadedMessages = answer.reverse() setMessages(loadedMessages) + setLoadedChatUid(currentChat) setOffset(answer.length) if (streamingRef.current && data?.access) { @@ -919,5 +927,7 @@ export function useChatModel( [currentChat, data?.access] ) - return { messages, sendMessage, loading, paginationLoading, getMessagesPagination, deleteMessage } + const displayMessages = loadedChatUid === currentChat ? messages : null + + return { messages: displayMessages, sendMessage, loading, paginationLoading, getMessagesPagination, deleteMessage } } @@ -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) + }} + > + +
Размышления + + + + +
+ +
+ + ) +} @@ -11,7 +11,7 @@ import { setParams as setParametres } from '#/app/store/model-parametres-store' import { useAppSelector } from '#/app/store/store' import { IModel } from '#/entities/model-entity' import BotParamsMap from '#/features/bot-params/bot-params-map' -import { selectCurrentChat } from '#/features/chats/chats-slice' +import { selectCurrentChat, resetChatsState, selectChatsLoading } from '#/features/chats/chats-slice' import { usePredictPrice } from '#/features/predict-price/model/use-predict-price' import Title from '#/features/title/title' import { TutorialContext } from '#/features/tutorial-context/tutorial-context' @@ -52,7 +52,9 @@ const Page: NextPageWithLayout = () => { const router = useRouter() const { push } = router + const routeSlug = typeof router.query.slug === 'string' ? router.query.slug : '' const currentChat = useAppSelector(selectCurrentChat) + const chatsLoading = useAppSelector(selectChatsLoading) const { messages, sendMessage, loading, paginationLoading, getMessagesPagination, deleteMessage } = useChatModel( currentChat, @@ -70,31 +72,52 @@ const Page: NextPageWithLayout = () => { ? '75vh' : `calc(100dvh - ${(botParams?.tags ?? []).length === 0 ? '200px' : '285px'})` - const isMessagesLoading = messages === null + const isBotReady = botParams !== null && botParams.slug === routeSlug && modelType === routeSlug + const isContentLoading = !isBotReady || chatsLoading || messages === null + + React.useLayoutEffect(() => { + setBotParams(null) + setModelType('') + setVersion('') + setFile(null) + setInputContent('') + dispatch(resetChatsState()) + }, [routeSlug, dispatch]) React.useEffect(() => { - if (data?.access) { - model_api.getBotParams(router.asPath.split('/')[2], data.access).then((res) => { - if (!res.title) return push('/404') - setBotParams(res) - setModelType(res.slug) - if (res.versions.length !== 0) { - setVersion(res.versions[0].slug) - dispatch( - setParametres( - res.parameters.reduce( - (a, v) => (v.versions.includes(res.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), - {} - ) + if (!data?.access || !routeSlug) { + return + } + + let cancelled = false + + model_api.getBotParams(routeSlug, data.access).then((res) => { + if (cancelled || res.slug !== routeSlug) { + return + } + if (!res.title) return push('/404') + setBotParams(res) + setModelType(res.slug) + if (res.versions.length !== 0) { + setVersion(res.versions[0].slug) + dispatch( + setParametres( + res.parameters.reduce( + (a, v) => (v.versions.includes(res.versions[0].slug) ? { ...a, [v.key]: v.values.default } : { ...a }), + {} ) ) - } else { - setVersion('') - dispatch(setParametres(res.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) - } - }) + ) + } else { + setVersion('') + dispatch(setParametres(res.parameters.reduce((a, v) => ({ ...a, [v.key]: v.values.default }), {}))) + } + }) + + return () => { + cancelled = true } - }, [data?.access, dispatch, push, router.asPath, router.query]) + }, [data?.access, dispatch, push, routeSlug]) const resetParams = () => { if (botParams) { @@ -269,7 +292,7 @@ const Page: NextPageWithLayout = () => { - {isMessagesLoading ? ( + {isContentLoading ? (