Add JSON export and import to toolbar by tmcfarlane · Pull Request #6 · tmcfarlane/flowchart · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/App.css
14 changes: 13 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,13 @@ function FlowChartEditor() {
darkMode={darkMode}
onToggleDarkMode={toggleDarkMode}
reactFlowWrapper={reactFlowWrapper}
nodes={nodes}
edges={edges}
onImportJson={(importedNodes, importedEdges) => {
saveToHistory()
setNodes(importedNodes)
setEdges(importedEdges)
}}
/>
<div ref={reactFlowWrapper} className="react-flow-wrapper">
<ReactFlow
Expand Down Expand Up @@ -1223,6 +1230,11 @@ function FlowChartEditor() {
onClose={dismissWelcomeAI}
variant="welcome"
onDismiss={dismissWelcomeAI}
onImportJson={(importedNodes, importedEdges) => {
saveToHistory()
setNodes(importedNodes)
setEdges(importedEdges)
}}
/>
)}
{/* Full AI chat overlay (triggered by pill button) */}
Expand All @@ -1249,7 +1261,7 @@ function FlowChartEditor() {
{!isAIBubbleOpen && !(showWelcomeAI && nodes.length === 0 && !aiProposal) && (
<button className="ai-floating-pill" onClick={toggleAI} aria-label="Open AI Assistant">
<img src={darkMode ? '/logo/logo_color.svg' : '/logo/logo_dark_pointer.svg'} alt="" className="ai-pill-logo" />
<span className="ai-pill-brand">Zero Click Dev</span>
<span className="ai-pill-brand">FlowChart</span>
</button>
)}
</div>
Expand Down
69 changes: 66 additions & 3 deletions src/components/AIChat.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,73 @@
}

.ai-bubble-logo {
width: 22px;
height: 22px;
margin-right: 8px;
width: 32px;
height: 32px;
margin-right: 10px;
flex-shrink: 0;
filter: brightness(0) invert(1);
align-self: center;
}

.ai-bubble-title {
flex: 1;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.3px;
display: flex;
flex-direction: column;
line-height: 1.2;
}

.ai-bubble-subtitle {
font-size: 11px;
font-weight: 400;
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
letter-spacing: 0.2px;
transition: color 0.15s ease;
}

.ai-bubble-subtitle-link {
color: inherit;
text-decoration: none;
font-weight: 500;
transition: color 0.15s ease;
}

.ai-bubble-subtitle-link:hover {
color: white;
text-decoration: underline;
}

.app.dark-mode .ai-bubble-subtitle {
color: rgba(120, 252, 214, 0.7);
}

.app.dark-mode .ai-bubble-subtitle-link:hover {
color: #78fcd6;
}

.ai-bubble-import {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 6px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: background 0.2s ease;
opacity: 0.7;
margin-right: 4px;
}

.ai-bubble-import:hover {
background: rgba(255, 255, 255, 0.2);
opacity: 1;
}

.ai-bubble-close {
Expand Down Expand Up @@ -240,6 +295,14 @@
background-clip: text;
}

.app.dark-mode .ai-bubble-import {
color: #78fcd6;
}

.app.dark-mode .ai-bubble-import:hover {
background: rgba(120, 252, 214, 0.2);
}

.app.dark-mode .ai-bubble-close {
color: #78fcd6;
}
Expand Down
77 changes: 73 additions & 4 deletions src/components/AIChat.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useState, useCallback, useEffect } from 'react'
import { useState, useCallback, useEffect, useRef } from 'react'
import { Node, Edge } from 'reactflow'
import './AIChat.css'
import { BaseFlowNode, BaseFlowEdge, EdgeStyle } from '../App'
import { resolveAzureIcons } from '../utils/azureIconRegistry'
import { createThread, getMessages, addMessage as addThreadMessage } from '../utils/conversationStore'
import { parseFlowJson } from '../utils/exportUtils'

const LOADING_MESSAGES = [
'Thinking about your flowchart...',
Expand Down Expand Up @@ -65,11 +66,14 @@ interface AIChatProps {
onClose: () => void
variant?: 'welcome' | 'full'
onDismiss?: () => void
onImportJson?: (nodes: Node[], edges: Edge[]) => void
}

function AIChat({ nodes, edges, onProposalReady, isOpen, onClose, variant = 'full', onDismiss }: AIChatProps) {
function AIChat({ nodes, edges, onProposalReady, isOpen, onClose, variant = 'full', onDismiss, onImportJson }: AIChatProps) {
const [inputValue, setInputValue] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [importError, setImportError] = useState<string | null>(null)
const welcomeFileInputRef = useRef<HTMLInputElement>(null)
const [error, setError] = useState<string | null>(null)
const [starterPrompts] = useState(() => pickRandomPrompts(4))

Expand Down Expand Up @@ -309,14 +313,58 @@ function AIChat({ nodes, edges, onProposalReady, isOpen, onClose, variant = 'ful
return () => window.removeEventListener('keydown', handleEscape)
}, [isOpen, onClose])

const handleWelcomeImport = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file || !onImportJson) return

const reader = new FileReader()
reader.onload = (event) => {
try {
const result = parseFlowJson(event.target?.result as string)
onImportJson(result.nodes, result.edges)
setImportError(null)
} catch (err) {
setImportError(err instanceof Error ? err.message : 'Failed to import file.')
}
}
reader.onerror = () => {
setImportError('Could not read the selected file.')
}
reader.readAsText(file)
e.target.value = ''
}

if (!isOpen) return null

if (variant === 'welcome') {
return (
<div className="ai-welcome-prompt" role="dialog" aria-labelledby="ai-welcome-title">
<div className="ai-bubble-header">
<img src="/logo/logo_color.svg" alt="Zero Click Dev" className="ai-bubble-logo" />
<span id="ai-welcome-title" className="ai-bubble-title">Zero Click Dev</span>
<span id="ai-welcome-title" className="ai-bubble-title">
FlowChart
<span className="ai-bubble-subtitle">by <a href="https://zeroclickdev.ai" target="_blank" rel="noopener noreferrer" className="ai-bubble-subtitle-link">Zero Click Dev</a></span>
</span>
<button
className="ai-bubble-import"
onClick={() => welcomeFileInputRef.current?.click()}
title="Import from JSON"
aria-label="Import from JSON"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 2v8" />
<path d="M4 6l4-4 4 4" />
<path d="M2 10v3a1 1 0 001 1h10a1 1 0 001-1v-3" />
</svg>
</button>
<input
ref={welcomeFileInputRef}
type="file"
accept=".json,application/json"
onChange={handleWelcomeImport}
style={{ display: 'none' }}
aria-label="Import JSON file"
/>
<button className="ai-bubble-close" onClick={onClose} title="Close" aria-label="Close">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M1 1l12 12M13 1L1 13" />
Expand Down Expand Up @@ -387,6 +435,24 @@ function AIChat({ nodes, edges, onProposalReady, isOpen, onClose, variant = 'ful
</button>
)}
</div>
{importError && (
<div
className="confirm-overlay"
onClick={() => setImportError(null)}
role="dialog"
aria-modal="true"
>
<div className="confirm-dialog" onClick={(e) => e.stopPropagation()}>
<h2 className="confirm-title">Invalid JSON Format</h2>
<p className="confirm-body">{importError}</p>
<div className="confirm-actions">
<button className="confirm-button confirm-cancel" onClick={() => setImportError(null)}>
OK
</button>
</div>
</div>
</div>
)}
</div>
)
}
Expand All @@ -402,7 +468,10 @@ function AIChat({ nodes, edges, onProposalReady, isOpen, onClose, variant = 'ful
<div className="ai-bubble-prompt" onClick={(e) => e.stopPropagation()}>
<div className="ai-bubble-header">
<img src="/logo/logo_color.svg" alt="Zero Click Dev" className="ai-bubble-logo" />
<span id="ai-bubble-title" className="ai-bubble-title">Zero Click Dev</span>
<span id="ai-bubble-title" className="ai-bubble-title">
FlowChart
<span className="ai-bubble-subtitle">by <a href="https://zeroclickdev.ai" target="_blank" rel="noopener noreferrer" className="ai-bubble-subtitle-link">Zero Click Dev</a></span>
</span>
<button className="ai-bubble-close" onClick={onClose} title="Close" aria-label="Close">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M1 1l12 12M13 1L1 13" />
Expand Down
18 changes: 18 additions & 0 deletions src/components/Toolbar.css
Loading