import { useStore } from '@nanostores/react'; import { AnimatePresence, motion } from 'framer-motion'; import { computed } from 'nanostores'; import { memo, useEffect, useRef, useState } from 'react'; import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki'; import { chatStore } from '../../lib/stores/chat'; import { getArtifactKey, workbenchStore, type ActionState } from '../../lib/stores/workbench'; import { classNames } from '../../utils/classNames'; import { cubicEasingFn } from '../../utils/easings'; import { IconButton } from '../ui/IconButton'; const highlighterOptions = { langs: ['shell'], themes: ['light-plus', 'dark-plus'], }; const shellHighlighter: HighlighterGeneric = import.meta.hot?.data.shellHighlighter ?? (await createHighlighter(highlighterOptions)); if (import.meta.hot) { import.meta.hot.data.shellHighlighter = shellHighlighter; } interface ArtifactProps { artifactId: string; messageId: string; } export const Artifact = memo(({ artifactId, messageId }: ArtifactProps) => { const userToggledActions = useRef(false); const [showActions, setShowActions] = useState(false); const chat = useStore(chatStore); const artifacts = useStore(workbenchStore.artifacts); const artifact = artifacts[getArtifactKey(artifactId, messageId)]; const actions = useStore( computed(artifact.actions, (actions) => { return Object.values(actions); }), ); const toggleActions = () => { userToggledActions.current = true; setShowActions(!showActions); }; useEffect(() => { if (actions.length && !showActions && !userToggledActions.current) { setShowActions(true); } }, [actions]); return (
{actions.length && (
)}
{showActions && actions.length > 0 && (

Actions

    {actions.map((action, index) => { const { status, type, content, abort } = action; return (
  • {status === 'running' ? (
    ) : status === 'pending' ? (
    ) : status === 'complete' ? (
    ) : status === 'failed' || status === 'aborted' ? (
    ) : null}
    {type === 'file' ? (
    Create {action.filePath}
    ) : type === 'shell' ? (
    Run command {abort !== undefined && status === 'running' && ( abort()} /> )}
    ) : null}
    {type === 'shell' && }
  • ); })}
)}
); }); function getTextColor(status: ActionState['status']) { switch (status) { case 'pending': { return 'text-gray-500'; } case 'running': { return 'text-gray-1000'; } case 'complete': { return 'text-positive-600'; } case 'aborted': { return 'text-gray-600'; } case 'failed': { return 'text-negative-600'; } default: { return undefined; } } } interface ShellCodeBlockProps { classsName?: string; code: string; } function ShellCodeBlock({ classsName, code }: ShellCodeBlockProps) { return (
); }