From e5eb5e5d75561257ab7cf7efe4f134bd003d75de Mon Sep 17 00:00:00 2001 From: MasterPtato Date: Tue, 2 Dec 2025 17:58:34 -0800 Subject: [PATCH 1/4] chore: fix runner config creation --- .../pegboard/src/ops/runner_config/delete.rs | 8 +++++++- .../pegboard/src/ops/runner_config/upsert.rs | 17 ++++++++++++++++- engine/packages/universalpubsub/src/chunking.rs | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/engine/packages/pegboard/src/ops/runner_config/delete.rs b/engine/packages/pegboard/src/ops/runner_config/delete.rs index 7c19fc0ca4..b3d3c96b64 100644 --- a/engine/packages/pegboard/src/ops/runner_config/delete.rs +++ b/engine/packages/pegboard/src/ops/runner_config/delete.rs @@ -44,12 +44,18 @@ pub async fn pegboard_runner_config_delete(ctx: &OperationCtx, input: &Input) -> // Bump pool when a serverless config is modified if delete_pool { - ctx.signal(crate::workflows::runner_pool::Bump {}) + let res = ctx + .signal(crate::workflows::runner_pool::Bump {}) .to_workflow::() .tag("namespace_id", input.namespace_id) .tag("runner_name", input.name.clone()) + .graceful_not_found() .send() .await?; + + if res.is_none() { + tracing::debug!(namespace_id=?input.namespace_id, name=%input.name, "no runner pool workflow to bump"); + } } Ok(()) diff --git a/engine/packages/pegboard/src/ops/runner_config/upsert.rs b/engine/packages/pegboard/src/ops/runner_config/upsert.rs index 9c4f00ad72..09da081335 100644 --- a/engine/packages/pegboard/src/ops/runner_config/upsert.rs +++ b/engine/packages/pegboard/src/ops/runner_config/upsert.rs @@ -169,12 +169,27 @@ pub async fn pegboard_runner_config_upsert(ctx: &OperationCtx, input: &Input) -> .dispatch() .await?; } else if input.config.affects_pool() { - ctx.signal(crate::workflows::runner_pool::Bump {}) + let res = ctx + .signal(crate::workflows::runner_pool::Bump {}) .to_workflow::() .tag("namespace_id", input.namespace_id) .tag("runner_name", input.name.clone()) + .graceful_not_found() .send() .await?; + + // Backfill + if res.is_none() { + ctx.workflow(crate::workflows::runner_pool::Input { + namespace_id: input.namespace_id, + runner_name: input.name.clone(), + }) + .tag("namespace_id", input.namespace_id) + .tag("runner_name", input.name.clone()) + .unique() + .dispatch() + .await?; + } } Ok(res.endpoint_config_changed) diff --git a/engine/packages/universalpubsub/src/chunking.rs b/engine/packages/universalpubsub/src/chunking.rs index 2d276233fc..03be93cf0b 100644 --- a/engine/packages/universalpubsub/src/chunking.rs +++ b/engine/packages/universalpubsub/src/chunking.rs @@ -103,7 +103,7 @@ impl ChunkTracker { .retain(|_, buffer| now.duration_since(buffer.last_chunk_ts) < CHUNK_BUFFER_MAX_AGE); let size_after = self.chunks_in_process.len(); - tracing::debug!( + tracing::trace!( ?size_before, ?size_after, "performed chunk buffer garbage collection" From a36dc2681f2c528b74df11fe7b51e542fd32011b Mon Sep 17 00:00:00 2001 From: MasterPtato Date: Thu, 4 Dec 2025 12:38:44 -0800 Subject: [PATCH 2/4] fix: add serverless backfill --- Cargo.lock | 14 +++ Cargo.toml | 17 ++-- engine/packages/engine/Cargo.toml | 1 + engine/packages/engine/src/run_config.rs | 6 ++ .../packages/serverless-backfill/Cargo.toml | 13 +++ .../packages/serverless-backfill/src/lib.rs | 87 +++++++++++++++++++ .../06-rivet-engine-singleton-deployment.yaml | 79 ----------------- scripts/run/k8s/engine.sh | 5 -- 8 files changed, 130 insertions(+), 92 deletions(-) create mode 100644 engine/packages/serverless-backfill/Cargo.toml create mode 100644 engine/packages/serverless-backfill/src/lib.rs delete mode 100644 k8s/engine/06-rivet-engine-singleton-deployment.yaml diff --git a/Cargo.lock b/Cargo.lock index f4262236de..b9ffce2777 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4478,6 +4478,7 @@ dependencies = [ "rivet-pools", "rivet-runner-protocol", "rivet-runtime", + "rivet-serverless-backfill", "rivet-service-manager", "rivet-telemetry", "rivet-term", @@ -4739,6 +4740,19 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "rivet-serverless-backfill" +version = "0.1.0" +dependencies = [ + "anyhow", + "gasoline", + "pegboard", + "rivet-config", + "rivet-types", + "tracing", + "universaldb", +] + [[package]] name = "rivet-service-manager" version = "2.0.25" diff --git a/Cargo.toml b/Cargo.toml index 5ea0101e0d..4d31be8acb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" -members = ["engine/packages/actor-kv","engine/packages/api-builder","engine/packages/api-peer","engine/packages/api-public","engine/packages/api-types","engine/packages/api-util","engine/packages/bootstrap","engine/packages/cache","engine/packages/cache-purge","engine/packages/cache-result","engine/packages/clickhouse-inserter","engine/packages/clickhouse-user-query","engine/packages/config","engine/packages/dump-openapi","engine/packages/engine","engine/packages/env","engine/packages/epoxy","engine/packages/error","engine/packages/error-macros","engine/packages/gasoline","engine/packages/gasoline-macros","engine/packages/guard","engine/packages/guard-core","engine/packages/logs","engine/packages/metrics","engine/packages/namespace","engine/packages/pegboard","engine/packages/pegboard-gateway","engine/packages/pegboard-runner","engine/packages/pools","engine/packages/runtime","engine/packages/service-manager","engine/packages/telemetry","engine/packages/test-deps","engine/packages/test-deps-docker","engine/packages/tracing-reconfigure","engine/packages/types","engine/packages/universaldb","engine/packages/universalpubsub","engine/packages/util","engine/packages/util-id","engine/packages/workflow-worker","engine/sdks/rust/api-full","engine/sdks/rust/data","engine/sdks/rust/epoxy-protocol","engine/sdks/rust/runner-protocol","engine/sdks/rust/ups-protocol"] +members = ["engine/packages/actor-kv","engine/packages/api-builder","engine/packages/api-peer","engine/packages/api-public","engine/packages/api-types","engine/packages/api-util","engine/packages/bootstrap","engine/packages/cache","engine/packages/cache-purge","engine/packages/cache-result","engine/packages/clickhouse-inserter","engine/packages/clickhouse-user-query","engine/packages/config","engine/packages/dump-openapi","engine/packages/engine","engine/packages/env","engine/packages/epoxy","engine/packages/error","engine/packages/error-macros","engine/packages/gasoline","engine/packages/gasoline-macros","engine/packages/guard","engine/packages/guard-core","engine/packages/logs","engine/packages/metrics","engine/packages/namespace","engine/packages/pegboard","engine/packages/pegboard-gateway","engine/packages/pegboard-runner","engine/packages/pools","engine/packages/runtime","engine/packages/serverless-backfill","engine/packages/service-manager","engine/packages/telemetry","engine/packages/test-deps","engine/packages/test-deps-docker","engine/packages/tracing-reconfigure","engine/packages/tracing-utils","engine/packages/types","engine/packages/universaldb","engine/packages/universalpubsub","engine/packages/util","engine/packages/util-id","engine/packages/workflow-worker","engine/sdks/rust/api-full","engine/sdks/rust/data","engine/sdks/rust/epoxy-protocol","engine/sdks/rust/runner-protocol","engine/sdks/rust/ups-protocol"] [workspace.package] version = "2.0.25" @@ -83,10 +83,13 @@ tracing = "0.1.40" tracing-core = "0.1" tracing-opentelemetry = "0.29" tracing-slog = "0.2" -vergen = { version = "9.0.4", features = ["build", "cargo", "rustc"] } vergen-gitcl = "1.0.0" reqwest-eventsource = "0.6.0" +[workspace.dependencies.vergen] +version = "9.0.4" +features = ["build","cargo","rustc"] + [workspace.dependencies.sentry] version = "0.45.0" default-features = false @@ -148,7 +151,7 @@ features = ["now"] [workspace.dependencies.clap] version = "4.3" -features = ["derive", "cargo"] +features = ["derive","cargo"] [workspace.dependencies.rivet-term] git = "https://github.com/rivet-dev/rivet-term" @@ -357,6 +360,9 @@ path = "engine/packages/pools" [workspace.dependencies.rivet-runtime] path = "engine/packages/runtime" +[workspace.dependencies.rivet-serverless-backfill] +path = "engine/packages/serverless-backfill" + [workspace.dependencies.rivet-service-manager] path = "engine/packages/service-manager" @@ -425,8 +431,3 @@ debug = false lto = "fat" codegen-units = 1 opt-level = 3 - -# strip = true -# panic = "abort" -# overflow-checks = false -# debug-assertions = false diff --git a/engine/packages/engine/Cargo.toml b/engine/packages/engine/Cargo.toml index 0463022ad3..f1b6d7ebb9 100644 --- a/engine/packages/engine/Cargo.toml +++ b/engine/packages/engine/Cargo.toml @@ -32,6 +32,7 @@ rivet-guard.workspace = true rivet-logs.workspace = true rivet-pools.workspace = true rivet-runtime.workspace = true +rivet-serverless-backfill.workspace = true rivet-service-manager.workspace = true rivet-telemetry.workspace = true rivet-term.workspace = true diff --git a/engine/packages/engine/src/run_config.rs b/engine/packages/engine/src/run_config.rs index 635575993d..a17033164a 100644 --- a/engine/packages/engine/src/run_config.rs +++ b/engine/packages/engine/src/run_config.rs @@ -40,6 +40,12 @@ pub fn config(_rivet_config: rivet_config::Config) -> Result { |config, pools| Box::pin(rivet_cache_purge::start(config, pools)), false, ), + Service::new( + "serverless_backfill", + ServiceKind::Oneshot, + |config, pools| Box::pin(rivet_serverless_backfill::start(config, pools)), + false, + ), ]; Ok(RunConfigData { services }) diff --git a/engine/packages/serverless-backfill/Cargo.toml b/engine/packages/serverless-backfill/Cargo.toml new file mode 100644 index 0000000000..a1ad64bf2b --- /dev/null +++ b/engine/packages/serverless-backfill/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rivet-serverless-backfill" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow.workspace = true +gas.workspace = true +pegboard.workspace = true +rivet-config.workspace = true +rivet-types.workspace = true +tracing.workspace = true +universaldb.workspace = true diff --git a/engine/packages/serverless-backfill/src/lib.rs b/engine/packages/serverless-backfill/src/lib.rs new file mode 100644 index 0000000000..5213a2be43 --- /dev/null +++ b/engine/packages/serverless-backfill/src/lib.rs @@ -0,0 +1,87 @@ +use anyhow::Result; +use futures_util::{StreamExt, TryStreamExt}; +use gas::prelude::*; +use universaldb::options::StreamingMode; +use universaldb::utils::IsolationLevel::*; + +#[tracing::instrument(skip_all)] +pub async fn start(config: rivet_config::Config, pools: rivet_pools::Pools) -> Result<()> { + let cache = rivet_cache::CacheInner::from_env(&config, pools.clone())?; + let ctx = StandaloneCtx::new( + db::DatabaseKv::from_pools(pools.clone()).await?, + config.clone(), + pools, + cache, + "serverless_backfill", + Id::new_v1(config.dc_label()), + Id::new_v1(config.dc_label()), + )?; + + let serverless_data = ctx + .udb()? + .run(|tx| async move { + let tx = tx.with_subspace(pegboard::keys::subspace()); + + let serverless_desired_subspace = pegboard::keys::subspace().subspace( + &rivet_types::keys::pegboard::ns::ServerlessDesiredSlotsKey::entire_subspace(), + ); + + tx.get_ranges_keyvalues( + universaldb::RangeOption { + mode: StreamingMode::WantAll, + ..(&serverless_desired_subspace).into() + }, + // NOTE: This is a snapshot to prevent conflict with updates to this subspace + Snapshot, + ) + .map(|res| { + tx.unpack::(res?.key()) + }) + .try_collect::>() + .await + }) + .custom_instrument(tracing::info_span!("read_serverless_tx")) + .await?; + + if serverless_data.is_empty() { + return Ok(()); + } + + tracing::info!("backfilling serverless"); + + let runner_configs = ctx + .op(pegboard::ops::runner_config::get::Input { + runners: serverless_data + .iter() + .map(|key| (key.namespace_id, key.runner_name.clone())) + .collect(), + bypass_cache: true, + }) + .await?; + + for key in &serverless_data { + if !runner_configs + .iter() + .any(|rc| rc.namespace_id == key.namespace_id) + { + tracing::debug!( + namespace_id=?key.namespace_id, + runner_name=?key.runner_name, + "runner config not found, likely deleted" + ); + continue; + }; + + ctx.workflow(pegboard::workflows::runner_pool::Input { + namespace_id: key.namespace_id, + runner_name: key.runner_name.clone(), + }) + .tag("namespace_id", key.namespace_id) + .tag("runner_name", key.runner_name.clone()) + .unique() + .dispatch() + .await?; + } + + Ok(()) +} diff --git a/k8s/engine/06-rivet-engine-singleton-deployment.yaml b/k8s/engine/06-rivet-engine-singleton-deployment.yaml deleted file mode 100644 index 6d8666b351..0000000000 --- a/k8s/engine/06-rivet-engine-singleton-deployment.yaml +++ /dev/null @@ -1,79 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: rivet-engine-singleton - component: singleton - service: engine - name: rivet-engine-singleton - namespace: rivet-engine -spec: - replicas: 1 - selector: - matchLabels: - app: rivet-engine-singleton - template: - metadata: - annotations: - checksum/config: REPLACE_WITH_CONFIG_CHECKSUM - cluster-autoscaler.kubernetes.io/safe-to-evict: "false" - labels: - app: rivet-engine-singleton - component: singleton - service: engine - spec: - containers: - - args: - - start - - --services - - singleton - - --services - - api-peer - env: - - name: RIVET_CONFIG_PATH - value: /etc/rivet/config.jsonc - image: rivet-engine:local - imagePullPolicy: Never - livenessProbe: - failureThreshold: 3 - httpGet: - path: /health - port: 6421 - periodSeconds: 10 - timeoutSeconds: 5 - name: rivet-engine - ports: - - containerPort: 6421 - name: api-peer - protocol: TCP - readinessProbe: - failureThreshold: 2 - httpGet: - path: /health - port: 6421 - periodSeconds: 5 - timeoutSeconds: 3 - resources: - limits: - cpu: 4000m - memory: 8Gi - requests: - cpu: 2000m - memory: 4Gi - startupProbe: - failureThreshold: 30 - httpGet: - path: /health - port: 6421 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - volumeMounts: - - mountPath: /etc/rivet - name: config - readOnly: true - serviceAccountName: rivet-engine - volumes: - - configMap: - name: engine-config - name: config diff --git a/scripts/run/k8s/engine.sh b/scripts/run/k8s/engine.sh index b726cfa856..b84ba64465 100755 --- a/scripts/run/k8s/engine.sh +++ b/scripts/run/k8s/engine.sh @@ -54,16 +54,11 @@ kubectl apply -f 02-engine-configmap.yaml kubectl apply -f 03-rivet-engine-deployment.yaml kubectl apply -f 04-rivet-engine-service.yaml kubectl apply -f 05-rivet-engine-hpa.yaml -kubectl apply -f 06-rivet-engine-singleton-deployment.yaml # Wait for engine to be ready echo "Waiting for engine to be ready..." kubectl -n "${NAMESPACE}" wait --for=condition=ready pod -l app=rivet-engine --timeout=300s -# Wait for singleton to be ready -echo "Waiting for singleton to be ready..." -kubectl -n "${NAMESPACE}" wait --for=condition=ready pod -l app=rivet-engine-singleton --timeout=300s - echo "" echo "Deployment complete." echo "" From 181aa4d4dfec81774c44206faa63b58791cbac5b Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 9 Dec 2025 15:09:14 -0800 Subject: [PATCH 3/4] feat(examples): ai-ap-builder-freestyle --- .../ai-app-builder-freestyle/.env.example | 17 + examples/ai-app-builder-freestyle/.gitignore | 47 + .../ai-app-builder-freestyle/.prettierignore | 1 + .../ai-app-builder-freestyle/components.json | 21 + .../ai-app-builder-freestyle/docs/forking.md | 80 + .../eslint.config.mjs | 16 + examples/ai-app-builder-freestyle/icon.png | Bin 0 -> 21759 bytes .../ai-app-builder-freestyle/next.config.ts | 14 + .../ai-app-builder-freestyle/package.json | 70 + .../postcss.config.mjs | 5 + .../public/android-chrome-192x192.png | Bin 0 -> 10604 bytes .../public/android-chrome-512x512.png | Bin 0 -> 37034 bytes .../public/apple-touch-icon.png | Bin 0 -> 9388 bytes .../public/favicon-16x16.png | Bin 0 -> 547 bytes .../public/favicon-32x32.png | Bin 0 -> 1231 bytes .../public/favicon.ico | Bin 0 -> 15406 bytes .../public/logos/cursor.png | Bin 0 -> 47702 bytes .../public/logos/expo.svg | 8 + .../public/logos/next.svg | 8 + .../public/logos/vite.svg | 2 + .../public/logos/vscode.svg | 123 + .../public/manifest.json | 40 + .../public/site.webmanifest | 1 + .../scripts/update-ai-sdk.bash | 1 + .../src/actions/create-app.ts | 129 + .../src/actions/get-app.ts | 26 + .../src/actions/request-dev-server.ts | 18 + .../src/actions/send-chat-message.ts | 117 + .../src/actions/stop-stream.ts | 10 + .../src/app/api/rivet/[...all]/route.ts | 6 + .../src/app/app/[id]/loading.tsx | 12 + .../src/app/app/[id]/page.tsx | 160 ++ .../src/app/app/new/loading.tsx | 12 + .../src/app/app/new/page.tsx | 83 + .../src/app/globals.css | 309 +++ .../src/app/layout.tsx | 49 + .../src/app/loading.tsx | 5 + .../ai-app-builder-freestyle/src/app/page.tsx | 141 + .../src/components/ExampleButton.tsx | 32 + .../src/components/app-card.tsx | 87 + .../src/components/app-wrapper.tsx | 172 ++ .../src/components/chat.tsx | 245 ++ .../src/components/chatinput.tsx | 180 ++ .../src/components/framework-selector.tsx | 72 + .../src/components/loader.css | 26 + .../src/components/prompt-input.tsx | 32 + .../src/components/share-button.tsx | 108 + .../src/components/theme-provider.tsx | 49 + .../src/components/tools.tsx | 309 +++ .../src/components/topbar.tsx | 147 + .../src/components/ui/button.tsx | 58 + .../src/components/ui/card.tsx | 92 + .../src/components/ui/chat-container.tsx | 246 ++ .../src/components/ui/code-block.tsx | 98 + .../src/components/ui/dialog.tsx | 135 + .../src/components/ui/dropdown-menu.tsx | 309 +++ .../src/components/ui/form.tsx | 168 ++ .../src/components/ui/input.tsx | 21 + .../src/components/ui/label.tsx | 24 + .../src/components/ui/markdown.tsx | 250 ++ .../src/components/ui/prompt-input.tsx | 210 ++ .../src/components/ui/tabs.tsx | 66 + .../src/components/ui/textarea.tsx | 18 + .../src/components/ui/tooltip.tsx | 61 + .../src/components/user-apps.tsx | 77 + .../src/components/webview-actions.ts | 21 + .../src/components/webview.tsx | 59 + .../src/hooks/typing-animation.ts | 84 + .../src/lib/README.md | 162 ++ .../src/lib/freestyle.ts | 5 + .../src/lib/image-compression.ts | 81 + .../ai-app-builder-freestyle/src/lib/index.ts | 15 + .../src/lib/internal/ai-service.ts | 203 ++ .../ai-app-builder-freestyle/src/lib/model.ts | 3 + .../src/lib/system.ts | 33 + .../src/lib/templates.ts | 20 + .../ai-app-builder-freestyle/src/lib/utils.ts | 10 + .../ai-app-builder-freestyle/src/logo.svg | 5 + .../src/rivet/client.ts | 12 + .../src/rivet/registry.ts | 179 ++ .../src/rivet/server.ts | 11 + .../tailwind.config.ts | 86 + .../ai-app-builder-freestyle/tsconfig.json | 33 + package.json | 7 + pnpm-lock.yaml | 2373 ++++++++++++++++- scripts/run/docker/nuke-rocksdb.sh | 24 +- 86 files changed, 8169 insertions(+), 80 deletions(-) create mode 100644 examples/ai-app-builder-freestyle/.env.example create mode 100644 examples/ai-app-builder-freestyle/.gitignore create mode 100644 examples/ai-app-builder-freestyle/.prettierignore create mode 100644 examples/ai-app-builder-freestyle/components.json create mode 100644 examples/ai-app-builder-freestyle/docs/forking.md create mode 100644 examples/ai-app-builder-freestyle/eslint.config.mjs create mode 100644 examples/ai-app-builder-freestyle/icon.png create mode 100644 examples/ai-app-builder-freestyle/next.config.ts create mode 100644 examples/ai-app-builder-freestyle/package.json create mode 100644 examples/ai-app-builder-freestyle/postcss.config.mjs create mode 100644 examples/ai-app-builder-freestyle/public/android-chrome-192x192.png create mode 100644 examples/ai-app-builder-freestyle/public/android-chrome-512x512.png create mode 100644 examples/ai-app-builder-freestyle/public/apple-touch-icon.png create mode 100644 examples/ai-app-builder-freestyle/public/favicon-16x16.png create mode 100644 examples/ai-app-builder-freestyle/public/favicon-32x32.png create mode 100644 examples/ai-app-builder-freestyle/public/favicon.ico create mode 100644 examples/ai-app-builder-freestyle/public/logos/cursor.png create mode 100644 examples/ai-app-builder-freestyle/public/logos/expo.svg create mode 100644 examples/ai-app-builder-freestyle/public/logos/next.svg create mode 100644 examples/ai-app-builder-freestyle/public/logos/vite.svg create mode 100644 examples/ai-app-builder-freestyle/public/logos/vscode.svg create mode 100644 examples/ai-app-builder-freestyle/public/manifest.json create mode 100644 examples/ai-app-builder-freestyle/public/site.webmanifest create mode 100644 examples/ai-app-builder-freestyle/scripts/update-ai-sdk.bash create mode 100644 examples/ai-app-builder-freestyle/src/actions/create-app.ts create mode 100644 examples/ai-app-builder-freestyle/src/actions/get-app.ts create mode 100644 examples/ai-app-builder-freestyle/src/actions/request-dev-server.ts create mode 100644 examples/ai-app-builder-freestyle/src/actions/send-chat-message.ts create mode 100644 examples/ai-app-builder-freestyle/src/actions/stop-stream.ts create mode 100644 examples/ai-app-builder-freestyle/src/app/api/rivet/[...all]/route.ts create mode 100644 examples/ai-app-builder-freestyle/src/app/app/[id]/loading.tsx create mode 100644 examples/ai-app-builder-freestyle/src/app/app/[id]/page.tsx create mode 100644 examples/ai-app-builder-freestyle/src/app/app/new/loading.tsx create mode 100644 examples/ai-app-builder-freestyle/src/app/app/new/page.tsx create mode 100644 examples/ai-app-builder-freestyle/src/app/globals.css create mode 100644 examples/ai-app-builder-freestyle/src/app/layout.tsx create mode 100644 examples/ai-app-builder-freestyle/src/app/loading.tsx create mode 100644 examples/ai-app-builder-freestyle/src/app/page.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ExampleButton.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/app-card.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/app-wrapper.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/chat.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/chatinput.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/framework-selector.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/loader.css create mode 100644 examples/ai-app-builder-freestyle/src/components/prompt-input.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/share-button.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/theme-provider.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/tools.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/topbar.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/button.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/card.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/chat-container.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/code-block.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/dialog.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/dropdown-menu.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/form.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/input.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/label.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/markdown.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/prompt-input.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/tabs.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/textarea.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/ui/tooltip.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/user-apps.tsx create mode 100644 examples/ai-app-builder-freestyle/src/components/webview-actions.ts create mode 100644 examples/ai-app-builder-freestyle/src/components/webview.tsx create mode 100644 examples/ai-app-builder-freestyle/src/hooks/typing-animation.ts create mode 100644 examples/ai-app-builder-freestyle/src/lib/README.md create mode 100644 examples/ai-app-builder-freestyle/src/lib/freestyle.ts create mode 100644 examples/ai-app-builder-freestyle/src/lib/image-compression.ts create mode 100644 examples/ai-app-builder-freestyle/src/lib/index.ts create mode 100644 examples/ai-app-builder-freestyle/src/lib/internal/ai-service.ts create mode 100644 examples/ai-app-builder-freestyle/src/lib/model.ts create mode 100644 examples/ai-app-builder-freestyle/src/lib/system.ts create mode 100644 examples/ai-app-builder-freestyle/src/lib/templates.ts create mode 100644 examples/ai-app-builder-freestyle/src/lib/utils.ts create mode 100644 examples/ai-app-builder-freestyle/src/logo.svg create mode 100644 examples/ai-app-builder-freestyle/src/rivet/client.ts create mode 100644 examples/ai-app-builder-freestyle/src/rivet/registry.ts create mode 100644 examples/ai-app-builder-freestyle/src/rivet/server.ts create mode 100644 examples/ai-app-builder-freestyle/tailwind.config.ts create mode 100644 examples/ai-app-builder-freestyle/tsconfig.json diff --git a/examples/ai-app-builder-freestyle/.env.example b/examples/ai-app-builder-freestyle/.env.example new file mode 100644 index 0000000000..cf53184f97 --- /dev/null +++ b/examples/ai-app-builder-freestyle/.env.example @@ -0,0 +1,17 @@ +FREESTYLE_API_KEY=... +ANTHROPIC_API_KEY=... + +PREVIEW_DOMAIN=thepreviewdomain.com + +# Rivet Configuration +# For local development with embedded engine, do NOT set RIVET_ENDPOINT +# The toNextHandler runs the engine automatically +# For production with external Rivet server, uncomment and set: +# RIVET_ENDPOINT=https://your-rivet-server.com +# RIVET_NAMESPACE= +# RIVET_TOKEN= + +# Client-side Rivet configuration (must be absolute URL) +NEXT_PUBLIC_RIVET_ENDPOINT=http://localhost:3000/api/rivet +NEXT_PUBLIC_RIVET_NAMESPACE= +NEXT_PUBLIC_RIVET_TOKEN= diff --git a/examples/ai-app-builder-freestyle/.gitignore b/examples/ai-app-builder-freestyle/.gitignore new file mode 100644 index 0000000000..2b3e5c52b5 --- /dev/null +++ b/examples/ai-app-builder-freestyle/.gitignore @@ -0,0 +1,47 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# git repositories +/git/ + +dump.rdb \ No newline at end of file diff --git a/examples/ai-app-builder-freestyle/.prettierignore b/examples/ai-app-builder-freestyle/.prettierignore new file mode 100644 index 0000000000..af37bb1c91 --- /dev/null +++ b/examples/ai-app-builder-freestyle/.prettierignore @@ -0,0 +1 @@ +src/resumable-stream \ No newline at end of file diff --git a/examples/ai-app-builder-freestyle/components.json b/examples/ai-app-builder-freestyle/components.json new file mode 100644 index 0000000000..0e8b6332f3 --- /dev/null +++ b/examples/ai-app-builder-freestyle/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/examples/ai-app-builder-freestyle/docs/forking.md b/examples/ai-app-builder-freestyle/docs/forking.md new file mode 100644 index 0000000000..aa212b0152 --- /dev/null +++ b/examples/ai-app-builder-freestyle/docs/forking.md @@ -0,0 +1,80 @@ +# Forking Guide + +This documentation is for developers who want to work with the Adorable codebase. It's a good starting place to experiment with building your own AI-powered app builder. + +For additional context on building app builders with AI, see the [Freestyle guide on Building an App Builder](https://docs.freestyle.sh/guides/app-builder). + +## This is where the prompt goes + +The system prompt is located in `src/lib/system.ts` and exported as `SYSTEM_MESSAGE`. + +**Tips for updating the system prompt:** + +- Keep instructions clear and specific +- Test changes by creating a new app to see how the AI behaves +- The prompt defines the AI's personality and workflow preferences +- Consider adding domain-specific guidance for your use case + +## This is where the tools go + +Tools are located in the `src/tools/` directory. Each tool is a Mastra tool created with `createTool()`. + +**Existing tools:** + +- `todo-tool.ts` - Manages todo lists for the AI agent to track tasks +- `morph-tool.ts` - File editing tool that uses Morph API for code modifications + +**Adding new tools:** + +1. Create a new file in `src/tools/` +2. Export the tool using Mastra's `createTool()` function +3. Import and add it to the `tools` object in `src/mastra/agents/builder.ts` + +The tools are automatically available to the AI agent through the chat API in `src/app/api/chat/route.ts`. + +Add context, web search, databases or any other tools to supercharge the AI's capabilities and make it more useful for your specific app-building needs. + +## This is where the model goes + +The AI model configuration is in `src/lib/model.ts`. + +By default, we use Claude 4 Sonnet, but GPT-5 and Claude 4 Opus also work well. + +## This is where the UI goes + +**Important UI files:** + +- `src/app/page.tsx` - Main landing page with prompt input and examples +- `src/app/app/[id]/page.tsx` - Individual app chat interface +- `src/components/chat.tsx` - Main chat component for AI interactions +- `src/components/ui/` - Reusable UI components (buttons, inputs, etc.) +- `src/components/user-apps.tsx` - User's app list display +- `src/components/webview.tsx` - App preview iframe component + +## Where the database stuff goes + +**Database configuration:** + +- `src/db/schema.ts` - Database schema definitions and db connection +- Server actions in `src/actions/` handle database operations +- Use Drizzle ORM for type-safe database queries +- Run `npx drizzle-kit push` to apply schema changes + +## Database Schema + +The database schema is defined in `src/db/schema.ts` and includes: + +- `appsTable` - Application metadata +- `appUsers` - User permissions for apps +- `messagesTable` - Chat message history +- `appDeployments` - Deployment tracking + +## Environment Variables + +Required environment variables: + +- `DATABASE_URL` - PostgreSQL connection string +- `REDIS_URL` - Redis connection string +- `ANTHROPIC_API_KEY` - Claude API key +- `FREESTYLE_API_KEY` - Freestyle platform key +- Stack Auth variables (see README.md) diff --git a/examples/ai-app-builder-freestyle/eslint.config.mjs b/examples/ai-app-builder-freestyle/eslint.config.mjs new file mode 100644 index 0000000000..c85fb67c46 --- /dev/null +++ b/examples/ai-app-builder-freestyle/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/examples/ai-app-builder-freestyle/icon.png b/examples/ai-app-builder-freestyle/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..413236cfb1fde4ca17f928e11077e0580926252d GIT binary patch literal 21759 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 Eg36=lI{*Lx literal 0 HcmV?d00001 diff --git a/examples/ai-app-builder-freestyle/public/apple-touch-icon.png b/examples/ai-app-builder-freestyle/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bbbe792066cdffd0c1ed18bd2d567be5ef403abf GIT binary patch literal 9388 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Ξl(AHYh zBB7s+ge;aRtu^P(;44!m1zYawT3QVxslLnt+JYJ`bqBYa{33S;JyTheRw3dx1hK`sE|IL@My4zvC>bI3C>>m`hWbAAl&1;ctsVQ1DVm+LqefVS3} zM9k`vj-s$K3!^b|4%VRC|JCkeU`f7O?{Hku&6YmT4WP>($Z?#`lFpUH1oyhndGVMCEY^MD0ZHgp1326dp}8AxJ^x#bDL?CT5m=nHH};G4Af3Z&o_cKZg>mb z+5^Kow{r`2iEB55Fs$-H_t|kNa4#n0CHz+H;xw#<_5n9(bU4`b;9Je5 zt&M=vF2(umlUv z?8Sw01a3ggST1Jzjdd&P8R>u}{vhwsifR|OX#rifkwgc$C0OZYEx>V206G0c>N*Cx zm4zjP_n8GSEdrQ@&B!dQ^VCN5jl%-G=>c8AH9;>6T7cu0kaDL36adF%!HQGdOFEt( zLmU)WtS!+7{>iXOI&|U#(*q8c+eg-wc*_E~p`sIE0ULB&0q7tNfiTBGPo&rh0pfG> zM|cp6P6Hi)4&9aPB~2q-e30h(cPuV(;vXgb m(*FaYgf!R-N$f-b0000Px$-AP12R5(wilRe1NQ5431*C9v{!NtWPmqsxXw+273CKtg;ocb692}GQn6hWub z#aBhpMa99jK}ZY~#1PzS&_obkXDL61f83jj*xL)$S^nJrIp?|edCqeLSD|Uz6W|Tt z0v;XH-++(6jSymT*&C;D%3Ph4}1WgUBBoL=TE?M zA;gapKnZwtDjtu=)a!LNn+-mnk6j#xM!^0I;PH4A zPm9HJ?;uvI72$B0Znt{|P!#-rzoKBX+3p1>m&?rOb26FCnSv6r*Xt3F$I0b#xLhu_ z+bz4@PKkMYdyCm@rqk(=N~P58CX-15C=sP~)fh*kkrHXST+(bdnayTIqfzCB!C*in z5>a`#+pYYk#&~q%*+|)JmP8_<#t?KLHDM-7g_uaE1T?002ovPDHLkV1gNT_*4J@ literal 0 HcmV?d00001 diff --git a/examples/ai-app-builder-freestyle/public/favicon-32x32.png b/examples/ai-app-builder-freestyle/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..678740b5610a467d6bc7258226b193f75ca6ac4d GIT binary patch literal 1231 zcmV;=1Tg!FP)Px(iAh93R9HvNS8FJ?cNqWu)8z1h+Hx4N!knXMAIz~Ra-J}4rEMrhn4DrL2{9@8 zK(m$hK?x=2)DrW7y;(-iryR?i3Uj{qUVqQ?bl=Z&Kkwu4zI%6lyRZ9yU;p2A`d!z} z@JJB^!4yC^fFJ;#06qagI+7Cr8vy)l=^L#81VJzYPyrwlzz2`KSpC=yfJOj6Fvc$AY{&u-1i=A7HvrdXUabDv z9DpwvV@v8UGyn*K;00h1z(;TCa;2HS0faKfUWx5W00@HcDS+SK!R2BQK8&$tr9uTj zk@HDiVLv`T#>~tN&d$zYZEX!VH#g|%={+S_l#b7oH6aG@BY^LtduSY~g1dkA7I_ZUM{K6RfmMIu+R~yMhMn6_+cD^yPySuvx4i1K$ zogJ>Ot}rzqz!fXGKB_xJa_sv8@LMS;G)K7@sZJ>`DYS@J$EE>2Y+ zYDQiW0Ai>Q#V<-rOHol#fuNuu?uE0n^Rq5jenhoE1-!Vp$WtPcLjrL7dg!Q_WP5v? z=h5BW9pmHUyr^C()EUvy(P(aNMp{~$*xa240P;8^BLiJsU5Jg1ed*o&``Ou9?ofGo zIm*h)Gyq6akei#!dwOGI1GcudZw0{E*ckl%{ZUs}2ePl80!a}n^FKX34Np&6d|oak zB_*h=tmIrmKt#feP|}7&LqnWy3kwTTRaN!k+c!5i;o;!{Gcz;H&(Et5F5ZR`BD%Y~ zgO`^VKTLHH9!_iE^Mq)e>S$?ciO&g{R^~)Ri=~8$=jP@R7#N77qa)tKWF#pmNrhs4 zeI0vyd+_n`QR#+g434vOrUk z!cOF(6dE>;j*gOrWMX21cYv|6vG{iRl}rKRzgS8?7q934ueDqmC&hZ+rE5c$D{U>h t$N!j!K)kBUg8jVr0pe$+>HN=8oFUY6pE+`iK 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⩔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 literal 0 HcmV?d00001 diff --git a/examples/ai-app-builder-freestyle/public/logos/expo.svg b/examples/ai-app-builder-freestyle/public/logos/expo.svg new file mode 100644 index 0000000000..ba74db8010 --- /dev/null +++ b/examples/ai-app-builder-freestyle/public/logos/expo.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/examples/ai-app-builder-freestyle/public/logos/next.svg b/examples/ai-app-builder-freestyle/public/logos/next.svg new file mode 100644 index 0000000000..50ccbbd18e --- /dev/null +++ b/examples/ai-app-builder-freestyle/public/logos/next.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/examples/ai-app-builder-freestyle/public/logos/vite.svg b/examples/ai-app-builder-freestyle/public/logos/vite.svg new file mode 100644 index 0000000000..f14b90f6f4 --- /dev/null +++ b/examples/ai-app-builder-freestyle/public/logos/vite.svg @@ -0,0 +1,2 @@ + +file_type_vite \ No newline at end of file diff --git a/examples/ai-app-builder-freestyle/public/logos/vscode.svg b/examples/ai-app-builder-freestyle/public/logos/vscode.svg new file mode 100644 index 0000000000..a54dd0e57c --- /dev/null +++ b/examples/ai-app-builder-freestyle/public/logos/vscode.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/ai-app-builder-freestyle/public/manifest.json b/examples/ai-app-builder-freestyle/public/manifest.json new file mode 100644 index 0000000000..b74ad95400 --- /dev/null +++ b/examples/ai-app-builder-freestyle/public/manifest.json @@ -0,0 +1,40 @@ +{ + "short_name": "Adorable", + "name": "Adorable", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "/android-chrome-512x512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/apple-touch-icon.png", + "type": "image/png", + "sizes": "180x180" + }, + { + "src": "/favicon-32x32.png", + "type": "image/png", + "sizes": "32x32" + }, + { + "src": "/favicon-16x16.png", + "type": "image/png", + "sizes": "16x16" + } + ], + "id": "/?source=pwa", + "start_url": "/?source=pwa", + "background_color": "#ffffff", + "display": "standalone", + "scope": "/", + "theme_color": "#ffffff", + "shortcuts": [], + "description": "Adorable is an open source AI app builder.", + "screenshots": [] +} diff --git a/examples/ai-app-builder-freestyle/public/site.webmanifest b/examples/ai-app-builder-freestyle/public/site.webmanifest new file mode 100644 index 0000000000..45dc8a2065 --- /dev/null +++ b/examples/ai-app-builder-freestyle/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/examples/ai-app-builder-freestyle/scripts/update-ai-sdk.bash b/examples/ai-app-builder-freestyle/scripts/update-ai-sdk.bash new file mode 100644 index 0000000000..0c31b330a8 --- /dev/null +++ b/examples/ai-app-builder-freestyle/scripts/update-ai-sdk.bash @@ -0,0 +1 @@ +npm i --force ai@beta @ai-sdk/anthropic@beta @ai-sdk/react@beta @ai-sdk/ui-utils@canary mastra@ai-v5 @mastra/core@ai-v5 @mastra/mcp@ai-v5 @mastra/memory@ai-v5 @mastra/pg@ai-v5 \ No newline at end of file diff --git a/examples/ai-app-builder-freestyle/src/actions/create-app.ts b/examples/ai-app-builder-freestyle/src/actions/create-app.ts new file mode 100644 index 0000000000..961cb2a219 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/actions/create-app.ts @@ -0,0 +1,129 @@ +"use server"; + +import { getRivetClient } from "@/rivet/server"; +import { freestyle } from "@/lib/freestyle"; +import { templates } from "@/lib/templates"; +import { AIService } from "@/lib/internal/ai-service"; + +export async function createApp({ + appId, + templateId, + initialMessage, +}: { + appId: string; + templateId: string; + initialMessage?: string; +}) { + console.log("[createApp] Starting app creation", { appId, templateId, initialMessage }); + + if (!appId) { + throw new Error("App ID is required"); + } + + if (!templateId || !templates[templateId]) { + throw new Error(`Invalid template: ${templateId}`); + } + + // Create git repository + console.log("[createApp] Creating git repository..."); + const repo = await freestyle.createGitRepository({ + name: initialMessage || "Unnamed App", + public: true, + source: { + type: "git", + url: templates[templateId].repo, + }, + }); + console.log("[createApp] Git repository created", { repoId: repo.repoId }); + + // Create git identity for this app + console.log("[createApp] Creating git identity..."); + const gitIdentity = await freestyle.createGitIdentity(); + console.log("[createApp] Git identity created", { identityId: gitIdentity.id }); + + // Grant write permission + console.log("[createApp] Granting git permission..."); + await freestyle.grantGitPermission({ + identityId: gitIdentity.id, + repoId: repo.repoId, + permission: "write", + }); + console.log("[createApp] Git permission granted"); + + // Create access token + console.log("[createApp] Creating git access token..."); + const token = await freestyle.createGitAccessToken({ + identityId: gitIdentity.id, + }); + console.log("[createApp] Git access token created", { tokenId: token.id }); + + // Request dev server + console.log("[createApp] Requesting dev server..."); + const { mcpEphemeralUrl, fs } = await freestyle.requestDevServer({ + repoId: repo.repoId, + }); + console.log("[createApp] Dev server ready", { mcpEphemeralUrl }); + + // Create the app in the appStore actor + console.log("[createApp] Creating app in appStore actor..."); + const client = getRivetClient(); + const appActor = client.appStore.getOrCreate([appId]); + const appInfo = await appActor.createApp({ + name: initialMessage || "Unnamed App", + description: "No description", + gitRepo: repo.repoId, + baseId: "nextjs-dkjfgdf", + previewDomain: null, + freestyleIdentity: gitIdentity.id, + freestyleAccessToken: token.token, + freestyleAccessTokenId: token.id, + }); + console.log("[createApp] App created in appStore", { appId: appInfo.id, name: appInfo.name }); + + // If there's an initial message, send it to the AI (simple request/response, no streaming) + if (initialMessage) { + console.log("[createApp] Sending initial message to AI..."); + const streamActor = client.streamState.getOrCreate([appId]); + await streamActor.setRunning(); + console.log("[createApp] Stream state set to running"); + + try { + console.log("[createApp] Calling AIService.sendMessage..."); + await AIService.sendMessage( + appId, + mcpEphemeralUrl, + fs, + { + id: crypto.randomUUID(), + parts: [ + { + text: initialMessage, + type: "text", + }, + ], + role: "user", + }, + [], // No previous messages for initial creation + { + maxSteps: 100, + maxOutputTokens: 64000, + } + ); + console.log("[createApp] AIService.sendMessage completed successfully"); + + await streamActor.clear(); + console.log("[createApp] Stream state cleared"); + } catch (error) { + console.error("[createApp] AIService.sendMessage failed:", error); + await streamActor.clear(); + throw error; + } + } + + console.log("[createApp] App creation complete, returning result", { appId: appInfo.id }); + return { + id: appInfo.id, + name: appInfo.name, + gitRepo: appInfo.gitRepo, + }; +} diff --git a/examples/ai-app-builder-freestyle/src/actions/get-app.ts b/examples/ai-app-builder-freestyle/src/actions/get-app.ts new file mode 100644 index 0000000000..3db23d59ba --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/actions/get-app.ts @@ -0,0 +1,26 @@ +"use server"; + +import { getRivetClient } from "@/rivet/server"; + +export async function getApp(appId: string) { + const client = getRivetClient(); + const appActor = client.appStore.get([appId]); + + try { + const info = await appActor.getInfo(); + if (!info) { + return null; + } + + const messages = await appActor.getMessages(); + const deployments = await appActor.getDeployments(); + + return { + info, + messages, + deployments, + }; + } catch { + return null; + } +} diff --git a/examples/ai-app-builder-freestyle/src/actions/request-dev-server.ts b/examples/ai-app-builder-freestyle/src/actions/request-dev-server.ts new file mode 100644 index 0000000000..6f55fe9849 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/actions/request-dev-server.ts @@ -0,0 +1,18 @@ +"use server"; + +import { freestyle } from "@/lib/freestyle"; + +export async function requestDevServer({ repoId }: { repoId: string }) { + if (!repoId) { + throw new Error("Repo ID is required"); + } + + const { codeServerUrl, ephemeralUrl } = await freestyle.requestDevServer({ + repoId, + }); + + return { + codeServerUrl, + ephemeralUrl, + }; +} diff --git a/examples/ai-app-builder-freestyle/src/actions/send-chat-message.ts b/examples/ai-app-builder-freestyle/src/actions/send-chat-message.ts new file mode 100644 index 0000000000..c6aa6c3e7d --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/actions/send-chat-message.ts @@ -0,0 +1,117 @@ +"use server"; + +import { getApp } from "@/actions/get-app"; +import { freestyle } from "@/lib/freestyle"; +import { UIMessage, type CoreMessage } from "ai"; +import { getRivetClient } from "@/rivet/server"; +import { AIService } from "@/lib/internal/ai-service"; + +/** + * Server action to send a chat message and get a response. + * Simple request/response - no streaming. + */ +export async function sendChatMessage( + appId: string, + message: UIMessage +): Promise { + console.log("[sendChatMessage] Starting...", { appId, messageId: message.id }); + + const client = getRivetClient(); + + console.log("[sendChatMessage] Getting app..."); + const app = await getApp(appId); + if (!app) { + throw new Error("App not found"); + } + console.log("[sendChatMessage] App found", { appName: app.info?.name }); + + // Check if a stream is already running + console.log("[sendChatMessage] Checking stream status..."); + const streamActor = client.streamState.getOrCreate([appId]); + const status = await streamActor.getStatus(); + console.log("[sendChatMessage] Stream status:", status); + + if (status === "running") { + // Stop previous stream + console.log("[sendChatMessage] Aborting previous stream..."); + await streamActor.abort(); + // Wait a bit for cleanup + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + // Mark as running + console.log("[sendChatMessage] Setting stream to running..."); + await streamActor.setRunning(); + + try { + // Get dev server + console.log("[sendChatMessage] Requesting dev server..."); + const { mcpEphemeralUrl, fs } = await freestyle.requestDevServer({ + repoId: app.info.gitRepo, + }); + console.log("[sendChatMessage] Dev server ready", { mcpEphemeralUrl }); + + // Get previous messages from the app store + console.log("[sendChatMessage] Getting previous messages..."); + const appActor = client.appStore.getOrCreate([appId]); + const storedMessages = await appActor.getMessages(); + console.log("[sendChatMessage] Got previous messages", { count: storedMessages.length }); + + // Convert UIMessages to CoreMessage format expected by the AI service + const previousMessages: CoreMessage[] = storedMessages.map((m: UIMessage) => { + const content = m.parts + .map((part) => { + if (part.type === "text") { + return part.text; + } + return ""; + }) + .join(""); + return { role: m.role as "user" | "assistant", content }; + }); + + // Send message to AI + console.log("[sendChatMessage] Calling AIService.sendMessage..."); + const response = await AIService.sendMessage( + appId, + mcpEphemeralUrl, + fs, + message, + previousMessages, + { + maxSteps: 100, + maxOutputTokens: 64000, + } + ); + console.log("[sendChatMessage] AIService.sendMessage completed", { responseTextLength: response.text?.length || 0 }); + + // Clear running status + console.log("[sendChatMessage] Clearing stream status..."); + await streamActor.clear(); + + // Create assistant message from response + const assistantMessage: UIMessage = { + id: crypto.randomUUID(), + role: "assistant", + parts: [ + { + type: "text", + text: response.text, + }, + ], + }; + + // Save both messages to the app store + console.log("[sendChatMessage] Saving messages to app store..."); + await appActor.addMessage(message); + await appActor.addMessage(assistantMessage); + console.log("[sendChatMessage] Messages saved"); + + console.log("[sendChatMessage] Returning assistant message"); + return assistantMessage; + } catch (error) { + console.error("[sendChatMessage] Error occurred:", error); + await streamActor.clear(); + throw error; + } +} diff --git a/examples/ai-app-builder-freestyle/src/actions/stop-stream.ts b/examples/ai-app-builder-freestyle/src/actions/stop-stream.ts new file mode 100644 index 0000000000..099d970500 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/actions/stop-stream.ts @@ -0,0 +1,10 @@ +"use server"; + +import { getRivetClient } from "@/rivet/server"; + +export async function stopStreamAction(appId: string) { + const client = getRivetClient(); + const streamActor = client.streamState.getOrCreate([appId]); + await streamActor.abort(); + return { success: true }; +} diff --git a/examples/ai-app-builder-freestyle/src/app/api/rivet/[...all]/route.ts b/examples/ai-app-builder-freestyle/src/app/api/rivet/[...all]/route.ts new file mode 100644 index 0000000000..a9ebd3e0a7 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/app/api/rivet/[...all]/route.ts @@ -0,0 +1,6 @@ +import { toNextHandler } from "@rivetkit/next-js"; +import { registry } from "@/rivet/registry"; + +export const maxDuration = 300; + +export const { GET, POST, PUT, PATCH, HEAD, OPTIONS } = toNextHandler(registry); diff --git a/examples/ai-app-builder-freestyle/src/app/app/[id]/loading.tsx b/examples/ai-app-builder-freestyle/src/app/app/[id]/loading.tsx new file mode 100644 index 0000000000..47a56c9c9e --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/app/app/[id]/loading.tsx @@ -0,0 +1,12 @@ +import "@/components/loader.css"; + +export default function Loading() { + return ( +
+
+
Loading App
+
+
+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/app/app/[id]/page.tsx b/examples/ai-app-builder-freestyle/src/app/app/[id]/page.tsx new file mode 100644 index 0000000000..88541ac061 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/app/app/[id]/page.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { useParams } from "next/navigation"; +import AppWrapper from "@/components/app-wrapper"; +import { buttonVariants } from "@/components/ui/button"; +import Link from "next/link"; +import { client } from "@/rivet/client"; +import type { AppInfo } from "@/rivet/registry"; +import type { UIMessage } from "ai"; +import { requestDevServer } from "@/actions/request-dev-server"; + +interface DevServerInfo { + codeServerUrl: string; + ephemeralUrl: string; +} + +export default function AppPage() { + const params = useParams(); + const id = params.id as string; + + const [appInfo, setAppInfo] = useState(null); + const [messages, setMessages] = useState([]); + const [devServer, setDevServer] = useState(null); + const [streamStatus, setStreamStatus] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const appStoreConnectionRef = useRef(null); + const streamStateConnectionRef = useRef(null); + + // Load app data on mount + useEffect(() => { + loadAppData(); + }, [id]); + + // Connect to actors for real-time updates + useEffect(() => { + if (!appInfo) return; + let mounted = true; + + const setupConnections = async () => { + try { + // Connect to appStore for new messages + const appStoreConnection = await client.appStore.getOrCreate([id]).connect(); + if (!mounted) return; + appStoreConnectionRef.current = appStoreConnection; + + appStoreConnection.on("newMessage", (message: UIMessage) => { + if (mounted) { + setMessages((prev) => [...prev, message]); + } + }); + + // Connect to streamState for status updates + const streamConnection = await client.streamState.getOrCreate([id]).connect(); + if (!mounted) return; + streamStateConnectionRef.current = streamConnection; + + streamConnection.on("abort", () => { + if (mounted) setStreamStatus(null); + }); + + // Get initial stream status + const status = await client.streamState.getOrCreate([id]).getStatus(); + if (mounted) setStreamStatus(status); + } catch (err) { + console.error("Failed to setup actor connections:", err); + } + }; + + setupConnections(); + + return () => { + mounted = false; + if (appStoreConnectionRef.current?.disconnect) { + appStoreConnectionRef.current.disconnect(); + } + if (streamStateConnectionRef.current?.disconnect) { + streamStateConnectionRef.current.disconnect(); + } + }; + }, [id, appInfo]); + + async function loadAppData() { + try { + const data = await client.appStore.getOrCreate([id]).getAll(); + + if (!data.info) { + setError("App not found"); + setIsLoading(false); + return; + } + + setAppInfo(data.info); + setMessages(data.messages); + + // Get stream status + const status = await client.streamState.getOrCreate([id]).getStatus(); + setStreamStatus(status); + + // Request dev server + const devServerData = await requestDevServer({ repoId: data.info.gitRepo }); + setDevServer(devServerData); + setIsLoading(false); + } catch (err) { + console.error("Failed to load app:", err); + setError(err instanceof Error ? err.message : "Failed to load app"); + setIsLoading(false); + } + } + + if (isLoading) { + return ( +
+
Loading app...
+
+ ); + } + + if (error || !appInfo) { + return ; + } + + if (!devServer) { + return ( +
+
Starting dev server...
+
+ ); + } + + return ( + + ); +} + +function ProjectNotFound() { + return ( +
+ Project not found. +
+ + Go back to home + +
+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/app/app/new/loading.tsx b/examples/ai-app-builder-freestyle/src/app/app/new/loading.tsx new file mode 100644 index 0000000000..c67e7a4206 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/app/app/new/loading.tsx @@ -0,0 +1,12 @@ +import "@/components/loader.css"; + +export default function Loading() { + return ( +
+
+
Creating App
+
+
+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/app/app/new/page.tsx b/examples/ai-app-builder-freestyle/src/app/app/new/page.tsx new file mode 100644 index 0000000000..7c667cd04d --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/app/app/new/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { client } from "@/rivet/client"; +import { templates } from "@/lib/templates"; +import { createApp as createAppAction } from "@/actions/create-app"; + +export default function NewAppPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const [isCreating, setIsCreating] = useState(false); + const [error, setError] = useState(null); + const hasStartedRef = useRef(false); + + const message = searchParams.get("message") || ""; + const templateId = searchParams.get("template") || "nextjs"; + + useEffect(() => { + if (!hasStartedRef.current) { + hasStartedRef.current = true; + createApp(); + } + }, []); + + async function createApp() { + if (isCreating) return; + setIsCreating(true); + + try { + // Validate template + if (!templates[templateId]) { + throw new Error( + `Template ${templateId} not found. Available templates: ${Object.keys(templates).join(", ")}` + ); + } + + // Generate a new app ID + const appId = crypto.randomUUID(); + + // Call the server action to create the app + const data = await createAppAction({ + appId, + templateId, + initialMessage: message ? decodeURIComponent(message) : undefined, + }); + + // Add app to global list + await client.appList.getOrCreate(["global"]).addApp(appId); + + // Redirect to the app + router.push(`/app/${data.id}`); + } catch (err) { + console.error("Failed to create app:", err); + setError(err instanceof Error ? err.message : "Failed to create app"); + } + } + + if (error) { + return ( +
+
Error: {error}
+ +
+ ); + } + + return ( +
+
+
Creating your app...
+
+ Setting up {templates[templateId]?.name || templateId} template +
+
+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/app/globals.css b/examples/ai-app-builder-freestyle/src/app/globals.css new file mode 100644 index 0000000000..5546611155 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/app/globals.css @@ -0,0 +1,309 @@ +@import "tailwindcss"; + +@custom-variant dark (&:is(.dark *)); + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + --background: oklch(0.98 0 0); + --foreground: oklch(0.145 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +body { + margin: 0; +} + +/* Custom Markdown Styling */ +.prose-container a { + @apply text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 underline; +} + +.prose-container pre { + @apply bg-gray-100 dark:bg-gray-800 p-3 rounded-lg my-4 overflow-x-auto; +} + +.prose-container code { + @apply font-mono text-sm px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded; +} + +.prose-container pre code { + @apply bg-transparent p-0; +} + +.prose-container p { + @apply mb-4; +} + +.prose-container ul { + @apply list-disc pl-6 mb-4; +} + +.prose-container ol { + @apply list-decimal pl-6 mb-4; +} + +.prose-container li { + @apply mb-1; +} + +.prose-container h1, +.prose-container h2, +.prose-container h3, +.prose-container h4 { + @apply font-semibold mt-6 mb-3; +} + +.prose-container h1 { + @apply text-2xl; +} + +.prose-container h2 { + @apply text-xl; +} + +.prose-container h3 { + @apply text-lg; +} + +.prose-container blockquote { + @apply border-l-4 border-gray-300 dark:border-gray-700 pl-4 italic my-4 text-gray-700 dark:text-gray-300; +} + +.prose-container table { + @apply min-w-full divide-y divide-gray-300 dark:divide-gray-700 my-4; +} + +.prose-container th { + @apply px-4 py-2 text-left text-sm font-medium; +} + +.prose-container td { + @apply px-4 py-2 text-sm border-t border-gray-200 dark:border-gray-800; +} + +.prose-container img { + @apply max-w-full h-auto rounded-md my-4; +} + +.prose-container hr { + @apply my-8 border-gray-300 dark:border-gray-700; +} + +body { + margin: 0; + height: 100vh; + height: 100dvh; /* Dynamic viewport height for mobile */ +} + +/* Safe area insets for mobile Safari */ +@supports (height: 100dvh) { + body { + height: 100dvh; + } +} + +@supports (padding: env(safe-area-inset-top)) { + .safe-top { + padding-top: env(safe-area-inset-top); + } + + .safe-bottom { + padding-bottom: env(safe-area-inset-bottom); + } + + .safe-left { + padding-left: env(safe-area-inset-left); + } + + .safe-right { + padding-right: env(safe-area-inset-right); + } +} + +/* Dot pattern */ +.dot-pattern { + background-image: radial-gradient(circle, #000 1px, transparent 1px); + background-size: 30px 30px; +} diff --git a/examples/ai-app-builder-freestyle/src/app/layout.tsx b/examples/ai-app-builder-freestyle/src/app/layout.tsx new file mode 100644 index 0000000000..59b4632dda --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/app/layout.tsx @@ -0,0 +1,49 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; +import { ThemeProvider } from "@/components/theme-provider"; +import { Toaster } from "sonner"; +import { cn } from "@/lib/utils"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Rivet AI App Builder", + description: "Open Source AI App Builder For Rivet", + manifest: "/manifest.json", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + {children} + + + + ); +} diff --git a/examples/ai-app-builder-freestyle/src/app/loading.tsx b/examples/ai-app-builder-freestyle/src/app/loading.tsx new file mode 100644 index 0000000000..4b2299563e --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/app/loading.tsx @@ -0,0 +1,5 @@ +export default function Loading() { + // Stack uses React Suspense, which will render this page while user data is being fetched. + // See: https://nextjs.org/docs/app/api-reference/file-conventions/loading + return <>; +} diff --git a/examples/ai-app-builder-freestyle/src/app/page.tsx b/examples/ai-app-builder-freestyle/src/app/page.tsx new file mode 100644 index 0000000000..731b637d23 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/app/page.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { PromptInput, PromptInputActions } from "@/components/ui/prompt-input"; +import { FrameworkSelector } from "@/components/framework-selector"; +import Image from "next/image"; +import LogoSvg from "@/logo.svg"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { ExampleButton } from "@/components/ExampleButton"; +import { UserApps } from "@/components/user-apps"; +import { PromptInputTextareaWithTypingAnimation } from "@/components/prompt-input"; + +export default function Home() { + const [prompt, setPrompt] = useState(""); + const [framework, setFramework] = useState("nextjs"); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + const handleSubmit = async () => { + setIsLoading(true); + + router.push( + `/app/new?message=${encodeURIComponent(prompt)}&template=${framework}` + ); + }; + + return ( +
+
+ {/*

+ freestyle.sh +

+ Adorable Logo*/} +
+ {/* User actions could go here */} +
+
+ +
+
+

+ Rivet AI App Builder +

+ +
+
+
+ + } + isLoading={isLoading} + value={prompt} + onValueChange={setPrompt} + onSubmit={handleSubmit} + className="relative z-10 border-none bg-transparent shadow-none focus-within:border-gray-400 focus-within:ring-1 focus-within:ring-gray-200 transition-all duration-200 ease-in-out " + > + + + + + +
+
+
+ + +
+
+
+ +
+
+ ); +} + +function Examples({ setPrompt }: { setPrompt: (text: string) => void }) { + return ( +
+
+ { + console.log("Example clicked:", text); + setPrompt(text); + }} + /> + { + console.log("Example clicked:", text); + setPrompt(text); + }} + /> + { + console.log("Example clicked:", text); + setPrompt(text); + }} + /> +
+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/ExampleButton.tsx b/examples/ai-app-builder-freestyle/src/components/ExampleButton.tsx new file mode 100644 index 0000000000..2bf46c3f25 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/ExampleButton.tsx @@ -0,0 +1,32 @@ +"use client"; + +import React from "react"; +import { Button } from "./ui/button"; + +interface ExampleButtonProps { + text: string; + promptText: string; + onClick: (text: string) => void; + className?: string; +} + +export function ExampleButton({ + text, + promptText, + onClick, + className, +}: ExampleButtonProps) { + return ( + + ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/app-card.tsx b/examples/ai-app-builder-freestyle/src/components/app-card.tsx new file mode 100644 index 0000000000..8edb560dbd --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/app-card.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card"; +import Link from "next/link"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; +import { Trash, ExternalLink, MoreVertical } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { client } from "@/rivet/client"; +import { toast } from "sonner"; + +type AppCardProps = { + id: string; + name: string; + createdAt: Date; + onDelete?: () => void; +}; + +export function AppCard({ id, name, createdAt, onDelete }: AppCardProps) { + const router = useRouter(); + + const handleOpen = (e: React.MouseEvent) => { + e.preventDefault(); + router.push(`/app/${id}`); + }; + + const handleDelete = async (e: React.MouseEvent) => { + e.preventDefault(); + + try { + await client.appStore.getOrCreate([id]).deleteApp(); + await client.appList.getOrCreate(["global"]).removeApp(id); + toast.success("App deleted successfully"); + if (onDelete) { + onDelete(); + } + } catch (error) { + console.error("Failed to delete app:", error); + toast.error("Failed to delete app"); + } + }; + + return ( + + + + + {name} + + + Created {createdAt.toLocaleDateString()} + + + + +
+ + + + + + + + Open + + + + + + Delete + + + +
+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/app-wrapper.tsx b/examples/ai-app-builder-freestyle/src/components/app-wrapper.tsx new file mode 100644 index 0000000000..9a0d4c7406 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/app-wrapper.tsx @@ -0,0 +1,172 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import Chat from "./chat"; +import { TopBar } from "./topbar"; +import { MessageCircle, Monitor } from "lucide-react"; +import WebView from "./webview"; +import { UIMessage } from "ai"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); + +export default function AppWrapper({ + appName, + repo, + initialMessages, + appId, + repoId, + baseId, + domain, + running, + codeServerUrl, + consoleUrl, +}: { + appName: string; + repo: string; + appId: string; + respond?: boolean; + initialMessages: UIMessage[]; + repoId: string; + baseId: string; + codeServerUrl: string; + consoleUrl: string; + domain?: string; + running: boolean; +}) { + const [mobileActiveTab, setMobileActiveTab] = useState<"chat" | "preview">( + "chat" + ); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < 768); // md breakpoint + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + return () => window.removeEventListener("resize", checkMobile); + }, []); + + useEffect(() => { + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = "auto"; // or 'visible' + }; + }, []); + + return ( +
+ {/* Desktop and Mobile container */} +
+ {/* Chat component - positioned for both mobile and desktop */} +
+ + + } + appId={appId} + initialMessages={initialMessages} + key={appId} + running={running} + /> + +
+ + {/* Preview component - positioned for both mobile and desktop */} +
+
+ +
+
+
+ + {/* Mobile tab navigation */} + {isMobile && ( +
+ + +
+ )} +
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/chat.tsx b/examples/ai-app-builder-freestyle/src/components/chat.tsx new file mode 100644 index 0000000000..a5b3175b1d --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/chat.tsx @@ -0,0 +1,245 @@ +"use client"; + +// TODO: Replace this simple request/response chat with useChat from @ai-sdk/react +// when we have proper streaming support via Rivet actors + +import Image from "next/image"; + +import { PromptInputBasic } from "./chatinput"; +import { Markdown } from "./ui/markdown"; +import { useState, useEffect, useRef } from "react"; +import { ChatContainer } from "./ui/chat-container"; +import { UIMessage } from "ai"; +import { ToolMessage } from "./tools"; +import { CompressedImage } from "@/lib/image-compression"; +import { client } from "@/rivet/client"; +import { stopStreamAction } from "@/actions/stop-stream"; +import { sendChatMessage } from "@/actions/send-chat-message"; + +export default function Chat(props: { + appId: string; + initialMessages: UIMessage[]; + isLoading?: boolean; + topBar?: React.ReactNode; + running: boolean; +}) { + const [messages, setMessages] = useState(props.initialMessages); + const [isGenerating, setIsGenerating] = useState(props.running); + const streamConnectionRef = useRef(null); + + // Connect to streamState actor for abort events + useEffect(() => { + let mounted = true; + + const setupConnection = async () => { + try { + // Get initial status + const status = await client.streamState + .getOrCreate([props.appId]) + .getStatus(); + if (mounted) setIsGenerating(status === "running"); + + // Connect to actor for events + const connection = await client.streamState + .getOrCreate([props.appId]) + .connect(); + if (!mounted) return; + + streamConnectionRef.current = connection; + + // Listen for abort events + connection.on("abort", () => { + if (mounted) setIsGenerating(false); + }); + } catch (err) { + console.error("Failed to connect to stream state:", err); + } + }; + + setupConnection(); + + return () => { + mounted = false; + if (streamConnectionRef.current?.disconnect) { + streamConnectionRef.current.disconnect(); + } + }; + }, [props.appId]); + + const [input, setInput] = useState(""); + + const handleSendMessage = async (userMessage: UIMessage) => { + // Add user message to UI + setMessages((prev) => [...prev, userMessage]); + setIsGenerating(true); + + try { + // Send message and get response + const assistantMessage = await sendChatMessage(props.appId, userMessage); + + // Add assistant message to UI + setMessages((prev) => [...prev, assistantMessage]); + } catch (error) { + console.error("Error sending message:", error); + } finally { + setIsGenerating(false); + } + }; + + const onSubmit = (e?: React.FormEvent) => { + if (e?.preventDefault) { + e.preventDefault(); + } + + const userMessage: UIMessage = { + id: crypto.randomUUID(), + role: "user", + parts: [ + { + type: "text", + text: input, + }, + ], + }; + + handleSendMessage(userMessage); + setInput(""); + }; + + const onSubmitWithImages = (text: string, images: CompressedImage[]) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parts: any[] = []; + + if (text.trim()) { + parts.push({ + type: "text", + text: text, + }); + } + + images.forEach((image) => { + parts.push({ + type: "file", + mediaType: image.mimeType, + url: image.data, + }); + }); + + const userMessage: UIMessage = { + id: crypto.randomUUID(), + role: "user", + parts, + }; + + handleSendMessage(userMessage); + setInput(""); + }; + + async function handleStop() { + await stopStreamAction(props.appId); + } + + return ( +
+ {props.topBar} +
+ + {messages.map((message: any) => ( + + ))} + +
+
+ { + setInput(value); + }} + onSubmit={onSubmit} + onSubmitWithImages={onSubmitWithImages} + isGenerating={props.isLoading || isGenerating} + /> +
+
+ ); +} + +function MessageBody({ message }: { message: any }) { + if (message.role === "user") { + return ( +
+
+ {message.parts.map((part: any, index: number) => { + if (part.type === "text") { + return
{part.text}
; + } else if ( + part.type === "file" && + part.mediaType?.startsWith("image/") + ) { + return ( +
+ User uploaded image +
+ ); + } + return
unexpected message
; + })} +
+
+ ); + } + + if (Array.isArray(message.parts) && message.parts.length !== 0) { + return ( +
+ {message.parts.map((part: any, index: any) => { + if (part.type === "text") { + return ( +
+ + {part.text} + +
+ ); + } + + if (part.type.startsWith("tool-")) { + return ; + } + })} +
+ ); + } + + if (message.parts) { + return ( + + {message.parts + .map((part: any) => + part.type === "text" ? part.text : "[something went wrong]" + ) + .join("")} + + ); + } + + return ( +
+

Something went wrong

+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/chatinput.tsx b/examples/ai-app-builder-freestyle/src/components/chatinput.tsx new file mode 100644 index 0000000000..46a48ffc65 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/chatinput.tsx @@ -0,0 +1,180 @@ +"use client"; + +import Image from "next/image"; +import { + PromptInput, + // PromptInputAction, + // PromptInputActions, + PromptInputTextarea, +} from "@/components/ui/prompt-input"; +import { Button } from "@/components/ui/button"; +import { ArrowUp, SquareIcon, Paperclip, X } from "lucide-react"; +import { useState, useEffect, useRef } from "react"; +import { compressImage, CompressedImage } from "@/lib/image-compression"; + +interface PromptInputBasicProps { + onSubmit?: (e?: React.FormEvent) => void; + onSubmitWithImages?: (text: string, images: CompressedImage[]) => void; + isGenerating?: boolean; + input?: string; + disabled?: boolean; + onValueChange?: (value: string) => void; + stop: () => void; +} + +export function PromptInputBasic({ + onSubmit: handleSubmit, + onSubmitWithImages, + stop, + isGenerating = false, + input = "", + onValueChange, + disabled, +}: PromptInputBasicProps) { + const [isLoading, setIsLoading] = useState(false); + const [images, setImages] = useState([]); + const [isCompressing, setIsCompressing] = useState(false); + const fileInputRef = useRef(null); + + useEffect(() => { + setIsLoading(isGenerating); + }, [isGenerating]); + + const handleFileSelect = async ( + event: React.ChangeEvent + ) => { + const files = event.target.files; + if (!files) return; + + setIsCompressing(true); + const newImages: CompressedImage[] = []; + + for (const file of Array.from(files)) { + if (file.type.startsWith("image/")) { + try { + const compressed = await compressImage(file); + newImages.push(compressed); + } catch (error) { + console.error("Failed to compress image:", error); + } + } + } + + setImages((prev) => [...prev, ...newImages]); + setIsCompressing(false); + + // Clear the file input + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const removeImage = (index: number) => { + setImages((prev) => prev.filter((_, i) => i !== index)); + }; + + const handleSubmitWithData = () => { + if (onSubmitWithImages && (input.trim() || images.length > 0)) { + onSubmitWithImages(input, images); + setImages([]); + } else if (handleSubmit) { + handleSubmit(); + } + }; + + return ( +
+ {/* Image previews */} + {images.length > 0 && ( +
+ {images.map((image, index) => ( +
+ {`Upload + +
+ {Math.round(image.compressedSize / 1024)}KB +
+
+ ))} +
+ )} + + onValueChange?.(value)} + isLoading={isLoading || isCompressing} + onSubmit={handleSubmitWithData} + className="w-full border dark:bg-accent shadow-sm rounded-lg border-gray-300focus-within:border-gray-400 focus-within:ring-1 transition-all duration-200 ease-in-out focus-within:ring-gray-200 border-gray-300" + > + + + + + +
+ + + {isGenerating ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/framework-selector.tsx b/examples/ai-app-builder-freestyle/src/components/framework-selector.tsx new file mode 100644 index 0000000000..91a3eb7c89 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/framework-selector.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { ChevronDownIcon } from "lucide-react"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { templates } from "@/lib/templates"; + +type FrameworkSelectorProps = { + value?: keyof typeof templates; + onChange: (value: keyof typeof templates) => void; + className?: string; +}; + +export function FrameworkSelector({ + value = "nextjs", + onChange, + className, +}: FrameworkSelectorProps) { + return ( +
+ + + + + + {Object.entries(templates).map(([key, template]) => ( + onChange(key)} + className="gap-2 text-xs" + > + {template.name} + {templates[key].name} + + ))} + + +
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/loader.css b/examples/ai-app-builder-freestyle/src/components/loader.css new file mode 100644 index 0000000000..59138dba98 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/loader.css @@ -0,0 +1,26 @@ +.loader { + height: 4px; + width: 130px; + --c: no-repeat linear-gradient(#6100ee 0 0); + background: var(--c), var(--c), #d7b8fc; + background-size: 60% 100%; + animation: l16 3s infinite; +} + +@keyframes l16 { + 0% { + background-position: + -150% 0, + -150% 0; + } + 66% { + background-position: + 250% 0, + -150% 0; + } + 100% { + background-position: + 250% 0, + 250% 0; + } +} diff --git a/examples/ai-app-builder-freestyle/src/components/prompt-input.tsx b/examples/ai-app-builder-freestyle/src/components/prompt-input.tsx new file mode 100644 index 0000000000..ec478ceec2 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/prompt-input.tsx @@ -0,0 +1,32 @@ +import { useRef } from "react"; +import { PromptInputTextarea } from "./ui/prompt-input"; +import { useTypingAnimation } from "../hooks/typing-animation"; + +export function PromptInputTextareaWithTypingAnimation() { + const placeholderRef = useRef(null); + + const exampleIdeas = [ + "a dog food marketplace", + "a personal portfolio website for my mother's bakery", + "a B2B SaaS for burrito shops to sell burritos", + "a social network for coders to find grass to touch", + ]; + + const { displayText } = useTypingAnimation({ + texts: exampleIdeas, + baseText: "I want to build", + typingSpeed: 100, + erasingSpeed: 50, + pauseDuration: 2000, + initialDelay: 500, + }); + + return ( + {}} + /> + ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/share-button.tsx b/examples/ai-app-builder-freestyle/src/components/share-button.tsx new file mode 100644 index 0000000000..20ac67e638 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/share-button.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Share2Icon, + LinkIcon, + CopyIcon, + ExternalLinkIcon, +} from "lucide-react"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; + +interface ShareButtonProps { + className?: string; + domain?: string; + appId: string; +} + +export function ShareButton({ className, domain }: ShareButtonProps) { + const copyToClipboard = (text: string) => { + navigator.clipboard + .writeText(text) + .then(() => { + toast.success("Link copied to clipboard!"); + }) + .catch(() => { + toast.error("Failed to copy link"); + }); + }; + + return ( + + + + + + + Share App + + {domain + ? "Share your app using the preview domain." + : "No preview domain available yet."} + + + +
+ {domain ? ( + <> +
+ +
+
+ +
+ https://{domain} +
+
+ +
+
+ +
+ +
+ + ) : ( +
+

+ No preview domain available yet. +

+
+ )} +
+
+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/theme-provider.tsx b/examples/ai-app-builder-freestyle/src/components/theme-provider.tsx new file mode 100644 index 0000000000..3f28dbf860 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/theme-provider.tsx @@ -0,0 +1,49 @@ +"use client"; + +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; + +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children}; +} + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/tools.tsx b/examples/ai-app-builder-freestyle/src/components/tools.tsx new file mode 100644 index 0000000000..94d79cebe2 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/tools.tsx @@ -0,0 +1,309 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { CodeBlock, CodeBlockCode } from "./ui/code-block"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ToolInvocationType = any; + +export function ToolMessage({ + toolInvocation, +}: { + toolInvocation: ToolInvocationType; + className?: string; +}) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const input = (toolInvocation as any).input as Record | undefined; + + if (toolInvocation.type === "tool-list_directory") { + return ( + + ); + } + + if (toolInvocation.type === "tool-read_file") { + return ( + + ); + } + + if (toolInvocation.type === "tool-edit_file") { + return ; + } + + if (toolInvocation.type === "tool-write_file") { + return ; + } + + if (toolInvocation.type === "tool-exec") { + return ( + + ); + } + + if (toolInvocation.type === "tool-create_directory") { + return ( + + ); + } + + if (toolInvocation.type === "tool-update_todo_list") { + return ( + +
+ {toolInvocation.input?.items?.map?.( + ( + item: { description: string; completed: boolean }, + index: number + ) => ( +
+ {/* Minimal sleek checkbox */} +
+
+ {item.completed && ( + + + + )} +
+
+ + {item.description} + +
+ ) + )} +
+
+ ); + } + + // Fallback for other tools + return ( + + {/*
+ {JSON.stringify(toolInvocation.args, null, 2)} +
+ {toolInvocation.state === "result" && ( +
+ {toolInvocation.result.content?.map((content, index) => ( + + ))} +
+ )} */} +
+ ); +} + +function DefaultContentRenderer(props: { + content: { + type: "text"; + text: string; + }; +}) { + if (props.content?.type === "text") { + return
{props.content?.text}
; + } + + return ( +
{JSON.stringify(props.content)}
+ ); +} + +function EditFileTool({ + toolInvocation, +}: { + toolInvocation: ToolInvocationType; +}) { + return ( + +
+ {toolInvocation.input?.edits?.map?.( + (edit: { newText: string; oldText: string }, index: number) => + (edit.oldText || edit.newText) && ( + + + {edit.oldText?.split("\n").length > 5 && ( +
+ +{edit.oldText?.split("\n").length - 5} more +
+ )} + + {edit.newText?.split("\n").length > 5 && ( +
+ +{edit.newText?.split("\n").length - 5} more +
+ )} +
+ ) + )} +
+
+ ); +} + +// function StreamLines({ text }: { text: string }) { +// const [lines, setLines] = useState(text.split("\n")); + +// // useEffect(() => { +// // const newLines = text.split("\n"); +// // setLines(); +// // }, [text]); + +// return ( +//
+// {lines.join("\n")} +//
+// ); +// } + +function WriteFileTool({ + toolInvocation, +}: { + toolInvocation: ToolInvocationType; +}) { + return ( + + {toolInvocation.input?.content && ( + + + {toolInvocation.input?.content?.split("\n").length > 5 && ( +
+ +{toolInvocation.input?.content?.split("\n").length - 5} more +
+ )} +
+ )} +
+ ); +} + +function ToolBlock(props: { + toolInvocation?: ToolInvocationType; + name: string; + argsText?: string; + children?: React.ReactNode; +}) { + return ( +
+
+
+
+ {props.toolInvocation?.state !== "output-available" && ( +
+ )} +
+
+ {props.name} + {props.argsText} +
+
+ {(props.children &&
{props.children}
) || + (props.toolInvocation?.state === "output-available" && + props.toolInvocation.output?.isError && + props.toolInvocation.output?.content?.map( + (content: { type: "text"; text: string }, i: number) => ( + + + + ) + ))} +
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/topbar.tsx b/examples/ai-app-builder-freestyle/src/components/topbar.tsx new file mode 100644 index 0000000000..16cb197428 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/topbar.tsx @@ -0,0 +1,147 @@ +import { + ArrowUpRightIcon, + ComputerIcon, + GlobeIcon, + HomeIcon, + TerminalIcon, +} from "lucide-react"; +import Link from "next/link"; +import React, { useState } from "react"; +import { Button } from "./ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "./ui/dialog"; + +export function TopBar({ + appName, + children, + repoId, + consoleUrl, + codeServerUrl, +}: { + appName: string; + children?: React.ReactNode; + repoId: string; + consoleUrl: string; + codeServerUrl: string; +}) { + const [modalOpen, setModalOpen] = useState(false); + + return ( +
+ + + + + + + + + + + Open In + +
+
+
+ + Browser +
+ + + + {/*
+ + Local +
+ +
+ +
+ +
+ +
*/} +
+
+
+
+
+ ); +} diff --git a/examples/ai-app-builder-freestyle/src/components/ui/button.tsx b/examples/ai-app-builder-freestyle/src/components/ui/button.tsx new file mode 100644 index 0000000000..f117ce7547 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", + outline: + "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/examples/ai-app-builder-freestyle/src/components/ui/card.tsx b/examples/ai-app-builder-freestyle/src/components/ui/card.tsx new file mode 100644 index 0000000000..113d66c74d --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/examples/ai-app-builder-freestyle/src/components/ui/chat-container.tsx b/examples/ai-app-builder-freestyle/src/components/ui/chat-container.tsx new file mode 100644 index 0000000000..0ffcf71404 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/ui/chat-container.tsx @@ -0,0 +1,246 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { Children, useCallback, useEffect, useRef, useState } from "react"; + +const useAutoScroll = ( + containerRef: React.RefObject, + enabled: boolean, +) => { + const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); + const lastScrollTopRef = useRef(0); + const autoScrollingRef = useRef(false); + const [newMessageAdded, setNewMessageAdded] = useState(false); + const prevChildrenCountRef = useRef(0); + const scrollTriggeredRef = useRef(false); + + const isAtBottom = useCallback((element: HTMLDivElement) => { + const { scrollTop, scrollHeight, clientHeight } = element; + return scrollHeight - scrollTop - clientHeight <= 8; + }, []); + + const scrollToBottom = useCallback( + (behavior: ScrollBehavior = "instant") => { + const container = containerRef.current; + if (!container) return; + + autoScrollingRef.current = true; + scrollTriggeredRef.current = true; + + const targetScrollTop = container.scrollHeight - container.clientHeight; + + container.scrollTo({ + top: targetScrollTop, + behavior: behavior, + }); + + const checkScrollEnd = () => { + if (Math.abs(container.scrollTop - targetScrollTop) < 5) { + autoScrollingRef.current = false; + scrollTriggeredRef.current = false; + return; + } + + requestAnimationFrame(checkScrollEnd); + }; + + requestAnimationFrame(checkScrollEnd); + + const safetyTimeout = setTimeout(() => { + autoScrollingRef.current = false; + scrollTriggeredRef.current = false; + }, 500); + + try { + const handleScrollEnd = () => { + autoScrollingRef.current = false; + scrollTriggeredRef.current = false; + clearTimeout(safetyTimeout); + container.removeEventListener("scrollend", handleScrollEnd); + }; + + container.addEventListener("scrollend", handleScrollEnd, { + once: true, + }); + } catch (e) { + // scrollend event not supported in this browser, fallback to requestAnimationFrame + } + }, + [containerRef], + ); + + useEffect(() => { + if (!enabled) return; + + const container = containerRef?.current; + if (!container) return; + + lastScrollTopRef.current = container.scrollTop; + + const handleScroll = () => { + if (autoScrollingRef.current) return; + + const currentScrollTop = container.scrollTop; + + if (currentScrollTop < lastScrollTopRef.current && autoScrollEnabled) { + setAutoScrollEnabled(false); + } + + if (isAtBottom(container) && !autoScrollEnabled) { + setAutoScrollEnabled(true); + } + + lastScrollTopRef.current = currentScrollTop; + }; + + const handleWheel = (e: WheelEvent) => { + if (e.deltaY < 0 && autoScrollEnabled) { + setAutoScrollEnabled(false); + } + }; + + const handleTouchStart = () => { + lastScrollTopRef.current = container.scrollTop; + }; + + const handleTouchMove = () => { + if (container.scrollTop < lastScrollTopRef.current && autoScrollEnabled) { + setAutoScrollEnabled(false); + } + + lastScrollTopRef.current = container.scrollTop; + }; + + const handleTouchEnd = () => { + if (isAtBottom(container) && !autoScrollEnabled) { + setAutoScrollEnabled(true); + } + }; + + container.addEventListener("scroll", handleScroll, { passive: true }); + container.addEventListener("wheel", handleWheel, { passive: true }); + container.addEventListener("touchstart", handleTouchStart, { + passive: true, + }); + container.addEventListener("touchmove", handleTouchMove, { passive: true }); + container.addEventListener("touchend", handleTouchEnd, { passive: true }); + + return () => { + container.removeEventListener("scroll", handleScroll); + container.removeEventListener("wheel", handleWheel); + container.removeEventListener("touchstart", handleTouchStart); + container.removeEventListener("touchmove", handleTouchMove); + container.removeEventListener("touchend", handleTouchEnd); + }; + }, [containerRef, enabled, autoScrollEnabled, isAtBottom]); + + return { + autoScrollEnabled, + scrollToBottom, + isScrolling: autoScrollingRef.current, + scrollTriggered: scrollTriggeredRef.current, + newMessageAdded, + setNewMessageAdded, + prevChildrenCountRef, + }; +}; + +export type ChatContainerProps = { + children: React.ReactNode; + className?: string; + autoScroll?: boolean; + scrollToRef?: React.RefObject; + ref?: React.RefObject; +} & React.HTMLAttributes; + +function ChatContainer({ + className, + children, + autoScroll = true, + scrollToRef, + ref, + ...props +}: ChatContainerProps) { + const containerRef = useRef(null); + const localBottomRef = useRef(null); + const bottomRef = scrollToRef || localBottomRef; + const chatContainerRef = ref || containerRef; + const prevChildrenRef = useRef(null); + const contentChangedWithoutNewMessageRef = useRef(false); + + const { + autoScrollEnabled, + scrollToBottom, + isScrolling, + scrollTriggered, + newMessageAdded, + setNewMessageAdded, + prevChildrenCountRef, + } = useAutoScroll(chatContainerRef, autoScroll); + + useEffect(() => { + const childrenArray = Children.toArray(children); + const currentChildrenCount = childrenArray.length; + + if (currentChildrenCount > prevChildrenCountRef.current) { + setNewMessageAdded(true); + } else if (prevChildrenRef.current !== children) { + contentChangedWithoutNewMessageRef.current = true; + } + + prevChildrenCountRef.current = currentChildrenCount; + prevChildrenRef.current = children; + }, [children, setNewMessageAdded]); + + useEffect(() => { + if (!autoScroll) return; + + const scrollHandler = () => { + if (newMessageAdded) { + scrollToBottom("instant"); + setNewMessageAdded(false); + contentChangedWithoutNewMessageRef.current = false; + } else if ( + contentChangedWithoutNewMessageRef.current && + autoScrollEnabled && + !isScrolling && + !scrollTriggered + ) { + scrollToBottom("instant"); + contentChangedWithoutNewMessageRef.current = false; + } + }; + + requestAnimationFrame(scrollHandler); + }, [ + children, + autoScroll, + autoScrollEnabled, + isScrolling, + scrollTriggered, + scrollToBottom, + newMessageAdded, + setNewMessageAdded, + ]); + + return ( +
+ {children} + + ); +} + +export { ChatContainer }; diff --git a/examples/ai-app-builder-freestyle/src/components/ui/code-block.tsx b/examples/ai-app-builder-freestyle/src/components/ui/code-block.tsx new file mode 100644 index 0000000000..19109ccbe6 --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/ui/code-block.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { useTheme } from "next-themes"; +import React, { useEffect, useState } from "react"; +import { codeToHtml } from "shiki"; + +export type CodeBlockProps = { + children?: React.ReactNode; + className?: string; +} & React.HTMLProps; + +function CodeBlock({ children, className, ...props }: CodeBlockProps) { + return ( +
+ {children} +
+ ); +} + +export type CodeBlockCodeProps = { + code: string; + language?: string; + theme?: string; + className?: string; +} & React.HTMLProps; + +function CodeBlockCode({ + code, + language = "tsx", + className, + ...props +}: CodeBlockCodeProps) { + const { theme: browserTheme } = useTheme(); + + const [highlightedHtml, setHighlightedHtml] = useState(null); + + const theme = browserTheme === "dark" ? "github-dark" : "github-light"; + + useEffect(() => { + async function highlight() { + if (!code) { + setHighlightedHtml("
"); + return; + } + + const html = await codeToHtml(code, { lang: language, theme }); + setHighlightedHtml(html); + } + highlight(); + }, [code, language, theme]); + + const classNames = cn( + "w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4", + className, + ); + + // SSR fallback: render plain code if not hydrated yet + return highlightedHtml ? ( +
+ ) : ( +
+
+        {code}
+      
+
+ ); +} + +export type CodeBlockGroupProps = React.HTMLAttributes; + +function CodeBlockGroup({ + children, + className, + ...props +}: CodeBlockGroupProps) { + return ( +
+ {children} +
+ ); +} + +export { CodeBlockGroup, CodeBlockCode, CodeBlock }; diff --git a/examples/ai-app-builder-freestyle/src/components/ui/dialog.tsx b/examples/ai-app-builder-freestyle/src/components/ui/dialog.tsx new file mode 100644 index 0000000000..7d7a9d318d --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/ui/dialog.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/examples/ai-app-builder-freestyle/src/components/ui/dropdown-menu.tsx b/examples/ai-app-builder-freestyle/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000000..46a4140a2a --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,309 @@ +"use client"; + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +type PointerDownEvent = Parameters< + NonNullable +>[0]; +type PointerDownOutsideEvent = Parameters< + NonNullable< + DropdownMenuPrimitive.DropdownMenuContentProps["onPointerDownOutside"] + > +>[0]; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + onPointerDown, + onPointerDownOutside, + onCloseAutoFocus, + ...props +}: React.ComponentProps) { + const isCloseFromMouse = React.useRef(false); + + const handlePointerDown = React.useCallback( + (e: PointerDownEvent) => { + isCloseFromMouse.current = true; + onPointerDown?.(e); + }, + [onPointerDown], + ); + + const handlePointerDownOutside = React.useCallback( + (e: PointerDownOutsideEvent) => { + isCloseFromMouse.current = true; + onPointerDownOutside?.(e); + }, + [onPointerDownOutside], + ); + + const handleCloseAutoFocus = React.useCallback( + (e: Event) => { + if (onCloseAutoFocus) { + return onCloseAutoFocus(e); + } + + if (!isCloseFromMouse.current) { + return; + } + + e.preventDefault(); + isCloseFromMouse.current = false; + }, + [onCloseAutoFocus], + ); + + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +}; diff --git a/examples/ai-app-builder-freestyle/src/components/ui/form.tsx b/examples/ai-app-builder-freestyle/src/components/ui/form.tsx new file mode 100644 index 0000000000..bf4214d8ac --- /dev/null +++ b/examples/ai-app-builder-freestyle/src/components/ui/form.tsx @@ -0,0 +1,168 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form"; + +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId(); + + return ( + +
+ + ); +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField(); + + return ( +