-
Notifications
You must be signed in to change notification settings - Fork 284
Claude/client workflow save k qr90 #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 6 commits
17be624
fabb3c0
ba229e7
e6d114e
e039e8f
5b766a7
b26f2a4
5a5bb1b
7c437e5
c365558
72f790d
4cb11fa
d633d20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,6 +68,7 @@ export function Header() { | |
| isSaving, | ||
| setWorkflowMetadata, | ||
| saveToFile, | ||
| saveWorkflow, | ||
| loadWorkflow, | ||
| previousWorkflowSnapshot, | ||
| revertToSnapshot, | ||
|
|
@@ -83,6 +84,7 @@ export function Header() { | |
| isSaving: state.isSaving, | ||
| setWorkflowMetadata: state.setWorkflowMetadata, | ||
| saveToFile: state.saveToFile, | ||
| saveWorkflow: state.saveWorkflow, | ||
| loadWorkflow: state.loadWorkflow, | ||
| previousWorkflowSnapshot: state.previousWorkflowSnapshot, | ||
| revertToSnapshot: state.revertToSnapshot, | ||
|
|
@@ -94,6 +96,7 @@ export function Header() { | |
| const [showProjectModal, setShowProjectModal] = useState(false); | ||
| const [projectModalMode, setProjectModalMode] = useState<"new" | "settings">("new"); | ||
| const fileInputRef = useRef<HTMLInputElement>(null); | ||
| const uploadInputRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| const isProjectConfigured = !!workflowName; | ||
| const canSave = !!(workflowId && workflowName && saveDirectoryPath); | ||
|
|
@@ -119,6 +122,35 @@ export function Header() { | |
| fileInputRef.current?.click(); | ||
| }; | ||
|
|
||
| const handleDownloadWorkflow = () => { | ||
| saveWorkflow(workflowName || undefined); | ||
| }; | ||
|
|
||
| const handleUploadWorkflow = () => { | ||
| uploadInputRef.current?.click(); | ||
| }; | ||
|
|
||
| const handleUploadFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const file = e.target.files?.[0]; | ||
| if (!file) return; | ||
|
|
||
| const reader = new FileReader(); | ||
| reader.onload = async (event) => { | ||
| try { | ||
| const workflow = JSON.parse(event.target?.result as string) as WorkflowFile; | ||
| if (workflow.version && workflow.nodes && workflow.edges) { | ||
| await loadWorkflow(workflow); | ||
| } else { | ||
|
Comment on lines
+133
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strengthen JSON schema checks before calling At Line 141, truthy checks allow malformed payloads through (e.g., non-array 🔧 Proposed fix+ const isValidWorkflowImport = (value: unknown): value is WorkflowFile => {
+ if (!value || typeof value !== "object") return false;
+ const wf = value as Partial<WorkflowFile> & { nodes?: unknown; edges?: unknown };
+ if (wf.version !== 1) return false;
+ if (typeof wf.name !== "string" || !Array.isArray(wf.nodes) || !Array.isArray(wf.edges)) return false;
+
+ const nodesValid = wf.nodes.every((node) => {
+ if (!node || typeof node !== "object") return false;
+ const n = node as { id?: unknown; type?: unknown };
+ return typeof n.id === "string" && typeof n.type === "string";
+ });
+
+ const edgesValid = wf.edges.every((edge) => {
+ if (!edge || typeof edge !== "object") return false;
+ const e = edge as { id?: unknown; source?: unknown; target?: unknown };
+ return (
+ typeof e.id === "string" &&
+ typeof e.source === "string" &&
+ typeof e.target === "string"
+ );
+ });
+
+ return nodesValid && edgesValid;
+ };
+
const handleUploadFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
@@
- const workflow = JSON.parse(event.target?.result as string) as WorkflowFile;
- if (workflow.version && workflow.nodes && workflow.edges) {
- await loadWorkflow(workflow);
+ const parsed = JSON.parse(event.target?.result as string);
+ if (isValidWorkflowImport(parsed)) {
+ await loadWorkflow(parsed);
} else {
alert("Invalid workflow file format");
}Based on learnings: Validate workflows using 🤖 Prompt for AI Agents |
||
| alert("Invalid workflow file format"); | ||
| } | ||
| } catch { | ||
| alert("Failed to parse workflow file"); | ||
| } | ||
| }; | ||
| reader.readAsText(file); | ||
| e.target.value = ""; | ||
| }; | ||
|
|
||
| const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const file = e.target.files?.[0]; | ||
| if (!file) return; | ||
|
|
@@ -189,6 +221,49 @@ export function Header() { | |
| } | ||
| }, [revertToSnapshot]); | ||
|
|
||
| const clientWorkflowButtons = ( | ||
| <div className="flex items-center gap-0.5 ml-1 pl-1 border-l border-neutral-700/50"> | ||
| <button | ||
| onClick={handleDownloadWorkflow} | ||
| className="p-1.5 text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 rounded transition-colors" | ||
| title="Download workflow" | ||
| > | ||
| <svg | ||
| className="w-4 h-4" | ||
| fill="none" | ||
| viewBox="0 0 24 24" | ||
| stroke="currentColor" | ||
| strokeWidth={1.5} | ||
| > | ||
| <path | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" | ||
| /> | ||
| </svg> | ||
| </button> | ||
| <button | ||
| onClick={handleUploadWorkflow} | ||
| className="p-1.5 text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 rounded transition-colors" | ||
| title="Upload workflow" | ||
| > | ||
| <svg | ||
| className="w-4 h-4" | ||
| fill="none" | ||
| viewBox="0 0 24 24" | ||
| stroke="currentColor" | ||
| strokeWidth={1.5} | ||
| > | ||
| <path | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" | ||
| /> | ||
| </svg> | ||
| </button> | ||
| </div> | ||
| ); | ||
|
|
||
| const settingsButtons = ( | ||
| <div className="flex items-center gap-0.5 ml-1 pl-1 border-l border-neutral-700/50"> | ||
| <button | ||
|
|
@@ -233,6 +308,13 @@ export function Header() { | |
| onChange={handleFileChange} | ||
| className="hidden" | ||
| /> | ||
| <input | ||
| ref={uploadInputRef} | ||
| type="file" | ||
| accept=".json" | ||
| onChange={handleUploadFileChange} | ||
| className="hidden" | ||
| /> | ||
| <header className="h-11 bg-neutral-900 border-b border-neutral-800 flex items-center justify-between px-4 shrink-0"> | ||
| <div className="flex items-center gap-2"> | ||
| <button | ||
|
|
@@ -321,6 +403,7 @@ export function Header() { | |
| </div> | ||
|
|
||
| {settingsButtons} | ||
| {clientWorkflowButtons} | ||
| </> | ||
| ) : ( | ||
| <> | ||
|
|
@@ -370,6 +453,7 @@ export function Header() { | |
| </div> | ||
|
|
||
| {settingsButtons} | ||
| {clientWorkflowButtons} | ||
| </> | ||
| )} | ||
| </div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Image→Prompt Constructor menu path currently creates an unconnected node
Line 112 exposes
promptConstructorin the image-target menu, buthandleMenuSelect(insrc/components/WorkflowCanvas.tsx) does not maphandleType === "image"+nodeType === "promptConstructor"to a target handle. The node is created, but the dropped connection is not created.💡 Suggested fix (in
src/components/WorkflowCanvas.tsx)📝 Committable suggestion
🤖 Prompt for AI Agents