diff --git a/eslint.config.mjs b/eslint.config.mjs index c904562..8e27ad6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,6 +9,8 @@ export default [ { rules: { '@blitz/catch-error-name': 'off', + '@typescript-eslint/no-this-alias': 'off', + '@typescript-eslint/no-empty-object-type': 'off', }, }, { diff --git a/packages/bolt/app/components/chat/Artifact.tsx b/packages/bolt/app/components/chat/Artifact.tsx index 188e4db..781b1df 100644 --- a/packages/bolt/app/components/chat/Artifact.tsx +++ b/packages/bolt/app/components/chat/Artifact.tsx @@ -1,34 +1,176 @@ import { useStore } from '@nanostores/react'; -import { workspaceStore } from '~/lib/stores/workspace'; +import { AnimatePresence, motion } from 'framer-motion'; +import { computed } from 'nanostores'; +import { useState } from 'react'; +import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki'; +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 function Artifact({ messageId }: ArtifactProps) { - const artifacts = useStore(workspaceStore.artifacts); +export function Artifact({ artifactId, messageId }: ArtifactProps) { + const [showActions, setShowActions] = useState(false); - const artifact = artifacts[messageId]; + const artifacts = useStore(workbenchStore.artifacts); + const artifact = artifacts[getArtifactKey(artifactId, messageId)]; + + const actions = useStore( + computed(artifact.actions, (actions) => { + return Object.values(actions); + }), + ); return ( - + + {actions.length && ( + setShowActions(!showActions)} + > +
+
+
+
+ )} +
+ + + {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' && } +
  • + ); + })} +
+
+
+
)} - -
-
{artifact?.title}
- Click to open code -
- +
+ + ); +} + +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 ( +
); } diff --git a/packages/bolt/app/components/chat/BaseChat.tsx b/packages/bolt/app/components/chat/BaseChat.tsx index 64b4712..e61770d 100644 --- a/packages/bolt/app/components/chat/BaseChat.tsx +++ b/packages/bolt/app/components/chat/BaseChat.tsx @@ -2,16 +2,14 @@ import type { Message } from 'ai'; import type { LegacyRef } from 'react'; import React from 'react'; import { ClientOnly } from 'remix-utils/client-only'; -import { IconButton } from '~/components/ui/IconButton'; -import { Workspace } from '~/components/workspace/Workspace.client'; -import { classNames } from '~/utils/classNames'; +import { classNames } from '../../utils/classNames'; +import { IconButton } from '../ui/IconButton'; +import { Workbench } from '../workbench/Workbench.client'; import { Messages } from './Messages.client'; import { SendButton } from './SendButton.client'; interface BaseChatProps { textareaRef?: LegacyRef | undefined; - messagesSlot?: React.ReactNode; - workspaceSlot?: React.ReactNode; chatStarted?: boolean; isStreaming?: boolean; messages?: Message[]; @@ -80,14 +78,17 @@ export const BaseChat = React.forwardRef(