From 9e43fb976e01e44f9e1de1a50dea91d669252989 Mon Sep 17 00:00:00 2001
From: Nicholas Charriere
Date: Mon, 6 Apr 2026 14:13:06 -0700
Subject: [PATCH] Remove all app-builder stuff
---
README.md | 28 +-
packages/api/ai/app-parser.mts | 103 ---
packages/api/ai/generate.mts | 84 +--
packages/api/ai/logger.mts | 31 -
packages/api/ai/plan-parser.mts | 284 --------
packages/api/apps/app.mts | 132 ----
packages/api/apps/disk.mts | 396 -----------
packages/api/apps/git.mts | 132 ----
packages/api/apps/processes.mts | 165 -----
packages/api/apps/schemas.mts | 14 -
.../templates/react-typescript/index.html | 15 -
.../templates/react-typescript/package.json | 28 -
.../react-typescript/postcss.config.js | 6 -
.../templates/react-typescript/src/App.tsx | 12 -
.../templates/react-typescript/src/index.css | 7 -
.../templates/react-typescript/src/main.tsx | 10 -
.../react-typescript/src/vite-env.d.ts | 1 -
.../react-typescript/tailwind.config.js | 11 -
.../templates/react-typescript/tsconfig.json | 24 -
.../templates/react-typescript/vite.config.ts | 7 -
packages/api/apps/utils.mts | 9 -
packages/api/config.mts | 29 +-
packages/api/db/schema.mts | 17 -
packages/api/drizzle/0017_drop_apps.sql | 1 +
packages/api/drizzle/meta/0017_snapshot.json | 220 ++++++
packages/api/drizzle/meta/_journal.json | 7 +
packages/api/exec.mts | 15 -
packages/api/package.json | 6 +-
packages/api/prompts/app-builder.txt | 89 ---
packages/api/prompts/app-editor.txt | 87 ---
packages/api/server/channels/app.mts | 247 -------
packages/api/server/http.mts | 442 +-----------
packages/api/server/ws.mts | 4 -
packages/api/test/app-parser.test.mts | 44 --
packages/api/test/plan-parser.test.mts | 136 ----
packages/components/package.json | 1 +
packages/shared/index.mts | 4 -
packages/shared/package.json | 1 +
packages/shared/src/schemas/apps.mts | 8 -
packages/shared/src/schemas/files.mts | 0
packages/shared/src/schemas/websockets.mts | 67 --
packages/shared/src/types/apps.mts | 36 -
packages/shared/src/types/feedback.mts | 4 -
packages/shared/src/types/history.mts | 74 --
packages/shared/src/types/websockets.mts | 33 -
packages/web/package.json | 6 -
packages/web/src/clients/http/apps.ts | 346 ---------
packages/web/src/clients/websocket/index.ts | 52 --
.../src/components/apps/AiFeedbackModal.tsx | 60 --
.../web/src/components/apps/bottom-drawer.tsx | 137 ----
.../web/src/components/apps/create-modal.tsx | 153 ----
.../web/src/components/apps/diff-modal.tsx | 95 ---
.../web/src/components/apps/diff-stats.tsx | 30 -
packages/web/src/components/apps/editor.tsx | 95 ---
packages/web/src/components/apps/header.tsx | 300 --------
packages/web/src/components/apps/lib/diff.ts | 87 ---
.../web/src/components/apps/lib/file-tree.ts | 236 -------
packages/web/src/components/apps/lib/path.ts | 7 -
.../web/src/components/apps/local-storage.ts | 15 -
packages/web/src/components/apps/markdown.tsx | 10 -
.../components/apps/package-install-toast.tsx | 109 ---
.../src/components/apps/panels/explorer.tsx | 384 ----------
.../src/components/apps/panels/settings.tsx | 65 --
packages/web/src/components/apps/sidebar.tsx | 191 -----
packages/web/src/components/apps/types.ts | 21 -
packages/web/src/components/apps/use-app.tsx | 52 --
.../web/src/components/apps/use-files.tsx | 284 --------
packages/web/src/components/apps/use-logs.tsx | 114 ---
.../src/components/apps/use-package-json.tsx | 146 ----
.../web/src/components/apps/use-preview.tsx | 91 ---
.../web/src/components/apps/use-version.tsx | 82 ---
packages/web/src/components/chat.tsx | 662 ------------------
.../web/src/components/delete-app-dialog.tsx | 63 --
.../components/keyboard-shortcuts-dialog.tsx | 6 +-
packages/web/src/components/onboarding.tsx | 51 --
packages/web/src/components/srcbook-cards.tsx | 56 +-
packages/web/src/main.tsx | 44 --
packages/web/src/routes/apps/context.tsx | 46 --
packages/web/src/routes/apps/files-show.tsx | 21 -
packages/web/src/routes/apps/files.tsx | 26 -
packages/web/src/routes/apps/layout.tsx | 69 --
packages/web/src/routes/apps/loaders.tsx | 24 -
packages/web/src/routes/apps/preview.tsx | 72 --
packages/web/src/routes/home.tsx | 76 +-
packages/web/src/routes/session.tsx | 3 +-
pnpm-lock.yaml | 322 +--------
86 files changed, 259 insertions(+), 7521 deletions(-)
delete mode 100644 packages/api/ai/app-parser.mts
delete mode 100644 packages/api/ai/logger.mts
delete mode 100644 packages/api/ai/plan-parser.mts
delete mode 100644 packages/api/apps/app.mts
delete mode 100644 packages/api/apps/disk.mts
delete mode 100644 packages/api/apps/git.mts
delete mode 100644 packages/api/apps/processes.mts
delete mode 100644 packages/api/apps/schemas.mts
delete mode 100644 packages/api/apps/templates/react-typescript/index.html
delete mode 100644 packages/api/apps/templates/react-typescript/package.json
delete mode 100644 packages/api/apps/templates/react-typescript/postcss.config.js
delete mode 100644 packages/api/apps/templates/react-typescript/src/App.tsx
delete mode 100644 packages/api/apps/templates/react-typescript/src/index.css
delete mode 100644 packages/api/apps/templates/react-typescript/src/main.tsx
delete mode 100644 packages/api/apps/templates/react-typescript/src/vite-env.d.ts
delete mode 100644 packages/api/apps/templates/react-typescript/tailwind.config.js
delete mode 100644 packages/api/apps/templates/react-typescript/tsconfig.json
delete mode 100644 packages/api/apps/templates/react-typescript/vite.config.ts
delete mode 100644 packages/api/apps/utils.mts
create mode 100644 packages/api/drizzle/0017_drop_apps.sql
create mode 100644 packages/api/drizzle/meta/0017_snapshot.json
delete mode 100644 packages/api/prompts/app-builder.txt
delete mode 100644 packages/api/prompts/app-editor.txt
delete mode 100644 packages/api/server/channels/app.mts
delete mode 100644 packages/api/test/app-parser.test.mts
delete mode 100644 packages/api/test/plan-parser.test.mts
delete mode 100644 packages/shared/src/schemas/apps.mts
delete mode 100644 packages/shared/src/schemas/files.mts
delete mode 100644 packages/shared/src/types/apps.mts
delete mode 100644 packages/shared/src/types/feedback.mts
delete mode 100644 packages/shared/src/types/history.mts
delete mode 100644 packages/web/src/clients/http/apps.ts
delete mode 100644 packages/web/src/components/apps/AiFeedbackModal.tsx
delete mode 100644 packages/web/src/components/apps/bottom-drawer.tsx
delete mode 100644 packages/web/src/components/apps/create-modal.tsx
delete mode 100644 packages/web/src/components/apps/diff-modal.tsx
delete mode 100644 packages/web/src/components/apps/diff-stats.tsx
delete mode 100644 packages/web/src/components/apps/editor.tsx
delete mode 100644 packages/web/src/components/apps/header.tsx
delete mode 100644 packages/web/src/components/apps/lib/diff.ts
delete mode 100644 packages/web/src/components/apps/lib/file-tree.ts
delete mode 100644 packages/web/src/components/apps/lib/path.ts
delete mode 100644 packages/web/src/components/apps/local-storage.ts
delete mode 100644 packages/web/src/components/apps/markdown.tsx
delete mode 100644 packages/web/src/components/apps/package-install-toast.tsx
delete mode 100644 packages/web/src/components/apps/panels/explorer.tsx
delete mode 100644 packages/web/src/components/apps/panels/settings.tsx
delete mode 100644 packages/web/src/components/apps/sidebar.tsx
delete mode 100644 packages/web/src/components/apps/types.ts
delete mode 100644 packages/web/src/components/apps/use-app.tsx
delete mode 100644 packages/web/src/components/apps/use-files.tsx
delete mode 100644 packages/web/src/components/apps/use-logs.tsx
delete mode 100644 packages/web/src/components/apps/use-package-json.tsx
delete mode 100644 packages/web/src/components/apps/use-preview.tsx
delete mode 100644 packages/web/src/components/apps/use-version.tsx
delete mode 100644 packages/web/src/components/chat.tsx
delete mode 100644 packages/web/src/components/delete-app-dialog.tsx
delete mode 100644 packages/web/src/components/onboarding.tsx
delete mode 100644 packages/web/src/routes/apps/context.tsx
delete mode 100644 packages/web/src/routes/apps/files-show.tsx
delete mode 100644 packages/web/src/routes/apps/files.tsx
delete mode 100644 packages/web/src/routes/apps/layout.tsx
delete mode 100644 packages/web/src/routes/apps/loaders.tsx
delete mode 100644 packages/web/src/routes/apps/preview.tsx
diff --git a/README.md b/README.md
index 6d92126fd..0c9906af0 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
- Online app builder ·
+ Examples ·
Discord ·
Youtube ·
Hub
@@ -18,36 +18,16 @@
## Maintainers Note
-Srcbook is currently not under active development. After overwhelming demand of making it a managed service, we have built and are operating https://getmocha.com for this purpose!
-
-In the future, we might revive Srcbook for its notebook product (over the app builder one, which is better served by Mocha).
+Srcbook is not under active development.
## Srcbook
-Srcbook is a TypeScript-centric app development platform, with 2 main products:
-
-- an AI app builder (also available [hosted online](https://srcbook.com/))
-- a TypeScript notebook
+Srcbook is a TypeScript notebook that runs locally on your machine.
Srcbook is open-source (apache2) and runs locally on your machine. You'll need to bring your own API key for AI usage (we strongly recommend Anthropic with `claude-3-5-sonnet-latest`).
## Features
-### App Builder
-
-- AI app builder for TypeScript
-- Create, edit and run web apps
-- Use AI to generate the boilerplate, modify the code, and fix things
-- Edit the app with a hot-reloading web preview
-
-
-
-
-
-
-
-### Notebooks
-
- Create, run, and share TypeScript notebooks
- Export to valid markdown format (.src.md)
- AI features for exploring and iterating on ideas
@@ -120,7 +100,7 @@ Options:
Commands:
start [options] Start the Srcbook server
- import [options] Import a Notebook
+ import [options] Import a notebook
help [command] display help for command
```
diff --git a/packages/api/ai/app-parser.mts b/packages/api/ai/app-parser.mts
deleted file mode 100644
index 215f6cf4e..000000000
--- a/packages/api/ai/app-parser.mts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { XMLParser } from 'fast-xml-parser';
-
-// TODO reuse and cleanup types
-export interface FileContent {
- filename: string;
- content: string;
-}
-
-export type Project = {
- id: string;
- items: (File | Command)[];
-};
-
-type File = {
- type: 'file';
- filename: string;
- content: string;
-};
-
-type Command = {
- type: 'command';
- content: string;
-};
-
-type ParsedResult = {
- project: {
- '@_id': string;
- file?: { '@_filename': string; '#text': string }[] | { '@_filename': string; '#text': string };
- command?: string[] | string;
- };
-};
-
-export function parseProjectXML(response: string): Project {
- try {
- const parser = new XMLParser({
- ignoreAttributes: false,
- attributeNamePrefix: '@_',
- textNodeName: '#text',
- });
- const result = parser.parse(response) as ParsedResult;
-
- if (!result.project) {
- throw new Error('Invalid response: missing project tag');
- }
-
- const project: Project = {
- id: result.project['@_id'],
- items: [],
- };
-
- const files = Array.isArray(result.project.file)
- ? result.project.file
- : [result.project.file].filter(Boolean);
- const commands = Array.isArray(result.project.command)
- ? result.project.command
- : [result.project.command].filter(Boolean);
-
- // TODO this ruins the order as it makes all the file changes first.
- // @FIXME: later
- for (const file of files) {
- if (file) {
- project.items.push({
- type: 'file',
- filename: file['@_filename'],
- content: file['#text'],
- });
- }
- }
-
- for (const command of commands) {
- if (command) {
- project.items.push({
- type: 'command',
- content: command,
- });
- }
- }
-
- return project;
- } catch (error) {
- console.error('Error parsing XML for the app:', error);
- throw new Error('Failed to parse XML response');
- }
-}
-
-export function buildProjectXml(files: FileContent[], projectId: string): string {
- const fileXmls = files
- .map(
- (file) => `
-
-
- `,
- )
- .join('\n');
-
- return `
-
-${fileXmls}
-
- `.trim();
-}
diff --git a/packages/api/ai/generate.mts b/packages/api/ai/generate.mts
index 13245e148..e0a3c0113 100644
--- a/packages/api/ai/generate.mts
+++ b/packages/api/ai/generate.mts
@@ -1,4 +1,4 @@
-import { streamText, generateText, type GenerateTextResult } from 'ai';
+import { generateText, type GenerateTextResult } from 'ai';
import { getModel } from './config.mjs';
import {
type CodeLanguageType,
@@ -12,8 +12,6 @@ import { readFileSync } from 'node:fs';
import Path from 'node:path';
import { PROMPTS_DIR } from '../constants.mjs';
import { encode, decodeCells } from '../srcmd.mjs';
-import { buildProjectXml, type FileContent } from '../ai/app-parser.mjs';
-import { logAppGeneration } from './logger.mjs';
const makeGenerateSrcbookSystemPrompt = () => {
return readFileSync(Path.join(PROMPTS_DIR, 'srcbook-generator.txt'), 'utf-8');
@@ -26,34 +24,6 @@ const makeGenerateCellSystemPrompt = (language: CodeLanguageType) => {
const makeFixDiagnosticsSystemPrompt = () => {
return readFileSync(Path.join(PROMPTS_DIR, 'fix-cell-diagnostics.txt'), 'utf-8');
};
-const makeAppBuilderSystemPrompt = () => {
- return readFileSync(Path.join(PROMPTS_DIR, 'app-builder.txt'), 'utf-8');
-};
-const makeAppEditorSystemPrompt = () => {
- return readFileSync(Path.join(PROMPTS_DIR, 'app-editor.txt'), 'utf-8');
-};
-
-const makeAppEditorUserPrompt = (projectId: string, files: FileContent[], query: string) => {
- const projectXml = buildProjectXml(files, projectId);
- const userRequestXml = `${query}`;
- return `Following below are the project XML and the user request.
-
-${projectXml}
-
-${userRequestXml}
- `.trim();
-};
-
-const makeAppCreateUserPrompt = (projectId: string, files: FileContent[], query: string) => {
- const projectXml = buildProjectXml(files, projectId);
- const userRequestXml = `${query}`;
- return `Following below are the project XML and the user request.
-
-${projectXml}
-
-${userRequestXml}
- `.trim();
-};
const makeGenerateCellUserPrompt = (session: SessionType, insertIdx: number, query: string) => {
// Make sure we copy cells so we don't mutate the session
@@ -242,55 +212,3 @@ export async function fixDiagnostics(
return result.text;
}
-
-export async function generateApp(
- projectId: string,
- files: FileContent[],
- query: string,
-): Promise {
- const model = await getModel();
- const result = await generateText({
- model,
- system: makeAppBuilderSystemPrompt(),
- prompt: makeAppCreateUserPrompt(projectId, files, query),
- });
- return result.text;
-}
-
-export async function streamEditApp(
- projectId: string,
- files: FileContent[],
- query: string,
- appId: string,
- planId: string,
-) {
- const model = await getModel();
-
- const systemPrompt = makeAppEditorSystemPrompt();
- const userPrompt = makeAppEditorUserPrompt(projectId, files, query);
-
- let response = '';
-
- const result = await streamText({
- model,
- system: systemPrompt,
- prompt: userPrompt,
- onChunk: (chunk) => {
- if (chunk.chunk.type === 'text-delta') {
- response += chunk.chunk.textDelta;
- }
- },
- onFinish: () => {
- if (process.env.SRCBOOK_DISABLE_ANALYTICS !== 'true') {
- logAppGeneration({
- appId,
- planId,
- llm_request: { model, system: systemPrompt, prompt: userPrompt },
- llm_response: response,
- });
- }
- },
- });
-
- return result.textStream;
-}
diff --git a/packages/api/ai/logger.mts b/packages/api/ai/logger.mts
deleted file mode 100644
index 47cb5fc80..000000000
--- a/packages/api/ai/logger.mts
+++ /dev/null
@@ -1,31 +0,0 @@
-export type AppGenerationLog = {
- appId: string;
- planId: string;
- llm_request: any;
- llm_response: any;
-};
-
-/*
- * Log the LLM request / response to the analytics server.
- * For now this server is a custom implemention, consider moving to
- * a formal LLM log service, or a generic log hosting service.
- * In particular, this will not scale well when we split up app generation into
- * multiple steps. We will need spans/traces at that point.
- */
-export async function logAppGeneration(log: AppGenerationLog): Promise {
- try {
- const response = await fetch('https://hub.srcbook.com/api/app_generation_log', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(log),
- });
-
- if (!response.ok) {
- console.error('Error sending app generation log');
- }
- } catch (error) {
- console.error('Error sending app generation log:', error);
- }
-}
diff --git a/packages/api/ai/plan-parser.mts b/packages/api/ai/plan-parser.mts
deleted file mode 100644
index ac00e33f8..000000000
--- a/packages/api/ai/plan-parser.mts
+++ /dev/null
@@ -1,284 +0,0 @@
-import { XMLParser } from 'fast-xml-parser';
-import Path from 'node:path';
-import { type App as DBAppType } from '../db/schema.mjs';
-import { loadFile } from '../apps/disk.mjs';
-import { StreamingXMLParser, TagType } from './stream-xml-parser.mjs';
-import { ActionChunkType, DescriptionChunkType } from '@srcbook/shared';
-
-// The ai proposes a plan that we expect to contain both files and commands
-// Here is an example of a plan:
-
-/*
- * Example of a plan:
- *
- *
- *
- * {Short justification of changes. Be as brief as possible, like a commit message}
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- * npm install
- * react-redux
- * react-router-dom
- *
- * ...
- *
- */
-
-interface FileAction {
- type: 'file';
- dirname: string;
- basename: string;
- path: string;
- modified: string;
- original: string | null; // null if this is a new file. Consider using an enum for 'edit' | 'create' | 'delete' instead.
- description: string;
-}
-
-type NpmInstallCommand = {
- type: 'command';
- command: 'npm install';
- packages: string[];
- description: string;
-};
-
-// Later we can add more commands. For now, we only support npm install
-type Command = NpmInstallCommand;
-
-export interface Plan {
- // The high level description of the plan
- // Will be shown to the user above the diff box.
- id: string;
- query: string;
- description: string;
- actions: (FileAction | Command)[];
-}
-
-interface ParsedResult {
- plan: {
- planDescription: string;
- action:
- | {
- '@_type': string;
- description: string;
- file?: { '@_filename': string; '#text': string };
- commandType?: string;
- package?: string | string[];
- }[]
- | {
- '@_type': string;
- description: string;
- file?: { '@_filename': string; '#text': string };
- commandType?: string;
- package?: string | string[];
- };
- };
-}
-
-export async function parsePlan(
- response: string,
- app: DBAppType,
- query: string,
- planId: string,
-): Promise {
- try {
- const parser = new XMLParser({
- ignoreAttributes: false,
- attributeNamePrefix: '@_',
- textNodeName: '#text',
- });
- const result = parser.parse(response) as ParsedResult;
-
- if (!result.plan) {
- throw new Error('Invalid response: missing plan tag');
- }
-
- const plan: Plan = {
- id: planId,
- query,
- actions: [],
- description: result.plan.planDescription,
- };
- const actions = Array.isArray(result.plan.action) ? result.plan.action : [result.plan.action];
-
- for (const action of actions) {
- if (action['@_type'] === 'file' && action.file) {
- const filePath = action.file['@_filename'];
- let originalContent = null;
-
- try {
- const fileContent = await loadFile(app, filePath);
- originalContent = fileContent.source;
- } catch (error) {
- // If the file doesn't exist, it's likely that it's a new file.
- }
-
- plan.actions.push({
- type: 'file',
- path: filePath,
- dirname: Path.dirname(filePath),
- basename: Path.basename(filePath),
- modified: action.file['#text'],
- original: originalContent,
- description: action.description,
- });
- } else if (action['@_type'] === 'command' && action.commandType === 'npm install') {
- if (!action.package) {
- console.error('Invalid response: missing package tag');
- continue;
- }
- plan.actions.push({
- type: 'command',
- command: 'npm install',
- packages: Array.isArray(action.package) ? action.package : [action.package],
- description: action.description,
- });
- }
- }
-
- return plan;
- } catch (error) {
- console.error('Error parsing XML:', error);
- throw new Error('Failed to parse XML response');
- }
-}
-
-export function getPackagesToInstall(plan: Plan): string[] {
- return plan.actions
- .filter(
- (action): action is NpmInstallCommand =>
- action.type === 'command' && action.command === 'npm install',
- )
- .flatMap((action) => action.packages);
-}
-export async function streamParsePlan(
- stream: AsyncIterable,
- app: DBAppType,
- _query: string,
- planId: string,
-) {
- let parser: StreamingXMLParser;
- const parsePromises: Promise[] = [];
-
- return new ReadableStream({
- async pull(controller) {
- if (parser === undefined) {
- parser = new StreamingXMLParser({
- async onTag(tag) {
- if (tag.name === 'planDescription' || tag.name === 'action') {
- const promise = (async () => {
- const chunk = await toStreamingChunk(app, tag, planId);
- if (chunk) {
- controller.enqueue(JSON.stringify(chunk) + '\n');
- }
- })();
- parsePromises.push(promise);
- }
- },
- });
- }
-
- try {
- for await (const chunk of stream) {
- parser.parse(chunk);
- }
- // Wait for all pending parse operations to complete before closing
- await Promise.all(parsePromises);
- controller.close();
- } catch (error) {
- console.error(error);
- controller.enqueue(
- JSON.stringify({
- type: 'error',
- data: { content: 'Error while parsing streaming response' },
- }) + '\n',
- );
- controller.error(error);
- }
- },
- });
-}
-
-async function toStreamingChunk(
- app: DBAppType,
- tag: TagType,
- planId: string,
-): Promise {
- switch (tag.name) {
- case 'planDescription':
- return {
- type: 'description',
- planId: planId,
- data: { content: tag.content },
- } as DescriptionChunkType;
- case 'action': {
- const descriptionTag = tag.children.find((t) => t.name === 'description');
- const description = descriptionTag?.content ?? '';
- const type = tag.attributes.type;
-
- if (type === 'file') {
- const fileTag = tag.children.find((t) => t.name === 'file')!;
-
- const filePath = fileTag.attributes.filename as string;
- let originalContent = null;
-
- try {
- const fileContent = await loadFile(app, filePath);
- originalContent = fileContent.source;
- } catch (error) {
- // If the file doesn't exist, it's likely that it's a new file.
- }
-
- return {
- type: 'action',
- planId: planId,
- data: {
- type: 'file',
- description,
- path: filePath,
- dirname: Path.dirname(filePath),
- basename: Path.basename(filePath),
- modified: fileTag.content,
- original: originalContent,
- },
- } as ActionChunkType;
- } else if (type === 'command') {
- const commandTag = tag.children.find((t) => t.name === 'commandType')!;
- const packageTags = tag.children.filter((t) => t.name === 'package');
-
- return {
- type: 'action',
- planId: planId,
- data: {
- type: 'command',
- description,
- command: commandTag.content,
- packages: packageTags.map((t) => t.content),
- },
- } as ActionChunkType;
- } else {
- return null;
- }
- }
- default:
- return null;
- }
-}
diff --git a/packages/api/apps/app.mts b/packages/api/apps/app.mts
deleted file mode 100644
index 9a42dee8b..000000000
--- a/packages/api/apps/app.mts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { randomid, type AppType } from '@srcbook/shared';
-import { db } from '../db/index.mjs';
-import { type App as DBAppType, apps as appsTable } from '../db/schema.mjs';
-import { applyPlan, createViteApp, deleteViteApp, getFlatFilesForApp } from './disk.mjs';
-import { CreateAppSchemaType, CreateAppWithAiSchemaType } from './schemas.mjs';
-import { asc, desc, eq } from 'drizzle-orm';
-import { npmInstall } from './processes.mjs';
-import { generateApp } from '../ai/generate.mjs';
-import { toValidPackageName } from '../apps/utils.mjs';
-import { getPackagesToInstall, parsePlan } from '../ai/plan-parser.mjs';
-import { commitAllFiles, initRepo } from './git.mjs';
-
-function toSecondsSinceEpoch(date: Date): number {
- return Math.floor(date.getTime() / 1000);
-}
-
-export function serializeApp(app: DBAppType): AppType {
- return {
- id: app.externalId,
- name: app.name,
- createdAt: toSecondsSinceEpoch(app.createdAt),
- updatedAt: toSecondsSinceEpoch(app.updatedAt),
- };
-}
-
-async function insert(attrs: Pick): Promise {
- const [app] = await db.insert(appsTable).values(attrs).returning();
- return app!;
-}
-
-export async function createAppWithAi(data: CreateAppWithAiSchemaType): Promise {
- const app = await insert({
- name: data.name,
- externalId: randomid(),
- });
-
- await createViteApp(app);
-
- await initRepo(app);
-
- // Note: we don't surface issues or retries and this is "running in the background".
- // In this case it works in our favor because we'll kickoff generation while it happens
- const firstNpmInstallProcess = npmInstall(app.externalId, {
- stdout(data) {
- console.log(data.toString('utf8'));
- },
- stderr(data) {
- console.error(data.toString('utf8'));
- },
- onExit(code) {
- console.log(`npm install exit code: ${code}`);
- },
- });
-
- const files = await getFlatFilesForApp(app.externalId);
- const result = await generateApp(toValidPackageName(app.name), files, data.prompt);
- const plan = await parsePlan(result, app, data.prompt, randomid());
- await applyPlan(app, plan);
-
- const packagesToInstall = getPackagesToInstall(plan);
-
- if (packagesToInstall.length > 0) {
- await firstNpmInstallProcess;
-
- console.log('installing packages', packagesToInstall);
- npmInstall(app.externalId, {
- packages: packagesToInstall,
- stdout(data) {
- console.log(data.toString('utf8'));
- },
- stderr(data) {
- console.error(data.toString('utf8'));
- },
- onExit(code) {
- console.log(`npm install exit code: ${code}`);
- console.log('Applying git commit');
- commitAllFiles(app, `Add dependencies: ${packagesToInstall.join(', ')}`);
- },
- });
- }
-
- return app;
-}
-export async function createApp(data: CreateAppSchemaType): Promise {
- const app = await insert({
- name: data.name,
- externalId: randomid(),
- });
-
- await createViteApp(app);
-
- // TODO: handle this better.
- // This should be done somewhere else and surface issues or retries.
- // Not awaiting here because it's "happening in the background".
- npmInstall(app.externalId, {
- stdout(data) {
- console.log(data.toString('utf8'));
- },
- stderr(data) {
- console.error(data.toString('utf8'));
- },
- onExit(code) {
- console.log(`npm install exit code: ${code}`);
- },
- });
-
- return app;
-}
-
-export async function deleteApp(id: string) {
- await db.delete(appsTable).where(eq(appsTable.externalId, id));
- await deleteViteApp(id);
-}
-
-export function loadApps(sort: 'asc' | 'desc') {
- const sorter = sort === 'asc' ? asc : desc;
- return db.select().from(appsTable).orderBy(sorter(appsTable.updatedAt));
-}
-
-export async function loadApp(id: string) {
- const [app] = await db.select().from(appsTable).where(eq(appsTable.externalId, id));
- return app;
-}
-
-export async function updateApp(id: string, attrs: { name: string }) {
- const [updatedApp] = await db
- .update(appsTable)
- .set({ name: attrs.name })
- .where(eq(appsTable.externalId, id))
- .returning();
- return updatedApp;
-}
diff --git a/packages/api/apps/disk.mts b/packages/api/apps/disk.mts
deleted file mode 100644
index 05b2d6601..000000000
--- a/packages/api/apps/disk.mts
+++ /dev/null
@@ -1,396 +0,0 @@
-import type { RmOptions } from 'node:fs';
-import fs from 'node:fs/promises';
-import type { Project } from '../ai/app-parser.mjs';
-import Path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { type App as DBAppType } from '../db/schema.mjs';
-import { APPS_DIR } from '../constants.mjs';
-import { toValidPackageName } from './utils.mjs';
-import { DirEntryType, FileEntryType, FileType } from '@srcbook/shared';
-import { FileContent } from '../ai/app-parser.mjs';
-import type { Plan } from '../ai/plan-parser.mjs';
-import archiver from 'archiver';
-import { wss } from '../index.mjs';
-
-export function pathToApp(id: string) {
- return Path.join(APPS_DIR, id);
-}
-
-export function broadcastFileUpdated(app: DBAppType, file: FileType) {
- wss.broadcast(`app:${app.externalId}`, 'file:updated', { file });
-}
-
-// Use this rather than fs.writeFile to ensure we notify the client that the file has been updated.
-export async function writeFile(app: DBAppType, file: FileType) {
- // Guard against absolute / relative path issues for safety
- let path = file.path;
- if (!path.startsWith(pathToApp(app.externalId))) {
- path = Path.join(pathToApp(app.externalId), file.path);
- }
- const dirPath = Path.dirname(path);
- await fs.mkdir(dirPath, { recursive: true });
- await fs.writeFile(path, file.source, 'utf-8');
- broadcastFileUpdated(app, file);
-}
-
-function pathToTemplate(template: string) {
- return Path.resolve(fileURLToPath(import.meta.url), '..', 'templates', template);
-}
-
-export function deleteViteApp(id: string) {
- return fs.rm(pathToApp(id), { recursive: true });
-}
-
-export async function applyPlan(app: DBAppType, plan: Plan) {
- try {
- for (const item of plan.actions) {
- if (item.type === 'file') {
- const basename = Path.basename(item.path);
- await writeFile(app, {
- path: item.path,
- name: basename,
- source: item.modified,
- binary: isBinary(basename),
- });
- }
- }
- } catch (e) {
- console.error('Error applying plan to app', app.externalId, e);
- throw e;
- }
-}
-
-export async function createAppFromProject(app: DBAppType, project: Project) {
- const appPath = pathToApp(app.externalId);
- await fs.mkdir(appPath, { recursive: true });
-
- for (const item of project.items) {
- if (item.type === 'file') {
- await writeFile(app, {
- path: item.filename,
- name: Path.basename(item.filename),
- source: item.content,
- binary: isBinary(Path.basename(item.filename)),
- });
- } else if (item.type === 'command') {
- // For now, we'll just log the commands
- // TODO: execute the commands in the right order.
- console.log(`Command to execute: ${item.content}`);
- }
- }
- return app;
-}
-
-export async function createViteApp(app: DBAppType) {
- const appPath = pathToApp(app.externalId);
-
- // Use recursive because its parent directory may not exist.
- await fs.mkdir(appPath, { recursive: true });
-
- // Scaffold all the necessary project files.
- await scaffold(app, appPath);
-
- return app;
-}
-
-/**
- * Scaffolds a new Vite app using a predefined template.
- *
- *
- * The current template includes: React, TypeScript, Vite, Tailwind CSS
- *
- * This function performs the following steps:
- * 1. Copies all template files to the destination directory
- * 2. Updates the package.json with the new app name
- * 3. Updates the index.html title with the app name
- *
- * @param {DBAppType} app - The database app object.
- * @param {string} destDir - The destination directory for the app.
- * @returns {Promise}
- */
-async function scaffold(app: DBAppType, destDir: string) {
- const template = `react-typescript`;
-
- function write(file: string, content?: string) {
- const targetPath = Path.join(destDir, file);
- return content === undefined
- ? copy(Path.join(templateDir, file), targetPath)
- : writeFile(app, {
- path: targetPath,
- name: Path.basename(targetPath),
- source: content,
- binary: isBinary(Path.basename(targetPath)),
- });
- }
-
- const templateDir = pathToTemplate(template);
- const files = await fs.readdir(templateDir);
- for (const file of files.filter((f) => f !== 'package.json')) {
- await write(file);
- }
-
- const [pkgContents, idxContents] = await Promise.all([
- fs.readFile(Path.join(templateDir, 'package.json'), 'utf-8'),
- fs.readFile(Path.join(templateDir, 'index.html'), 'utf-8'),
- ]);
-
- const pkg = JSON.parse(pkgContents);
- pkg.name = toValidPackageName(app.name);
- const updatedPkgContents = JSON.stringify(pkg, null, 2) + '\n';
-
- const updatedIdxContents = idxContents.replace(
- /.*<\/title>/,
- `${app.name}`,
- );
-
- await Promise.all([
- write('package.json', updatedPkgContents),
- write('index.html', updatedIdxContents),
- ]);
-}
-
-export async function fileUpdated(app: DBAppType, file: FileType) {
- return writeFile(app, file);
-}
-
-async function copy(src: string, dest: string) {
- const stat = await fs.stat(src);
- if (stat.isDirectory()) {
- return copyDir(src, dest);
- } else {
- return fs.copyFile(src, dest);
- }
-}
-
-async function copyDir(srcDir: string, destDir: string) {
- await fs.mkdir(destDir, { recursive: true });
- const files = await fs.readdir(srcDir);
- for (const file of files) {
- const srcFile = Path.resolve(srcDir, file);
- const destFile = Path.resolve(destDir, file);
- await copy(srcFile, destFile);
- }
-}
-
-export async function loadDirectory(
- app: DBAppType,
- path: string,
- excludes = ['node_modules', 'dist', '.git'],
-): Promise {
- const projectDir = Path.join(APPS_DIR, app.externalId);
- const dirPath = Path.join(projectDir, path);
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
-
- const children = entries
- .filter((entry) => excludes.indexOf(entry.name) === -1)
- .map((entry) => {
- const fullPath = Path.join(dirPath, entry.name);
- const relativePath = Path.relative(projectDir, fullPath);
- const paths = getPathInfo(relativePath);
- return entry.isDirectory()
- ? { ...paths, type: 'directory' as const, children: null }
- : { ...paths, type: 'file' as const };
- });
-
- const relativePath = Path.relative(projectDir, dirPath);
-
- return {
- ...getPathInfo(relativePath),
- type: 'directory' as const,
- children: children,
- };
-}
-
-export async function createDirectory(
- app: DBAppType,
- dirname: string,
- basename: string,
-): Promise {
- const projectDir = Path.join(APPS_DIR, app.externalId);
- const dirPath = Path.join(projectDir, dirname, basename);
-
- await fs.mkdir(dirPath, { recursive: false });
-
- const relativePath = Path.relative(projectDir, dirPath);
-
- return {
- ...getPathInfo(relativePath),
- type: 'directory' as const,
- children: null,
- };
-}
-
-export function deleteDirectory(app: DBAppType, path: string) {
- return deleteEntry(app, path, { recursive: true, force: true });
-}
-
-export async function renameDirectory(
- app: DBAppType,
- path: string,
- name: string,
-): Promise {
- const result = await rename(app, path, name);
- return { ...result, type: 'directory' as const, children: null };
-}
-
-export async function loadFile(app: DBAppType, path: string): Promise {
- const projectDir = Path.join(APPS_DIR, app.externalId);
- const filePath = Path.join(projectDir, path);
- const relativePath = Path.relative(projectDir, filePath);
- const basename = Path.basename(filePath);
-
- if (isBinary(basename)) {
- return { path: relativePath, name: basename, source: `TODO: handle this`, binary: true };
- } else {
- return {
- path: relativePath,
- name: basename,
- source: await fs.readFile(filePath, 'utf-8'),
- binary: false,
- };
- }
-}
-
-export async function createFile(
- app: DBAppType,
- dirname: string,
- basename: string,
- source: string,
-): Promise {
- const filePath = Path.join(dirname, basename);
-
- await writeFile(app, {
- path: filePath,
- name: basename,
- source,
- binary: isBinary(basename),
- });
- return { ...getPathInfo(filePath), type: 'file' as const };
-}
-
-export function deleteFile(app: DBAppType, path: string) {
- return deleteEntry(app, path);
-}
-
-export async function renameFile(
- app: DBAppType,
- path: string,
- name: string,
-): Promise {
- const result = await rename(app, path, name);
- return { ...result, type: 'file' as const };
-}
-
-async function rename(app: DBAppType, path: string, name: string) {
- const projectDir = Path.join(APPS_DIR, app.externalId);
- const oldPath = Path.join(projectDir, path);
- const dirname = Path.dirname(oldPath);
- const newPath = Path.join(dirname, name);
- await fs.rename(oldPath, newPath);
- const relativePath = Path.relative(projectDir, newPath);
- return getPathInfo(relativePath);
-}
-
-function deleteEntry(app: DBAppType, path: string, options: RmOptions = {}) {
- const filePath = Path.join(APPS_DIR, app.externalId, path);
- return fs.rm(filePath, options);
-}
-
-// TODO: This does not scale.
-// What's the best way to know whether a file is a "binary"
-// file or not? Inspecting bytes for invalid utf8?
-const TEXT_FILE_EXTENSIONS = [
- '.ts',
- '.cts',
- '.mts',
- '.tsx',
- '.js',
- '.cjs',
- '.mjs',
- '.jsx',
- '.md',
- '.markdown',
- '.json',
- '.css',
- '.html',
-];
-
-export function toFileType(path: string, source: string): FileType {
- return {
- path,
- name: Path.basename(path),
- source,
- binary: isBinary(Path.basename(path)),
- };
-}
-
-function isBinary(basename: string) {
- const isDotfile = basename.startsWith('.'); // Assume these are text for now, e.g., .gitignore
- const isTextFile = TEXT_FILE_EXTENSIONS.includes(Path.extname(basename));
- return !(isDotfile || isTextFile);
-}
-
-function getPathInfo(path: string) {
- if (Path.isAbsolute(path)) {
- throw new Error(`Expected a relative path but got '${path}'`);
- }
-
- path = path === '' ? '.' : path;
-
- return {
- path: path,
- dirname: Path.dirname(path),
- basename: Path.basename(path),
- };
-}
-
-export async function getFlatFilesForApp(id: string): Promise {
- const appPath = pathToApp(id);
- return getFlatFiles(appPath);
-}
-
-async function getFlatFiles(dir: string, basePath: string = ''): Promise {
- const entries = await fs.readdir(dir, { withFileTypes: true });
- let files: FileContent[] = [];
-
- for (const entry of entries) {
- const relativePath = Path.join(basePath, entry.name);
- const fullPath = Path.join(dir, entry.name);
-
- if (entry.isDirectory()) {
- // TODO better ignore list mechanism. Should use a glob
- if (!['.git', 'node_modules'].includes(entry.name)) {
- files = files.concat(await getFlatFiles(fullPath, relativePath));
- }
- } else if (entry.isFile() && entry.name !== 'package-lock.json') {
- const content = await fs.readFile(fullPath, 'utf-8');
- files.push({ filename: relativePath, content });
- }
- }
-
- return files;
-}
-
-export async function createZipFromApp(app: DBAppType): Promise {
- const appPath = pathToApp(app.externalId);
- const archive = archiver('zip', { zlib: { level: 9 } });
- const chunks: any[] = [];
-
- return new Promise((resolve, reject) => {
- archive.directory(appPath, false);
-
- archive.on('error', (err) => {
- console.error('Error creating zip archive:', err);
- reject(err);
- });
-
- archive.on('data', (chunk: Buffer) => chunks.push(chunk));
-
- archive.on('end', () => {
- const buffer = Buffer.concat(chunks);
- resolve(buffer);
- });
-
- archive.finalize();
- });
-}
diff --git a/packages/api/apps/git.mts b/packages/api/apps/git.mts
deleted file mode 100644
index 2ef5fbfd0..000000000
--- a/packages/api/apps/git.mts
+++ /dev/null
@@ -1,132 +0,0 @@
-import simpleGit, { SimpleGit, DefaultLogFields, ListLogLine } from 'simple-git';
-import fs from 'node:fs/promises';
-import { broadcastFileUpdated, pathToApp, toFileType } from './disk.mjs';
-import type { App as DBAppType } from '../db/schema.mjs';
-import Path from 'node:path';
-
-// Helper to get git instance for an app
-function getGit(app: DBAppType): SimpleGit {
- const dir = pathToApp(app.externalId);
- return simpleGit(dir);
-}
-
-// Initialize a git repository in the app directory
-export async function initRepo(app: DBAppType): Promise {
- const git = getGit(app);
- await git.init();
- await commitAllFiles(app, 'Initial commit');
-}
-
-// Commit all current files in the app directory
-export async function commitAllFiles(app: DBAppType, message: string): Promise {
- const git = getGit(app);
-
- // Stage all files
- await git.add('.');
-
- // Create commit
- await git.commit(message, {
- '--author': 'Srcbook ',
- });
-
- // Get the exact SHA of the new commit. Sometimes it's 'HEAD ' for some reason
- const sha = await git.revparse(['HEAD']);
- return sha;
-}
-
-// Checkout to a specific commit, and notify the client that the files have changed
-export async function checkoutCommit(app: DBAppType, commitSha: string): Promise {
- const git = getGit(app);
- // get the files that are different between the current state and the commit
- const files = await getChangedFiles(app, commitSha);
-
- // we might have a dirty working directory, so we need to stash any changes
- // TODO: we should probably handle this better
- await git.stash();
-
- // checkout the commit
- await git.checkout(commitSha);
-
- // notify the client to update the files
- for (const file of files.added) {
- const source = await fs.readFile(Path.join(pathToApp(app.externalId), file), 'utf-8');
- broadcastFileUpdated(app, toFileType(file, source));
- }
- for (const file of files.modified) {
- const source = await fs.readFile(Path.join(pathToApp(app.externalId), file), 'utf-8');
- broadcastFileUpdated(app, toFileType(file, source));
- }
-}
-
-// Get commit history
-export async function getCommitHistory(
- app: DBAppType,
- limit: number = 100,
-): Promise> {
- const git = getGit(app);
- const log = await git.log({ maxCount: limit });
- return log.all;
-}
-
-// Helper function to ensure the repo exists
-export async function ensureRepoExists(app: DBAppType): Promise {
- const git = getGit(app);
- const isRepo = await git.checkIsRepo();
-
- if (!isRepo) {
- await initRepo(app);
- }
-}
-
-// Get the current commit SHA
-export async function getCurrentCommitSha(app: DBAppType): Promise {
- const git = getGit(app);
- // There might not be a .git initialized yet, so we need to handle that
- const isRepo = await git.checkIsRepo();
- if (!isRepo) {
- await initRepo(app);
- }
-
- const revparse = await git.revparse(['HEAD']);
-
- return revparse;
-}
-
-// Get list of changed files between current state and a commit
-export async function getChangedFiles(
- app: DBAppType,
- commitSha: string,
-): Promise<{ added: string[]; modified: string[]; deleted: string[] }> {
- const git = getGit(app);
-
- // Get the diff between current state and the specified commit
- const diffSummary = await git.diff(['--name-status', commitSha]);
-
- const changes = {
- added: [] as string[],
- modified: [] as string[],
- deleted: [] as string[],
- };
-
- // Parse the diff output
- diffSummary.split('\n').forEach((line) => {
- const [status, ...fileParts] = line.split('\t');
- const file = fileParts.join('\t'); // Handle filenames with tabs
-
- if (!file || !status) return;
-
- switch (status[0]) {
- case 'A':
- changes.added.push(file);
- break;
- case 'M':
- changes.modified.push(file);
- break;
- case 'D':
- changes.deleted.push(file);
- break;
- }
- });
-
- return changes;
-}
diff --git a/packages/api/apps/processes.mts b/packages/api/apps/processes.mts
deleted file mode 100644
index e758acb1c..000000000
--- a/packages/api/apps/processes.mts
+++ /dev/null
@@ -1,165 +0,0 @@
-import { ChildProcess } from 'node:child_process';
-import { pathToApp } from './disk.mjs';
-import { npmInstall as execNpmInstall, vite as execVite } from '../exec.mjs';
-import { wss } from '../index.mjs';
-
-export type ProcessType = 'npm:install' | 'vite:server';
-
-export interface NpmInstallProcessType {
- type: 'npm:install';
- process: ChildProcess;
-}
-
-export interface ViteServerProcessType {
- type: 'vite:server';
- process: ChildProcess;
- port: number | null;
-}
-
-export type AppProcessType = NpmInstallProcessType | ViteServerProcessType;
-
-class Processes {
- private map: Map = new Map();
-
- has(appId: string, type: ProcessType) {
- return this.map.has(this.toKey(appId, type));
- }
-
- get(appId: string, type: ProcessType) {
- return this.map.get(this.toKey(appId, type));
- }
-
- set(appId: string, process: AppProcessType) {
- this.map.set(this.toKey(appId, process.type), process);
- }
-
- del(appId: string, type: ProcessType) {
- return this.map.delete(this.toKey(appId, type));
- }
-
- private toKey(appId: string, type: ProcessType) {
- return `${appId}:${type}`;
- }
-}
-
-const processes = new Processes();
-
-export function getAppProcess(appId: string, type: 'npm:install'): NpmInstallProcessType;
-export function getAppProcess(appId: string, type: 'vite:server'): ViteServerProcessType;
-export function getAppProcess(appId: string, type: ProcessType): AppProcessType {
- switch (type) {
- case 'npm:install':
- return processes.get(appId, type) as NpmInstallProcessType;
- case 'vite:server':
- return processes.get(appId, type) as ViteServerProcessType;
- }
-}
-
-export function setAppProcess(appId: string, process: AppProcessType) {
- processes.set(appId, process);
-}
-
-export function deleteAppProcess(appId: string, process: ProcessType) {
- processes.del(appId, process);
-}
-
-async function waitForProcessToComplete(process: AppProcessType) {
- if (process.process.exitCode !== null) {
- return process;
- }
-
- return new Promise((resolve, reject) => {
- process.process.once('exit', () => {
- resolve(process);
- });
- process.process.once('error', (err) => {
- reject(err);
- });
- });
-}
-
-/**
- * Runs npm install for the given app.
- *
- * If there's already a process running npm install, it will return that process.
- */
-export function npmInstall(
- appId: string,
- options: Omit[0]>, 'cwd'> & { onStart?: () => void },
-) {
- const runningProcess = processes.get(appId, 'npm:install');
- if (runningProcess) {
- return waitForProcessToComplete(runningProcess);
- }
-
- wss.broadcast(`app:${appId}`, 'deps:install:status', { status: 'installing' });
- if (options.onStart) {
- options.onStart();
- }
-
- const newlyStartedProcess: NpmInstallProcessType = {
- type: 'npm:install',
- process: execNpmInstall({
- ...options,
-
- cwd: pathToApp(appId),
- stdout: (data) => {
- wss.broadcast(`app:${appId}`, 'deps:install:log', {
- log: { type: 'stdout', data: data.toString('utf8') },
- });
-
- if (options.stdout) {
- options.stdout(data);
- }
- },
- stderr: (data) => {
- wss.broadcast(`app:${appId}`, 'deps:install:log', {
- log: { type: 'stderr', data: data.toString('utf8') },
- });
-
- if (options.stderr) {
- options.stderr(data);
- }
- },
- onExit: (code, signal) => {
- // We must clean up this process so that we can run npm install again
- deleteAppProcess(appId, 'npm:install');
-
- wss.broadcast(`app:${appId}`, 'deps:install:status', {
- status: code === 0 ? 'complete' : 'failed',
- code,
- });
-
- if (code === 0) {
- wss.broadcast(`app:${appId}`, 'deps:status:response', {
- nodeModulesExists: true,
- });
- }
-
- if (options.onExit) {
- options.onExit(code, signal);
- }
- },
- }),
- };
- processes.set(appId, newlyStartedProcess);
-
- return waitForProcessToComplete(newlyStartedProcess);
-}
-
-/**
- * Runs a vite dev server for the given app.
- *
- * If there's already a process running the vite dev server, it will return that process.
- */
-export function viteServer(appId: string, options: Omit[0], 'cwd'>) {
- if (!processes.has(appId, 'vite:server')) {
- processes.set(appId, {
- type: 'vite:server',
- process: execVite({ cwd: pathToApp(appId), ...options }),
- port: null,
- });
- }
-
- return processes.get(appId, 'vite:server');
-}
diff --git a/packages/api/apps/schemas.mts b/packages/api/apps/schemas.mts
deleted file mode 100644
index f2b1597dd..000000000
--- a/packages/api/apps/schemas.mts
+++ /dev/null
@@ -1,14 +0,0 @@
-import z from 'zod';
-
-export const CreateAppSchema = z.object({
- name: z.string(),
- prompt: z.string().optional(),
-});
-
-export const CreateAppWithAiSchema = z.object({
- name: z.string(),
- prompt: z.string(),
-});
-
-export type CreateAppSchemaType = z.infer;
-export type CreateAppWithAiSchemaType = z.infer;
diff --git a/packages/api/apps/templates/react-typescript/index.html b/packages/api/apps/templates/react-typescript/index.html
deleted file mode 100644
index b903bd21a..000000000
--- a/packages/api/apps/templates/react-typescript/index.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
- Vite + React + TS
-
-
-
-
-
-
-
-
diff --git a/packages/api/apps/templates/react-typescript/package.json b/packages/api/apps/templates/react-typescript/package.json
deleted file mode 100644
index 888ca13cd..000000000
--- a/packages/api/apps/templates/react-typescript/package.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "vite-react-typescript-starter",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc -b && vite build",
- "lint": "eslint .",
- "preview": "vite preview"
- },
- "dependencies": {
- "lucide-react": "^0.453.0",
- "react": "^18.3.1",
- "react-dom": "^18.3.1"
- },
- "devDependencies": {
- "@types/react": "^18.3.6",
- "@types/react-dom": "^18.3.0",
- "@vitejs/plugin-react": "^4.3.1",
- "autoprefixer": "^10.4.20",
- "globals": "^15.9.0",
- "postcss": "^8.4.47",
- "tailwindcss": "^3.4.14",
- "typescript": "^5.5.3",
- "vite": "^5.4.6"
- }
-}
diff --git a/packages/api/apps/templates/react-typescript/postcss.config.js b/packages/api/apps/templates/react-typescript/postcss.config.js
deleted file mode 100644
index 2e7af2b7f..000000000
--- a/packages/api/apps/templates/react-typescript/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-}
diff --git a/packages/api/apps/templates/react-typescript/src/App.tsx b/packages/api/apps/templates/react-typescript/src/App.tsx
deleted file mode 100644
index 386cc16f0..000000000
--- a/packages/api/apps/templates/react-typescript/src/App.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import './index.css'
-
-function App() {
-
- return (
-
- Hello world!
-
- )
-}
-
-export default App
diff --git a/packages/api/apps/templates/react-typescript/src/index.css b/packages/api/apps/templates/react-typescript/src/index.css
deleted file mode 100644
index 9b3c93868..000000000
--- a/packages/api/apps/templates/react-typescript/src/index.css
+++ /dev/null
@@ -1,7 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
-}
diff --git a/packages/api/apps/templates/react-typescript/src/main.tsx b/packages/api/apps/templates/react-typescript/src/main.tsx
deleted file mode 100644
index 6f4ac9bcc..000000000
--- a/packages/api/apps/templates/react-typescript/src/main.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import App from './App.tsx'
-import './index.css'
-
-createRoot(document.getElementById('root')!).render(
-
-
- ,
-)
diff --git a/packages/api/apps/templates/react-typescript/src/vite-env.d.ts b/packages/api/apps/templates/react-typescript/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe2a..000000000
--- a/packages/api/apps/templates/react-typescript/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/packages/api/apps/templates/react-typescript/tailwind.config.js b/packages/api/apps/templates/react-typescript/tailwind.config.js
deleted file mode 100644
index dca8ba02d..000000000
--- a/packages/api/apps/templates/react-typescript/tailwind.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- content: [
- "./index.html",
- "./src/**/*.{js,ts,jsx,tsx}",
- ],
- theme: {
- extend: {},
- },
- plugins: [],
-}
diff --git a/packages/api/apps/templates/react-typescript/tsconfig.json b/packages/api/apps/templates/react-typescript/tsconfig.json
deleted file mode 100644
index f0a235055..000000000
--- a/packages/api/apps/templates/react-typescript/tsconfig.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
- },
- "include": ["src"]
-}
diff --git a/packages/api/apps/templates/react-typescript/vite.config.ts b/packages/api/apps/templates/react-typescript/vite.config.ts
deleted file mode 100644
index 627a31962..000000000
--- a/packages/api/apps/templates/react-typescript/vite.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { defineConfig } from 'vite';
-import react from '@vitejs/plugin-react';
-
-// https://vitejs.dev/config/
-export default defineConfig({
- plugins: [react()],
-});
diff --git a/packages/api/apps/utils.mts b/packages/api/apps/utils.mts
deleted file mode 100644
index 3636400b8..000000000
--- a/packages/api/apps/utils.mts
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copied from https://github.com/vitejs/vite/tree/main/packages/create-vite
-export function toValidPackageName(projectName: string) {
- return projectName
- .trim()
- .toLowerCase()
- .replace(/\s+/g, '-')
- .replace(/^[._]/, '')
- .replace(/[^a-z\d\-~]+/g, '-');
-}
diff --git a/packages/api/config.mts b/packages/api/config.mts
index 49133fad5..d68d4d6d6 100644
--- a/packages/api/config.mts
+++ b/packages/api/config.mts
@@ -1,14 +1,6 @@
import { eq, and, inArray } from 'drizzle-orm';
import { type SecretWithAssociatedSessions, randomid } from '@srcbook/shared';
-import { MessageType, HistoryType } from '@srcbook/shared';
-import {
- configs,
- type Config,
- secrets,
- type Secret,
- secretsToSession,
- apps,
-} from './db/schema.mjs';
+import { configs, type Config, secrets, type Secret, secretsToSession } from './db/schema.mjs';
import { db } from './db/index.mjs';
import { HOME_DIR } from './constants.mjs';
@@ -52,25 +44,6 @@ export async function updateConfig(attrs: Partial) {
return db.update(configs).set(attrs).returning();
}
-export async function getHistory(appId: string): Promise {
- const results = await db.select().from(apps).where(eq(apps.externalId, appId)).limit(1);
- const history = results[0]!.history;
- return JSON.parse(history);
-}
-
-export async function appendToHistory(appId: string, messages: MessageType | MessageType[]) {
- const results = await db.select().from(apps).where(eq(apps.externalId, appId)).limit(1);
- const history = results[0]!.history;
- const decodedHistory = JSON.parse(history);
- const newHistory = Array.isArray(messages)
- ? [...decodedHistory, ...messages]
- : [...decodedHistory, messages];
- await db
- .update(apps)
- .set({ history: JSON.stringify(newHistory) })
- .where(eq(apps.externalId, appId));
-}
-
export async function getSecrets(): Promise> {
const secretsResult = await db.select().from(secrets);
const secretsToSessionResult = await db
diff --git a/packages/api/db/schema.mts b/packages/api/db/schema.mts
index 7d0700f81..7d832941a 100644
--- a/packages/api/db/schema.mts
+++ b/packages/api/db/schema.mts
@@ -1,4 +1,3 @@
-import { sql } from 'drizzle-orm';
import { sqliteTable, text, integer, unique } from 'drizzle-orm/sqlite-core';
import { randomid } from '@srcbook/shared';
@@ -48,19 +47,3 @@ export const secretsToSession = sqliteTable(
);
export type SecretsToSession = typeof secretsToSession.$inferSelect;
-
-export const apps = sqliteTable('apps', {
- id: integer('id').primaryKey(),
- name: text('name').notNull(),
- externalId: text('external_id').notNull().unique(),
- history: text('history').notNull().default('[]'), // JSON encoded value of the history
- historyVersion: integer('history_version').notNull().default(1), // internal versioning of history type for migrations
- createdAt: integer('created_at', { mode: 'timestamp' })
- .notNull()
- .default(sql`(unixepoch())`),
- updatedAt: integer('updated_at', { mode: 'timestamp' })
- .notNull()
- .default(sql`(unixepoch())`),
-});
-
-export type App = typeof apps.$inferSelect;
diff --git a/packages/api/drizzle/0017_drop_apps.sql b/packages/api/drizzle/0017_drop_apps.sql
new file mode 100644
index 000000000..cb57bc248
--- /dev/null
+++ b/packages/api/drizzle/0017_drop_apps.sql
@@ -0,0 +1 @@
+DROP TABLE IF EXISTS `apps`;
diff --git a/packages/api/drizzle/meta/0017_snapshot.json b/packages/api/drizzle/meta/0017_snapshot.json
new file mode 100644
index 000000000..3b22b2212
--- /dev/null
+++ b/packages/api/drizzle/meta/0017_snapshot.json
@@ -0,0 +1,220 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "5a12096f-88f6-4d4e-b4ab-311e4f2c88c8",
+ "prevId": "f20efb4d-77a9-41b3-9aa0-43192b59caef",
+ "tables": {
+ "config": {
+ "name": "config",
+ "columns": {
+ "base_dir": {
+ "name": "base_dir",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "default_language": {
+ "name": "default_language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'typescript'"
+ },
+ "openai_api_key": {
+ "name": "openai_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "anthropic_api_key": {
+ "name": "anthropic_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "xai_api_key": {
+ "name": "xai_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "gemini_api_key": {
+ "name": "gemini_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "openrouter_api_key": {
+ "name": "openrouter_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_api_key": {
+ "name": "custom_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enabled_analytics": {
+ "name": "enabled_analytics",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "srcbook_installation_id": {
+ "name": "srcbook_installation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'s6qi7h4bk4014r3sm1kuvfrfac'"
+ },
+ "ai_provider": {
+ "name": "ai_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'openai'"
+ },
+ "ai_model": {
+ "name": "ai_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'gpt-4o'"
+ },
+ "ai_base_url": {
+ "name": "ai_base_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "subscription_email": {
+ "name": "subscription_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "secrets": {
+ "name": "secrets",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "secrets_name_unique": {
+ "name": "secrets_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "secrets_to_sessions": {
+ "name": "secrets_to_sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "secret_id": {
+ "name": "secret_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "secrets_to_sessions_session_id_secret_id_unique": {
+ "name": "secrets_to_sessions_session_id_secret_id_unique",
+ "columns": [
+ "session_id",
+ "secret_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "secrets_to_sessions_secret_id_secrets_id_fk": {
+ "name": "secrets_to_sessions_secret_id_secrets_id_fk",
+ "tableFrom": "secrets_to_sessions",
+ "tableTo": "secrets",
+ "columnsFrom": [
+ "secret_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ }
+ },
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/packages/api/drizzle/meta/_journal.json b/packages/api/drizzle/meta/_journal.json
index 801f9614d..3be2e35cc 100644
--- a/packages/api/drizzle/meta/_journal.json
+++ b/packages/api/drizzle/meta/_journal.json
@@ -120,6 +120,13 @@
"when": 1743191674243,
"tag": "0016_add_openrouter_api_key",
"breakpoints": true
+ },
+ {
+ "idx": 17,
+ "version": "6",
+ "when": 1775433600000,
+ "tag": "0017_drop_apps",
+ "breakpoints": true
}
]
}
diff --git a/packages/api/exec.mts b/packages/api/exec.mts
index ba87e58bb..921de55e0 100644
--- a/packages/api/exec.mts
+++ b/packages/api/exec.mts
@@ -23,10 +23,6 @@ export type NPMInstallRequestType = BaseExecRequestType & {
args?: Array;
};
-type NpxRequestType = BaseExecRequestType & {
- args: Array;
-};
-
type SpawnCallRequestType = {
cwd: string;
env: NodeJS.ProcessEnv;
@@ -159,14 +155,3 @@ export function npmInstall(options: NPMInstallRequestType) {
env: process.env,
});
}
-
-/**
- * Run vite.
- */
-export function vite(options: NpxRequestType) {
- return spawnCall({
- ...options,
- command: Path.join(options.cwd, 'node_modules', '.bin', 'vite'),
- env: process.env,
- });
-}
diff --git a/packages/api/package.json b/packages/api/package.json
index f1c259254..2a1c7c893 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -11,7 +11,7 @@
"dev": "vite-node -w dev-server.mts",
"test": "vitest",
"prebuild": "rm -rf ./dist",
- "build": "tsc && cp -R ./drizzle ./dist/drizzle && cp -R ./srcbook/examples ./dist/srcbook/examples && cp -R ./prompts ./dist/prompts && cp -R ./apps/templates ./dist/apps/templates",
+ "build": "tsc && cp -R ./drizzle ./dist/drizzle && cp -R ./srcbook/examples ./dist/srcbook/examples && cp -R ./prompts ./dist/prompts",
"lint": "eslint . --max-warnings 0",
"check-types": "tsc",
"depcheck": "depcheck",
@@ -28,21 +28,17 @@
"@ai-sdk/provider": "^1.0.1",
"@srcbook/shared": "workspace:^",
"ai": "^3.4.33",
- "archiver": "^7.0.1",
"better-sqlite3": "^11.3.0",
"cors": "^2.8.5",
"depcheck": "^1.4.7",
"drizzle-orm": "^0.33.0",
"express": "^4.20.0",
- "fast-xml-parser": "^4.5.0",
"marked": "catalog:",
"posthog-node": "^4.2.0",
- "simple-git": "^3.27.0",
"ws": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
- "@types/archiver": "^6.0.2",
"@types/better-sqlite3": "^7.6.11",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
diff --git a/packages/api/prompts/app-builder.txt b/packages/api/prompts/app-builder.txt
deleted file mode 100644
index 2a88185f5..000000000
--- a/packages/api/prompts/app-builder.txt
+++ /dev/null
@@ -1,89 +0,0 @@
-## Context
-
-- You are helping a user build a front-end website application. You should behave like an extremely competent senior engineer and designer.
-- The user is asking you to create the app from scratch through a and you will be given the skeleton of the app that already exists as a .
-- You will be given an app skeleton in the following format:
-
-
-
-
-
-
-
- ...
-
-- You will be given the user request, passed as:
-
- {user request in plain english}
-
-
-
-## Instructions
-
-- Your job is to come up with the relevant changes, you do so by suggesting a with one or more and a .
-- There can be one or more in a .
-- A is a brief description of your plan in plain english. It will be shown to the user as context.
-- An is one of:
- - type="file": a new or updated file with ALL of the new contents
- - type="command": a command that the user will run in the command line. Currently the only supported command is 'npm install': it allows you to install one or more npm packages.
-- When installing dependencies, don't update the package.json file. Instead use the with the npm install; running this command will update the package.json.
-- Only respond with the plan, all information you provide should be in it.
-- You will receive a user request like "build a todo list app" or "build a food logger". It might be a lot more requirements, but keep your MVP functional and simple.
-- You should use localStorage for storage, unless specifically requested otherwise
-- Your stack is React, vite, typescript, tailwind. Keep things simple.
-- The goal is to get a FUNCTIONAL MVP. All of the parts for this MVP should be included.
-- Your job is to be precise and effective, so avoid extraneous steps even if they offer convenience.
-- Do not talk or worry about testing. The user wants to _use_ the app: the core goal is for it to _work_.
-- For react: modularize components into their own files, even small ones. We don't want one large App.tsx with everything inline, but different components in their respective src/components/{Component}.tsx files
-- For styles: apply modern, minimalistic styles. Things shoud look modern, clean and slick.
-- Use lucide-react for icons. It is pre-installed
-- If the user asks for features that require routing, favor using react-router
-
-
-## Example response
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- npm install
- {package1}
- {package2}
-
- ...
-
\ No newline at end of file
diff --git a/packages/api/prompts/app-editor.txt b/packages/api/prompts/app-editor.txt
deleted file mode 100644
index b75174da5..000000000
--- a/packages/api/prompts/app-editor.txt
+++ /dev/null
@@ -1,87 +0,0 @@
-## Context
-
-- You are helping a user build a front-end website application. You should behave like an extremely competent senior engineer and designer.
-- The user wants to make a change to update or fix the app. Your goal is to help him with that request by suggesting updates for files.
-- The structure we use to describe the app is the following:
-
-
-
-
-
-
-
-
-- You will be passed the app with the above format, as well as the user request, passed as:
-
- {user request in plain english}
-
-
-
-## Instructions
-
-- Your job is to come up with the relevant changes, you do so by suggesting a with one or more and a .
-- There can be one or more in a .
-- A is a brief description of your plan in plain english. It will be shown to the user as context.
-- An is one of:
- - type="file": a new or updated file with ALL of the new contents
- - type="command": a command that the user will run in the command line. Currently the only supported command is 'npm install': it allows you to install one or more npm packages.
-- When installing dependencies, don't update the package.json file. Instead use the with the npm install; running this command will update the package.json.
-- Only respond with the plan, all information you provide should be in it.
-- You should use localStorage for storage, unless specifically requested otherwise.
-- Your stack is React, vite, typescript, tailwind. Keep things simple.
-- The goal is to get a FUNCTIONAL MVP. All of the parts for this MVP should be included.
-- Your job is to be precise and effective, so avoid extraneous steps even if they offer convenience.
-- Do not talk or worry about testing. The user wants to _use_ the app: the core goal is for it to _work_.
-- For react: modularize components into their own files, even small ones. We don't want one large App.tsx with everything inline, but different components in their respective src/components/{Component}.tsx files
-- For styles: apply modern, minimalistic styles. Things shoud look modern, clean and slick.
-- Use lucide-react for icons. It is pre-installed
-- If the user asks for features that require routing, favor using react-router
-
-
-## Example response
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- npm install
- react-redux
- react-router-dom
-
- ...
-
\ No newline at end of file
diff --git a/packages/api/server/channels/app.mts b/packages/api/server/channels/app.mts
deleted file mode 100644
index 1d8602703..000000000
--- a/packages/api/server/channels/app.mts
+++ /dev/null
@@ -1,247 +0,0 @@
-import path from 'node:path';
-import fs from 'node:fs/promises';
-import {
- PreviewStartPayloadSchema,
- PreviewStopPayloadSchema,
- FileUpdatedPayloadSchema,
- FileType,
- FileUpdatedPayloadType,
- PreviewStartPayloadType,
- PreviewStopPayloadType,
- DepsInstallPayloadType,
- DepsInstallPayloadSchema,
- DepsClearPayloadType,
- DepsStatusPayloadSchema,
-} from '@srcbook/shared';
-
-import WebSocketServer, {
- type MessageContextType,
- type ConnectionContextType,
-} from '../ws-client.mjs';
-import { loadApp } from '../../apps/app.mjs';
-import { fileUpdated, pathToApp } from '../../apps/disk.mjs';
-import { directoryExists } from '../../fs-utils.mjs';
-import {
- getAppProcess,
- setAppProcess,
- deleteAppProcess,
- npmInstall,
- viteServer,
-} from '../../apps/processes.mjs';
-
-const VITE_PORT_REGEX = /Local:.*http:\/\/localhost:([0-9]{1,4})/;
-
-type AppContextType = MessageContextType<'appId'>;
-
-async function previewStart(
- _payload: PreviewStartPayloadType,
- context: AppContextType,
- wss: WebSocketServer,
-) {
- const app = await loadApp(context.params.appId);
-
- if (!app) {
- return;
- }
-
- const existingProcess = getAppProcess(app.externalId, 'vite:server');
-
- if (existingProcess) {
- wss.broadcast(`app:${app.externalId}`, 'preview:status', {
- status: 'running',
- url: `http://localhost:${existingProcess.port}/`,
- });
- return;
- }
-
- wss.broadcast(`app:${app.externalId}`, 'preview:status', {
- url: null,
- status: 'booting',
- });
-
- const onChangePort = (newPort: number) => {
- const process = getAppProcess(app.externalId, 'vite:server');
-
- // This is not expected to happen
- if (!process) {
- wss.broadcast(`app:${app.externalId}`, 'preview:status', {
- url: null,
- status: 'stopped',
- code: null,
- });
- return;
- }
-
- setAppProcess(app.externalId, { ...process, port: newPort });
-
- wss.broadcast(`app:${app.externalId}`, 'preview:status', {
- url: `http://localhost:${newPort}/`,
- status: 'running',
- });
- };
-
- viteServer(app.externalId, {
- args: [],
- stdout: (data) => {
- const encodedData = data.toString('utf8');
- console.log(encodedData);
-
- wss.broadcast(`app:${app.externalId}`, 'preview:log', {
- log: {
- type: 'stdout',
- data: encodedData,
- },
- });
-
- const potentialPortMatch = VITE_PORT_REGEX.exec(encodedData);
- if (potentialPortMatch) {
- const portString = potentialPortMatch[1]!;
- const port = parseInt(portString, 10);
- onChangePort(port);
- }
- },
- stderr: (data) => {
- const encodedData = data.toString('utf8');
- console.error(encodedData);
-
- wss.broadcast(`app:${app.externalId}`, 'preview:log', {
- log: {
- type: 'stderr',
- data: encodedData,
- },
- });
- },
- onExit: (code) => {
- deleteAppProcess(app.externalId, 'vite:server');
-
- wss.broadcast(`app:${app.externalId}`, 'preview:status', {
- url: null,
- status: 'stopped',
- code: code,
- });
- },
- onError: (_error) => {
- // Errors happen when we try to run vite before node modules are installed.
- // Make sure we clean up the app process and inform the client.
- deleteAppProcess(app.externalId, 'vite:server');
-
- // TODO: Use a different event to communicate to the client there was an error.
- // If the error is ENOENT, for example, it means node_modules and/or vite is missing.
- wss.broadcast(`app:${app.externalId}`, 'preview:status', {
- url: null,
- status: 'stopped',
- code: null,
- });
- },
- });
-}
-
-async function previewStop(
- _payload: PreviewStopPayloadType,
- context: AppContextType,
- conn: ConnectionContextType,
-) {
- const app = await loadApp(context.params.appId);
-
- if (!app) {
- return;
- }
-
- const result = getAppProcess(app.externalId, 'vite:server');
-
- if (!result) {
- conn.reply(`app:${app.externalId}`, 'preview:status', {
- url: null,
- status: 'stopped',
- code: null,
- });
- return;
- }
-
- // Killing the process should result in its onExit handler being called.
- // The onExit handler will remove the process from the processMetadata map
- // and send the `preview:status` event with a value of 'stopped'
- result.process.kill('SIGTERM');
-}
-
-async function dependenciesInstall(payload: DepsInstallPayloadType, context: AppContextType) {
- const app = await loadApp(context.params.appId);
-
- if (!app) {
- return;
- }
-
- npmInstall(app.externalId, {
- packages: payload.packages ?? undefined,
- });
-}
-
-async function clearNodeModules(
- _payload: DepsClearPayloadType,
- context: AppContextType,
- conn: ConnectionContextType,
-) {
- const app = await loadApp(context.params.appId);
-
- if (!app) {
- return;
- }
-
- const appPath = pathToApp(app.externalId);
- const nodeModulesPath = path.join(appPath, 'node_modules');
- await fs.rm(nodeModulesPath, { recursive: true, force: true });
-
- conn.reply(`app:${app.externalId}`, 'deps:status:response', {
- nodeModulesExists: false,
- });
-}
-
-async function dependenciesStatus(
- _payload: DepsClearPayloadType,
- context: AppContextType,
- conn: ConnectionContextType,
-) {
- const app = await loadApp(context.params.appId);
-
- if (!app) {
- return;
- }
-
- const appPath = pathToApp(app.externalId);
- const nodeModulesPath = path.join(appPath, 'node_modules');
- conn.reply(`app:${app.externalId}`, 'deps:status:response', {
- nodeModulesExists: await directoryExists(nodeModulesPath),
- });
-}
-
-async function onFileUpdated(payload: FileUpdatedPayloadType, context: AppContextType) {
- const app = await loadApp(context.params.appId);
-
- if (!app) {
- return;
- }
-
- fileUpdated(app, payload.file as FileType);
-}
-
-export function register(wss: WebSocketServer) {
- wss
- .channel('app:')
- .on('preview:start', PreviewStartPayloadSchema, (payload, context) =>
- previewStart(payload, context, wss),
- )
- .on('preview:stop', PreviewStopPayloadSchema, previewStop)
- .on('deps:install', DepsInstallPayloadSchema, dependenciesInstall)
- .on('deps:clear', DepsInstallPayloadSchema, clearNodeModules)
- .on('deps:status', DepsStatusPayloadSchema, dependenciesStatus)
- .on('file:updated', FileUpdatedPayloadSchema, onFileUpdated)
- .onJoin((_payload, context, conn) => {
- const appExternalId = (context as AppContextType).params.appId;
-
- // When connecting, send back info about an in flight npm install if one exists
- const npmInstallProcess = getAppProcess(appExternalId, 'npm:install');
- if (npmInstallProcess) {
- conn.reply(`app:${appExternalId}`, 'deps:install:status', { status: 'installing' });
- }
- });
-}
diff --git a/packages/api/server/http.mts b/packages/api/server/http.mts
index 9ce3fedf9..b967d2239 100644
--- a/packages/api/server/http.mts
+++ b/packages/api/server/http.mts
@@ -2,7 +2,7 @@ import Path from 'node:path';
import { posthog } from '../posthog-client.mjs';
import fs from 'node:fs/promises';
import { SRCBOOKS_DIR } from '../constants.mjs';
-import express, { type Application, type Response } from 'express';
+import express, { type Application } from 'express';
import cors from 'cors';
import {
createSession,
@@ -13,15 +13,12 @@ import {
listSessions,
exportSrcmdText,
} from '../session.mjs';
-import { generateCells, generateSrcbook, healthcheck, streamEditApp } from '../ai/generate.mjs';
-import { streamParsePlan } from '../ai/plan-parser.mjs';
+import { generateCells, generateSrcbook, healthcheck } from '../ai/generate.mjs';
import {
getConfig,
updateConfig,
getSecrets,
addSecret,
- getHistory,
- appendToHistory,
removeSecret,
associateSecretWithSession,
disassociateSecretWithSession,
@@ -38,32 +35,6 @@ import { readdir } from '../fs-utils.mjs';
import { EXAMPLE_SRCBOOKS } from '../srcbook/examples.mjs';
import { pathToSrcbook } from '../srcbook/path.mjs';
import { isSrcmdPath } from '../srcmd/paths.mjs';
-import {
- loadApps,
- loadApp,
- createApp,
- serializeApp,
- deleteApp,
- createAppWithAi,
- updateApp,
-} from '../apps/app.mjs';
-import { toValidPackageName } from '../apps/utils.mjs';
-import {
- deleteFile,
- renameFile,
- loadDirectory,
- loadFile,
- createFile,
- createDirectory,
- renameDirectory,
- deleteDirectory,
- getFlatFilesForApp,
-} from '../apps/disk.mjs';
-import { CreateAppSchema } from '../apps/schemas.mjs';
-import { AppGenerationFeedbackType } from '@srcbook/shared';
-import { createZipFromApp } from '../apps/disk.mjs';
-import { checkoutCommit, commitAllFiles, getCurrentCommitSha } from '../apps/git.mjs';
-import { streamJsonResponse } from './utils.mjs';
const app: Application = express();
@@ -422,415 +393,6 @@ router.post('/subscribe', cors(), async (req, res) => {
}
});
-function error500(res: Response, e: Error) {
- const error = e as unknown as Error;
- console.error(error);
- return res.status(500).json({ error: 'An unexpected error occurred.' });
-}
-
-router.options('/apps', cors());
-router.post('/apps', cors(), async (req, res) => {
- const result = CreateAppSchema.safeParse(req.body);
-
- if (result.success === false) {
- const errors = result.error.errors.map((error) => error.message);
- return res.status(400).json({ errors });
- }
-
- const attrs = result.data;
-
- posthog.capture({
- event: 'user created app',
- properties: { prompt: typeof attrs.prompt === 'string' ? attrs.prompt : 'N/A' },
- });
-
- try {
- if (typeof attrs.prompt === 'string') {
- const app = await createAppWithAi({ name: attrs.name, prompt: attrs.prompt });
- return res.json({ data: serializeApp(app) });
- } else {
- // TODO do we really need to keep this?
- const app = await createApp(attrs);
- return res.json({ data: serializeApp(app) });
- }
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps', cors());
-router.get('/apps', cors(), async (req, res) => {
- const sort = req.query.sort === 'desc' ? 'desc' : 'asc';
-
- try {
- const apps = await loadApps(sort);
- return res.json({ data: apps.map(serializeApp) });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id', cors());
-router.get('/apps/:id', cors(), async (req, res) => {
- const { id } = req.params;
-
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- return res.json({ data: serializeApp(app) });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id', cors());
-router.put('/apps/:id', cors(), async (req, res) => {
- const { id } = req.params;
- const { name } = req.body;
-
- if (typeof name !== 'string' || name.trim() === '') {
- return res.status(400).json({ error: 'Name is required' });
- }
-
- try {
- const app = await updateApp(id, { name });
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- return res.json({ data: serializeApp(app) });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id', cors());
-router.delete('/apps/:id', cors(), async (req, res) => {
- const { id } = req.params;
-
- try {
- await deleteApp(id);
- return res.json({ deleted: true });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/directories', cors());
-router.get('/apps/:id/directories', cors(), async (req, res) => {
- const { id } = req.params;
-
- // TODO: validate and ensure path is not absolute
- const path = typeof req.query.path === 'string' ? req.query.path : '.';
-
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- const directory = await loadDirectory(app, path);
-
- return res.json({ data: directory });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/edit', cors());
-router.post('/apps/:id/edit', cors(), async (req, res) => {
- const { id } = req.params;
- const { query, planId } = req.body;
- posthog.capture({ event: 'user edited app with ai' });
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
- const validName = toValidPackageName(app.name);
- const files = await getFlatFilesForApp(String(app.externalId));
- const result = await streamEditApp(validName, files, query, app.externalId, planId);
- const planStream = await streamParsePlan(result, app, query, planId);
-
- return streamJsonResponse(planStream, res, { status: 200 });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/commit', cors());
-router.get('/apps/:id/commit', cors(), async (req, res) => {
- const { id } = req.params;
- const app = await loadApp(id);
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- const sha = await getCurrentCommitSha(app);
- return res.json({ sha });
-});
-router.post('/apps/:id/commit', cors(), async (req, res) => {
- const { id } = req.params;
- const { message } = req.body;
- // import the commit function from the apps/git.mjs file
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- const sha = await commitAllFiles(app, message);
- return res.json({ sha });
-});
-
-router.options('/apps/:id/checkout/:sha', cors());
-router.post('/apps/:id/checkout/:sha', cors(), async (req, res) => {
- const { id, sha } = req.params;
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- await checkoutCommit(app, sha);
- return res.json({ success: true, sha });
-});
-
-router.options('/apps/:id/directories', cors());
-router.post('/apps/:id/directories', cors(), async (req, res) => {
- const { id } = req.params;
-
- // TODO: validate and ensure path is not absolute
- const { dirname, basename } = req.body;
-
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- const directory = await createDirectory(app, dirname, basename);
-
- return res.json({ data: directory });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/directories', cors());
-router.delete('/apps/:id/directories', cors(), async (req, res) => {
- const { id } = req.params;
-
- // TODO: validate and ensure path is not absolute
- const path = typeof req.query.path === 'string' ? req.query.path : '.';
-
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- await deleteDirectory(app, path);
-
- return res.json({ data: { deleted: true } });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/directories/rename', cors());
-router.post('/apps/:id/directories/rename', cors(), async (req, res) => {
- const { id } = req.params;
-
- // TODO: validate and ensure path is not absolute
- const path = typeof req.query.path === 'string' ? req.query.path : '.';
- const name = req.query.name as string;
-
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- const directory = await renameDirectory(app, path, name);
-
- return res.json({ data: directory });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/files', cors());
-router.get('/apps/:id/files', cors(), async (req, res) => {
- const { id } = req.params;
-
- // TODO: validate and ensure path is not absolute
- const path = typeof req.query.path === 'string' ? req.query.path : '.';
-
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- const file = await loadFile(app, path);
-
- return res.json({ data: file });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/files', cors());
-router.post('/apps/:id/files', cors(), async (req, res) => {
- const { id } = req.params;
-
- // TODO: validate and ensure path is not absolute
- const { dirname, basename, source } = req.body;
-
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- const file = await createFile(app, dirname, basename, source);
-
- return res.json({ data: file });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/files', cors());
-router.delete('/apps/:id/files', cors(), async (req, res) => {
- const { id } = req.params;
-
- // TODO: validate and ensure path is not absolute
- const path = typeof req.query.path === 'string' ? req.query.path : '.';
-
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- await deleteFile(app, path);
-
- return res.json({ data: { deleted: true } });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/files/rename', cors());
-router.post('/apps/:id/files/rename', cors(), async (req, res) => {
- const { id } = req.params;
-
- // TODO: validate and ensure path is not absolute
- const path = typeof req.query.path === 'string' ? req.query.path : '.';
- const name = req.query.name as string;
-
- try {
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- const file = await renameFile(app, path, name);
-
- return res.json({ data: file });
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
-router.options('/apps/:id/export', cors());
-router.post('/apps/:id/export', cors(), async (req, res) => {
- const { id } = req.params;
- const { name } = req.body;
-
- try {
- posthog.capture({ event: 'user exported app' });
- const app = await loadApp(id);
-
- if (!app) {
- return res.status(404).json({ error: 'App not found' });
- }
-
- const zipBuffer = await createZipFromApp(app);
-
- res.setHeader('Content-Type', 'application/zip');
- res.setHeader('Content-Disposition', `attachment; filename="${name}.zip"`);
- res.send(zipBuffer);
- } catch (e) {
- return error500(res, e as Error);
- }
-});
-
app.use('/api', router);
export default app;
-
-router.options('/apps/:id/history', cors());
-router.get('/apps/:id/history', cors(), async (req, res) => {
- const { id } = req.params;
- const history = await getHistory(id);
- return res.json({ data: history });
-});
-
-router.post('/apps/:id/history', cors(), async (req, res) => {
- const { id } = req.params;
- const { messages } = req.body;
- await appendToHistory(id, messages);
- return res.json({ data: { success: true } });
-});
-
-router.options('/apps/:id/feedback', cors());
-router.post('/apps/:id/feedback', cors(), async (req, res) => {
- const { id } = req.params;
- const { planId, feedback } = req.body as AppGenerationFeedbackType;
-
- if (process.env.SRCBOOK_DISABLE_ANALYTICS === 'true') {
- return res.status(403).json({ error: 'Analytics are disabled' });
- }
- posthog.capture({ event: 'user sent feedback', properties: { type: feedback.type } });
-
- try {
- const response = await fetch('https://hub.srcbook.com/api/app_generation_feedback', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- appId: id,
- planId,
- feedback,
- }),
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const result = await response.json();
- return res.json(result);
- } catch (error) {
- console.error('Error sending feedback:', error);
- return res.status(500).json({ error: 'Failed to send feedback' });
- }
-});
diff --git a/packages/api/server/ws.mts b/packages/api/server/ws.mts
index 062576f92..d0db26ebf 100644
--- a/packages/api/server/ws.mts
+++ b/packages/api/server/ws.mts
@@ -63,7 +63,6 @@ import WebSocketServer, { MessageContextType } from './ws-client.mjs';
import { filenameFromPath, pathToCodeFile } from '../srcbook/path.mjs';
import { normalizeDiagnostic } from '../tsserver/utils.mjs';
import { removeCodeCellFromDisk } from '../srcbook/index.mjs';
-import { register as registerAppChannel } from './channels/app.mjs';
type SessionsContextType = MessageContextType<'sessionId'>;
@@ -890,7 +889,4 @@ wss
TsServerDefinitionLocationRequestPayloadSchema,
getCompletions,
);
-
-registerAppChannel(wss);
-
export default wss;
diff --git a/packages/api/test/app-parser.test.mts b/packages/api/test/app-parser.test.mts
deleted file mode 100644
index 8afaf72c3..000000000
--- a/packages/api/test/app-parser.test.mts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { parseProjectXML } from '../ai/app-parser.mjs';
-
-describe.skip('parseProjectXML', () => {
- it('should correctly parse XML and return a Project object', () => {
- const testXML = `
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- const result = parseProjectXML(testXML);
-
- const expectedResult = {
- id: 'test-project',
- items: [
- { type: 'file', filename: './test1.txt', content: 'Test content 1' },
- { type: 'file', filename: './test2.txt', content: 'Test content 2' },
- { type: 'command', content: 'npm install' },
- ],
- };
-
- expect(result).toEqual(expectedResult);
- });
-
- it('should throw an error for invalid XML', () => {
- const invalidXML = 'XML';
-
- expect(() => parseProjectXML(invalidXML)).toThrow('Failed to parse XML response');
- });
-
- it('should throw an error for XML without a project tag', () => {
- const noProjectXML = 'Content';
-
- expect(() => parseProjectXML(noProjectXML)).toThrow('Failed to parse XML response');
- });
-});
diff --git a/packages/api/test/plan-parser.test.mts b/packages/api/test/plan-parser.test.mts
deleted file mode 100644
index 5a095c83e..000000000
--- a/packages/api/test/plan-parser.test.mts
+++ /dev/null
@@ -1,136 +0,0 @@
-import { expect, test, describe } from 'vitest';
-import { parsePlan } from '../ai/plan-parser.mjs';
-import { type App as DBAppType } from '../db/schema.mjs';
-import { vi } from 'vitest';
-
-// Mock the loadFile function
-vi.mock('../apps/disk.mjs', () => ({
- loadFile: vi.fn().mockImplementation((_app, filePath) => {
- if (filePath === 'src/App.tsx') {
- return Promise.resolve({ source: 'Original App.tsx content' });
- }
- return Promise.reject(new Error('File not found'));
- }),
-}));
-
-const mockApp: DBAppType = {
- id: 123,
- externalId: '123',
- name: 'Test App',
- createdAt: new Date(),
- updatedAt: new Date(),
- history: '',
- historyVersion: 1,
-};
-
-const mockXMLResponse = `
-
- Implement a basic todo list app
-
- Update App.tsx with todo list functionality
-
- ([]);
- const [inputValue, setInputValue] = useState('');
-
- useEffect(() => {
- const storedTodos = localStorage.getItem('todos');
- if (storedTodos) {
- setTodos(JSON.parse(storedTodos));
- }
- }, []);
-
- useEffect(() => {
- localStorage.setItem('todos', JSON.stringify(todos));
- }, [todos]);
-
- const addTodo = () => {
- if (inputValue.trim() !== '') {
- setTodos([...todos, { id: Date.now(), text: inputValue, completed: false }]);
- setInputValue('');
- }
- };
-
- const toggleTodo = (id: number) => {
- setTodos(todos.map(todo =>
- todo.id === id ? { ...todo, completed: !todo.completed } : todo
- ));
- };
-
- return (
-
-
Todo List
-
- setInputValue(e.target.value)}
- className="flex-grow p-2 border rounded-l"
- placeholder="Add a new todo"
- />
-
-
-
-
- );
-}
-
-export default App;
- ]]>
-
-
-
- Install required packages
- npm install
- @types/react
- @types/react-dom
-
-
-`;
-
-describe('parsePlan', () => {
- test('should correctly parse a plan with file and command actions', async () => {
- const plan = await parsePlan(mockXMLResponse, mockApp, 'test query', '123445');
-
- expect(plan.id).toBe('123445');
- expect(plan.query).toBe('test query');
- expect(plan.description).toBe('Implement a basic todo list app');
- expect(plan.actions).toHaveLength(2);
-
- // Check file action
- const fileAction = plan.actions[0] as any;
- expect(fileAction.type).toBe('file');
- expect(fileAction.path).toBe('src/App.tsx');
- expect(fileAction.modified).toContain('function App()');
- expect(fileAction.original).toBe('Original App.tsx content');
- expect(fileAction.description).toBe('Update App.tsx with todo list functionality');
-
- // Check command action
- const commandAction = plan.actions[1] as any;
- expect(commandAction.type).toBe('command');
- expect(commandAction.command).toBe('npm install');
- expect(commandAction.packages).toEqual(['@types/react', '@types/react-dom']);
- expect(commandAction.description).toBe('Install required packages');
- });
-});
diff --git a/packages/components/package.json b/packages/components/package.json
index a276aa9d9..6c1bbee51 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -3,6 +3,7 @@
"version": "0.0.7",
"type": "module",
"main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
"scripts": {
"prebuild": "rm -rf ./dist",
"build": "tsc",
diff --git a/packages/shared/index.mts b/packages/shared/index.mts
index 8c9bbf844..2d0c3093e 100644
--- a/packages/shared/index.mts
+++ b/packages/shared/index.mts
@@ -1,13 +1,9 @@
-export * from './src/schemas/apps.mjs';
export * from './src/schemas/cells.mjs';
export * from './src/schemas/tsserver.mjs';
export * from './src/schemas/websockets.mjs';
-export * from './src/types/apps.mjs';
export * from './src/types/cells.mjs';
export * from './src/types/tsserver.mjs';
-export * from './src/types/history.mjs';
export * from './src/types/websockets.mjs';
export * from './src/types/secrets.mjs';
-export * from './src/types/feedback.mjs';
export * from './src/utils.mjs';
export * from './src/ai.mjs';
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 5cbc63bac..c4fb092ee 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -3,6 +3,7 @@
"version": "0.0.13",
"type": "module",
"main": "./dist/index.mjs",
+ "types": "./dist/index.d.mts",
"scripts": {
"prebuild": "rimraf ./dist",
"build": "tsc",
diff --git a/packages/shared/src/schemas/apps.mts b/packages/shared/src/schemas/apps.mts
deleted file mode 100644
index 95f8a64bd..000000000
--- a/packages/shared/src/schemas/apps.mts
+++ /dev/null
@@ -1,8 +0,0 @@
-import z from 'zod';
-
-export const FileSchema = z.object({
- path: z.string(),
- name: z.string(),
- source: z.string(),
- binary: z.boolean(),
-});
diff --git a/packages/shared/src/schemas/files.mts b/packages/shared/src/schemas/files.mts
deleted file mode 100644
index e69de29bb..000000000
diff --git a/packages/shared/src/schemas/websockets.mts b/packages/shared/src/schemas/websockets.mts
index d2245f56e..9f4fce1e7 100644
--- a/packages/shared/src/schemas/websockets.mts
+++ b/packages/shared/src/schemas/websockets.mts
@@ -7,7 +7,6 @@ import {
TsServerQuickInfoResponseSchema,
TsServerCompletionEntriesSchema,
} from './tsserver.mjs';
-import { FileSchema } from './apps.mjs';
// A _message_ over websockets
export const WebSocketMessageSchema = z.tuple([
@@ -138,72 +137,6 @@ export const TsConfigUpdatedPayloadSchema = z.object({
source: z.string(),
});
-//////////
-// APPS //
-//////////
-
-export const FilePayloadSchema = z.object({
- file: FileSchema,
-});
-
-export const FileCreatedPayloadSchema = z.object({
- file: FileSchema,
-});
-
-// Used both from client > server and server > client
-export const FileUpdatedPayloadSchema = z.object({
- file: FileSchema,
-});
-
-export const FileRenamedPayloadSchema = z.object({
- oldPath: z.string(),
- newPath: z.string(),
-});
-
-export const FileDeletedPayloadSchema = z.object({
- path: z.string(),
-});
-
-export const PreviewStatusPayloadSchema = z.union([
- z.object({ url: z.string().nullable(), status: z.enum(['booting', 'running']) }),
- z.object({
- url: z.string().nullable(),
- status: z.literal('stopped'),
- code: z.number().int().nullable(),
- }),
-]);
-
-export const PreviewStartPayloadSchema = z.object({});
-export const PreviewStopPayloadSchema = z.object({});
-
-export const PreviewLogPayloadSchema = z.object({
- log: z.union([
- z.object({ type: z.literal('stdout'), data: z.string() }),
- z.object({ type: z.literal('stderr'), data: z.string() }),
- ]),
-});
-
-export const DepsInstallLogPayloadSchema = z.object({
- log: z.union([
- z.object({ type: z.literal('stdout'), data: z.string() }),
- z.object({ type: z.literal('stderr'), data: z.string() }),
- ]),
-});
-
-export const DepsInstallStatusPayloadSchema = z.union([
- z.object({ status: z.literal('installing') }),
- z.object({
- status: z.enum(['complete', 'failed']),
- code: z.number().int(),
- }),
-]);
-
-export const DepsClearPayloadSchema = z.object({});
-export const DepsStatusPayloadSchema = z.object({});
-export const DepsStatusResponsePayloadSchema = z.object({
- nodeModulesExists: z.boolean(),
-});
-
///////////////////////
// APPS & NOTEBOOKS //
///////////////////////
diff --git a/packages/shared/src/types/apps.mts b/packages/shared/src/types/apps.mts
deleted file mode 100644
index 83bd39e34..000000000
--- a/packages/shared/src/types/apps.mts
+++ /dev/null
@@ -1,36 +0,0 @@
-import z from 'zod';
-
-import { FileSchema } from '../schemas/apps.mjs';
-
-export type AppType = {
- id: string;
- name: string;
- createdAt: number;
- updatedAt: number;
-};
-
-export type DirEntryType = {
- type: 'directory';
- // The full path relative to app root, e.g. src/assets
- path: string;
- // The path dirname relative to app root, e.g. src
- dirname: string;
- // The path basename relative to app root, e.g. assets
- basename: string;
- // null if not loaded
- children: FsEntryTreeType | null;
-};
-
-export type FileEntryType = {
- type: 'file';
- // The full path relative to app root, e.g. src/components/input.tsx
- path: string;
- // The path dirname relative to app root, e.g. src/components
- dirname: string;
- // The path basename relative to app root, e.g. input.tsx
- basename: string;
-};
-
-export type FsEntryTreeType = Array;
-
-export type FileType = z.infer;
diff --git a/packages/shared/src/types/feedback.mts b/packages/shared/src/types/feedback.mts
deleted file mode 100644
index 182cfc82c..000000000
--- a/packages/shared/src/types/feedback.mts
+++ /dev/null
@@ -1,4 +0,0 @@
-export type AppGenerationFeedbackType = {
- planId: string;
- feedback: any;
-};
diff --git a/packages/shared/src/types/history.mts b/packages/shared/src/types/history.mts
deleted file mode 100644
index 0e495c0f6..000000000
--- a/packages/shared/src/types/history.mts
+++ /dev/null
@@ -1,74 +0,0 @@
-export type FileDiffType = {
- modified: string;
- original: string | null;
- basename: string;
- dirname: string;
- path: string;
- additions: number;
- deletions: number;
- type: 'edit' | 'create' | 'delete';
-};
-
-export type UserMessageType = {
- type: 'user';
- message: string;
- planId: string;
-};
-
-export type CommandMessageType = {
- type: 'command';
- planId: string;
- command: 'npm install';
- packages: string[];
- description: string;
-};
-
-export type DiffMessageType = {
- type: 'diff';
- planId: string;
- version: string;
- diff: FileDiffType[];
-};
-
-export type PlanMessageType = {
- type: 'plan';
- planId: string;
- content: string;
-};
-
-export type MessageType = UserMessageType | DiffMessageType | CommandMessageType | PlanMessageType;
-
-export type HistoryType = Array;
-
-//////////////////////////////////////////
-// When streaming file objects from LLM //
-//////////////////////////////////////////
-
-export type DescriptionChunkType = {
- type: 'description';
- planId: string;
- data: { content: string };
-};
-
-export type FileActionChunkType = {
- type: 'file';
- description: string;
- modified: string;
- original: string | null;
- basename: string;
- dirname: string;
- path: string;
-};
-
-export type CommandActionChunkType = {
- type: 'command';
- description: string;
- command: 'npm install';
- packages: string[];
-};
-
-export type ActionChunkType = {
- type: 'action';
- planId: string;
- data: FileActionChunkType | CommandActionChunkType;
-};
diff --git a/packages/shared/src/types/websockets.mts b/packages/shared/src/types/websockets.mts
index aa0fcb450..02f8234d4 100644
--- a/packages/shared/src/types/websockets.mts
+++ b/packages/shared/src/types/websockets.mts
@@ -29,20 +29,6 @@ import {
TsServerDefinitionLocationRequestPayloadSchema,
TsServerDefinitionLocationResponsePayloadSchema,
TsServerCompletionEntriesPayloadSchema,
- FilePayloadSchema,
- FileCreatedPayloadSchema,
- FileUpdatedPayloadSchema,
- FileRenamedPayloadSchema,
- FileDeletedPayloadSchema,
- PreviewStatusPayloadSchema,
- PreviewStopPayloadSchema,
- PreviewStartPayloadSchema,
- DepsInstallLogPayloadSchema,
- DepsInstallStatusPayloadSchema,
- DepsClearPayloadSchema,
- DepsStatusPayloadSchema,
- DepsStatusResponsePayloadSchema,
- PreviewLogPayloadSchema,
} from '../schemas/websockets.mjs';
export type CellExecPayloadType = z.infer;
@@ -59,9 +45,6 @@ export type AiGeneratedCellPayloadType = z.infer;
export type DepsInstallPayloadType = z.infer;
-export type DepsClearPayloadType = z.infer;
-export type DepsStatusPayloadType = z.infer;
-export type DepsStatusResponsePayloadType = z.infer;
export type DepsValidateResponsePayloadType = z.infer;
export type DepsValidatePayloadType = z.infer;
@@ -97,19 +80,3 @@ export type TsServerDefinitionLocationResponsePayloadType = z.infer<
export type TsServerCompletionEntriesPayloadType = z.infer<
typeof TsServerCompletionEntriesPayloadSchema
>;
-
-//////////
-// APPS //
-//////////
-
-export type FilePayloadType = z.infer;
-export type FileCreatedPayloadType = z.infer;
-export type FileUpdatedPayloadType = z.infer;
-export type FileRenamedPayloadType = z.infer;
-export type FileDeletedPayloadType = z.infer;
-export type PreviewStatusPayloadType = z.infer;
-export type PreviewStartPayloadType = z.infer;
-export type PreviewStopPayloadType = z.infer;
-export type PreviewLogPayloadType = z.infer;
-export type DepsInstallLogPayloadType = z.infer;
-export type DepsInstallStatusPayloadType = z.infer;
diff --git a/packages/web/package.json b/packages/web/package.json
index ae218d91d..7cda29a2b 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -14,13 +14,10 @@
},
"dependencies": {
"@codemirror/autocomplete": "^6.18.1",
- "@codemirror/lang-css": "^6.3.0",
- "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.2.5",
"@codemirror/lint": "^6.8.1",
- "@codemirror/merge": "^6.7.0",
"@codemirror/state": "^6.4.1",
"@srcbook/components": "workspace:^",
"@srcbook/shared": "workspace:^",
@@ -28,10 +25,8 @@
"@uiw/react-codemirror": "^4.23.2",
"clsx": "^2.1.1",
"codemirror": "^6.0.1",
- "diff": "^7.0.0",
"lucide-react": "^0.439.0",
"marked": "catalog:",
- "marked-react": "^2.0.0",
"posthog-js": "^1.174.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -46,7 +41,6 @@
"zod": "catalog:"
},
"devDependencies": {
- "@types/diff": "^5.2.3",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.7.0",
diff --git a/packages/web/src/clients/http/apps.ts b/packages/web/src/clients/http/apps.ts
deleted file mode 100644
index 8f62101c8..000000000
--- a/packages/web/src/clients/http/apps.ts
+++ /dev/null
@@ -1,346 +0,0 @@
-import type {
- ActionChunkType,
- AppGenerationFeedbackType,
- AppType,
- DescriptionChunkType,
- DirEntryType,
- FileEntryType,
- FileType,
-} from '@srcbook/shared';
-import SRCBOOK_CONFIG from '@/config';
-import type { HistoryType, MessageType } from '@srcbook/shared';
-import { StreamToIterable } from '@srcbook/shared';
-
-const API_BASE_URL = `${SRCBOOK_CONFIG.api.origin}/api`;
-
-export async function createApp(request: {
- name: string;
- prompt?: string;
-}): Promise<{ data: AppType }> {
- const response = await fetch(API_BASE_URL + '/apps', {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify(request),
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function deleteApp(id: string): Promise {
- const response = await fetch(API_BASE_URL + '/apps/' + id, {
- method: 'DELETE',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-}
-
-export async function loadApps(sort: 'asc' | 'desc'): Promise<{ data: AppType[] }> {
- const response = await fetch(API_BASE_URL + '/apps?sort=' + sort, {
- method: 'GET',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function loadApp(id: string): Promise<{ data: AppType }> {
- const response = await fetch(API_BASE_URL + '/apps/' + id, {
- method: 'GET',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function updateApp(id: string, attrs: { name: string }): Promise<{ data: AppType }> {
- const response = await fetch(API_BASE_URL + '/apps/' + id, {
- method: 'PUT',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify(attrs),
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function loadDirectory(id: string, path: string): Promise<{ data: DirEntryType }> {
- const queryParams = new URLSearchParams({ path });
-
- const response = await fetch(API_BASE_URL + `/apps/${id}/directories?${queryParams}`, {
- method: 'GET',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function createDirectory(
- id: string,
- dirname: string,
- basename: string,
-): Promise<{ data: DirEntryType }> {
- const response = await fetch(API_BASE_URL + `/apps/${id}/directories`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ dirname, basename }),
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function deleteDirectory(
- id: string,
- path: string,
-): Promise<{ data: { deleted: true } }> {
- const queryParams = new URLSearchParams({ path });
-
- const response = await fetch(API_BASE_URL + `/apps/${id}/directories?${queryParams}`, {
- method: 'DELETE',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function renameDirectory(
- id: string,
- path: string,
- name: string,
-): Promise<{ data: DirEntryType }> {
- const queryParams = new URLSearchParams({ path, name });
-
- const response = await fetch(API_BASE_URL + `/apps/${id}/directories/rename?${queryParams}`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function loadFile(id: string, path: string): Promise<{ data: FileType }> {
- const queryParams = new URLSearchParams({ path });
-
- const response = await fetch(API_BASE_URL + `/apps/${id}/files?${queryParams}`, {
- method: 'GET',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function createFile(
- id: string,
- dirname: string,
- basename: string,
- source: string,
-): Promise<{ data: FileEntryType }> {
- const response = await fetch(API_BASE_URL + `/apps/${id}/files`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ dirname, basename, source }),
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function deleteFile(id: string, path: string): Promise<{ data: { deleted: true } }> {
- const queryParams = new URLSearchParams({ path });
-
- const response = await fetch(API_BASE_URL + `/apps/${id}/files?${queryParams}`, {
- method: 'DELETE',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function renameFile(
- id: string,
- path: string,
- name: string,
-): Promise<{ data: FileEntryType }> {
- const queryParams = new URLSearchParams({ path, name });
-
- const response = await fetch(API_BASE_URL + `/apps/${id}/files/rename?${queryParams}`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function aiEditApp(
- id: string,
- query: string,
- planId: string,
-): Promise> {
- const response = await fetch(API_BASE_URL + `/apps/${id}/edit`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ query, planId }),
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- const JSONDecoder = new TransformStream({
- transform(chunk, controller) {
- const lines = chunk.split('\n');
- for (const line of lines) {
- if (line.trim() !== '') {
- const parsed = JSON.parse(line);
- controller.enqueue(parsed);
- }
- }
- },
- });
-
- return StreamToIterable(
- response.body!.pipeThrough(new TextDecoderStream()).pipeThrough(JSONDecoder),
- );
-}
-
-export async function loadHistory(id: string): Promise<{ data: HistoryType }> {
- const response = await fetch(API_BASE_URL + `/apps/${id}/history`, {
- method: 'GET',
- headers: { 'content-type': 'application/json' },
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Request failed');
- }
-
- return response.json();
-}
-
-export async function appendToHistory(id: string, messages: MessageType | MessageType[]) {
- const response = await fetch(API_BASE_URL + `/apps/${id}/history`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ messages }),
- });
- return response.json();
-}
-
-export async function aiGenerationFeedback(id: string, feedback: AppGenerationFeedbackType) {
- const response = await fetch(API_BASE_URL + `/apps/${id}/feedback`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify(feedback),
- });
- return response.json();
-}
-
-export async function exportApp(id: string, name: string): Promise {
- const response = await fetch(API_BASE_URL + `/apps/${id}/export`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ name }),
- });
-
- if (!response.ok) {
- console.error(response);
- throw new Error('Export failed');
- }
-
- return response.blob();
-}
-
-type VersionResponse = {
- sha: string;
-};
-
-export async function getCurrentVersion(id: string): Promise {
- const response = await fetch(API_BASE_URL + `/apps/${id}/commit`, {
- method: 'GET',
- headers: { 'content-type': 'application/json' },
- });
- return response.json();
-}
-
-export async function commitVersion(id: string, message: string): Promise {
- const response = await fetch(API_BASE_URL + `/apps/${id}/commit`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ message }),
- });
-
- return response.json();
-}
-
-export async function checkoutVersion(
- id: string,
- sha: string,
-): Promise<{ success: true; sha: string }> {
- const response = await fetch(API_BASE_URL + `/apps/${id}/checkout/${sha}`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- });
- return response.json();
-}
diff --git a/packages/web/src/clients/websocket/index.ts b/packages/web/src/clients/websocket/index.ts
index 04cd670c4..40de71d7c 100644
--- a/packages/web/src/clients/websocket/index.ts
+++ b/packages/web/src/clients/websocket/index.ts
@@ -27,20 +27,6 @@ import {
TsServerDefinitionLocationResponsePayloadSchema,
TsServerDefinitionLocationRequestPayloadSchema,
TsServerCompletionEntriesPayloadSchema,
- FileCreatedPayloadSchema,
- FileUpdatedPayloadSchema,
- FileRenamedPayloadSchema,
- FileDeletedPayloadSchema,
- FilePayloadSchema,
- PreviewStatusPayloadSchema,
- PreviewStartPayloadSchema,
- PreviewStopPayloadSchema,
- DepsInstallLogPayloadSchema,
- DepsInstallStatusPayloadSchema,
- DepsClearPayloadSchema,
- DepsStatusResponsePayloadSchema,
- DepsStatusPayloadSchema,
- PreviewLogPayloadSchema,
} from '@srcbook/shared';
import Channel from '@/clients/websocket/channel';
import WebSocketClient from '@/clients/websocket/client';
@@ -96,41 +82,3 @@ export class SessionChannel extends Channel<
});
}
}
-
-const IncomingAppEvents = {
- file: FilePayloadSchema,
- 'file:updated': FileUpdatedPayloadSchema,
- 'preview:status': PreviewStatusPayloadSchema,
- 'preview:log': PreviewLogPayloadSchema,
- 'deps:install:log': DepsInstallLogPayloadSchema,
- 'deps:install:status': DepsInstallStatusPayloadSchema,
- 'deps:status:response': DepsStatusResponsePayloadSchema,
-};
-
-const OutgoingAppEvents = {
- 'file:created': FileCreatedPayloadSchema,
- 'file:updated': FileUpdatedPayloadSchema,
- 'file:renamed': FileRenamedPayloadSchema,
- 'file:deleted': FileDeletedPayloadSchema,
- 'preview:start': PreviewStartPayloadSchema,
- 'preview:stop': PreviewStopPayloadSchema,
- 'deps:install': DepsInstallPayloadSchema,
- 'deps:clear': DepsClearPayloadSchema,
- 'deps:status': DepsStatusPayloadSchema,
-};
-
-export class AppChannel extends Channel {
- appId: string;
-
- static create(appId: string) {
- return new AppChannel(appId);
- }
-
- constructor(appId: string) {
- super(client, `app:${appId}`, {
- incoming: IncomingAppEvents,
- outgoing: OutgoingAppEvents,
- });
- this.appId = appId;
- }
-}
diff --git a/packages/web/src/components/apps/AiFeedbackModal.tsx b/packages/web/src/components/apps/AiFeedbackModal.tsx
deleted file mode 100644
index 9396d3ed5..000000000
--- a/packages/web/src/components/apps/AiFeedbackModal.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import * as React from 'react';
-import {
- Button,
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@srcbook/components';
-import TextareaAutosize from 'react-textarea-autosize';
-
-interface AiFeedbackModalProps {
- isOpen: boolean;
- onClose: () => void;
- onSubmit: (feedback: string) => void;
-}
-
-export function AiFeedbackModal({ isOpen, onClose, onSubmit }: AiFeedbackModalProps) {
- const [feedback, setFeedback] = React.useState('');
-
- const handleSubmit = () => {
- onSubmit(feedback);
- setFeedback('');
- onClose();
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
- e.preventDefault();
- handleSubmit();
- }
- };
-
- return (
-
- );
-}
diff --git a/packages/web/src/components/apps/bottom-drawer.tsx b/packages/web/src/components/apps/bottom-drawer.tsx
deleted file mode 100644
index fb81c22d8..000000000
--- a/packages/web/src/components/apps/bottom-drawer.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import { BanIcon, XIcon } from 'lucide-react';
-import { useHotkeys } from 'react-hotkeys-hook';
-
-import { Button } from '@srcbook/components/src/components/ui/button';
-import { cn } from '@/lib/utils.ts';
-import { useLogs } from './use-logs';
-import { useEffect, useRef } from 'react';
-
-const DRAWER_HEIGHT = 320;
-
-export default function BottomDrawer() {
- const { logs, clearLogs, open, togglePane, closePane } = useLogs();
-
- useHotkeys('mod+shift+y', () => {
- togglePane();
- });
-
- const scrollWrapperRef = useRef(null);
-
- // Scroll to the bottom of the logs panel when the user opens the panel fresh
- useEffect(() => {
- if (!scrollWrapperRef.current) {
- return;
- }
- scrollWrapperRef.current.scrollTop = scrollWrapperRef.current.scrollHeight;
- }, [open]);
-
- // Determine if the user has scrolled all the way to the bottom of the div
- const scrollPinnedToBottomRef = useRef(false);
- useEffect(() => {
- if (!scrollWrapperRef.current) {
- return;
- }
- const element = scrollWrapperRef.current;
-
- const onScroll = () => {
- scrollPinnedToBottomRef.current =
- element.scrollTop === element.scrollHeight - element.clientHeight;
- };
-
- element.addEventListener('scroll', onScroll);
- return () => element.removeEventListener('scroll', onScroll);
- }, []);
-
- // If the user has scrolled all the way to the bottom, then keep the bottom scroll pinned as new
- // logs come in.
- useEffect(() => {
- if (!scrollWrapperRef.current) {
- return;
- }
-
- if (scrollPinnedToBottomRef.current) {
- scrollWrapperRef.current.scrollTop = scrollWrapperRef.current.scrollHeight;
- }
- }, [logs]);
-
- return (
-
-
-
-
-
- {open && logs.length > 0 && (
-
- )}
- {open && (
-
- )}
-
-
-
- {open && (
-
-
-
- {logs.map((log, index) => (
-
- |
-
- {log.timestamp.toISOString()}
-
- |
-
- {log.source}
- |
-
-
- {log.message}
-
- |
-
- ))}
-
-
- {logs.length === 0 && (
-
- No logs
-
- )}
-
- )}
-
- );
-}
diff --git a/packages/web/src/components/apps/create-modal.tsx b/packages/web/src/components/apps/create-modal.tsx
deleted file mode 100644
index 99900be3d..000000000
--- a/packages/web/src/components/apps/create-modal.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import { useState, KeyboardEvent } from 'react';
-import { cn } from '@/lib/utils';
-import { Input } from '@srcbook/components/src/components/ui/input';
-import { Button } from '@srcbook/components/src/components/ui/button';
-import { useNavigate } from 'react-router-dom';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@srcbook/components/src/components/ui/dialog';
-
-import { HelpCircle, Sparkles, Loader2 } from 'lucide-react';
-import { Textarea } from '@srcbook/components/src/components/ui/textarea';
-import {
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
- Tooltip,
-} from '@srcbook/components/src/components/ui/tooltip';
-import { useSettings } from '../use-settings';
-
-type PropsType = {
- onClose: () => void;
- onCreate: (name: string, prompt?: string) => Promise;
-};
-
-export default function CreateAppModal({ onClose, onCreate }: PropsType) {
- const [name, setName] = useState('');
- const [prompt, setPrompt] = useState('');
-
- const { aiEnabled } = useSettings();
- const navigate = useNavigate();
-
- const [submitting, setSubmitting] = useState(false);
-
- const validPrompt = prompt.trim() !== '';
-
- async function onSubmit(e: React.FormEvent) {
- e.preventDefault();
- e.stopPropagation();
-
- if (submitting || !validPrompt) {
- return;
- }
-
- setSubmitting(true);
-
- try {
- await onCreate(name, prompt.trim() === '' ? undefined : prompt);
- } finally {
- setSubmitting(false);
- }
- }
-
- const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
- onSubmit(e);
- }
- };
-
- return (
-
- );
-}
diff --git a/packages/web/src/components/apps/diff-modal.tsx b/packages/web/src/components/apps/diff-modal.tsx
deleted file mode 100644
index e3bee5803..000000000
--- a/packages/web/src/components/apps/diff-modal.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { cn } from '@/lib/utils';
-import { Button } from '@srcbook/components';
-import {
- Dialog,
- DialogContent,
- DialogTitle,
- DialogDescription,
-} from '@srcbook/components/src/components/ui/dialog';
-import { Undo2Icon } from 'lucide-react';
-import type { FileDiffType } from '@srcbook/shared';
-import { DiffSquares, DiffStats } from './diff-stats';
-import { DiffEditor } from './editor';
-
-type PropsType = {
- onUndoAll: () => void;
- onClose: () => void;
- files: FileDiffType[];
-};
-
-export default function DiffModal({ files, onClose, onUndoAll }: PropsType) {
- return (
-
- );
-}
-
-function DiffModalHeader({
- numFiles,
- onClose,
- onUndoAll,
-}: {
- numFiles: number;
- onClose: () => void;
- onUndoAll: () => void;
-}) {
- return (
-
-
- {`${numFiles} ${numFiles === 1 ? 'file' : 'files'} changed`}
-
-
-
-
-
-
- );
-}
-
-function FileDiff({ file }: { file: FileDiffType }) {
- return (
-
- );
-}
diff --git a/packages/web/src/components/apps/diff-stats.tsx b/packages/web/src/components/apps/diff-stats.tsx
deleted file mode 100644
index 23b6bbfdb..000000000
--- a/packages/web/src/components/apps/diff-stats.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { cn } from '@/lib/utils';
-import { calculateSquares } from './lib/diff';
-
-export function DiffStats(props: { additions: number; deletions: number; className?: string }) {
- return (
-
- +{props.additions}
- -{props.deletions}
-
- );
-}
-
-export function DiffSquares(props: { additions: number; deletions: number; className?: string }) {
- const squares = calculateSquares(props.additions, props.deletions);
-
- return (
-
- {squares.map((square, index) => (
-
- ))}
-
- );
-}
diff --git a/packages/web/src/components/apps/editor.tsx b/packages/web/src/components/apps/editor.tsx
deleted file mode 100644
index bf77eeef1..000000000
--- a/packages/web/src/components/apps/editor.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import CodeMirror from '@uiw/react-codemirror';
-import { css } from '@codemirror/lang-css';
-import { html } from '@codemirror/lang-html';
-import { json } from '@codemirror/lang-json';
-import { javascript } from '@codemirror/lang-javascript';
-import { markdown } from '@codemirror/lang-markdown';
-import useTheme from '@srcbook/components/src/components/use-theme';
-import { extname } from './lib/path';
-import { EditorView } from 'codemirror';
-import { EditorState } from '@codemirror/state';
-import { unifiedMergeView } from '@codemirror/merge';
-
-export function CodeEditor({
- path,
- source,
- onChange,
-}: {
- path: string;
- source: string;
- onChange: (updatedSource: string) => void;
-}) {
- const { codeTheme } = useTheme();
-
- const languageExtension = getCodeMirrorLanguageExtension(path);
- const extensions = languageExtension ? [languageExtension] : [];
-
- return (
-
- );
-}
-
-export function DiffEditor({
- path,
- modified,
- original,
- collapseUnchanged,
-}: {
- path: string;
- modified: string;
- original: string | null;
- collapseUnchanged?: {
- minSize: number;
- margin: number;
- };
-}) {
- const { codeTheme } = useTheme();
-
- const extensions = [
- EditorView.editable.of(false),
- EditorState.readOnly.of(true),
- unifiedMergeView({
- original: original ?? '',
- mergeControls: false,
- highlightChanges: false,
- collapseUnchanged: collapseUnchanged,
- }),
- ];
-
- const languageExtension = getCodeMirrorLanguageExtension(path);
-
- if (languageExtension) {
- extensions.unshift(languageExtension);
- }
-
- return ;
-}
-
-function getCodeMirrorLanguageExtension(path: string) {
- switch (extname(path)) {
- case '.json':
- return json();
- case '.css':
- return css();
- case '.html':
- return html();
- case '.md':
- case '.markdown':
- return markdown();
- case '.js':
- case '.cjs':
- case '.mjs':
- case '.jsx':
- case '.ts':
- case '.cts':
- case '.mts':
- case '.tsx':
- return javascript({ typescript: true, jsx: true });
- }
-}
diff --git a/packages/web/src/components/apps/header.tsx b/packages/web/src/components/apps/header.tsx
deleted file mode 100644
index f65161403..000000000
--- a/packages/web/src/components/apps/header.tsx
+++ /dev/null
@@ -1,300 +0,0 @@
-import {
- ShareIcon,
- PlayIcon,
- StopCircleIcon,
- PlayCircleIcon,
- Code2Icon,
- Loader2Icon,
- CircleAlertIcon,
- PanelBottomOpenIcon,
- PanelBottomCloseIcon,
- ExternalLinkIcon,
-} from 'lucide-react';
-import { Link } from 'react-router-dom';
-import { SrcbookLogo } from '@/components/logos';
-
-import { Button } from '@srcbook/components/src/components/ui/button';
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from '@srcbook/components/src/components/ui/tooltip';
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogDescription,
-} from '@srcbook/components/src/components/ui/dialog';
-import { cn } from '@/lib/utils';
-import { usePackageJson } from './use-package-json';
-import { useApp } from './use-app';
-import { Input } from '@srcbook/components';
-import { useState } from 'react';
-import { usePreview } from './use-preview';
-import { exportApp } from '@/clients/http/apps';
-import { toast } from 'sonner';
-import { useLogs } from './use-logs';
-
-export type HeaderTab = 'code' | 'preview';
-
-type PropsType = {
- className?: string;
- tab: HeaderTab;
- onChangeTab: (newTab: HeaderTab) => void;
-};
-
-export default function EditorHeader(props: PropsType) {
- const { app, updateApp } = useApp();
- const { url, start: startPreview, stop: stopPreview, status: previewStatus } = usePreview();
- const { status: npmInstallStatus, nodeModulesExists } = usePackageJson();
- const [isExporting, setIsExporting] = useState(false);
- const { open, togglePane, panelIcon } = useLogs();
-
- const [nameChangeDialogOpen, setNameChangeDialogOpen] = useState(false);
-
- const handleExport = async () => {
- try {
- setIsExporting(true);
- const blob = await exportApp(app.id, app.name);
- const url = window.URL.createObjectURL(blob);
-
- // Create a temporary anchor element to trigger the download
- const a = document.createElement('a');
- a.style.display = 'none';
- a.href = url;
- a.download = `${app.name}.zip`;
-
- // Append to the document, trigger click, and remove
- document.body.appendChild(a);
- a.click();
- window.URL.revokeObjectURL(url);
- document.body.removeChild(a);
-
- toast.success('App exported successfully!');
- setIsExporting(false);
- } catch (error) {
- console.error('Export failed:', error);
- toast.error('Failed to export app. Please try again.');
- }
- };
-
- return (
- <>
- {nameChangeDialogOpen && (
- {
- updateApp({ name });
- setNameChangeDialogOpen(false);
- }}
- onClose={() => {
- setNameChangeDialogOpen(false);
- }}
- />
- )}
-
- {npmInstallStatus === 'installing' ? (
-
- ) : null}
-
-
- >
- );
-}
-
-function UpdateAppNameDialog(props: {
- name: string;
- onClose: () => void;
- onUpdate: (name: string) => void;
-}) {
- const [name, setName] = useState(props.name);
-
- return (
-
- );
-}
diff --git a/packages/web/src/components/apps/lib/diff.ts b/packages/web/src/components/apps/lib/diff.ts
deleted file mode 100644
index 5407b8ff7..000000000
--- a/packages/web/src/components/apps/lib/diff.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import * as Diff from 'diff';
-
-export function diffFiles(
- original: string,
- modified: string,
-): { additions: number; deletions: number } {
- const changes: Diff.Change[] = Diff.diffLines(original, modified);
-
- let additions: number = 0;
- let deletions: number = 0;
-
- changes.forEach((part: Diff.Change) => {
- if (part.added) {
- additions += part.count ?? 0;
- } else if (part.removed) {
- deletions += part.count ?? 0;
- }
- });
-
- return { additions, deletions };
-}
-
-type AddedType = 1;
-type RemovedType = -1;
-type UnChangedType = 0;
-type ChangeType = AddedType | RemovedType | UnChangedType;
-
-export function calculateSquares(
- additions: number,
- deletions: number,
- maxSquares: number = 5,
-): ChangeType[] {
- const totalChanges = additions + deletions;
-
- if (totalChanges === 0) {
- return Array(maxSquares).fill(0);
- }
-
- if (totalChanges <= maxSquares) {
- return createSquares(additions, deletions, maxSquares);
- }
-
- // Calculate the proportion of added and removed lines
- const addedProportion = additions / totalChanges;
-
- // Calculate the number of squares for added, ensuring at least 1 if there are any additions
- let addedSquares = Math.round(addedProportion * maxSquares);
- addedSquares = additions > 0 ? Math.max(1, addedSquares) : 0;
-
- // Calculate removed squares, ensuring at least 1 if there are any removals
- let deletedSquares = maxSquares - addedSquares;
- deletedSquares = deletions > 0 ? Math.max(1, deletedSquares) : 0;
-
- // Final adjustment to ensure we don't exceed maxSquares
- if (addedSquares + deletedSquares > maxSquares) {
- if (additions > deletions) {
- deletedSquares = maxSquares - addedSquares;
- } else {
- addedSquares = maxSquares - deletedSquares;
- }
- }
-
- return createSquares(addedSquares, deletedSquares, maxSquares);
-}
-
-function createSquares(added: number, deleted: number, max: number): ChangeType[] {
- if (added + deleted > max) {
- console.error(`Expected max ${max} squares but got ${added + deleted}`);
- }
-
- const result: ChangeType[] = [];
-
- for (let i = 0; i < added; i++) {
- result.push(1);
- }
-
- for (let i = 0; i < deleted; i++) {
- result.push(-1);
- }
-
- // If there's remaining space, fill with 'unchanged'
- for (let i = 0, len = max - result.length; i < len; i++) {
- result.push(0);
- }
-
- return result;
-}
diff --git a/packages/web/src/components/apps/lib/file-tree.ts b/packages/web/src/components/apps/lib/file-tree.ts
deleted file mode 100644
index f30aadd76..000000000
--- a/packages/web/src/components/apps/lib/file-tree.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-import type { DirEntryType, FileEntryType, FsEntryTreeType } from '@srcbook/shared';
-
-/**
- * Sorts a file tree (in place) by name. Folders come first, then files.
- */
-export function sortTree(tree: DirEntryType): DirEntryType {
- tree.children?.sort((a, b) => {
- if (a.type === 'directory') sortTree(a);
- if (b.type === 'directory') sortTree(b);
- if (a.type === 'directory' && b.type === 'file') return -1;
- if (a.type === 'file' && b.type === 'directory') return 1;
- return a.basename.localeCompare(b.basename);
- });
-
- return tree;
-}
-
-/**
- * Update a directory node in the file tree.
- *
- * This function is complex due to the merging of children. We do it to maintain
- * nested state of a given tree. Consider the following file tree that the user
- * has open in their file tree viewer:
- *
- * /src
- * │
- * ├── components
- * │ ├── ui
- * │ │ └── table
- * │ │ ├── index.tsx
- * │ │ └── show.tsx
- * │ │
- * │ └── use-files.tsx
- * │
- * └── index.tsx
- *
- * If the user closes and then reopens the "components" folder, the reopening of
- * the "components" folder will make a call to load its children. However, calls
- * to load children only load the immediate children, not all nested children.
- * This means that the call will not load the "ui" folder's children.
- *
- * Now, given that the user had previously opened the "ui" folder and we have the
- * results of that folder loaded in our state, we don't want to throw away those
- * values. So we merge the children of the new node and any nested children of
- * the old node.
- *
- * This supports behavior where a user may open many nested folders and then close
- * and later reopen a ancestor folder. We want the tree to look the same when the
- * reopen occurs with only the immediate children updated.
- */
-export function updateDirNode(tree: DirEntryType, node: DirEntryType): DirEntryType {
- return sortTree(doUpdateDirNode(tree, node));
-}
-
-function doUpdateDirNode(tree: DirEntryType, node: DirEntryType): DirEntryType {
- if (tree.path === node.path) {
- if (node.children === null) {
- return { ...node, children: tree.children };
- } else {
- return { ...node, children: merge(tree.children, node.children) };
- }
- }
-
- if (tree.children) {
- return {
- ...tree,
- children: tree.children.map((entry) => {
- if (entry.type === 'directory') {
- return doUpdateDirNode(entry, node);
- } else {
- return entry;
- }
- }),
- };
- }
-
- return tree;
-}
-
-function merge(oldChildren: FsEntryTreeType | null, newChildren: FsEntryTreeType): FsEntryTreeType {
- if (!oldChildren) {
- return newChildren;
- }
-
- return newChildren.map((newChild) => {
- const oldChild = oldChildren.find((old) => old.path === newChild.path);
-
- if (oldChild && oldChild.type === 'directory' && newChild.type === 'directory') {
- return {
- ...newChild,
- children:
- newChild.children === null
- ? oldChild.children
- : merge(oldChild.children, newChild.children),
- };
- }
-
- return newChild;
- });
-}
-
-export function renameDirNode(
- tree: DirEntryType,
- oldNode: DirEntryType,
- newNode: DirEntryType,
-): DirEntryType {
- return sortTree(doRenameDirNode(tree, oldNode, newNode));
-}
-
-function doRenameDirNode(
- tree: DirEntryType,
- oldNode: DirEntryType,
- newNode: DirEntryType,
-): DirEntryType {
- const children =
- tree.children === null
- ? null
- : tree.children.map((entry) => {
- if (entry.type === 'directory') {
- return doRenameDirNode(entry, oldNode, newNode);
- } else {
- if (entry.path.startsWith(oldNode.path)) {
- return { ...entry, path: entry.path.replace(oldNode.path, newNode.path) };
- } else {
- return entry;
- }
- }
- });
-
- if (tree.path === oldNode.path) {
- return { ...newNode, children };
- } else if (tree.path.startsWith(oldNode.path)) {
- const path = tree.path.replace(oldNode.path, newNode.path);
- return { ...tree, path, children };
- } else {
- return { ...tree, children };
- }
-}
-
-export function updateFileNode(
- tree: DirEntryType,
- oldNode: FileEntryType,
- newNode: FileEntryType,
-): DirEntryType {
- return sortTree(doUpdateFileNode(tree, oldNode, newNode));
-}
-
-function doUpdateFileNode(
- tree: DirEntryType,
- oldNode: FileEntryType,
- newNode: FileEntryType,
-): DirEntryType {
- if (tree.children === null) {
- return tree;
- }
-
- const children = [];
-
- for (const entry of tree.children) {
- if (entry.path === oldNode.path) {
- children.push(newNode);
- } else {
- if (entry.type === 'directory') {
- children.push(doUpdateFileNode(entry, oldNode, newNode));
- } else {
- children.push(entry);
- }
- }
- }
-
- return { ...tree, children };
-}
-
-/**
- * Delete a node from the file tree.
- *
- * This doesn't affect sort order, so no need to call sortTree.
- */
-export function deleteNode(tree: DirEntryType, path: string): DirEntryType {
- if (tree.children === null) {
- return tree;
- }
-
- const children: FsEntryTreeType = [];
-
- for (const entry of tree.children) {
- if (entry.path === path) {
- continue;
- }
-
- if (entry.type === 'directory') {
- children.push(deleteNode(entry, path));
- } else {
- children.push(entry);
- }
- }
-
- return { ...tree, children };
-}
-
-/**
- * Create a new node in the file tree.
- */
-export function createNode(tree: DirEntryType, node: DirEntryType | FileEntryType): DirEntryType {
- return sortTree(doCreateNode(tree, node));
-}
-
-function doCreateNode(tree: DirEntryType, node: DirEntryType | FileEntryType): DirEntryType {
- if (tree.children === null) {
- return tree;
- }
-
- // To avoid duplicate entries in the tree, ensure that we 'upsert' here.
- if (tree.path === node.dirname) {
- const idx = tree.children.findIndex((entry) => entry.path === node.path);
- const children = [...tree.children];
-
- if (idx === -1) {
- children.push(node);
- } else {
- children.splice(idx, 1, node);
- }
-
- return { ...tree, children };
- }
-
- const children = tree.children.map((entry) => {
- if (entry.type === 'directory') {
- return doCreateNode(entry, node);
- } else {
- return entry;
- }
- });
-
- return { ...tree, children };
-}
diff --git a/packages/web/src/components/apps/lib/path.ts b/packages/web/src/components/apps/lib/path.ts
deleted file mode 100644
index d92407176..000000000
--- a/packages/web/src/components/apps/lib/path.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// This file and client side code assumes posix paths. It is incomplete and handles basic
-// functionality. That should be ok as we expect a subset of behavior and assume simple paths.
-
-export function extname(path: string) {
- const idx = path.lastIndexOf('.');
- return idx === -1 ? '' : path.slice(idx);
-}
diff --git a/packages/web/src/components/apps/local-storage.ts b/packages/web/src/components/apps/local-storage.ts
deleted file mode 100644
index 553420872..000000000
--- a/packages/web/src/components/apps/local-storage.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { FileType } from '@srcbook/shared';
-
-export function getLastOpenedFile(appId: string) {
- const value = window.localStorage.getItem(`apps:${appId}:last_opened_file`);
-
- if (typeof value === 'string') {
- return JSON.parse(value);
- }
-
- return null;
-}
-
-export function setLastOpenedFile(appId: string, file: FileType) {
- return window.localStorage.setItem(`apps:${appId}:last_opened_file`, JSON.stringify(file));
-}
diff --git a/packages/web/src/components/apps/markdown.tsx b/packages/web/src/components/apps/markdown.tsx
deleted file mode 100644
index b5df654c7..000000000
--- a/packages/web/src/components/apps/markdown.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import MarkdownReact from 'marked-react';
-import { cn } from '@srcbook/components';
-
-export default function Markdown(props: { source: string; className?: string }) {
- return (
-
- {props.source}
-
- );
-}
diff --git a/packages/web/src/components/apps/package-install-toast.tsx b/packages/web/src/components/apps/package-install-toast.tsx
deleted file mode 100644
index 004c6e3cc..000000000
--- a/packages/web/src/components/apps/package-install-toast.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { useEffect, useState } from 'react';
-import { CircleAlertIcon, InfoIcon, Loader2Icon } from 'lucide-react';
-
-import { usePackageJson } from './use-package-json';
-import { useLogs } from './use-logs';
-import { Button } from '@srcbook/components/src/components/ui/button';
-import { cn } from '@/lib/utils';
-
-const ToastWrapper: React.FC<{
- showToast: boolean;
- className?: string;
- children: React.ReactNode;
-}> = ({ className, showToast, children }) => (
-
- {children}
-
-);
-
-const PackageInstallToast: React.FunctionComponent = () => {
- const { togglePane } = useLogs();
- const { status, npmInstall, nodeModulesExists } = usePackageJson();
- const [showToast, setShowToast] = useState(false);
-
- useEffect(() => {
- if (nodeModulesExists === false && (status === 'idle' || status === 'complete')) {
- setShowToast(true);
- } else if (nodeModulesExists === true) {
- setShowToast(false);
- }
- }, [nodeModulesExists, status]);
-
- switch (status) {
- case 'installing':
- return (
-
-
-
- Installing Packages...
-
-
-
-
- );
-
- case 'failed':
- return (
-
-
-
- Packages failed to install
-
-
-
-
-
-
-
- );
-
- case 'idle':
- case 'complete':
- return (
-
-
-
- Packages need to be installed
-
-
-
-
- );
- }
-};
-
-export default PackageInstallToast;
diff --git a/packages/web/src/components/apps/panels/explorer.tsx b/packages/web/src/components/apps/panels/explorer.tsx
deleted file mode 100644
index cc37ba0a6..000000000
--- a/packages/web/src/components/apps/panels/explorer.tsx
+++ /dev/null
@@ -1,384 +0,0 @@
-import { useEffect, useRef, useState } from 'react';
-import { FileIcon, ChevronRightIcon } from 'lucide-react';
-import { useFiles } from '../use-files';
-import type { DirEntryType, FileEntryType } from '@srcbook/shared';
-import { cn } from '@srcbook/components';
-import {
- ContextMenu,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuTrigger,
-} from '@srcbook/components/src/components/ui/context-menu';
-import { useVersion } from '../use-version';
-
-export default function ExplorerPanel() {
- const { fileTree } = useFiles();
- const { currentVersion } = useVersion();
- const [editingEntry, setEditingEntry] = useState(null);
- const [newEntry, setNewEntry] = useState(null);
-
- return (
-
-
-
-
-
-
-
- setNewEntry({ type: 'file', path: 'untitled', dirname: '.', basename: 'untitled' })
- }
- >
- New file...
-
-
- setNewEntry({
- type: 'directory',
- path: 'untitled',
- dirname: '.',
- basename: 'untitled',
- children: null,
- })
- }
- >
- New folder...
-
-
-
-
- {currentVersion && (
-
- version: {currentVersion.sha.slice(0, 7)}
-
- )}
-
- );
-}
-
-function FileTree(props: {
- depth: number;
- tree: DirEntryType;
- newEntry: FileEntryType | DirEntryType | null;
- setNewEntry: (entry: FileEntryType | DirEntryType | null) => void;
- editingEntry: FileEntryType | DirEntryType | null;
- setEditingEntry: (entry: FileEntryType | DirEntryType | null) => void;
-}) {
- const { depth, tree, newEntry, setNewEntry, editingEntry, setEditingEntry } = props;
-
- const {
- openFile,
- createFile,
- deleteFile,
- renameFile,
- openedFile,
- toggleFolder,
- isFolderOpen,
- openFolder,
- createFolder,
- deleteFolder,
- renameFolder,
- } = useFiles();
-
- if (tree.children === null) {
- return null;
- }
-
- const dirEntries = [];
- const fileEntries = [];
-
- for (const entry of tree.children) {
- if (entry.type === 'directory') {
- dirEntries.push(entry);
- } else {
- fileEntries.push(entry);
- }
- }
-
- const elements = [];
-
- if (newEntry !== null && newEntry.type === 'directory' && newEntry.dirname === tree.path) {
- elements.push(
-
- {
- createFolder(tree.path, name);
- setNewEntry(null);
- }}
- onCancel={() => setNewEntry(null)}
- />
- ,
- );
- }
-
- for (const entry of dirEntries) {
- const opened = isFolderOpen(entry);
-
- if (editingEntry?.path === entry.path) {
- elements.push(
-
- {
- renameFolder(entry, name);
- setEditingEntry(null);
- }}
- onCancel={() => setEditingEntry(null)}
- />
- ,
- );
- } else {
- elements.push(
-
- toggleFolder(entry)}
- onDelete={() => deleteFolder(entry)}
- onRename={() => setEditingEntry(entry)}
- onNewFile={() => {
- if (!isFolderOpen(entry)) {
- openFolder(entry);
- }
- setNewEntry({
- type: 'file',
- path: entry.path + '/untitled',
- dirname: entry.path,
- basename: 'untitled',
- });
- }}
- onNewfolder={() => {
- if (!isFolderOpen(entry)) {
- openFolder(entry);
- }
- setNewEntry({
- type: 'directory',
- path: entry.path + '/untitled',
- dirname: entry.path,
- basename: 'untitled',
- children: null,
- });
- }}
- />
- ,
- );
- }
-
- if (opened) {
- elements.push(
- ,
- );
- }
- }
-
- if (newEntry !== null && newEntry.type === 'file' && newEntry.dirname === tree.path) {
- elements.push(
-
- {
- const diskEntry = await createFile(tree.path, name);
- openFile(diskEntry);
- setNewEntry(null);
- }}
- onCancel={() => setNewEntry(null)}
- />
- ,
- );
- }
-
- for (const entry of fileEntries) {
- if (entry.path === editingEntry?.path) {
- elements.push(
-
- {
- renameFile(entry, name);
- setEditingEntry(null);
- }}
- onCancel={() => setEditingEntry(null)}
- />
- ,
- );
- } else {
- elements.push(
-
- openFile(entry)}
- onDelete={() => deleteFile(entry)}
- onRename={() => setEditingEntry(entry)}
- />
- ,
- );
- }
- }
-
- return elements;
-}
-
-function FileNode(props: {
- depth: number;
- label: string;
- active: boolean;
- onClick: () => void;
- onDelete: () => void;
- onRename: () => void;
-}) {
- return (
-
-
- } />
-
-
- Rename
- Delete
-
-
- );
-}
-
-function FolderNode(props: {
- depth: number;
- label: string;
- opened: boolean;
- onClick: () => void;
- onDelete: () => void;
- onRename: () => void;
- onNewFile: () => void;
- onNewfolder: () => void;
-}) {
- return (
-
-
-
- }
- />
-
- {
- // This is an important line of code. It is needed to prevent focus
- // from returning to other elements when this menu is closed. Without this,
- // when a user clicks "New [file|folder]" or "Rename", the input box will
- // render and sometimes immediately dismiss because this returns focus to
- // the button element the user right clicked on, causing the input's onBlur
- // to trigger.
- e.preventDefault();
- }}
- >
- New file...
- New folder...
- Rename
- Delete
-
-
- );
-}
-
-function EditNameNode(props: {
- depth: number;
- name: string;
- onSubmit: (name: string) => void;
- onCancel: () => void;
-}) {
- const ref = useRef(null);
-
- useEffect(() => {
- function focusAndSelect() {
- const input = ref.current;
-
- if (input) {
- input.focus();
- const idx = input.value.lastIndexOf('.');
- input.setSelectionRange(0, idx === -1 ? input.value.length : idx);
- }
- }
-
- // This setTimeout is intentional. We need to draw focus to this
- // input after the current event loop clears out because other elements
- // are getting focused in some situations immediately after this renders.
- setTimeout(focusAndSelect, 0);
- }, []);
-
- return (
- {
- if (e.key === 'Enter' && ref.current) {
- e.preventDefault();
- e.stopPropagation();
- props.onSubmit(ref.current.value);
- } else if (e.key === 'Escape') {
- ref.current?.blur();
- }
- }}
- />
- );
-}
-
-function Node(props: {
- depth: number;
- label: string;
- icon: React.ReactNode;
- active?: boolean;
- onClick: () => void;
-}) {
- const { depth, label, icon, active, onClick } = props;
-
- return (
-
- );
-}
diff --git a/packages/web/src/components/apps/panels/settings.tsx b/packages/web/src/components/apps/panels/settings.tsx
deleted file mode 100644
index dc5954ea4..000000000
--- a/packages/web/src/components/apps/panels/settings.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Button } from '@srcbook/components/src/components/ui/button';
-import { usePackageJson } from '../use-package-json';
-import { PackagePlus } from 'lucide-react';
-import Shortcut from '@srcbook/components/src/components/keyboard-shortcut';
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from '@srcbook/components/src/components/ui/tooltip';
-
-export default function PackagesPanel() {
- const { setShowInstallModal, npmInstall, clearNodeModules, nodeModulesExists, status } =
- usePackageJson();
-
- return (
-
-
-
- To add packages, you can simply ask the AI in chat, or use the button below.
-
-
-
-
-
-
-
-
-
- Install packages
-
-
-
-
-
-
-
-
- If you suspect your node_modules are corrupted, you can clear them and reinstall all
- packages.
-
-
-
-
-
-
-
-
- Re-run npm install. This will run against the package.json
- from the project root.
-
-
-
-
-
-
- );
-}
diff --git a/packages/web/src/components/apps/sidebar.tsx b/packages/web/src/components/apps/sidebar.tsx
deleted file mode 100644
index dd82aa53d..000000000
--- a/packages/web/src/components/apps/sidebar.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-import { useState } from 'react';
-import useTheme from '@srcbook/components/src/components/use-theme';
-
-import {
- ChevronsLeftIcon,
- FlagIcon,
- FolderTreeIcon,
- KeyboardIcon,
- MoonIcon,
- PackageIcon,
- SunIcon,
-} from 'lucide-react';
-import { Button } from '@srcbook/components/src/components/ui/button';
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from '@srcbook/components/src/components/ui/tooltip';
-import KeyboardShortcutsDialog from '../keyboard-shortcuts-dialog';
-import FeedbackDialog from '../feedback-dialog';
-import { cn } from '@/lib/utils';
-import ExplorerPanel from './panels/explorer';
-import PackagesPanel from './panels/settings';
-import { usePackageJson } from './use-package-json';
-
-export type PanelType = 'explorer' | 'packages';
-
-function getTitleForPanel(panel: PanelType | null): string | null {
- switch (panel) {
- case 'explorer':
- return 'Files';
- case 'packages':
- return 'Manage Packages';
- default:
- return null;
- }
-}
-
-type SidebarProps = {
- initialPanel: PanelType | null;
-};
-
-export default function Sidebar({ initialPanel }: SidebarProps) {
- const { theme, toggleTheme } = useTheme();
-
- const { status } = usePackageJson();
- const [panel, _setPanel] = useState(initialPanel);
- const [showShortcuts, setShowShortcuts] = useState(false);
- const [showFeedback, setShowFeedback] = useState(false);
-
- function setPanel(nextPanel: PanelType) {
- _setPanel(nextPanel === panel ? null : nextPanel);
- }
-
- return (
- <>
-
-
-
-
-
-
-
setPanel('explorer')}>
-
-
-
setPanel('packages')}>
-
-
-
-
-
- {theme === 'light' ? (
-
- ) : (
-
- )}
-
- setShowShortcuts(true)}
- >
-
-
- setShowFeedback(true)}
- >
-
-
-
-
-
{
- if (panel !== null) {
- setPanel(panel);
- }
- }}
- >
- {panel === 'explorer' && }
- {panel === 'packages' && }
-
-
- >
- );
-}
-
-function NavItemWithTooltip(props: {
- children: React.ReactNode;
- tooltipContent: string;
- onClick: () => void;
-}) {
- return (
-
-
-
-
-
- {props.tooltipContent}
-
-
- );
-}
-
-function Panel(props: {
- open: boolean;
- title: string | null;
- onClose: () => void;
- children: React.ReactNode;
-}) {
- if (!props.open) {
- return null;
- }
-
- return (
-
-
-
{props.title}
-
-
-
{props.children}
-
- );
-}
diff --git a/packages/web/src/components/apps/types.ts b/packages/web/src/components/apps/types.ts
deleted file mode 100644
index cf2c0cd49..000000000
--- a/packages/web/src/components/apps/types.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { CommandMessageType } from '@srcbook/shared';
-
-export type FileType = {
- type: 'file';
- modified: string;
- original: string | null;
- path: string;
- basename: string;
- dirname: string;
- description: string;
-};
-
-// TODO this should likely all be shared types eventually.
-export type PlanItemType = FileType | CommandMessageType;
-
-export type PlanType = {
- id: string;
- query: string;
- description: string;
- actions: Array;
-};
diff --git a/packages/web/src/components/apps/use-app.tsx b/packages/web/src/components/apps/use-app.tsx
deleted file mode 100644
index a7f2f4206..000000000
--- a/packages/web/src/components/apps/use-app.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { createContext, useContext, useEffect, useRef, useState } from 'react';
-import type { AppType } from '@srcbook/shared';
-import { updateApp as doUpdateApp } from '@/clients/http/apps';
-import { AppChannel } from '@/clients/websocket';
-
-export interface AppContextValue {
- app: AppType;
- channel: AppChannel;
- updateApp: (attrs: { name: string }) => void;
-}
-
-const AppContext = createContext(undefined);
-
-type ProviderPropsType = {
- app: AppType;
- children: React.ReactNode;
-};
-
-export function AppProvider({ app: initialApp, children }: ProviderPropsType) {
- const [app, setApp] = useState(initialApp);
-
- const channelRef = useRef(AppChannel.create(app.id));
-
- useEffect(() => {
- // If the app ID has changed, create a new channel for the new app.
- if (channelRef.current.appId !== app.id) {
- channelRef.current.unsubscribe();
- channelRef.current = AppChannel.create(app.id);
- }
-
- // Subscribe to the channel
- channelRef.current.subscribe();
-
- // Unsubscribe when the component is unmounted
- return () => channelRef.current.unsubscribe();
- }, [app.id]);
-
- async function updateApp(attrs: { name: string }) {
- const { data: updatedApp } = await doUpdateApp(app.id, attrs);
- setApp(updatedApp);
- }
-
- return (
-
- {children}
-
- );
-}
-
-export function useApp(): AppContextValue {
- return useContext(AppContext) as AppContextValue;
-}
diff --git a/packages/web/src/components/apps/use-files.tsx b/packages/web/src/components/apps/use-files.tsx
deleted file mode 100644
index 47eee2e43..000000000
--- a/packages/web/src/components/apps/use-files.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-import React, {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useReducer,
- useRef,
- useState,
-} from 'react';
-
-import type {
- FileType,
- DirEntryType,
- FileEntryType,
- FileUpdatedPayloadType,
-} from '@srcbook/shared';
-import { AppChannel } from '@/clients/websocket';
-import {
- createFile as doCreateFile,
- deleteFile as doDeleteFile,
- renameFile as doRenameFile,
- createDirectory,
- deleteDirectory,
- renameDirectory,
- loadDirectory,
-} from '@/clients/http/apps';
-import {
- createNode,
- deleteNode,
- renameDirNode,
- sortTree,
- updateDirNode,
- updateFileNode,
-} from './lib/file-tree';
-import { useApp } from './use-app';
-import { useNavigate } from 'react-router-dom';
-import { setLastOpenedFile } from './local-storage';
-
-export interface FilesContextValue {
- fileTree: DirEntryType;
- openedFile: FileType | null;
- openFile: (entry: FileEntryType) => void;
- createFile: (dirname: string, basename: string, source?: string) => Promise;
- updateFile: (modified: FileType) => void;
- renameFile: (entry: FileEntryType, name: string) => Promise;
- deleteFile: (entry: FileEntryType) => Promise;
- createFolder: (dirname: string, basename: string) => Promise;
- renameFolder: (entry: DirEntryType, name: string) => Promise;
- deleteFolder: (entry: DirEntryType) => Promise;
- openFolder: (entry: DirEntryType) => Promise;
- closeFolder: (entry: DirEntryType) => void;
- toggleFolder: (entry: DirEntryType) => void;
- isFolderOpen: (entry: DirEntryType) => boolean;
-}
-
-const FilesContext = createContext(undefined);
-
-type ProviderPropsType = {
- channel: AppChannel;
- children: React.ReactNode;
- initialOpenedFile: FileType | null;
- rootDirEntries: DirEntryType;
-};
-
-export function FilesProvider({
- channel,
- rootDirEntries,
- initialOpenedFile,
- children,
-}: ProviderPropsType) {
- // Because we use refs for our state, we need a way to trigger
- // component re-renders when the ref state changes.
- //
- // https://legacy.reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate
- //
- const [, forceComponentRerender] = useReducer((x) => x + 1, 0);
-
- const { app } = useApp();
- const navigateTo = useNavigate();
-
- const fileTreeRef = useRef(sortTree(rootDirEntries));
- const openedDirectoriesRef = useRef>(new Set());
- const [openedFile, _setOpenedFile] = useState(initialOpenedFile);
-
- const setOpenedFile = useCallback(
- (fn: (file: FileType | null) => FileType | null) => {
- _setOpenedFile((prevOpenedFile) => {
- const openedFile = fn(prevOpenedFile);
- if (openedFile) {
- setLastOpenedFile(app.id, openedFile);
- }
- return openedFile;
- });
- },
- [app.id],
- );
-
- // Handle file updates from the server
- useEffect(() => {
- function onFileUpdated(payload: FileUpdatedPayloadType) {
- setOpenedFile(() => payload.file);
- forceComponentRerender();
- }
- channel.on('file:updated', onFileUpdated);
-
- return () => {
- channel.off('file:updated', onFileUpdated);
- };
- }, [channel, setOpenedFile]);
-
- const navigateToFile = useCallback(
- (file: { path: string }) => {
- navigateTo(`/apps/${app.id}/files/${encodeURIComponent(file.path)}`);
- },
- [app.id, navigateTo],
- );
-
- useEffect(() => {
- if (initialOpenedFile !== null && initialOpenedFile?.path !== openedFile?.path) {
- setOpenedFile(() => initialOpenedFile);
- }
- }, [initialOpenedFile, openedFile?.path, setOpenedFile]);
-
- const openFile = useCallback(
- (entry: FileEntryType) => {
- navigateToFile(entry);
- },
- [navigateToFile],
- );
-
- const createFile = useCallback(
- async (dirname: string, basename: string, source?: string) => {
- source = source || '';
- const { data: fileEntry } = await doCreateFile(app.id, dirname, basename, source);
- fileTreeRef.current = createNode(fileTreeRef.current, fileEntry);
- forceComponentRerender(); // required
- return fileEntry;
- },
- [app.id],
- );
-
- const updateFile = useCallback(
- (modified: FileType) => {
- channel.push('file:updated', { file: modified });
- setOpenedFile(() => modified);
- forceComponentRerender();
- },
- [channel, setOpenedFile],
- );
-
- const deleteFile = useCallback(
- async (entry: FileEntryType) => {
- await doDeleteFile(app.id, entry.path);
- setOpenedFile((openedFile) => {
- if (openedFile && openedFile.path === entry.path) {
- return null;
- }
- return openedFile;
- });
- fileTreeRef.current = deleteNode(fileTreeRef.current, entry.path);
- forceComponentRerender(); // required
- },
- [app.id, setOpenedFile],
- );
-
- const renameFile = useCallback(
- async (entry: FileEntryType, name: string) => {
- const { data: newEntry } = await doRenameFile(app.id, entry.path, name);
- setOpenedFile((openedFile) => {
- if (openedFile && openedFile.path === entry.path) {
- return { ...openedFile, path: newEntry.path, name: newEntry.basename };
- }
- return openedFile;
- });
- fileTreeRef.current = updateFileNode(fileTreeRef.current, entry, newEntry);
- forceComponentRerender(); // required
- },
- [app.id, setOpenedFile],
- );
-
- const isFolderOpen = useCallback((entry: DirEntryType) => {
- return openedDirectoriesRef.current.has(entry.path);
- }, []);
-
- const openFolder = useCallback(
- async (entry: DirEntryType) => {
- // Optimistically open the folder.
- openedDirectoriesRef.current.add(entry.path);
- forceComponentRerender();
- const { data: directory } = await loadDirectory(app.id, entry.path);
- fileTreeRef.current = updateDirNode(fileTreeRef.current, directory);
- forceComponentRerender();
- },
- [app.id],
- );
-
- const closeFolder = useCallback((entry: DirEntryType) => {
- openedDirectoriesRef.current.delete(entry.path);
- forceComponentRerender();
- }, []);
-
- const toggleFolder = useCallback(
- (entry: DirEntryType) => {
- if (isFolderOpen(entry)) {
- closeFolder(entry);
- } else {
- openFolder(entry);
- }
- },
- [isFolderOpen, openFolder, closeFolder],
- );
-
- const createFolder = useCallback(
- async (dirname: string, basename: string) => {
- const { data: folderEntry } = await createDirectory(app.id, dirname, basename);
- fileTreeRef.current = createNode(fileTreeRef.current, folderEntry);
- forceComponentRerender(); // required
- openFolder(folderEntry);
- },
- [app.id, openFolder],
- );
-
- const deleteFolder = useCallback(
- async (entry: DirEntryType) => {
- await deleteDirectory(app.id, entry.path);
- setOpenedFile((openedFile) => {
- if (openedFile && openedFile.path.startsWith(entry.path)) {
- return null;
- }
- return openedFile;
- });
- openedDirectoriesRef.current.delete(entry.path);
- fileTreeRef.current = deleteNode(fileTreeRef.current, entry.path);
- forceComponentRerender(); // required
- },
- [app.id, setOpenedFile],
- );
-
- const renameFolder = useCallback(
- async (entry: DirEntryType, name: string) => {
- const { data: newEntry } = await renameDirectory(app.id, entry.path, name);
-
- setOpenedFile((openedFile) => {
- if (openedFile && openedFile.path.startsWith(entry.path)) {
- return { ...openedFile, path: openedFile.path.replace(entry.path, newEntry.path) };
- }
- return openedFile;
- });
-
- if (openedDirectoriesRef.current.has(entry.path)) {
- openedDirectoriesRef.current.delete(entry.path);
- openedDirectoriesRef.current.add(newEntry.path);
- }
-
- fileTreeRef.current = renameDirNode(fileTreeRef.current, entry, newEntry);
-
- forceComponentRerender(); // required
- },
- [app.id, setOpenedFile],
- );
-
- const context: FilesContextValue = {
- fileTree: fileTreeRef.current,
- openedFile,
- openFile,
- createFile,
- updateFile,
- renameFile,
- deleteFile,
- createFolder,
- renameFolder,
- deleteFolder,
- openFolder,
- closeFolder,
- toggleFolder,
- isFolderOpen,
- };
-
- return {children};
-}
-
-export function useFiles(): FilesContextValue {
- return useContext(FilesContext) as FilesContextValue;
-}
diff --git a/packages/web/src/components/apps/use-logs.tsx b/packages/web/src/components/apps/use-logs.tsx
deleted file mode 100644
index d9dc49469..000000000
--- a/packages/web/src/components/apps/use-logs.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
-
-import { AppChannel } from '@/clients/websocket';
-import { DepsInstallLogPayloadType, PreviewLogPayloadType } from '@srcbook/shared';
-
-export type LogMessage = {
- type: 'stderr' | 'stdout' | 'info';
- source: 'srcbook' | 'vite' | 'npm';
- timestamp: Date;
- message: string;
-};
-
-export interface LogsContextValue {
- logs: Array;
- clearLogs: () => void;
- unreadLogsCount: number;
- panelIcon: 'default' | 'error';
-
- addLog: (type: LogMessage['type'], source: LogMessage['source'], message: string) => void;
-
- open: boolean;
- togglePane: () => void;
- closePane: () => void;
-}
-
-const LogsContext = createContext(undefined);
-
-type ProviderPropsType = {
- channel: AppChannel;
- children: React.ReactNode;
-};
-
-export function LogsProvider({ channel, children }: ProviderPropsType) {
- const [logs, setLogs] = useState>([]);
- const [unreadLogsCount, setUnreadLogsCount] = useState(0);
- const [panelIcon, setPanelIcon] = useState('default');
-
- const [open, setOpen] = useState(false);
-
- function clearLogs() {
- setLogs([]);
- setPanelIcon('default');
- setUnreadLogsCount(0);
- }
-
- const addLog = useCallback(
- (type: LogMessage['type'], source: LogMessage['source'], message: LogMessage['message']) => {
- setLogs((logs) => [...logs, { type, message, source, timestamp: new Date() }]);
- if (type === 'stderr') {
- setPanelIcon('error');
- }
- setUnreadLogsCount((n) => n + 1);
- },
- [],
- );
-
- function togglePane() {
- setOpen((n) => !n);
- setPanelIcon('default');
- setUnreadLogsCount(0);
- }
-
- function closePane() {
- setOpen(false);
- setPanelIcon('default');
- setUnreadLogsCount(0);
- }
-
- // As the server generates logs, show them in the logs panel
- useEffect(() => {
- function onPreviewLog(payload: PreviewLogPayloadType) {
- for (const row of payload.log.data.split('\n')) {
- addLog(payload.log.type, 'vite', row);
- }
- }
-
- channel.on('preview:log', onPreviewLog);
-
- function onDepsInstallLog(payload: DepsInstallLogPayloadType) {
- for (const row of payload.log.data.split('\n')) {
- addLog(payload.log.type, 'npm', row);
- }
- }
- channel.on('deps:install:log', onDepsInstallLog);
-
- return () => {
- channel.off('preview:log', onPreviewLog);
- channel.off('deps:install:log', onDepsInstallLog);
- };
- }, [channel, addLog]);
-
- return (
-
- {children}
-
- );
-}
-
-export function useLogs(): LogsContextValue {
- return useContext(LogsContext) as LogsContextValue;
-}
diff --git a/packages/web/src/components/apps/use-package-json.tsx b/packages/web/src/components/apps/use-package-json.tsx
deleted file mode 100644
index 1c0d1c215..000000000
--- a/packages/web/src/components/apps/use-package-json.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
-import { OutputType } from '@srcbook/components/src/types';
-import { AppChannel } from '@/clients/websocket';
-import {
- DepsInstallLogPayloadType,
- DepsInstallStatusPayloadType,
- DepsStatusResponsePayloadType,
-} from '@srcbook/shared';
-import { useLogs } from './use-logs';
-
-type NpmInstallStatus = 'idle' | 'installing' | 'complete' | 'failed';
-
-export interface PackageJsonContextValue {
- npmInstall: (packages?: string[]) => Promise;
- clearNodeModules: () => void;
-
- nodeModulesExists: boolean | null;
- status: NpmInstallStatus;
- installing: boolean;
- failed: boolean;
- output: Array;
- showInstallModal: boolean;
- setShowInstallModal: (value: boolean) => void;
-}
-
-const PackageJsonContext = createContext(undefined);
-
-type ProviderPropsType = {
- channel: AppChannel;
- children: React.ReactNode;
-};
-
-export function PackageJsonProvider({ channel, children }: ProviderPropsType) {
- const [status, setStatus] = useState('idle');
- const [output, setOutput] = useState>([]);
- const [nodeModulesExists, setNodeModulesExists] = useState(null);
- const [showInstallModal, setShowInstallModal] = useState(false);
- const { addLog } = useLogs();
-
- useEffect(() => {
- channel.push('deps:status', {});
- }, [channel]);
-
- useEffect(() => {
- const callback = (data: DepsStatusResponsePayloadType) => {
- setNodeModulesExists(data.nodeModulesExists);
- };
- channel.on('deps:status:response', callback);
-
- return () => {
- channel.off('deps:status:response', callback);
- };
- }, [channel]);
-
- useEffect(() => {
- const callback = (payload: DepsInstallStatusPayloadType) => {
- setStatus(payload.status);
- };
- channel.on('deps:install:status', callback);
-
- return () => {
- channel.off('deps:install:status', callback);
- };
- }, [channel]);
-
- const npmInstall = useCallback(
- async (packages?: Array) => {
- addLog(
- 'info',
- 'srcbook',
- `Running ${!packages ? 'npm install' : `npm install ${packages.join(' ')}`}...`,
- );
-
- // NOTE: caching of the log output is required here because socket events that call callback
- // functions in here hold on to old scope values
- let contents = '';
-
- return new Promise((resolve, reject) => {
- const logCallback = ({ log }: DepsInstallLogPayloadType) => {
- setOutput((old) => [...old, log]);
- contents += log.data;
- };
- channel.on('deps:install:log', logCallback);
-
- const statusCallback = (payload: DepsInstallStatusPayloadType) => {
- switch (payload.status) {
- case 'installing':
- break;
- case 'failed':
- case 'complete':
- channel.off('deps:install:log', logCallback);
- channel.off('deps:install:status', statusCallback);
-
- addLog(
- 'info',
- 'srcbook',
- `${!packages ? 'npm install' : `npm install ${packages.join(' ')}`} exited with status code ${payload.code}`,
- );
-
- if (payload.status === 'complete') {
- resolve();
- } else {
- reject(new Error(`Error running npm install: ${contents}`));
- }
- break;
- }
- };
- channel.on('deps:install:status', statusCallback);
-
- setOutput([]);
- setStatus('installing');
- channel.push('deps:install', { packages });
- });
- },
- [channel, addLog],
- );
-
- const clearNodeModules = useCallback(() => {
- channel.push('deps:clear', {});
- setOutput([]);
- }, [channel]);
-
- const context: PackageJsonContextValue = {
- npmInstall,
- clearNodeModules,
- nodeModulesExists,
- status,
- installing: status === 'installing',
- failed: status === 'failed',
- output,
- showInstallModal,
- setShowInstallModal,
- };
-
- return {children};
-}
-
-export function usePackageJson() {
- const context = useContext(PackageJsonContext);
-
- if (!context) {
- throw new Error('usePackageJson must be used within a PackageJsonProvider');
- }
-
- return context;
-}
diff --git a/packages/web/src/components/apps/use-preview.tsx b/packages/web/src/components/apps/use-preview.tsx
deleted file mode 100644
index 0ea327e52..000000000
--- a/packages/web/src/components/apps/use-preview.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
-
-import { AppChannel } from '@/clients/websocket';
-import { PreviewStatusPayloadType } from '@srcbook/shared';
-import useEffectOnce from '@/components/use-effect-once';
-import { usePackageJson } from './use-package-json';
-import { useLogs } from './use-logs';
-
-export type PreviewStatusType = 'booting' | 'connecting' | 'running' | 'stopped';
-
-export interface PreviewContextValue {
- url: string | null;
- status: PreviewStatusType;
- stop: () => void;
- start: () => void;
- exitCode: number | null;
-}
-
-const PreviewContext = createContext(undefined);
-
-type ProviderPropsType = {
- channel: AppChannel;
- children: React.ReactNode;
-};
-
-export function PreviewProvider({ channel, children }: ProviderPropsType) {
- const [url, setUrl] = useState(null);
- const [status, setStatus] = useState('connecting');
- const [exitCode, setExitCode] = useState(null);
-
- const { npmInstall, nodeModulesExists } = usePackageJson();
- const { addLog } = useLogs();
-
- useEffect(() => {
- function onStatusUpdate(payload: PreviewStatusPayloadType) {
- setUrl(payload.url);
- setStatus(payload.status);
-
- switch (payload.status) {
- case 'booting':
- addLog('info', 'srcbook', 'Dev server is booting...');
- break;
- case 'running':
- addLog('info', 'srcbook', `Dev server is running at ${payload.url}`);
- break;
- case 'stopped':
- addLog('info', 'srcbook', `Dev server exited with status ${payload.code}`);
- setExitCode(payload.code);
- break;
- }
- }
-
- channel.on('preview:status', onStatusUpdate);
-
- return () => channel.off('preview:status', onStatusUpdate);
- }, [channel, addLog]);
-
- async function start() {
- if (nodeModulesExists === false) {
- await npmInstall();
- }
- channel.push('preview:start', {});
- }
-
- const stop = useCallback(() => {
- channel.push('preview:stop', {});
- }, [channel]);
-
- // If the node_modules directory gets deleted, then stop the preview server
- useEffect(() => {
- if (nodeModulesExists !== false) {
- return;
- }
- stop();
- }, [nodeModulesExists, stop]);
-
- // When the page initially loads, start the vite server
- useEffectOnce(() => {
- start();
- });
-
- return (
-
- {children}
-
- );
-}
-
-export function usePreview(): PreviewContextValue {
- return useContext(PreviewContext) as PreviewContextValue;
-}
diff --git a/packages/web/src/components/apps/use-version.tsx b/packages/web/src/components/apps/use-version.tsx
deleted file mode 100644
index 978b5f595..000000000
--- a/packages/web/src/components/apps/use-version.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
-import { useApp } from './use-app';
-import { checkoutVersion, commitVersion, getCurrentVersion } from '@/clients/http/apps';
-
-interface Version {
- sha: string;
- message?: string;
-}
-
-interface VersionContextType {
- currentVersion: Version | null;
- createVersion: (message: string) => Promise;
- checkout: (sha: string) => Promise;
- fetchVersions: () => Promise;
-}
-
-const VersionContext = createContext(undefined);
-
-export const VersionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const { app } = useApp();
- const [currentVersion, setCurrentVersion] = useState(null);
-
- const fetchVersion = useCallback(async () => {
- if (!app) return;
-
- try {
- const currentVersionResponse = await getCurrentVersion(app.id);
- setCurrentVersion({ sha: currentVersionResponse.sha });
- } catch (error) {
- console.error('Error fetching current version:', error);
- }
- }, [app]);
-
- useEffect(() => {
- fetchVersion();
- }, [fetchVersion]);
-
- const commitFiles = useCallback(
- async (message: string) => {
- if (!app) return;
-
- try {
- const response = await commitVersion(app.id, message);
- setCurrentVersion({ sha: response.sha, message });
- return response.sha;
- } catch (error) {
- console.error('Error committing files:', error);
- }
- },
- [app],
- );
-
- const checkout = useCallback(
- async (sha: string) => {
- if (!app) return;
-
- try {
- const { sha: checkoutSha } = await checkoutVersion(app.id, sha);
- setCurrentVersion({ sha: checkoutSha });
- } catch (error) {
- console.error('Error checking out version:', error);
- }
- },
- [app],
- );
-
- return (
-
- {children}
-
- );
-};
-
-export const useVersion = () => {
- const context = useContext(VersionContext);
- if (context === undefined) {
- throw new Error('useVersion must be used within a VersionProvider');
- }
- return context;
-};
diff --git a/packages/web/src/components/chat.tsx b/packages/web/src/components/chat.tsx
deleted file mode 100644
index 1cdaaf66d..000000000
--- a/packages/web/src/components/chat.tsx
+++ /dev/null
@@ -1,662 +0,0 @@
-import {
- Button,
- cn,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from '@srcbook/components';
-import Markdown from './apps/markdown.js';
-import { diffFiles } from './apps/lib/diff.js';
-import TextareaAutosize from 'react-textarea-autosize';
-import {
- ArrowUp,
- Minus,
- Paperclip,
- LoaderCircle,
- History,
- PanelTopOpen,
- Loader,
- ViewIcon,
- Undo2Icon,
- Redo2Icon,
- GripHorizontal,
- ThumbsUp,
- ThumbsDown,
- GitMerge,
- EllipsisVertical,
-} from 'lucide-react';
-import * as React from 'react';
-import {
- aiEditApp,
- loadHistory,
- appendToHistory,
- aiGenerationFeedback,
-} from '@/clients/http/apps.js';
-import { AppType, randomid } from '@srcbook/shared';
-import { useFiles } from './apps/use-files';
-import { type FileType } from './apps/types';
-import type {
- FileDiffType,
- UserMessageType,
- MessageType,
- HistoryType,
- CommandMessageType,
- PlanMessageType,
- DiffMessageType,
-} from '@srcbook/shared';
-import { DiffStats } from './apps/diff-stats.js';
-import { useApp } from './apps/use-app.js';
-import { usePackageJson } from './apps/use-package-json.js';
-import { AiFeedbackModal } from './apps/AiFeedbackModal';
-import { useVersion } from './apps/use-version.js';
-import { Link } from 'react-router-dom';
-
-function Chat({
- history,
- loading,
- onClose,
- app,
- fileDiffs,
- diffApplied,
- revertDiff,
- reApplyDiff,
- openDiffModal,
-}: {
- history: HistoryType;
- loading: 'description' | 'actions' | null;
- onClose: () => void;
- app: AppType;
- fileDiffs: FileDiffType[];
- diffApplied: boolean;
- revertDiff: () => void;
- reApplyDiff: () => void;
- openDiffModal: () => void;
-}) {
- const { npmInstall } = usePackageJson();
- const messagesEndRef = React.useRef(null);
-
- // Tried scrolling with flex-direction: column-reverse but it didn't work
- // with generated content, so fallback to using JS
- const scrollToBottom = () => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- };
-
- React.useEffect(() => {
- scrollToBottom();
- }, [history, loading]);
-
- return (
-
-
- Chat
-
-
-
-
-
-
- {/* TODO: each message object needs a unique identifier */}
- {history.map((message: MessageType, index: number) => {
- if (message.type === 'user') {
- return (
-
- {message.message}
-
- );
- } else if (message.type === 'command') {
- const packages = message.packages;
- if (!packages) {
- console.error(
- 'The only supported command is `npm install
`. Got:',
- message.command,
- );
- return;
- }
- return (
-
-
Install dependencies
-
-
- {`npm install ${packages.join(' ')}`}
-
-
-
-
- );
- } else if (message.type === 'plan') {
- return ;
- } else if (message.type === 'diff') {
- // Calculate the incremental version, i.e. v1 for the first diffbox, v2 for the second, etc.
- // This is separate from the git version number.
- const diffs = history.filter((m) => m.type === 'diff');
- const currentDiffIndex = diffs.findIndex((m) => m === message);
- const incrementalVersion = currentDiffIndex + 1;
-
- return (
-
- );
- }
- })}
-
- 0 ? '' : 'hidden')}>
-
-
-
-
- {loading !== null && (
-
-
{' '}
-
- {loading === 'description'
- ? 'Generating plan...'
- : 'Applying changes (this can take a while)...'}
-
-
- )}
- {/* empty div for scrolling */}
-
-
-
-
- );
-}
-
-function Query({
- onSubmit,
- onFocus,
- isLoading,
- isVisible,
- setVisible,
-}: {
- onSubmit: (query: string) => Promise;
- onFocus: () => void;
- isLoading: boolean;
- isVisible: boolean;
- setVisible: (visible: boolean) => void;
-}) {
- const [query, setQuery] = React.useState('');
-
- const handleSubmit = () => {
- const value = query.trim();
- if (value) {
- setQuery('');
- onSubmit(value);
- }
- };
-
- return (
-
-
setQuery(e.target.value)}
- onFocus={onFocus}
- value={query}
- onKeyDown={(e) => {
- if (e.metaKey && !e.shiftKey && e.key === 'Enter') {
- e.preventDefault();
- e.stopPropagation();
- handleSubmit();
- }
- }}
- />
-
-
-
-
-
-
-
- Coming soon!
-
-
-
-
-
- );
-}
-
-function DiffBox({
- files,
- app,
- incrementalVersion,
- version,
- planId,
-}: {
- files: FileDiffType[];
- app: AppType;
- incrementalVersion: number;
- version: string;
- planId: string;
-}) {
- const [showFeedbackToast, setShowFeedbackToast] = React.useState(false);
- const [feedbackGiven, _setFeedbackGiven] = React.useState(null);
- const [isFeedbackModalOpen, setIsFeedbackModalOpen] = React.useState(false);
-
- const { checkout, currentVersion } = useVersion();
-
- const setFeedbackGiven = (feedback: 'positive' | 'negative') => {
- setShowFeedbackToast(true);
- _setFeedbackGiven(feedback);
- setTimeout(() => setShowFeedbackToast(false), 2500);
- };
-
- const handleFeedbackSubmit = (feedbackText: string) => {
- setFeedbackGiven('negative');
- aiGenerationFeedback(app.id, { planId, feedback: { type: 'negative', text: feedbackText } });
- };
-
- return (
- <>
-
-
-
-
- {app.name}
- V{incrementalVersion}
-
- {/* We don't need this guard if we assume only new apps */}
- {version && (
-
-
-
-
- #{version ? version.slice(0, 7) : 'unknown version'}
-
-
-
-
-
-
-
-
- checkout(version)}>
- Revert to this version
-
- alert('Coming soon!')}>
- Fork this version
-
-
-
-
- )}
-
-
- {files.map((file) => (
-
- ))}
-
-
-
-
-
-
- {showFeedbackToast && (
-
Thanks for the feedback!
- )}
-
- setIsFeedbackModalOpen(false)}
- onSubmit={handleFeedbackSubmit}
- />
- >
- );
-}
-
-export function DraggableChatPanel(props: { children: React.ReactNode }): React.JSX.Element {
- const [isDragging, setIsDragging] = React.useState(false);
- const [position, setPosition] = React.useState({ x: 20, y: 20 });
- const chatRef = React.useRef(null);
- const dragStartPos = React.useRef({ x: 0, y: 0 });
- const [showOverlay, setShowOverlay] = React.useState(false);
-
- const handleMouseDown = (e: React.MouseEvent) => {
- if (e.target instanceof Element && e.target.closest('.drag-handle')) {
- setIsDragging(true);
- setShowOverlay(true);
- dragStartPos.current = {
- x: e.clientX + position.x,
- y: e.clientY + position.y,
- };
- }
- };
-
- const handleMouseMove = (e: MouseEvent) => {
- if (isDragging && chatRef.current) {
- const newX = dragStartPos.current.x - e.clientX;
- const newY = dragStartPos.current.y - e.clientY;
-
- // Ensure the chat panel stays within the viewport
- const maxX = window.innerWidth - chatRef.current.offsetWidth;
- const maxY = window.innerHeight - chatRef.current.offsetHeight;
-
- setPosition({
- x: Math.max(0, Math.min(newX, maxX)),
- y: Math.max(0, Math.min(newY, maxY)),
- });
- }
- };
-
- const handleMouseUp = () => {
- setIsDragging(false);
- setShowOverlay(false);
- };
-
- React.useEffect(() => {
- if (showOverlay) {
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- } else {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- }
- return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isDragging, showOverlay]);
-
- // Note: we show a full screen overlay otherwise the mouse events
- // don't fire correctly when hovering over the iframe.
-
- return (
- <>
- {showOverlay && (
-
- )}
- {/* eslint-disable-next-line */}
-
-
-
- {props.children}
-
-
- >
- );
-}
-
-type PropsType = {
- triggerDiffModal: (props: { files: FileDiffType[]; onUndoAll: () => void } | null) => void;
-};
-
-export function ChatPanel(props: PropsType): React.JSX.Element {
- const { app } = useApp();
-
- const [history, setHistory] = React.useState([]);
- const [fileDiffs, setFileDiffs] = React.useState([]);
- const [visible, setVisible] = React.useState(false);
- const [loading, setLoading] = React.useState<'description' | 'actions' | null>(null);
- const [diffApplied, setDiffApplied] = React.useState(false);
- const { createFile, deleteFile } = useFiles();
- const { createVersion } = useVersion();
-
- // Initialize history from the DB
- React.useEffect(() => {
- loadHistory(app.id)
- .then(({ data }) => setHistory(data))
- .catch((error) => {
- console.error('Error fetching chat history:', error);
- });
- }, [app]);
-
- const handleSubmit = async (query: string) => {
- const planId = randomid();
- setLoading('description');
- setFileDiffs([]);
- const userMessage = { type: 'user', message: query, planId } as UserMessageType;
- setHistory((prevHistory) => [...prevHistory, userMessage]);
- appendToHistory(app.id, userMessage);
- setVisible(true);
-
- const iterable = await aiEditApp(app.id, query, planId);
-
- const fileUpdates: FileType[] = [];
-
- for await (const message of iterable) {
- if (message.type === 'description') {
- const planMessage = {
- type: 'plan',
- content: message.data.content,
- planId,
- } as PlanMessageType;
- setHistory((prevHistory) => [...prevHistory, planMessage]);
- appendToHistory(app.id, planMessage);
- setLoading('actions');
- } else if (message.type === 'action') {
- if (message.data.type === 'command') {
- const commandMessage = {
- type: 'command',
- command: message.data.command,
- packages: message.data.packages,
- description: message.data.description,
- planId,
- } as CommandMessageType;
- setHistory((prevHistory) => [...prevHistory, commandMessage]);
- appendToHistory(app.id, commandMessage);
- } else if (message.data.type === 'file') {
- fileUpdates.push(message.data);
- }
- } else {
- console.error('Unknown message type:', message);
- }
- }
-
- if (fileUpdates.length > 0) {
- // Write the changes
- for (const update of fileUpdates) {
- createFile(update.dirname, update.basename, update.modified);
- }
-
- // Create a new version
- const version = await createVersion(`Changes for planId: ${planId}`);
-
- const fileDiffs: FileDiffType[] = fileUpdates.map((file: FileType) => {
- const { additions, deletions } = diffFiles(file.original ?? '', file.modified);
- return {
- modified: file.modified,
- original: file.original,
- basename: file.basename,
- dirname: file.dirname,
- path: file.path,
- additions,
- deletions,
- type: file.original ? 'edit' : ('create' as 'edit' | 'create'),
- };
- });
-
- const diffMessage = { type: 'diff', diff: fileDiffs, planId, version } as DiffMessageType;
- setHistory((prevHistory) => [...prevHistory, diffMessage]);
- appendToHistory(app.id, diffMessage);
-
- setFileDiffs(fileDiffs);
- setDiffApplied(true);
- }
- setLoading(null);
- };
-
- // TODO: this closes over state that might be stale.
- // This probably needs to use a ref for file diffs to
- // ensure the most recent state is always referenced.
- const revertDiff = () => {
- for (const file of fileDiffs) {
- if (file.original) {
- createFile(file.dirname, file.basename, file.original);
- } else {
- // TODO: this needs some testing, this shows the idea only
- deleteFile({
- type: 'file',
- path: file.path,
- dirname: file.dirname,
- basename: file.basename,
- });
- }
- }
- setDiffApplied(false);
- };
-
- const reApplyDiff = () => {
- for (const file of fileDiffs) {
- createFile(file.dirname, file.basename, file.modified);
- }
- setDiffApplied(true);
- };
-
- const handleClose = () => {
- setVisible(false);
- };
-
- const handleFocus = () => {
- if (history.length > 0) {
- setVisible(true);
- }
- };
-
- function openDiffModal() {
- props.triggerDiffModal({
- files: fileDiffs,
- onUndoAll: () => {
- revertDiff();
- props.triggerDiffModal(null);
- },
- });
- }
-
- return (
-
-
- {visible && (
-
- )}
-
-
-
- );
-}
diff --git a/packages/web/src/components/delete-app-dialog.tsx b/packages/web/src/components/delete-app-dialog.tsx
deleted file mode 100644
index e4edcb69a..000000000
--- a/packages/web/src/components/delete-app-dialog.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { deleteApp } from '@/clients/http/apps';
-import { useState } from 'react';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from '@srcbook/components/src/components/ui/dialog';
-import { Button } from '@srcbook/components/src/components/ui/button';
-import { AppType } from '@srcbook/shared';
-
-type PropsType = {
- app: AppType;
- onClose: () => void;
- onDeleted: () => void;
-};
-
-export default function DeleteAppModal({ app, onClose, onDeleted }: PropsType) {
- const [error, setError] = useState(null);
-
- async function onDelete() {
- try {
- await deleteApp(app.id);
- onDeleted();
- } catch (err) {
- console.error(err);
- setError('Something went wrong. Please try again.');
- setTimeout(() => setError(null), 3000);
- }
- }
-
- return (
-
- );
-}
diff --git a/packages/web/src/components/keyboard-shortcuts-dialog.tsx b/packages/web/src/components/keyboard-shortcuts-dialog.tsx
index 650998ddf..0960041a0 100644
--- a/packages/web/src/components/keyboard-shortcuts-dialog.tsx
+++ b/packages/web/src/components/keyboard-shortcuts-dialog.tsx
@@ -38,8 +38,8 @@ export default function KeyboardShortcutsDialog({
- AI chat
-
+ AI Actions
+
{!readOnly ? (
<>
Markdown edit
@@ -55,8 +55,6 @@ export default function KeyboardShortcutsDialog({
description="format code using Prettier"
/>
- App Builder
-
>
) : null}
diff --git a/packages/web/src/components/onboarding.tsx b/packages/web/src/components/onboarding.tsx
deleted file mode 100644
index d4b573245..000000000
--- a/packages/web/src/components/onboarding.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react';
-import { LayoutGridIcon, FileTextIcon } from 'lucide-react';
-import { AiSettings } from '@/routes/settings';
-
-const OnboardingModal: React.FunctionComponent = () => {
- return (
-
-
Welcome to Srcbook!
-
Srcbook is an AI-powered TypeScript app builder and interactive playground.
-
-
-
With Srcbook you can:
-
-
-
-
-
-
App builder
-
Create Web Applications with the speed of thinking
-
-
-
-
-
-
-
Notebook
-
- Experimenting without the hassle of setting up environments
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default OnboardingModal;
diff --git a/packages/web/src/components/srcbook-cards.tsx b/packages/web/src/components/srcbook-cards.tsx
index 200a5a1e7..82c43168b 100644
--- a/packages/web/src/components/srcbook-cards.tsx
+++ b/packages/web/src/components/srcbook-cards.tsx
@@ -1,4 +1,4 @@
-import { Sparkles, Circle, PlusIcon, Trash2, Import, LayoutGrid } from 'lucide-react';
+import { Sparkles, Circle, PlusIcon, Trash2, Import } from 'lucide-react';
import { Button } from '@srcbook/components/src/components/ui/button';
import { CodeLanguageType } from '@srcbook/shared';
import { SrcbookLogo } from './logos';
@@ -175,41 +175,6 @@ export function SrcbookCard(props: SrcbookCardPropsType) {
);
}
-type AppCardPropsType = {
- name: string;
- onClick: () => void;
- onDelete: () => void;
-};
-
-export function AppCard(props: AppCardPropsType) {
- function onDelete(e: React.MouseEvent) {
- e.stopPropagation();
- props.onDelete();
- }
-
- return (
-
-
-
- {props.name}
-
-
- TS
-
-
-
- );
-}
-
export function GenerateSrcbookButton(props: { onClick: () => void }) {
return (
void }) {
- return (
- props.onClick()}
- className="active:translate-y-0.5 bg-[#F6EEFB80] dark:bg-[#331F4780] border-sb-purple-20 dark:border-sb-purple-80 hover:border-sb-purple-60 text-sb-purple-70 dark:text-sb-purple-20"
- >
-
-
-
-
Create App
-
- New
-
-
-
-
- );
-}
-
export function ImportSrcbookButton(props: { onClick: () => void }) {
return (
,
errorElement: ,
},
- {
- path: '/apps/:id',
- loader: appIndex,
- element: ,
- errorElement: ,
- children: [
- {
- path: '',
- loader: appPreview,
- element: (
-
-
-
- ),
- },
- {
- path: '/apps/:id/files',
- loader: appPreview,
- element: (
-
-
-
- ),
- },
- {
- path: '/apps/:id/files/:path',
- loader: appFilesShow,
- element: (
-
-
-
- ),
- },
- ],
- },
{
path: '/',
element: (
diff --git a/packages/web/src/routes/apps/context.tsx b/packages/web/src/routes/apps/context.tsx
deleted file mode 100644
index 01a73f89f..000000000
--- a/packages/web/src/routes/apps/context.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Outlet, useLoaderData } from 'react-router-dom';
-import type { AppType, DirEntryType, FileType } from '@srcbook/shared';
-
-import { FilesProvider } from '@/components/apps/use-files';
-import { PreviewProvider } from '@/components/apps/use-preview';
-import { LogsProvider } from '@/components/apps/use-logs';
-import { PackageJsonProvider } from '@/components/apps/use-package-json';
-import { AppProvider, useApp } from '@/components/apps/use-app';
-import { VersionProvider } from '@/components/apps/use-version';
-
-export function AppContext() {
- const { app } = useLoaderData() as { app: AppType };
-
- return (
-
-
-
- );
-}
-
-type AppLoaderDataType = {
- rootDirEntries: DirEntryType;
- initialOpenedFile: FileType | null;
-};
-
-export function AppProviders(props: { children: React.ReactNode }) {
- const { initialOpenedFile, rootDirEntries } = useLoaderData() as AppLoaderDataType;
-
- const { channel } = useApp();
-
- return (
-
-
-
-
- {props.children}
-
-
-
-
- );
-}
diff --git a/packages/web/src/routes/apps/files-show.tsx b/packages/web/src/routes/apps/files-show.tsx
deleted file mode 100644
index c504c8326..000000000
--- a/packages/web/src/routes/apps/files-show.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useFiles } from '@/components/apps/use-files';
-import { CodeEditor } from '@/components/apps/editor';
-import AppLayout from './layout';
-
-export default function AppFilesShow() {
- const { openedFile, updateFile } = useFiles();
-
- /* TODO: Handle 404s */
-
- return (
-
- {openedFile && (
- updateFile({ ...openedFile, source })}
- />
- )}
-
- );
-}
diff --git a/packages/web/src/routes/apps/files.tsx b/packages/web/src/routes/apps/files.tsx
deleted file mode 100644
index 7735bd289..000000000
--- a/packages/web/src/routes/apps/files.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { useNavigate } from 'react-router-dom';
-import AppLayout from './layout';
-import { getLastOpenedFile } from '@/components/apps/local-storage';
-import { useApp } from '@/components/apps/use-app';
-import { useEffect } from 'react';
-
-export default function AppFiles() {
- const navigateTo = useNavigate();
-
- const { app } = useApp();
-
- useEffect(() => {
- const file = getLastOpenedFile(app.id);
- if (file) {
- navigateTo(`/apps/${app.id}/files/${encodeURIComponent(file.path)}`);
- }
- }, [app.id, navigateTo]);
-
- return (
-
-
- Use the file explorer to open a file for editing
-
-
- );
-}
diff --git a/packages/web/src/routes/apps/layout.tsx b/packages/web/src/routes/apps/layout.tsx
deleted file mode 100644
index 601c68fbb..000000000
--- a/packages/web/src/routes/apps/layout.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import Sidebar, { type PanelType } from '@/components/apps/sidebar';
-import BottomDrawer from '@/components/apps/bottom-drawer';
-import { ChatPanel } from '@/components/chat';
-import DiffModal from '@/components/apps/diff-modal';
-import { FileDiffType } from '@srcbook/shared';
-import Header, { type HeaderTab } from '@/components/apps/header';
-import { useApp } from '@/components/apps/use-app';
-import PackageInstallToast from '@/components/apps/package-install-toast';
-import { usePackageJson } from '@/components/apps/use-package-json';
-import InstallPackageModal from '@/components/install-package-modal';
-import { useHotkeys } from 'react-hotkeys-hook';
-
-export default function AppLayout(props: {
- activeTab: HeaderTab;
- activePanel: PanelType | null;
- children: React.ReactNode;
-}) {
- const navigateTo = useNavigate();
- const { app } = useApp();
-
- const { installing, npmInstall, output, showInstallModal, setShowInstallModal } =
- usePackageJson();
-
- const [diffModalProps, triggerDiffModal] = useState<{
- files: FileDiffType[];
- onUndoAll: () => void;
- } | null>(null);
-
- useHotkeys('mod+i', () => {
- setShowInstallModal(true);
- });
-
- return (
- <>
- {diffModalProps && triggerDiffModal(null)} />}
-
-