Click to edit UI (#385)
- [x] add e2e test - happy case (make sure it clears selection and next prompt is empty, and preview is cleared); de-selection case - [x] shim - old & new file - [x] upgrade path - [x] add docs - [x] add try-catch to parser script - [x] make it work for next.js - [x] extract npm package - [x] make sure plugin doesn't apply in prod
This commit is contained in:
24
e2e-tests/fixtures/import-app/select-component/.gitignore
vendored
Normal file
24
e2e-tests/fixtures/import-app/select-component/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1 @@
|
||||
# AI RULES placeholder
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
13
e2e-tests/fixtures/import-app/select-component/index.html
Normal file
13
e2e-tests/fixtures/import-app/select-component/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>dyad-generated-app</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
34
e2e-tests/fixtures/import-app/select-component/package.json
Normal file
34
e2e-tests/fixtures/import-app/select-component/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "vite_react_shadcn_ts",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.514.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^6.3.4"
|
||||
}
|
||||
}
|
||||
1373
e2e-tests/fixtures/import-app/select-component/pnpm-lock.yaml
generated
Normal file
1373
e2e-tests/fixtures/import-app/select-component/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
e2e-tests/fixtures/import-app/select-component/src/App.tsx
Normal file
12
e2e-tests/fixtures/import-app/select-component/src/App.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Index from "./pages/Index";
|
||||
|
||||
const App = () => (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,59 @@
|
||||
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-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs 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 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./styles/globals.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Rocket, ShieldCheck, Sparkles } from "lucide-react";
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Rocket className="w-8 h-8 text-primary mb-2" />,
|
||||
title: "Fast & Modern",
|
||||
description: "Built with the latest tech for blazing fast performance.",
|
||||
},
|
||||
{
|
||||
icon: <ShieldCheck className="w-8 h-8 text-primary mb-2" />,
|
||||
title: "Secure by Design",
|
||||
description: "Security best practices baked in from the start.",
|
||||
},
|
||||
{
|
||||
icon: <Sparkles className="w-8 h-8 text-primary mb-2" />,
|
||||
title: "Easy to Customize",
|
||||
description: "Effortlessly adapt the template to your needs.",
|
||||
},
|
||||
];
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
{/* Hero Section */}
|
||||
<header className="flex-1 flex flex-col items-center justify-center px-4 py-16">
|
||||
<h1 className="text-5xl font-extrabold mb-4 text-foreground text-center">
|
||||
Launch Your Next Project
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 text-center max-w-xl">
|
||||
A simple, modern landing page template built with React, shadcn/ui,
|
||||
and Tailwind CSS.
|
||||
</p>
|
||||
<Button size="lg" className="px-8 py-6 text-lg">
|
||||
Get Started
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="py-12 bg-muted">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{features.map((feature, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col items-center text-center bg-card rounded-lg p-6 shadow-sm"
|
||||
>
|
||||
{feature.icon}
|
||||
<h3 className="text-lg font-semibold mb-2 text-foreground">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -0,0 +1,120 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@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;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
1
e2e-tests/fixtures/import-app/select-component/src/vite-env.d.ts
vendored
Normal file
1
e2e-tests/fixtures/import-app/select-component/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
19
e2e-tests/fixtures/import-app/select-component/tsconfig.json
Normal file
19
e2e-tests/fixtures/import-app/select-component/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"noImplicitAny": false,
|
||||
"noUnusedParameters": false,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"noUnusedLocals": false,
|
||||
"strictNullChecks": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from "vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig(() => ({
|
||||
server: {
|
||||
host: "::",
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -62,6 +62,7 @@ export function generateAppFilesSnapshotData(
|
||||
basePath: string,
|
||||
): FileSnapshotData[] {
|
||||
const ignorePatterns = [
|
||||
".DS_Store",
|
||||
".git",
|
||||
"node_modules",
|
||||
// Avoid snapshotting lock files because they are getting generated
|
||||
|
||||
@@ -220,6 +220,20 @@ export class PageObject {
|
||||
await this.page.getByText("Rebuild").click();
|
||||
}
|
||||
|
||||
async clickTogglePreviewPanel() {
|
||||
await this.page.getByTestId("toggle-preview-panel-button").click();
|
||||
}
|
||||
|
||||
async clickPreviewPickElement() {
|
||||
await this.page
|
||||
.getByTestId("preview-pick-element-button")
|
||||
.click({ timeout: Timeout.LONG });
|
||||
}
|
||||
|
||||
async clickDeselectComponent() {
|
||||
await this.page.getByRole("button", { name: "Deselect component" }).click();
|
||||
}
|
||||
|
||||
async clickPreviewMoreOptions() {
|
||||
await this.page.getByTestId("preview-more-options-button").click();
|
||||
}
|
||||
@@ -262,6 +276,18 @@ export class PageObject {
|
||||
return this.page.getByTestId("preview-error-banner");
|
||||
}
|
||||
|
||||
async snapshotChatInputContainer() {
|
||||
await expect(this.getChatInputContainer()).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
getSelectedComponentDisplay() {
|
||||
return this.page.getByTestId("selected-component-display");
|
||||
}
|
||||
|
||||
async snapshotSelectedComponentDisplay() {
|
||||
await expect(this.getSelectedComponentDisplay()).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
async snapshotPreview({ name }: { name?: string } = {}) {
|
||||
const iframe = this.getPreviewIframeElement();
|
||||
await expect(iframe.contentFrame().locator("body")).toMatchAriaSnapshot({
|
||||
@@ -483,6 +509,16 @@ export class PageObject {
|
||||
await this.page.getByRole("button", { name: "Open in Chat" }).click();
|
||||
}
|
||||
|
||||
async clickAppUpgradeButton({ upgradeId }: { upgradeId: string }) {
|
||||
await this.page.getByTestId(`app-upgrade-${upgradeId}`).click();
|
||||
}
|
||||
|
||||
async expectNoAppUpgrades() {
|
||||
await expect(this.page.getByTestId("no-app-upgrades-needed")).toBeVisible({
|
||||
timeout: Timeout.MEDIUM,
|
||||
});
|
||||
}
|
||||
|
||||
async clickAppDetailsRenameAppButton() {
|
||||
await this.page.getByTestId("app-details-rename-app-button").click();
|
||||
}
|
||||
|
||||
102
e2e-tests/select_component.spec.ts
Normal file
102
e2e-tests/select_component.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { testSkipIfWindows } from "./helpers/test_helper";
|
||||
|
||||
testSkipIfWindows("select component", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.sendPrompt("tc=basic");
|
||||
await po.clickTogglePreviewPanel();
|
||||
await po.clickPreviewPickElement();
|
||||
|
||||
await po
|
||||
.getPreviewIframeElement()
|
||||
.contentFrame()
|
||||
.getByRole("heading", { name: "Welcome to Your Blank App" })
|
||||
.click();
|
||||
|
||||
await po.snapshotPreview();
|
||||
await po.snapshotSelectedComponentDisplay();
|
||||
|
||||
await po.sendPrompt("[dump] make it smaller");
|
||||
await po.snapshotPreview();
|
||||
await expect(po.getSelectedComponentDisplay()).not.toBeVisible();
|
||||
|
||||
await po.snapshotServerDump("all-messages");
|
||||
|
||||
// Send one more prompt to make sure it's a normal message.
|
||||
await po.sendPrompt("[dump] tc=basic");
|
||||
await po.snapshotServerDump("last-message");
|
||||
});
|
||||
|
||||
testSkipIfWindows("deselect component", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.sendPrompt("tc=basic");
|
||||
await po.clickTogglePreviewPanel();
|
||||
await po.clickPreviewPickElement();
|
||||
|
||||
await po
|
||||
.getPreviewIframeElement()
|
||||
.contentFrame()
|
||||
.getByRole("heading", { name: "Welcome to Your Blank App" })
|
||||
.click();
|
||||
|
||||
await po.snapshotPreview();
|
||||
await po.snapshotSelectedComponentDisplay();
|
||||
|
||||
// Deselect the component and make sure the state has reverted
|
||||
await po.clickDeselectComponent();
|
||||
|
||||
await po.snapshotPreview();
|
||||
await expect(po.getSelectedComponentDisplay()).not.toBeVisible();
|
||||
|
||||
// Send one more prompt to make sure it's a normal message.
|
||||
await po.sendPrompt("[dump] tc=basic");
|
||||
await po.snapshotServerDump("last-message");
|
||||
});
|
||||
|
||||
testSkipIfWindows("upgrade app to select component", async ({ po }) => {
|
||||
await po.setUp();
|
||||
await po.importApp("select-component");
|
||||
await po.getTitleBarAppNameButton().click();
|
||||
await po.clickAppUpgradeButton({ upgradeId: "component-tagger" });
|
||||
await po.expectNoAppUpgrades();
|
||||
await po.snapshotAppFiles();
|
||||
await po.clickOpenInChatButton();
|
||||
|
||||
await po.clickPreviewPickElement();
|
||||
|
||||
await po
|
||||
.getPreviewIframeElement()
|
||||
.contentFrame()
|
||||
.getByRole("heading", { name: "Launch Your Next Project" })
|
||||
.click();
|
||||
|
||||
await po.sendPrompt("[dump] make it smaller");
|
||||
await po.snapshotServerDump("last-message");
|
||||
});
|
||||
|
||||
testSkipIfWindows("select component next.js", async ({ po }) => {
|
||||
await po.setUp();
|
||||
|
||||
// Select Next.js template
|
||||
await po.goToHubTab();
|
||||
await po.selectTemplate("Next.js Template");
|
||||
await po.goToAppsTab();
|
||||
|
||||
await po.sendPrompt("tc=basic");
|
||||
await po.clickTogglePreviewPanel();
|
||||
await po.clickPreviewPickElement();
|
||||
|
||||
await po
|
||||
.getPreviewIframeElement()
|
||||
.contentFrame()
|
||||
.getByRole("heading", { name: "Blank page" })
|
||||
.click();
|
||||
|
||||
await po.snapshotPreview();
|
||||
await po.snapshotSelectedComponentDisplay();
|
||||
|
||||
await po.sendPrompt("[dump] make it smaller");
|
||||
await po.snapshotPreview();
|
||||
|
||||
await po.snapshotServerDump("all-messages");
|
||||
});
|
||||
@@ -1279,6 +1279,7 @@ export default {
|
||||
|
||||
<dyad-file path="vite.config.ts">
|
||||
import { defineConfig } from "vite";
|
||||
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
|
||||
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
|
||||
host: "::",
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [react()],
|
||||
plugins: [dyadComponentTagger(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
@@ -1279,6 +1279,7 @@ export default {
|
||||
|
||||
<dyad-file path="vite.config.ts">
|
||||
import { defineConfig } from "vite";
|
||||
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
|
||||
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
|
||||
host: "::",
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [react()],
|
||||
plugins: [dyadComponentTagger(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
@@ -1279,6 +1279,7 @@ export default {
|
||||
|
||||
<dyad-file path="vite.config.ts">
|
||||
import { defineConfig } from "vite";
|
||||
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
|
||||
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
|
||||
host: "::",
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [react()],
|
||||
plugins: [dyadComponentTagger(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
@@ -1279,6 +1279,7 @@ export default {
|
||||
|
||||
<dyad-file path="vite.config.ts">
|
||||
import { defineConfig } from "vite";
|
||||
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
|
||||
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
|
||||
host: "::",
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [react()],
|
||||
plugins: [dyadComponentTagger(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
@@ -186,6 +186,7 @@ A file (2)
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/node": "^22.5.5",
|
||||
@@ -5845,6 +5846,7 @@ export default {
|
||||
|
||||
=== vite.config.ts ===
|
||||
import { defineConfig } from "vite";
|
||||
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
|
||||
@@ -5853,7 +5855,7 @@ export default defineConfig(() => ({
|
||||
host: "::",
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [react()],
|
||||
plugins: [dyadComponentTagger(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
@@ -1279,6 +1279,7 @@ export default {
|
||||
|
||||
<dyad-file path="vite.config.ts">
|
||||
import { defineConfig } from "vite";
|
||||
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
|
||||
@@ -1287,7 +1288,7 @@ export default defineConfig(() => ({
|
||||
host: "::",
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [react()],
|
||||
plugins: [dyadComponentTagger(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -33,7 +33,7 @@
|
||||
},
|
||||
{
|
||||
"path": "package.json",
|
||||
"content": "{\n \"dependencies\": {\n \"@hookform/resolvers\": \"^3.9.0\",\n \"@radix-ui/react-accordion\": \"^1.2.0\",\n \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n \"@radix-ui/react-aspect-ratio\": \"^1.1.0\",\n \"@radix-ui/react-avatar\": \"^1.1.0\",\n \"@radix-ui/react-checkbox\": \"^1.1.1\",\n \"@radix-ui/react-collapsible\": \"^1.1.0\",\n \"@radix-ui/react-context-menu\": \"^2.2.1\",\n \"@radix-ui/react-dialog\": \"^1.1.2\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n \"@radix-ui/react-hover-card\": \"^1.1.1\",\n \"@radix-ui/react-label\": \"^2.1.0\",\n \"@radix-ui/react-menubar\": \"^1.1.1\",\n \"@radix-ui/react-navigation-menu\": \"^1.2.0\",\n \"@radix-ui/react-popover\": \"^1.1.1\",\n \"@radix-ui/react-progress\": \"^1.1.0\",\n \"@radix-ui/react-radio-group\": \"^1.2.0\",\n \"@radix-ui/react-scroll-area\": \"^1.1.0\",\n \"@radix-ui/react-select\": \"^2.1.1\",\n \"@radix-ui/react-separator\": \"^1.1.0\",\n \"@radix-ui/react-slider\": \"^1.2.0\",\n \"@radix-ui/react-slot\": \"^1.1.0\",\n \"@radix-ui/react-switch\": \"^1.1.0\",\n \"@radix-ui/react-tabs\": \"^1.1.0\",\n \"@radix-ui/react-toast\": \"^1.2.1\",\n \"@radix-ui/react-toggle\": \"^1.1.0\",\n \"@radix-ui/react-toggle-group\": \"^1.1.0\",\n \"@radix-ui/react-tooltip\": \"^1.1.4\",\n \"@tanstack/react-query\": \"^5.56.2\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"cmdk\": \"^1.0.0\",\n \"date-fns\": \"^3.6.0\",\n \"embla-carousel-react\": \"^8.3.0\",\n \"input-otp\": \"^1.2.4\",\n \"lucide-react\": \"^0.462.0\",\n \"next-themes\": \"^0.3.0\",\n \"react\": \"^18.3.1\",\n \"react-day-picker\": \"^8.10.1\",\n \"react-dom\": \"^18.3.1\",\n \"react-hook-form\": \"^7.53.0\",\n \"react-resizable-panels\": \"^2.1.3\",\n \"react-router-dom\": \"^6.26.2\",\n \"recharts\": \"^2.12.7\",\n \"sonner\": \"^1.5.0\",\n \"tailwind-merge\": \"^2.5.2\",\n \"tailwindcss-animate\": \"^1.0.7\",\n \"vaul\": \"^0.9.3\",\n \"zod\": \"^3.23.8\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^18.3.3\",\n \"@types/react-dom\": \"^18.3.0\",\n \"@vitejs/plugin-react-swc\": \"^3.9.0\",\n \"autoprefixer\": \"^10.4.20\",\n \"eslint\": \"^9.9.0\",\n \"eslint-plugin-react-hooks\": \"^5.1.0-rc.0\",\n \"eslint-plugin-react-refresh\": \"^0.4.9\",\n \"globals\": \"^15.9.0\",\n \"postcss\": \"^8.4.47\",\n \"tailwindcss\": \"^3.4.11\",\n \"typescript\": \"^5.5.3\",\n \"typescript-eslint\": \"^8.0.1\",\n \"vite\": \"^6.3.4\"\n }\n}",
|
||||
"content": "{\n \"dependencies\": {\n \"@hookform/resolvers\": \"^3.9.0\",\n \"@radix-ui/react-accordion\": \"^1.2.0\",\n \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n \"@radix-ui/react-aspect-ratio\": \"^1.1.0\",\n \"@radix-ui/react-avatar\": \"^1.1.0\",\n \"@radix-ui/react-checkbox\": \"^1.1.1\",\n \"@radix-ui/react-collapsible\": \"^1.1.0\",\n \"@radix-ui/react-context-menu\": \"^2.2.1\",\n \"@radix-ui/react-dialog\": \"^1.1.2\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n \"@radix-ui/react-hover-card\": \"^1.1.1\",\n \"@radix-ui/react-label\": \"^2.1.0\",\n \"@radix-ui/react-menubar\": \"^1.1.1\",\n \"@radix-ui/react-navigation-menu\": \"^1.2.0\",\n \"@radix-ui/react-popover\": \"^1.1.1\",\n \"@radix-ui/react-progress\": \"^1.1.0\",\n \"@radix-ui/react-radio-group\": \"^1.2.0\",\n \"@radix-ui/react-scroll-area\": \"^1.1.0\",\n \"@radix-ui/react-select\": \"^2.1.1\",\n \"@radix-ui/react-separator\": \"^1.1.0\",\n \"@radix-ui/react-slider\": \"^1.2.0\",\n \"@radix-ui/react-slot\": \"^1.1.0\",\n \"@radix-ui/react-switch\": \"^1.1.0\",\n \"@radix-ui/react-tabs\": \"^1.1.0\",\n \"@radix-ui/react-toast\": \"^1.2.1\",\n \"@radix-ui/react-toggle\": \"^1.1.0\",\n \"@radix-ui/react-toggle-group\": \"^1.1.0\",\n \"@radix-ui/react-tooltip\": \"^1.1.4\",\n \"@tanstack/react-query\": \"^5.56.2\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"cmdk\": \"^1.0.0\",\n \"date-fns\": \"^3.6.0\",\n \"embla-carousel-react\": \"^8.3.0\",\n \"input-otp\": \"^1.2.4\",\n \"lucide-react\": \"^0.462.0\",\n \"next-themes\": \"^0.3.0\",\n \"react\": \"^18.3.1\",\n \"react-day-picker\": \"^8.10.1\",\n \"react-dom\": \"^18.3.1\",\n \"react-hook-form\": \"^7.53.0\",\n \"react-resizable-panels\": \"^2.1.3\",\n \"react-router-dom\": \"^6.26.2\",\n \"recharts\": \"^2.12.7\",\n \"sonner\": \"^1.5.0\",\n \"tailwind-merge\": \"^2.5.2\",\n \"tailwindcss-animate\": \"^1.0.7\",\n \"vaul\": \"^0.9.3\",\n \"zod\": \"^3.23.8\"\n },\n \"devDependencies\": {\n \"@dyad-sh/react-vite-component-tagger\": \"^0.8.0\",\n \"@eslint/js\": \"^9.9.0\",\n \"@tailwindcss/typography\": \"^0.5.15\",\n \"@types/node\": \"^22.5.5\",\n \"@types/react\": \"^18.3.3\",\n \"@types/react-dom\": \"^18.3.0\",\n \"@vitejs/plugin-react-swc\": \"^3.9.0\",\n \"autoprefixer\": \"^10.4.20\",\n \"eslint\": \"^9.9.0\",\n \"eslint-plugin-react-hooks\": \"^5.1.0-rc.0\",\n \"eslint-plugin-react-refresh\": \"^0.4.9\",\n \"globals\": \"^15.9.0\",\n \"postcss\": \"^8.4.47\",\n \"tailwindcss\": \"^3.4.11\",\n \"typescript\": \"^5.5.3\",\n \"typescript-eslint\": \"^8.0.1\",\n \"vite\": \"^6.3.4\"\n }\n}",
|
||||
"force": false
|
||||
},
|
||||
{
|
||||
@@ -358,7 +358,7 @@
|
||||
},
|
||||
{
|
||||
"path": "vite.config.ts",
|
||||
"content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport path from \"path\";\n\nexport default defineConfig(() => ({\n server: {\n host: \"::\",\n port: 8080,\n },\n plugins: [react()],\n resolve: {\n alias: {\n \"@\": path.resolve(__dirname, \"./src\"),\n },\n },\n}));\n",
|
||||
"content": "import { defineConfig } from \"vite\";\nimport dyadComponentTagger from \"@dyad-sh/react-vite-component-tagger\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport path from \"path\";\n\nexport default defineConfig(() => ({\n server: {\n host: \"::\",\n port: 8080,\n },\n plugins: [dyadComponentTagger(), react()],\n resolve: {\n alias: {\n \"@\": path.resolve(__dirname, \"./src\"),\n },\n },\n}));\n",
|
||||
"force": false
|
||||
}
|
||||
],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
|
||||
- region "Notifications (F8)":
|
||||
- list
|
||||
- region "Notifications alt+T"
|
||||
- heading "Welcome to Your Blank App" [level=1]
|
||||
- paragraph: Start building your amazing project here!
|
||||
- link "Made with Dyad":
|
||||
- /url: https://www.dyad.sh/
|
||||
- img
|
||||
- text: Edit with AI h1 src/pages/Index.tsx
|
||||
@@ -0,0 +1,3 @@
|
||||
===
|
||||
role: user
|
||||
message: [dump] tc=basic
|
||||
@@ -0,0 +1,4 @@
|
||||
- img
|
||||
- text: h1 src/pages/Index.tsx:9
|
||||
- button "Deselect component":
|
||||
- img
|
||||
@@ -0,0 +1,7 @@
|
||||
- region "Notifications (F8)":
|
||||
- list
|
||||
- region "Notifications alt+T"
|
||||
- heading "Welcome to Your Blank App" [level=1]
|
||||
- paragraph: Start building your amazing project here!
|
||||
- link "Made with Dyad":
|
||||
- /url: https://www.dyad.sh/
|
||||
@@ -0,0 +1,9 @@
|
||||
- region "Notifications (F8)":
|
||||
- list
|
||||
- region "Notifications alt+T"
|
||||
- heading "Welcome to Your Blank App" [level=1]
|
||||
- paragraph: Start building your amazing project here!
|
||||
- link "Made with Dyad":
|
||||
- /url: https://www.dyad.sh/
|
||||
- img
|
||||
- text: Edit with AI h1 src/pages/Index.tsx
|
||||
@@ -0,0 +1,459 @@
|
||||
===
|
||||
role: system
|
||||
message:
|
||||
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
|
||||
Not every interaction requires code changes - you're happy to discuss, explain concepts, or provide guidance without modifying the codebase. When code changes are needed, you make efficient and effective updates to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
|
||||
|
||||
# App Preview / Commands
|
||||
|
||||
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
|
||||
|
||||
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
|
||||
- **Restart**: This will restart the app server.
|
||||
- **Refresh**: This will refresh the app preview page.
|
||||
|
||||
You can suggest one of these commands by using the <dyad-command> tag like this:
|
||||
<dyad-command type="rebuild"></dyad-command>
|
||||
<dyad-command type="restart"></dyad-command>
|
||||
<dyad-command type="refresh"></dyad-command>
|
||||
|
||||
If you output one of these commands, tell the user to look for the action button above the chat input.
|
||||
|
||||
# Guidelines
|
||||
|
||||
Always reply to the user in the same language they are using.
|
||||
|
||||
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
|
||||
|
||||
Before proceeding with any code edits, check whether the user's request has already been implemented. If it has, inform the user without making any changes.
|
||||
|
||||
If the user's input is unclear, ambiguous, or purely informational:
|
||||
|
||||
Provide explanations, guidance, or suggestions without modifying the code.
|
||||
If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
|
||||
Respond using regular markdown formatting, including for code.
|
||||
Proceed with code edits only if the user explicitly requests changes or new features that have not already been implemented. Only edit files that are related to the user's request and leave all other files alone. Look for clear indicators like "add," "change," "update," "remove," or other action words related to modifying the code. A user asking a question doesn't necessarily mean they want you to write code.
|
||||
|
||||
If the requested change already exists, you must NOT proceed with any code changes. Instead, respond explaining that the code already includes the requested feature or fix.
|
||||
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
|
||||
|
||||
- Briefly explain the needed changes in a few short sentences, without being too technical.
|
||||
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
|
||||
- Use <dyad-rename> for renaming files.
|
||||
- Use <dyad-delete> for removing files.
|
||||
- Use <dyad-add-dependency> for installing packages.
|
||||
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
|
||||
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
|
||||
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
|
||||
|
||||
Before sending your final answer, review every import statement you output and do the following:
|
||||
|
||||
First-party imports (modules that live in this project)
|
||||
- Only import files/modules that have already been described to you.
|
||||
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
|
||||
|
||||
Third-party imports (anything that would come from npm)
|
||||
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
|
||||
|
||||
Do not leave any import unresolved.
|
||||
|
||||
Important Notes:
|
||||
- If the requested feature or change has already been implemented, only inform the user and do not modify the code.
|
||||
- Use regular markdown formatting for explanations when no code changes are needed. Only use <dyad-write>, <dyad-rename>, <dyad-delete>, and <dyad-add-dependency>.
|
||||
|
||||
# Examples
|
||||
|
||||
## Example 1: Adding a new component
|
||||
|
||||
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
|
||||
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
|
||||
|
||||
const variantClasses = {
|
||||
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
||||
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
|
||||
danger: "bg-red-600 hover:bg-red-700 text-white"
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
</dyad-write>
|
||||
|
||||
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import Button from './components/Button';
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">My Application</h1>
|
||||
|
||||
<div className="space-x-2">
|
||||
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
|
||||
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
|
||||
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
|
||||
</div>
|
||||
|
||||
{/* ... keep existing code (rest of the component) */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
</dyad-write>
|
||||
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
|
||||
|
||||
## Example 2: Installing a package and creating a Toast notification system
|
||||
|
||||
### User prompt
|
||||
|
||||
I want to add a toast
|
||||
### System response
|
||||
|
||||
OK first, you need to install the package.
|
||||
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
|
||||
|
||||
### User prompt
|
||||
|
||||
OK, I've installed the package. Now, I want to create a ToastProvider component.
|
||||
|
||||
### System response
|
||||
|
||||
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
const ToastProvider = () => { <Toaster/> };
|
||||
export default ToastProvider;
|
||||
</dyad-write>
|
||||
|
||||
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import ToastProvider from './components/ToastProvider';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<ToastProvider />
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
</dyad-write>
|
||||
|
||||
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
|
||||
"use client";
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
export const showSuccess = (message) => {
|
||||
toast.success(message);
|
||||
};
|
||||
export const showError = (message) => {
|
||||
toast.error(message);
|
||||
};
|
||||
export const showLoading = (message) => {
|
||||
return toast.loading(message);
|
||||
};
|
||||
export const dismissToast = (toastId) => {
|
||||
toast.dismiss(toastId);
|
||||
};
|
||||
</dyad-write>
|
||||
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
|
||||
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
|
||||
|
||||
## Example 3: Renaming and deleting files
|
||||
|
||||
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
|
||||
|
||||
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
const ProfileCard = ({ user }) => {
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<div className="flex items-center">
|
||||
{user.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.name}
|
||||
className="w-12 h-12 rounded-full mr-4"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
|
||||
<User className="text-gray-500" size={24} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-medium text-lg">{user.name}</h3>
|
||||
<p className="text-gray-500">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ... keep existing code (user details section) */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileCard;
|
||||
</dyad-write>
|
||||
|
||||
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
|
||||
|
||||
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import ProfileCard from '../components/ProfileCard';
|
||||
|
||||
const Dashboard = () => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||
|
||||
<ProfileCard user={currentUser} />
|
||||
|
||||
{/* ... keep existing code (rest of dashboard content) */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
</dyad-write>
|
||||
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
|
||||
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
|
||||
|
||||
# Additional Guidelines
|
||||
|
||||
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like:
|
||||
|
||||
letting the user know that they should implement some components
|
||||
partially implement features
|
||||
refer to non-existing files. All imports MUST exist in the codebase.
|
||||
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
|
||||
|
||||
Immediate Component Creation
|
||||
You MUST create a new file for every new component or hook, no matter how small.
|
||||
Never add new components to existing files, even if they seem related.
|
||||
Aim for components that are 100 lines of code or less.
|
||||
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
|
||||
|
||||
Important Rules for dyad-write operations:
|
||||
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
|
||||
- Always specify the correct file path when using dyad-write.
|
||||
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
|
||||
- Make sure to close all tags when writing files, with a line break before the closing tag.
|
||||
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
|
||||
- Prioritize creating small, focused files and components.
|
||||
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
|
||||
|
||||
Coding guidelines
|
||||
- ALWAYS generate responsive designs.
|
||||
- Use toasts components to inform the user about important events.
|
||||
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
|
||||
|
||||
Do not hesitate to extensively use console logs to follow the flow of the code. This will be very helpful when debugging.
|
||||
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
|
||||
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
|
||||
|
||||
|
||||
# Thinking Process
|
||||
|
||||
Before responding to user requests, ALWAYS use <think></think> tags to carefully plan your approach. This structured thinking process helps you organize your thoughts and ensure you provide the most accurate and helpful response. Your thinking should:
|
||||
|
||||
- Use **bullet points** to break down the steps
|
||||
- **Bold key insights** and important considerations
|
||||
- Follow a clear analytical framework
|
||||
|
||||
Example of proper thinking structure for a debugging request:
|
||||
|
||||
<think>
|
||||
• **Identify the specific UI/FE bug described by the user**
|
||||
- "Form submission button doesn't work when clicked"
|
||||
- User reports clicking the button has no effect
|
||||
- This appears to be a **functional issue**, not just styling
|
||||
|
||||
• **Examine relevant components in the codebase**
|
||||
- Form component at `src/components/ContactForm.jsx`
|
||||
- Button component at `src/components/Button.jsx`
|
||||
- Form submission logic in `src/utils/formHandlers.js`
|
||||
- **Key observation**: onClick handler in Button component doesn't appear to be triggered
|
||||
|
||||
• **Diagnose potential causes**
|
||||
- Event handler might not be properly attached to the button
|
||||
- **State management issue**: form validation state might be blocking submission
|
||||
- Button could be disabled by a condition we're missing
|
||||
- Event propagation might be stopped elsewhere
|
||||
- Possible React synthetic event issues
|
||||
|
||||
• **Plan debugging approach**
|
||||
- Add console.logs to track execution flow
|
||||
- **Fix #1**: Ensure onClick prop is properly passed through Button component
|
||||
- **Fix #2**: Check form validation state before submission
|
||||
- **Fix #3**: Verify event handler is properly bound in the component
|
||||
- Add error handling to catch and display submission issues
|
||||
|
||||
• **Consider improvements beyond the fix**
|
||||
- Add visual feedback when button is clicked (loading state)
|
||||
- Implement better error handling for form submissions
|
||||
- Add logging to help debug edge cases
|
||||
</think>
|
||||
|
||||
After completing your thinking process, proceed with your response following the guidelines above. Remember to be concise in your explanations to the user while being thorough in your thinking process.
|
||||
|
||||
This structured thinking ensures you:
|
||||
1. Don't miss important aspects of the request
|
||||
2. Consider all relevant factors before making changes
|
||||
3. Deliver more accurate and helpful responses
|
||||
4. Maintain a consistent approach to problem-solving
|
||||
|
||||
|
||||
# Tech Stack
|
||||
|
||||
- You are building a React application.
|
||||
- Use TypeScript.
|
||||
- Use React Router. KEEP the routes in src/App.tsx
|
||||
- Always put source code in the src folder.
|
||||
- Put pages into src/pages/
|
||||
- Put components into src/components/
|
||||
- The main page (default page) is src/pages/Index.tsx
|
||||
- UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components!
|
||||
- ALWAYS try to use the shadcn/ui library.
|
||||
- Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects.
|
||||
|
||||
Available packages and libraries:
|
||||
|
||||
- The lucide-react package is installed for icons.
|
||||
- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again.
|
||||
- You have ALL the necessary Radix UI components installed.
|
||||
- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them.
|
||||
|
||||
|
||||
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
|
||||
|
||||
# REMEMBER
|
||||
|
||||
> **CODE FORMATTING IS NON-NEGOTIABLE:**
|
||||
> **NEVER, EVER** use markdown code blocks (```) for code.
|
||||
> **ONLY** use <dyad-write> tags for **ALL** code output.
|
||||
> Using ``` for code is **PROHIBITED**.
|
||||
> Using <dyad-write> for code is **MANDATORY**.
|
||||
> Any instance of code within ``` is a **CRITICAL FAILURE**.
|
||||
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
|
||||
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
|
||||
|
||||
|
||||
|
||||
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
|
||||
tell them that they need to add supabase to their app.
|
||||
|
||||
The following response will show a button that allows the user to add supabase to their app.
|
||||
|
||||
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||
|
||||
# Examples
|
||||
|
||||
## Example 1: User wants to use Supabase
|
||||
|
||||
### User prompt
|
||||
|
||||
I want to use supabase in my app.
|
||||
|
||||
### Assistant response
|
||||
|
||||
You need to first add Supabase to your app.
|
||||
|
||||
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||
|
||||
## Example 2: User wants to add auth to their app
|
||||
|
||||
### User prompt
|
||||
|
||||
I want to add auth to my app.
|
||||
|
||||
### Assistant response
|
||||
|
||||
You need to first add Supabase to your app and then we can add auth.
|
||||
|
||||
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||
|
||||
|
||||
===
|
||||
role: user
|
||||
message: This is my codebase. <dyad-file path="src/pages/Index.tsx">
|
||||
// Update this page (the content is just a fallback if you fail to update the page)
|
||||
|
||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Start building your amazing project here!
|
||||
</p>
|
||||
</div>
|
||||
<MadeWithDyad />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
||||
</dyad-file>
|
||||
|
||||
|
||||
|
||||
===
|
||||
role: assistant
|
||||
message: OK, got it. I'm ready to help
|
||||
|
||||
===
|
||||
role: user
|
||||
message: tc=basic
|
||||
|
||||
===
|
||||
role: assistant
|
||||
message: This is a simple basic response
|
||||
|
||||
===
|
||||
role: user
|
||||
message: [dump] make it smaller
|
||||
|
||||
Selected component: h1 (file: src/pages/Index.tsx)
|
||||
|
||||
Snippet:
|
||||
```
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1> // <-- EDIT HERE
|
||||
<p className="text-xl text-gray-600">
|
||||
Start building your amazing project here!
|
||||
</p>
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
- img
|
||||
- text: h1 src/pages/Index.tsx:9
|
||||
- button "Deselect component":
|
||||
- img
|
||||
@@ -0,0 +1,3 @@
|
||||
===
|
||||
role: user
|
||||
message: [dump] tc=basic
|
||||
@@ -0,0 +1,7 @@
|
||||
- region "Notifications (F8)":
|
||||
- list
|
||||
- region "Notifications alt+T"
|
||||
- heading "Welcome to Your Blank App" [level=1]
|
||||
- paragraph: Start building your amazing project here!
|
||||
- link "Made with Dyad":
|
||||
- /url: https://www.dyad.sh/
|
||||
@@ -0,0 +1,12 @@
|
||||
- main:
|
||||
- heading "Blank page" [level=1]
|
||||
- link "Made with Dyad":
|
||||
- /url: https://www.dyad.sh/
|
||||
- status:
|
||||
- img
|
||||
- text: Static route
|
||||
- button "Hide static indicator":
|
||||
- img
|
||||
- alert
|
||||
- img
|
||||
- text: Edit with AI h1 src/app/page.tsx
|
||||
@@ -0,0 +1,505 @@
|
||||
===
|
||||
role: system
|
||||
message:
|
||||
<role> You are Dyad, an AI editor that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
|
||||
Not every interaction requires code changes - you're happy to discuss, explain concepts, or provide guidance without modifying the codebase. When code changes are needed, you make efficient and effective updates to codebases while following best practices for maintainability and readability. You take pride in keeping things simple and elegant. You are friendly and helpful, always aiming to provide clear explanations. </role>
|
||||
|
||||
# App Preview / Commands
|
||||
|
||||
Do *not* tell the user to run shell commands. Instead, they can do one of the following commands in the UI:
|
||||
|
||||
- **Rebuild**: This will rebuild the app from scratch. First it deletes the node_modules folder and then it re-installs the npm packages and then starts the app server.
|
||||
- **Restart**: This will restart the app server.
|
||||
- **Refresh**: This will refresh the app preview page.
|
||||
|
||||
You can suggest one of these commands by using the <dyad-command> tag like this:
|
||||
<dyad-command type="rebuild"></dyad-command>
|
||||
<dyad-command type="restart"></dyad-command>
|
||||
<dyad-command type="refresh"></dyad-command>
|
||||
|
||||
If you output one of these commands, tell the user to look for the action button above the chat input.
|
||||
|
||||
# Guidelines
|
||||
|
||||
Always reply to the user in the same language they are using.
|
||||
|
||||
- Use <dyad-chat-summary> for setting the chat summary (put this at the end). The chat summary should be less than a sentence, but more than a few words. YOU SHOULD ALWAYS INCLUDE EXACTLY ONE CHAT TITLE
|
||||
|
||||
Before proceeding with any code edits, check whether the user's request has already been implemented. If it has, inform the user without making any changes.
|
||||
|
||||
If the user's input is unclear, ambiguous, or purely informational:
|
||||
|
||||
Provide explanations, guidance, or suggestions without modifying the code.
|
||||
If the requested change has already been made in the codebase, point this out to the user, e.g., "This feature is already implemented as described."
|
||||
Respond using regular markdown formatting, including for code.
|
||||
Proceed with code edits only if the user explicitly requests changes or new features that have not already been implemented. Only edit files that are related to the user's request and leave all other files alone. Look for clear indicators like "add," "change," "update," "remove," or other action words related to modifying the code. A user asking a question doesn't necessarily mean they want you to write code.
|
||||
|
||||
If the requested change already exists, you must NOT proceed with any code changes. Instead, respond explaining that the code already includes the requested feature or fix.
|
||||
If new code needs to be written (i.e., the requested feature does not exist), you MUST:
|
||||
|
||||
- Briefly explain the needed changes in a few short sentences, without being too technical.
|
||||
- Use <dyad-write> for creating or updating files. Try to create small, focused files that will be easy to maintain. Use only one <dyad-write> block per file. Do not forget to close the dyad-write tag after writing the file. If you do NOT need to change a file, then do not use the <dyad-write> tag.
|
||||
- Use <dyad-rename> for renaming files.
|
||||
- Use <dyad-delete> for removing files.
|
||||
- Use <dyad-add-dependency> for installing packages.
|
||||
- If the user asks for multiple packages, use <dyad-add-dependency packages="package1 package2 package3"></dyad-add-dependency>
|
||||
- MAKE SURE YOU USE SPACES BETWEEN PACKAGES AND NOT COMMAS.
|
||||
- After all of the code changes, provide a VERY CONCISE, non-technical summary of the changes made in one sentence, nothing more. This summary should be easy for non-technical users to understand. If an action, like setting a env variable is required by user, make sure to include it in the summary.
|
||||
|
||||
Before sending your final answer, review every import statement you output and do the following:
|
||||
|
||||
First-party imports (modules that live in this project)
|
||||
- Only import files/modules that have already been described to you.
|
||||
- If you need a project file that does not yet exist, create it immediately with <dyad-write> before finishing your response.
|
||||
|
||||
Third-party imports (anything that would come from npm)
|
||||
- If the package is not listed in package.json, install it with <dyad-add-dependency>.
|
||||
|
||||
Do not leave any import unresolved.
|
||||
|
||||
Important Notes:
|
||||
- If the requested feature or change has already been implemented, only inform the user and do not modify the code.
|
||||
- Use regular markdown formatting for explanations when no code changes are needed. Only use <dyad-write>, <dyad-rename>, <dyad-delete>, and <dyad-add-dependency>.
|
||||
|
||||
# Examples
|
||||
|
||||
## Example 1: Adding a new component
|
||||
|
||||
<dyad-write path="src/components/Button.jsx" description="Creating a new Button component with Tailwind styling">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const Button = ({ children, variant = 'primary', onClick, disabled = false }) => {
|
||||
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
|
||||
|
||||
const variantClasses = {
|
||||
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
||||
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
|
||||
danger: "bg-red-600 hover:bg-red-700 text-white"
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
</dyad-write>
|
||||
|
||||
<dyad-write path="src/App.jsx" description="Updating the App.jsx file to use the new Button component.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import Button from './components/Button';
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">My Application</h1>
|
||||
|
||||
<div className="space-x-2">
|
||||
<Button onClick={() => console.log('Primary clicked')}>Primary Button</Button>
|
||||
<Button variant="secondary" onClick={() => console.log('Secondary clicked')}>Secondary Button</Button>
|
||||
<Button variant="danger" onClick={() => console.log('Danger clicked')}>Danger Button</Button>
|
||||
</div>
|
||||
|
||||
{/* ... keep existing code (rest of the component) */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
</dyad-write>
|
||||
<dyad-chat-summary>Adding a new component</dyad-chat-summary>
|
||||
|
||||
## Example 2: Installing a package and creating a Toast notification system
|
||||
|
||||
### User prompt
|
||||
|
||||
I want to add a toast
|
||||
### System response
|
||||
|
||||
OK first, you need to install the package.
|
||||
<dyad-add-dependency packages="react-hot-toast another-react-package"></dyad-add-dependency>
|
||||
|
||||
### User prompt
|
||||
|
||||
OK, I've installed the package. Now, I want to create a ToastProvider component.
|
||||
|
||||
### System response
|
||||
|
||||
<dyad-write path="src/components/ToastProvider.jsx" description="Creating a ToastProvider component.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
const ToastProvider = () => { <Toaster/> };
|
||||
export default ToastProvider;
|
||||
</dyad-write>
|
||||
|
||||
<dyad-write path="src/main.jsx" description="Updating the main file to include the ToastProvider.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import ToastProvider from './components/ToastProvider';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<ToastProvider />
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
</dyad-write>
|
||||
|
||||
<dyad-write path="src/utils/toast.js" description="Creating a utility file to make using toasts easier.">
|
||||
"use client";
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
export const showSuccess = (message) => {
|
||||
toast.success(message);
|
||||
};
|
||||
export const showError = (message) => {
|
||||
toast.error(message);
|
||||
};
|
||||
export const showLoading = (message) => {
|
||||
return toast.loading(message);
|
||||
};
|
||||
export const dismissToast = (toastId) => {
|
||||
toast.dismiss(toastId);
|
||||
};
|
||||
</dyad-write>
|
||||
<dyad-chat-summary>Installing & using toast</dyad-chat-summary>
|
||||
I've added a toast notification system using react-hot-toast, created a ToastProvider component, and added utility functions to make showing notifications easy throughout the app.
|
||||
|
||||
## Example 3: Renaming and deleting files
|
||||
|
||||
<dyad-rename from="src/components/UserProfile.jsx" to="src/components/ProfileCard.jsx"></dyad-rename>
|
||||
|
||||
<dyad-write path="src/components/ProfileCard.jsx" description="Updating the ProfileCard component with better styling.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
const ProfileCard = ({ user }) => {
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<div className="flex items-center">
|
||||
{user.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.name}
|
||||
className="w-12 h-12 rounded-full mr-4"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center mr-4">
|
||||
<User className="text-gray-500" size={24} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-medium text-lg">{user.name}</h3>
|
||||
<p className="text-gray-500">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ... keep existing code (user details section) */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileCard;
|
||||
</dyad-write>
|
||||
|
||||
<dyad-delete path="src/components/Analytics.jsx"></dyad-delete>
|
||||
|
||||
<dyad-write path="src/pages/Dashboard.jsx" description="Updating any imports in files that were using these components.">
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import ProfileCard from '../components/ProfileCard';
|
||||
|
||||
const Dashboard = () => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||
|
||||
<ProfileCard user={currentUser} />
|
||||
|
||||
{/* ... keep existing code (rest of dashboard content) */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
</dyad-write>
|
||||
<dyad-chat-summary>Renaming profile file</dyad-chat-summary>
|
||||
I've renamed the UserProfile component to ProfileCard, updated its styling, removed an unused Analytics component, and updated imports in the Dashboard page.
|
||||
|
||||
# Additional Guidelines
|
||||
|
||||
All edits you make on the codebase will directly be built and rendered, therefore you should NEVER make partial changes like:
|
||||
|
||||
letting the user know that they should implement some components
|
||||
partially implement features
|
||||
refer to non-existing files. All imports MUST exist in the codebase.
|
||||
If a user asks for many features at once, you do not have to implement them all as long as the ones you implement are FULLY FUNCTIONAL and you clearly communicate to the user that you didn't implement some specific features.
|
||||
|
||||
Immediate Component Creation
|
||||
You MUST create a new file for every new component or hook, no matter how small.
|
||||
Never add new components to existing files, even if they seem related.
|
||||
Aim for components that are 100 lines of code or less.
|
||||
Continuously be ready to refactor files that are getting too large. When they get too large, ask the user if they want you to refactor them.
|
||||
|
||||
Important Rules for dyad-write operations:
|
||||
- Only make changes that were directly requested by the user. Everything else in the files must stay exactly as it was.
|
||||
- Always specify the correct file path when using dyad-write.
|
||||
- Ensure that the code you write is complete, syntactically correct, and follows the existing coding style and conventions of the project.
|
||||
- Make sure to close all tags when writing files, with a line break before the closing tag.
|
||||
- IMPORTANT: Only use ONE <dyad-write> block per file that you write!
|
||||
- Prioritize creating small, focused files and components.
|
||||
- do NOT be lazy and ALWAYS write the entire file. It needs to be a complete file.
|
||||
|
||||
Coding guidelines
|
||||
- ALWAYS generate responsive designs.
|
||||
- Use toasts components to inform the user about important events.
|
||||
- Don't catch errors with try/catch blocks unless specifically requested by the user. It's important that errors are thrown since then they bubble back to you so that you can fix them.
|
||||
|
||||
Do not hesitate to extensively use console logs to follow the flow of the code. This will be very helpful when debugging.
|
||||
DO NOT OVERENGINEER THE CODE. You take great pride in keeping things simple and elegant. You don't start by writing very complex error handling, fallback mechanisms, etc. You focus on the user's request and make the minimum amount of changes needed.
|
||||
DON'T DO MORE THAN WHAT THE USER ASKS FOR.
|
||||
|
||||
|
||||
# Thinking Process
|
||||
|
||||
Before responding to user requests, ALWAYS use <think></think> tags to carefully plan your approach. This structured thinking process helps you organize your thoughts and ensure you provide the most accurate and helpful response. Your thinking should:
|
||||
|
||||
- Use **bullet points** to break down the steps
|
||||
- **Bold key insights** and important considerations
|
||||
- Follow a clear analytical framework
|
||||
|
||||
Example of proper thinking structure for a debugging request:
|
||||
|
||||
<think>
|
||||
• **Identify the specific UI/FE bug described by the user**
|
||||
- "Form submission button doesn't work when clicked"
|
||||
- User reports clicking the button has no effect
|
||||
- This appears to be a **functional issue**, not just styling
|
||||
|
||||
• **Examine relevant components in the codebase**
|
||||
- Form component at `src/components/ContactForm.jsx`
|
||||
- Button component at `src/components/Button.jsx`
|
||||
- Form submission logic in `src/utils/formHandlers.js`
|
||||
- **Key observation**: onClick handler in Button component doesn't appear to be triggered
|
||||
|
||||
• **Diagnose potential causes**
|
||||
- Event handler might not be properly attached to the button
|
||||
- **State management issue**: form validation state might be blocking submission
|
||||
- Button could be disabled by a condition we're missing
|
||||
- Event propagation might be stopped elsewhere
|
||||
- Possible React synthetic event issues
|
||||
|
||||
• **Plan debugging approach**
|
||||
- Add console.logs to track execution flow
|
||||
- **Fix #1**: Ensure onClick prop is properly passed through Button component
|
||||
- **Fix #2**: Check form validation state before submission
|
||||
- **Fix #3**: Verify event handler is properly bound in the component
|
||||
- Add error handling to catch and display submission issues
|
||||
|
||||
• **Consider improvements beyond the fix**
|
||||
- Add visual feedback when button is clicked (loading state)
|
||||
- Implement better error handling for form submissions
|
||||
- Add logging to help debug edge cases
|
||||
</think>
|
||||
|
||||
After completing your thinking process, proceed with your response following the guidelines above. Remember to be concise in your explanations to the user while being thorough in your thinking process.
|
||||
|
||||
This structured thinking ensures you:
|
||||
1. Don't miss important aspects of the request
|
||||
2. Consider all relevant factors before making changes
|
||||
3. Deliver more accurate and helpful responses
|
||||
4. Maintain a consistent approach to problem-solving
|
||||
|
||||
|
||||
# AI Development Rules
|
||||
|
||||
This document outlines the technology stack and specific library usage guidelines for this Next.js application. Adhering to these rules will help maintain consistency, improve collaboration, and ensure the AI assistant can effectively understand and modify the codebase.
|
||||
|
||||
## Tech Stack Overview
|
||||
|
||||
The application is built using the following core technologies:
|
||||
|
||||
* **Framework**: Next.js (App Router)
|
||||
* **Language**: TypeScript
|
||||
* **UI Components**: Shadcn/UI - A collection of re-usable UI components built with Radix UI and Tailwind CSS.
|
||||
* **Styling**: Tailwind CSS - A utility-first CSS framework for rapid UI development.
|
||||
* **Icons**: Lucide React - A comprehensive library of simply beautiful SVG icons.
|
||||
* **Forms**: React Hook Form for managing form state and validation, typically with Zod for schema validation.
|
||||
* **State Management**: Primarily React Context API and built-in React hooks (`useState`, `useReducer`).
|
||||
* **Notifications/Toasts**: Sonner for displaying non-intrusive notifications.
|
||||
* **Charts**: Recharts for data visualization.
|
||||
* **Animation**: `tailwindcss-animate` and animation capabilities built into Radix UI components.
|
||||
|
||||
## Library Usage Guidelines
|
||||
|
||||
To ensure consistency and leverage the chosen stack effectively, please follow these rules:
|
||||
|
||||
1. **UI Components**:
|
||||
* **Primary Choice**: Always prioritize using components from the `src/components/ui/` directory (Shadcn/UI components).
|
||||
* **Custom Components**: If a required component is not available in Shadcn/UI, create a new component in `src/components/` following Shadcn/UI's composition patterns (i.e., building on Radix UI primitives and styled with Tailwind CSS).
|
||||
* **Avoid**: Introducing new, third-party UI component libraries without discussion.
|
||||
|
||||
2. **Styling**:
|
||||
* **Primary Choice**: Exclusively use Tailwind CSS utility classes for all styling.
|
||||
* **Global Styles**: Reserve `src/app/globals.css` for base Tailwind directives, global CSS variable definitions, and minimal base styling. Avoid adding component-specific styles here.
|
||||
* **CSS-in-JS**: Do not use CSS-in-JS libraries (e.g., Styled Components, Emotion).
|
||||
|
||||
3. **Icons**:
|
||||
* **Primary Choice**: Use icons from the `lucide-react` library.
|
||||
|
||||
4. **Forms**:
|
||||
* **Management**: Use `react-hook-form` for all form logic (state, validation, submission).
|
||||
* **Validation**: Use `zod` for schema-based validation with `react-hook-form` via `@hookform/resolvers`.
|
||||
|
||||
5. **State Management**:
|
||||
* **Local State**: Use React's `useState` and `useReducer` hooks for component-level state.
|
||||
* **Shared/Global State**: For state shared between multiple components, prefer React Context API.
|
||||
* **Complex Global State**: If application state becomes significantly complex, discuss the potential introduction of a dedicated state management library (e.g., Zustand, Jotai) before implementing.
|
||||
|
||||
6. **Routing**:
|
||||
* Utilize the Next.js App Router (file-system based routing in the `app/` directory).
|
||||
|
||||
7. **API Calls & Data Fetching**:
|
||||
* **Client-Side**: Use the native `fetch` API or a simple wrapper around it.
|
||||
* **Server-Side (Next.js)**: Leverage Next.js Route Handlers (in `app/api/`) or Server Actions for server-side logic and data fetching.
|
||||
|
||||
8. **Animations**:
|
||||
* Use `tailwindcss-animate` plugin and the animation utilities provided by Radix UI components.
|
||||
|
||||
9. **Notifications/Toasts**:
|
||||
* Use the `Sonner` component (from `src/components/ui/sonner.tsx`) for all toast notifications.
|
||||
|
||||
10. **Charts & Data Visualization**:
|
||||
* Use `recharts` and its associated components (e.g., `src/components/ui/chart.tsx`) for displaying charts.
|
||||
|
||||
11. **Utility Functions**:
|
||||
* General-purpose helper functions should be placed in `src/lib/utils.ts`.
|
||||
* Ensure functions are well-typed and serve a clear, reusable purpose.
|
||||
|
||||
12. **Custom Hooks**:
|
||||
* Custom React hooks should be placed in the `src/hooks/` directory (e.g., `src/hooks/use-mobile.tsx`).
|
||||
|
||||
13. **TypeScript**:
|
||||
* Write all new code in TypeScript.
|
||||
* Strive for strong typing and leverage TypeScript's features to improve code quality and maintainability. Avoid using `any` where possible.
|
||||
|
||||
By following these guidelines, we can build a more robust, maintainable, and consistent application.
|
||||
|
||||
Directory names MUST be all lower-case (src/pages, src/components, etc.). File names may use mixed-case if you like.
|
||||
|
||||
# REMEMBER
|
||||
|
||||
> **CODE FORMATTING IS NON-NEGOTIABLE:**
|
||||
> **NEVER, EVER** use markdown code blocks (```) for code.
|
||||
> **ONLY** use <dyad-write> tags for **ALL** code output.
|
||||
> Using ``` for code is **PROHIBITED**.
|
||||
> Using <dyad-write> for code is **MANDATORY**.
|
||||
> Any instance of code within ``` is a **CRITICAL FAILURE**.
|
||||
> **REPEAT: NO MARKDOWN CODE BLOCKS. USE <dyad-write> EXCLUSIVELY FOR CODE.**
|
||||
> Do NOT use <dyad-file> tags in the output. ALWAYS use <dyad-write> to generate code.
|
||||
|
||||
|
||||
|
||||
If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
|
||||
tell them that they need to add supabase to their app.
|
||||
|
||||
The following response will show a button that allows the user to add supabase to their app.
|
||||
|
||||
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||
|
||||
# Examples
|
||||
|
||||
## Example 1: User wants to use Supabase
|
||||
|
||||
### User prompt
|
||||
|
||||
I want to use supabase in my app.
|
||||
|
||||
### Assistant response
|
||||
|
||||
You need to first add Supabase to your app.
|
||||
|
||||
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||
|
||||
## Example 2: User wants to add auth to their app
|
||||
|
||||
### User prompt
|
||||
|
||||
I want to add auth to my app.
|
||||
|
||||
### Assistant response
|
||||
|
||||
You need to first add Supabase to your app and then we can add auth.
|
||||
|
||||
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||
|
||||
|
||||
===
|
||||
role: user
|
||||
message: This is my codebase. <dyad-file path="src/app/page.tsx">
|
||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-1 items-center sm:items-start">
|
||||
<h1>Blank page</h1>
|
||||
</main>
|
||||
<MadeWithDyad />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
</dyad-file>
|
||||
|
||||
|
||||
|
||||
===
|
||||
role: assistant
|
||||
message: OK, got it. I'm ready to help
|
||||
|
||||
===
|
||||
role: user
|
||||
message: tc=basic
|
||||
|
||||
===
|
||||
role: assistant
|
||||
message: This is a simple basic response
|
||||
|
||||
===
|
||||
role: user
|
||||
message: [dump] make it smaller
|
||||
|
||||
Selected component: h1 (file: src/app/page.tsx)
|
||||
|
||||
Snippet:
|
||||
```
|
||||
<main className="flex flex-col gap-8 row-start-1 items-center sm:items-start">
|
||||
<h1>Blank page</h1> // <-- EDIT HERE
|
||||
</main>
|
||||
<MadeWithDyad />
|
||||
</div>
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
- img
|
||||
- text: h1 src/app/page.tsx:7
|
||||
- button "Deselect component":
|
||||
- img
|
||||
@@ -0,0 +1,10 @@
|
||||
- main:
|
||||
- heading "Blank page" [level=1]
|
||||
- link "Made with Dyad":
|
||||
- /url: https://www.dyad.sh/
|
||||
- status:
|
||||
- img
|
||||
- text: Static route
|
||||
- button "Hide static indicator":
|
||||
- img
|
||||
- alert
|
||||
@@ -0,0 +1,494 @@
|
||||
=== .gitignore ===
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
=== AI_RULES.md ===
|
||||
# AI RULES placeholder
|
||||
|
||||
|
||||
=== components.json ===
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
|
||||
|
||||
=== index.html ===
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>dyad-generated-app</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
=== package.json ===
|
||||
{
|
||||
"name": "vite_react_shadcn_ts",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.514.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^6.3.4"
|
||||
},
|
||||
"packageManager": "<scrubbed>"
|
||||
}
|
||||
|
||||
=== src/App.tsx ===
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Index from "./pages/Index";
|
||||
|
||||
const App = () => (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
||||
|
||||
=== src/components/ui/button.tsx ===
|
||||
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-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs 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 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
||||
|
||||
=== src/lib/utils.ts ===
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
|
||||
=== src/main.tsx ===
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./styles/globals.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
||||
|
||||
=== src/pages/Index.tsx ===
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Rocket, ShieldCheck, Sparkles } from "lucide-react";
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Rocket className="w-8 h-8 text-primary mb-2" />,
|
||||
title: "Fast & Modern",
|
||||
description: "Built with the latest tech for blazing fast performance.",
|
||||
},
|
||||
{
|
||||
icon: <ShieldCheck className="w-8 h-8 text-primary mb-2" />,
|
||||
title: "Secure by Design",
|
||||
description: "Security best practices baked in from the start.",
|
||||
},
|
||||
{
|
||||
icon: <Sparkles className="w-8 h-8 text-primary mb-2" />,
|
||||
title: "Easy to Customize",
|
||||
description: "Effortlessly adapt the template to your needs.",
|
||||
},
|
||||
];
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-background">
|
||||
{/* Hero Section */}
|
||||
<header className="flex-1 flex flex-col items-center justify-center px-4 py-16">
|
||||
<h1 className="text-5xl font-extrabold mb-4 text-foreground text-center">
|
||||
Launch Your Next Project
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 text-center max-w-xl">
|
||||
A simple, modern landing page template built with React, shadcn/ui,
|
||||
and Tailwind CSS.
|
||||
</p>
|
||||
<Button size="lg" className="px-8 py-6 text-lg">
|
||||
Get Started
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="py-12 bg-muted">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{features.map((feature, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col items-center text-center bg-card rounded-lg p-6 shadow-sm"
|
||||
>
|
||||
{feature.icon}
|
||||
<h3 className="text-lg font-semibold mb-2 text-foreground">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
||||
|
||||
=== src/styles/globals.css ===
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@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;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
=== src/vite-env.d.ts ===
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
|
||||
=== tsconfig.app.json ===
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
|
||||
=== tsconfig.json ===
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"noImplicitAny": false,
|
||||
"noUnusedParameters": false,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"noUnusedLocals": false,
|
||||
"strictNullChecks": false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
=== tsconfig.node.json ===
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
|
||||
=== vite.config.ts ===
|
||||
import { defineConfig } from "vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
server: {
|
||||
host: "::",
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [dyadComponentTagger(), react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,14 @@
|
||||
===
|
||||
role: user
|
||||
message: [dump] make it smaller
|
||||
|
||||
Selected component: h1 (file: src/pages/Index.tsx)
|
||||
|
||||
Snippet:
|
||||
```
|
||||
<header className="flex-1 flex flex-col items-center justify-center px-4 py-16">
|
||||
<h1 className="text-5xl font-extrabold mb-4 text-foreground text-center"> // <-- EDIT HERE
|
||||
Launch Your Next Project
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 text-center max-w-xl">
|
||||
```
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dyad",
|
||||
"version": "0.7.5",
|
||||
"version": "0.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dyad",
|
||||
"version": "0.7.5",
|
||||
"version": "0.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.8",
|
||||
|
||||
202
packages/@dyad-sh/nextjs-webpack-component-tagger/LICENSE
Normal file
202
packages/@dyad-sh/nextjs-webpack-component-tagger/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 Dyad Tech, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
65
packages/@dyad-sh/nextjs-webpack-component-tagger/README.md
Normal file
65
packages/@dyad-sh/nextjs-webpack-component-tagger/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# @dyad-sh/nextjs-webpack-component-tagger
|
||||
|
||||
A webpack loader for Next.js that automatically adds `data-dyad-id` and `data-dyad-name` attributes to your React components. This is useful for identifying components in the DOM, for example for testing or analytics.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @dyad-sh/nextjs-webpack-component-tagger
|
||||
# or
|
||||
yarn add @dyad-sh/nextjs-webpack-component-tagger
|
||||
# or
|
||||
pnpm add @dyad-sh/nextjs-webpack-component-tagger
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add the loader to your `next.config.js` file:
|
||||
|
||||
```ts
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
webpack: (config) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
config.module.rules.push({
|
||||
test: /\.(jsx|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
enforce: "pre",
|
||||
use: "@dyad-sh/nextjs-webpack-component-tagger",
|
||||
});
|
||||
}
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
```
|
||||
|
||||
The loader will automatically add `data-dyad-id` and `data-dyad-name` to all your React components.
|
||||
|
||||
The `data-dyad-id` will be a unique identifier for each component instance, in the format `path/to/file.tsx:line:column`.
|
||||
|
||||
The `data-dyad-name` will be the name of the component.
|
||||
|
||||
## Testing & Publishing
|
||||
|
||||
Bump it to an alpha version and test in Dyad app, eg. `"version": "0.0.1-alpha.0",`
|
||||
|
||||
Then publish it:
|
||||
|
||||
```sh
|
||||
cd packages/@dyad-sh/nextjs-webpack-component-tagger/ && npm run prepublishOnly && npm publish
|
||||
```
|
||||
|
||||
Update the package version in the nextjs-template repo in your personal fork.
|
||||
|
||||
Update the `src/shared/templates.ts` to use your fork of the next.js template, e.g.
|
||||
|
||||
```
|
||||
githubUrl: "https://github.com/wwwillchen/nextjs-template",
|
||||
```
|
||||
|
||||
Run the E2E tests and make sure it passes.
|
||||
|
||||
Then, bump to a normal version, e.g. "0.1.0" and then re-publish. We'll try to match the main Dyad app version where possible.
|
||||
3858
packages/@dyad-sh/nextjs-webpack-component-tagger/package-lock.json
generated
Normal file
3858
packages/@dyad-sh/nextjs-webpack-component-tagger/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@dyad-sh/nextjs-webpack-component-tagger",
|
||||
"version": "0.8.0",
|
||||
"description": "A webpack loader that automatically adds data attributes to your React components in Next.js.",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"dev": "npm run build -- --watch",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"webpack",
|
||||
"webpack-loader",
|
||||
"nextjs",
|
||||
"react",
|
||||
"dyad"
|
||||
],
|
||||
"author": "Dyad",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.8.9",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^8.52.0",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
109
packages/@dyad-sh/nextjs-webpack-component-tagger/src/index.ts
Normal file
109
packages/@dyad-sh/nextjs-webpack-component-tagger/src/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { parse } from "@babel/parser";
|
||||
import MagicString from "magic-string";
|
||||
import path from "node:path";
|
||||
import { walk } from "estree-walker";
|
||||
|
||||
const VALID_EXTENSIONS = new Set([".jsx", ".tsx"]);
|
||||
|
||||
/**
|
||||
* A webpack loader that adds `data-dyad-*` attributes to JSX elements.
|
||||
*/
|
||||
export default function dyadTaggerLoader(this: any, code: string) {
|
||||
// Signal that this is an async loader
|
||||
const callback = this.async();
|
||||
|
||||
const transform = async () => {
|
||||
try {
|
||||
// Skip non-JSX files and node_modules
|
||||
if (
|
||||
!VALID_EXTENSIONS.has(path.extname(this.resourcePath)) ||
|
||||
this.resourcePath.includes("node_modules")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse the AST
|
||||
const ast = parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["jsx", "typescript"],
|
||||
sourceFilename: this.resourcePath,
|
||||
});
|
||||
|
||||
const ms = new MagicString(code);
|
||||
const fileRelative = path.relative(this.rootContext, this.resourcePath);
|
||||
let transformCount = 0;
|
||||
|
||||
// Walk the AST and transform JSX elements
|
||||
walk(ast as any, {
|
||||
enter: (node: any) => {
|
||||
try {
|
||||
if (node.type !== "JSXOpeningElement") return;
|
||||
|
||||
// Extract the tag/component name
|
||||
if (node.name?.type !== "JSXIdentifier") return;
|
||||
const tagName = node.name.name;
|
||||
if (!tagName) return;
|
||||
|
||||
// Skip if already tagged
|
||||
const alreadyTagged = node.attributes?.some(
|
||||
(attr: any) =>
|
||||
attr.type === "JSXAttribute" &&
|
||||
attr.name?.name === "data-dyad-id",
|
||||
);
|
||||
if (alreadyTagged) return;
|
||||
|
||||
// Build the dyad ID
|
||||
const loc = node.loc?.start;
|
||||
if (!loc) return;
|
||||
const dyadId = `${fileRelative}:${loc.line}:${loc.column}`;
|
||||
|
||||
// Inject the attributes
|
||||
if (node.name.end != null) {
|
||||
ms.appendLeft(
|
||||
node.name.end,
|
||||
` data-dyad-id="${dyadId}" data-dyad-name="${tagName}"`,
|
||||
);
|
||||
transformCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[dyad-tagger] Warning: Failed to process JSX node in ${this.resourcePath}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Return null if no changes were made
|
||||
if (transformCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const transformedCode = ms.toString();
|
||||
return {
|
||||
code: transformedCode,
|
||||
map: ms.generateMap({ hires: true }),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[dyad-tagger] Warning: Failed to transform ${this.resourcePath}:`,
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
transform()
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
callback(null, result.code, result.map);
|
||||
} else {
|
||||
callback(null, code);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`[dyad-tagger] ERROR in ${this.resourcePath}:`, err);
|
||||
// Return original code instead of throwing
|
||||
callback(null, code);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020"],
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
4
packages/@dyad-sh/react-vite-component-tagger/.npmignore
Normal file
4
packages/@dyad-sh/react-vite-component-tagger/.npmignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
src
|
||||
tsconfig.json
|
||||
.DS_Store
|
||||
202
packages/@dyad-sh/react-vite-component-tagger/LICENSE
Normal file
202
packages/@dyad-sh/react-vite-component-tagger/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 Dyad Tech, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
54
packages/@dyad-sh/react-vite-component-tagger/README.md
Normal file
54
packages/@dyad-sh/react-vite-component-tagger/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# @dyad-sh/react-vite-component-tagger
|
||||
|
||||
A Vite plugin that automatically adds `data-dyad-id` and `data-dyad-name` attributes to your React components. This is useful for identifying components in the DOM, for example for testing or analytics.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @dyad-sh/react-vite-component-tagger
|
||||
# or
|
||||
yarn add @dyad-sh/react-vite-component-tagger
|
||||
# or
|
||||
pnpm add @dyad-sh/react-vite-component-tagger
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add the plugin to your `vite.config.ts` file:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import dyadTagger from "@dyad-sh/react-vite-component-tagger";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), dyadTagger()],
|
||||
});
|
||||
```
|
||||
|
||||
The plugin will automatically add `data-dyad-id` and `data-dyad-name` to all your React components.
|
||||
|
||||
The `data-dyad-id` will be a unique identifier for each component instance, in the format `path/to/file.tsx:line:column`.
|
||||
|
||||
The `data-dyad-name` will be the name of the component.
|
||||
|
||||
## Testing & Publishing
|
||||
|
||||
Bump it to an alpha version and test in Dyad app, eg. `"version": "0.0.1-alpha.0",`
|
||||
|
||||
Then publish it:
|
||||
|
||||
```sh
|
||||
cd packages/@dyad-sh/react-vite-component-tagger/ && npm run prepublishOnly && npm publish
|
||||
```
|
||||
|
||||
Update the scaffold like this:
|
||||
|
||||
```sh
|
||||
cd scaffold && pnpm remove @dyad-sh/react-vite-component-tagger && pnpm add -D @dyad-sh/react-vite-component-tagger
|
||||
```
|
||||
|
||||
Run the E2E tests and make sure it passes.
|
||||
|
||||
Then, bump to a normal version, e.g. "0.1.0" and then re-publish. We'll try to match the main Dyad app version where possible.
|
||||
3646
packages/@dyad-sh/react-vite-component-tagger/package-lock.json
generated
Normal file
3646
packages/@dyad-sh/react-vite-component-tagger/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
packages/@dyad-sh/react-vite-component-tagger/package.json
Normal file
44
packages/@dyad-sh/react-vite-component-tagger/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@dyad-sh/react-vite-component-tagger",
|
||||
"version": "0.8.0",
|
||||
"description": "A Vite plugin that automatically adds data attributes to your React components.",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"dev": "npm run build -- --watch",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"vite",
|
||||
"vite-plugin",
|
||||
"react",
|
||||
"dyad"
|
||||
],
|
||||
"author": "Dyad",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.8.9",
|
||||
"eslint": "^8.52.0",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
90
packages/@dyad-sh/react-vite-component-tagger/src/index.ts
Normal file
90
packages/@dyad-sh/react-vite-component-tagger/src/index.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { parse } from "@babel/parser";
|
||||
import MagicString from "magic-string";
|
||||
import path from "node:path";
|
||||
import { walk } from "estree-walker";
|
||||
import type { Plugin } from "vite";
|
||||
|
||||
const VALID_EXTENSIONS = new Set([".jsx", ".tsx"]);
|
||||
|
||||
/**
|
||||
* Returns a Vite / esbuild plug-in.
|
||||
*/
|
||||
export default function dyadTagger(): Plugin {
|
||||
return {
|
||||
name: "vite-plugin-dyad-tagger",
|
||||
apply: "serve",
|
||||
enforce: "pre",
|
||||
|
||||
async transform(code: string, id: string) {
|
||||
try {
|
||||
// Ignore non-jsx files and files inside node_modules
|
||||
if (
|
||||
!VALID_EXTENSIONS.has(path.extname(id)) ||
|
||||
id.includes("node_modules")
|
||||
)
|
||||
return null;
|
||||
|
||||
const ast = parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["jsx", "typescript"],
|
||||
});
|
||||
|
||||
const ms = new MagicString(code);
|
||||
const fileRelative = path.relative(process.cwd(), id);
|
||||
|
||||
walk(ast as any, {
|
||||
enter(node: any) {
|
||||
try {
|
||||
if (node.type !== "JSXOpeningElement") return;
|
||||
|
||||
// ── 1. Extract the tag / component name ──────────────────────────────
|
||||
if (node.name?.type !== "JSXIdentifier") return;
|
||||
const tagName = node.name.name as string;
|
||||
if (!tagName) return;
|
||||
|
||||
// ── 2. Check whether the tag already has data-dyad-id ───────────────
|
||||
const alreadyTagged = node.attributes?.some(
|
||||
(attr: any) =>
|
||||
attr.type === "JSXAttribute" &&
|
||||
attr.name?.name === "data-dyad-id",
|
||||
);
|
||||
if (alreadyTagged) return;
|
||||
|
||||
// ── 3. Build the id "relative/file.jsx:line:column" ─────────────────
|
||||
const loc = node.loc?.start;
|
||||
if (!loc) return;
|
||||
const dyadId = `${fileRelative}:${loc.line}:${loc.column}`;
|
||||
|
||||
// ── 4. Inject the attributes just after the tag name ────────────────
|
||||
if (node.name.end != null) {
|
||||
ms.appendLeft(
|
||||
node.name.end,
|
||||
` data-dyad-id="${dyadId}" data-dyad-name="${tagName}"`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[dyad-tagger] Warning: Failed to process JSX node in ${id}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// If nothing changed bail out.
|
||||
if (ms.toString() === code) return null;
|
||||
|
||||
return {
|
||||
code: ms.toString(),
|
||||
map: ms.generateMap({ hires: true }),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[dyad-tagger] Warning: Failed to transform ${id}:`,
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
16
packages/@dyad-sh/react-vite-component-tagger/tsconfig.json
Normal file
16
packages/@dyad-sh/react-vite-component-tagger/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020"],
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "dist",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -62,6 +62,7 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dyad-sh/react-vite-component-tagger": "^0.8.0",
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/node": "^22.5.5",
|
||||
|
||||
57
scaffold/pnpm-lock.yaml
generated
57
scaffold/pnpm-lock.yaml
generated
@@ -156,6 +156,9 @@ importers:
|
||||
specifier: ^3.23.8
|
||||
version: 3.24.3
|
||||
devDependencies:
|
||||
'@dyad-sh/react-vite-component-tagger':
|
||||
specifier: ^0.8.0
|
||||
version: 0.8.0(vite@6.3.4(@types/node@22.14.1)(jiti@1.21.7)(yaml@2.7.1))
|
||||
'@eslint/js':
|
||||
specifier: ^9.9.0
|
||||
version: 9.24.0
|
||||
@@ -211,10 +214,32 @@ packages:
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1':
|
||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.27.1':
|
||||
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/parser@7.27.5':
|
||||
resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/runtime@7.27.0':
|
||||
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.27.6':
|
||||
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@dyad-sh/react-vite-component-tagger@0.8.0':
|
||||
resolution: {integrity: sha512-97DrCLm4+kpy5fp90cLfuhjhLlhi5/p0nzCU9/pU4V9/aIpQCb+RQ/E3BJdzpMOcoieR81zPZTEiLTW5CYKgSQ==}
|
||||
peerDependencies:
|
||||
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.3':
|
||||
resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1677,6 +1702,9 @@ packages:
|
||||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
esutils@2.0.3:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1895,6 +1923,9 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
|
||||
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
|
||||
merge2@1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -2433,10 +2464,30 @@ snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.27.1': {}
|
||||
|
||||
'@babel/parser@7.27.5':
|
||||
dependencies:
|
||||
'@babel/types': 7.27.6
|
||||
|
||||
'@babel/runtime@7.27.0':
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/types@7.27.6':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
||||
'@dyad-sh/react-vite-component-tagger@0.8.0(vite@6.3.4(@types/node@22.14.1)(jiti@1.21.7)(yaml@2.7.1))':
|
||||
dependencies:
|
||||
'@babel/parser': 7.27.5
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.17
|
||||
vite: 6.3.4(@types/node@22.14.1)(jiti@1.21.7)(yaml@2.7.1)
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.3':
|
||||
optional: true
|
||||
|
||||
@@ -3867,6 +3918,8 @@ snapshots:
|
||||
|
||||
estraverse@5.3.0: {}
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
@@ -4049,6 +4102,10 @@ snapshots:
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
magic-string@0.30.17:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
micromatch@4.0.8:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
|
||||
@@ -7,7 +8,7 @@ export default defineConfig(() => ({
|
||||
host: "::",
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [react()],
|
||||
plugins: [dyadComponentTagger(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
||||
6
src/atoms/previewAtoms.ts
Normal file
6
src/atoms/previewAtoms.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ComponentSelection } from "@/ipc/ipc_types";
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const selectedComponentPreviewAtom = atom<ComponentSelection | null>(
|
||||
null,
|
||||
);
|
||||
152
src/components/AppUpgrades.tsx
Normal file
152
src/components/AppUpgrades.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AppUpgrade } from "@/ipc/ipc_types";
|
||||
|
||||
export function AppUpgrades({ appId }: { appId: number | null }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: upgrades,
|
||||
isLoading,
|
||||
error: queryError,
|
||||
} = useQuery({
|
||||
queryKey: ["app-upgrades", appId],
|
||||
queryFn: () => {
|
||||
if (!appId) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return IpcClient.getInstance().getAppUpgrades({ appId });
|
||||
},
|
||||
enabled: !!appId,
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: executeUpgrade,
|
||||
isPending: isUpgrading,
|
||||
error: mutationError,
|
||||
variables: upgradingVariables,
|
||||
} = useMutation({
|
||||
mutationFn: (upgradeId: string) => {
|
||||
if (!appId) {
|
||||
throw new Error("appId is not set");
|
||||
}
|
||||
return IpcClient.getInstance().executeAppUpgrade({
|
||||
appId,
|
||||
upgradeId,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["app-upgrades", appId] });
|
||||
},
|
||||
});
|
||||
|
||||
const handleUpgrade = (upgradeId: string) => {
|
||||
executeUpgrade(upgradeId);
|
||||
};
|
||||
|
||||
if (!appId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">
|
||||
App Upgrades
|
||||
</h3>
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (queryError) {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">
|
||||
App Upgrades
|
||||
</h3>
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Error loading upgrades</AlertTitle>
|
||||
<AlertDescription>{queryError.message}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const currentUpgrades = upgrades?.filter((u) => u.isNeeded) ?? [];
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">
|
||||
App Upgrades
|
||||
</h3>
|
||||
{currentUpgrades.length === 0 ? (
|
||||
<div
|
||||
data-testid="no-app-upgrades-needed"
|
||||
className="p-4 bg-green-50 border border-green-200 dark:bg-green-900/20 dark:border-green-800/50 rounded-lg text-sm text-green-800 dark:text-green-300"
|
||||
>
|
||||
App is up-to-date and has all Dyad capabilities enabled
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{currentUpgrades.map((upgrade: AppUpgrade) => (
|
||||
<div
|
||||
key={upgrade.id}
|
||||
className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg flex justify-between items-start"
|
||||
>
|
||||
<div className="flex-grow">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">
|
||||
{upgrade.title}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{upgrade.description}
|
||||
</p>
|
||||
{mutationError && upgradingVariables === upgrade.id && (
|
||||
<Alert
|
||||
variant="destructive"
|
||||
className="mt-3 dark:bg-destructive/15"
|
||||
>
|
||||
<Terminal className="h-4 w-4" />
|
||||
<AlertTitle className="dark:text-red-200">
|
||||
Upgrade Failed
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-xs text-red-400 dark:text-red-300">
|
||||
{(mutationError as Error).message}{" "}
|
||||
<a
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
IpcClient.getInstance().openExternalUrl(
|
||||
upgrade.manualUpgradeUrl ?? "https://dyad.sh/docs",
|
||||
);
|
||||
}}
|
||||
className="underline font-medium hover:dark:text-red-200"
|
||||
>
|
||||
Manual Upgrade Instructions
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleUpgrade(upgrade.id)}
|
||||
disabled={isUpgrading && upgradingVariables === upgrade.id}
|
||||
className="ml-4 flex-shrink-0"
|
||||
size="sm"
|
||||
data-testid={`app-upgrade-${upgrade.id}`}
|
||||
>
|
||||
{isUpgrading && upgradingVariables === upgrade.id ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : null}
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -191,6 +191,7 @@ export function ChatHeader({
|
||||
</div>
|
||||
|
||||
<button
|
||||
data-testid="toggle-preview-panel-button"
|
||||
onClick={onTogglePreview}
|
||||
className="cursor-pointer p-2 hover:bg-(--background-lightest) rounded-md"
|
||||
>
|
||||
|
||||
@@ -61,6 +61,9 @@ import { DragDropOverlay } from "./DragDropOverlay";
|
||||
import { showError, showExtraFilesToast } from "@/lib/toast";
|
||||
import { ChatInputControls } from "../ChatInputControls";
|
||||
import { ChatErrorBox } from "./ChatErrorBox";
|
||||
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
|
||||
import { SelectedComponentDisplay } from "./SelectedComponentDisplay";
|
||||
|
||||
const showTokenBarAtom = atom(false);
|
||||
|
||||
export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
@@ -78,6 +81,9 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
const [, setMessages] = useAtom<Message[]>(chatMessagesAtom);
|
||||
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
|
||||
const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom);
|
||||
const [selectedComponent, setSelectedComponent] = useAtom(
|
||||
selectedComponentPreviewAtom,
|
||||
);
|
||||
|
||||
// Use the attachments hook
|
||||
const {
|
||||
@@ -148,6 +154,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
|
||||
const currentInput = inputValue;
|
||||
setInputValue("");
|
||||
setSelectedComponent(null);
|
||||
|
||||
// Send message with attachments and clear them after sending
|
||||
await streamMessage({
|
||||
@@ -155,6 +162,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
chatId,
|
||||
attachments,
|
||||
redo: false,
|
||||
selectedComponent,
|
||||
});
|
||||
clearAttachments();
|
||||
posthog.capture("chat:submit");
|
||||
@@ -282,6 +290,8 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<SelectedComponentDisplay />
|
||||
|
||||
{/* Use the AttachmentsList component */}
|
||||
<AttachmentsList
|
||||
attachments={attachments}
|
||||
|
||||
@@ -20,87 +20,87 @@ const ChatMessage = ({ message, isLastMessage }: ChatMessageProps) => {
|
||||
message.role === "assistant" ? "justify-start" : "justify-end"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-lg p-2 mt-2 ${
|
||||
message.role === "assistant"
|
||||
? "w-full max-w-3xl mx-auto"
|
||||
: "bg-(--sidebar-accent)"
|
||||
}`}
|
||||
>
|
||||
{message.role === "assistant" &&
|
||||
!message.content &&
|
||||
isStreaming &&
|
||||
isLastMessage ? (
|
||||
<div className="flex h-6 items-center space-x-2 p-2">
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
delay: 0.4,
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
delay: 0.8,
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="prose dark:prose-invert prose-headings:mb-2 prose-p:my-1 prose-pre:my-0 max-w-none"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{message.role === "assistant" ? (
|
||||
<>
|
||||
<DyadMarkdownParser content={message.content} />
|
||||
{isLastMessage && isStreaming && (
|
||||
<div className="mt-4 ml-4 relative w-5 h-5 animate-spin">
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full"></div>
|
||||
<div className="absolute bottom-0 left-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-80"></div>
|
||||
<div className="absolute bottom-0 right-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-60"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<VanillaMarkdownParser content={message.content} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{message.approvalState && (
|
||||
<div className="mt-2 flex items-center justify-end space-x-1 text-xs">
|
||||
{message.approvalState === "approved" ? (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
<span>Approved</span>
|
||||
</>
|
||||
) : message.approvalState === "rejected" ? (
|
||||
<>
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
<span>Rejected</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className={`mt-2 w-full max-w-3xl mx-auto`}>
|
||||
<div
|
||||
className={`rounded-lg p-2 ${
|
||||
message.role === "assistant" ? "" : "ml-24 bg-(--sidebar-accent)"
|
||||
}`}
|
||||
>
|
||||
{message.role === "assistant" &&
|
||||
!message.content &&
|
||||
isStreaming &&
|
||||
isLastMessage ? (
|
||||
<div className="flex h-6 items-center space-x-2 p-2">
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
delay: 0.4,
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="h-3 w-3 rounded-full bg-(--primary) dark:bg-blue-500"
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration: 0.4,
|
||||
ease: "easeOut",
|
||||
delay: 0.8,
|
||||
repeatDelay: 1.2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="prose dark:prose-invert prose-headings:mb-2 prose-p:my-1 prose-pre:my-0 max-w-none"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{message.role === "assistant" ? (
|
||||
<>
|
||||
<DyadMarkdownParser content={message.content} />
|
||||
{isLastMessage && isStreaming && (
|
||||
<div className="mt-4 ml-4 relative w-5 h-5 animate-spin">
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full"></div>
|
||||
<div className="absolute bottom-0 left-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-80"></div>
|
||||
<div className="absolute bottom-0 right-0 w-2 h-2 bg-(--primary) dark:bg-blue-500 rounded-full opacity-60"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<VanillaMarkdownParser content={message.content} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{message.approvalState && (
|
||||
<div className="mt-2 flex items-center justify-end space-x-1 text-xs">
|
||||
{message.approvalState === "approved" ? (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
<span>Approved</span>
|
||||
</>
|
||||
) : message.approvalState === "rejected" ? (
|
||||
<>
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
<span>Rejected</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
47
src/components/chat/SelectedComponentDisplay.tsx
Normal file
47
src/components/chat/SelectedComponentDisplay.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
|
||||
import { useAtom } from "jotai";
|
||||
import { Code2, X } from "lucide-react";
|
||||
|
||||
export function SelectedComponentDisplay() {
|
||||
const [selectedComponent, setSelectedComponent] = useAtom(
|
||||
selectedComponentPreviewAtom,
|
||||
);
|
||||
|
||||
if (!selectedComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-2 pb-1" data-testid="selected-component-display">
|
||||
<div className="flex items-center justify-between rounded-md bg-indigo-600/10 px-2 py-1 text-sm">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<Code2
|
||||
size={16}
|
||||
className="flex-shrink-0 text-indigo-600 dark:text-indigo-400"
|
||||
/>
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<span
|
||||
className="truncate font-medium text-indigo-800 dark:text-indigo-300"
|
||||
title={selectedComponent.name}
|
||||
>
|
||||
{selectedComponent.name}
|
||||
</span>
|
||||
<span
|
||||
className="truncate text-xs text-indigo-600/80 dark:text-indigo-400/80"
|
||||
title={`${selectedComponent.relativePath}:${selectedComponent.lineNumber}`}
|
||||
>
|
||||
{selectedComponent.relativePath}:{selectedComponent.lineNumber}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedComponent(null)}
|
||||
className="ml-2 flex-shrink-0 rounded-full p-0.5 hover:bg-indigo-600/20"
|
||||
title="Deselect component"
|
||||
>
|
||||
<X size={18} className="text-indigo-600 dark:text-indigo-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ChevronDown,
|
||||
Lightbulb,
|
||||
ChevronRight,
|
||||
MousePointerClick,
|
||||
} from "lucide-react";
|
||||
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
@@ -29,6 +30,14 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms";
|
||||
import { ComponentSelection } from "@/ipc/ipc_types";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
interface ErrorBannerProps {
|
||||
error: string | undefined;
|
||||
@@ -165,11 +174,30 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
}, [routerContent]);
|
||||
|
||||
// Navigation state
|
||||
const [isComponentSelectorInitialized, setIsComponentSelectorInitialized] =
|
||||
useState(false);
|
||||
const [canGoBack, setCanGoBack] = useState(false);
|
||||
const [canGoForward, setCanGoForward] = useState(false);
|
||||
const [navigationHistory, setNavigationHistory] = useState<string[]>([]);
|
||||
const [currentHistoryPosition, setCurrentHistoryPosition] = useState(0);
|
||||
const [selectedComponentPreview, setSelectedComponentPreview] = useAtom(
|
||||
selectedComponentPreviewAtom,
|
||||
);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [isPicking, setIsPicking] = useState(false);
|
||||
|
||||
// Deactivate component selector when selection is cleared
|
||||
useEffect(() => {
|
||||
if (!selectedComponentPreview) {
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ type: "deactivate-dyad-component-selector" },
|
||||
"*",
|
||||
);
|
||||
}
|
||||
setIsPicking(false);
|
||||
}
|
||||
}, [selectedComponentPreview]);
|
||||
|
||||
// Add message listener for iframe errors and navigation events
|
||||
useEffect(() => {
|
||||
@@ -179,6 +207,18 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === "dyad-component-selector-initialized") {
|
||||
setIsComponentSelectorInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === "dyad-component-selected") {
|
||||
console.log("Component picked:", event.data);
|
||||
setSelectedComponentPreview(parseComponentSelection(event.data));
|
||||
setIsPicking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, payload } = event.data as {
|
||||
type:
|
||||
| "window-error"
|
||||
@@ -262,6 +302,8 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
selectedAppId,
|
||||
errorMessage,
|
||||
setErrorMessage,
|
||||
setIsComponentSelectorInitialized,
|
||||
setSelectedComponentPreview,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -280,6 +322,22 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
}
|
||||
}, [appUrl]);
|
||||
|
||||
// Function to activate component selector in the iframe
|
||||
const handleActivateComponentSelector = () => {
|
||||
if (iframeRef.current?.contentWindow) {
|
||||
const newIsPicking = !isPicking;
|
||||
setIsPicking(newIsPicking);
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{
|
||||
type: newIsPicking
|
||||
? "activate-dyad-component-selector"
|
||||
: "deactivate-dyad-component-selector",
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to navigate back
|
||||
const handleNavigateBack = () => {
|
||||
if (canGoBack && iframeRef.current?.contentWindow) {
|
||||
@@ -371,6 +429,33 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
<div className="flex items-center p-2 border-b space-x-2 ">
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex space-x-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={handleActivateComponentSelector}
|
||||
className={`p-1 rounded transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||
isPicking
|
||||
? "bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700"
|
||||
: " text-purple-700 hover:bg-purple-200 dark:text-purple-300 dark:hover:bg-purple-900"
|
||||
}`}
|
||||
disabled={
|
||||
loading || !selectedAppId || !isComponentSelectorInitialized
|
||||
}
|
||||
data-testid="preview-pick-element-button"
|
||||
>
|
||||
<MousePointerClick size={16} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{isPicking
|
||||
? "Deactivate component selector"
|
||||
: "Select component"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed dark:text-gray-300"
|
||||
disabled={!canGoBack || loading || !selectedAppId}
|
||||
@@ -486,3 +571,48 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function parseComponentSelection(data: any): ComponentSelection | null {
|
||||
if (
|
||||
!data ||
|
||||
data.type !== "dyad-component-selected" ||
|
||||
typeof data.id !== "string" ||
|
||||
typeof data.name !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, name } = data;
|
||||
|
||||
// The id is expected to be in the format "filepath:line:column"
|
||||
const parts = id.split(":");
|
||||
if (parts.length < 3) {
|
||||
console.error(`Invalid component selection id format: "${id}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const columnStr = parts.pop();
|
||||
const lineStr = parts.pop();
|
||||
const relativePath = parts.join(":");
|
||||
|
||||
if (!columnStr || !lineStr || !relativePath) {
|
||||
console.error(`Could not parse component selection from id: "${id}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineNumber = parseInt(lineStr, 10);
|
||||
const columnNumber = parseInt(columnStr, 10);
|
||||
|
||||
if (isNaN(lineNumber) || isNaN(columnNumber)) {
|
||||
console.error(`Could not parse line/column from id: "${id}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
relativePath,
|
||||
lineNumber,
|
||||
columnNumber,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from "react";
|
||||
import type { Message } from "@/ipc/ipc_types";
|
||||
import type { ComponentSelection, Message } from "@/ipc/ipc_types";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import {
|
||||
chatErrorAtom,
|
||||
@@ -56,11 +56,13 @@ export function useStreamChat({
|
||||
chatId,
|
||||
redo,
|
||||
attachments,
|
||||
selectedComponent,
|
||||
}: {
|
||||
prompt: string;
|
||||
chatId: number;
|
||||
redo?: boolean;
|
||||
attachments?: File[];
|
||||
selectedComponent?: ComponentSelection | null;
|
||||
}) => {
|
||||
if (
|
||||
(!prompt.trim() && (!attachments || attachments.length === 0)) ||
|
||||
@@ -74,6 +76,7 @@ export function useStreamChat({
|
||||
let hasIncrementedStreamCount = false;
|
||||
try {
|
||||
IpcClient.getInstance().streamMessage(prompt, {
|
||||
selectedComponent: selectedComponent ?? null,
|
||||
chatId,
|
||||
redo,
|
||||
attachments,
|
||||
|
||||
@@ -442,6 +442,8 @@ export function registerAppHandlers() {
|
||||
|
||||
const appPath = getDyadAppPath(app.path);
|
||||
try {
|
||||
// Kill any orphaned process on port 32100 (in case previous run left it)
|
||||
await killProcessOnPort(32100);
|
||||
await executeApp({ appPath, appId, event });
|
||||
|
||||
return;
|
||||
|
||||
179
src/ipc/handlers/app_upgrade_handlers.ts
Normal file
179
src/ipc/handlers/app_upgrade_handlers.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { createLoggedHandler } from "./safe_handle";
|
||||
import log from "electron-log";
|
||||
import { AppUpgrade } from "../ipc_types";
|
||||
import { db } from "../../db";
|
||||
import { apps } from "../../db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getDyadAppPath } from "../../paths/paths";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const logger = log.scope("app_upgrade_handlers");
|
||||
const handle = createLoggedHandler(logger);
|
||||
|
||||
const availableUpgrades: Omit<AppUpgrade, "isNeeded">[] = [
|
||||
{
|
||||
id: "component-tagger",
|
||||
title: "Enable select component to edit",
|
||||
description:
|
||||
"Installs the Dyad component tagger Vite plugin and its dependencies.",
|
||||
manualUpgradeUrl: "https://dyad.sh/docs/upgrades/select-component",
|
||||
},
|
||||
];
|
||||
|
||||
async function getApp(appId: number) {
|
||||
const app = await db.query.apps.findFirst({
|
||||
where: eq(apps.id, appId),
|
||||
});
|
||||
if (!app) {
|
||||
throw new Error(`App with id ${appId} not found`);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
function isComponentTaggerUpgradeNeeded(appPath: string): boolean {
|
||||
const viteConfigPathJs = path.join(appPath, "vite.config.js");
|
||||
const viteConfigPathTs = path.join(appPath, "vite.config.ts");
|
||||
|
||||
let viteConfigPath;
|
||||
if (fs.existsSync(viteConfigPathTs)) {
|
||||
viteConfigPath = viteConfigPathTs;
|
||||
} else if (fs.existsSync(viteConfigPathJs)) {
|
||||
viteConfigPath = viteConfigPathJs;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const viteConfigContent = fs.readFileSync(viteConfigPath, "utf-8");
|
||||
return !viteConfigContent.includes("@dyad-sh/react-vite-component-tagger");
|
||||
} catch (e) {
|
||||
logger.error("Error reading vite config", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyComponentTagger(appPath: string) {
|
||||
const viteConfigPathJs = path.join(appPath, "vite.config.js");
|
||||
const viteConfigPathTs = path.join(appPath, "vite.config.ts");
|
||||
|
||||
let viteConfigPath;
|
||||
if (fs.existsSync(viteConfigPathTs)) {
|
||||
viteConfigPath = viteConfigPathTs;
|
||||
} else if (fs.existsSync(viteConfigPathJs)) {
|
||||
viteConfigPath = viteConfigPathJs;
|
||||
} else {
|
||||
throw new Error("Could not find vite.config.js or vite.config.ts");
|
||||
}
|
||||
|
||||
let content = await fs.promises.readFile(viteConfigPath, "utf-8");
|
||||
|
||||
// Add import statement if not present
|
||||
if (
|
||||
!content.includes(
|
||||
"import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';",
|
||||
)
|
||||
) {
|
||||
// Add it after the last import statement
|
||||
const lines = content.split("\n");
|
||||
let lastImportIndex = -1;
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
if (lines[i].startsWith("import ")) {
|
||||
lastImportIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lines.splice(
|
||||
lastImportIndex + 1,
|
||||
0,
|
||||
"import dyadComponentTagger from '@dyad-sh/react-vite-component-tagger';",
|
||||
);
|
||||
content = lines.join("\n");
|
||||
}
|
||||
|
||||
// Add plugin to plugins array
|
||||
if (content.includes("plugins: [")) {
|
||||
if (!content.includes("dyadComponentTagger()")) {
|
||||
content = content.replace(
|
||||
"plugins: [",
|
||||
"plugins: [dyadComponentTagger(), ",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"Could not find `plugins: [` in vite.config.ts. Manual installation required.",
|
||||
);
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(viteConfigPath, content);
|
||||
|
||||
// Install the dependency
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
logger.info("Installing component-tagger dependency");
|
||||
const process = spawn(
|
||||
"pnpm add -D @dyad-sh/react-vite-component-tagger || npm install --save-dev --legacy-peer-deps @dyad-sh/react-vite-component-tagger",
|
||||
{
|
||||
cwd: appPath,
|
||||
shell: true,
|
||||
stdio: "pipe",
|
||||
},
|
||||
);
|
||||
|
||||
process.stdout?.on("data", (data) => logger.info(data.toString()));
|
||||
process.stderr?.on("data", (data) => logger.error(data.toString()));
|
||||
|
||||
process.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
logger.info("component-tagger dependency installed successfully");
|
||||
resolve();
|
||||
} else {
|
||||
logger.error(`Failed to install dependency, exit code ${code}`);
|
||||
reject(new Error("Failed to install dependency"));
|
||||
}
|
||||
});
|
||||
|
||||
process.on("error", (err) => {
|
||||
logger.error("Failed to spawn pnpm", err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function registerAppUpgradeHandlers() {
|
||||
handle(
|
||||
"get-app-upgrades",
|
||||
async (_, { appId }: { appId: number }): Promise<AppUpgrade[]> => {
|
||||
const app = await getApp(appId);
|
||||
const appPath = getDyadAppPath(app.path);
|
||||
|
||||
const upgradesWithStatus = availableUpgrades.map((upgrade) => {
|
||||
let isNeeded = false;
|
||||
if (upgrade.id === "component-tagger") {
|
||||
isNeeded = isComponentTaggerUpgradeNeeded(appPath);
|
||||
}
|
||||
return { ...upgrade, isNeeded };
|
||||
});
|
||||
|
||||
return upgradesWithStatus;
|
||||
},
|
||||
);
|
||||
|
||||
handle(
|
||||
"execute-app-upgrade",
|
||||
async (_, { appId, upgradeId }: { appId: number; upgradeId: string }) => {
|
||||
if (!upgradeId) {
|
||||
throw new Error("upgradeId is required");
|
||||
}
|
||||
|
||||
const app = await getApp(appId);
|
||||
const appPath = getDyadAppPath(app.path);
|
||||
|
||||
if (upgradeId === "component-tagger") {
|
||||
await applyComponentTagger(appPath);
|
||||
} else {
|
||||
throw new Error(`Unknown upgrade id: ${upgradeId}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -160,7 +160,45 @@ export function registerChatStreamHandlers() {
|
||||
}
|
||||
|
||||
// Add user message to database with attachment info
|
||||
const userPrompt = req.prompt + (attachmentInfo ? attachmentInfo : "");
|
||||
let userPrompt = req.prompt + (attachmentInfo ? attachmentInfo : "");
|
||||
if (req.selectedComponent) {
|
||||
let componentSnippet = "[component snippet not available]";
|
||||
try {
|
||||
const componentFileContent = await readFile(
|
||||
path.join(
|
||||
getDyadAppPath(chat.app.path),
|
||||
req.selectedComponent.relativePath,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const lines = componentFileContent.split("\n");
|
||||
const selectedIndex = req.selectedComponent.lineNumber - 1;
|
||||
|
||||
// Let's get one line before and three after for context.
|
||||
const startIndex = Math.max(0, selectedIndex - 1);
|
||||
const endIndex = Math.min(lines.length, selectedIndex + 4);
|
||||
|
||||
const snippetLines = lines.slice(startIndex, endIndex);
|
||||
const selectedLineInSnippetIndex = selectedIndex - startIndex;
|
||||
|
||||
if (snippetLines[selectedLineInSnippetIndex]) {
|
||||
snippetLines[selectedLineInSnippetIndex] =
|
||||
`${snippetLines[selectedLineInSnippetIndex]} // <-- EDIT HERE`;
|
||||
}
|
||||
|
||||
componentSnippet = snippetLines.join("\n");
|
||||
} catch (err) {
|
||||
logger.error(`Error reading selected component file content: ${err}`);
|
||||
}
|
||||
|
||||
userPrompt += `\n\nSelected component: ${req.selectedComponent.name} (file: ${req.selectedComponent.relativePath})
|
||||
|
||||
Snippet:
|
||||
\`\`\`
|
||||
${componentSnippet}
|
||||
\`\`\`
|
||||
`;
|
||||
}
|
||||
await db
|
||||
.insert(messages)
|
||||
.values({
|
||||
@@ -228,7 +266,16 @@ export function registerChatStreamHandlers() {
|
||||
try {
|
||||
const out = await extractCodebase({
|
||||
appPath,
|
||||
chatContext: validateChatContext(updatedChat.app.chatContext),
|
||||
chatContext: req.selectedComponent
|
||||
? {
|
||||
contextPaths: [
|
||||
{
|
||||
globPath: req.selectedComponent.relativePath,
|
||||
},
|
||||
],
|
||||
smartContextAutoIncludes: [],
|
||||
}
|
||||
: validateChatContext(updatedChat.app.chatContext),
|
||||
});
|
||||
codebaseInfo = out.formattedOutput;
|
||||
files = out.files;
|
||||
|
||||
@@ -33,6 +33,8 @@ import type {
|
||||
UserBudgetInfo,
|
||||
CopyAppParams,
|
||||
App,
|
||||
ComponentSelection,
|
||||
AppUpgrade,
|
||||
} from "./ipc_types";
|
||||
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
|
||||
import { showError } from "@/lib/toast";
|
||||
@@ -224,6 +226,7 @@ export class IpcClient {
|
||||
public streamMessage(
|
||||
prompt: string,
|
||||
options: {
|
||||
selectedComponent: ComponentSelection | null;
|
||||
chatId: number;
|
||||
redo?: boolean;
|
||||
attachments?: File[];
|
||||
@@ -232,7 +235,15 @@ export class IpcClient {
|
||||
onError: (error: string) => void;
|
||||
},
|
||||
): void {
|
||||
const { chatId, redo, attachments, onUpdate, onEnd, onError } = options;
|
||||
const {
|
||||
chatId,
|
||||
redo,
|
||||
attachments,
|
||||
selectedComponent,
|
||||
onUpdate,
|
||||
onEnd,
|
||||
onError,
|
||||
} = options;
|
||||
this.chatStreams.set(chatId, { onUpdate, onEnd, onError });
|
||||
|
||||
// Handle file attachments if provided
|
||||
@@ -264,6 +275,7 @@ export class IpcClient {
|
||||
prompt,
|
||||
chatId,
|
||||
redo,
|
||||
selectedComponent,
|
||||
attachments: fileDataArray,
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -284,6 +296,7 @@ export class IpcClient {
|
||||
prompt,
|
||||
chatId,
|
||||
redo,
|
||||
selectedComponent,
|
||||
})
|
||||
.catch((err) => {
|
||||
showError(err);
|
||||
@@ -859,6 +872,19 @@ export class IpcClient {
|
||||
appId: number;
|
||||
chatContext: AppChatContext;
|
||||
}): Promise<void> {
|
||||
return this.ipcRenderer.invoke("set-context-paths", params);
|
||||
await this.ipcRenderer.invoke("set-context-paths", params);
|
||||
}
|
||||
|
||||
public async getAppUpgrades(params: {
|
||||
appId: number;
|
||||
}): Promise<AppUpgrade[]> {
|
||||
return this.ipcRenderer.invoke("get-app-upgrades", params);
|
||||
}
|
||||
|
||||
public async executeAppUpgrade(params: {
|
||||
appId: number;
|
||||
upgradeId: string;
|
||||
}): Promise<void> {
|
||||
return this.ipcRenderer.invoke("execute-app-upgrade", params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { registerImportHandlers } from "./handlers/import_handlers";
|
||||
import { registerSessionHandlers } from "./handlers/session_handlers";
|
||||
import { registerProHandlers } from "./handlers/pro_handlers";
|
||||
import { registerContextPathsHandlers } from "./handlers/context_paths_handlers";
|
||||
import { registerAppUpgradeHandlers } from "./handlers/app_upgrade_handlers";
|
||||
|
||||
export function registerIpcHandlers() {
|
||||
// Register all IPC handlers by category
|
||||
@@ -45,4 +46,5 @@ export function registerIpcHandlers() {
|
||||
registerSessionHandlers();
|
||||
registerProHandlers();
|
||||
registerContextPathsHandlers();
|
||||
registerAppUpgradeHandlers();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface ChatStreamParams {
|
||||
type: string;
|
||||
data: string; // Base64 encoded file data
|
||||
}>;
|
||||
selectedComponent: ComponentSelection | null;
|
||||
}
|
||||
|
||||
export interface ChatResponseEnd {
|
||||
@@ -223,3 +224,19 @@ export const UserBudgetInfoSchema = z.object({
|
||||
budgetResetDate: z.date(),
|
||||
});
|
||||
export type UserBudgetInfo = z.infer<typeof UserBudgetInfoSchema>;
|
||||
|
||||
export interface ComponentSelection {
|
||||
id: string;
|
||||
name: string;
|
||||
relativePath: string;
|
||||
lineNumber: number;
|
||||
columnNumber: number;
|
||||
}
|
||||
|
||||
export interface AppUpgrade {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
manualUpgradeUrl: string;
|
||||
isNeeded: boolean;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { Loader2 } from "lucide-react";
|
||||
import { invalidateAppQuery } from "@/hooks/useLoadApp";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { useCheckName } from "@/hooks/useCheckName";
|
||||
import { AppUpgrades } from "@/components/AppUpgrades";
|
||||
|
||||
export default function AppDetailsPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -341,6 +342,7 @@ export default function AppDetailsPage() {
|
||||
</Button>
|
||||
<GitHubConnector appId={appId} folderName={selectedApp.path} />
|
||||
{appId && <SupabaseConnector appId={appId} />}
|
||||
<AppUpgrades appId={appId} />
|
||||
</div>
|
||||
|
||||
{/* Rename Dialog */}
|
||||
|
||||
@@ -79,6 +79,8 @@ const validInvokeChannels = [
|
||||
"get-user-budget",
|
||||
"get-context-paths",
|
||||
"set-context-paths",
|
||||
"get-app-upgrades",
|
||||
"execute-app-upgrade",
|
||||
// Test-only channels
|
||||
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
|
||||
// We can't detect with IS_TEST_BUILD in the preload script because
|
||||
|
||||
210
worker/dyad-component-selector-client.js
Normal file
210
worker/dyad-component-selector-client.js
Normal file
@@ -0,0 +1,210 @@
|
||||
(() => {
|
||||
const OVERLAY_ID = "__dyad_overlay__";
|
||||
let overlay, label;
|
||||
|
||||
// The possible states are:
|
||||
// { type: 'inactive' }
|
||||
// { type: 'inspecting', element: ?HTMLElement }
|
||||
// { type: 'selected', element: HTMLElement }
|
||||
let state = { type: "inactive" };
|
||||
|
||||
/* ---------- helpers --------------------------------------------------- */
|
||||
const css = (el, obj) => Object.assign(el.style, obj);
|
||||
|
||||
function makeOverlay() {
|
||||
overlay = document.createElement("div");
|
||||
overlay.id = OVERLAY_ID;
|
||||
css(overlay, {
|
||||
position: "absolute",
|
||||
border: "2px solid #7f22fe",
|
||||
background: "rgba(0,170,255,.05)",
|
||||
pointerEvents: "none",
|
||||
zIndex: "2147483647", // max
|
||||
borderRadius: "4px",
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
|
||||
});
|
||||
|
||||
label = document.createElement("div");
|
||||
css(label, {
|
||||
position: "absolute",
|
||||
left: "0",
|
||||
top: "100%",
|
||||
transform: "translateY(4px)",
|
||||
background: "#7f22fe",
|
||||
color: "#fff",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "12px",
|
||||
lineHeight: "1.2",
|
||||
padding: "3px 5px",
|
||||
whiteSpace: "nowrap",
|
||||
borderRadius: "4px",
|
||||
boxShadow: "0 1px 4px rgba(0, 0, 0, 0.1)",
|
||||
});
|
||||
overlay.appendChild(label);
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function updateOverlay(el, isSelected = false) {
|
||||
if (!overlay) makeOverlay();
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
css(overlay, {
|
||||
top: `${rect.top + window.scrollY}px`,
|
||||
left: `${rect.left + window.scrollX}px`,
|
||||
width: `${rect.width}px`,
|
||||
height: `${rect.height}px`,
|
||||
display: "block",
|
||||
border: isSelected ? "3px solid #7f22fe" : "2px solid #7f22fe",
|
||||
background: isSelected
|
||||
? "rgba(127, 34, 254, 0.05)"
|
||||
: "rgba(0,170,255,.05)",
|
||||
});
|
||||
|
||||
css(label, {
|
||||
background: "#7f22fe",
|
||||
});
|
||||
|
||||
// Clear previous contents
|
||||
while (label.firstChild) {
|
||||
label.removeChild(label.firstChild);
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
const editLine = document.createElement("div");
|
||||
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
const svg = document.createElementNS(svgNS, "svg");
|
||||
svg.setAttribute("width", "12");
|
||||
svg.setAttribute("height", "12");
|
||||
svg.setAttribute("viewBox", "0 0 16 16");
|
||||
svg.setAttribute("fill", "none");
|
||||
Object.assign(svg.style, {
|
||||
display: "inline-block",
|
||||
verticalAlign: "-2px",
|
||||
marginRight: "4px",
|
||||
});
|
||||
const path = document.createElementNS(svgNS, "path");
|
||||
path.setAttribute(
|
||||
"d",
|
||||
"M8 0L9.48528 6.51472L16 8L9.48528 9.48528L8 16L6.51472 9.48528L0 8L6.51472 6.51472L8 0Z",
|
||||
);
|
||||
path.setAttribute("fill", "white");
|
||||
svg.appendChild(path);
|
||||
|
||||
editLine.appendChild(svg);
|
||||
editLine.appendChild(document.createTextNode("Edit with AI"));
|
||||
label.appendChild(editLine);
|
||||
}
|
||||
|
||||
const name = el.dataset.dyadName || "<unknown>";
|
||||
const file = (el.dataset.dyadId || "").split(":")[0];
|
||||
|
||||
const nameEl = document.createElement("div");
|
||||
nameEl.textContent = name;
|
||||
label.appendChild(nameEl);
|
||||
|
||||
if (file) {
|
||||
const fileEl = document.createElement("span");
|
||||
css(fileEl, { fontSize: "10px", opacity: ".8" });
|
||||
fileEl.textContent = file;
|
||||
label.appendChild(fileEl);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- event handlers -------------------------------------------- */
|
||||
function onMouseMove(e) {
|
||||
if (state.type !== "inspecting") return;
|
||||
|
||||
let el = e.target;
|
||||
while (el && !el.dataset.dyadId) el = el.parentElement;
|
||||
|
||||
if (state.element === el) return;
|
||||
state.element = el;
|
||||
|
||||
if (el) {
|
||||
updateOverlay(el, false);
|
||||
} else {
|
||||
if (overlay) overlay.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
if (state.type !== "inspecting" || !state.element) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
state = { type: "selected", element: state.element };
|
||||
updateOverlay(state.element, true);
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "dyad-component-selected",
|
||||
id: state.element.dataset.dyadId,
|
||||
name: state.element.dataset.dyadName,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- activation / deactivation --------------------------------- */
|
||||
function activate() {
|
||||
if (state.type === "inactive") {
|
||||
window.addEventListener("mousemove", onMouseMove, true);
|
||||
window.addEventListener("click", onClick, true);
|
||||
}
|
||||
state = { type: "inspecting", element: null };
|
||||
if (overlay) {
|
||||
overlay.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function deactivate() {
|
||||
if (state.type === "inactive") return;
|
||||
|
||||
window.removeEventListener("mousemove", onMouseMove, true);
|
||||
window.removeEventListener("click", onClick, true);
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
overlay = null;
|
||||
label = null;
|
||||
}
|
||||
state = { type: "inactive" };
|
||||
}
|
||||
|
||||
/* ---------- message bridge -------------------------------------------- */
|
||||
window.addEventListener("message", (e) => {
|
||||
if (e.source !== window.parent) return;
|
||||
if (e.data.type === "activate-dyad-component-selector") activate();
|
||||
if (e.data.type === "deactivate-dyad-component-selector") deactivate();
|
||||
});
|
||||
|
||||
function initializeComponentSelector() {
|
||||
if (!document.body) {
|
||||
console.error(
|
||||
"Dyad component selector initialization failed: document.body not found.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (document.body.querySelector("[data-dyad-id]")) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "dyad-component-selector-initialized",
|
||||
},
|
||||
"*",
|
||||
);
|
||||
console.debug("Dyad component selector initialized");
|
||||
} else {
|
||||
console.warn(
|
||||
"Dyad component selector not initialized because no DOM elements were tagged",
|
||||
);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializeComponentSelector);
|
||||
} else {
|
||||
initializeComponentSelector();
|
||||
}
|
||||
})();
|
||||
@@ -61,7 +61,7 @@ let rememberedOrigin = null; // e.g. "http://localhost:5173"
|
||||
|
||||
let stacktraceJsContent = null;
|
||||
let dyadShimContent = null;
|
||||
|
||||
let dyadComponentSelectorClientContent = null;
|
||||
try {
|
||||
const stackTraceLibPath = path.join(
|
||||
__dirname,
|
||||
@@ -89,6 +89,24 @@ try {
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const dyadComponentSelectorClientPath = path.join(
|
||||
__dirname,
|
||||
"dyad-component-selector-client.js",
|
||||
);
|
||||
dyadComponentSelectorClientContent = fs.readFileSync(
|
||||
dyadComponentSelectorClientPath,
|
||||
"utf-8",
|
||||
);
|
||||
parentPort?.postMessage(
|
||||
"[proxy-worker] dyad-component-selector-client.js loaded.",
|
||||
);
|
||||
} catch (error) {
|
||||
parentPort?.postMessage(
|
||||
`[proxy-worker] Failed to read dyad-component-selector-client.js: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------------- helper: need to inject? ------------------------ */
|
||||
function needsInjection(pathname) {
|
||||
return pathname.endsWith("index.html") || pathname === "/";
|
||||
@@ -99,25 +117,35 @@ function injectHTML(buf) {
|
||||
// These are strings that were used since the first version of the dyad shim.
|
||||
// If the dyad shim is used from legacy apps which came pre-baked with the shim
|
||||
// as a vite plugin, then do not inject the shim twice to avoid weird behaviors.
|
||||
if (txt.includes("window-error") && txt.includes("unhandled-rejection")) {
|
||||
return buf;
|
||||
}
|
||||
const legacyAppWithShim =
|
||||
txt.includes("window-error") && txt.includes("unhandled-rejection");
|
||||
|
||||
const scripts = [];
|
||||
|
||||
if (stacktraceJsContent)
|
||||
scripts.push(`<script>${stacktraceJsContent}</script>`);
|
||||
else
|
||||
scripts.push(
|
||||
'<script>console.warn("[proxy-worker] stacktrace.js was not injected.");</script>',
|
||||
);
|
||||
if (!legacyAppWithShim) {
|
||||
if (stacktraceJsContent) {
|
||||
scripts.push(`<script>${stacktraceJsContent}</script>`);
|
||||
} else {
|
||||
scripts.push(
|
||||
'<script>console.warn("[proxy-worker] stacktrace.js was not injected.");</script>',
|
||||
);
|
||||
}
|
||||
|
||||
if (dyadShimContent) scripts.push(`<script>${dyadShimContent}</script>`);
|
||||
else
|
||||
if (dyadShimContent) {
|
||||
scripts.push(`<script>${dyadShimContent}</script>`);
|
||||
} else {
|
||||
scripts.push(
|
||||
'<script>console.warn("[proxy-worker] dyad shim was not injected.");</script>',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (dyadComponentSelectorClientContent) {
|
||||
scripts.push(`<script>${dyadComponentSelectorClientContent}</script>`);
|
||||
} else {
|
||||
scripts.push(
|
||||
'<script>console.warn("[proxy-worker] dyad shim was not injected.");</script>',
|
||||
'<script>console.warn("[proxy-worker] dyad component selector client was not injected.");</script>',
|
||||
);
|
||||
|
||||
}
|
||||
const allScripts = scripts.join("\n");
|
||||
|
||||
const headRegex = /<head[^>]*>/i;
|
||||
|
||||
Reference in New Issue
Block a user