Compare commits
2 Commits
cd7a54f38b
...
027495829d
| Author | SHA1 | Date | |
|---|---|---|---|
| 027495829d | |||
| 14137101f3 |
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "comfyui-frontend",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "React frontend for ComfyUI-like AI image generation tool with ROCm support",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"web-vitals": "^2.1.4",
|
|
||||||
"axios": "^1.6.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.11.0",
|
|
||||||
"@types/react": "^18.2.45",
|
|
||||||
"@types/react-dom": "^18.2.18",
|
|
||||||
"typescript": "^4.9.5"
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
.app {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header {
|
|
||||||
background-color: #282c34;
|
|
||||||
padding: 20px;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-right: 1px solid #ddd;
|
|
||||||
padding: 15px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-area {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 8px 16px;
|
|
||||||
margin-right: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs button.active {
|
|
||||||
border-bottom: 3px solid #007acc;
|
|
||||||
color: #007acc;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import NodeEditor from './components/NodeEditor';
|
|
||||||
import PreviewPane from './components/PreviewPane';
|
|
||||||
import NodePanel from './components/NodePanel';
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [activeTab, setActiveTab] = useState<'editor' | 'preview'>('editor');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="app">
|
|
||||||
<header className="app-header">
|
|
||||||
<h1>ComfyUI Rust - AMD GPU Accelerated</h1>
|
|
||||||
<p>Image Generation with ROCm Support for RX 9070 XT</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="main-content">
|
|
||||||
<div className="sidebar">
|
|
||||||
<NodePanel />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="editor-area">
|
|
||||||
<div className="tabs">
|
|
||||||
<button
|
|
||||||
className={activeTab === 'editor' ? 'active' : ''}
|
|
||||||
onClick={() => setActiveTab('editor')}
|
|
||||||
>
|
|
||||||
Node Editor
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={activeTab === 'preview' ? 'active' : ''}
|
|
||||||
onClick={() => setActiveTab('preview')}
|
|
||||||
>
|
|
||||||
Preview
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeTab === 'editor' ? (
|
|
||||||
<NodeEditor />
|
|
||||||
) : (
|
|
||||||
<PreviewPane />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
.node-canvas {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node {
|
|
||||||
background-color: white;
|
|
||||||
border: 2px solid #007acc;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node.selected {
|
|
||||||
border-color: #ff6b35;
|
|
||||||
box-shadow: 0 0 0 2px #ff6b35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-header {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
padding: 8px;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-content {
|
|
||||||
padding: 8px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-line {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas-status {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
left: 10px;
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
|
||||||
import './NodeEditor.css';
|
|
||||||
|
|
||||||
interface Node {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
position: { x: number; y: number };
|
|
||||||
data: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Connection {
|
|
||||||
id: string;
|
|
||||||
sourceNodeId: string;
|
|
||||||
targetNodeId: string;
|
|
||||||
sourceHandleId: string;
|
|
||||||
targetHandleId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NodeEditor: React.FC = () => {
|
|
||||||
const [nodes, setNodes] = useState<Node[]>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
type: 'text-encoder',
|
|
||||||
position: { x: 100, y: 100 },
|
|
||||||
data: { prompt: 'A beautiful landscape' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
type: 'image-generator',
|
|
||||||
position: { x: 400, y: 150 },
|
|
||||||
data: { steps: 20, cfg: 7.0 }
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [connections, setConnections] = useState<Connection[]>([
|
|
||||||
{
|
|
||||||
id: 'c1',
|
|
||||||
sourceNodeId: '1',
|
|
||||||
targetNodeId: '2',
|
|
||||||
sourceHandleId: 'output',
|
|
||||||
targetHandleId: 'input'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [selectedNode, setSelectedNode] = useState<string | null>(null);
|
|
||||||
const [draggedNode, setDraggedNode] = useState<string | null>(null);
|
|
||||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Handle node dragging
|
|
||||||
const handleMouseDown = (e: React.MouseEvent, nodeId: string) => {
|
|
||||||
if (e.button !== 0) return; // Only left mouse button
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const node = nodes.find(n => n.id === nodeId);
|
|
||||||
if (!node) return;
|
|
||||||
|
|
||||||
setDraggedNode(nodeId);
|
|
||||||
setSelectedNode(nodeId);
|
|
||||||
|
|
||||||
const rect = canvasRef.current?.getBoundingClientRect();
|
|
||||||
if (rect) {
|
|
||||||
setDragOffset({
|
|
||||||
x: e.clientX - rect.left - node.position.x,
|
|
||||||
y: e.clientY - rect.top - node.position.y
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle mouse move for dragging nodes
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
if (!draggedNode || !canvasRef.current) return;
|
|
||||||
|
|
||||||
const rect = canvasRef.current.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left - dragOffset.x;
|
|
||||||
const y = e.clientY - rect.top - dragOffset.y;
|
|
||||||
|
|
||||||
setNodes(prev => prev.map(node =>
|
|
||||||
node.id === draggedNode ? { ...node, position: { x, y } } : node
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
setDraggedNode(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (draggedNode) {
|
|
||||||
window.addEventListener('mousemove', handleMouseMove);
|
|
||||||
window.addEventListener('mouseup', handleMouseUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('mousemove', handleMouseMove);
|
|
||||||
window.removeEventListener('mouseup', handleMouseUp);
|
|
||||||
};
|
|
||||||
}, [draggedNode, dragOffset]);
|
|
||||||
|
|
||||||
// Add a new node
|
|
||||||
const addNode = (type: string, x: number, y: number) => {
|
|
||||||
const newNode: Node = {
|
|
||||||
id: `node-${Date.now()}`,
|
|
||||||
type,
|
|
||||||
position: { x, y },
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
setNodes([...nodes, newNode]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle canvas click to add nodes
|
|
||||||
const handleCanvasClick = (e: React.MouseEvent) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
// Clicked on empty space, add new node at click position
|
|
||||||
const rect = canvasRef.current?.getBoundingClientRect();
|
|
||||||
if (rect) {
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
addNode('text-encoder', x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render connection lines between nodes
|
|
||||||
const renderConnections = () => {
|
|
||||||
return connections.map(conn => {
|
|
||||||
const sourceNode = nodes.find(n => n.id === conn.sourceNodeId);
|
|
||||||
const targetNode = nodes.find(n => n.id === conn.targetNodeId);
|
|
||||||
|
|
||||||
if (!sourceNode || !targetNode) return null;
|
|
||||||
|
|
||||||
// Calculate positions for connection line
|
|
||||||
const startX = sourceNode.position.x + 100; // Node width is 200, handle position in center
|
|
||||||
const startY = sourceNode.position.y + 30; // Handle height offset
|
|
||||||
|
|
||||||
const endX = targetNode.position.x;
|
|
||||||
const endY = targetNode.position.y + 30; // Handle height offset
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg key={conn.id} className="connection-line" style={{ position: 'absolute', top: 0, left: 0 }}>
|
|
||||||
<line
|
|
||||||
x1={startX}
|
|
||||||
y1={startY}
|
|
||||||
x2={endX}
|
|
||||||
y2={endY}
|
|
||||||
stroke="#007acc"
|
|
||||||
strokeWidth="2"
|
|
||||||
markerEnd="url(#arrowhead)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={canvasRef}
|
|
||||||
className="node-canvas"
|
|
||||||
onClick={handleCanvasClick}
|
|
||||||
>
|
|
||||||
{/* Connection lines */}
|
|
||||||
{renderConnections()}
|
|
||||||
|
|
||||||
{/* Node definitions for arrowheads */}
|
|
||||||
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
|
|
||||||
<defs>
|
|
||||||
<marker
|
|
||||||
id="arrowhead"
|
|
||||||
markerWidth="10"
|
|
||||||
markerHeight="7"
|
|
||||||
refX="0"
|
|
||||||
refY="3.5"
|
|
||||||
orient="auto"
|
|
||||||
>
|
|
||||||
<polygon points="0 0, 10 3.5, 0 7" fill="#007acc" />
|
|
||||||
</marker>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{/* Render nodes */}
|
|
||||||
{nodes.map(node => (
|
|
||||||
<div
|
|
||||||
key={node.id}
|
|
||||||
className={`node ${selectedNode === node.id ? 'selected' : ''}`}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: node.position.x,
|
|
||||||
top: node.position.y,
|
|
||||||
width: 200,
|
|
||||||
height: 60,
|
|
||||||
cursor: 'move'
|
|
||||||
}}
|
|
||||||
onMouseDown={(e) => handleMouseDown(e, node.id)}
|
|
||||||
>
|
|
||||||
<div className="node-header">
|
|
||||||
<span>{node.type}</span>
|
|
||||||
</div>
|
|
||||||
<div className="node-content">
|
|
||||||
{node.type === 'text-encoder' && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={node.data.prompt || ''}
|
|
||||||
placeholder="Enter prompt..."
|
|
||||||
onChange={(e) => {
|
|
||||||
setNodes(nodes.map(n =>
|
|
||||||
n.id === node.id ? { ...n, data: { ...n.data, prompt: e.target.value } } : n
|
|
||||||
));
|
|
||||||
}}
|
|
||||||
className="prompt-input"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{node.type === 'image-generator' && (
|
|
||||||
<div>
|
|
||||||
<label>Steps: </label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={node.data.steps || 20}
|
|
||||||
onChange={(e) => {
|
|
||||||
setNodes(nodes.map(n =>
|
|
||||||
n.id === node.id ? { ...n, data: { ...n.data, steps: parseInt(e.target.value) } } : n
|
|
||||||
));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Status indicators */}
|
|
||||||
<div className="canvas-status">
|
|
||||||
<span>Nodes: {nodes.length}</span>
|
|
||||||
<span>Connections: {connections.length}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NodeEditor;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
.node-panel {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-panel h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #333;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-types {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-type {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
cursor: grab;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-type:hover {
|
|
||||||
border-color: #007acc;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 122, 204, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-icon {
|
|
||||||
display: inline-block;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
background-color: #007acc;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 20px;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-right: 10px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-info {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
width: calc(100% - 35px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-info h4 {
|
|
||||||
margin: 0 0 5px 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-info p {
|
|
||||||
margin: 0;
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import './NodePanel.css';
|
|
||||||
|
|
||||||
const NodePanel: React.FC = () => {
|
|
||||||
const nodeTypes = [
|
|
||||||
{ id: 'text-encoder', name: 'Text Encoder', description: 'Encode text prompts' },
|
|
||||||
{ id: 'image-generator', name: 'Image Generator', description: 'Generate images from prompts' },
|
|
||||||
{ id: 'vae-decoder', name: 'VAE Decoder', description: 'Decode latent representations' },
|
|
||||||
{ id: 'control-net', name: 'Control Net', description: 'Apply control signals' },
|
|
||||||
{ id: 'upscale', name: 'Upscaler', description: 'Increase image resolution' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="node-panel">
|
|
||||||
<h3>Node Library</h3>
|
|
||||||
<div className="node-types">
|
|
||||||
{nodeTypes.map(node => (
|
|
||||||
<div
|
|
||||||
key={node.id}
|
|
||||||
className="node-type"
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) => {
|
|
||||||
e.dataTransfer.setData('nodeType', node.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="node-icon">□</div>
|
|
||||||
<div className="node-info">
|
|
||||||
<h4>{node.name}</h4>
|
|
||||||
<p>{node.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NodePanel;
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
.preview-pane {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-pane h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-controls {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-button {
|
|
||||||
background-color: #007acc;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-button:hover:not(:disabled) {
|
|
||||||
background-color: #005a9e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-button:disabled {
|
|
||||||
background-color: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 20px;
|
|
||||||
background-color: #e9ecef;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #007acc;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
min-height: 300px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generated-image {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 512px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-info {
|
|
||||||
background-color: #e9f7fe;
|
|
||||||
border: 1px solid #b3e5fc;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-info h4 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #0066cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-info p {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import './PreviewPane.css';
|
|
||||||
|
|
||||||
const PreviewPane: React.FC = () => {
|
|
||||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
|
||||||
const [progress, setProgress] = useState(0);
|
|
||||||
|
|
||||||
// Simulate image generation
|
|
||||||
const startGeneration = () => {
|
|
||||||
setIsGenerating(true);
|
|
||||||
setProgress(0);
|
|
||||||
|
|
||||||
// Simulate progress updates
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setProgress(prev => {
|
|
||||||
if (prev >= 100) {
|
|
||||||
clearInterval(interval);
|
|
||||||
setImageUrl('https://placehold.co/512x512?text=Generated+Image');
|
|
||||||
setIsGenerating(false);
|
|
||||||
return 100;
|
|
||||||
}
|
|
||||||
return prev + 10;
|
|
||||||
});
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="preview-pane">
|
|
||||||
<h3>Preview</h3>
|
|
||||||
|
|
||||||
<div className="preview-controls">
|
|
||||||
<button
|
|
||||||
onClick={startGeneration}
|
|
||||||
disabled={isGenerating}
|
|
||||||
className="generate-button"
|
|
||||||
>
|
|
||||||
{isGenerating ? 'Generating...' : 'Generate Image'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="progress-container">
|
|
||||||
{isGenerating && (
|
|
||||||
<div className="progress-bar">
|
|
||||||
<div
|
|
||||||
className="progress-fill"
|
|
||||||
style={{ width: `${progress}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p>{isGenerating ? `Progress: ${progress}%` : 'No generation in progress'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="preview-content">
|
|
||||||
{imageUrl ? (
|
|
||||||
<img src={imageUrl} alt="Generated preview" className="generated-image" />
|
|
||||||
) : (
|
|
||||||
<div className="placeholder">
|
|
||||||
<p>Preview will appear here after generation</p>
|
|
||||||
<p>Drag nodes from the panel to create a workflow</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="preview-info">
|
|
||||||
<h4>GPU Information</h4>
|
|
||||||
<p><strong>Device:</strong> Radeon RX 9070 XT</p>
|
|
||||||
<p><strong>Architecture:</strong> gfx900</p>
|
|
||||||
<p><strong>Status:</strong> Ready for ROCm acceleration</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PreviewPane;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
|
||||||
document.getElementById('root') as HTMLElement
|
|
||||||
);
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
// Create an Axios instance with default configuration
|
|
||||||
const apiClient = axios.create({
|
|
||||||
baseURL: 'http://localhost:8080',
|
|
||||||
timeout: 30000,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Request interceptor to add auth token if needed
|
|
||||||
apiClient.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
// Add authorization header if needed
|
|
||||||
const token = localStorage.getItem('authToken');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Response interceptor for handling errors
|
|
||||||
apiClient.interceptors.response.use(
|
|
||||||
(response) => {
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
// Handle unauthorized access
|
|
||||||
localStorage.removeItem('authToken');
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// API interfaces
|
|
||||||
export interface InferenceRequest {
|
|
||||||
prompt: string;
|
|
||||||
negative_prompt?: string;
|
|
||||||
guidance_scale?: number;
|
|
||||||
steps?: number;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
seed?: number;
|
|
||||||
model_name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InferenceResponse {
|
|
||||||
task_id: string;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TaskStatusResponse {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
status: string;
|
|
||||||
progress: number;
|
|
||||||
created_at: number;
|
|
||||||
updated_at: number;
|
|
||||||
model_name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelInfo {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
model_type: string;
|
|
||||||
version: string;
|
|
||||||
loaded: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API functions
|
|
||||||
export const api = {
|
|
||||||
// Health check endpoint
|
|
||||||
healthCheck: () => apiClient.get('/health'),
|
|
||||||
|
|
||||||
// System info including GPU details
|
|
||||||
getSystemInfo: () => apiClient.get('/system-info'),
|
|
||||||
|
|
||||||
// Start inference task
|
|
||||||
startInference: (request: InferenceRequest) =>
|
|
||||||
apiClient.post<InferenceResponse>('/infer', request),
|
|
||||||
|
|
||||||
// Get all models
|
|
||||||
getModels: () => apiClient.get<ModelInfo[]>('/models'),
|
|
||||||
|
|
||||||
// Get task status
|
|
||||||
getTaskStatus: (taskId: string) =>
|
|
||||||
apiClient.get<TaskStatusResponse>(`/tasks/${taskId}`),
|
|
||||||
|
|
||||||
// Get all tasks
|
|
||||||
getAllTasks: () => apiClient.get<TaskStatusResponse[]>('/tasks'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default apiClient;
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
mod node_editor;
|
mod node_editor;
|
||||||
mod node_panel;
|
mod node_panel;
|
||||||
@@ -53,7 +52,7 @@ impl eframe::App for ComfyUIApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result<()> {
|
||||||
let native_options = eframe::NativeOptions {
|
let native_options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_title("ComfyUI Rust Frontend")
|
.with_title("ComfyUI Rust Frontend")
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ impl NodeEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) {
|
pub fn ui(&mut self, ui: &mut egui::Ui, _api_client: &ApiClient) {
|
||||||
ui.heading("Node Editor");
|
ui.heading("Node Editor");
|
||||||
|
|
||||||
// Create a scroll area for the node editor
|
// Create a scroll area for the node editor
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ impl PreviewPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) {
|
pub fn ui(&mut self, ui: &mut egui::Ui, _api_client: &ApiClient) {
|
||||||
ui.set_min_width(300.0);
|
ui.set_min_width(300.0);
|
||||||
ui.heading("Preview Pane");
|
ui.heading("Preview Pane");
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ impl PreviewPane {
|
|||||||
// Display current preview info
|
// Display current preview info
|
||||||
ui.label(format!("Image: {}", self.image_name));
|
ui.label(format!("Image: {}", self.image_name));
|
||||||
|
|
||||||
if let Some(data) = &self.image_data {
|
if let Some(_data) = &self.image_data {
|
||||||
// Try to display the image (simplified)
|
// Try to display the image (simplified)
|
||||||
ui.label("Image would be displayed here");
|
ui.label("Image would be displayed here");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user