feat: redesign terminal UI with theme support and retro aesthetic

- Add terminal-chat-interface component with dual-panel layout
- Implement light/dark mode with next-themes
- Reorganize shadcn components to shadcn-io subdirectory
- Add custom retro icons (security, terminal, bot, etc.)
- Update color scheme with oklch values for both themes
- Add theme toggle and Gitea repository link
- Include corner bracket accents and grid/scan line effects
- Fix hydration mismatch for session time display
This commit is contained in:
nicholai 2025-10-09 04:00:19 -06:00
parent 8cbc9538ca
commit 074c79f302
33 changed files with 968 additions and 282 deletions

6
.gitignore vendored
View File

@ -166,6 +166,12 @@ Thumbs.db
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
.cursorrules
.cursorrules/*
!.cursorrules/.gitignore
.cursor/*
.cursor/*/*
.cursor
# Turborepo cache (if you introduce turbo later)
.turbo/

View File

@ -15,6 +15,8 @@
"@icons-pack/react-simple-icons": "^13.8.0",
"@opennextjs/cloudflare": "^1.3.0",
"@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-label": "^2.1.7",
"@radix-ui/react-scroll-area": "^1.2.10",

View File

@ -17,6 +17,12 @@ importers:
'@radix-ui/react-alert-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-avatar':
specifier: ^1.1.10
version: 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-collapsible':
specifier: ^1.1.12
version: 1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@ -1219,6 +1225,32 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-avatar@1.1.10':
resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collapsible@1.1.12':
resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@ -1504,6 +1536,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-is-hydrated@0.1.0':
resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
@ -4548,6 +4589,11 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
@ -6308,6 +6354,35 @@ snapshots:
'@types/react': 19.2.2
'@types/react-dom': 19.2.1(@types/react@19.2.2)
'@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.1(@types/react@19.2.2)
'@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.1(@types/react@19.2.2)
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
@ -6591,6 +6666,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.2)(react@19.1.0)':
dependencies:
react: 19.1.0
use-sync-external-store: 1.6.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.1.0)':
dependencies:
react: 19.1.0
@ -10466,6 +10548,10 @@ snapshots:
dependencies:
react: 19.1.0
use-sync-external-store@1.6.0(react@19.1.0):
dependencies:
react: 19.1.0
utils-merge@1.0.1: {}
uuid@9.0.1: {}

View File

@ -3,180 +3,165 @@
@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 {
--color-background: var(--background);
--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-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-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0rem;
--background: oklch(0 0 0);
--foreground: oklch(0.8664 0.2948 142.4953);
--card: oklch(0.2178 0 0);
--card-foreground: oklch(0.8664 0.2948 142.4953);
--popover: oklch(0.1448 0 0);
--popover-foreground: oklch(0.8664 0.2948 142.4953);
--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);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
@layer base {
@ -187,4 +172,30 @@
@apply bg-background text-foreground;
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;
}
}

View File

@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
const geistSans = Geist({
variable: "--font-geist-sans",
@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Bandit Runner",
description: "Security Automation Console",
};
export default function RootLayout({
@ -23,11 +24,18 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);

View File

@ -1,103 +1,5 @@
import Image from "next/image";
import { TerminalChatInterface } from "@/components/terminal-chat-interface"
export default function Home() {
return (
<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>
);
return <TerminalChatInterface />
}

View 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>
)
}

View 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>
)
}

View 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>
}

View 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>
)
}

View File

@ -1,7 +1,7 @@
'use client';
import { Button } from '@repo/shadcn-ui/components/ui/button';
import { cn } from '@repo/shadcn-ui/lib/utils';
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';

View File

@ -2,8 +2,8 @@ import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@repo/shadcn-ui/components/ui/avatar';
import { cn } from '@repo/shadcn-ui/lib/utils';
} from '@/components/ui/shadcn-io/avatar';
import { cn } from '@/lib/utils';
import type { UIMessage } from 'ai';
import type { ComponentProps, HTMLAttributes } from 'react';

View File

@ -5,8 +5,8 @@ import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@repo/shadcn-ui/components/ui/collapsible';
import { cn } from '@repo/shadcn-ui/lib/utils';
} 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';

View File

@ -1,6 +1,6 @@
'use client';
import { cn } from '@repo/shadcn-ui/lib/utils';
import { cn } from '@/lib/utils';
import type { ComponentProps, HTMLAttributes } from 'react';
import { isValidElement, memo } from 'react';
import ReactMarkdown, { type Options } from 'react-markdown';

View File

@ -1,12 +1,12 @@
'use client';
import { Badge } from '@repo/shadcn-ui/components/ui/badge';
import { Badge } from '@/components/ui/shadcn-io/badge';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@repo/shadcn-ui/components/ui/collapsible';
import { cn } from '@repo/shadcn-ui/lib/utils';
} from '@/components/ui/shadcn-io/collapsible';
import { cn } from '@/lib/utils';
import type { ToolUIPart } from 'ai';
import {
CheckCircleIcon,

View File

@ -4,7 +4,7 @@ import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { buttonVariants } from "@/components/ui/shadcn-io/button"
function AlertDialog({
...props

View 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 }

View File

@ -86,14 +86,14 @@ import {
useEffect,
useState,
} from 'react';
import { Button } from '@/components/ui/button';
import { Button } from '@/components/ui/shadcn-io/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
} from '@/components/ui/shadcn-io/select';
import { cn } from '@/lib/utils';
export type BundledLanguage = string;

View File

@ -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 }