From 253acdcce9444cbd8e74abdbe041aa148230a1d4 Mon Sep 17 00:00:00 2001 From: itwillwork Date: Tue, 14 Jul 2026 01:23:03 +0300 Subject: [PATCH 01/11] add generate --- src/pages/api/generate-code.ts | 62 ++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/pages/api/generate-code.ts diff --git a/src/pages/api/generate-code.ts b/src/pages/api/generate-code.ts new file mode 100644 index 000000000000..55a4f917a547 --- /dev/null +++ b/src/pages/api/generate-code.ts @@ -0,0 +1,62 @@ +import type {NextApiRequest, NextApiResponse} from 'next'; +import OpenAI from 'openai'; + +type GenerateCodeRequestBody = { + input: string; +}; + +const DEFAULT_BASE_URL = 'https://ai.api.cloud.yandex.net/v1'; +const API_KEY = process.env.OPENAI_API_KEY; +const BASE_URL = process.env.BASE_URL || DEFAULT_BASE_URL; +const PROJECT_ID = process.env.YANDEX_PROJECT_ID; +const PROMPT_ID = process.env.GENERATE_CODE_PROMPT_ID; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({error: 'Method not allowed'}); + } + + try { + const {input} = req.body as GenerateCodeRequestBody; + + if (!input || typeof input !== 'string' || input.trim().length === 0) { + return res.status(400).json({error: 'Input is required'}); + } + + const openai = new OpenAI({ + apiKey: API_KEY, + baseURL: BASE_URL, + defaultHeaders: PROJECT_ID ? {'OpenAI-Project': PROJECT_ID} : undefined, + }); + + const response = await openai.responses.create({ + prompt: PROMPT_ID ? {id: PROMPT_ID} : undefined, + input, + tools: [ + { + type: 'web_search', + filters: { + allowed_domains: [ + 'https://gravity-ui.com/ru/components/', + 'https://github.com/gravity-ui/uikit/', + ], + }, + search_context_size: 'medium', + user_location: { + type: 'approximate', + region: '225', + }, + }, + ], + }); + + return res.status(200).json({output: response.output_text}); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error in generate-code API:', error); + + return res.status(500).json({ + error: error instanceof Error ? error.message : 'Unknown error', + }); + } +} From eb21bcdd4ba9b83cf4e04f2d6aa74cdebf857833 Mon Sep 17 00:00:00 2001 From: itwillwork Date: Tue, 14 Jul 2026 02:07:08 +0300 Subject: [PATCH 02/11] generate modal --- src/blocks/CustomHeader/CustomHeader.tsx | 21 ++- .../CustomHeader/GenerateCodeModal.scss | 56 +++++++ src/blocks/CustomHeader/GenerateCodeModal.tsx | 142 ++++++++++++++++++ .../MDXRenderer/Sandbox/stackblitz.ts | 33 +++- src/components/Menu/Menu.tsx | 19 ++- src/pages/api/generate-code.ts | 3 +- 6 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 src/blocks/CustomHeader/GenerateCodeModal.scss create mode 100644 src/blocks/CustomHeader/GenerateCodeModal.tsx diff --git a/src/blocks/CustomHeader/CustomHeader.tsx b/src/blocks/CustomHeader/CustomHeader.tsx index aba6766a5347..20e83dd9b970 100644 --- a/src/blocks/CustomHeader/CustomHeader.tsx +++ b/src/blocks/CustomHeader/CustomHeader.tsx @@ -1,4 +1,4 @@ -import {ChevronRight} from '@gravity-ui/icons'; +import {ChevronRight, SparklesFill} from '@gravity-ui/icons'; import {Animatable, AnimateBlock, Col, Grid, Row, YFMWrapper} from '@gravity-ui/page-constructor'; import {Button, ButtonProps, Flex, Icon, IconData, Text} from '@gravity-ui/uikit'; import React from 'react'; @@ -10,6 +10,7 @@ import {block} from '../../utils'; import {CustomBlock} from '../constants'; import './CustomHeader.scss'; +import {GenerateCodeModal} from './GenerateCodeModal'; const b = block('custom-header'); @@ -62,6 +63,7 @@ export const CustomHeader: React.FC = ({ banner, }) => { const locale = useLocale(); + const [isGenerateModalOpen, setGenerateModalOpen] = React.useState(false); const showNewsBlock = news && news.items && news.items.length > 0; const showBannerBlock = banner?.content; @@ -127,8 +129,25 @@ export const CustomHeader: React.FC = ({ ); })} + ) : null} + setGenerateModalOpen(false)} + /> diff --git a/src/blocks/CustomHeader/GenerateCodeModal.scss b/src/blocks/CustomHeader/GenerateCodeModal.scss new file mode 100644 index 000000000000..eea6fff826cd --- /dev/null +++ b/src/blocks/CustomHeader/GenerateCodeModal.scss @@ -0,0 +1,56 @@ +@use '../../variables.scss'; + +$block: '.#{variables.$ns}generate-code-modal'; + +#{$block} { + &__textarea-wrap { + position: relative; + } + + &__textarea { + font-size: 16px; + width: 100%; + } + + &__textarea-loader { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + border-radius: inherit; + background: var(--g-color-base-float-heavy); + opacity: 0.9; + pointer-events: none; + } + + &__loader-text { + opacity: 0; + transition: opacity 0.3s ease; + + &_visible { + opacity: 1; + } + } + + &__counter { + position: absolute; + right: 8px; + bottom: 8px; + pointer-events: none; + } + + &__result { + white-space: pre-wrap; + word-break: break-word; + font-size: 13px; + line-height: 1.5; + padding: 12px; + border-radius: 6px; + background: var(--g-color-base-misc-light); + max-height: 300px; + overflow-y: auto; + } +} diff --git a/src/blocks/CustomHeader/GenerateCodeModal.tsx b/src/blocks/CustomHeader/GenerateCodeModal.tsx new file mode 100644 index 000000000000..bd75ebbba096 --- /dev/null +++ b/src/blocks/CustomHeader/GenerateCodeModal.tsx @@ -0,0 +1,142 @@ +import {Play} from '@gravity-ui/icons'; +import {Button, Dialog, Flex, Icon, Spin, Text, TextArea} from '@gravity-ui/uikit'; +import React from 'react'; + +import {openStackblitzFromGenerated} from '../../components/MDXRenderer/Sandbox/stackblitz'; +import {block} from '../../utils'; + +import './GenerateCodeModal.scss'; + +const b = block('generate-code-modal'); + +const MAX_LENGTH = 200; + +const LOADING_TEXTS = ['Пишем компонент', 'Создаем песочницу', 'Реализуем стили', 'Отлаживаем']; + +function useLoadingText(active: boolean) { + const [index, setIndex] = React.useState(0); + const [visible, setVisible] = React.useState(true); + + React.useEffect(() => { + if (!active) return; + + setIndex(Math.floor(Math.random() * LOADING_TEXTS.length)); + setVisible(true); + + const interval = setInterval(() => { + setVisible(false); + setTimeout(() => { + setIndex(Math.floor(Math.random() * LOADING_TEXTS.length)); + setVisible(true); + }, 300); + }, 3000); + + return () => clearInterval(interval); + }, [active]); + + return {text: LOADING_TEXTS[index], visible}; +} + +interface GenerateCodeModalProps { + open: boolean; + onClose: () => void; +} + +export const GenerateCodeModal: React.FC = ({open, onClose}) => { + const [input, setInput] = React.useState(''); + const [result, setResult] = React.useState(''); + const [loading, setLoading] = React.useState(false); + const {text: loadingText, visible: loadingTextVisible} = useLoadingText(loading); + + const isOverLimit = input.length > MAX_LENGTH; + + const handleGenerate = async () => { + if (!input.trim() || isOverLimit) return; + + setLoading(true); + setResult(''); + + try { + const response = await fetch('/api/generate-code', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({input}), + }); + + const data = await response.json(); + const output = data.code ?? data.error ?? ''; + setResult(output); + if (data.code) { + openStackblitzFromGenerated(data.code); + } + } catch { + setResult('Произошла ошибка. Попробуйте ещё раз.'); + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setInput(''); + setResult(''); + onClose(); + }; + + return ( + + + + +
+