import { useCallback, useEffect, useRef, useState } from 'react'; const PANEL_HEIGHT_STORAGE_KEY = 'nodes_ui_flow_interactive_panel_height'; const DEFAULT_PANEL_HEIGHT = 320; const MIN_PANEL_HEIGHT = 250; const TOOLBAR_FALLBACK_HEIGHT = 72; const MAX_PANEL_TOP_GAP = 8; function getMaxPanelHeight() { if (typeof window === 'undefined') { return DEFAULT_PANEL_HEIGHT; } const toolbarHeight = document.querySelector('.toolbar')?.getBoundingClientRect().height || TOOLBAR_FALLBACK_HEIGHT; return Math.max(MIN_PANEL_HEIGHT, window.innerHeight - toolbarHeight - MAX_PANEL_TOP_GAP); } function readInitialPanelHeight() { if (typeof window === 'undefined') { return DEFAULT_PANEL_HEIGHT; } const savedHeight = Number(window.localStorage.getItem(PANEL_HEIGHT_STORAGE_KEY)); if (!Number.isFinite(savedHeight)) { return DEFAULT_PANEL_HEIGHT; } return Math.min(Math.max(savedHeight, MIN_PANEL_HEIGHT), getMaxPanelHeight()); } export default function InteractivePanel({ isDemo, pendingQuestion, transcript, inputValue, isBusy, llmProvider, llmAutoAnswer, writeTranscriptLogs, llmRolePrompt, onInputChange, onLlmAutoAnswerChange, onWriteTranscriptLogsChange, onLlmRolePromptChange, onSubmitAnswer, onSimulateAnswer, onSaveSessionLog, }) { const [panelHeight, setPanelHeight] = useState(readInitialPanelHeight); const canAnswer = Boolean(pendingQuestion) && !isBusy && inputValue.trim().length > 0; const canSimulate = Boolean(pendingQuestion) && !isBusy && llmRolePrompt.trim().length > 0; const providerLabel = { deepinfra: 'DeepInfra', ollama: 'Ollama', }[llmProvider] || llmProvider; const transcriptRef = useRef(null); const resizeRef = useRef(null); const clampPanelHeight = useCallback((height) => ( Math.min(Math.max(Math.round(height), MIN_PANEL_HEIGHT), getMaxPanelHeight()) ), []); useEffect(() => { if (transcriptRef.current) { transcriptRef.current.scrollTop = transcriptRef.current.scrollHeight; } }, [transcript]); useEffect(() => { if (isDemo) { return; } window.localStorage.setItem(PANEL_HEIGHT_STORAGE_KEY, String(panelHeight)); }, [isDemo, panelHeight]); useEffect(() => { if (isDemo) { return undefined; } const handleResize = () => { setPanelHeight((currentHeight) => clampPanelHeight(currentHeight)); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, [clampPanelHeight, isDemo]); useEffect(() => () => { document.body.classList.remove('is-resizing-interactive-panel'); }, []); const stopResize = useCallback((event) => { const resizeState = resizeRef.current; if (!resizeState || resizeState.pointerId !== event.pointerId) { return; } resizeRef.current = null; if (event.currentTarget.hasPointerCapture?.(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } document.body.classList.remove('is-resizing-interactive-panel'); }, []); const handleResizeStart = useCallback((event) => { if (isDemo) { return; } if (event.button !== 0) { return; } event.preventDefault(); resizeRef.current = { pointerId: event.pointerId, startY: event.clientY, startHeight: panelHeight, }; event.currentTarget.setPointerCapture?.(event.pointerId); document.body.classList.add('is-resizing-interactive-panel'); }, [isDemo, panelHeight]); const handleResizeMove = useCallback((event) => { if (isDemo) { return; } const resizeState = resizeRef.current; if (!resizeState || resizeState.pointerId !== event.pointerId) { return; } const nextHeight = resizeState.startHeight + resizeState.startY - event.clientY; setPanelHeight(clampPanelHeight(nextHeight)); }, [clampPanelHeight, isDemo]); const handleResizeKeyDown = useCallback((event) => { if (isDemo) { return; } if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { return; } event.preventDefault(); const step = event.shiftKey ? 80 : 24; setPanelHeight((currentHeight) => ( clampPanelHeight(currentHeight + (event.key === 'ArrowUp' ? step : -step)) )); }, [clampPanelHeight, isDemo]); return (
{!isDemo ? (
) : null}
Интерактивный прогон
{pendingQuestion ? pendingQuestion.question : 'Запустите сценарий, чтобы дойти до следующего вопроса.'}
{isBusy ? 'идет' : pendingQuestion ? 'ждем ответ' : 'готово'}
{transcript.length === 0 ? (
Диалог появится здесь во время интерактивного прогона.
) : ( transcript.map((message) => (
{message.role === 'assistant' ? 'Ассистент' : 'Пользователь'}

{message.text}

)) )}
onInputChange(event.target.value)} />