├── metadata.json ├── .gitignore ├── index.tsx ├── package.json ├── vite.config.ts ├── README.md ├── tsconfig.json ├── index.html ├── components ├── Edge.tsx ├── Sidebar.tsx ├── OutputDisplay.tsx ├── FileUploader.tsx ├── icons.tsx └── Node.tsx ├── types.ts ├── services └── geminiService.ts └── App.tsx /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BananaFlow|ZHO", 3 | "description": "A node-based application to create powerful workflows by connecting various Gemini AI models like Gemini 2.5 Flash, Nano Banana for image editing, and Veo for video generation. Visually construct and execute complex AI tasks.", 4 | "requestFramePermissions": [] 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import App from './App'; 5 | 6 | const rootElement = document.getElementById('root'); 7 | if (!rootElement) { 8 | throw new Error("Could not find root element to mount to"); 9 | } 10 | 11 | const root = ReactDOM.createRoot(rootElement); 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bananaflow|zho", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react-dom": "^19.1.1", 13 | "react": "^19.1.1", 14 | "@google/genai": "^1.17.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.14.0", 18 | "typescript": "~5.8.2", 19 | "vite": "^6.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig, loadEnv } from 'vite'; 3 | 4 | export default defineConfig(({ mode }) => { 5 | const env = loadEnv(mode, '.', ''); 6 | return { 7 | define: { 8 | 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), 9 | 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) 10 | }, 11 | resolve: { 12 | alias: { 13 | '@': path.resolve(__dirname, '.'), 14 | } 15 | } 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | GHBanner 3 |
4 | 5 | # Run and deploy your AI Studio app 6 | 7 | This contains everything you need to run your app locally. 8 | 9 | View your app in AI Studio: https://ai.studio/apps/drive/1LaFPPkxLyV2lUTHoxpeoxWLB6AHYItqX 10 | 11 | ## Run Locally 12 | 13 | **Prerequisites:** Node.js 14 | 15 | 16 | 1. Install dependencies: 17 | `npm install` 18 | 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key 19 | 3. Run the app: 20 | `npm run dev` 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 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 | ], 16 | "moduleResolution": "bundler", 17 | "isolatedModules": true, 18 | "moduleDetection": "force", 19 | "allowJs": true, 20 | "jsx": "react-jsx", 21 | "paths": { 22 | "@/*": [ 23 | "./*" 24 | ] 25 | }, 26 | "allowImportingTsExtensions": true, 27 | "noEmit": true 28 | } 29 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Gemini Node Workflow 9 | 10 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /components/Edge.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import type { Point } from '../types'; 4 | 5 | interface EdgeProps { 6 | start: Point; 7 | end: Point; 8 | } 9 | 10 | const getCurvePath = (start: Point, end: Point): string => { 11 | const dx = end.x - start.x; 12 | const controlPoint1 = { x: start.x + dx / 2, y: start.y }; 13 | const controlPoint2 = { x: end.x - dx / 2, y: end.y }; 14 | return `M${start.x},${start.y} C${controlPoint1.x},${controlPoint1.y} ${controlPoint2.x},${controlPoint2.y} ${end.x},${end.y}`; 15 | }; 16 | 17 | const EdgeComponent: React.FC = ({ start, end }) => { 18 | if (!start || !end) return null; 19 | 20 | const path = getCurvePath(start, end); 21 | 22 | return ( 23 | 30 | ); 31 | }; 32 | 33 | export default React.memo(EdgeComponent); 34 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum NodeType { 3 | TEXT_INPUT = 'TEXT_INPUT', 4 | IMAGE_INPUT = 'IMAGE_INPUT', 5 | TEXT_GENERATOR = 'TEXT_GENERATOR', 6 | IMAGE_EDITOR = 'IMAGE_EDITOR', 7 | VIDEO_GENERATOR = 'VIDEO_GENERATOR', 8 | OUTPUT_DISPLAY = 'OUTPUT_DISPLAY', 9 | } 10 | 11 | export enum NodeStatus { 12 | IDLE = 'IDLE', 13 | PROCESSING = 'PROCESSING', 14 | COMPLETED = 'COMPLETED', 15 | ERROR = 'ERROR', 16 | } 17 | 18 | export interface NodeInput { 19 | id: string; 20 | label: string; 21 | type: 'text' | 'image' | 'video' | 'any'; 22 | } 23 | 24 | export interface NodeOutput { 25 | id: string; 26 | label: string; 27 | type: 'text' | 'image' | 'video' | 'any'; 28 | } 29 | 30 | export interface NodeData { 31 | label: string; 32 | inputs: NodeInput[]; 33 | outputs: NodeOutput[]; 34 | content: any; 35 | status: NodeStatus; 36 | errorMessage?: string; 37 | scale?: number; 38 | } 39 | 40 | export interface Node { 41 | id: string; 42 | type: NodeType; 43 | position: { x: number; y: number }; 44 | data: NodeData; 45 | } 46 | 47 | export interface Edge { 48 | id: string; 49 | sourceNodeId: string; 50 | sourceHandleId: string; 51 | targetNodeId: string; 52 | targetHandleId: string; 53 | } 54 | 55 | export interface Point { 56 | x: number; 57 | y: number; 58 | } -------------------------------------------------------------------------------- /components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NodeType } from '../types'; 3 | import { TextIcon, ImageIcon, MagicIcon, VideoIcon, OutputIcon } from './icons'; 4 | 5 | interface SidebarProps { 6 | onAddNode: (type: NodeType) => void; 7 | } 8 | 9 | const SidebarButton: React.FC<{ icon: React.ReactNode; label: string; onClick: () => void }> = ({ icon, label, onClick }) => ( 10 | 17 | ); 18 | 19 | const Sidebar: React.FC = ({ onAddNode }) => { 20 | return ( 21 |
22 |

Nodes

23 |
24 | } 26 | label="Text Input" 27 | onClick={() => onAddNode(NodeType.TEXT_INPUT)} 28 | /> 29 | } 31 | label="Image Input" 32 | onClick={() => onAddNode(NodeType.IMAGE_INPUT)} 33 | /> 34 | } 36 | label="Text Generator" 37 | onClick={() => onAddNode(NodeType.TEXT_GENERATOR)} 38 | /> 39 | } 41 | label="Image Editor" 42 | onClick={() => onAddNode(NodeType.IMAGE_EDITOR)} 43 | /> 44 | } 46 | label="Video Generator" 47 | onClick={() => onAddNode(NodeType.VIDEO_GENERATOR)} 48 | /> 49 | } 51 | label="Output" 52 | onClick={() => onAddNode(NodeType.OUTPUT_DISPLAY)} 53 | /> 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default Sidebar; -------------------------------------------------------------------------------- /components/OutputDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NodeStatus } from '../types'; 3 | 4 | interface OutputDisplayProps { 5 | content: any; 6 | status: NodeStatus; 7 | errorMessage?: string; 8 | progressMessage?: string; 9 | } 10 | 11 | export const OutputDisplay: React.FC = ({ content, status, errorMessage, progressMessage }) => { 12 | if (status === NodeStatus.PROCESSING) { 13 | return ( 14 |
15 |
16 |

{progressMessage || 'Processing...'}

17 |
18 | ); 19 | } 20 | 21 | if (status === NodeStatus.ERROR) { 22 | return ( 23 |
24 |

Error

25 |

{errorMessage}

26 |
27 | ); 28 | } 29 | 30 | if (status === NodeStatus.COMPLETED && content) { 31 | if (typeof content === 'string') { 32 | if (content.startsWith('data:image')) { 33 | return Generated output; 34 | } 35 | if (content.startsWith('blob:')) { // Video 36 | return