Compare commits

11 Commits
31 changed files with 117 additions and 771 deletions
+5 -7
View File
@@ -11,13 +11,11 @@ Docuservix docs — шаблон документационного сайта
## Commands
Используется **yarn**.
- `yarn start` — dev-сервер
- `yarn build` — production-сборка в `build/`
- `yarn typecheck` — проверка типов (tsc)
- `yarn prettier:check` — проверка форматирования
- `yarn prettier:fix` — автоформатирование
- `npm start` — dev-сервер
- `npm run build` — production-сборка в `build/`
- `npm run typecheck` — проверка типов (tsc)
- `npm run prettier:check` — проверка форматирования
- `npm run prettier:fix` — автоформатирование
## Architecture
-67
View File
@@ -1,70 +1,3 @@
# Добро пожаловать в Docuservix!
Вам надо настроить публикацию документации по инструкции в https://git.jt4d.ru/jt4d/docuservix
---
```mermaid
classDiagram
class IApi
class ProductService {
+IProduct[] getProducts
}
ProductService --> IApi
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}
```
Для современного мира высококачественный прототип будущего проекта предполагает независимые способы
реализации существующих финансовых и административных условий. Идейные соображения высшего порядка,
а также начало повседневной работы по формированию позиции обеспечивает широкому кругу
(специалистов) участие в формировании существующих финансовых и административных условий.
Безусловно, курс на социально-ориентированный национальный проект прекрасно подходит для реализации
глубокомысленных рассуждений. Каждый из нас понимает очевидную вещь: консультация с широким активом
играет определяющее значение для модели развития. Не следует, однако, забывать, что выбранный нами
инновационный путь создаёт предпосылки для форм воздействия.
Есть над чем задуматься: реплицированные с зарубежных источников, современные исследования призывают
нас к новым свершениям, которые, в свою очередь, должны быть подвергнуты целой серии независимых
исследований. Приятно, граждане, наблюдать, как многие известные личности будут обнародованы.
Следует отметить, что повышение уровня гражданского сознания играет определяющее значение для
экспериментов, поражающих по своей масштабности и грандиозности.
Безусловно, внедрение современных методик создаёт необходимость включения в производственный план
целого ряда внеочередных мероприятий с учётом комплекса позиций, занимаемых участниками в отношении
поставленных задач. Кстати, акционеры крупнейших компаний разоблачены. В своём стремлении улучшить
пользовательский опыт мы упускаем, что диаграммы связей, превозмогая сложившуюся непростую
экономическую ситуацию, разоблачены.
```ts
/**
* Очень важный класс
*/
export class Foo {
bar(value: number): number {
console.log('Bar: ' + value);
return value;
}
}
```
+1 -7
View File
@@ -35,7 +35,7 @@ const config: Config = {
markdown: {
mermaid: true,
},
plugins: [docuservix()],
plugins: [docuservix],
themes: ['@docusaurus/theme-mermaid'],
// Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future
@@ -127,12 +127,6 @@ const config: Config = {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
mermaid: {
theme: {
light: 'default',
dark: 'base',
},
},
} satisfies Preset.ThemeConfig,
};
+1 -3
View File
@@ -48,9 +48,7 @@
"js-yaml": "^4.2.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1"
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.10.1",
@@ -1,59 +0,0 @@
.MD p:last-child {
margin-bottom: 0;
}
.MD p:first-child {
margin-top: 0;
}
.MD code {
padding: 0.15em 0.4em;
border-radius: 4px;
background: var(--ifm-color-emphasis-200);
font-size: 0.85em;
}
.MD pre {
margin: 0.5em 0;
padding: 0.75em;
overflow-x: auto;
border-radius: 6px;
background: var(--ifm-color-emphasis-100);
}
.MD pre code {
padding: 0;
background: none;
}
.MD ul,
.MD ol {
padding-left: 1.5em;
margin: 0.5em 0;
}
.MD table {
width: 100%;
margin: 0.5em 0;
border-collapse: collapse;
font-size: 0.9em;
}
.MD th,
.MD td {
padding: 0.4em 0.75em;
border: 1px solid var(--ifm-color-emphasis-300);
text-align: left;
}
.MD th {
background: var(--ifm-color-emphasis-100);
font-weight: var(--ifm-font-weight-semibold);
}
.MD blockquote {
margin: 0.5em 0;
padding: 0.25em 1em;
border-left: 3px solid var(--ifm-color-emphasis-300);
color: var(--ifm-color-emphasis-700);
}
@@ -1,17 +0,0 @@
import React, { ReactNode } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import styles from './MD.module.css';
interface MDProps {
children: string;
}
export function MD({ children }: MDProps): ReactNode {
return (
<div className={styles.MD}>
<Markdown remarkPlugins={[remarkGfm]}>{children}</Markdown>
</div>
);
}
@@ -1 +0,0 @@
export { MD } from './MD';
-88
View File
@@ -1,88 +0,0 @@
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<IChatMessage[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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,
};
}
-119
View File
@@ -1,119 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useOptions } from '@docuservix/hooks/useOptions';
import { ChatStatus } from '@docuservix/models/chat';
interface UseChatStatusResult {
status: ChatStatus;
apiUrl: string;
recheck: () => void;
}
const POLL_INTERVAL_MS = 30_000;
const REQUEST_TIMEOUT_MS = 5_000;
function sameStatus(a: ChatStatus, b: ChatStatus): boolean {
if (a.kind !== b.kind) return false;
if (a.kind === 'server' && b.kind === 'server') return a.label === b.label;
return true;
}
export function useChatStatus(): UseChatStatusResult {
const apiUrl = useOptions().api ?? '';
const statusApiUrl = apiUrl + '/v1/status';
const [status, setStatus] = useState<ChatStatus>({ kind: 'connecting' });
const abortRef = useRef<AbortController | null>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const updateStatus = useCallback((next: ChatStatus): void => {
setStatus((prev) => (sameStatus(prev, next) ? prev : next));
}, []);
const check = useCallback(async (): Promise<void> => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
let timedOut = false;
const timeoutId = setTimeout(() => {
timedOut = true;
controller.abort();
}, REQUEST_TIMEOUT_MS);
try {
const res = await fetch(statusApiUrl, { signal: controller.signal });
if (!res.ok) {
updateStatus({ kind: 'offline' });
return;
}
const parsed = (await res.json()) as { status?: unknown } | null | undefined;
const raw = typeof parsed?.status === 'string' ? parsed.status : null;
if (raw === null || raw.length === 0) {
updateStatus({ kind: 'offline' });
return;
}
updateStatus({
kind: 'server',
label: raw.charAt(0).toUpperCase() + raw.slice(1),
});
} catch {
if (!controller.signal.aborted || timedOut) {
updateStatus({ kind: 'offline' });
}
} finally {
clearTimeout(timeoutId);
}
}, [statusApiUrl, updateStatus]);
const scheduleNext = useCallback((): void => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(async () => {
if (document.visibilityState === 'visible') {
await check();
}
scheduleNext();
}, POLL_INTERVAL_MS);
}, [check]);
const recheck = useCallback((): void => {
updateStatus({ kind: 'connecting' });
void check();
scheduleNext();
}, [check, scheduleNext, updateStatus]);
useEffect(() => {
void check();
scheduleNext();
const onVisibilityChange = (): void => {
if (document.visibilityState === 'visible') {
void check();
scheduleNext();
}
};
document.addEventListener('visibilitychange', onVisibilityChange);
return (): void => {
document.removeEventListener('visibilitychange', onVisibilityChange);
abortRef.current?.abort();
if (timerRef.current !== null) {
clearTimeout(timerRef.current);
}
};
}, [check, scheduleNext]);
return { status, apiUrl, recheck };
}
-7
View File
@@ -1,7 +0,0 @@
import { usePluginData } from '@docusaurus/useGlobalData';
import { DocuservixOptions } from '@docuservix/models/docuservix';
export function useOptions(): DocuservixOptions {
return usePluginData('docuservix') as DocuservixOptions;
}
+4 -12
View File
@@ -2,12 +2,7 @@ import path from 'path';
import type { LoadContext, Plugin } from '@docusaurus/types';
import { DocuservixOptions } from '@docuservix/models/docuservix';
export default function docuservix(options: Partial<DocuservixOptions> = {}) {
const api = process.env.DOCUSERVIX_API || options.api || '/api';
return function pluginDocuservix(_context: LoadContext): Plugin {
function pluginDocuservix(_context: LoadContext): Plugin {
return {
name: 'docuservix',
@@ -22,11 +17,7 @@ export default function docuservix(options: Partial<DocuservixOptions> = {}) {
},
async contentLoaded({ actions }) {
const { addRoute, setGlobalData } = actions;
setGlobalData({
api,
});
const { addRoute } = actions;
addRoute({
path: '/chat',
@@ -35,5 +26,6 @@ export default function docuservix(options: Partial<DocuservixOptions> = {}) {
});
},
};
};
}
export default pluginDocuservix;
-17
View File
@@ -1,25 +1,8 @@
export type ChatStatus =
| { kind: 'connecting' }
| { kind: 'offline' }
| { kind: 'server'; label: string };
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[];
}
export function sourceToUrl(file: string, anchor: string): string {
return `/${file}${anchor ? `#${anchor}` : ''}`;
}
-3
View File
@@ -1,3 +0,0 @@
export interface DocuservixOptions {
api?: string;
}
+15 -9
View File
@@ -1,21 +1,27 @@
import Layout from '@theme/Layout';
import { ReactNode } from 'react';
import { useChat } from '@docuservix/hooks/useChat';
import { IChat } from '@docuservix/models/chat';
import { Chat } from '@docuservix/widgets/chat';
export function ChatPage(): ReactNode {
const { dialog, typing, statusMessage, sendMessage } = useChat();
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 {
return (
<Layout title="Чат">
<main className="container margin-vert--lg">
<Chat
dialog={dialog}
typing={typing}
statusMessage={statusMessage}
onSend={sendMessage}
/>
<Chat dialog={dialog} />
</main>
</Layout>
);
@@ -7,18 +7,3 @@
border-radius: var(--ifm-global-radius);
overflow: hidden;
}
.Chat__statusMessage {
font-size: .65rem;
color: #666;
bottom: 0;
left: 0;
right: 0;
padding: .75rem 2.5rem;
background: #eee;
}
.Chat__statusMessage i {
margin-right: .25rem;
color: #667eea;
}
+5 -7
View File
@@ -1,7 +1,7 @@
import block from 'bem-css-modules';
import React, { ReactNode } from 'react';
import { IChat } from '@docuservix/models/chat';
import { IChat, IChatMessage } from '@docuservix/models/chat';
import styles from './Chat.module.css';
import { Header } from './Header';
@@ -12,12 +12,11 @@ const b = block(styles, 'Chat');
interface ChatProps {
dialog: IChat;
typing?: boolean;
statusMessage?: string;
loading?: boolean;
onSend?: (text: string) => void;
}
export function Chat({ dialog, typing, statusMessage, onSend }: ChatProps): ReactNode {
export function Chat({ dialog, loading, onSend }: ChatProps): ReactNode {
const { messages } = dialog;
return (
@@ -25,11 +24,10 @@ export function Chat({ dialog, typing, statusMessage, onSend }: ChatProps): Reac
<Header />
<Messages
messages={messages}
typing={typing}
loading={loading}
/>
{statusMessage && <div className={b('statusMessage')}>{statusMessage}</div>}
<Input
disabled={typing}
loading={loading}
onSend={onSend}
/>
</div>
@@ -24,3 +24,9 @@
color: var(--ifm-font-color-base);
font-size: 1.125rem;
}
.Header__info p {
margin: 0;
color: var(--ifm-color-emphasis-600);
font-size: 0.85rem;
}
+1 -2
View File
@@ -3,7 +3,6 @@ import React, { ReactNode } from 'react';
import styles from './Header.module.css';
import { RobotIcon } from './icons';
import { Status } from './Status';
const b = block(styles, 'Header');
@@ -15,7 +14,7 @@ export function Header(): ReactNode {
</div>
<div className={b('info')}>
<h3>AI Assistant</h3>
<Status />
<p>Ready to help</p>
</div>
</div>
);
@@ -33,9 +33,6 @@
}
.Input__send {
display: flex;
justify-content: center;
align-items: center;
width: 3.25rem;
height: 3.25rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+5 -5
View File
@@ -7,17 +7,17 @@ import styles from './Input.module.css';
const b = block(styles, 'Input');
interface InputProps {
disabled?: boolean;
loading?: boolean;
onSend?: (text: string) => void;
}
export function Input({ disabled, onSend }: InputProps): ReactNode {
export function Input({ loading, onSend }: InputProps): ReactNode {
const [input, setInput] = useState('');
const handleSend = () => {
const text = input.trim();
if (!text || disabled) {
if (!text || loading) {
return;
}
@@ -41,12 +41,12 @@ export function Input({ disabled, onSend }: InputProps): ReactNode {
onKeyDown={handleKeyDown}
placeholder="Type your message here..."
rows={1}
disabled={disabled}
disabled={loading}
/>
<button
className={b('send')}
onClick={handleSend}
disabled={disabled || !input.trim()}
disabled={loading || !input.trim()}
>
<PaperPlaneIcon />
</button>
@@ -1,5 +1,5 @@
.Message {
max-width: 90%;
max-width: 85%;
animation: slideIn 0.3s ease-out;
}
@@ -34,37 +34,8 @@
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) {
@media (min-width: 576px) {
.Message {
max-width: 100%;
max-width: 75%;
}
}
+2 -25
View File
@@ -1,11 +1,6 @@
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');
@@ -13,30 +8,12 @@ const b = block(styles, 'Message');
interface MessageProps {
role: 'user' | 'assistant';
content: string;
sources?: IChatSource[];
}
export function Message({ role, content, sources }: MessageProps): ReactNode {
export function Message({ role, content }: MessageProps): ReactNode {
return (
<div className={b({ role })}>
<div className={b('content')}>
<MD>{content}</MD>
</div>
{sources && sources.length > 0 && (
<div className={b('sources')}>
<div className={b('sourcesLabel')}>Источники:</div>
{sources.map((src, j) => (
<Link
key={j}
to={sourceToUrl(src.file, src.anchor)}
className={b('sourceLink')}
>
{src.heading}
</Link>
))}
</div>
)}
<div className={b('content')}>{content}</div>
</div>
);
}
+3 -4
View File
@@ -10,10 +10,10 @@ const b = block(styles, 'Messages');
interface MessagesProps {
messages: IChatMessage[];
typing?: boolean;
loading?: boolean;
}
export function Messages({ messages, typing }: MessagesProps): ReactNode {
export function Messages({ messages, loading }: MessagesProps): ReactNode {
return (
<div className={b()}>
{messages.map((msg, i) => (
@@ -21,11 +21,10 @@ export function Messages({ messages, typing }: MessagesProps): ReactNode {
key={i}
role={msg.role}
content={msg.content}
sources={msg.sources}
/>
))}
{typing && (
{loading && (
<div className={b('typing')}>
<div className={b('typingIndicator')}>
<span></span>
@@ -1,53 +0,0 @@
.Status {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0;
background: none;
border: none;
cursor: pointer;
color: var(--ifm-color-emphasis-600);
font-size: 0.85rem;
line-height: 1;
}
.Status:hover .Status__label {
color: var(--ifm-color-emphasis-800);
}
.Status:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 2px;
border-radius: 4px;
}
.Status__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--ifm-color-emphasis-400);
flex-shrink: 0;
}
.Status_kind_server .Status__dot {
background: #22c55e;
}
.Status_kind_offline .Status__dot {
background: #ef4444;
}
.Status_kind_connecting .Status__dot {
background: #eab308;
animation: Status__pulse 1s ease-in-out infinite;
}
@keyframes Status__pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
@@ -1,38 +0,0 @@
import block from 'bem-css-modules';
import React, { ReactNode } from 'react';
import { useChatStatus } from '@docuservix/hooks/useChatStatus';
import { ChatStatus } from '@docuservix/models/chat';
import styles from './Status.module.css';
const b = block(styles, 'Status');
function getLabel(status: ChatStatus): string {
switch (status.kind) {
case 'connecting':
return 'Подключение…';
case 'offline':
return 'Offline';
case 'server':
return status.label;
}
}
export function Status(): ReactNode {
const { status, apiUrl, recheck } = useChatStatus();
const label = getLabel(status);
return (
<button
type="button"
className={b({ kind: status.kind })}
onClick={recheck}
title={apiUrl}
aria-label={`Статус: ${label}. Нажмите для проверки`}
>
<span className={b('dot')} />
<span className={b('label')}>{label}</span>
</button>
);
}
+13 -12
View File
@@ -1,30 +1,31 @@
import React, { ReactNode } from 'react';
import React from 'react';
export function RobotIcon(): ReactNode {
export function RobotIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 640 512"
fill="currentColor"
viewBox="0 0 16 16"
width="1em"
height="1em"
{...props}
>
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135" />
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5" />
<path d="M320 0c17.7 0 32 14.3 32 32v64h120c48.6 0 88 39.4 88 88v272c0 48.6-39.4 88-88 88H168c-48.6 0-88-39.4-88-88V184c0-48.6 39.4-88 88-88h120V32c0-17.7 14.3-32 32-32zM208 384c-8.8 0-16 7.2-16 16s7.2 16 16 16h224c8.8 0 16-7.2 16-16s-7.2-16-16-16H208zm-48-96a32 32 0 1 0 0-64 32 32 0 1 0 0 64zm288-32a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zM64 224c-17.7 0-32 14.3-32 32v64c0 17.7 14.3 32 32 32s32-14.3 32-32V256c0-17.7-14.3-32-32-32zm512 0c-17.7 0-32 14.3-32 32v64c0 17.7 14.3 32 32 32s32-14.3 32-32V256c0-17.7-14.3-32-32-32z" />
</svg>
);
}
export function PaperPlaneIcon(): ReactNode {
export function PaperPlaneIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 512 512"
fill="currentColor"
viewBox="0 0 16 16"
width="1em"
height="1em"
{...props}
>
<path d="M15.964.686a.5.5 0 0 0-.65-.65L.767 5.855H.766l-.452.18a.5.5 0 0 0-.082.887l.41.26.001.002 4.995 3.178 3.178 4.995.002.002.26.41a.5.5 0 0 0 .886-.083zm-1.833 1.89L6.637 10.07l-.215-.338a.5.5 0 0 0-.154-.154l-.338-.215 7.494-7.494 1.178-.471z" />
<path d="M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 googl492.3 160 480v-83.6c0-4 1.5-7.8 4.2-10.7l167.6-182.8c5.8-6.3 5.4-16.1-.9-21.9s-16.1-5.4-21.9 .9L106.2 350.8 13.7 313.4C3.2 309 -2 297.3 .5 286.4s11.3-18.9 22.4-21L490.5 .6c10.1-2 20.6 1.7 27.6 8.7L498.1 5.6z" />
</svg>
);
}
+9 -30
View File
@@ -17,35 +17,14 @@
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* Nord dark theme — https://www.nordtheme.com */
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
/* Frost — основной акцент (nord8) */
--ifm-color-primary: #88c0d0;
--ifm-color-primary-dark: #76b5c7;
--ifm-color-primary-darker: #6dafc3;
--ifm-color-primary-darkest: #5e81ac;
--ifm-color-primary-light: #95c8d8;
--ifm-color-primary-lighter: #9ecfdd;
--ifm-color-primary-lightest: #b4dce8;
/* Polar Night — фоны */
--ifm-background-color: #2e3440;
--ifm-background-surface-color: #3b4252;
--ifm-navbar-background-color: #2e3440;
--ifm-footer-background-color: #2e3440;
/* Snow Storm — текст */
--ifm-font-color-base: #eceff4;
--ifm-font-color-secondary: #d8dee9;
/* Emphasis scale: Polar Night → Snow Storm */
--ifm-color-emphasis-100: #3b4252;
--ifm-color-emphasis-200: #434c5e;
--ifm-color-emphasis-300: #4c566a;
--ifm-color-emphasis-400: #7b8ba0;
--ifm-color-emphasis-500: #d8dee9;
--ifm-color-emphasis-600: #e5e9f0;
--ifm-color-emphasis-700: #eceff4;
--docusaurus-highlighted-code-line-bg: rgba(136, 192, 208, 0.15);
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
+26
View File
@@ -1,12 +1,26 @@
import Link from '@docusaurus/Link';
import { useHistory } from '@docusaurus/router';
import useBaseUrl from '@docusaurus/useBaseUrl';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Heading from '@theme/Heading';
import Layout from '@theme/Layout';
import clsx from 'clsx';
import type { ReactNode } from 'react';
import { IChat } from '@docuservix/models/chat';
import { Chat } from '@docuservix/widgets/chat';
import styles from './index.module.css';
const startDialog: IChat = {
messages: [
{
role: 'assistant',
content: 'Чем могу помочь?',
},
],
};
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
@@ -35,6 +49,12 @@ function HomepageHeader() {
export default function Home(): ReactNode {
const { siteConfig } = useDocusaurusContext();
const history = useHistory();
const chatUrl = useBaseUrl('/chat');
const handleSend = (text: string) => {
history.push(`${chatUrl}?q=${encodeURIComponent(text)}`);
};
return (
<Layout
@@ -42,6 +62,12 @@ export default function Home(): ReactNode {
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
<main className="container margin-vert--lg">
<Chat
dialog={startDialog}
onSend={handleSend}
/>
</main>
</Layout>
);
}
-82
View File
@@ -1,82 +0,0 @@
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import { ErrorBoundaryErrorMessageFallback } from '@docusaurus/theme-common';
import { useColorMode } from '@docusaurus/theme-common';
import {
MermaidContainerClassName,
useMermaidConfig,
useMermaidRenderResult,
} from '@docusaurus/theme-mermaid/client';
import type { Props } from '@theme/Mermaid';
import type { RenderResult } from 'mermaid';
import React, { useRef, useEffect, useMemo, type ReactNode } from 'react';
import styles from './styles.module.css';
const NORD_THEME_VARIABLES = {
background: '#2e3440',
mainBkg: '#3b4252',
primaryColor: '#5e81ac',
primaryTextColor: '#eceff4',
primaryBorderColor: '#88c0d0',
secondaryColor: '#434c5e',
secondaryTextColor: '#d8dee9',
secondaryBorderColor: '#4c566a',
tertiaryColor: '#3b4252',
tertiaryTextColor: '#d8dee9',
tertiaryBorderColor: '#4c566a',
lineColor: '#88c0d0',
edgeLabelBackground: '#434c5e',
textColor: '#eceff4',
titleColor: '#88c0d0',
nodeBorder: '#4c566a',
clusterBkg: '#434c5e',
clusterBorder: '#4c566a',
};
function MermaidRenderResult({ renderResult }: { renderResult: RenderResult }): ReactNode {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const div = ref.current!;
renderResult.bindFunctions?.(div);
}, [renderResult]);
return (
<div
ref={ref}
className={`${MermaidContainerClassName} ${styles.container}`}
// eslint-disable-next-line react/no-danger -- mermaid renders trusted SVG
dangerouslySetInnerHTML={{ __html: renderResult.svg }}
/>
);
}
function MermaidRenderer({ value }: Props): ReactNode {
const { colorMode } = useColorMode();
const baseConfig = useMermaidConfig();
const config = useMemo(
() =>
colorMode === 'dark'
? { ...baseConfig, themeVariables: NORD_THEME_VARIABLES }
: baseConfig,
[colorMode, baseConfig],
);
const renderResult = useMermaidRenderResult({ text: value, config });
if (renderResult === null) {
return null;
}
return <MermaidRenderResult renderResult={renderResult} />;
}
export default function Mermaid(props: Props): ReactNode {
return (
<ErrorBoundary fallback={(params) => <ErrorBoundaryErrorMessageFallback {...params} />}>
<MermaidRenderer {...props} />
</ErrorBoundary>
);
}
-7
View File
@@ -1,7 +0,0 @@
.container {
max-width: 100%;
}
.container > svg {
max-width: 100%;
}
+1 -23
View File
@@ -7308,11 +7308,6 @@ html-tags@^3.3.1:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
html-url-attributes@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87"
integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==
html-void-elements@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
@@ -10561,23 +10556,6 @@ react-loadable-ssr-addon-v5-slorber@^1.0.3:
dependencies:
"@types/react" "*"
react-markdown@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-10.1.0.tgz#e22bc20faddbc07605c15284255653c0f3bad5ca"
integrity sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==
dependencies:
"@types/hast" "^3.0.0"
"@types/mdast" "^4.0.0"
devlop "^1.0.0"
hast-util-to-jsx-runtime "^2.0.0"
html-url-attributes "^3.0.0"
mdast-util-to-hast "^13.0.0"
remark-parse "^11.0.0"
remark-rehype "^11.0.0"
unified "^11.0.0"
unist-util-visit "^5.0.0"
vfile "^6.0.0"
react-router-config@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
@@ -10831,7 +10809,7 @@ remark-frontmatter@^5.0.0:
micromark-extension-frontmatter "^2.0.0"
unified "^11.0.0"
remark-gfm@^4.0.0, remark-gfm@^4.0.1:
remark-gfm@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b"
integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==