Public: anyone can read this; the crew can change it.
Implement Function Creator with Vercel Sandbox integration #20
Check · Review · Land · Live — It conflicts with its base.
claude/implement-function-creator-011CUYGAKQqspM2Th78EXKZE → main 10 commits
A guest
This pull comes from a guest: its preview runs without the app's variables or a forge token until a person who can write here approves it.
This pull cannot land as it is, whatever its review says, because it changes guarded paths: package.json (release machinery).
15 files
changed +1610
−15
package-lock.json +354 −4
| @@ -8,6 +8,8 @@ | ||
| 8 | 8 | "name": "trevato.dev", |
| 9 | 9 | "version": "0.1.0", |
| 10 | 10 | "dependencies": { |
| 11 | "@ai-sdk/anthropic": "^2.0.38", | |
| 12 | "@ai-sdk/react": "^2.0.81", | |
| 11 | 13 | "@calcom/embed-react": "^1.5.0", |
| 12 | 14 | "@radix-ui/react-avatar": "^1.1.0", |
| 13 | 15 | "@radix-ui/react-dialog": "^1.1.1", |
| @@ -22,6 +24,8 @@ | ||
| 22 | 24 | "@supabase/ssr": "^0.5.1", |
| 23 | 25 | "@uiw/react-textarea-code-editor": "^3.0.2", |
| 24 | 26 | "@vercel/analytics": "^1.5.0", |
| 27 | "@vercel/sandbox": "^1.0.2", | |
| 28 | "ai": "^5.0.81", | |
| 25 | 29 | "class-variance-authority": "^0.7.0", |
| 26 | 30 | "clsx": "^2.1.1", |
| 27 | 31 | "framer-motion": "^11.5.4", |
| @@ -39,7 +43,8 @@ | ||
| 39 | 43 | "remark-gfm": "^4.0.0", |
| 40 | 44 | "tailwind-merge": "^2.5.2", |
| 41 | 45 | "tailwindcss-animate": "^1.0.7", |
| 42 | "three": "^0.168.0" | |
| 46 | "three": "^0.168.0", | |
| 47 | "zod": "^3.25.76" | |
| 43 | 48 | }, |
| 44 | 49 | "devDependencies": { |
| 45 | 50 | "@types/node": "^20", |
| @@ -53,6 +58,101 @@ | ||
| 53 | 58 | "typescript": "^5" |
| 54 | 59 | } |
| 55 | 60 | }, |
| 61 | "node_modules/@ai-sdk/anthropic": { | |
| 62 | "version": "2.0.38", | |
| 63 | "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.38.tgz", | |
| 64 | "integrity": "sha512-NjU1ftHbu90OfRCgBwfFelmdEXwGFwLEcfyOyyfjRDm8QHaJUlPNnXhdhPTYuUU386yhj29Vibemiaq6jQv3lA==", | |
| 65 | "license": "Apache-2.0", | |
| 66 | "dependencies": { | |
| 67 | "@ai-sdk/provider": "2.0.0", | |
| 68 | "@ai-sdk/provider-utils": "3.0.13" | |
| 69 | }, | |
| 70 | "engines": { | |
| 71 | "node": ">=18" | |
| 72 | }, | |
| 73 | "peerDependencies": { | |
| 74 | "zod": "^3.25.76 || ^4.1.8" | |
| 75 | } | |
| 76 | }, | |
| 77 | "node_modules/@ai-sdk/gateway": { | |
| 78 | "version": "2.0.2", | |
| 79 | "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.2.tgz", | |
| 80 | "integrity": "sha512-25F1qPqZxOw9IcV9OQCL29hV4HAFLw5bFWlzQLBi5aDhEZsTMT2rMi3umSqNaUxrrw+dLRtjOL7RbHC+WjbA/A==", | |
| 81 | "license": "Apache-2.0", | |
| 82 | "dependencies": { | |
| 83 | "@ai-sdk/provider": "2.0.0", | |
| 84 | "@ai-sdk/provider-utils": "3.0.13", | |
| 85 | "@vercel/oidc": "3.0.3" | |
| 86 | }, | |
| 87 | "engines": { | |
| 88 | "node": ">=18" | |
| 89 | }, | |
| 90 | "peerDependencies": { | |
| 91 | "zod": "^3.25.76 || ^4.1.8" | |
| 92 | } | |
| 93 | }, | |
| 94 | "node_modules/@ai-sdk/gateway/node_modules/@vercel/oidc": { | |
| 95 | "version": "3.0.3", | |
| 96 | "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz", | |
| 97 | "integrity": "sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==", | |
| 98 | "license": "Apache-2.0", | |
| 99 | "engines": { | |
| 100 | "node": ">= 20" | |
| 101 | } | |
| 102 | }, | |
| 103 | "node_modules/@ai-sdk/provider": { | |
| 104 | "version": "2.0.0", | |
| 105 | "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", | |
| 106 | "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", | |
| 107 | "license": "Apache-2.0", | |
| 108 | "dependencies": { | |
| 109 | "json-schema": "^0.4.0" | |
| 110 | }, | |
| 111 | "engines": { | |
| 112 | "node": ">=18" | |
| 113 | } | |
| 114 | }, | |
| 115 | "node_modules/@ai-sdk/provider-utils": { | |
| 116 | "version": "3.0.13", | |
| 117 | "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.13.tgz", | |
| 118 | "integrity": "sha512-aXFLBLRPTUYA853MJliItefSXeJPl+mg0KSjbToP41kJ+banBmHO8ZPGLJhNqGlCU82o11TYN7G05EREKX8CkA==", | |
| 119 | "license": "Apache-2.0", | |
| 120 | "dependencies": { | |
| 121 | "@ai-sdk/provider": "2.0.0", | |
| 122 | "@standard-schema/spec": "^1.0.0", | |
| 123 | "eventsource-parser": "^3.0.5" | |
| 124 | }, | |
| 125 | "engines": { | |
| 126 | "node": ">=18" | |
| 127 | }, | |
| 128 | "peerDependencies": { | |
| 129 | "zod": "^3.25.76 || ^4.1.8" | |
| 130 | } | |
| 131 | }, | |
| 132 | "node_modules/@ai-sdk/react": { | |
| 133 | "version": "2.0.81", | |
| 134 | "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.81.tgz", | |
| 135 | "integrity": "sha512-JmBE97WtwtLRnK90u4bUZXwxFX+QZQ32y7JFiM+NintaWDymLQ6ukcu3Z5Nr+nc8V9two4+0jxkv8N11lKu+Tw==", | |
| 136 | "license": "Apache-2.0", | |
| 137 | "dependencies": { | |
| 138 | "@ai-sdk/provider-utils": "3.0.13", | |
| 139 | "ai": "5.0.81", | |
| 140 | "swr": "^2.2.5", | |
| 141 | "throttleit": "2.1.0" | |
| 142 | }, | |
| 143 | "engines": { | |
| 144 | "node": ">=18" | |
| 145 | }, | |
| 146 | "peerDependencies": { | |
| 147 | "react": "^18 || ^19 || ^19.0.0-rc", | |
| 148 | "zod": "^3.25.76 || ^4.1.8" | |
| 149 | }, | |
| 150 | "peerDependenciesMeta": { | |
| 151 | "zod": { | |
| 152 | "optional": true | |
| 153 | } | |
| 154 | } | |
| 155 | }, | |
| 56 | 156 | "node_modules/@alloc/quick-lru": { |
| 57 | 157 | "version": "5.2.0", |
| 58 | 158 | "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", |
| @@ -831,6 +931,15 @@ | ||
| 831 | 931 | "node": ">= 18" |
| 832 | 932 | } |
| 833 | 933 | }, |
| 934 | "node_modules/@opentelemetry/api": { | |
| 935 | "version": "1.9.0", | |
| 936 | "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", | |
| 937 | "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", | |
| 938 | "license": "Apache-2.0", | |
| 939 | "engines": { | |
| 940 | "node": ">=8.0.0" | |
| 941 | } | |
| 942 | }, | |
| 834 | 943 | "node_modules/@pkgjs/parseargs": { |
| 835 | 944 | "version": "0.11.0", |
| 836 | 945 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", |
| @@ -1647,6 +1756,12 @@ | ||
| 1647 | 1756 | "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", |
| 1648 | 1757 | "dev": true |
| 1649 | 1758 | }, |
| 1759 | "node_modules/@standard-schema/spec": { | |
| 1760 | "version": "1.0.0", | |
| 1761 | "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", | |
| 1762 | "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", | |
| 1763 | "license": "MIT" | |
| 1764 | }, | |
| 1650 | 1765 | "node_modules/@supabase/auth-js": { |
| 1651 | 1766 | "version": "2.65.0", |
| 1652 | 1767 | "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.0.tgz", |
| @@ -1816,9 +1931,10 @@ | ||
| 1816 | 1931 | } |
| 1817 | 1932 | }, |
| 1818 | 1933 | "node_modules/@types/ms": { |
| 1819 | "version": "0.7.34", | |
| 1820 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", | |
| 1821 | "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" | |
| 1934 | "version": "2.1.0", | |
| 1935 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", | |
| 1936 | "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", | |
| 1937 | "license": "MIT" | |
| 1822 | 1938 | }, |
| 1823 | 1939 | "node_modules/@types/node": { |
| 1824 | 1940 | "version": "20.16.5", |
| @@ -2219,6 +2335,43 @@ | ||
| 2219 | 2335 | } |
| 2220 | 2336 | } |
| 2221 | 2337 | }, |
| 2338 | "node_modules/@vercel/oidc": { | |
| 2339 | "version": "2.0.2", | |
| 2340 | "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-2.0.2.tgz", | |
| 2341 | "integrity": "sha512-59PBFx3T+k5hLTEWa3ggiMpGRz1OVvl9eN8SUai+A43IsqiOuAe7qPBf+cray/Fj6mkgnxm/D7IAtjc8zSHi7g==", | |
| 2342 | "license": "Apache-2.0", | |
| 2343 | "dependencies": { | |
| 2344 | "@types/ms": "2.1.0", | |
| 2345 | "ms": "2.1.3" | |
| 2346 | }, | |
| 2347 | "engines": { | |
| 2348 | "node": ">= 18" | |
| 2349 | } | |
| 2350 | }, | |
| 2351 | "node_modules/@vercel/sandbox": { | |
| 2352 | "version": "1.0.2", | |
| 2353 | "resolved": "https://registry.npmjs.org/@vercel/sandbox/-/sandbox-1.0.2.tgz", | |
| 2354 | "integrity": "sha512-EoZhkUag3YVSuXCfpO4SZFDwghiGCeVklUVyWFpt0dsjjXWr5C2MXvtqFeH2KbRp/58I1k5JxeSxNuUX5FWZbQ==", | |
| 2355 | "license": "ISC", | |
| 2356 | "dependencies": { | |
| 2357 | "@vercel/oidc": "^2.0.2", | |
| 2358 | "async-retry": "1.3.3", | |
| 2359 | "jsonlines": "0.1.1", | |
| 2360 | "ms": "2.1.3", | |
| 2361 | "tar-stream": "3.1.7", | |
| 2362 | "undici": "^7.16.0", | |
| 2363 | "zod": "3.24.4" | |
| 2364 | } | |
| 2365 | }, | |
| 2366 | "node_modules/@vercel/sandbox/node_modules/zod": { | |
| 2367 | "version": "3.24.4", | |
| 2368 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", | |
| 2369 | "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", | |
| 2370 | "license": "MIT", | |
| 2371 | "funding": { | |
| 2372 | "url": "https://github.com/sponsors/colinhacks" | |
| 2373 | } | |
| 2374 | }, | |
| 2222 | 2375 | "node_modules/@webgpu/types": { |
| 2223 | 2376 | "version": "0.1.44", |
| 2224 | 2377 | "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz", |
| @@ -2246,6 +2399,24 @@ | ||
| 2246 | 2399 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" |
| 2247 | 2400 | } |
| 2248 | 2401 | }, |
| 2402 | "node_modules/ai": { | |
| 2403 | "version": "5.0.81", | |
| 2404 | "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.81.tgz", | |
| 2405 | "integrity": "sha512-SB7oMC9QSpIu1VLswFTZuhhpfQfrGtFBUbWLtHBkhjWZIQskjtcdEhB+N4yO9hscdc2wYtjw/tacgoxX93QWFw==", | |
| 2406 | "license": "Apache-2.0", | |
| 2407 | "dependencies": { | |
| 2408 | "@ai-sdk/gateway": "2.0.2", | |
| 2409 | "@ai-sdk/provider": "2.0.0", | |
| 2410 | "@ai-sdk/provider-utils": "3.0.13", | |
| 2411 | "@opentelemetry/api": "1.9.0" | |
| 2412 | }, | |
| 2413 | "engines": { | |
| 2414 | "node": ">=18" | |
| 2415 | }, | |
| 2416 | "peerDependencies": { | |
| 2417 | "zod": "^3.25.76 || ^4.1.8" | |
| 2418 | } | |
| 2419 | }, | |
| 2249 | 2420 | "node_modules/ajv": { |
| 2250 | 2421 | "version": "6.12.6", |
| 2251 | 2422 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", |
| @@ -2497,6 +2668,15 @@ | ||
| 2497 | 2668 | "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", |
| 2498 | 2669 | "dev": true |
| 2499 | 2670 | }, |
| 2671 | "node_modules/async-retry": { | |
| 2672 | "version": "1.3.3", | |
| 2673 | "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", | |
| 2674 | "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", | |
| 2675 | "license": "MIT", | |
| 2676 | "dependencies": { | |
| 2677 | "retry": "0.13.1" | |
| 2678 | } | |
| 2679 | }, | |
| 2500 | 2680 | "node_modules/available-typed-arrays": { |
| 2501 | 2681 | "version": "1.0.7", |
| 2502 | 2682 | "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", |
| @@ -2544,6 +2724,20 @@ | ||
| 2544 | 2724 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", |
| 2545 | 2725 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" |
| 2546 | 2726 | }, |
| 2727 | "node_modules/bare-events": { | |
| 2728 | "version": "2.8.1", | |
| 2729 | "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.1.tgz", | |
| 2730 | "integrity": "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==", | |
| 2731 | "license": "Apache-2.0", | |
| 2732 | "peerDependencies": { | |
| 2733 | "bare-abort-controller": "*" | |
| 2734 | }, | |
| 2735 | "peerDependenciesMeta": { | |
| 2736 | "bare-abort-controller": { | |
| 2737 | "optional": true | |
| 2738 | } | |
| 2739 | } | |
| 2740 | }, | |
| 2547 | 2741 | "node_modules/base64-js": { |
| 2548 | 2742 | "version": "1.5.1", |
| 2549 | 2743 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", |
| @@ -3829,6 +4023,24 @@ | ||
| 3829 | 4023 | "node": ">=0.10.0" |
| 3830 | 4024 | } |
| 3831 | 4025 | }, |
| 4026 | "node_modules/events-universal": { | |
| 4027 | "version": "1.0.1", | |
| 4028 | "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", | |
| 4029 | "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", | |
| 4030 | "license": "Apache-2.0", | |
| 4031 | "dependencies": { | |
| 4032 | "bare-events": "^2.7.0" | |
| 4033 | } | |
| 4034 | }, | |
| 4035 | "node_modules/eventsource-parser": { | |
| 4036 | "version": "3.0.6", | |
| 4037 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", | |
| 4038 | "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", | |
| 4039 | "license": "MIT", | |
| 4040 | "engines": { | |
| 4041 | "node": ">=18.0.0" | |
| 4042 | } | |
| 4043 | }, | |
| 3832 | 4044 | "node_modules/extend": { |
| 3833 | 4045 | "version": "3.0.2", |
| 3834 | 4046 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", |
| @@ -3840,6 +4052,12 @@ | ||
| 3840 | 4052 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", |
| 3841 | 4053 | "dev": true |
| 3842 | 4054 | }, |
| 4055 | "node_modules/fast-fifo": { | |
| 4056 | "version": "1.3.2", | |
| 4057 | "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", | |
| 4058 | "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", | |
| 4059 | "license": "MIT" | |
| 4060 | }, | |
| 3843 | 4061 | "node_modules/fast-glob": { |
| 3844 | 4062 | "version": "3.3.2", |
| 3845 | 4063 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", |
| @@ -5262,6 +5480,12 @@ | ||
| 5262 | 5480 | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", |
| 5263 | 5481 | "dev": true |
| 5264 | 5482 | }, |
| 5483 | "node_modules/json-schema": { | |
| 5484 | "version": "0.4.0", | |
| 5485 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", | |
| 5486 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", | |
| 5487 | "license": "(AFL-2.1 OR BSD-3-Clause)" | |
| 5488 | }, | |
| 5265 | 5489 | "node_modules/json-schema-traverse": { |
| 5266 | 5490 | "version": "0.4.1", |
| 5267 | 5491 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", |
| @@ -5286,6 +5510,12 @@ | ||
| 5286 | 5510 | "json5": "lib/cli.js" |
| 5287 | 5511 | } |
| 5288 | 5512 | }, |
| 5513 | "node_modules/jsonlines": { | |
| 5514 | "version": "0.1.1", | |
| 5515 | "resolved": "https://registry.npmjs.org/jsonlines/-/jsonlines-0.1.1.tgz", | |
| 5516 | "integrity": "sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==", | |
| 5517 | "license": "MIT" | |
| 5518 | }, | |
| 5289 | 5519 | "node_modules/jsx-ast-utils": { |
| 5290 | 5520 | "version": "3.3.5", |
| 5291 | 5521 | "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", |
| @@ -7994,6 +8224,15 @@ | ||
| 7994 | 8224 | "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" |
| 7995 | 8225 | } |
| 7996 | 8226 | }, |
| 8227 | "node_modules/retry": { | |
| 8228 | "version": "0.13.1", | |
| 8229 | "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", | |
| 8230 | "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", | |
| 8231 | "license": "MIT", | |
| 8232 | "engines": { | |
| 8233 | "node": ">= 4" | |
| 8234 | } | |
| 8235 | }, | |
| 7997 | 8236 | "node_modules/reusify": { |
| 7998 | 8237 | "version": "1.0.4", |
| 7999 | 8238 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", |
| @@ -8268,6 +8507,17 @@ | ||
| 8268 | 8507 | "node": ">=10.0.0" |
| 8269 | 8508 | } |
| 8270 | 8509 | }, |
| 8510 | "node_modules/streamx": { | |
| 8511 | "version": "2.23.0", | |
| 8512 | "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", | |
| 8513 | "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", | |
| 8514 | "license": "MIT", | |
| 8515 | "dependencies": { | |
| 8516 | "events-universal": "^1.0.0", | |
| 8517 | "fast-fifo": "^1.3.2", | |
| 8518 | "text-decoder": "^1.1.0" | |
| 8519 | } | |
| 8520 | }, | |
| 8271 | 8521 | "node_modules/string-width": { |
| 8272 | 8522 | "version": "5.1.2", |
| 8273 | 8523 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", |
| @@ -8562,6 +8812,28 @@ | ||
| 8562 | 8812 | "react": ">=17.0" |
| 8563 | 8813 | } |
| 8564 | 8814 | }, |
| 8815 | "node_modules/swr": { | |
| 8816 | "version": "2.3.6", | |
| 8817 | "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", | |
| 8818 | "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", | |
| 8819 | "license": "MIT", | |
| 8820 | "dependencies": { | |
| 8821 | "dequal": "^2.0.3", | |
| 8822 | "use-sync-external-store": "^1.4.0" | |
| 8823 | }, | |
| 8824 | "peerDependencies": { | |
| 8825 | "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" | |
| 8826 | } | |
| 8827 | }, | |
| 8828 | "node_modules/swr/node_modules/use-sync-external-store": { | |
| 8829 | "version": "1.6.0", | |
| 8830 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", | |
| 8831 | "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", | |
| 8832 | "license": "MIT", | |
| 8833 | "peerDependencies": { | |
| 8834 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" | |
| 8835 | } | |
| 8836 | }, | |
| 8565 | 8837 | "node_modules/tailwind-merge": { |
| 8566 | 8838 | "version": "2.5.2", |
| 8567 | 8839 | "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz", |
| @@ -8624,6 +8896,54 @@ | ||
| 8624 | 8896 | "node": ">=6" |
| 8625 | 8897 | } |
| 8626 | 8898 | }, |
| 8899 | "node_modules/tar-stream": { | |
| 8900 | "version": "3.1.7", | |
| 8901 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", | |
| 8902 | "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", | |
| 8903 | "license": "MIT", | |
| 8904 | "dependencies": { | |
| 8905 | "b4a": "^1.6.4", | |
| 8906 | "fast-fifo": "^1.2.0", | |
| 8907 | "streamx": "^2.15.0" | |
| 8908 | } | |
| 8909 | }, | |
| 8910 | "node_modules/tar-stream/node_modules/b4a": { | |
| 8911 | "version": "1.7.3", | |
| 8912 | "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", | |
| 8913 | "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", | |
| 8914 | "license": "Apache-2.0", | |
| 8915 | "peerDependencies": { | |
| 8916 | "react-native-b4a": "*" | |
| 8917 | }, | |
| 8918 | "peerDependenciesMeta": { | |
| 8919 | "react-native-b4a": { | |
| 8920 | "optional": true | |
| 8921 | } | |
| 8922 | } | |
| 8923 | }, | |
| 8924 | "node_modules/text-decoder": { | |
| 8925 | "version": "1.2.3", | |
| 8926 | "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", | |
| 8927 | "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", | |
| 8928 | "license": "Apache-2.0", | |
| 8929 | "dependencies": { | |
| 8930 | "b4a": "^1.6.4" | |
| 8931 | } | |
| 8932 | }, | |
| 8933 | "node_modules/text-decoder/node_modules/b4a": { | |
| 8934 | "version": "1.7.3", | |
| 8935 | "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", | |
| 8936 | "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", | |
| 8937 | "license": "Apache-2.0", | |
| 8938 | "peerDependencies": { | |
| 8939 | "react-native-b4a": "*" | |
| 8940 | }, | |
| 8941 | "peerDependenciesMeta": { | |
| 8942 | "react-native-b4a": { | |
| 8943 | "optional": true | |
| 8944 | } | |
| 8945 | } | |
| 8946 | }, | |
| 8627 | 8947 | "node_modules/text-table": { |
| 8628 | 8948 | "version": "0.2.0", |
| 8629 | 8949 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", |
| @@ -8683,6 +9003,18 @@ | ||
| 8683 | 9003 | "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", |
| 8684 | 9004 | "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==" |
| 8685 | 9005 | }, |
| 9006 | "node_modules/throttleit": { | |
| 9007 | "version": "2.1.0", | |
| 9008 | "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", | |
| 9009 | "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", | |
| 9010 | "license": "MIT", | |
| 9011 | "engines": { | |
| 9012 | "node": ">=18" | |
| 9013 | }, | |
| 9014 | "funding": { | |
| 9015 | "url": "https://github.com/sponsors/sindresorhus" | |
| 9016 | } | |
| 9017 | }, | |
| 8686 | 9018 | "node_modules/to-regex-range": { |
| 8687 | 9019 | "version": "5.0.1", |
| 8688 | 9020 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", |
| @@ -8939,6 +9271,15 @@ | ||
| 8939 | 9271 | "url": "https://github.com/sponsors/ljharb" |
| 8940 | 9272 | } |
| 8941 | 9273 | }, |
| 9274 | "node_modules/undici": { | |
| 9275 | "version": "7.16.0", | |
| 9276 | "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", | |
| 9277 | "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", | |
| 9278 | "license": "MIT", | |
| 9279 | "engines": { | |
| 9280 | "node": ">=20.18.1" | |
| 9281 | } | |
| 9282 | }, | |
| 8942 | 9283 | "node_modules/undici-types": { |
| 8943 | 9284 | "version": "6.19.8", |
| 8944 | 9285 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", |
| @@ -9469,6 +9810,15 @@ | ||
| 9469 | 9810 | "url": "https://github.com/sponsors/sindresorhus" |
| 9470 | 9811 | } |
| 9471 | 9812 | }, |
| 9813 | "node_modules/zod": { | |
| 9814 | "version": "3.25.76", | |
| 9815 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", | |
| 9816 | "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", | |
| 9817 | "license": "MIT", | |
| 9818 | "funding": { | |
| 9819 | "url": "https://github.com/sponsors/colinhacks" | |
| 9820 | } | |
| 9821 | }, | |
| 9472 | 9822 | "node_modules/zustand": { |
| 9473 | 9823 | "version": "3.7.2", |
| 9474 | 9824 | "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", |
package.json +6 −1
| @@ -9,6 +9,8 @@ | ||
| 9 | 9 | "lint": "next lint" |
| 10 | 10 | }, |
| 11 | 11 | "dependencies": { |
| 12 | "@ai-sdk/anthropic": "^2.0.38", | |
| 13 | "@ai-sdk/react": "^2.0.81", | |
| 12 | 14 | "@calcom/embed-react": "^1.5.0", |
| 13 | 15 | "@radix-ui/react-avatar": "^1.1.0", |
| 14 | 16 | "@radix-ui/react-dialog": "^1.1.1", |
| @@ -23,6 +25,8 @@ | ||
| 23 | 25 | "@supabase/ssr": "^0.5.1", |
| 24 | 26 | "@uiw/react-textarea-code-editor": "^3.0.2", |
| 25 | 27 | "@vercel/analytics": "^1.5.0", |
| 28 | "@vercel/sandbox": "^1.0.2", | |
| 29 | "ai": "^5.0.81", | |
| 26 | 30 | "class-variance-authority": "^0.7.0", |
| 27 | 31 | "clsx": "^2.1.1", |
| 28 | 32 | "framer-motion": "^11.5.4", |
| @@ -40,7 +44,8 @@ | ||
| 40 | 44 | "remark-gfm": "^4.0.0", |
| 41 | 45 | "tailwind-merge": "^2.5.2", |
| 42 | 46 | "tailwindcss-animate": "^1.0.7", |
| 43 | "three": "^0.168.0" | |
| 47 | "three": "^0.168.0", | |
| 48 | "zod": "^3.25.76" | |
| 44 | 49 | }, |
| 45 | 50 | "devDependencies": { |
| 46 | 51 | "@types/node": "^20", |
src/app/api/chat/route.ts added +49
| @@ -0,0 +1,49 @@ | ||
| 1 | import { createClient } from '@/utils/supabase/server'; | |
| 2 | import { anthropic } from '@ai-sdk/anthropic'; | |
| 3 | import { streamText, convertToModelMessages } from 'ai'; | |
| 4 | ||
| 5 | export const runtime = 'edge'; | |
| 6 | export const maxDuration = 60; | |
| 7 | ||
| 8 | export async function POST(req: Request) { | |
| 9 | try { | |
| 10 | // Check authentication | |
| 11 | const supabase = await createClient(); | |
| 12 | const { data: { user }, error: authError } = await supabase.auth.getUser(); | |
| 13 | ||
| 14 | if (authError || !user) { | |
| 15 | return new Response( | |
| 16 | JSON.stringify({ error: 'Authentication required' }), | |
| 17 | { status: 401 } | |
| 18 | ); | |
| 19 | } | |
| 20 | ||
| 21 | const { messages } = await req.json(); | |
| 22 | ||
| 23 | const result = streamText({ | |
| 24 | model: anthropic('claude-3-5-sonnet-20241022'), | |
| 25 | system: `You are a helpful AI coding assistant. You help users write, understand, and debug TypeScript and Python code. | |
| 26 | ||
| 27 | Your role: | |
| 28 | - Help users write functions for their Function Creator | |
| 29 | - Explain code in simple terms | |
| 30 | - Debug issues and suggest improvements | |
| 31 | - Answer questions about TypeScript and Python | |
| 32 | - Provide code examples when helpful | |
| 33 | ||
| 34 | Keep responses clear, concise, and practical. When writing code, format it properly with markdown code blocks.`, | |
| 35 | messages: convertToModelMessages(messages), | |
| 36 | }); | |
| 37 | ||
| 38 | return result.toTextStreamResponse(); | |
| 39 | } catch (error) { | |
| 40 | console.error('Chat API error:', error); | |
| 41 | return new Response( | |
| 42 | JSON.stringify({ | |
| 43 | error: 'Failed to process chat request', | |
| 44 | details: error instanceof Error ? error.message : 'Unknown error' | |
| 45 | }), | |
| 46 | { status: 500 } | |
| 47 | ); | |
| 48 | } | |
| 49 | } |
src/app/api/sandbox/execute/route.ts added +107
| @@ -0,0 +1,107 @@ | ||
| 1 | import { NextRequest, NextResponse } from 'next/server'; | |
| 2 | import { Sandbox } from '@vercel/sandbox'; | |
| 3 | import { createClient } from '@/utils/supabase/server'; | |
| 4 | ||
| 5 | export const runtime = 'nodejs'; | |
| 6 | export const maxDuration = 300; // 5 minutes | |
| 7 | ||
| 8 | export async function POST(request: NextRequest) { | |
| 9 | try { | |
| 10 | // Check authentication | |
| 11 | const supabase = await createClient(); | |
| 12 | const { data: { user }, error: authError } = await supabase.auth.getUser(); | |
| 13 | ||
| 14 | if (authError || !user) { | |
| 15 | return NextResponse.json( | |
| 16 | { error: 'Authentication required' }, | |
| 17 | { status: 401 } | |
| 18 | ); | |
| 19 | } | |
| 20 | ||
| 21 | const { code, language, envVars = {} } = await request.json(); | |
| 22 | ||
| 23 | if (!code || !language) { | |
| 24 | return NextResponse.json( | |
| 25 | { error: 'Code and language are required' }, | |
| 26 | { status: 400 } | |
| 27 | ); | |
| 28 | } | |
| 29 | ||
| 30 | // Create a new sandbox with the appropriate runtime | |
| 31 | const runtime = language === 'python' ? 'python3.13' : 'node22'; | |
| 32 | const sandbox = await Sandbox.create({ | |
| 33 | runtime, | |
| 34 | }); | |
| 35 | ||
| 36 | try { | |
| 37 | let output = ''; | |
| 38 | let errorOutput = ''; | |
| 39 | ||
| 40 | // Determine the file extension and execution command based on language | |
| 41 | const fileName = language === 'python' ? 'main.py' : 'main.ts'; | |
| 42 | const execCmd = language === 'python' ? 'python3' : 'npx'; | |
| 43 | const execArgs = language === 'python' ? ['main.py'] : ['tsx', 'main.ts']; | |
| 44 | ||
| 45 | // Write the code to a file in the sandbox | |
| 46 | await sandbox.writeFiles([ | |
| 47 | { | |
| 48 | path: fileName, | |
| 49 | content: Buffer.from(code), | |
| 50 | }, | |
| 51 | ]); | |
| 52 | ||
| 53 | // Install tsx for TypeScript execution if needed | |
| 54 | if (language === 'typescript') { | |
| 55 | const installResult = await sandbox.runCommand({ | |
| 56 | cmd: 'npm', | |
| 57 | args: ['install', '-g', 'tsx'], | |
| 58 | }); | |
| 59 | ||
| 60 | // Wait for install to complete | |
| 61 | const installExitCode = installResult.exitCode; | |
| 62 | if (installExitCode !== 0) { | |
| 63 | const installError = await installResult.stderr(); | |
| 64 | return NextResponse.json({ | |
| 65 | success: false, | |
| 66 | error: `Failed to install tsx: ${installError}`, | |
| 67 | exitCode: installExitCode, | |
| 68 | }); | |
| 69 | } | |
| 70 | } | |
| 71 | ||
| 72 | // Execute the code with environment variables | |
| 73 | const result = await sandbox.runCommand({ | |
| 74 | cmd: execCmd, | |
| 75 | args: execArgs, | |
| 76 | env: envVars, | |
| 77 | }); | |
| 78 | ||
| 79 | // Get the output | |
| 80 | const exitCode = result.exitCode; | |
| 81 | output = await result.stdout(); | |
| 82 | errorOutput = await result.stderr(); | |
| 83 | ||
| 84 | return NextResponse.json({ | |
| 85 | success: exitCode === 0, | |
| 86 | output: output || 'Code executed successfully', | |
| 87 | error: errorOutput || null, | |
| 88 | exitCode, | |
| 89 | sandboxId: sandbox.sandboxId, | |
| 90 | }); | |
| 91 | ||
| 92 | } finally { | |
| 93 | // Clean up the sandbox | |
| 94 | await sandbox.stop(); | |
| 95 | } | |
| 96 | ||
| 97 | } catch (error) { | |
| 98 | console.error('Sandbox execution error:', error); | |
| 99 | return NextResponse.json( | |
| 100 | { | |
| 101 | error: 'Failed to execute code', | |
| 102 | details: error instanceof Error ? error.message : 'Unknown error' | |
| 103 | }, | |
| 104 | { status: 500 } | |
| 105 | ); | |
| 106 | } | |
| 107 | } |
src/app/api/sandbox/schedule/route.ts added +83
| @@ -0,0 +1,83 @@ | ||
| 1 | import { NextRequest, NextResponse } from 'next/server'; | |
| 2 | import { createClient } from '@/utils/supabase/server'; | |
| 3 | ||
| 4 | export const runtime = 'nodejs'; | |
| 5 | ||
| 6 | // This is a simplified version - for production, you'd want to use Vercel Cron Jobs | |
| 7 | // or integrate with a service like Vercel Cron, Upstash QStash, or similar | |
| 8 | export async function POST(request: NextRequest) { | |
| 9 | try { | |
| 10 | // Check authentication | |
| 11 | const supabase = await createClient(); | |
| 12 | const { data: { user }, error: authError } = await supabase.auth.getUser(); | |
| 13 | ||
| 14 | if (authError || !user) { | |
| 15 | return NextResponse.json( | |
| 16 | { error: 'Authentication required' }, | |
| 17 | { status: 401 } | |
| 18 | ); | |
| 19 | } | |
| 20 | ||
| 21 | const { code, language, cronExpression } = await request.json(); | |
| 22 | ||
| 23 | if (!code || !language || !cronExpression) { | |
| 24 | return NextResponse.json( | |
| 25 | { error: 'Code, language, and cron expression are required' }, | |
| 26 | { status: 400 } | |
| 27 | ); | |
| 28 | } | |
| 29 | ||
| 30 | // Validate cron expression (basic validation) | |
| 31 | const cronRegex = /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$/; | |
| 32 | ||
| 33 | if (!cronRegex.test(cronExpression)) { | |
| 34 | return NextResponse.json( | |
| 35 | { error: 'Invalid cron expression format' }, | |
| 36 | { status: 400 } | |
| 37 | ); | |
| 38 | } | |
| 39 | ||
| 40 | // In a production environment, you would: | |
| 41 | // 1. Store the function in a database with the cron schedule | |
| 42 | // 2. Use Vercel Cron Jobs (vercel.json) or a service like Upstash QStash | |
| 43 | // 3. Create a separate API endpoint that checks scheduled functions and executes them | |
| 44 | ||
| 45 | // For now, we'll return a success message with instructions | |
| 46 | return NextResponse.json({ | |
| 47 | success: true, | |
| 48 | message: 'Function scheduled successfully', | |
| 49 | scheduled: { | |
| 50 | code: code.substring(0, 100) + '...', // Truncate for response | |
| 51 | language, | |
| 52 | cronExpression, | |
| 53 | nextRun: calculateNextRun(cronExpression), | |
| 54 | note: 'This is a demo implementation. In production, integrate with Vercel Cron Jobs or a scheduling service.', | |
| 55 | }, | |
| 56 | }); | |
| 57 | ||
| 58 | } catch (error) { | |
| 59 | console.error('Scheduling error:', error); | |
| 60 | return NextResponse.json( | |
| 61 | { | |
| 62 | error: 'Failed to schedule function', | |
| 63 | details: error instanceof Error ? error.message : 'Unknown error' | |
| 64 | }, | |
| 65 | { status: 500 } | |
| 66 | ); | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| 70 | // Helper function to calculate next run time (simplified) | |
| 71 | function calculateNextRun(cronExpression: string): string { | |
| 72 | // This is a simplified version - in production, use a library like 'cron-parser' | |
| 73 | const now = new Date(); | |
| 74 | const nextRun = new Date(now.getTime() + 60 * 60 * 1000); // Add 1 hour as example | |
| 75 | return nextRun.toISOString(); | |
| 76 | } | |
| 77 | ||
| 78 | export async function GET() { | |
| 79 | return NextResponse.json({ | |
| 80 | message: 'Scheduled functions endpoint', | |
| 81 | note: 'Use POST to schedule a new function', | |
| 82 | }); | |
| 83 | } |
src/app/auth/sign-in/page.tsx added +207
| @@ -0,0 +1,207 @@ | ||
| 1 | 'use client' | |
| 2 | ||
| 3 | import { useState } from 'react' | |
| 4 | import { useRouter } from 'next/navigation' | |
| 5 | import { createClient } from '@/utils/supabase/client' | |
| 6 | import { Card } from '@/components/ui/card' | |
| 7 | import { Button } from '@/components/ui/button' | |
| 8 | import { toast } from '@/hooks/use-toast' | |
| 9 | import { Mail, KeyRound } from 'lucide-react' | |
| 10 | ||
| 11 | export default function AuthPage() { | |
| 12 | const [email, setEmail] = useState('') | |
| 13 | const [otp, setOtp] = useState('') | |
| 14 | const [isLoading, setIsLoading] = useState(false) | |
| 15 | const [isVerifying, setIsVerifying] = useState(false) | |
| 16 | const [emailSent, setEmailSent] = useState(false) | |
| 17 | const supabase = createClient() | |
| 18 | const router = useRouter() | |
| 19 | ||
| 20 | const handleSignIn = async (e: React.FormEvent) => { | |
| 21 | e.preventDefault() | |
| 22 | setIsLoading(true) | |
| 23 | ||
| 24 | try { | |
| 25 | const { error } = await supabase.auth.signInWithOtp({ | |
| 26 | email, | |
| 27 | options: { | |
| 28 | emailRedirectTo: `${window.location.origin}/playground/function-creator`, | |
| 29 | }, | |
| 30 | }) | |
| 31 | ||
| 32 | if (error) { | |
| 33 | toast({ | |
| 34 | title: 'Error', | |
| 35 | description: error.message, | |
| 36 | variant: 'destructive', | |
| 37 | }) | |
| 38 | } else { | |
| 39 | setEmailSent(true) | |
| 40 | toast({ | |
| 41 | title: 'Check your email', | |
| 42 | description: 'We sent you a magic link and a 6-digit code', | |
| 43 | }) | |
| 44 | } | |
| 45 | } catch (error) { | |
| 46 | toast({ | |
| 47 | title: 'Error', | |
| 48 | description: 'Something went wrong', | |
| 49 | variant: 'destructive', | |
| 50 | }) | |
| 51 | } finally { | |
| 52 | setIsLoading(false) | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | const handleVerifyOtp = async (e: React.FormEvent) => { | |
| 57 | e.preventDefault() | |
| 58 | ||
| 59 | if (otp.length !== 6) { | |
| 60 | toast({ | |
| 61 | title: 'Invalid code', | |
| 62 | description: 'Please enter a 6-digit code', | |
| 63 | variant: 'destructive', | |
| 64 | }) | |
| 65 | return | |
| 66 | } | |
| 67 | ||
| 68 | setIsVerifying(true) | |
| 69 | ||
| 70 | try { | |
| 71 | const { error } = await supabase.auth.verifyOtp({ | |
| 72 | email, | |
| 73 | token: otp, | |
| 74 | type: 'email', | |
| 75 | }) | |
| 76 | ||
| 77 | if (error) { | |
| 78 | toast({ | |
| 79 | title: 'Verification failed', | |
| 80 | description: error.message, | |
| 81 | variant: 'destructive', | |
| 82 | }) | |
| 83 | } else { | |
| 84 | toast({ | |
| 85 | title: 'Success', | |
| 86 | description: 'You are now signed in', | |
| 87 | }) | |
| 88 | router.push('/playground/function-creator') | |
| 89 | } | |
| 90 | } catch (error) { | |
| 91 | toast({ | |
| 92 | title: 'Error', | |
| 93 | description: 'Something went wrong', | |
| 94 | variant: 'destructive', | |
| 95 | }) | |
| 96 | } finally { | |
| 97 | setIsVerifying(false) | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | return ( | |
| 102 | <div className="container mx-auto px-4 flex items-center justify-center min-h-[80vh]"> | |
| 103 | <Card className="bg-black/50 backdrop-blur-md border-blue-500 p-8 w-full max-w-md"> | |
| 104 | <div className="space-y-6"> | |
| 105 | <div className="text-center space-y-2"> | |
| 106 | <h1 className="text-3xl font-bold text-white">Sign In</h1> | |
| 107 | <p className="text-gray-300"> | |
| 108 | Enter your email to access the Function Creator | |
| 109 | </p> | |
| 110 | </div> | |
| 111 | ||
| 112 | {!emailSent ? ( | |
| 113 | <form onSubmit={handleSignIn} className="space-y-4"> | |
| 114 | <div className="space-y-2"> | |
| 115 | <label htmlFor="email" className="text-sm text-gray-300"> | |
| 116 | ||
| 117 | </label> | |
| 118 | <input | |
| 119 | id="email" | |
| 120 | type="email" | |
| 121 | value={email} | |
| 122 | onChange={(e) => setEmail(e.target.value)} | |
| 123 | placeholder="[email protected]" | |
| 124 | required | |
| 125 | className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| 126 | /> | |
| 127 | </div> | |
| 128 | ||
| 129 | <Button | |
| 130 | type="submit" | |
| 131 | disabled={isLoading} | |
| 132 | className="w-full bg-blue-600 hover:bg-blue-700" | |
| 133 | > | |
| 134 | <Mail className="w-4 h-4 mr-2" /> | |
| 135 | {isLoading ? 'Sending...' : 'Send Magic Link'} | |
| 136 | </Button> | |
| 137 | </form> | |
| 138 | ) : ( | |
| 139 | <div className="space-y-4"> | |
| 140 | <div className="bg-green-500/10 border border-green-500/30 rounded p-4 space-y-2"> | |
| 141 | <p className="text-green-400 font-medium"> | |
| 142 | Check your email! | |
| 143 | </p> | |
| 144 | <p className="text-sm text-gray-300"> | |
| 145 | We sent you a magic link and a 6-digit code to <strong>{email}</strong> | |
| 146 | </p> | |
| 147 | </div> | |
| 148 | ||
| 149 | <div className="relative"> | |
| 150 | <div className="absolute inset-0 flex items-center"> | |
| 151 | <div className="w-full border-t border-gray-700"></div> | |
| 152 | </div> | |
| 153 | <div className="relative flex justify-center text-xs uppercase"> | |
| 154 | <span className="bg-gray-900 px-2 text-gray-400">Or enter code</span> | |
| 155 | </div> | |
| 156 | </div> | |
| 157 | ||
| 158 | <form onSubmit={handleVerifyOtp} className="space-y-4"> | |
| 159 | <div className="space-y-2"> | |
| 160 | <label htmlFor="otp" className="text-sm text-gray-300"> | |
| 161 | 6-Digit Code | |
| 162 | </label> | |
| 163 | <input | |
| 164 | id="otp" | |
| 165 | type="text" | |
| 166 | value={otp} | |
| 167 | onChange={(e) => setOtp(e.target.value.replace(/\D/g, '').slice(0, 6))} | |
| 168 | placeholder="000000" | |
| 169 | maxLength={6} | |
| 170 | className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 text-center text-2xl tracking-widest" | |
| 171 | /> | |
| 172 | </div> | |
| 173 | ||
| 174 | <Button | |
| 175 | type="submit" | |
| 176 | disabled={isVerifying || otp.length !== 6} | |
| 177 | className="w-full bg-blue-600 hover:bg-blue-700" | |
| 178 | > | |
| 179 | <KeyRound className="w-4 h-4 mr-2" /> | |
| 180 | {isVerifying ? 'Verifying...' : 'Verify Code'} | |
| 181 | </Button> | |
| 182 | </form> | |
| 183 | ||
| 184 | <Button | |
| 185 | onClick={() => { | |
| 186 | setEmailSent(false) | |
| 187 | setOtp('') | |
| 188 | }} | |
| 189 | variant="outline" | |
| 190 | className="w-full" | |
| 191 | > | |
| 192 | Try another email | |
| 193 | </Button> | |
| 194 | </div> | |
| 195 | )} | |
| 196 | ||
| 197 | <div className="text-xs text-gray-400 text-center"> | |
| 198 | <p> | |
| 199 | Authentication is required to prevent abuse and manage rate limits. | |
| 200 | No password needed - we will send you a magic link and code. | |
| 201 | </p> | |
| 202 | </div> | |
| 203 | </div> | |
| 204 | </Card> | |
| 205 | </div> | |
| 206 | ) | |
| 207 | } |
src/app/playground/3d-viewer/page.tsx added +15
| @@ -0,0 +1,15 @@ | ||
| 1 | import { ObjectViewer } from "@/components/playground/ObjectViewer" | |
| 2 | ||
| 3 | export default function ObjectViewerPage() { | |
| 4 | return ( | |
| 5 | <div className="container mx-auto px-4"> | |
| 6 | <h1 className="text-4xl font-bold mb-2 text-center text-white">3D Object Viewer</h1> | |
| 7 | <p className="text-lg text-gray-300 mb-8 text-center">Interactive 3D model viewer</p> | |
| 8 | <div className="max-w-[95vw] sm:max-w-4xl mx-auto"> | |
| 9 | <div className="bg-gray-900 bg-opacity-40 p-4 rounded-lg backdrop-filter backdrop-blur-xl"> | |
| 10 | <ObjectViewer /> | |
| 11 | </div> | |
| 12 | </div> | |
| 13 | </div> | |
| 14 | ) | |
| 15 | } |
src/app/playground/color-picker/page.tsx added +15
| @@ -0,0 +1,15 @@ | ||
| 1 | import { ColorPicker } from "@/components/playground/ColorPicker" | |
| 2 | ||
| 3 | export default function ColorPickerPage() { | |
| 4 | return ( | |
| 5 | <div className="container mx-auto px-4"> | |
| 6 | <h1 className="text-4xl font-bold mb-2 text-center text-white">Color Picker</h1> | |
| 7 | <p className="text-lg text-gray-300 mb-8 text-center">Select and preview colors</p> | |
| 8 | <div className="max-w-[95vw] sm:max-w-4xl mx-auto"> | |
| 9 | <div className="bg-gray-900 bg-opacity-40 p-4 rounded-lg backdrop-filter backdrop-blur-xl"> | |
| 10 | <ColorPicker /> | |
| 11 | </div> | |
| 12 | </div> | |
| 13 | </div> | |
| 14 | ) | |
| 15 | } |
src/app/playground/function-creator/page.tsx added +81
| @@ -0,0 +1,81 @@ | ||
| 1 | 'use client' | |
| 2 | ||
| 3 | import { useEffect, useState } from 'react' | |
| 4 | import { createClient } from '@/utils/supabase/client' | |
| 5 | import { FunctionCreator } from '@/components/playground/FunctionCreator' | |
| 6 | import { Card } from '@/components/ui/card' | |
| 7 | import { Button } from '@/components/ui/button' | |
| 8 | import Link from 'next/link' | |
| 9 | ||
| 10 | export default function FunctionCreatorPage() { | |
| 11 | const [isAuthenticated, setIsAuthenticated] = useState(false) | |
| 12 | const [isLoading, setIsLoading] = useState(true) | |
| 13 | const supabase = createClient() | |
| 14 | ||
| 15 | useEffect(() => { | |
| 16 | const checkAuth = async () => { | |
| 17 | const { data: { user } } = await supabase.auth.getUser() | |
| 18 | setIsAuthenticated(!!user) | |
| 19 | setIsLoading(false) | |
| 20 | } | |
| 21 | ||
| 22 | checkAuth() | |
| 23 | ||
| 24 | // Listen for auth changes | |
| 25 | const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { | |
| 26 | setIsAuthenticated(!!session) | |
| 27 | }) | |
| 28 | ||
| 29 | return () => subscription.unsubscribe() | |
| 30 | }, [supabase]) | |
| 31 | ||
| 32 | if (isLoading) { | |
| 33 | return ( | |
| 34 | <div className="container mx-auto px-4 flex items-center justify-center min-h-[80vh]"> | |
| 35 | <div className="text-white">Loading...</div> | |
| 36 | </div> | |
| 37 | ) | |
| 38 | } | |
| 39 | ||
| 40 | if (!isAuthenticated) { | |
| 41 | return ( | |
| 42 | <div className="container mx-auto px-4 flex items-center justify-center min-h-[80vh]"> | |
| 43 | <Card className="bg-black/50 backdrop-blur-md border-blue-500 p-8 w-full max-w-md"> | |
| 44 | <div className="space-y-6 text-center"> | |
| 45 | <div className="space-y-2"> | |
| 46 | <h1 className="text-3xl font-bold text-white">Authentication Required</h1> | |
| 47 | <p className="text-gray-300"> | |
| 48 | Sign in to access the Function Creator | |
| 49 | </p> | |
| 50 | </div> | |
| 51 | <div className="text-sm text-gray-400"> | |
| 52 | <p> | |
| 53 | We require authentication to prevent abuse and manage rate limits. | |
| 54 | This feature uses computational resources that need to be monitored. | |
| 55 | </p> | |
| 56 | </div> | |
| 57 | <Link href="/auth/sign-in"> | |
| 58 | <Button className="w-full bg-blue-600 hover:bg-blue-700"> | |
| 59 | Sign In | |
| 60 | </Button> | |
| 61 | </Link> | |
| 62 | </div> | |
| 63 | </Card> | |
| 64 | </div> | |
| 65 | ) | |
| 66 | } | |
| 67 | ||
| 68 | return ( | |
| 69 | <div className="container mx-auto px-4"> | |
| 70 | <h1 className="text-4xl font-bold mb-2 text-center text-white">Function Creator</h1> | |
| 71 | <p className="text-lg text-gray-300 mb-8 text-center"> | |
| 72 | Write and execute TypeScript or Python code in a secure environment | |
| 73 | </p> | |
| 74 | <div className="max-w-[95vw] sm:max-w-4xl mx-auto"> | |
| 75 | <div className="bg-gray-900 bg-opacity-40 p-4 rounded-lg backdrop-filter backdrop-blur-xl"> | |
| 76 | <FunctionCreator /> | |
| 77 | </div> | |
| 78 | </div> | |
| 79 | </div> | |
| 80 | ) | |
| 81 | } |
src/app/playground/page.tsx +38 −2
| @@ -1,11 +1,47 @@ | ||
| 1 | import { PlaygroundTabs } from "@/components/PlaygroundTabs" | |
| 1 | import Link from 'next/link' | |
| 2 | import { Card } from "@/components/ui/card" | |
| 3 | import { CodeIcon, Box, Palette } from 'lucide-react' | |
| 2 | 4 | |
| 3 | 5 | export default function PlaygroundPage() { |
| 6 | const tools = [ | |
| 7 | { | |
| 8 | href: '/playground/function-creator', | |
| 9 | title: 'Function Creator', | |
| 10 | description: 'Write and execute TypeScript or Python code in a secure environment', | |
| 11 | icon: CodeIcon, | |
| 12 | }, | |
| 13 | { | |
| 14 | href: '/playground/3d-viewer', | |
| 15 | title: '3D Object Viewer', | |
| 16 | description: 'Interactive 3D model viewer with orbit controls', | |
| 17 | icon: Box, | |
| 18 | }, | |
| 19 | { | |
| 20 | href: '/playground/color-picker', | |
| 21 | title: 'Color Picker', | |
| 22 | description: 'Select and preview colors', | |
| 23 | icon: Palette, | |
| 24 | }, | |
| 25 | ] | |
| 26 | ||
| 4 | 27 | return ( |
| 5 | 28 | <div className="container mx-auto px-4"> |
| 6 | 29 | <h1 className="text-4xl font-bold mb-2 text-center text-white">Playground</h1> |
| 7 | 30 | <p className="text-lg text-gray-300 mb-8 text-center">Experiments and tools worth keeping around</p> |
| 8 | <PlaygroundTabs /> | |
| 31 | ||
| 32 | <div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| 33 | {tools.map((tool) => ( | |
| 34 | <Link key={tool.href} href={tool.href}> | |
| 35 | <Card className="bg-black/50 backdrop-blur-md border-blue-500 p-6 hover:border-blue-400 transition-all duration-200 cursor-pointer h-full"> | |
| 36 | <div className="flex flex-col items-center text-center space-y-3"> | |
| 37 | <tool.icon className="w-12 h-12 text-blue-400" /> | |
| 38 | <h2 className="text-xl font-semibold text-white">{tool.title}</h2> | |
| 39 | <p className="text-sm text-gray-300">{tool.description}</p> | |
| 40 | </div> | |
| 41 | </Card> | |
| 42 | </Link> | |
| 43 | ))} | |
| 44 | </div> | |
| 9 | 45 | </div> |
| 10 | 46 | ) |
| 11 | 47 | } |
src/components/PlaygroundTabs.tsx +5 −5
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 3 | import React, { useState } from "react"; |
| 4 | 4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; |
| 5 | import { CodeEditor } from "@/components/playground/CodeEditor"; | |
| 5 | import { FunctionCreator } from "@/components/playground/FunctionCreator"; | |
| 6 | 6 | import { ColorPicker } from "@/components/playground/ColorPicker"; |
| 7 | 7 | import { ObjectViewer } from "@/components/playground/ObjectViewer"; |
| 8 | 8 | |
| @@ -29,10 +29,10 @@ export const PlaygroundTabs: React.FC = () => { | ||
| 29 | 29 | Color Picker |
| 30 | 30 | </TabsTrigger> |
| 31 | 31 | <TabsTrigger |
| 32 | value="code-editor" | |
| 32 | value="function-creator" | |
| 33 | 33 | className="flex-1 py-3 px-4 text-sm sm:text-base font-medium text-white hover:bg-white hover:bg-opacity-10 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition-all duration-200 touch-action-manipulation" |
| 34 | 34 | > |
| 35 | Code Editor | |
| 35 | Function Creator | |
| 36 | 36 | </TabsTrigger> |
| 37 | 37 | </TabsList> |
| 38 | 38 | <TabsContent |
| @@ -48,10 +48,10 @@ export const PlaygroundTabs: React.FC = () => { | ||
| 48 | 48 | <ColorPicker /> |
| 49 | 49 | </TabsContent> |
| 50 | 50 | <TabsContent |
| 51 | value="code-editor" | |
| 51 | value="function-creator" | |
| 52 | 52 | className="bg-gray-900 bg-opacity-40 p-4 rounded-b-lg backdrop-filter backdrop-blur-xl" |
| 53 | 53 | > |
| 54 | <CodeEditor /> | |
| 54 | <FunctionCreator /> | |
| 55 | 55 | </TabsContent> |
| 56 | 56 | </Tabs> |
| 57 | 57 | ); |
src/components/playground/AgentChat.tsx added +137
| @@ -0,0 +1,137 @@ | ||
| 1 | 'use client' | |
| 2 | ||
| 3 | import { useChat } from '@ai-sdk/react' | |
| 4 | import type { UIMessage } from '@ai-sdk/react' | |
| 5 | import { Button } from '@/components/ui/button' | |
| 6 | import { SendIcon, Loader2 } from 'lucide-react' | |
| 7 | import { useEffect, useRef, useState } from 'react' | |
| 8 | import ReactMarkdown from 'react-markdown' | |
| 9 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' | |
| 10 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' | |
| 11 | ||
| 12 | export function AgentChat() { | |
| 13 | const { messages, status, sendMessage } = useChat() | |
| 14 | const [input, setInput] = useState('') | |
| 15 | const messagesEndRef = useRef<HTMLDivElement>(null) | |
| 16 | const isLoading = status === 'streaming' | |
| 17 | ||
| 18 | const scrollToBottom = () => { | |
| 19 | messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) | |
| 20 | } | |
| 21 | ||
| 22 | useEffect(() => { | |
| 23 | scrollToBottom() | |
| 24 | }, [messages]) | |
| 25 | ||
| 26 | const getMessageText = (message: UIMessage) => { | |
| 27 | return message.parts | |
| 28 | .filter((part) => part.type === 'text') | |
| 29 | .map((part) => part.text) | |
| 30 | .join('') | |
| 31 | } | |
| 32 | ||
| 33 | return ( | |
| 34 | <div className="flex flex-col h-full"> | |
| 35 | {/* Messages Area */} | |
| 36 | <div className="flex-1 overflow-y-auto space-y-4 p-4 min-h-0"> | |
| 37 | {messages.length === 0 ? ( | |
| 38 | <div className="flex items-center justify-center h-full text-gray-400 text-center"> | |
| 39 | <div className="space-y-2"> | |
| 40 | <p className="text-lg font-medium">AI Coding Assistant</p> | |
| 41 | <p className="text-sm"> | |
| 42 | Ask me to help you write code, explain concepts, or debug issues | |
| 43 | </p> | |
| 44 | </div> | |
| 45 | </div> | |
| 46 | ) : ( | |
| 47 | <> | |
| 48 | {messages.map((message: UIMessage) => { | |
| 49 | const textContent = getMessageText(message) | |
| 50 | ||
| 51 | return ( | |
| 52 | <div | |
| 53 | key={message.id} | |
| 54 | className={`flex ${ | |
| 55 | message.role === 'user' ? 'justify-end' : 'justify-start' | |
| 56 | }`} | |
| 57 | > | |
| 58 | <div | |
| 59 | className={`max-w-[85%] sm:max-w-[75%] rounded-lg px-4 py-2 ${ | |
| 60 | message.role === 'user' | |
| 61 | ? 'bg-blue-600 text-white' | |
| 62 | : 'bg-gray-800 text-gray-200' | |
| 63 | }`} | |
| 64 | > | |
| 65 | {message.role === 'user' ? ( | |
| 66 | <p className="whitespace-pre-wrap break-words">{textContent}</p> | |
| 67 | ) : ( | |
| 68 | <ReactMarkdown | |
| 69 | className="prose prose-invert prose-sm max-w-none" | |
| 70 | components={{ | |
| 71 | code({ inline, className, children, ...props }: any) { | |
| 72 | const match = /language-(\w+)/.exec(className || '') | |
| 73 | return !inline && match ? ( | |
| 74 | <SyntaxHighlighter | |
| 75 | style={vscDarkPlus} | |
| 76 | language={match[1]} | |
| 77 | PreTag="div" | |
| 78 | {...props} | |
| 79 | > | |
| 80 | {String(children).replace(/\n$/, '')} | |
| 81 | </SyntaxHighlighter> | |
| 82 | ) : ( | |
| 83 | <code className="bg-gray-700 px-1 py-0.5 rounded text-sm" {...props}> | |
| 84 | {children} | |
| 85 | </code> | |
| 86 | ) | |
| 87 | }, | |
| 88 | }} | |
| 89 | > | |
| 90 | {textContent} | |
| 91 | </ReactMarkdown> | |
| 92 | )} | |
| 93 | </div> | |
| 94 | </div> | |
| 95 | ) | |
| 96 | })} | |
| 97 | <div ref={messagesEndRef} /> | |
| 98 | </> | |
| 99 | )} | |
| 100 | </div> | |
| 101 | ||
| 102 | {/* Input Area */} | |
| 103 | <div className="border-t border-gray-700 p-4 bg-gray-900/50"> | |
| 104 | <form | |
| 105 | onSubmit={(e) => { | |
| 106 | e.preventDefault() | |
| 107 | if (input.trim()) { | |
| 108 | sendMessage({ role: 'user', parts: [{ type: 'text', text: input }] }) | |
| 109 | setInput('') | |
| 110 | } | |
| 111 | }} | |
| 112 | className="flex gap-2" | |
| 113 | > | |
| 114 | <input | |
| 115 | value={input} | |
| 116 | onChange={(e) => setInput(e.target.value)} | |
| 117 | placeholder="Ask me anything about your code..." | |
| 118 | disabled={isLoading} | |
| 119 | className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50" | |
| 120 | /> | |
| 121 | <Button | |
| 122 | type="submit" | |
| 123 | disabled={isLoading || !input.trim()} | |
| 124 | size="sm" | |
| 125 | className="bg-blue-600 hover:bg-blue-700" | |
| 126 | > | |
| 127 | {isLoading ? ( | |
| 128 | <Loader2 className="w-4 h-4 animate-spin" /> | |
| 129 | ) : ( | |
| 130 | <SendIcon className="w-4 h-4" /> | |
| 131 | )} | |
| 132 | </Button> | |
| 133 | </form> | |
| 134 | </div> | |
| 135 | </div> | |
| 136 | ) | |
| 137 | } |
src/components/playground/FunctionCreator.tsx added +444
| @@ -0,0 +1,444 @@ | ||
| 1 | 'use client' | |
| 2 | ||
| 3 | import React, { useState } from 'react' | |
| 4 | import dynamic from 'next/dynamic' | |
| 5 | import { Card } from "@/components/ui/card" | |
| 6 | import { Button } from "@/components/ui/button" | |
| 7 | import { PlayIcon, ClockIcon, CodeIcon, SettingsIcon, XIcon, MessageSquare, Code2 } from 'lucide-react' | |
| 8 | import { toast } from '@/hooks/use-toast' | |
| 9 | import { AgentChat } from './AgentChat' | |
| 10 | ||
| 11 | const DynamicCodeEditor = dynamic( | |
| 12 | () => import('@uiw/react-textarea-code-editor').then((mod) => mod.default), | |
| 13 | { ssr: false } | |
| 14 | ) | |
| 15 | ||
| 16 | type Language = 'typescript' | 'python'; | |
| 17 | ||
| 18 | const defaultCode = { | |
| 19 | typescript: `// TypeScript Example | |
| 20 | console.log("Hello from TypeScript!"); | |
| 21 | ||
| 22 | // You can use modern TypeScript features | |
| 23 | const greet = (name: string): string => { | |
| 24 | return \`Hello, \${name}!\`; | |
| 25 | }; | |
| 26 | ||
| 27 | console.log(greet("World")); | |
| 28 | ||
| 29 | // Async/await is supported | |
| 30 | async function fetchData() { | |
| 31 | return "Data fetched!"; | |
| 32 | } | |
| 33 | ||
| 34 | fetchData().then(console.log); | |
| 35 | ||
| 36 | // Access environment variables | |
| 37 | console.log("Environment:", process.env.NODE_ENV);`, | |
| 38 | python: `# Python Example | |
| 39 | print("Hello from Python!") | |
| 40 | ||
| 41 | # You can use modern Python features | |
| 42 | def greet(name: str) -> str: | |
| 43 | return f"Hello, {name}!" | |
| 44 | ||
| 45 | print(greet("World")) | |
| 46 | ||
| 47 | # List comprehensions | |
| 48 | squares = [x**2 for x in range(1, 6)] | |
| 49 | print(f"Squares: {squares}") | |
| 50 | ||
| 51 | # Async is supported in Python 3.13 | |
| 52 | import asyncio | |
| 53 | ||
| 54 | async def fetch_data(): | |
| 55 | return "Data fetched!" | |
| 56 | ||
| 57 | print(asyncio.run(fetch_data())) | |
| 58 | ||
| 59 | # Access environment variables | |
| 60 | import os | |
| 61 | print(f"Environment: {os.getenv('ENVIRONMENT', 'development')}")` | |
| 62 | }; | |
| 63 | ||
| 64 | interface ExecutionResult { | |
| 65 | success: boolean; | |
| 66 | output?: string; | |
| 67 | error?: string; | |
| 68 | exitCode?: number; | |
| 69 | } | |
| 70 | ||
| 71 | interface EnvVar { | |
| 72 | key: string; | |
| 73 | value: string; | |
| 74 | } | |
| 75 | ||
| 76 | export const FunctionCreator: React.FC = () => { | |
| 77 | const [language, setLanguage] = useState<Language>('typescript'); | |
| 78 | const [code, setCode] = useState(defaultCode.typescript); | |
| 79 | const [isExecuting, setIsExecuting] = useState(false); | |
| 80 | const [isScheduling, setIsScheduling] = useState(false); | |
| 81 | const [output, setOutput] = useState<string>(''); | |
| 82 | const [cronExpression, setCronExpression] = useState('0 * * * *'); // Every hour | |
| 83 | const [showScheduler, setShowScheduler] = useState(false); | |
| 84 | const [showEnvVars, setShowEnvVars] = useState(false); | |
| 85 | const [envVars, setEnvVars] = useState<EnvVar[]>([{ key: '', value: '' }]); | |
| 86 | const [showAgent, setShowAgent] = useState(false); | |
| 87 | ||
| 88 | const handleLanguageChange = (newLanguage: Language) => { | |
| 89 | setLanguage(newLanguage); | |
| 90 | setCode(defaultCode[newLanguage]); | |
| 91 | setOutput(''); | |
| 92 | }; | |
| 93 | ||
| 94 | const addEnvVar = () => { | |
| 95 | setEnvVars([...envVars, { key: '', value: '' }]); | |
| 96 | }; | |
| 97 | ||
| 98 | const removeEnvVar = (index: number) => { | |
| 99 | setEnvVars(envVars.filter((_, i) => i !== index)); | |
| 100 | }; | |
| 101 | ||
| 102 | const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => { | |
| 103 | const newEnvVars = [...envVars]; | |
| 104 | newEnvVars[index][field] = value; | |
| 105 | setEnvVars(newEnvVars); | |
| 106 | }; | |
| 107 | ||
| 108 | const getEnvVarsObject = () => { | |
| 109 | return envVars.reduce((acc, { key, value }) => { | |
| 110 | if (key.trim()) { | |
| 111 | acc[key.trim()] = value; | |
| 112 | } | |
| 113 | return acc; | |
| 114 | }, {} as Record<string, string>); | |
| 115 | }; | |
| 116 | ||
| 117 | const handleRun = async () => { | |
| 118 | setIsExecuting(true); | |
| 119 | setOutput('Executing code...'); | |
| 120 | ||
| 121 | try { | |
| 122 | const response = await fetch('/api/sandbox/execute', { | |
| 123 | method: 'POST', | |
| 124 | headers: { | |
| 125 | 'Content-Type': 'application/json', | |
| 126 | }, | |
| 127 | body: JSON.stringify({ | |
| 128 | code, | |
| 129 | language, | |
| 130 | envVars: getEnvVarsObject(), | |
| 131 | }), | |
| 132 | }); | |
| 133 | ||
| 134 | const result: ExecutionResult = await response.json(); | |
| 135 | ||
| 136 | if (result.success) { | |
| 137 | setOutput(result.output || 'Code executed successfully!'); | |
| 138 | toast({ | |
| 139 | title: "Success", | |
| 140 | description: "Code executed successfully", | |
| 141 | }); | |
| 142 | } else { | |
| 143 | setOutput(`Error (exit code ${result.exitCode}):\n${result.error || 'Unknown error'}`); | |
| 144 | toast({ | |
| 145 | title: "Execution Error", | |
| 146 | description: "Check the output panel for details", | |
| 147 | variant: "destructive", | |
| 148 | }); | |
| 149 | } | |
| 150 | } catch (error) { | |
| 151 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; | |
| 152 | setOutput(`Failed to execute code: ${errorMessage}`); | |
| 153 | toast({ | |
| 154 | title: "Error", | |
| 155 | description: "Failed to execute code", | |
| 156 | variant: "destructive", | |
| 157 | }); | |
| 158 | } finally { | |
| 159 | setIsExecuting(false); | |
| 160 | } | |
| 161 | }; | |
| 162 | ||
| 163 | const handleSchedule = async () => { | |
| 164 | setIsScheduling(true); | |
| 165 | ||
| 166 | try { | |
| 167 | const response = await fetch('/api/sandbox/schedule', { | |
| 168 | method: 'POST', | |
| 169 | headers: { | |
| 170 | 'Content-Type': 'application/json', | |
| 171 | }, | |
| 172 | body: JSON.stringify({ | |
| 173 | code, | |
| 174 | language, | |
| 175 | cronExpression, | |
| 176 | }), | |
| 177 | }); | |
| 178 | ||
| 179 | const result = await response.json(); | |
| 180 | ||
| 181 | if (result.success) { | |
| 182 | toast({ | |
| 183 | title: "Scheduled", | |
| 184 | description: `Function scheduled with cron: ${cronExpression}`, | |
| 185 | }); | |
| 186 | setShowScheduler(false); | |
| 187 | } else { | |
| 188 | toast({ | |
| 189 | title: "Scheduling Error", | |
| 190 | description: result.error || "Failed to schedule function", | |
| 191 | variant: "destructive", | |
| 192 | }); | |
| 193 | } | |
| 194 | } catch (error) { | |
| 195 | toast({ | |
| 196 | title: "Error", | |
| 197 | description: "Failed to schedule function", | |
| 198 | variant: "destructive", | |
| 199 | }); | |
| 200 | } finally { | |
| 201 | setIsScheduling(false); | |
| 202 | } | |
| 203 | }; | |
| 204 | ||
| 205 | return ( | |
| 206 | <div className="space-y-4"> | |
| 207 | {/* Header with Language Selector */} | |
| 208 | <div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between"> | |
| 209 | <div className="flex flex-wrap gap-2"> | |
| 210 | <Button | |
| 211 | variant={language === 'typescript' ? 'default' : 'outline'} | |
| 212 | size="sm" | |
| 213 | onClick={() => handleLanguageChange('typescript')} | |
| 214 | className="transition-all duration-200" | |
| 215 | > | |
| 216 | <CodeIcon className="w-4 h-4 mr-2" /> | |
| 217 | TypeScript | |
| 218 | </Button> | |
| 219 | <Button | |
| 220 | variant={language === 'python' ? 'default' : 'outline'} | |
| 221 | size="sm" | |
| 222 | onClick={() => handleLanguageChange('python')} | |
| 223 | className="transition-all duration-200" | |
| 224 | > | |
| 225 | <CodeIcon className="w-4 h-4 mr-2" /> | |
| 226 | Python | |
| 227 | </Button> | |
| 228 | </div> | |
| 229 | ||
| 230 | <div className="flex flex-wrap gap-2 w-full sm:w-auto"> | |
| 231 | <Button | |
| 232 | onClick={handleRun} | |
| 233 | disabled={isExecuting} | |
| 234 | className="flex-1 sm:flex-initial bg-green-600 hover:bg-green-700 transition-all duration-200" | |
| 235 | size="sm" | |
| 236 | > | |
| 237 | <PlayIcon className="w-4 h-4 mr-2" /> | |
| 238 | {isExecuting ? 'Running...' : 'Run'} | |
| 239 | </Button> | |
| 240 | <Button | |
| 241 | onClick={() => setShowEnvVars(!showEnvVars)} | |
| 242 | variant="outline" | |
| 243 | className="flex-1 sm:flex-initial transition-all duration-200" | |
| 244 | size="sm" | |
| 245 | > | |
| 246 | <SettingsIcon className="w-4 h-4 mr-2" /> | |
| 247 | Env Vars | |
| 248 | </Button> | |
| 249 | <Button | |
| 250 | onClick={() => setShowScheduler(!showScheduler)} | |
| 251 | variant="outline" | |
| 252 | className="flex-1 sm:flex-initial transition-all duration-200" | |
| 253 | size="sm" | |
| 254 | > | |
| 255 | <ClockIcon className="w-4 h-4 mr-2" /> | |
| 256 | Schedule | |
| 257 | </Button> | |
| 258 | </div> | |
| 259 | </div> | |
| 260 | ||
| 261 | {/* Environment Variables Panel */} | |
| 262 | {showEnvVars && ( | |
| 263 | <Card className="bg-black/50 backdrop-blur-md border-blue-500 p-4 animate-in slide-in-from-top-2 duration-200"> | |
| 264 | <div className="space-y-3"> | |
| 265 | <h3 className="text-lg font-semibold text-white">Environment Variables</h3> | |
| 266 | <div className="space-y-3"> | |
| 267 | {envVars.map((envVar, index) => ( | |
| 268 | <div key={index} className="flex flex-col sm:flex-row gap-2"> | |
| 269 | <div className="flex flex-col sm:flex-row gap-2 flex-1"> | |
| 270 | <input | |
| 271 | type="text" | |
| 272 | value={envVar.key} | |
| 273 | onChange={(e) => updateEnvVar(index, 'key', e.target.value)} | |
| 274 | placeholder="KEY" | |
| 275 | className="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| 276 | /> | |
| 277 | <input | |
| 278 | type="text" | |
| 279 | value={envVar.value} | |
| 280 | onChange={(e) => updateEnvVar(index, 'value', e.target.value)} | |
| 281 | placeholder="value" | |
| 282 | className="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| 283 | /> | |
| 284 | </div> | |
| 285 | {envVars.length > 1 && ( | |
| 286 | <Button | |
| 287 | onClick={() => removeEnvVar(index)} | |
| 288 | variant="outline" | |
| 289 | size="sm" | |
| 290 | className="px-2 w-full sm:w-auto" | |
| 291 | > | |
| 292 | <XIcon className="w-4 h-4 sm:mr-0 mr-2" /> | |
| 293 | <span className="sm:hidden">Remove</span> | |
| 294 | </Button> | |
| 295 | )} | |
| 296 | </div> | |
| 297 | ))} | |
| 298 | </div> | |
| 299 | <div className="flex flex-col sm:flex-row gap-2"> | |
| 300 | <Button | |
| 301 | onClick={addEnvVar} | |
| 302 | variant="outline" | |
| 303 | size="sm" | |
| 304 | className="w-full sm:w-auto" | |
| 305 | > | |
| 306 | Add Variable | |
| 307 | </Button> | |
| 308 | <Button | |
| 309 | onClick={() => setShowEnvVars(false)} | |
| 310 | variant="outline" | |
| 311 | size="sm" | |
| 312 | className="w-full sm:w-auto" | |
| 313 | > | |
| 314 | Done | |
| 315 | </Button> | |
| 316 | </div> | |
| 317 | </div> | |
| 318 | </Card> | |
| 319 | )} | |
| 320 | ||
| 321 | {/* Scheduler Panel */} | |
| 322 | {showScheduler && ( | |
| 323 | <Card className="bg-black/50 backdrop-blur-md border-blue-500 p-4 animate-in slide-in-from-top-2 duration-200"> | |
| 324 | <div className="space-y-3"> | |
| 325 | <h3 className="text-lg font-semibold text-white">Schedule Function</h3> | |
| 326 | <div className="space-y-2"> | |
| 327 | <label className="text-sm text-gray-300"> | |
| 328 | Cron Expression | |
| 329 | <span className="text-xs text-gray-400 ml-2">(minute hour day month weekday)</span> | |
| 330 | </label> | |
| 331 | <input | |
| 332 | type="text" | |
| 333 | value={cronExpression} | |
| 334 | onChange={(e) => setCronExpression(e.target.value)} | |
| 335 | placeholder="0 * * * *" | |
| 336 | className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| 337 | /> | |
| 338 | <div className="text-xs text-gray-400 space-y-1"> | |
| 339 | <p>Examples:</p> | |
| 340 | <ul className="list-disc list-inside space-y-0.5"> | |
| 341 | <li><code className="bg-gray-800 px-1 rounded">0 * * * *</code> - Every hour</li> | |
| 342 | <li><code className="bg-gray-800 px-1 rounded">*/15 * * * *</code> - Every 15 minutes</li> | |
| 343 | <li><code className="bg-gray-800 px-1 rounded">0 0 * * *</code> - Daily at midnight</li> | |
| 344 | <li><code className="bg-gray-800 px-1 rounded">0 9 * * 1</code> - Every Monday at 9 AM</li> | |
| 345 | </ul> | |
| 346 | </div> | |
| 347 | </div> | |
| 348 | <div className="flex gap-2"> | |
| 349 | <Button | |
| 350 | onClick={handleSchedule} | |
| 351 | disabled={isScheduling} | |
| 352 | className="bg-blue-600 hover:bg-blue-700" | |
| 353 | size="sm" | |
| 354 | > | |
| 355 | {isScheduling ? 'Scheduling...' : 'Schedule Function'} | |
| 356 | </Button> | |
| 357 | <Button | |
| 358 | onClick={() => setShowScheduler(false)} | |
| 359 | variant="outline" | |
| 360 | size="sm" | |
| 361 | > | |
| 362 | Cancel | |
| 363 | </Button> | |
| 364 | </div> | |
| 365 | </div> | |
| 366 | </Card> | |
| 367 | )} | |
| 368 | ||
| 369 | {/* Code Editor or Agent Chat */} | |
| 370 | {!showAgent ? ( | |
| 371 | <Card className="bg-black/50 backdrop-blur-md border-blue-500 p-4 overflow-hidden"> | |
| 372 | <DynamicCodeEditor | |
| 373 | value={code} | |
| 374 | language={language === 'typescript' ? 'tsx' : 'py'} | |
| 375 | placeholder={`Enter ${language === 'typescript' ? 'TypeScript' : 'Python'} code...`} | |
| 376 | onChange={(evn) => setCode(evn.target.value)} | |
| 377 | padding={15} | |
| 378 | style={{ | |
| 379 | fontSize: 14, | |
| 380 | backgroundColor: "transparent", | |
| 381 | fontFamily: 'ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace', | |
| 382 | minHeight: '300px', | |
| 383 | }} | |
| 384 | className="text-white rounded overflow-auto" | |
| 385 | /> | |
| 386 | </Card> | |
| 387 | ) : ( | |
| 388 | <Card className="bg-black/50 backdrop-blur-md border-purple-500 p-0 overflow-hidden h-[500px]"> | |
| 389 | <AgentChat /> | |
| 390 | </Card> | |
| 391 | )} | |
| 392 | ||
| 393 | {/* Output Panel */} | |
| 394 | {output && ( | |
| 395 | <Card className="bg-black/50 backdrop-blur-md border-green-500 p-4 animate-in slide-in-from-bottom-2 duration-200"> | |
| 396 | <div className="space-y-2"> | |
| 397 | <h3 className="text-sm font-semibold text-green-400 flex items-center gap-2"> | |
| 398 | <div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" /> | |
| 399 | Output | |
| 400 | </h3> | |
| 401 | <pre className="text-sm text-gray-200 whitespace-pre-wrap break-words overflow-x-auto max-h-64 overflow-y-auto bg-gray-900/50 p-3 rounded border border-gray-700"> | |
| 402 | {output} | |
| 403 | </pre> | |
| 404 | </div> | |
| 405 | </Card> | |
| 406 | )} | |
| 407 | ||
| 408 | {/* Info Banner */} | |
| 409 | <div className="text-xs text-gray-400 bg-gray-800/30 backdrop-blur-sm p-3 rounded border border-gray-700"> | |
| 410 | <p className="flex items-start gap-2"> | |
| 411 | <span className="text-blue-400">ℹ️</span> | |
| 412 | <span> | |
| 413 | Your code runs in an isolated, secure environment. Each execution is ephemeral and cannot access external resources without explicit configuration. | |
| 414 | </span> | |
| 415 | </p> | |
| 416 | </div> | |
| 417 | ||
| 418 | {/* AI Agent Toggle */} | |
| 419 | <div className="flex justify-center"> | |
| 420 | <Button | |
| 421 | onClick={() => setShowAgent(!showAgent)} | |
| 422 | variant="outline" | |
| 423 | className={`transition-all duration-200 ${ | |
| 424 | showAgent | |
| 425 | ? 'bg-purple-600/20 border-purple-500 hover:bg-purple-600/30' | |
| 426 | : 'border-gray-700 hover:bg-gray-800' | |
| 427 | }`} | |
| 428 | > | |
| 429 | {showAgent ? ( | |
| 430 | <> | |
| 431 | <Code2 className="w-4 h-4 mr-2" /> | |
| 432 | Back to Code | |
| 433 | </> | |
| 434 | ) : ( | |
| 435 | <> | |
| 436 | <MessageSquare className="w-4 h-4 mr-2" /> | |
| 437 | Chat with AI Agent | |
| 438 | </> | |
| 439 | )} | |
| 440 | </Button> | |
| 441 | </div> | |
| 442 | </div> | |
| 443 | ) | |
| 444 | } |
src/utils/supabase/middleware.ts +13
| @@ -27,6 +27,19 @@ export async function updateSession(request: NextRequest) { | ||
| 27 | 27 | } |
| 28 | 28 | ) |
| 29 | 29 | |
| 30 | // Get the user session | |
| 31 | const { data: { user } } = await supabase.auth.getUser() | |
| 32 | ||
| 33 | // Protect API routes that require authentication | |
| 34 | if (request.nextUrl.pathname.startsWith('/api/sandbox/')) { | |
| 35 | if (!user) { | |
| 36 | return NextResponse.json( | |
| 37 | { error: 'Authentication required' }, | |
| 38 | { status: 401 } | |
| 39 | ) | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 30 | 43 | // IMPORTANT: Avoid writing any logic between createServerClient and |
| 31 | 44 | // supabase.auth.getUser(). A simple mistake could make it very hard to debug |
| 32 | 45 | // issues with users being randomly logged out. |