mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 05:28:46 +02:00
feat: fix settings header overlap, redesign pattern editor
- Fix SettingsPanel and PatternEditor headers with pt-12 to clear the title bar overlay zone - Replace mode dropdown with visual mode selector cards showing icon, description, and inline flow diagrams for each pattern - Add flow pill diagrams (Single, Sequential, Concurrent, Handoff, Group Chat, Magentic) illustrating how agents interact - Add reasoning effort selector (Low/Medium/High/Maximum) to agent cards — previously not exposed in the editor - Improve agent cards with numbered badges and 3-column layout for name, model, and reasoning fields Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,26 @@
|
||||
import { AlertCircle, CheckCircle, ChevronLeft, Plus, Trash2 } from 'lucide-react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowLeftRight,
|
||||
CheckCircle,
|
||||
ChevronLeft,
|
||||
GitFork,
|
||||
ListOrdered,
|
||||
Lock,
|
||||
MessageSquare,
|
||||
Plus,
|
||||
Trash2,
|
||||
Users,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { validatePatternDefinition, type OrchestrationMode, type PatternDefinition } from '@shared/domain/pattern';
|
||||
import {
|
||||
validatePatternDefinition,
|
||||
type OrchestrationMode,
|
||||
type PatternDefinition,
|
||||
type ReasoningEffort,
|
||||
type PatternAgentDefinition,
|
||||
} from '@shared/domain/pattern';
|
||||
|
||||
interface PatternEditorProps {
|
||||
pattern: PatternDefinition;
|
||||
@@ -11,7 +31,131 @@ interface PatternEditorProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const modes: OrchestrationMode[] = ['single', 'sequential', 'concurrent', 'handoff', 'group-chat', 'magentic'];
|
||||
const reasoningEfforts: { value: ReasoningEffort; label: string }[] = [
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'xhigh', label: 'Maximum' },
|
||||
];
|
||||
|
||||
interface ModeInfo {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const modeInfo: Record<OrchestrationMode, ModeInfo> = {
|
||||
single: {
|
||||
icon: MessageSquare,
|
||||
label: 'Single',
|
||||
description: 'Direct conversation with one agent',
|
||||
},
|
||||
sequential: {
|
||||
icon: ListOrdered,
|
||||
label: 'Sequential',
|
||||
description: 'Agents process in order, each refining the result',
|
||||
},
|
||||
concurrent: {
|
||||
icon: GitFork,
|
||||
label: 'Concurrent',
|
||||
description: 'Multiple agents respond in parallel',
|
||||
},
|
||||
handoff: {
|
||||
icon: ArrowLeftRight,
|
||||
label: 'Handoff',
|
||||
description: 'Triage agent routes to specialists',
|
||||
},
|
||||
'group-chat': {
|
||||
icon: Users,
|
||||
label: 'Group Chat',
|
||||
description: 'Agents iterate in managed round-robin',
|
||||
},
|
||||
magentic: {
|
||||
icon: Lock,
|
||||
label: 'Magentic',
|
||||
description: 'Not yet available in .NET',
|
||||
},
|
||||
};
|
||||
|
||||
function FlowPill({ children, variant = 'agent' }: { children: ReactNode; variant?: 'user' | 'agent' }) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex rounded px-1.5 py-0.5 text-[9px] font-semibold leading-none ${
|
||||
variant === 'user'
|
||||
? 'bg-indigo-500/20 text-indigo-400'
|
||||
: 'bg-zinc-700/60 text-zinc-400'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function FlowArrow({ label }: { label?: string }) {
|
||||
return <span className="text-[10px] leading-none text-zinc-600">{label ?? '→'}</span>;
|
||||
}
|
||||
|
||||
function ModeFlowDiagram({ mode }: { mode: OrchestrationMode }) {
|
||||
switch (mode) {
|
||||
case 'single':
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<FlowPill variant="user">You</FlowPill>
|
||||
<FlowArrow />
|
||||
<FlowPill>Agent</FlowPill>
|
||||
<FlowArrow />
|
||||
<FlowPill variant="user">You</FlowPill>
|
||||
</div>
|
||||
);
|
||||
case 'sequential':
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<FlowPill>A₁</FlowPill>
|
||||
<FlowArrow />
|
||||
<FlowPill>A₂</FlowPill>
|
||||
<FlowArrow />
|
||||
<FlowPill>A₃</FlowPill>
|
||||
</div>
|
||||
);
|
||||
case 'concurrent':
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<FlowPill variant="user">You</FlowPill>
|
||||
<FlowArrow />
|
||||
<div className="flex gap-0.5">
|
||||
<FlowPill>A₁</FlowPill>
|
||||
<FlowPill>A₂</FlowPill>
|
||||
<FlowPill>A₃</FlowPill>
|
||||
</div>
|
||||
<FlowArrow />
|
||||
<FlowPill variant="user">You</FlowPill>
|
||||
</div>
|
||||
);
|
||||
case 'handoff':
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<FlowPill>Triage</FlowPill>
|
||||
<FlowArrow label="⇄" />
|
||||
<FlowPill>S₁</FlowPill>
|
||||
<FlowPill>S₂</FlowPill>
|
||||
</div>
|
||||
);
|
||||
case 'group-chat':
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<FlowPill>A₁</FlowPill>
|
||||
<FlowArrow label="⇄" />
|
||||
<FlowPill>A₂</FlowPill>
|
||||
<FlowArrow label="⇄" />
|
||||
<FlowPill>A₃</FlowPill>
|
||||
</div>
|
||||
);
|
||||
case 'magentic':
|
||||
return <span className="text-[10px] italic text-zinc-700">Coming soon</span>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function InputField({
|
||||
label,
|
||||
@@ -53,7 +197,7 @@ function InputField({
|
||||
export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave, onBack }: PatternEditorProps) {
|
||||
const issues = validatePatternDefinition(pattern);
|
||||
|
||||
function updateAgent(agentId: string, patch: Record<string, string>) {
|
||||
function updateAgent(agentId: string, patch: Partial<PatternAgentDefinition>) {
|
||||
onChange({
|
||||
...pattern,
|
||||
agents: pattern.agents.map((a) => (a.id === agentId ? { ...a, ...patch } : a)),
|
||||
@@ -62,8 +206,8 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave,
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-zinc-800 px-6 py-3">
|
||||
{/* Header — top padding clears the title bar overlay zone */}
|
||||
<div className="flex items-center justify-between border-b border-zinc-800 px-6 pb-3 pt-12">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="flex size-8 items-center justify-center rounded-lg text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
||||
@@ -104,7 +248,7 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave,
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<div className="mx-auto max-w-2xl space-y-8">
|
||||
{/* Validation banner */}
|
||||
{issues.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
@@ -129,35 +273,17 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave,
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Basic fields */}
|
||||
{/* Name + description */}
|
||||
<section className="space-y-4">
|
||||
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
|
||||
General
|
||||
</h4>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InputField
|
||||
label="Name"
|
||||
onChange={(v) => onChange({ ...pattern, name: v })}
|
||||
placeholder="Pattern name"
|
||||
value={pattern.name}
|
||||
/>
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-zinc-400">Mode</span>
|
||||
<select
|
||||
className="w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 text-[13px] text-zinc-100 outline-none transition focus:border-indigo-500/50"
|
||||
onChange={(e) =>
|
||||
onChange({ ...pattern, mode: e.target.value as OrchestrationMode })
|
||||
}
|
||||
value={pattern.mode}
|
||||
>
|
||||
{modes.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<InputField
|
||||
label="Name"
|
||||
onChange={(v) => onChange({ ...pattern, name: v })}
|
||||
placeholder="Pattern name"
|
||||
value={pattern.name}
|
||||
/>
|
||||
<InputField
|
||||
label="Description"
|
||||
multiline
|
||||
@@ -167,6 +293,56 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave,
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* Mode selector cards */}
|
||||
<section className="space-y-4">
|
||||
<h4 className="text-[12px] font-semibold uppercase tracking-wider text-zinc-500">
|
||||
Orchestration Mode
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{(Object.keys(modeInfo) as OrchestrationMode[]).map((mode) => {
|
||||
const info = modeInfo[mode];
|
||||
const Icon = info.icon;
|
||||
const selected = pattern.mode === mode;
|
||||
const disabled = mode === 'magentic';
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`flex flex-col rounded-xl border p-3 text-left transition ${
|
||||
selected
|
||||
? 'border-indigo-500/40 bg-indigo-500/5 ring-1 ring-indigo-500/20'
|
||||
: disabled
|
||||
? 'cursor-not-allowed border-zinc-800/50 opacity-40'
|
||||
: 'border-zinc-800 hover:border-zinc-700 hover:bg-zinc-900/60'
|
||||
}`}
|
||||
disabled={disabled}
|
||||
key={mode}
|
||||
onClick={() => onChange({ ...pattern, mode })}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon
|
||||
className={`size-4 ${selected ? 'text-indigo-400' : 'text-zinc-500'}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-[12px] font-semibold ${
|
||||
selected ? 'text-indigo-200' : 'text-zinc-300'
|
||||
}`}
|
||||
>
|
||||
{info.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] leading-snug text-zinc-500">
|
||||
{info.description}
|
||||
</p>
|
||||
<div className="mt-2.5">
|
||||
<ModeFlowDiagram mode={mode} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Agents */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -186,6 +362,7 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave,
|
||||
description: '',
|
||||
instructions: '',
|
||||
model: 'gpt-5.4',
|
||||
reasoningEffort: 'high',
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -193,7 +370,7 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave,
|
||||
type="button"
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
Add
|
||||
Add agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -204,12 +381,17 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave,
|
||||
key={agent.id}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-[12px] font-medium text-zinc-500">
|
||||
Agent {index + 1}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex size-5 items-center justify-center rounded bg-zinc-700/60 text-[10px] font-bold text-zinc-400">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="text-[12px] font-medium text-zinc-300">
|
||||
{agent.name || 'Unnamed agent'}
|
||||
</span>
|
||||
</div>
|
||||
{pattern.agents.length > 1 && (
|
||||
<button
|
||||
className="text-[12px] text-zinc-600 transition hover:text-red-400"
|
||||
className="flex items-center gap-1 text-[12px] text-zinc-600 transition hover:text-red-400"
|
||||
onClick={() =>
|
||||
onChange({
|
||||
...pattern,
|
||||
@@ -218,11 +400,13 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave,
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<InputField
|
||||
label="Name"
|
||||
onChange={(v) => updateAgent(agent.id, { name: v })}
|
||||
@@ -234,22 +418,41 @@ export function PatternEditor({ pattern, isBuiltin, onChange, onDelete, onSave,
|
||||
placeholder="e.g. gpt-5.4"
|
||||
value={agent.model}
|
||||
/>
|
||||
<div className="sm:col-span-2">
|
||||
<InputField
|
||||
label="Description"
|
||||
onChange={(v) => updateAgent(agent.id, { description: v })}
|
||||
value={agent.description}
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<InputField
|
||||
label="Instructions"
|
||||
multiline
|
||||
onChange={(v) => updateAgent(agent.id, { instructions: v })}
|
||||
placeholder="System prompt for this agent..."
|
||||
value={agent.instructions}
|
||||
/>
|
||||
</div>
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-zinc-400">Reasoning</span>
|
||||
<select
|
||||
className="w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 text-[13px] text-zinc-100 outline-none transition focus:border-indigo-500/50"
|
||||
onChange={(e) =>
|
||||
updateAgent(agent.id, {
|
||||
reasoningEffort: e.target.value as ReasoningEffort,
|
||||
})
|
||||
}
|
||||
value={agent.reasoningEffort ?? 'high'}
|
||||
>
|
||||
{reasoningEfforts.map((r) => (
|
||||
<option key={r.value} value={r.value}>
|
||||
{r.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<InputField
|
||||
label="Description"
|
||||
onChange={(v) => updateAgent(agent.id, { description: v })}
|
||||
placeholder="What this agent does..."
|
||||
value={agent.description}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<InputField
|
||||
label="Instructions"
|
||||
multiline
|
||||
onChange={(v) => updateAgent(agent.id, { instructions: v })}
|
||||
placeholder="System prompt for this agent..."
|
||||
value={agent.instructions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -54,8 +54,8 @@ export function SettingsPanel({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col bg-[var(--color-surface-0)]">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-[var(--color-border)] px-6 py-3">
|
||||
{/* Header — top padding clears the title bar overlay zone */}
|
||||
<div className="flex items-center justify-between border-b border-[var(--color-border)] px-6 pb-3 pt-12">
|
||||
<h2 className="text-sm font-semibold text-zinc-100">Settings</h2>
|
||||
<button
|
||||
className="flex size-8 items-center justify-center rounded-lg text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
|
||||
|
||||
Reference in New Issue
Block a user