Skip to content

Commit e999a3d

Browse files
Add ProgressIndicator component
1 parent d6b1195 commit e999a3d

1 file changed

Lines changed: 191 additions & 0 deletions

File tree

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { useStore } from '@nanostores/react';
2+
import { motion } from 'framer-motion';
3+
import { computed } from 'nanostores';
4+
import { memo } from 'react';
5+
import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki';
6+
import type { ActionState } from '~/lib/runtime/action-runner';
7+
import { workbenchStore } from '~/lib/stores/workbench';
8+
import { classNames } from '~/utils/classNames';
9+
import { cubicEasingFn } from '~/utils/easings';
10+
import { WORK_DIR } from '~/utils/constants';
11+
12+
const highlighterOptions = {
13+
langs: ['shell'],
14+
themes: ['light-plus', 'dark-plus'],
15+
};
16+
17+
const shellHighlighter: HighlighterGeneric<BundledLanguage, BundledTheme> =
18+
import.meta.hot?.data.shellHighlighter ?? (await createHighlighter(highlighterOptions));
19+
20+
if (import.meta.hot) {
21+
import.meta.hot.data.shellHighlighter = shellHighlighter;
22+
}
23+
24+
export const ProgressIndicator = memo(() => {
25+
const currentArtifactMessageId = useStore(workbenchStore.currentArtifactMessageId);
26+
const artifacts = useStore(workbenchStore.artifacts);
27+
28+
if (!currentArtifactMessageId) {
29+
return null;
30+
}
31+
32+
const artifact = artifacts[currentArtifactMessageId];
33+
34+
if (!artifact) {
35+
return null;
36+
}
37+
38+
const actions = useStore(
39+
computed(artifact.runner.actions, (actions) => {
40+
// Filter out Supabase actions except for migrations
41+
return Object.values(actions).filter((action) => {
42+
// Exclude actions with type 'supabase' or actions that contain 'supabase' in their content
43+
return action.type !== 'supabase' && !(action.type === 'shell' && action.content?.includes('supabase'));
44+
});
45+
}),
46+
);
47+
48+
return (
49+
<div className="p-5 bg-codinit-elements-actions-background">
50+
<ActionList actions={actions} />
51+
</div>
52+
);
53+
});
54+
55+
interface ShellCodeBlockProps {
56+
classsName?: string;
57+
code: string;
58+
}
59+
60+
function ShellCodeBlock({ classsName, code }: ShellCodeBlockProps) {
61+
return (
62+
<div
63+
className={classNames('text-xs', classsName)}
64+
dangerouslySetInnerHTML={{
65+
__html: shellHighlighter.codeToHtml(code, {
66+
lang: 'shell',
67+
theme: 'dark-plus',
68+
}),
69+
}}
70+
></div>
71+
);
72+
}
73+
74+
interface ActionListProps {
75+
actions: ActionState[];
76+
}
77+
78+
const actionVariants = {
79+
hidden: { opacity: 0, y: 20 },
80+
visible: { opacity: 1, y: 0 },
81+
};
82+
83+
function openArtifactInWorkbench(filePath: any) {
84+
if (workbenchStore.currentView.get() !== 'code') {
85+
workbenchStore.currentView.set('code');
86+
}
87+
88+
workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`);
89+
}
90+
91+
const ActionList = memo(({ actions }: ActionListProps) => {
92+
return (
93+
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
94+
<ul className="list-none space-y-2.5">
95+
{actions.map((action, index) => {
96+
const { status, type, content } = action;
97+
const isLast = index === actions.length - 1;
98+
99+
return (
100+
<motion.li
101+
key={index}
102+
variants={actionVariants}
103+
initial="hidden"
104+
animate="visible"
105+
transition={{
106+
duration: 0.2,
107+
ease: cubicEasingFn,
108+
}}
109+
>
110+
<div className="flex items-center gap-1.5 text-sm">
111+
<div className={classNames('text-lg', getIconColor(action.status))}>
112+
{status === 'running' ? (
113+
<>
114+
{type !== 'start' ? (
115+
<div className="i-svg-spinners:90-ring-with-bg"></div>
116+
) : (
117+
<div className="i-ph:terminal-window-duotone text-codinit-elements-textPrimary"></div>
118+
)}
119+
</>
120+
) : status === 'pending' ? (
121+
<div className="i-ph:circle-duotone text-codinit-elements-textPrimary"></div>
122+
) : status === 'complete' ? (
123+
<div className="i-ph:check"></div>
124+
) : status === 'failed' || status === 'aborted' ? (
125+
<div className="i-ph:x"></div>
126+
) : null}
127+
</div>
128+
{type === 'file' ? (
129+
<div>
130+
Create{' '}
131+
<code
132+
className="bg-codinit-elements-artifacts-inlineCode-background text-codinit-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-codinit-elements-item-contentAccent hover:underline cursor-pointer"
133+
onClick={() => openArtifactInWorkbench(action.filePath)}
134+
>
135+
{action.filePath}
136+
</code>
137+
</div>
138+
) : type === 'shell' ? (
139+
<div className="flex items-center w-full min-h-[28px]">
140+
<span className="flex-1">Run command</span>
141+
</div>
142+
) : type === 'start' ? (
143+
<a
144+
onClick={(e) => {
145+
e.preventDefault();
146+
workbenchStore.currentView.set('preview');
147+
}}
148+
className="flex items-center w-full min-h-[28px]"
149+
>
150+
<span className="flex-1">Start Application</span>
151+
</a>
152+
) : null}
153+
</div>
154+
{(type === 'shell' || type === 'start') && (
155+
<ShellCodeBlock
156+
classsName={classNames('mt-1', {
157+
'mb-3.5': !isLast,
158+
})}
159+
code={content}
160+
/>
161+
)}
162+
</motion.li>
163+
);
164+
})}
165+
</ul>
166+
</motion.div>
167+
);
168+
});
169+
170+
function getIconColor(status: ActionState['status']) {
171+
switch (status) {
172+
case 'pending': {
173+
return 'text-codinit-elements-textTertiary';
174+
}
175+
case 'running': {
176+
return 'text-codinit-elements-loader-progress';
177+
}
178+
case 'complete': {
179+
return 'text-codinit-elements-icon-success';
180+
}
181+
case 'aborted': {
182+
return 'text-codinit-elements-textSecondary';
183+
}
184+
case 'failed': {
185+
return 'text-codinit-elements-icon-error';
186+
}
187+
default: {
188+
return undefined;
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)