├── .env.example
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── actions
└── export.ts
├── app
├── (app)
│ ├── layout.tsx
│ └── map
│ │ ├── head.tsx
│ │ └── page.tsx
├── globals.css
├── head.tsx
├── layout.tsx
├── page.module.css
├── page.tsx
└── providers.tsx
├── components
├── Home
│ ├── ActionButtons.tsx
│ ├── Features.tsx
│ ├── Footer.tsx
│ ├── Main.tsx
│ ├── Navigation.tsx
│ └── TryItButton.tsx
├── Layout
│ └── AppLayout
│ │ ├── AppLayout.tsx
│ │ ├── Menu.tsx
│ │ ├── OpenAIAKDialog.tsx
│ │ └── Top.tsx
├── Map
│ ├── Edges
│ │ └── CustomizableEdge.tsx
│ ├── Flow.tsx
│ ├── FlowWrapper.tsx
│ ├── Generator
│ │ └── Generator.tsx
│ ├── Index.tsx
│ ├── Nodes
│ │ ├── AIContentModal.tsx
│ │ ├── RootNode.tsx
│ │ └── TopicNode.tsx
│ ├── Panel
│ │ └── TopPanel
│ │ │ ├── Theme.tsx
│ │ │ └── TopPanel.tsx
│ └── Plugins
│ │ └── DataSaver.tsx
├── Providers
│ └── Analytics.tsx
└── ui
│ ├── AspectRadio.tsx
│ ├── BlockContainer.tsx
│ ├── Button.tsx
│ ├── Dialog.tsx
│ ├── DropdownMenu.tsx
│ ├── Icon.tsx
│ ├── Input.tsx
│ ├── Label.tsx
│ ├── Loader.tsx
│ ├── MenuBar.tsx
│ ├── NodeHeader.tsx
│ ├── Popover.tsx
│ ├── Select.tsx
│ ├── Separator.tsx
│ ├── Textarea.tsx
│ ├── Toast.tsx
│ ├── Toaster.tsx
│ ├── ToggleGroup.tsx
│ ├── ToggleInput.tsx
│ ├── ToggleTextarea.tsx
│ └── Tooltip.tsx
├── data
├── defaultEdges.ts
├── defaultNodes.ts
└── defaultPalettes.ts
├── hooks
└── use-toast.ts
├── next.config.js
├── package.json
├── pages
└── api
│ └── ideas.ts
├── postcss.config.js
├── process-env.d.ts
├── public
├── app-img.jpg
├── app-map.jpg
├── favicon.ico
├── next.svg
├── thirteen.svg
└── vercel.svg
├── stores
└── mapStore.ts
├── styles
└── globals.css
├── tailwind.config.js
├── tsconfig.json
├── utils
├── api
│ └── suggestions.ts
├── blob.ts
├── classnames.ts
├── constants
│ ├── export.ts
│ ├── headerTypes.ts
│ ├── modes.ts
│ ├── openai.ts
│ └── questions.ts
├── data.ts
├── filesystem.ts
├── id.ts
├── json.ts
├── node.ts
├── openai
│ ├── client.ts
│ └── topics.ts
├── providers
│ └── ConfigurationProvider.tsx
├── storage.ts
└── types.ts
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_OPENAI_API_KEY=""
2 | NEXT_PUBLIC_OPENAI_COMPLETION_MODEL="gpt-3.5-turbo"
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.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 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | .vscode
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "printWidth": 120,
4 | "semi": true,
5 | "singleQuote": true,
6 | "tabWidth": 2,
7 | "trailingComma": "all",
8 | "useTabs": true,
9 | "jsxSingleQuote": true
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Fernando Palacios
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mentalist AI
2 |
3 | An open source map mental application built using Next.js 13, Open AI.
4 |
5 | > **Warning**
6 | > This app is a work in progress. I'm building this in public. You can follow the progress on Twitter [@fernandops26](https://twitter.com/fernandops26).
7 |
8 | ## Demo
9 |
10 | 
11 |
12 | 
13 |
14 | ## About this project
15 |
16 | Mentalist is a web application built with Next.js 13 and integrated with OpenAI. The goal of Mentalist is to help users generate ideas and think creatively by providing a starting point of a topic.
17 |
18 | ## Getting started
19 |
20 | To get started, clone the repository and install the dependencies:
21 |
22 | ```bash
23 | git clone git@github.com:fernandops26/mentalist-ai.git
24 |
25 | cd mentalist-ai/
26 |
27 | yarn install
28 | ```
29 |
30 | Following installation, create a `.env.local` file in the root of the project and add the following environment variables:
31 |
32 | ```bash
33 | NEXT_PUBLIC_OPENAI_API_KEY=""
34 | NEXT_PUBLIC_OPENAI_COMPLETION_MODEL="gpt-3.5-turbo"
35 | ```
36 |
37 | You can get your OpenAI API key by signing up for an account [here](https://platform.openai.com/).
38 |
39 | Once you have created your `.env.local` file, you can run the development server:
40 |
41 | ```bash
42 | yarn dev
43 | ```
44 |
45 | ## Features
46 |
47 | Coming soon
48 |
49 | ## Roadmap
50 |
51 | Coming soon
52 |
53 | ## License
54 |
55 | Licensed under the [MIT license](https://github.com/fernandops26/mentalist-ai/blob/main/LICENSE).
56 |
--------------------------------------------------------------------------------
/actions/export.ts:
--------------------------------------------------------------------------------
1 | import { palettes } from '@/data/defaultPalettes';
2 | import { loadFromJSON, saveAsJSON } from '@/utils/data';
3 | import { readFullContentObj, restoreProject, updateConfig } from '@/utils/storage';
4 |
5 | export const actionSaveFileToDisk = async () => {
6 | const data = readFullContentObj();
7 |
8 | await saveAsJSON('Mentalist', data.version, data.map, data.config);
9 | };
10 |
11 | export const actionLoadFileFromDisk = async () => {
12 | try {
13 | const data = await loadFromJSON();
14 |
15 | if (data.config?.palette == null) {
16 | data.config = {
17 | ...data.config,
18 | palette: palettes[0].id,
19 | };
20 | }
21 |
22 | restoreProject(data);
23 |
24 | return true;
25 | } catch (error) {
26 | console.error(error);
27 | }
28 |
29 | return false;
30 | };
31 |
--------------------------------------------------------------------------------
/app/(app)/layout.tsx:
--------------------------------------------------------------------------------
1 | import AppLayout from '@/components/Layout/AppLayout/AppLayout';
2 | import React from 'react';
3 |
4 | interface LayoutProps {
5 | children: React.ReactNode;
6 | }
7 | export default function Layout({ children }: LayoutProps) {
8 | return {children};
9 | }
10 |
--------------------------------------------------------------------------------
/app/(app)/map/head.tsx:
--------------------------------------------------------------------------------
1 | export default function Head() {
2 | return (
3 | <>
4 | Your Content Ideation Map | Mentalist
5 |
6 |
11 |
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/app/(app)/map/page.tsx:
--------------------------------------------------------------------------------
1 | import Map from '@/components/Map/Index';
2 | import Link from 'next/link';
3 |
4 | export default async function MapPage() {
5 | return (
6 |
7 |
14 |
15 |
16 |
Hi mentalister! 👋
17 |
18 | We are working to expand the use of mentalist-ai on mobile view, for now you can use it in browser
19 |
20 |
21 |
22 | Follow on{' '}
23 |
29 | Twitter
30 | {' '}
31 | for updates
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --max-width: 1100px;
3 | --border-radius: 12px;
4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono',
5 | 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
6 |
7 | --foreground-rgb: 0, 0, 0;
8 | --background-start-rgb: 214, 219, 220;
9 | --background-end-rgb: 255, 255, 255;
10 |
11 | --primary-glow: conic-gradient(
12 | from 180deg at 50% 50%,
13 | #16abff33 0deg,
14 | #0885ff33 55deg,
15 | #54d6ff33 120deg,
16 | #0071ff33 160deg,
17 | transparent 360deg
18 | );
19 | --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
20 |
21 | --tile-start-rgb: 239, 245, 249;
22 | --tile-end-rgb: 228, 232, 233;
23 | --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080);
24 |
25 | --callout-rgb: 238, 240, 241;
26 | --callout-border-rgb: 172, 175, 176;
27 | --card-rgb: 180, 185, 188;
28 | --card-border-rgb: 131, 134, 135;
29 | }
30 |
31 | @media (prefers-color-scheme: dark) {
32 | :root {
33 | --foreground-rgb: 255, 255, 255;
34 | --background-start-rgb: 0, 0, 0;
35 | --background-end-rgb: 0, 0, 0;
36 |
37 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
38 | --secondary-glow: linear-gradient(to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3));
39 |
40 | --tile-start-rgb: 2, 13, 46;
41 | --tile-end-rgb: 2, 5, 19;
42 | --tile-border: conic-gradient(#ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80);
43 |
44 | --callout-rgb: 20, 20, 20;
45 | --callout-border-rgb: 108, 108, 108;
46 | --card-rgb: 100, 100, 100;
47 | --card-border-rgb: 200, 200, 200;
48 | }
49 | }
50 |
51 | * {
52 | box-sizing: border-box;
53 | padding: 0;
54 | margin: 0;
55 | }
56 |
57 | html,
58 | body {
59 | max-width: 100vw;
60 | overflow-x: hidden;
61 | }
62 |
63 | body {
64 | color: rgb(var(--foreground-rgb));
65 | background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
66 | }
67 |
68 | a {
69 | color: inherit;
70 | text-decoration: none;
71 | }
72 |
73 | @media (prefers-color-scheme: dark) {
74 | html {
75 | color-scheme: dark;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/head.tsx:
--------------------------------------------------------------------------------
1 | export default function Head() {
2 | return (
3 | <>
4 | Your Ultimate Content Ideation Tool | Mentalist
5 |
6 |
11 |
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css';
2 | import Providers from './providers';
3 |
4 | export default function RootLayout({ children }: { children: React.ReactNode }) {
5 | return (
6 |
7 | {/*
8 | will contain the components returned by the nearest parent
9 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
10 | */}
11 |
13 | {children}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/app/page.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .description {
11 | display: inherit;
12 | justify-content: inherit;
13 | align-items: inherit;
14 | font-size: 0.85rem;
15 | max-width: var(--max-width);
16 | width: 100%;
17 | z-index: 2;
18 | font-family: var(--font-mono);
19 | }
20 |
21 | .description a {
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 | gap: 0.5rem;
26 | }
27 |
28 | .description p {
29 | position: relative;
30 | margin: 0;
31 | padding: 1rem;
32 | background-color: rgba(var(--callout-rgb), 0.5);
33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34 | border-radius: var(--border-radius);
35 | }
36 |
37 | .code {
38 | font-weight: 700;
39 | font-family: var(--font-mono);
40 | }
41 |
42 | .grid {
43 | display: grid;
44 | grid-template-columns: repeat(3, minmax(33%, auto));
45 | width: var(--max-width);
46 | max-width: 100%;
47 | }
48 |
49 | .card {
50 | padding: 1rem 1.2rem;
51 | border-radius: var(--border-radius);
52 | background: rgba(var(--card-rgb), 0);
53 | border: 1px solid rgba(var(--card-border-rgb), 0);
54 | transition: background 200ms, border 200ms;
55 | }
56 |
57 | .card span {
58 | display: inline-block;
59 | transition: transform 200ms;
60 | }
61 |
62 | .card h2 {
63 | font-weight: 600;
64 | margin-bottom: 0.7rem;
65 | }
66 |
67 | .card p {
68 | margin: 0;
69 | opacity: 0.6;
70 | font-size: 0.9rem;
71 | line-height: 1.5;
72 | max-width: 34ch;
73 | }
74 |
75 | .center {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | position: relative;
80 | padding: 4rem 0;
81 | }
82 |
83 | .center::before {
84 | background: var(--secondary-glow);
85 | border-radius: 50%;
86 | width: 480px;
87 | height: 360px;
88 | margin-left: -400px;
89 | }
90 |
91 | .center::after {
92 | background: var(--primary-glow);
93 | width: 240px;
94 | height: 180px;
95 | z-index: -1;
96 | }
97 |
98 | .center::before,
99 | .center::after {
100 | content: '';
101 | left: 50%;
102 | position: absolute;
103 | filter: blur(45px);
104 | transform: translateZ(0);
105 | }
106 |
107 | .logo,
108 | .thirteen {
109 | position: relative;
110 | }
111 |
112 | .thirteen {
113 | display: flex;
114 | justify-content: center;
115 | align-items: center;
116 | width: 75px;
117 | height: 75px;
118 | padding: 25px 10px;
119 | margin-left: 16px;
120 | transform: translateZ(0);
121 | border-radius: var(--border-radius);
122 | overflow: hidden;
123 | box-shadow: 0px 2px 8px -1px #0000001a;
124 | }
125 |
126 | .thirteen::before,
127 | .thirteen::after {
128 | content: '';
129 | position: absolute;
130 | z-index: -1;
131 | }
132 |
133 | /* Conic Gradient Animation */
134 | .thirteen::before {
135 | animation: 6s rotate linear infinite;
136 | width: 200%;
137 | height: 200%;
138 | background: var(--tile-border);
139 | }
140 |
141 | /* Inner Square */
142 | .thirteen::after {
143 | inset: 0;
144 | padding: 1px;
145 | border-radius: var(--border-radius);
146 | background: linear-gradient(to bottom right, rgba(var(--tile-start-rgb), 1), rgba(var(--tile-end-rgb), 1));
147 | background-clip: content-box;
148 | }
149 |
150 | /* Enable hover only on non-touch devices */
151 | @media (hover: hover) and (pointer: fine) {
152 | .card:hover {
153 | background: rgba(var(--card-rgb), 0.1);
154 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
155 | }
156 |
157 | .card:hover span {
158 | transform: translateX(4px);
159 | }
160 | }
161 |
162 | @media (prefers-reduced-motion) {
163 | .thirteen::before {
164 | animation: none;
165 | }
166 |
167 | .card:hover span {
168 | transform: none;
169 | }
170 | }
171 |
172 | /* Mobile and Tablet */
173 | @media (max-width: 1023px) {
174 | .content {
175 | padding: 4rem;
176 | }
177 |
178 | .grid {
179 | grid-template-columns: 1fr;
180 | margin-bottom: 120px;
181 | max-width: 320px;
182 | text-align: center;
183 | }
184 |
185 | .card {
186 | padding: 1rem 2.5rem;
187 | }
188 |
189 | .card h2 {
190 | margin-bottom: 0.5rem;
191 | }
192 |
193 | .center {
194 | padding: 8rem 0 6rem;
195 | }
196 |
197 | .center::before {
198 | transform: none;
199 | height: 300px;
200 | }
201 |
202 | .description {
203 | font-size: 0.8rem;
204 | }
205 |
206 | .description a {
207 | padding: 1rem;
208 | }
209 |
210 | .description p,
211 | .description div {
212 | display: flex;
213 | justify-content: center;
214 | position: fixed;
215 | width: 100%;
216 | }
217 |
218 | .description p {
219 | align-items: center;
220 | inset: 0 0 auto;
221 | padding: 2rem 1rem 1.4rem;
222 | border-radius: 0;
223 | border: none;
224 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
225 | background: linear-gradient(to bottom, rgba(var(--background-start-rgb), 1), rgba(var(--callout-rgb), 0.5));
226 | background-clip: padding-box;
227 | backdrop-filter: blur(24px);
228 | }
229 |
230 | .description div {
231 | align-items: flex-end;
232 | pointer-events: none;
233 | inset: auto 0 0;
234 | padding: 2rem;
235 | height: 200px;
236 | background: linear-gradient(to bottom, transparent 0%, rgb(var(--background-end-rgb)) 40%);
237 | z-index: 1;
238 | }
239 | }
240 |
241 | @media (prefers-color-scheme: dark) {
242 | .vercelLogo {
243 | filter: invert(1);
244 | }
245 |
246 | .logo,
247 | .thirteen img {
248 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
249 | }
250 | }
251 |
252 | @keyframes rotate {
253 | from {
254 | transform: rotate(360deg);
255 | }
256 | to {
257 | transform: rotate(0deg);
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Footer } from '@/components/Home/Footer';
2 | import { Navigation } from '@/components/Home/Navigation';
3 | import Main from '@/components/Home/Main';
4 | import Features from '@/components/Home/Features';
5 |
6 | export default function Home() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import FlowWrapper from '@/components/Map/FlowWrapper';
4 | import { AnalyticsWrapper } from '@/components/Providers/Analytics';
5 | import { Toaster } from '@/components/ui/Toaster';
6 | import { TooltipProvider } from '@/components/ui/Tooltip';
7 | import { ConfigurationProvider } from '@/utils/providers/ConfigurationProvider';
8 |
9 | interface ProvidersProps {
10 | children: React.ReactNode;
11 | }
12 |
13 | export default function Providers({ children }: ProvidersProps) {
14 | return (
15 | <>
16 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 |
24 | >
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/Home/ActionButtons.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Button } from '@/components/ui/Button';
3 |
4 | export default function ActionButtons() {
5 | const goToMap = () => {
6 | window.open('/map', '_self');
7 | };
8 |
9 | const goToGithub = () => {
10 | window.open('https://github.com/fernandops26/mentalist-ai', '_blank', 'noopener');
11 | };
12 |
13 | return (
14 |
15 |
16 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/components/Home/Features.tsx:
--------------------------------------------------------------------------------
1 | import IconComponent from '../ui/Icon';
2 | import TryItButton from './TryItButton';
3 |
4 | const features = [
5 | {
6 | imgSrc: 'https://tailus.io/sources/blocks/stacked/preview/images/avatars/burger.png',
7 | icon: 'idea',
8 | imgAlt: 'burger illustration',
9 | title: 'Generate content ideas',
10 | description: 'Easily generate ideas for content related to any social media topic',
11 | },
12 | {
13 | imgSrc: 'https://tailus.io/sources/blocks/stacked/preview/images/avatars/trowel.png',
14 | imgAlt: 'trowel illustration',
15 | icon: 'candy',
16 | title: 'One-Click Title Suggestions',
17 | description: 'Quickly find engaging and effective titles for your content',
18 | },
19 | {
20 | imgSrc: 'https://tailus.io/sources/blocks/stacked/preview/images/avatars/package-delivery.png',
21 | imgAlt: 'package delivery illustration',
22 | icon: 'twitter',
23 | title: 'Customizable Content Approach',
24 | description:
25 | 'Generate the perfect title for your YouTube, blog, or Twitter content to help it stand out and get noticed',
26 | },
27 | {
28 | imgSrc: 'https://tailus.io/sources/blocks/stacked/preview/images/avatars/metal.png',
29 | imgAlt: 'metal illustration',
30 | icon: 'code',
31 | title: 'Developed with Next.js 13',
32 | description:
33 | 'Take advantage of the latest features and improvements in Next.js 13 for faster and more efficient development',
34 | },
35 | ];
36 |
37 | export default function Features() {
38 | return (
39 |
40 |
41 |
42 |
Features
43 |
44 |
45 | {features.map((feature) => (
46 |
47 | ))}
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | interface FeatureProps {
55 | icon: string;
56 | imgAlt: string;
57 | title: string;
58 | description: string;
59 | }
60 | function Feature({ icon, imgAlt, title, description }: FeatureProps) {
61 | return (
62 |
63 |
64 |
65 |
66 |
67 |
{title}
68 |
{description}
69 |
70 |
71 | {/*
75 |
76 | */}
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/components/Home/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import IconComponent from '../ui/Icon';
3 |
4 | export function Footer() {
5 | return (
6 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/components/Home/Main.tsx:
--------------------------------------------------------------------------------
1 | import { AspectRatio } from '@/components/ui/AspectRadio';
2 | import ActionButtons from './ActionButtons';
3 |
4 | export default function Main() {
5 | return (
6 |
7 |
8 |
9 |
Your Ultimate Content Ideation Tool
10 |
11 | Easily Create Mind Maps and Generate Content and Title Ideas Based on Topics
12 |
13 |
16 |
17 |
18 |
Use your own OpenAI api key
19 |
20 |
21 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/components/Home/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Link from 'next/link';
3 | import IconComponent from '../ui/Icon';
4 |
5 | interface MainNavProps {
6 | children?: React.ReactNode;
7 | }
8 |
9 | export function Navigation({ children }: MainNavProps) {
10 | return (
11 |
12 |
13 |
14 | Mentalist
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/Home/TryItButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Button } from '../ui/Button';
3 |
4 | export default function TryItButton() {
5 | const goToMap = () => {
6 | window.open('/map', '_self');
7 | };
8 |
9 | return (
10 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/components/Layout/AppLayout/AppLayout.tsx:
--------------------------------------------------------------------------------
1 | import Top from './Top';
2 |
3 | interface AppLayoutProps {
4 | children: React.ReactNode;
5 | }
6 |
7 | export default function AppLayout({ children }: AppLayoutProps) {
8 | return (
9 |
10 |
11 |
{children}
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/components/Layout/AppLayout/Menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { actionLoadFileFromDisk, actionSaveFileToDisk } from '@/actions/export';
4 | import IconComponent from '@/components/ui/Icon';
5 | import {
6 | Menubar,
7 | MenubarContent,
8 | MenubarItem,
9 | MenubarMenu,
10 | MenubarSeparator,
11 | MenubarTrigger,
12 | } from '@/components/ui/MenuBar';
13 | import { useToast } from '@/hooks/use-toast';
14 | import useMapStore from '@/stores/mapStore';
15 | import Link from 'next/link';
16 | import { useState } from 'react';
17 | import { OpenAIAKDialog } from './OpenAIAKDialog';
18 |
19 | export function Menu() {
20 | const { toast } = useToast();
21 | const [open, setOpen] = useState(false);
22 |
23 | const loadFromStorage = useMapStore((s) => s.loadFromStorage);
24 |
25 | const onExport = async () => {
26 | await actionSaveFileToDisk();
27 |
28 | toast({
29 | title: 'Map successfully exported',
30 | description: 'Your mind map has been exported correctly.',
31 | });
32 | };
33 |
34 | const onLoad = async () => {
35 | const loaded = await actionLoadFileFromDisk();
36 |
37 | if (!loaded) {
38 | return;
39 | }
40 |
41 | loadFromStorage();
42 |
43 | toast({
44 | title: 'Map successfully loaded',
45 | description: 'Your mind map has been loaded correctly.',
46 | });
47 | };
48 |
49 | return (
50 |
51 |
52 |
53 |
54 |
55 |
56 | setOpen(true)}> Manage OpenAI ApiKey
57 |
58 |
59 | Open
60 | Export
61 |
62 |
63 |
64 | Follow Updates
65 | {' '}
66 |
67 |
68 |
74 | Github
75 | {' '}
76 |
77 |
78 |
79 | {open && }
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/components/Layout/AppLayout/OpenAIAKDialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@/components/ui/Button';
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogDescription,
8 | DialogFooter,
9 | DialogHeader,
10 | DialogTitle,
11 | } from '@/components/ui/Dialog';
12 |
13 | import { Input } from '@/components/ui/Input';
14 | import { Label } from '@/components/ui/Label';
15 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select';
16 | import { AvailableModel, AVAILABLE_MODELS, isAvailableModel } from '@/utils/constants/openai';
17 |
18 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider';
19 | import React, { useState } from 'react';
20 |
21 | interface OpenAIAKDialogProps {
22 | isOpen: boolean;
23 | openChange: (isOpen: boolean) => void;
24 | }
25 |
26 | export const OpenAIAKDialog = ({ isOpen, openChange }: OpenAIAKDialogProps) => {
27 | const { token, updateToken, model, updateModel } = useConfiguration();
28 |
29 | const [apiKey, setApiKey] = useState(() => token ?? '');
30 | const [apiModel, setApiModel] = useState(() => model);
31 |
32 | const handleSave = () => {
33 | updateToken(apiKey);
34 | updateModel(apiModel);
35 | openChange(false);
36 | };
37 |
38 | return (
39 |
87 | );
88 | };
89 |
--------------------------------------------------------------------------------
/components/Layout/AppLayout/Top.tsx:
--------------------------------------------------------------------------------
1 | import IconComponent from '@/components/ui/Icon';
2 | import Link from 'next/link';
3 | import { Menu } from './Menu';
4 |
5 | export default function Top() {
6 | return (
7 |
8 |
9 |
10 |
11 |
Mentalist
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/Map/Edges/CustomizableEdge.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { EdgeProps, getBezierPath } from 'reactflow';
3 |
4 | export default function CustomizableEdge({
5 | id,
6 | sourceX,
7 | sourceY,
8 | targetX,
9 | targetY,
10 | sourcePosition,
11 | targetPosition,
12 | style = {},
13 | data,
14 | markerEnd,
15 | }: EdgeProps) {
16 | const [edgePath] = getBezierPath({
17 | sourceX,
18 | sourceY,
19 | sourcePosition,
20 | targetX,
21 | targetY,
22 | targetPosition,
23 | });
24 |
25 | return (
26 | <>
27 |
28 |
29 |
30 |
31 | >
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/components/Map/Flow.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import 'reactflow/dist/style.css';
4 |
5 | import React, { useEffect, useRef } from 'react';
6 | import ReactFlow, { Background, Controls, MiniMap } from 'reactflow';
7 | import useMapStore, { RFState } from '@/stores/mapStore';
8 | import { shallow } from 'zustand/shallow';
9 | import { nodeTypes } from '@/data/defaultNodes';
10 | import { edgeTypes } from '@/data/defaultEdges';
11 | import DataSaver from './Plugins/DataSaver';
12 | import TopPanel from './Panel/TopPanel/TopPanel';
13 |
14 | const panOnDrag = [1, 2];
15 |
16 | const selector = (state: RFState) => ({
17 | nodes: state.nodes,
18 | edges: state.edges,
19 | viewport: state.viewport,
20 | onNodesChange: state.onNodesChange,
21 | onEdgesChange: state.onEdgesChange,
22 | onConnectStart: state.onConnectStart,
23 | onConnectEnd: state.onConnectEnd,
24 | setReactFlowWrapper: state.setReactFlowWrapper,
25 | onInit: state.onInit,
26 | instance: state.instance,
27 | });
28 |
29 | const fitViewOptions = {
30 | padding: 3,
31 | };
32 |
33 | function Flow() {
34 | const reactFlowWrapper = useRef(null);
35 | const {
36 | nodes,
37 | edges,
38 | viewport,
39 | onNodesChange,
40 | onEdgesChange,
41 | onConnectStart,
42 | onConnectEnd,
43 | setReactFlowWrapper,
44 | onInit,
45 | } = useMapStore(selector, shallow);
46 |
47 | useEffect(() => {
48 | setReactFlowWrapper(reactFlowWrapper);
49 | }, [setReactFlowWrapper]);
50 |
51 | return (
52 | <>
53 |
54 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | >
79 | );
80 | }
81 |
82 | export default Flow;
83 |
--------------------------------------------------------------------------------
/components/Map/FlowWrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { ReactFlowProvider } from 'reactflow';
5 |
6 | interface FlowWrapperProps {
7 | children: React.ReactNode;
8 | }
9 |
10 | export default function FlowWrapper({ children }: FlowWrapperProps) {
11 | return {children};
12 | }
13 |
--------------------------------------------------------------------------------
/components/Map/Generator/Generator.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@/components/ui/Button';
4 | import { Label } from '@/components/ui/Label';
5 | import Loader from '@/components/ui/Loader';
6 | import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select';
7 | import { useState } from 'react';
8 |
9 | interface GeneratorProps {
10 | onGenerate: ({ accurateFor, type }: { accurateFor: string; type: string }) => void;
11 | isLoading: boolean;
12 | }
13 | export default function Generator({ onGenerate, isLoading }: GeneratorProps) {
14 | const [selected, setSelected] = useState({
15 | accurateFor: 'blogs',
16 | type: 'content ideas',
17 | });
18 |
19 | return (
20 |
21 |
22 |
Generator
23 |
24 | Enter parameters to give direction to the generator.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {isLoading && (
71 |
72 | {' '}
73 |
74 |
75 | )}
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/components/Map/Index.tsx:
--------------------------------------------------------------------------------
1 | import Flow from './Flow';
2 |
3 | export default function Map() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/components/Map/Nodes/AIContentModal.tsx:
--------------------------------------------------------------------------------
1 | import { Popover, PopoverContent, PopoverAnchor } from '@/components/ui/Popover';
2 | import { SUBTOPIC } from '@/utils/constants/headerTypes';
3 | import Generator from '../Generator/Generator';
4 | import { useToast } from '@/hooks/use-toast';
5 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider';
6 | import { ReactNode, useState } from 'react';
7 | import useMapStore from '@/stores/mapStore';
8 | import { generateIdeas } from '@/utils/api/suggestions';
9 |
10 | interface AIContentProps {
11 | id: string;
12 | isOpen: boolean;
13 | onChangeOpen: (open: boolean) => void;
14 | children: ReactNode;
15 | }
16 |
17 | export default function AIContentModal({ id, isOpen, onChangeOpen, children }: AIContentProps) {
18 | const { token, model } = useConfiguration();
19 | const { toast } = useToast();
20 | const [isLoading, setIsLoading] = useState(false);
21 | const getNodeContext = useMapStore((s) => s.getNodeContext);
22 | const addChildrenNodes = useMapStore((s) => s.addChildrenNodes);
23 | const removeElement = useMapStore((s) => s.removeElement);
24 |
25 | const generator = async ({ accurateFor, type }: { accurateFor: string; type: string }) => {
26 | if (!token) {
27 | toast({
28 | variant: 'destructive',
29 | title: 'You need to configure your OpenAI API key first',
30 | description: 'Use the hamburger menu located at the top of the page and configure your OpenAI API key.',
31 | });
32 |
33 | return;
34 | }
35 |
36 | setIsLoading(true);
37 | const { main, context } = getNodeContext(id);
38 |
39 | const ideas = await generateIdeas({
40 | main,
41 | context,
42 | token,
43 | accurateFor,
44 | type,
45 | model,
46 | });
47 |
48 | const newNodes = ideas.map((idea: string) => ({
49 | text: idea,
50 | type: SUBTOPIC,
51 | parentId: id,
52 | }));
53 |
54 | addChildrenNodes(id, 'topicNode', newNodes);
55 | setIsLoading(false);
56 | onChangeOpen(false);
57 | };
58 |
59 | return (
60 |
61 | {children}
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/components/Map/Nodes/RootNode.tsx:
--------------------------------------------------------------------------------
1 | import useMapStore from '@/stores/mapStore';
2 | import { memo, useEffect, useState } from 'react';
3 | import { Handle, Position } from 'reactflow';
4 | import { useDebounce } from 'react-use';
5 | import ToggleInput from '@/components/ui/ToggleInput';
6 | import BlockContainer from '@/components/ui/BlockContainer';
7 | // import NodeHeader from '@/components/ui/NodeHeader';
8 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider';
9 | import AIContentModal from './AIContentModal';
10 | import { AI_MODE } from '@/utils/constants/modes';
11 |
12 | const RootNode = ({ id, data }: any) => {
13 | const { mode } = useConfiguration();
14 | const [isOpenAIModal, setIsOpenAIModal] = useState(false);
15 | const [value, setValue] = useState(data.text);
16 |
17 | const updateText = useMapStore((s) => s.updateText);
18 |
19 | const [, cancel] = useDebounce(
20 | () => {
21 | updateText(id, value);
22 | },
23 | 1000,
24 | [value],
25 | );
26 |
27 | useEffect(() => {
28 | setValue(data.text);
29 | }, [data.text]);
30 |
31 | const onClick = () => {
32 | switch (mode) {
33 | case AI_MODE:
34 | setIsOpenAIModal(true);
35 | break;
36 | }
37 | };
38 |
39 | const renderContent = () => {
40 | if (mode === AI_MODE) {
41 | return {value}
;
42 | }
43 |
44 | return ;
45 | };
46 |
47 | return (
48 |
49 |
50 |
51 | {/* */}
52 | {renderContent()}
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default memo(RootNode);
61 |
--------------------------------------------------------------------------------
/components/Map/Nodes/TopicNode.tsx:
--------------------------------------------------------------------------------
1 | import useMapStore from '@/stores/mapStore';
2 | import { memo, useEffect, useState } from 'react';
3 | import { Handle, Position } from 'reactflow';
4 | import { useDebounce } from 'react-use';
5 | import BlockContainer from '@/components/ui/BlockContainer';
6 | // import NodeHeader from '@/components/ui/NodeHeader';
7 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider';
8 | import ToggleTextarea from '@/components/ui/ToggleTextarea';
9 | import AIContentModal from './AIContentModal';
10 | import { AI_MODE } from '@/utils/constants/modes';
11 |
12 | const TopicNode = ({ id, data }: any) => {
13 | const { mode } = useConfiguration();
14 |
15 | const [value, setValue] = useState(data.text);
16 | const [isOpenAIModal, setIsOpenAIModal] = useState(false);
17 |
18 | const updateText = useMapStore((s) => s.updateText);
19 | // const updateInnerType = useMapStore((s) => s.updateInnerType);
20 | const removeElement = useMapStore((s) => s.removeElement);
21 |
22 | const [, cancel] = useDebounce(
23 | () => {
24 | updateText(id, value);
25 | },
26 | 1000,
27 | [value],
28 | );
29 |
30 | useEffect(() => {
31 | setValue(data.text);
32 | }, [data.text]);
33 |
34 | // const updateType = (type: string) => {
35 | // updateInnerType(id, type);
36 | // };
37 |
38 | const onRemove = () => {
39 | removeElement(id);
40 | };
41 |
42 | const onClick = () => {
43 | switch (mode) {
44 | case AI_MODE:
45 | setIsOpenAIModal(true);
46 | break;
47 | }
48 | };
49 |
50 | const renderContent = () => {
51 | if (mode === AI_MODE) {
52 | return {value}
;
53 | }
54 |
55 | return ;
56 | };
57 |
58 | return (
59 |
60 | {/*
*/}
61 |
62 |
63 |
64 |
65 | {renderContent()}
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default memo(TopicNode);
74 |
--------------------------------------------------------------------------------
/components/Map/Panel/TopPanel/Theme.tsx:
--------------------------------------------------------------------------------
1 | import { palettes } from '@/data/defaultPalettes';
2 | import useMapStore from '@/stores/mapStore';
3 | import cn from '@/utils/classnames';
4 | import { getConfigKey, updateConfig } from '@/utils/storage';
5 | import { PaletteElement } from '@/utils/types';
6 | import { useState } from 'react';
7 |
8 | export default function Theme() {
9 | const [activePalette, setActivePalette] = useState(getConfigKey('palette'));
10 | const applyPalette = useMapStore((s) => s.applyPalette);
11 |
12 | const onChoosePalette = (selectedPalette: PaletteElement) => {
13 | updateConfig('palette', selectedPalette.id);
14 | setActivePalette(selectedPalette.id);
15 | applyPalette(selectedPalette);
16 | };
17 |
18 | return (
19 |
20 | {palettes.map((palette) => {
21 | const rootStyles = palette.root.buildStyles();
22 |
23 | return (
24 |
onChoosePalette(palette)}
27 | className={cn(
28 | 'p-2 hover:bg-slate-100 hover:cursor-pointer flex rounded',
29 | palette.id === activePalette ? 'bg-slate-200' : '',
30 | )}
31 | >
32 |
33 | {palette.colors.map((color) => {
34 | const styles = palette.node.buildStyles(color);
35 |
36 | return
;
37 | })}
38 |
39 | );
40 | })}
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/Map/Panel/TopPanel/TopPanel.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import IconComponent from '@/components/ui/Icon';
3 | import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover';
4 | import { Separator } from '@/components/ui/Separator';
5 | import { ToggleGroup, ToggleGroupItem } from '@/components/ui/ToggleGroup';
6 | import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/Tooltip';
7 | import cn from '@/utils/classnames';
8 | import { AI_MODE, AvailableModes, SELECTION_MODE } from '@/utils/constants/modes';
9 | import { useConfiguration } from '@/utils/providers/ConfigurationProvider';
10 | import { Panel } from 'reactflow';
11 | import Theme from './Theme';
12 |
13 | export default function ModePanel() {
14 | const { mode, updateMode } = useConfiguration();
15 |
16 | const handleUpdate = (value: string) => {
17 | if (!value) {
18 | return;
19 | }
20 |
21 | updateMode(value as AvailableModes);
22 | };
23 |
24 | return (
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Selection Mode
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | AI Mode
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
77 |
78 |
79 |
80 |
81 |
82 |
Theme
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | Customization
91 |
92 |
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/components/Map/Plugins/DataSaver.tsx:
--------------------------------------------------------------------------------
1 | import useMapStore from '@/stores/mapStore';
2 | import { saveMap } from '@/utils/storage';
3 | import { useEffect } from 'react';
4 | import { useNodesInitialized, useReactFlow } from 'reactflow';
5 |
6 | function DataSaver() {
7 | const store = useMapStore();
8 | const reactFlowInstance = useReactFlow();
9 | const nodesInitialized = useNodesInitialized();
10 |
11 | useEffect(() => {
12 | if (nodesInitialized) {
13 | const map = reactFlowInstance.toObject();
14 |
15 | saveMap(map);
16 | }
17 | }, [store.nodes, store.edges, store.viewport]);
18 |
19 | return null;
20 | }
21 |
22 | export default DataSaver;
23 |
--------------------------------------------------------------------------------
/components/Providers/Analytics.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Analytics } from '@vercel/analytics/react';
3 |
4 | export function AnalyticsWrapper() {
5 | return ;
6 | }
7 |
--------------------------------------------------------------------------------
/components/ui/AspectRadio.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root;
6 |
7 | export { AspectRatio };
8 |
--------------------------------------------------------------------------------
/components/ui/BlockContainer.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import IconComponent from './Icon';
3 |
4 | interface BlockContainerProps {
5 | children: ReactNode;
6 | onRemove?: () => void;
7 | menu?: ReactNode;
8 | }
9 |
10 | export default function BlockContainer({ children, onRemove, menu }: BlockContainerProps) {
11 | return (
12 |
13 | {onRemove && (
14 |
onRemove()}
17 | >
18 |
19 |
20 |
21 |
22 | )}
23 |
24 | {menu &&
{menu}
}
25 | {children}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/components/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | import cn from '@/utils/classnames';
2 | import { VariantProps, cva } from 'class-variance-authority';
3 | import React from 'react';
4 |
5 | const buttonVariants = cva(
6 | 'active:scale-95 inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none data-[state=open]:bg-slate-100',
7 | {
8 | variants: {
9 | variant: {
10 | default: 'bg-slate-900 text-white hover:bg-slate-700',
11 | destructive: 'bg-red-500 text-white hover:bg-red-600',
12 | outline: 'bg-transparent border border-slate-200 hover:bg-slate-100',
13 | subtle: 'bg-slate-100 text-slate-900 hover:bg-slate-200',
14 | ghost: 'bg-transparent hover:bg-slate-100 data-[state=open]:bg-transparent',
15 | link: 'bg-transparent underline-offset-4 hover:underline text-slate-900 hover:bg-transparent',
16 | },
17 | size: {
18 | default: 'h-10 py-2 px-4',
19 | sm: 'h-9 px-2 rounded-md',
20 | lg: 'h-11 px-8 rounded-md',
21 | },
22 | },
23 | defaultVariants: {
24 | variant: 'default',
25 | size: 'default',
26 | },
27 | },
28 | );
29 |
30 | export interface ButtonProps
31 | extends React.ButtonHTMLAttributes,
32 | VariantProps {
33 | [x: string]: any;
34 | }
35 |
36 | const Button = React.forwardRef(({ className, variant, size, ...props }, ref) => {
37 | return ;
38 | });
39 |
40 | Button.displayName = 'Button';
41 |
42 | export { Button, buttonVariants };
43 |
--------------------------------------------------------------------------------
/components/ui/Dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as DialogPrimitive from '@radix-ui/react-dialog';
5 | import { X } from 'lucide-react';
6 | import cn from '@/utils/classnames';
7 |
8 | const Dialog = DialogPrimitive.Root;
9 |
10 | const DialogTrigger = DialogPrimitive.Trigger;
11 |
12 | const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => (
13 |
14 | {children}
15 |
16 | );
17 | DialogPortal.displayName = DialogPrimitive.Portal.displayName;
18 |
19 | const DialogOverlay = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, children, ...props }, ref) => (
23 |
31 | ));
32 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
33 |
34 | const DialogContent = React.forwardRef<
35 | React.ElementRef,
36 | React.ComponentPropsWithoutRef
37 | >(({ className, children, ...props }, ref) => (
38 |
39 |
40 |
49 | {children}
50 |
51 |
52 | Close
53 |
54 |
55 |
56 | ));
57 | DialogContent.displayName = DialogPrimitive.Content.displayName;
58 |
59 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
60 |
61 | );
62 | DialogHeader.displayName = 'DialogHeader';
63 |
64 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
65 |
66 | );
67 | DialogFooter.displayName = 'DialogFooter';
68 |
69 | const DialogTitle = React.forwardRef<
70 | React.ElementRef,
71 | React.ComponentPropsWithoutRef
72 | >(({ className, ...props }, ref) => (
73 |
74 | ));
75 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
76 |
77 | const DialogDescription = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef
80 | >(({ className, ...props }, ref) => (
81 |
82 | ));
83 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
84 |
85 | export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription };
86 |
--------------------------------------------------------------------------------
/components/ui/DropdownMenu.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
5 | import { Check, ChevronRight, Circle } from 'lucide-react';
6 | import cn from '@/utils/classnames';
7 |
8 | const DropdownMenu = DropdownMenuPrimitive.Root;
9 |
10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
11 |
12 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
13 |
14 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
15 |
16 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
17 |
18 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
19 |
20 | const DropdownMenuSubTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef & {
23 | inset?: boolean;
24 | }
25 | >(({ className, inset, children, ...props }, ref) => (
26 |
35 | {children}
36 |
37 |
38 | ));
39 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
40 |
41 | const DropdownMenuSubContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, ...props }, ref) => (
45 |
53 | ));
54 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
55 |
56 | const DropdownMenuContent = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, sideOffset = 4, ...props }, ref) => (
60 |
61 |
70 |
71 | ));
72 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
73 |
74 | const DropdownMenuItem = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef & {
77 | inset?: boolean;
78 | }
79 | >(({ className, inset, ...props }, ref) => (
80 |
89 | ));
90 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
91 |
92 | const DropdownMenuCheckboxItem = React.forwardRef<
93 | React.ElementRef,
94 | React.ComponentPropsWithoutRef
95 | >(({ className, children, checked, ...props }, ref) => (
96 |
105 |
106 |
107 |
108 |
109 |
110 | {children}
111 |
112 | ));
113 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
114 |
115 | const DropdownMenuRadioItem = React.forwardRef<
116 | React.ElementRef,
117 | React.ComponentPropsWithoutRef
118 | >(({ className, children, ...props }, ref) => (
119 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ));
135 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
136 |
137 | const DropdownMenuLabel = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef & {
140 | inset?: boolean;
141 | }
142 | >(({ className, inset, ...props }, ref) => (
143 |
148 | ));
149 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
150 |
151 | const DropdownMenuSeparator = React.forwardRef<
152 | React.ElementRef,
153 | React.ComponentPropsWithoutRef
154 | >(({ className, ...props }, ref) => (
155 |
156 | ));
157 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
158 |
159 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
160 | return ;
161 | };
162 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
163 |
164 | export {
165 | DropdownMenu,
166 | DropdownMenuTrigger,
167 | DropdownMenuContent,
168 | DropdownMenuItem,
169 | DropdownMenuCheckboxItem,
170 | DropdownMenuRadioItem,
171 | DropdownMenuLabel,
172 | DropdownMenuSeparator,
173 | DropdownMenuShortcut,
174 | DropdownMenuGroup,
175 | DropdownMenuPortal,
176 | DropdownMenuSub,
177 | DropdownMenuSubContent,
178 | DropdownMenuSubTrigger,
179 | DropdownMenuRadioGroup,
180 | };
181 |
--------------------------------------------------------------------------------
/components/ui/Icon.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Circle,
3 | FlagTriangleRight,
4 | CircleDot,
5 | LucideIcon,
6 | X,
7 | Wand,
8 | Github,
9 | Twitter,
10 | AirVent,
11 | Lightbulb,
12 | Candy,
13 | Code,
14 | Menu,
15 | MousePointer,
16 | Edit,
17 | Palette,
18 | } from 'lucide-react';
19 |
20 | type IconComponentProps = { name: string; [x: string]: any };
21 | type IconTypes = { [name: string]: LucideIcon };
22 |
23 | const iconTypes: IconTypes = {
24 | circle: Circle,
25 | triangle: FlagTriangleRight,
26 | circleDot: CircleDot,
27 | close: X,
28 | want: Wand,
29 | github: Github,
30 | twitter: Twitter,
31 | logo: AirVent,
32 | idea: Lightbulb,
33 | candy: Candy,
34 | code: Code,
35 | menu: Menu,
36 | mousePointer: MousePointer,
37 | fileEdit: Edit,
38 | palette: Palette,
39 | };
40 |
41 | const IconComponent = ({ name, ...props }: IconComponentProps) => {
42 | let Icon = iconTypes[name];
43 |
44 | return ;
45 | };
46 |
47 | export default IconComponent;
48 |
--------------------------------------------------------------------------------
/components/ui/Input.tsx:
--------------------------------------------------------------------------------
1 | import cn from '@/utils/classnames';
2 | import * as React from 'react';
3 |
4 | export interface InputProps extends React.InputHTMLAttributes {}
5 |
6 | const Input = React.forwardRef(({ className, ...props }, ref) => {
7 | return (
8 |
16 | );
17 | });
18 | Input.displayName = 'Input';
19 |
20 | export { Input };
21 |
--------------------------------------------------------------------------------
/components/ui/Label.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import cn from '@/utils/classnames';
6 |
7 | const Label = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 | ));
20 | Label.displayName = LabelPrimitive.Root.displayName;
21 |
22 | export { Label };
23 |
--------------------------------------------------------------------------------
/components/ui/Loader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cn from '@/utils/classnames';
3 |
4 | interface LoaderProps {
5 | className?: string;
6 | }
7 |
8 | export default function Loader({ className = 'h-2 w-2' }: LoaderProps) {
9 | return (
10 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/ui/MenuBar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as MenubarPrimitive from '@radix-ui/react-menubar';
5 | import { Check, ChevronRight, Circle } from 'lucide-react';
6 | import cn from '@/utils/classnames';
7 |
8 | const MenubarMenu = MenubarPrimitive.Menu;
9 |
10 | const MenubarGroup = MenubarPrimitive.Group;
11 |
12 | const MenubarPortal = MenubarPrimitive.Portal;
13 |
14 | const MenubarSub = MenubarPrimitive.Sub;
15 |
16 | const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
17 |
18 | const Menubar = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
27 | ));
28 | Menubar.displayName = MenubarPrimitive.Root.displayName;
29 |
30 | const MenubarTrigger = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
42 | ));
43 | MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
44 |
45 | const MenubarSubTrigger = React.forwardRef<
46 | React.ElementRef,
47 | React.ComponentPropsWithoutRef & {
48 | inset?: boolean;
49 | }
50 | >(({ className, inset, children, ...props }, ref) => (
51 |
60 | {children}
61 |
62 |
63 | ));
64 | MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
65 |
66 | const MenubarSubContent = React.forwardRef<
67 | React.ElementRef,
68 | React.ComponentPropsWithoutRef
69 | >(({ className, ...props }, ref) => (
70 |
78 | ));
79 | MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
80 |
81 | const MenubarContent = React.forwardRef<
82 | React.ElementRef,
83 | React.ComponentPropsWithoutRef
84 | >(({ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props }, ref) => (
85 |
86 |
97 |
98 | ));
99 | MenubarContent.displayName = MenubarPrimitive.Content.displayName;
100 |
101 | const MenubarItem = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef & {
104 | inset?: boolean;
105 | }
106 | >(({ className, inset, ...props }, ref) => (
107 |
116 | ));
117 | MenubarItem.displayName = MenubarPrimitive.Item.displayName;
118 |
119 | const MenubarCheckboxItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, checked, ...props }, ref) => (
123 |
132 |
133 |
134 |
135 |
136 |
137 | {children}
138 |
139 | ));
140 | MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
141 |
142 | const MenubarRadioItem = React.forwardRef<
143 | React.ElementRef,
144 | React.ComponentPropsWithoutRef
145 | >(({ className, children, ...props }, ref) => (
146 |
154 |
155 |
156 |
157 |
158 |
159 | {children}
160 |
161 | ));
162 | MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
163 |
164 | const MenubarLabel = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef & {
167 | inset?: boolean;
168 | }
169 | >(({ className, inset, ...props }, ref) => (
170 |
175 | ));
176 | MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
177 |
178 | const MenubarSeparator = React.forwardRef<
179 | React.ElementRef,
180 | React.ComponentPropsWithoutRef
181 | >(({ className, ...props }, ref) => (
182 |
183 | ));
184 | MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
185 |
186 | const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes) => {
187 | return ;
188 | };
189 | MenubarShortcut.displayname = 'MenubarShortcut';
190 |
191 | export {
192 | Menubar,
193 | MenubarMenu,
194 | MenubarTrigger,
195 | MenubarContent,
196 | MenubarItem,
197 | MenubarSeparator,
198 | MenubarLabel,
199 | MenubarCheckboxItem,
200 | MenubarRadioGroup,
201 | MenubarRadioItem,
202 | MenubarPortal,
203 | MenubarSubContent,
204 | MenubarSubTrigger,
205 | MenubarGroup,
206 | MenubarSub,
207 | MenubarShortcut,
208 | };
209 |
--------------------------------------------------------------------------------
/components/ui/NodeHeader.tsx:
--------------------------------------------------------------------------------
1 | import { MAIN_TOPIC, SUBTOPIC, DETAIL } from '@/utils/constants/headerTypes';
2 | import {
3 | DropdownMenu,
4 | DropdownMenuCheckboxItem,
5 | DropdownMenuContent,
6 | DropdownMenuLabel,
7 | DropdownMenuRadioGroup,
8 | DropdownMenuRadioItem,
9 | DropdownMenuSeparator,
10 | DropdownMenuTrigger,
11 | } from '@/components/ui/DropdownMenu';
12 |
13 | import IconComponent from './Icon';
14 | import { ReactNode, useState } from 'react';
15 |
16 | const Icons = {
17 | [MAIN_TOPIC]: 'circle',
18 | [SUBTOPIC]: 'triangle',
19 | [DETAIL]: 'circleDot',
20 | };
21 |
22 | const typesOptions = [
23 | { icon: 'circle', value: MAIN_TOPIC, text: 'Topic' },
24 | { icon: 'triangle', value: SUBTOPIC, text: 'Subtopic' },
25 | { icon: 'circleDot', value: DETAIL, text: 'Detail' },
26 | ];
27 |
28 | interface NodeHeaderProps {
29 | text: string;
30 | onChangeType?: (value: string) => void;
31 | type: keyof typeof Icons;
32 | }
33 |
34 | export default function NodeHeader({ type, onChangeType }: NodeHeaderProps) {
35 | const iconName: string = Icons[type];
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
{typesOptions.find((to) => to.value === type)?.text}
47 |
48 | );
49 | }
50 |
51 | interface DropdownMenuHeaderProps {
52 | children: ReactNode;
53 | type: string;
54 | onChangeType?: (type: string) => void;
55 | }
56 |
57 | export function DropdownMenuHeader({ children, type, onChangeType = (value: string) => {} }: DropdownMenuHeaderProps) {
58 | return (
59 |
60 | {children}
61 |
62 |
63 | {typesOptions.map((typeOption) => (
64 |
65 | {typeOption.text}
66 |
67 | ))}
68 | {/* Top
69 | Bottom
70 | Right */}
71 |
72 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/components/ui/Popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as PopoverPrimitive from '@radix-ui/react-popover';
5 | import cn from '@/utils/classnames';
6 |
7 | const Popover = PopoverPrimitive.Root;
8 |
9 | const PopoverTrigger = PopoverPrimitive.Trigger;
10 |
11 | const PopoverAnchor = PopoverPrimitive.Anchor;
12 |
13 | const PopoverContent = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
17 |
18 |
28 |
29 | ));
30 |
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
32 |
33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
34 |
--------------------------------------------------------------------------------
/components/ui/Select.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SelectPrimitive from '@radix-ui/react-select';
5 | import { Check, ChevronDown } from 'lucide-react';
6 | import cn from '@/utils/classnames';
7 |
8 | const Select = SelectPrimitive.Root;
9 |
10 | const SelectGroup = SelectPrimitive.Group;
11 |
12 | const SelectValue = SelectPrimitive.Value;
13 |
14 | const SelectTrigger = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, children, ...props }, ref) => (
18 |
26 | {children}
27 |
28 |
29 | ));
30 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
31 |
32 | const SelectContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
45 | {children}
46 |
47 |
48 | ));
49 | SelectContent.displayName = SelectPrimitive.Content.displayName;
50 |
51 | const SelectLabel = React.forwardRef<
52 | React.ElementRef,
53 | React.ComponentPropsWithoutRef
54 | >(({ className, ...props }, ref) => (
55 |
60 | ));
61 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
62 |
63 | const SelectItem = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, children, ...props }, ref) => (
67 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | {children}
82 |
83 | ));
84 | SelectItem.displayName = SelectPrimitive.Item.displayName;
85 |
86 | const SelectSeparator = React.forwardRef<
87 | React.ElementRef,
88 | React.ComponentPropsWithoutRef
89 | >(({ className, ...props }, ref) => (
90 |
91 | ));
92 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
93 |
94 | export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator };
95 |
--------------------------------------------------------------------------------
/components/ui/Separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator';
5 | import cn from '@/utils/classnames';
6 |
7 | const Separator = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
11 |
18 | ));
19 | Separator.displayName = SeparatorPrimitive.Root.displayName;
20 |
21 | export { Separator };
22 |
--------------------------------------------------------------------------------
/components/ui/Textarea.tsx:
--------------------------------------------------------------------------------
1 | import cn from '@/utils/classnames';
2 | import * as React from 'react';
3 |
4 | export interface TextareaProps extends React.TextareaHTMLAttributes {}
5 |
6 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
7 | return (
8 |
16 | );
17 | });
18 |
19 | Textarea.displayName = 'Textarea';
20 |
21 | export { Textarea };
22 |
--------------------------------------------------------------------------------
/components/ui/Toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ToastPrimitives from '@radix-ui/react-toast';
3 | import { VariantProps, cva } from 'class-variance-authority';
4 | import { X } from 'lucide-react';
5 |
6 | import cn from '@/utils/classnames';
7 |
8 | const ToastProvider = ToastPrimitives.Provider;
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
24 |
25 | const toastVariants = cva(
26 | 'data-[swipe=move]:transition-none grow-1 group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full mt-4 data-[state=closed]:slide-out-to-right-full dark:border-slate-700 last:mt-0 sm:last:mt-4',
27 | {
28 | variants: {
29 | variant: {
30 | default: 'bg-white border-slate-200 dark:bg-slate-800 dark:border-slate-700 text-white',
31 | destructive: 'group destructive bg-red-600 text-white border-red-600 dark:border-red-600',
32 | },
33 | },
34 | defaultVariants: {
35 | variant: 'default',
36 | },
37 | },
38 | );
39 |
40 | const Toast = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef & VariantProps
43 | >(({ className, variant, ...props }, ref) => {
44 | return ;
45 | });
46 | Toast.displayName = ToastPrimitives.Root.displayName;
47 |
48 | const ToastAction = React.forwardRef<
49 | React.ElementRef,
50 | React.ComponentPropsWithoutRef
51 | >(({ className, ...props }, ref) => (
52 |
60 | ));
61 | ToastAction.displayName = ToastPrimitives.Action.displayName;
62 |
63 | const ToastClose = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, ...props }, ref) => (
67 |
76 |
77 |
78 | ));
79 | ToastClose.displayName = ToastPrimitives.Close.displayName;
80 |
81 | const ToastTitle = React.forwardRef<
82 | React.ElementRef,
83 | React.ComponentPropsWithoutRef
84 | >(({ className, ...props }, ref) => (
85 |
86 | ));
87 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
88 |
89 | const ToastDescription = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
94 | ));
95 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
96 |
97 | type ToastProps = React.ComponentPropsWithoutRef;
98 |
99 | type ToastActionElement = React.ReactElement;
100 |
101 | export {
102 | type ToastProps,
103 | type ToastActionElement,
104 | ToastProvider,
105 | ToastViewport,
106 | Toast,
107 | ToastTitle,
108 | ToastDescription,
109 | ToastClose,
110 | ToastAction,
111 | };
112 |
--------------------------------------------------------------------------------
/components/ui/Toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useToast } from '@/hooks/use-toast';
4 |
5 | import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from '@/components/ui/Toast';
6 |
7 | export function Toaster() {
8 | const { toasts } = useToast();
9 |
10 | return (
11 |
12 | {toasts.map(function ({ id, title, description, action, ...props }) {
13 | return (
14 |
15 |
16 | {title && {title}}
17 | {description && {description}}
18 |
19 | {action}
20 |
21 |
22 | );
23 | })}
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/components/ui/ToggleGroup.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
3 | import cn from '@/utils/classnames';
4 | import React from 'react';
5 |
6 | const ToggleGroup = ToggleGroupPrimitive.Root;
7 |
8 | const ToggleGroupItem = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
23 | {children}
24 |
25 | ));
26 |
27 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
28 |
29 | export { ToggleGroup, ToggleGroupItem };
30 |
--------------------------------------------------------------------------------
/components/ui/ToggleInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, useState, memo, useCallback } from 'react';
2 | import cn from 'classnames';
3 |
4 | interface ToggleInputProps {
5 | value: string;
6 | setValue: Function;
7 | className?: string;
8 | }
9 |
10 | function ToggleInput({ value, setValue, className }: ToggleInputProps) {
11 | const [showInputEle, setShowInputEle] = useState(false);
12 |
13 | const onChange = useCallback((e: ChangeEvent) => {
14 | setValue(e.target.value);
15 | }, []);
16 |
17 | return (
18 |
19 | {showInputEle ? (
20 | setShowInputEle(false)}
26 | autoFocus
27 | />
28 | ) : (
29 | setShowInputEle(true)}>
30 | {value || 'Change this text'}
31 |
32 | )}
33 |
34 | );
35 | }
36 |
37 | export default memo(ToggleInput);
38 |
--------------------------------------------------------------------------------
/components/ui/ToggleTextarea.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, useState, memo, useCallback } from 'react';
2 | import cn from 'classnames';
3 | import { Textarea } from './Textarea';
4 |
5 | interface ToggleInputProps {
6 | value: string;
7 | setValue: Function;
8 | className?: string;
9 | }
10 |
11 | function ToggleInput({ value, setValue, className }: ToggleInputProps) {
12 | const [showInputEle, setShowInputEle] = useState(false);
13 |
14 | const onChange = useCallback((e: ChangeEvent) => {
15 | setValue(e.target.value);
16 | }, []);
17 |
18 | return (
19 |
20 | {showInputEle ? (
21 |
35 | );
36 | }
37 |
38 | export default memo(ToggleInput);
39 |
--------------------------------------------------------------------------------
/components/ui/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
5 | import cn from '@/utils/classnames';
6 |
7 | const TooltipProvider = TooltipPrimitive.Provider;
8 |
9 | const Tooltip = ({ ...props }) => ;
10 | Tooltip.displayName = TooltipPrimitive.Tooltip.displayName;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/data/defaultEdges.ts:
--------------------------------------------------------------------------------
1 | import { Edge } from 'reactflow';
2 | import CustomizableEdge from '@/components/Map/Edges/CustomizableEdge';
3 |
4 | export const edgeTypes = {
5 | customizable: CustomizableEdge,
6 | };
7 |
8 | export const initialEdges: Array = [
9 | // { id: '0', source: '1', target: '2' },
10 | ];
11 |
--------------------------------------------------------------------------------
/data/defaultNodes.ts:
--------------------------------------------------------------------------------
1 | import RootNode from '@/components/Map/Nodes/RootNode';
2 | import TopicNode from '@/components/Map/Nodes/TopicNode';
3 | import { Node } from 'reactflow';
4 | import { MAIN_TOPIC } from '@/utils/constants/headerTypes';
5 |
6 | export const nodeTypes = {
7 | rootNode: RootNode,
8 | topicNode: TopicNode,
9 | };
10 |
11 | export const initialNodes: Array = [
12 | {
13 | id: 'root',
14 | data: { text: 'AI conquering the world', type: MAIN_TOPIC },
15 | position: { x: 0, y: 0 },
16 | type: 'rootNode',
17 | },
18 | // {
19 | // id: '1',
20 | // data: { label: 'Hello' },
21 | // position: { x: 0, y: 0 },
22 | // type: 'input',
23 | // },
24 | // {
25 | // id: '2',
26 | // data: { label: 'World' },
27 | // position: { x: 100, y: 100 },
28 | // },
29 | ];
30 |
--------------------------------------------------------------------------------
/data/defaultPalettes.ts:
--------------------------------------------------------------------------------
1 | import { PaletteElement } from '@/utils/types';
2 | import fontColorContrast from 'font-color-contrast';
3 |
4 | export const palettes: PaletteElement[] = [
5 | {
6 | id: 'palette-0',
7 | name: 'Palette 0',
8 | colors: ['#0b132b', '#1c2541', '#3a506b', '#5bc0be', '#6fffe9'],
9 | root: {
10 | buildStyles: () => {
11 | return {
12 | backgroundColor: '#fff',
13 | color: '#1E293B',
14 | borderStyle: 'solid',
15 | borderWidth: 1,
16 | borderColor: '#0b132b',
17 | borderRadius: '10px',
18 | };
19 | },
20 | },
21 | node: {
22 | buildStyles: (color: string) => {
23 | return {
24 | backgroundColor: 'white',
25 | borderStyle: 'solid',
26 | borderWidth: 1,
27 | borderColor: color,
28 | color: fontColorContrast('white'),
29 | borderRadius: '10px',
30 | };
31 | },
32 | },
33 | edge: {
34 | buildStyles: (color: string) => {
35 | return {
36 | stroke: color,
37 | };
38 | },
39 | type: 'default',
40 | },
41 | },
42 | {
43 | id: 'palette-1',
44 | name: 'Palette 1',
45 | colors: ['#f72585', '#7209b7', '#3a0ca3', '#4361ee', '#4cc9f0'],
46 | root: {
47 | buildStyles: () => {
48 | return {
49 | backgroundColor: '#fff',
50 | color: '#1E293B',
51 | borderStyle: 'solid',
52 | borderWidth: 1,
53 | borderColor: '#f72585',
54 | borderRadius: '10px',
55 | };
56 | },
57 | },
58 | node: {
59 | buildStyles: (color: string) => {
60 | return {
61 | backgroundColor: color,
62 | borderStyle: 'solid',
63 | borderWidth: 1,
64 | borderColor: color,
65 | color: fontColorContrast(color),
66 | borderRadius: '10px',
67 | };
68 | },
69 | },
70 | edge: {
71 | buildStyles: (color: string) => {
72 | return {
73 | stroke: color,
74 | };
75 | },
76 | type: 'default',
77 | },
78 | },
79 | {
80 | id: 'palette-2',
81 | name: 'Palette 2',
82 | colors: ['#ffbe0b', '#fb5607', '#ff006e', '#8338ec', '#3a86ff'],
83 | root: {
84 | buildStyles: () => {
85 | return {
86 | backgroundColor: '#fff',
87 | color: '#1E293B',
88 | borderStyle: 'solid',
89 | borderWidth: 1,
90 | borderColor: '#ffbe0b',
91 | borderRadius: '10px',
92 | };
93 | },
94 | },
95 | node: {
96 | buildStyles: (color: string) => {
97 | return {
98 | backgroundColor: color,
99 | borderStyle: 'solid',
100 | borderWidth: 1,
101 | borderColor: color,
102 | color: fontColorContrast(color),
103 | borderRadius: '10px',
104 | };
105 | },
106 | },
107 | edge: {
108 | buildStyles: (color: string) => {
109 | return {
110 | stroke: color,
111 | };
112 | },
113 | type: 'default',
114 | },
115 | },
116 | {
117 | id: 'palette-3',
118 | name: 'Palette 3',
119 | colors: ['#03045e', '#023e8a', '#0077b6', '#0096c7', '#00b4d8'],
120 | root: {
121 | buildStyles: () => {
122 | return {
123 | backgroundColor: '#fff',
124 | color: '#1E293B',
125 | borderStyle: 'solid',
126 | borderWidth: 1,
127 | borderColor: '#03045e',
128 | borderRadius: '10px',
129 | };
130 | },
131 | },
132 | node: {
133 | buildStyles: (color: string) => {
134 | return {
135 | borderStyle: 'solid',
136 | borderWidth: 1,
137 | borderColor: color,
138 | backgroundColor: color,
139 | color: fontColorContrast(color),
140 | borderRadius: '10px',
141 | };
142 | },
143 | },
144 | edge: {
145 | buildStyles: (color: string) => {
146 | return {
147 | stroke: color,
148 | };
149 | },
150 | type: 'default',
151 | },
152 | },
153 | {
154 | id: 'palette-4',
155 | name: 'Palette 4',
156 | colors: ['#03045e', '#023e8a', '#0077b6', '#0096c7', '#00b4d8'],
157 | root: {
158 | buildStyles: () => {
159 | return {
160 | backgroundColor: '#fff',
161 | color: '#1E293B',
162 | borderStyle: 'solid',
163 | borderWidth: 1,
164 | borderColor: '#03045e',
165 | borderRadius: '10px',
166 | };
167 | },
168 | },
169 | node: {
170 | buildStyles: (color: string) => {
171 | return {
172 | borderStyle: 'solid',
173 | borderWidth: 1,
174 | borderColor: color,
175 | backgroundColor: color,
176 | color: fontColorContrast(color),
177 | borderRadius: '10px',
178 | };
179 | },
180 | },
181 | edge: {
182 | buildStyles: (color: string) => {
183 | return {
184 | stroke: color,
185 | };
186 | },
187 | type: 'default',
188 | },
189 | },
190 | {
191 | id: 'palette-5',
192 | name: 'Palette 5',
193 | colors: ['#8ecae6', '#219ebc', '#023047', '#ffb703', '#fb8500'],
194 | root: {
195 | buildStyles: () => {
196 | return {
197 | backgroundColor: '#fff',
198 | color: '#1E293B',
199 | borderStyle: 'solid',
200 | borderWidth: 1,
201 | borderColor: '#8ecae6',
202 | borderRadius: '10px',
203 | };
204 | },
205 | },
206 | node: {
207 | buildStyles: (color: string) => {
208 | return {
209 | borderStyle: 'solid',
210 | borderWidth: 1,
211 | borderColor: color,
212 | backgroundColor: color,
213 | color: fontColorContrast(color),
214 | borderRadius: '10px',
215 | };
216 | },
217 | },
218 | edge: {
219 | buildStyles: (color: string) => {
220 | return {
221 | stroke: color,
222 | };
223 | },
224 | type: 'default',
225 | },
226 | },
227 | {
228 | id: 'palette-6',
229 | name: 'Palette 6',
230 | colors: ['#cdb4db', '#ffc8dd', '#ffafcc', '#bde0fe', '#a2d2ff'],
231 | root: {
232 | buildStyles: () => {
233 | return {
234 | backgroundColor: '#fff',
235 | color: '#1E293B',
236 | borderStyle: 'solid',
237 | borderWidth: 1,
238 | borderColor: '#cdb4db',
239 | borderRadius: '10px',
240 | };
241 | },
242 | },
243 | node: {
244 | buildStyles: (color: string) => {
245 | return {
246 | borderStyle: 'solid',
247 | borderWidth: 1,
248 | borderColor: color,
249 | backgroundColor: color,
250 | color: fontColorContrast(color),
251 | borderRadius: '10px',
252 | };
253 | },
254 | },
255 | edge: {
256 | buildStyles: (color: string) => {
257 | return {
258 | stroke: color,
259 | };
260 | },
261 | type: 'default',
262 | },
263 | },
264 | {
265 | id: 'palette-7',
266 | name: 'Palette 7',
267 | colors: ['#ef476f', '#ffd166', '#06d6a0', '#118ab2', '#073b4c'],
268 | root: {
269 | buildStyles: () => {
270 | return {
271 | backgroundColor: '#fff',
272 | color: '#1E293B',
273 | borderStyle: 'solid',
274 | borderWidth: 1,
275 | borderColor: '#ef476f',
276 | borderRadius: '10px',
277 | };
278 | },
279 | },
280 | node: {
281 | buildStyles: (color: string) => {
282 | return {
283 | borderStyle: 'solid',
284 | borderWidth: 1,
285 | borderColor: color,
286 | backgroundColor: color,
287 | color: fontColorContrast(color),
288 | borderRadius: '10px',
289 | };
290 | },
291 | },
292 | edge: {
293 | buildStyles: (color: string) => {
294 | return {
295 | stroke: color,
296 | };
297 | },
298 | type: 'default',
299 | },
300 | },
301 | {
302 | id: 'palette-8',
303 | name: 'Palette 8',
304 | colors: ['#7400b8', '#6930c3', '#5e60ce', '#5390d9', '#4ea8de'],
305 | root: {
306 | buildStyles: () => {
307 | return {
308 | backgroundColor: '#fff',
309 | color: '#1E293B',
310 | borderStyle: 'solid',
311 | borderWidth: 1,
312 | borderColor: '#7400b8',
313 | borderRadius: '10px',
314 | };
315 | },
316 | },
317 | node: {
318 | buildStyles: (color: string) => {
319 | return {
320 | borderStyle: 'solid',
321 | borderWidth: 1,
322 | borderColor: color,
323 | backgroundColor: color,
324 | color: fontColorContrast(color),
325 | borderRadius: '10px',
326 | };
327 | },
328 | },
329 | edge: {
330 | buildStyles: (color: string) => {
331 | return {
332 | stroke: color,
333 | };
334 | },
335 | type: 'default',
336 | },
337 | },
338 | {
339 | id: 'palette-9',
340 | name: 'Palette 9',
341 | colors: ['#d9ed92', '#b5e48c', '#99d98c', '#76c893', '#52b69a'],
342 | root: {
343 | buildStyles: () => {
344 | return {
345 | backgroundColor: '#fff',
346 | color: '#1E293B',
347 | borderStyle: 'solid',
348 | borderWidth: 1,
349 | borderColor: '#d9ed92',
350 | borderRadius: '10px',
351 | };
352 | },
353 | },
354 | node: {
355 | buildStyles: (color: string) => {
356 | return {
357 | borderStyle: 'solid',
358 | borderWidth: 1,
359 | borderColor: color,
360 | backgroundColor: color,
361 | color: fontColorContrast(color),
362 | borderRadius: '10px',
363 | };
364 | },
365 | },
366 | edge: {
367 | buildStyles: (color: string) => {
368 | return {
369 | stroke: color,
370 | };
371 | },
372 | type: 'default',
373 | },
374 | },
375 | {
376 | id: 'palette-10',
377 | name: 'Palette 10',
378 | colors: ['#ffcdb2', '#ffb4a2', '#e5989b', '#b5838d', '#6d6875'],
379 | root: {
380 | buildStyles: () => {
381 | return {
382 | backgroundColor: '#fff',
383 | color: '#1E293B',
384 | borderStyle: 'solid',
385 | borderWidth: 1,
386 | borderColor: '#ffcdb2',
387 | borderRadius: '10px',
388 | };
389 | },
390 | },
391 | node: {
392 | buildStyles: (color: string) => {
393 | return {
394 | borderStyle: 'solid',
395 | borderWidth: 1,
396 | borderColor: color,
397 | backgroundColor: color,
398 | color: fontColorContrast(color),
399 | borderRadius: '10px',
400 | };
401 | },
402 | },
403 | edge: {
404 | buildStyles: (color: string) => {
405 | return {
406 | stroke: color,
407 | };
408 | },
409 | type: 'default',
410 | },
411 | },
412 | {
413 | id: 'palette-11',
414 | name: 'Palette 11',
415 | colors: ['#03071e', '#370617', '#6a040f', '#9d0208', '#d00000'],
416 | root: {
417 | buildStyles: () => {
418 | return {
419 | backgroundColor: '#fff',
420 | color: '#1E293B',
421 | borderStyle: 'solid',
422 | borderWidth: 1,
423 | borderColor: '#03071e',
424 | borderRadius: '10px',
425 | };
426 | },
427 | },
428 | node: {
429 | buildStyles: (color: string) => {
430 | return {
431 | borderStyle: 'solid',
432 | borderWidth: 1,
433 | borderColor: color,
434 | backgroundColor: color,
435 | color: fontColorContrast(color),
436 | borderRadius: '10px',
437 | };
438 | },
439 | },
440 | edge: {
441 | buildStyles: (color: string) => {
442 | return {
443 | stroke: color,
444 | };
445 | },
446 | type: 'default',
447 | },
448 | },
449 | {
450 | id: 'palette-12',
451 | name: 'Palette 12',
452 | colors: ['#2b2d42', '#8d99ae', '#edf2f4', '#ef233c', '#d90429'],
453 | root: {
454 | buildStyles: () => {
455 | return {
456 | backgroundColor: '#fff',
457 | color: '#1E293B',
458 | borderStyle: 'solid',
459 | borderWidth: 1,
460 | borderColor: '#2b2d42',
461 | borderRadius: '10px',
462 | };
463 | },
464 | },
465 | node: {
466 | buildStyles: (color: string) => {
467 | return {
468 | borderStyle: 'solid',
469 | borderWidth: 1,
470 | borderColor: color,
471 | backgroundColor: color,
472 | color: fontColorContrast(color),
473 | borderRadius: '10px',
474 | };
475 | },
476 | },
477 | edge: {
478 | buildStyles: (color: string) => {
479 | return {
480 | stroke: color,
481 | };
482 | },
483 | type: 'default',
484 | },
485 | },
486 | {
487 | id: 'palette-13',
488 | name: 'Palette 13',
489 | colors: ['#d8e2dc', '#ffe5d9', '#ffcad4', '#f4acb7', '#9d8189'],
490 | root: {
491 | buildStyles: () => {
492 | return {
493 | backgroundColor: '#fff',
494 | color: '#1E293B',
495 | borderStyle: 'solid',
496 | borderWidth: 1,
497 | borderColor: '#d8e2dc',
498 | borderRadius: '10px',
499 | };
500 | },
501 | },
502 | node: {
503 | buildStyles: (color: string) => {
504 | return {
505 | borderStyle: 'solid',
506 | borderWidth: 1,
507 | borderColor: color,
508 | backgroundColor: color,
509 | color: fontColorContrast(color),
510 | borderRadius: '10px',
511 | };
512 | },
513 | },
514 | edge: {
515 | buildStyles: (color: string) => {
516 | return {
517 | stroke: color,
518 | };
519 | },
520 | type: 'default',
521 | },
522 | },
523 | {
524 | id: 'palette-14',
525 | name: 'Palette 14',
526 | colors: ['#f08080', '#f4978e', '#f8ad9d', '#fbc4ab', '#ffdab9'],
527 | root: {
528 | buildStyles: () => {
529 | return {
530 | backgroundColor: '#fff',
531 | color: '#1E293B',
532 | borderStyle: 'solid',
533 | borderWidth: 1,
534 | borderColor: '#f08080',
535 | borderRadius: '10px',
536 | };
537 | },
538 | },
539 | node: {
540 | buildStyles: (color: string) => {
541 | return {
542 | borderStyle: 'solid',
543 | borderWidth: 1,
544 | borderColor: color,
545 | backgroundColor: color,
546 | color: fontColorContrast(color),
547 | borderRadius: '10px',
548 | };
549 | },
550 | },
551 | edge: {
552 | buildStyles: (color: string) => {
553 | return {
554 | stroke: color,
555 | };
556 | },
557 | type: 'default',
558 | },
559 | },
560 | {
561 | id: 'palette-15',
562 | name: 'Palette 15',
563 | colors: ['#f7b267', '#f79d65', '#f4845f', '#f27059', '#f25c54'],
564 | root: {
565 | buildStyles: () => {
566 | return {
567 | backgroundColor: '#fff',
568 | color: '#1E293B',
569 | borderStyle: 'solid',
570 | borderWidth: 1,
571 | borderColor: '#f7b267',
572 | borderRadius: '10px',
573 | };
574 | },
575 | },
576 | node: {
577 | buildStyles: (color: string) => {
578 | return {
579 | borderStyle: 'solid',
580 | borderWidth: 1,
581 | borderColor: color,
582 | backgroundColor: color,
583 | color: fontColorContrast(color),
584 | borderRadius: '10px',
585 | };
586 | },
587 | },
588 | edge: {
589 | buildStyles: (color: string) => {
590 | return {
591 | stroke: color,
592 | };
593 | },
594 | type: 'default',
595 | },
596 | },
597 | {
598 | id: 'palette-16',
599 | name: 'Palette 16',
600 | colors: ['#ffffff', '#84dcc6', '#a5ffd6', '#ffa69e', '#ff686b'],
601 | root: {
602 | buildStyles: () => {
603 | return {
604 | backgroundColor: '#fff',
605 | color: '#1E293B',
606 | borderStyle: 'solid',
607 | borderWidth: 1,
608 | borderColor: '#ffffff',
609 | borderRadius: '10px',
610 | };
611 | },
612 | },
613 | node: {
614 | buildStyles: (color: string) => {
615 | return {
616 | borderStyle: 'solid',
617 | borderWidth: 1,
618 | borderColor: color,
619 | backgroundColor: color,
620 | color: fontColorContrast(color),
621 | borderRadius: '10px',
622 | };
623 | },
624 | },
625 | edge: {
626 | buildStyles: (color: string) => {
627 | return {
628 | stroke: color,
629 | };
630 | },
631 | type: 'default',
632 | },
633 | },
634 | {
635 | id: 'palette-17',
636 | name: 'Palette 17',
637 | colors: ['#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51'],
638 | root: {
639 | buildStyles: () => {
640 | return {
641 | backgroundColor: '#fff',
642 | color: '#1E293B',
643 | borderStyle: 'solid',
644 | borderWidth: 1,
645 | borderColor: '#264653',
646 | borderRadius: '10px',
647 | };
648 | },
649 | },
650 | node: {
651 | buildStyles: (color: string) => {
652 | return {
653 | borderStyle: 'solid',
654 | borderWidth: 1,
655 | borderColor: color,
656 | backgroundColor: color,
657 | color: fontColorContrast(color),
658 | borderRadius: '10px',
659 | };
660 | },
661 | },
662 | edge: {
663 | buildStyles: (color: string) => {
664 | return {
665 | stroke: color,
666 | };
667 | },
668 | type: 'default',
669 | },
670 | },
671 | {
672 | id: 'palette-18',
673 | name: 'Palette 18',
674 | colors: ['#606c38', '#283618', '#fefae0', '#dda15e', '#bc6c25'],
675 | root: {
676 | buildStyles: () => {
677 | return {
678 | backgroundColor: '#fff',
679 | color: '#1E293B',
680 | borderStyle: 'solid',
681 | borderWidth: 1,
682 | borderColor: '#606c38',
683 | borderRadius: '10px',
684 | };
685 | },
686 | },
687 | node: {
688 | buildStyles: (color: string) => {
689 | return {
690 | borderStyle: 'solid',
691 | borderWidth: 1,
692 | borderColor: color,
693 | backgroundColor: color,
694 | color: fontColorContrast(color),
695 | borderRadius: '10px',
696 | };
697 | },
698 | },
699 | edge: {
700 | buildStyles: (color: string) => {
701 | return {
702 | stroke: color,
703 | };
704 | },
705 | type: 'default',
706 | },
707 | },
708 | {
709 | id: 'palette-19',
710 | name: 'Palette 19',
711 | colors: ['#ff9f1c', '#ffbf69', '#ffffff', '#cbf3f0', '#2ec4b6'],
712 | root: {
713 | buildStyles: () => {
714 | return {
715 | backgroundColor: '#fff',
716 | color: '#1E293B',
717 | borderStyle: 'solid',
718 | borderWidth: 1,
719 | borderColor: '#ff9f1c',
720 | borderRadius: '10px',
721 | };
722 | },
723 | },
724 | node: {
725 | buildStyles: (color: string) => {
726 | return {
727 | borderStyle: 'solid',
728 | borderWidth: 1,
729 | borderColor: color,
730 | backgroundColor: color,
731 | color: fontColorContrast(color),
732 | borderRadius: '10px',
733 | };
734 | },
735 | },
736 | edge: {
737 | buildStyles: (color: string) => {
738 | return {
739 | stroke: color,
740 | };
741 | },
742 | type: 'default',
743 | },
744 | },
745 | {
746 | id: 'palette-20',
747 | name: 'Palette 20',
748 | colors: ['#5f0f40', '#9a031e', '#fb8b24', '#e36414', '#0f4c5c'],
749 | root: {
750 | buildStyles: () => {
751 | return {
752 | backgroundColor: '#fff',
753 | color: '#1E293B',
754 | borderStyle: 'solid',
755 | borderWidth: 1,
756 | borderColor: '#5f0f40',
757 | borderRadius: '10px',
758 | };
759 | },
760 | },
761 | node: {
762 | buildStyles: (color: string) => {
763 | return {
764 | borderStyle: 'solid',
765 | borderWidth: 1,
766 | borderColor: color,
767 | backgroundColor: color,
768 | color: fontColorContrast(color),
769 | borderRadius: '10px',
770 | };
771 | },
772 | },
773 | edge: {
774 | buildStyles: (color: string) => {
775 | return {
776 | stroke: color,
777 | };
778 | },
779 | type: 'default',
780 | },
781 | },
782 | {
783 | id: 'palette-21',
784 | name: 'Palette 21',
785 | colors: ['#70d6ff', '#ff70a6', '#ff9770', '#ffd670', '#e9ff70'],
786 | root: {
787 | buildStyles: () => {
788 | return {
789 | backgroundColor: '#fff',
790 | color: '#1E293B',
791 | borderStyle: 'solid',
792 | borderWidth: 1,
793 | borderColor: '#70d6ff',
794 | borderRadius: '10px',
795 | };
796 | },
797 | },
798 | node: {
799 | buildStyles: (color: string) => {
800 | return {
801 | borderStyle: 'solid',
802 | borderWidth: 1,
803 | borderColor: color,
804 | backgroundColor: color,
805 | color: fontColorContrast(color),
806 | borderRadius: '10px',
807 | };
808 | },
809 | },
810 | edge: {
811 | buildStyles: (color: string) => {
812 | return {
813 | stroke: color,
814 | };
815 | },
816 | type: 'default',
817 | },
818 | },
819 | {
820 | id: 'palette-22',
821 | name: 'Palette 22',
822 | colors: ['#55dde0', '#33658a', '#2f4858', '#f6ae2d', '#f26419'],
823 | root: {
824 | buildStyles: () => {
825 | return {
826 | backgroundColor: '#fff',
827 | color: '#1E293B',
828 | borderStyle: 'solid',
829 | borderWidth: 1,
830 | borderColor: '#55dde0',
831 | borderRadius: '10px',
832 | };
833 | },
834 | },
835 | node: {
836 | buildStyles: (color: string) => {
837 | return {
838 | borderStyle: 'solid',
839 | borderWidth: 1,
840 | borderColor: color,
841 | backgroundColor: color,
842 | color: fontColorContrast(color),
843 | borderRadius: '10px',
844 | };
845 | },
846 | },
847 | edge: {
848 | buildStyles: (color: string) => {
849 | return {
850 | stroke: color,
851 | };
852 | },
853 | type: 'default',
854 | },
855 | },
856 | {
857 | id: 'palette-23',
858 | name: 'Palette 23',
859 | colors: ['#000814', '#001d3d', '#003566', '#ffc300', '#ffd60a'],
860 | root: {
861 | buildStyles: () => {
862 | return {
863 | backgroundColor: '#fff',
864 | color: '#1E293B',
865 | borderStyle: 'solid',
866 | borderWidth: 1,
867 | borderColor: '#000814',
868 | borderRadius: '10px',
869 | };
870 | },
871 | },
872 | node: {
873 | buildStyles: (color: string) => {
874 | return {
875 | borderStyle: 'solid',
876 | borderWidth: 1,
877 | borderColor: color,
878 | backgroundColor: color,
879 | color: fontColorContrast(color),
880 | borderRadius: '10px',
881 | };
882 | },
883 | },
884 | edge: {
885 | buildStyles: (color: string) => {
886 | return {
887 | stroke: color,
888 | };
889 | },
890 | type: 'default',
891 | },
892 | },
893 | {
894 | id: 'palette-24',
895 | name: 'Palette 24',
896 | colors: ['#007f5f', '#2b9348', '#55a630', '#80b918', '#aacc00'],
897 | root: {
898 | buildStyles: () => {
899 | return {
900 | backgroundColor: '#fff',
901 | color: '#1E293B',
902 | borderStyle: 'solid',
903 | borderWidth: 1,
904 | borderColor: '#007f5f',
905 | borderRadius: '10px',
906 | };
907 | },
908 | },
909 | node: {
910 | buildStyles: (color: string) => {
911 | return {
912 | borderStyle: 'solid',
913 | borderWidth: 1,
914 | borderColor: color,
915 | backgroundColor: color,
916 | color: fontColorContrast(color),
917 | borderRadius: '10px',
918 | };
919 | },
920 | },
921 | edge: {
922 | buildStyles: (color: string) => {
923 | return {
924 | stroke: color,
925 | };
926 | },
927 | type: 'default',
928 | },
929 | },
930 | {
931 | id: 'palette-25',
932 | name: 'Palette 25',
933 | colors: ['#ff7b00', '#ff8800', '#ff9500', '#ffa200', '#ffaa00'],
934 | root: {
935 | buildStyles: () => {
936 | return {
937 | backgroundColor: '#fff',
938 | color: '#1E293B',
939 | borderStyle: 'solid',
940 | borderWidth: 1,
941 | borderColor: '#ff7b00',
942 | borderRadius: '10px',
943 | };
944 | },
945 | },
946 | node: {
947 | buildStyles: (color: string) => {
948 | return {
949 | borderStyle: 'solid',
950 | borderWidth: 1,
951 | borderColor: color,
952 | backgroundColor: color,
953 | color: fontColorContrast(color),
954 | borderRadius: '10px',
955 | };
956 | },
957 | },
958 | edge: {
959 | buildStyles: (color: string) => {
960 | return {
961 | stroke: color,
962 | };
963 | },
964 | type: 'default',
965 | },
966 | },
967 | {
968 | id: 'palette-26',
969 | name: 'Palette 26',
970 | colors: ['#00296b', '#003f88', '#00509d', '#fdc500', '#ffd500'],
971 | root: {
972 | buildStyles: () => {
973 | return {
974 | backgroundColor: '#fff',
975 | color: '#1E293B',
976 | borderStyle: 'solid',
977 | borderWidth: 1,
978 | borderColor: '#00296b',
979 | borderRadius: '10px',
980 | };
981 | },
982 | },
983 | node: {
984 | buildStyles: (color: string) => {
985 | return {
986 | borderStyle: 'solid',
987 | borderWidth: 1,
988 | borderColor: color,
989 | backgroundColor: color,
990 | color: fontColorContrast(color),
991 | borderRadius: '10px',
992 | };
993 | },
994 | },
995 | edge: {
996 | buildStyles: (color: string) => {
997 | return {
998 | stroke: color,
999 | };
1000 | },
1001 | type: 'default',
1002 | },
1003 | },
1004 | ];
1005 |
--------------------------------------------------------------------------------
/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from 'react';
3 |
4 | import { ToastActionElement, type ToastProps } from '@/components/ui/Toast';
5 |
6 | const TOAST_LIMIT = 1;
7 | const TOAST_REMOVE_DELAY = 1000;
8 |
9 | type ToasterToast = ToastProps & {
10 | id: string;
11 | title?: React.ReactNode;
12 | description?: React.ReactNode;
13 | action?: ToastActionElement;
14 | };
15 |
16 | const actionTypes = {
17 | ADD_TOAST: 'ADD_TOAST',
18 | UPDATE_TOAST: 'UPDATE_TOAST',
19 | DISMISS_TOAST: 'DISMISS_TOAST',
20 | REMOVE_TOAST: 'REMOVE_TOAST',
21 | } as const;
22 |
23 | let count = 0;
24 |
25 | function genId() {
26 | count = (count + 1) % Number.MAX_VALUE;
27 | return count.toString();
28 | }
29 |
30 | type ActionType = typeof actionTypes;
31 |
32 | type Action =
33 | | {
34 | type: ActionType['ADD_TOAST'];
35 | toast: ToasterToast;
36 | }
37 | | {
38 | type: ActionType['UPDATE_TOAST'];
39 | toast: Partial;
40 | }
41 | | {
42 | type: ActionType['DISMISS_TOAST'];
43 | toastId?: ToasterToast['id'];
44 | }
45 | | {
46 | type: ActionType['REMOVE_TOAST'];
47 | toastId?: ToasterToast['id'];
48 | };
49 |
50 | interface State {
51 | toasts: ToasterToast[];
52 | }
53 |
54 | const toastTimeouts = new Map>();
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return;
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId);
63 | dispatch({
64 | type: 'REMOVE_TOAST',
65 | toastId: toastId,
66 | });
67 | }, TOAST_REMOVE_DELAY);
68 |
69 | toastTimeouts.set(toastId, timeout);
70 | };
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case 'ADD_TOAST':
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | };
79 |
80 | case 'UPDATE_TOAST':
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
84 | };
85 |
86 | case 'DISMISS_TOAST': {
87 | const { toastId } = action;
88 |
89 | // ! Side effects ! - This could be extracted into a dismissToast() action,
90 | // but I'll keep it here for simplicity
91 | if (toastId) {
92 | addToRemoveQueue(toastId);
93 | } else {
94 | state.toasts.forEach((toast) => {
95 | addToRemoveQueue(toast.id);
96 | });
97 | }
98 |
99 | return {
100 | ...state,
101 | toasts: state.toasts.map((t) =>
102 | t.id === toastId || toastId === undefined
103 | ? {
104 | ...t,
105 | open: false,
106 | }
107 | : t,
108 | ),
109 | };
110 | }
111 | case 'REMOVE_TOAST':
112 | if (action.toastId === undefined) {
113 | return {
114 | ...state,
115 | toasts: [],
116 | };
117 | }
118 | return {
119 | ...state,
120 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
121 | };
122 | }
123 | };
124 |
125 | const listeners: Array<(state: State) => void> = [];
126 |
127 | let memoryState: State = { toasts: [] };
128 |
129 | function dispatch(action: Action) {
130 | memoryState = reducer(memoryState, action);
131 | listeners.forEach((listener) => {
132 | listener(memoryState);
133 | });
134 | }
135 |
136 | interface Toast extends Omit {}
137 |
138 | function toast({ ...props }: Toast) {
139 | const id = genId();
140 |
141 | const update = (props: ToasterToast) =>
142 | dispatch({
143 | type: 'UPDATE_TOAST',
144 | toast: { ...props, id },
145 | });
146 | const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
147 |
148 | dispatch({
149 | type: 'ADD_TOAST',
150 | toast: {
151 | ...props,
152 | id,
153 | open: true,
154 | onOpenChange: (open) => {
155 | if (!open) dismiss();
156 | },
157 | },
158 | });
159 |
160 | return {
161 | id: id,
162 | dismiss,
163 | update,
164 | };
165 | }
166 |
167 | function useToast() {
168 | const [state, setState] = React.useState(memoryState);
169 |
170 | React.useEffect(() => {
171 | listeners.push(setState);
172 | return () => {
173 | const index = listeners.indexOf(setState);
174 | if (index > -1) {
175 | listeners.splice(index, 1);
176 | }
177 | };
178 | }, [state]);
179 |
180 | return {
181 | ...state,
182 | toast,
183 | dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
184 | };
185 | }
186 |
187 | export { useToast, toast };
188 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | appDir: true,
5 | },
6 | images: {
7 | remotePatterns: [
8 | {
9 | protocol: 'https',
10 | hostname: 'res.cloudinary.com',
11 | },
12 | ],
13 | },
14 | };
15 |
16 | module.exports = nextConfig;
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mentalist-ai",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "format": "prettier --ignore-path .gitignore --write ."
11 | },
12 | "dependencies": {
13 | "@next/font": "13.1.6",
14 | "@radix-ui/react-aspect-ratio": "1.0.1",
15 | "@radix-ui/react-dialog": "1.0.2",
16 | "@radix-ui/react-dropdown-menu": "2.0.2",
17 | "@radix-ui/react-label": "2.0.0",
18 | "@radix-ui/react-menubar": "1.0.0",
19 | "@radix-ui/react-popover": "1.0.3",
20 | "@radix-ui/react-select": "1.2.0",
21 | "@radix-ui/react-toast": "1.1.2",
22 | "@vercel/analytics": "0.1.10",
23 | "class-variance-authority": "0.4.0",
24 | "classnames": "2.3.2",
25 | "dagre": "0.8.5",
26 | "lucide-react": "0.109.0",
27 | "next": "13.1.6",
28 | "openai": "3.2.1",
29 | "react": "18.2.0",
30 | "react-dom": "18.2.0",
31 | "react-use": "17.4.0",
32 | "reactflow": "11.5.3",
33 | "zustand": "4.3.2",
34 | "browser-fs-access": "0.33.0",
35 | "@radix-ui/react-toggle-group": "1.0.3",
36 | "@radix-ui/react-tooltip": "1.0.5",
37 | "tailwindcss-radix": "2.8.0",
38 | "@radix-ui/react-separator": "1.0.2",
39 | "graphology": "0.25.1",
40 | "font-color-contrast": "11.1.0"
41 | },
42 | "devDependencies": {
43 | "@types/dagre": "0.7.48",
44 | "@types/node": "18.11.18",
45 | "@types/react": "18.0.27",
46 | "@types/react-dom": "18.0.10",
47 | "autoprefixer": "10.4.13",
48 | "eslint": "8.33.0",
49 | "eslint-config-next": "13.1.6",
50 | "postcss": "8.4.21",
51 | "prettier": "^2.8.4",
52 | "prisma": "4.9.0",
53 | "tailwindcss": "3.2.4",
54 | "typescript": "4.9.5",
55 | "graphology-types": "0.24.7"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/pages/api/ideas.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import { AVAILABLE_MODELS, isAvailableModel } from '@/utils/constants/openai';
3 | import { generateContentIdeas } from '@/utils/openai/topics';
4 | import type { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | type Data = {
7 | ideas: Array;
8 | };
9 |
10 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
11 | const { main = '', context = [], token = '', model, accurateFor, type } = req.body;
12 |
13 | if (!main) {
14 | return res.status(200).json({ ideas: [] });
15 | }
16 |
17 | const ideas = await generateContentIdeas({
18 | topic: main,
19 | context,
20 | token,
21 | accurateFor,
22 | type,
23 | model: isAvailableModel(model) ? model : AVAILABLE_MODELS[0],
24 | });
25 |
26 | res.status(200).json({ ideas });
27 | }
28 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/process-env.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace NodeJS {
2 | interface ProcessEnv {
3 | NEXT_PUBLIC_OPENAI_API_KEY?: string;
4 | NEXT_PUBLIC_OPENAI_COMPLETION_MODEL?: string;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/public/app-img.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fernandops26/mentalist-ai/232ef1d09c32140bd0d4393c7ac74343bbd20568/public/app-img.jpg
--------------------------------------------------------------------------------
/public/app-map.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fernandops26/mentalist-ai/232ef1d09c32140bd0d4393c7ac74343bbd20568/public/app-map.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fernandops26/mentalist-ai/232ef1d09c32140bd0d4393c7ac74343bbd20568/public/favicon.ico
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/stores/mapStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import {
3 | applyEdgeChanges,
4 | applyNodeChanges,
5 | Edge,
6 | EdgeChange,
7 | Node,
8 | NodeChange,
9 | OnNodesChange,
10 | OnEdgesChange,
11 | OnConnectStartParams,
12 | ReactFlowInstance,
13 | Viewport,
14 | } from 'reactflow';
15 | import Graph from 'graphology';
16 |
17 | import { findLeafNodes, generateEdges, generateNodes } from '@/utils/node';
18 | import { MouseEvent as ReactMouseEvent, RefObject, TouchEvent as ReactTouchEvent } from 'react';
19 | import { SUBTOPIC } from '@/utils/constants/headerTypes';
20 | import { nextId } from '@/utils/id';
21 | import { getConfigKey, loadMapData, updateConfig } from '@/utils/storage';
22 | import { PaletteElement } from '@/utils/types';
23 | import { palettes } from '@/data/defaultPalettes';
24 |
25 | type ColorMap = {
26 | [key: string]: string;
27 | };
28 |
29 | export type RFState = {
30 | instance: ReactFlowInstance | null;
31 | onInit: (instance: ReactFlowInstance) => void;
32 | nodes: Node[];
33 | edges: Edge[];
34 | loadFromStorage: () => void;
35 | viewport: Viewport;
36 | connectionNodeId: string | null;
37 | reactFlowWrapper: RefObject | null;
38 | setReactFlowWrapper: (ref: RefObject) => void;
39 | onNodesChange: OnNodesChange;
40 | onEdgesChange: OnEdgesChange;
41 | onConnectStart: (event: ReactMouseEvent | ReactTouchEvent, params: OnConnectStartParams) => void;
42 | onConnectEnd: (event: MouseEvent | TouchEvent) => void;
43 | updateText: (nodeId: string, text: string) => void;
44 | updateInnerType: (nodeId: string, text: string) => void;
45 | addChildrenNodes: (nodeId: string, type: string, data: Array) => void;
46 | getNodeContext: (nodeId: string) => { main: string; context: Array };
47 | removeElement: (nodeId: string) => void;
48 | applyPalette: (palette: PaletteElement) => void;
49 | };
50 |
51 | // const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
52 | // data.nodes,
53 | // data.edges
54 | // );
55 |
56 | const useMapStore = create((set, get) => ({
57 | instance: null,
58 | onInit: (instance: ReactFlowInstance) => {
59 | set({
60 | instance,
61 | });
62 |
63 | get().loadFromStorage();
64 | },
65 | viewport: {
66 | x: 0,
67 | y: 0,
68 | zoom: 1,
69 | },
70 | nodes: [],
71 | edges: [],
72 | reactFlowWrapper: null,
73 | connectionNodeId: null,
74 | loadFromStorage: () => {
75 | const data = loadMapData();
76 |
77 | const instance = get().instance;
78 | if (!instance) {
79 | return;
80 | }
81 |
82 | set({
83 | nodes: data.nodes,
84 | edges: data.edges,
85 | viewport: data.viewport,
86 | });
87 |
88 | const selectedPaletteId = getConfigKey('palette');
89 | let selectedPalette = palettes.find((palette) => palette.id === selectedPaletteId)!;
90 |
91 | get().applyPalette(selectedPalette);
92 | },
93 | getNodeContext: (nodeId: string) => {
94 | const nodes = findLeafNodes(get().nodes, nodeId).reverse();
95 |
96 | const rootNode = nodes.shift()!;
97 | const main = rootNode.data.text;
98 | const context = nodes.map((node) => node.data.text);
99 |
100 | return {
101 | main,
102 | context,
103 | };
104 | },
105 | setReactFlowWrapper: (ref: RefObject) => {
106 | set({
107 | reactFlowWrapper: ref,
108 | });
109 | },
110 | onNodesChange: (changes: Array) => {
111 | const sureChanges = changes.filter((change) => !(change.type === 'remove' && change.id === 'root'));
112 | const newNodes = applyNodeChanges(sureChanges, get().nodes);
113 |
114 | set({
115 | nodes: newNodes,
116 | });
117 | },
118 | onEdgesChange: (changes: Array) => {
119 | const newEdges = applyEdgeChanges(changes, get().edges);
120 | set({
121 | edges: newEdges,
122 | });
123 | },
124 | onConnectStart: (event: ReactMouseEvent | ReactTouchEvent, params: OnConnectStartParams) => {
125 | if ((event.target as any).classList.contains('source')) {
126 | set({
127 | connectionNodeId: params.nodeId,
128 | });
129 | }
130 | },
131 | onConnectEnd: (event: any) => {
132 | const targetIsPane = event.target.classList.contains('react-flow__pane');
133 | const connectingNodeId = get().connectionNodeId!;
134 | const reactFlowWrapper = get().reactFlowWrapper;
135 |
136 | const selectedPaletteId = getConfigKey('palette');
137 | let selectedPalette = palettes.find((palette) => palette.id === selectedPaletteId)!;
138 |
139 | if (targetIsPane && connectingNodeId && reactFlowWrapper?.current) {
140 | // we need to remove the wrapper bounds, in order to get the correct position
141 | const { top, left } = reactFlowWrapper.current.getBoundingClientRect();
142 |
143 | const { project } = get().instance!;
144 | const id = nextId();
145 |
146 | const newNode: Node = {
147 | id,
148 | type: 'topicNode',
149 | // we are removing the half of the node width (75) to center the new node
150 | position: project({
151 | x: event.clientX - left - 100,
152 | y: event.clientY - top,
153 | }),
154 | data: {
155 | text: 'Subtopic title',
156 | type: SUBTOPIC,
157 | parentId: connectingNodeId,
158 | },
159 | };
160 |
161 | set({
162 | nodes: get().nodes.concat(newNode),
163 | edges: get().edges.concat({
164 | id: nextId(),
165 | source: connectingNodeId,
166 | target: id,
167 | }),
168 | connectionNodeId: null,
169 | });
170 |
171 | get().applyPalette(selectedPalette);
172 | }
173 | },
174 | updateInnerType: (nodeId: string, type: string) => {
175 | const newNodes = get().nodes.map((node) => {
176 | if (node.id === nodeId) {
177 | node.data = { ...node.data, type };
178 | }
179 |
180 | return node;
181 | });
182 |
183 | set({
184 | nodes: newNodes,
185 | });
186 | },
187 | updateText: (nodeId: string, text: string) => {
188 | const newNodes = get().nodes.map((node) => {
189 | if (node.id === nodeId) {
190 | node.data = { ...node.data, text };
191 | }
192 |
193 | return node;
194 | });
195 |
196 | set({
197 | nodes: newNodes,
198 | });
199 | },
200 | addChildrenNodes: (nodeId: string, type: string, data: Array) => {
201 | const node = get().nodes.find((node) => node.id == nodeId)!;
202 |
203 | const selectedPaletteId = getConfigKey('palette');
204 | let selectedPalette = palettes.find((palette) => palette.id === selectedPaletteId)!;
205 |
206 | if (data.length === 0) {
207 | return;
208 | }
209 |
210 | const newNodes = generateNodes(type, node, data);
211 |
212 | const newEdges = generateEdges(node.id, newNodes);
213 |
214 | const allNodes = [...get().nodes, ...newNodes];
215 | const allEdges = [...get().edges, ...newEdges];
216 |
217 | set({ nodes: [...allNodes] });
218 | set({ edges: [...allEdges] });
219 |
220 | get().applyPalette(selectedPalette);
221 | },
222 | removeElement: (nodeId: string) => {
223 | const node = get().nodes.find((node) => node.id == nodeId)!;
224 |
225 | const instance = get().instance;
226 | if (!instance) {
227 | return;
228 | }
229 |
230 | instance.deleteElements({ nodes: [node] });
231 | },
232 | applyPalette: (palette: PaletteElement) => {
233 | const instance = get().instance;
234 | if (!instance) {
235 | return;
236 | }
237 |
238 | const graph = new Graph();
239 |
240 | const nodes = get().nodes;
241 | const edges = get().edges;
242 |
243 | nodes.forEach((node) => {
244 | graph.addNode(node.id, node);
245 | });
246 |
247 | edges.forEach((edge) => {
248 | graph.addEdge(edge.source, edge.target, {
249 | ...edge,
250 | });
251 | });
252 |
253 | const colorMap: ColorMap = {};
254 | let colorIndex = 0;
255 |
256 | graph.forEachNode((nodeId, attributes) => {
257 | const parentNodeAttributes = attributes.data.parentId ? graph.getNodeAttributes(attributes.data.parentId) : null;
258 |
259 | const parentColor = parentNodeAttributes ? colorMap[parentNodeAttributes.id] : null;
260 |
261 | if (nodeId === 'root') {
262 | graph.setNodeAttribute(nodeId, 'style', {
263 | ...attributes.style,
264 | ...palette.root.buildStyles(),
265 | });
266 |
267 | return;
268 | }
269 |
270 | if (!parentColor) {
271 | colorMap[nodeId] = palette.colors[colorIndex];
272 | colorIndex = (colorIndex + 1) % palette.colors.length;
273 | } else {
274 | colorMap[nodeId] = parentColor;
275 | }
276 |
277 | const color = colorMap[nodeId];
278 |
279 | graph.setNodeAttribute(nodeId, 'style', {
280 | ...attributes.style,
281 | ...palette.node.buildStyles(color),
282 | });
283 | });
284 |
285 | graph.forEachEdge((edgeId, attributes) => {
286 | const sourceColor = colorMap[attributes.source];
287 | const targetColor = colorMap[attributes.target];
288 |
289 | const color = sourceColor || targetColor;
290 |
291 | graph.mergeEdgeAttributes(attributes.source, attributes.target, {
292 | ...attributes,
293 | type: 'customizable',
294 | animated: false,
295 | style: {
296 | ...attributes.style,
297 | ...palette.edge.buildStyles(color),
298 | },
299 | });
300 | });
301 |
302 | const exported = graph.export();
303 |
304 | const newNodes = exported.nodes.map((node) => node.attributes) as Node[];
305 | const newEdges = exported.edges.map((edge) => edge.attributes) as Edge[];
306 |
307 | set({ nodes: newNodes, edges: newEdges });
308 | },
309 | }));
310 |
311 | export default useMapStore;
312 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .react-flow .react-flow__handle {
6 | bottom: -10px;
7 | @apply rounded-full w-4 h-4;
8 | }
9 |
10 | .react-flow .react-flow__handle.source {
11 | @apply bg-white border-4 border-slate-800 rounded-full w-4 h-4;
12 | }
13 |
14 | .react-flow .react-flow__handle.target {
15 | @apply bg-transparent border-none w-4 h-4 border-transparent;
16 | }
17 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./app/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [require('tailwindcss-radix')()],
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "baseUrl": ".",
23 | "paths": {
24 | "@/*": ["./*"]
25 | }
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/utils/api/suggestions.ts:
--------------------------------------------------------------------------------
1 | import { AvailableModel } from '../constants/openai';
2 |
3 | const host = '';
4 |
5 | interface generateIdeasProps {
6 | main: string;
7 | context: Array;
8 | token: string;
9 | model: AvailableModel;
10 | accurateFor: string;
11 | type: string;
12 | }
13 |
14 | export const generateIdeas = async ({ main, context, token, model, accurateFor, type }: generateIdeasProps) => {
15 | const res = await fetch('/api/ideas', {
16 | method: 'POST',
17 | headers: {
18 | 'Content-Type': 'application/json',
19 | },
20 | body: JSON.stringify({ main, context, accurateFor, type, token, model }),
21 | });
22 |
23 | const content = await res.json();
24 |
25 | return content.ideas;
26 | };
27 |
--------------------------------------------------------------------------------
/utils/blob.ts:
--------------------------------------------------------------------------------
1 | import { MIME_TYPES } from './constants/export';
2 | import { isValidMentalistData } from './json';
3 |
4 | export const loadFromBlob = async (blob: Blob) => {
5 | const resolved = await loadDataFromBlob(blob);
6 |
7 | if (resolved.type !== MIME_TYPES.mentalist) {
8 | throw new Error("Couldn't load mentalist file");
9 | }
10 |
11 | return resolved.data;
12 | };
13 |
14 | const parseFileContents = async (blob: Blob | File) => {
15 | let contents: string = await new Promise((resolve) => {
16 | const reader = new FileReader();
17 | reader.readAsText(blob, 'utf8');
18 | reader.onloadend = () => {
19 | if (reader.readyState === FileReader.DONE) {
20 | resolve(reader.result as string);
21 | }
22 | };
23 | });
24 |
25 | return contents;
26 | };
27 |
28 | export const loadDataFromBlob = async (blob: Blob | File) => {
29 | const contents = await parseFileContents(blob);
30 | try {
31 | const data = JSON.parse(contents);
32 |
33 | if (isValidMentalistData(data)) {
34 | return {
35 | type: MIME_TYPES.mentalist,
36 | data: {
37 | type: data.type,
38 | version: data.version,
39 | map: data.map,
40 | config: data.config,
41 | },
42 | };
43 | }
44 | throw new Error('Invalid mentalist file');
45 | } catch (error: any) {
46 | console.error(error.message);
47 | throw new Error("Couldn't parse mentalist file");
48 | }
49 | };
50 |
51 | export type ValueOf = T[keyof T];
52 |
53 | export const createFile = (
54 | blob: File | Blob | ArrayBuffer,
55 | mimeType: ValueOf,
56 | name: string | undefined,
57 | ) => {
58 | return new File([blob], name || '', {
59 | type: mimeType,
60 | });
61 | };
62 |
63 | export const normalizeFile = async (file: File) => {
64 | if (!file.type) {
65 | if (file?.name?.endsWith('.mentalist')) {
66 | file = createFile(await blobToArrayBuffer(file), MIME_TYPES.mentalist, file.name);
67 | }
68 | }
69 |
70 | return file;
71 | };
72 |
73 | export const blobToArrayBuffer = (blob: Blob): Promise => {
74 | if ('arrayBuffer' in blob) {
75 | return blob.arrayBuffer();
76 | }
77 |
78 | return new Promise((resolve, reject) => {
79 | const reader = new FileReader();
80 | reader.onload = (event) => {
81 | if (!event.target?.result) {
82 | return reject(new Error("Couldn't convert blob to ArrayBuffer"));
83 | }
84 |
85 | resolve(event.target.result as ArrayBuffer);
86 | };
87 |
88 | reader.readAsArrayBuffer(blob);
89 | });
90 | };
91 |
--------------------------------------------------------------------------------
/utils/classnames.ts:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 |
3 | // interface ClassNames
4 | export default function cn(...classes: any) {
5 | return classnames(classes);
6 | }
7 |
--------------------------------------------------------------------------------
/utils/constants/export.ts:
--------------------------------------------------------------------------------
1 | export const MIME_TYPES = {
2 | mentalist: 'application/vnd.mentalist+json',
3 | json: 'application/json',
4 | } as const;
5 |
6 | export const EXPORT_DATA_TYPES = {
7 | mentalist: 'mentalist',
8 | } as const;
9 |
--------------------------------------------------------------------------------
/utils/constants/headerTypes.ts:
--------------------------------------------------------------------------------
1 | export const MAIN_TOPIC = '1';
2 | export const SUBTOPIC = '2';
3 | export const DETAIL = '3';
4 |
--------------------------------------------------------------------------------
/utils/constants/modes.ts:
--------------------------------------------------------------------------------
1 | export const SELECTION_MODE = 'selection';
2 | export const AI_MODE = 'ai';
3 |
4 | export const AVAILABLE_MODES = [SELECTION_MODE, AI_MODE] as const;
5 |
6 | export type AvailableModes = (typeof AVAILABLE_MODES)[number];
7 |
8 | export const isAvailableMode = (mode: string): mode is AvailableModes => {
9 | return AVAILABLE_MODES.includes(mode as AvailableModes);
10 | };
11 |
--------------------------------------------------------------------------------
/utils/constants/openai.ts:
--------------------------------------------------------------------------------
1 | export const AVAILABLE_MODELS = ['text-davinci-003', 'gpt-3.5-turbo'] as const;
2 |
3 | export type AvailableModel = (typeof AVAILABLE_MODELS)[number];
4 |
5 | export const isAvailableModel = (model: string): model is AvailableModel => {
6 | return AVAILABLE_MODELS.includes(model as AvailableModel);
7 | };
8 |
--------------------------------------------------------------------------------
/utils/constants/questions.ts:
--------------------------------------------------------------------------------
1 | const questions: Array = ['WILL', 'WHICH', 'WHY', 'WHAT', 'WHO', 'WHEN', 'WHERE', 'HOW', 'ARE', 'CAN'];
2 |
3 | export default questions;
4 |
--------------------------------------------------------------------------------
/utils/data.ts:
--------------------------------------------------------------------------------
1 | import { ReactFlowJsonObject } from 'reactflow';
2 | import { loadFromBlob, normalizeFile } from './blob';
3 | import { EXPORT_DATA_TYPES, MIME_TYPES } from './constants/export';
4 | import { fileOpen, fileSave } from './filesystem';
5 | import { Config, ExportedDataState, ImportedDataState, MapState } from './types';
6 |
7 | export const serializeAsJSON = (map: MapState, version: string, config: Config | undefined): string => {
8 | const data: ExportedDataState = {
9 | type: EXPORT_DATA_TYPES.mentalist,
10 | version,
11 | map,
12 | config,
13 | };
14 |
15 | return JSON.stringify(data, null, 2);
16 | };
17 |
18 | export const saveAsJSON = async (name: string, version: string, map: MapState, config: Config | undefined) => {
19 | const serialized = serializeAsJSON(map, version, config);
20 | const blob = new Blob([serialized], {
21 | type: MIME_TYPES.mentalist,
22 | });
23 |
24 | const fileHandle = await fileSave(blob, {
25 | name,
26 | extension: 'mentalist',
27 | description: 'Mentalist AI file',
28 | });
29 |
30 | return { fileHandle };
31 | };
32 |
33 | export const loadFromJSON = async (): Promise => {
34 | const file = await fileOpen({
35 | description: 'Mentalist AI files',
36 | });
37 |
38 | return loadFromBlob(await normalizeFile(file));
39 | };
40 |
--------------------------------------------------------------------------------
/utils/filesystem.ts:
--------------------------------------------------------------------------------
1 | import {
2 | fileOpen as _fileOpen,
3 | fileSave as _fileSave,
4 | supported as nativeFileSystemSupported,
5 | } from 'browser-fs-access';
6 | import { MIME_TYPES } from './constants/export';
7 |
8 | type FILE_EXTENSION = 'json' | 'mentalist';
9 |
10 | export const fileSave = (
11 | blob: Blob,
12 | opts: {
13 | /** name without the extension */
14 | name: string;
15 | extension: FILE_EXTENSION;
16 | description: string;
17 | },
18 | ) => {
19 | return _fileSave(blob, {
20 | fileName: `${opts.name}.${opts.extension}`,
21 | description: opts.description,
22 | extensions: [`.${opts.extension}`],
23 | });
24 | };
25 |
26 | export const fileOpen = (opts: {
27 | extensions?: FILE_EXTENSION[];
28 | description: string;
29 | multiple?: M;
30 | }): Promise => {
31 | type RetType = M extends false | undefined ? File : File[];
32 |
33 | const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
34 | mimeTypes.push(MIME_TYPES[type]);
35 |
36 | return mimeTypes;
37 | }, [] as string[]);
38 |
39 | const extensions = opts.extensions?.reduce((acc, ext) => {
40 | return acc.concat(`.${ext}`);
41 | }, [] as string[]);
42 |
43 | return _fileOpen({
44 | description: opts.description,
45 | extensions,
46 | mimeTypes,
47 | multiple: opts.multiple ?? false,
48 | }) as Promise;
49 | };
50 |
51 | export { nativeFileSystemSupported };
52 |
--------------------------------------------------------------------------------
/utils/id.ts:
--------------------------------------------------------------------------------
1 | export const nextId = (): string => {
2 | return crypto.randomUUID().toString();
3 | };
4 |
--------------------------------------------------------------------------------
/utils/json.ts:
--------------------------------------------------------------------------------
1 | import { EXPORT_DATA_TYPES } from './constants/export';
2 | import { ImportedDataState } from './types';
3 |
4 | export const isValidMentalistData = (data?: { type?: any; version?: any; map?: any }): data is ImportedDataState => {
5 | return (
6 | data?.type === EXPORT_DATA_TYPES.mentalist &&
7 | typeof data.map === 'object' &&
8 | Array.isArray(data.map.nodes) &&
9 | Array.isArray(data.map.edges)
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/utils/node.ts:
--------------------------------------------------------------------------------
1 | import { nextId } from './id';
2 | import { Edge, Node, Position } from 'reactflow';
3 | import dagre from 'dagre';
4 | import { PaletteElement, PaletteElementNodeStyles } from './types';
5 | import { CSSProperties } from 'react';
6 |
7 | export function generateNodes(type: string, parentNode: Node, data: Array) {
8 | const parentWidth = parentNode.width!;
9 | const elementsCount = data.length;
10 | const space = 100;
11 | const totalArea = elementsCount * parentWidth + space * (elementsCount - 1);
12 | const xStartPos = parentNode.position.x - totalArea / 2;
13 |
14 | let y = 200;
15 | let x = xStartPos - parentWidth;
16 |
17 | return data.map(function (item) {
18 | x += space + parentWidth;
19 |
20 | return {
21 | id: nextId(),
22 | position: {
23 | x,
24 | y: parentNode.position.y + y,
25 | },
26 | data: item,
27 | type,
28 | };
29 | });
30 | }
31 |
32 | export const generateEdges = (parentId: string, childrens: Array) => {
33 | return childrens.map((childrenNode) => {
34 | return {
35 | id: nextId(),
36 | source: parentId,
37 | target: childrenNode.id,
38 | animated: true,
39 | style: { stroke: '#1A192B' },
40 | };
41 | });
42 | };
43 |
44 | const dagreGraph = new dagre.graphlib.Graph();
45 | dagreGraph.setDefaultEdgeLabel(() => ({}));
46 |
47 | const nodeWidth = 240;
48 | const nodeHeight = 36;
49 |
50 | export const getLayoutedElements = (
51 | nodes: Array,
52 | edges: Array,
53 | direction = 'TB',
54 | ): { nodes: Array; edges: Array } => {
55 | const isHorizontal = direction === 'LR';
56 | dagreGraph.setGraph({ rankdir: direction });
57 |
58 | nodes.forEach((node) => {
59 | dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
60 | });
61 |
62 | edges.forEach((edge) => {
63 | dagreGraph.setEdge(edge.source, edge.target);
64 | });
65 |
66 | dagre.layout(dagreGraph);
67 |
68 | const newNodes: Array = nodes.map((node) => {
69 | const nodeWithPosition = dagreGraph.node(node.id);
70 | node.targetPosition = isHorizontal ? Position.Left : Position.Top;
71 | node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
72 |
73 | // We are shifting the dagre node position (anchor=center center) to the top left
74 | // so it matches the React Flow node anchor point (top left).
75 | node.position = {
76 | x: nodeWithPosition.x - nodeWidth / 2,
77 | y: nodeWithPosition.y - nodeHeight / 2,
78 | };
79 |
80 | return node;
81 | });
82 |
83 | return { nodes: newNodes, edges };
84 | };
85 |
86 | export const findLeafNodes = (nodes: Array, nodeId: string) => {
87 | const node = nodes.find((node) => node.id === nodeId)!;
88 |
89 | let tree = [node];
90 | if (node.data.parentId) {
91 | tree = tree.concat(findLeafNodes(nodes, node.data.parentId));
92 | }
93 |
94 | return tree;
95 | };
96 |
97 | export const getNodeColor = (depth: number, palette: PaletteElement) => {
98 | const colors = palette.colors;
99 | const index = depth % colors.length;
100 | const color = colors[index];
101 |
102 | const style = palette.node.buildStyles(color);
103 |
104 | return { style, color };
105 | };
106 |
107 | export const getEdgeColor = (depth: number, palette: PaletteElement) => {
108 | const colors = palette.colors;
109 | const index = depth % colors.length;
110 | const color = colors[index];
111 |
112 | const style = palette.edge.buildStyles(color);
113 |
114 | return { style, color };
115 | };
116 |
--------------------------------------------------------------------------------
/utils/openai/client.ts:
--------------------------------------------------------------------------------
1 | import { Configuration, OpenAIApi } from 'openai';
2 |
3 | const configuration = new Configuration({
4 | apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
5 | });
6 |
7 | export const getClient = (token: string) => {
8 | const config = new Configuration({
9 | apiKey: token,
10 | });
11 |
12 | return new OpenAIApi(config);
13 | };
14 |
15 | const OPEN_AI_API = new OpenAIApi(configuration);
16 |
17 | export default OPEN_AI_API;
18 |
--------------------------------------------------------------------------------
/utils/openai/topics.ts:
--------------------------------------------------------------------------------
1 | import { AvailableModel } from '../constants/openai';
2 | import { getClient } from './client';
3 |
4 | const CHAT_GPT_MODEL: AvailableModel = 'gpt-3.5-turbo';
5 |
6 | interface GenerateContentIdeasProps {
7 | topic: string;
8 | context: Array;
9 | token: string;
10 | model: AvailableModel;
11 | accurateFor: string;
12 | type: string;
13 | }
14 |
15 | export const generateContentIdeas = async ({
16 | topic,
17 | context,
18 | token,
19 | model = 'text-davinci-003',
20 | accurateFor,
21 | type,
22 | }: GenerateContentIdeasProps) => {
23 | let prompt = `Generate 3 items of ${type} for ${accurateFor} of topic "${topic}" in the context of subtopics "${context.join(
24 | ' > ',
25 | )}". Format them as list.`;
26 |
27 | console.log({ prompt });
28 |
29 | let ideaString = '';
30 |
31 | if (model === CHAT_GPT_MODEL) {
32 | const completion = await getClient(token).createChatCompletion({
33 | model,
34 | messages: [
35 | {
36 | role: 'user',
37 | content: prompt,
38 | },
39 | ],
40 | max_tokens: 180,
41 | temperature: 0.5,
42 | top_p: 1,
43 | frequency_penalty: 0,
44 | presence_penalty: 0,
45 | });
46 |
47 | ideaString = completion.data.choices[0].message?.content ?? '';
48 | } else {
49 | const completion = await getClient(token).createCompletion({
50 | model,
51 | prompt,
52 | max_tokens: 180,
53 | temperature: 0.5,
54 | top_p: 1,
55 | frequency_penalty: 0,
56 | presence_penalty: 0,
57 | });
58 |
59 | ideaString = completion.data.choices[0].text ?? '';
60 | }
61 |
62 | const ideas = ideaString
63 | .trim()
64 | .split('\n')
65 | .map((item: string) =>
66 | item
67 | .trim()
68 | .replace(/^\d+\. /, '')
69 | .replace(/'/g, '')
70 | .replace(/"/g, ''),
71 | )
72 | .filter((idea: string) => idea.trim().length !== 0);
73 |
74 | return ideas;
75 | };
76 |
--------------------------------------------------------------------------------
/utils/providers/ConfigurationProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { createContext, useCallback, useContext, useEffect, useState } from 'react';
4 | import { SELECTION_MODE, AvailableModes, isAvailableMode } from '../constants/modes';
5 | import { AvailableModel, isAvailableModel } from '../constants/openai';
6 | import { getLocalConfigKey, saveLocalConfigKey } from '../storage';
7 |
8 | export interface ConfigurationContextValue {
9 | updateToken: (token: string) => void;
10 | token: string;
11 | mode: AvailableModes | undefined;
12 |
13 | model: AvailableModel;
14 | updateModel: (model: AvailableModel) => void;
15 | updateMode: (mode: AvailableModes) => void;
16 | }
17 |
18 | const ConfigurationContext = createContext({
19 | updateToken: () => {},
20 | token: '',
21 | mode: undefined,
22 |
23 | model: 'text-davinci-003',
24 | updateModel: () => {},
25 | updateMode: () => {},
26 | });
27 |
28 | interface ConfigurationProviderProps {
29 | children: React.ReactNode;
30 | }
31 |
32 | export const ConfigurationProvider = ({ children }: ConfigurationProviderProps) => {
33 | const [token, setToken] = useState('');
34 | const [model, setModel] = useState('text-davinci-003');
35 | const [mode, setMode] = useState();
36 |
37 | useEffect(() => {
38 | const defaultMode = getLocalConfigKey('mode') ?? SELECTION_MODE;
39 | setMode(isAvailableMode(defaultMode) ? defaultMode : SELECTION_MODE);
40 |
41 | const defaultModel = getLocalConfigKey('model') ?? process.env.NEXT_PUBLIC_OPENAI_COMPLETION_MODEL!;
42 |
43 | const model = isAvailableModel(defaultModel) ? defaultModel : 'text-davinci-003';
44 |
45 | setModel(model);
46 |
47 | const defaultToken = getLocalConfigKey('openAI')! ?? process.env.NEXT_PUBLIC_OPENAI_API_KEY!;
48 | setToken(defaultToken);
49 | }, []);
50 |
51 | const onUpdateToken = useCallback((token: string) => {
52 | setToken(token);
53 | saveLocalConfigKey('openAI', token);
54 | }, []);
55 |
56 | const onUpdateModel = useCallback((model: AvailableModel) => {
57 | setModel(model);
58 | saveLocalConfigKey('model', model);
59 | }, []);
60 |
61 | const onUpdateMode = useCallback((mode: AvailableModes) => {
62 | setMode(mode);
63 | saveLocalConfigKey('mode', mode);
64 | }, []);
65 |
66 | const value = {
67 | updateToken: onUpdateToken,
68 | token,
69 | mode,
70 |
71 | model,
72 | updateModel: onUpdateModel,
73 | updateMode: onUpdateMode,
74 | };
75 |
76 | return {children};
77 | };
78 |
79 | export const useConfiguration = () => {
80 | return useContext(ConfigurationContext);
81 | };
82 |
--------------------------------------------------------------------------------
/utils/storage.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { ReactFlowJsonObject } from 'reactflow';
3 | import { initialNodes } from '@/data/defaultNodes';
4 | import { initialEdges } from '@/data/defaultEdges';
5 | import { Config, DataState, ImportedDataState, MapState } from './types';
6 | import { palettes } from '@/data/defaultPalettes';
7 |
8 | const KEY = 'mentalist-data';
9 | const TYPE = 'mentalist';
10 | const VERSION = 1;
11 |
12 | export const saveMap = (obj: ReactFlowJsonObject) => {
13 | saveProjectKey('map', formatMap(obj) as MapState);
14 | };
15 |
16 | export const saveProjectKey = (key: keyof DataState, value: any) => {
17 | const data = readFullContentObj();
18 |
19 | data[key] = value;
20 |
21 | localStorage.setItem(KEY, JSON.stringify(data));
22 | };
23 |
24 | export const loadMapData = (): ReactFlowJsonObject => {
25 | const data = typeof window !== 'undefined' && localStorage.getItem(KEY);
26 |
27 | if (!data) {
28 | return {
29 | nodes: initialNodes,
30 | edges: initialEdges,
31 | viewport: {
32 | x: 0,
33 | y: 0,
34 | zoom: 1,
35 | },
36 | };
37 | }
38 |
39 | const jsonObj = JSON.parse(data);
40 |
41 | if (Array.isArray(jsonObj) || !Object.hasOwn(jsonObj, 'type')) {
42 | // old version
43 | return jsonObj[0] as ReactFlowJsonObject;
44 | }
45 |
46 | return jsonObj.map as ReactFlowJsonObject;
47 | };
48 |
49 | export const readFullContentObj = (): DataState => {
50 | const data = typeof window !== 'undefined' && localStorage.getItem(KEY);
51 |
52 | const jsonData = JSON.parse(data as any);
53 |
54 | return formatObject(jsonData);
55 | };
56 |
57 | const formatObject = (data: any) => {
58 | return {
59 | type: data?.type || TYPE,
60 | version: data?.version || VERSION,
61 | map: data?.map || {
62 | nodes: [],
63 | edges: [],
64 | viewport: {
65 | x: 0,
66 | y: 0,
67 | zoom: 1,
68 | },
69 | },
70 | config: data?.config
71 | ? data?.config
72 | : {
73 | palette: palettes[0].id,
74 | },
75 | };
76 | };
77 |
78 | const formatMap = (obj: ReactFlowJsonObject) => {
79 | return {
80 | nodes: obj.nodes,
81 | edges: obj.edges,
82 | viewport: obj.viewport,
83 | };
84 | };
85 |
86 | export const updateConfig = (key: string, value: any) => {
87 | const data = readFullContentObj();
88 |
89 | data.config = {
90 | ...data.config,
91 | [key]: value,
92 | };
93 |
94 | saveProjectKey('config', data.config);
95 | };
96 |
97 | export const getConfigKey = (key: keyof Config): any => {
98 | const data = readFullContentObj();
99 |
100 | return data.config ? data.config[key] : null;
101 | };
102 |
103 | export const restoreProject = (dataState: ImportedDataState) => {
104 | localStorage.setItem(KEY, JSON.stringify(dataState));
105 | };
106 |
107 | const obj = {
108 | openAI: 'mentalist-openai-key',
109 | model: 'mentalist-model',
110 | mode: 'mentalist-mode',
111 | };
112 |
113 | export const saveLocalConfigKey = (key: keyof typeof obj, value: string): void => {
114 | const keyToUse: string = obj[key];
115 |
116 | localStorage.setItem(keyToUse, value);
117 | };
118 |
119 | export const getLocalConfigKey = (key: keyof typeof obj) => {
120 | const keyToUse: string = obj[key];
121 |
122 | return localStorage.getItem(keyToUse);
123 | };
124 |
--------------------------------------------------------------------------------
/utils/types.ts:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from 'react';
2 | import { ReactFlowJsonObject } from 'reactflow';
3 |
4 | export interface DataState {
5 | type: string;
6 | version: string;
7 | map: MapState;
8 | config?: Config;
9 | }
10 |
11 | export interface ExportedDataState {
12 | type: string;
13 | version: string;
14 | map: MapState;
15 | config?: Config;
16 | }
17 |
18 | export interface ImportedDataState {
19 | type?: string;
20 | version?: number;
21 | map?: MapState;
22 | config?: Config;
23 | }
24 |
25 | export interface MapState extends ReactFlowJsonObject {
26 | nodes: NodeElement[];
27 | edges: EdgeElement[];
28 | viewport: ViewportState;
29 | }
30 |
31 | interface NodeElement {
32 | width: number;
33 | height: number;
34 | id: string;
35 | data: any;
36 | position: {
37 | x: number;
38 | y: number;
39 | };
40 | type: string;
41 | selected: boolean;
42 | dragging: boolean;
43 | positionAbsolute: {
44 | x: number;
45 | y: number;
46 | };
47 | }
48 |
49 | interface EdgeElement {
50 | id: string;
51 | source: string;
52 | target: string;
53 | style: any;
54 | }
55 |
56 | interface ViewportState {
57 | x: number;
58 | y: number;
59 | zoom: number;
60 | }
61 |
62 | export interface PaletteElement {
63 | id: string;
64 | name: string;
65 | colors: string[];
66 | root: {
67 | buildStyles: () => PaletteElementNodeStyles;
68 | };
69 | node: {
70 | buildStyles: (color: string) => PaletteElementNodeStyles;
71 | };
72 | edge: {
73 | buildStyles: (color: string) => PaletteElementEdgeStyles;
74 | type: string;
75 | };
76 | }
77 |
78 | export interface PaletteElementNodeStyles extends CSSProperties {}
79 |
80 | export interface PaletteElementEdgeStyles extends CSSProperties {}
81 |
82 | export interface Config {
83 | palette?: string;
84 | }
85 |
86 | export interface LocalStorageKeys {
87 | openKey: 'mentalist-openaiKey';
88 | apiModel: 'mentalist-apiModel';
89 | }
90 |
--------------------------------------------------------------------------------