Skip to content

Commit

Permalink
feat(app-builder): app loading updates (#179)
Browse files Browse the repository at this point in the history
Signed-off-by: Petr Kadlec <[email protected]>
  • Loading branch information
kapetr authored Jan 14, 2025
1 parent 80fd5f1 commit 513211e
Show file tree
Hide file tree
Showing 23 changed files with 170 additions and 114 deletions.
1 change: 1 addition & 0 deletions src/components/Spinner/BouncingDotsAnimation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"v":"5.5.2","nm":"Bee Processing","fr":30,"ip":0,"op":43,"w":1080,"h":1080,"ddd":0,"assets":[{"id":"v2_Slower_02(10)-precomp","layers":[{"nm":"offset","ind":3,"hd":false,"ty":3,"ks":{"a":{"a":0,"k":[0,0]},"p":{"a":0,"k":[5000,5000]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"bm":0,"ip":0,"op":43,"st":0,"ao":0,"ddd":0},{"nm":"Left","ind":4,"parent":3,"hd":false,"ty":4,"shapes":[{"it":[{"ks":{"a":0,"k":{"c":true,"i":[[17.4,17.4],[-17.4,17.4],[-17.4,-17.4],[17.4,-17.4]],"o":[[-17.4,-17.4],[17.4,-17.4],[17.4,17.4],[-17.4,17.4]],"v":[[-31.56,31.56],[-31.56,-31.56],[31.56,-31.56],[31.56,31.56]]}},"ty":"sh"},{"c":{"a":0,"k":[0,0,0]},"o":{"a":0,"k":100},"ty":"fl"},{"r":{"a":0,"k":0},"ty":"rd"},{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"ty":"tr"}],"ty":"gr"}],"ks":{"a":{"a":0,"k":[0,0]},"p":{"a":1,"k":[{"i":{"x":[0.5],"y":[0]},"o":{"x":[0.10999999940395355],"y":[0]},"s":[-155.37,0],"t":3},{"i":{"x":[0.8899999856948853],"y":[1]},"o":{"x":[0.5],"y":[1]},"s":[0.89,0],"t":10},{"i":{"x":[0.550000011920929],"y":[1]},"o":{"x":[0.45500001311302185],"y":[0]},"s":[-155.11,0],"t":17}]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":-180}},"bm":0,"ip":0,"op":43,"st":0,"ao":0,"ddd":0},{"nm":"middle","ind":5,"parent":3,"hd":false,"ty":4,"shapes":[{"it":[{"ks":{"a":0,"k":{"c":true,"i":[[17.4,17.4],[-17.4,17.4],[-17.4,-17.4],[17.4,-17.4]],"o":[[-17.4,-17.4],[17.4,-17.4],[17.4,17.4],[-17.4,17.4]],"v":[[-31.56,31.56],[-31.56,-31.56],[31.56,-31.56],[31.56,31.56]]}},"ty":"sh"},{"c":{"a":0,"k":[0,0,0]},"o":{"a":0,"k":100},"ty":"fl"},{"r":{"a":0,"k":0},"ty":"rd"},{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"ty":"tr"}],"ty":"gr"}],"ks":{"a":{"a":0,"k":[0,0]},"p":{"a":1,"k":[{"i":{"x":[0.8899999856948853],"y":[1]},"o":{"x":[0.5],"y":[1]},"s":[0.76,0],"t":7},{"i":{"x":[0.550000011920929],"y":[1]},"o":{"x":[0.45500001311302185],"y":[0]},"s":[0.76,-125],"t":20},{"i":{"x":[0.550000011920929],"y":[1]},"o":{"x":[0.45500001311302185],"y":[0]},"s":[0.76,-120],"t":22},{"i":{"x":[0.550000011920929],"y":[1]},"o":{"x":[0.45500001311302185],"y":[0]},"s":[0.76,25],"t":26},{"i":{"x":[0.550000011920929],"y":[1]},"o":{"x":[0.45500001311302185],"y":[0]},"s":[0.76,7],"t":32},{"i":{"x":[0.550000011920929],"y":[1]},"o":{"x":[0.45500001311302185],"y":[0]},"s":[0.76,0],"t":36}]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":-180}},"bm":0,"ip":0,"op":43,"st":0,"ao":0,"ddd":0},{"nm":"Right","ind":6,"parent":3,"hd":false,"ty":4,"shapes":[{"it":[{"ks":{"a":0,"k":{"c":true,"i":[[17.4,17.4],[-17.4,17.4],[-17.4,-17.4],[17.4,-17.4]],"o":[[-17.4,-17.4],[17.4,-17.4],[17.4,17.4],[-17.4,17.4]],"v":[[-31.56,31.56],[-31.56,-31.56],[31.56,-31.56],[31.56,31.56]]}},"ty":"sh"},{"c":{"a":0,"k":[0,0,0]},"o":{"a":0,"k":100},"ty":"fl"},{"r":{"a":0,"k":0},"ty":"rd"},{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"ty":"tr"}],"ty":"gr"}],"ks":{"a":{"a":0,"k":[0,0]},"p":{"a":1,"k":[{"i":{"x":[0.5],"y":[0]},"o":{"x":[0.10999999940395355],"y":[0]},"s":[155.14,0],"t":3},{"i":{"x":[0.8899999856948853],"y":[1]},"o":{"x":[0.5],"y":[1]},"s":[0.89,0],"t":10},{"i":{"x":[0.550000011920929],"y":[1]},"o":{"x":[0.45500001311302185],"y":[0]},"s":[154.89,0],"t":17}]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":-180}},"bm":0,"ip":0,"op":43,"st":0,"ao":0,"ddd":0}]}],"layers":[{"nm":"root","ind":2,"hd":false,"ty":3,"ks":{"a":{"a":0,"k":[0,0]},"p":{"a":0,"k":[540,540]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"bm":0,"ip":0,"op":43,"st":0,"ao":0,"ddd":0},{"nm":"v2_Slower_02","ind":3,"parent":2,"hd":false,"ty":0,"h":99999,"refId":"v2_Slower_02(10)-precomp","w":99999,"ks":{"a":{"a":0,"k":[5000,5000]},"p":{"a":0,"k":[0.11,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":-180}},"bm":0,"ip":0,"op":43,"st":0,"ao":0,"ddd":0}]}
70 changes: 21 additions & 49 deletions src/components/Spinner/Spinner.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,59 +17,31 @@
@use 'styles/common' as *;

.root {
fill: currentColor;
block-size: 1rem;
inline-size: 1rem;
vertical-align: text-bottom;
overflow: visible;
--jump-duration: 2s;
inline-size: 1.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
transform: translate(0, 0.2rem);

path {
transform: translateY(3px);
animation-name: eggbounce;
animation-duration: var(--jump-duration);
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;

&:nth-child(2) {
animation-delay: calc(var(--jump-duration) * 0.3333);
}
&:nth-child(3) {
animation-delay: calc(var(--jump-duration) * 0.6666);
&.size-sm {
inline-size: 1rem;
transform: translate(0, 0.1rem);
.content {
block-size: 2.8rem;
min-inline-size: 2.8rem;
transform: translate(0, 0.1rem);
}
}
}

/* if empty time slot in animation is desired after tv3, switch to 12.5% and 25% */
@keyframes eggbounce {
0% {
transform: translateY(rem(3px));
}
16.6666% {
transform: translateY(-0.4rem);
}
33.3333% {
transform: translateY(rem(3px));
}
100% {
transform: translateY(rem(3px));
}
}

@keyframes eggtype {
0% {
transform: translateY(rem(4px));
}
25% {
transform: translateY(-0.1rem);
}
50% {
transform: translateY(rem(4px));
}
75% {
transform: translateY(-0.1rem);
}
100% {
transform: translateY(rem(4px));
.content {
display: flex;
block-size: 3.5rem;
min-inline-size: 3.5rem;
transform: translate(0, 0.2rem);
svg path {
fill: currentColor !important;
}
}
}
23 changes: 16 additions & 7 deletions src/components/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2024 IBM Corp.
* Copyright 2025 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,14 +14,23 @@
* limitations under the License.
*/

import Lottie from 'lottie-react';
import SpinnerAnimation from './BouncingDotsAnimation.json';
import classes from './Spinner.module.scss';
import clsx from 'clsx';

export function Spinner() {
interface Props {
size?: 'sm' | 'md';
}

export function Spinner({ size = 'md' }: Props) {
return (
<svg version="1.1" className={classes.root} viewBox="0 0 20 20">
<path d="M5.2,10.6c0,1.1-0.9,2-2,2s-2.5-0.9-2.5-2s1.4-2,2.5-2S5.2,9.5,5.2,10.6z" />
<path d="M13.2,9.5c0,1.7-1.3,3.1-3,3.1c-1.7,0-3-1.4-3-3.1c0-1.7,1.3-3.9,3-3.9C11.9,5.6,13.2,7.8,13.2,9.5z" />
<path d="M18.8,11.7c-0.6,0.9-1.8,1.2-2.8,0.6c-0.9-0.6-1.2-1.8-0.6-2.8c0.6-0.9,2.1-1.6,3-1C19.4,9.1,19.4,10.8,18.8,11.7z" />
</svg>
<div className={clsx(classes.root, classes[`size-${size}`])}>
<Lottie
className={classes.content}
animationData={SpinnerAnimation}
loop
/>
</div>
);
}
27 changes: 25 additions & 2 deletions src/modules/apps/builder/AppBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
ChatMessage,
MessageMetadata,
MessageWithFiles,
UserChatMessage,
} from '@/modules/chat/types';
import { useLayoutActions } from '@/store/layout';
import { isNotNull } from '@/utils/helpers';
Expand All @@ -51,6 +52,7 @@ import { useAppBuilder, useAppBuilderApi } from './AppBuilderProvider';
import { ArtifactSharedIframe } from './ArtifactSharedIframe';
import { SourceCodeEditor } from './SourceCodeEditor';
import { useAppContext } from '@/layout/providers/AppProvider';
import { isBotMessage } from '@/modules/chat/utils';

interface Props {
thread?: Thread;
Expand Down Expand Up @@ -85,6 +87,14 @@ export function AppBuilder({ assistant, thread, initialMessages }: Props) {
[organization.id, project.id, queryClient, setCode, thread],
);

const handleMessageContentUpdated = useCallback(
(message: string) => {
const pythonAppCode = extractCodeFromMessageContent(message);
if (pythonAppCode) setCode(pythonAppCode);
},
[setCode],
);

const handleBeforePostMessage = useCallback(
async (thread: Thread, messages: ChatMessage[]) => {
const lastMessage = getLastMessageWithCode(messages);
Expand Down Expand Up @@ -147,6 +157,7 @@ export function AppBuilder({ assistant, thread, initialMessages }: Props) {
}}
onMessageCompleted={handleMessageCompleted}
onBeforePostMessage={handleBeforePostMessage}
onMessageDeltaEventResponse={handleMessageContentUpdated}
>
<AppBuilderContent />
</ChatProvider>
Expand All @@ -159,7 +170,7 @@ function AppBuilderContent() {
const router = useRouter();
const { project, organization } = useAppContext();
const { openModal } = useModal();
const { getMessages, sendMessage, thread } = useChat();
const { getMessages, sendMessage } = useChat();
const { setArtifact, setMobilePreviewOpen } = useAppBuilderApi();
const { code, artifact, mobilePreviewOpen, isSharedClone } = useAppBuilder();
const { setLayout } = useLayoutActions();
Expand All @@ -183,6 +194,16 @@ function AppBuilderContent() {

const icon = artifact?.uiMetadata.icon;

const isCodePending = message?.pending;
useEffect(() => {
if (isCodePending) {
setSelectedTab(TabsKeys.SourceCode);
setMobilePreviewOpen(true);
} else {
setSelectedTab(TabsKeys.Preview);
}
}, [isCodePending, setMobilePreviewOpen]);

useEffect(() => {
const navbarProps = getAppBuilderNavbarProps(
project.id,
Expand Down Expand Up @@ -351,6 +372,7 @@ function AppBuilderContent() {
variant="builder"
sourceCode={code}
onFixError={handleFixError}
isPending={isCodePending}
/>
</TabPanel>
<TabPanel key={TabsKeys.SourceCode}>
Expand All @@ -371,7 +393,8 @@ enum TabsKeys {
}

export function getLastMessageWithCode(messages: ChatMessage[]) {
return messages.find((message) =>
const lastMessage = messages.findLast((message) =>
Boolean(extractCodeFromMessageContent(message.content)),
);
return isBotMessage(lastMessage) ? lastMessage : undefined;
}
2 changes: 0 additions & 2 deletions src/modules/apps/builder/AppBuilderProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
'use client';
import { useStateWithRef } from '@/hooks/useStateWithRef';
import { useOnboardingCompleted } from '@/modules/users/useOnboardingCompleted';
import { ONBOARDING_PARAM } from '@/utils/constants';
import { useSearchParams } from 'next/navigation';
import {
createContext,
Expand All @@ -44,7 +43,6 @@ export function AppBuilderProvider({
children,
}: PropsWithChildren<Props>) {
const searchParams = useSearchParams();
const isOnboarding = searchParams?.has(ONBOARDING_PARAM);
const templateKey = searchParams?.get('template');
const template = templateKey
? ARTIFACT_TEMPLATES.find((template) => template.key === templateKey)
Expand Down
1 change: 0 additions & 1 deletion src/modules/apps/builder/ArtifactSharedIframe.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

:global(.#{$prefix}--loading-overlay) {
position: absolute;
background-color: $layer-02;
}
}
.app {
Expand Down
32 changes: 18 additions & 14 deletions src/modules/apps/builder/ArtifactSharedIframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,22 @@ import { useTheme } from '@/layout/providers/ThemeProvider';
import { USERCONTENT_SITE_URL } from '@/utils/constants';
import { removeTrailingSlash } from '@/utils/helpers';
import { Loading } from '@carbon/react';
import clsx from 'clsx';
import { useCallback, useEffect, useRef, useState } from 'react';
import classes from './ArtifactSharedIframe.module.scss';
import AppPlaceholder from './Placeholder.svg';
import clsx from 'clsx';

interface Props {
variant: 'detail' | 'builder';
sourceCode: string | null;
isPending?: boolean;
onFixError?: (errorText: string) => void;
}

function getErrorMessage(error: unknown) {
if (error instanceof ApiError && error.code === 'too_many_requests') {
return 'You have exceeded the limit for using LLM functions';
}
if (error instanceof Error && error.message) {
return error.message;
}
return 'Unknown error when calling LLM function.';
}

export function ArtifactSharedIframe({
variant,
sourceCode,
isPending,
variant,
onFixError,
}: Props) {
const iframeRef = useRef<HTMLIFrameElement>(null);
Expand All @@ -67,6 +59,8 @@ export function ArtifactSharedIframe({
);

useEffect(() => {
if (isPending) return;

postMessage({
type: PostMessageType.UPDATE_STATE,
stateChange: {
Expand All @@ -79,7 +73,7 @@ export function ArtifactSharedIframe({
ancestorOrigin: window.location.origin,
},
});
}, [sourceCode, onFixError, theme, postMessage]);
}, [sourceCode, onFixError, theme, postMessage, isPending]);

const handleMessage = useCallback(
async (event: MessageEvent<StliteMessage>) => {
Expand Down Expand Up @@ -172,7 +166,7 @@ export function ArtifactSharedIframe({
<AppPlaceholder />
</div>
) : (
state === State.LOADING && <Loading />
(state === State.LOADING || isPending) && <Loading />
)}
</div>
);
Expand Down Expand Up @@ -234,3 +228,13 @@ export type StliteMessage =
request_id: string;
payload: { errorText: string };
};

function getErrorMessage(error: unknown) {
if (error instanceof ApiError && error.code === 'too_many_requests') {
return 'You have exceeded the limit for using LLM functions';
}
if (error instanceof Error && error.message) {
return error.message;
}
return 'Unknown error when calling LLM function.';
}
17 changes: 13 additions & 4 deletions src/modules/apps/builder/SourceCodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@

import { EditableSyntaxHighlighter } from '@/components/EditableSyntaxHighlighter/EditableSyntaxHighlighter';
import classes from './SourceCodeEditor.module.scss';
import { useEffect, useId, useState } from 'react';
import { useEffect, useId, useRef, useState } from 'react';
import { useAppBuilder, useAppBuilderApi } from './AppBuilderProvider';
import { Button } from '@carbon/react';
import { useChat } from '@/modules/chat/providers/ChatProvider';

interface Props {
onSaveCode: () => void;
Expand All @@ -28,21 +29,29 @@ export function SourceCodeEditor({ onSaveCode }: Props) {
const id = useId();
const { setCode: saveCode } = useAppBuilderApi();
const { code: savedCode } = useAppBuilder();
const { status } = useChat();
const [code, setCode] = useState(savedCode ?? '');
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (savedCode) setCode(savedCode);
}, [savedCode]);
if (savedCode) {
setCode(savedCode);

if (containerRef.current && status === 'fetching')
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
}, [savedCode, status]);

return (
<div className={classes.root}>
<div className={classes.code}>
<div className={classes.code} ref={containerRef}>
<EditableSyntaxHighlighter
id={`${id}:code`}
value={code ?? 'No code available'}
onChange={setCode}
required
rows={16}
readOnly={status === 'fetching'}
/>
</div>
<div className={classes.buttons}>
Expand Down
12 changes: 11 additions & 1 deletion src/modules/apps/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@
*/

export function extractCodeFromMessageContent(content: string) {
return [...content.matchAll(/```python-app\n([\s\S]*?)```/g)].at(-1)?.at(-1);
const testCompleteCodeRegex = /```python-app\n[\s\S]*?```/g;
const completeCodeRegex = /```python-app\n([\s\S]*?)```/g;
const incompleteCodeRegex = /```python-app\n([\s\S]*)/g;

return [
...(testCompleteCodeRegex.test(content)
? content.matchAll(completeCodeRegex)
: content.matchAll(incompleteCodeRegex)),
]
.at(-1)
?.at(-1);
}

export function extractAppMetadataFromStreamlitCode(code: string) {
Expand Down
4 changes: 2 additions & 2 deletions src/modules/chat/assistant-plan/PlanStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { encodeEntityWithMetadata } from '@/app/api/utils';
import { ExpandPanel } from '@/components/ExpandPanel/ExpandPanel';
import { ExpandPanelButton } from '@/components/ExpandPanelButton/ExpandPanelButton';
import { LineClampText } from '@/components/LineClampText/LineClampText';
import { Spinner } from '@/components/Spinner/Spinner';
import { Tooltip } from '@/components/Tooltip/Tooltip';
import { useToolInfo } from '@/modules/tools/hooks/useToolInfo';
import { fadeProps } from '@/utils/fadeProps';
Expand Down Expand Up @@ -57,6 +56,7 @@ import classes from './PlanStep.module.scss';
import { useUserSetting } from '@/layout/hooks/useUserSetting';
import { getToolApproval, getToolReferenceFromToolCall } from './utils';
import { useAppContext } from '@/layout/providers/AppProvider';
import { Spinner } from '@/components/Spinner/Spinner';

interface Props {
step: AssistantPlanStep;
Expand Down Expand Up @@ -335,7 +335,7 @@ const getStepStatus = (

const STEP_STATUS_ICON: Record<ExtendedStepStatus, ReactElement> = {
completed: <CheckmarkFilled size={16} aria-label="finished" />,
in_progress: <Spinner aria-label="executing" />,
in_progress: <Spinner size="sm" aria-label="executing" />,
unknown: <WarningFilled size={16} aria-label="unknown" />,
failed: <StepStatusIcon icon={ErrorFilled} label="Failed" />,
cancelled: <StepStatusIcon icon={ErrorOutline} label="Cancelled" />,
Expand Down
Loading

0 comments on commit 513211e

Please sign in to comment.