MindSpark / src /components /FlowchartView.tsx
TheK3R1M's picture
Initial secure upload
fd4dc0d verified
import React, { useEffect, useState, useCallback, useRef } from 'react';
import mermaid from 'mermaid';
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
import { Note, NoteType } from '../../types';
import { chatWithStep } from '../../services/geminiService';
import ReactMarkdown from 'react-markdown';
import { motion, AnimatePresence } from 'framer-motion';
interface FlowchartViewProps {
notes: Record<string, Note>;
projectTitle: string;
rootNoteId: string;
}
interface ChatMessage {
role: 'user' | 'model';
content: string;
}
const FlowchartView: React.FC<FlowchartViewProps> = ({ notes, projectTitle, rootNoteId }) => {
const [svgContent, setSvgContent] = useState<string>('');
const [isRendering, setIsRendering] = useState(false);
const [selectedNoteId, setSelectedNoteId] = useState<string | null>(null);
const [chatHistories, setChatHistories] = useState<Record<string, ChatMessage[]>>({});
const [userInput, setUserInput] = useState('');
const [isAsking, setIsAsking] = useState(false);
const chatEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
mermaid.initialize({
startOnLoad: false,
theme: 'dark',
securityLevel: 'loose',
flowchart: {
useMaxWidth: true,
htmlLabels: true,
curve: 'basis'
}
});
}, []);
const generateMermaidCode = useCallback(() => {
if (!notes[rootNoteId]) return "";
let code = "graph TD\n";
// Styling
code += "classDef root fill:#4f46e5,stroke:#fff,stroke-width:2px,color:#fff,cursor:pointer;\n";
code += "classDef text fill:#1e293b,stroke:#475569,stroke-width:1px,color:#cbd5e1,cursor:pointer;\n";
code += "classDef code fill:#0f172a,stroke:#3b82f6,stroke-width:1px,color:#60a5fa,cursor:pointer;\n";
code += "classDef image fill:#1e293b,stroke:#8b5cf6,stroke-width:1px,color:#a78bfa,cursor:pointer;\n";
code += "classDef selected fill:#fbbf24,stroke:#fff,stroke-width:3px,color:#000,cursor:pointer;\n";
const visited = new Set<string>();
const processNote = (noteId: string) => {
if (visited.has(noteId)) return "";
visited.add(noteId);
const note = notes[noteId];
if (!note) return "";
let nodeCode = "";
const id = note.id.replace(/-/g, '');
const title = note.title.replace(/[()"[\]{}]/g, "'");
// Node definition
const isSelected = selectedNoteId === note.id;
const styleClass = isSelected ? 'selected' : (
note.type === NoteType.ROOT ? 'root' :
note.type === NoteType.CODE ? 'code' :
note.type === NoteType.IMAGE ? 'image' : 'text'
);
if (note.type === NoteType.ROOT) {
nodeCode += ` ${id}("${title}"):::${styleClass}\n`;
} else {
nodeCode += ` ${id}["${title}"]:::${styleClass}\n`;
}
// Connections
if (note.children && note.children.length > 0) {
note.children.forEach(childId => {
const child = notes[childId];
if (child) {
const childCleanId = child.id.replace(/-/g, '');
nodeCode += ` ${id} --> ${childCleanId}\n`;
nodeCode += processNote(childId);
}
});
}
return nodeCode;
};
code += processNote(rootNoteId);
return code;
}, [notes, rootNoteId, selectedNoteId]);
useEffect(() => {
const code = generateMermaidCode();
if (!code) return;
let isMounted = true;
const renderDiagram = async () => {
setIsRendering(true);
try {
const id = `mermaid-render-${Date.now()}`;
const { svg } = await mermaid.render(id, code);
if (isMounted) {
setSvgContent(svg);
}
} catch (error) {
console.error("Mermaid render error:", error);
} finally {
if (isMounted) setIsRendering(false);
}
};
renderDiagram();
return () => { isMounted = false; };
}, [generateMermaidCode]);
// Handle node clicks
const handleSvgClick = (e: React.MouseEvent) => {
const target = e.target as SVGElement;
const node = target.closest('.node');
if (node) {
const nodeIdClean = node.id.split('-')[0]; // Mermaid nodes have IDs
// Map back to our IDs
const foundId = Object.keys(notes).find(id => id.replace(/-/g, '') === nodeIdClean);
if (foundId) {
setSelectedNoteId(foundId);
}
}
};
const handleSendMessage = async () => {
if (!selectedNoteId || !userInput.trim() || isAsking) return;
const note = notes[selectedNoteId];
const currentHistory = chatHistories[selectedNoteId] || [];
const newHistory: ChatMessage[] = [...currentHistory, { role: 'user', content: userInput }];
setChatHistories(prev => ({ ...prev, [selectedNoteId]: newHistory }));
setUserInput('');
setIsAsking(true);
try {
const response = await chatWithStep(
projectTitle,
note.title,
note.content,
userInput,
currentHistory.map(m => ({ role: m.role, parts: [{ text: m.content }] }))
);
setChatHistories(prev => ({
...prev,
[selectedNoteId]: [...newHistory, { role: 'model', content: response }]
}));
} catch (error) {
console.error("Chat error:", error);
} finally {
setIsAsking(false);
}
};
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [chatHistories, selectedNoteId]);
const selectedNote = selectedNoteId ? notes[selectedNoteId] : null;
return (
<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">
<div className="flex items-center justify-between p-8 border-b border-white/5 bg-white/5">
<div>
<h3 className="text-xl font-black text-white tracking-tight flex items-center gap-3">
<div className="w-8 h-8 bg-indigo-500 rounded-xl flex items-center justify-center">
<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 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" />
</svg>
</div>
Akıl Haritası Tuvali
</h3>
<p className="text-sm text-slate-400 mt-1">"{projectTitle}" projesinin etkileşimli haritası</p>
</div>
<div className="flex items-center gap-4">
{isRendering && (
<div className="text-[10px] text-indigo-400 font-black tracking-widest animate-pulse uppercase">Modelleniyor...</div>
)}
<div className="flex bg-black/20 p-1 rounded-xl border border-white/5">
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-indigo-500/20">
<div className="w-2 h-2 rounded-full bg-indigo-400"></div>
<span className="text-[10px] text-indigo-300 font-black uppercase tracking-wider">Kök Bilgi</span>
</div>
</div>
</div>
</div>
<div className="flex-1 flex overflow-hidden relative">
{/* Canvas Area */}
<div className="flex-1 bg-[#0F1117] relative cursor-grab active:cursor-grabbing" onClick={handleSvgClick}>
<TransformWrapper
initialScale={1}
minScale={0.5}
maxScale={3}
centerOnInit
>
{({ zoomIn, zoomOut, resetTransform }) => (
<>
<div className="absolute top-4 left-4 z-10 flex flex-col gap-2">
<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>
<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>
<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>
</div>
<TransformComponent wrapperClass="!w-full !h-full" contentClass="!w-full !h-full flex items-center justify-center">
<div
className="mermaid-container p-20"
dangerouslySetInnerHTML={{ __html: svgContent }}
></div>
</TransformComponent>
</>
)}
</TransformWrapper>
</div>
{/* AI Chat Assistant Sidebar */}
<AnimatePresence>
{selectedNote && (
<motion.div
initial={{ x: 400 }}
animate={{ x: 0 }}
exit={{ x: 400 }}
className="w-96 bg-[#161B22] border-l border-white/5 flex flex-col shadow-2xl z-20"
>
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-indigo-500/10 text-indigo-400 rounded-2xl flex items-center justify-center">
<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>
</div>
<div className="min-w-0">
<h4 className="text-white font-black text-sm truncate">{selectedNote.title}</h4>
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">Adım Asistanı</p>
</div>
</div>
<button onClick={() => setSelectedNoteId(null)} className="p-2 text-slate-500 hover:text-white transition-colors">
<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>
</button>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-4 custom-scrollbar bg-[#0D1117]">
{(!chatHistories[selectedNoteId || ''] || chatHistories[selectedNoteId || ''].length === 0) && (
<div className="h-full flex flex-col items-center justify-center text-center opacity-40">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
<p className="text-sm font-medium">Bu adım hakkında<br/>AI asistanına soru sor.</p>
</div>
)}
{chatHistories[selectedNoteId || '']?.map((msg, idx) => (
<div key={idx} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<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'}`}>
<div className="markdown-body text-xs prose-invert leading-relaxed">
<ReactMarkdown>
{msg.content}
</ReactMarkdown>
</div>
</div>
</div>
))}
{isAsking && (
<div className="flex justify-start">
<div className="bg-white/5 px-4 py-3 rounded-2xl flex items-center gap-2">
<div className="flex gap-1">
<div className="w-1 h-1 bg-slate-500 rounded-full animate-bounce"></div>
<div className="w-1 h-1 bg-slate-500 rounded-full animate-bounce [animation-delay:0.2s]"></div>
<div className="w-1 h-1 bg-slate-500 rounded-full animate-bounce [animation-delay:0.4s]"></div>
</div>
</div>
</div>
)}
<div ref={chatEndRef} />
</div>
<div className="p-4 border-t border-white/5 bg-[#0F1117]">
<div className="relative group">
<input
type="text"
value={userInput}
onChange={(e) => setUserInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Bir şey sor..."
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"
/>
<button
onClick={handleSendMessage}
disabled={isAsking || !userInput.trim()}
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"
>
<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>
</button>
</div>
<p className="text-[10px] text-center text-slate-600 mt-3 font-bold uppercase tracking-widest">Gemini 3 Flash Pro</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
};
export default FlowchartView;