Compare commits

...

231 Commits

Author SHA1 Message Date
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
60 changed files with 2717 additions and 543 deletions

View File

@ -7,19 +7,19 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-yaml - id: check-yaml
- id: check-added-large-files - id: check-added-large-files
- repo: local # - repo: local
hooks: # hooks:
- id: poetry-pytest # - id: poetry-pytest
name: Run pytest with Poetry # name: Run pytest with Poetry
entry: poetry run --directory backend pytest # entry: poetry run --directory backend pytest
language: system # language: system
pass_filenames: false # pass_filenames: false
always_run: true # always_run: true
files: ^backend/ # files: ^backend/
# - id: poetry-pyright # - id: poetry-pyright
# name: Run pyright with Poetry # name: Run pyright with Poetry
# entry: poetry run --directory backend pyright # entry: poetry run --directory backend pyright
# language: system # language: system
# pass_filenames: false # pass_filenames: false
# always_run: true # always_run: true
# files: ^backend/ # files: ^backend/

View File

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

View File

@ -22,3 +22,11 @@ DEBUG_DIR = os.environ.get("DEBUG_DIR", "")
# Set to True when running in production (on the hosted version) # Set to True when running in production (on the hosted version)
# Used as a feature flag to enable or disable certain features # Used as a feature flag to enable or disable certain features
IS_PROD = os.environ.get("IS_PROD", False) 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", "")

View File

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

View File

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

View File

@ -4,6 +4,7 @@ from typing import Any, Awaitable, Callable, List, cast
from anthropic import AsyncAnthropic from anthropic import AsyncAnthropic
from openai import AsyncOpenAI from openai import AsyncOpenAI
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk
import sentry_sdk
from config import IS_DEBUG_ENABLED from config import IS_DEBUG_ENABLED
from debug.DebugFileWriter import DebugFileWriter from debug.DebugFileWriter import DebugFileWriter
from image_processing.utils import process_image 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 # 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): class Llm(Enum):
GPT_4_VISION = "gpt-4-vision-preview" GPT_4_VISION = "gpt-4-vision-preview"
GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09" GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09"
@ -62,6 +64,25 @@ async def stream_openai_response(
full_response = "" full_response = ""
async for chunk in stream: # type: ignore async for chunk in stream: # type: ignore
assert isinstance(chunk, ChatCompletionChunk) 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 ( if (
chunk.choices chunk.choices
and len(chunk.choices) > 0 and len(chunk.choices) > 0
@ -138,6 +159,14 @@ async def stream_claude_response(
# Return final message # Return final message
response = await stream.get_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 # Close the Anthropic client
await client.close() await client.close()

View File

@ -4,10 +4,27 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
import os
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from routes import screenshot, generate_code, home, evals 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) app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
# Configure CORS settings # Configure CORS settings

782
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 image_generation.core import create_alt_url_mapping
from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS
from prompts.screenshot_system_prompts import 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 prompts.types import Stack
from video.utils import assemble_claude_prompt_video from video.utils import assemble_claude_prompt_video
@ -132,3 +133,22 @@ def assemble_prompt(
"content": user_content, "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" pre-commit = "^3.6.2"
anthropic = "^0.18.0" anthropic = "^0.18.0"
moviepy = "^1.0.3" moviepy = "^1.0.3"
sentry-sdk = {extras = ["fastapi"], version = "^1.38.0"}
pillow = "^10.3.0" pillow = "^10.3.0"
types-pillow = "^10.2.0.20240520" types-pillow = "^10.2.0.20240520"
aiohttp = "^3.9.5" aiohttp = "^3.9.5"

View File

@ -1,14 +1,15 @@
import asyncio import asyncio
from dataclasses import dataclass from dataclasses import dataclass
import os
from fastapi import APIRouter, WebSocket from fastapi import APIRouter, WebSocket
import openai import openai
from codegen.utils import extract_html_content from codegen.utils import extract_html_content
from config import ( from config import (
ANTHROPIC_API_KEY,
IS_PROD, IS_PROD,
NUM_VARIANTS, NUM_VARIANTS,
OPENAI_API_KEY,
OPENAI_BASE_URL, OPENAI_BASE_URL,
PLATFORM_ANTHROPIC_API_KEY,
PLATFORM_OPENAI_API_KEY,
REPLICATE_API_KEY, REPLICATE_API_KEY,
SHOULD_MOCK_AI_RESPONSE, SHOULD_MOCK_AI_RESPONSE,
) )
@ -20,8 +21,11 @@ from llm import (
stream_claude_response_native, stream_claude_response_native,
stream_openai_response, stream_openai_response,
) )
from fs_logging.core import write_logs
from mock_llm import mock_completion from mock_llm import mock_completion
from typing import Dict, List, 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, List, Literal, cast, get_args from typing import Any, Callable, Coroutine, Dict, List, Literal, cast, get_args
from image_generation.core import generate_images from image_generation.core import generate_images
from prompts import create_prompt from prompts import create_prompt
@ -94,6 +98,7 @@ class ExtractedParams:
openai_api_key: str | None openai_api_key: str | None
anthropic_api_key: str | None anthropic_api_key: str | None
openai_base_url: str | None openai_base_url: str | None
payment_method: PaymentMethod
async def extract_params( async def extract_params(
@ -123,14 +128,56 @@ async def extract_params(
await throw_error(f"Invalid model: {code_generation_model_str}") await throw_error(f"Invalid model: {code_generation_model_str}")
raise ValueError(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( # Read the auth token from the request (on the hosted version)
params, "openAiApiKey", OPENAI_API_KEY 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")
# If neither is provided, we throw an error later only if Claude is used. openai_api_key = None
anthropic_api_key = get_from_settings_dialog_or_env( anthropic_api_key = None
params, "anthropicApiKey", ANTHROPIC_API_KEY
) # 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")
# Use the user's OpenAI API key from the settings dialog if they are not a subscriber
if not openai_api_key:
openai_api_key = get_from_settings_dialog_or_env(params, "openAiApiKey", None)
if openai_api_key:
payment_method = PaymentMethod.OPENAI_API_KEY
print("Using OpenAI API key from user's settings dialog")
print("Payment method: ", payment_method)
if payment_method is PaymentMethod.UNKNOWN:
await throw_error(
"Please subscribe to a paid plan to generate code. If you are a subscriber and seeing this error, please contact support."
)
raise Exception("No payment method found")
# Base URL for OpenAI API # Base URL for OpenAI API
openai_base_url: str | None = None openai_base_url: str | None = None
@ -153,6 +200,7 @@ async def extract_params(
openai_api_key=openai_api_key, openai_api_key=openai_api_key,
anthropic_api_key=anthropic_api_key, anthropic_api_key=anthropic_api_key,
openai_base_url=openai_base_url, openai_base_url=openai_base_url,
payment_method=payment_method,
) )
@ -213,6 +261,7 @@ async def stream_code(websocket: WebSocket):
openai_base_url = extracted_params.openai_base_url openai_base_url = extracted_params.openai_base_url
anthropic_api_key = extracted_params.anthropic_api_key anthropic_api_key = extracted_params.anthropic_api_key
should_generate_images = extracted_params.should_generate_images should_generate_images = extracted_params.should_generate_images
payment_method = extracted_params.payment_method
# Auto-upgrade usage of older models # Auto-upgrade usage of older models
code_generation_model = auto_upgrade_model(code_generation_model) code_generation_model = auto_upgrade_model(code_generation_model)
@ -249,6 +298,9 @@ async def stream_code(websocket: WebSocket):
else: else:
try: try:
if input_mode == "video": if input_mode == "video":
if IS_PROD:
raise Exception("Video mode is not supported in prod")
if not anthropic_api_key: if not anthropic_api_key:
await throw_error( 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" "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"
@ -266,7 +318,6 @@ async def stream_code(websocket: WebSocket):
) )
] ]
else: else:
# Depending on the presence and absence of various keys, # Depending on the presence and absence of various keys,
# we decide which models to run # we decide which models to run
variant_models = [] variant_models = []
@ -282,7 +333,7 @@ async def stream_code(websocket: WebSocket):
) )
raise Exception("No OpenAI or Anthropic key") 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): for index, model in enumerate(variant_models):
if model == "openai": if model == "openai":
if openai_api_key is None: if openai_api_key is None:
@ -356,10 +407,30 @@ async def stream_code(websocket: WebSocket):
completions = [extract_html_content(completion) for completion in completions] completions = [extract_html_content(completion) for completion in completions]
# Write the messages dict into a log so that we can debug later # 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:
# TODO*
# assert exact_llm_version is not None, "exact_llm_version is not set"
await send_to_saas_backend(
prompt_messages,
# TODO*: Store both completions
completions[0],
payment_method=payment_method,
# TODO*
llm_version=Llm.GPT_4O_2024_05_13,
stack=stack,
is_imported_from_code=bool(params.get("isImportedFromCode", False)),
includes_result_image=bool(params.get("resultImage", False)),
input_mode=input_mode,
auth_token=params["authToken"],
)
except Exception as e:
print("Error sending to SaaS backend", e)
## Image Generation ## Image Generation
for index, _ in enumerate(completions): for index, _ in enumerate(completions):
await send_message("status", "Generating images...", index) await send_message("status", "Generating images...", index)

View File

@ -0,0 +1,56 @@
from enum import Enum
import httpx
from openai.types.chat import ChatCompletionMessageParam
from typing import List
import json
from config import 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(
prompt_messages: List[ChatCompletionMessageParam],
completion: str,
payment_method: PaymentMethod,
llm_version: Llm,
stack: Stack,
is_imported_from_code: bool,
includes_result_image: bool,
input_mode: InputMode,
auth_token: str | None = None,
):
if IS_PROD:
async with httpx.AsyncClient() as client:
url = BACKEND_SAAS_URL + "/generations/store"
data = json.dumps(
{
"prompt": json.dumps(prompt_messages),
"completion": completion,
"payment_method": payment_method.value,
"llm_version": llm_version.value,
"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 {auth_token}", # Add the auth token to the headers
}
response = await client.post(url, content=data, headers=headers)
response_data = response.json()
return response_data

View File

@ -0,0 +1,24 @@
import httpx
from pydantic import BaseModel
from config import BACKEND_SAAS_URL
class SubscriptionCreditsResponse(BaseModel):
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 fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
import httpx import httpx
from config import PLATFORM_SCREENSHOTONE_API_KEY
from routes.saas_utils import does_user_have_subscription_credits
router = APIRouter() router = APIRouter()
@ -12,10 +15,31 @@ def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str:
async def capture_screenshot( 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: ) -> bytes:
api_base_url = "https://api.screenshotone.com/take" 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 = { params = {
"access_key": api_key, "access_key": api_key,
"url": target_url, "url": target_url,
@ -44,7 +68,8 @@ async def capture_screenshot(
class ScreenshotRequest(BaseModel): class ScreenshotRequest(BaseModel):
url: str url: str
apiKey: str apiKey: str | None
authToken: str
class ScreenshotResponse(BaseModel): class ScreenshotResponse(BaseModel):
@ -56,9 +81,10 @@ async def app_screenshot(request: ScreenshotRequest):
# Extract the URL from the request body # Extract the URL from the request body
url = request.url url = request.url
api_key = request.apiKey api_key = request.apiKey
auth_token = request.authToken
# TODO: Add error handling # 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 # Convert the image bytes to a data url
data_url = bytes_to_data_url(image_bytes, "image/png") data_url = bytes_to_data_url(image_bytes, "image/png")

View File

@ -16,6 +16,21 @@
<!-- Injected code for hosted version --> <!-- Injected code for hosted version -->
<%- injectHead %> <%- 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> <title>Screenshot to Code</title>
<!-- Open Graph Meta Tags --> <!-- Open Graph Meta Tags -->

View File

@ -13,12 +13,16 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@clerk/clerk-react": "5.4.2",
"@codemirror/lang-html": "^6.4.6", "@codemirror/lang-html": "^6.4.6",
"@intercom/messenger-js-sdk": "^0.0.11",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@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-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5", "@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-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
@ -30,12 +34,15 @@
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4", "@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", "class-variance-authority": "^0.7.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"posthog-js": "^1.128.1",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -43,6 +50,8 @@
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-icons": "^4.12.0", "react-icons": "^4.12.0",
"react-router-dom": "^6.20.1", "react-router-dom": "^6.20.1",
"react-tweet": "^3.2.0",
"react-youtube": "^10.1.0",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"thememirror": "^2.0.1", "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 { generateCode } from "./generateCode";
import { IS_FREE_TRIAL_ENABLED, IS_RUNNING_ON_CLOUD } from "./config";
import SettingsDialog from "./components/settings/SettingsDialog"; import SettingsDialog from "./components/settings/SettingsDialog";
import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types"; import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types";
import { IS_RUNNING_ON_CLOUD } from "./config";
import { PicoBadge } from "./components/messages/PicoBadge"; import { PicoBadge } from "./components/messages/PicoBadge";
import { OnboardingNote } from "./components/messages/OnboardingNote"; import { OnboardingNote } from "./components/messages/OnboardingNote";
import { usePersistedState } from "./hooks/usePersistedState"; import { usePersistedState } from "./hooks/usePersistedState";
import TermsOfServiceDialog from "./components/TermsOfServiceDialog"; import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants"; import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
import { addEvent } from "./lib/analytics";
import { extractHistory } from "./components/history/utils"; import { extractHistory } from "./components/history/utils";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useAuth } from "@clerk/clerk-react";
import { useStore } from "./store/store";
import { Stack } from "./lib/stacks"; import { Stack } from "./lib/stacks";
import { CodeGenerationModel } from "./lib/models"; import { CodeGenerationModel } from "./lib/models";
import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator"; import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator";
import TipLink from "./components/messages/TipLink"; import TipLink from "./components/messages/TipLink";
import { useAppStore } from "./store/app-store"; import { useAppStore } from "./store/app-store";
import GenerateFromText from "./components/generate-from-text/GenerateFromText";
import { useProjectStore } from "./store/project-store"; import { useProjectStore } from "./store/project-store";
import Sidebar from "./components/sidebar/Sidebar";
import PreviewPane from "./components/preview/PreviewPane"; import PreviewPane from "./components/preview/PreviewPane";
import DeprecationMessage from "./components/messages/DeprecationMessage"; import DeprecationMessage from "./components/messages/DeprecationMessage";
import { GenerationSettings } from "./components/settings/GenerationSettings"; import { GenerationSettings } from "./components/settings/GenerationSettings";
import StartPane from "./components/start-pane/StartPane"; import StartPane from "./components/start-pane/StartPane";
import { takeScreenshot } from "./lib/takeScreenshot"; import { takeScreenshot } from "./lib/takeScreenshot";
import Sidebar from "./components/sidebar/Sidebar";
import { Commit } from "./components/commits/types"; import { Commit } from "./components/commits/types";
import { createCommit } from "./components/commits/utils"; 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 { const {
// Inputs // Inputs
inputMode, inputMode,
@ -140,12 +155,21 @@ function App() {
return; return;
} }
addEvent("Regenerate");
// Re-run the create // Re-run the create
doCreate(referenceImages, inputMode); if (inputMode === "image" || inputMode === "video") {
doCreate(referenceImages, inputMode);
} else {
// TODO: Fix this
doCreateFromText(initialPrompt);
}
}; };
// Used when the user cancels the code generation // Used when the user cancels the code generation
const cancelCodeGeneration = () => { const cancelCodeGeneration = () => {
addEvent("Cancel");
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE); 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 // Reset the execution console
resetExecutionConsoles(); resetExecutionConsoles();
@ -178,7 +202,12 @@ function App() {
setAppState(AppState.CODING); setAppState(AppState.CODING);
// Merge settings with params // Merge settings with params
const updatedParams = { ...params, ...settings }; const authToken = await getToken();
const updatedParams = {
...params,
...settings,
authToken: authToken || undefined,
};
const baseCommitObject = { const baseCommitObject = {
variants: [{ code: "" }, { code: "" }], variants: [{ code: "" }, { code: "" }],
@ -233,7 +262,10 @@ function App() {
} }
// Initial version creation // Initial version creation
function doCreate(referenceImages: string[], inputMode: "image" | "video") { async function doCreate(
referenceImages: string[],
inputMode: "image" | "video"
) {
// Reset any existing state // Reset any existing state
reset(); reset();
@ -243,6 +275,7 @@ function App() {
// Kick off the code generation // Kick off the code generation
if (referenceImages.length > 0) { if (referenceImages.length > 0) {
addEvent("Create");
doGenerateCode({ doGenerateCode({
generationType: "create", generationType: "create",
image: referenceImages[0], image: referenceImages[0],
@ -251,6 +284,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 // Subsequent updates
async function doUpdate( async function doUpdate(
updateInstruction: string, updateInstruction: string,
@ -272,6 +318,7 @@ function App() {
try { try {
historyTree = extractHistory(head, commits); historyTree = extractHistory(head, commits);
} catch { } catch {
addEvent("HistoryTreeFailed");
toast.error( toast.error(
"Version history is invalid. This shouldn't happen. Please contact support or open a Github issue." "Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
); );
@ -345,7 +392,7 @@ function App() {
{IS_RUNNING_ON_CLOUD && <PicoBadge />} {IS_RUNNING_ON_CLOUD && <PicoBadge />}
{IS_RUNNING_ON_CLOUD && ( {IS_RUNNING_ON_CLOUD && (
<TermsOfServiceDialog <TermsOfServiceDialog
open={!settings.isTermOfServiceAccepted} open={false}
onOpenChange={handleTermDialogOpenChange} onOpenChange={handleTermDialogOpenChange}
/> />
)} )}
@ -370,7 +417,14 @@ function App() {
{/* Show tip link until coding is complete */} {/* Show tip link until coding is complete */}
{appState !== AppState.CODE_READY && <TipLink />} {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 */} {/* Rest of the sidebar when we're not in the initial state */}
{(appState === AppState.CODING || {(appState === AppState.CODING ||
@ -386,6 +440,8 @@ function App() {
</div> </div>
<main className="py-2 lg:pl-96"> <main className="py-2 lg:pl-96">
{!!navbarComponent && navbarComponent}
{appState === AppState.INITIAL && ( {appState === AppState.INITIAL && (
<StartPane <StartPane
doCreate={doCreate} doCreate={doCreate}

View File

@ -7,6 +7,8 @@ import { URLS } from "../urls";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import ScreenRecorder from "./recording/ScreenRecorder"; import ScreenRecorder from "./recording/ScreenRecorder";
import { ScreenRecorderState } from "../types"; import { ScreenRecorderState } from "../types";
import { IS_RUNNING_ON_CLOUD } from "../config";
import { addEvent } from "../lib/analytics";
const baseStyle = { const baseStyle = {
flex: 1, flex: 1,
@ -82,6 +84,17 @@ function ImageUpload({ setReferenceImages }: Props) {
"video/webm": [".webm"], "video/webm": [".webm"],
}, },
onDrop: (acceptedFiles) => { 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 // Set up the preview thumbnail images
setFiles( setFiles(
acceptedFiles.map((file: File) => acceptedFiles.map((file: File) =>
@ -173,24 +186,29 @@ function ImageUpload({ setReferenceImages }: Props) {
</p> </p>
</div> </div>
)} )}
{screenRecorderState === ScreenRecorderState.INITIAL && ( {/* Disable on prod for now */}
<div className="text-center text-sm text-slate-800 mt-4"> {!IS_RUNNING_ON_CLOUD && (
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or record <>
your screen to clone a whole app (experimental).{" "} {screenRecorderState === ScreenRecorderState.INITIAL && (
<a <div className="text-center text-sm text-slate-800 mt-4">
className="underline" <Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or
href={URLS["intro-to-video"]} record your screen to clone a whole app (experimental).{" "}
target="_blank" <a
> className="underline"
Learn more. href={URLS["intro-to-video"]}
</a> target="_blank"
</div> >
Learn more.
</a>
</div>
)}
<ScreenRecorder
screenRecorderState={screenRecorderState}
setScreenRecorderState={setScreenRecorderState}
generateCode={setReferenceImages}
/>
</>
)} )}
<ScreenRecorder
screenRecorderState={screenRecorderState}
setScreenRecorderState={setScreenRecorderState}
generateCode={setReferenceImages}
/>
</section> </section>
); );
} }

View File

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

View File

@ -3,6 +3,8 @@ import { HTTP_BACKEND_URL } from "../config";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Input } from "./ui/input"; import { Input } from "./ui/input";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useStore } from "../store/store";
import { useAuth } from "@clerk/clerk-react";
interface Props { interface Props {
screenshotOneApiKey: string | null; screenshotOneApiKey: string | null;
@ -13,28 +15,31 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [referenceUrl, setReferenceUrl] = useState(""); const [referenceUrl, setReferenceUrl] = useState("");
// Hosted version only
const subscriberTier = useStore((state) => state.subscriberTier);
const { getToken } = useAuth();
async function takeScreenshot() { async function takeScreenshot() {
if (!screenshotOneApiKey) { if (!referenceUrl) {
toast.error( return toast.error("Please enter a URL");
"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) { if (!screenshotOneApiKey && subscriberTier === "free") {
toast.error("Please enter a URL"); return toast.error(
return; "Please upgrade to a paid plan to use the screenshot feature."
);
} }
if (referenceUrl) { if (referenceUrl) {
try { try {
setIsLoading(true); setIsLoading(true);
const authToken = await getToken();
const response = await fetch(`${HTTP_BACKEND_URL}/api/screenshot`, { const response = await fetch(`${HTTP_BACKEND_URL}/api/screenshot`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
url: referenceUrl, url: referenceUrl,
apiKey: screenshotOneApiKey, apiKey: screenshotOneApiKey,
authToken,
}), }),
headers: { headers: {
"Content-Type": "application/json", "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 <div
className="flex justify-between truncate flex-1 p-2" className="flex justify-between truncate flex-1 p-2 plausible-event-name=HistoryClick"
onClick={() => onClick={() =>
shouldDisableReverts shouldDisableReverts
? toast.error( ? 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() { export function OnboardingNote() {
const setPricingDialogOpen = useStore((state) => state.setPricingDialogOpen);
return ( return (
<div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm"> <div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm">
<span> <span>
To use Screenshot to Code,{" "}
<a <a
className="inline underline hover:opacity-70" className="inline underline hover:opacity-70 cursor-pointer"
href="https://buy.stripe.com/8wM6sre70gBW1nqaEE" onClick={() => setPricingDialogOpen(true)}
target="_blank" target="_blank"
> >
buy some credits (100 generations for $36) Subscribe to get started
</a>{" "} </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.
</span> </span>
</div> </div>
); );

View File

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

View File

@ -58,14 +58,14 @@ function CodeTab({ code, setCode, settings }: Props) {
<div className="flex justify-start items-center px-4 mb-2"> <div className="flex justify-start items-center px-4 mb-2">
<span <span
title="Copy Code" 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} onClick={copyCode}
> >
Copy Code <FaCopy className="ml-2" /> Copy Code <FaCopy className="ml-2" />
</span> </span>
<Button <Button
onClick={doOpenInCodepenio} 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{" "} Open in{" "}
<img <img

View File

@ -9,6 +9,7 @@ import {
CODE_GENERATION_MODEL_DESCRIPTIONS, CODE_GENERATION_MODEL_DESCRIPTIONS,
CodeGenerationModel, CodeGenerationModel,
} from "../../lib/models"; } from "../../lib/models";
import { IS_RUNNING_ON_CLOUD } from "../../config";
import { Badge } from "../ui/badge"; import { Badge } from "../ui/badge";
interface Props { interface Props {
@ -42,15 +43,18 @@ function ModelSettingsSection({
<SelectGroup> <SelectGroup>
{Object.values(CodeGenerationModel).map((model) => ( {Object.values(CodeGenerationModel).map((model) => (
<SelectItem key={model} value={model}> <SelectItem key={model} value={model}>
<div className="flex items-center"> <div className="flex flex-col">
<span className="font-semibold"> <div className="flex items-center">
{CODE_GENERATION_MODEL_DESCRIPTIONS[model].name} <span className="font-semibold">
</span> {CODE_GENERATION_MODEL_DESCRIPTIONS[model].name}
{CODE_GENERATION_MODEL_DESCRIPTIONS[model].inBeta && ( </span>
<Badge className="ml-2" variant="secondary"> {!IS_RUNNING_ON_CLOUD &&
Beta CODE_GENERATION_MODEL_DESCRIPTIONS[model].inBeta && (
</Badge> <Badge className="ml-2" variant="secondary">
)} Beta
</Badge>
)}
</div>
</div> </div>
</SelectItem> </SelectItem>
))} ))}

View File

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

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 React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons" // 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< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -24,8 +24,8 @@ const DialogOverlay = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@ -42,14 +42,14 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {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" /> <Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close> */}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ));
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ const DialogHeader = ({
className, className,
@ -62,8 +62,8 @@ const DialogHeader = ({
)} )}
{...props} {...props}
/> />
) );
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ const DialogFooter = ({
className, className,
@ -76,8 +76,8 @@ const DialogFooter = ({
)} )}
{...props} {...props}
/> />
) );
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@ -91,8 +91,8 @@ const DialogTitle = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@ -103,8 +103,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { export {
Dialog, Dialog,
@ -117,4 +117,4 @@ export {
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, 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

@ -8,5 +8,23 @@ export const WS_BACKEND_URL =
export const HTTP_BACKEND_URL = export const HTTP_BACKEND_URL =
import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001"; import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001";
// Hosted version only
export const PICO_BACKEND_FORM_SECRET = export const PICO_BACKEND_FORM_SECRET =
import.meta.env.VITE_PICO_BACKEND_FORM_SECRET || null; 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 // Will generate a static error if a model in the enum above is not in the descriptions
export const CODE_GENERATION_MODEL_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 }, "gpt-4o-2024-05-13": { name: "GPT-4o", inBeta: false },
"claude-3-5-sonnet-20240620": { name: "Claude 3.5 Sonnet", inBeta: false }, "claude-3-5-sonnet-20240620": {
"gpt-4-turbo-2024-04-09": { name: "GPT-4 Turbo (deprecated)", inBeta: false }, name: "Claude 3.5 Sonnet",
gpt_4_vision: { name: "GPT-4 Vision (deprecated)", inBeta: false }, inBeta: false,
claude_3_sonnet: { name: "Claude 3 (deprecated)", 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 React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { Toaster } from "react-hot-toast"; 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 EvalsPage from "./components/evals/EvalsPage.tsx";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 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( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<Router> <ClerkProvider
<Routes> publishableKey={CLERK_PUBLISHABLE_KEY}
<Route path="/" element={<App />} /> localization={{
<Route path="/evals" element={<EvalsPage />} /> footerPageLink__privacy:
</Routes> "By signing up, you accept our terms of service and consent to receiving occasional product updates via email.",
</Router> }}
<Toaster toastOptions={{ className: "dark:bg-zinc-950 dark:text-white" }} /> >
<Router>
<Routes>
<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" }}
/>
</ClerkProvider>
</React.StrictMode> </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 // Store for app-wide state
interface ProjectStore { interface ProjectStore {
// Inputs // Inputs
inputMode: "image" | "video"; inputMode: "image" | "video" | "text";
setInputMode: (mode: "image" | "video") => void; setInputMode: (mode: "image" | "video" | "text") => void;
isImportedFromCode: boolean; isImportedFromCode: boolean;
setIsImportedFromCode: (imported: boolean) => void; setIsImportedFromCode: (imported: boolean) => void;
referenceImages: string[]; 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 { export interface CodeGenerationParams {
generationType: "create" | "update"; generationType: "create" | "update";
inputMode: "image" | "video"; inputMode: "image" | "video" | "text";
image: string; image: string;
resultImage?: string; resultImage?: string;
history?: string[]; history?: string[];
isImportedFromCode?: boolean; isImportedFromCode?: boolean;
authToken?: string;
} }
export type FullGenerationSettings = CodeGenerationParams & Settings; export type FullGenerationSettings = CodeGenerationParams & Settings;

View File

@ -16,7 +16,7 @@ export default ({ mode }) => {
inject: { inject: {
data: { data: {
injectHead: process.env.VITE_IS_DEPLOYED 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" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== 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": "@codemirror/autocomplete@^6.0.0":
version "6.11.0" version "6.11.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.11.0.tgz#406dee8bf5342dfb48920ad75454d3406ddf9963" 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" resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz"
integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== 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": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" 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" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3" "@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": "@radix-ui/react-checkbox@^1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b" 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-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3" "@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": "@radix-ui/react-focus-guards@1.0.1":
version "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" 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" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3" "@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": "@radix-ui/react-popover@^1.0.7":
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c" resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
@ -1795,6 +1877,18 @@
dependencies: dependencies:
"@sinonjs/commons" "^3.0.0" "@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": "@tootallnate/quickjs-emscripten@^0.23.0":
version "0.23.0" version "0.23.0"
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
@ -1858,6 +1952,11 @@
dependencies: dependencies:
"@types/node" "*" "@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": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" 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: dependencies:
source-map "~0.6.0" 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: cliui@^8.0.1:
version "8.0.1" version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" 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" resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== 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: csstype@^3.0.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz" 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: dependencies:
ms "2.1.2" 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: dedent@^1.0.0:
version "1.5.3" version "1.5.3"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a"
@ -3237,9 +3353,9 @@ extract-zip@2.0.1:
optionalDependencies: optionalDependencies:
"@types/yauzl" "^2.9.1" "@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" 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== integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-fifo@^1.1.0, fast-fifo@^1.2.0: fast-fifo@^1.1.0, fast-fifo@^1.2.0:
@ -3289,6 +3405,11 @@ fd-slicer@~1.1.0:
dependencies: dependencies:
pend "~1.2.0" 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: file-entry-cache@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" 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: dependencies:
is-glob "^4.0.3" 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: glob@7.1.6:
version "7.1.6" version "7.1.6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" 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" resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== 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: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" 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" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== 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: local-pkg@^0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" 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" pkg-types "^1.0.3"
ufo "^1.3.0" 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: ms@2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
@ -4804,6 +4945,19 @@ postcss@^8.4.32:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" 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: prelude-ls@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" 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" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@^15.8.1: prop-types@15.8.1, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -5016,6 +5170,24 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4" invariant "^2.2.4"
tslib "^2.0.0" 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: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" 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" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== 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: sisteransi@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" 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" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.6.0.tgz#94807562bddc68fa90f2e02c5fd5b6865bb4e98e"
integrity sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg== 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: streamx@^2.13.0, streamx@^2.15.0:
version "2.16.1" version "2.16.1"
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.16.1.tgz#2b311bd34832f08aa6bb4d6a80297c9caef89614" 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" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 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: tailwind-merge@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.0.0.tgz#a0f3a8c874ebae5feec5595614d08245a5f88a39" 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" semver "^7.5.3"
yargs-parser "^21.0.1" 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: tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
version "2.6.2" version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" 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" 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== 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: util-deprecate@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" 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" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== 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: zod@3.22.4:
version "3.22.4" version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"