Initial secure upload
Browse files- .gitignore +23 -20
- App.tsx +0 -0
- LICENSE +21 -0
- README.md +52 -58
- components/CalendarView.tsx +107 -0
- components/LoadingSpinner.tsx +8 -0
- components/NoteCard.tsx +716 -0
- components/VoiceRecorder.tsx +129 -0
- firebase-blueprint.json +173 -0
- firebase.ts +54 -0
- firestore.rules +89 -0
- index.html +144 -0
- index.tsx +15 -0
- metadata.json +5 -0
- package-lock.json +0 -0
- package.json +34 -32
- services/geminiService.ts +359 -0
- src/components/FlowchartView.tsx +315 -0
- tsconfig.json +30 -0
- types.ts +92 -0
- utils/sanitize.ts +42 -0
- vite.config.ts +51 -0
.gitignore
CHANGED
|
@@ -1,23 +1,26 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
|
| 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 |
-
|
| 30 |
|
| 31 |
-
|
| 32 |
-
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
| 33 |
|
| 34 |
-
|
| 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 |
-
|
| 43 |
|
| 44 |
-
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
|
| 49 |
|
| 50 |
-
|
| 51 |
|
| 52 |
-
|
|
|
|
| 53 |
|
| 54 |
-
##
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
###
|
|
|
|
| 61 |
|
| 62 |
-
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
|
| 66 |
-
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
|
| 72 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
|
| 76 |
-
##
|
|
|
|
| 77 |
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
|
| 81 |
|
| 82 |
-
|
|
|
|
| 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": "
|
| 3 |
-
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
-
"
|
| 6 |
-
|
| 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 |
-
"
|
| 17 |
-
"
|
| 18 |
-
"
|
| 19 |
-
"
|
|
|
|
| 20 |
},
|
| 21 |
-
"
|
| 22 |
-
"
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
},
|
| 27 |
-
"
|
| 28 |
-
"
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 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 = `\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 |
+
});
|