├── .devcontainer └── devcontainer.json ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── doc-report.yml │ ├── feature-request.yml │ ├── misc.yml │ └── translate-report.yml ├── dependabot.yml ├── images │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ └── 6.png └── workflows │ ├── license-copyright.yml │ └── nextjs.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── [lng] │ ├── chat │ │ ├── chat.tsx │ │ └── page.tsx │ ├── create │ │ ├── buy-subscription.tsx │ │ ├── create.tsx │ │ ├── no-session.tsx │ │ └── page.tsx │ ├── favicon.ico │ ├── generations │ │ ├── edit │ │ │ ├── editpage.tsx │ │ │ └── page.tsx │ │ ├── genpage.tsx │ │ ├── page.tsx │ │ └── view │ │ │ ├── page.tsx │ │ │ └── viewpage.tsx │ ├── globals.css │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ ├── me │ │ ├── ManageSubscriptionButton.tsx │ │ └── page.tsx │ ├── page.tsx │ ├── pricing │ │ └── page.tsx │ ├── prosemirror.css │ ├── settings │ │ └── page.tsx │ ├── signin │ │ ├── [id] │ │ │ └── page.tsx │ │ └── page.tsx │ └── templates │ │ ├── edit │ │ ├── editpage.tsx │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── templatepage.tsx │ │ └── view │ │ ├── page.tsx │ │ └── viewpage.tsx ├── api │ ├── chat │ │ └── route.ts │ ├── completions │ │ └── route.ts │ ├── models │ │ └── route.ts │ └── webhooks │ │ └── route.ts ├── auth │ ├── callback │ │ └── route.ts │ └── reset_password │ │ └── route.ts └── i18n │ ├── client.ts │ ├── index.ts │ ├── locales │ ├── en │ │ └── common.json │ ├── es │ │ └── common.json │ └── fr │ │ └── common.json │ └── settings.ts ├── components.json ├── components ├── chat-box.tsx ├── complex-gen.tsx ├── features.tsx ├── footer.tsx ├── format-dialog.tsx ├── format-selector.tsx ├── generation-item.tsx ├── icons.tsx ├── loading.tsx ├── mobile-nav.tsx ├── model-selector.tsx ├── navbar.tsx ├── peyronnet-logo.tsx ├── pricing-table.tsx ├── pricing.tsx ├── result-displayer.tsx ├── selectors │ ├── color-selector.tsx │ ├── link-selector.tsx │ ├── math-selector.tsx │ ├── node-selector.tsx │ └── text-buttons.tsx ├── spotlight.tsx ├── system-template-creator.tsx ├── tailwind-editor.tsx ├── theme-provider.tsx ├── ui │ ├── AccountForms │ │ ├── CustomerPortalForm.tsx │ │ ├── EmailForm.tsx │ │ ├── NameForm.tsx │ │ └── SignOutForm.tsx │ ├── AuthForms │ │ ├── EmailSignIn.tsx │ │ ├── ForgotPassword.tsx │ │ ├── OauthSignIn.tsx │ │ ├── PasswordSignIn.tsx │ │ ├── Separator.tsx │ │ ├── Signup.tsx │ │ └── UpdatePassword.tsx │ ├── accordion.tsx │ ├── button.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── flip-words.tsx │ ├── hover-card.tsx │ ├── input.tsx │ ├── moving-border.tsx │ ├── navigation-menu.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── spotlight-effect.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── tooltip.tsx │ └── use-toast.ts ├── variable-highlight.tsx ├── variable-item-view.tsx └── variable-item.tsx ├── i18n.json ├── lib ├── ai-chat.ts ├── ai-completions.ts ├── editor-extensions.ts ├── formats.ts ├── generation-step.ts ├── history.ts ├── image-upload.ts ├── languages.ts ├── models.ts ├── prompts │ ├── system.ts │ └── user.ts ├── recipe.ts ├── recipes │ ├── complex-essay-global.ts │ ├── complex-essay-literrature.ts │ ├── complex-essay-philo.ts │ └── complex-philo-analysis.ts ├── settings.ts ├── system-template.ts ├── utils.ts ├── variable.ts └── version.ts ├── middleware.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── MeshDark.svg ├── MeshLight.svg ├── images │ ├── app-dark.png │ ├── app.png │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png │ └── screens │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ └── 7.png ├── logo.png ├── logo.svg ├── logodark.svg ├── logolight.svg ├── manifest.json ├── next.svg ├── sw.js ├── vercel.svg └── workbox-4754cb34.js ├── tsconfig.json ├── types_db.ts └── utils ├── auth-helpers ├── client.ts ├── server.ts └── settings.ts ├── cn.ts ├── helpers.ts ├── mouse-position.tsx ├── stripe ├── client.ts ├── config.ts └── server.ts ├── supabase ├── admin.ts ├── client.ts ├── middleware.ts ├── queries.ts └── server.ts └── update-quotas.ts /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Synapsy Write Dev Container", 3 | "image": "mcr.microsoft.com/vscode/devcontainers/typescript-node:latest", 4 | "customizations": { 5 | "vscode": { 6 | "settings": { 7 | "terminal.integrated.shell.linux": "/bin/bash" 8 | }, 9 | "extensions": [ 10 | "dbaeumer.vscode-eslint", 11 | "esbenp.prettier-vscode", 12 | "github.vscode-github-actions", 13 | "bradlc.vscode-tailwindcss" 14 | ] 15 | } 16 | }, 17 | "postCreateCommand": "npm install", 18 | "forwardPorts": [3000], 19 | "remoteUser": "node" 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | package.json @lpeyr 2 | package-lock.json @lpeyr -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | title: "[Bug] " 4 | labels: [bug] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | attributes: 12 | label: What is the bug? 13 | description: Describe the bug, what is happening? 14 | placeholder: Tell us what you see! 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: How to reproduce the bug? 20 | description: Tell use how to reproduce the bug. 21 | placeholder: How to reproduce the bug? 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Screenshots 27 | description: Join a screenshot if you can. 28 | placeholder: Drag or paste an image here. 29 | validations: 30 | required: false 31 | - type: dropdown 32 | attributes: 33 | label: Operating System 34 | description: What operating system are you using? 35 | options: 36 | - Windows 37 | - macOS 38 | - Linux 39 | - Other 40 | validations: 41 | required: true 42 | - type: dropdown 43 | attributes: 44 | label: Browser 45 | description: What browser are you using? 46 | options: 47 | - Chrome 48 | - Firefox 49 | - Microsoft Edge (Chromium) 50 | - Microsoft Internet Explorer 51 | - Opera 52 | - Safari 53 | - Other 54 | validations: 55 | required: true 56 | - type: input 57 | attributes: 58 | label: What is the version of the browser? 59 | description: Tell us the precise version of your web browser. 60 | placeholder: v112.0.1722.48 61 | validations: 62 | required: false 63 | - type: input 64 | attributes: 65 | label: What is the version of the web app? 66 | description: Tell us the precise version of the web app you are using. 67 | placeholder: v1.5.0.2108 68 | validations: 69 | required: true 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/doc-report.yml: -------------------------------------------------------------------------------- 1 | name: Documentation Report 2 | description: File a documentation issue report. 3 | title: "[Documentation] " 4 | labels: [documentation] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill this documentation issue report! 10 | - type: textarea 11 | attributes: 12 | label: What is the problem? 13 | description: Describe the problem. 14 | placeholder: There is an issue in... 15 | validations: 16 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request/Suggest a feature. 3 | title: "[Enhancement] " 4 | labels: [enhancement] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to suggest a new feature for this project! 10 | - type: textarea 11 | attributes: 12 | label: Enhancement 13 | description: Describe your feature request/your idea here. 14 | placeholder: I would like to see... 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Other informations 20 | description: If you have other informations, like mockups, etc., put them here. 21 | placeholder: Add other informations here. 22 | validations: 23 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/misc.yml: -------------------------------------------------------------------------------- 1 | name: Misc 2 | description: Your issue doesn't fit in any categories. 3 | title: "[Misc] " 4 | labels: [misc] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this issue! 10 | - type: textarea 11 | attributes: 12 | label: Description of your issue 13 | description: Describe the problem. 14 | placeholder: Descirbe here your issue. 15 | validations: 16 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/translate-report.yml: -------------------------------------------------------------------------------- 1 | name: Translation issues 2 | description: Report a translation issue. 3 | title: "[Translation] " 4 | labels: [translation] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this translation issue report! 10 | - type: textarea 11 | attributes: 12 | label: Description of the problem 13 | description: Describe the translation problem. 14 | placeholder: Error in... 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Propose a translation 20 | description: Propose a new translation. 21 | placeholder: It should be... 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Screenshot 27 | description: If you have a screenshot, paste it here. 28 | placeholder: Drag or paste an image here. 29 | validations: 30 | required: false -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | target-branch: deps 8 | assignees: 9 | - lpeyr 10 | open-pull-requests-limit: 25 11 | -------------------------------------------------------------------------------- /.github/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synapsy-ai/write/ff07c44ee9e3f62bd60c5c1a1d16330aaeeb3b81/.github/images/1.png -------------------------------------------------------------------------------- /.github/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synapsy-ai/write/ff07c44ee9e3f62bd60c5c1a1d16330aaeeb3b81/.github/images/2.png -------------------------------------------------------------------------------- /.github/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synapsy-ai/write/ff07c44ee9e3f62bd60c5c1a1d16330aaeeb3b81/.github/images/3.png -------------------------------------------------------------------------------- /.github/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synapsy-ai/write/ff07c44ee9e3f62bd60c5c1a1d16330aaeeb3b81/.github/images/4.png -------------------------------------------------------------------------------- /.github/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synapsy-ai/write/ff07c44ee9e3f62bd60c5c1a1d16330aaeeb3b81/.github/images/5.png -------------------------------------------------------------------------------- /.github/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synapsy-ai/write/ff07c44ee9e3f62bd60c5c1a1d16330aaeeb3b81/.github/images/6.png -------------------------------------------------------------------------------- /.github/workflows/license-copyright.yml: -------------------------------------------------------------------------------- 1 | name: License Check Copyright 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 1 1 *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | run: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - uses: FantasticFiasco/action-update-license-year@v2 16 | with: 17 | token: ${{ secrets.ACTIONS_TOKEN }} 18 | commitTitle: 'Updated license copyright' 19 | assignees: 'lpeyr' 20 | labels: documentation -------------------------------------------------------------------------------- /.github/workflows/nextjs.yml: -------------------------------------------------------------------------------- 1 | name: Build Next.js Site 2 | on: 3 | push: 4 | pull_request: 5 | branches: 6 | - main # This will trigger the workflow on pull requests targeting the main branch 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Create .env with Github Secrets 15 | run: | 16 | touch .env 17 | echo NEXT_PUBLIC_SUPABASE_URL=$ENV_VAR_1 >> .env 18 | echo NEXT_PUBLIC_SUPABASE_ANON_KEY=$ENV_VAR_2 >> .env 19 | echo SUPABASE_SERVICE_ROLE_KEY=$ENV_VAR_3 >> .env 20 | echo OPENAI_API_KEY=$ENV_VAR_4 >> .env 21 | echo MISTRAL_API_KEY=$ENV_VAR_5 >> .env 22 | echo STRIPE_SECRET_KEY=$ENV_VAR_6 >> .env 23 | echo STRIPE_WEBHOOK_SECRET=$ENV_VAR_7 >> .env 24 | echo ANTHROPIC_API_KEY=$ENV_VAR_8 >> .env 25 | env: 26 | ENV_VAR_1: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} 27 | ENV_VAR_2: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} 28 | ENV_VAR_3: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} 29 | ENV_VAR_4: ${{ secrets.OPENAI_API_KEY }} 30 | ENV_VAR_5: ${{ secrets.MISTRAL_API_KEY }} 31 | ENV_VAR_6: ${{ secrets.STRIPE_SECRET_KEY }} 32 | ENV_VAR_7: ${{ secrets.STRIPE_WEBHOOK_SECRET }} 33 | ENV_VAR_8: ${{ secrets.ANTHROPIC_API_KEY }} 34 | - name: Setup Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: 18.x 38 | 39 | - name: Install dependencies 40 | run: npm install 41 | 42 | - name: Build 43 | run: npm run build 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # Local Netlify folder 38 | .netlify 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Synapsy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/[lng]/chat/page.tsx: -------------------------------------------------------------------------------- 1 | import Chat from "./chat"; 2 | import React, { Suspense } from "react"; 3 | import type { Metadata } from "next"; 4 | import { 5 | getUser, 6 | getSubscriptions, 7 | getUserDetails, 8 | setUserQuotas, 9 | } from "@/utils/supabase/queries"; 10 | import { createClient } from "@/utils/supabase/server"; 11 | import { DefaultLanguageParams } from "@/lib/languages"; 12 | import LoadingUI from "@/components/loading"; 13 | 14 | export const metadata: Metadata = { 15 | title: "Chat", 16 | description: 17 | "Chat with Synapsy Assistant to improve your documents or get information about them.", 18 | }; 19 | export default async function ChatPage({ 20 | params, 21 | }: { 22 | params: DefaultLanguageParams; 23 | }) { 24 | const { lng } = await params; 25 | const supabase = await createClient(); 26 | const [user, userDetails, subscriptions] = await Promise.all([ 27 | getUser(supabase), 28 | getUserDetails(supabase), 29 | getSubscriptions(supabase), 30 | ]); 31 | async function getQuotas(): Promise { 32 | if (!user) return 0; 33 | if (!userDetails) return 0; 34 | if (!userDetails?.write_gpt4_quota) { 35 | if (isSubscribed()) { 36 | const q = getInterval() === "year" ? 120 : 10; 37 | setUserQuotas(supabase, userDetails.id, q); 38 | return q; 39 | } 40 | } 41 | return userDetails.write_gpt4_quota || 0; 42 | } 43 | function getInterval(): "month" | "year" | "none" { 44 | if (!user || !subscriptions) return "none"; 45 | for (let i = 0; i < subscriptions?.length; i++) { 46 | if ( 47 | subscriptions[i].prices?.products?.name?.toLowerCase().includes("write") 48 | ) { 49 | return subscriptions[i].prices?.interval === "year" ? "year" : "month"; 50 | } 51 | } 52 | return "none"; 53 | } 54 | function isSubscribed(): boolean { 55 | if (!user || !subscriptions) return false; 56 | for (let i = 0; i < subscriptions?.length; i++) { 57 | if ( 58 | subscriptions[i].prices?.products?.name?.toLowerCase().includes("write") 59 | ) { 60 | return true; 61 | } 62 | } 63 | return false; 64 | } 65 | const q = await getQuotas(); 66 | return ( 67 | }> 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /app/[lng]/create/buy-subscription.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslation } from "@/app/i18n/client"; 3 | import Pricing from "@/components/pricing"; 4 | import { Tables } from "@/types_db"; 5 | import { User } from "@supabase/supabase-js"; 6 | import { useTheme } from "next-themes"; 7 | import Image from "next/image"; 8 | 9 | type Subscription = Tables<"subscriptions">; 10 | type Product = Tables<"products">; 11 | type Price = Tables<"prices">; 12 | 13 | interface ProductWithPrices extends Product { 14 | prices: Price[]; 15 | } 16 | interface PriceWithProduct extends Price { 17 | products: Product | null; 18 | } 19 | interface SubscriptionWithProduct extends Subscription { 20 | prices: PriceWithProduct | null; 21 | } 22 | export default function BuySubscription(props: { 23 | user: User; 24 | subscriptions: SubscriptionWithProduct[] | null; 25 | products: ProductWithPrices[]; 26 | lng: string; 27 | }) { 28 | const { t } = useTranslation(props.lng, "common"); 29 | return ( 30 |
31 |
32 |
33 | Synapsy Logo 41 |
42 |

{t("unlock-ai-sub")}

43 |
44 |
45 | 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/[lng]/create/no-session.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslation } from "@/app/i18n/client"; 3 | import PeyronnetLogo from "@/components/peyronnet-logo"; 4 | import { Button } from "@/components/ui/button"; 5 | import { useTheme } from "next-themes"; 6 | import Image from "next/image"; 7 | import Link from "next/link"; 8 | export default function NoSession(props: { lng: string }) { 9 | const { t } = useTranslation(props.lng, "common"); 10 | return ( 11 |
12 |
13 |
14 |
15 | Synapsy Logo 25 |
26 | 27 |
28 | 29 | {t("new")} 30 | 31 |

{t("unlock-power-ai")}

32 |

{t("account-desc")}

33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/[lng]/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getProducts, 3 | getSubscriptions, 4 | getUser, 5 | getUserDetails, 6 | setUserQuotas, 7 | } from "@/utils/supabase/queries"; 8 | import Create from "./create"; 9 | import { createClient } from "@/utils/supabase/server"; 10 | import { DefaultLanguageParams } from "@/lib/languages"; 11 | import { Suspense } from "react"; 12 | import LoadingUI from "@/components/loading"; 13 | 14 | export default async function CreatePage({ 15 | params, 16 | }: { 17 | params: DefaultLanguageParams; 18 | }) { 19 | const { lng } = await params; 20 | const supabase = await createClient(); 21 | const [user, userDetails, products, subscriptions] = await Promise.all([ 22 | getUser(supabase), 23 | getUserDetails(supabase), 24 | getProducts(supabase), 25 | getSubscriptions(supabase), 26 | ]); 27 | async function getQuotas(): Promise { 28 | if (!user) return 0; 29 | if (!userDetails) return 0; 30 | if (!userDetails.write_gpt4_quota) { 31 | if (isSubscribed()) { 32 | const q = getInterval() === "year" ? 120 : 10; 33 | setUserQuotas(supabase, user.id, q); 34 | return q; 35 | } 36 | } 37 | return userDetails.write_gpt4_quota || 0; 38 | } 39 | function getInterval(): "month" | "year" | "none" { 40 | if (!user || !subscriptions) return "none"; 41 | for (let i = 0; i < subscriptions?.length; i++) { 42 | if ( 43 | subscriptions[i].prices?.products?.name?.toLowerCase().includes("write") 44 | ) { 45 | return subscriptions[i].prices?.interval === "year" ? "year" : "month"; 46 | } 47 | } 48 | return "none"; 49 | } 50 | function isSubscribed(): boolean { 51 | if (!user || !subscriptions) return false; 52 | for (let i = 0; i < subscriptions?.length; i++) { 53 | if ( 54 | subscriptions[i].prices?.products?.name?.toLowerCase().includes("write") 55 | ) { 56 | return true; 57 | } 58 | } 59 | return false; 60 | } 61 | const q = await getQuotas(); 62 | return ( 63 | }> 64 | 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /app/[lng]/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synapsy-ai/write/ff07c44ee9e3f62bd60c5c1a1d16330aaeeb3b81/app/[lng]/favicon.ico -------------------------------------------------------------------------------- /app/[lng]/generations/edit/editpage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { HistoryItem } from "@/lib/history"; 4 | import { useSearchParams } from "next/navigation"; 5 | import { use, useState } from "react"; 6 | import { useTranslation } from "@/app/i18n/client"; 7 | import { typesToString } from "@/lib/formats"; 8 | import TailwindEditor from "@/components/tailwind-editor"; 9 | import { generateJSON } from "@tiptap/html"; 10 | import { defaultExtensions } from "@/lib/editor-extensions"; 11 | import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; 12 | import { DefaultLanguageParams } from "@/lib/languages"; 13 | 14 | export default function GenerationEditPage({ 15 | params, 16 | }: { 17 | params: DefaultLanguageParams; 18 | }) { 19 | const { lng } = use(params); 20 | const searchParams = useSearchParams(); 21 | const id = searchParams.get("id") ?? 0; 22 | let el: HistoryItem = { 23 | date: new Date(), 24 | prompt: "", 25 | content: "", 26 | template: "para", 27 | }; 28 | if (typeof window !== "undefined") { 29 | el = JSON.parse(localStorage.getItem("synapsy_write_history") ?? "[]")[id]; 30 | } 31 | 32 | const { t } = useTranslation(lng, "common"); 33 | const [content, setContent] = useState(el.content); 34 | const c = generateJSON(content, [...defaultExtensions]); 35 | return ( 36 |
37 |
38 |

{t("edit")}

39 |
40 | 41 |
42 | 43 |
44 | 45 | 46 | {t("overview")} 47 | 48 | 49 |
50 |
51 | {t("format")} 52 | 53 | {t(typesToString(el.template))} 54 | 55 |
56 |
57 | {t("date")} 58 | 59 | {new Date(el.date).toLocaleString()} 60 | 61 |
62 | {el.template !== "manual" && ( 63 |
64 | {t("prompt")} 65 | 66 | {el.prompt} 67 | 68 |
69 | )} 70 |
71 |
72 |
73 |
74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /app/[lng]/generations/edit/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Suspense } from "react"; 3 | import GenerationEditPage from "./editpage"; 4 | import { DefaultLanguageParams } from "@/lib/languages"; 5 | import LoadingUI from "@/components/loading"; 6 | 7 | export default function ViewPage({ 8 | params, 9 | }: { 10 | params: DefaultLanguageParams; 11 | }) { 12 | return ( 13 | }> 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/[lng]/generations/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Suspense } from "react"; 3 | import GenerationsPage from "./genpage"; 4 | import { DefaultLanguageParams } from "@/lib/languages"; 5 | import LoadingUI from "@/components/loading"; 6 | 7 | export default function ViewPage({ 8 | params, 9 | }: { 10 | params: DefaultLanguageParams; 11 | }) { 12 | return ( 13 | }> 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/[lng]/generations/view/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Suspense } from "react"; 3 | import GenerationViewPage from "./viewpage"; 4 | import { DefaultLanguageParams } from "@/lib/languages"; 5 | import LoadingUI from "@/components/loading"; 6 | 7 | export default function ViewPage({ 8 | params, 9 | }: { 10 | params: DefaultLanguageParams; 11 | }) { 12 | return ( 13 | }> 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/[lng]/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Manrope } from "next/font/google"; 4 | import { dir } from "i18next"; 5 | import { languages } from "../i18n/settings"; 6 | import NavBar from "@/components/navbar"; 7 | import { ThemeProvider } from "@/components/theme-provider"; 8 | import Script from "next/script"; 9 | import MobileNavBar from "@/components/mobile-nav"; 10 | import { Toaster } from "@/components/ui/toaster"; 11 | import { Suspense, use } from "react"; 12 | import { DefaultLanguageParams } from "@/lib/languages"; 13 | import LoadingUI from "@/components/loading"; 14 | 15 | const manrope = Manrope({ subsets: ["latin"] }); 16 | export async function generateStaticParams() { 17 | return languages.map((lng) => ({ lng })); 18 | } 19 | 20 | export const metadata: Metadata = { 21 | title: { default: "Synapsy Write", template: "%s | Synapsy Write" }, 22 | description: 23 | "AI-powered document generator. Generate essays, articles and complex documents using automations and AI.", 24 | twitter: { 25 | description: 26 | "AI-powered document generator. Generate essays, articles and complex documents using automations and AI.", 27 | site: "@PeyronnetGroup", 28 | card: "summary_large_image", 29 | images: "https://peyronnet.group/synapsy_write_social.png", 30 | }, 31 | applicationName: "Synapsy Write", 32 | appleWebApp: { 33 | capable: true, 34 | statusBarStyle: "default", 35 | title: "Synapsy Write", 36 | }, 37 | openGraph: { 38 | title: "Synapsy Write", 39 | description: 40 | "AI-powered document generator. Generate essays, articles and complex documents using automations and AI.", 41 | images: "https://peyronnet.group/synapsy_write_social.png", 42 | }, 43 | }; 44 | 45 | export default function RootLayout({ 46 | children, 47 | params, 48 | }: { 49 | children: React.ReactNode; 50 | params: DefaultLanguageParams; 51 | }) { 52 | const { lng } = use(params); 53 | return ( 54 | 55 | 56 | 57 | 58 | 62 | 63 | 64 | 65 | 70 | 75 | 80 | 88 | 89 | 95 | 96 | 97 | }>{children} 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /app/[lng]/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export default function SignIn() { 4 | return redirect(`/signin/password_signin`); 5 | } 6 | -------------------------------------------------------------------------------- /app/[lng]/me/ManageSubscriptionButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTranslation } from "@/app/i18n/client"; 4 | import { Button } from "@/components/ui/button"; 5 | import { postData } from "@/utils/helpers"; 6 | 7 | import { Session } from "@supabase/supabase-js"; 8 | import { useRouter } from "next/navigation"; 9 | 10 | interface Props { 11 | session: Session; 12 | lng: string; 13 | } 14 | 15 | export default function ManageSubscriptionButton({ session, lng }: Props) { 16 | const { t } = useTranslation(lng, "common"); 17 | const router = useRouter(); 18 | const redirectToCustomerPortal = async () => { 19 | try { 20 | const { url } = await postData({ 21 | url: "/api/create-portal-link", 22 | }); 23 | router.push(url); 24 | } catch (error) { 25 | if (error) return alert((error as Error).message); 26 | } 27 | }; 28 | 29 | return ( 30 |
31 |

{t("manage-stripe")}

32 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/[lng]/me/page.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "@/app/i18n"; 2 | import Link from "next/link"; 3 | import { redirect } from "next/navigation"; 4 | import { use } from "react"; 5 | import { 6 | getUserDetails, 7 | getSubscriptions, 8 | getUser, 9 | } from "@/utils/supabase/queries"; 10 | import { createClient } from "@/utils/supabase/server"; 11 | import CustomerPortalForm from "@/components/ui/AccountForms/CustomerPortalForm"; 12 | import EmailForm from "@/components/ui/AccountForms/EmailForm"; 13 | import NameForm from "@/components/ui/AccountForms/NameForm"; 14 | import SignOutForm from "@/components/ui/AccountForms/SignOutForm"; 15 | import { DefaultLanguageParams, Language } from "@/lib/languages"; 16 | 17 | export default async function Account({ 18 | params, 19 | }: { 20 | params: DefaultLanguageParams; 21 | }) { 22 | const { lng } = await params; 23 | // eslint-disable-next-line react-hooks/rules-of-hooks 24 | const { t } = await useTranslation(lng, "common"); 25 | const supabase = await createClient(); 26 | const [user, userDetails, subscription] = await Promise.all([ 27 | getUser(supabase), 28 | getUserDetails(supabase), 29 | getSubscriptions(supabase), 30 | ]); 31 | 32 | if (!user) { 33 | return redirect("/signin"); 34 | } 35 | 36 | return ( 37 |
38 |
39 |
40 |

{t("my-account")}

41 |

42 | {" "} 43 | {t("welcome-msg").replace( 44 | "[[user]]", 45 | userDetails?.full_name || "user", 46 | )} 47 |

48 |
49 |
50 |
51 |

52 | {t("account-read-only-1")}{" "} 53 | 59 | account.peyronnet.group 60 | {" "} 61 | {t("account-read-only-2")} 62 |

63 |
64 | 65 | 66 | 67 | 68 |
69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/[lng]/pricing/page.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "@/app/i18n"; 2 | import SiteFooter from "@/components/footer"; 3 | import Pricing from "@/components/pricing"; 4 | import PricingFeatureTable from "@/components/pricing-table"; 5 | import { DefaultLanguageParams } from "@/lib/languages"; 6 | import { 7 | getUser, 8 | getProducts, 9 | getSubscriptions, 10 | } from "@/utils/supabase/queries"; 11 | import { createClient } from "@/utils/supabase/server"; 12 | 13 | export const metadata = { 14 | title: "Pricing", 15 | description: 16 | "Get more information about the available plans of Synapsy Write.", 17 | }; 18 | 19 | export default async function PricingPage({ 20 | params, 21 | }: { 22 | params: DefaultLanguageParams; 23 | }) { 24 | const { lng } = await params; 25 | const supabase = await createClient(); 26 | const [user, products, subscriptions] = await Promise.all([ 27 | getUser(supabase), 28 | getProducts(supabase), 29 | getSubscriptions(supabase), 30 | ]); 31 | // eslint-disable-next-line react-hooks/rules-of-hooks 32 | const { t } = await useTranslation(lng, "common"); 33 | 34 | return ( 35 | <> 36 |
37 | 43 |
44 |

{t("features")}

45 |

{t("features-desc")}

46 |
47 | 48 |
49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/[lng]/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { getDefaultSignInView } from "@/utils/auth-helpers/settings"; 3 | import { cookies } from "next/headers"; 4 | 5 | export default async function SignIn() { 6 | const cookieStore = await cookies(); 7 | const preferredSignInView = 8 | cookieStore.get("preferredSignInView")?.value || null; 9 | const defaultView = getDefaultSignInView(preferredSignInView); 10 | 11 | return redirect(`/signin/${defaultView}`); 12 | } 13 | -------------------------------------------------------------------------------- /app/[lng]/templates/edit/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Suspense } from "react"; 3 | import { Skeleton } from "@/components/ui/skeleton"; 4 | import EditTemplatePage from "./editpage"; 5 | import { DefaultLanguageParams } from "@/lib/languages"; 6 | 7 | export default function ViewPage({ 8 | params, 9 | }: { 10 | params: DefaultLanguageParams; 11 | }) { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | function LoadingUI() { 19 | return ( 20 |
21 |

Loading...

22 |
23 | 24 | 25 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/[lng]/templates/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Suspense } from "react"; 3 | import { DefaultLanguageParams } from "@/lib/languages"; 4 | import LoadingUI from "@/components/loading"; 5 | import TemplatesPage from "./templatepage"; 6 | 7 | export default function ViewPage({ 8 | params, 9 | }: { 10 | params: DefaultLanguageParams; 11 | }) { 12 | return ( 13 | }> 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/[lng]/templates/view/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Suspense } from "react"; 3 | import { Skeleton } from "@/components/ui/skeleton"; 4 | import ViewTemplatePage from "./viewpage"; 5 | import { DefaultLanguageParams } from "@/lib/languages"; 6 | 7 | export default function ViewPage({ 8 | params, 9 | }: { 10 | params: DefaultLanguageParams; 11 | }) { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | function LoadingUI() { 19 | return ( 20 |
21 |

Loading...

22 |
23 | 24 | 25 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { streamText } from "ai"; 2 | import { openai } from "@ai-sdk/openai"; 3 | 4 | export async function POST(req: Request) { 5 | const json = await req.json(); 6 | const { 7 | messages, 8 | model, 9 | temperature, 10 | top_p, 11 | frequency_penalty, 12 | presence_penalty, 13 | } = json; 14 | 15 | const res = streamText({ 16 | model: openai(model), 17 | messages: messages, 18 | temperature: temperature, 19 | topP: top_p, 20 | frequencyPenalty: frequency_penalty, 21 | presencePenalty: presence_penalty, 22 | }); 23 | 24 | return res.toDataStreamResponse(); 25 | } 26 | -------------------------------------------------------------------------------- /app/api/completions/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import OpenAI from "openai"; 3 | 4 | const openai = new OpenAI({ 5 | apiKey: process.env.OPENAI_API_KEY, 6 | }); 7 | 8 | export async function POST(req: Request) { 9 | const json = await req.json(); 10 | const { 11 | messages, 12 | model, 13 | temperature, 14 | top_p, 15 | frequency_penalty, 16 | presence_penalty, 17 | previewToken, 18 | } = json; 19 | 20 | const res = await openai.chat.completions.create({ 21 | model: model, 22 | messages, 23 | temperature: temperature, 24 | top_p: top_p, 25 | frequency_penalty: frequency_penalty, 26 | presence_penalty: presence_penalty, 27 | }); 28 | 29 | return new NextResponse(res.choices[0].message.content); 30 | } 31 | -------------------------------------------------------------------------------- /app/api/models/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import OpenAI from "openai"; 3 | import { Mistral } from "@mistralai/mistralai"; 4 | import { Anthropic } from "@anthropic-ai/sdk"; 5 | 6 | const openai = new OpenAI({ 7 | apiKey: process.env.OPENAI_API_KEY, 8 | }); 9 | 10 | const mistral = new Mistral({ 11 | apiKey: process.env.MISTRAL_API_KEY, 12 | }); 13 | 14 | const anthropic = new Anthropic({ 15 | apiKey: process.env.ANTHROPIC_API_KEY, 16 | }); 17 | 18 | export async function GET(req: Request) { 19 | let models = await openai.models.list(); 20 | let mistralModels = await mistral.models.list(); 21 | let anthropicModels = await anthropic.models.list(); 22 | let response = { 23 | openAiModels: models.data.map((model) => model.id).sort(), 24 | mistralModels: mistralModels.data?.map((model) => model.id).sort(), 25 | anthropicModels: anthropicModels.data.map((model) => model.id).sort(), 26 | }; 27 | return new NextResponse(JSON.stringify(response)); 28 | } 29 | -------------------------------------------------------------------------------- /app/api/webhooks/route.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { stripe } from "@/utils/stripe/config"; 3 | import { 4 | upsertProductRecord, 5 | upsertPriceRecord, 6 | manageSubscriptionStatusChange, 7 | deleteProductRecord, 8 | deletePriceRecord, 9 | } from "@/utils/supabase/admin"; 10 | const relevantEvents = new Set([ 11 | "product.created", 12 | "product.updated", 13 | "product.deleted", 14 | "price.created", 15 | "price.updated", 16 | "price.deleted", 17 | "checkout.session.completed", 18 | "customer.subscription.created", 19 | "customer.subscription.updated", 20 | "customer.subscription.deleted", 21 | "invoice.paid", 22 | ]); 23 | 24 | export async function POST(req: Request) { 25 | const body = await req.text(); 26 | const sig = req.headers.get("stripe-signature") as string; 27 | const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; 28 | let event: Stripe.Event; 29 | 30 | try { 31 | if (!sig || !webhookSecret) 32 | return new Response("Webhook secret not found.", { status: 400 }); 33 | event = stripe.webhooks.constructEvent(body, sig, webhookSecret); 34 | console.log(`🔔 Webhook received: ${event.type}`); 35 | } catch (err: any) { 36 | console.log(`❌ Error message: ${err.message}`); 37 | return new Response(`Webhook Error: ${err.message}`, { status: 400 }); 38 | } 39 | 40 | if (relevantEvents.has(event.type)) { 41 | try { 42 | switch (event.type) { 43 | case "product.created": 44 | case "product.updated": 45 | await upsertProductRecord(event.data.object as Stripe.Product); 46 | break; 47 | case "price.created": 48 | case "price.updated": 49 | await upsertPriceRecord(event.data.object as Stripe.Price); 50 | break; 51 | case "price.deleted": 52 | await deletePriceRecord(event.data.object as Stripe.Price); 53 | break; 54 | case "product.deleted": 55 | await deleteProductRecord(event.data.object as Stripe.Product); 56 | break; 57 | case "customer.subscription.created": 58 | case "customer.subscription.updated": 59 | case "customer.subscription.deleted": 60 | const subscription = event.data.object as Stripe.Subscription; 61 | await manageSubscriptionStatusChange( 62 | subscription.id, 63 | subscription.customer as string, 64 | event.type === "customer.subscription.created", 65 | ); 66 | break; 67 | case "checkout.session.completed": 68 | const checkoutSession = event.data.object as Stripe.Checkout.Session; 69 | if (checkoutSession.mode === "subscription") { 70 | const subscriptionId = checkoutSession.subscription; 71 | await manageSubscriptionStatusChange( 72 | subscriptionId as string, 73 | checkoutSession.customer as string, 74 | true, 75 | ); 76 | } 77 | break; 78 | default: 79 | throw new Error("Unhandled relevant event!"); 80 | } 81 | } catch (error) { 82 | console.log(error); 83 | return new Response( 84 | `Webhook handler failed. View your Next.js function logs. ${error}`, 85 | { 86 | status: 400, 87 | }, 88 | ); 89 | } 90 | } 91 | return new Response(JSON.stringify({ received: true })); 92 | } 93 | -------------------------------------------------------------------------------- /app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server"; 2 | import { NextResponse } from "next/server"; 3 | import { NextRequest } from "next/server"; 4 | import { getErrorRedirect, getStatusRedirect } from "@/utils/helpers"; 5 | 6 | export async function GET(request: NextRequest) { 7 | // The `/auth/callback` route is required for the server-side auth flow implemented 8 | // by the `@supabase/ssr` package. It exchanges an auth code for the user's session. 9 | const requestUrl = new URL(request.url); 10 | const code = requestUrl.searchParams.get("code"); 11 | 12 | if (code) { 13 | const supabase = await createClient(); 14 | 15 | const { error } = await supabase.auth.exchangeCodeForSession(code); 16 | 17 | if (error) { 18 | return NextResponse.redirect( 19 | getErrorRedirect( 20 | `https://write.peyronnet.group/signin`, 21 | error.name, 22 | "Sorry, we weren't able to log you in. Please try again.", 23 | ), 24 | ); 25 | } 26 | } 27 | 28 | // URL to redirect to after sign in process completes 29 | return NextResponse.redirect( 30 | getStatusRedirect( 31 | `https://write.peyronnet.group/me`, 32 | "Success!", 33 | "You are now signed in.", 34 | ), 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/auth/reset_password/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server"; 2 | import { NextResponse } from "next/server"; 3 | import { NextRequest } from "next/server"; 4 | import { getErrorRedirect, getStatusRedirect } from "@/utils/helpers"; 5 | 6 | export async function GET(request: NextRequest) { 7 | // The `/auth/callback` route is required for the server-side auth flow implemented 8 | // by the `@supabase/ssr` package. It exchanges an auth code for the user's session. 9 | const requestUrl = new URL(request.url); 10 | const code = requestUrl.searchParams.get("code"); 11 | 12 | if (code) { 13 | const supabase = await createClient(); 14 | 15 | const { error } = await supabase.auth.exchangeCodeForSession(code); 16 | 17 | if (error) { 18 | return NextResponse.redirect( 19 | getErrorRedirect( 20 | `https://write.peyronnet.group/signin/forgot_password`, 21 | error.name, 22 | "Sorry, we weren't able to log you in. Please try again.", 23 | ), 24 | ); 25 | } 26 | } 27 | 28 | // URL to redirect to after sign in process completes 29 | return NextResponse.redirect( 30 | getStatusRedirect( 31 | `https://write.peyronnet.group/signin/update_password`, 32 | "You are now signed in.", 33 | "Please enter a new password for your account.", 34 | ), 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/i18n/client.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import i18next from "i18next"; 5 | import { 6 | initReactI18next, 7 | useTranslation as useTranslationOrg, 8 | } from "react-i18next"; 9 | import { useCookies } from "react-cookie"; 10 | import resourcesToBackend from "i18next-resources-to-backend"; 11 | import LanguageDetector from "i18next-browser-languagedetector"; 12 | import { getOptions, languages, cookieName } from "./settings"; 13 | 14 | const runsOnServerSide = typeof window === "undefined"; 15 | 16 | // 17 | i18next 18 | .use(initReactI18next) 19 | .use(LanguageDetector) 20 | .use( 21 | resourcesToBackend( 22 | (language: string, namespace: string) => 23 | import(`./locales/${language}/${namespace}.json`), 24 | ), 25 | ) 26 | .init({ 27 | ...getOptions(), 28 | lng: undefined, // let detect the language on client side 29 | detection: { 30 | order: ["path", "htmlTag", "cookie", "navigator"], 31 | }, 32 | preload: runsOnServerSide ? languages : [], 33 | }); 34 | 35 | export function useTranslation(lng: string, ns: string, options?: any) { 36 | const [cookies, setCookie] = useCookies([cookieName]); 37 | const ret = useTranslationOrg(ns, options); 38 | const { i18n } = ret; 39 | if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) { 40 | i18n.changeLanguage(lng); 41 | } else { 42 | // eslint-disable-next-line react-hooks/rules-of-hooks 43 | const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage); 44 | // eslint-disable-next-line react-hooks/rules-of-hooks 45 | useEffect(() => { 46 | if (activeLng === i18n.resolvedLanguage) return; 47 | setActiveLng(i18n.resolvedLanguage); 48 | }, [activeLng, i18n.resolvedLanguage]); 49 | // eslint-disable-next-line react-hooks/rules-of-hooks 50 | useEffect(() => { 51 | if (!lng || i18n.resolvedLanguage === lng) return; 52 | i18n.changeLanguage(lng); 53 | }, [lng, i18n]); 54 | // eslint-disable-next-line react-hooks/rules-of-hooks 55 | useEffect(() => { 56 | if (cookies.i18next === lng) return; 57 | setCookie(cookieName, lng, { path: "/" }); 58 | }, [lng, cookies.i18next]); 59 | } 60 | return ret; 61 | } 62 | -------------------------------------------------------------------------------- /app/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createInstance } from "i18next"; 2 | import resourcesToBackend from "i18next-resources-to-backend"; 3 | import { initReactI18next } from "react-i18next/initReactI18next"; 4 | import { getOptions } from "./settings"; 5 | 6 | const initI18next = async (lng: any, ns: any) => { 7 | const i18nInstance = createInstance(); 8 | await i18nInstance 9 | .use(initReactI18next) 10 | .use( 11 | resourcesToBackend( 12 | (language: any, namespace: any) => 13 | import(`./locales/${language}/${namespace}.json`), 14 | ), 15 | ) 16 | .init(getOptions(lng, ns)); 17 | return i18nInstance; 18 | }; 19 | 20 | export async function useTranslation(lng: any, ns: any, options: any = {}) { 21 | const i18nextInstance = await initI18next(lng, ns); 22 | return { 23 | t: i18nextInstance.getFixedT( 24 | lng, 25 | Array.isArray(ns) ? ns[0] : ns, 26 | options.keyPrefix, 27 | ), 28 | i18n: i18nextInstance, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /app/i18n/settings.ts: -------------------------------------------------------------------------------- 1 | export const fallbackLng = "en"; 2 | export const languages = [fallbackLng, "fr", "es"]; 3 | export const defaultNS = "translation"; 4 | export const cookieName = "i18next"; 5 | 6 | export function getOptions(lng = fallbackLng, ns = defaultNS) { 7 | return { 8 | // debug: true, 9 | supportedLngs: languages, 10 | fallbackLng, 11 | lng, 12 | fallbackNS: defaultNS, 13 | defaultNS, 14 | ns, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /components/chat-box.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslation } from "@/app/i18n/client"; 3 | import { ChatMessage } from "@/lib/ai-completions"; 4 | import ResultDisplayer from "./result-displayer"; 5 | import { useEffect, useState } from "react"; 6 | import { Copy, Sparkles } from "lucide-react"; 7 | import { Button } from "./ui/button"; 8 | import { 9 | Tooltip, 10 | TooltipContent, 11 | TooltipProvider, 12 | TooltipTrigger, 13 | } from "./ui/tooltip"; 14 | import parse from "html-react-parser"; 15 | 16 | interface ChatBoxProps { 17 | lng: string; 18 | messages: ChatMessage[]; 19 | isLoading: boolean; 20 | } 21 | export default function ChatBox(props: ChatBoxProps) { 22 | const lng: any = props.lng; 23 | const { t } = useTranslation(lng, "common"); 24 | const [msg, setMsg] = useState(props.messages); 25 | const [loading, setLoading] = useState(props.isLoading); 26 | 27 | useEffect(() => { 28 | setMsg(props.messages); 29 | setLoading(props.isLoading); 30 | }, [props.messages, props.isLoading]); 31 | 32 | return ( 33 |
34 | {msg.map((m, i) => ( 35 |
39 |

42 | 43 | {t(m.role === "assistant" ? "synapsy-assistant" : "you")} 44 | 45 | {m.role === "assistant" && } 46 |

47 | {i === msg.length - 1 && loading ? ( 48 |

49 | {parse( 50 | m.content 51 | .replaceAll("", "") 52 | .replaceAll("", "") 53 | .replaceAll("", "") 54 | .replaceAll("", "") 55 | .replaceAll("", "") 56 | .replaceAll("\n\n", "
") 57 | .replaceAll("\n\n\n", "\n") 58 | .replaceAll("\n \n", "
") 59 | .replaceAll("\n", "
") 60 | .replaceAll("```html", "") 61 | .replaceAll("```", "") 62 | .replaceAll("

", ""), 63 | )} 64 | 65 |

66 | ) : ( 67 | 74 | )} 75 |
76 | 77 | 78 | 79 | 88 | 89 | 90 |

{t("copy")}

91 |
92 |
93 |
94 |
95 |
96 | ))} 97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /components/complex-gen.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslation } from "@/app/i18n/client"; 3 | import GenerationStep from "@/lib/generation-step"; 4 | import { Check, Loader2, PencilLineIcon } from "lucide-react"; 5 | import { useEffect, useState } from "react"; 6 | 7 | interface ComplexGenItemProps { 8 | lng: string; 9 | steps: GenerationStep[]; 10 | } 11 | 12 | export default function ComplexGenItem(props: ComplexGenItemProps) { 13 | const { t } = useTranslation(props.lng, "common"); 14 | const [steps, setSteps] = useState(props.steps); 15 | 16 | useEffect(() => { 17 | setSteps(props.steps); 18 | }, [props.steps]); 19 | 20 | return ( 21 |
22 |
23 |

24 | 25 | 26 | {t("complex-gen")} 27 |

28 |

{t("complex-gen-desc")}

29 |
30 |
31 | {steps && 32 | steps.map((s, i) => ( 33 | <> 34 | {s.done ? ( 35 | 36 | ) : ( 37 | 38 | )} 39 |

{t(s.i18nname)}

40 | 41 | ))} 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /components/features.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslation } from "@/app/i18n/client"; 3 | import { Check } from "lucide-react"; 4 | 5 | interface Props { 6 | lng: string; 7 | productName: string; 8 | } 9 | export default function PricingFeatures(props: Props) { 10 | const { t } = useTranslation(props.lng, "common"); 11 | if (!props.productName.toLowerCase().includes("synapsy write")) { 12 | return <>; 13 | } 14 | if (props.productName.toLowerCase().includes("premium")) { 15 | return ( 16 |
17 | 18 |

19 | {t("feature-ai")} (20/{t("month")}) 20 |

21 | 22 |

{t("advanced-instructions")}

23 | 24 |

{t("table-generator")}

25 | 26 |

{t("essays")}

27 | 28 |

{t("text-analysis")}

29 | 30 |

{t("variable-editor")}

31 | 32 |
33 | ); 34 | } 35 | 36 | if (props.productName.toLowerCase().includes("pro")) { 37 | return ( 38 |
39 | 40 |

41 | {t("feature-ai")} ({t("unlimited")}) 42 |

43 | 44 |

{t("unlimited-access")}

45 | 46 |

{t("advanced-instructions")}

47 | 48 |

{t("table-generator")}

49 | 50 |

{t("essays")}

51 | 52 |

{t("text-analysis")}

53 | 54 |

{t("variable-editor")}

55 | {props.lng === "fr" && } 56 |
57 | ); 58 | } 59 | 60 | return ( 61 |
62 | 63 |

64 | {t("feature-ai")} (10/{t("month")}) 65 |

66 | 67 |

{t("advanced-instructions")}

68 | 69 |

{t("table-generator")}

70 | 71 |

{t("essays")}

72 | 73 |

{t("text-analysis")}

74 | 75 |

{t("variable-editor")}

76 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | 4 | import { useTranslation } from "@/app/i18n/client"; 5 | import { version } from "@/lib/version"; 6 | import PeyronnetLogo from "./peyronnet-logo"; 7 | 8 | export default function SiteFooter({ 9 | params: { lng }, 10 | }: { 11 | params: { lng: any }; 12 | }) { 13 | const { t } = useTranslation(lng, "common"); 14 | return ( 15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 | 27 | 32 | 33 | 38 | 39 | 44 | 45 | 50 | 55 |
56 |
57 | ); 58 | } 59 | 60 | interface FooterLinkProps { 61 | title: string; 62 | description: string; 63 | link: string; 64 | } 65 | 66 | function FooterLink(props: FooterLinkProps) { 67 | return ( 68 | 72 |

73 | {props.title} 74 |

75 |

{props.description}

76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /components/format-selector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Check, ChevronsUpDown } from "lucide-react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { Button } from "@/components/ui/button"; 8 | import { 9 | Command, 10 | CommandEmpty, 11 | CommandGroup, 12 | CommandInput, 13 | CommandItem, 14 | CommandList, 15 | } from "@/components/ui/command"; 16 | import { 17 | Popover, 18 | PopoverContent, 19 | PopoverTrigger, 20 | } from "@/components/ui/popover"; 21 | import formats from "@/lib/formats"; 22 | import { useTranslation } from "@/app/i18n/client"; 23 | 24 | export function FormatSelector(props: { lng: string; setVal: Function }) { 25 | const [open, setOpen] = React.useState(false); 26 | const [value, setValue] = React.useState(""); 27 | const { t } = useTranslation(props.lng, "common"); 28 | 29 | let vals: [{ val: string; text: string }?] = []; 30 | formats.forEach((el) => { 31 | el.options.forEach((opt) => { 32 | vals.push({ 33 | val: t(opt.text).toLowerCase() + opt.val, 34 | text: t(opt.text), 35 | }); 36 | }); 37 | }); 38 | 39 | return ( 40 | 41 | 42 | 53 | 54 | 55 | 56 | 57 | {t("no-formats")} 58 | 59 | {formats.map((cat) => ( 60 | 61 | {cat.options.map((f) => ( 62 | { 66 | setValue(currentValue === value ? "" : currentValue); 67 | props.setVal(f.val); 68 | setOpen(false); 69 | }} 70 | > 71 | 79 | {t(f.text)} 80 | 81 | ))} 82 | 83 | ))} 84 | 85 | 86 | 87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /components/generation-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipProvider, 8 | TooltipTrigger, 9 | } from "./ui/tooltip"; 10 | import { HistoryItem, removeFromHistory } from "@/lib/history"; 11 | import { useTranslation } from "@/app/i18n/client"; 12 | import { Calendar, MoreVertical } from "lucide-react"; 13 | import { 14 | DropdownMenu, 15 | DropdownMenuContent, 16 | DropdownMenuItem, 17 | DropdownMenuTrigger, 18 | } from "./ui/dropdown-menu"; 19 | import { Button } from "./ui/button"; 20 | 21 | export function GenerationItem(props: { 22 | item: HistoryItem; 23 | lng: string; 24 | id: number; 25 | refresh: Function; 26 | }) { 27 | const { t } = useTranslation(props.lng, "common"); 28 | function getRandomGradient() { 29 | const gradients = [ 30 | "bg-linear-to-r from-yellow-400 to-pink-500", 31 | "bg-linear-to-r from-green-400 to-blue-500", 32 | "bg-linear-to-r from-purple-400 to-red-500", 33 | "bg-linear-to-r from-pink-400 to-blue-500", 34 | "bg-linear-to-r from-indigo-500 to-purple-600", 35 | "bg-linear-to-r from-pink-500 to-indigo-600", 36 | "bg-linear-to-r from-red-500 to-yellow-500", 37 | ]; 38 | return gradients[Math.floor(Math.random() * gradients.length)]; 39 | } 40 | 41 | return ( 42 | 43 | 44 | 45 |
{}} 47 | className="m-2 flex w-[380px] flex-col overflow-hidden rounded-md border border-slate-200 bg-white shadow-xs transition hover:-translate-y-2 hover:shadow-md dark:border-slate-700 dark:bg-slate-900 sm:w-[360px]" 48 | > 49 | 56 | 57 | 61 | 62 | {new Date(props.item.date).toLocaleString()} 63 | 64 | 65 | 69 | {props.item.prompt.length > 30 70 | ? props.item.prompt.substring(0, 30) + "..." 71 | : props.item.prompt} 72 | 73 | 74 | 75 | 78 | 79 | 80 | {props.item.template !== "ideas" && 81 | props.item.template !== "table" && 82 | props.item.template !== "ph_visual_outline" ? ( 83 | 86 | {t("edit")} 87 | 88 | ) : ( 89 | <> 90 | )} 91 | { 93 | removeFromHistory(props.id); 94 | props.refresh(); 95 | }} 96 | > 97 | {t("delete")} 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 | {props.item.prompt} 107 | 108 |
109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /components/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Loader2 } from "lucide-react"; 3 | 4 | export default function LoadingUI() { 5 | return ( 6 |
7 | 8 |

Loading...

9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components/selectors/link-selector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "@/lib/utils"; 3 | import { useEditor } from "novel"; 4 | import { Check, Trash } from "lucide-react"; 5 | import { useEffect, useRef } from "react"; 6 | import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; 7 | import { Button } from "../ui/button"; 8 | import { useTranslation } from "@/app/i18n/client"; 9 | 10 | export function isValidUrl(url: string) { 11 | try { 12 | new URL(url); 13 | return true; 14 | } catch (e) { 15 | return false; 16 | } 17 | } 18 | export function getUrlFromString(str: string) { 19 | if (isValidUrl(str)) return str; 20 | try { 21 | if (str.includes(".") && !str.includes(" ")) { 22 | return new URL(`https://${str}`).toString(); 23 | } 24 | } catch (e) { 25 | return null; 26 | } 27 | } 28 | interface LinkSelectorProps { 29 | open: boolean; 30 | onOpenChange: (open: boolean) => void; 31 | lng: string; 32 | } 33 | 34 | export const LinkSelector = ({ 35 | open, 36 | onOpenChange, 37 | lng, 38 | }: LinkSelectorProps) => { 39 | const { t } = useTranslation(lng, "common"); 40 | const inputRef = useRef(null); 41 | const { editor } = useEditor(); 42 | 43 | // Autofocus on input by default 44 | useEffect(() => { 45 | inputRef.current && inputRef.current?.focus(); 46 | }); 47 | if (!editor) return null; 48 | 49 | return ( 50 | 51 | 52 | 62 | 63 | 64 |
{ 66 | const target = e.currentTarget as HTMLFormElement; 67 | e.preventDefault(); 68 | const input = target[0] as HTMLInputElement; 69 | const url = getUrlFromString(input.value); 70 | url && editor.chain().focus().setLink({ href: url }).run(); 71 | }} 72 | className="flex p-1" 73 | > 74 | 81 | {editor.getAttributes("link").href ? ( 82 | 93 | ) : ( 94 | 97 | )} 98 |
99 |
100 |
101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /components/selectors/math-selector.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { SigmaIcon } from "lucide-react"; 3 | import { useEditor } from "novel"; 4 | import { Button } from "../ui/button"; 5 | 6 | export const MathSelector = () => { 7 | const { editor } = useEditor(); 8 | 9 | if (!editor) return null; 10 | 11 | return ( 12 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /components/selectors/text-buttons.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { EditorBubbleItem, useEditor } from "novel"; 5 | import { 6 | BoldIcon, 7 | ItalicIcon, 8 | UnderlineIcon, 9 | StrikethroughIcon, 10 | CodeIcon, 11 | } from "lucide-react"; 12 | import type { SelectorItem } from "./node-selector"; 13 | import { Button } from "../ui/button"; 14 | 15 | export const TextButtons = () => { 16 | const { editor } = useEditor(); 17 | if (!editor) return null; 18 | const items: SelectorItem[] = [ 19 | { 20 | name: "bold", 21 | translation: "bold", 22 | isActive: (editor) => editor.isActive("bold"), 23 | command: (editor) => editor.chain().focus().toggleBold().run(), 24 | icon: BoldIcon, 25 | }, 26 | { 27 | name: "italic", 28 | translation: "italic", 29 | isActive: (editor) => editor.isActive("italic"), 30 | command: (editor) => editor.chain().focus().toggleItalic().run(), 31 | icon: ItalicIcon, 32 | }, 33 | { 34 | name: "underline", 35 | translation: "underline", 36 | isActive: (editor) => editor.isActive("underline"), 37 | command: (editor) => editor.chain().focus().toggleUnderline().run(), 38 | icon: UnderlineIcon, 39 | }, 40 | { 41 | name: "strike", 42 | translation: "strike", 43 | isActive: (editor) => editor.isActive("strike"), 44 | command: (editor) => editor.chain().focus().toggleStrike().run(), 45 | icon: StrikethroughIcon, 46 | }, 47 | { 48 | name: "code", 49 | translation: "code-inline", 50 | isActive: (editor) => editor.isActive("code"), 51 | command: (editor) => editor.chain().focus().toggleCode().run(), 52 | icon: CodeIcon, 53 | }, 54 | ]; 55 | return ( 56 |
57 | {items.map((item, index) => ( 58 | { 61 | item.command(editor); 62 | }} 63 | > 64 | 71 | 72 | ))} 73 |
74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /components/spotlight.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useRef, useState, useEffect } from "react"; 4 | import MousePosition from "@/utils/mouse-position"; 5 | 6 | type SpotlightProps = { 7 | children: React.ReactNode; 8 | className?: string; 9 | }; 10 | 11 | export default function Spotlight({ 12 | children, 13 | className = "", 14 | }: SpotlightProps) { 15 | const containerRef = useRef(null); 16 | const mousePosition = MousePosition(); 17 | const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); 18 | const containerSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); 19 | const [boxes, setBoxes] = useState>([]); 20 | 21 | useEffect(() => { 22 | containerRef.current && 23 | setBoxes( 24 | Array.from(containerRef.current.children).map( 25 | (el) => el as HTMLElement, 26 | ), 27 | ); 28 | }, []); 29 | 30 | useEffect(() => { 31 | initContainer(); 32 | window.addEventListener("resize", initContainer); 33 | 34 | return () => { 35 | window.removeEventListener("resize", initContainer); 36 | }; 37 | }, [setBoxes]); 38 | 39 | useEffect(() => { 40 | onMouseMove(); 41 | }, [mousePosition]); 42 | 43 | const initContainer = () => { 44 | if (containerRef.current) { 45 | containerSize.current.w = containerRef.current.offsetWidth; 46 | containerSize.current.h = containerRef.current.offsetHeight; 47 | } 48 | }; 49 | 50 | const onMouseMove = () => { 51 | if (containerRef.current) { 52 | const rect = containerRef.current.getBoundingClientRect(); 53 | const { w, h } = containerSize.current; 54 | const x = mousePosition.x - rect.left; 55 | const y = mousePosition.y - rect.top; 56 | const inside = x < w && x > 0 && y < h && y > 0; 57 | if (inside) { 58 | mouse.current.x = x; 59 | mouse.current.y = y; 60 | boxes.forEach((box) => { 61 | const boxX = 62 | -(box.getBoundingClientRect().left - rect.left) + mouse.current.x; 63 | const boxY = 64 | -(box.getBoundingClientRect().top - rect.top) + mouse.current.y; 65 | box.style.setProperty("--mouse-x", `${boxX}px`); 66 | box.style.setProperty("--mouse-y", `${boxY}px`); 67 | }); 68 | } 69 | } 70 | }; 71 | 72 | return ( 73 |
74 | {children} 75 |
76 | ); 77 | } 78 | 79 | type SpotlightCardProps = { 80 | children: React.ReactNode; 81 | className?: string; 82 | }; 83 | 84 | export function SpotlightCard({ 85 | children, 86 | className = "", 87 | }: SpotlightCardProps) { 88 | return ( 89 |
92 | {children} 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /components/system-template-creator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslation } from "@/app/i18n/client"; 3 | import { Settings } from "@/lib/settings"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogFooter, 9 | DialogHeader, 10 | DialogTitle, 11 | DialogTrigger, 12 | } from "./ui/dialog"; 13 | import { Button } from "./ui/button"; 14 | import { Textarea } from "./ui/textarea"; 15 | import { Input } from "./ui/input"; 16 | import { Close } from "@radix-ui/react-dialog"; 17 | import { useState } from "react"; 18 | 19 | export default function SystemTemplateCreator(props: { 20 | lng: string; 21 | setTemplates: Function; 22 | }) { 23 | const { t } = useTranslation(props.lng, "common"); 24 | const [name, setName] = useState(""); 25 | const [prompt, setPrompt] = useState(""); 26 | let s: Settings = { key: "" }; 27 | if (typeof window !== "undefined") { 28 | s = JSON.parse(localStorage.getItem("synapsy_settings") ?? "{}"); 29 | s.models ??= ["gpt-3.5-turbo"]; 30 | s.system_templates ??= []; 31 | localStorage.setItem("synapsy_settings", JSON.stringify(s)); 32 | } 33 | 34 | function createTemplate() { 35 | s.system_templates?.push({ name: name, prompt: prompt }); 36 | localStorage.setItem("synapsy_settings", JSON.stringify(s)); 37 | props.setTemplates([...(s.system_templates ?? [])]); 38 | setName(""); 39 | setPrompt(""); 40 | } 41 | return ( 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | {t("system-template-creator")} 50 | 51 | {t("system-template-creator-desc")} 52 | 53 | 54 |
55 |

{t("name")}

56 | setName(v.target.value)} /> 57 |

{t("prompt")}

58 |