This commit is contained in:
Ariel Weinberger
2025-07-16 14:56:32 -05:00
parent 2573da0708
commit d2a2a3a588
29 changed files with 957 additions and 769 deletions
+2 -1
View File
@@ -3,4 +3,5 @@ PUBLIC_CONSOLE_FEATURE_FLAGS=
PUBLIC_APPWRITE_MULTI_REGION=false
PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1
PUBLIC_STRIPE_KEY=
PUBLIC_GROWTH_ENDPOINT=
PUBLIC_GROWTH_ENDPOINT=
PUBLIC_AI_SERVICE_BASE_URL=http://localhost:8889
+50 -49
View File
@@ -15,21 +15,23 @@ const exec = promisify(_exec);
import fs from 'fs';
import path from 'path';
import { createImagineClient } from '@/lib/imagine/create-artifact-client';
import { IMAGINE_JWT } from '@/lib/constants';
export const handleChatRequest = async (c: Context) => {
console.log("incoming request");
const signal = c.req.raw.signal;
const token = c.req.header('X-Imagine-Token');
if (!token) {
return c.json({ error: 'Unauthorized' }, 401);
}
let body: ChatRequestBodyType;
// Parse request body
try {
const json = await c.req.json();
// console.log("json", json);
body = chatRequestBodySchema.parse(json);
} catch (error) {
if (error instanceof z.ZodError) {
console.log("zod error", error);
return c.json({ errors: error.issues }, 400);
}
@@ -37,51 +39,47 @@ export const handleChatRequest = async (c: Context) => {
return c.json('An unknown error occurred', 500);
}
const { id: conversationId, messages, trigger } = body;
const { id: conversationId, messages, trigger, artifactId, projectId } = body;
if (trigger === "submit-tool-result") {
// Skip
return c.body(null, 200);
if (trigger === 'submit-tool-result') {
// Skip
return c.body(null, 200);
}
console.log("body", body);
const artifactId = process.env.IMAGINE_ARTIFACT_ID!; // TODO: dynamically from body
const projectId = process.env.IMAGINE_PROJECT_ID!; // TODO: dynamically from body
const imagineClient = await createImagineClient({
projectId,
token: IMAGINE_JWT, // TODO: use the token from the request
projectId,
token // TODO: use the token from the request
});
const convos = await imagineClient.listConversations(artifactId);
const imagineConvo = convos.conversations[0];
const uiMessages = imagineConvo.messages.messages;
const isNewConversation = uiMessages.length === 0;
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);
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 });
console.log("Installing dependencies...");
await exec(`bun install`, { cwd: 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
});
console.log('Installing dependencies...');
await exec(`bun install`, { cwd: workspaceDir });
}
} else {
console.log("Not a new conversation, skipping workspace clone");
console.log('Not a new conversation, skipping workspace clone');
}
let didError = false;
@@ -90,22 +88,28 @@ export const handleChatRequest = async (c: Context) => {
originalMessages: messages,
execute: async (params) => {
const writer = params.writer as WriterType;
const runtimeContext = createRuntimeContext({ writer, artifactId, restMessages, isFirstMessage: isNewConversation, signal });
const runtimeContext = createRuntimeContext({
writer,
artifactId,
restMessages,
isFirstMessage: isNewConversation,
signal
});
const run = await mastra.getWorkflow('codeWorkflow').createRunAsync();
const result = run.stream({
inputData: {
userPrompt: latestMessageTextPart.text
userPrompt: latestMessageTextPart.text
},
runtimeContext
});
writer.write({
type: 'start',
type: 'start'
});
writer.write({
type: 'start-step'
type: 'start-step'
});
for await (const chunk of result.stream) {
@@ -117,7 +121,7 @@ export const handleChatRequest = async (c: Context) => {
});
writer.write({
type: 'finish',
type: 'finish'
});
},
onFinish: async (event) => {
@@ -128,17 +132,14 @@ export const handleChatRequest = async (c: Context) => {
const { messages } = event;
console.log("Saving messages to imagine");
await imagineClient.updateConversation(artifactId, imagineConvo.$id, "Test Conversation", { messages } as any);
console.log("Messages saved to imagine");
// await updateConversation({
// conversation: {
// ...conversation,
// uiMessages: messages,
// },
// artifactId,
// projectId,
// });
console.log('Saving messages to imagine');
await imagineClient.updateConversation(
artifactId,
imagineConvo.$id,
'Test Conversation',
messages
);
console.log('Messages saved to imagine');
},
onError: (error) => {
didError = true;
+2 -2
View File
@@ -1,8 +1,8 @@
import { UIMessage } from "ai";
import { z } from "zod";
import { ImagineUIMessage } from "@/shared-types";
export const chatRequestBodySchema = z.object({
messages: z.array(z.custom<UIMessage>()).min(1),
messages: z.array(z.custom<ImagineUIMessage>()).min(1),
trigger: z.string(),
id: z.string(),
artifactId: z.string(),
+13 -5
View File
@@ -1,4 +1,3 @@
import { IMAGINE_JWT } from "@/lib/constants";
import { createImagineClient } from "@/lib/imagine/create-artifact-client";
import { Conversation } from "@/lib/imagine/imagine-api-client";
import { Context } from "hono";
@@ -7,20 +6,24 @@ const mapConversation = (conversation: Conversation) => {
return {
id: conversation.$id,
name: conversation.name,
messages: conversation.messages.messages,
messages: conversation.messages,
};
};
export const getConversation = async (c: Context) => {
console.log("getConversation");
const { conversationId } = c.req.param();
const token = c.req.header("X-Imagine-Token");
if (!token) {
return c.json({ error: "Unauthorized" }, 401);
}
const projectId = process.env.IMAGINE_PROJECT_ID!;
const artifactId = process.env.IMAGINE_ARTIFACT_ID!;
const imagineClient = await createImagineClient({
projectId,
token: IMAGINE_JWT, // TODO: use the token from the request
token
});
const conversation = await imagineClient.getConversation(artifactId, conversationId);
@@ -32,10 +35,15 @@ export const getConversations = async (c: Context) => {
console.log("getConversations");
const projectId = process.env.IMAGINE_PROJECT_ID!;
const artifactId = process.env.IMAGINE_ARTIFACT_ID!;
const token = c.req.header("X-Imagine-Token");
if (!token) {
return c.json({ error: "Unauthorized" }, 401);
}
const imagineClient = await createImagineClient({
projectId,
token: IMAGINE_JWT, // TODO: use the token from the request
token
});
const conversations = await imagineClient.listConversations(artifactId);
@@ -1,13 +1,13 @@
import { z } from "zod";
export const checkpointDataUIPartSchema = z.object({
export const checkpointUIDataPartSchema = z.object({
commitSha: z.string(),
commitMessage: z.string(),
timestamp: z.string(),
});
export interface CheckpointDataUIPart {
export interface CheckpointUIDataPart {
type: "checkpoint";
id: string;
data: z.infer<typeof checkpointDataUIPartSchema>;
data: z.infer<typeof checkpointUIDataPartSchema>;
}
@@ -0,0 +1,20 @@
import { z } from "zod";
export const thinkingUIDataPartSchema = z.union([
z.object({
text: z.string(),
durationMs: z.null(),
state: z.enum(["streaming"]),
}),
z.object({
text: z.string(),
durationMs: z.number(),
state: z.enum(["done"]),
})
]);
export interface ThinkingUIDataPart {
type: "thinking";
id: string;
data: z.infer<typeof thinkingUIDataPartSchema>;
}
@@ -1,17 +1,9 @@
import { RuntimeContext } from "@mastra/core/runtime-context";
import { InferUITool } from "ai";
import { fileTools } from "../tools/file-tools";
import { UIMessage, UIMessageStreamWriter } from "ai";
import { z } from "zod";
import { checkpointDataUIPartSchema } from "../../custom-parts/checkpoint";
import { UIMessageStreamWriter } from "ai";
import { createSynapseClient, SynapseHTTPClient } from "@/lib/synapse-http-client";
import { ImagineUIMessage } from "@/shared-types";
export type WriterType = UIMessageStreamWriter<UIMessage<unknown, {
checkpoint: z.infer<typeof checkpointDataUIPartSchema>;
}, {
readFile: InferUITool<typeof fileTools.readFileTool>;
writeFile: InferUITool<typeof fileTools.writeFileTool>;
}>>;
export type WriterType = UIMessageStreamWriter<ImagineUIMessage>;
export type RuntimeContextPayload = {
writer: WriterType;
skipWritingToolCalls?: boolean;
@@ -81,10 +81,22 @@ const planStep = createStep({
const id = createIdGenerator({ size: 10 })();
// writer.write({
// type: "reasoning-start",
// id,
// });
const thinkingId = createIdGenerator({ size: 10 })();
writer.write({
type: "reasoning-start",
id,
});
id: thinkingId,
type: "data-thinking",
data: {
text: "",
durationMs: null,
state: "streaming",
}
})
const startTime = Date.now();
@@ -161,20 +173,21 @@ ${userPrompt}
const delta = partialObject.plan.slice(previousPlan.length);
previousPlan = partialObject.plan;
writer.write({
type: "reasoning-delta",
id,
delta,
});
// writer.write({
// type: "data-thoughts",
// type: "reasoning-delta",
// id,
// data: {
// status: "streaming",
// text: partialObject.plan,
// }
// delta,
// });
writer.write({
type: "data-thinking",
id: thinkingId,
data: {
text: partialObject.plan ?? "",
durationMs: null,
state: "streaming",
}
});
}
console.log("[planStep] postStream", latestPartialObject);
@@ -185,26 +198,26 @@ ${userPrompt}
const endTime = Date.now();
const timeTaken = endTime - startTime;
// writer.write({
// type: "data-thoughts",
// id,
// data: {
// status: "done",
// timeTaken,
// text: latestPartialObject.plan,
// }
// });
writer.write({
type: "reasoning-end",
id,
providerMetadata: {
imagine: {
durationInMs: timeTaken,
}
} as any,
type: "data-thinking",
id: thinkingId,
data: {
text: latestPartialObject.plan,
durationMs: timeTaken,
state: "done",
}
});
// writer.write({
// type: "reasoning-end",
// id,
// providerMetadata: {
// imagine: {
// durationInMs: timeTaken,
// }
// } as any,
// });
const { plan, shouldInvolveUIDeveloper } = latestPartialObject;
return {
plan,
-175
View File
@@ -1,175 +0,0 @@
import { tool } from "ai";
import { z } from "zod";
import { createSynapseClient } from "../../synapse-http-client";
const readFileTool = tool({
name: "readFile",
description: "Read a file from the repository",
inputSchema: z.object({
path: z.string().describe("The path to the file to read"),
}),
outputSchema: z.object({
path: z.string().describe("The path to the file"),
content: z.string().describe("The content of the file"),
}),
execute: async ({ path }) => {
console.log(`[TOOL - readFile] Reading file at path: ${path}`);
const synapse = createSynapseClient();
const result = await synapse.readFile({ path });
return {
path: result.path,
content: result.content,
};
},
});
const writeFilesTool = tool({
name: "writeFiles",
description: "Write multiple files to the repository",
inputSchema: z.object({
files: z.array(z.object({
path: z.string().describe("The path to the file to write"),
content: z.string().describe("The content of the file to write"),
})),
}),
outputSchema: z.object({
successFiles: z.array(z.object({ path: z.string().describe("The path to the file") })),
errorFiles: z.array(z.object({ path: z.string().describe("The path to the file"), error: z.string().describe("The error message") })),
}),
execute: async ({ files }) => {
const { successFiles, errorFiles } = await writeFiles(files);
return {
successFiles,
errorFiles,
};
},
});
const writeFileTool = tool({
name: "writeFile",
description: "Write a single file to the repository. If the file does not exist, it will be created. If the file exists, it will be overwritten. You must provide the full file contents.",
inputSchema: z.object({
path: z.string().describe("The path to the file to write"),
content: z.string().describe("The content of the file to write"),
}),
outputSchema: z.object({
success: z.boolean().describe("Whether the file was written successfully"),
error: z.string().optional().describe("The error message if the file was not written successfully"),
}),
execute: async ({ path, content }) => {
const { errorFiles } = await writeFiles([{ path, content }]);
if (errorFiles.length > 0) {
return {
success: false,
error: errorFiles[0].error,
};
} else {
return {
success: true,
};
}
},
});
async function writeFiles(files: { path: string, content: string }[]) {
const synapse = createSynapseClient();
const successFiles: { path: string }[] = [];
const errorFiles: { path: string, error: string }[] = [];
for (const file of files) {
try {
await synapse.createOrUpdateFile({ filepath: file.path, content: file.content });
successFiles.push({ path: file.path });
} catch (error) {
errorFiles.push({ path: file.path, error: error instanceof Error ? error.message : "Unknown error" });
}
}
return {
successFiles,
errorFiles,
};
}
const listFilesInDirectoryTool = tool({
name: "listFilesInDirectory",
description: "List all files in a directory",
inputSchema: z.object({
path: z.string().default("/").describe("The path to the directory to list files from"),
recursive: z.boolean().default(false).describe("Whether to list files recursively (dig into subdirectories)"),
}),
outputSchema: z.object({
files: z.array(z.object({
path: z.string().describe("The path to the file"),
})),
}),
execute: async ({ path, recursive }) => {
console.log(`[TOOL - listFilesInDirectory] Listing files in directory at path: ${path}, recursive: ${recursive}`);
const synapse = createSynapseClient();
const files = await synapse.listFilesInDir({
dirPath: path || "/",
recursive: recursive || false,
withContent: false,
additionalIgnorePatterns: [],
})
console.log(`[TOOL - listFilesInDirectory] Found ${files.length} files in directory at path: ${path}`);
return {
files,
};
},
});
const deleteFileTool = tool({
name: "deleteFile",
description: "Delete a file from the repository",
inputSchema: z.object({
path: z.string().describe("The path to the file to delete"),
}),
outputSchema: z.object({
success: z.boolean().describe("Whether the file was deleted successfully"),
}),
execute: async ({ path }) => {
const synapse = createSynapseClient();
await synapse.deleteFile({ filepath: path });
return {
success: true,
};
},
});
const moveFileTool = tool({
name: "moveFile",
description: "Move a file from one path to another. Also useful when renaming. If the directory does not exist, it will be created.",
inputSchema: z.object({
path: z.string().describe("The path to the file to move"),
newPath: z.string().describe("The new path to the file"),
intent: z.enum(["move", "rename"]).describe("The intent of the move operation. This will help render the appropriate UI to the user."),
}),
outputSchema: z.object({
success: z.boolean().describe("Whether the file was moved successfully"),
}),
execute: async ({ path, newPath }) => {
const synapse = createSynapseClient();
await synapse.updateFilePath({ filepath: path, newPath });
return {
success: true,
};
},
});
export const fileTools = {
readFileTool,
// writeFilesTool,
writeFileTool,
listFilesInDirectoryTool,
deleteFileTool,
moveFileTool,
};
@@ -1,31 +0,0 @@
import z from "zod";
import { tool } from "ai";
export const describeThoughtTool = tool({
name: "describeThought",
description: "You use this tool to describe your thoughts in between steps, instead of outputting simple text.",
inputSchema: z.object({
thought: z.string().describe("The thought you want to describe. E.g. 'I will read all files in the repository to better understand the structure of the project.'"),
}),
execute: async ({ thought }) => {
console.log("[thought] Thought recorded", thought);
return "Thought recorded";
},
})
export const respondToUser = tool({
name: "summary",
description: "You use this tool to summarize what you did to the user after you are done.",
inputSchema: z.object({
summary: z.string().describe("The summary of what you did to the user after you are done. This supports markdown format. Keep it concise, 3-4 sentences max. No code blocks in the markdown."),
}),
execute: async ({ summary }) => {
console.log("[summary] Summary recorded", summary);
return "Summary recorded";
},
})
export const reasoningTools = {
describeThoughtTool,
respondToUser,
}
-2
View File
@@ -1,4 +1,2 @@
export const OVERRIDE_BASE_DIR = `./tmp/workspace/artifact/${process.env.ARTIFACT_ID}`;
export const ANTHROPIC_MAX_CACHE_CONTROL_BLOCKS = 4; // Anthropic only allows 4 cache control blocks
export const IMAGINE_JWT
= "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiI2ODc2ZjJlOTAwMDczNzExZjljMiIsInNlc3Npb25JZCI6IjY4NzZmMmU5NTJlZGZjODkxMWViIiwiZXhwIjoxNzUyNjM5MzAzfQ._bezYFaxf5vj_pkhEqAhR9Dy2Dzmv2iV_kVMWr-UKWE";
-3
View File
@@ -15,9 +15,6 @@ export class GitRepositoryUtils {
withContent: true,
additionalIgnorePatterns: [],
});
console.log("files", files);
return files.map((file) => ({
path: file.path,
content: file.content!,
@@ -1,6 +1,6 @@
import { ImagineUIMessage } from '@/shared-types';
import { AppwriteException } from '@appwrite.io/console';
import type { Client, Payload } from '@appwrite.io/console';
import { UIMessage } from 'ai';
export class Imagine {
client: Client;
@@ -234,7 +234,7 @@ and all associated resources will be removed.
artifactId: string,
conversationId: string,
name?: string,
messages?: UIMessage[]
messages?: ImagineUIMessage[]
): Promise<Conversation> {
if (typeof artifactId === 'undefined') {
throw new AppwriteException('Missing required parameter: "artifactId"');
@@ -569,9 +569,7 @@ export type Conversation = {
/**
* Messages (UI)
*/
messages: {
messages: UIMessage[];
};
messages: ImagineUIMessage[];
};
/**
* ConversationsMessage
+8
View File
@@ -6,6 +6,7 @@ dotenv.config({ path: path.resolve(__dirname, '../.env') });
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { HTTPException } from 'hono/http-exception';
import { fileURLToPath } from 'url';
import { handleChatRequest } from '@/handlers/chat/route';
import { getConversation, getConversations } from './handlers/conversation';
@@ -14,6 +15,13 @@ const app = new Hono();
// Middleware
app.use('*', cors());
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse();
}
console.error(err);
return new Response('Something went wrong', { status: 500 });
});
// Routes
app.post("/api/chat", handleChatRequest);
+21
View File
@@ -0,0 +1,21 @@
import { checkpointUIDataPartSchema } from '@/lib/ai/custom-parts/checkpoint';
import { thinkingUIDataPartSchema } from '@/lib/ai/custom-parts/thinking';
import { fileTools } from '@/lib/ai/mastra/tools/file-tools';
import { InferUIDataParts, InferUITool, ToolUIPart, UIMessage } from 'ai';
type ImagineTools = {
readFile: InferUITool<typeof fileTools.readFileTool>;
writeFile: InferUITool<typeof fileTools.writeFileTool>;
listFilesInDirectory: InferUITool<typeof fileTools.listFilesInDirectoryTool>;
deleteFile: InferUITool<typeof fileTools.deleteFileTool>;
moveFile: InferUITool<typeof fileTools.moveFileTool>;
};
export type ImagineUIToolParts = ToolUIPart<ImagineTools>;
export type ImagineUIDataParts = InferUIDataParts<{
checkpoint: typeof checkpointUIDataPartSchema;
thinking: typeof thinkingUIDataPartSchema;
}>;
export type ImagineUIMessage = UIMessage<never, ImagineUIDataParts, ImagineTools>;
+2 -2
View File
@@ -6,7 +6,6 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
@@ -14,7 +13,8 @@
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [],
"composite": true,
"declaration": true,
"paths": {
"@/*": ["./src/*"]
}
+2 -2
View File
@@ -8,7 +8,7 @@
"dev": "vite dev",
"build": "node build.js",
"preview": "vite preview",
"prepare": "svelte-kit sync",
"prepare": "svelte-kit sync || echo ''",
"clean": "rm -rf node_modules && rm -rf .svelte_kit && pnpm i --force",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
@@ -81,7 +81,7 @@
"prettier-plugin-svelte": "^3.3.3",
"rehype-highlight": "^7.0.2",
"sass": "^1.86.0",
"svelte": "^5.25.3",
"svelte": "^5.36.3",
"svelte-check": "^4.1.5",
"svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2",
+85 -85
View File
@@ -10,7 +10,7 @@ importers:
dependencies:
'@ai-sdk/svelte':
specifier: 3.0.0-beta.19
version: 3.0.0-beta.19(svelte@5.25.3)(zod@3.25.67)
version: 3.0.0-beta.19(svelte@5.36.3)(zod@3.25.67)
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@a5e5564
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@a5e5564
@@ -19,19 +19,19 @@ importers:
version: 0.25.0
'@appwrite.io/pink-icons-svelte':
specifier: ^2.0.0-RC.1
version: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3)
version: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.36.3)
'@appwrite.io/pink-legacy':
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778(svelte@5.25.3)
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778(svelte@5.36.3)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
'@sentry/sveltekit':
specifier: ^8.38.0
version: 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
version: 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@stripe/stripe-js':
specifier: ^3.5.0
version: 3.5.0
@@ -79,10 +79,10 @@ importers:
version: 2.0.1
svelte-confetti:
specifier: ^1.4.0
version: 1.4.0(svelte@5.25.3)
version: 1.4.0(svelte@5.36.3)
svelte-exmarkdown:
specifier: ^5.0.0
version: 5.0.1(svelte@5.25.3)
version: 5.0.1(svelte@5.36.3)
tippy.js:
specifier: ^6.3.7
version: 6.3.7
@@ -95,22 +95,22 @@ importers:
version: 9.24.0
'@melt-ui/pp':
specifier: ^0.3.2
version: 0.3.2(@melt-ui/svelte@0.86.5(svelte@5.25.3))(svelte@5.25.3)
version: 0.3.2(@melt-ui/svelte@0.86.5(svelte@5.36.3))(svelte@5.36.3)
'@melt-ui/svelte':
specifier: ^0.86.5
version: 0.86.5(svelte@5.25.3)
version: 0.86.5(svelte@5.36.3)
'@playwright/test':
specifier: ^1.51.1
version: 1.51.1
'@sveltejs/adapter-static':
specifier: ^3.0.8
version: 3.0.8(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))
version: 3.0.8(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))
'@sveltejs/kit':
specifier: ^2.20.2
version: 2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
version: 2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/vite-plugin-svelte':
specifier: ^5.0.3
version: 5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
version: 5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@testing-library/dom':
specifier: ^10.4.0
version: 10.4.0
@@ -119,7 +119,7 @@ importers:
version: 6.6.3
'@testing-library/svelte':
specifier: ^5.2.4
version: 5.2.7(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))(vitest@3.0.9)
version: 5.2.7(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))(vitest@3.0.9)
'@testing-library/user-event':
specifier: ^14.6.1
version: 14.6.1(@testing-library/dom@10.4.0)
@@ -158,7 +158,7 @@ importers:
version: 10.1.1(eslint@9.23.0)
eslint-plugin-svelte:
specifier: ^3.3.3
version: 3.3.3(eslint@9.23.0)(svelte@5.25.3)
version: 3.3.3(eslint@9.23.0)(svelte@5.36.3)
globals:
specifier: ^16.0.0
version: 16.0.0
@@ -176,7 +176,7 @@ importers:
version: 3.5.3
prettier-plugin-svelte:
specifier: ^3.3.3
version: 3.3.3(prettier@3.5.3)(svelte@5.25.3)
version: 3.3.3(prettier@3.5.3)(svelte@5.36.3)
rehype-highlight:
specifier: ^7.0.2
version: 7.0.2
@@ -184,14 +184,14 @@ importers:
specifier: ^1.86.0
version: 1.86.0
svelte:
specifier: ^5.25.3
version: 5.25.3
specifier: ^5.36.3
version: 5.36.3
svelte-check:
specifier: ^4.1.5
version: 4.1.5(picomatch@4.0.2)(svelte@5.25.3)(typescript@5.8.2)
version: 4.1.5(picomatch@4.0.2)(svelte@5.36.3)(typescript@5.8.2)
svelte-preprocess:
specifier: ^6.0.3
version: 6.0.3(@babel/core@7.26.10)(postcss-load-config@3.1.4(postcss@8.5.3))(postcss@8.5.3)(sass@1.86.0)(svelte@5.25.3)(typescript@5.8.2)
version: 6.0.3(@babel/core@7.26.10)(postcss-load-config@3.1.4(postcss@8.5.3))(postcss@8.5.3)(sass@1.86.0)(svelte@5.36.3)(typescript@5.8.2)
svelte-sequential-preprocessor:
specifier: ^2.0.2
version: 2.0.2
@@ -2121,8 +2121,8 @@ packages:
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
engines: {node: '>=0.10'}
esrap@1.4.5:
resolution: {integrity: sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==}
esrap@2.1.0:
resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
@@ -3312,8 +3312,8 @@ packages:
resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==}
engines: {node: '>=16'}
svelte@5.25.3:
resolution: {integrity: sha512-J9rcZ/xVJonAoESqVGHHZhrNdVbrCfkdB41BP6eiwHMoFShD9it3yZXApVYMHdGfCshBsZCKsajwJeBbS/M1zg==}
svelte@5.36.3:
resolution: {integrity: sha512-0NsgzxP60BXZ0SDAJzFb4CEIuHK5Qtubx69bVSDSORvS053+IZqA+wKK1b742US4eCuKo1KJOu7sAB+vRpK9JQ==}
engines: {node: '>=18'}
symbol-tree@3.2.4:
@@ -3701,11 +3701,11 @@ snapshots:
dependencies:
json-schema: 0.4.0
'@ai-sdk/svelte@3.0.0-beta.19(svelte@5.25.3)(zod@3.25.67)':
'@ai-sdk/svelte@3.0.0-beta.19(svelte@5.36.3)(zod@3.25.67)':
dependencies:
'@ai-sdk/provider-utils': 3.0.0-beta.3(zod@3.25.67)
ai: 5.0.0-beta.19(zod@3.25.67)
svelte: 5.25.3
svelte: 5.36.3
optionalDependencies:
zod: 3.25.67
@@ -3750,13 +3750,13 @@ snapshots:
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@a5e5564': {}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870(svelte@5.36.3)':
dependencies:
svelte: 5.25.3
svelte: 5.36.3
'@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.36.3)':
dependencies:
svelte: 5.25.3
svelte: 5.36.3
'@appwrite.io/pink-icons@0.25.0': {}
@@ -3767,20 +3767,20 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778(svelte@5.36.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870(svelte@5.25.3)
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870(svelte@5.36.3)
'@floating-ui/dom': 1.6.13
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3)
'@melt-ui/svelte': 0.86.6(svelte@5.25.3)
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.36.3))(svelte@5.36.3)
'@melt-ui/svelte': 0.86.6(svelte@5.36.3)
ansicolor: 2.0.3
d3: 7.9.0
fuse.js: 7.1.0
pretty-bytes: 6.1.1
shiki: 1.29.2
svelte: 5.25.3
svelte-motion: 0.12.2(svelte@5.25.3)
svelte-sonner: 0.3.28(svelte@5.25.3)
svelte: 5.36.3
svelte-motion: 0.12.2(svelte@5.36.3)
svelte-sonner: 0.3.28(svelte@5.36.3)
'@asamuzakjp/css-color@3.1.1':
dependencies:
@@ -4082,21 +4082,21 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@melt-ui/pp@0.3.2(@melt-ui/svelte@0.86.5(svelte@5.25.3))(svelte@5.25.3)':
'@melt-ui/pp@0.3.2(@melt-ui/svelte@0.86.5(svelte@5.36.3))(svelte@5.36.3)':
dependencies:
'@melt-ui/svelte': 0.86.5(svelte@5.25.3)
'@melt-ui/svelte': 0.86.5(svelte@5.36.3)
estree-walker: 3.0.3
magic-string: 0.30.17
svelte: 5.25.3
svelte: 5.36.3
'@melt-ui/pp@0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3)':
'@melt-ui/pp@0.3.2(@melt-ui/svelte@0.86.6(svelte@5.36.3))(svelte@5.36.3)':
dependencies:
'@melt-ui/svelte': 0.86.6(svelte@5.25.3)
'@melt-ui/svelte': 0.86.6(svelte@5.36.3)
estree-walker: 3.0.3
magic-string: 0.30.17
svelte: 5.25.3
svelte: 5.36.3
'@melt-ui/svelte@0.86.5(svelte@5.25.3)':
'@melt-ui/svelte@0.86.5(svelte@5.36.3)':
dependencies:
'@floating-ui/core': 1.6.9
'@floating-ui/dom': 1.6.13
@@ -4104,9 +4104,9 @@ snapshots:
dequal: 2.0.3
focus-trap: 7.6.4
nanoid: 5.1.5
svelte: 5.25.3
svelte: 5.36.3
'@melt-ui/svelte@0.86.6(svelte@5.25.3)':
'@melt-ui/svelte@0.86.6(svelte@5.36.3)':
dependencies:
'@floating-ui/core': 1.6.9
'@floating-ui/dom': 1.6.13
@@ -4114,7 +4114,7 @@ snapshots:
dequal: 2.0.3
focus-trap: 7.6.4
nanoid: 5.1.5
svelte: 5.25.3
svelte: 5.36.3
'@nodelib/fs.scandir@2.1.5':
dependencies:
@@ -4696,21 +4696,21 @@ snapshots:
'@opentelemetry/semantic-conventions': 1.30.0
'@sentry/core': 8.55.0
'@sentry/svelte@8.55.0(svelte@5.25.3)':
'@sentry/svelte@8.55.0(svelte@5.36.3)':
dependencies:
'@sentry/browser': 8.55.0
'@sentry/core': 8.55.0
magic-string: 0.30.7
svelte: 5.25.3
svelte: 5.36.3
'@sentry/sveltekit@8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))':
'@sentry/sveltekit@8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))':
dependencies:
'@sentry/core': 8.55.0
'@sentry/node': 8.55.0
'@sentry/opentelemetry': 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)
'@sentry/svelte': 8.55.0(svelte@5.25.3)
'@sentry/svelte': 8.55.0(svelte@5.36.3)
'@sentry/vite-plugin': 2.22.6
'@sveltejs/kit': 2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/kit': 2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
magic-string: 0.30.7
magicast: 0.2.8
sorcery: 1.0.0
@@ -4778,13 +4778,13 @@ snapshots:
dependencies:
acorn: 8.14.1
'@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))':
'@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))':
dependencies:
'@sveltejs/kit': 2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/kit': 2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))':
'@sveltejs/kit@2.20.2(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))':
dependencies:
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@types/cookie': 0.6.0
cookie: 0.6.0
devalue: 5.1.1
@@ -4796,26 +4796,26 @@ snapshots:
sade: 1.8.1
set-cookie-parser: 2.7.1
sirv: 3.0.1
svelte: 5.25.3
svelte: 5.36.3
vite: 6.2.3(@types/node@22.13.14)(sass@1.86.0)
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))':
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))':
dependencies:
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
debug: 4.4.0
svelte: 5.25.3
svelte: 5.36.3
vite: 6.2.3(@types/node@22.13.14)(sass@1.86.0)
transitivePeerDependencies:
- supports-color
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))':
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0)))(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
debug: 4.4.0
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.17
svelte: 5.25.3
svelte: 5.36.3
vite: 6.2.3(@types/node@22.13.14)(sass@1.86.0)
vitefu: 1.0.6(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))
transitivePeerDependencies:
@@ -4846,10 +4846,10 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
'@testing-library/svelte@5.2.7(svelte@5.25.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))(vitest@3.0.9)':
'@testing-library/svelte@5.2.7(svelte@5.36.3)(vite@6.2.3(@types/node@22.13.14)(sass@1.86.0))(vitest@3.0.9)':
dependencies:
'@testing-library/dom': 10.4.0
svelte: 5.25.3
svelte: 5.36.3
optionalDependencies:
vite: 6.2.3(@types/node@22.13.14)(sass@1.86.0)
vitest: 3.0.9(@types/debug@4.1.12)(@types/node@22.13.14)(@vitest/ui@3.0.9)(jsdom@26.0.0)(sass@1.86.0)
@@ -5732,7 +5732,7 @@ snapshots:
dependencies:
eslint: 9.23.0
eslint-plugin-svelte@3.3.3(eslint@9.23.0)(svelte@5.25.3):
eslint-plugin-svelte@3.3.3(eslint@9.23.0)(svelte@5.36.3):
dependencies:
'@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0)
'@jridgewell/sourcemap-codec': 1.5.0
@@ -5744,9 +5744,9 @@ snapshots:
postcss-load-config: 3.1.4(postcss@8.5.3)
postcss-safe-parser: 7.0.1(postcss@8.5.3)
semver: 7.7.1
svelte-eslint-parser: 1.1.1(svelte@5.25.3)
svelte-eslint-parser: 1.1.1(svelte@5.36.3)
optionalDependencies:
svelte: 5.25.3
svelte: 5.36.3
transitivePeerDependencies:
- ts-node
@@ -5813,7 +5813,7 @@ snapshots:
dependencies:
estraverse: 5.3.0
esrap@1.4.5:
esrap@2.1.0:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -6799,10 +6799,10 @@ snapshots:
prelude-ls@1.2.1: {}
prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.25.3):
prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.36.3):
dependencies:
prettier: 3.5.3
svelte: 5.25.3
svelte: 5.36.3
prettier@3.5.3: {}
@@ -7116,23 +7116,23 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@4.1.5(picomatch@4.0.2)(svelte@5.25.3)(typescript@5.8.2):
svelte-check@4.1.5(picomatch@4.0.2)(svelte@5.36.3)(typescript@5.8.2):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
chokidar: 4.0.3
fdir: 6.4.3(picomatch@4.0.2)
picocolors: 1.1.1
sade: 1.8.1
svelte: 5.25.3
svelte: 5.36.3
typescript: 5.8.2
transitivePeerDependencies:
- picomatch
svelte-confetti@1.4.0(svelte@5.25.3):
svelte-confetti@1.4.0(svelte@5.36.3):
dependencies:
svelte: 5.25.3
svelte: 5.36.3
svelte-eslint-parser@1.1.1(svelte@5.25.3):
svelte-eslint-parser@1.1.1(svelte@5.36.3):
dependencies:
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@@ -7141,30 +7141,30 @@ snapshots:
postcss-scss: 4.0.9(postcss@8.5.3)
postcss-selector-parser: 7.1.0
optionalDependencies:
svelte: 5.25.3
svelte: 5.36.3
svelte-exmarkdown@5.0.1(svelte@5.25.3):
svelte-exmarkdown@5.0.1(svelte@5.36.3):
dependencies:
remark-gfm: 4.0.1
remark-parse: 11.0.0
remark-rehype: 11.1.2
svelte: 5.25.3
svelte: 5.36.3
unified: 11.0.5
transitivePeerDependencies:
- supports-color
svelte-motion@0.12.2(svelte@5.25.3):
svelte-motion@0.12.2(svelte@5.36.3):
dependencies:
'@types/react': 18.3.22
framesync: 6.1.2
popmotion: 11.0.5
style-value-types: 5.1.2
svelte: 5.25.3
svelte: 5.36.3
tslib: 2.8.1
svelte-preprocess@6.0.3(@babel/core@7.26.10)(postcss-load-config@3.1.4(postcss@8.5.3))(postcss@8.5.3)(sass@1.86.0)(svelte@5.25.3)(typescript@5.8.2):
svelte-preprocess@6.0.3(@babel/core@7.26.10)(postcss-load-config@3.1.4(postcss@8.5.3))(postcss@8.5.3)(sass@1.86.0)(svelte@5.36.3)(typescript@5.8.2):
dependencies:
svelte: 5.25.3
svelte: 5.36.3
optionalDependencies:
'@babel/core': 7.26.10
postcss: 8.5.3
@@ -7177,9 +7177,9 @@ snapshots:
svelte: 4.2.19
tslib: 2.7.0
svelte-sonner@0.3.28(svelte@5.25.3):
svelte-sonner@0.3.28(svelte@5.36.3):
dependencies:
svelte: 5.25.3
svelte: 5.36.3
svelte@4.2.19:
dependencies:
@@ -7198,7 +7198,7 @@ snapshots:
magic-string: 0.30.17
periscopic: 3.1.0
svelte@5.25.3:
svelte@5.36.3:
dependencies:
'@ampproject/remapping': 2.3.0
'@jridgewell/sourcemap-codec': 1.5.0
@@ -7209,7 +7209,7 @@ snapshots:
axobject-query: 4.1.0
clsx: 2.1.1
esm-env: 1.2.2
esrap: 1.4.5
esrap: 2.1.0
is-reference: 3.0.3
locate-character: 3.0.0
magic-string: 0.30.17
@@ -0,0 +1,7 @@
import { VARS } from "../system";
export class AIService {
private baseUrl = `${VARS.AI_SERVICE_BASE_URL}/api`;
constructor() {}
}
+29 -125
View File
@@ -10,7 +10,6 @@
} from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import Conversation from './conversation.svelte';
import type { StreamParser } from './parser';
import type { EventHandler } from 'svelte/elements';
import { page } from '$app/state';
import { studio } from '../studio.svelte';
@@ -18,44 +17,32 @@
import { sdk } from '$lib/stores/sdk';
import { conversation, showChat } from '$lib/stores/chat';
import { DefaultChatTransport } from 'ai';
import type { ImagineUIMessage } from '$shared-types';
import { VARS } from '../../../system';
type Props = {
width: number;
hasSubNavigation: boolean;
parser: StreamParser;
};
let { width, hasSubNavigation, parser }: Props = $props();
let { width, hasSubNavigation }: Props = $props();
let minimizeChat = $state($isSmallViewport ? true : false);
let message = $state('');
let firstByteReceived = $state(true);
let chatTextareaRef: HTMLTextAreaElement | null = $state(null);
const chatBody: {
token: string | null;
projectId: string;
artifactId: string | null;
conversationId: string | null;
} = {
token: null,
projectId: page.params.project,
artifactId: null,
conversationId: null
};
const chat = new Chat({
const chat = new Chat<ImagineUIMessage>({
id: $conversation.data?.$id,
transport: new DefaultChatTransport({
api: 'http://localhost:8889/api/chat',
// body: chatBody
api: `${VARS.AI_SERVICE_BASE_URL}/api/chat`,
})
});
// const chat = new Chat({
// transport: new DefaultChatTransport({
// api: 'http://localhost:8889/api/chat',
// body: chatBody
// })
// });
$effect(() => {
if ($conversation.data?.messages) {
chat.messages = $conversation.data.messages;
}
});
const onkeydown: EventHandler<KeyboardEvent, HTMLTextAreaElement> = (event) => {
if (event.key === 'Enter') {
@@ -77,116 +64,34 @@
if (chatTextareaRef && !$isSmallViewport) {
chatTextareaRef.focus();
}
refreshToken(); // TODO: remove
});
async function refreshToken() {
// const token = await sdk.forConsoleIn(page.params.region).account.createJWT();
const token = await sdk
.forProject(page.params.region, page.params.project)
.account.createJWT();
chatBody.token = token.jwt;
return token;
}
// let controller: AbortController;
async function createMessage() {
await refreshToken();
const token = await refreshToken();
console.log('$conversation', $conversation);
chatBody.artifactId = $conversation?.data?.artifactId ?? null;
chatBody.conversationId = $conversation?.data?.$id ?? null;
const group = Symbol();
if (message.startsWith('!')) {
const [type, ...segments] = message.split(' ');
const content = segments.join(' ');
message = '';
const value = `<action type="${type.replace('!', '')}">${content}</action>`;
parser.chunk(value, 'system', { group });
parser.end();
return;
}
console.log('artifactId', $conversation?.data?.artifactId);
// chatIns
chat.sendMessage({
role: "user",
text: message,
}, {
body: {
projectId: page.params.project,
artifactId: page.params.artifact,
},
headers: {
"X-Imagine-Token": token.jwt
}
});
message = '';
// const initialMessage = message;
// try {
// parser.chunk(message, 'user');
// firstByteReceived = false;
// message = '';
// controller = new AbortController();
// studio.streaming = true;
// const url = `${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}/imagine/artifacts/${$conversation.data.artifactId}/conversations/${$conversation.data.$id}/messages`;
// const response = await fetch(
// url,
// {
// method: "POST",
// headers: {
// 'Content-Type': 'application/json',
// 'X-Appwrite-Project': page.params.project,
// 'X-Appwrite-Mode': 'admin'
// },
// credentials: 'include',
// body: JSON.stringify({
// content: initialMessage,
// type: 'text'
// }),
// signal: controller.signal
// }
// )
// const response = await fetch(
// url,
// {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'X-Appwrite-Project': page.params.project,
// 'X-Appwrite-Mode': 'admin'
// },
// credentials: 'include',
// body: JSON.stringify({
// content: initialMessage,
// type: 'text'
// }),
// signal: controller.signal
// }
// );
// if (!response.ok) {
// throw new Error(`${response.status} Error: ${await response.text()}`);
// }
// invalidate(Dependencies.ARTIFACTS);
// const reader = response.body.getReader();
// const decoder = new TextDecoder();
// let chunk = await reader.read();
// while (!chunk.done) {
// if (!firstByteReceived) firstByteReceived = true;
// parser.chunk(decoder.decode(chunk.value), 'system', {
// group
// });
// chunk = await reader.read();
// }
// parser.end();
// } catch (error) {
// if (error instanceof Error) {
// parser.chunk(error.message, 'error');
// }
// message = initialMessage;
// } finally {
// firstByteReceived = true;
// studio.streaming = false;
// }
// }
}
let tokens = $state(2);
@@ -226,7 +131,7 @@
<Divider />
</div>
<Conversation {chat} {parser} thinking={!firstByteReceived} />
<Conversation {chat} />
{#if tokens < 2}
<UpgradePrompt>
@@ -242,7 +147,6 @@
{/if}
</UpgradePrompt>
{/if}
<Conversation {chat} {parser} thinking={chat.status === 'submitted'} />
{/if}
<form {onsubmit} class="input" class:minimize-chat={minimizeChat}>
<Layout.Stack
@@ -1,31 +1,19 @@
<script lang="ts">
import 'highlight.js/styles/atom-one-light.css';
import { StreamParser, type ParsedItem } from './parser';
import { Icon, Layout, ShimmerText, Tag, Typography } from '@appwrite.io/pink-svelte';
import { Icon, Spinner, Layout, Tag, Typography } from '@appwrite.io/pink-svelte';
import { IconArrowDown } from '@appwrite.io/pink-icons-svelte';
import { slide } from 'svelte/transition';
import type { UIEventHandler, WheelEventHandler } from 'svelte/elements';
import Message from './message.svelte';
import { studio } from '../studio.svelte';
import { Chat } from '@ai-sdk/svelte';
import type { ImagineUIMessage } from '$shared-types';
type Props = {
parser: StreamParser;
autoscroll?: boolean;
thinking?: boolean;
chat: Chat;
chat: Chat<ImagineUIMessage>;
};
let { autoscroll = $bindable(true), thinking = false, chat }: Props = $props();
// const chunks = writable<ParsedItem[]>([
// {
// id: Symbol(),
// from: 'user',
// group: null,
// content: "Test",
// complete: true,
// }
// ]);
let { autoscroll = $bindable(true), chat }: Props = $props();
function scrollToBottom(smooth: boolean = true) {
document
@@ -33,7 +21,7 @@
.scrollIntoView({ behavior: smooth ? 'smooth' : 'instant' });
}
const onwheel: WheelEventHandler<HTMLDivElement> = (event) => {
const onwheel: WheelEventHandler<HTMLDivElement> = () => {
if (studio.streaming) autoscroll = false;
};
@@ -51,35 +39,22 @@
autoscroll = false;
}
};
$effect(() => {
console.log("DATA", chat.data);
});
</script>
<div class="overflow" {onwheel} {onscroll}>
<section>
<!-- {#each $chunks as message (message.id)}
{#each chat.messages as message (message.id)}
<Message {message} />
{/each} -->
{#each chat.messages as message, messageIndex (messageIndex)}
<Message
message={{
id: Symbol(message.id),
from: (message.role === 'user' ? 'user' : 'assistant') as any,
group: null,
content: message.content,
complete: true
}} />
<!-- <pre style:border="1px solid black" style:padding="1rem">{JSON.stringify(message, null, 2)}</pre> -->
{/each}
{#if thinking}
<Typography.Code size="s">
<ShimmerText>thinking...</ShimmerText>
</Typography.Code>
{#if chat.status === "submitted" || chat.status === "streaming"}
<Icon size="m" icon={Spinner} />
{/if}
{#if chat.status === "error"}
<span style:color="var(--fgcolor-error)">{chat.error?.message}</span>
{/if}
<div id="bottom"></div>
</section>
@@ -0,0 +1,301 @@
<script lang="ts">
import 'highlight.js/styles/atom-one-light.css';
import type { ParsedItem } from './parser';
import { Card, Layout, ShimmerText, Spinner, Typography, Icon } from '@appwrite.io/pink-svelte';
import { IconCheckCircle } from '@appwrite.io/pink-icons-svelte';
import Markdown, { type Plugin } from 'svelte-exmarkdown';
import Li from './(markdown)/Li.svelte';
import H1 from './(markdown)/H1.svelte';
import H2 from './(markdown)/H2.svelte';
import A from './(markdown)/A.svelte';
import Strong from './(markdown)/Strong.svelte';
import Em from './(markdown)/Em.svelte';
import Ul from './(markdown)/Ul.svelte';
import Ol from './(markdown)/Ol.svelte';
import rehypeHighlight from 'rehype-highlight';
import { queue } from './queue.svelte';
import { studio } from '../studio.svelte';
type Props = {
message: ParsedItem;
};
let { message }: Props = $props();
const tickets = $state([message.group]);
async function processQueueItem() {
if (tickets.length === 0 || !message.group) return;
const ticket = tickets.pop();
if (!ticket) return;
const list = queue.lists[message.group];
if (!list || !list.some((item) => item.status === 'waiting')) {
tickets.push(ticket);
return;
}
const item = queue.dequeue(message.group);
if (!item) {
tickets.push(ticket);
return;
}
try {
const action = item.data;
switch (action.type) {
case 'file':
await studio.synapse.dispatch('fs', {
operation: 'updateFile',
params: {
filepath: action.src,
content: action.content
}
});
studio.filesystem.add(action.src);
break;
case 'shell':
await studio.synapse.dispatch(
'terminal',
{
operation: 'createCommand',
params: {
command: action.content + '\n'
}
},
{
noReturn: action.content.endsWith('run dev'),
timeout: 30_000
}
);
break;
}
if (message.group) {
const status = action.complete ? 'done' : 'waiting';
queue.update(message.group, item.id, { status });
}
} catch (error) {
console.error('Error processing queue item:', error);
if (message.group) {
queue.update(message.group, item.id, { status: 'failed' });
}
} finally {
tickets.push(ticket);
setTimeout(checkForMoreItems, 100);
}
}
function checkForMoreItems() {
if (!message.group) return;
const list = queue.lists[message.group];
if (tickets.length > 0 && list && list.some((item) => item.status === 'waiting')) {
processQueueItem();
}
}
$effect(() => {
if (!('type' in message)) return;
if (message.type !== 'actions') return;
if (!message.group) return;
const list = queue.lists[message.group];
if (!list) return;
if (tickets.length > 0 && list.some((item) => item.status === 'waiting')) {
processQueueItem();
}
});
const plugins: Plugin[] = [
{
rehypePlugin: rehypeHighlight,
renderer: {
h1: H1,
h2: H2,
a: A,
strong: Strong,
em: Em,
ul: Ul,
ol: Ol,
li: Li
}
}
];
</script>
{#if 'type' in message}
{#key message.content}
{#if message.type === 'actions'}
<Card.Base variant="primary" padding="none">
<Card.Base variant="secondary" padding="xs" class="title">
<Layout.Stack
direction="row"
alignItems="center"
justifyContent="space-between">
<Typography.Text variant="m-500">Version 0</Typography.Text>
{#if !message.complete}
<Spinner />
{/if}
</Layout.Stack>
</Card.Base>
<div class="actions">
<div class="grid-line"></div>
{#each message.actions as action}
{@const actionInQueue = queue.lists[action.group]?.find(
(n) => n.data.id === action.id
)}
{#if actionInQueue}
<Layout.Stack
direction="row"
alignItems="center"
justifyContent="space-between">
{#if action.type === 'file'}
<Typography.Code size="s" class="file">
<span class="icon">
{#if actionInQueue.status === 'done'}
<Icon
size="s"
--icon-size-s="12px"
icon={IconCheckCircle} />
{:else}
<Spinner size="s" --icon-size-s="12px" />
{/if}
</span>
{action.src}</Typography.Code>
<Typography.Code size="s">
{#if actionInQueue.status === 'waiting'}
Waiting
{:else if actionInQueue.status === 'processing'}
<ShimmerText>Generating</ShimmerText>
{:else if actionInQueue.status === 'done'}
Generated
{:else if actionInQueue.status === 'failed'}
<span style:color="var(--fgcolor-error)">Failed</span>
{/if}
</Typography.Code>
{:else if action.type === 'shell'}
<Typography.Code class="file" size="s">
<span class="icon">
{#if actionInQueue.status === 'done'}
<Icon
size="s"
--icon-size-s="12px"
icon={IconCheckCircle} />
{:else}
<Spinner size="s" --icon-size-s="12px" />
{/if}
</span>
{action.content}</Typography.Code>
<Typography.Code size="s">
{#if actionInQueue.status === 'waiting'}
Waiting
{:else if actionInQueue.status === 'processing'}
<ShimmerText>Running</ShimmerText>
{:else if actionInQueue.status === 'done'}
Completed
{:else if actionInQueue.status === 'failed'}
<span style:color="var(--fgcolor-error)">Failed</span>
{/if}
</Typography.Code>
{/if}
</Layout.Stack>
{/if}
{/each}
{#if !message.complete}
<Typography.Code size="s">
<ShimmerText>thinking...</ShimmerText>
</Typography.Code>
{/if}
</div>
</Card.Base>
{/if}
{/key}
{:else}
{#snippet text()}
<Markdown md={message.content} {plugins} />
{/snippet}
{#if message.from === 'user'}
<div class="message">
{@render text()}
</div>
{:else if message.from === 'error'}
<div class="message">
{@render text()}
</div>
{:else}
{@render text()}
{/if}
{/if}
<style lang="scss">
.message {
width: 90%;
float: right;
display: inline-flex;
padding: 0.5rem;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 0.25rem;
flex-shrink: 0;
margin-inline-start: auto;
border-radius: 0.5rem 0px 0.5rem 0.5rem;
background: var(--bgcolor-neutral-default);
box-shadow:
0px 1.022px 4.089px 0px rgba(55, 59, 77, 0.1),
0px 1.022px 4.089px -1.022px rgba(55, 59, 77, 0.1);
}
:global(.title) {
margin-top: -2px;
margin-left: -1px;
width: calc(100% + 2px) !important;
}
.actions {
padding: var(--space-6);
position: relative;
margin-left: var(--space-6);
:global(.file) {
display: flex;
align-items: center;
gap: 4px;
}
.icon {
display: flex;
align-items: center;
margin-left: -18px;
margin-right: 4px;
background-color: var(--bgcolor-neutral-primary);
flex: 0 0;
position: relative;
z-index: 10;
}
}
.grid-line {
position: absolute;
inset: 0;
width: 1px;
background: linear-gradient(
to bottom,
transparent 0%,
var(--border-neutral) 25%,
var(--border-neutral) 75%,
transparent 100%
);
}
:global(pre) {
margin: 0;
}
:global(pre code.hljs) {
padding: 0;
}
</style>
+43 -195
View File
@@ -1,8 +1,5 @@
<script lang="ts">
import 'highlight.js/styles/atom-one-light.css';
import type { ParsedItem } from './parser';
import { Card, Layout, ShimmerText, Spinner, Typography, Icon } from '@appwrite.io/pink-svelte';
import { IconCheckCircle } from '@appwrite.io/pink-icons-svelte';
import Markdown, { type Plugin } from 'svelte-exmarkdown';
import Li from './(markdown)/Li.svelte';
import H1 from './(markdown)/H1.svelte';
@@ -13,101 +10,14 @@
import Ul from './(markdown)/Ul.svelte';
import Ol from './(markdown)/Ol.svelte';
import rehypeHighlight from 'rehype-highlight';
import { queue } from './queue.svelte';
import { studio } from '../studio.svelte';
import type { ImagineUIMessage, ImagineUIToolParts } from '$shared-types';
import Thinking from './thinking.svelte';
import ToolCalls from './toolCalls.svelte';
type Props = {
message: ParsedItem;
message: ImagineUIMessage;
};
let { message }: Props = $props();
const tickets = $state([message.group]);
async function processQueueItem() {
if (tickets.length === 0 || !message.group) return;
const ticket = tickets.pop();
if (!ticket) return;
const list = queue.lists[message.group];
if (!list || !list.some((item) => item.status === 'waiting')) {
tickets.push(ticket);
return;
}
const item = queue.dequeue(message.group);
if (!item) {
tickets.push(ticket);
return;
}
try {
const action = item.data;
switch (action.type) {
case 'file':
await studio.synapse.dispatch('fs', {
operation: 'updateFile',
params: {
filepath: action.src,
content: action.content
}
});
studio.filesystem.add(action.src);
break;
case 'shell':
await studio.synapse.dispatch(
'terminal',
{
operation: 'createCommand',
params: {
command: action.content + '\n'
}
},
{
noReturn: action.content.endsWith('run dev'),
timeout: 30_000
}
);
break;
}
if (message.group) {
const status = action.complete ? 'done' : 'waiting';
queue.update(message.group, item.id, { status });
}
} catch (error) {
console.error('Error processing queue item:', error);
if (message.group) {
queue.update(message.group, item.id, { status: 'failed' });
}
} finally {
tickets.push(ticket);
setTimeout(checkForMoreItems, 100);
}
}
function checkForMoreItems() {
if (!message.group) return;
const list = queue.lists[message.group];
if (tickets.length > 0 && list && list.some((item) => item.status === 'waiting')) {
processQueueItem();
}
}
$effect(() => {
if (!('type' in message)) return;
if (message.type !== 'actions') return;
if (!message.group) return;
const list = queue.lists[message.group];
if (!list) return;
if (tickets.length > 0 && list.some((item) => item.status === 'waiting')) {
processQueueItem();
}
});
const plugins: Plugin[] = [
{
@@ -124,115 +34,53 @@
}
}
];
const organizedParts = $derived(() => {
const nonTextParts = message.parts.filter(p => p.type !== 'text');
const textParts = message.parts.filter(p => p.type === 'text');
const finalText = textParts[textParts.length - 1];
return {
nonTextParts,
toolCalls: nonTextParts.filter(p => p.type.startsWith('tool-')) as ImagineUIToolParts[],
finalText
};
});
</script>
{#if 'type' in message}
{#key message.content}
{#if message.type === 'actions'}
<Card.Base variant="primary" padding="none">
<Card.Base variant="secondary" padding="xs" class="title">
<Layout.Stack
direction="row"
alignItems="center"
justifyContent="space-between">
<Typography.Text variant="m-500">Version 0</Typography.Text>
{#if !message.complete}
<Spinner />
{/if}
</Layout.Stack>
</Card.Base>
<div class="actions">
<div class="grid-line"></div>
{#each message.actions as action}
{@const actionInQueue = queue.lists[action.group]?.find(
(n) => n.data.id === action.id
)}
{#if actionInQueue}
<Layout.Stack
direction="row"
alignItems="center"
justifyContent="space-between">
{#if action.type === 'file'}
<Typography.Code size="s" class="file">
<span class="icon">
{#if actionInQueue.status === 'done'}
<Icon
size="s"
--icon-size-s="12px"
icon={IconCheckCircle} />
{:else}
<Spinner size="s" --icon-size-s="12px" />
{/if}
</span>
{action.src}</Typography.Code>
<Typography.Code size="s">
{#if actionInQueue.status === 'waiting'}
Waiting
{:else if actionInQueue.status === 'processing'}
<ShimmerText>Generating</ShimmerText>
{:else if actionInQueue.status === 'done'}
Generated
{:else if actionInQueue.status === 'failed'}
<span style:color="var(--fgcolor-error)">Failed</span>
{/if}
</Typography.Code>
{:else if action.type === 'shell'}
<Typography.Code class="file" size="s">
<span class="icon">
{#if actionInQueue.status === 'done'}
<Icon
size="s"
--icon-size-s="12px"
icon={IconCheckCircle} />
{:else}
<Spinner size="s" --icon-size-s="12px" />
{/if}
</span>
{action.content}</Typography.Code>
<Typography.Code size="s">
{#if actionInQueue.status === 'waiting'}
Waiting
{:else if actionInQueue.status === 'processing'}
<ShimmerText>Running</ShimmerText>
{:else if actionInQueue.status === 'done'}
Completed
{:else if actionInQueue.status === 'failed'}
<span style:color="var(--fgcolor-error)">Failed</span>
{/if}
</Typography.Code>
{/if}
</Layout.Stack>
{/if}
{/each}
{#if !message.complete}
<Typography.Code size="s">
<ShimmerText>thinking...</ShimmerText>
</Typography.Code>
{/if}
</div>
</Card.Base>
<!-- User Message -->
{#if message.role === 'user'}
{#each message.parts as part, partIndex (partIndex)}
{#if part.type === 'text'}
<div class="message">
{part.text}
</div>
{/if}
{/key}
{:else}
{#snippet text()}
<Markdown md={message.content} {plugins} />
{/snippet}
{#if message.from === 'user'}
<div class="message">
{@render text()}
</div>
{:else if message.from === 'error'}
<div class="message">
{@render text()}
</div>
{:else}
{@render text()}
{/each}
{/if}
<!-- Assistant Messages -->
{#if message.role === 'assistant'}
<!-- Non-text parts -->
{#each organizedParts().nonTextParts as part, partIndex (partIndex)}
{#if part.type === 'data-thinking'}
<Thinking data={part.data} didReceiveFirstAsisstantTextChunk={part.data.text.length > 0} />
{/if}
{/each}
<!-- Tool calls -->
{#if organizedParts().toolCalls.length > 0}
<ToolCalls toolCallParts={organizedParts().toolCalls} />
{/if}
<!-- Final text -->
{#if organizedParts().finalText}
<Markdown md={organizedParts().finalText.text} {plugins} />
{/if}
{/if}
<style lang="scss">
.message {
width: 90%;
float: right;
display: inline-flex;
padding: 0.5rem;
@@ -0,0 +1,89 @@
<script lang="ts">
import { Typography, ShimmerText } from '@appwrite.io/pink-svelte';
import { IconChevronDown, IconChevronUp } from '@appwrite.io/pink-icons-svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import type { ImagineUIDataParts } from '$shared-types';
let {
data,
didReceiveFirstAsisstantTextChunk
}: { data: ImagineUIDataParts['thinking']; didReceiveFirstAsisstantTextChunk: boolean } =
$props();
let expanded = $state(true);
$effect(() => {
if (data.state === 'done' && didReceiveFirstAsisstantTextChunk) {
setTimeout(() => {
expanded = false;
}, 3000);
}
});
function toggle() {
expanded = !expanded;
}
</script>
<button class="thinking-container" onclick={toggle} type="button">
{#if data.state === 'streaming'}
<Typography.Code size="s">
<ShimmerText>Thinking...</ShimmerText>
{#if expanded && data.text}
<div class="thoughts">
{data.text}
</div>
{/if}
</Typography.Code>
{:else if data.state === 'done'}
<Typography.Code size="s">
<span class="summary">
Thought for {Math.floor(data.durationMs / 1000)} seconds
<Icon icon={expanded ? IconChevronUp : IconChevronDown} size="s" />
</span>
{#if expanded && data.text}
<div class="thoughts">
{data.text}
</div>
{/if}
</Typography.Code>
{/if}
</button>
<style>
.thinking-container {
background: none;
border: none;
padding: 0;
text-align: left;
cursor: pointer;
width: 100%;
}
.thinking-container:hover {
opacity: 0.8;
}
.summary {
display: inline-flex;
gap: 4px;
align-items: center;
justify-content: space-between;
}
.toggle-hint {
font-size: 0.8em;
opacity: 0.6;
margin-left: 0.5rem;
}
.thoughts {
margin-top: 0.5rem;
padding: 0.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
white-space: pre-wrap;
font-family: inherit;
}
</style>
@@ -0,0 +1,196 @@
<script lang="ts">
import { Icon } from '@appwrite.io/pink-svelte';
import { IconCheckCircle } from '@appwrite.io/pink-icons-svelte';
import { Spinner } from '@appwrite.io/pink-svelte';
import { slide } from 'svelte/transition';
import type { ImagineUIToolParts } from '$shared-types';
let { toolCallParts }: { toolCallParts: ImagineUIToolParts[] } = $props();
const EXPANDED_SHOW_ITEMS = 3;
let isExpanded = $state(false);
// Filter and process parts
let filteredParts = $derived(() => {
let parts = toolCallParts.filter(
(part) =>
part.type.startsWith('tool-readFile') || part.type.startsWith('tool-writeFile')
);
const showMoreButton = parts.length > EXPANDED_SHOW_ITEMS;
// If showMoreButton is true and not expanded, only show last N items
if (showMoreButton && !isExpanded) {
parts = parts.slice(-EXPANDED_SHOW_ITEMS);
}
return { parts, showMoreButton };
});
function toggleExpanded() {
isExpanded = !isExpanded;
}
</script>
{#if filteredParts().parts.length > 0}
<div class="tool-calls-container">
<div class="header">
<span class="version-text">Version 1</span>
</div>
<div class="content" class:has-more={filteredParts.length > EXPANDED_SHOW_ITEMS}>
{#each filteredParts().parts as toolCall, i (i)}
{@const isLoading = toolCall.state === 'input-available'}
<div
class="tool-item"
style:opacity={isExpanded || i < EXPANDED_SHOW_ITEMS ? 1 : 0}
transition:slide={{ duration: 300 }}>
<div class={`icon-container ${isLoading ? 'icon-xs' : ''}`}>
{#if isLoading}
<Icon icon={Spinner} size="s" />
{:else}
<Icon icon={IconCheckCircle} size="s" />
{/if}
</div>
<div class="tool-text">
{#if toolCall.type === 'tool-writeFile'}
{isLoading
? `Writing file ${toolCall.input.path}...`
: `Wrote file ${toolCall.input.path}`}
{:else if toolCall.type === 'tool-readFile'}
{isLoading
? `Reading file ${toolCall.input.path}...`
: `Read file ${toolCall.input.path}`}
{/if}
</div>
<div class="connector-line"></div>
</div>
{/each}
</div>
{#if filteredParts().showMoreButton}
<button class="show-more-btn" onclick={toggleExpanded}>
{isExpanded ? 'Show less' : 'Show more'}
</button>
{/if}
</div>
{/if}
<style>
.tool-calls-container {
position: relative;
margin-top: 0.4rem;
margin-bottom: 0.4rem;
font-size: 0.75rem;
background: white;
border: 1px solid #e5e5e5;
border-top: 0;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.header {
background: rgba(245, 245, 245, 0.5);
border: 1px solid #e5e5e5;
border-radius: 0.125rem 0.125rem 0 0;
border-left: 0;
border-right: 0;
color: #737373;
height: 2rem;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
}
.version-text {
font-size: 0.75rem;
font-weight: 500;
}
.content {
padding: 0.5rem;
overflow: hidden;
}
.content.has-more {
margin: 0.5rem 0;
}
.tool-item {
font-size: 0.725rem;
color: #a3a3a3;
height: 1.375rem;
font-family: monospace;
gap: 0.5rem;
display: flex;
align-items: center;
position: relative;
}
.icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 0.875rem;
background: white;
z-index: 10;
color: #d4d4d4;
}
.icon {
width: 0.75rem;
height: 0.75rem;
}
.tool-text {
flex: 1;
display: flex;
align-items: center;
}
.connector-line {
position: absolute;
background: #e5e5e5;
height: 0.625rem;
top: -0.3125rem;
width: 1px;
left: 0.4375rem;
}
.icon-xs {
transform: scale(0.8);
}
:global(.show-more-btn) {
font-size: 0.625rem !important;
background: #fafafa !important;
border-radius: 9999px !important;
border: 1px solid #e5e5e5 !important;
height: 1.25rem !important;
padding: 0 0.5rem !important;
position: absolute !important;
z-index: 20 !important;
left: 50% !important;
transform: translateX(-50%) translateY(50%) !important;
bottom: 0 !important;
line-height: 1.2 !important;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>
+5
View File
@@ -1,3 +1,4 @@
import type { ImagineUIMessage } from '$shared-types';
import { AppwriteException } from '@appwrite.io/console';
import type { Client, Payload } from '@appwrite.io/console';
@@ -561,6 +562,10 @@ export type Conversation = {
* Search index string.
*/
search: string;
/**
* Messages (UI)
*/
messages: ImagineUIMessage[];
};
/**
* ConversationsMessage
+2 -1
View File
@@ -17,7 +17,8 @@ export const VARS = {
APPWRITE_ENDPOINT: env.PUBLIC_APPWRITE_ENDPOINT ?? undefined,
GROWTH_ENDPOINT: env.PUBLIC_GROWTH_ENDPOINT ?? undefined,
PUBLIC_STRIPE_KEY: env.PUBLIC_STRIPE_KEY ?? undefined,
PROJECT_PROFILE: (env.PUBLIC_PROJECT_PROFILE as Profile) ?? undefined
PROJECT_PROFILE: (env.PUBLIC_PROJECT_PROFILE as Profile) ?? undefined,
AI_SERVICE_BASE_URL: env.PUBLIC_AI_SERVICE_BASE_URL ?? undefined
};
export const ENV = {
+10 -2
View File
@@ -12,6 +12,14 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler"
}
"moduleResolution": "bundler",
"paths": {
"$shared-types": [
"./ai-service/src/shared-types"
]
}
},
"references": [
{ "path": "./ai-service" }
]
}
+4 -1
View File
@@ -35,7 +35,10 @@ export default defineConfig({
]
},
server: {
port: 3000
port: 3000,
watch: {
ignored: ['**/ai-service/**']
}
},
test: {
workspace: [