mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
stuck 2.0
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>;
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
+24
-17
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user