From 1d2086a78cb8ae0485d55b7f178a607d3289715e Mon Sep 17 00:00:00 2001 From: Ariel Weinberger Date: Thu, 17 Jul 2025 20:35:23 -0500 Subject: [PATCH] stuck 2.0 --- ai-service/src/handlers/chat/route.ts | 119 +++++++++--------- .../lib/ai/custom-parts/workspace-state.ts | 12 ++ ai-service/src/lib/synapse-http-client.ts | 39 ++++-- ai-service/src/shared-types/index.ts | 2 + src/lib/components/studio/chat/chat.svelte | 21 +++- src/lib/stores/chat.ts | 10 ++ .../studio/artifact-[artifact]/+page.svelte | 41 +++--- 7 files changed, 155 insertions(+), 89 deletions(-) create mode 100644 ai-service/src/lib/ai/custom-parts/workspace-state.ts diff --git a/ai-service/src/handlers/chat/route.ts b/ai-service/src/handlers/chat/route.ts index 560ca5482..7ab738ff0 100644 --- a/ai-service/src/handlers/chat/route.ts +++ b/ai-service/src/handlers/chat/route.ts @@ -6,15 +6,10 @@ import { createUIMessageStream, createUIMessageStreamResponse, streamText, - TextPart, + TextPart } from 'ai'; import { createRuntimeContext, WriterType } from '../../lib/ai/mastra/utils/runtime-context'; import { mastra } from '../../lib/ai/mastra'; -import { exec as _exec } from 'child_process'; -import { promisify } from 'util'; -const exec = promisify(_exec); -import fs from 'fs'; -import path from 'path'; import { createImagineClient } from '@/lib/imagine/create-artifact-client'; import { anthropic } from '@ai-sdk/anthropic'; import { Workspace } from '@/lib/imagine/workspaces-api-client'; @@ -26,6 +21,9 @@ export const handleChatRequest = async (c: Context) => { const signal = c.req.raw.signal; const token = c.req.header('X-Imagine-Token'); + + console.log('token', token); + if (!token) { return c.json({ error: 'Unauthorized' }, 401); } @@ -34,7 +32,6 @@ export const handleChatRequest = async (c: Context) => { /** Create workspace */ - // Parse request body try { const json = await c.req.json(); @@ -54,8 +51,8 @@ export const handleChatRequest = async (c: Context) => { // save to file // return c.body(null, 200); return streamText({ - model: anthropic("claude-3-7-sonnet-20250219"), - messages: convertToModelMessages(messages), + model: anthropic('claude-3-7-sonnet-20250219'), + messages: convertToModelMessages(messages) }).toUIMessageStreamResponse(); } @@ -67,78 +64,74 @@ export const handleChatRequest = async (c: Context) => { const imagineConvo = await imagineClient.getConversation(artifactId, conversationId); if (!imagineConvo) { - throw new Error("Conversation not found"); + throw new Error('Conversation not found'); } // Create workspace - console.log(`===== create workspace =====`); - let workspace: Workspace; - let isFound = false; + const workspaceUrl = `${process.env.WORKSPACE_URL_PROTOCOL}://${artifactId}.${process.env.WORKSPACE_URL_DOMAIN}:${process.env.WORKSPACE_URL_PORT}`; + console.log('workspaceUrl', workspaceUrl); - try{ + try { + console.log('Getting workspace'); workspace = await workspacesClient.get(artifactId); + console.log('Found existing workspace', workspace); } catch (error) { - if ((error as AppwriteException).type === "workspace_not_found") { - console.log("Workspace not found, creating workspace"); - isFound = false; + if ((error as AppwriteException).type === 'workspace_not_found') { + console.log('Workspace not found, creating...'); + workspace = await workspacesClient.create(artifactId, artifactId, "s-1vcpu-1gb"); + console.log('Created new workspace', workspace); + console.log('Creating proxy rule'); + console.time("createWorkspaceProxyRule"); + const proxyRule = await workspacesClient.createWorkspaceProxyRule( + `${artifactId}.functions.localhost`, + workspace.$id + ); + console.timeEnd("createWorkspaceProxyRule"); + console.log('Created proxy rule', proxyRule); } else { throw error; } } - - if (!isFound) { - console.log("Creating workspce") - workspace = await workspacesClient.create(artifactId, artifactId); - console.log("Workspace", workspace); - console.log("Creating proxy rule") - const proxyRule = await workspacesClient.createWorkspaceProxyRule(`${artifactId}.functions.localhost`, workspace.$id); - console.log("Proxy rule", proxyRule); - console.log(`===== create workspace =====`); - } else { - console.log("Workspace already exists, skipping creation"); - } - + const synapseClient = createSynapseClient({ artifactId }); - - const result = await synapseClient.listFilesInDir({ - dirPath: "/usr/local/server" + + console.log("Creating artifact directory"); + await synapseClient.executeCommand({ + command: "mkdir -p artifact", + cwd: "/usr/local" + }); + + console.log("Cloning template") + await synapseClient.executeCommand({ + command: "bunx giget@latest gh:appwrite/templates-for-frameworks/base-vite-template .", + cwd: "/usr/local/artifact", + timeout: 60000, }); - console.log("Result", result); + console.log("Installing dependencies") + await synapseClient.executeCommand({ + command: "bun install", + cwd: "/usr/local/artifact", + timeout: 60000, + }); - return; + console.log("Running bun dev in the background") + await synapseClient.startBackgroundProcess({ + command: "bun", + args: ["run", "dev"], + cwd: "/usr/local/artifact", + }); const convertedMessages = convertToModelMessages(messages); const latestMessage = convertedMessages[convertedMessages.length - 1]; - + const latestMessageTextPart = latestMessage.content[0] as TextPart; const restMessages = convertedMessages.slice(0, -1); - - // If it's a new conversation, we need to clone the workspace - // This is temporary and will be handled by Synapse shortly! const isNewConversation = restMessages.length === 0; - // if (isNewConversation) { - // const workspaceDir = path.resolve(process.cwd(), `./tmp/workspace/artifact/${artifactId}`); - // console.log('workspaceDir', workspaceDir); - // const exists = fs.existsSync(workspaceDir); - - // if (exists) { - // console.log('Workspace already exists, skipping clone'); - // } else { - // console.log('Workspace does not exist, creating directory...'); - // await exec(`mkdir -p ${workspaceDir}`); - // console.log("Cloning template 'base-vite-template'..."); - // await exec(`pnpx degit appwrite/templates-for-frameworks/base-vite-template .`, { - // cwd: workspaceDir - // }); - // } - // } else { - // console.log('Not a new conversation, skipping workspace clone'); - // } let didError = false; @@ -153,6 +146,16 @@ export const handleChatRequest = async (c: Context) => { isFirstMessage: isNewConversation, signal }); + + writer.write({ + type: "data-workspace-state", + data: { + state: "ready", + workspaceUrl, + }, + transient: true + }) + c.set('runtimeContext', runtimeContext); const run = await mastra.getWorkflow('codeWorkflow').createRunAsync(); @@ -191,7 +194,7 @@ export const handleChatRequest = async (c: Context) => { const { messages } = event; - console.log("finalMessagesToSave", JSON.stringify(messages, null, 2)); + console.log('finalMessagesToSave', JSON.stringify(messages, null, 2)); console.log('Saving messages to imagine'); await imagineClient.updateConversation( diff --git a/ai-service/src/lib/ai/custom-parts/workspace-state.ts b/ai-service/src/lib/ai/custom-parts/workspace-state.ts new file mode 100644 index 000000000..5b7fbbdbb --- /dev/null +++ b/ai-service/src/lib/ai/custom-parts/workspace-state.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const workspaceStateUIDataPartSchema = z.object({ + state: z.enum(["ready"]), + workspaceUrl: z.string(), + }); + +export interface WorkspaceStateUIDataPart { + type: "workspace-state"; + id: string; + data: z.infer; +} diff --git a/ai-service/src/lib/synapse-http-client.ts b/ai-service/src/lib/synapse-http-client.ts index 7125b52cd..66454e24f 100644 --- a/ai-service/src/lib/synapse-http-client.ts +++ b/ai-service/src/lib/synapse-http-client.ts @@ -19,8 +19,7 @@ export class SynapseHTTPClient { constructor({ endpoint, artifactId }: { endpoint: string; artifactId: string }) { this.endpoint = endpoint; - // this.artifactBasePath = `artifact/${artifactId}`; - this.artifactBasePath = `/usr/local/artifact`; + this.artifactBasePath = `/usr/local`; this.artifactId = artifactId; } @@ -121,6 +120,31 @@ export class SynapseHTTPClient { return response; } + async startBackgroundProcess({ + command, + args, + cwd + }: { + command: string; + args: string[]; + cwd: string; + }): Promise<{ success: true; pid: number } | { success: false; error: string }> { + console.log('startBackgroundProcess', { command, cwd, args }); + + const response = await this.request({ + type: 'stateless', + operation: 'startBackgroundProcess', + params: { + command, + args, + cwd + } + }); + + console.log("Response", response); + + return response; + } async executeCommand({ command, cwd, @@ -135,16 +159,15 @@ export class SynapseHTTPClient { output: string; exitCode: number; }> { - const safeCwd = _path.join(this.artifactBasePath, cwd); - console.log('executeCommand', { command, cwd: safeCwd }); + console.log('executeCommand', { command, cwd }); try { const response = await this.request({ - type: 'terminal', + type: 'stateless', operation: 'executeCommand', params: { command, - cwd: safeCwd, + cwd, timeout } }); @@ -195,7 +218,7 @@ export class SynapseHTTPClient { } }); - console.log("Response", response); + console.log('Response', response); const data: ListFilesInDirResult[] = response.data; @@ -211,8 +234,6 @@ export class SynapseHTTPClient { private async request(body: Record) { const endpoint = `${this.endpoint}`; - console.log("Endpoint", endpoint); - const response = await fetch(endpoint, { method: 'POST', headers: { diff --git a/ai-service/src/shared-types/index.ts b/ai-service/src/shared-types/index.ts index c2c3f4fa2..c782fd99a 100644 --- a/ai-service/src/shared-types/index.ts +++ b/ai-service/src/shared-types/index.ts @@ -1,5 +1,6 @@ import { checkpointUIDataPartSchema } from '@/lib/ai/custom-parts/checkpoint'; import { thinkingUIDataPartSchema } from '@/lib/ai/custom-parts/thinking'; +import { workspaceStateUIDataPartSchema } from '@/lib/ai/custom-parts/workspace-state'; import { fileTools } from '@/lib/ai/mastra/tools/file-tools'; import { InferUIDataParts, InferUITool, ToolUIPart, UIMessage } from 'ai'; @@ -16,6 +17,7 @@ export type ImagineUIToolParts = ToolUIPart; export type ImagineUIDataParts = InferUIDataParts<{ checkpoint: typeof checkpointUIDataPartSchema; thinking: typeof thinkingUIDataPartSchema; + "workspace-state": typeof workspaceStateUIDataPartSchema; }>; export type ImagineUIMessage = UIMessage; \ No newline at end of file diff --git a/src/lib/components/studio/chat/chat.svelte b/src/lib/components/studio/chat/chat.svelte index f529e0008..e6726f5d3 100644 --- a/src/lib/components/studio/chat/chat.svelte +++ b/src/lib/components/studio/chat/chat.svelte @@ -15,10 +15,11 @@ import { studio } from '../studio.svelte'; import UpgradePrompt from '$routes/(console)/project-[region]-[project]/studio/artifact-[artifact]/upgradePrompt.svelte'; import { sdk } from '$lib/stores/sdk'; - import { conversation, showChat } from '$lib/stores/chat'; + import { conversation, showChat, workspaceState } from '$lib/stores/chat'; import { DefaultChatTransport } from 'ai'; - import type { ImagineUIMessage } from '$shared-types'; + import type { ImagineUIDataParts, ImagineUIMessage } from '$shared-types'; import { VARS } from '../../../system'; + import { SvelteURL } from 'svelte/reactivity'; type Props = { width: number; @@ -39,7 +40,19 @@ transport: new DefaultChatTransport({ api: `${VARS.AI_SERVICE_BASE_URL}/api/chat`, }), - messages: $conversation.data.messages ?? [] + messages: $conversation.data.messages ?? [], + onData: (dataPart) => { + console.log("data", dataPart); + + if (dataPart.type === "data-workspace-state") { + const data = dataPart.data as ImagineUIDataParts["workspace-state"]; + const { state, workspaceUrl } = data; + workspaceState.set({ + ready: state === "ready", + workspaceUrl: new SvelteURL(workspaceUrl), + }); + } + } }); const onkeydown: EventHandler = (event) => { @@ -62,8 +75,6 @@ if (chatTextareaRef && !$isSmallViewport) { chatTextareaRef.focus(); } - - refreshToken(); // TODO: remove }); async function refreshToken() { diff --git a/src/lib/stores/chat.ts b/src/lib/stores/chat.ts index e63c0bf3a..fe22149b7 100644 --- a/src/lib/stores/chat.ts +++ b/src/lib/stores/chat.ts @@ -1,7 +1,17 @@ import { writable } from 'svelte/store'; import type { Conversation } from '$lib/sdk/imagine'; import { asyncWritable } from '$lib/helpers/stores'; +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(); +export const workspaceState = writable({ + ready: false, + workspaceUrl: null, +}); diff --git a/src/routes/(console)/project-[region]-[project]/studio/artifact-[artifact]/+page.svelte b/src/routes/(console)/project-[region]-[project]/studio/artifact-[artifact]/+page.svelte index fd5b9a1d2..c32c3e2fe 100644 --- a/src/routes/(console)/project-[region]-[project]/studio/artifact-[artifact]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/studio/artifact-[artifact]/+page.svelte @@ -9,13 +9,9 @@ } from '@appwrite.io/pink-icons-svelte'; import { Layout, Icon, Divider, Button } from '@appwrite.io/pink-svelte'; import { previewFrameRef } from '$routes/(console)/project-[region]-[project]/store'; - import { SvelteURL } from 'svelte/reactivity'; import type { EventHandler } from 'svelte/elements'; import { onMount } from 'svelte'; - - // let previewUrl = new SvelteURL('https://preview.torsten.work'); - // let previewUrl = new SvelteURL('http://localhost:1234'); - let previewUrl = new SvelteURL('http://localhost:5173'); + import { workspaceState } from '$lib/stores/chat'; let iframeRef: HTMLIFrameElement | null = $state(null); let iframeContainerRef: HTMLDivElement | null = $state(null); @@ -38,8 +34,8 @@ event.preventDefault(); const formData = new FormData(event.currentTarget); const path = formData.get('path'); - if (typeof path === 'string') { - previewUrl.pathname = path; + if (typeof path === 'string' && $workspaceState.workspaceUrl) { + $workspaceState.workspaceUrl.pathname = path; } }; @@ -55,6 +51,7 @@ variant="extra-compact" type="button" size="s" + disabled={!$workspaceState.ready} onclick={() => alert('back button clicked')}> @@ -62,6 +59,7 @@ variant="extra-compact" type="button" size="s" + disabled={!$workspaceState.ready} onclick={() => alert('forward button clicked')}> @@ -69,13 +67,19 @@ variant="extra-compact" type="button" size="s" + disabled={!$workspaceState.ready} on:click={() => (refresh = !refresh)}> - + { showMobileDevice = !showMobileDevice; }} @@ -84,7 +88,8 @@ @@ -99,14 +104,16 @@ class="iframe-container" class:mobile-container={showMobileDevice} bind:this={iframeContainerRef}> - + {#if $workspaceState.ready && $workspaceState.workspaceUrl} + {#key refresh} + + {/key} + {/if}