refactor: workbench store and move logic into action runner (#4)

This commit is contained in:
Dominic Elm
2024-07-22 17:40:28 +02:00
committed by GitHub
parent cae55a7026
commit f4987a4ecd
10 changed files with 295 additions and 237 deletions

View File

@@ -3,11 +3,11 @@ 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 type { ActionState } from '../../lib/runtime/action-runner';
import { chatStore } from '../../lib/stores/chat';
import { getArtifactKey, workbenchStore, type ActionState } from '../../lib/stores/workbench';
import { workbenchStore } from '../../lib/stores/workbench';
import { classNames } from '../../utils/classNames';
import { cubicEasingFn } from '../../utils/easings';
import { IconButton } from '../ui/IconButton';
const highlighterOptions = {
langs: ['shell'],
@@ -22,20 +22,19 @@ if (import.meta.hot) {
}
interface ArtifactProps {
artifactId: string;
messageId: string;
}
export const Artifact = memo(({ artifactId, messageId }: ArtifactProps) => {
export const Artifact = memo(({ 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 artifact = artifacts[messageId];
const actions = useStore(
computed(artifact.actions, (actions) => {
computed(artifact.runner.actions, (actions) => {
return Object.values(actions);
}),
);
@@ -100,50 +99,7 @@ export const Artifact = memo(({ artifactId, messageId }: ArtifactProps) => {
transition={{ duration: 0.15 }}
>
<div className="p-4 text-left border-t">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<h4 className="font-semibold mb-2">Actions</h4>
<ul className="list-none space-y-2.5">
{actions.map((action, index) => {
const { status, type, content, abort } = action;
return (
<li key={index}>
<div className="flex items-center gap-1.5">
<div className={classNames('text-lg', getTextColor(action.status))}>
{status === 'running' ? (
<div className="i-svg-spinners:90-ring-with-bg"></div>
) : status === 'pending' ? (
<div className="i-ph:circle-duotone"></div>
) : status === 'complete' ? (
<div className="i-ph:check-circle-duotone"></div>
) : status === 'failed' || status === 'aborted' ? (
<div className="i-ph:x-circle-duotone"></div>
) : null}
</div>
{type === 'file' ? (
<div>
Create <code className="bg-gray-100 text-gray-700">{action.filePath}</code>
</div>
) : type === 'shell' ? (
<div className="flex items-center w-full min-h-[28px]">
<span className="flex-1">Run command</span>
{abort !== undefined && status === 'running' && (
<IconButton icon="i-ph:x-circle" size="xl" onClick={() => abort()} />
)}
</div>
) : null}
</div>
{type === 'shell' && <ShellCodeBlock classsName="mt-1" code={content} />}
</li>
);
})}
</ul>
</motion.div>
<ActionList actions={actions} />
</div>
</motion.div>
)}
@@ -188,3 +144,61 @@ function ShellCodeBlock({ classsName, code }: ShellCodeBlockProps) {
></div>
);
}
interface ActionListProps {
actions: ActionState[];
}
const actionVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
const ActionList = memo(({ actions }: ActionListProps) => {
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
<ul className="list-none space-y-2.5">
{actions.map((action, index) => {
const { status, type, content } = action;
return (
<motion.li
key={index}
variants={actionVariants}
initial="hidden"
animate="visible"
transition={{
duration: 0.2,
ease: cubicEasingFn,
}}
>
<div className="flex items-center gap-1.5">
<div className={classNames('text-lg', getTextColor(action.status))}>
{status === 'running' ? (
<div className="i-svg-spinners:90-ring-with-bg"></div>
) : status === 'pending' ? (
<div className="i-ph:circle-duotone"></div>
) : status === 'complete' ? (
<div className="i-ph:check-circle-duotone"></div>
) : status === 'failed' || status === 'aborted' ? (
<div className="i-ph:x-circle-duotone"></div>
) : null}
</div>
{type === 'file' ? (
<div>
Create <code className="bg-gray-100 text-gray-700">{action.filePath}</code>
</div>
) : type === 'shell' ? (
<div className="flex items-center w-full min-h-[28px]">
<span className="flex-1">Run command</span>
</div>
) : null}
</div>
{type === 'shell' && <ShellCodeBlock classsName="mt-1" code={content} />}
</motion.li>
);
})}
</ul>
</motion.div>
);
});

View File

@@ -21,18 +21,13 @@ export const Markdown = memo(({ children }: MarkdownProps) => {
return {
div: ({ className, children, node, ...props }) => {
if (className?.includes('__boltArtifact__')) {
const artifactId = node?.properties.dataArtifactId as string;
const messageId = node?.properties.dataMessageId as string;
if (!artifactId) {
logger.debug(`Invalid artifact id ${messageId}`);
}
if (!messageId) {
logger.debug(`Invalid message id ${messageId}`);
logger.error(`Invalid message id ${messageId}`);
}
return <Artifact artifactId={artifactId} messageId={messageId} />;
return <Artifact messageId={messageId} />;
}
return (

View File

@@ -13,7 +13,14 @@ export const Preview = memo(() => {
const [iframeUrl, setIframeUrl] = useState<string | undefined>();
useEffect(() => {
if (activePreview && !iframeUrl) {
if (!activePreview) {
setUrl('');
setIframeUrl(undefined);
return;
}
if (!iframeUrl) {
const { baseUrl } = activePreview;
setUrl(baseUrl);
@@ -31,16 +38,16 @@ export const Preview = memo(() => {
<div className="w-full h-full flex flex-col">
<div className="bg-gray-100 rounded-t-lg p-2 flex items-center space-x-1.5">
<div className="flex items-center gap-2 text-gray-800">
<div className="i-ph:app-window-duotone scale-130 ml-1.5"></div>
<div className="i-ph:app-window-duotone scale-130 ml-1.5" />
<span className="text-sm">Preview</span>
</div>
<div className="flex-grow"></div>
<div className="flex-grow" />
</div>
<div className="bg-white p-2 flex items-center gap-1.5">
<IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
<div className="flex items-center gap-1 flex-grow bg-gray-100 rounded-full px-3 py-1 text-sm text-gray-600 hover:bg-gray-200 hover:focus-within:bg-white focus-within:bg-white focus-within:ring-2 focus-within:ring-accent">
<div className="bg-white rounded-full p-[2px] -ml-1">
<div className="i-ph:info-bold text-lg"></div>
<div className="i-ph:info-bold text-lg" />
</div>
<input
className="w-full bg-transparent outline-none"
@@ -54,7 +61,7 @@ export const Preview = memo(() => {
</div>
<div className="flex-1 bg-white border-t">
{activePreview ? (
<iframe ref={iframeRef} className="border-none w-full h-full" src={iframeUrl}></iframe>
<iframe ref={iframeRef} className="border-none w-full h-full" src={iframeUrl} />
) : (
<div className="flex w-full h-full justify-center items-center">No preview available</div>
)}