TheK3R1M commited on
Commit
fd4dc0d
·
verified ·
1 Parent(s): f22509b

Initial secure upload

Browse files
.gitignore CHANGED
@@ -1,23 +1,26 @@
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
- # production
12
- /build
13
-
14
- # misc
15
- .DS_Store
16
- .env.local
17
- .env.development.local
18
- .env.test.local
19
- .env.production.local
20
-
21
  npm-debug.log*
22
  yarn-debug.log*
23
  yarn-error.log*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  npm-debug.log*
5
  yarn-debug.log*
6
  yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+ .env
15
+ .env.*
16
+
17
+ # Editor directories and files
18
+ .vscode/*
19
+ !.vscode/extensions.json
20
+ .idea
21
+ .DS_Store
22
+ *.suo
23
+ *.ntvs*
24
+ *.njsproj
25
+ *.sln
26
+ *.sw?
App.tsx ADDED
The diff for this file is too large to render. See raw diff
 
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kerim
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.
README.md CHANGED
@@ -1,82 +1,76 @@
1
- ---
2
- title: MindSpark
3
- emoji: 🐠
4
- colorFrom: indigo
5
- colorTo: red
6
- sdk: static
7
- pinned: false
8
- app_build_command: npm run build
9
- app_file: build/index.html
10
- short_description: Ideas to professional workflows
11
- ---
12
-
13
- # Getting Started with Create React App
14
-
15
- This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
16
-
17
- ## Available Scripts
18
-
19
- In the project directory, you can run:
20
-
21
- ### `npm start`
22
-
23
- Runs the app in the development mode.\
24
- Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
25
-
26
- The page will reload when you make changes.\
27
- You may also see any lint errors in the console.
28
 
29
- ### `npm test`
30
 
31
- Launches the test runner in the interactive watch mode.\
32
- See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
33
 
34
- ### `npm run build`
35
-
36
- Builds the app for production to the `build` folder.\
37
- It correctly bundles React in production mode and optimizes the build for the best performance.
38
-
39
- The build is minified and the filenames include the hashes.\
40
- Your app is ready to be deployed!
41
 
42
- See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
43
 
44
- ### `npm run eject`
45
 
46
- **Note: this is a one-way operation. Once you `eject`, you can't go back!**
 
 
 
47
 
48
- If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
49
 
50
- Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
51
 
52
- You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
 
53
 
54
- ## Learn More
 
 
 
55
 
56
- You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
 
 
 
 
57
 
58
- To learn React, check out the [React documentation](https://reactjs.org/).
 
 
59
 
60
- ### Code Splitting
 
61
 
62
- This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
 
63
 
64
- ### Analyzing the Bundle Size
65
 
66
- This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
67
 
68
- ### Making a Progressive Web App
 
 
 
69
 
70
- This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
71
 
72
- ### Advanced Configuration
 
 
 
 
 
73
 
74
- This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
75
 
76
- ### Deployment
 
77
 
78
- This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
 
 
 
79
 
80
- ### `npm run build` fails to minify
81
 
82
- This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
 
1
+ # 🧠 MindSpark (AI Notebook)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ **MindSpark** is a next-generation AI-powered note-taking and project management platform that transforms your complex ideas into structured, actionable, and detailed project plans.
4
 
5
+ Powered by Google Gemini models (Gemini 2.5 Flash, Gemini 3 Flash Preview, and Image), this platform takes a simple idea and turns it into a professional workflow.
 
6
 
7
+ ---
 
 
 
 
 
 
8
 
9
+ ## 🎯 Purpose of the Application
10
 
11
+ Whether you want to write a novel, develop a new mobile app, start a business, or design an RPG game, you might not know "where to start." **The main goal of MindSpark is to eliminate this starting barrier and act as your "Board of Directors" while bringing your vision to life.**
12
 
13
+ You just express your idea (or tell it with your voice). MindSpark:
14
+ 1. **Breaks down** the steps needed to reach your goal.
15
+ 2. Assigns a **Virtual Agent** (e.g., Database Architect, Fiction Writer, UI/UX Designer) for each step as an expert in that task.
16
+ 3. Produces the **text, code, or vision image** you need through these agents in seconds.
17
 
18
+ ---
19
 
20
+ ## Key Features
21
 
22
+ ### 🕵️‍♂️ Dynamic Expert Agent Mechanism
23
+ A virtual team of experts is automatically established based on your project type. Tasks don't just stay as a list; they are assigned to agents with roles like *System Architect*, *Content Strategist*, or *Art Director*. You can see which agent took on that job in each note.
24
 
25
+ ### 🪄 Prompt Boost & Voice Input
26
+ Struggling to express your idea?
27
+ - **Voice to Text:** Just record your idea by speaking. AI transcribes it.
28
+ - **Boost:** Transform a short sentence like "I want to make a game" into a rich, technical, and creative command with one click.
29
 
30
+ ### 📦 Automatic Content and Asset Generation
31
+ AI generates content planned for each step:
32
+ - **Text:** Detailed tutorials, stories, strategies.
33
+ - **Code:** Actionable software architectures and code blocks.
34
+ - **Image:** High-quality AI-drawn concept art that Comic solidifies your idea.
35
 
36
+ ### ☁️ Cloud Sync and Offline Support
37
+ - Log in with your Google account and sync all your projects to the cloud via **Firebase**.
38
+ - Even when working offline, your browser memory (Local Storage) keeps your projects safe.
39
 
40
+ ### 📋 Drag, Drop, and Manage
41
+ Change the priorities of tasks (steps) with drag-and-drop, mark completed ones, add new tags, files, or audio recordings.
42
 
43
+ ### 📥 Export
44
+ Download your massive project file as a highly readable **PDF** document or a developer-friendly **Markdown (.md)** file.
45
 
46
+ ---
47
 
48
+ ## 🚀 How It Works?
49
 
50
+ 1. **Ignite Your Idea:** Type or speak "I want to design a sci-fi base desktop board game" in the box on the main screen.
51
+ 2. **AI Planning:** The intelligence behind it divides this huge idea into logical steps (e.g., Universe rules, Characters, Game mechanics, Design, etc.) and assigns them to agents.
52
+ 3. **Production:** MindSpark writes text, generates code, and draws images sequentially according to the content of each step.
53
+ 4. **Iteration:** Review the results, add your own notes, redraw images by giving feedback, or add new steps.
54
 
55
+ ---
56
 
57
+ ## 🛠️ Tech Stack
58
+ * **Frontend:** React 19, TypeScript, Vite
59
+ * **Styling & UI:** Tailwind CSS, Framer Motion (Animations), Lucide React (Icons)
60
+ * **AI:** `@google/genai` (Gemini 2.5 Flash, Gemini 3 Preview, Gemini 3 Flash Image)
61
+ * **Backend / Database:** Firebase (Auth, Firestore)
62
+ * **Desktop & File Capabilities:** Vite PWA, dnd-kit (Drag-and-Drop), html2pdf.js / jspdf (PDF Export)
63
 
64
+ ---
65
 
66
+ ## ⚖️ License
67
+ This project is licensed under the **Polyform Non-Commercial License 1.0.0**.
68
 
69
+ **What does this mean?**
70
+ - You can use and modify this project for **personal, non-commercial use**.
71
+ - You **cannot** use this project or its code for commercial purposes, profit, or business activities without explicit permission from the owner.
72
+ - Ownership and copyright belong to the original author.
73
 
74
+ ---
75
 
76
+ > *Big ideas deserve great starts. With MindSpark, none of your ideas will remain just a dream.*
components/CalendarView.tsx ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Note, Project } from '../types';
3
+ import { motion } from 'motion/react';
4
+
5
+ interface CalendarViewProps {
6
+ notes: Record<string, Note>;
7
+ projects: Project[];
8
+ onNavigateToNote: (noteId: string) => void;
9
+ onFilterDate?: (dateStr: string) => void;
10
+ }
11
+
12
+ export const CalendarView: React.FC<CalendarViewProps> = ({ notes, projects, onNavigateToNote, onFilterDate }) => {
13
+ const [currentDate, setCurrentDate] = React.useState(new Date());
14
+
15
+ const getDaysInMonth = (year: number, month: number) => {
16
+ return new Date(year, month + 1, 0).getDate();
17
+ };
18
+
19
+ const getFirstDayOfMonth = (year: number, month: number) => {
20
+ return new Date(year, month, 1).getDay();
21
+ };
22
+
23
+ const year = currentDate.getFullYear();
24
+ const month = currentDate.getMonth();
25
+ const daysInMonth = getDaysInMonth(year, month);
26
+ const firstDay = getFirstDayOfMonth(year, month);
27
+
28
+ const days = Array.from({ length: daysInMonth }, (_, i) => i + 1);
29
+ const padding = Array.from({ length: firstDay === 0 ? 6 : firstDay - 1 }, (_, i) => i); // Adjust for Monday start
30
+
31
+ const notesByDate: Record<string, Note[]> = {};
32
+ Object.values(notes).forEach(note => {
33
+ const date = new Date(note.timestamp).toDateString();
34
+ if (!notesByDate[date]) notesByDate[date] = [];
35
+ notesByDate[date].push(note);
36
+ });
37
+
38
+ const nextMonth = () => setCurrentDate(new Date(year, month + 1, 1));
39
+ const prevMonth = () => setCurrentDate(new Date(year, month - 1, 1));
40
+
41
+ const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
42
+
43
+ return (
44
+ <div className="bg-[#0F1117] border border-white/10 rounded-[2.5rem] p-8 shadow-2xl">
45
+ <div className="flex justify-between items-center mb-8">
46
+ <h3 className="text-2xl font-black text-white tracking-tight">
47
+ {monthNames[month]} {year}
48
+ </h3>
49
+ <div className="flex gap-2">
50
+ <button onClick={prevMonth} className="p-2 bg-white/5 hover:bg-white/10 rounded-xl text-slate-400 transition-all">
51
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
52
+ <path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
53
+ </svg>
54
+ </button>
55
+ <button onClick={nextMonth} className="p-2 bg-white/5 hover:bg-white/10 rounded-xl text-slate-400 transition-all">
56
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
57
+ <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
58
+ </svg>
59
+ </button>
60
+ </div>
61
+ </div>
62
+
63
+ <div className="grid grid-cols-7 gap-4">
64
+ {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(day => (
65
+ <div key={day} className="text-center text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] pb-4">
66
+ {day}
67
+ </div>
68
+ ))}
69
+
70
+ {padding.map(i => <div key={`pad-${i}`} className="aspect-square"></div>)}
71
+
72
+ {days.map(day => {
73
+ const date = new Date(year, month, day).toDateString();
74
+ const dayNotes = notesByDate[date] || [];
75
+ const isToday = new Date().toDateString() === date;
76
+
77
+ return (
78
+ <div
79
+ key={day}
80
+ onClick={() => {
81
+ const dateStr = new Date(year, month, day).toLocaleDateString('en-US', { day: 'numeric', month: 'short' }).toUpperCase();
82
+ onFilterDate && onFilterDate(dateStr);
83
+ }}
84
+ className={`aspect-square rounded-2xl border p-2 flex flex-col gap-1 transition-all cursor-pointer ${
85
+ isToday ? 'bg-indigo-500/10 border-indigo-500/40 hover:bg-indigo-500/20' : 'bg-white/[0.02] border-white/5 hover:border-white/10 hover:bg-white/[0.05]'
86
+ }`}
87
+ >
88
+ <span className={`text-xs font-bold ${isToday ? 'text-indigo-400' : 'text-slate-500'}`}>{day}</span>
89
+ <div className="flex-1 overflow-y-auto custom-scrollbar space-y-1">
90
+ {dayNotes.map(note => (
91
+ <button
92
+ key={note.id}
93
+ onClick={() => onNavigateToNote(note.id)}
94
+ className="w-full text-left text-[9px] p-1.5 rounded-lg bg-indigo-500/20 text-indigo-300 truncate hover:bg-indigo-500 hover:text-white transition-all"
95
+ title={note.title}
96
+ >
97
+ {note.title}
98
+ </button>
99
+ ))}
100
+ </div>
101
+ </div>
102
+ );
103
+ })}
104
+ </div>
105
+ </div>
106
+ );
107
+ };
components/LoadingSpinner.tsx ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ export const LoadingSpinner: React.FC = () => (
4
+ <svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
5
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
6
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
7
+ </svg>
8
+ );
components/NoteCard.tsx ADDED
@@ -0,0 +1,716 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef } from 'react';
2
+ import { Note, NoteType, GenerationStatus, Attachment } from '../types';
3
+ import { LoadingSpinner } from './LoadingSpinner';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
7
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
8
+
9
+ interface NoteCardProps {
10
+ note: Note;
11
+ isRoot?: boolean;
12
+ onRetry?: (noteId: string) => void;
13
+ onExport?: () => void;
14
+ isExporting?: boolean;
15
+ onUpdate?: (noteId: string, updates: Partial<Note> | string) => void;
16
+ onDelete?: (noteId: string) => void;
17
+ onRegenerateImage?: (noteId: string, feedback: string) => void;
18
+ onAddAttachment?: (noteId: string, attachment: Attachment) => void;
19
+ onRemoveAttachment?: (noteId: string, attachmentId: string) => void;
20
+ onAddTag?: (noteId: string, tag: string) => void;
21
+ onRemoveTag?: (noteId: string, tag: string) => void;
22
+ getNoteTitle?: (noteId: string) => string;
23
+ onNavigateToNote?: (noteId: string) => void;
24
+ dragHandleProps?: any;
25
+ }
26
+
27
+ const NoteCard: React.FC<NoteCardProps> = ({
28
+ note,
29
+ isRoot = false,
30
+ onRetry,
31
+ onExport,
32
+ isExporting = false,
33
+ onUpdate,
34
+ onDelete,
35
+ onRegenerateImage,
36
+ onAddAttachment,
37
+ onRemoveAttachment,
38
+ onAddTag,
39
+ onRemoveTag,
40
+ getNoteTitle,
41
+ onNavigateToNote,
42
+ dragHandleProps
43
+ }) => {
44
+ const [isEditing, setIsEditing] = React.useState(false);
45
+ const [editContent, setEditContent] = React.useState(note.content);
46
+ const [feedback, setFeedback] = React.useState('');
47
+ const [showFeedback, setShowFeedback] = React.useState(false);
48
+ const [useFeedback, setUseFeedback] = React.useState(false);
49
+ const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
50
+ const [showSuccess, setShowSuccess] = React.useState(false);
51
+ const fileInputRef = useRef<HTMLInputElement>(null);
52
+
53
+ const isImage = note.type === NoteType.IMAGE;
54
+ const isLoading = note.status === GenerationStatus.GENERATING;
55
+ const isIdle = note.status === GenerationStatus.IDLE;
56
+ const isError = note.status === GenerationStatus.ERROR;
57
+
58
+ const handleSave = () => {
59
+ if (onUpdate) {
60
+ onUpdate(note.id, { content: editContent });
61
+ }
62
+ setIsEditing(false);
63
+ setShowSuccess(true);
64
+ setTimeout(() => setShowSuccess(false), 2500);
65
+ };
66
+
67
+ const toggleTask = () => {
68
+ if (onUpdate) {
69
+ onUpdate(note.id, { isTask: !note.isTask });
70
+ }
71
+ };
72
+
73
+ const toggleTaskStatus = () => {
74
+ if (onUpdate) {
75
+ onUpdate(note.id, { isCompleted: !note.isCompleted });
76
+ }
77
+ };
78
+
79
+ const handleCancel = () => {
80
+ setEditContent(note.content);
81
+ setIsEditing(false);
82
+ };
83
+
84
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
85
+ const file = e.target.files?.[0];
86
+ if (!file || !onAddAttachment) return;
87
+
88
+ const reader = new FileReader();
89
+ reader.onloadend = () => {
90
+ const base64String = reader.result as string;
91
+ let type: 'image' | 'audio' | 'file' = 'file';
92
+ if (file.type.startsWith('image/')) type = 'image';
93
+ else if (file.type.startsWith('audio/')) type = 'audio';
94
+
95
+ const newAttachment: Attachment = {
96
+ id: Date.now().toString(),
97
+ type,
98
+ url: base64String,
99
+ name: file.name
100
+ };
101
+ onAddAttachment(note.id, newAttachment);
102
+ };
103
+ reader.readAsDataURL(file);
104
+ if (fileInputRef.current) fileInputRef.current.value = '';
105
+ };
106
+
107
+ const renderAttachments = () => {
108
+ if (!note.attachments || note.attachments.length === 0) return null;
109
+
110
+ return (
111
+ <div className="mt-4 space-y-2 border-t border-slate-700/50 pt-4">
112
+ <h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Attachments</h4>
113
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
114
+ {note.attachments.map(attachment => (
115
+ <div key={attachment.id} className="relative group/attach bg-slate-900/50 border border-slate-700 rounded-lg p-3 flex items-center gap-3">
116
+ {attachment.type === 'image' && (
117
+ <div className="w-10 h-10 rounded overflow-hidden shrink-0 bg-slate-800">
118
+ <img src={attachment.url} alt={attachment.name} className="w-full h-full object-cover" />
119
+ </div>
120
+ )}
121
+ {attachment.type === 'audio' && (
122
+ <div className="w-10 h-10 rounded shrink-0 bg-indigo-900/30 text-indigo-400 flex items-center justify-center">
123
+ 🎵
124
+ </div>
125
+ )}
126
+ {attachment.type === 'file' && (
127
+ <div className="w-10 h-10 rounded shrink-0 bg-slate-800 text-slate-400 flex items-center justify-center">
128
+ 📄
129
+ </div>
130
+ )}
131
+
132
+ <div className="flex-1 min-w-0">
133
+ <p className="text-sm text-slate-300 truncate" title={attachment.name}>{attachment.name}</p>
134
+ {attachment.type === 'audio' && (
135
+ <audio src={attachment.url} controls className="w-full h-6 mt-1" />
136
+ )}
137
+ {attachment.type === 'file' && (
138
+ <a href={attachment.url} download={attachment.name} className="text-xs text-indigo-400 hover:underline">Download</a>
139
+ )}
140
+ </div>
141
+
142
+ {onRemoveAttachment && (
143
+ <button
144
+ onClick={() => onRemoveAttachment(note.id, attachment.id)}
145
+ className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover/attach:opacity-100 transition-opacity shadow-lg hide-in-pdf"
146
+ title="Remove Attachment"
147
+ >
148
+ ×
149
+ </button>
150
+ )}
151
+ </div>
152
+ ))}
153
+ </div>
154
+ </div>
155
+ );
156
+ };
157
+
158
+ const renderContent = () => {
159
+ if (isLoading) {
160
+ return (
161
+ <div className="flex flex-col items-center justify-center p-12 space-y-4 text-slate-400 bg-slate-900/30 rounded-lg border border-dashed border-slate-700">
162
+ <LoadingSpinner />
163
+ <span className="text-sm animate-pulse text-indigo-400 font-medium">Preparing content...</span>
164
+ <p className="text-xs max-w-xs text-center opacity-60">AI is detailing this step, please wait.</p>
165
+ </div>
166
+ );
167
+ }
168
+
169
+ if (isIdle) {
170
+ return (
171
+ <div className="p-6 bg-slate-900/20 rounded-lg border border-dashed border-slate-700 text-slate-500 italic text-sm">
172
+ {note.content || "Content will be generated when it's time for this step..."}
173
+ </div>
174
+ )
175
+ }
176
+
177
+ if (isError) {
178
+ return (
179
+ <div className="flex flex-col items-center justify-center p-6 gap-3 border border-red-900/50 bg-red-900/10 rounded-lg text-center">
180
+ <p className="text-red-400">An error occurred while generating content.</p>
181
+ {onRetry && (
182
+ <button
183
+ onClick={() => onRetry(note.id)}
184
+ className="px-4 py-2 bg-red-800/50 hover:bg-red-700/50 text-red-200 text-sm rounded-md transition-colors border border-red-700/50 flex items-center gap-2"
185
+ >
186
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
187
+ <path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v3.292a1 1 0 01-2 0V12.899a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
188
+ </svg>
189
+ Retry
190
+ </button>
191
+ )}
192
+ </div>
193
+ );
194
+ }
195
+
196
+ if (isEditing) {
197
+ return (
198
+ <div className="flex flex-col gap-4 bg-slate-900/50 p-4 rounded-xl border border-indigo-500/30">
199
+ <textarea
200
+ value={editContent}
201
+ onChange={(e) => setEditContent(e.target.value)}
202
+ className="w-full bg-slate-950 text-slate-200 p-5 rounded-lg border border-slate-700 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none min-h-[250px] font-mono text-sm leading-relaxed shadow-inner"
203
+ placeholder="Write note content here..."
204
+ />
205
+ <div className="flex justify-end gap-3">
206
+ <button
207
+ onClick={handleCancel}
208
+ className="px-5 py-2.5 text-sm font-medium text-slate-300 bg-slate-800 hover:bg-slate-700 rounded-lg transition-colors border border-slate-600"
209
+ >
210
+ Cancel
211
+ </button>
212
+ <button
213
+ onClick={handleSave}
214
+ className="px-5 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors shadow-lg shadow-indigo-500/20 flex items-center gap-2"
215
+ >
216
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
217
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
218
+ </svg>
219
+ Save
220
+ </button>
221
+ </div>
222
+ </div>
223
+ );
224
+ }
225
+
226
+ if (isImage) {
227
+ if (note.content && note.content.startsWith('data:image')) {
228
+ return (
229
+ <div className="flex flex-col items-center p-4 bg-slate-950 rounded-lg border border-slate-800">
230
+ <img src={note.content} alt={note.title} className="max-w-full h-auto rounded shadow-2xl border border-slate-700" />
231
+ <p className="text-xs text-slate-500 mt-2 italic">Generated by Gemini Image</p>
232
+
233
+ <div className="w-full mt-6 pt-6 border-t border-slate-800">
234
+ <div className="flex gap-4 max-w-md mx-auto">
235
+ <label className="flex items-center gap-2 text-sm text-slate-300 cursor-pointer hover:text-white transition-colors">
236
+ <input
237
+ type="checkbox"
238
+ checked={useFeedback}
239
+ onChange={(e) => setUseFeedback(e.target.checked)}
240
+ className="rounded border-slate-600 text-indigo-500 focus:ring-indigo-500 bg-slate-800"
241
+ />
242
+ Revise image with feedback
243
+ </label>
244
+
245
+ {useFeedback && (
246
+ <textarea
247
+ value={feedback}
248
+ onChange={(e) => setFeedback(e.target.value)}
249
+ placeholder="Ex: Darker atmosphere, make the character's hair blue..."
250
+ className="w-full bg-slate-900 text-slate-300 p-3 rounded-lg border border-slate-700 text-xs focus:outline-none focus:border-indigo-500 transition-colors"
251
+ rows={3}
252
+ />
253
+ )}
254
+
255
+ <button
256
+ onClick={() => {
257
+ if (onRegenerateImage) onRegenerateImage(note.id, useFeedback ? feedback : '');
258
+ setFeedback('');
259
+ setUseFeedback(false);
260
+ }}
261
+ className="w-full bg-indigo-600/20 hover:bg-indigo-600/30 text-indigo-300 border border-indigo-500/30 py-2 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
262
+ >
263
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
264
+ <path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v3.292a1 1 0 01-2 0V12.899a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
265
+ </svg>
266
+ Regenerate
267
+ </button>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ );
272
+ }
273
+ return (
274
+ <div className="p-10 bg-[#0B0F19]/40 rounded-[2rem] border border-dashed border-white/10 text-center space-y-6">
275
+ <div className="w-20 h-20 bg-orange-500/10 rounded-[1.5rem] flex items-center justify-center mx-auto text-orange-400">
276
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
277
+ </div>
278
+ <div className="space-y-2">
279
+ <h4 className="text-white font-black text-xs uppercase tracking-[0.2em]">Image Generation Failed</h4>
280
+ <p className="text-slate-500 text-xs leading-relaxed max-w-sm mx-auto">
281
+ {note.content || "An unknown error occurred."}
282
+ </p>
283
+ </div>
284
+ {onRetry && (
285
+ <button
286
+ onClick={() => onRetry(note.id)}
287
+ className="bg-white/5 border border-white/10 hover:border-white/20 text-indigo-400 hover:text-white px-6 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all"
288
+ >
289
+ Retry
290
+ </button>
291
+ )}
292
+ </div>
293
+ );
294
+ }
295
+
296
+ // Advanced Markdown-like rendering
297
+ return (
298
+ <div
299
+ className="prose prose-invert prose-slate max-w-none group/content relative"
300
+ >
301
+ {!isRoot && note.type !== NoteType.IMAGE && (
302
+ <button
303
+ onClick={() => setIsEditing(true)}
304
+ className="absolute -right-2 -top-2 opacity-0 group-hover/content:opacity-100 transition-opacity bg-indigo-600 text-white text-[10px] px-2 py-1 rounded shadow-lg z-10 hide-in-pdf"
305
+ >
306
+ Click to edit
307
+ </button>
308
+ )}
309
+ <div className="text-slate-300 leading-7 font-light markdown-body">
310
+ <ReactMarkdown
311
+ remarkPlugins={[remarkGfm]}
312
+ components={{
313
+ code({node, inline, className, children, ...props}: any) {
314
+ const match = /language-(\w+)/.exec(className || '')
315
+ return !inline && match ? (
316
+ <SyntaxHighlighter
317
+ {...props}
318
+ children={String(children).replace(/\n$/, '')}
319
+ style={vscDarkPlus}
320
+ language={match[1]}
321
+ PreTag="div"
322
+ className="rounded-xl border border-white/10 my-6 text-sm shadow-lg shadow-black/20"
323
+ />
324
+ ) : (
325
+ <code {...props} className={`${className} bg-white/5 text-indigo-300 px-1.5 py-0.5 rounded-md text-sm border border-white/5`}>
326
+ {children}
327
+ </code>
328
+ )
329
+ },
330
+ img({node, ...props}: any) {
331
+ if (!props.src) return null;
332
+ return (
333
+ <span className="my-8 flex justify-center">
334
+ <img {...props} src={props.src} className="max-w-full h-auto rounded-xl shadow-2xl shadow-black/40 border border-white/10" />
335
+ </span>
336
+ )
337
+ },
338
+ h1: ({node, ...props}: any) => <h1 {...props} className="text-2xl md:text-3xl font-bold text-white mt-8 mb-6 tracking-tight" />,
339
+ h2: ({node, ...props}: any) => <h2 {...props} className="text-xl md:text-2xl font-bold text-indigo-200 mt-8 mb-4 tracking-tight" />,
340
+ h3: ({node, ...props}: any) => <h3 {...props} className="text-lg md:text-xl font-bold text-indigo-300 mt-6 mb-3 pb-2 border-b border-white/5" />,
341
+ hr: ({node, ...props}: any) => <hr {...props} className="my-8 border-white/5" />,
342
+ blockquote: ({node, ...props}: any) => <blockquote {...props} className="border-l-4 border-indigo-500/50 pl-5 italic text-slate-400 my-6 bg-white/[0.02] py-3 rounded-r-lg" />,
343
+ a: ({node, ...props}: any) => <a {...props} className="text-indigo-400 hover:text-indigo-300 hover:underline decoration-indigo-500/30 underline-offset-4 transition-colors" target="_blank" rel="noopener noreferrer" />,
344
+ ul: ({node, ...props}: any) => <ul {...props} className="list-disc pl-6 mb-6 space-y-2 marker:text-indigo-500/50" />,
345
+ ol: ({node, ...props}: any) => <ol {...props} className="list-decimal pl-6 mb-6 space-y-2 marker:text-indigo-500/50" />,
346
+ li: ({node, ...props}: any) => <li {...props} className="text-slate-300 pl-1" />,
347
+ p: ({node, children, ...props}: any) => {
348
+ const hasImage = node?.children?.some((child: any) => child.tagName === 'img');
349
+ if (hasImage) {
350
+ return <div {...props} className="mb-4">{children}</div>;
351
+ }
352
+ return <p {...props} className="mb-4">{children}</p>;
353
+ }
354
+ }}
355
+ >
356
+ {note.content}
357
+ </ReactMarkdown>
358
+ </div>
359
+ </div>
360
+ );
361
+ };
362
+
363
+ const [showTagInput, setShowTagInput] = React.useState(false);
364
+ const [newTagText, setNewTagText] = React.useState('');
365
+
366
+ const handleAddTagSubmit = () => {
367
+ if (newTagText.trim() && onAddTag) {
368
+ onAddTag(note.id, newTagText.trim());
369
+ setNewTagText('');
370
+ setShowTagInput(false);
371
+ }
372
+ };
373
+
374
+ const renderTags = () => {
375
+ return (
376
+ <div className="mt-4 flex flex-wrap items-center gap-2 border-t border-slate-700/50 pt-4">
377
+ <span className="text-xs font-semibold text-slate-400 uppercase tracking-wider mr-2">Tags:</span>
378
+ {(note.tags || []).map(tag => (
379
+ <span key={tag} className="bg-indigo-900/40 text-indigo-300 text-xs px-2 py-1 rounded-md flex items-center gap-1 border border-indigo-500/30">
380
+ #{tag}
381
+ {onRemoveTag && (
382
+ <button onClick={() => onRemoveTag(note.id, tag)} className="hover:text-red-400 ml-1 hide-in-pdf">×</button>
383
+ )}
384
+ </span>
385
+ ))}
386
+ {onAddTag && (
387
+ <div className="flex items-center gap-2">
388
+ {showTagInput ? (
389
+ <div className="flex items-center gap-2 animate-fadeIn">
390
+ <input
391
+ type="text"
392
+ autoFocus
393
+ value={newTagText}
394
+ onChange={(e) => setNewTagText(e.target.value)}
395
+ onKeyDown={(e) => {
396
+ if (e.key === 'Enter') handleAddTagSubmit();
397
+ if (e.key === 'Escape') { setShowTagInput(false); setNewTagText(''); }
398
+ }}
399
+ placeholder="Tag name..."
400
+ className="bg-slate-900 border border-indigo-500/50 rounded-md px-2 py-1 text-xs text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
401
+ />
402
+ <button onClick={handleAddTagSubmit} className="text-emerald-400 hover:text-emerald-300">
403
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
404
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
405
+ </svg>
406
+ </button>
407
+ <button onClick={() => setShowTagInput(false)} className="text-slate-500 hover:text-slate-300">×</button>
408
+ </div>
409
+ ) : (
410
+ <button
411
+ onClick={() => setShowTagInput(true)}
412
+ className="text-xs text-slate-500 hover:text-indigo-400 border border-dashed border-slate-600 rounded-md px-2 py-1 hide-in-pdf"
413
+ >
414
+ + Tag
415
+ </button>
416
+ )}
417
+ </div>
418
+ )}
419
+ </div>
420
+ );
421
+ };
422
+
423
+ const [linkedNotesSearch, setLinkedNotesSearch] = React.useState('');
424
+
425
+ const renderLinkedNotes = () => {
426
+ if (!note.linkedNoteIds || note.linkedNoteIds.length === 0 || !getNoteTitle) return null;
427
+
428
+ const filteredLinkedNotes = note.linkedNoteIds.filter(id => {
429
+ const title = getNoteTitle(id);
430
+ return title && title.toLowerCase().includes(linkedNotesSearch.toLowerCase());
431
+ });
432
+
433
+ return (
434
+ <div className="mt-4 space-y-3 border-t border-slate-700/50 pt-4">
435
+ <div className="flex items-center justify-between">
436
+ <h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Linked Notes</h4>
437
+ {note.linkedNoteIds.length > 3 && (
438
+ <div className="relative w-48">
439
+ <input
440
+ type="text"
441
+ placeholder="Search links..."
442
+ value={linkedNotesSearch}
443
+ onChange={(e) => setLinkedNotesSearch(e.target.value)}
444
+ className="w-full bg-slate-900 text-slate-300 text-[10px] rounded pl-6 pr-2 py-1 border border-slate-700 focus:border-indigo-500 focus:outline-none"
445
+ />
446
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3 absolute left-2 top-1.5 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
447
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
448
+ </svg>
449
+ </div>
450
+ )}
451
+ </div>
452
+ <div className="flex flex-wrap gap-2">
453
+ {filteredLinkedNotes.length > 0 ? filteredLinkedNotes.map(linkedNoteId => {
454
+ const title = getNoteTitle(linkedNoteId);
455
+ if (!title) return null;
456
+ return (
457
+ <button
458
+ key={linkedNoteId}
459
+ onClick={() => onNavigateToNote && onNavigateToNote(linkedNoteId)}
460
+ className="bg-slate-800 border border-slate-700 rounded-md px-3 py-1.5 text-sm text-indigo-300 flex items-center gap-2 hover:bg-slate-700 hover:border-indigo-500 transition-colors cursor-pointer"
461
+ >
462
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
463
+ <path fillRule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clipRule="evenodd" />
464
+ </svg>
465
+ {title}
466
+ </button>
467
+ );
468
+ }) : (
469
+ <p className="text-xs text-slate-500 italic">No matching links found.</p>
470
+ )}
471
+ </div>
472
+ </div>
473
+ );
474
+ };
475
+
476
+ const getIcon = () => {
477
+ switch (note.type) {
478
+ case NoteType.IMAGE: return '🎨';
479
+ case NoteType.CODE: return '💻';
480
+ case NoteType.ROOT: return '📁';
481
+ default: return '📝';
482
+ }
483
+ };
484
+
485
+ const handleCopy = () => {
486
+ if(note.type === NoteType.IMAGE) return;
487
+ navigator.clipboard.writeText(note.content);
488
+ };
489
+
490
+ const handleExportNoteMD = () => {
491
+ if (note.type === NoteType.IMAGE) return;
492
+ const blob = new Blob([note.content], { type: 'text/markdown' });
493
+ const url = URL.createObjectURL(blob);
494
+ const a = document.createElement('a');
495
+ a.href = url;
496
+ a.download = `${note.title.replace(/\s+/g, '_')}.md`;
497
+ document.body.appendChild(a);
498
+ a.click();
499
+ document.body.removeChild(a);
500
+ URL.revokeObjectURL(url);
501
+ };
502
+
503
+ return (
504
+ <div className={`
505
+ relative group transition-all duration-700 break-inside-avoid
506
+ ${isRoot
507
+ ? 'bg-gradient-to-br from-[#1A1D26] to-[#0F1117] border-indigo-500/40 border-2 rounded-[2.5rem] shadow-2xl shadow-black/60'
508
+ : 'bg-[#161B26]/30 backdrop-blur-xl border border-white/5 rounded-[2rem] hover:border-indigo-500/30 hover:bg-[#161B26]/50 shadow-xl shadow-black/30 transition-all duration-500'
509
+ }
510
+ `}>
511
+ {/* Header */}
512
+ <div
513
+ className={`flex flex-col sm:flex-row sm:items-center justify-between px-6 py-5 md:px-8 md:py-6 border-b ${isRoot ? 'border-white/10' : 'border-white/5'} gap-4`}
514
+ >
515
+ <div className="flex items-center gap-5 min-w-0">
516
+ {dragHandleProps && (
517
+ <div
518
+ {...dragHandleProps}
519
+ className="cursor-grab active:cursor-grabbing text-slate-600 hover:text-slate-300 p-2.5 -ml-3 rounded-2xl hover:bg-white/5 hide-in-pdf transition-all duration-300"
520
+ title="Drag"
521
+ >
522
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
523
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8h16M4 16h16" />
524
+ </svg>
525
+ </div>
526
+ )}
527
+ <div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-transform duration-500 group-hover:scale-105 ${isRoot ? 'bg-indigo-500 text-white shadow-xl shadow-indigo-500/30' : 'bg-[#1F2937]/50 text-slate-400 border border-white/5'}`}>
528
+ <span className="text-2xl">{getIcon()}</span>
529
+ </div>
530
+ <div className="min-w-0">
531
+ <div className="flex items-center gap-4">
532
+ {note.isTask && (
533
+ <button
534
+ onClick={(e) => { e.stopPropagation(); toggleTaskStatus(); }}
535
+ className={`w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all shrink-0 ${note.isCompleted ? 'bg-indigo-500 border-indigo-500 text-white' : 'border-slate-700 hover:border-indigo-500'}`}
536
+ >
537
+ {note.isCompleted && (
538
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
539
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
540
+ </svg>
541
+ )}
542
+ </button>
543
+ )}
544
+ <h3
545
+ title={note.title}
546
+ className={`font-black tracking-tight truncate ${isRoot ? 'text-2xl md:text-3xl text-white' : 'text-lg md:text-xl text-slate-100'} ${note.isCompleted ? 'line-through opacity-50' : ''}`}
547
+ >
548
+ {note.title}
549
+ </h3>
550
+ {showSuccess && (
551
+ <span className="px-3 py-1 bg-emerald-500/10 text-emerald-400 text-[10px] font-black rounded-full uppercase tracking-[0.2em] animate-pulse border border-emerald-500/20">
552
+ Saved
553
+ </span>
554
+ )}
555
+ </div>
556
+ <div className="flex items-center gap-3 mt-1.5">
557
+ {isRoot && <span className="text-[10px] text-indigo-400 font-black tracking-[0.3em] uppercase">Master Project</span>}
558
+ {!isRoot && <span className="text-[10px] text-slate-500 font-black tracking-[0.3em] uppercase">Step {note.status === GenerationStatus.COMPLETED ? 'Complete' : 'Pending'}</span>}
559
+ <span className="w-1.5 h-1.5 rounded-full bg-slate-800"></span>
560
+ <span className="text-[10px] text-slate-600 font-black uppercase tracking-[0.2em]">{new Date(note.timestamp).toLocaleDateString('en-US', { day: 'numeric', month: 'short' })}</span>
561
+ {note.assignedAgent && (
562
+ <>
563
+ <span className="w-1.5 h-1.5 rounded-full bg-indigo-500/50"></span>
564
+ <div className="flex items-center gap-1.5 px-2 py-0.5 bg-indigo-500/10 border border-indigo-500/20 rounded-md">
565
+ <span className="text-[9px] text-indigo-400 font-black uppercase tracking-wider">Agent: {note.assignedAgent}</span>
566
+ {note.agentRole && (
567
+ <span className="text-[9px] text-slate-500 font-medium italic">({note.agentRole})</span>
568
+ )}
569
+ </div>
570
+ </>
571
+ )}
572
+ </div>
573
+ </div>
574
+ </div>
575
+
576
+ <div className="flex flex-wrap gap-2 items-center justify-start sm:justify-end hide-in-pdf sm:ml-auto shrink-0">
577
+ {isRoot && onExport && !isLoading && (
578
+ <button
579
+ onClick={onExport}
580
+ disabled={isExporting}
581
+ className={`flex items-center gap-2 px-4 py-2 md:px-6 md:py-3 rounded-xl md:rounded-2xl text-[10px] md:text-[11px] font-black uppercase tracking-[0.1em] md:tracking-[0.2em] transition-all shadow-xl ${isExporting ? 'bg-indigo-800 text-indigo-300 cursor-not-allowed shadow-none' : 'bg-indigo-600 hover:bg-indigo-500 text-white shadow-indigo-600/30 hover:-translate-y-0.5 active:translate-y-0'}`}
582
+ title="Download Project"
583
+ >
584
+ {isExporting ? (
585
+ <svg className="animate-spin h-4 w-4 md:h-5 md:w-5 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
586
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
587
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
588
+ </svg>
589
+ ) : (
590
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 md:h-5 md:w-5" viewBox="0 0 20 20" fill="currentColor">
591
+ <path fillRule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clipRule="evenodd" />
592
+ </svg>
593
+ )}
594
+ <span className="hidden xs:inline">{isExporting ? '...' : 'Export'}</span>
595
+ {!isExporting && <span className="xs:hidden">EXP</span>}
596
+ </button>
597
+ )}
598
+
599
+ {note.type !== NoteType.IMAGE && note.content && !isLoading && !isIdle && !isError && (
600
+ <>
601
+ <button onClick={handleCopy} className="opacity-0 group-hover:opacity-100 transition-all text-[10px] uppercase tracking-[0.2em] bg-white/5 hover:bg-indigo-600 text-slate-400 hover:text-white px-4 py-2.5 rounded-2xl font-black border border-white/5 shadow-lg">
602
+ Copy
603
+ </button>
604
+ <button onClick={handleExportNoteMD} className="opacity-0 group-hover:opacity-100 transition-all text-[10px] uppercase tracking-[0.2em] bg-white/5 hover:bg-indigo-600 text-slate-400 hover:text-white px-4 py-2.5 rounded-2xl font-black border border-white/5 shadow-lg">
605
+ MD
606
+ </button>
607
+ </>
608
+ )}
609
+
610
+ {!isRoot && onDelete && (
611
+ <div className="relative opacity-0 group-hover:opacity-100 transition-all flex items-center">
612
+ {showDeleteConfirm ? (
613
+ <div className="flex items-center gap-2 bg-red-500/10 border border-red-500/20 rounded-2xl p-1.5">
614
+ <button onClick={(e) => { e.stopPropagation(); onDelete(note.id); }} className="text-[10px] uppercase tracking-[0.2em] bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-xl font-black transition-all shadow-lg">Delete</button>
615
+ <button onClick={(e) => { e.stopPropagation(); setShowDeleteConfirm(false); }} className="text-[10px] uppercase tracking-[0.2em] bg-slate-800 hover:bg-slate-700 text-slate-300 px-4 py-2 rounded-xl font-black transition-all">Cancel</button>
616
+ </div>
617
+ ) : (
618
+ <button
619
+ onPointerDown={(e) => e.stopPropagation()}
620
+ onClick={(e) => { e.stopPropagation(); setShowDeleteConfirm(true); }}
621
+ className="p-3 text-slate-600 hover:text-red-400 hover:bg-red-500/10 rounded-2xl transition-all duration-300"
622
+ title="Delete Step"
623
+ >
624
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
625
+ <path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
626
+ </svg>
627
+ </button>
628
+ )}
629
+ </div>
630
+ )}
631
+ </div>
632
+ </div>
633
+
634
+ {/* Content */}
635
+ <div className={`px-6 py-6 md:px-10 md:py-8 ${isRoot ? 'bg-white/[0.01]' : ''}`}>
636
+ {renderContent()}
637
+ {renderAttachments()}
638
+ {renderTags()}
639
+ {renderLinkedNotes()}
640
+ </div>
641
+
642
+ {/* Footer Actions */}
643
+ <div className="px-6 py-4 md:px-8 md:py-5 border-t border-white/5 bg-black/40 flex justify-between items-center hide-in-pdf rounded-b-[2rem]">
644
+ <div className="flex items-center gap-6">
645
+ {!isRoot && onUpdate && (
646
+ <button
647
+ onClick={toggleTask}
648
+ className={`text-[11px] font-black uppercase tracking-[0.2em] flex items-center gap-3 transition-all group/footer ${note.isTask ? 'text-indigo-400' : 'text-slate-500 hover:text-indigo-400'}`}
649
+ >
650
+ <div className={`w-8 h-8 rounded-xl flex items-center justify-center transition-all duration-300 ${note.isTask ? 'bg-indigo-500/20' : 'bg-white/5 group-hover/footer:bg-indigo-500/10'}`}>
651
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
652
+ <path fillRule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
653
+ </svg>
654
+ </div>
655
+ {note.isTask ? 'Task' : 'Convert to Task'}
656
+ </button>
657
+ )}
658
+ {onAddAttachment && (
659
+ <div>
660
+ <input
661
+ type="file"
662
+ ref={fileInputRef}
663
+ onChange={handleFileChange}
664
+ className="hidden"
665
+ accept="image/*,audio/*,.pdf,.doc,.docx,.txt"
666
+ />
667
+ <button
668
+ onClick={() => fileInputRef.current?.click()}
669
+ className="text-[11px] font-black uppercase tracking-[0.2em] flex items-center gap-3 text-slate-500 hover:text-indigo-400 transition-all group/footer"
670
+ >
671
+ <div className="w-8 h-8 rounded-xl bg-white/5 flex items-center justify-center group-hover/footer:bg-indigo-500/10 transition-all duration-300">
672
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
673
+ <path fillRule="evenodd" d="M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z" clipRule="evenodd" />
674
+ </svg>
675
+ </div>
676
+ Add File
677
+ </button>
678
+ </div>
679
+ )}
680
+ {onAddTag && (
681
+ <button
682
+ onClick={() => {
683
+ setShowTagInput(true);
684
+ setTimeout(() => {
685
+ const noteEl = document.getElementById(`note-${note.id}`);
686
+ if (noteEl) noteEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
687
+ }, 100);
688
+ }}
689
+ className="text-[11px] font-black uppercase tracking-[0.2em] flex items-center gap-3 text-slate-500 hover:text-indigo-400 transition-all group/footer"
690
+ >
691
+ <div className="w-8 h-8 rounded-xl bg-white/5 flex items-center justify-center group-hover/footer:bg-indigo-500/10 transition-all duration-300">
692
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
693
+ <path fillRule="evenodd" d="M17.707 9.293l-5-5a1 1 0 00-1.414 0l-8 8a1 1 0 000 1.414l5 5a1 1 0 001.414 0l8-8a1 1 0 000-1.414zM9 14a1 1 0 11-2 0 1 1 0 012 0z" clipRule="evenodd" />
694
+ </svg>
695
+ </div>
696
+ Tag
697
+ </button>
698
+ )}
699
+ </div>
700
+
701
+ <div className="flex items-center gap-4">
702
+ <div className="text-[10px] text-slate-700 font-black uppercase tracking-[0.2em]">
703
+ ID: {note.id.split('-')[0]}
704
+ </div>
705
+ </div>
706
+ </div>
707
+
708
+ {/* Decorative footer for root */}
709
+ {isRoot && (
710
+ <div className="h-2 bg-gradient-to-r from-indigo-500 via-purple-600 to-indigo-500 w-full rounded-b-3xl opacity-30 blur-[1px]"></div>
711
+ )}
712
+ </div>
713
+ );
714
+ };
715
+
716
+ export default React.memo(NoteCard);
components/VoiceRecorder.tsx ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'motion/react';
3
+
4
+ interface VoiceRecorderProps {
5
+ onTranscription: (text: string) => void;
6
+ isProcessing: boolean;
7
+ }
8
+
9
+ export const VoiceRecorder: React.FC<VoiceRecorderProps> = ({ onTranscription, isProcessing }) => {
10
+ const [isRecording, setIsRecording] = useState(false);
11
+ const [recordingTime, setRecordingTime] = useState(0);
12
+ const mediaRecorderRef = useRef<MediaRecorder | null>(null);
13
+ const chunksRef = useRef<Blob[]>([]);
14
+ const timerRef = useRef<NodeJS.Timeout | null>(null);
15
+
16
+ useEffect(() => {
17
+ if (isRecording) {
18
+ timerRef.current = setInterval(() => {
19
+ setRecordingTime(prev => prev + 1);
20
+ }, 1000);
21
+ } else {
22
+ if (timerRef.current) clearInterval(timerRef.current);
23
+ setRecordingTime(0);
24
+ }
25
+ return () => {
26
+ if (timerRef.current) clearInterval(timerRef.current);
27
+ };
28
+ }, [isRecording]);
29
+
30
+ const startRecording = async () => {
31
+ try {
32
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
33
+ const mediaRecorder = new MediaRecorder(stream);
34
+ mediaRecorderRef.current = mediaRecorder;
35
+ chunksRef.current = [];
36
+
37
+ mediaRecorder.ondataavailable = (e) => {
38
+ if (e.data.size > 0) {
39
+ chunksRef.current.push(e.data);
40
+ }
41
+ };
42
+
43
+ mediaRecorder.onstop = async () => {
44
+ const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' });
45
+ const reader = new FileReader();
46
+ reader.onloadend = () => {
47
+ const base64Audio = (reader.result as string).split(',')[1];
48
+ handleTranscription(base64Audio);
49
+ };
50
+ reader.readAsDataURL(audioBlob);
51
+
52
+ // Stop all tracks
53
+ stream.getTracks().forEach(track => track.stop());
54
+ };
55
+
56
+ mediaRecorder.start();
57
+ setIsRecording(true);
58
+ } catch (err) {
59
+ console.error("Microphone access denied:", err);
60
+ alert("Mikrofon erişimi reddedildi.");
61
+ }
62
+ };
63
+
64
+ const stopRecording = () => {
65
+ if (mediaRecorderRef.current && isRecording) {
66
+ mediaRecorderRef.current.stop();
67
+ setIsRecording(false);
68
+ }
69
+ };
70
+
71
+ const handleTranscription = async (base64Audio: string) => {
72
+ // We'll use Gemini to transcribe.
73
+ // Since we don't have a direct transcription API here,
74
+ // we'll send it as a prompt with audio data.
75
+ try {
76
+ // This will be handled in App.tsx or a service
77
+ onTranscription(base64Audio);
78
+ } catch (error) {
79
+ console.error("Transcription failed:", error);
80
+ }
81
+ };
82
+
83
+ const formatTime = (seconds: number) => {
84
+ const mins = Math.floor(seconds / 60);
85
+ const secs = seconds % 60;
86
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
87
+ };
88
+
89
+ return (
90
+ <div className="flex items-center gap-4">
91
+ <AnimatePresence>
92
+ {isRecording && (
93
+ <motion.div
94
+ initial={{ opacity: 0, x: -20 }}
95
+ animate={{ opacity: 1, x: 0 }}
96
+ exit={{ opacity: 0, x: -20 }}
97
+ className="flex items-center gap-3 bg-red-500/10 border border-red-500/20 px-4 py-2 rounded-2xl"
98
+ >
99
+ <div className="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div>
100
+ <span className="text-xs font-black text-red-400 font-mono">{formatTime(recordingTime)}</span>
101
+ </motion.div>
102
+ )}
103
+ </AnimatePresence>
104
+
105
+ <button
106
+ onClick={isRecording ? stopRecording : startRecording}
107
+ disabled={isProcessing}
108
+ className={`w-12 h-12 rounded-2xl flex items-center justify-center transition-all duration-500 shadow-lg ${
109
+ isRecording
110
+ ? 'bg-red-500 text-white animate-pulse shadow-red-500/30'
111
+ : 'bg-white/5 text-slate-400 hover:bg-indigo-600 hover:text-white border border-white/5'
112
+ } ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}
113
+ title={isRecording ? "Kaydı Durdur" : "Sesli Not Al"}
114
+ >
115
+ {isProcessing ? (
116
+ <div className="w-5 h-5 border-2 border-current border-t-transparent rounded-full animate-spin"></div>
117
+ ) : isRecording ? (
118
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
119
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z" clipRule="evenodd" />
120
+ </svg>
121
+ ) : (
122
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
123
+ <path fillRule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clipRule="evenodd" />
124
+ </svg>
125
+ )}
126
+ </button>
127
+ </div>
128
+ );
129
+ };
firebase-blueprint.json ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "entities": {
3
+ "User": {
4
+ "title": "User",
5
+ "description": "A user of the application.",
6
+ "type": "object",
7
+ "properties": {
8
+ "email": {
9
+ "type": "string",
10
+ "format": "email",
11
+ "description": "The user's email address."
12
+ },
13
+ "createdAt": {
14
+ "type": "number",
15
+ "description": "Timestamp when the user was created."
16
+ }
17
+ },
18
+ "required": ["email", "createdAt"]
19
+ },
20
+ "Project": {
21
+ "title": "Project",
22
+ "description": "A project containing notes.",
23
+ "type": "object",
24
+ "properties": {
25
+ "id": {
26
+ "type": "string",
27
+ "description": "The project ID."
28
+ },
29
+ "userId": {
30
+ "type": "string",
31
+ "description": "The ID of the user who owns the project."
32
+ },
33
+ "title": {
34
+ "type": "string",
35
+ "description": "The title of the project."
36
+ },
37
+ "rootNoteId": {
38
+ "type": "string",
39
+ "description": "The ID of the root note for this project."
40
+ },
41
+ "createdAt": {
42
+ "type": "number",
43
+ "description": "Timestamp when the project was created."
44
+ },
45
+ "categoryId": {
46
+ "type": "string",
47
+ "description": "Optional ID of the category this project belongs to."
48
+ },
49
+ "creatorId": {
50
+ "type": "string",
51
+ "description": "The ID of the user who created the project."
52
+ },
53
+ "creatorEmail": {
54
+ "type": "string",
55
+ "description": "The email of the user who created the project."
56
+ },
57
+ "collaborators": {
58
+ "type": "array",
59
+ "items": { "type": "string" },
60
+ "description": "List of user IDs who are collaborators on this project."
61
+ }
62
+ },
63
+ "required": ["id", "userId", "title", "rootNoteId", "createdAt", "creatorId"]
64
+ },
65
+ "Note": {
66
+ "title": "Note",
67
+ "description": "A note within a project.",
68
+ "type": "object",
69
+ "properties": {
70
+ "id": {
71
+ "type": "string",
72
+ "description": "The note ID."
73
+ },
74
+ "userId": {
75
+ "type": "string",
76
+ "description": "The ID of the user who owns the note."
77
+ },
78
+ "projectId": {
79
+ "type": "string",
80
+ "description": "The ID of the project this note belongs to."
81
+ },
82
+ "parentId": {
83
+ "type": ["string", "null"],
84
+ "description": "The ID of the parent note, or null if it's the root note."
85
+ },
86
+ "title": {
87
+ "type": "string",
88
+ "description": "The title of the note."
89
+ },
90
+ "content": {
91
+ "type": "string",
92
+ "description": "The content of the note (Markdown or Image URL)."
93
+ },
94
+ "type": {
95
+ "type": "string",
96
+ "enum": ["ROOT", "TEXT", "CODE", "IMAGE"],
97
+ "description": "The type of the note."
98
+ },
99
+ "status": {
100
+ "type": "string",
101
+ "enum": ["IDLE", "PLANNING", "GENERATING", "COMPLETED", "ERROR"],
102
+ "description": "The generation status of the note."
103
+ },
104
+ "children": {
105
+ "type": "array",
106
+ "items": { "type": "string" },
107
+ "description": "IDs of child notes."
108
+ },
109
+ "timestamp": {
110
+ "type": "number",
111
+ "description": "Timestamp when the note was created or last updated."
112
+ },
113
+ "attachments": {
114
+ "type": "string",
115
+ "description": "JSON stringified array of attachments."
116
+ },
117
+ "tags": {
118
+ "type": "array",
119
+ "items": { "type": "string" },
120
+ "description": "Tags associated with the note."
121
+ },
122
+ "linkedNoteIds": {
123
+ "type": "array",
124
+ "items": { "type": "string" },
125
+ "description": "IDs of linked notes."
126
+ }
127
+ },
128
+ "required": ["id", "userId", "projectId", "title", "content", "type", "status", "children", "timestamp"]
129
+ },
130
+ "Category": {
131
+ "title": "Category",
132
+ "description": "A category for organizing projects.",
133
+ "type": "object",
134
+ "properties": {
135
+ "id": {
136
+ "type": "string",
137
+ "description": "The category ID."
138
+ },
139
+ "userId": {
140
+ "type": "string",
141
+ "description": "The ID of the user who owns the category."
142
+ },
143
+ "name": {
144
+ "type": "string",
145
+ "description": "The name of the category."
146
+ },
147
+ "color": {
148
+ "type": "string",
149
+ "description": "The color associated with the category."
150
+ }
151
+ },
152
+ "required": ["id", "userId", "name", "color"]
153
+ }
154
+ },
155
+ "firestore": {
156
+ "/users/{userId}": {
157
+ "schema": { "$ref": "#/entities/User" },
158
+ "description": "User profiles."
159
+ },
160
+ "/projects/{projectId}": {
161
+ "schema": { "$ref": "#/entities/Project" },
162
+ "description": "Projects created by users."
163
+ },
164
+ "/notes/{noteId}": {
165
+ "schema": { "$ref": "#/entities/Note" },
166
+ "description": "Notes created by users."
167
+ },
168
+ "/categories/{categoryId}": {
169
+ "schema": { "$ref": "#/entities/Category" },
170
+ "description": "Categories created by users."
171
+ }
172
+ }
173
+ }
firebase.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { initializeApp } from 'firebase/app';
2
+ import { getAuth, GoogleAuthProvider, signInWithPopup, signOut } from 'firebase/auth';
3
+ import { getFirestore, doc, getDocFromServer } from 'firebase/firestore';
4
+
5
+ const firebaseConfig = {
6
+ apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
7
+ authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
8
+ projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
9
+ storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
10
+ messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
11
+ appId: import.meta.env.VITE_FIREBASE_APP_ID,
12
+ measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
13
+ };
14
+
15
+ // Check if Firebase config is complete to avoid "invalid-api-key" error
16
+ if (!firebaseConfig.apiKey) {
17
+ console.warn("Firebase API Key is missing. Please check your .env file or environment variables.");
18
+ }
19
+
20
+ const app = initializeApp(firebaseConfig);
21
+ export const auth = getAuth(app);
22
+ export const db = getFirestore(app, import.meta.env.VITE_FIREBASE_FIRESTORE_DATABASE_ID || '(default)');
23
+ export const googleProvider = new GoogleAuthProvider();
24
+
25
+ export const signInWithGoogle = async () => {
26
+ try {
27
+ const result = await signInWithPopup(auth, googleProvider);
28
+ return result.user;
29
+ } catch (error) {
30
+ console.error("Error signing in with Google", error);
31
+ throw error;
32
+ }
33
+ };
34
+
35
+ export const logOut = async () => {
36
+ try {
37
+ await signOut(auth);
38
+ } catch (error) {
39
+ console.error("Error signing out", error);
40
+ throw error;
41
+ }
42
+ };
43
+
44
+ // Test connection
45
+ async function testConnection() {
46
+ try {
47
+ await getDocFromServer(doc(db, 'test', 'connection'));
48
+ } catch (error) {
49
+ if(error instanceof Error && error.message.includes('the client is offline')) {
50
+ console.error("Please check your Firebase configuration. ");
51
+ }
52
+ }
53
+ }
54
+ testConnection();
firestore.rules ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ rules_version = '2';
2
+
3
+ service cloud.firestore {
4
+ match /databases/{database}/documents {
5
+
6
+ // Default deny
7
+ match /{document=**} {
8
+ allow read, write: if false;
9
+ }
10
+
11
+ // Helper functions
12
+ function isSignedIn() {
13
+ return request.auth != null;
14
+ }
15
+
16
+ function isOwner(userId) {
17
+ return isSignedIn() && request.auth.uid == userId;
18
+ }
19
+
20
+ function isValidId(id) {
21
+ return id is string && id.size() <= 128 && id.matches('^[a-zA-Z0-9_\\-]+$');
22
+ }
23
+
24
+ function incoming() {
25
+ return request.resource.data;
26
+ }
27
+
28
+ function existing() {
29
+ return resource.data;
30
+ }
31
+
32
+ // Helper for checking project collaboration
33
+ function isProjectCollaborator(projectId) {
34
+ let project = get(/databases/$(database)/documents/projects/$(projectId)).data;
35
+ return isOwner(project.creatorId) || (isSignedIn() && ('collaborators' in project) && (project.collaborators is list) && (request.auth.uid in project.collaborators));
36
+ }
37
+
38
+ // --- Users ---
39
+ match /users/{userId} {
40
+ // Allow reading user profiles if signed in (needed for collaborator lookup and sharing)
41
+ allow read: if isSignedIn();
42
+ allow create: if isOwner(userId);
43
+ allow update: if isOwner(userId) && (!('email' in incoming()) || incoming().email == existing().email);
44
+ }
45
+
46
+ // --- Projects ---
47
+ match /projects/{projectId} {
48
+ function canReadProject() {
49
+ return isOwner(resource.data.creatorId) || (request.auth.uid in resource.data.collaborators);
50
+ }
51
+
52
+ function canUpdateProject() {
53
+ // Owners can update everything. Collaborators can mostly update nothing except status/etc if we had any.
54
+ // For now, let's allow collaborators to update if they are in the list.
55
+ return isOwner(resource.data.creatorId) || (request.auth.uid in resource.data.collaborators);
56
+ }
57
+
58
+ allow read: if canReadProject();
59
+ allow create: if isSignedIn() && incoming().creatorId == request.auth.uid;
60
+ allow update: if canUpdateProject();
61
+ allow delete: if isOwner(resource.data.creatorId);
62
+ }
63
+
64
+ // --- Notes ---
65
+ match /notes/{noteId} {
66
+ function canReadNote() {
67
+ return isOwner(resource.data.userId) || isProjectCollaborator(resource.data.projectId);
68
+ }
69
+
70
+ function canWriteNote() {
71
+ let projectId = request.method == 'create' ? incoming().projectId : existing().projectId;
72
+ return isOwner(request.auth.uid) || isProjectCollaborator(projectId);
73
+ }
74
+
75
+ allow read: if canReadNote();
76
+ allow create: if isSignedIn() && incoming().userId == request.auth.uid && isProjectCollaborator(incoming().projectId);
77
+ allow update: if isSignedIn() && isProjectCollaborator(existing().projectId);
78
+ allow delete: if isSignedIn() && (isOwner(resource.data.userId) || isProjectCollaborator(resource.data.projectId));
79
+ }
80
+
81
+ // --- Categories ---
82
+ match /categories/{categoryId} {
83
+ allow read: if isOwner(resource.data.userId);
84
+ allow create: if isSignedIn() && incoming().userId == request.auth.uid;
85
+ allow update: if isOwner(resource.data.userId);
86
+ allow delete: if isOwner(resource.data.userId);
87
+ }
88
+ }
89
+ }
index.html ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>MindSpark - AI Notebook</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ theme: {
11
+ extend: {
12
+ colors: {
13
+ primary: '#6366f1',
14
+ secondary: '#4f46e5',
15
+ dark: '#1e293b',
16
+ surface: '#334155',
17
+ },
18
+ fontFamily: {
19
+ sans: ['Inter', 'sans-serif'],
20
+ },
21
+ },
22
+ },
23
+ }
24
+ </script>
25
+ <style>
26
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
27
+
28
+ :root {
29
+ --bg-deep: #0B0F19;
30
+ --bg-surface: #0F1117;
31
+ --accent: #6366f1;
32
+ }
33
+
34
+ body {
35
+ font-family: 'Inter', sans-serif;
36
+ background-color: var(--bg-deep);
37
+ color: #f8fafc;
38
+ -webkit-font-smoothing: antialiased;
39
+ -moz-osx-font-smoothing: grayscale;
40
+ }
41
+
42
+ /* Fluid Typography */
43
+ html {
44
+ font-size: 14px;
45
+ }
46
+ @media (min-width: 768px) {
47
+ html { font-size: 15px; }
48
+ }
49
+ @media (min-width: 1280px) {
50
+ html { font-size: 16px; }
51
+ }
52
+
53
+ /* Custom scrollbar */
54
+ ::-webkit-scrollbar {
55
+ width: 6px;
56
+ height: 6px;
57
+ }
58
+ ::-webkit-scrollbar-track {
59
+ background: transparent;
60
+ }
61
+ ::-webkit-scrollbar-thumb {
62
+ background: rgba(255, 255, 255, 0.1);
63
+ border-radius: 10px;
64
+ }
65
+ ::-webkit-scrollbar-thumb:hover {
66
+ background: rgba(255, 255, 255, 0.2);
67
+ }
68
+
69
+ /* Animations */
70
+ @keyframes fadeIn {
71
+ from { opacity: 0; transform: translateY(10px); }
72
+ to { opacity: 1; transform: translateY(0); }
73
+ }
74
+ .animate-fadeIn {
75
+ animation: fadeIn 0.6s cubic-bezier(0.23, 1, 0.32, 1) forwards;
76
+ }
77
+
78
+ /* Glassmorphism utilities */
79
+ .glass {
80
+ background: rgba(255, 255, 255, 0.03);
81
+ backdrop-filter: blur(12px);
82
+ -webkit-backdrop-filter: blur(12px);
83
+ border: 1px solid rgba(255, 255, 255, 0.05);
84
+ }
85
+
86
+ .glass-dark {
87
+ background: rgba(15, 17, 23, 0.6);
88
+ backdrop-filter: blur(20px);
89
+ -webkit-backdrop-filter: blur(20px);
90
+ border: 1px solid rgba(255, 255, 255, 0.05);
91
+ }
92
+
93
+ /* Custom scrollbar for specific elements */
94
+ .custom-scrollbar::-webkit-scrollbar {
95
+ width: 4px;
96
+ }
97
+ .custom-scrollbar::-webkit-scrollbar-thumb {
98
+ background: rgba(99, 102, 241, 0.2);
99
+ }
100
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
101
+ background: rgba(99, 102, 241, 0.4);
102
+ }
103
+
104
+ /* Mermaid Styles */
105
+ .mermaid-container {
106
+ width: 100%;
107
+ overflow-x: auto;
108
+ background: rgba(255, 255, 255, 0.02);
109
+ padding: 2rem;
110
+ border-radius: 1.5rem;
111
+ border: 1px solid rgba(255, 255, 255, 0.05);
112
+ }
113
+ .mermaid-container svg {
114
+ max-width: 100% !important;
115
+ height: auto !important;
116
+ }
117
+ .mermaid-container .node rect,
118
+ .mermaid-container .node circle {
119
+ transition: all 0.3s ease;
120
+ }
121
+ .mermaid-container .node:hover rect,
122
+ .mermaid-container .node:hover circle {
123
+ filter: brightness(1.2);
124
+ stroke-width: 3px;
125
+ stroke: #6366f1;
126
+ }
127
+ </style>
128
+ <script type="importmap">
129
+ {
130
+ "imports": {
131
+ "@google/genai": "https://aistudiocdn.com/@google/genai@^1.30.0",
132
+ "react/": "https://aistudiocdn.com/react@^19.2.0/",
133
+ "react": "https://aistudiocdn.com/react@^19.2.0",
134
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/"
135
+ }
136
+ }
137
+ </script>
138
+ <link rel="stylesheet" href="/index.css">
139
+ </head>
140
+ <body>
141
+ <div id="root"></div>
142
+ <script type="module" src="/index.tsx"></script>
143
+ </body>
144
+ </html>
index.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ const rootElement = document.getElementById('root');
6
+ if (!rootElement) {
7
+ throw new Error("Could not find root element to mount to");
8
+ }
9
+
10
+ const root = ReactDOM.createRoot(rootElement);
11
+ root.render(
12
+ <React.StrictMode>
13
+ <App />
14
+ </React.StrictMode>
15
+ );
metadata.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "name": "MindSpark",
3
+ "description": "AI-powered smart notebook that transforms your ideas into structured step-by-step guides.",
4
+ "requestFramePermissions": ["microphone"]
5
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,39 +1,41 @@
1
  {
2
- "name": "react-template",
3
- "version": "0.1.0",
4
  "private": true,
5
- "dependencies": {
6
- "@testing-library/dom": "^10.4.0",
7
- "@testing-library/jest-dom": "^6.6.3",
8
- "@testing-library/react": "^16.3.0",
9
- "@testing-library/user-event": "^13.5.0",
10
- "react": "^19.1.0",
11
- "react-dom": "^19.1.0",
12
- "react-scripts": "5.0.1",
13
- "web-vitals": "^2.1.4"
14
- },
15
  "scripts": {
16
- "start": "react-scripts start",
17
- "build": "react-scripts build",
18
- "test": "react-scripts test",
19
- "eject": "react-scripts eject"
 
20
  },
21
- "eslintConfig": {
22
- "extends": [
23
- "react-app",
24
- "react-app/jest"
25
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  },
27
- "browserslist": {
28
- "production": [
29
- ">0.2%",
30
- "not dead",
31
- "not op_mini all"
32
- ],
33
- "development": [
34
- "last 1 chrome version",
35
- "last 1 firefox version",
36
- "last 1 safari version"
37
- ]
38
  }
39
  }
 
1
  {
2
+ "name": "akıl-defteri",
 
3
  "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
 
 
 
 
 
 
 
 
6
  "scripts": {
7
+ "dev": "vite",
8
+ "start": "vite",
9
+ "build": "vite build",
10
+ "preview": "vite preview",
11
+ "lint": "tsc --noEmit"
12
  },
13
+ "dependencies": {
14
+ "@dnd-kit/core": "^6.3.1",
15
+ "@dnd-kit/sortable": "^10.0.0",
16
+ "@dnd-kit/utilities": "^3.2.2",
17
+ "@google/genai": "^1.30.0",
18
+ "@google/generative-ai": "^0.24.1",
19
+ "@types/react-syntax-highlighter": "^15.5.13",
20
+ "firebase": "^12.11.0",
21
+ "html2canvas": "^1.4.1",
22
+ "html2pdf.js": "^0.14.0",
23
+ "jspdf": "^4.2.0",
24
+ "lucide-react": "^1.7.0",
25
+ "mermaid": "^11.14.0",
26
+ "motion": "^12.38.0",
27
+ "react": "^19.2.0",
28
+ "react-dom": "^19.2.0",
29
+ "react-markdown": "^10.1.0",
30
+ "react-syntax-highlighter": "^16.1.1",
31
+ "react-zoom-pan-pinch": "^4.0.3",
32
+ "remark-gfm": "^4.0.1",
33
+ "vite-plugin-pwa": "^1.2.0"
34
  },
35
+ "devDependencies": {
36
+ "@types/node": "^22.14.0",
37
+ "@vitejs/plugin-react": "^5.0.0",
38
+ "typescript": "~5.8.2",
39
+ "vite": "^6.2.0"
 
 
 
 
 
 
40
  }
41
  }
services/geminiService.ts ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { GoogleGenAI, Type, Schema } from "@google/genai";
2
+ import { ProjectPlan, NoteType, StyleMemory } from "../types";
3
+
4
+ const getAI = () => {
5
+ const userKey = localStorage.getItem('user_gemini_api_key');
6
+ const apiKey = userKey || process.env.GEMINI_API_KEY;
7
+ if (!apiKey) {
8
+ console.warn("Gemini API Key is missing. Please set it in Settings.");
9
+ }
10
+ return new GoogleGenAI({ apiKey: apiKey || 'dummy-key' });
11
+ };
12
+
13
+ // Helper to compress base64 images to avoid Firestore 1MB limit
14
+ const resizeBase64Image = (base64Str: string, maxWidth = 800, maxHeight = 800): Promise<string> => {
15
+ return new Promise((resolve) => {
16
+ const img = new Image();
17
+ img.src = base64Str;
18
+ img.onload = () => {
19
+ const canvas = document.createElement('canvas');
20
+ let width = img.width;
21
+ let height = img.height;
22
+
23
+ if (width > height) {
24
+ if (width > maxWidth) {
25
+ height *= maxWidth / width;
26
+ width = maxWidth;
27
+ }
28
+ } else {
29
+ if (height > maxHeight) {
30
+ width *= maxHeight / height;
31
+ height = maxHeight;
32
+ }
33
+ }
34
+
35
+ canvas.width = width;
36
+ canvas.height = height;
37
+ const ctx = canvas.getContext('2d');
38
+ if (ctx) {
39
+ ctx.drawImage(img, 0, 0, width, height);
40
+ resolve(canvas.toDataURL('image/jpeg', 0.7));
41
+ } else {
42
+ resolve(base64Str);
43
+ }
44
+ };
45
+ img.onerror = () => resolve(base64Str);
46
+ });
47
+ };
48
+
49
+ // System instruction for the planner
50
+ const PLANNER_SYSTEM_INSTRUCTION = `
51
+ You are the chief architect of the "MindSpark" system.
52
+ Analyze the user's complex request and plan it like a task distribution for an expert team.
53
+
54
+ YOUR TASK:
55
+ 1. Determine 4-8 steps required for the project.
56
+ 2. Assign an "Expert Agent" (assignedAgent) and their "Role" (agentRole) for each step.
57
+ 3. Steps should follow each other and be in a logical flow.
58
+
59
+ STYLE GUIDE (MEMORY):
60
+ If any past styles or preferences are provided to you, keep this tone and approach in your planning.
61
+
62
+ AGENT ASSIGNMENT RULES:
63
+ - Determine specific experts based on the project type (Software Developer, Designer, Writer, Analyst, Strategist, etc.).
64
+ - Determine the 'type' property of each step based on the content: 'text', 'code', 'image'.
65
+ - Provide the answer strictly in JSON format. Use English language.
66
+ `;
67
+
68
+ // Schema for structured JSON output
69
+ const planSchema: Schema = {
70
+ type: Type.OBJECT,
71
+ properties: {
72
+ title: { type: Type.STRING, description: "A creative and engaging main title for the project" },
73
+ summary: { type: Type.STRING, description: "A short, motivating summary of what this notebook is about." },
74
+ steps: {
75
+ type: Type.ARRAY,
76
+ items: {
77
+ type: Type.OBJECT,
78
+ properties: {
79
+ title: { type: Type.STRING, description: "Title of the step" },
80
+ description: { type: Type.STRING, description: "A short summary of what will be done in this step." },
81
+ type: { type: Type.STRING, enum: ["text", "code", "image"], description: "Content type" },
82
+ assignedAgent: { type: Type.STRING, description: "Name of the expert agent taking the task (e.g., 'Code Architect')" },
83
+ agentRole: { type: Type.STRING, description: "Specific role and responsibility assigned to the agent in this step." },
84
+ },
85
+ required: ["title", "description", "type", "assignedAgent", "agentRole"],
86
+ },
87
+ },
88
+ },
89
+ required: ["title", "summary", "steps"],
90
+ };
91
+
92
+ const isQuotaError = (error: any): boolean => {
93
+ const errorStr = JSON.stringify(error).toLowerCase();
94
+ return (
95
+ error?.status === 'RESOURCE_EXHAUSTED' ||
96
+ error?.code === 429 ||
97
+ errorStr.includes('429') ||
98
+ errorStr.includes('resource_exhausted') ||
99
+ errorStr.includes('quota exceeded') ||
100
+ errorStr.includes('limit reached')
101
+ );
102
+ };
103
+
104
+ export const boostPrompt = async (prompt: string): Promise<string> => {
105
+ try {
106
+ const response = await getAI().models.generateContent({
107
+ model: 'gemini-3-flash-preview',
108
+ contents: `Rewrite and enrich the following text to be a perfect command (prompt) or a great project idea to be given to an AI assistant.
109
+ - If it's a short and simple idea, detail it and add depth.
110
+ - If it's complex and messy, structure and clarify it.
111
+ - Use a professional, inspiring, and highly effective language.
112
+ - Provide only the improved text, do not add any explanations before or after like "Here is the text:".
113
+
114
+ Original Text:
115
+ ${prompt}`,
116
+ });
117
+ return response.text ? response.text.trim() : prompt;
118
+ } catch (error) {
119
+ console.error("Prompt boost error:", error);
120
+ if (isQuotaError(error)) {
121
+ return prompt; // Fallback to original prompt if quota hit
122
+ }
123
+ throw error;
124
+ }
125
+ };
126
+
127
+ export const transcribeAudio = async (base64Audio: string): Promise<string> => {
128
+ try {
129
+ const response = await getAI().models.generateContent({
130
+ model: 'gemini-2.5-flash',
131
+ contents: [
132
+ "Transcribe this audio recording into text. Return only the text, do not provide any other explanation. If the audio is not understandable, leave it blank.",
133
+ {
134
+ inlineData: {
135
+ mimeType: "audio/webm",
136
+ data: base64Audio
137
+ }
138
+ }
139
+ ]
140
+ });
141
+ return response.text ? response.text.trim() : "";
142
+ } catch (error) {
143
+ console.error("Transcription error:", error);
144
+ if (isQuotaError(error)) {
145
+ throw new Error("AI system is very busy (Quota reached). Please try typing your input as text.");
146
+ }
147
+ throw error;
148
+ }
149
+ };
150
+
151
+ export const createProjectPlan = async (userPrompt: string, memories: StyleMemory[] = []): Promise<ProjectPlan> => {
152
+ try {
153
+ const memoryContext = memories.length > 0
154
+ ? `\n\nPAST PREFERENCES AND MEMORY:\n${memories.map(m => `- Project: ${m.projectName}, Style: ${m.styleKeywords.join(', ')}, Summary: ${m.summary}`).join('\n')}\n\nPlease take these styles and tone into account.`
155
+ : "";
156
+
157
+ const response = await getAI().models.generateContent({
158
+ model: 'gemini-3.1-pro-preview',
159
+ contents: userPrompt + memoryContext,
160
+ config: {
161
+ systemInstruction: PLANNER_SYSTEM_INSTRUCTION,
162
+ responseMimeType: "application/json",
163
+ responseSchema: planSchema
164
+ },
165
+ });
166
+
167
+ if (!response.text) throw new Error("Plan could not be created.");
168
+ return JSON.parse(response.text) as ProjectPlan;
169
+ } catch (error) {
170
+ console.error("Plan Error:", error);
171
+ if (isQuotaError(error)) {
172
+ throw new Error("System is very busy right now (Quota limit exceeded). Please try again in a few minutes or use a shorter description.");
173
+ }
174
+ throw error;
175
+ }
176
+ };
177
+
178
+ export const generateStepContent = async (
179
+ projectTitle: string,
180
+ stepTitle: string,
181
+ stepDescription: string,
182
+ stepType: NoteType,
183
+ memories: StyleMemory[] = []
184
+ ): Promise<string> => {
185
+ const memoryContext = memories.length > 0
186
+ ? `\nPAST STYLE AND TONE:\n${memories.map(m => `- ${m.styleKeywords.join(', ')}`).join(', ')}\nPlease reflect this style and tone in this content as well.`
187
+ : "";
188
+
189
+ const contextPrompt = `
190
+ CONTEXT: We are working on a project named "${projectTitle}".
191
+ CURRENT TASK: "${stepTitle}"
192
+ TASK DETAIL: ${stepDescription}
193
+ ${memoryContext}
194
+
195
+ YOUR MISSION:
196
+ Prepare a detailed, educational, and directly applicable content for this step in the "MindSpark" format.
197
+ When the user reads this page, they should have everything they need to complete this step.
198
+
199
+ If Type is 'TEXT':
200
+ - Explain the subject in depth.
201
+ - Increase readability using bullet points, lists, and bold text.
202
+ - Speak like a professional mentor.
203
+
204
+ If Type is 'CODE':
205
+ - Write the necessary code blocks.
206
+ - Explain what the codes do with comment lines or explanations.
207
+
208
+ Use Markdown format.
209
+ `;
210
+
211
+ try {
212
+ if (stepType === NoteType.IMAGE) {
213
+ const imagePrompt = `
214
+ High quality concept art illustration of: ${stepTitle}.
215
+ Context: ${stepDescription}.
216
+ Project Theme: ${projectTitle}.
217
+ Style: Digital art, highly detailed, cinematic lighting, intricate textures, atmospheric, professional composition, masterpiece, 8k resolution, concept art style.
218
+ No text, no labels, no watermarks, high quality.
219
+ `;
220
+
221
+ try {
222
+ const response = await getAI().models.generateContent({
223
+ model: 'gemini-2.5-flash-image',
224
+ contents: { parts: [{ text: imagePrompt }] },
225
+ config: {
226
+ imageConfig: {
227
+ aspectRatio: '16:9',
228
+ },
229
+ },
230
+ });
231
+
232
+ const parts = response.candidates?.[0]?.content?.parts || [];
233
+ for (const part of parts) {
234
+ if (part.inlineData) {
235
+ const base64 = `data:${part.inlineData.mimeType || 'image/jpeg'};base64,${part.inlineData.data}`;
236
+ return await resizeBase64Image(base64);
237
+ }
238
+ }
239
+ } catch (innerError) {
240
+ console.error("Image generation failed:", innerError);
241
+ if (isQuotaError(innerError)) {
242
+ return "--- Image could not be generated (System Busy / Quota Limit) ---\n" + stepDescription;
243
+ }
244
+ }
245
+
246
+ return "--- Image could not be generated ---\n" + stepDescription;
247
+ } else if (stepType === NoteType.TEXT) {
248
+ const textPromise = getAI().models.generateContent({
249
+ model: 'gemini-3-flash-preview',
250
+ contents: contextPrompt,
251
+ }).catch(e => {
252
+ console.error("Text content generation error:", e);
253
+ if (isQuotaError(e)) {
254
+ return { text: "Content could not be generated because the system is busy (Quota reached). Please try the 'Regenerate' option later." };
255
+ }
256
+ throw e;
257
+ });
258
+
259
+ const imagePrompt = `
260
+ High quality concept art illustration of: ${stepTitle}.
261
+ Context: ${stepDescription}.
262
+ Project Theme: ${projectTitle}.
263
+ Style: Digital art, highly detailed, cinematic lighting, concept art style, clean composition.
264
+ No text, no labels, no watermarks, high quality.
265
+ `;
266
+
267
+ const imagePromise = getAI().models.generateContent({
268
+ model: 'gemini-2.5-flash-image',
269
+ contents: { parts: [{ text: imagePrompt }] },
270
+ config: {
271
+ imageConfig: {
272
+ aspectRatio: '16:9',
273
+ },
274
+ },
275
+ }).catch(e => {
276
+ console.error("Image generation failed for text note:", e);
277
+ return null;
278
+ });
279
+
280
+ const [textResponse, imageResponse] = await Promise.all([textPromise, imagePromise]);
281
+ let content = (textResponse as any).text || "Content could not be generated.";
282
+
283
+ if (imageResponse) {
284
+ const parts = imageResponse.candidates?.[0]?.content?.parts || [];
285
+ for (const part of parts) {
286
+ if (part.inlineData) {
287
+ const base64 = `data:${part.inlineData.mimeType || 'image/jpeg'};base64,${part.inlineData.data}`;
288
+ const resizedBase64 = await resizeBase64Image(base64);
289
+ content = `![${stepTitle}](${resizedBase64})\n\n` + content;
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ return content;
295
+ } else {
296
+ // Code generation
297
+ try {
298
+ const response = await getAI().models.generateContent({
299
+ model: 'gemini-3-flash-preview',
300
+ contents: contextPrompt,
301
+ });
302
+ return response.text || "Content could not be generated.";
303
+ } catch (e) {
304
+ if (isQuotaError(e)) {
305
+ return "```\n// Code could not be generated due to system busy.\n// Please try again later.\n```";
306
+ }
307
+ throw e;
308
+ }
309
+ }
310
+ } catch (error) {
311
+ console.error("Content Gen Error:", error);
312
+ if (isQuotaError(error)) {
313
+ return "System is very busy right now. Please try this step again later by clicking 'Regenerate'.";
314
+ }
315
+ throw error;
316
+ }
317
+ };
318
+
319
+ export const chatWithStep = async (
320
+ projectTitle: string,
321
+ noteTitle: string,
322
+ noteContent: string,
323
+ userQuestion: string,
324
+ history: any[] = []
325
+ ): Promise<string> => {
326
+ try {
327
+ const contents = [
328
+ ...history.map(h => ({
329
+ role: h.role === 'model' ? 'model' : 'user',
330
+ parts: h.parts
331
+ })),
332
+ { role: 'user', parts: [{ text: userQuestion }] }
333
+ ];
334
+
335
+ const response = await getAI().models.generateContent({
336
+ model: 'gemini-3-flash-preview',
337
+ contents: contents,
338
+ config: {
339
+ systemInstruction: `You are an expert in the "MindSpark" project.
340
+ CONTEXT: We are in the "${noteTitle}" step within the "${projectTitle}" project.
341
+ CONTENT OF THIS STEP:
342
+ ${noteContent.substring(0, 1000)}...
343
+
344
+ YOUR MISSION: To answer the user's questions about this step, deepen the content, or offer alternative suggestions.
345
+ - Provide short, concise, and technically deep answers.
346
+ - Be helpful, show the way.
347
+ - Use Markdown.`
348
+ }
349
+ });
350
+
351
+ return response.text || "Sorry, an answer could not be generated.";
352
+ } catch (error) {
353
+ console.error("Step Chat Error:", error);
354
+ if (isQuotaError(error)) {
355
+ return "Sorry, the system is currently busy (Quota reached). Please ask again in a few minutes.";
356
+ }
357
+ throw error;
358
+ }
359
+ };
src/components/FlowchartView.tsx ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback, useRef } from 'react';
2
+ import mermaid from 'mermaid';
3
+ import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
4
+ import { Note, NoteType } from '../../types';
5
+ import { chatWithStep } from '../../services/geminiService';
6
+ import ReactMarkdown from 'react-markdown';
7
+ import { motion, AnimatePresence } from 'framer-motion';
8
+
9
+ interface FlowchartViewProps {
10
+ notes: Record<string, Note>;
11
+ projectTitle: string;
12
+ rootNoteId: string;
13
+ }
14
+
15
+ interface ChatMessage {
16
+ role: 'user' | 'model';
17
+ content: string;
18
+ }
19
+
20
+ const FlowchartView: React.FC<FlowchartViewProps> = ({ notes, projectTitle, rootNoteId }) => {
21
+ const [svgContent, setSvgContent] = useState<string>('');
22
+ const [isRendering, setIsRendering] = useState(false);
23
+ const [selectedNoteId, setSelectedNoteId] = useState<string | null>(null);
24
+ const [chatHistories, setChatHistories] = useState<Record<string, ChatMessage[]>>({});
25
+ const [userInput, setUserInput] = useState('');
26
+ const [isAsking, setIsAsking] = useState(false);
27
+ const chatEndRef = useRef<HTMLDivElement>(null);
28
+
29
+ useEffect(() => {
30
+ mermaid.initialize({
31
+ startOnLoad: false,
32
+ theme: 'dark',
33
+ securityLevel: 'loose',
34
+ flowchart: {
35
+ useMaxWidth: true,
36
+ htmlLabels: true,
37
+ curve: 'basis'
38
+ }
39
+ });
40
+ }, []);
41
+
42
+ const generateMermaidCode = useCallback(() => {
43
+ if (!notes[rootNoteId]) return "";
44
+
45
+ let code = "graph TD\n";
46
+
47
+ // Styling
48
+ code += "classDef root fill:#4f46e5,stroke:#fff,stroke-width:2px,color:#fff,cursor:pointer;\n";
49
+ code += "classDef text fill:#1e293b,stroke:#475569,stroke-width:1px,color:#cbd5e1,cursor:pointer;\n";
50
+ code += "classDef code fill:#0f172a,stroke:#3b82f6,stroke-width:1px,color:#60a5fa,cursor:pointer;\n";
51
+ code += "classDef image fill:#1e293b,stroke:#8b5cf6,stroke-width:1px,color:#a78bfa,cursor:pointer;\n";
52
+ code += "classDef selected fill:#fbbf24,stroke:#fff,stroke-width:3px,color:#000,cursor:pointer;\n";
53
+
54
+ const visited = new Set<string>();
55
+
56
+ const processNote = (noteId: string) => {
57
+ if (visited.has(noteId)) return "";
58
+ visited.add(noteId);
59
+
60
+ const note = notes[noteId];
61
+ if (!note) return "";
62
+
63
+ let nodeCode = "";
64
+ const id = note.id.replace(/-/g, '');
65
+ const title = note.title.replace(/[()"[\]{}]/g, "'");
66
+
67
+ // Node definition
68
+ const isSelected = selectedNoteId === note.id;
69
+ const styleClass = isSelected ? 'selected' : (
70
+ note.type === NoteType.ROOT ? 'root' :
71
+ note.type === NoteType.CODE ? 'code' :
72
+ note.type === NoteType.IMAGE ? 'image' : 'text'
73
+ );
74
+
75
+ if (note.type === NoteType.ROOT) {
76
+ nodeCode += ` ${id}("${title}"):::${styleClass}\n`;
77
+ } else {
78
+ nodeCode += ` ${id}["${title}"]:::${styleClass}\n`;
79
+ }
80
+
81
+ // Connections
82
+ if (note.children && note.children.length > 0) {
83
+ note.children.forEach(childId => {
84
+ const child = notes[childId];
85
+ if (child) {
86
+ const childCleanId = child.id.replace(/-/g, '');
87
+ nodeCode += ` ${id} --> ${childCleanId}\n`;
88
+ nodeCode += processNote(childId);
89
+ }
90
+ });
91
+ }
92
+
93
+ return nodeCode;
94
+ };
95
+
96
+ code += processNote(rootNoteId);
97
+ return code;
98
+ }, [notes, rootNoteId, selectedNoteId]);
99
+
100
+ useEffect(() => {
101
+ const code = generateMermaidCode();
102
+ if (!code) return;
103
+
104
+ let isMounted = true;
105
+ const renderDiagram = async () => {
106
+ setIsRendering(true);
107
+ try {
108
+ const id = `mermaid-render-${Date.now()}`;
109
+ const { svg } = await mermaid.render(id, code);
110
+ if (isMounted) {
111
+ setSvgContent(svg);
112
+ }
113
+ } catch (error) {
114
+ console.error("Mermaid render error:", error);
115
+ } finally {
116
+ if (isMounted) setIsRendering(false);
117
+ }
118
+ };
119
+
120
+ renderDiagram();
121
+ return () => { isMounted = false; };
122
+ }, [generateMermaidCode]);
123
+
124
+ // Handle node clicks
125
+ const handleSvgClick = (e: React.MouseEvent) => {
126
+ const target = e.target as SVGElement;
127
+ const node = target.closest('.node');
128
+ if (node) {
129
+ const nodeIdClean = node.id.split('-')[0]; // Mermaid nodes have IDs
130
+ // Map back to our IDs
131
+ const foundId = Object.keys(notes).find(id => id.replace(/-/g, '') === nodeIdClean);
132
+ if (foundId) {
133
+ setSelectedNoteId(foundId);
134
+ }
135
+ }
136
+ };
137
+
138
+ const handleSendMessage = async () => {
139
+ if (!selectedNoteId || !userInput.trim() || isAsking) return;
140
+
141
+ const note = notes[selectedNoteId];
142
+ const currentHistory = chatHistories[selectedNoteId] || [];
143
+ const newHistory: ChatMessage[] = [...currentHistory, { role: 'user', content: userInput }];
144
+
145
+ setChatHistories(prev => ({ ...prev, [selectedNoteId]: newHistory }));
146
+ setUserInput('');
147
+ setIsAsking(true);
148
+
149
+ try {
150
+ const response = await chatWithStep(
151
+ projectTitle,
152
+ note.title,
153
+ note.content,
154
+ userInput,
155
+ currentHistory.map(m => ({ role: m.role, parts: [{ text: m.content }] }))
156
+ );
157
+
158
+ setChatHistories(prev => ({
159
+ ...prev,
160
+ [selectedNoteId]: [...newHistory, { role: 'model', content: response }]
161
+ }));
162
+ } catch (error) {
163
+ console.error("Chat error:", error);
164
+ } finally {
165
+ setIsAsking(false);
166
+ }
167
+ };
168
+
169
+ useEffect(() => {
170
+ chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
171
+ }, [chatHistories, selectedNoteId]);
172
+
173
+ const selectedNote = selectedNoteId ? notes[selectedNoteId] : null;
174
+
175
+ return (
176
+ <div className="w-full h-full min-h-[600px] bg-slate-900/50 backdrop-blur-xl rounded-[2.5rem] border border-white/5 flex flex-col overflow-hidden relative">
177
+ <div className="flex items-center justify-between p-8 border-b border-white/5 bg-white/5">
178
+ <div>
179
+ <h3 className="text-xl font-black text-white tracking-tight flex items-center gap-3">
180
+ <div className="w-8 h-8 bg-indigo-500 rounded-xl flex items-center justify-center">
181
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
182
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clipRule="evenodd" />
183
+ </svg>
184
+ </div>
185
+ Akıl Haritası Tuvali
186
+ </h3>
187
+ <p className="text-sm text-slate-400 mt-1">"{projectTitle}" projesinin etkileşimli haritası</p>
188
+ </div>
189
+ <div className="flex items-center gap-4">
190
+ {isRendering && (
191
+ <div className="text-[10px] text-indigo-400 font-black tracking-widest animate-pulse uppercase">Modelleniyor...</div>
192
+ )}
193
+ <div className="flex bg-black/20 p-1 rounded-xl border border-white/5">
194
+ <div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-indigo-500/20">
195
+ <div className="w-2 h-2 rounded-full bg-indigo-400"></div>
196
+ <span className="text-[10px] text-indigo-300 font-black uppercase tracking-wider">Kök Bilgi</span>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ <div className="flex-1 flex overflow-hidden relative">
203
+ {/* Canvas Area */}
204
+ <div className="flex-1 bg-[#0F1117] relative cursor-grab active:cursor-grabbing" onClick={handleSvgClick}>
205
+ <TransformWrapper
206
+ initialScale={1}
207
+ minScale={0.5}
208
+ maxScale={3}
209
+ centerOnInit
210
+ >
211
+ {({ zoomIn, zoomOut, resetTransform }) => (
212
+ <>
213
+ <div className="absolute top-4 left-4 z-10 flex flex-col gap-2">
214
+ <button onClick={() => zoomIn()} className="p-3 bg-white/5 hover:bg-white/10 text-white rounded-2xl border border-white/5 transition-all"><svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" /></svg></button>
215
+ <button onClick={() => zoomOut()} className="p-3 bg-white/5 hover:bg-white/10 text-white rounded-2xl border border-white/5 transition-all"><svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" /></svg></button>
216
+ <button onClick={() => resetTransform()} className="p-3 bg-white/5 hover:bg-white/10 text-white rounded-2xl border border-white/10 transition-all font-black text-[10px] uppercase">Sıfırla</button>
217
+ </div>
218
+ <TransformComponent wrapperClass="!w-full !h-full" contentClass="!w-full !h-full flex items-center justify-center">
219
+ <div
220
+ className="mermaid-container p-20"
221
+ dangerouslySetInnerHTML={{ __html: svgContent }}
222
+ ></div>
223
+ </TransformComponent>
224
+ </>
225
+ )}
226
+ </TransformWrapper>
227
+ </div>
228
+
229
+ {/* AI Chat Assistant Sidebar */}
230
+ <AnimatePresence>
231
+ {selectedNote && (
232
+ <motion.div
233
+ initial={{ x: 400 }}
234
+ animate={{ x: 0 }}
235
+ exit={{ x: 400 }}
236
+ className="w-96 bg-[#161B22] border-l border-white/5 flex flex-col shadow-2xl z-20"
237
+ >
238
+ <div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
239
+ <div className="flex items-center gap-3">
240
+ <div className="w-10 h-10 bg-indigo-500/10 text-indigo-400 rounded-2xl flex items-center justify-center">
241
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z" clipRule="evenodd" /></svg>
242
+ </div>
243
+ <div className="min-w-0">
244
+ <h4 className="text-white font-black text-sm truncate">{selectedNote.title}</h4>
245
+ <p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">Adım Asistanı</p>
246
+ </div>
247
+ </div>
248
+ <button onClick={() => setSelectedNoteId(null)} className="p-2 text-slate-500 hover:text-white transition-colors">
249
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" /></svg>
250
+ </button>
251
+ </div>
252
+
253
+ <div className="flex-1 overflow-y-auto p-6 space-y-4 custom-scrollbar bg-[#0D1117]">
254
+ {(!chatHistories[selectedNoteId || ''] || chatHistories[selectedNoteId || ''].length === 0) && (
255
+ <div className="h-full flex flex-col items-center justify-center text-center opacity-40">
256
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
257
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
258
+ </svg>
259
+ <p className="text-sm font-medium">Bu adım hakkında<br/>AI asistanına soru sor.</p>
260
+ </div>
261
+ )}
262
+ {chatHistories[selectedNoteId || '']?.map((msg, idx) => (
263
+ <div key={idx} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
264
+ <div className={`max-w-[85%] px-4 py-3 rounded-2xl text-sm ${msg.role === 'user' ? 'bg-indigo-600 text-white font-medium' : 'bg-white/5 text-slate-200 border border-white/5'}`}>
265
+ <div className="markdown-body text-xs prose-invert leading-relaxed">
266
+ <ReactMarkdown>
267
+ {msg.content}
268
+ </ReactMarkdown>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ ))}
273
+ {isAsking && (
274
+ <div className="flex justify-start">
275
+ <div className="bg-white/5 px-4 py-3 rounded-2xl flex items-center gap-2">
276
+ <div className="flex gap-1">
277
+ <div className="w-1 h-1 bg-slate-500 rounded-full animate-bounce"></div>
278
+ <div className="w-1 h-1 bg-slate-500 rounded-full animate-bounce [animation-delay:0.2s]"></div>
279
+ <div className="w-1 h-1 bg-slate-500 rounded-full animate-bounce [animation-delay:0.4s]"></div>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ )}
284
+ <div ref={chatEndRef} />
285
+ </div>
286
+
287
+ <div className="p-4 border-t border-white/5 bg-[#0F1117]">
288
+ <div className="relative group">
289
+ <input
290
+ type="text"
291
+ value={userInput}
292
+ onChange={(e) => setUserInput(e.target.value)}
293
+ onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
294
+ placeholder="Bir şey sor..."
295
+ className="w-full bg-white/5 border border-white/10 rounded-2xl px-5 py-4 text-sm text-white focus:outline-none focus:border-indigo-500 transition-all pr-12"
296
+ />
297
+ <button
298
+ onClick={handleSendMessage}
299
+ disabled={isAsking || !userInput.trim()}
300
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-2.5 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:hover:bg-indigo-600 rounded-xl text-white transition-all shadow-lg shadow-indigo-600/20"
301
+ >
302
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" /></svg>
303
+ </button>
304
+ </div>
305
+ <p className="text-[10px] text-center text-slate-600 mt-3 font-bold uppercase tracking-widest">Gemini 3 Flash Pro</p>
306
+ </div>
307
+ </motion.div>
308
+ )}
309
+ </AnimatePresence>
310
+ </div>
311
+ </div>
312
+ );
313
+ };
314
+
315
+ export default FlowchartView;
tsconfig.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "types": [
14
+ "node",
15
+ "vite/client"
16
+ ],
17
+ "moduleResolution": "bundler",
18
+ "isolatedModules": true,
19
+ "moduleDetection": "force",
20
+ "allowJs": true,
21
+ "jsx": "react-jsx",
22
+ "paths": {
23
+ "@/*": [
24
+ "./*"
25
+ ]
26
+ },
27
+ "allowImportingTsExtensions": true,
28
+ "noEmit": true
29
+ }
30
+ }
types.ts ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export enum NoteType {
2
+ ROOT = 'ROOT',
3
+ TEXT = 'TEXT',
4
+ CODE = 'CODE',
5
+ IMAGE = 'IMAGE',
6
+ }
7
+
8
+ export enum GenerationStatus {
9
+ IDLE = 'IDLE',
10
+ PLANNING = 'PLANNING',
11
+ GENERATING = 'GENERATING',
12
+ COMPLETED = 'COMPLETED',
13
+ ERROR = 'ERROR',
14
+ }
15
+
16
+ export interface NoteStep {
17
+ title: string;
18
+ description: string;
19
+ type: 'text' | 'code' | 'image';
20
+ agentRole?: string;
21
+ assignedAgent?: string;
22
+ }
23
+
24
+ export interface ProjectPlan {
25
+ title: string;
26
+ summary: string;
27
+ steps: NoteStep[];
28
+ }
29
+
30
+ export interface Category {
31
+ id: string;
32
+ name: string;
33
+ color: string;
34
+ }
35
+
36
+ export interface Attachment {
37
+ id: string;
38
+ type: 'image' | 'audio' | 'file';
39
+ url: string; // Base64 data URL
40
+ name: string;
41
+ }
42
+
43
+ export interface Note {
44
+ id: string;
45
+ projectId: string;
46
+ parentId: string | null;
47
+ title: string;
48
+ content: string; // Markdown or Image URL
49
+ type: NoteType;
50
+ status: GenerationStatus;
51
+ children: string[]; // IDs of children
52
+ timestamp: number;
53
+ attachments?: Attachment[];
54
+ tags?: string[];
55
+ linkedNoteIds?: string[];
56
+ isTask?: boolean;
57
+ isCompleted?: boolean;
58
+ dueDate?: number;
59
+ agentRole?: string;
60
+ assignedAgent?: string;
61
+ lastEditedBy?: string;
62
+ lastEditedAt?: number;
63
+ }
64
+
65
+ export interface StyleMemory {
66
+ id: string;
67
+ userId: string;
68
+ projectName: string;
69
+ styleKeywords: string[];
70
+ summary: string;
71
+ timestamp: number;
72
+ }
73
+
74
+ export interface AITemplate {
75
+ id: string;
76
+ name: string;
77
+ prompt: string;
78
+ icon: string;
79
+ }
80
+
81
+ export interface Project {
82
+ id: string;
83
+ title: string;
84
+ summary?: string;
85
+ originalPrompt?: string;
86
+ rootNoteId: string;
87
+ createdAt: number;
88
+ categoryId?: string;
89
+ creatorId: string;
90
+ creatorEmail?: string;
91
+ collaborators?: string[]; // Array of user IDs
92
+ }
utils/sanitize.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Note, Project, NoteType, GenerationStatus } from '../types';
2
+
3
+ export const sanitizeProject = (p: any, userId: string): Project & { userId: string } => ({
4
+ id: String(p.id || crypto.randomUUID()),
5
+ userId: String(userId),
6
+ title: String(p.title || 'İsimsiz Proje').substring(0, 100),
7
+ rootNoteId: String(p.rootNoteId || crypto.randomUUID()),
8
+ createdAt: typeof p.createdAt === 'number' ? p.createdAt : Date.now(),
9
+ creatorId: String(p.creatorId || userId),
10
+ creatorEmail: String(p.creatorEmail || ''),
11
+ collaborators: Array.isArray(p.collaborators) ? p.collaborators.map(String) : [],
12
+ ...(p.summary ? { summary: String(p.summary).substring(0, 1000) } : {}),
13
+ ...(p.originalPrompt ? { originalPrompt: String(p.originalPrompt).substring(0, 5000) } : {}),
14
+ ...(p.categoryId ? { categoryId: String(p.categoryId) } : {})
15
+ });
16
+
17
+ export const sanitizeNote = (n: any, userId: string): Note & { userId: string } => ({
18
+ id: String(n.id || crypto.randomUUID()),
19
+ userId: String(userId),
20
+ projectId: String(n.projectId || crypto.randomUUID()),
21
+ ...(n.parentId ? { parentId: String(n.parentId) } : { parentId: null }),
22
+ title: String(n.title || 'İsimsiz Not').substring(0, 200),
23
+ content: String(n.content || '').substring(0, 1000000),
24
+ type: ['ROOT', 'TEXT', 'CODE', 'IMAGE'].includes(n.type) ? n.type : NoteType.TEXT,
25
+ status: ['IDLE', 'PLANNING', 'GENERATING', 'COMPLETED', 'ERROR'].includes(n.status) ? n.status : GenerationStatus.IDLE,
26
+ children: Array.isArray(n.children) ? n.children.map(String).slice(0, 1000) : [],
27
+ timestamp: typeof n.timestamp === 'number' ? n.timestamp : Date.now(),
28
+ lastEditedBy: String(n.lastEditedBy || userId),
29
+ lastEditedAt: typeof n.lastEditedAt === 'number' ? n.lastEditedAt : Date.now(),
30
+ ...(Array.isArray(n.attachments) ? { attachments: n.attachments.slice(0, 100) } : {}),
31
+ ...(Array.isArray(n.tags) ? { tags: n.tags.map(String).slice(0, 50) } : {}),
32
+ ...(Array.isArray(n.linkedNoteIds) ? { linkedNoteIds: n.linkedNoteIds.map(String).slice(0, 100) } : {}),
33
+ ...(n.agentRole ? { agentRole: String(n.agentRole).substring(0, 100) } : {}),
34
+ ...(n.assignedAgent ? { assignedAgent: String(n.assignedAgent).substring(0, 50) } : {})
35
+ });
36
+
37
+ export const sanitizeCategory = (c: any, userId: string) => ({
38
+ id: String(c.id || crypto.randomUUID()),
39
+ userId: String(userId),
40
+ name: String(c.name || 'İsimsiz Kategori').substring(0, 50),
41
+ color: String(c.color || '#6366f1').substring(0, 20)
42
+ });
vite.config.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import { defineConfig, loadEnv } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+ import { VitePWA } from 'vite-plugin-pwa';
5
+
6
+ export default defineConfig(({ mode }) => {
7
+ const env = loadEnv(mode, '.', '');
8
+ return {
9
+ server: {
10
+ port: 3000,
11
+ host: '0.0.0.0',
12
+ },
13
+ plugins: [
14
+ react(),
15
+ VitePWA({
16
+ registerType: 'autoUpdate',
17
+ includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
18
+ workbox: {
19
+ maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
20
+ },
21
+ manifest: {
22
+ name: 'MindSpark',
23
+ short_name: 'MindSpark',
24
+ description: 'AI-powered smart notebook',
25
+ theme_color: '#0B0F19',
26
+ icons: [
27
+ {
28
+ src: 'pwa-192x192.png',
29
+ sizes: '192x192',
30
+ type: 'image/png'
31
+ },
32
+ {
33
+ src: 'pwa-512x512.png',
34
+ sizes: '512x512',
35
+ type: 'image/png'
36
+ }
37
+ ]
38
+ }
39
+ })
40
+ ],
41
+ define: {
42
+ 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
43
+ 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
44
+ },
45
+ resolve: {
46
+ alias: {
47
+ '@': path.resolve(__dirname, '.'),
48
+ }
49
+ }
50
+ };
51
+ });