Public: anyone can read this; the crew can change it.

Implement Function Creator with Vercel Sandbox integration #20

opened this view on origin

Check · Review · Land · Live — It conflicts with its base. Next: builder, it conflicts with its base.

Conflicts with main

claude/implement-function-creator-011CUYGAKQqspM2Th78EXKZE → main 10 commits

  • Can't merge. Conflicts in 3 files: package-lock.json, package.json, src/components/playground/FunctionCreator.tsx. Merge main into the branch and push again.

    Check faileddeploy 8185565

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).

Review

Sign in to review this change.
15 files changed +1610 −15
package-lock.json +354 −4
@@ -8,6 +8,8 @@
88 "name": "trevato.dev",
99 "version": "0.1.0",
1010 "dependencies": {
11 "@ai-sdk/anthropic": "^2.0.38",
12 "@ai-sdk/react": "^2.0.81",
1113 "@calcom/embed-react": "^1.5.0",
1214 "@radix-ui/react-avatar": "^1.1.0",
1315 "@radix-ui/react-dialog": "^1.1.1",
@@ -22,6 +24,8 @@
2224 "@supabase/ssr": "^0.5.1",
2325 "@uiw/react-textarea-code-editor": "^3.0.2",
2426 "@vercel/analytics": "^1.5.0",
27 "@vercel/sandbox": "^1.0.2",
28 "ai": "^5.0.81",
2529 "class-variance-authority": "^0.7.0",
2630 "clsx": "^2.1.1",
2731 "framer-motion": "^11.5.4",
@@ -39,7 +43,8 @@
3943 "remark-gfm": "^4.0.0",
4044 "tailwind-merge": "^2.5.2",
4145 "tailwindcss-animate": "^1.0.7",
42 "three": "^0.168.0"
46 "three": "^0.168.0",
47 "zod": "^3.25.76"
4348 },
4449 "devDependencies": {
4550 "@types/node": "^20",
@@ -53,6 +58,101 @@
5358 "typescript": "^5"
5459 }
5560 },
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 },
56156 "node_modules/@alloc/quick-lru": {
57157 "version": "5.2.0",
58158 "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -831,6 +931,15 @@
831931 "node": ">= 18"
832932 }
833933 },
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 },
834943 "node_modules/@pkgjs/parseargs": {
835944 "version": "0.11.0",
836945 "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1647,6 +1756,12 @@
16471756 "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==",
16481757 "dev": true
16491758 },
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 },
16501765 "node_modules/@supabase/auth-js": {
16511766 "version": "2.65.0",
16521767 "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.0.tgz",
@@ -1816,9 +1931,10 @@
18161931 }
18171932 },
18181933 "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"
18221938 },
18231939 "node_modules/@types/node": {
18241940 "version": "20.16.5",
@@ -2219,6 +2335,43 @@
22192335 }
22202336 }
22212337 },
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 },
22222375 "node_modules/@webgpu/types": {
22232376 "version": "0.1.44",
22242377 "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz",
@@ -2246,6 +2399,24 @@
22462399 "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
22472400 }
22482401 },
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 },
22492420 "node_modules/ajv": {
22502421 "version": "6.12.6",
22512422 "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2497,6 +2668,15 @@
24972668 "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
24982669 "dev": true
24992670 },
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 },
25002680 "node_modules/available-typed-arrays": {
25012681 "version": "1.0.7",
25022682 "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2544,6 +2724,20 @@
25442724 "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
25452725 "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
25462726 },
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 },
25472741 "node_modules/base64-js": {
25482742 "version": "1.5.1",
25492743 "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3829,6 +4023,24 @@
38294023 "node": ">=0.10.0"
38304024 }
38314025 },
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 },
38324044 "node_modules/extend": {
38334045 "version": "3.0.2",
38344046 "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -3840,6 +4052,12 @@
38404052 "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
38414053 "dev": true
38424054 },
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 },
38434061 "node_modules/fast-glob": {
38444062 "version": "3.3.2",
38454063 "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -5262,6 +5480,12 @@
52625480 "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
52635481 "dev": true
52645482 },
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 },
52655489 "node_modules/json-schema-traverse": {
52665490 "version": "0.4.1",
52675491 "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -5286,6 +5510,12 @@
52865510 "json5": "lib/cli.js"
52875511 }
52885512 },
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 },
52895519 "node_modules/jsx-ast-utils": {
52905520 "version": "3.3.5",
52915521 "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -7994,6 +8224,15 @@
79948224 "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
79958225 }
79968226 },
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 },
79978236 "node_modules/reusify": {
79988237 "version": "1.0.4",
79998238 "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -8268,6 +8507,17 @@
82688507 "node": ">=10.0.0"
82698508 }
82708509 },
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 },
82718521 "node_modules/string-width": {
82728522 "version": "5.1.2",
82738523 "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -8562,6 +8812,28 @@
85628812 "react": ">=17.0"
85638813 }
85648814 },
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 },
85658837 "node_modules/tailwind-merge": {
85668838 "version": "2.5.2",
85678839 "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",
@@ -8624,6 +8896,54 @@
86248896 "node": ">=6"
86258897 }
86268898 },
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 },
86278947 "node_modules/text-table": {
86288948 "version": "0.2.0",
86298949 "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -8683,6 +9003,18 @@
86839003 "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
86849004 "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="
86859005 },
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 },
86869018 "node_modules/to-regex-range": {
86879019 "version": "5.0.1",
86889020 "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -8939,6 +9271,15 @@
89399271 "url": "https://github.com/sponsors/ljharb"
89409272 }
89419273 },
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 },
89429283 "node_modules/undici-types": {
89439284 "version": "6.19.8",
89449285 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@@ -9469,6 +9810,15 @@
94699810 "url": "https://github.com/sponsors/sindresorhus"
94709811 }
94719812 },
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 },
94729822 "node_modules/zustand": {
94739823 "version": "3.7.2",
94749824 "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
package.json +6 −1
@@ -9,6 +9,8 @@
99 "lint": "next lint"
1010 },
1111 "dependencies": {
12 "@ai-sdk/anthropic": "^2.0.38",
13 "@ai-sdk/react": "^2.0.81",
1214 "@calcom/embed-react": "^1.5.0",
1315 "@radix-ui/react-avatar": "^1.1.0",
1416 "@radix-ui/react-dialog": "^1.1.1",
@@ -23,6 +25,8 @@
2325 "@supabase/ssr": "^0.5.1",
2426 "@uiw/react-textarea-code-editor": "^3.0.2",
2527 "@vercel/analytics": "^1.5.0",
28 "@vercel/sandbox": "^1.0.2",
29 "ai": "^5.0.81",
2630 "class-variance-authority": "^0.7.0",
2731 "clsx": "^2.1.1",
2832 "framer-motion": "^11.5.4",
@@ -40,7 +44,8 @@
4044 "remark-gfm": "^4.0.0",
4145 "tailwind-merge": "^2.5.2",
4246 "tailwindcss-animate": "^1.0.7",
43 "three": "^0.168.0"
47 "three": "^0.168.0",
48 "zod": "^3.25.76"
4449 },
4550 "devDependencies": {
4651 "@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 Email
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'
24
35 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
427 return (
528 <div className="container mx-auto px-4">
629 <h1 className="text-4xl font-bold mb-2 text-center text-white">Playground</h1>
730 <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>
945 </div>
1046 )
1147 }
src/components/FloatingNavigation.tsx +56 −3
@@ -1,19 +1,48 @@
11 'use client'
22
3 import React, { useState } from 'react'
3 import React, { useState, useEffect } from 'react'
44 import { Button } from "@/components/ui/button"
55 import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
66 import Link from 'next/link'
7 import { Menu } from 'lucide-react'
7 import { useRouter } from 'next/navigation'
8 import { Menu, LogOut } from 'lucide-react'
9 import { createClient } from '@/utils/supabase/client'
810
911 const navItems = [
1012 { href: '/', label: 'Home' },
11 { href: '/playground', label: 'Playground' },
13 { href: '/playground/function-creator', label: 'Function Creator' },
14 { href: '/playground/3d-viewer', label: '3D Viewer' },
15 { href: '/playground/color-picker', label: 'Color Picker' },
1216 { href: '/projects', label: 'Side Projects' },
1317 ]
1418
1519 export const FloatingNavigation: React.FC = () => {
1620 const [open, setOpen] = useState(false)
21 const [userEmail, setUserEmail] = useState<string | null>(null)
22 const router = useRouter()
23 const supabase = createClient()
24
25 useEffect(() => {
26 const checkAuth = async () => {
27 const { data: { user } } = await supabase.auth.getUser()
28 setUserEmail(user?.email || null)
29 }
30
31 checkAuth()
32
33 // Listen for auth changes
34 const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
35 setUserEmail(session?.user?.email || null)
36 })
37
38 return () => subscription.unsubscribe()
39 }, [supabase])
40
41 const handleSignOut = async () => {
42 await supabase.auth.signOut()
43 setOpen(false)
44 router.push('/')
45 }
1746
1847 return (
1948 <div className="fixed top-4 right-4 z-50">
@@ -35,6 +64,30 @@ export const FloatingNavigation: React.FC = () => {
3564 {item.label}
3665 </Link>
3766 ))}
67
68 {userEmail && (
69 <>
70 <div className="border-t border-gray-700 pt-4 mt-4">
71 <div className="space-y-3">
72 <div className="text-sm text-gray-400">
73 Signed in as:
74 </div>
75 <div className="text-sm text-white font-medium break-all">
76 {userEmail}
77 </div>
78 <Button
79 onClick={handleSignOut}
80 variant="outline"
81 size="sm"
82 className="w-full"
83 >
84 <LogOut className="w-4 h-4 mr-2" />
85 Sign Out
86 </Button>
87 </div>
88 </div>
89 </>
90 )}
3891 </nav>
3992 </SheetContent>
4093 </Sheet>
src/components/PlaygroundTabs.tsx +5 −5
@@ -2,7 +2,7 @@
22
33 import React, { useState } from "react";
44 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
5 import { CodeEditor } from "@/components/playground/CodeEditor";
5 import { FunctionCreator } from "@/components/playground/FunctionCreator";
66 import { ColorPicker } from "@/components/playground/ColorPicker";
77 import { ObjectViewer } from "@/components/playground/ObjectViewer";
88
@@ -29,10 +29,10 @@ export const PlaygroundTabs: React.FC = () => {
2929 Color Picker
3030 </TabsTrigger>
3131 <TabsTrigger
32 value="code-editor"
32 value="function-creator"
3333 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"
3434 >
35 Code Editor
35 Function Creator
3636 </TabsTrigger>
3737 </TabsList>
3838 <TabsContent
@@ -48,10 +48,10 @@ export const PlaygroundTabs: React.FC = () => {
4848 <ColorPicker />
4949 </TabsContent>
5050 <TabsContent
51 value="code-editor"
51 value="function-creator"
5252 className="bg-gray-900 bg-opacity-40 p-4 rounded-b-lg backdrop-filter backdrop-blur-xl"
5353 >
54 <CodeEditor />
54 <FunctionCreator />
5555 </TabsContent>
5656 </Tabs>
5757 );
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) {
2727 }
2828 )
2929
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
3043 // IMPORTANT: Avoid writing any logic between createServerClient and
3144 // supabase.auth.getUser(). A simple mistake could make it very hard to debug
3245 // issues with users being randomly logged out.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs