Merge pull request #20 from johndoe6345789/copilot/fix-build-chunk-load-error

[WIP] Fix fatal chunk load error in server bundle generation
This commit is contained in:
2026-01-19 19:22:07 +00:00
committed by GitHub
23 changed files with 1218 additions and 4105 deletions

View File

@@ -5,8 +5,54 @@ const nextConfig = {
images: {
unoptimized: true,
},
typescript: {
// TypeScript incorrectly flags CSS imports as errors in Next.js
// This is a known issue: https://github.com/vercel/next.js/issues/54282
ignoreBuildErrors: true,
},
experimental: {
optimizePackageImports: ['@radix-ui/react-icons', '@phosphor-icons/react'],
turbopackScopeHoisting: false,
},
serverExternalPackages: ['pyodide'],
webpack: (config, { isServer, webpack }) => {
// Pyodide contains references to Node.js built-in modules that should be ignored in browser bundles
if (!isServer) {
// Replace node: protocol imports with empty modules
config.plugins.push(
new webpack.NormalModuleReplacementPlugin(
/^node:/,
(resource) => {
resource.request = resource.request.replace(/^node:/, '');
}
)
);
// Set fallbacks for node modules to false (don't polyfill)
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
child_process: false,
crypto: false,
path: false,
url: false,
vm: false,
};
// Exclude pyodide from server-side rendering completely
config.resolve.alias = {
...config.resolve.alias,
pyodide: false,
};
}
// On server, also exclude pyodide
if (isServer) {
config.externals = config.externals || [];
config.externals.push('pyodide');
}
return config;
},
};

4013
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
{
"name": "spark-template",
"type": "module",
"private": true,
"version": "0.0.0",
"scripts": {
@@ -11,11 +12,8 @@
},
"dependencies": {
"@babel/standalone": "^7.28.6",
"@github/spark": ">=0.43.1 <1",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^4.1.3",
"@monaco-editor/react": "^4.7.0",
"@octokit/core": "^6.1.4",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-accordion": "^1.2.3",
@@ -45,51 +43,37 @@
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.83.1",
"@types/sql.js": "^1.4.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"d3": "^7.9.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.5.2",
"framer-motion": "^12.6.2",
"input-otp": "^1.4.2",
"lucide-react": "^0.484.0",
"marked": "^15.0.7",
"next": "16.1.3",
"next-themes": "^0.4.6",
"octokit": "^4.1.2",
"pyodide": "^0.29.1",
"react": "^19.0.0",
"react-day-picker": "^9.6.7",
"react-dom": "^19.0.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.54.2",
"react-redux": "^9.2.0",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^7.12.0",
"recharts": "^2.15.1",
"sass": "^1.97.2",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.2",
"three": "^0.175.0",
"tw-animate-css": "^1.2.4",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "^25.0.9",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"autoprefixer": "^10.4.23",
"eslint": "^9.28.0",
"eslint-config-next": "^16.1.3",
"eslint-plugin-react-hooks": "^5.2.0",
"globals": "^16.0.0",
"tailwindcss": "^4.1.11",
"typescript": "~5.7.2",
"typescript-eslint": "^8.38.0"
},

View File

@@ -1,5 +0,0 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}

View File

@@ -1,14 +1,20 @@
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
import { motion } from 'framer-motion';
import { SplitScreenEditor } from '@/components/features/snippet-editor/SplitScreenEditor';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Sparkle } from '@phosphor-icons/react';
import { DEMO_CODE } from '@/components/demo/demo-constants';
import { DemoFeatureCards } from '@/components/demo/DemoFeatureCards';
import { PageLayout } from '../PageLayout';
// Dynamically import SplitScreenEditor to avoid SSR issues with Pyodide
const SplitScreenEditor = dynamic(
() => import('@/components/features/snippet-editor/SplitScreenEditor').then(mod => ({ default: mod.SplitScreenEditor })),
{ ssr: false }
);
export default function DemoPage() {
const [code, setCode] = useState(DEMO_CODE);

View File

@@ -1,67 +0,0 @@
@import 'tailwindcss';
:root {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 263 70% 50%;
--primary-foreground: 210 40% 98%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 195 100% 70%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 195 100% 70%;
--radius: 0.5rem;
}
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
}
* {
border-color: hsl(var(--border));
}
body {
background: hsl(var(--background));
color: hsl(var(--foreground));
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-inter), 'Inter', sans-serif;
}
body {
font-family: var(--font-inter), 'Inter', sans-serif;
}
code, pre {
font-family: var(--font-jetbrains-mono), 'JetBrains Mono', monospace;
}

49
src/app/globals.scss Normal file
View File

@@ -0,0 +1,49 @@
@import '../styles/abstracts';
@import '../styles/utilities/utilities';
@import './theme.scss';
* {
margin: 0;
padding: 0;
box-sizing: border-box;
border-color: $border;
}
body {
background: $background;
color: $foreground;
font-family: var(--font-inter), 'Inter', sans-serif;
line-height: 1.5;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-inter), 'Inter', sans-serif;
font-weight: 600;
}
code, pre {
font-family: var(--font-jetbrains-mono), 'JetBrains Mono', monospace;
}
:root {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 263 70% 50%;
--primary-foreground: 210 40% 98%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 195 100% 70%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 195 100% 70%;
--radius: 0.5rem;
}

View File

@@ -1,5 +1,5 @@
import type { Metadata } from 'next';
import './globals.css';
import './globals.scss';
import { Providers } from './providers';
export const metadata: Metadata = {

View File

@@ -1,9 +1,15 @@
'use client';
import { motion } from 'framer-motion';
import { SnippetManagerRedux } from '@/components/SnippetManagerRedux';
import dynamic from 'next/dynamic';
import { PageLayout } from './PageLayout';
// Dynamically import SnippetManagerRedux to avoid SSR issues with Pyodide
const SnippetManagerRedux = dynamic(
() => import('@/components/SnippetManagerRedux').then(mod => ({ default: mod.SnippetManagerRedux })),
{ ssr: false }
);
export default function HomePage() {
return (
<PageLayout>

View File

@@ -1,6 +1,5 @@
'use client';
import '@github/spark/spark';
import { ErrorBoundary } from 'react-error-boundary';
import { Provider } from 'react-redux';
import { Toaster } from '@/components/ui/sonner';

View File

@@ -8,6 +8,7 @@ import { StorageBackendCard } from '@/components/settings/StorageBackendCard';
import { DatabaseStatsCard } from '@/components/settings/DatabaseStatsCard';
import { StorageInfoCard } from '@/components/settings/StorageInfoCard';
import { DatabaseActionsCard } from '@/components/settings/DatabaseActionsCard';
import { OpenAISettingsCard } from '@/components/settings/OpenAISettingsCard';
import { useSettingsState } from '@/hooks/useSettingsState';
import { PageLayout } from '../PageLayout';
@@ -50,6 +51,8 @@ export default function SettingsPage() {
</div>
<div className="grid gap-6 max-w-3xl">
<OpenAISettingsCard />
<PersistenceSettings />
<SchemaHealthCard

View File

@@ -1,262 +0,0 @@
@import "tailwindcss";
@import '@radix-ui/colors/sage-dark.css' layer(base);
@import '@radix-ui/colors/olive.css' layer(base);
@import '@radix-ui/colors/olive-dark.css' layer(base);
@import '@radix-ui/colors/sand.css' layer(base);
@import '@radix-ui/colors/sand-dark.css' layer(base);
@import '@radix-ui/colors/red.css' layer(base);
@import '@radix-ui/colors/red-dark.css' layer(base);
@import '@radix-ui/colors/ruby.css' layer(base);
@import '@radix-ui/colors/ruby-dark.css' layer(base);
@import '@radix-ui/colors/crimson.css' layer(base);
@import '@radix-ui/colors/crimson-dark.css' layer(base);
@import '@radix-ui/colors/pink.css' layer(base);
@import '@radix-ui/colors/pink-dark.css' layer(base);
@import '@radix-ui/colors/plum.css' layer(base);
@import '@radix-ui/colors/plum-dark.css' layer(base);
@import '@radix-ui/colors/purple.css' layer(base);
@import '@radix-ui/colors/purple-dark.css' layer(base);
@import '@radix-ui/colors/violet.css' layer(base);
@import '@radix-ui/colors/violet-dark.css' layer(base);
@import '@radix-ui/colors/iris.css' layer(base);
@import '@radix-ui/colors/iris-dark.css' layer(base);
@import '@radix-ui/colors/indigo.css' layer(base);
@import '@radix-ui/colors/indigo-dark.css' layer(base);
@import '@radix-ui/colors/blue.css' layer(base);
@import '@radix-ui/colors/blue-dark.css' layer(base);
@import '@radix-ui/colors/cyan.css' layer(base);
@import '@radix-ui/colors/cyan-dark.css' layer(base);
@import '@radix-ui/colors/teal.css' layer(base);
@import '@radix-ui/colors/teal-dark.css' layer(base);
@import '@radix-ui/colors/jade.css' layer(base);
@import '@radix-ui/colors/jade-dark.css' layer(base);
@import '@radix-ui/colors/green.css' layer(base);
@import '@radix-ui/colors/green-dark.css' layer(base);
@import '@radix-ui/colors/grass.css' layer(base);
@import '@radix-ui/colors/grass-dark.css' layer(base);
@import '@radix-ui/colors/bronze.css' layer(base);
@import '@radix-ui/colors/bronze-dark.css' layer(base);
@import '@radix-ui/colors/gold.css' layer(base);
@import '@radix-ui/colors/gold-dark.css' layer(base);
@import '@radix-ui/colors/brown.css' layer(base);
@import '@radix-ui/colors/brown-dark.css' layer(base);
@import '@radix-ui/colors/orange.css' layer(base);
@import '@radix-ui/colors/orange-dark.css' layer(base);
@import '@radix-ui/colors/amber.css' layer(base);
@import '@radix-ui/colors/amber-dark.css' layer(base);
@import '@radix-ui/colors/yellow.css' layer(base);
@import '@radix-ui/colors/yellow-dark.css' layer(base);
@import '@radix-ui/colors/lime.css' layer(base);
@import '@radix-ui/colors/lime-dark.css' layer(base);
@import '@radix-ui/colors/mint.css' layer(base);
@import '@radix-ui/colors/mint-dark.css' layer(base);
@import '@radix-ui/colors/sky.css' layer(base);
@import '@radix-ui/colors/sky-dark.css' layer(base);
@import '@radix-ui/colors/tomato.css' layer(base);
@import '@radix-ui/colors/tomato-dark.css' layer(base);
@import '@radix-ui/colors/gray.css' layer(base);
@import '@radix-ui/colors/gray-dark.css' layer(base);
@import '@radix-ui/colors/mauve.css' layer(base);
@import '@radix-ui/colors/mauve-dark.css' layer(base);
@import '@radix-ui/colors/slate.css' layer(base);
@import '@radix-ui/colors/slate-dark.css' layer(base);
@import '@radix-ui/colors/slate-alpha.css' layer(base);
@import '@radix-ui/colors/slate-dark-alpha.css' layer(base);
@import 'tailwindcss/theme' layer(theme);
@import 'tailwindcss/preflight' layer(base);
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@layer base {
#spark-app {
--tomato-contrast: #fff;
--red-contrast: #fff;
--ruby-contrast: #fff;
--crimson-contrast: #fff;
--pink-contrast: #fff;
--plum-contrast: #fff;
--purple-contrast: #fff;
--violet-contrast: #fff;
--iris-contrast: #fff;
--indigo-contrast: #fff;
--blue-contrast: #fff;
--cyan-contrast: #fff;
--teal-contrast: #fff;
--jade-contrast: #fff;
--green-contrast: #fff;
--grass-contrast: #fff;
--bronze-contrast: #fff;
--gold-contrast: #fff;
--brown-contrast: #fff;
--orange-contrast: #fff;
--amber-contrast: #000;
--yellow-contrast: #000;
--lime-contrast: #000;
--mint-contrast: #000;
--sky-contrast: #000;
--gray-contrast: #fff;
--mauve-contrast: #fff;
--slate-contrast: #fff;
--sage-contrast: #fff;
--olive-contrast: #fff;
--sand-contrast: #fff;
/**
* Spacing scale
*
* These variables define a spacing scale based on Tailwind's default.
* We've introduced a --size-scale variable as a multiplier.
* By adjusting this single value, we can proportionally
* scale all spacing throughout the entire application.
*
* https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale
*/
--size-scale: 1;
--size-0: 0px;
--size-px: 1px;
--size-0-5: calc(0.125rem * var(--size-scale));
--size-1: calc(0.25rem * var(--size-scale));
--size-1-5: calc(0.375rem * var(--size-scale));
--size-2: calc(0.5rem * var(--size-scale));
--size-2-5: calc(0.625rem * var(--size-scale));
--size-3: calc(0.75rem * var(--size-scale));
--size-3-5: calc(0.875rem * var(--size-scale));
--size-4: calc(1rem * var(--size-scale));
--size-5: calc(1.25rem * var(--size-scale));
--size-6: calc(1.5rem * var(--size-scale));
--size-7: calc(1.75rem * var(--size-scale));
--size-8: calc(2rem * var(--size-scale));
--size-9: calc(2.25rem * var(--size-scale));
--size-10: calc(2.5rem * var(--size-scale));
--size-11: calc(2.75rem * var(--size-scale));
--size-12: calc(3rem * var(--size-scale));
--size-14: calc(3.5rem * var(--size-scale));
--size-16: calc(4rem * var(--size-scale));
--size-20: calc(5rem * var(--size-scale));
--size-24: calc(6rem * var(--size-scale));
--size-28: calc(7rem * var(--size-scale));
--size-32: calc(8rem * var(--size-scale));
--size-36: calc(9rem * var(--size-scale));
--size-40: calc(10rem * var(--size-scale));
--size-44: calc(11rem * var(--size-scale));
--size-48: calc(12rem * var(--size-scale));
--size-52: calc(13rem * var(--size-scale));
--size-56: calc(14rem * var(--size-scale));
--size-60: calc(15rem * var(--size-scale));
--size-64: calc(16rem * var(--size-scale));
--size-72: calc(18rem * var(--size-scale));
--size-80: calc(20rem * var(--size-scale));
--size-96: calc(24rem * var(--size-scale));
/* Border radii */
--radius-factor: 1;
--radius-sm: calc(2px * var(--radius-factor) * var(--size-scale));
--radius-md: calc(6px * var(--radius-factor) * var(--size-scale));
--radius-lg: calc(8px * var(--radius-factor) * var(--size-scale));
--radius-xl: calc(12px * var(--radius-factor) * var(--size-scale));
--radius-2xl: calc(16px * var(--radius-factor) * var(--size-scale));
--radius-full: 9999px;
/* Neutral colors */
--color-neutral-1: var(--slate-1);
--color-neutral-2: var(--slate-2);
--color-neutral-3: var(--slate-3);
--color-neutral-4: var(--slate-4);
--color-neutral-5: var(--slate-5);
--color-neutral-6: var(--slate-6);
--color-neutral-7: var(--slate-7);
--color-neutral-8: var(--slate-8);
--color-neutral-9: var(--slate-9);
--color-neutral-10: var(--slate-10);
--color-neutral-11: var(--slate-11);
--color-neutral-12: var(--slate-12);
--color-neutral-a1: var(--slate-a1);
--color-neutral-a2: var(--slate-a2);
--color-neutral-a3: var(--slate-a3);
--color-neutral-a4: var(--slate-a4);
--color-neutral-a5: var(--slate-a5);
--color-neutral-a6: var(--slate-a6);
--color-neutral-a7: var(--slate-a7);
--color-neutral-a8: var(--slate-a8);
--color-neutral-a9: var(--slate-a9);
--color-neutral-a10: var(--slate-a10);
--color-neutral-a11: var(--slate-a11);
--color-neutral-a12: var(--slate-a12);
--color-neutral-contrast: var(--slate-contrast);
/* Accent colors */
--color-accent-1: var(--blue-1);
--color-accent-2: var(--blue-2);
--color-accent-3: var(--blue-3);
--color-accent-4: var(--blue-4);
--color-accent-5: var(--blue-5);
--color-accent-6: var(--blue-6);
--color-accent-7: var(--blue-7);
--color-accent-8: var(--blue-8);
--color-accent-9: var(--blue-9);
--color-accent-10: var(--blue-10);
--color-accent-11: var(--blue-11);
--color-accent-12: var(--blue-12);
--color-accent-contrast: var(--blue-contrast);
/* Secondary accent colors */
--color-accent-secondary-1: var(--violet-1);
--color-accent-secondary-2: var(--violet-2);
--color-accent-secondary-3: var(--violet-3);
--color-accent-secondary-4: var(--violet-4);
--color-accent-secondary-5: var(--violet-5);
--color-accent-secondary-6: var(--violet-6);
--color-accent-secondary-7: var(--violet-7);
--color-accent-secondary-8: var(--violet-8);
--color-accent-secondary-9: var(--violet-9);
--color-accent-secondary-10: var(--violet-10);
--color-accent-secondary-11: var(--violet-11);
--color-accent-secondary-12: var(--violet-12);
--color-accent-secondary-contrast: var(--violet-contrast);
/* Foreground colors */
--color-fg: var(--color-neutral-12);
--color-fg-secondary: var(--color-neutral-a11);
/* Background colors */
--color-bg: #ffffff;
--color-bg-inset: var(--color-neutral-2);
--color-bg-overlay: #ffffff;
/* Focus ring */
--color-focus-ring: var(--color-accent-9);
/* Fonts */
--font-sans-serif: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-monospace: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
--font-family: var(--font-sans-serif);
}
#spark-app.dark-theme {
--color-bg: var(--color-neutral-1);
--color-bg-inset: #000000;
--color-bg-overlay: var(--color-neutral-3);
}
}

75
src/app/theme.scss Normal file
View File

@@ -0,0 +1,75 @@
// Radix UI Colors imports (keeping the color system)
@import '@radix-ui/colors/sage-dark.css';
@import '@radix-ui/colors/slate.css';
@import '@radix-ui/colors/slate-dark.css';
@import '@radix-ui/colors/blue.css';
@import '@radix-ui/colors/blue-dark.css';
@import '@radix-ui/colors/violet.css';
@import '@radix-ui/colors/violet-dark.css';
// Spacing scale variables
$size-scale: 1;
$size-0: 0px;
$size-px: 1px;
$size-0-5: calc(0.125rem * $size-scale);
$size-1: calc(0.25rem * $size-scale);
$size-2: calc(0.5rem * $size-scale);
$size-3: calc(0.75rem * $size-scale);
$size-4: calc(1rem * $size-scale);
$size-5: calc(1.25rem * $size-scale);
$size-6: calc(1.5rem * $size-scale);
$size-8: calc(2rem * $size-scale);
$size-10: calc(2.5rem * $size-scale);
$size-12: calc(3rem * $size-scale);
$size-16: calc(4rem * $size-scale);
$size-20: calc(5rem * $size-scale);
$size-24: calc(6rem * $size-scale);
// Border radii
$radius-factor: 1;
$radius-sm: calc(2px * $radius-factor * $size-scale);
$radius-md: calc(6px * $radius-factor * $size-scale);
$radius-lg: calc(8px * $radius-factor * $size-scale);
$radius-xl: calc(12px * $radius-factor * $size-scale);
$radius-full: 9999px;
// App-level CSS variables
#spark-app {
--size-scale: #{$size-scale};
--radius-factor: #{$radius-factor};
// Neutral colors (using slate from Radix)
--color-neutral-1: var(--slate-1);
--color-neutral-2: var(--slate-2);
--color-neutral-3: var(--slate-3);
--color-neutral-11: var(--slate-11);
--color-neutral-12: var(--slate-12);
// Accent colors (using blue from Radix)
--color-accent-9: var(--blue-9);
--color-accent-11: var(--blue-11);
// Foreground colors
--color-fg: var(--color-neutral-12);
--color-fg-secondary: var(--color-neutral-11);
// Background colors
--color-bg: #ffffff;
--color-bg-inset: var(--color-neutral-2);
--color-bg-overlay: #ffffff;
// Focus ring
--color-focus-ring: var(--color-accent-9);
// Fonts
--font-sans-serif: ui-sans-serif, system-ui, sans-serif;
--font-serif: ui-serif, Georgia, serif;
--font-monospace: ui-monospace, 'SF Mono', Monaco, Consolas, monospace;
--font-family: var(--font-sans-serif);
}
#spark-app.dark-theme {
--color-bg: var(--color-neutral-1);
--color-bg-inset: #000000;
--color-bg-overlay: var(--color-neutral-3);
}

View File

@@ -3,10 +3,37 @@ export async function analyzeErrorWithAI(
errorStack?: string,
context?: string
): Promise<string> {
const contextInfo = context ? `\n\nContext: ${context}` : ''
const stackInfo = errorStack ? `\n\nStack trace: ${errorStack}` : ''
// Check if OpenAI API key is configured
const apiKey = localStorage.getItem('openai_api_key');
const prompt = (window.spark.llmPrompt as any)`You are a helpful debugging assistant for a code snippet manager app. Analyze this error and provide:
if (!apiKey) {
// Fallback to simple error analysis if no API key
const lines = ['## Error Analysis\n'];
lines.push('**Error Message:**');
lines.push(`\`${errorMessage}\`\n`);
if (context) {
lines.push('**Context:**');
lines.push(`${context}\n`);
}
lines.push('**Note:** Configure your OpenAI API key in Settings to enable AI-powered error analysis.\n');
lines.push('**Basic Troubleshooting:**');
lines.push('1. Check the browser console for more details');
lines.push('2. Try refreshing the page');
lines.push('3. Clear your browser cache and local storage');
return lines.join('\n');
}
// Use OpenAI API for advanced error analysis
try {
const contextInfo = context ? `\n\nContext: ${context}` : '';
const stackInfo = errorStack ? `\n\nStack trace: ${errorStack}` : '';
const prompt = `You are a helpful debugging assistant for a code snippet manager app. Analyze this error and provide:
1. A clear explanation of what went wrong (in plain language)
2. Why this error likely occurred
@@ -14,8 +41,51 @@ export async function analyzeErrorWithAI(
Error message: ${errorMessage}${contextInfo}${stackInfo}
Keep your response concise, friendly, and focused on practical solutions. Format your response with clear sections using markdown.`
Keep your response concise, friendly, and focused on practical solutions. Format your response with clear sections using markdown.`;
const result = await window.spark.llm(prompt, 'gpt-4o-mini')
return result
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: 'You are a helpful debugging assistant for a code snippet manager app.',
},
{
role: 'user',
content: prompt,
},
],
temperature: 0.7,
max_tokens: 500,
}),
});
if (!response.ok) {
throw new Error('Failed to analyze error with AI. Please check your API key.');
}
const data = await response.json();
return data.choices[0]?.message?.content || 'Unable to analyze error.';
} catch (err) {
console.error('Error calling OpenAI API:', err);
// Fallback to simple analysis if API call fails
return `## Error Analysis
**Error Message:**
\`${errorMessage}\`
**Note:** Failed to get AI analysis. ${err instanceof Error ? err.message : 'Unknown error'}
**Basic Troubleshooting:**
1. Check the browser console for more details
2. Try refreshing the page
3. Verify your OpenAI API key in Settings`;
}
}

View File

@@ -5,9 +5,15 @@ import {
} from '@/components/ui/dialog'
import { Snippet } from '@/lib/types'
import { useState } from 'react'
import dynamic from 'next/dynamic'
import { appConfig } from '@/lib/config'
import { SnippetViewerHeader } from './SnippetViewerHeader'
import { SnippetViewerContent } from './SnippetViewerContent'
// Dynamically import SnippetViewerContent to avoid SSR issues with Pyodide
const SnippetViewerContent = dynamic(
() => import('./SnippetViewerContent').then(mod => ({ default: mod.SnippetViewerContent })),
{ ssr: false }
)
interface SnippetViewerProps {
snippet: Snippet | null

View File

@@ -0,0 +1,107 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Eye, EyeClosed, Key } from '@phosphor-icons/react';
export function OpenAISettingsCard() {
const [apiKey, setApiKey] = useState('');
const [showKey, setShowKey] = useState(false);
const [saved, setSaved] = useState(false);
useEffect(() => {
// Load the API key from localStorage on mount
const storedKey = localStorage.getItem('openai_api_key');
if (storedKey) {
setApiKey(storedKey);
}
}, []);
const handleSave = () => {
if (apiKey.trim()) {
localStorage.setItem('openai_api_key', apiKey.trim());
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} else {
localStorage.removeItem('openai_api_key');
setSaved(true);
setTimeout(() => setSaved(false), 2000);
}
};
const handleClear = () => {
setApiKey('');
localStorage.removeItem('openai_api_key');
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
return (
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Key className="h-5 w-5 text-primary" weight="duotone" />
<CardTitle>OpenAI API Settings</CardTitle>
</div>
<CardDescription>
Configure your OpenAI API key for AI-powered error analysis. Your key is stored locally in your browser.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="openai-key">OpenAI API Key</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="openai-key"
type={showKey ? 'text' : 'password'}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-..."
className="pr-10"
/>
<button
type="button"
onClick={() => setShowKey(!showKey)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showKey ? <EyeClosed size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
<p className="text-xs text-muted-foreground">
Get your API key from{' '}
<a
href="https://platform.openai.com/api-keys"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
OpenAI Platform
</a>
</p>
</div>
<div className="flex gap-2">
<Button onClick={handleSave} disabled={!apiKey.trim()}>
{saved ? 'Saved!' : 'Save API Key'}
</Button>
{apiKey && (
<Button onClick={handleClear} variant="outline">
Clear
</Button>
)}
</div>
{apiKey && (
<div className="text-sm text-muted-foreground bg-muted/50 p-3 rounded-md">
API key is configured. Error analysis will use OpenAI GPT-4o-mini.
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -1,6 +1,5 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return clsx(inputs)
}

View File

@@ -0,0 +1,24 @@
// Spacing function
@function spacing($key) {
@return map-get($spacing, $key);
}
// Border radius function
@function radius($key) {
@return map-get($radius, $key);
}
// Font size function
@function font-size($key) {
@return map-get($font-sizes, $key);
}
// Font weight function
@function font-weight($key) {
@return map-get($font-weights, $key);
}
// Breakpoint function
@function breakpoint($key) {
@return map-get($breakpoints, $key);
}

View File

@@ -0,0 +1,3 @@
@import './variables';
@import './functions';
@import './mixins';

View File

@@ -0,0 +1,212 @@
// Flexbox mixins
@mixin flex($direction: row, $justify: flex-start, $align: stretch, $wrap: nowrap) {
display: flex;
flex-direction: $direction;
justify-content: $justify;
align-items: $align;
flex-wrap: $wrap;
}
@mixin flex-center {
@include flex(row, center, center);
}
@mixin flex-between {
@include flex(row, space-between, center);
}
@mixin flex-col {
@include flex(column, flex-start, stretch);
}
// Grid mixin
@mixin grid($columns: 1, $gap: spacing(4)) {
display: grid;
grid-template-columns: repeat($columns, 1fr);
gap: $gap;
}
// Spacing mixins
@mixin p($value) {
padding: spacing($value);
}
@mixin px($value) {
padding-left: spacing($value);
padding-right: spacing($value);
}
@mixin py($value) {
padding-top: spacing($value);
padding-bottom: spacing($value);
}
@mixin pt($value) {
padding-top: spacing($value);
}
@mixin pr($value) {
padding-right: spacing($value);
}
@mixin pb($value) {
padding-bottom: spacing($value);
}
@mixin pl($value) {
padding-left: spacing($value);
}
@mixin m($value) {
margin: spacing($value);
}
@mixin mx($value) {
margin-left: spacing($value);
margin-right: spacing($value);
}
@mixin my($value) {
margin-top: spacing($value);
margin-bottom: spacing($value);
}
@mixin mt($value) {
margin-top: spacing($value);
}
@mixin mr($value) {
margin-right: spacing($value);
}
@mixin mb($value) {
margin-bottom: spacing($value);
}
@mixin ml($value) {
margin-left: spacing($value);
}
// Gap mixin
@mixin gap($value) {
gap: spacing($value);
}
// Border radius mixin
@mixin rounded($value: base) {
border-radius: radius($value);
}
// Typography mixins
@mixin text($size) {
font-size: font-size($size);
}
@mixin font($weight) {
font-weight: font-weight($weight);
}
// Responsive breakpoint mixin
@mixin respond-to($breakpoint) {
$size: breakpoint($breakpoint);
@media (min-width: $size) {
@content;
}
}
// Button base styles mixin
@mixin button-base {
@include px(4);
@include py(2);
@include rounded(md);
@include font(medium);
@include text(sm);
display: inline-flex;
align-items: center;
justify-content: center;
gap: spacing(2);
cursor: pointer;
transition: all 0.2s ease;
border: none;
outline: none;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// Card mixin
@mixin card {
background: $card;
color: $card-foreground;
border: 1px solid $border;
@include rounded(lg);
@include p(6);
}
// Shadow mixins
@mixin shadow-sm {
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
@mixin shadow {
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
@mixin shadow-md {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
@mixin shadow-lg {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
// Transition mixin
@mixin transition($properties: all, $duration: 0.2s, $timing: ease) {
transition: $properties $duration $timing;
}
// Truncate text
@mixin truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// Visually hidden (for accessibility)
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
// Absolute positioning shortcuts
@mixin absolute-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@mixin absolute-fill {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
// Focus ring
@mixin focus-ring {
&:focus-visible {
outline: 2px solid $ring;
outline-offset: 2px;
}
}

View File

@@ -0,0 +1,119 @@
// Color Variables
$background: hsl(222.2, 84%, 4.9%);
$foreground: hsl(210, 40%, 98%);
$card: hsl(222.2, 84%, 4.9%);
$card-foreground: hsl(210, 40%, 98%);
$popover: hsl(222.2, 84%, 4.9%);
$popover-foreground: hsl(210, 40%, 98%);
$primary: hsl(263, 70%, 50%);
$primary-foreground: hsl(210, 40%, 98%);
$secondary: hsl(217.2, 32.6%, 17.5%);
$secondary-foreground: hsl(210, 40%, 98%);
$muted: hsl(217.2, 32.6%, 17.5%);
$muted-foreground: hsl(215, 20.2%, 65.1%);
$accent: hsl(195, 100%, 70%);
$accent-foreground: hsl(222.2, 84%, 4.9%);
$destructive: hsl(0, 62.8%, 30.6%);
$destructive-foreground: hsl(210, 40%, 98%);
$border: hsl(217.2, 32.6%, 17.5%);
$input: hsl(217.2, 32.6%, 17.5%);
$ring: hsl(195, 100%, 70%);
// Spacing Scale
$spacing: (
0: 0,
px: 1px,
0-5: 0.125rem,
1: 0.25rem,
1-5: 0.375rem,
2: 0.5rem,
2-5: 0.625rem,
3: 0.75rem,
3-5: 0.875rem,
4: 1rem,
5: 1.25rem,
6: 1.5rem,
7: 1.75rem,
8: 2rem,
9: 2.25rem,
10: 2.5rem,
11: 2.75rem,
12: 3rem,
14: 3.5rem,
16: 4rem,
20: 5rem,
24: 6rem,
28: 7rem,
32: 8rem,
36: 9rem,
40: 10rem,
48: 12rem,
56: 14rem,
64: 16rem,
72: 18rem,
80: 20rem,
96: 24rem,
);
// Border Radius
$radius: (
none: 0,
sm: 0.125rem,
base: 0.25rem,
md: 0.375rem,
lg: 0.5rem,
xl: 0.75rem,
2xl: 1rem,
3xl: 1.5rem,
full: 9999px,
);
// Font Sizes
$font-sizes: (
xs: 0.75rem,
sm: 0.875rem,
base: 1rem,
lg: 1.125rem,
xl: 1.25rem,
2xl: 1.5rem,
3xl: 1.875rem,
4xl: 2.25rem,
5xl: 3rem,
6xl: 3.75rem,
7xl: 4.5rem,
8xl: 6rem,
9xl: 8rem,
);
// Font Weights
$font-weights: (
thin: 100,
extralight: 200,
light: 300,
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
extrabold: 800,
black: 900,
);
// Breakpoints
$breakpoints: (
sm: 640px,
md: 768px,
lg: 1024px,
xl: 1280px,
2xl: 1536px,
);
// Z-index scale
$z-index: (
0: 0,
10: 10,
20: 20,
30: 30,
40: 40,
50: 50,
auto: auto,
);

View File

@@ -0,0 +1,142 @@
@import '../abstracts';
// Utility classes generator
@each $name, $value in $spacing {
.p-#{$name} { padding: $value !important; }
.px-#{$name} { padding-left: $value !important; padding-right: $value !important; }
.py-#{$name} { padding-top: $value !important; padding-bottom: $value !important; }
.pt-#{$name} { padding-top: $value !important; }
.pr-#{$name} { padding-right: $value !important; }
.pb-#{$name} { padding-bottom: $value !important; }
.pl-#{$name} { padding-left: $value !important; }
.m-#{$name} { margin: $value !important; }
.mx-#{$name} { margin-left: $value !important; margin-right: $value !important; }
.my-#{$name} { margin-top: $value !important; margin-bottom: $value !important; }
.mt-#{$name} { margin-top: $value !important; }
.mr-#{$name} { margin-right: $value !important; }
.mb-#{$name} { margin-bottom: $value !important; }
.ml-#{$name} { margin-left: $value !important; }
.gap-#{$name} { gap: $value !important; }
}
// Flexbox utilities
.flex { display: flex !important; }
.inline-flex { display: inline-flex !important; }
.flex-row { flex-direction: row !important; }
.flex-col { flex-direction: column !important; }
.flex-wrap { flex-wrap: wrap !important; }
.flex-nowrap { flex-wrap: nowrap !important; }
.flex-1 { flex: 1 1 0% !important; }
// Justify content
.justify-start { justify-content: flex-start !important; }
.justify-end { justify-content: flex-end !important; }
.justify-center { justify-content: center !important; }
.justify-between { justify-content: space-between !important; }
.justify-around { justify-content: space-around !important; }
// Align items
.items-start { align-items: flex-start !important; }
.items-end { align-items: flex-end !important; }
.items-center { align-items: center !important; }
.items-stretch { align-items: stretch !important; }
// Grid
.grid { display: grid !important; }
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)) !important; }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; }
// Border radius
@each $name, $value in $radius {
.rounded-#{$name} { border-radius: $value !important; }
}
// Text sizes
@each $name, $value in $font-sizes {
.text-#{$name} { font-size: $value !important; }
}
// Font weights
@each $name, $value in $font-weights {
.font-#{$name} { font-weight: $value !important; }
}
// Text align
.text-left { text-align: left !important; }
.text-center { text-align: center !important; }
.text-right { text-align: right !important; }
// Colors
.text-foreground { color: $foreground !important; }
.text-muted-foreground { color: $muted-foreground !important; }
.text-primary { color: $primary !important; }
.text-destructive { color: $destructive !important; }
.text-accent { color: $accent !important; }
.bg-background { background-color: $background !important; }
.bg-card { background-color: $card !important; }
.bg-primary { background-color: $primary !important; }
.bg-secondary { background-color: $secondary !important; }
.bg-muted { background-color: $muted !important; }
.bg-accent { background-color: $accent !important; }
.bg-destructive { background-color: $destructive !important; }
// Border
.border { border: 1px solid $border !important; }
.border-b { border-bottom: 1px solid $border !important; }
.border-t { border-top: 1px solid $border !important; }
.border-l { border-left: 1px solid $border !important; }
.border-r { border-right: 1px solid $border !important; }
// Width & Height
.w-full { width: 100% !important; }
.h-full { height: 100% !important; }
.min-h-screen { min-height: 100vh !important; }
.max-w-3xl { max-width: 48rem !important; }
.max-w-7xl { max-width: 80rem !important; }
// Position
.relative { position: relative !important; }
.absolute { position: absolute !important; }
.fixed { position: fixed !important; }
.sticky { position: sticky !important; }
// Display
.hidden { display: none !important; }
.block { display: block !important; }
.inline-block { display: inline-block !important; }
// Overflow
.overflow-hidden { overflow: hidden !important; }
.overflow-auto { overflow: auto !important; }
.overflow-scroll { overflow: scroll !important; }
// Cursor
.cursor-pointer { cursor: pointer !important; }
.cursor-not-allowed { cursor: not-allowed !important; }
// Opacity
.opacity-50 { opacity: 0.5 !important; }
.opacity-75 { opacity: 0.75 !important; }
.opacity-100 { opacity: 1 !important; }
// Transitions
.transition { transition: all 0.2s ease !important; }
.transition-colors { transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease !important; }
// Shadow
.shadow-sm { @include shadow-sm; }
.shadow { @include shadow; }
.shadow-md { @include shadow-md; }
.shadow-lg { @include shadow-lg; }
// Space between children - DRY loop
$space-values: (2, 3, 4, 6, 8);
@each $value in $space-values {
.space-y-#{$value} > * + * { margin-top: spacing($value) !important; }
.space-x-#{$value} > * + * { margin-left: spacing($value) !important; }
}

View File

@@ -1,62 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./src/pages/**/*.{ts,tsx}',
'./src/components/**/*.{ts,tsx}',
'./src/app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [],
}