From f5181ef8a02c18719a274ab1837021da495751ff Mon Sep 17 00:00:00 2001 From: Arswarog Date: Tue, 16 Jun 2026 13:58:03 +0300 Subject: [PATCH] COPY docuservix --- docusaurus.config.ts | 10 + plugins/docuservix-search/index.ts | 46 ++++ .../theme/ChatPage/index.tsx | 210 +++++++++++++++ .../theme/ChatPage/styles.module.css | 203 +++++++++++++++ .../theme/SearchBar/index.tsx | 215 ++++++++++++++++ .../theme/SearchBar/styles.module.css | 150 +++++++++++ .../theme/SearchPage/index.tsx | 243 ++++++++++++++++++ .../theme/SearchPage/styles.module.css | 170 ++++++++++++ plugins/docuservix-search/types.ts | 26 ++ src/search-providers.ts | 77 ++++++ 10 files changed, 1350 insertions(+) create mode 100644 plugins/docuservix-search/index.ts create mode 100644 plugins/docuservix-search/theme/ChatPage/index.tsx create mode 100644 plugins/docuservix-search/theme/ChatPage/styles.module.css create mode 100644 plugins/docuservix-search/theme/SearchBar/index.tsx create mode 100644 plugins/docuservix-search/theme/SearchBar/styles.module.css create mode 100644 plugins/docuservix-search/theme/SearchPage/index.tsx create mode 100644 plugins/docuservix-search/theme/SearchPage/styles.module.css create mode 100644 plugins/docuservix-search/types.ts create mode 100644 src/search-providers.ts diff --git a/docusaurus.config.ts b/docusaurus.config.ts index d8a76c9..fb1ed37 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -89,6 +89,16 @@ const config: Config = { ], ], + themes: ['@docusaurus/theme-mermaid'], + + plugins: [ + [ + path.resolve(__dirname, './plugins/docuservix-search/index.ts'), + { + providersModule: require.resolve('./src/search-providers'), + }, + ], + ], themeConfig: { // Replace with your project's social card image: 'img/docusaurus-social-card.jpg', diff --git a/plugins/docuservix-search/index.ts b/plugins/docuservix-search/index.ts new file mode 100644 index 0000000..0b3ada7 --- /dev/null +++ b/plugins/docuservix-search/index.ts @@ -0,0 +1,46 @@ +import path from 'path'; +import type { LoadContext, Plugin } from '@docusaurus/types'; + +interface SearchPluginOptions { + providersModule: string; +} + +export default function docuservixSearchPlugin( + _ctx: LoadContext, + options: SearchPluginOptions, +): Plugin { + return { + name: 'docuservix-search', + + getThemePath() { + return path.resolve(__dirname, './theme'); + }, + + getTypeScriptThemePath() { + return path.resolve(__dirname, './theme'); + }, + + configureWebpack() { + return { + resolve: { + alias: { + '@docuservix-search/config': options.providersModule, + }, + }, + }; + }, + + async contentLoaded({ actions }) { + actions.addRoute({ + path: '/search', + component: '@theme/SearchPage', + exact: true, + }); + actions.addRoute({ + path: '/chat', + component: '@theme/ChatPage', + exact: true, + }); + }, + }; +} diff --git a/plugins/docuservix-search/theme/ChatPage/index.tsx b/plugins/docuservix-search/theme/ChatPage/index.tsx new file mode 100644 index 0000000..bffb6b6 --- /dev/null +++ b/plugins/docuservix-search/theme/ChatPage/index.tsx @@ -0,0 +1,210 @@ +import { useLocation } from '@docusaurus/router'; +import Link from '@docusaurus/Link'; +import Layout from '@theme/Layout'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +import { chatUrl } from '@docuservix-search/config'; + +import styles from './styles.module.css'; + +interface Source { + file: string; + heading: string; + anchor: string; + score: number; +} + +interface Message { + role: 'user' | 'assistant'; + content: string; + sources?: Source[]; +} + +function stripNumericPrefixes(p: string): string { + return p + .split('/') + .map((seg) => seg.replace(/^\d+-/, '')) + .join('/'); +} + +function sourceToUrl(file: string, anchor: string): string { + let p = file.replace(/^docs\//, '').replace(/\.md$/, ''); + + p = stripNumericPrefixes(p); + + return `/docs/${p}${anchor ? `#${anchor}` : ''}`; +} + +function sourceToPath(file: string): string { + const p = file.replace(/^docs\//, '').replace(/\.md$/, ''); + + return stripNumericPrefixes(p); +} + +function useQuery(): string { + const location = useLocation(); + const params = new URLSearchParams(location.search); + + return params.get('q') ?? ''; +} + +export default function ChatPage(): JSX.Element { + const urlQuery = useQuery(); + + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const messagesEndRef = useRef(null); + const initialSentRef = useRef(false); + + const sendMessage = useCallback(async (content: string, history: Message[]) => { + if (!content.trim()) return; + + const userMessage: Message = { role: 'user', content }; + const newHistory = [...history, userMessage]; + + setMessages(newHistory); + setInput(''); + setLoading(true); + setError(null); + + try { + const res = await fetch(chatUrl, { + 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?: Source[] } = 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); + } + }, []); + + useEffect(() => { + if (urlQuery && !initialSentRef.current) { + initialSentRef.current = true; + sendMessage(urlQuery, []); + } + }, [urlQuery, sendMessage]); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, loading]); + + const handleSend = () => { + if (!loading && input.trim()) { + sendMessage(input, messages); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + return ( + +
+ {urlQuery && ( +
+ + ← Назад к поиску + +
+ )} + +
+ {messages.length === 0 && !loading && ( +
Задайте вопрос...
+ )} + + {messages.map((msg, i) => ( +
+
+
{msg.content}
+ + {msg.sources && msg.sources.length > 0 && ( +
+
Источники:
+ {msg.sources.map((src, j) => ( + + {src.heading || sourceToPath(src.file)} + + ))} +
+ )} +
+
+ ))} + + {loading && ( +
+
+ + + +
+
+ )} + + {error && ( +
+
{error}
+
+ )} + +
+
+ +
+