From 33236cf6a9d4dc4564a8d9d35bb5f1f3b136eb71 Mon Sep 17 00:00:00 2001 From: Arswarog Date: Wed, 8 Jul 2026 11:11:53 +0300 Subject: [PATCH 1/2] =?UTF-8?q?chat:=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=BA=D0=B5=D0=BD=D0=B4=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/docuservix/hooks/useChat.ts | 88 ++++++++++++++++++++++ plugins/docuservix/hooks/useOptions.ts | 7 ++ plugins/docuservix/index.ts | 12 ++- plugins/docuservix/models/chat.ts | 29 +++++++ plugins/docuservix/models/docuservix.ts | 3 + plugins/docuservix/pages/chat/ChatPage.tsx | 22 ++---- 6 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 plugins/docuservix/hooks/useChat.ts create mode 100644 plugins/docuservix/hooks/useOptions.ts create mode 100644 plugins/docuservix/models/docuservix.ts diff --git a/plugins/docuservix/hooks/useChat.ts b/plugins/docuservix/hooks/useChat.ts new file mode 100644 index 0000000..191654d --- /dev/null +++ b/plugins/docuservix/hooks/useChat.ts @@ -0,0 +1,88 @@ +import { useLocation } from '@docusaurus/router'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useOptions } from '@docuservix/hooks/useOptions'; +import { IChat, IChatMessage, IChatSource } from '@docuservix/models/chat'; + +interface UseChatResult { + dialog: IChat; + typing: boolean; + statusMessage?: string; + sendMessage: (text: string) => void; +} + +function useQuery(): string { + const location = useLocation(); + const params = new URLSearchParams(location.search); + + return params.get('q') ?? ''; +} + +export function useChat(): UseChatResult { + const chatEndpoint = useOptions().api + '/v1/chat'; + const urlQuery = useQuery(); + + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const initialSentRef = useRef(false); + const messagesEndRef = useRef(messages); + + messagesEndRef.current = messages; + + const sendMessage = useCallback( + async (text: string) => { + const content = text.trim(); + + if (!content) { + return; + } + + const userMessage: IChatMessage = { role: 'user', content }; + const newHistory = [...messagesEndRef.current, userMessage]; + + setMessages(newHistory); + setLoading(true); + setError(null); + + try { + const res = await fetch(chatEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages: newHistory }), + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } + + const data: { answer: string; sources?: IChatSource[] } = await res.json(); + + setMessages((prev) => [ + ...prev, + { role: 'assistant', content: data.answer, sources: data.sources }, + ]); + } catch (err) { + setError(err instanceof Error ? err.message : 'Ошибка при обращении к серверу'); + } finally { + setLoading(false); + } + }, + [chatEndpoint], + ); + + useEffect(() => { + if (urlQuery && !initialSentRef.current) { + initialSentRef.current = true; + sendMessage(urlQuery); + } + }, [urlQuery, sendMessage]); + + return { + dialog: { messages }, + typing: loading, + statusMessage: error ?? undefined, + sendMessage, + }; +} diff --git a/plugins/docuservix/hooks/useOptions.ts b/plugins/docuservix/hooks/useOptions.ts new file mode 100644 index 0000000..50c9cee --- /dev/null +++ b/plugins/docuservix/hooks/useOptions.ts @@ -0,0 +1,7 @@ +import { usePluginData } from '@docusaurus/useGlobalData'; + +import { DocuservixOptions } from '@docuservix/models/docuservix'; + +export function useOptions(): DocuservixOptions { + return usePluginData('docuservix') as DocuservixOptions; +} diff --git a/plugins/docuservix/index.ts b/plugins/docuservix/index.ts index 50dbf9c..d9eedbf 100644 --- a/plugins/docuservix/index.ts +++ b/plugins/docuservix/index.ts @@ -2,7 +2,11 @@ import path from 'path'; import type { LoadContext, Plugin } from '@docusaurus/types'; -export default function docuservix() { +import { DocuservixOptions } from '@docuservix/models/docuservix'; + +export default function docuservix(options: Partial = {}) { + const api = process.env.DOCUSERVIX_API || options.api || '/api'; + return function pluginDocuservix(_context: LoadContext): Plugin { return { name: 'docuservix', @@ -18,7 +22,11 @@ export default function docuservix() { }, async contentLoaded({ actions }) { - const { addRoute } = actions; + const { addRoute, setGlobalData } = actions; + + setGlobalData({ + api, + }); addRoute({ path: '/chat', diff --git a/plugins/docuservix/models/chat.ts b/plugins/docuservix/models/chat.ts index bdc8330..e1d7afd 100644 --- a/plugins/docuservix/models/chat.ts +++ b/plugins/docuservix/models/chat.ts @@ -2,7 +2,36 @@ export interface IChat { messages: IChatMessage[]; } +export interface IChatSource { + file: string; + heading: string; + anchor: string; + score: number; +} + export interface IChatMessage { role: 'user' | 'assistant'; content: string; + sources?: IChatSource[]; +} + +function stripNumericPrefixes(p: string): string { + return p + .split('/') + .map((seg) => seg.replace(/^\d+-/, '')) + .join('/'); +} + +export function sourceToUrl(file: string, anchor: string): string { + let p = file.replace(/^docs\//, '').replace(/\.md$/, ''); + + p = stripNumericPrefixes(p); + + return `/docs/${p}${anchor ? `#${anchor}` : ''}`; +} + +export function sourceToPath(file: string): string { + const p = file.replace(/^docs\//, '').replace(/\.md$/, ''); + + return stripNumericPrefixes(p); } diff --git a/plugins/docuservix/models/docuservix.ts b/plugins/docuservix/models/docuservix.ts new file mode 100644 index 0000000..040b9b3 --- /dev/null +++ b/plugins/docuservix/models/docuservix.ts @@ -0,0 +1,3 @@ +export interface DocuservixOptions { + api?: string; +} diff --git a/plugins/docuservix/pages/chat/ChatPage.tsx b/plugins/docuservix/pages/chat/ChatPage.tsx index 8783705..df6a316 100644 --- a/plugins/docuservix/pages/chat/ChatPage.tsx +++ b/plugins/docuservix/pages/chat/ChatPage.tsx @@ -1,30 +1,20 @@ import Layout from '@theme/Layout'; import { ReactNode } from 'react'; -import { IChat } from '@docuservix/models/chat'; +import { useChat } from '@docuservix/hooks/useChat'; import { Chat } from '@docuservix/widgets/chat'; -const dialog: IChat = { - messages: [ - { - role: 'user', - content: 'Can you show me some CSS animations? It can be simple tools like chatbots...', - }, - { - role: 'assistant', - content: "Hello! I'm your **AI assistant**. How can I help you today?", - }, - ], -}; - export function ChatPage(): ReactNode { + const { dialog, typing, statusMessage, sendMessage } = useChat(); + return (
-- 2.54.0 From 8ff4ba6c94f35574fc36320c6a702c3c477ebd8d Mon Sep 17 00:00:00 2001 From: Arswarog Date: Wed, 8 Jul 2026 11:12:26 +0300 Subject: [PATCH 2/2] =?UTF-8?q?chat/widget:=20=D0=BE=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/chat/Message.module.css | 29 +++++++++++++++++++ plugins/docuservix/widgets/chat/Message.tsx | 21 +++++++++++++- plugins/docuservix/widgets/chat/Messages.tsx | 1 + 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/plugins/docuservix/widgets/chat/Message.module.css b/plugins/docuservix/widgets/chat/Message.module.css index 4e3d362..0484719 100644 --- a/plugins/docuservix/widgets/chat/Message.module.css +++ b/plugins/docuservix/widgets/chat/Message.module.css @@ -34,6 +34,35 @@ border-bottom-right-radius: 0.25rem; } +.Message__sources { + margin-top: 8px; + padding: 8px 1rem 0; + border-top: 1px solid var(--ifm-color-emphasis-200); +} + +.Message__sourcesLabel { + margin-bottom: 4px; + color: var(--ifm-color-emphasis-600); + font-weight: var(--ifm-font-weight-semibold); + font-size: 0.75rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.Message__sourceLink { + display: block; + overflow: hidden; + color: var(--ifm-color-primary); + font-size: 0.8rem; + white-space: nowrap; + text-decoration: none; + text-overflow: ellipsis; +} + +.Message__sourceLink:hover { + text-decoration: underline; +} + @media (max-width: 576px) { .Message { max-width: 100%; diff --git a/plugins/docuservix/widgets/chat/Message.tsx b/plugins/docuservix/widgets/chat/Message.tsx index a09498f..fa120bd 100644 --- a/plugins/docuservix/widgets/chat/Message.tsx +++ b/plugins/docuservix/widgets/chat/Message.tsx @@ -1,8 +1,11 @@ +import Link from '@docusaurus/Link'; import block from 'bem-css-modules'; import React, { ReactNode } from 'react'; import { MD } from '@docuservix/entities/markdown'; +import { IChatSource, sourceToPath, sourceToUrl } from '@docuservix/models/chat'; + import styles from './Message.module.css'; const b = block(styles, 'Message'); @@ -10,14 +13,30 @@ const b = block(styles, 'Message'); interface MessageProps { role: 'user' | 'assistant'; content: string; + sources?: IChatSource[]; } -export function Message({ role, content }: MessageProps): ReactNode { +export function Message({ role, content, sources }: MessageProps): ReactNode { return (
{content}
+ + {sources && sources.length > 0 && ( +
+
Источники:
+ {sources.map((src, j) => ( + + {src.heading || sourceToPath(src.file)} + + ))} +
+ )}
); } diff --git a/plugins/docuservix/widgets/chat/Messages.tsx b/plugins/docuservix/widgets/chat/Messages.tsx index 776b728..878332f 100644 --- a/plugins/docuservix/widgets/chat/Messages.tsx +++ b/plugins/docuservix/widgets/chat/Messages.tsx @@ -21,6 +21,7 @@ export function Messages({ messages, typing }: MessagesProps): ReactNode { key={i} role={msg.role} content={msg.content} + sources={msg.sources} /> ))} -- 2.54.0