Compare commits

...

255 Commits
main ... hosted

Author SHA1 Message Date
Abi Raja
fc70ecac6b when payment method is unknown, stop execution 2024-09-20 13:34:55 +02:00
Abi Raja
06ce9f8c32 get user_id when we check if the user has an active subscription 2024-09-18 15:27:56 +02:00
Abi Raja
1bf3340502 pass user_id and api secret to saas API for storage 2024-09-18 15:20:50 +02:00
Abi Raja
76defbb1c2 bump timeout on storing generations request 2024-09-17 17:45:54 +02:00
Abi Raja
1b37c4e063 capture errors sending to saas backend in sentry 2024-09-17 17:10:40 +02:00
Abi Raja
7b08507b61 capture underlying exception when only one of the generations fails 2024-09-17 16:56:09 +02:00
Abi Raja
659b82b214 log to sentry when one generation fails 2024-09-17 16:00:17 +02:00
Abi Raja
b41f3c65c7 Merge branch 'main' into hosted 2024-09-17 15:56:48 +02:00
Abi Raja
2d1c1019ac Merge branch 'main' into hosted 2024-09-17 12:20:33 +02:00
Abi Raja
d7666fe5ae fix up Plausible events 2024-09-16 15:51:45 +02:00
Abi Raja
76188a7465 force image generation to be true on prod 2024-09-16 15:27:11 +02:00
Abi Raja
6379a1f1c6 Merge branch 'main' into hosted 2024-09-16 15:24:14 +02:00
Abi Raja
3a51e83ee9 disable various irrelevant settings on prod 2024-09-16 14:56:13 +02:00
Abi Raja
a03e823ecb disallow OpenAI key usage 2024-09-16 14:23:34 +02:00
Abi Raja
73827716f9 Merge branch 'main' into hosted 2024-09-14 17:45:06 +02:00
Abi Raja
efc575e033 Merge branch 'main' into hosted 2024-09-13 14:53:01 +02:00
Abi Raja
56526636fb revert sentry change 2024-09-13 11:12:03 +02:00
Abi Raja
da806ff6f2 track variant click 2024-09-13 11:06:51 +02:00
Abi Raja
2888893072 only raise a exception for no payment found when the user is a subscriber 2024-09-12 15:50:01 +02:00
Abi Raja
33793051bc temp log more with sentry 2024-09-12 15:04:39 +02:00
Abi Raja
f9aa14b566 implement saas storage of multiple generations 2024-09-12 12:30:12 +02:00
Abi Raja
960e905a73 fix the lock file 2024-09-10 17:30:21 +02:00
Abi Raja
f1501c0aaf uncomment precommit config 2024-09-10 14:51:52 +02:00
Abi Raja
6600035267 support text to code 2024-09-10 14:46:16 +02:00
Abi Raja
86f6781682 support all forms of payment (subscription, api key, etc.) 2024-09-10 14:28:16 +02:00
Abi Raja
c107f4eda5 merge 2024-09-08 16:50:18 +02:00
Abi Raja
901d3e87ed upgrade clerk library version 2024-08-21 16:45:18 -04:00
Abi Raja
f0fef4f5e3 add an FAQs page 2024-07-30 13:19:49 -04:00
Abi Raja
92f2933f0a update page path to not be nested 2024-07-30 12:54:09 -04:00
Abi Raja
0a953fde63 add a checkout success page that just redirects to home page 2024-07-30 12:47:04 -04:00
Abi Raja
ba317d3e2d switch gtag conversion id 2024-07-30 12:32:12 -04:00
Abi Raja
3d056fcb10 capture image generation errors in Sentry 2024-07-30 10:53:40 -04:00
Abi Raja
2d877c0d84 remove one spinner component 2024-07-30 10:27:54 -04:00
Abi Raja
a40e5da1be move files 2024-07-30 10:21:18 -04:00
Abi Raja
e3e6e6a884 remove feedback call note placement and openai down issue 2024-07-30 10:20:45 -04:00
Abi Raja
8d1f76abec merge updates 2024-07-30 10:12:41 -04:00
Abi Raja
13d3ff6e38 Merge branch 'main' into hosted 2024-07-30 10:12:19 -04:00
Abi Raja
3d44c4406d track going to checkout page as a conversion for Google ads 2024-07-29 17:15:35 -04:00
Abi Raja
64a4ab8c90 improve pricing sell 2024-07-23 11:35:00 -04:00
Abi Raja
4b616342c6 make FAQs 2 column 2024-07-22 15:51:58 -04:00
Abi Raja
b7985b9a4e show FAQ link only on dialog, not on pricing page 2024-07-22 15:50:10 -04:00
Abi Raja
97bd1c924a link to FAQs from Pricing Dialog 2024-07-22 15:46:43 -04:00
Abi Raja
ad9c3eafcc update FAQs 2024-07-22 15:42:39 -04:00
Abi Raja
75317cb542 improve pricing dialog further 2024-07-22 15:34:33 -04:00
Abi Raja
f17a6c1242 add some FAQs 2024-07-22 15:22:54 -04:00
Abi Raja
9230162a8c clear up pricing 2024-07-22 15:05:34 -04:00
Abi Raja
2274268b2f add a separate pricing page 2024-07-22 14:59:51 -04:00
Abi Raja
75e93a66b8 show pricing dialog when credit usage is clicked 2024-07-22 14:48:28 -04:00
Abi Raja
54557de762 remove old non-subscription code 2024-07-22 14:47:09 -04:00
Abi Raja
f9c35839de move files to organize better 2024-07-22 14:37:27 -04:00
Abi Raja
1b22c1ab14 add Google tag 2024-07-22 12:17:34 -04:00
Abi Raja
138cc1f1a5 Merge branch 'main' into hosted 2024-07-19 07:56:13 -04:00
Abi Raja
b733400e91 throw exceptions with stack traces 2024-07-16 12:00:53 -04:00
Abi Raja
f7d0dbb7ce ping Sentry when Claude response is too long 2024-07-15 19:15:35 -04:00
Abi Raja
c092418e47 log and capture to sentry when OpenAI response is too long 2024-07-15 19:14:37 -04:00
Abi Raja
f9d6279b53 Merge branch 'main' into hosted 2024-07-15 19:03:52 -04:00
Abi Raja
5c7aa62414 log stop reason for Claude 2024-07-15 16:41:18 -04:00
Abi Raja
1a26e325be remove unused isPaid label 2024-07-11 16:51:20 -04:00
Abi Raja
50d1288693 Merge branch 'main' into hosted 2024-07-11 16:36:18 -04:00
Abi Raja
4bf472cc23 fix style on avatar dropdown 2024-07-11 15:49:11 -04:00
Abi Raja
456374dc9d improve free user detection (anytime before subscriber tier is loaded or if it is free) 2024-07-11 15:45:04 -04:00
Abi Raja
8a6557ce0c add more separators and link to feature requests 2024-07-11 15:41:24 -04:00
Abi Raja
ba277121db make avatar dropdown more obvious 2024-07-11 15:35:10 -04:00
Abi Raja
b80f0ec7bf Merge branch 'main' into hosted 2024-07-11 14:32:26 -04:00
Abi Raja
55ffd26a52 Update PricingDialog.tsx 2024-07-10 16:17:09 -04:00
Abi Raja
457b95d272 send along subscriber tier for user 2024-07-10 16:06:09 -04:00
Abi Raja
b7c81158ca change email support to intercom contact support 2024-07-10 15:50:49 -04:00
Abi Raja
1d1cbfc6f6 remove all references to crisp 2024-07-09 17:16:51 -04:00
Abi Raja
7f7d0c3504 send input mode to saas backend 2024-07-09 16:45:07 -04:00
Abi Raja
8c11366bd0 add sentry log for text generation 2024-07-09 13:17:00 -04:00
Abi Raja
7c132fd1bd Merge branch 'generate-from-text' into hosted 2024-07-09 13:04:30 -04:00
Abi Raja
2ec4bf59d3 add a beta label 2024-07-09 13:03:27 -04:00
Abi Raja
d13ae72c06 add basic text to ui generation 2024-07-09 12:53:07 -04:00
Abi Raja
d8b1c0ba4d initialize intercom for all users regardless of subscriber status 2024-07-07 14:05:08 -04:00
Abi Raja
0171ff9f3b support intercom for all users 2024-07-07 13:34:02 -04:00
Abi Raja
549210dcd4 Merge branch 'main' into hosted 2024-07-03 10:09:23 -04:00
Abi Raja
c1fa5c4e97 Merge branch 'main' into hosted 2024-06-27 17:59:23 +08:00
Abi Raja
92ea62e9bf Merge branch 'main' into hosted 2024-06-27 17:37:01 +08:00
Abi Raja
1f9e93a2c7 capture the stack trace properly 2024-06-27 12:33:55 +08:00
Abi Raja
5e00b406af capture HTML tag extraction in Sentry so that we have full completion text 2024-06-27 12:25:05 +08:00
Abi Raja
cda34ae642 Merge branch 'main' into hosted 2024-06-26 16:54:33 +08:00
Abi Raja
b44f583a90 Merge branch 'main' into hosted 2024-06-26 16:42:16 +08:00
Abi Raja
a928631a49 Merge branch 'main' into hosted 2024-06-26 13:44:31 +08:00
Abi Raja
efdad0ad50 Merge branch 'main' into hosted 2024-06-26 13:28:52 +08:00
Abi Raja
cea45af385 update lock file 2024-06-26 12:42:29 +08:00
Abi Raja
fed7fe50d6 Merge branch 'main' into hosted 2024-06-26 12:39:11 +08:00
Abi Raja
8951864d63 update star count 2024-06-24 16:28:17 +08:00
Abi Raja
d2369cb0a0 Merge branch 'main' into hosted 2024-06-22 19:43:52 +08:00
Abi Raja
ae08466405 store stack and some other properties for generations on prod 2024-06-07 15:05:26 -04:00
Abi Raja
df5f954ee2 remove help scout 2024-06-07 14:13:39 -04:00
Abi Raja
5109695873 add comment 2024-06-07 14:12:01 -04:00
Abi Raja
d56242af2c add crisp 2024-06-07 14:11:29 -04:00
Abi Raja
7d4d62aa41 Merge branch 'select-and-edit' into hosted 2024-06-05 14:49:15 -04:00
Abi Raja
29978828a4 add help scout support chat for Pro users 2024-06-05 14:20:37 -04:00
Abi Raja
2f260f5442 disable free trial 2024-05-31 13:39:02 -04:00
Abi Raja
801458eb50 enable free trial 2024-05-31 12:03:34 -04:00
Abi Raja
214dbb60e6 toggle feature flag 2024-05-30 19:24:03 -04:00
Abi Raja
d4e3405d0a if user is trialing, set payment method to trial 2024-05-30 17:40:26 -04:00
Abi Raja
d96931eeae enable free trial 2024-05-30 17:18:54 -04:00
Abi Raja
7293c979df alert me via sentry 2024-05-30 12:46:40 -04:00
Abi Raja
b191d72d12 abstract feedback call note into a feature flag 2024-05-30 12:43:00 -04:00
Abi Raja
3e9cf7fdcb Update yarn.lock 2024-05-30 10:48:52 -04:00
Abi Raja
f84b9134e5 Merge branch 'select-and-edit' into hosted 2024-05-30 10:48:22 -04:00
Abi Raja
df800c7ab3 Revert "show feedback call note"
This reverts commit 14932448fb.
2024-05-29 22:21:43 -04:00
Abi Raja
14932448fb show feedback call note 2024-05-29 13:25:18 -04:00
Abi Raja
b4d8618838 disable feedback call note 2024-05-24 13:02:25 -04:00
Abi Raja
9a910c98ea enable 2024-05-24 10:37:39 -04:00
Abi Raja
a4ede44a0e show feedback call note 2024-05-24 10:35:41 -04:00
Abi Raja
6e29558a4d disable feedback call note 2024-05-23 17:40:29 -04:00
Abi Raja
4ac083eb46 update copy 2024-05-23 17:11:50 -04:00
Abi Raja
0b9e44653d fix typo 2024-05-23 17:10:13 -04:00
Abi Raja
6e3de3b1d7 show feedback call note 2024-05-23 17:04:33 -04:00
Abi Raja
8c0d820140 Update yarn.lock 2024-05-23 16:46:59 -04:00
Abi Raja
558e8634eb Merge branch 'main' into hosted 2024-05-23 16:46:22 -04:00
Abi Raja
ea7d238606 Merge branch 'main' into hosted 2024-05-14 11:48:35 -04:00
Abi Raja
9df95d9916 stop raising exceptions for "user has no credits" here (saas backend includes it) 2024-05-14 11:11:05 -04:00
Abi Raja
9a13fcc3d0 Merge branch 'main' into hosted 2024-05-13 16:06:10 -04:00
Abi Raja
711c193b32 bump posthog version 2024-04-19 16:09:46 -04:00
Abi Raja
a227704d41 unmask all inputs except for passwords for posthog 2024-04-19 16:06:34 -04:00
Abi Raja
aed7e3dacf instrument cancel and create 2024-04-18 12:59:32 -04:00
Abi Raja
07fc02a15d Merge branch 'main' into hosted 2024-04-18 12:49:35 -04:00
Abi Raja
c6b04aefbe Merge branch 'main' into hosted 2024-04-16 16:34:00 -04:00
Abi Raja
9730f6ae7f feature requests → feedback on badge 2024-04-15 16:51:04 -04:00
Abi Raja
522b7b8e23 Merge branch 'main' into hosted 2024-04-15 14:19:35 -04:00
Abi Raja
c51ff4d7ad remove more references 2024-04-10 15:38:03 -04:00
Abi Raja
60171bcc0b Merge branch 'main' into hosted 2024-04-10 14:55:07 -04:00
Abi Raja
1aec82f53d stop writing logs to disk since we're capturing them in the DB 2024-04-10 13:52:49 -04:00
Abi Raja
a197bd9223 update error message 2024-04-10 13:43:13 -04:00
Abi Raja
fa944890ad Merge branch 'main' into hosted 2024-04-10 13:39:17 -04:00
Abi Raja
575847f845 switch to subscribe banner 2024-04-05 14:40:51 -04:00
Abi Raja
273a77f7c4 add tracking for regenerate 2024-04-04 16:06:34 -04:00
Abi Raja
87d5a1da57 Merge branch 'main' into hosted 2024-04-04 15:53:27 -04:00
Abi Raja
ab8baca1ab disable better error message for videos on hosted version 2024-03-25 15:31:24 -04:00
Abi Raja
ac86e42126 add tips to avatar dropdown 2024-03-25 14:47:18 -04:00
Abi Raja
e1f39cac78 Merge branch 'main' into hosted 2024-03-25 14:41:05 -04:00
Abi Raja
7f8e0cbbd1
Merge pull request #258 from Aman-Manwani/changesv1
added resonsiveness in the Homepage
2024-03-20 12:02:37 -04:00
Abi Raja
6ea61b472d update star count 2024-03-20 12:01:18 -04:00
Abi Raja
196cf5865b set a default for inputMode param 2024-03-19 18:21:17 -04:00
Abi Raja
1d8f6641a2 do not allow non-subscribers to use Claude 2024-03-19 13:38:09 -04:00
Abi Raja
3a1634bac2 improve look of dropdown 2024-03-19 13:32:09 -04:00
Abi Raja
f171ec1e6e update pricing dialog 2024-03-19 13:29:17 -04:00
Abi Raja
ea02e2daea Update OnboardingNote.tsx 2024-03-19 13:24:12 -04:00
Abi Raja
8209c7bd26 show user that Claude 3 sonnet is for paid plans 2024-03-19 13:24:07 -04:00
Abi Raja
b6222ddbc9 hide & disable video functionality on prod 2024-03-19 13:12:41 -04:00
Abi Raja
785a135460 Merge branch 'main' into hosted 2024-03-19 12:09:31 -04:00
Abi Raja
7edbe16325 Merge branch 'main' into hosted 2024-03-19 10:31:12 -04:00
Abi Raja
f67cfb6d8a send along llm_version to database 2024-03-19 10:15:35 -04:00
Abi Raja
f6079542f7 Merge branch 'main' into hosted 2024-03-19 09:56:07 -04:00
Abi Raja
71dfde3892 Merge branch 'main' into hosted 2024-03-18 16:41:14 -04:00
Aman Manwani
b66dcc5df5 added resonsiveness 2024-03-08 10:11:31 +05:30
Abi Raja
c303c32996 update star count 2024-02-08 14:02:57 -05:00
Abi Raja
b4fb612856 add tweets to landing page 2024-02-08 14:02:16 -05:00
Abi Raja
2950c3cebe update subheadline 2024-02-08 13:17:06 -05:00
Abi Raja
9f7c0b4b35 add TODO 2024-01-30 14:33:50 -05:00
Abi Raja
ab15aff021 Support screenshot by URL for all paying customers 2024-01-30 14:32:57 -05:00
Abi Raja
e9140c331b for a subscription, regardless of the key setting, it should just work 2024-01-30 13:51:35 -05:00
Abi Raja
de834d83e5 update redirect URL 2024-01-30 12:16:03 -05:00
Abi Raja
ac858e252b add a new landing page 2024-01-29 12:36:03 -05:00
Abi Raja
b04dba001d add posthog for paid users 2024-01-23 15:08:15 -05:00
Abi Raja
234e806a6d clarify access code 2024-01-11 11:24:34 -08:00
Abi Raja
d834940c24 fix typo 2024-01-11 11:03:48 -08:00
Abi Raja
25fccc0ef5 update copy to reflect usage is monthly 2024-01-11 10:59:53 -08:00
Abi Raja
5256d428e0 Show number of credits left 2024-01-11 09:55:13 -08:00
Abi Raja
a46ff8692c revert defaulting to 'free' for subscriber tier to prevent flashes 2024-01-10 17:23:43 -08:00
Abi Raja
cddc99dc19 update to use SAAS_BACKEND_URL on the front-end everywhere 2024-01-10 15:53:41 -08:00
Abi Raja
f3ca39b40a read BACKEND_SAAS_URL env var rather than hard-coded values 2024-01-10 15:47:10 -08:00
Abi Raja
97591336c3 default subscriberTier to free 2024-01-10 08:41:04 -08:00
Abi Raja
b35738524b Merge branch 'main' into hosted 2024-01-09 10:02:29 -08:00
Abi Raja
a2c0ac1171 Merge branch 'main' into hosted 2024-01-09 09:52:32 -08:00
Abi Raja
dbbb29f0d1 Merge branch 'main' into hosted 2024-01-08 13:51:50 -08:00
Abi Raja
94ac6760b7 add comment 2024-01-08 09:22:38 -08:00
Abi Raja
77a8377e99 update "Upgrade plan" to "Manage billing" 2024-01-05 10:38:28 -08:00
Abi Raja
c70e4958b9 bump response timeout 2024-01-05 06:13:58 -08:00
Abi Raja
56c90d9c83 add direct link to Stripe Customer Portal 2024-01-04 16:57:10 -08:00
Abi Raja
71ceeb533f Update “buy credits” UX in settings modal to do subscriptions instead 2024-01-04 16:04:40 -08:00
Abi Raja
a165df2735 don't show purchase modal if user is already a subscriber 2024-01-03 16:51:39 -05:00
Abi Raja
c7482be855 fix up frontend ux for subscriptions 2024-01-03 16:48:04 -05:00
Abi Raja
2bc22b1653 update copy 2024-01-03 16:14:55 -05:00
Abi Raja
8ff2037579 set up a type for user response from backend 2024-01-03 16:09:22 -05:00
Abi Raja
1f5bec4521 add a custom dropdown 2024-01-03 15:47:45 -05:00
Abi Raja
5d7fe8b363 read current subscriber tier and don't show onboarding note 2023-12-23 17:35:45 -05:00
Abi Raja
a974c91c76 add a subscription pricing table 2023-12-23 17:06:15 -05:00
Abi Raja
231d334679 create checkout sessions when subscribe is clicked 2023-12-22 12:45:11 -05:00
Abi Raja
9bc5817aa4 Merge branch 'main' into hosted 2023-12-21 10:56:52 -05:00
Abi Raja
204d449dd4 Revert "switch to subscription model"
This reverts commit dde125b8c0.
2023-12-19 23:56:19 -05:00
Abi Raja
dde125b8c0 switch to subscription model 2023-12-19 12:10:08 -05:00
Abi Raja
c3b7ff7246 update URL to prod URL 2023-12-19 11:45:19 -05:00
Abi Raja
1a5f05d574 support subscription credits and store payment method for each generation 2023-12-19 11:40:00 -05:00
Abi Raja
730e58da72 forward generations to saas backend on the hosted version 2023-12-18 17:32:11 -05:00
Abi Raja
b579f326dd center text 2023-12-15 20:32:56 -05:00
Abi Raja
d5364fb5aa hide "an open source project by Pico" and fix clicking for pricing dialog 2023-12-15 20:30:30 -05:00
Abi Raja
dc3de0b470 add tos acceptance as part of sign up 2023-12-15 20:17:30 -05:00
Abi Raja
54017dbcd9 don't send authenticated request when the user is not authenticated 2023-12-15 12:57:36 -05:00
Abi Raja
dcb0116c06 enable login 2023-12-15 12:36:15 -05:00
Abi Raja
0f16c1d8a2 fix type errors 2023-12-14 17:06:40 -05:00
Abi Raja
bc9330fd57 disable login 2023-12-14 17:04:36 -05:00
Abi Raja
44d3776bd8 hit the backend with the current user 2023-12-14 16:49:29 -05:00
Abi Raja
732ffc33e1 Merge branch 'main' into hosted 2023-12-14 10:39:41 -05:00
Abi Raja
ff3ca97241 track history clicks 2023-12-14 06:57:41 -05:00
Abi Raja
d9cb13b1c2 Merge branch 'main' into hosted 2023-12-14 06:53:03 -05:00
Abi Raja
08cd5384be remove email submissions now that we have sign in 2023-12-12 12:18:36 -05:00
Abi Raja
d3ec75873c make user log in before they can use the app 2023-12-12 11:34:50 -05:00
Abi Raja
0d74d43eb6 fix bug 2023-12-11 19:05:59 -05:00
Abi Raja
1cdfd7d1ac Merge branch 'main' into hosted 2023-12-11 18:56:29 -05:00
Abi Raja
59e8974ebc Improve credit purchase UX 2023-12-11 16:38:48 -05:00
Abi Raja
64789d1c29 use addEvent everywhere 2023-12-10 17:58:41 -05:00
Abi Raja
adbc459347 use addEvent instead of window.plausible 2023-12-10 16:32:26 -05:00
Abi Raja
af8fb1b9bb Update poetry.lock 2023-12-10 16:23:53 -05:00
Abi Raja
9ccc47920a Merge branch 'main' into hosted 2023-12-10 16:23:41 -05:00
Abi Raja
70a67c0ea7 track HistoryTreeFailed 2023-12-08 16:49:50 -05:00
Abi Raja
7155a0616e Merge branch 'main' into hosted 2023-12-08 16:48:47 -05:00
Abi Raja
0ea6f091e8 add plausible tracking for errors 2023-12-08 16:29:15 -05:00
Abi Raja
b8a94e7efb add plausible tracking for edits 2023-12-07 16:29:21 -05:00
Abi Raja
7975e0a6f7 add plausible tracking for copy code 2023-12-07 16:28:22 -05:00
Abi Raja
5a114866f2 Merge branch 'main' into hosted 2023-12-07 13:36:28 -05:00
Abi Raja
2dbf5a3c3f move purchase link to right 2023-12-07 11:58:11 -05:00
Abi Raja
1fb390e48c Merge branch 'main' into hosted 2023-12-07 11:52:37 -05:00
Abi Raja
c5e0a536ce update openai status 2023-12-06 13:36:23 -05:00
Abi Raja
0e51e59554 update status 2023-12-06 12:04:30 -05:00
Abi Raja
d72912f11c Merge branch 'main' into hosted 2023-12-06 12:04:20 -05:00
Abi Raja
4c4a19a40a Merge branch 'main' into hosted 2023-12-06 10:50:42 -05:00
Abi Raja
67e1d2b3d3 Merge branch 'main' into hosted 2023-12-06 10:10:02 -05:00
Abi Raja
afb1b3b036 track email conversion rates with plausible 2023-12-05 11:42:59 -05:00
Abi Raja
0f7425eb3b Merge branch 'main' into hosted 2023-12-05 11:36:28 -05:00
Abi Raja
d21ccc5627 improve code quality 2023-12-04 16:50:50 -05:00
Abi Raja
737062d091 Merge branch 'main' into hosted 2023-12-04 16:46:41 -05:00
Abi Raja
7d67abb3a3 add sentry to prod 2023-12-04 08:44:28 -05:00
Abi Raja
b8b5e933bd Merge branch 'main' into hosted 2023-12-03 19:59:25 -05:00
Abi Raja
5568672416 Merge branch 'main' into hosted 2023-12-03 14:46:05 -05:00
Abi Raja
1081c100aa update price copy 2023-12-03 12:53:22 -05:00
Abi Raja
e031662b13 tweak price 2023-12-02 09:39:08 -05:00
Abi Raja
4b77361494 test another price 2023-11-30 23:29:09 -05:00
Abi Raja
0d9f93ea1f add plausible tracking for OutputSettings 2023-11-30 18:10:38 -05:00
Abi Raja
b7d808b227 Merge branch 'main' into hosted 2023-11-30 17:46:13 -05:00
Abi Raja
0979fd5f2b update onboarding note and pricing 2023-11-29 20:56:48 -05:00
Abi Raja
bb5be35928 Merge branch 'main' into hosted 2023-11-29 18:04:27 -05:00
Abi Raja
bc036f04e7 update price 2023-11-29 17:24:51 -05:00
Abi Raja
a6be4379e5 Merge branch 'main' into hosted 2023-11-29 16:43:44 -05:00
Abi Raja
0143223290 Merge branch 'main' into hosted 2023-11-29 14:45:29 -05:00
Abi Raja
9b87034846 Merge branch 'main' into hosted 2023-11-29 14:37:49 -05:00
Abi Raja
228e59a46e update checkout link 2023-11-29 14:33:51 -05:00
Abi Raja
46fdd36f11 Merge branch 'main' into hosted 2023-11-29 14:30:29 -05:00
Abi Raja
058aee85f2 track Downloads 2023-11-29 05:35:22 -05:00
Abi Raja
8d3643a2c3 Merge branch 'main' into hosted 2023-11-29 04:50:29 -05:00
Abi Raja
0222cd06a0 update z-index for code link 2023-11-28 21:29:39 -05:00
Abi Raja
b3139f9f54 Merge branch 'main' into hosted 2023-11-28 21:26:13 -05:00
Abi Raja
dfdef74b21 add back payment link 2023-11-28 20:58:03 -05:00
Abi Raja
8dd0c658d5 use correct plausible version 2023-11-28 20:45:38 -05:00
Abi Raja
439fb89645 track framework usage 2023-11-28 20:44:51 -05:00
Abi Raja
7a7be7460f hide payment link 2023-11-28 20:38:45 -05:00
Abi Raja
7eb88b2cec Merge branch 'main' into hosted 2023-11-28 20:37:52 -05:00
Abi Raja
e84ac95603 track Codepen event 2023-11-28 18:13:22 -05:00
Abi Raja
7190bb461f Merge branch 'main' into hosted 2023-11-28 17:48:11 -05:00
Abi Raja
37b4db944c
Update README.md 2023-11-28 17:31:32 -05:00
Abi Raja
2dddc479b2 add business plan Stripe payment link 2023-11-28 17:25:31 -05:00
61 changed files with 3306 additions and 967 deletions

View File

@ -16,10 +16,10 @@ repos:
# pass_filenames: false
# always_run: true
# files: ^backend/
# # - id: poetry-pyright
# # name: Run pyright with Poetry
# # entry: poetry run --directory backend pyright
# # language: system
# # pass_filenames: false
# # always_run: true
# # files: ^backend/
# - id: poetry-pyright
# name: Run pyright with Poetry
# entry: poetry run --directory backend pyright
# language: system
# pass_filenames: false
# always_run: true
# files: ^backend/

View File

@ -1,5 +1,7 @@
import re
import sentry_sdk
def extract_html_content(text: str):
# Use regex to find content within <html> tags and include the tags themselves
@ -11,4 +13,8 @@ def extract_html_content(text: str):
print(
"[HTML Extraction] No <html> tags found in the generated content: " + text
)
try:
raise Exception("No <html> tags found in the generated content")
except:
sentry_sdk.capture_exception()
return text

View File

@ -22,3 +22,12 @@ DEBUG_DIR = os.environ.get("DEBUG_DIR", "")
# Set to True when running in production (on the hosted version)
# Used as a feature flag to enable or disable certain features
IS_PROD = os.environ.get("IS_PROD", False)
# Hosted version only
PLATFORM_OPENAI_API_KEY = os.environ.get("PLATFORM_OPENAI_API_KEY", "")
PLATFORM_ANTHROPIC_API_KEY = os.environ.get("PLATFORM_ANTHROPIC_API_KEY", "")
PLATFORM_SCREENSHOTONE_API_KEY = os.environ.get("PLATFORM_SCREENSHOTONE_API_KEY", "")
BACKEND_SAAS_URL = os.environ.get("BACKEND_SAAS_URL", "")
BACKEND_SAAS_API_SECRET = os.environ.get("BACKEND_SAAS_API_SECRET", "")

View File

@ -4,4 +4,5 @@ from typing import Literal
InputMode = Literal[
"image",
"video",
"text",
]

View File

@ -3,6 +3,7 @@ import re
from typing import Dict, List, Literal, Union
from openai import AsyncOpenAI
from bs4 import BeautifulSoup
import sentry_sdk
from image_generation.replicate import call_replicate
@ -29,6 +30,10 @@ async def process_tasks(
for result in results:
if isinstance(result, BaseException):
print(f"An exception occurred: {result}")
try:
raise result
except Exception:
sentry_sdk.capture_exception()
processed_results.append(None)
else:
processed_results.append(result)

View File

@ -4,6 +4,7 @@ from typing import Any, Awaitable, Callable, List, cast
from anthropic import AsyncAnthropic
from openai import AsyncOpenAI
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk
import sentry_sdk
from config import IS_DEBUG_ENABLED
from debug.DebugFileWriter import DebugFileWriter
from image_processing.utils import process_image
@ -12,6 +13,7 @@ from utils import pprint_prompt
# Actual model versions that are passed to the LLMs and stored in our logs
# Keep in sync with s2c-saas repo & DB column `llm_version`
class Llm(Enum):
GPT_4_VISION = "gpt-4-vision-preview"
GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09"
@ -62,6 +64,25 @@ async def stream_openai_response(
full_response = ""
async for chunk in stream: # type: ignore
assert isinstance(chunk, ChatCompletionChunk)
# Log finish reason for OpenAI but don't halt streaming if it fails
try:
# Print finish reason if it exists
if (
chunk.choices
and len(chunk.choices) > 0
and chunk.choices[0].finish_reason
):
finish_reason = chunk.choices[0].finish_reason
print("[STOP REASON] OpenAI " + finish_reason)
if finish_reason == "length":
try:
raise Exception("OpenAI response too long")
except Exception as e:
sentry_sdk.capture_exception()
except Exception as e:
sentry_sdk.capture_exception(e)
if (
chunk.choices
and len(chunk.choices) > 0
@ -138,6 +159,14 @@ async def stream_claude_response(
# Return final message
response = await stream.get_final_message()
# Log stop reason
print("[STOP REASON] " + str(response.stop_reason))
if response.stop_reason == "max_tokens":
try:
raise Exception("Claude response too long")
except Exception:
sentry_sdk.capture_exception()
# Close the Anthropic client
await client.close()

View File

@ -4,10 +4,27 @@ from dotenv import load_dotenv
load_dotenv()
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routes import screenshot, generate_code, home, evals
from config import IS_PROD
# Setup Sentry (only relevant in prod)
if IS_PROD:
import sentry_sdk
SENTRY_DSN = os.environ.get("SENTRY_DSN")
if not SENTRY_DSN:
raise Exception("SENTRY_DSN not found in prod environment")
sentry_sdk.init(
dsn=SENTRY_DSN,
traces_sample_rate=0,
profiles_sample_rate=0.1,
)
# Setup FastAPI
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
# Configure CORS settings

1590
backend/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ from custom_types import InputMode
from image_generation.core import create_alt_url_mapping
from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS
from prompts.screenshot_system_prompts import SYSTEM_PROMPTS
from prompts.text_prompts import SYSTEM_PROMPTS as TEXT_SYSTEM_PROMPTS
from prompts.types import Stack
from video.utils import assemble_claude_prompt_video
@ -42,12 +43,17 @@ async def create_prompt(
prompt_messages.append(message)
else:
# Assemble the prompt for non-imported code
if input_mode == "image":
if params.get("resultImage"):
prompt_messages = assemble_prompt(
params["image"], stack, params["resultImage"]
)
else:
prompt_messages = assemble_prompt(params["image"], stack)
elif input_mode == "text":
prompt_messages = assemble_text_prompt(params["image"], stack)
else:
raise Exception("Invalid input mode")
if params["generationType"] == "update":
# Transform the history tree into message format
@ -132,3 +138,22 @@ def assemble_prompt(
"content": user_content,
},
]
def assemble_text_prompt(
text_prompt: str,
stack: Stack,
) -> list[ChatCompletionMessageParam]:
system_content = TEXT_SYSTEM_PROMPTS[stack]
return [
{
"role": "system",
"content": system_content,
},
{
"role": "user",
"content": "Generate UI for " + text_prompt,
},
]

View File

@ -0,0 +1,37 @@
import unittest
from prompts.text_prompts import HTML_TAILWIND_SYSTEM_PROMPT
class TestTextPrompts(unittest.TestCase):
def test_html_tailwind_system_prompt(self):
self.maxDiff = None
print(HTML_TAILWIND_SYSTEM_PROMPT)
expected_prompt = """
You are an expert Tailwind developer.
- Make sure to make it look modern and sleek.
- Use modern, professional fonts and colors.
- Follow UX best practices.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
In terms of libraries,
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
Reply with only the code, and no text/explanation before and after the code.
"""
self.assertEqual(HTML_TAILWIND_SYSTEM_PROMPT.strip(), expected_prompt.strip())
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,126 @@
from prompts.types import SystemPrompts
GENERAL_INSTRUCTIONS = """
- Make sure to make it look modern and sleek.
- Use modern, professional fonts and colors.
- Follow UX best practices.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later."""
LIBRARY_INSTRUCTIONS = """
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>"""
FORMAT_INSTRUCTIONS = """
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
Reply with only the code, and no text/explanation before and after the code.
"""
HTML_TAILWIND_SYSTEM_PROMPT = f"""
You are an expert Tailwind developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
HTML_CSS_SYSTEM_PROMPT = f"""
You are an expert HTML, CSS and JS developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
REACT_TAILWIND_SYSTEM_PROMPT = f"""
You are an expert React/Tailwind developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use these script to include React so that it can run on a standalone page:
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
BOOTSTRAP_SYSTEM_PROMPT = f"""
You are an expert Bootstrap, HTML and JS developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use this script to include Bootstrap: <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
IONIC_TAILWIND_SYSTEM_PROMPT = f"""
You are an expert Ionic/Tailwind developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use these script to include Ionic so that it can run on a standalone page:
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css" />
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- ionicons for icons, add the following <script > tags near the end of the page, right before the closing </body> tag:
<script type="module">
import ionicons from 'https://cdn.jsdelivr.net/npm/ionicons/+esm'
</script>
<script nomodule src="https://cdn.jsdelivr.net/npm/ionicons/dist/esm/ionicons.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/ionicons/dist/collection/components/icon/icon.min.css" rel="stylesheet">
{FORMAT_INSTRUCTIONS}
"""
VUE_TAILWIND_SYSTEM_PROMPT = f"""
You are an expert Vue/Tailwind developer.
{GENERAL_INSTRUCTIONS}
In terms of libraries,
- Use these script to include Vue so that it can run on a standalone page:
<script src="https://registry.npmmirror.com/vue/3.3.11/files/dist/vue.global.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
{LIBRARY_INSTRUCTIONS}
{FORMAT_INSTRUCTIONS}
"""
SVG_SYSTEM_PROMPT = f"""
You are an expert at building SVGs.
{GENERAL_INSTRUCTIONS}
Return only the full code in <svg></svg> tags.
Do not include markdown "```" or "```svg" at the start or end.
"""
SYSTEM_PROMPTS = SystemPrompts(
html_css=HTML_CSS_SYSTEM_PROMPT,
html_tailwind=HTML_TAILWIND_SYSTEM_PROMPT,
react_tailwind=REACT_TAILWIND_SYSTEM_PROMPT,
bootstrap=BOOTSTRAP_SYSTEM_PROMPT,
ionic_tailwind=IONIC_TAILWIND_SYSTEM_PROMPT,
vue_tailwind=VUE_TAILWIND_SYSTEM_PROMPT,
svg=SVG_SYSTEM_PROMPT,
)

View File

@ -17,6 +17,7 @@ httpx = "^0.25.1"
pre-commit = "^3.6.2"
anthropic = "^0.18.0"
moviepy = "^1.0.3"
sentry-sdk = {extras = ["fastapi"], version = "^1.38.0"}
pillow = "^10.3.0"
types-pillow = "^10.2.0.20240520"
aiohttp = "^3.9.5"

View File

@ -2,13 +2,14 @@ import asyncio
from dataclasses import dataclass
from fastapi import APIRouter, WebSocket
import openai
import sentry_sdk
from codegen.utils import extract_html_content
from config import (
ANTHROPIC_API_KEY,
IS_PROD,
NUM_VARIANTS,
OPENAI_API_KEY,
OPENAI_BASE_URL,
PLATFORM_ANTHROPIC_API_KEY,
PLATFORM_OPENAI_API_KEY,
REPLICATE_API_KEY,
SHOULD_MOCK_AI_RESPONSE,
)
@ -20,9 +21,12 @@ from llm import (
stream_claude_response_native,
stream_openai_response,
)
from fs_logging.core import write_logs
from mock_llm import mock_completion
from typing import Any, Callable, Coroutine, Dict, List, Literal, cast, get_args
from typing import Dict, cast, get_args
from image_generation.core import generate_images
from routes.logging_utils import PaymentMethod, send_to_saas_backend
from routes.saas_utils import does_user_have_subscription_credits
from typing import Any, Callable, Coroutine, Dict, Literal, cast, get_args
from image_generation.core import generate_images
from prompts import create_prompt
from prompts.claude_prompts import VIDEO_PROMPT
@ -87,6 +91,7 @@ async def perform_image_generation(
@dataclass
class ExtractedParams:
user_id: str
stack: Stack
input_mode: InputMode
code_generation_model: Llm
@ -94,6 +99,7 @@ class ExtractedParams:
openai_api_key: str | None
anthropic_api_key: str | None
openai_base_url: str | None
payment_method: PaymentMethod
async def extract_params(
@ -123,14 +129,61 @@ async def extract_params(
await throw_error(f"Invalid model: {code_generation_model_str}")
raise ValueError(f"Invalid model: {code_generation_model_str}")
openai_api_key = get_from_settings_dialog_or_env(
params, "openAiApiKey", OPENAI_API_KEY
# Read the auth token from the request (on the hosted version)
auth_token = params.get("authToken")
if not auth_token:
await throw_error("You need to be logged in to use screenshot to code")
raise Exception("No auth token")
openai_api_key = None
anthropic_api_key = None
# Track how this generation is being paid for
payment_method: PaymentMethod = PaymentMethod.UNKNOWN
# If the user is a subscriber, use the platform API key
# TODO: Rename does_user_have_subscription_credits
res = await does_user_have_subscription_credits(auth_token)
if res.status != "not_subscriber":
if (
res.status == "subscriber_has_credits"
or res.status == "subscriber_is_trialing"
):
payment_method = (
PaymentMethod.SUBSCRIPTION
if res.status == "subscriber_has_credits"
else PaymentMethod.TRIAL
)
openai_api_key = PLATFORM_OPENAI_API_KEY
anthropic_api_key = PLATFORM_ANTHROPIC_API_KEY
print("Subscription - using platform API key")
elif res.status == "subscriber_has_no_credits":
await throw_error(
"Your subscription has run out of monthly credits. Contact support and we can add more credits to your account for free."
)
else:
await throw_error("Unknown error occurred. Contact support.")
raise Exception("Unknown error occurred when checking subscription credits")
user_id = res.user_id
print("Payment method: ", payment_method)
if payment_method is PaymentMethod.UNKNOWN:
openai_api_key = get_from_settings_dialog_or_env(params, "openAiApiKey", None)
if not openai_api_key:
await throw_error(
"Please subscribe to a paid plan to generate code. If you are a subscriber and seeing this error, please contact support."
)
else:
sentry_sdk.capture_exception(Exception("OpenAI key is no longer supported"))
await throw_error(
"Using your own OpenAI key is no longer supported due to the costs of running this website. Please subscribe to a paid plan to generate code. If you are a subscriber and seeing this error, please contact support."
)
# If neither is provided, we throw an error later only if Claude is used.
anthropic_api_key = get_from_settings_dialog_or_env(
params, "anthropicApiKey", ANTHROPIC_API_KEY
)
if res.status != "not_subscriber":
raise Exception("No payment method found")
# Base URL for OpenAI API
openai_base_url: str | None = None
@ -143,9 +196,12 @@ async def extract_params(
print("Using official OpenAI URL")
# Get the image generation flag from the request. Fall back to True if not provided.
should_generate_images = bool(params.get("isImageGenerationEnabled", True))
should_generate_images = (
bool(params.get("isImageGenerationEnabled", True)) if not IS_PROD else True
)
return ExtractedParams(
user_id=user_id,
stack=validated_stack,
input_mode=validated_input_mode,
code_generation_model=code_generation_model,
@ -153,6 +209,7 @@ async def extract_params(
openai_api_key=openai_api_key,
anthropic_api_key=anthropic_api_key,
openai_base_url=openai_base_url,
payment_method=payment_method,
)
@ -206,6 +263,7 @@ async def stream_code(websocket: WebSocket):
print("Received params")
extracted_params = await extract_params(params, throw_error)
user_id = extracted_params.user_id
stack = extracted_params.stack
input_mode = extracted_params.input_mode
code_generation_model = extracted_params.code_generation_model
@ -213,6 +271,11 @@ async def stream_code(websocket: WebSocket):
openai_base_url = extracted_params.openai_base_url
anthropic_api_key = extracted_params.anthropic_api_key
should_generate_images = extracted_params.should_generate_images
payment_method = extracted_params.payment_method
# If the payment method is unknown, we shouldn't proceed
if payment_method is PaymentMethod.UNKNOWN:
return
# Auto-upgrade usage of older models
code_generation_model = auto_upgrade_model(code_generation_model)
@ -246,9 +309,13 @@ async def stream_code(websocket: WebSocket):
if SHOULD_MOCK_AI_RESPONSE:
completions = [await mock_completion(process_chunk, input_mode=input_mode)]
variant_models = [Llm.GPT_4O_2024_05_13]
else:
try:
if input_mode == "video":
if IS_PROD:
raise Exception("Video mode is not supported in prod")
if not anthropic_api_key:
await throw_error(
"Video only works with Anthropic models. No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env or in the settings dialog"
@ -265,26 +332,35 @@ async def stream_code(websocket: WebSocket):
include_thinking=True,
)
]
variant_models = [Llm.CLAUDE_3_OPUS]
else:
# Depending on the presence and absence of various keys,
# we decide which models to run
variant_models = []
if openai_api_key and anthropic_api_key:
variant_models = ["anthropic", "openai"]
variant_models = [
Llm.CLAUDE_3_5_SONNET_2024_06_20,
Llm.GPT_4O_2024_05_13,
]
elif openai_api_key:
variant_models = ["openai", "openai"]
variant_models = [
Llm.GPT_4O_2024_05_13,
Llm.GPT_4O_2024_05_13,
]
elif anthropic_api_key:
variant_models = ["anthropic", "anthropic"]
variant_models = [
Llm.CLAUDE_3_5_SONNET_2024_06_20,
Llm.CLAUDE_3_5_SONNET_2024_06_20,
]
else:
await throw_error(
"No OpenAI or Anthropic API key found. Please add the environment variable OPENAI_API_KEY or ANTHROPIC_API_KEY to backend/.env or in the settings dialog. If you add it to .env, make sure to restart the backend server."
)
raise Exception("No OpenAI or Anthropic key")
tasks: List[Coroutine[Any, Any, str]] = []
tasks: list[Coroutine[Any, Any, str]] = []
for index, model in enumerate(variant_models):
if model == "openai":
if model == Llm.GPT_4O_2024_05_13:
if openai_api_key is None:
await throw_error("OpenAI API key is missing.")
raise Exception("OpenAI API key is missing.")
@ -298,7 +374,7 @@ async def stream_code(websocket: WebSocket):
model=Llm.GPT_4O_2024_05_13,
)
)
elif model == "anthropic":
elif model == Llm.CLAUDE_3_5_SONNET_2024_06_20:
if anthropic_api_key is None:
await throw_error("Anthropic API key is missing.")
raise Exception("Anthropic API key is missing.")
@ -328,6 +404,12 @@ async def stream_code(websocket: WebSocket):
if isinstance(completion, Exception):
completions[index] = ""
print("Generation failed for variant", index)
try:
raise Exception(
"One of the generations failed"
) from completion
except:
sentry_sdk.capture_exception()
print("Models used for generation: ", variant_models)
@ -372,10 +454,27 @@ async def stream_code(websocket: WebSocket):
completions = [extract_html_content(completion) for completion in completions]
# Write the messages dict into a log so that we can debug later
write_logs(prompt_messages, completions[0])
# write_logs(prompt_messages, completion) # type: ignore
if IS_PROD:
# Catch any errors from sending to SaaS backend and continue
try:
await send_to_saas_backend(
user_id,
prompt_messages,
completions,
payment_method=payment_method,
llm_versions=variant_models,
stack=stack,
is_imported_from_code=bool(params.get("isImportedFromCode", False)),
includes_result_image=bool(params.get("resultImage", False)),
input_mode=input_mode,
)
except Exception as e:
print("Error sending to SaaS backend", e)
sentry_sdk.capture_exception(e)
## Image Generation
for index, _ in enumerate(completions):
await send_message("status", "Generating images...", index)

View File

@ -0,0 +1,58 @@
from enum import Enum
import httpx
from openai.types.chat import ChatCompletionMessageParam
from typing import List
import json
from config import BACKEND_SAAS_API_SECRET, BACKEND_SAAS_URL, IS_PROD
from custom_types import InputMode
from llm import Llm
from prompts.types import Stack
class PaymentMethod(Enum):
LEGACY = "legacy"
UNKNOWN = "unknown"
OPENAI_API_KEY = "openai_api_key"
SUBSCRIPTION = "subscription"
TRIAL = "trial"
async def send_to_saas_backend(
user_id: str,
prompt_messages: List[ChatCompletionMessageParam],
completions: list[str],
llm_versions: list[Llm],
payment_method: PaymentMethod,
stack: Stack,
is_imported_from_code: bool,
includes_result_image: bool,
input_mode: InputMode,
):
if IS_PROD:
async with httpx.AsyncClient() as client:
url = BACKEND_SAAS_URL + "/generations/store"
data = json.dumps(
{
"user_id": user_id,
"prompt": json.dumps(prompt_messages),
"completions": completions,
"payment_method": payment_method.value,
"llm_versions": [llm_version.value for llm_version in llm_versions],
"stack": stack,
"is_imported_from_code": is_imported_from_code,
"includes_result_image": includes_result_image,
"input_mode": input_mode,
}
)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {BACKEND_SAAS_API_SECRET}", # Add the auth token to the headers
}
response = await client.post(url, content=data, headers=headers, timeout=10)
response.raise_for_status()
response_data = response.json()
return response_data

View File

@ -0,0 +1,25 @@
import httpx
from pydantic import BaseModel
from config import BACKEND_SAAS_URL
class SubscriptionCreditsResponse(BaseModel):
user_id: str
status: str
async def does_user_have_subscription_credits(
auth_token: str,
):
async with httpx.AsyncClient() as client:
url = BACKEND_SAAS_URL + "/credits/has_credits"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {auth_token}",
}
response = await client.post(url, headers=headers, timeout=60)
parsed_response = SubscriptionCreditsResponse.parse_obj(response.json())
return parsed_response

View File

@ -2,6 +2,9 @@ import base64
from fastapi import APIRouter
from pydantic import BaseModel
import httpx
from config import PLATFORM_SCREENSHOTONE_API_KEY
from routes.saas_utils import does_user_have_subscription_credits
router = APIRouter()
@ -12,10 +15,31 @@ def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str:
async def capture_screenshot(
target_url: str, api_key: str, device: str = "desktop"
target_url: str, api_key: str | None, auth_token: str, device: str = "desktop"
) -> bytes:
api_base_url = "https://api.screenshotone.com/take"
# Get auth token
if not auth_token:
raise Exception("No auth token with capture_screenshot")
# TODO: Clean up this code and send the users correct error messages
# If API key is not passed in, only use the platform ScreenshotOne API key if the user is a subscriber
if not api_key:
res = await does_user_have_subscription_credits(auth_token)
if res.status == "not_subscriber":
raise Exception(
"capture_screenshot - User is not subscriber and has no API key"
)
elif res.status == "subscriber_has_credits":
api_key = PLATFORM_SCREENSHOTONE_API_KEY
elif res.status == "subscriber_has_no_credits":
raise Exception("capture_screenshot - User has no credits")
else:
raise Exception(
"capture_screenshot - Unknown error occurred when checking subscription credits"
)
params = {
"access_key": api_key,
"url": target_url,
@ -44,7 +68,8 @@ async def capture_screenshot(
class ScreenshotRequest(BaseModel):
url: str
apiKey: str
apiKey: str | None
authToken: str
class ScreenshotResponse(BaseModel):
@ -56,9 +81,10 @@ async def app_screenshot(request: ScreenshotRequest):
# Extract the URL from the request body
url = request.url
api_key = request.apiKey
auth_token = request.authToken
# TODO: Add error handling
image_bytes = await capture_screenshot(url, api_key=api_key)
image_bytes = await capture_screenshot(url, api_key=api_key, auth_token=auth_token)
# Convert the image bytes to a data url
data_url = bytes_to_data_url(image_bytes, "image/png")

View File

@ -16,6 +16,21 @@
<!-- Injected code for hosted version -->
<%- injectHead %>
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=AW-16649848443"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "AW-16649848443");
</script>
<title>Screenshot to Code</title>
<!-- Open Graph Meta Tags -->

View File

@ -13,12 +13,16 @@
"test": "jest"
},
"dependencies": {
"@clerk/clerk-react": "5.4.2",
"@codemirror/lang-html": "^6.4.6",
"@intercom/messenger-js-sdk": "^0.0.11",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
@ -30,12 +34,15 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@stripe/stripe-js": "^2.2.2",
"@types/gtag.js": "^0.0.20",
"class-variance-authority": "^0.7.0",
"classnames": "^2.3.2",
"clsx": "^2.0.0",
"codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3",
"html2canvas": "^1.4.1",
"posthog-js": "^1.128.1",
"nanoid": "^5.0.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -43,6 +50,8 @@
"react-hot-toast": "^2.4.1",
"react-icons": "^4.12.0",
"react-router-dom": "^6.20.1",
"react-tweet": "^3.2.0",
"react-youtube": "^10.1.0",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"thememirror": "^2.0.1",

Binary file not shown.

View File

@ -1,31 +1,46 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { generateCode } from "./generateCode";
import { IS_FREE_TRIAL_ENABLED, IS_RUNNING_ON_CLOUD } from "./config";
import SettingsDialog from "./components/settings/SettingsDialog";
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
import { IS_RUNNING_ON_CLOUD } from "./config";
import { PicoBadge } from "./components/messages/PicoBadge";
import { OnboardingNote } from "./components/messages/OnboardingNote";
import { usePersistedState } from "./hooks/usePersistedState";
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
import { addEvent } from "./lib/analytics";
import { extractHistory } from "./components/history/utils";
import toast from "react-hot-toast";
import { useAuth } from "@clerk/clerk-react";
import { useStore } from "./store/store";
import { Stack } from "./lib/stacks";
import { CodeGenerationModel } from "./lib/models";
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator";
import TipLink from "./components/messages/TipLink";
import { useAppStore } from "./store/app-store";
import GenerateFromText from "./components/generate-from-text/GenerateFromText";
import { useProjectStore } from "./store/project-store";
import Sidebar from "./components/sidebar/Sidebar";
import PreviewPane from "./components/preview/PreviewPane";
import DeprecationMessage from "./components/messages/DeprecationMessage";
import { GenerationSettings } from "./components/settings/GenerationSettings";
import StartPane from "./components/start-pane/StartPane";
import { takeScreenshot } from "./lib/takeScreenshot";
import Sidebar from "./components/sidebar/Sidebar";
import { Commit } from "./components/commits/types";
import { createCommit } from "./components/commits/utils";
function App() {
interface Props {
navbarComponent?: JSX.Element;
}
function App({ navbarComponent }: Props) {
const [initialPrompt, setInitialPrompt] = useState<string>("");
// Relevant for hosted version only
// TODO: Move to AppContainer
const { getToken } = useAuth();
const subscriberTier = useStore((state) => state.subscriberTier);
const {
// Inputs
inputMode,
@ -140,12 +155,21 @@ function App() {
return;
}
addEvent("Regenerate");
// Re-run the create
if (inputMode === "image" || inputMode === "video") {
doCreate(referenceImages, inputMode);
} else {
// TODO: Fix this
doCreateFromText(initialPrompt);
}
};
// Used when the user cancels the code generation
const cancelCodeGeneration = () => {
addEvent("Cancel");
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE);
};
@ -170,7 +194,7 @@ function App() {
}
};
function doGenerateCode(params: CodeGenerationParams) {
async function doGenerateCode(params: CodeGenerationParams) {
// Reset the execution console
resetExecutionConsoles();
@ -178,7 +202,12 @@ function App() {
setAppState(AppState.CODING);
// Merge settings with params
const updatedParams = { ...params, ...settings };
const authToken = await getToken();
const updatedParams = {
...params,
...settings,
authToken: authToken || undefined,
};
const baseCommitObject = {
variants: [{ code: "" }, { code: "" }],
@ -227,13 +256,17 @@ function App() {
},
// On complete
() => {
addEvent("CreateSuccessful");
setAppState(AppState.CODE_READY);
}
);
}
// Initial version creation
function doCreate(referenceImages: string[], inputMode: "image" | "video") {
async function doCreate(
referenceImages: string[],
inputMode: "image" | "video"
) {
// Reset any existing state
reset();
@ -243,6 +276,7 @@ function App() {
// Kick off the code generation
if (referenceImages.length > 0) {
addEvent("Create");
doGenerateCode({
generationType: "create",
image: referenceImages[0],
@ -251,6 +285,19 @@ function App() {
}
}
function doCreateFromText(text: string) {
// Reset any existing state
reset();
setInputMode("text");
setInitialPrompt(text);
doGenerateCode({
generationType: "create",
inputMode: "text",
image: text,
});
}
// Subsequent updates
async function doUpdate(
updateInstruction: string,
@ -272,6 +319,7 @@ function App() {
try {
historyTree = extractHistory(head, commits);
} catch {
addEvent("HistoryTreeFailed");
toast.error(
"Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
);
@ -293,10 +341,11 @@ function App() {
? await takeScreenshot()
: undefined;
addEvent("Edit");
doGenerateCode({
generationType: "update",
inputMode,
image: referenceImages[0],
image: inputMode === "text" ? initialPrompt : referenceImages[0],
resultImage,
history: updatedHistory,
isImportedFromCode,
@ -345,7 +394,7 @@ function App() {
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
{IS_RUNNING_ON_CLOUD && (
<TermsOfServiceDialog
open={!settings.isTermOfServiceAccepted}
open={false}
onOpenChange={handleTermDialogOpenChange}
/>
)}
@ -366,7 +415,14 @@ function App() {
{/* Show tip link until coding is complete */}
{appState !== AppState.CODE_READY && <TipLink />}
{IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />}
{IS_RUNNING_ON_CLOUD &&
!settings.openAiApiKey &&
!IS_FREE_TRIAL_ENABLED &&
subscriberTier === "free" && <OnboardingNote />}
{appState === AppState.INITIAL && (
<GenerateFromText doCreateFromText={doCreateFromText} />
)}
{/* Rest of the sidebar when we're not in the initial state */}
{(appState === AppState.CODING ||
@ -382,6 +438,8 @@ function App() {
</div>
<main className="py-2 lg:pl-96">
{!!navbarComponent && navbarComponent}
{appState === AppState.INITIAL && (
<StartPane
doCreate={doCreate}

View File

@ -7,6 +7,8 @@ import { URLS } from "../urls";
import { Badge } from "./ui/badge";
import ScreenRecorder from "./recording/ScreenRecorder";
import { ScreenRecorderState } from "../types";
import { IS_RUNNING_ON_CLOUD } from "../config";
import { addEvent } from "../lib/analytics";
const baseStyle = {
flex: 1,
@ -82,6 +84,17 @@ function ImageUpload({ setReferenceImages }: Props) {
"video/webm": [".webm"],
},
onDrop: (acceptedFiles) => {
if (IS_RUNNING_ON_CLOUD) {
const isVideo = acceptedFiles.some((file) =>
file.type.startsWith("video/")
);
if (isVideo) {
toast.error("Videos are not yet supported on the hosted version.");
addEvent("VideoUpload");
return;
}
}
// Set up the preview thumbnail images
setFiles(
acceptedFiles.map((file: File) =>
@ -173,10 +186,13 @@ function ImageUpload({ setReferenceImages }: Props) {
</p>
</div>
)}
{/* Disable on prod for now */}
{!IS_RUNNING_ON_CLOUD && (
<>
{screenRecorderState === ScreenRecorderState.INITIAL && (
<div className="text-center text-sm text-slate-800 mt-4">
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or record
your screen to clone a whole app (experimental).{" "}
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or
record your screen to clone a whole app (experimental).{" "}
<a
className="underline"
href={URLS["intro-to-video"]}
@ -191,6 +207,8 @@ function ImageUpload({ setReferenceImages }: Props) {
setScreenRecorderState={setScreenRecorderState}
generateCode={setReferenceImages}
/>
</>
)}
</section>
);
}

View File

@ -7,9 +7,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
import { Input } from "./ui/input";
import toast from "react-hot-toast";
import { PICO_BACKEND_FORM_SECRET } from "../config";
import { addEvent } from "../lib/analytics";
const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
@ -17,40 +15,19 @@ const TermsOfServiceDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => {
const [email, setEmail] = React.useState("");
const onSubscribe = async () => {
await fetch("https://backend.buildpicoapps.com/form", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, secret: PICO_BACKEND_FORM_SECRET }),
});
};
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="mb-2 text-xl">
Enter your email to get started
One last step
</AlertDialogTitle>
</AlertDialogHeader>
<div className="mb-2">
<Input
placeholder="Email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
}}
/>
</div>
<div className="flex flex-col space-y-3 text-sm">
<p>
By providing your email, you consent to receiving occasional product
updates, and you accept the{" "}
You consent to receiving occasional product updates via email, and
you accept the{" "}
<a
href="https://a.picoapps.xyz/camera-write"
target="_blank"
@ -76,13 +53,8 @@ const TermsOfServiceDialog: React.FC<{
<AlertDialogFooter>
<AlertDialogAction
onClick={(e) => {
if (!email.trim() || !email.trim().includes("@")) {
e.preventDefault();
toast.error("Please enter your email");
} else {
onSubscribe();
}
onClick={() => {
addEvent("EmailSubmit");
}}
>
Agree & Continue

View File

@ -3,6 +3,8 @@ import { HTTP_BACKEND_URL } from "../config";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { toast } from "react-hot-toast";
import { useStore } from "../store/store";
import { useAuth } from "@clerk/clerk-react";
interface Props {
screenshotOneApiKey: string | null;
@ -13,28 +15,31 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [referenceUrl, setReferenceUrl] = useState("");
// Hosted version only
const subscriberTier = useStore((state) => state.subscriberTier);
const { getToken } = useAuth();
async function takeScreenshot() {
if (!screenshotOneApiKey) {
toast.error(
"Please add a ScreenshotOne API key in the Settings dialog. This is optional - you can also drag/drop and upload images directly.",
{ duration: 8000 }
);
return;
if (!referenceUrl) {
return toast.error("Please enter a URL");
}
if (!referenceUrl) {
toast.error("Please enter a URL");
return;
if (!screenshotOneApiKey && subscriberTier === "free") {
return toast.error(
"Please upgrade to a paid plan to use the screenshot feature."
);
}
if (referenceUrl) {
try {
setIsLoading(true);
const authToken = await getToken();
const response = await fetch(`${HTTP_BACKEND_URL}/api/screenshot`, {
method: "POST",
body: JSON.stringify({
url: referenceUrl,
apiKey: screenshotOneApiKey,
authToken,
}),
headers: {
"Content-Type": "application/json",

View File

@ -0,0 +1,11 @@
import Spinner from "../core/Spinner";
function FullPageSpinner() {
return (
<div className="w-full h-screen flex items-center justify-center">
<Spinner />
</div>
);
}
export default FullPageSpinner;

View File

@ -0,0 +1,57 @@
import { useState, useRef, useEffect } from "react";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import toast from "react-hot-toast";
interface GenerateFromTextProps {
doCreateFromText: (text: string) => void;
}
function GenerateFromText({ doCreateFromText }: GenerateFromTextProps) {
const [isOpen, setIsOpen] = useState(false);
const [text, setText] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (isOpen && textareaRef.current) {
textareaRef.current.focus();
}
}, [isOpen]);
const handleGenerate = () => {
if (text.trim() === "") {
// Assuming there's a toast function available in the context
toast.error("Please enter a prompt to generate from");
return;
}
doCreateFromText(text);
};
return (
<div className="mt-4">
{!isOpen ? (
<div className="flex justify-center">
<Button variant="secondary" onClick={() => setIsOpen(true)}>
Generate from text prompt [BETA]
</Button>
</div>
) : (
<>
<Textarea
ref={textareaRef}
rows={2}
placeholder="A Saas admin dashboard"
className="w-full mb-4"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<div className="flex justify-end">
<Button onClick={handleGenerate}>Generate</Button>
</div>
</>
)}
</div>
);
}
export default GenerateFromText;

View File

@ -46,7 +46,7 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
)}
>
<div
className="flex justify-between truncate flex-1 p-2"
className="flex justify-between truncate flex-1 p-2 plausible-event-name=HistoryClick"
onClick={() =>
shouldDisableReverts
? toast.error(

View File

@ -0,0 +1,101 @@
import { useUser } from "@clerk/clerk-react";
import posthog from "posthog-js";
import App from "../../App";
import { useEffect, useRef } from "react";
import FullPageSpinner from "../core/FullPageSpinner";
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { useStore } from "../../store/store";
import AvatarDropdown from "./AvatarDropdown";
import { UserResponse } from "./types";
import { POSTHOG_HOST, POSTHOG_KEY, SAAS_BACKEND_URL } from "../../config";
import LandingPage from "./LandingPage";
import Intercom from "@intercom/messenger-js-sdk";
function AppContainer() {
const { isSignedIn, isLoaded } = useUser();
const setSubscriberTier = useStore((state) => state.setSubscriberTier);
// For fetching user
const authenticatedFetch = useAuthenticatedFetch();
const isInitRequestInProgress = useRef(false);
// Get information from our backend about the user (subscription status)
useEffect(() => {
const init = async () => {
// Make sure there's only one request in progress
// so that we don't create multiple users
if (isInitRequestInProgress.current) return;
isInitRequestInProgress.current = true;
const user: UserResponse = await authenticatedFetch(
SAAS_BACKEND_URL + "/users/create",
"POST"
);
// If the user is not signed in, authenticatedFetch will return undefined
if (!user) {
isInitRequestInProgress.current = false;
return;
}
if (!user.subscriber_tier) {
setSubscriberTier("free");
} else {
// Initialize PostHog only for paid users
// and unmask all inputs except for passwords
posthog.init(POSTHOG_KEY, {
api_host: POSTHOG_HOST,
session_recording: {
maskAllInputs: false,
maskInputOptions: {
password: true,
},
},
});
// Identify the user to PostHog
posthog.identify(user.email, {
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
});
setSubscriberTier(user.subscriber_tier);
}
// Initialize Intercom
Intercom({
app_id: "c5eiaj9m",
user_id: user.email,
name: user.first_name,
email: user.email,
"Subscriber Tier": user.subscriber_tier || "free",
});
isInitRequestInProgress.current = false;
};
init();
}, []);
// If Clerk is still loading, show a spinner
if (!isLoaded) return <FullPageSpinner />;
// If the user is not signed in, show the landing page
if (isLoaded && !isSignedIn) return <LandingPage />;
// If the user is signed in, show the app
return (
<>
<App
navbarComponent={
<div className="flex justify-end items-center gap-x-2 px-10 mt-0 mb-4">
<AvatarDropdown />
</div>
}
/>
</>
);
}
export default AppContainer;

View File

@ -0,0 +1,154 @@
import { useClerk, useUser } from "@clerk/clerk-react";
import { Avatar, AvatarImage, AvatarFallback } from "../ui/avatar";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuLabel,
} from "../ui/dropdown-menu";
import { useStore } from "../../store/store";
import { capitalize } from "./utils";
import StripeCustomerPortalLink from "./StripeCustomerPortalLink";
import { Progress } from "../ui/progress";
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { SAAS_BACKEND_URL } from "../../config";
import { CreditsUsage } from "./types";
import { useState } from "react";
import toast from "react-hot-toast";
import { showNewMessage } from "@intercom/messenger-js-sdk";
import { URLS } from "../../urls";
export default function AvatarDropdown() {
const [isOpen, setIsOpen] = useState(false);
const [isLoadingUsage, setIsLoadingUsage] = useState(false);
const [usedCredits, setUsedCredits] = useState(0);
const [totalCredits, setTotalCredits] = useState(0);
const subscriberTier = useStore((state) => state.subscriberTier);
const setPricingDialogOpen = useStore((state) => state.setPricingDialogOpen);
const isFreeUser = subscriberTier === "free" || !subscriberTier;
const { user, isLoaded, isSignedIn } = useUser();
const { signOut } = useClerk();
const authenticatedFetch = useAuthenticatedFetch();
const openPricingDialog = () => setPricingDialogOpen(true);
async function open(isOpen: boolean) {
setIsOpen(isOpen);
// Do not fetch usage if the user is a free user
// or that information hasn't loaded yet
// or the dropdown is closed
if (isFreeUser || !subscriberTier || !isOpen) return;
setIsLoadingUsage(true);
try {
const res: CreditsUsage = await authenticatedFetch(
SAAS_BACKEND_URL + "/credits/usage",
"POST"
);
setUsedCredits(res.used_monthly_credits);
setTotalCredits(res.total_monthly_credits);
} catch (e) {
toast.error(
"Failed to fetch credit usage. Please contact support to get this issue fixed."
);
} finally {
setIsLoadingUsage(false);
}
}
// If Clerk is still loading or user is logged out, don't show anything
if (!isLoaded || !isSignedIn) return null;
return (
<>
<DropdownMenu open={isOpen} onOpenChange={open}>
<DropdownMenuTrigger asChild>
<div className="flex items-center space-x-2 cursor-pointer">
<span className="text-sm">Your account</span>
<Avatar className="w-8 h-8">
<AvatarImage src={user?.imageUrl} alt="Profile image" />
<AvatarFallback>{user?.firstName}</AvatarFallback>
</Avatar>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end">
{/* Free users */}
{isFreeUser && (
<DropdownMenuItem asChild={true}>
<a onClick={openPricingDialog}>Get pro</a>
</DropdownMenuItem>
)}
{/* Paying user */}
{!isFreeUser && (
<>
<DropdownMenuLabel onClick={openPricingDialog}>
{capitalize(subscriberTier) + " Subscriber"}
</DropdownMenuLabel>
{/* Loading credit usage */}
{isLoadingUsage && (
<DropdownMenuItem className="text-xs text-gray-700">
Loading credit usage...
</DropdownMenuItem>
)}
{/* Credits usage */}
{!isLoadingUsage && (
<>
<DropdownMenuItem onClick={openPricingDialog}>
<Progress value={(usedCredits / totalCredits) * 100} />
</DropdownMenuItem>
<DropdownMenuItem
className="text-xs text-gray-700"
onClick={openPricingDialog}
>
{usedCredits} out of {totalCredits} credits used for{" "}
{new Date().toLocaleString("default", { month: "long" })}.
{subscriberTier !== "pro" && (
<> Upgrade to Pro to get more credits.</>
)}
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem asChild={true}>
<a href={URLS.tips} target="_blank">
Tips for better results
</a>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild={true}>
<a onClick={() => showNewMessage("")}>Contact support</a>
</DropdownMenuItem>
<DropdownMenuItem asChild={true}>
<a
href="https://screenshot-to-code.canny.io/feature-requests"
target="_blank"
>
Feature requests
</a>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild={true}>
<StripeCustomerPortalLink label="Manage billing" />
</DropdownMenuItem>
<DropdownMenuItem asChild={true}>
<StripeCustomerPortalLink label="Cancel subscription" />
</DropdownMenuItem>
</>
)}
{/* All users */}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut()}>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
}

View File

@ -0,0 +1,20 @@
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const CheckoutSuccessPage: React.FC = () => {
const navigate = useNavigate();
useEffect(() => {
// Redirect to home page after a short delay
const redirectTimer = setTimeout(() => {
navigate("/");
}, 200);
// Clean up the timer if the component unmounts
return () => clearTimeout(redirectTimer);
}, [navigate]);
return <div></div>;
};
export default CheckoutSuccessPage;

View File

@ -0,0 +1,47 @@
function FAQs() {
const faqs = [
{
question: "How do credits work?",
answer:
"Each creation, whether from a screenshot or text, consumes 1 credit. Every additional edit also consumes 1 credit. If you run out of credits, you can easily upgrade your plan to obtain more.",
},
{
question: "When do credits reset?",
answer:
"Your credits reset at the beginning of each month and do not roll over. Every month, on the 1st, you will receive a fresh batch of credits.",
},
{
question: "Can I cancel my plan?",
answer:
"Yes, you can cancel your plan at any time. Your plan will remain active until the end of the billing cycle.",
},
{
question: "Can I upgrade or downgrade my plan?",
answer:
"Yes, you can change your plan at any time. The changes will take effect immediately.",
},
{
question: "What payment methods do you accept?",
answer:
"We accept all major credit cards, Alipay, Amazon Pay and Cash App Pay. Certain payment methods may not be available in your country.",
},
];
return (
<div className="max-w-5xl mx-auto mb-16">
<h2 className="text-3xl font-bold mb-8 text-center">
Frequently Asked Questions
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{faqs.map((faq, index) => (
<div key={index} className="border-b border-gray-200 pb-6">
<h3 className="text-lg font-semibold mb-2">{faq.question}</h3>
<p className="text-gray-600">{faq.answer}</p>
</div>
))}
</div>
</div>
);
}
export default FAQs;

View File

@ -0,0 +1,14 @@
import React from "react";
import Footer from "./LandingPage/Footer";
import FAQs from "./FAQs";
const FaqsPage: React.FC = () => {
return (
<div className="container mx-auto px-4 py-8">
<FAQs />
<Footer />
</div>
);
};
export default FaqsPage;

View File

@ -0,0 +1,174 @@
import { FaGithub } from "react-icons/fa";
import Footer from "./LandingPage/Footer";
import { Button } from "../ui/button";
import { SignUp } from "@clerk/clerk-react";
import { useState } from "react";
import { Dialog, DialogContent } from "../ui/dialog";
import { Tweet } from "react-tweet";
// import YouTube, { YouTubeProps } from "react-youtube";
const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
function LandingPage() {
const [isAuthPopupOpen, setIsAuthPopupOpen] = useState(false);
const signIn = () => {
setIsAuthPopupOpen(true);
};
// const youtubeOpts: YouTubeProps["opts"] = {
// height: "262.5", // Increased by 50%
// width: "480", // Increased by 50%
// playerVars: {
// autoplay: 1,
// },
// };
return (
<div className="w-full xl:w-[1000px] mx-auto mt-4">
{/* Auth dialog */}
<Dialog
open={isAuthPopupOpen}
onOpenChange={(value) => setIsAuthPopupOpen(value)}
>
<DialogContent className="flex justify-center">
<SignUp
fallbackRedirectUrl="/"
appearance={{
elements: {
// formButtonPrimary: "bg-slate-500 hover:bg-slate-400 text-sm",
cardBox: {
boxShadow: "none",
borderRadius: "0",
border: "none",
backgroundColor: "transparent",
},
card: {
borderRadius: "0",
border: "none",
backgroundColor: "transparent",
},
footer: {
display: "flex",
flexDirection: "column",
textAlign: "center",
background: "transparent",
},
footerAction: {
marginBottom: "5px",
},
},
layout: { privacyPageUrl: "https://a.picoapps.xyz/camera-write" },
}}
/>
</DialogContent>
</Dialog>
{/* Navbar */}
<nav className="border-b border-gray-200 px-4 py-2">
<div className="flex justify-between items-center">
<div className="text-lg font-semibold">Screenshot to Code</div>
<div className="flex items-center space-x-4">
<Button variant="secondary" onClick={signIn}>
Sign in
</Button>
<Button onClick={signIn}>Get started</Button>
</div>
</div>
</nav>
{/* Hero */}
<header className="px-4 py-16">
<div className="mx-auto">
<h2 className="text-5xl font-bold leading-tight mb-6">
Build User Interfaces 10x Faster
</h2>
<p className="text-gray-600 text-xl mb-6">
Convert any screenshot or design to clean code (with support for
most frameworks)
</p>
<div className="flex gap-4 flex-col sm:flex-row">
<Button size="lg" className="text-lg py-6 px-8" onClick={signIn}>
Get started
</Button>
<Button
variant="secondary"
onClick={() =>
window.open(
"https://github.com/abi/screenshot-to-code",
"_blank"
)
}
className="flex items-center space-x-2 text-gray-600 hover:text-gray-900 py-6 px-8"
>
<FaGithub size={24} />
<span>GitHub</span>
<span className="text-sm bg-gray-200 rounded-full px-2 py-1">
53,939 stars
</span>
</Button>
</div>
</div>
</header>
{/* Logo wall */}
<div className="mx-auto mt-12 px-4 sm:px-0">
<p className="text-gray-600 text-xl mb-10 text-center">
#1 tool used by developers and designers from leading companies. Fully
open source with 53,000+ stars on GitHub.
</p>
<div
className="mx-auto grid max-w-lg items-center gap-x-2
gap-y-10 sm:max-w-xl grid-cols-3 lg:mx-0 lg:max-w-none mt-10"
>
{LOGOS.map((companyName) => (
<img
key={companyName}
className="col-span-1 max-h-8 w-full object-contain
grayscale opacity-50 hover:opacity-100"
src={`https://picoapps.xyz/logos/${companyName}.png`}
alt={companyName}
width={120}
height={48}
/>
))}
</div>
</div>
{/* Video section */}
{/* <div className="px-4 mt-20 mb-10 text-center">
<video
src="/demos/youtube.mp4"
className="max-w-lg mx-auto rounded-md w-full sm:w-auto"
autoPlay
loop
muted
/>
<div className="mt-6">
Watch Screenshot to Code convert a screenshot of YouTube to
HTML/Tailwind
</div>
</div> */}
{/* Here's what users have to say */}
<div className="mt-16">
<h2 className="text-gray-600 text-2xl mb-4 text-center">
Here's what users have to say
</h2>
<div className="px-3 grid grid-cols-1 sm:grid-cols-2 gap-2 items-start justify-items-center">
{/* <YouTube videoId="b2xi5qiiTOI" opts={youtubeOpts} /> */}
<Tweet id="1733865178905661940" />
{/* <Tweet id="1727586760584991054" /> Other Rowan Cheung tweet */}
<Tweet id="1727105236811366669" />
<Tweet id="1732032876739224028" />
<Tweet id="1728496255473459339" />
</div>
</div>
{/* Footer */}
<Footer />
</div>
);
}
export default LandingPage;

View File

@ -0,0 +1,35 @@
const Footer = () => {
return (
<footer className="flex justify-between border-t border-gray-200 pt-4 mb-6 px-4 sm:px-0">
<div className="flex flex-col">
<span className="text-xl mb-2">Screenshot to Code</span>
<span className="text-xs">
© {new Date().getFullYear()} WhimsyWorks, Inc. All rights reserved.
</span>
{/* <div
className="bg-gray-800 text-white text-sm px-2 py-2
rounded-full flex items-center space-x-2"
>
<span>Built with</span>
<i className="fas fa-bolt text-yellow-400"></i>
<span>Screenshot to Code</span>
</div> */}
</div>
<div className="flex flex-col text-sm text-gray-600 mr-4">
<span className="uppercase">Company</span>
<div>WhimsyWorks Inc.</div>
<div>Made in NYC 🗽</div>
<a href="https://github.com/abi/screenshot-to-code" target="_blank">
Github
</a>
<a href="mailto:support@picoapps.xyz" target="_blank">
Contact
</a>
<a href="https://a.picoapps.xyz/camera-write" target="_blank">
Terms of Service
</a>
</div>
</footer>
);
};
export default Footer;

View File

@ -0,0 +1,19 @@
import React from "react";
import Footer from "./LandingPage/Footer";
import PricingPlans from "./payments/PricingPlans";
import FAQs from "./FAQs";
const PricingPage: React.FC = () => {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Screenshot to Code Pricing</h1>
<PricingPlans shouldShowFAQLink={false} />
{/* Spacer */}
<div className="text-center mt-8"></div>
<FAQs />
<Footer />
</div>
);
};
export default PricingPage;

View File

@ -0,0 +1,48 @@
import toast from "react-hot-toast";
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { addEvent } from "../../lib/analytics";
import { SAAS_BACKEND_URL } from "../../config";
import { PortalSessionResponse } from "./types";
import { forwardRef, useState } from "react";
import Spinner from "../core/Spinner";
interface Props {
label: string;
}
const StripeCustomerPortalLink = forwardRef<HTMLAnchorElement, Props>(
({ label, ...props }, ref) => {
const [isLoading, setIsLoading] = useState(false);
const authenticatedFetch = useAuthenticatedFetch();
const redirectToBillingPortal = async () => {
try {
setIsLoading(true);
const res: PortalSessionResponse = await authenticatedFetch(
SAAS_BACKEND_URL + "/payments/create_portal_session",
"POST"
);
window.location.href = res.url;
} catch (e) {
toast.error(
"Error directing you to the billing portal. Please email support and we'll get it fixed right away."
);
addEvent("StripeBillingPortalError");
} finally {
setIsLoading(false);
}
};
return (
<a {...props} ref={ref} onClick={redirectToBillingPortal}>
<div className="flex gap-x-2">
{label} {isLoading && <Spinner />}
</div>
</a>
);
}
);
StripeCustomerPortalLink.displayName = "StripeCustomerPortalLink";
export default StripeCustomerPortalLink;

View File

@ -0,0 +1,70 @@
import React from "react";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../ui/dialog";
import { useStore } from "../../../store/store";
import PricingPlans from "./PricingPlans";
const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
const PricingDialog: React.FC = () => {
const subscriberTier = useStore((state) => state.subscriberTier);
const [showDialog, setShowDialog] = useStore((state) => [
state.isPricingDialogOpen,
state.setPricingDialogOpen,
]);
return (
<Dialog open={showDialog} onOpenChange={(isOpen) => setShowDialog(isOpen)}>
{subscriberTier === "free" && (
<DialogTrigger
className="fixed z-50 bottom-28 right-5 rounded-md shadow-lg bg-black
text-white px-4 text-xs py-3 cursor-pointer"
>
get 100 code generations for $15
</DialogTrigger>
)}
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle className="text-3xl text-center">
Ship Code Faster
</DialogTitle>
</DialogHeader>
<PricingPlans />
<DialogFooter></DialogFooter>
{/* Logos */}
<div className="max-w-lg mx-auto">
<div
className="mx-auto grid max-w-lg items-center gap-x-2
gap-y-10 sm:max-w-xl grid-cols-6 lg:mx-0 lg:max-w-none mt-4"
>
{LOGOS.map((companyName) => (
<img
key={companyName}
className="col-span-1 max-h-12 w-full object-contain grayscale opacity-50 hover:opacity-100"
src={`https://picoapps.xyz/logos/${companyName}.png`}
alt={companyName}
width={120}
height={48}
/>
))}
</div>
<div className="text-gray-600 leading-tight text-sm mt-4 text-center">
Designers and engineers from these organizations use Screenshot to
Code to build interfaces faster.
</div>
</div>
</DialogContent>
</Dialog>
);
};
export default PricingDialog;

View File

@ -0,0 +1,145 @@
import { FaCheckCircle } from "react-icons/fa";
import Spinner from "../../core/Spinner";
import React from "react";
import { Button } from "../../ui/button";
import useStripeCheckout from "./useStripeCheckout";
interface PricingPlansProps {
shouldShowFAQLink?: boolean;
}
function PricingPlans({ shouldShowFAQLink = true }: PricingPlansProps) {
const { checkout, isLoadingCheckout } = useStripeCheckout();
const [paymentInterval, setPaymentInterval] = React.useState<
"monthly" | "yearly"
>("monthly");
return (
<>
<div className="flex justify-center gap-x-2 mt-2">
<Button
variant={paymentInterval === "monthly" ? "default" : "secondary"}
onClick={() => setPaymentInterval("monthly")}
>
Monthly
</Button>
<Button
variant={paymentInterval === "yearly" ? "default" : "secondary"}
onClick={() => setPaymentInterval("yearly")}
>
Yearly (2 months free)
</Button>
</div>
<div className="flex justify-center items-center">
<div className="grid grid-cols-2 gap-8 p-2">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="font-semibold">Hobby</h2>
<p className="text-gray-500">Great to start</p>
<div className="my-4">
<span className="text-4xl font-bold">
{paymentInterval === "monthly" ? "$15" : "$150"}
</span>
<span className="text-gray-500">
{paymentInterval === "monthly" ? "/ month" : "/ year"}
</span>
</div>
<button
className="bg-black text-white rounded py-2 px-4 w-full text-sm
flex justify-center items-center gap-x-2"
onClick={() =>
checkout(
paymentInterval === "monthly"
? "hobby_monthly"
: "hobby_yearly"
)
}
>
Subscribe {isLoadingCheckout && <Spinner />}
</button>
<ul className="mt-4 space-y-2">
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
100 credits / mo
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
All supported AI models
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
Full code access
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
Chat support
</li>
</ul>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="font-semibold">Pro</h2>
<p className="text-gray-500">Higher limits</p>
<div className="my-4">
<span className="text-4xl font-bold">
{paymentInterval === "monthly" ? "$40" : "$400"}
</span>
<span className="text-gray-500">
{paymentInterval === "monthly" ? "/ month" : "/ year"}
</span>
</div>
<button
className="bg-black text-white rounded py-2 px-4 w-full text-sm
flex justify-center items-center gap-x-2"
onClick={() =>
checkout(
paymentInterval === "monthly" ? "pro_monthly" : "pro_yearly"
)
}
>
Subscribe {isLoadingCheckout && <Spinner />}
</button>
<ul className="mt-4 space-y-2">
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
300 credits / mo
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
All supported AI models
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
Full code access
</li>
<li className="flex items-center">
<FaCheckCircle className="text-black mr-2" />
Chat support
</li>
</ul>
</div>
</div>
</div>
<p className="text-center text-xs text-gray-600 mt-1">
1 credit = 1 code generation. Cancel subscription at any time. <br />{" "}
{shouldShowFAQLink && (
<>
<a
href="/pricing"
target="_blank"
className="text-blue-900 underline"
>
For more information, visit our FAQs
</a>{" "}
or contact support.
</>
)}
</p>
</>
);
}
export default PricingPlans;

View File

@ -0,0 +1,71 @@
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { SAAS_BACKEND_URL, STRIPE_PUBLISHABLE_KEY } from "../../../config";
import { addEvent } from "../../../lib/analytics";
import { Stripe, loadStripe } from "@stripe/stripe-js";
import { useAuthenticatedFetch } from "../useAuthenticatedFetch";
interface CreateCheckoutSessionResponse {
sessionId: string;
}
export default function useStripeCheckout() {
const authenticatedFetch = useAuthenticatedFetch();
const [stripe, setStripe] = useState<Stripe | null>(null);
const [isLoadingCheckout, setIsLoadingCheckout] = useState(false);
const checkout = async (priceLookupKey: string) => {
const rewardfulReferralId = "xxx"; // TODO: Use later with Rewardful
if (!stripe) {
addEvent("StripeNotLoaded");
return;
}
try {
setIsLoadingCheckout(true);
// Create a Checkout Session
const res: CreateCheckoutSessionResponse = await authenticatedFetch(
`${SAAS_BACKEND_URL}/payments/create_checkout_session` +
`?price_lookup_key=${priceLookupKey}` +
`&rewardful_referral_id=${rewardfulReferralId}`,
"POST"
);
// Track going to checkout page as a conversion
gtag("event", "conversion", {
send_to: "AW-16649848443/AKZpCJbP2cYZEPuMooM-",
});
// Redirect to Stripe Checkout
const { error } = await stripe.redirectToCheckout({
sessionId: res.sessionId,
});
if (error) {
throw new Error(error.message);
}
} catch (e) {
toast.error("Error directing you to checkout. Please contact support.");
addEvent("StripeCheckoutError");
} finally {
setIsLoadingCheckout(false);
}
};
// Load Stripe when the component mounts
useEffect(() => {
async function load() {
try {
setStripe(await loadStripe(STRIPE_PUBLISHABLE_KEY));
} catch (e) {
console.error(e);
addEvent("StripeFailedToLoad");
}
}
load();
}, []);
return { checkout, isLoadingCheckout };
}

View File

@ -0,0 +1,17 @@
// Keep in sync with saas backend
export interface UserResponse {
email: string;
first_name: string;
last_name: string;
subscriber_tier: string;
stripe_customer_id: string;
}
export interface PortalSessionResponse {
url: string;
}
export interface CreditsUsage {
total_monthly_credits: number;
used_monthly_credits: number;
}

View File

@ -0,0 +1,44 @@
import { useAuth } from "@clerk/clerk-react";
type FetchMethod = "GET" | "POST" | "PUT" | "DELETE";
// Assumes that the backend is using JWTs for authentication
// and assumes JSON responses
// *If response code is not 200 OK or if there's any other error, throws an error
export const useAuthenticatedFetch = () => {
const { getToken } = useAuth();
const authenticatedFetch = async (
url: string,
method: FetchMethod = "GET",
body: object | null | undefined = null
) => {
const accessToken = await getToken();
if (!accessToken) return;
const headers: HeadersInit = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
};
const options: RequestInit = {
method,
headers,
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
}
const json = await response.json();
return json;
};
return authenticatedFetch;
};

View File

@ -0,0 +1,6 @@
export function capitalize(str: string): string {
if (str.length === 0) {
return str;
}
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

View File

@ -1,25 +1,18 @@
import { useStore } from "../../store/store";
export function OnboardingNote() {
const setPricingDialogOpen = useStore((state) => state.setPricingDialogOpen);
return (
<div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm">
<span>
To use Screenshot to Code,{" "}
<a
className="inline underline hover:opacity-70"
href="https://buy.stripe.com/8wM6sre70gBW1nqaEE"
className="inline underline hover:opacity-70 cursor-pointer"
onClick={() => setPricingDialogOpen(true)}
target="_blank"
>
buy some credits (100 generations for $36)
</a>{" "}
or use your own OpenAI API key with GPT4 vision access.{" "}
<a
href="https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md"
className="inline underline hover:opacity-70"
target="_blank"
>
Follow these instructions to get yourself a key.
</a>{" "}
and paste it in the Settings dialog (gear icon above). Your key is only
stored in your browser. Never stored on our servers.
Subscribe to get started
</a>
</span>
</div>
);

View File

@ -1,7 +1,12 @@
import PricingDialog from "../hosted/payments/PricingDialog";
export function PicoBadge() {
return (
<>
<a
<div>
<PricingDialog />
</div>
{/* <a
href="https://screenshot-to-code.canny.io/feature-requests"
target="_blank"
>
@ -9,17 +14,9 @@ export function PicoBadge() {
className="fixed z-50 bottom-16 right-5 rounded-md shadow bg-black
text-white px-4 text-xs py-3 cursor-pointer"
>
feature requests?
feedback
</div>
</a>
<a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank">
<div
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
bg-white px-4 text-xs py-3 cursor-pointer"
>
an open source project by Pico
</div>
</a>
</a> */}
</>
);
}

View File

@ -58,14 +58,14 @@ function CodeTab({ code, setCode, settings }: Props) {
<div className="flex justify-start items-center px-4 mb-2">
<span
title="Copy Code"
className="bg-black text-white flex items-center justify-center hover:text-black hover:bg-gray-100 cursor-pointer rounded-lg text-sm p-2.5"
className="bg-black text-white flex items-center justify-center hover:text-black hover:bg-gray-100 cursor-pointer rounded-lg text-sm p-2.5 plausible-event-name=CopyCode"
onClick={copyCode}
>
Copy Code <FaCopy className="ml-2" />
</span>
<Button
onClick={doOpenInCodepenio}
className="bg-gray-100 text-black ml-2 py-2 px-4 border border-black rounded-md hover:bg-gray-400 focus:outline-none"
className="bg-gray-100 text-black ml-2 py-2 px-4 border border-black rounded-md hover:bg-gray-400 focus:outline-none plausible-event-name=Codepen"
>
Open in{" "}
<img

View File

@ -8,6 +8,7 @@ import {
} from "../ui/select";
import { Badge } from "../ui/badge";
import { Stack, STACK_DESCRIPTIONS } from "../../lib/stacks";
import { addEvent } from "../../lib/analytics";
function generateDisplayComponent(stack: Stack) {
const stackComponents = STACK_DESCRIPTIONS[stack].components;
@ -43,7 +44,10 @@ function OutputSettingsSection({
<span>{label}</span>
<Select
value={stack}
onValueChange={(value: string) => setStack(value as Stack)}
onValueChange={(value: string) => {
addEvent("OutputSettings", { stack: value });
setStack(value as Stack);
}}
disabled={shouldDisableUpdates}
>
<SelectTrigger className="col-span-2" id="output-settings-js">

View File

@ -46,6 +46,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
<DialogTitle className="mb-4">Settings</DialogTitle>
</DialogHeader>
{!IS_RUNNING_ON_CLOUD && (
<div className="flex items-center space-x-2">
<Label htmlFor="image-generation">
<div>DALL-E Placeholder Image Generation</div>
@ -64,13 +65,16 @@ function SettingsDialog({ settings, setSettings }: Props) {
}
/>
</div>
)}
<div className="flex flex-col space-y-6">
{!IS_RUNNING_ON_CLOUD && (
<div>
<Label htmlFor="openai-api-key">
<div>OpenAI API key</div>
<div className="font-light mt-1 mb-2 text-xs leading-relaxed">
Only stored in your browser. Never stored on servers. Overrides
your .env config.
Only stored in your browser. Never stored on servers.
Overrides your .env config.
</div>
</Label>
@ -86,6 +90,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
}
/>
</div>
)}
{!IS_RUNNING_ON_CLOUD && (
<div>
@ -110,12 +115,13 @@ function SettingsDialog({ settings, setSettings }: Props) {
</div>
)}
{!IS_RUNNING_ON_CLOUD && (
<div>
<Label htmlFor="anthropic-api-key">
<div>Anthropic API key</div>
<div className="font-light mt-1 text-xs leading-relaxed">
Only stored in your browser. Never stored on servers. Overrides
your .env config.
Only stored in your browser. Never stored on servers.
Overrides your .env config.
</div>
</Label>
@ -131,7 +137,9 @@ function SettingsDialog({ settings, setSettings }: Props) {
}
/>
</div>
)}
{!IS_RUNNING_ON_CLOUD && (
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
@ -165,6 +173,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
</AccordionContent>
</AccordionItem>
</Accordion>
)}
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">

View File

@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -1,16 +1,16 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
// import { Cross2Icon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -24,8 +24,8 @@ const DialogOverlay = React.forwardRef<
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@ -42,14 +42,14 @@ const DialogContent = React.forwardRef<
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
{/* <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Close> */}
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
@ -62,8 +62,8 @@ const DialogHeader = ({
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
@ -76,8 +76,8 @@ const DialogFooter = ({
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@ -91,8 +91,8 @@ const DialogTitle = React.forwardRef<
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@ -103,8 +103,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
@ -117,4 +117,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
}
};

View File

@ -0,0 +1,203 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -0,0 +1,19 @@
function FeedbackCallNote() {
return (
<div className="bg-blue-100 text-blue-800 p-4 rounded-lg">
<p className="text-sm">
Share your feedback with us on a{" "}
<a
href="https://dub.sh/DK4JOEY"
className="text-blue-800 underline"
target="_blank"
rel="noopener noreferrer"
>
15 min call (in English) and get $50 via Paypal or Amazon gift card.
</a>
</p>
</div>
);
}
export default FeedbackCallNote;

View File

@ -24,7 +24,8 @@ function Variants() {
{variants.map((_, index) => (
<div
key={index}
className={`p-2 border rounded-md cursor-pointer ${
className={`p-2 border rounded-md cursor-pointer
plausible-event-name=VariantClick ${
index === selectedVariantIndex
? "bg-blue-100 dark:bg-blue-900"
: "bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700"

View File

@ -8,5 +8,23 @@ export const WS_BACKEND_URL =
export const HTTP_BACKEND_URL =
import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001";
// Hosted version only
export const PICO_BACKEND_FORM_SECRET =
import.meta.env.VITE_PICO_BACKEND_FORM_SECRET || null;
export const CLERK_PUBLISHABLE_KEY =
import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || null;
export const STRIPE_PUBLISHABLE_KEY =
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || null;
export const SAAS_BACKEND_URL = import.meta.env.VITE_SAAS_BACKEND_URL || null;
// Feature flags
export const SHOULD_SHOW_FEEDBACK_CALL_NOTE = false;
export const IS_FREE_TRIAL_ENABLED = false;
// PostHog
export const POSTHOG_KEY = import.meta.env.VITE_POSTHOG_KEY || null;
export const POSTHOG_HOST = import.meta.env.VITE_POSTHOG_HOST || null;

View File

@ -0,0 +1,7 @@
export function addEvent(eventName: string, props = {}) {
try {
window.plausible(eventName, { props });
} catch (e) {
// silently fail in non-production environments
}
}

View File

@ -10,11 +10,26 @@ export enum CodeGenerationModel {
// Will generate a static error if a model in the enum above is not in the descriptions
export const CODE_GENERATION_MODEL_DESCRIPTIONS: {
[key in CodeGenerationModel]: { name: string; inBeta: boolean };
[key in CodeGenerationModel]: {
name: string;
inBeta: boolean;
};
} = {
"gpt-4o-2024-05-13": { name: "GPT-4o", inBeta: false },
"claude-3-5-sonnet-20240620": { name: "Claude 3.5 Sonnet", inBeta: false },
"gpt-4-turbo-2024-04-09": { name: "GPT-4 Turbo (deprecated)", inBeta: false },
gpt_4_vision: { name: "GPT-4 Vision (deprecated)", inBeta: false },
claude_3_sonnet: { name: "Claude 3 (deprecated)", inBeta: false },
"claude-3-5-sonnet-20240620": {
name: "Claude 3.5 Sonnet",
inBeta: false,
},
"gpt-4-turbo-2024-04-09": {
name: "GPT-4 Turbo (deprecated)",
inBeta: false,
},
gpt_4_vision: {
name: "GPT-4 Vision (deprecated)",
inBeta: false,
},
claude_3_sonnet: {
name: "Claude 3 (deprecated)",
inBeta: false,
},
};

View File

@ -1,19 +1,37 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { Toaster } from "react-hot-toast";
import AppContainer from "./components/hosted/AppContainer.tsx";
import { ClerkProvider } from "@clerk/clerk-react";
import EvalsPage from "./components/evals/EvalsPage.tsx";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { CLERK_PUBLISHABLE_KEY } from "./config.ts";
import "./index.css";
import PricingPage from "./components/hosted/PricingPage.tsx";
import CheckoutSuccessPage from "./components/hosted/CheckoutSuccessPage.tsx";
import FaqsPage from "./components/hosted/FaqsPage.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ClerkProvider
publishableKey={CLERK_PUBLISHABLE_KEY}
localization={{
footerPageLink__privacy:
"By signing up, you accept our terms of service and consent to receiving occasional product updates via email.",
}}
>
<Router>
<Routes>
<Route path="/" element={<App />} />
<Route path="/" element={<AppContainer />} />
<Route path="/evals" element={<EvalsPage />} />
<Route path="/pricing" element={<PricingPage />} />
<Route path="/faqs" element={<FaqsPage />} />
<Route path="/checkout-success" element={<CheckoutSuccessPage />} />
</Routes>
</Router>
<Toaster toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }} />
<Toaster
toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }}
/>
</ClerkProvider>
</React.StrictMode>
);

19
frontend/src/plausible.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
// plausible.d.ts
// Define the Plausible function type
type Plausible = (eventName: string, options?: PlausibleOptions) => void;
// Define the Plausible options type
interface PlausibleOptions {
callback?: () => void;
props?: Record<string, any>;
}
// Extend the Window interface to include the `plausible` function
declare global {
interface Window {
plausible: Plausible;
}
}
export {};

View File

@ -4,8 +4,8 @@ import { Commit, CommitHash } from "../components/commits/types";
// Store for app-wide state
interface ProjectStore {
// Inputs
inputMode: "image" | "video";
setInputMode: (mode: "image" | "video") => void;
inputMode: "image" | "video" | "text";
setInputMode: (mode: "image" | "video" | "text") => void;
isImportedFromCode: boolean;
setIsImportedFromCode: (imported: boolean) => void;
referenceImages: string[];

View File

@ -0,0 +1,16 @@
import { create } from "zustand";
interface Store {
isPricingDialogOpen: boolean;
setPricingDialogOpen: (isOpen: boolean) => void;
subscriberTier: string;
setSubscriberTier: (tier: string) => void;
}
export const useStore = create<Store>((set) => ({
isPricingDialogOpen: false,
setPricingDialogOpen: (isOpen: boolean) =>
set(() => ({ isPricingDialogOpen: isOpen })),
subscriberTier: "",
setSubscriberTier: (tier: string) => set(() => ({ subscriberTier: tier })),
}));

View File

@ -33,11 +33,12 @@ export enum ScreenRecorderState {
export interface CodeGenerationParams {
generationType: "create" | "update";
inputMode: "image" | "video";
inputMode: "image" | "video" | "text";
image: string;
resultImage?: string;
history?: string[];
isImportedFromCode?: boolean;
authToken?: string;
}
export type FullGenerationSettings = CodeGenerationParams & Settings;

View File

@ -16,7 +16,7 @@ export default ({ mode }) => {
inject: {
data: {
injectHead: process.env.VITE_IS_DEPLOYED
? '<script defer="" data-domain="screenshottocode.com" src="https://plausible.io/js/script.js"></script>'
? '<script defer="" data-domain="screenshottocode.com" src="https://plausible.io/js/script.tagged-events.outbound-links.js"></script><script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>'
: "",
},
},

View File

@ -493,6 +493,33 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@clerk/clerk-react@5.4.2":
version "5.4.2"
resolved "https://registry.yarnpkg.com/@clerk/clerk-react/-/clerk-react-5.4.2.tgz#866c23b83ef32cd27ff402c9e75ce8ab1149495a"
integrity sha512-F6F9yZ2lZDv365M6rn4Cpct0V2ZxFMoSxxRGag1du9QP73+twL7YoEeIimp+j4/RxH5uSsGv9JL2I7a6jnktHw==
dependencies:
"@clerk/shared" "2.5.2"
"@clerk/types" "4.14.0"
tslib "2.4.1"
"@clerk/shared@2.5.2":
version "2.5.2"
resolved "https://registry.yarnpkg.com/@clerk/shared/-/shared-2.5.2.tgz#03aba401cdd4ce91eede0978bba7f3ffd771f16c"
integrity sha512-3+I5vMhkn3wSqCuoxIIXRma3m8zpLJBH11MO2uxKOAeZJRyeALY0jGPeyptskl+Fl9j9Rtan0OWJIisMN8TiAA==
dependencies:
"@clerk/types" "4.14.0"
glob-to-regexp "0.4.1"
js-cookie "3.0.5"
std-env "^3.7.0"
swr "^2.2.0"
"@clerk/types@4.14.0":
version "4.14.0"
resolved "https://registry.yarnpkg.com/@clerk/types/-/types-4.14.0.tgz#2215d3a8337f984a401c5a819b793de622d223a5"
integrity sha512-d3MUcWtXGTOS7QYCRVrOra7NYAGRiNWlb+Ke2KMSb+Z3lea6WlKeHa18uVnAkIVxBXtHlzdU2kwW0PrHkx8j9Q==
dependencies:
csstype "3.1.1"
"@codemirror/autocomplete@^6.0.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.11.0.tgz#406dee8bf5342dfb48920ad75454d3406ddf9963"
@ -894,6 +921,11 @@
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz"
integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==
"@intercom/messenger-js-sdk@^0.0.11":
version "0.0.11"
resolved "https://registry.yarnpkg.com/@intercom/messenger-js-sdk/-/messenger-js-sdk-0.0.11.tgz#ffdf37891826296d514a496e13a8d07e3d101c7e"
integrity sha512-jBHXO2+cGoBHYQMPaLP8eUm4AREcTWXlfd9shlBLSyEkFuW8+So/ynUDlftvWYz81KvGohRWYauw6vLRH/AlfA==
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -1308,6 +1340,17 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-avatar@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623"
integrity sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-checkbox@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"
@ -1403,6 +1446,20 @@
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3"
"@radix-ui/react-dropdown-menu@^2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63"
integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-menu" "2.0.6"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-focus-guards@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
@ -1457,6 +1514,31 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-menu@2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e"
integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-collection" "1.0.3"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-direction" "1.0.1"
"@radix-ui/react-dismissable-layer" "1.0.5"
"@radix-ui/react-focus-guards" "1.0.1"
"@radix-ui/react-focus-scope" "1.0.4"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-popper" "1.1.3"
"@radix-ui/react-portal" "1.0.4"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-roving-focus" "1.0.4"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-use-callback-ref" "1.0.1"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-popover@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
@ -1795,6 +1877,18 @@
dependencies:
"@sinonjs/commons" "^3.0.0"
"@stripe/stripe-js@^2.2.2":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-2.4.0.tgz#7a7e5b187b9e9bb43073edd946ec3e9a778e61bd"
integrity sha512-WFkQx1mbs2b5+7looI9IV1BLa3bIApuN3ehp9FP58xGg7KL9hCHDECgW3BwO9l9L+xBPVAD7Yjn1EhGe6EDTeA==
"@swc/helpers@^0.5.3":
version "0.5.11"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.11.tgz#5bab8c660a6e23c13b2d23fcd1ee44a2db1b0cb7"
integrity sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==
dependencies:
tslib "^2.4.0"
"@tootallnate/quickjs-emscripten@^0.23.0":
version "0.23.0"
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
@ -1858,6 +1952,11 @@
dependencies:
"@types/node" "*"
"@types/gtag.js@^0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.20.tgz#e47edabb4ed5ecac90a079275958e6c929d7c08a"
integrity sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
@ -2599,6 +2698,11 @@ clean-css@^5.2.2:
dependencies:
source-map "~0.6.0"
client-only@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
@ -2772,6 +2876,11 @@ cssesc@^3.0.0:
resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
csstype@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
csstype@^3.0.2:
version "3.1.2"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz"
@ -2789,6 +2898,13 @@ debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, de
dependencies:
ms "2.1.2"
debug@^2.6.6:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
dedent@^1.0.0:
version "1.5.3"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a"
@ -3237,9 +3353,9 @@ extract-zip@2.0.1:
optionalDependencies:
"@types/yauzl" "^2.9.1"
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
fast-deep-equal@3.1.3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-fifo@^1.1.0, fast-fifo@^1.2.0:
@ -3289,6 +3405,11 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
@ -3460,6 +3581,11 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
glob-to-regexp@0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@7.1.6:
version "7.1.6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
@ -4153,6 +4279,11 @@ jiti@^1.19.1:
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
js-cookie@3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
@ -4257,6 +4388,11 @@ lines-and-columns@^1.1.6:
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
load-script@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4"
integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==
local-pkg@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c"
@ -4422,6 +4558,11 @@ mlly@^1.2.0, mlly@^1.4.2:
pkg-types "^1.0.3"
ufo "^1.3.0"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
@ -4804,6 +4945,19 @@ postcss@^8.4.32:
picocolors "^1.0.0"
source-map-js "^1.0.2"
posthog-js@^1.128.1:
version "1.136.2"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.136.2.tgz#a90bc908665f7bbc9b366e16a0a38e40aacef7dc"
integrity sha512-9oTUB/JDayzV+hB4f7u+ZNUbfnkGHLxyZw+FOE59pCgmbWHcJxhpGbu2Xlyv027/iHIjQbn1mtm2wJmBI2BuqA==
dependencies:
fflate "^0.4.8"
preact "^10.19.3"
preact@^10.19.3:
version "10.22.0"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.22.0.tgz#a50f38006ae438d255e2631cbdaf7488e6dd4e16"
integrity sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
@ -4831,7 +4985,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.8.1:
prop-types@15.8.1, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -5016,6 +5170,24 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4"
tslib "^2.0.0"
react-tweet@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/react-tweet/-/react-tweet-3.2.1.tgz#000d9bf2b2ce919fdec0e14241f05631e8917143"
integrity sha512-dktP3RMuwRB4pnSDocKpSsW5Hq1IXRW6fONkHhxT5EBIXsKZzdQuI70qtub1XN2dtZdkJWWxfBm/Q+kN+vRYFA==
dependencies:
"@swc/helpers" "^0.5.3"
clsx "^2.0.0"
swr "^2.2.4"
react-youtube@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/react-youtube/-/react-youtube-10.1.0.tgz#7e5670c764f12eb408166e8eb438d788dc64e8b5"
integrity sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==
dependencies:
fast-deep-equal "3.1.3"
prop-types "15.8.1"
youtube-player "5.5.2"
react@^18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
@ -5186,6 +5358,11 @@ signal-exit@^4.1.0:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
sister@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/sister/-/sister-3.0.2.tgz#bb3e39f07b1f75bbe1945f29a27ff1e5a2f26be4"
integrity sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==
sisteransi@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@ -5271,6 +5448,11 @@ std-env@^3.5.0:
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.6.0.tgz#94807562bddc68fa90f2e02c5fd5b6865bb4e98e"
integrity sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==
std-env@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
streamx@^2.13.0, streamx@^2.15.0:
version "2.16.1"
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.16.1.tgz#2b311bd34832f08aa6bb4d6a80297c9caef89614"
@ -5376,6 +5558,14 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
swr@^2.2.0, swr@^2.2.4:
version "2.2.5"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b"
integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==
dependencies:
client-only "^0.0.1"
use-sync-external-store "^1.2.0"
tailwind-merge@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.0.0.tgz#a0f3a8c874ebae5feec5595614d08245a5f88a39"
@ -5557,6 +5747,11 @@ ts-jest@^29.1.2:
semver "^7.5.3"
yargs-parser "^21.0.1"
tslib@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
@ -5652,6 +5847,11 @@ use-sync-external-store@1.2.0:
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
use-sync-external-store@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
@ -5943,6 +6143,15 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
youtube-player@5.5.2:
version "5.5.2"
resolved "https://registry.yarnpkg.com/youtube-player/-/youtube-player-5.5.2.tgz#052b86b1eabe21ff331095ffffeae285fa7f7cb5"
integrity sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==
dependencies:
debug "^2.6.6"
load-script "^1.0.0"
sister "^3.0.0"
zod@3.22.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"