| | import React, { useMemo, useState, useRef, useEffect } from 'react'; |
| | import { useRecoilValue } from 'recoil'; |
| | import type { TAttachment } from 'librechat-data-provider'; |
| | import ProgressText from '~/components/Chat/Messages/Content/ProgressText'; |
| | import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; |
| | import { useProgress, useLocalize } from '~/hooks'; |
| | import { AttachmentGroup } from './Attachment'; |
| | import Stdout from './Stdout'; |
| | import { cn } from '~/utils'; |
| | import store from '~/store'; |
| |
|
| | interface ParsedArgs { |
| | lang?: string; |
| | code?: string; |
| | } |
| |
|
| | export function useParseArgs(args?: string): ParsedArgs | null { |
| | return useMemo(() => { |
| | let parsedArgs: ParsedArgs | string | undefined | null = args; |
| | try { |
| | parsedArgs = JSON.parse(args || ''); |
| | } catch { |
| | |
| | } |
| | if (typeof parsedArgs === 'object') { |
| | return parsedArgs; |
| | } |
| | const langMatch = args?.match(/"lang"\s*:\s*"(\w+)"/); |
| | const codeMatch = args?.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"(session_id|args)"|"\s*})/s); |
| |
|
| | let code = ''; |
| | if (codeMatch) { |
| | code = codeMatch[1]; |
| | if (code.endsWith('"}')) { |
| | code = code.slice(0, -2); |
| | } |
| | code = code.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\'); |
| | } |
| |
|
| | return { |
| | lang: langMatch ? langMatch[1] : '', |
| | code, |
| | }; |
| | }, [args]); |
| | } |
| |
|
| | export default function ExecuteCode({ |
| | isSubmitting, |
| | initialProgress = 0.1, |
| | args, |
| | output = '', |
| | attachments, |
| | }: { |
| | initialProgress: number; |
| | isSubmitting: boolean; |
| | args?: string; |
| | output?: string; |
| | attachments?: TAttachment[]; |
| | }) { |
| | const localize = useLocalize(); |
| | const hasOutput = output.length > 0; |
| | const outputRef = useRef<string>(output); |
| | const codeContentRef = useRef<HTMLDivElement>(null); |
| | const [isAnimating, setIsAnimating] = useState(false); |
| | const showAnalysisCode = useRecoilValue(store.showCode); |
| | const [showCode, setShowCode] = useState(showAnalysisCode); |
| | const [contentHeight, setContentHeight] = useState<number | undefined>(0); |
| |
|
| | const prevShowCodeRef = useRef<boolean>(showCode); |
| | const { lang, code } = useParseArgs(args) ?? ({} as ParsedArgs); |
| | const progress = useProgress(initialProgress); |
| |
|
| | useEffect(() => { |
| | if (output !== outputRef.current) { |
| | outputRef.current = output; |
| |
|
| | if (showCode && codeContentRef.current) { |
| | setTimeout(() => { |
| | if (codeContentRef.current) { |
| | const newHeight = codeContentRef.current.scrollHeight; |
| | setContentHeight(newHeight); |
| | } |
| | }, 10); |
| | } |
| | } |
| | }, [output, showCode]); |
| |
|
| | useEffect(() => { |
| | if (showCode !== prevShowCodeRef.current) { |
| | prevShowCodeRef.current = showCode; |
| |
|
| | if (showCode && codeContentRef.current) { |
| | setIsAnimating(true); |
| | requestAnimationFrame(() => { |
| | if (codeContentRef.current) { |
| | const height = codeContentRef.current.scrollHeight; |
| | setContentHeight(height); |
| | } |
| |
|
| | const timer = setTimeout(() => { |
| | setIsAnimating(false); |
| | }, 500); |
| |
|
| | return () => clearTimeout(timer); |
| | }); |
| | } else if (!showCode) { |
| | setIsAnimating(true); |
| | setContentHeight(0); |
| |
|
| | const timer = setTimeout(() => { |
| | setIsAnimating(false); |
| | }, 500); |
| |
|
| | return () => clearTimeout(timer); |
| | } |
| | } |
| | }, [showCode]); |
| |
|
| | useEffect(() => { |
| | if (!codeContentRef.current) { |
| | return; |
| | } |
| |
|
| | const resizeObserver = new ResizeObserver((entries) => { |
| | if (showCode && !isAnimating) { |
| | for (const entry of entries) { |
| | if (entry.target === codeContentRef.current) { |
| | setContentHeight(entry.contentRect.height); |
| | } |
| | } |
| | } |
| | }); |
| |
|
| | resizeObserver.observe(codeContentRef.current); |
| |
|
| | return () => { |
| | resizeObserver.disconnect(); |
| | }; |
| | }, [showCode, isAnimating]); |
| |
|
| | const cancelled = !isSubmitting && progress < 1; |
| |
|
| | return ( |
| | <> |
| | <div className="relative my-2.5 flex size-5 shrink-0 items-center gap-2.5"> |
| | <ProgressText |
| | progress={progress} |
| | onClick={() => setShowCode((prev) => !prev)} |
| | inProgressText={localize('com_ui_analyzing')} |
| | finishedText={ |
| | cancelled ? localize('com_ui_cancelled') : localize('com_ui_analyzing_finished') |
| | } |
| | hasInput={!!code?.length} |
| | isExpanded={showCode} |
| | error={cancelled} |
| | /> |
| | </div> |
| | <div |
| | className="relative mb-2" |
| | style={{ |
| | height: showCode ? contentHeight : 0, |
| | overflow: 'hidden', |
| | transition: |
| | 'height 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1)', |
| | opacity: showCode ? 1 : 0, |
| | transformOrigin: 'top', |
| | willChange: 'height, opacity', |
| | perspective: '1000px', |
| | backfaceVisibility: 'hidden', |
| | WebkitFontSmoothing: 'subpixel-antialiased', |
| | }} |
| | > |
| | <div |
| | className={cn( |
| | 'code-analyze-block mt-0.5 overflow-hidden rounded-xl bg-surface-primary', |
| | showCode && 'shadow-lg', |
| | )} |
| | ref={codeContentRef} |
| | style={{ |
| | transform: showCode ? 'translateY(0) scale(1)' : 'translateY(-8px) scale(0.98)', |
| | opacity: showCode ? 1 : 0, |
| | transition: |
| | 'transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1)', |
| | }} |
| | > |
| | {showCode && ( |
| | <div |
| | style={{ |
| | transform: showCode ? 'translateY(0)' : 'translateY(-4px)', |
| | opacity: showCode ? 1 : 0, |
| | transition: |
| | 'transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1)', |
| | }} |
| | > |
| | <MarkdownLite |
| | content={code ? `\`\`\`${lang}\n${code}\n\`\`\`` : ''} |
| | codeExecution={false} |
| | /> |
| | </div> |
| | )} |
| | {hasOutput && ( |
| | <div |
| | className={cn( |
| | 'bg-surface-tertiary p-4 text-xs', |
| | showCode ? 'border-t border-surface-primary-contrast' : '', |
| | )} |
| | style={{ |
| | transform: showCode ? 'translateY(0)' : 'translateY(-6px)', |
| | opacity: showCode ? 1 : 0, |
| | transition: |
| | 'transform 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s, opacity 0.45s cubic-bezier(0.19, 1, 0.22, 1) 0.05s', |
| | boxShadow: showCode ? '0 -1px 0 rgba(0,0,0,0.05)' : 'none', |
| | }} |
| | > |
| | <div className="prose flex flex-col-reverse"> |
| | <Stdout output={output} /> |
| | </div> |
| | </div> |
| | )} |
| | </div> |
| | </div> |
| | {attachments && attachments.length > 0 && <AttachmentGroup attachments={attachments} />} |
| | </> |
| | ); |
| | } |
| |
|