Compare commits
3 Commits
main
...
feat/termi
| Author | SHA1 | Date | |
|---|---|---|---|
| 4266a3ff43 | |||
| 074c79f302 | |||
| 8cbc9538ca |
6
.gitignore
vendored
6
.gitignore
vendored
@ -166,6 +166,12 @@ Thumbs.db
|
|||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
|
.cursorrules
|
||||||
|
.cursorrules/*
|
||||||
|
!.cursorrules/.gitignore
|
||||||
|
.cursor/*
|
||||||
|
.cursor/*/*
|
||||||
|
.cursor
|
||||||
|
|
||||||
# Turborepo cache (if you introduce turbo later)
|
# Turborepo cache (if you introduce turbo later)
|
||||||
.turbo/
|
.turbo/
|
||||||
|
|||||||
@ -2,6 +2,12 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@ -12,8 +12,11 @@
|
|||||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts"
|
"cf-typegen": "wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@icons-pack/react-simple-icons": "^13.8.0",
|
||||||
"@opennextjs/cloudflare": "^1.3.0",
|
"@opennextjs/cloudflare": "^1.3.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
@ -22,15 +25,26 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
|
"ai": "^5.0.62",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"harden-react-markdown": "^1.1.2",
|
||||||
|
"katex": "^0.16.23",
|
||||||
"lucide-react": "^0.545.0",
|
"lucide-react": "^0.545.0",
|
||||||
"next": "15.4.6",
|
"next": "15.4.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"shiki": "^3.13.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"use-stick-to-bottom": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@ -38,6 +52,7 @@
|
|||||||
"@types/node": "^20.19.19",
|
"@types/node": "^20.19.19",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.4.6",
|
"eslint-config-next": "15.4.6",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
|||||||
1602
bandit-runner-app/pnpm-lock.yaml
generated
1602
bandit-runner-app/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,180 +3,165 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(0.98 0 0);
|
||||||
|
--foreground: oklch(0.15 0 0);
|
||||||
|
--card: oklch(0.99 0 0);
|
||||||
|
--card-foreground: oklch(0.15 0 0);
|
||||||
|
--popover: oklch(0.99 0 0);
|
||||||
|
--popover-foreground: oklch(0.15 0 0);
|
||||||
|
--primary: oklch(0.35 0.15 250);
|
||||||
|
--primary-foreground: oklch(0.98 0 0);
|
||||||
|
--secondary: oklch(0.92 0 0);
|
||||||
|
--secondary-foreground: oklch(0.15 0 0);
|
||||||
|
--muted: oklch(0.94 0 0);
|
||||||
|
--muted-foreground: oklch(0.50 0 0);
|
||||||
|
--accent: oklch(0.88 0.08 250);
|
||||||
|
--accent-foreground: oklch(0.25 0.15 250);
|
||||||
|
--destructive: oklch(0.55 0.22 25);
|
||||||
|
--destructive-foreground: oklch(0.98 0 0);
|
||||||
|
--border: oklch(0.88 0 0);
|
||||||
|
--input: oklch(0.92 0 0);
|
||||||
|
--ring: oklch(0.35 0.15 250);
|
||||||
|
--chart-1: oklch(0.81 0.17 75.35);
|
||||||
|
--chart-2: oklch(0.55 0.22 264.53);
|
||||||
|
--chart-3: oklch(0.72 0 0);
|
||||||
|
--chart-4: oklch(0.92 0 0);
|
||||||
|
--chart-5: oklch(0.56 0 0);
|
||||||
|
--sidebar: oklch(0.99 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.15 0 0);
|
||||||
|
--sidebar-primary: oklch(0.35 0.15 250);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0 0);
|
||||||
|
--sidebar-accent: oklch(0.92 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.15 0 0);
|
||||||
|
--sidebar-border: oklch(0.92 0 0);
|
||||||
|
--sidebar-ring: oklch(0.35 0.15 250);
|
||||||
|
--font-sans: Geist, sans-serif;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--font-mono: Geist Mono, monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 2px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.18;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.08 0.01 250);
|
||||||
|
--foreground: oklch(0.90 0.05 200);
|
||||||
|
--card: oklch(0.12 0.01 250);
|
||||||
|
--card-foreground: oklch(0.90 0.05 200);
|
||||||
|
--popover: oklch(0.14 0.01 250);
|
||||||
|
--popover-foreground: oklch(0.90 0.05 200);
|
||||||
|
--primary: oklch(0.70 0.15 195);
|
||||||
|
--primary-foreground: oklch(0.08 0.01 250);
|
||||||
|
--secondary: oklch(0.22 0.01 250);
|
||||||
|
--secondary-foreground: oklch(0.90 0.05 200);
|
||||||
|
--muted: oklch(0.20 0.01 250);
|
||||||
|
--muted-foreground: oklch(0.55 0.03 200);
|
||||||
|
--accent: oklch(0.28 0.05 250);
|
||||||
|
--accent-foreground: oklch(0.75 0.15 180);
|
||||||
|
--destructive: oklch(0.60 0.20 25);
|
||||||
|
--destructive-foreground: oklch(0.95 0 0);
|
||||||
|
--border: oklch(0.24 0.02 250);
|
||||||
|
--input: oklch(0.28 0.02 250);
|
||||||
|
--ring: oklch(0.70 0.15 195);
|
||||||
|
--chart-1: oklch(0.81 0.17 75.35);
|
||||||
|
--chart-2: oklch(0.58 0.21 260.84);
|
||||||
|
--chart-3: oklch(0.56 0 0);
|
||||||
|
--chart-4: oklch(0.44 0 0);
|
||||||
|
--chart-5: oklch(0.92 0 0);
|
||||||
|
--sidebar: oklch(0.12 0.01 250);
|
||||||
|
--sidebar-foreground: oklch(0.90 0.05 200);
|
||||||
|
--sidebar-primary: oklch(0.70 0.15 195);
|
||||||
|
--sidebar-primary-foreground: oklch(0.08 0.01 250);
|
||||||
|
--sidebar-accent: oklch(0.28 0.05 250);
|
||||||
|
--sidebar-accent-foreground: oklch(0.75 0.15 180);
|
||||||
|
--sidebar-border: oklch(0.24 0.02 250);
|
||||||
|
--sidebar-ring: oklch(0.70 0.15 195);
|
||||||
|
--font-sans: Geist, sans-serif;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--font-mono: Geist Mono, monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 2px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.18;
|
||||||
|
--shadow-color: hsl(0 0% 0%);
|
||||||
|
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: 'JetBrains Mono', monospace;
|
|
||||||
--font-mono: 'JetBrains Mono', monospace;
|
|
||||||
--font-serif: 'JetBrains Mono', monospace;
|
|
||||||
--radius: 0rem;
|
|
||||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
|
||||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
|
||||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
|
||||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
|
||||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
|
||||||
--tracking-normal: var(--tracking-normal);
|
|
||||||
--shadow-2xl: var(--shadow-2xl);
|
|
||||||
--shadow-xl: var(--shadow-xl);
|
|
||||||
--shadow-lg: var(--shadow-lg);
|
|
||||||
--shadow-md: var(--shadow-md);
|
|
||||||
--shadow: var(--shadow);
|
|
||||||
--shadow-sm: var(--shadow-sm);
|
|
||||||
--shadow-xs: var(--shadow-xs);
|
|
||||||
--shadow-2xs: var(--shadow-2xs);
|
|
||||||
--spacing: var(--spacing);
|
|
||||||
--letter-spacing: var(--letter-spacing);
|
|
||||||
--shadow-offset-y: var(--shadow-offset-y);
|
|
||||||
--shadow-offset-x: var(--shadow-offset-x);
|
|
||||||
--shadow-spread: var(--shadow-spread);
|
|
||||||
--shadow-blur: var(--shadow-blur);
|
|
||||||
--shadow-opacity: var(--shadow-opacity);
|
|
||||||
--color-shadow-color: var(--shadow-color);
|
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-card: var(--card);
|
--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-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--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);
|
||||||
|
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
--radius: 0rem;
|
--shadow-xs: var(--shadow-xs);
|
||||||
--background: oklch(0 0 0);
|
--shadow-sm: var(--shadow-sm);
|
||||||
--foreground: oklch(0.8664 0.2948 142.4953);
|
--shadow: var(--shadow);
|
||||||
--card: oklch(0.2178 0 0);
|
--shadow-md: var(--shadow-md);
|
||||||
--card-foreground: oklch(0.8664 0.2948 142.4953);
|
--shadow-lg: var(--shadow-lg);
|
||||||
--popover: oklch(0.1448 0 0);
|
--shadow-xl: var(--shadow-xl);
|
||||||
--popover-foreground: oklch(0.8664 0.2948 142.4953);
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
--primary: oklch(0.7323 0.2492 142.4953);
|
|
||||||
--primary-foreground: oklch(0 0 0);
|
|
||||||
--secondary: oklch(0.5430 0.1848 142.4953);
|
|
||||||
--secondary-foreground: oklch(1.0000 0 0);
|
|
||||||
--muted: oklch(0.2782 0.0947 142.4953);
|
|
||||||
--muted-foreground: oklch(0.6394 0.2176 142.4953);
|
|
||||||
--accent: oklch(0.3895 0.1325 142.4953);
|
|
||||||
--accent-foreground: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--destructive: oklch(0.6280 0.2577 29.2339);
|
|
||||||
--border: oklch(0.3350 0.1140 142.4953);
|
|
||||||
--input: oklch(0.1448 0 0);
|
|
||||||
--ring: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--chart-1: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--chart-2: oklch(0.6863 0.2335 142.4953);
|
|
||||||
--chart-3: oklch(0.4932 0.1678 142.4953);
|
|
||||||
--chart-4: oklch(0.3895 0.1325 142.4953);
|
|
||||||
--chart-5: oklch(0.2782 0.0947 142.4953);
|
|
||||||
--sidebar: oklch(0.1149 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--sidebar-primary: oklch(0.7323 0.2492 142.4953);
|
|
||||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
|
||||||
--sidebar-accent: oklch(0.5430 0.1848 142.4953);
|
|
||||||
--sidebar-accent-foreground: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--sidebar-border: oklch(0.3350 0.1140 142.4953);
|
|
||||||
--sidebar-ring: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--destructive-foreground: oklch(1.0000 0 0);
|
|
||||||
--font-sans: 'JetBrains Mono', monospace;
|
|
||||||
--font-serif: 'JetBrains Mono', monospace;
|
|
||||||
--font-mono: 'JetBrains Mono', monospace;
|
|
||||||
--shadow-color: #00ff00;
|
|
||||||
--shadow-opacity: 0.2;
|
|
||||||
--shadow-blur: 0.2rem;
|
|
||||||
--shadow-spread: 0rem;
|
|
||||||
--shadow-offset-x: 0rem;
|
|
||||||
--shadow-offset-y: 0.1rem;
|
|
||||||
--letter-spacing: 0.025em;
|
|
||||||
--spacing: 0.25rem;
|
|
||||||
--shadow-2xs: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.10);
|
|
||||||
--shadow-xs: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.10);
|
|
||||||
--shadow-sm: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 1px 2px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 1px 2px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow-md: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 2px 4px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow-lg: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 4px 6px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow-xl: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 8px 10px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow-2xl: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.50);
|
|
||||||
--tracking-normal: 0.025em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: oklch(0 0 0);
|
|
||||||
--foreground: oklch(1.0000 0 0);
|
|
||||||
--card: oklch(0.1822 0 0);
|
|
||||||
--card-foreground: oklch(0.9706 0.0017 67.8024);
|
|
||||||
--popover: oklch(0.1448 0 0);
|
|
||||||
--popover-foreground: oklch(0.9706 0.0017 67.8024);
|
|
||||||
--primary: oklch(1.0000 0 0);
|
|
||||||
--primary-foreground: oklch(0 0 0);
|
|
||||||
--secondary: oklch(0.9706 0.0017 67.8024);
|
|
||||||
--secondary-foreground: oklch(0 0 0);
|
|
||||||
--muted: oklch(0.3979 0 0);
|
|
||||||
--muted-foreground: oklch(0.8975 0.0042 91.4501);
|
|
||||||
--accent: oklch(0.6829 0.0045 91.4665);
|
|
||||||
--accent-foreground: oklch(1.0000 0 0);
|
|
||||||
--destructive: oklch(0.6280 0.2577 29.2339);
|
|
||||||
--border: oklch(0.4794 0.0129 297.3461);
|
|
||||||
--input: oklch(0.1448 0 0);
|
|
||||||
--ring: oklch(1.0000 0 0);
|
|
||||||
--chart-1: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--chart-2: oklch(0.6863 0.2335 142.4953);
|
|
||||||
--chart-3: oklch(0.4932 0.1678 142.4953);
|
|
||||||
--chart-4: oklch(0.3895 0.1325 142.4953);
|
|
||||||
--chart-5: oklch(0.2782 0.0947 142.4953);
|
|
||||||
--sidebar: oklch(0.1149 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--sidebar-primary: oklch(0.7323 0.2492 142.4953);
|
|
||||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
|
||||||
--sidebar-accent: oklch(0.5430 0.1848 142.4953);
|
|
||||||
--sidebar-accent-foreground: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--sidebar-border: oklch(0.3350 0.1140 142.4953);
|
|
||||||
--sidebar-ring: oklch(0.8664 0.2948 142.4953);
|
|
||||||
--destructive-foreground: oklch(1.0000 0 0);
|
|
||||||
--radius: 0rem;
|
|
||||||
--font-sans: 'JetBrains Mono', monospace;
|
|
||||||
--font-serif: 'JetBrains Mono', monospace;
|
|
||||||
--font-mono: 'JetBrains Mono', monospace;
|
|
||||||
--shadow-color: #00ff00;
|
|
||||||
--shadow-opacity: 0.2;
|
|
||||||
--shadow-blur: 0.2rem;
|
|
||||||
--shadow-spread: 0rem;
|
|
||||||
--shadow-offset-x: 0rem;
|
|
||||||
--shadow-offset-y: 0.1rem;
|
|
||||||
--letter-spacing: 0.025em;
|
|
||||||
--spacing: 0.25rem;
|
|
||||||
--shadow-2xs: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.10);
|
|
||||||
--shadow-xs: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.10);
|
|
||||||
--shadow-sm: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 1px 2px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 1px 2px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow-md: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 2px 4px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow-lg: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 4px 6px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow-xl: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 8px 10px -1px hsl(120 100% 50% / 0.20);
|
|
||||||
--shadow-2xl: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@ -187,4 +172,30 @@
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
letter-spacing: var(--tracking-normal);
|
letter-spacing: var(--tracking-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes scan-lines {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cursor-blink {
|
||||||
|
0%, 49% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50%, 100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scan-lines {
|
||||||
|
animation: scan-lines 0.1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-cursor-blink {
|
||||||
|
animation: cursor-blink 1s step-end infinite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Bandit Runner",
|
||||||
description: "Generated by create next app",
|
description: "Security Automation Console",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -23,11 +24,18 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="dark"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,103 +1,5 @@
|
|||||||
import Image from "next/image";
|
import { TerminalChatInterface } from "@/components/terminal-chat-interface"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return <TerminalChatInterface />
|
||||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
|
||||||
<li className="mb-2 tracking-[-.01em]">
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
|
||||||
src/app/page.tsx
|
|
||||||
</code>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li className="tracking-[-.01em]">
|
|
||||||
Save and see your changes instantly.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
Deploy now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Read our docs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
117
bandit-runner-app/src/components/retro-icons.tsx
Normal file
117
bandit-runner-app/src/components/retro-icons.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
export function TerminalIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="square"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<rect x="2" y="3" width="20" height="18" />
|
||||||
|
<line x1="2" y1="7" x2="22" y2="7" />
|
||||||
|
<polyline points="6,11 9,14 6,17" />
|
||||||
|
<line x1="12" y1="17" x2="18" y2="17" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BotIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="square"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<rect x="6" y="8" width="12" height="10" />
|
||||||
|
<rect x="8" y="11" width="2" height="2" />
|
||||||
|
<rect x="14" y="11" width="2" height="2" />
|
||||||
|
<line x1="12" y1="5" x2="12" y2="8" />
|
||||||
|
<circle cx="12" cy="4" r="1" />
|
||||||
|
<line x1="6" y1="18" x2="4" y2="21" />
|
||||||
|
<line x1="18" y1="18" x2="20" y2="21" />
|
||||||
|
<line x1="9" y1="15" x2="15" y2="15" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DatabaseIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="square"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
||||||
|
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
|
||||||
|
<line x1="3" y1="12" x2="21" y2="12" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SecurityIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="square"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path d="M12 2L4 6v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V6l-8-4z" />
|
||||||
|
<path d="M9 12l2 2 4-4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GitBranchIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="square"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<circle cx="6" cy="6" r="3" />
|
||||||
|
<circle cx="18" cy="18" r="3" />
|
||||||
|
<line x1="6" y1="9" x2="6" y2="15" />
|
||||||
|
<path d="M18 15c0-3-3-4.5-6-4.5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServerIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="square"
|
||||||
|
strokeLinejoin="miter"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<rect x="2" y="2" width="20" height="6" />
|
||||||
|
<rect x="2" y="10" width="20" height="6" />
|
||||||
|
<rect x="2" y="18" width="20" height="4" />
|
||||||
|
<circle cx="6" cy="5" r="1" />
|
||||||
|
<circle cx="6" cy="13" r="1" />
|
||||||
|
<circle cx="6" cy="20" r="1" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
416
bandit-runner-app/src/components/terminal-chat-interface.tsx
Normal file
416
bandit-runner-app/src/components/terminal-chat-interface.tsx
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
import { useState, useRef, useEffect } from "react"
|
||||||
|
import { Github } from "lucide-react"
|
||||||
|
import { Input } from "@/components/ui/shadcn-io/input"
|
||||||
|
import { ScrollArea } from "@/components/ui/shadcn-io/scroll-area"
|
||||||
|
import { ThemeToggle } from "@/components/theme-toggle"
|
||||||
|
import { SecurityIcon } from "@/components/retro-icons"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface TerminalLine {
|
||||||
|
type: "input" | "output" | "error"
|
||||||
|
content: string
|
||||||
|
timestamp: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
type: "user" | "agent" | "typing"
|
||||||
|
content: string
|
||||||
|
timestamp: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCAN_LINES_OVERLAY =
|
||||||
|
"absolute inset-0 pointer-events-none bg-[linear-gradient(transparent_50%,hsl(var(--primary)/0.03)_50%)] bg-[length:100%_4px] animate-scan-lines"
|
||||||
|
|
||||||
|
const GRID_PATTERN =
|
||||||
|
"absolute inset-0 pointer-events-none opacity-[0.015] bg-[linear-gradient(hsl(var(--primary))_1px,transparent_1px),linear-gradient(90deg,hsl(var(--primary))_1px,transparent_1px)] bg-[size:20px_20px]"
|
||||||
|
|
||||||
|
export function TerminalChatInterface() {
|
||||||
|
const [terminalLines, setTerminalLines] = useState<TerminalLine[]>([
|
||||||
|
{ type: "output", content: "Bandit Runner Console v1.0", timestamp: new Date() },
|
||||||
|
{ type: "output", content: "System initialized. Ready for commands.", timestamp: new Date() },
|
||||||
|
{ type: "output", content: "", timestamp: new Date() },
|
||||||
|
])
|
||||||
|
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
|
||||||
|
{ type: "agent", content: "Agent ready. Awaiting commands...", timestamp: new Date() },
|
||||||
|
])
|
||||||
|
const [currentCommand, setCurrentCommand] = useState("")
|
||||||
|
const [chatInput, setChatInput] = useState("")
|
||||||
|
const [commandHistory, setCommandHistory] = useState<string[]>([])
|
||||||
|
const [historyIndex, setHistoryIndex] = useState(-1)
|
||||||
|
const [isTyping, setIsTyping] = useState(false)
|
||||||
|
const [sessionTime, setSessionTime] = useState("")
|
||||||
|
const [focusedPanel, setFocusedPanel] = useState<"terminal" | "chat">("terminal")
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
const terminalEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
const chatEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
const terminalInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const chatInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
setSessionTime(new Date().toLocaleTimeString())
|
||||||
|
terminalInputRef.current?.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
terminalEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}, [terminalLines])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
chatEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}, [chatMessages])
|
||||||
|
|
||||||
|
const formatTimestamp = (date: Date) => {
|
||||||
|
return date.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCommandSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!currentCommand.trim()) return
|
||||||
|
|
||||||
|
const command = currentCommand.trim()
|
||||||
|
setCommandHistory((prev) => [...prev, command])
|
||||||
|
setHistoryIndex(-1)
|
||||||
|
|
||||||
|
setTerminalLines((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ type: "input", content: `$ ${command}`, timestamp: new Date() },
|
||||||
|
])
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setTerminalLines((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
type: "output",
|
||||||
|
content: `Executing: ${command}...`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "output",
|
||||||
|
content: `Command completed successfully.`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
},
|
||||||
|
{ type: "output", content: "", timestamp: new Date() },
|
||||||
|
])
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
setCurrentCommand("")
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChatSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!chatInput.trim()) return
|
||||||
|
|
||||||
|
const message = chatInput.trim()
|
||||||
|
setChatMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
type: "user",
|
||||||
|
content: message,
|
||||||
|
timestamp: new Date(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
setChatInput("")
|
||||||
|
setIsTyping(true)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsTyping(false)
|
||||||
|
setChatMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
type: "agent",
|
||||||
|
content: `Processing: "${message}"`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCommandKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault()
|
||||||
|
if (commandHistory.length === 0) return
|
||||||
|
const newIndex = historyIndex === -1 ? commandHistory.length - 1 : Math.max(0, historyIndex - 1)
|
||||||
|
setHistoryIndex(newIndex)
|
||||||
|
setCurrentCommand(commandHistory[newIndex])
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault()
|
||||||
|
if (historyIndex === -1) return
|
||||||
|
const newIndex = historyIndex + 1
|
||||||
|
if (newIndex >= commandHistory.length) {
|
||||||
|
setHistoryIndex(-1)
|
||||||
|
setCurrentCommand("")
|
||||||
|
} else {
|
||||||
|
setHistoryIndex(newIndex)
|
||||||
|
setCurrentCommand(commandHistory[newIndex])
|
||||||
|
}
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
setFocusedPanel("chat")
|
||||||
|
chatInputRef.current?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChatKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setFocusedPanel("terminal")
|
||||||
|
terminalInputRef.current?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "j" && e.ctrlKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
setFocusedPanel("chat")
|
||||||
|
chatInputRef.current?.focus()
|
||||||
|
} else if (e.key === "k" && e.ctrlKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
setFocusedPanel("terminal")
|
||||||
|
terminalInputRef.current?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleGlobalKeyDown)
|
||||||
|
return () => window.removeEventListener("keydown", handleGlobalKeyDown)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen w-full bg-background flex flex-col font-mono overflow-hidden p-3 sm:p-6 md:p-8">
|
||||||
|
<div className="w-full max-w-[1900px] mx-auto flex flex-col h-full gap-4 sm:gap-6">
|
||||||
|
{/* Header with corner accents */}
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
{/* Corner brackets */}
|
||||||
|
<div className="absolute -left-1 -top-1 w-6 h-6 border-l-2 border-t-2 border-primary" />
|
||||||
|
<div className="absolute -right-1 -top-1 w-6 h-6 border-r-2 border-t-2 border-primary" />
|
||||||
|
<div className="absolute -left-1 -bottom-1 w-6 h-6 border-l-2 border-b-2 border-primary" />
|
||||||
|
<div className="absolute -right-1 -bottom-1 w-6 h-6 border-r-2 border-b-2 border-primary" />
|
||||||
|
|
||||||
|
<div className="border-l border-r border-border bg-card px-6 sm:px-8 py-3 sm:py-4">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3 sm:gap-4">
|
||||||
|
<div className="w-1 h-8 bg-primary" />
|
||||||
|
<SecurityIcon className="w-5 h-5 sm:w-6 sm:h-6 text-primary hidden xs:block" />
|
||||||
|
<div>
|
||||||
|
<div className="text-foreground text-sm sm:text-base font-bold tracking-wider">
|
||||||
|
BANDIT RUNNER
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-[10px] sm:text-xs">
|
||||||
|
Security Automation Console
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 sm:gap-4 text-[10px] sm:text-xs">
|
||||||
|
{mounted && (
|
||||||
|
<div className="hidden md:flex items-center gap-2">
|
||||||
|
<span className="text-muted-foreground">SESSION</span>
|
||||||
|
<span className="text-primary font-mono">{sessionTime}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-2 h-2 bg-accent-foreground" />
|
||||||
|
<span className="text-accent-foreground font-bold">ONLINE</span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://git.biohazardvfx.com/Nicholai/bandit-runner"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-8 h-8 flex items-center justify-center border border-border bg-card text-muted-foreground hover:text-primary hover:border-primary transition-colors"
|
||||||
|
title="View Repository"
|
||||||
|
>
|
||||||
|
<Github className="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content area */}
|
||||||
|
<div className="flex-1 grid grid-cols-1 lg:grid-cols-[1.2fr_0.8fr] gap-4 sm:gap-6 min-h-0">
|
||||||
|
{/* Terminal Panel */}
|
||||||
|
<div className="relative flex flex-col h-[55vh] lg:h-auto">
|
||||||
|
{/* Corner accents */}
|
||||||
|
<div className="absolute -left-1 -top-1 w-4 h-4 border-l-2 border-t-2 border-primary z-20" />
|
||||||
|
<div className="absolute -right-1 -top-1 w-4 h-4 border-r-2 border-t-2 border-primary z-20" />
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col border border-border bg-card relative overflow-hidden">
|
||||||
|
<div className={GRID_PATTERN} />
|
||||||
|
<div className={SCAN_LINES_OVERLAY} />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="relative z-10 flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-1 h-4 bg-primary" />
|
||||||
|
<span className="text-foreground text-xs font-bold tracking-widest">TERMINAL</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<div className="w-3 h-3 border border-primary/40" />
|
||||||
|
<div className="w-3 h-3 border border-primary/40" />
|
||||||
|
<div className="w-3 h-3 border border-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Terminal content */}
|
||||||
|
<ScrollArea className="flex-1 p-4 relative z-10">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{terminalLines.map((line, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={cn(
|
||||||
|
"text-xs md:text-sm leading-relaxed flex gap-3 font-mono",
|
||||||
|
line.type === "input" && "text-accent-foreground font-bold",
|
||||||
|
line.type === "output" && "text-foreground/80",
|
||||||
|
line.type === "error" && "text-destructive",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{line.content && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground text-[10px] sm:text-xs flex-shrink-0 w-20">
|
||||||
|
{formatTimestamp(line.timestamp)}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1">{line.content}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={terminalEndRef} />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Input area */}
|
||||||
|
<div className="border-t border-border p-3 bg-muted/20 relative z-10">
|
||||||
|
<form onSubmit={handleCommandSubmit} className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2 text-primary">
|
||||||
|
<div className="w-1 h-4 bg-primary" />
|
||||||
|
<span className="text-sm">$</span>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
ref={terminalInputRef}
|
||||||
|
value={currentCommand}
|
||||||
|
onChange={(e) => setCurrentCommand(e.target.value)}
|
||||||
|
onKeyDown={handleCommandKeyDown}
|
||||||
|
placeholder="enter command..."
|
||||||
|
className="flex-1 bg-transparent border-0 text-foreground placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0 font-mono text-sm h-6 px-0 caret-primary"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="border-t border-border px-3 py-1.5 bg-muted/30 relative z-10">
|
||||||
|
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||||
|
<span className="hidden sm:inline">user@bandit-runner</span>
|
||||||
|
<span>↑↓ history • ESC switch panels</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agent Panel */}
|
||||||
|
<div className="relative flex flex-col h-[40vh] lg:h-auto">
|
||||||
|
{/* Corner accents */}
|
||||||
|
<div className="absolute -left-1 -top-1 w-4 h-4 border-l-2 border-t-2 border-primary z-20" />
|
||||||
|
<div className="absolute -right-1 -top-1 w-4 h-4 border-r-2 border-t-2 border-primary z-20" />
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col border border-border bg-card relative overflow-hidden">
|
||||||
|
<div className={GRID_PATTERN} />
|
||||||
|
<div className={SCAN_LINES_OVERLAY} />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="relative z-10 flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-1 h-4 bg-accent-foreground" />
|
||||||
|
<span className="text-foreground text-xs font-bold tracking-widest">AGENT</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<div className="w-3 h-3 border border-accent-foreground/40" />
|
||||||
|
<div className="w-3 h-3 border border-accent-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
<ScrollArea className="flex-1 p-4 relative z-10">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{chatMessages.map((msg, idx) => (
|
||||||
|
<div key={idx} className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-[10px]">
|
||||||
|
<span className="text-muted-foreground font-mono">
|
||||||
|
{formatTimestamp(msg.timestamp)}
|
||||||
|
</span>
|
||||||
|
<div className="h-px flex-1 bg-border" />
|
||||||
|
<span className={cn(
|
||||||
|
"font-bold px-2 py-0.5 border",
|
||||||
|
msg.type === "user"
|
||||||
|
? "text-accent-foreground border-accent-foreground/30"
|
||||||
|
: "text-primary border-primary/30"
|
||||||
|
)}>
|
||||||
|
{msg.type === "user" ? "USER" : "AGENT"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn(
|
||||||
|
"text-xs md:text-sm leading-relaxed pl-4 border-l-2 font-mono",
|
||||||
|
msg.type === "user"
|
||||||
|
? "text-accent-foreground border-accent-foreground/30"
|
||||||
|
: "text-foreground/80 border-primary/30"
|
||||||
|
)}>
|
||||||
|
{msg.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{isTyping && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-[10px]">
|
||||||
|
<span className="text-muted-foreground font-mono">
|
||||||
|
{formatTimestamp(new Date())}
|
||||||
|
</span>
|
||||||
|
<div className="h-px flex-1 bg-border" />
|
||||||
|
<span className="text-primary border border-primary/30 font-bold px-2 py-0.5">
|
||||||
|
AGENT
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs md:text-sm text-foreground/60 pl-4 border-l-2 border-primary/30 flex items-center gap-2">
|
||||||
|
<span>Processing</span>
|
||||||
|
<span className="animate-cursor-blink text-primary">▊</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={chatEndRef} />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Input area */}
|
||||||
|
<div className="border-t border-border p-3 bg-muted/20 relative z-10">
|
||||||
|
<form onSubmit={handleChatSubmit} className="flex items-center gap-2 bg-background/40 px-3 py-2 border border-border/50">
|
||||||
|
<div className="flex items-center gap-2 text-primary">
|
||||||
|
<div className="w-1 h-4 bg-accent-foreground" />
|
||||||
|
<span className="text-sm">›</span>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
ref={chatInputRef}
|
||||||
|
value={chatInput}
|
||||||
|
onChange={(e) => setChatInput(e.target.value)}
|
||||||
|
onKeyDown={handleChatKeyDown}
|
||||||
|
placeholder="message agent..."
|
||||||
|
className="flex-1 bg-transparent border-0 text-foreground placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0 font-mono text-sm h-6 px-0 caret-accent-foreground"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="border-t border-border px-3 py-1.5 bg-muted/30 relative z-10">
|
||||||
|
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||||
|
<span>Ctrl+K/J nav</span>
|
||||||
|
<span className="hidden sm:inline">DeepSeek-V3</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
12
bandit-runner-app/src/components/theme-provider.tsx
Normal file
12
bandit-runner-app/src/components/theme-provider.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
||||||
|
|
||||||
40
bandit-runner-app/src/components/theme-toggle.tsx
Normal file
40
bandit-runner-app/src/components/theme-toggle.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Moon, Sun } from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
const [mounted, setMounted] = React.useState(false)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="w-8 h-8 flex items-center justify-center border border-border bg-card text-muted-foreground"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<div className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||||
|
className="w-8 h-8 flex items-center justify-center border border-primary/40 bg-card text-primary hover:border-primary transition-colors"
|
||||||
|
title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
|
||||||
|
>
|
||||||
|
{theme === "dark" ? (
|
||||||
|
<Sun className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Moon className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
65
bandit-runner-app/src/components/ui/shadcn-io/ai/actions.tsx
Normal file
65
bandit-runner-app/src/components/ui/shadcn-io/ai/actions.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/tooltip';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type ActionsProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const Actions = ({ className, children, ...props }: ActionsProps) => (
|
||||||
|
<div className={cn('flex items-center gap-1', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ActionProps = ComponentProps<typeof Button> & {
|
||||||
|
tooltip?: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Action = ({
|
||||||
|
tooltip,
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
variant = 'ghost',
|
||||||
|
size = 'sm',
|
||||||
|
...props
|
||||||
|
}: ActionProps) => {
|
||||||
|
const button = (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'size-9 p-1.5 text-muted-foreground hover:text-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
size={size}
|
||||||
|
type="button"
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<span className="sr-only">{label || tooltip}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{tooltip}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
};
|
||||||
212
bandit-runner-app/src/components/ui/shadcn-io/ai/branch.tsx
Normal file
212
bandit-runner-app/src/components/ui/shadcn-io/ai/branch.tsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { UIMessage } from 'ai';
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps, HTMLAttributes, ReactElement } from 'react';
|
||||||
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type BranchContextType = {
|
||||||
|
currentBranch: number;
|
||||||
|
totalBranches: number;
|
||||||
|
goToPrevious: () => void;
|
||||||
|
goToNext: () => void;
|
||||||
|
branches: ReactElement[];
|
||||||
|
setBranches: (branches: ReactElement[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BranchContext = createContext<BranchContextType | null>(null);
|
||||||
|
|
||||||
|
const useBranch = () => {
|
||||||
|
const context = useContext(BranchContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('Branch components must be used within Branch');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
defaultBranch?: number;
|
||||||
|
onBranchChange?: (branchIndex: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Branch = ({
|
||||||
|
defaultBranch = 0,
|
||||||
|
onBranchChange,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BranchProps) => {
|
||||||
|
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
||||||
|
const [branches, setBranches] = useState<ReactElement[]>([]);
|
||||||
|
|
||||||
|
const handleBranchChange = (newBranch: number) => {
|
||||||
|
setCurrentBranch(newBranch);
|
||||||
|
onBranchChange?.(newBranch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPrevious = () => {
|
||||||
|
const newBranch =
|
||||||
|
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
||||||
|
handleBranchChange(newBranch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToNext = () => {
|
||||||
|
const newBranch =
|
||||||
|
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
||||||
|
handleBranchChange(newBranch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue: BranchContextType = {
|
||||||
|
currentBranch,
|
||||||
|
totalBranches: branches.length,
|
||||||
|
goToPrevious,
|
||||||
|
goToNext,
|
||||||
|
branches,
|
||||||
|
setBranches,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BranchContext.Provider value={contextValue}>
|
||||||
|
<div
|
||||||
|
className={cn('grid w-full gap-2 [&>div]:pb-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</BranchContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchMessagesProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const BranchMessages = ({ children, ...props }: BranchMessagesProps) => {
|
||||||
|
const { currentBranch, setBranches, branches } = useBranch();
|
||||||
|
const childrenArray = Array.isArray(children) ? children : [children];
|
||||||
|
|
||||||
|
// Use useEffect to update branches when they change
|
||||||
|
useEffect(() => {
|
||||||
|
if (branches.length !== childrenArray.length) {
|
||||||
|
setBranches(childrenArray);
|
||||||
|
}
|
||||||
|
}, [childrenArray, branches, setBranches]);
|
||||||
|
|
||||||
|
return childrenArray.map((branch, index) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid gap-2 overflow-hidden [&>div]:pb-0',
|
||||||
|
index === currentBranch ? 'block' : 'hidden'
|
||||||
|
)}
|
||||||
|
key={branch.key}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{branch}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchSelectorProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
from: UIMessage['role'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BranchSelector = ({
|
||||||
|
className,
|
||||||
|
from,
|
||||||
|
...props
|
||||||
|
}: BranchSelectorProps) => {
|
||||||
|
const { totalBranches } = useBranch();
|
||||||
|
|
||||||
|
// Don't render if there's only one branch
|
||||||
|
if (totalBranches <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 self-end px-10',
|
||||||
|
from === 'assistant' ? 'justify-start' : 'justify-end',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchPreviousProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const BranchPrevious = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: BranchPreviousProps) => {
|
||||||
|
const { goToPrevious, totalBranches } = useBranch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
aria-label="Previous branch"
|
||||||
|
className={cn(
|
||||||
|
'size-7 shrink-0 rounded-full text-muted-foreground transition-colors',
|
||||||
|
'hover:bg-accent hover:text-foreground',
|
||||||
|
'disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={totalBranches <= 1}
|
||||||
|
onClick={goToPrevious}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronLeftIcon size={14} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchNextProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const BranchNext = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: BranchNextProps) => {
|
||||||
|
const { goToNext, totalBranches } = useBranch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
aria-label="Next branch"
|
||||||
|
className={cn(
|
||||||
|
'size-7 shrink-0 rounded-full text-muted-foreground transition-colors',
|
||||||
|
'hover:bg-accent hover:text-foreground',
|
||||||
|
'disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
disabled={totalBranches <= 1}
|
||||||
|
onClick={goToNext}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRightIcon size={14} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BranchPageProps = HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
|
export const BranchPage = ({ className, ...props }: BranchPageProps) => {
|
||||||
|
const { currentBranch, totalBranches } = useBranch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'font-medium text-muted-foreground text-xs tabular-nums',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{currentBranch + 1} of {totalBranches}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
148
bandit-runner-app/src/components/ui/shadcn-io/ai/code-block.tsx
Normal file
148
bandit-runner-app/src/components/ui/shadcn-io/ai/code-block.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/shadcn-io/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { CheckIcon, CopyIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps, HTMLAttributes, ReactNode } from 'react';
|
||||||
|
import { createContext, useContext, useState } from 'react';
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
|
import {
|
||||||
|
oneDark,
|
||||||
|
oneLight,
|
||||||
|
} from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||||
|
|
||||||
|
type CodeBlockContextType = {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||||
|
code: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
code: string;
|
||||||
|
language: string;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlock = ({
|
||||||
|
code,
|
||||||
|
language,
|
||||||
|
showLineNumbers = false,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CodeBlockProps) => (
|
||||||
|
<CodeBlockContext.Provider value={{ code }}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative w-full overflow-hidden rounded-md border bg-background text-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<SyntaxHighlighter
|
||||||
|
className="overflow-hidden dark:hidden"
|
||||||
|
codeTagProps={{
|
||||||
|
className: 'font-mono text-sm',
|
||||||
|
}}
|
||||||
|
customStyle={{
|
||||||
|
margin: 0,
|
||||||
|
padding: '1rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
color: 'hsl(var(--foreground))',
|
||||||
|
}}
|
||||||
|
language={language}
|
||||||
|
lineNumberStyle={{
|
||||||
|
color: 'hsl(var(--muted-foreground))',
|
||||||
|
paddingRight: '1rem',
|
||||||
|
minWidth: '2.5rem',
|
||||||
|
}}
|
||||||
|
showLineNumbers={showLineNumbers}
|
||||||
|
style={oneLight}
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
className="hidden overflow-hidden dark:block"
|
||||||
|
codeTagProps={{
|
||||||
|
className: 'font-mono text-sm',
|
||||||
|
}}
|
||||||
|
customStyle={{
|
||||||
|
margin: 0,
|
||||||
|
padding: '1rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
color: 'hsl(var(--foreground))',
|
||||||
|
}}
|
||||||
|
language={language}
|
||||||
|
lineNumberStyle={{
|
||||||
|
color: 'hsl(var(--muted-foreground))',
|
||||||
|
paddingRight: '1rem',
|
||||||
|
minWidth: '2.5rem',
|
||||||
|
}}
|
||||||
|
showLineNumbers={showLineNumbers}
|
||||||
|
style={oneDark}
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
{children && (
|
||||||
|
<div className="absolute top-2 right-2 flex items-center gap-2">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CodeBlockContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
||||||
|
onCopy?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockCopyButton = ({
|
||||||
|
onCopy,
|
||||||
|
onError,
|
||||||
|
timeout = 2000,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockCopyButtonProps) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const { code } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
if (typeof window === 'undefined' || !navigator.clipboard.writeText) {
|
||||||
|
onError?.(new Error('Clipboard API not available'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
setIsCopied(true);
|
||||||
|
onCopy?.();
|
||||||
|
setTimeout(() => setIsCopied(false), timeout);
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(error as Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn('shrink-0', className)}
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <Icon size={14} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { ArrowDownIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
|
||||||
|
|
||||||
|
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||||
|
|
||||||
|
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||||
|
<StickToBottom
|
||||||
|
className={cn('relative flex-1 overflow-y-auto', className)}
|
||||||
|
initial="smooth"
|
||||||
|
resize="smooth"
|
||||||
|
role="log"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ConversationContentProps = ComponentProps<
|
||||||
|
typeof StickToBottom.Content
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const ConversationContent = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ConversationContentProps) => (
|
||||||
|
<StickToBottom.Content className={cn('p-4', className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const ConversationScrollButton = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ConversationScrollButtonProps) => {
|
||||||
|
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
||||||
|
|
||||||
|
const handleScrollToBottom = useCallback(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [scrollToBottom]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
!isAtBottom && (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={handleScrollToBottom}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowDownIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
24
bandit-runner-app/src/components/ui/shadcn-io/ai/image.tsx
Normal file
24
bandit-runner-app/src/components/ui/shadcn-io/ai/image.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { Experimental_GeneratedImage } from 'ai';
|
||||||
|
|
||||||
|
export type ImageProps = Experimental_GeneratedImage & {
|
||||||
|
className?: string;
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Image = ({
|
||||||
|
base64,
|
||||||
|
uint8Array,
|
||||||
|
mediaType,
|
||||||
|
...props
|
||||||
|
}: ImageProps) => (
|
||||||
|
<img
|
||||||
|
{...props}
|
||||||
|
alt={props.alt}
|
||||||
|
className={cn(
|
||||||
|
'h-auto max-w-full overflow-hidden rounded-md',
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
src={`data:${mediaType};base64,${base64}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@ -0,0 +1,283 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Badge } from '@repo/shadcn-ui/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
type CarouselApi,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/carousel';
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/hover-card';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
|
||||||
|
import { type ComponentProps, useCallback, useEffect, useState, useRef, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
// Context to share carousel API with child components
|
||||||
|
const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
|
||||||
|
|
||||||
|
// Hook to access carousel API from the nearest InlineCitationCarousel parent
|
||||||
|
const useCarouselApi = () => {
|
||||||
|
const api = useContext(CarouselApiContext);
|
||||||
|
return api;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationProps = ComponentProps<'span'>;
|
||||||
|
|
||||||
|
export const InlineCitation = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationProps) => (
|
||||||
|
<span
|
||||||
|
className={cn('group inline items-center gap-1', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationTextProps = ComponentProps<'span'>;
|
||||||
|
|
||||||
|
export const InlineCitationText = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationTextProps) => (
|
||||||
|
<span
|
||||||
|
className={cn('transition-colors group-hover:bg-accent', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
|
||||||
|
|
||||||
|
export const InlineCitationCard = (props: InlineCitationCardProps) => (
|
||||||
|
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
|
||||||
|
sources: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InlineCitationCardTrigger = ({
|
||||||
|
sources,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCardTriggerProps) => (
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<Badge
|
||||||
|
className={cn('ml-1 rounded-full', className)}
|
||||||
|
variant="secondary"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{sources.length ? (
|
||||||
|
<>
|
||||||
|
{new URL(sources[0]).hostname}{' '}
|
||||||
|
{sources.length > 1 && `+${sources.length - 1}`}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'unknown'
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCardBodyProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCardBody = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCardBodyProps) => (
|
||||||
|
<HoverCardContent className={cn('relative w-80 p-0', className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;
|
||||||
|
|
||||||
|
export const InlineCitationCarousel = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselProps) => {
|
||||||
|
const [api, setApi] = useState<CarouselApi>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselApiContext.Provider value={api}>
|
||||||
|
<Carousel
|
||||||
|
className={cn('w-full', className)}
|
||||||
|
setApi={setApi}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Carousel>
|
||||||
|
</CarouselApiContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationCarouselContentProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselContent = (
|
||||||
|
props: InlineCitationCarouselContentProps
|
||||||
|
) => <CarouselContent {...props} />;
|
||||||
|
|
||||||
|
export type InlineCitationCarouselItemProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselItem = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselItemProps) => (
|
||||||
|
<CarouselItem className={cn('w-full space-y-2 p-4', className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCarouselHeaderProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselHeaderProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationCarouselIndexProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselIndex = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselIndexProps) => {
|
||||||
|
const api = useCarouselApi();
|
||||||
|
const [current, setCurrent] = useState(0);
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCount(api.scrollSnapList().length);
|
||||||
|
setCurrent(api.selectedScrollSnap() + 1);
|
||||||
|
|
||||||
|
api.on('select', () => {
|
||||||
|
setCurrent(api.selectedScrollSnap() + 1);
|
||||||
|
});
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? `${current}/${count}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationCarouselPrevProps = ComponentProps<'button'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselPrev = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselPrevProps) => {
|
||||||
|
const api = useCarouselApi();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (api) {
|
||||||
|
api.scrollPrev();
|
||||||
|
}
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-label="Previous"
|
||||||
|
className={cn('shrink-0', className)}
|
||||||
|
onClick={handleClick}
|
||||||
|
type="button"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon className="size-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationCarouselNextProps = ComponentProps<'button'>;
|
||||||
|
|
||||||
|
export const InlineCitationCarouselNext = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationCarouselNextProps) => {
|
||||||
|
const api = useCarouselApi();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (api) {
|
||||||
|
api.scrollNext();
|
||||||
|
}
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-label="Next"
|
||||||
|
className={cn('shrink-0', className)}
|
||||||
|
onClick={handleClick}
|
||||||
|
type="button"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon className="size-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InlineCitationSourceProps = ComponentProps<'div'> & {
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InlineCitationSource = ({
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: InlineCitationSourceProps) => (
|
||||||
|
<div className={cn('space-y-1', className)} {...props}>
|
||||||
|
{title && (
|
||||||
|
<h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
|
||||||
|
)}
|
||||||
|
{url && (
|
||||||
|
<p className="truncate break-all text-muted-foreground text-xs">{url}</p>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InlineCitationQuoteProps = ComponentProps<'blockquote'>;
|
||||||
|
|
||||||
|
export const InlineCitationQuote = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: InlineCitationQuoteProps) => (
|
||||||
|
<blockquote
|
||||||
|
className={cn(
|
||||||
|
'border-muted border-l-2 pl-3 text-muted-foreground text-sm italic',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
96
bandit-runner-app/src/components/ui/shadcn-io/ai/loader.tsx
Normal file
96
bandit-runner-app/src/components/ui/shadcn-io/ai/loader.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
type LoaderIconProps = {
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
|
||||||
|
<svg
|
||||||
|
height={size}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
style={{ color: 'currentcolor' }}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width={size}
|
||||||
|
>
|
||||||
|
<title>Loader</title>
|
||||||
|
<g clipPath="url(#clip0_2393_1490)">
|
||||||
|
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
<path
|
||||||
|
d="M8 16V12"
|
||||||
|
opacity="0.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.29773 1.52783L5.64887 4.7639"
|
||||||
|
opacity="0.9"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.7023 1.52783L10.3511 4.7639"
|
||||||
|
opacity="0.1"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12.7023 14.472L10.3511 11.236"
|
||||||
|
opacity="0.4"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3.29773 14.472L5.64887 11.236"
|
||||||
|
opacity="0.6"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15.6085 5.52783L11.8043 6.7639"
|
||||||
|
opacity="0.2"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0.391602 10.472L4.19583 9.23598"
|
||||||
|
opacity="0.7"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15.6085 10.4722L11.8043 9.2361"
|
||||||
|
opacity="0.3"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0.391602 5.52783L4.19583 6.7639"
|
||||||
|
opacity="0.8"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2393_1490">
|
||||||
|
<rect fill="white" height="16" width="16" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex animate-spin items-center justify-center',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<LoaderIcon size={size} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
64
bandit-runner-app/src/components/ui/shadcn-io/ai/message.tsx
Normal file
64
bandit-runner-app/src/components/ui/shadcn-io/ai/message.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from '@/components/ui/shadcn-io/avatar';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { UIMessage } from 'ai';
|
||||||
|
import type { ComponentProps, HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
from: UIMessage['role'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Message = ({ className, from, ...props }: MessageProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'group flex w-full items-end justify-end gap-2 py-4',
|
||||||
|
from === 'user' ? 'is-user' : 'is-assistant flex-row-reverse justify-end',
|
||||||
|
'[&>div]:max-w-[80%]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const MessageContent = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: MessageContentProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col gap-2 overflow-hidden rounded-lg px-4 py-3 text-foreground text-sm',
|
||||||
|
'group-[.is-user]:bg-primary group-[.is-user]:text-primary-foreground',
|
||||||
|
'group-[.is-assistant]:bg-secondary group-[.is-assistant]:text-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="is-user:dark">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type MessageAvatarProps = ComponentProps<typeof Avatar> & {
|
||||||
|
src: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MessageAvatar = ({
|
||||||
|
src,
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: MessageAvatarProps) => (
|
||||||
|
<Avatar
|
||||||
|
className={cn('size-8 ring ring-1 ring-border', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<AvatarImage alt="" className="mt-0 mb-0" src={src} />
|
||||||
|
<AvatarFallback>{name?.slice(0, 2) || 'ME'}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
@ -0,0 +1,225 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/select';
|
||||||
|
import { Textarea } from '@repo/shadcn-ui/components/ui/textarea';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { ChatStatus } from 'ai';
|
||||||
|
import { Loader2Icon, SendIcon, SquareIcon, XIcon } from 'lucide-react';
|
||||||
|
import type {
|
||||||
|
ComponentProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
} from 'react';
|
||||||
|
import { Children } from 'react';
|
||||||
|
|
||||||
|
export type PromptInputProps = HTMLAttributes<HTMLFormElement>;
|
||||||
|
|
||||||
|
export const PromptInput = ({ className, ...props }: PromptInputProps) => (
|
||||||
|
<form
|
||||||
|
className={cn(
|
||||||
|
'w-full divide-y overflow-hidden rounded-xl border bg-background shadow-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputTextareaProps = ComponentProps<typeof Textarea> & {
|
||||||
|
minHeight?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PromptInputTextarea = ({
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
placeholder = 'What would you like to know?',
|
||||||
|
minHeight = 48,
|
||||||
|
maxHeight = 164,
|
||||||
|
...props
|
||||||
|
}: PromptInputTextareaProps) => {
|
||||||
|
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
// Allow newline
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit on Enter (without Shift)
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.currentTarget.form;
|
||||||
|
if (form) {
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
className={cn(
|
||||||
|
'w-full resize-none rounded-none border-none p-3 shadow-none outline-none ring-0',
|
||||||
|
'field-sizing-content max-h-[6lh] bg-transparent dark:bg-transparent',
|
||||||
|
'focus-visible:ring-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
name="message"
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange?.(e);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PromptInputToolbarProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const PromptInputToolbar = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputToolbarProps) => (
|
||||||
|
<div
|
||||||
|
className={cn('flex items-center justify-between p-1', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const PromptInputTools = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputToolsProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1',
|
||||||
|
'[&_button:first-child]:rounded-bl-xl',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputButtonProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
export const PromptInputButton = ({
|
||||||
|
variant = 'ghost',
|
||||||
|
className,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: PromptInputButtonProps) => {
|
||||||
|
const newSize =
|
||||||
|
(size ?? Children.count(props.children) > 1) ? 'default' : 'icon';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 gap-1.5 rounded-lg',
|
||||||
|
variant === 'ghost' && 'text-muted-foreground',
|
||||||
|
newSize === 'default' && 'px-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
size={newSize}
|
||||||
|
type="button"
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PromptInputSubmitProps = ComponentProps<typeof Button> & {
|
||||||
|
status?: ChatStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PromptInputSubmit = ({
|
||||||
|
className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'icon',
|
||||||
|
status,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: PromptInputSubmitProps) => {
|
||||||
|
let Icon = <SendIcon className="size-4" />;
|
||||||
|
|
||||||
|
if (status === 'submitted') {
|
||||||
|
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
||||||
|
} else if (status === 'streaming') {
|
||||||
|
Icon = <SquareIcon className="size-4" />;
|
||||||
|
} else if (status === 'error') {
|
||||||
|
Icon = <XIcon className="size-4" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn('gap-1.5 rounded-lg', className)}
|
||||||
|
size={size}
|
||||||
|
type="submit"
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? Icon}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PromptInputModelSelectProps = ComponentProps<typeof Select>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelect = (props: PromptInputModelSelectProps) => (
|
||||||
|
<Select {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputModelSelectTriggerProps = ComponentProps<
|
||||||
|
typeof SelectTrigger
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelectTrigger = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputModelSelectTriggerProps) => (
|
||||||
|
<SelectTrigger
|
||||||
|
className={cn(
|
||||||
|
'border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors',
|
||||||
|
'hover:bg-accent hover:text-foreground [&[aria-expanded="true"]]:bg-accent [&[aria-expanded="true"]]:text-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputModelSelectContentProps = ComponentProps<
|
||||||
|
typeof SelectContent
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelectContent = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputModelSelectContentProps) => (
|
||||||
|
<SelectContent className={cn(className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputModelSelectItemProps = ComponentProps<typeof SelectItem>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelectItem = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputModelSelectItemProps) => (
|
||||||
|
<SelectItem className={cn(className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PromptInputModelSelectValueProps = ComponentProps<
|
||||||
|
typeof SelectValue
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const PromptInputModelSelectValue = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromptInputModelSelectValueProps) => (
|
||||||
|
<SelectValue className={cn(className)} {...props} />
|
||||||
|
);
|
||||||
180
bandit-runner-app/src/components/ui/shadcn-io/ai/reasoning.tsx
Normal file
180
bandit-runner-app/src/components/ui/shadcn-io/ai/reasoning.tsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/shadcn-io/collapsible';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { BrainIcon, ChevronDownIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import { createContext, memo, useContext, useEffect, useState } from 'react';
|
||||||
|
import { Response } from './response';
|
||||||
|
|
||||||
|
type ReasoningContextValue = {
|
||||||
|
isStreaming: boolean;
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (open: boolean) => void;
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
||||||
|
|
||||||
|
const useReasoning = () => {
|
||||||
|
const context = useContext(ReasoningContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('Reasoning components must be used within Reasoning');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
||||||
|
isStreaming?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTO_CLOSE_DELAY = 1000;
|
||||||
|
|
||||||
|
export const Reasoning = memo(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
isStreaming = false,
|
||||||
|
open,
|
||||||
|
defaultOpen = false,
|
||||||
|
onOpenChange,
|
||||||
|
duration: durationProp,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ReasoningProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useControllableState({
|
||||||
|
prop: open,
|
||||||
|
defaultProp: defaultOpen,
|
||||||
|
onChange: onOpenChange,
|
||||||
|
});
|
||||||
|
const [duration, setDuration] = useControllableState({
|
||||||
|
prop: durationProp,
|
||||||
|
defaultProp: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [hasAutoClosedRef, setHasAutoClosedRef] = useState(false);
|
||||||
|
const [startTime, setStartTime] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Track duration when streaming starts and ends
|
||||||
|
useEffect(() => {
|
||||||
|
if (isStreaming) {
|
||||||
|
if (startTime === null) {
|
||||||
|
setStartTime(Date.now());
|
||||||
|
}
|
||||||
|
} else if (startTime !== null) {
|
||||||
|
setDuration(Math.round((Date.now() - startTime) / 1000));
|
||||||
|
setStartTime(null);
|
||||||
|
}
|
||||||
|
}, [isStreaming, startTime, setDuration]);
|
||||||
|
|
||||||
|
// Auto-open when streaming starts, auto-close when streaming ends (once only)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isStreaming && !isOpen) {
|
||||||
|
setIsOpen(true);
|
||||||
|
} else if (!isStreaming && isOpen && !defaultOpen && !hasAutoClosedRef) {
|
||||||
|
// Add a small delay before closing to allow user to see the content
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setHasAutoClosedRef(true);
|
||||||
|
}, AUTO_CLOSE_DELAY);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosedRef]);
|
||||||
|
|
||||||
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
|
setIsOpen(newOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReasoningContext.Provider
|
||||||
|
value={{ isStreaming, isOpen, setIsOpen, duration }}
|
||||||
|
>
|
||||||
|
<Collapsible
|
||||||
|
className={cn('not-prose mb-4', className)}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
open={isOpen}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Collapsible>
|
||||||
|
</ReasoningContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ReasoningTriggerProps = ComponentProps<
|
||||||
|
typeof CollapsibleTrigger
|
||||||
|
> & {
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReasoningTrigger = memo(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
title = 'Reasoning',
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ReasoningTriggerProps) => {
|
||||||
|
const { isStreaming, isOpen, duration } = useReasoning();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleTrigger
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 text-muted-foreground text-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? (
|
||||||
|
<>
|
||||||
|
<BrainIcon className="size-4" />
|
||||||
|
{isStreaming || duration === 0 ? (
|
||||||
|
<p>Thinking...</p>
|
||||||
|
) : (
|
||||||
|
<p>Thought for {duration} seconds</p>
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={cn(
|
||||||
|
'size-4 text-muted-foreground transition-transform',
|
||||||
|
isOpen ? 'rotate-180' : 'rotate-0'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ReasoningContentProps = ComponentProps<
|
||||||
|
typeof CollapsibleContent
|
||||||
|
> & {
|
||||||
|
children: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReasoningContent = memo(
|
||||||
|
({ className, children, ...props }: ReasoningContentProps) => (
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'mt-4 text-sm',
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Response className="grid gap-2">{children}</Response>
|
||||||
|
</CollapsibleContent>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Reasoning.displayName = 'Reasoning';
|
||||||
|
ReasoningTrigger.displayName = 'ReasoningTrigger';
|
||||||
|
ReasoningContent.displayName = 'ReasoningContent';
|
||||||
392
bandit-runner-app/src/components/ui/shadcn-io/ai/response.tsx
Normal file
392
bandit-runner-app/src/components/ui/shadcn-io/ai/response.tsx
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { ComponentProps, HTMLAttributes } from 'react';
|
||||||
|
import { isValidElement, memo } from 'react';
|
||||||
|
import ReactMarkdown, { type Options } from 'react-markdown';
|
||||||
|
import rehypeKatex from 'rehype-katex';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkMath from 'remark-math';
|
||||||
|
import { CodeBlock, CodeBlockCopyButton } from './code-block';
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
import hardenReactMarkdown from 'harden-react-markdown';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses markdown text and removes incomplete tokens to prevent partial rendering
|
||||||
|
* of links, images, bold, and italic formatting during streaming.
|
||||||
|
*/
|
||||||
|
function parseIncompleteMarkdown(text: string): string {
|
||||||
|
if (!text || typeof text !== 'string') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = text;
|
||||||
|
|
||||||
|
// Handle incomplete links and images
|
||||||
|
// Pattern: [...] or ![...] where the closing ] is missing
|
||||||
|
const linkImagePattern = /(!?\[)([^\]]*?)$/;
|
||||||
|
const linkMatch = result.match(linkImagePattern);
|
||||||
|
if (linkMatch) {
|
||||||
|
// If we have an unterminated [ or ![, remove it and everything after
|
||||||
|
const startIndex = result.lastIndexOf(linkMatch[1]);
|
||||||
|
result = result.substring(0, startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete bold formatting (**)
|
||||||
|
const boldPattern = /(\*\*)([^*]*?)$/;
|
||||||
|
const boldMatch = result.match(boldPattern);
|
||||||
|
if (boldMatch) {
|
||||||
|
// Count the number of ** in the entire string
|
||||||
|
const asteriskPairs = (result.match(/\*\*/g) || []).length;
|
||||||
|
// If odd number of **, we have an incomplete bold - complete it
|
||||||
|
if (asteriskPairs % 2 === 1) {
|
||||||
|
result = `${result}**`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete italic formatting (__)
|
||||||
|
const italicPattern = /(__)([^_]*?)$/;
|
||||||
|
const italicMatch = result.match(italicPattern);
|
||||||
|
if (italicMatch) {
|
||||||
|
// Count the number of __ in the entire string
|
||||||
|
const underscorePairs = (result.match(/__/g) || []).length;
|
||||||
|
// If odd number of __, we have an incomplete italic - complete it
|
||||||
|
if (underscorePairs % 2 === 1) {
|
||||||
|
result = `${result}__`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete single asterisk italic (*)
|
||||||
|
const singleAsteriskPattern = /(\*)([^*]*?)$/;
|
||||||
|
const singleAsteriskMatch = result.match(singleAsteriskPattern);
|
||||||
|
if (singleAsteriskMatch) {
|
||||||
|
// Count single asterisks that aren't part of **
|
||||||
|
const singleAsterisks = result.split('').reduce((acc, char, index) => {
|
||||||
|
if (char === '*') {
|
||||||
|
// Check if it's part of a ** pair
|
||||||
|
const prevChar = result[index - 1];
|
||||||
|
const nextChar = result[index + 1];
|
||||||
|
if (prevChar !== '*' && nextChar !== '*') {
|
||||||
|
return acc + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// If odd number of single *, we have an incomplete italic - complete it
|
||||||
|
if (singleAsterisks % 2 === 1) {
|
||||||
|
result = `${result}*`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete single underscore italic (_)
|
||||||
|
const singleUnderscorePattern = /(_)([^_]*?)$/;
|
||||||
|
const singleUnderscoreMatch = result.match(singleUnderscorePattern);
|
||||||
|
if (singleUnderscoreMatch) {
|
||||||
|
// Count single underscores that aren't part of __
|
||||||
|
const singleUnderscores = result.split('').reduce((acc, char, index) => {
|
||||||
|
if (char === '_') {
|
||||||
|
// Check if it's part of a __ pair
|
||||||
|
const prevChar = result[index - 1];
|
||||||
|
const nextChar = result[index + 1];
|
||||||
|
if (prevChar !== '_' && nextChar !== '_') {
|
||||||
|
return acc + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// If odd number of single _, we have an incomplete italic - complete it
|
||||||
|
if (singleUnderscores % 2 === 1) {
|
||||||
|
result = `${result}_`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete inline code blocks (`) - but avoid code blocks (```)
|
||||||
|
const inlineCodePattern = /(`)([^`]*?)$/;
|
||||||
|
const inlineCodeMatch = result.match(inlineCodePattern);
|
||||||
|
if (inlineCodeMatch) {
|
||||||
|
// Check if we're dealing with a code block (triple backticks)
|
||||||
|
const hasCodeBlockStart = result.includes('```');
|
||||||
|
const codeBlockPattern = /```[\s\S]*?```/g;
|
||||||
|
const completeCodeBlocks = (result.match(codeBlockPattern) || []).length;
|
||||||
|
const allTripleBackticks = (result.match(/```/g) || []).length;
|
||||||
|
|
||||||
|
// If we have an odd number of ``` sequences, we're inside an incomplete code block
|
||||||
|
// In this case, don't complete inline code
|
||||||
|
const insideIncompleteCodeBlock = allTripleBackticks % 2 === 1;
|
||||||
|
|
||||||
|
if (!insideIncompleteCodeBlock) {
|
||||||
|
// Count the number of single backticks that are NOT part of triple backticks
|
||||||
|
let singleBacktickCount = 0;
|
||||||
|
for (let i = 0; i < result.length; i++) {
|
||||||
|
if (result[i] === '`') {
|
||||||
|
// Check if this backtick is part of a triple backtick sequence
|
||||||
|
const isTripleStart = result.substring(i, i + 3) === '```';
|
||||||
|
const isTripleMiddle =
|
||||||
|
i > 0 && result.substring(i - 1, i + 2) === '```';
|
||||||
|
const isTripleEnd = i > 1 && result.substring(i - 2, i + 1) === '```';
|
||||||
|
|
||||||
|
if (!(isTripleStart || isTripleMiddle || isTripleEnd)) {
|
||||||
|
singleBacktickCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If odd number of single backticks, we have an incomplete inline code - complete it
|
||||||
|
if (singleBacktickCount % 2 === 1) {
|
||||||
|
result = `${result}\``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incomplete strikethrough formatting (~~)
|
||||||
|
const strikethroughPattern = /(~~)([^~]*?)$/;
|
||||||
|
const strikethroughMatch = result.match(strikethroughPattern);
|
||||||
|
if (strikethroughMatch) {
|
||||||
|
// Count the number of ~~ in the entire string
|
||||||
|
const tildePairs = (result.match(/~~/g) || []).length;
|
||||||
|
// If odd number of ~~, we have an incomplete strikethrough - complete it
|
||||||
|
if (tildePairs % 2 === 1) {
|
||||||
|
result = `${result}~~`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a hardened version of ReactMarkdown
|
||||||
|
const HardenedMarkdown = hardenReactMarkdown(ReactMarkdown);
|
||||||
|
|
||||||
|
export type ResponseProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
options?: Options;
|
||||||
|
children: Options['children'];
|
||||||
|
allowedImagePrefixes?: ComponentProps<
|
||||||
|
ReturnType<typeof hardenReactMarkdown>
|
||||||
|
>['allowedImagePrefixes'];
|
||||||
|
allowedLinkPrefixes?: ComponentProps<
|
||||||
|
ReturnType<typeof hardenReactMarkdown>
|
||||||
|
>['allowedLinkPrefixes'];
|
||||||
|
defaultOrigin?: ComponentProps<
|
||||||
|
ReturnType<typeof hardenReactMarkdown>
|
||||||
|
>['defaultOrigin'];
|
||||||
|
parseIncompleteMarkdown?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const components: Options['components'] = {
|
||||||
|
ol: ({ node, children, className, ...props }) => (
|
||||||
|
<ol className={cn('ml-4 list-outside list-decimal', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
|
),
|
||||||
|
li: ({ node, children, className, ...props }) => (
|
||||||
|
<li className={cn('py-1', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
ul: ({ node, children, className, ...props }) => (
|
||||||
|
<ul className={cn('ml-4 list-outside list-disc', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
),
|
||||||
|
hr: ({ node, className, ...props }) => (
|
||||||
|
<hr className={cn('my-6 border-border', className)} {...props} />
|
||||||
|
),
|
||||||
|
strong: ({ node, children, className, ...props }) => (
|
||||||
|
<span className={cn('font-semibold', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
a: ({ node, children, className, ...props }) => (
|
||||||
|
<a
|
||||||
|
className={cn('font-medium text-primary underline', className)}
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
h1: ({ node, children, className, ...props }) => (
|
||||||
|
<h1
|
||||||
|
className={cn('mt-6 mb-2 font-semibold text-3xl', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
),
|
||||||
|
h2: ({ node, children, className, ...props }) => (
|
||||||
|
<h2
|
||||||
|
className={cn('mt-6 mb-2 font-semibold text-2xl', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
),
|
||||||
|
h3: ({ node, children, className, ...props }) => (
|
||||||
|
<h3 className={cn('mt-6 mb-2 font-semibold text-xl', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
),
|
||||||
|
h4: ({ node, children, className, ...props }) => (
|
||||||
|
<h4 className={cn('mt-6 mb-2 font-semibold text-lg', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</h4>
|
||||||
|
),
|
||||||
|
h5: ({ node, children, className, ...props }) => (
|
||||||
|
<h5
|
||||||
|
className={cn('mt-6 mb-2 font-semibold text-base', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h5>
|
||||||
|
),
|
||||||
|
h6: ({ node, children, className, ...props }) => (
|
||||||
|
<h6 className={cn('mt-6 mb-2 font-semibold text-sm', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</h6>
|
||||||
|
),
|
||||||
|
table: ({ node, children, className, ...props }) => (
|
||||||
|
<div className="my-4 overflow-x-auto">
|
||||||
|
<table
|
||||||
|
className={cn('w-full border-collapse border border-border', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ node, children, className, ...props }) => (
|
||||||
|
<thead className={cn('bg-muted/50', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</thead>
|
||||||
|
),
|
||||||
|
tbody: ({ node, children, className, ...props }) => (
|
||||||
|
<tbody className={cn('divide-y divide-border', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</tbody>
|
||||||
|
),
|
||||||
|
tr: ({ node, children, className, ...props }) => (
|
||||||
|
<tr className={cn('border-border border-b', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
),
|
||||||
|
th: ({ node, children, className, ...props }) => (
|
||||||
|
<th
|
||||||
|
className={cn('px-4 py-2 text-left font-semibold text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
),
|
||||||
|
td: ({ node, children, className, ...props }) => (
|
||||||
|
<td className={cn('px-4 py-2 text-sm', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
blockquote: ({ node, children, className, ...props }) => (
|
||||||
|
<blockquote
|
||||||
|
className={cn(
|
||||||
|
'my-4 border-muted-foreground/30 border-l-4 pl-4 text-muted-foreground italic',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
code: ({ node, className, ...props }) => {
|
||||||
|
const inline = node?.position?.start.line === node?.position?.end.line;
|
||||||
|
|
||||||
|
if (!inline) {
|
||||||
|
return <code className={className} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className={cn(
|
||||||
|
'rounded bg-muted px-1.5 py-0.5 font-mono text-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pre: ({ node, className, children }) => {
|
||||||
|
let language = 'javascript';
|
||||||
|
|
||||||
|
if (typeof node?.properties?.className === 'string') {
|
||||||
|
language = node.properties.className.replace('language-', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract code content from children safely
|
||||||
|
let code = '';
|
||||||
|
if (
|
||||||
|
isValidElement(children) &&
|
||||||
|
children.props &&
|
||||||
|
typeof (children.props as any).children === 'string'
|
||||||
|
) {
|
||||||
|
code = (children.props as any).children;
|
||||||
|
} else if (typeof children === 'string') {
|
||||||
|
code = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CodeBlock
|
||||||
|
className={cn('my-4 h-auto', className)}
|
||||||
|
code={code}
|
||||||
|
language={language}
|
||||||
|
>
|
||||||
|
<CodeBlockCopyButton
|
||||||
|
onCopy={() => console.log('Copied code to clipboard')}
|
||||||
|
onError={() => console.error('Failed to copy code to clipboard')}
|
||||||
|
/>
|
||||||
|
</CodeBlock>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Response = memo(
|
||||||
|
({
|
||||||
|
className,
|
||||||
|
options,
|
||||||
|
children,
|
||||||
|
allowedImagePrefixes,
|
||||||
|
allowedLinkPrefixes,
|
||||||
|
defaultOrigin,
|
||||||
|
parseIncompleteMarkdown: shouldParseIncompleteMarkdown = true,
|
||||||
|
...props
|
||||||
|
}: ResponseProps) => {
|
||||||
|
// Parse the children to remove incomplete markdown tokens if enabled
|
||||||
|
const parsedChildren =
|
||||||
|
typeof children === 'string' && shouldParseIncompleteMarkdown
|
||||||
|
? parseIncompleteMarkdown(children)
|
||||||
|
: children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<HardenedMarkdown
|
||||||
|
allowedImagePrefixes={allowedImagePrefixes ?? ['*']}
|
||||||
|
allowedLinkPrefixes={allowedLinkPrefixes ?? ['*']}
|
||||||
|
components={components}
|
||||||
|
defaultOrigin={defaultOrigin}
|
||||||
|
rehypePlugins={[rehypeKatex]}
|
||||||
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
|
{...options}
|
||||||
|
>
|
||||||
|
{parsedChildren}
|
||||||
|
</HardenedMarkdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(prevProps, nextProps) => prevProps.children === nextProps.children
|
||||||
|
);
|
||||||
|
|
||||||
|
Response.displayName = 'Response';
|
||||||
74
bandit-runner-app/src/components/ui/shadcn-io/ai/source.tsx
Normal file
74
bandit-runner-app/src/components/ui/shadcn-io/ai/source.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { BookIcon, ChevronDownIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type SourcesProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const Sources = ({ className, ...props }: SourcesProps) => (
|
||||||
|
<Collapsible
|
||||||
|
className={cn('not-prose mb-4 text-primary text-xs', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SourcesTrigger = ({
|
||||||
|
className,
|
||||||
|
count,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SourcesTriggerProps) => (
|
||||||
|
<CollapsibleTrigger className="flex items-center gap-2" {...props}>
|
||||||
|
{children ?? (
|
||||||
|
<>
|
||||||
|
<p className="font-medium">Used {count} sources</p>
|
||||||
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||||
|
|
||||||
|
export const SourcesContent = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: SourcesContentProps) => (
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'mt-3 flex w-fit flex-col gap-2',
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SourceProps = ComponentProps<'a'>;
|
||||||
|
|
||||||
|
export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
||||||
|
<a
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
href={href}
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? (
|
||||||
|
<>
|
||||||
|
<BookIcon className="h-4 w-4" />
|
||||||
|
<span className="block font-medium">{title}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import {
|
||||||
|
ScrollArea,
|
||||||
|
ScrollBar,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/scroll-area';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
||||||
|
|
||||||
|
export const Suggestions = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SuggestionsProps) => (
|
||||||
|
<ScrollArea className="w-full overflow-x-auto whitespace-nowrap" {...props}>
|
||||||
|
<div className={cn('flex w-max flex-nowrap items-center gap-2', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<ScrollBar className="hidden" orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SuggestionProps = Omit<ComponentProps<typeof Button>, 'onClick'> & {
|
||||||
|
suggestion: string;
|
||||||
|
onClick?: (suggestion: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Suggestion = ({
|
||||||
|
suggestion,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
variant = 'outline',
|
||||||
|
size = 'sm',
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SuggestionProps) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
onClick?.(suggestion);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn('cursor-pointer rounded-full px-4', className)}
|
||||||
|
onClick={handleClick}
|
||||||
|
size={size}
|
||||||
|
type="button"
|
||||||
|
variant={variant}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children || suggestion}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
94
bandit-runner-app/src/components/ui/shadcn-io/ai/task.tsx
Normal file
94
bandit-runner-app/src/components/ui/shadcn-io/ai/task.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { ChevronDownIcon, SearchIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export type TaskItemFileProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const TaskItemFile = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: TaskItemFileProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TaskItemProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const TaskItem = ({ children, className, ...props }: TaskItemProps) => (
|
||||||
|
<div className={cn('text-muted-foreground text-sm', className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TaskProps = ComponentProps<typeof Collapsible>;
|
||||||
|
|
||||||
|
export const Task = ({
|
||||||
|
defaultOpen = true,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: TaskProps) => (
|
||||||
|
<Collapsible
|
||||||
|
className={cn(
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
defaultOpen={defaultOpen}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TaskTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TaskTrigger = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
...props
|
||||||
|
}: TaskTriggerProps) => (
|
||||||
|
<CollapsibleTrigger asChild className={cn('group', className)} {...props}>
|
||||||
|
{children ?? (
|
||||||
|
<div className="flex cursor-pointer items-center gap-2 text-muted-foreground hover:text-foreground">
|
||||||
|
<SearchIcon className="size-4" />
|
||||||
|
<p className="text-sm">{title}</p>
|
||||||
|
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type TaskContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||||
|
|
||||||
|
export const TaskContent = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: TaskContentProps) => (
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="mt-4 space-y-2 border-muted border-l-2 pl-4">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
);
|
||||||
142
bandit-runner-app/src/components/ui/shadcn-io/ai/tool.tsx
Normal file
142
bandit-runner-app/src/components/ui/shadcn-io/ai/tool.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/shadcn-io/badge';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/shadcn-io/collapsible';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { ToolUIPart } from 'ai';
|
||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
CircleIcon,
|
||||||
|
ClockIcon,
|
||||||
|
WrenchIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import type { ComponentProps, ReactNode } from 'react';
|
||||||
|
import { CodeBlock } from './code-block';
|
||||||
|
|
||||||
|
export type ToolProps = ComponentProps<typeof Collapsible>;
|
||||||
|
|
||||||
|
export const Tool = ({ className, ...props }: ToolProps) => (
|
||||||
|
<Collapsible
|
||||||
|
className={cn('not-prose mb-4 w-full rounded-md border', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ToolHeaderProps = {
|
||||||
|
type: ToolUIPart['type'];
|
||||||
|
state: ToolUIPart['state'];
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: ToolUIPart['state']) => {
|
||||||
|
const labels = {
|
||||||
|
'input-streaming': 'Pending',
|
||||||
|
'input-available': 'Running',
|
||||||
|
'output-available': 'Completed',
|
||||||
|
'output-error': 'Error',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
'input-streaming': <CircleIcon className="size-4" />,
|
||||||
|
'input-available': <ClockIcon className="size-4 animate-pulse" />,
|
||||||
|
'output-available': <CheckCircleIcon className="size-4 text-green-600" />,
|
||||||
|
'output-error': <XCircleIcon className="size-4 text-red-600" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge className="rounded-full text-xs" variant="secondary">
|
||||||
|
{icons[status]}
|
||||||
|
{labels[status]}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToolHeader = ({
|
||||||
|
className,
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
...props
|
||||||
|
}: ToolHeaderProps) => (
|
||||||
|
<CollapsibleTrigger
|
||||||
|
className={cn(
|
||||||
|
'flex w-full items-center justify-between gap-4 p-3',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<WrenchIcon className="size-4 text-muted-foreground" />
|
||||||
|
<span className="font-medium text-sm">{type}</span>
|
||||||
|
{getStatusBadge(state)}
|
||||||
|
</div>
|
||||||
|
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
|
||||||
|
|
||||||
|
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ToolInputProps = ComponentProps<'div'> & {
|
||||||
|
input: ToolUIPart['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
||||||
|
<div className={cn('space-y-2 overflow-hidden p-4', className)} {...props}>
|
||||||
|
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
Parameters
|
||||||
|
</h4>
|
||||||
|
<div className="rounded-md bg-muted/50">
|
||||||
|
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ToolOutputProps = ComponentProps<'div'> & {
|
||||||
|
output: ReactNode;
|
||||||
|
errorText: ToolUIPart['errorText'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToolOutput = ({
|
||||||
|
className,
|
||||||
|
output,
|
||||||
|
errorText,
|
||||||
|
...props
|
||||||
|
}: ToolOutputProps) => {
|
||||||
|
if (!(output || errorText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('space-y-2 p-4', className)} {...props}>
|
||||||
|
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||||
|
{errorText ? 'Error' : 'Result'}
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'overflow-x-auto rounded-md text-xs [&_table]:w-full',
|
||||||
|
errorText
|
||||||
|
? 'bg-destructive/10 text-destructive'
|
||||||
|
: 'bg-muted/50 text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{errorText && <div>{errorText}</div>}
|
||||||
|
{output && <div>{output}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
269
bandit-runner-app/src/components/ui/shadcn-io/ai/web-preview.tsx
Normal file
269
bandit-runner-app/src/components/ui/shadcn-io/ai/web-preview.tsx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||||
|
import { Input } from '@repo/shadcn-ui/components/ui/input';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@repo/shadcn-ui/components/ui/tooltip';
|
||||||
|
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||||
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
|
import type { ComponentProps, ReactNode } from 'react';
|
||||||
|
import { createContext, useContext, useState } from 'react';
|
||||||
|
|
||||||
|
export type WebPreviewContextValue = {
|
||||||
|
url: string;
|
||||||
|
setUrl: (url: string) => void;
|
||||||
|
consoleOpen: boolean;
|
||||||
|
setConsoleOpen: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null);
|
||||||
|
|
||||||
|
const useWebPreview = () => {
|
||||||
|
const context = useContext(WebPreviewContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('WebPreview components must be used within a WebPreview');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebPreviewProps = ComponentProps<'div'> & {
|
||||||
|
defaultUrl?: string;
|
||||||
|
onUrlChange?: (url: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WebPreview = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
defaultUrl = '',
|
||||||
|
onUrlChange,
|
||||||
|
...props
|
||||||
|
}: WebPreviewProps) => {
|
||||||
|
const [url, setUrl] = useState(defaultUrl);
|
||||||
|
const [consoleOpen, setConsoleOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleUrlChange = (newUrl: string) => {
|
||||||
|
setUrl(newUrl);
|
||||||
|
onUrlChange?.(newUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue: WebPreviewContextValue = {
|
||||||
|
url,
|
||||||
|
setUrl: handleUrlChange,
|
||||||
|
consoleOpen,
|
||||||
|
setConsoleOpen,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebPreviewContext.Provider value={contextValue}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex size-full flex-col rounded-lg border bg-card',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</WebPreviewContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebPreviewNavigationProps = ComponentProps<'div'>;
|
||||||
|
|
||||||
|
export const WebPreviewNavigation = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: WebPreviewNavigationProps) => (
|
||||||
|
<div
|
||||||
|
className={cn('flex items-center gap-1 border-b p-2', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
|
||||||
|
tooltip?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WebPreviewNavigationButton = ({
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: WebPreviewNavigationButtonProps) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="h-8 w-8 p-0 hover:text-foreground"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{tooltip}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type WebPreviewUrlProps = ComponentProps<typeof Input>;
|
||||||
|
|
||||||
|
export const WebPreviewUrl = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onKeyDown,
|
||||||
|
...props
|
||||||
|
}: WebPreviewUrlProps) => {
|
||||||
|
const { url, setUrl } = useWebPreview();
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
setUrl(target.value);
|
||||||
|
}
|
||||||
|
onKeyDown?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use defaultValue for uncontrolled input when no onChange is provided
|
||||||
|
if (!onChange && !value) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
className="h-8 flex-1 text-sm"
|
||||||
|
defaultValue={url}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Enter URL..."
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
className="h-8 flex-1 text-sm"
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Enter URL..."
|
||||||
|
value={value ?? url}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebPreviewBodyProps = ComponentProps<'iframe'> & {
|
||||||
|
loading?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WebPreviewBody = ({
|
||||||
|
className,
|
||||||
|
loading,
|
||||||
|
src,
|
||||||
|
...props
|
||||||
|
}: WebPreviewBodyProps) => {
|
||||||
|
const { url } = useWebPreview();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1">
|
||||||
|
<iframe
|
||||||
|
className={cn('size-full', className)}
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
|
||||||
|
src={(src ?? url) || undefined}
|
||||||
|
title="Preview"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{loading}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebPreviewConsoleProps = ComponentProps<'div'> & {
|
||||||
|
logs?: Array<{
|
||||||
|
level: 'log' | 'warn' | 'error';
|
||||||
|
message: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WebPreviewConsole = ({
|
||||||
|
className,
|
||||||
|
logs = [],
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: WebPreviewConsoleProps) => {
|
||||||
|
const { consoleOpen, setConsoleOpen } = useWebPreview();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
className={cn('border-t bg-muted/50 font-mono text-sm', className)}
|
||||||
|
onOpenChange={setConsoleOpen}
|
||||||
|
open={consoleOpen}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="flex w-full items-center justify-between p-4 text-left font-medium hover:bg-muted/50"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
Console
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={cn(
|
||||||
|
'h-4 w-4 transition-transform duration-200',
|
||||||
|
consoleOpen && 'rotate-180'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent
|
||||||
|
className={cn(
|
||||||
|
'px-4 pb-4',
|
||||||
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="max-h-48 space-y-1 overflow-y-auto">
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground">No console output</p>
|
||||||
|
) : (
|
||||||
|
logs.map((log, index) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-xs',
|
||||||
|
log.level === 'error' && 'text-destructive',
|
||||||
|
log.level === 'warn' && 'text-yellow-600',
|
||||||
|
log.level === 'log' && 'text-foreground'
|
||||||
|
)}
|
||||||
|
key={`${log.timestamp.getTime()}-${index}`}
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{log.timestamp.toLocaleTimeString()}
|
||||||
|
</span>{' '}
|
||||||
|
{log.message}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { buttonVariants } from "@/components/ui/shadcn-io/button"
|
||||||
|
|
||||||
function AlertDialog({
|
function AlertDialog({
|
||||||
...props
|
...props
|
||||||
53
bandit-runner-app/src/components/ui/shadcn-io/avatar.tsx
Normal file
53
bandit-runner-app/src/components/ui/shadcn-io/avatar.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
className={cn(
|
||||||
|
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn("aspect-square size-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
@ -0,0 +1,620 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type IconType,
|
||||||
|
SiAstro,
|
||||||
|
SiBiome,
|
||||||
|
SiBower,
|
||||||
|
SiBun,
|
||||||
|
SiC,
|
||||||
|
SiCircleci,
|
||||||
|
SiCoffeescript,
|
||||||
|
SiCplusplus,
|
||||||
|
SiCss,
|
||||||
|
SiCssmodules,
|
||||||
|
SiDart,
|
||||||
|
SiDocker,
|
||||||
|
SiDocusaurus,
|
||||||
|
SiDotenv,
|
||||||
|
SiEditorconfig,
|
||||||
|
SiEslint,
|
||||||
|
SiGatsby,
|
||||||
|
SiGitignoredotio,
|
||||||
|
SiGnubash,
|
||||||
|
SiGo,
|
||||||
|
SiGraphql,
|
||||||
|
SiGrunt,
|
||||||
|
SiGulp,
|
||||||
|
SiHandlebarsdotjs,
|
||||||
|
SiHtml5,
|
||||||
|
SiJavascript,
|
||||||
|
SiJest,
|
||||||
|
SiJson,
|
||||||
|
SiLess,
|
||||||
|
SiMarkdown,
|
||||||
|
SiMdx,
|
||||||
|
SiMintlify,
|
||||||
|
SiMocha,
|
||||||
|
SiMysql,
|
||||||
|
SiNextdotjs,
|
||||||
|
SiPerl,
|
||||||
|
SiPhp,
|
||||||
|
SiPostcss,
|
||||||
|
SiPrettier,
|
||||||
|
SiPrisma,
|
||||||
|
SiPug,
|
||||||
|
SiPython,
|
||||||
|
SiR,
|
||||||
|
SiReact,
|
||||||
|
SiReadme,
|
||||||
|
SiRedis,
|
||||||
|
SiRemix,
|
||||||
|
SiRive,
|
||||||
|
SiRollupdotjs,
|
||||||
|
SiRuby,
|
||||||
|
SiSanity,
|
||||||
|
SiSass,
|
||||||
|
SiScala,
|
||||||
|
SiSentry,
|
||||||
|
SiShadcnui,
|
||||||
|
SiStorybook,
|
||||||
|
SiStylelint,
|
||||||
|
SiSublimetext,
|
||||||
|
SiSvelte,
|
||||||
|
SiSvg,
|
||||||
|
SiSwift,
|
||||||
|
SiTailwindcss,
|
||||||
|
SiToml,
|
||||||
|
SiTypescript,
|
||||||
|
SiVercel,
|
||||||
|
SiVite,
|
||||||
|
SiVuedotjs,
|
||||||
|
SiWebassembly,
|
||||||
|
} from '@icons-pack/react-simple-icons';
|
||||||
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
||||||
|
import { CheckIcon, CopyIcon } from 'lucide-react';
|
||||||
|
import type {
|
||||||
|
ComponentProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
cloneElement,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { Button } from '@/components/ui/shadcn-io/button';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/shadcn-io/select';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export type BundledLanguage = string;
|
||||||
|
|
||||||
|
const filenameIconMap = {
|
||||||
|
'.env': SiDotenv,
|
||||||
|
'*.astro': SiAstro,
|
||||||
|
'biome.json': SiBiome,
|
||||||
|
'.bowerrc': SiBower,
|
||||||
|
'bun.lockb': SiBun,
|
||||||
|
'*.c': SiC,
|
||||||
|
'*.cpp': SiCplusplus,
|
||||||
|
'.circleci/config.yml': SiCircleci,
|
||||||
|
'*.coffee': SiCoffeescript,
|
||||||
|
'*.module.css': SiCssmodules,
|
||||||
|
'*.css': SiCss,
|
||||||
|
'*.dart': SiDart,
|
||||||
|
Dockerfile: SiDocker,
|
||||||
|
'docusaurus.config.js': SiDocusaurus,
|
||||||
|
'.editorconfig': SiEditorconfig,
|
||||||
|
'.eslintrc': SiEslint,
|
||||||
|
'eslint.config.*': SiEslint,
|
||||||
|
'gatsby-config.*': SiGatsby,
|
||||||
|
'.gitignore': SiGitignoredotio,
|
||||||
|
'*.go': SiGo,
|
||||||
|
'*.graphql': SiGraphql,
|
||||||
|
'*.sh': SiGnubash,
|
||||||
|
'Gruntfile.*': SiGrunt,
|
||||||
|
'gulpfile.*': SiGulp,
|
||||||
|
'*.hbs': SiHandlebarsdotjs,
|
||||||
|
'*.html': SiHtml5,
|
||||||
|
'*.js': SiJavascript,
|
||||||
|
'*.json': SiJson,
|
||||||
|
'*.test.js': SiJest,
|
||||||
|
'*.less': SiLess,
|
||||||
|
'*.md': SiMarkdown,
|
||||||
|
'*.mdx': SiMdx,
|
||||||
|
'mintlify.json': SiMintlify,
|
||||||
|
'mocha.opts': SiMocha,
|
||||||
|
'*.mustache': SiHandlebarsdotjs,
|
||||||
|
'*.sql': SiMysql,
|
||||||
|
'next.config.*': SiNextdotjs,
|
||||||
|
'*.pl': SiPerl,
|
||||||
|
'*.php': SiPhp,
|
||||||
|
'postcss.config.*': SiPostcss,
|
||||||
|
'prettier.config.*': SiPrettier,
|
||||||
|
'*.prisma': SiPrisma,
|
||||||
|
'*.pug': SiPug,
|
||||||
|
'*.py': SiPython,
|
||||||
|
'*.r': SiR,
|
||||||
|
'*.rb': SiRuby,
|
||||||
|
'*.jsx': SiReact,
|
||||||
|
'*.tsx': SiReact,
|
||||||
|
'readme.md': SiReadme,
|
||||||
|
'*.rdb': SiRedis,
|
||||||
|
'remix.config.*': SiRemix,
|
||||||
|
'*.riv': SiRive,
|
||||||
|
'rollup.config.*': SiRollupdotjs,
|
||||||
|
'sanity.config.*': SiSanity,
|
||||||
|
'*.sass': SiSass,
|
||||||
|
'*.scss': SiSass,
|
||||||
|
'*.sc': SiScala,
|
||||||
|
'*.scala': SiScala,
|
||||||
|
'sentry.client.config.*': SiSentry,
|
||||||
|
'components.json': SiShadcnui,
|
||||||
|
'storybook.config.*': SiStorybook,
|
||||||
|
'stylelint.config.*': SiStylelint,
|
||||||
|
'.sublime-settings': SiSublimetext,
|
||||||
|
'*.svelte': SiSvelte,
|
||||||
|
'*.svg': SiSvg,
|
||||||
|
'*.swift': SiSwift,
|
||||||
|
'tailwind.config.*': SiTailwindcss,
|
||||||
|
'*.toml': SiToml,
|
||||||
|
'*.ts': SiTypescript,
|
||||||
|
'vercel.json': SiVercel,
|
||||||
|
'vite.config.*': SiVite,
|
||||||
|
'*.vue': SiVuedotjs,
|
||||||
|
'*.wasm': SiWebassembly,
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineNumberClassNames = cn(
|
||||||
|
'[&_code]:[counter-reset:line]',
|
||||||
|
'[&_code]:[counter-increment:line_0]',
|
||||||
|
'[&_.line]:before:content-[counter(line)]',
|
||||||
|
'[&_.line]:before:inline-block',
|
||||||
|
'[&_.line]:before:[counter-increment:line]',
|
||||||
|
'[&_.line]:before:w-4',
|
||||||
|
'[&_.line]:before:mr-4',
|
||||||
|
'[&_.line]:before:text-[13px]',
|
||||||
|
'[&_.line]:before:text-right',
|
||||||
|
'[&_.line]:before:text-muted-foreground/50',
|
||||||
|
'[&_.line]:before:font-mono',
|
||||||
|
'[&_.line]:before:select-none'
|
||||||
|
);
|
||||||
|
|
||||||
|
const darkModeClassNames = cn(
|
||||||
|
'dark:[&_.shiki]:!text-[var(--shiki-dark)]',
|
||||||
|
'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]',
|
||||||
|
'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]',
|
||||||
|
'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]',
|
||||||
|
'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]',
|
||||||
|
'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]',
|
||||||
|
'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]',
|
||||||
|
'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]',
|
||||||
|
'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]'
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineHighlightClassNames = cn(
|
||||||
|
'[&_.line.highlighted]:bg-blue-50',
|
||||||
|
'[&_.line.highlighted]:after:bg-blue-500',
|
||||||
|
'[&_.line.highlighted]:after:absolute',
|
||||||
|
'[&_.line.highlighted]:after:left-0',
|
||||||
|
'[&_.line.highlighted]:after:top-0',
|
||||||
|
'[&_.line.highlighted]:after:bottom-0',
|
||||||
|
'[&_.line.highlighted]:after:w-0.5',
|
||||||
|
'dark:[&_.line.highlighted]:!bg-blue-500/10'
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineDiffClassNames = cn(
|
||||||
|
'[&_.line.diff]:after:absolute',
|
||||||
|
'[&_.line.diff]:after:left-0',
|
||||||
|
'[&_.line.diff]:after:top-0',
|
||||||
|
'[&_.line.diff]:after:bottom-0',
|
||||||
|
'[&_.line.diff]:after:w-0.5',
|
||||||
|
'[&_.line.diff.add]:bg-emerald-50',
|
||||||
|
'[&_.line.diff.add]:after:bg-emerald-500',
|
||||||
|
'[&_.line.diff.remove]:bg-rose-50',
|
||||||
|
'[&_.line.diff.remove]:after:bg-rose-500',
|
||||||
|
'dark:[&_.line.diff.add]:!bg-emerald-500/10',
|
||||||
|
'dark:[&_.line.diff.remove]:!bg-rose-500/10'
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineFocusedClassNames = cn(
|
||||||
|
'[&_code:has(.focused)_.line]:blur-[2px]',
|
||||||
|
'[&_code:has(.focused)_.line.focused]:blur-none'
|
||||||
|
);
|
||||||
|
|
||||||
|
const wordHighlightClassNames = cn(
|
||||||
|
'[&_.highlighted-word]:bg-blue-50',
|
||||||
|
'dark:[&_.highlighted-word]:!bg-blue-500/10'
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeBlockClassName = cn(
|
||||||
|
'mt-0 bg-background text-sm',
|
||||||
|
'[&_pre]:py-4',
|
||||||
|
'[&_.shiki]:!bg-[var(--shiki-bg)]',
|
||||||
|
'[&_code]:w-full',
|
||||||
|
'[&_code]:grid',
|
||||||
|
'[&_code]:overflow-x-auto',
|
||||||
|
'[&_code]:bg-transparent',
|
||||||
|
'[&_.line]:px-4',
|
||||||
|
'[&_.line]:w-full',
|
||||||
|
'[&_.line]:relative'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
type CodeBlockData = {
|
||||||
|
language: string;
|
||||||
|
filename: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CodeBlockContextType = {
|
||||||
|
value: string | undefined;
|
||||||
|
onValueChange: ((value: string) => void) | undefined;
|
||||||
|
data: CodeBlockData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||||
|
value: undefined,
|
||||||
|
onValueChange: undefined,
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
defaultValue?: string;
|
||||||
|
value?: string;
|
||||||
|
onValueChange?: (value: string) => void;
|
||||||
|
data: CodeBlockData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlock = ({
|
||||||
|
value: controlledValue,
|
||||||
|
onValueChange: controlledOnValueChange,
|
||||||
|
defaultValue,
|
||||||
|
className,
|
||||||
|
data,
|
||||||
|
...props
|
||||||
|
}: CodeBlockProps) => {
|
||||||
|
const [value, onValueChange] = useControllableState({
|
||||||
|
defaultProp: defaultValue ?? '',
|
||||||
|
prop: controlledValue,
|
||||||
|
onChange: controlledOnValueChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CodeBlockContext.Provider value={{ value, onValueChange, data }}>
|
||||||
|
<div
|
||||||
|
className={cn('size-full overflow-hidden rounded-md border', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</CodeBlockContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export const CodeBlockHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockHeaderProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-row items-center border-b bg-secondary p-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockFilesProps = Omit<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
children: (item: CodeBlockData) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockFiles = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CodeBlockFilesProps) => {
|
||||||
|
const { data } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('flex grow flex-row items-center gap-2', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{data.map(children)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
icon?: IconType;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockFilename = ({
|
||||||
|
className,
|
||||||
|
icon,
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CodeBlockFilenameProps) => {
|
||||||
|
const { value: activeValue } = useContext(CodeBlockContext);
|
||||||
|
const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => {
|
||||||
|
const regex = new RegExp(
|
||||||
|
`^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$`
|
||||||
|
);
|
||||||
|
return regex.test(children as string);
|
||||||
|
})?.[1];
|
||||||
|
const Icon = icon ?? defaultIcon;
|
||||||
|
|
||||||
|
if (value !== activeValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{Icon && <Icon className="h-4 w-4 shrink-0" />}
|
||||||
|
<span className="flex-1 truncate">{children}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockSelectProps = ComponentProps<typeof Select>;
|
||||||
|
|
||||||
|
export const CodeBlockSelect = (props: CodeBlockSelectProps) => {
|
||||||
|
const { value, onValueChange } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
return <Select onValueChange={onValueChange} value={value} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;
|
||||||
|
|
||||||
|
export const CodeBlockSelectTrigger = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockSelectTriggerProps) => (
|
||||||
|
<SelectTrigger
|
||||||
|
className={cn(
|
||||||
|
'w-fit border-none text-muted-foreground text-xs shadow-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;
|
||||||
|
|
||||||
|
export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => (
|
||||||
|
<SelectValue {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockSelectContentProps = Omit<
|
||||||
|
ComponentProps<typeof SelectContent>,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
children: (item: CodeBlockData) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockSelectContent = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CodeBlockSelectContentProps) => {
|
||||||
|
const { data } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
return <SelectContent {...props}>{data.map(children)}</SelectContent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;
|
||||||
|
|
||||||
|
export const CodeBlockSelectItem = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockSelectItemProps) => (
|
||||||
|
<SelectItem className={cn('text-sm', className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
||||||
|
onCopy?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockCopyButton = ({
|
||||||
|
asChild,
|
||||||
|
onCopy,
|
||||||
|
onError,
|
||||||
|
timeout = 2000,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: CodeBlockCopyButtonProps) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const { data, value } = useContext(CodeBlockContext);
|
||||||
|
const code = data.find((item) => item.language === value)?.code;
|
||||||
|
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
if (
|
||||||
|
typeof window === 'undefined' ||
|
||||||
|
!navigator.clipboard.writeText ||
|
||||||
|
!code
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
setIsCopied(true);
|
||||||
|
onCopy?.();
|
||||||
|
|
||||||
|
setTimeout(() => setIsCopied(false), timeout);
|
||||||
|
}, onError);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (asChild) {
|
||||||
|
return cloneElement(children as ReactElement, {
|
||||||
|
// @ts-expect-error - we know this is a button
|
||||||
|
onClick: copyToClipboard,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={cn('shrink-0', className)}
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <Icon className="text-muted-foreground" size={14} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => (
|
||||||
|
<div {...props}>
|
||||||
|
<pre className="w-full">
|
||||||
|
<code>
|
||||||
|
{children
|
||||||
|
?.toString()
|
||||||
|
.split('\n')
|
||||||
|
.map((line, i) => (
|
||||||
|
<span className="line" key={i}>
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CodeBlockBodyProps = Omit<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
children: (item: CodeBlockData) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => {
|
||||||
|
const { data } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
return <div {...props}>{data.map(children)}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
value: string;
|
||||||
|
lineNumbers?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockItem = ({
|
||||||
|
children,
|
||||||
|
lineNumbers = true,
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: CodeBlockItemProps) => {
|
||||||
|
const { value: activeValue } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
|
if (value !== activeValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
codeBlockClassName,
|
||||||
|
lineHighlightClassNames,
|
||||||
|
lineDiffClassNames,
|
||||||
|
lineFocusedClassNames,
|
||||||
|
wordHighlightClassNames,
|
||||||
|
darkModeClassNames,
|
||||||
|
lineNumbers && lineNumberClassNames,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
themes?: {
|
||||||
|
light: string;
|
||||||
|
dark: string;
|
||||||
|
};
|
||||||
|
language?: BundledLanguage;
|
||||||
|
syntaxHighlighting?: boolean;
|
||||||
|
children: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockContent = ({
|
||||||
|
children,
|
||||||
|
themes = {
|
||||||
|
light: 'vitesse-light',
|
||||||
|
dark: 'vitesse-dark',
|
||||||
|
},
|
||||||
|
language = 'typescript',
|
||||||
|
syntaxHighlighting = true,
|
||||||
|
...props
|
||||||
|
}: CodeBlockContentProps) => {
|
||||||
|
const [highlightedCode, setHighlightedCode] = useState<string>('');
|
||||||
|
const [isLoading, setIsLoading] = useState(syntaxHighlighting);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!syntaxHighlighting) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadHighlightedCode = async () => {
|
||||||
|
try {
|
||||||
|
const { codeToHtml } = await import('shiki');
|
||||||
|
|
||||||
|
const html = await codeToHtml(children, {
|
||||||
|
lang: language,
|
||||||
|
themes: {
|
||||||
|
light: themes.light,
|
||||||
|
dark: themes.dark,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setHighlightedCode(html);
|
||||||
|
setIsLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to highlight code for language "${language}":`, error);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadHighlightedCode();
|
||||||
|
}, [children, language, themes, syntaxHighlighting]);
|
||||||
|
|
||||||
|
if (!syntaxHighlighting || isLoading) {
|
||||||
|
return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
transformerNotationDiff,
|
||||||
|
transformerNotationErrorLevel,
|
||||||
|
transformerNotationFocus,
|
||||||
|
transformerNotationHighlight,
|
||||||
|
transformerNotationWordHighlight,
|
||||||
|
} from '@shikijs/transformers';
|
||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
import {
|
||||||
|
type BundledLanguage,
|
||||||
|
type CodeOptionsMultipleThemes,
|
||||||
|
codeToHtml,
|
||||||
|
} from 'shiki';
|
||||||
|
|
||||||
|
export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
themes?: CodeOptionsMultipleThemes['themes'];
|
||||||
|
language?: BundledLanguage;
|
||||||
|
children: string;
|
||||||
|
syntaxHighlighting?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeBlockContent = async ({
|
||||||
|
children,
|
||||||
|
themes,
|
||||||
|
language,
|
||||||
|
syntaxHighlighting = true,
|
||||||
|
...props
|
||||||
|
}: CodeBlockContentProps) => {
|
||||||
|
const html = syntaxHighlighting
|
||||||
|
? await codeToHtml(children as string, {
|
||||||
|
lang: language ?? 'typescript',
|
||||||
|
themes: themes ?? {
|
||||||
|
light: 'vitesse-light',
|
||||||
|
dark: 'vitesse-dark',
|
||||||
|
},
|
||||||
|
transformers: [
|
||||||
|
transformerNotationDiff({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
transformerNotationHighlight({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
transformerNotationWordHighlight({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
transformerNotationFocus({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
transformerNotationErrorLevel({
|
||||||
|
matchAlgorithm: 'v3',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
: children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: "Kinda how Shiki works"
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||||
|
|
||||||
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
Loading…
x
Reference in New Issue
Block a user