stuck 2.0

This commit is contained in:
Ariel Weinberger
2025-07-17 20:35:23 -05:00
parent 0b932c99f4
commit 1d2086a78c
7 changed files with 155 additions and 89 deletions
+61 -58
View File
@@ -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(
@@ -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<typeof workspaceStateUIDataPartSchema>;
}
+30 -9
View File
@@ -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<string, unknown>) {
const endpoint = `${this.endpoint}`;
console.log("Endpoint", endpoint);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
+2
View File
@@ -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<ImagineTools>;
export type ImagineUIDataParts = InferUIDataParts<{
checkpoint: typeof checkpointUIDataPartSchema;
thinking: typeof thinkingUIDataPartSchema;
"workspace-state": typeof workspaceStateUIDataPartSchema;
}>;
export type ImagineUIMessage = UIMessage<never, ImagineUIDataParts, ImagineTools>;
+16 -5
View File
@@ -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<KeyboardEvent, HTMLTextAreaElement> = (event) => {
@@ -62,8 +75,6 @@
if (chatTextareaRef && !$isSmallViewport) {
chatTextareaRef.focus();
}
refreshToken(); // TODO: remove
});
async function refreshToken() {
+10
View File
@@ -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<Conversation>();
export const workspaceState = writable<WorkspaceState>({
ready: false,
workspaceUrl: null,
});
@@ -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')}>
<Icon icon={IconChevronLeft} color="--fgcolor-neutral-tertiary" />
</Button.Button>
@@ -62,6 +59,7 @@
variant="extra-compact"
type="button"
size="s"
disabled={!$workspaceState.ready}
onclick={() => alert('forward button clicked')}>
<Icon icon={IconChevronRight} color="--fgcolor-neutral-tertiary" />
</Button.Button>
@@ -69,13 +67,19 @@
variant="extra-compact"
type="button"
size="s"
disabled={!$workspaceState.ready}
on:click={() => (refresh = !refresh)}>
<Icon icon={IconRefresh} color="--fgcolor-neutral-tertiary" />
</Button.Button>
<InputText name="path" id="previewUrl" value={previewUrl.pathname} />
<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;
}}
@@ -84,7 +88,8 @@
<Button.Anchor
variant="extra-compact"
type="button"
href={previewUrl.toString()}
disabled={!$workspaceState.ready}
href={$workspaceState.workspaceUrl?.toString() ?? ''}
size="s"
target="_blank"
><Icon icon={IconExternalLink} color="--fgcolor-neutral-tertiary" />
@@ -99,14 +104,16 @@
class="iframe-container"
class:mobile-container={showMobileDevice}
bind:this={iframeContainerRef}>
<!-- {#key refresh}
<iframe
src={previewUrl.toString()}
bind:this={iframeRef}
id="preview-iframe"
title="preview">
</iframe>
{/key} -->
{#if $workspaceState.ready && $workspaceState.workspaceUrl}
{#key refresh}
<iframe
src={$workspaceState.workspaceUrl}
bind:this={iframeRef}
id="preview-iframe"
title="preview">
</iframe>
{/key}
{/if}
</div>
<style lang="scss">