mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
[aidan] feat/workflows: per-workflow color swatch
This commit is contained in:
@@ -105,6 +105,8 @@ class Workflow(BaseModel):
|
||||
title: str = "Untitled workflow"
|
||||
description: str = ""
|
||||
icon: str = ""
|
||||
# User-chosen swatch (hex). None falls back to the id-hash color in the UI.
|
||||
color: Optional[str] = None
|
||||
system_prompt: Optional[str] = None
|
||||
use_synced_prompt: bool = True
|
||||
steps: list[WorkflowStep] = Field(default_factory=list)
|
||||
@@ -212,6 +214,7 @@ class WorkflowCreate(BaseModel):
|
||||
unsaved: bool = False
|
||||
description: str = ""
|
||||
icon: str = ""
|
||||
color: Optional[str] = None
|
||||
system_prompt: Optional[str] = None
|
||||
use_synced_prompt: bool = True
|
||||
steps: list[WorkflowStep] = Field(default_factory=list)
|
||||
@@ -246,6 +249,7 @@ class WorkflowUpdate(BaseModel):
|
||||
auto_named: Optional[bool] = None
|
||||
description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
system_prompt: Optional[str] = None
|
||||
use_synced_prompt: Optional[bool] = None
|
||||
steps: Optional[list[WorkflowStep]] = None
|
||||
|
||||
@@ -248,6 +248,7 @@ async def create_workflow(body: WorkflowCreate):
|
||||
title=body.title,
|
||||
description=body.description,
|
||||
icon=body.icon,
|
||||
color=body.color,
|
||||
system_prompt=body.system_prompt,
|
||||
use_synced_prompt=body.use_synced_prompt,
|
||||
steps=body.steps,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useWC, WORKFLOW_PALETTE } from './uiKit';
|
||||
|
||||
// Small swatch button that opens a palette popover. Self-rendered (no portal)
|
||||
// so it survives the canvas compositor, same reasoning as RepeatField.
|
||||
const ColorSwatch: React.FC<{ value: string; onChange: (hex: string) => void; size?: number }> = ({ value, onChange, size = 14 }) => {
|
||||
const WC = useWC();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
return () => document.removeEventListener('mousedown', onDoc);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{ position: 'relative', flex: 'none' }}>
|
||||
<div
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
title="Workflow color"
|
||||
style={{ width: size, height: size, borderRadius: 4, cursor: 'pointer', background: value, boxShadow: `0 0 0 1px rgba(${WC.inkRGB},0.14)` }}
|
||||
/>
|
||||
{open && (
|
||||
<div style={{ position: 'absolute', top: size + 11, left: 0, zIndex: 45, background: WC.paper, border: `1px solid ${WC.line2}`, borderRadius: WC.radius.lg, boxShadow: WC.shadow.md, padding: 9, display: 'flex', gap: 7 }}>
|
||||
{WORKFLOW_PALETTE.map((hex) => {
|
||||
const active = hex.toLowerCase() === value.toLowerCase();
|
||||
return (
|
||||
<div
|
||||
key={hex}
|
||||
onClick={() => { onChange(hex); setOpen(false); }}
|
||||
style={{ width: 18, height: 18, borderRadius: 5, cursor: 'pointer', background: hex, boxShadow: active ? `0 0 0 2px ${WC.paper}, 0 0 0 3.5px ${hex}` : `0 0 0 1px rgba(${WC.inkRGB},0.12)` }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorSwatch;
|
||||
@@ -63,6 +63,8 @@ export interface Workflow {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
/** User-chosen swatch (hex). Null/undefined falls back to the id-hash color. */
|
||||
color?: string | null;
|
||||
system_prompt: string | null;
|
||||
use_synced_prompt: boolean;
|
||||
steps: WorkflowStep[];
|
||||
|
||||
Reference in New Issue
Block a user