mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
bootstrap progress updates
This commit is contained in:
@@ -18,6 +18,7 @@ import { createSynapseClient } from '@/lib/synapse-http-client';
|
||||
import { daytona } from '@/lib/daytona-client';
|
||||
import { Sandbox } from '@daytonaio/sdk';
|
||||
import { getOrCreateArtifactSandbox, startDevServer } from '@/lib/daytona-utils';
|
||||
import { OnStepUpdateFn, WorkspaceStepId, workspaceStepSchema } from '@/lib/ai/custom-parts/workspace-state';
|
||||
|
||||
export const handleChatRequest = async (c: Context) => {
|
||||
c.res.headers.set('x-vercel-ai-ui-message-stream', 'v1');
|
||||
@@ -68,15 +69,6 @@ export const handleChatRequest = async (c: Context) => {
|
||||
throw new Error('Conversation not found');
|
||||
}
|
||||
|
||||
const { sandbox } = await getOrCreateArtifactSandbox({
|
||||
artifactId
|
||||
});
|
||||
|
||||
const { previewUrl } = await startDevServer({
|
||||
sandbox
|
||||
});
|
||||
|
||||
const workspaceUrl = previewUrl;
|
||||
const convertedMessages = convertToModelMessages(messages);
|
||||
|
||||
const latestMessage = convertedMessages[convertedMessages.length - 1];
|
||||
@@ -91,23 +83,74 @@ export const handleChatRequest = async (c: Context) => {
|
||||
originalMessages: messages,
|
||||
execute: async (params) => {
|
||||
const writer = params.writer as WriterType;
|
||||
|
||||
writer.write({
|
||||
type: 'data-workspace-state',
|
||||
data: {
|
||||
state: 'pending',
|
||||
workspaceUrl: null,
|
||||
steps: []
|
||||
},
|
||||
transient: true
|
||||
});
|
||||
|
||||
const steps: z.infer<typeof workspaceStepSchema>[] = [];
|
||||
|
||||
const updateStep: OnStepUpdateFn = ({
|
||||
id,
|
||||
status,
|
||||
text
|
||||
}: Parameters<OnStepUpdateFn>[0]) => {
|
||||
const found = steps.find((step) => step.id === id);
|
||||
|
||||
if (found) {
|
||||
found.status = status;
|
||||
found.text = text;
|
||||
} else {
|
||||
steps.push({ id, status, text });
|
||||
}
|
||||
|
||||
writer.write({
|
||||
type: "data-workspace-state",
|
||||
data: {
|
||||
state: "in-progress",
|
||||
steps,
|
||||
workspaceUrl: null
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const { sandbox } = await getOrCreateArtifactSandbox({
|
||||
artifactId,
|
||||
onStepUpdate: updateStep
|
||||
});
|
||||
|
||||
const { previewUrl } = await startDevServer({
|
||||
sandbox,
|
||||
onStepUpdate: updateStep
|
||||
});
|
||||
|
||||
const workspaceUrl = previewUrl;
|
||||
|
||||
const runtimeContext = createRuntimeContext({
|
||||
writer,
|
||||
artifactId,
|
||||
restMessages,
|
||||
isFirstMessage: isNewConversation,
|
||||
signal,
|
||||
sandbox,
|
||||
sandbox
|
||||
});
|
||||
|
||||
console.log("Reporting compelted");
|
||||
|
||||
writer.write({
|
||||
type: 'data-workspace-state',
|
||||
type: "data-workspace-state",
|
||||
data: {
|
||||
state: 'ready',
|
||||
state: "completed",
|
||||
steps,
|
||||
workspaceUrl
|
||||
},
|
||||
transient: true
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
c.set('runtimeContext', runtimeContext);
|
||||
const run = await mastra.getWorkflow('codeWorkflow').createRunAsync();
|
||||
|
||||
@@ -1,8 +1,33 @@
|
||||
import { z } from "zod";
|
||||
|
||||
|
||||
export enum WorkspaceStepId {
|
||||
CREATE_SANDBOX = "create-sandbox",
|
||||
REPOSITORY_SETUP = "repository-setup",
|
||||
INSTALL_DEPENDENCIES = "install-dependencies",
|
||||
START_DEV_SERVER = "start-dev-server",
|
||||
}
|
||||
|
||||
export type OnStepUpdateFn = ({
|
||||
id,
|
||||
status,
|
||||
text
|
||||
}: {
|
||||
id: WorkspaceStepId;
|
||||
status: "pending" | "in-progress" | "completed";
|
||||
text: string;
|
||||
}) => void;
|
||||
|
||||
export const workspaceStepSchema = z.object({
|
||||
id: z.nativeEnum(WorkspaceStepId),
|
||||
status: z.enum(["pending", "in-progress", "completed"]),
|
||||
text: z.string(),
|
||||
});
|
||||
|
||||
export const workspaceStateUIDataPartSchema = z.object({
|
||||
state: z.enum(["ready"]),
|
||||
workspaceUrl: z.string(),
|
||||
state: z.enum(["pending", "in-progress", "completed"]),
|
||||
steps: z.array(workspaceStepSchema),
|
||||
workspaceUrl: z.string().nullable(),
|
||||
});
|
||||
|
||||
export interface WorkspaceStateUIDataPart {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Sandbox } from '@daytonaio/sdk';
|
||||
import { daytona } from './daytona-client';
|
||||
import { DaytonaNotFoundError } from '@daytonaio/sdk/src/errors/DaytonaError';
|
||||
import { WriterType } from './ai/mastra/utils/runtime-context';
|
||||
import { OnStepUpdateFn, WorkspaceStepId } from './ai/custom-parts/workspace-state';
|
||||
|
||||
const sandboxId = 'b62d2a47-2f7c-4367-aa53-4980f3fe6627';
|
||||
const baseDir = '/home/daytona/workspace';
|
||||
@@ -210,12 +212,12 @@ const createSandbox = async ({ artifactId }: { artifactId: string }) => {
|
||||
await deleteOldSandboxes();
|
||||
|
||||
try {
|
||||
sandbox = await createSandbox({
|
||||
artifactId
|
||||
});
|
||||
sandbox = await createSandbox({
|
||||
artifactId
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Error creating sandbox", e);
|
||||
throw e;
|
||||
console.log('Error creating sandbox', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,9 +226,11 @@ const createSandbox = async ({ artifactId }: { artifactId: string }) => {
|
||||
};
|
||||
|
||||
export const getOrCreateArtifactSandbox = async ({
|
||||
artifactId
|
||||
artifactId,
|
||||
onStepUpdate
|
||||
}: {
|
||||
artifactId: string;
|
||||
onStepUpdate: OnStepUpdateFn;
|
||||
}): Promise<{ sandbox: Sandbox }> => {
|
||||
const sandboxes = await daytona.list({
|
||||
artifactId
|
||||
@@ -237,10 +241,26 @@ export const getOrCreateArtifactSandbox = async ({
|
||||
let sandbox: Sandbox;
|
||||
|
||||
if (existingSandbox) {
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.CREATE_SANDBOX,
|
||||
status: 'in-progress',
|
||||
text: 'Getting existing workspace...'
|
||||
});
|
||||
sandbox = await daytona.get(existingSandbox.id);
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.CREATE_SANDBOX,
|
||||
status: 'completed',
|
||||
text: 'Workspace found'
|
||||
});
|
||||
} else {
|
||||
console.log('Workspace not found, creating...');
|
||||
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.CREATE_SANDBOX,
|
||||
status: 'in-progress',
|
||||
text: 'Creating workspace...'
|
||||
});
|
||||
|
||||
sandbox = await createSandbox({
|
||||
artifactId
|
||||
});
|
||||
@@ -252,6 +272,18 @@ export const getOrCreateArtifactSandbox = async ({
|
||||
console.log('Creating artifact directory');
|
||||
await sandbox.fs.createFolder(cwd, '755');
|
||||
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.CREATE_SANDBOX,
|
||||
status: 'completed',
|
||||
text: 'Sandbox created'
|
||||
});
|
||||
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.REPOSITORY_SETUP,
|
||||
status: 'in-progress',
|
||||
text: 'Setting up repository...'
|
||||
});
|
||||
|
||||
const ls = await sandbox.process.executeCommand('npm install -g bun', cwd, {}, 30 * 1000);
|
||||
|
||||
console.log('ls', ls);
|
||||
@@ -266,16 +298,46 @@ export const getOrCreateArtifactSandbox = async ({
|
||||
|
||||
console.log('Template cloned', clone);
|
||||
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.REPOSITORY_SETUP,
|
||||
status: 'completed',
|
||||
text: 'Repository set up'
|
||||
});
|
||||
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.INSTALL_DEPENDENCIES,
|
||||
status: 'in-progress',
|
||||
text: 'Installing dependencies...'
|
||||
});
|
||||
|
||||
console.log('Installing dependencies');
|
||||
const install = await sandbox.process.executeCommand('bun install', cwd, {}, 60);
|
||||
|
||||
console.log('Dependencies installed', install);
|
||||
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.INSTALL_DEPENDENCIES,
|
||||
status: 'completed',
|
||||
text: 'Dependencies installed'
|
||||
});
|
||||
}
|
||||
|
||||
return { sandbox };
|
||||
};
|
||||
|
||||
export async function startDevServer({ sandbox }: { sandbox: Sandbox }) {
|
||||
export async function startDevServer({
|
||||
sandbox,
|
||||
onStepUpdate
|
||||
}: {
|
||||
sandbox: Sandbox;
|
||||
onStepUpdate: OnStepUpdateFn;
|
||||
}) {
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.START_DEV_SERVER,
|
||||
status: 'in-progress',
|
||||
text: 'Starting dev server...'
|
||||
});
|
||||
|
||||
console.log('Exposing port 3000');
|
||||
const previewLink = await sandbox.getPreviewLink(3000);
|
||||
console.log('Preview link', { ...previewLink });
|
||||
@@ -307,15 +369,18 @@ export async function startDevServer({ sandbox }: { sandbox: Sandbox }) {
|
||||
if (!cmd.cmdId) {
|
||||
throw new Error('Command ID not found');
|
||||
}
|
||||
|
||||
// Wait 3 seconds
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
// Read logs
|
||||
const logs = await sandbox.process.getSessionCommandLogs('devserver', cmd.cmdId!);
|
||||
|
||||
console.log('logs', logs);
|
||||
}
|
||||
|
||||
onStepUpdate({
|
||||
id: WorkspaceStepId.START_DEV_SERVER,
|
||||
status: 'completed',
|
||||
text: 'Dev server started'
|
||||
});
|
||||
|
||||
// Wait 3 seconds
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
// Read logs
|
||||
|
||||
return {
|
||||
success: true,
|
||||
previewUrl: previewLink.url
|
||||
|
||||
@@ -46,10 +46,11 @@
|
||||
|
||||
if (dataPart.type === "data-workspace-state") {
|
||||
const data = dataPart.data as ImagineUIDataParts["workspace-state"];
|
||||
const { state, workspaceUrl } = data;
|
||||
const { state, workspaceUrl, steps } = data;
|
||||
workspaceState.set({
|
||||
ready: state === "ready",
|
||||
workspaceUrl: new SvelteURL(workspaceUrl),
|
||||
state,
|
||||
workspaceUrl: workspaceUrl ? new SvelteURL(workspaceUrl) : null,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Conversation } from '$lib/sdk/imagine';
|
||||
import { asyncWritable } from '$lib/helpers/stores';
|
||||
import type { WorkspaceStateUIDataPart } from '../../../ai-service/src/lib/ai/custom-parts/workspace-state';
|
||||
import type { SvelteURL } from 'svelte/reactivity';
|
||||
|
||||
type WorkspaceState = {
|
||||
ready: boolean;
|
||||
workspaceUrl: SvelteURL | null;
|
||||
}
|
||||
|
||||
export const showChat = writable(false);
|
||||
export const showPrompt = writable(false);
|
||||
export const conversation = asyncWritable<Conversation>();
|
||||
|
||||
|
||||
export type WorkspaceState = Omit<WorkspaceStateUIDataPart["data"], "workspaceUrl"> & {
|
||||
workspaceUrl: SvelteURL | null;
|
||||
}
|
||||
|
||||
export const workspaceState = writable<WorkspaceState>({
|
||||
ready: false,
|
||||
state: "pending",
|
||||
steps: [],
|
||||
workspaceUrl: null,
|
||||
});
|
||||
|
||||
+9
-7
@@ -12,6 +12,7 @@
|
||||
import type { EventHandler } from 'svelte/elements';
|
||||
import { onMount } from 'svelte';
|
||||
import { workspaceState } from '$lib/stores/chat';
|
||||
import WorkspaceProgress from './workspaceProgress.svelte';
|
||||
|
||||
let iframeRef: HTMLIFrameElement | null = $state(null);
|
||||
let iframeContainerRef: HTMLDivElement | null = $state(null);
|
||||
@@ -44,6 +45,12 @@
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={calculateIframeHeight} />
|
||||
|
||||
{#if $workspaceState.state === "in-progress"}
|
||||
<WorkspaceProgress />
|
||||
{/if}
|
||||
|
||||
{#if $workspaceState.state === "completed" && $workspaceState.workspaceUrl}
|
||||
<Layout.Stack direction="column" gap="s">
|
||||
<form {onsubmit}>
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
@@ -51,7 +58,6 @@
|
||||
variant="extra-compact"
|
||||
type="button"
|
||||
size="s"
|
||||
disabled={!$workspaceState.ready}
|
||||
onclick={() => alert('back button clicked')}>
|
||||
<Icon icon={IconChevronLeft} color="--fgcolor-neutral-tertiary" />
|
||||
</Button.Button>
|
||||
@@ -59,7 +65,6 @@
|
||||
variant="extra-compact"
|
||||
type="button"
|
||||
size="s"
|
||||
disabled={!$workspaceState.ready}
|
||||
onclick={() => alert('forward button clicked')}>
|
||||
<Icon icon={IconChevronRight} color="--fgcolor-neutral-tertiary" />
|
||||
</Button.Button>
|
||||
@@ -67,19 +72,16 @@
|
||||
variant="extra-compact"
|
||||
type="button"
|
||||
size="s"
|
||||
disabled={!$workspaceState.ready}
|
||||
on:click={() => (refresh = !refresh)}>
|
||||
<Icon icon={IconRefresh} color="--fgcolor-neutral-tertiary" />
|
||||
</Button.Button>
|
||||
<InputText
|
||||
disabled={!$workspaceState.ready}
|
||||
name="path"
|
||||
id="previewUrl"
|
||||
value={$workspaceState.workspaceUrl?.pathname ?? ''} />
|
||||
<Button.Button
|
||||
variant="extra-compact"
|
||||
type="button"
|
||||
disabled={!$workspaceState.ready}
|
||||
onclick={() => {
|
||||
showMobileDevice = !showMobileDevice;
|
||||
}}
|
||||
@@ -88,7 +90,6 @@
|
||||
<Button.Anchor
|
||||
variant="extra-compact"
|
||||
type="button"
|
||||
disabled={!$workspaceState.ready}
|
||||
href={$workspaceState.workspaceUrl?.toString() ?? ''}
|
||||
size="s"
|
||||
target="_blank"
|
||||
@@ -104,7 +105,7 @@
|
||||
class="iframe-container"
|
||||
class:mobile-container={showMobileDevice}
|
||||
bind:this={iframeContainerRef}>
|
||||
{#if $workspaceState.ready && $workspaceState.workspaceUrl}
|
||||
{#if $workspaceState["state"] === "completed" && $workspaceState.workspaceUrl}
|
||||
{#key refresh}
|
||||
<iframe
|
||||
src={$workspaceState.workspaceUrl.toString()}
|
||||
@@ -115,6 +116,7 @@
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.iframe-container {
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { workspaceState } from '$lib/stores/chat';
|
||||
import { IconCheckCircle } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Icon, Spinner } from '@appwrite.io/pink-svelte';
|
||||
</script>
|
||||
|
||||
<div class="workspace-progress">
|
||||
<div class="progress-steps">
|
||||
{#each $workspaceState.steps as step}
|
||||
<div class="step">
|
||||
<div class="step-status">
|
||||
{#if step.status === "in-progress"}
|
||||
<Icon icon={Spinner} size="s" />
|
||||
{/if}
|
||||
{#if step.status === "completed"}
|
||||
<Icon icon={IconCheckCircle} size="s" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="step-text">{step.text}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.workspace-progress {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.progress-steps {
|
||||
width: 240px;
|
||||
background-color: #282828;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.step {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.step:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user