Skip to content

Commit 92d7ae8

Browse files
author
JiaDe
committed
feat(admin-console): 8 UX optimizations for business flow
1. Agent Create: auto-name from Position + Employee selection 2. Employee Detail: View Agent / Edit SOUL / Create Agent buttons 3. Playground: dynamic tenant options from real employees API 4. Dashboard: Add Employee + Playground in Quick Actions 5. Agent Detail: remove dead Configure button, add Workspace link 6. Employee Create: IM channel selection + Add & Auto-Provision 7. Position Create: tool permissions + default skills selection 8. Portal Chat: delivery status, keyboard hint, warmup label
1 parent 7fe53c3 commit 92d7ae8

7 files changed

Lines changed: 138 additions & 40 deletions

File tree

enterprise/admin-console/src/pages/AgentFactory/AgentDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export default function AgentDetail() {
8282
<Button variant="default" onClick={() => navigate('/agents')}><ArrowLeft size={16} /> Back</Button>
8383
<Button variant="default" onClick={() => navigate(`/agents/${agent.id}/soul`)}><Edit3 size={16} /> Edit SOUL</Button>
8484
<Button variant="default" onClick={() => navigate('/playground')}><Play size={16} /> Test</Button>
85-
<Button variant="primary"><Settings size={16} /> Configure</Button>
85+
<Button variant="default" onClick={() => navigate('/workspace')}><Settings size={16} /> Workspace</Button>
8686
</div>
8787
}
8888
/>

enterprise/admin-console/src/pages/AgentFactory/AgentList.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default function AgentList() {
1717
const [newName, setNewName] = useState('');
1818
const [newPos, setNewPos] = useState('');
1919
const [newEmp, setNewEmp] = useState('');
20+
const [newChannels, setNewChannels] = useState<string[]>(['slack']);
2021
const [filterText, setFilterText] = useState('');
2122
const [filterDept, setFilterDept] = useState('all');
2223
const [filterStatus, setFilterStatus] = useState('all');
@@ -173,9 +174,22 @@ export default function AgentList() {
173174
{createStep === 0 && (
174175
<div className="space-y-4">
175176
<h4 className="text-sm font-medium text-text-primary">Step 1: Basic Configuration</h4>
176-
<Input label="Agent Name" value={newName} onChange={setNewName} placeholder="SA Agent - Zhang San" />
177-
<Select label="Position Template" value={newPos} onChange={setNewPos} options={posOptions} placeholder="Select position" description="Inherits SOUL, Skills, and tool permissions" />
178-
<Select label="Bind Employee" value={newEmp} onChange={setNewEmp} options={empOptions} placeholder="Select employee (only showing unbound)" />
177+
<Select label="Position Template" value={newPos} onChange={v => {
178+
setNewPos(v);
179+
// Auto-generate name when position changes
180+
const pos = POSITIONS.find(p => p.id === v);
181+
const emp = EMPLOYEES.find(e => e.id === newEmp);
182+
if (pos && emp) setNewName(`${pos.name} Agent - ${emp.name}`);
183+
else if (pos) setNewName(`${pos.name} Agent`);
184+
}} options={posOptions} placeholder="Select position" description="Inherits SOUL, Skills, and tool permissions" />
185+
<Select label="Bind Employee" value={newEmp} onChange={v => {
186+
setNewEmp(v);
187+
// Auto-generate name when employee changes
188+
const pos = POSITIONS.find(p => p.id === newPos);
189+
const emp = EMPLOYEES.find(e => e.id === v);
190+
if (pos && emp) setNewName(`${pos.name} Agent - ${emp.name}`);
191+
}} options={empOptions} placeholder="Select employee (only showing unbound)" />
192+
<Input label="Agent Name" value={newName} onChange={setNewName} placeholder="Auto-generated from position + employee" description="Auto-filled — edit if you want a custom name" />
179193
</div>
180194
)}
181195
{createStep === 1 && (

enterprise/admin-console/src/pages/Dashboard.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,17 +201,17 @@ export default function Dashboard() {
201201
<Card>
202202
<h3 className="mb-4 text-lg font-semibold text-text-primary">Quick Actions</h3>
203203
<div className="grid grid-cols-2 gap-2">
204-
<Button variant="primary" className="w-full" onClick={() => navigate('/org/positions')}>
205-
<Building2 size={16} /> New Position
204+
<Button variant="primary" className="w-full" onClick={() => navigate('/org/employees')}>
205+
<Users size={16} /> Add Employee
206206
</Button>
207207
<Button variant="default" className="w-full" onClick={() => navigate('/agents')}>
208208
<Bot size={16} /> New Agent
209209
</Button>
210210
<Button variant="default" className="w-full" onClick={() => navigate('/bindings')}>
211211
<Link2 size={16} /> Bind Employee
212212
</Button>
213-
<Button variant="default" className="w-full" onClick={() => navigate('/monitor')}>
214-
<Activity size={16} /> Monitor
213+
<Button variant="default" className="w-full" onClick={() => navigate('/playground')}>
214+
<MessageSquare size={16} /> Playground
215215
</Button>
216216
</div>
217217
</Card>

enterprise/admin-console/src/pages/Organization/Employees.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export default function Employees() {
5757
const [newName, setNewName] = useState('');
5858
const [newNo, setNewNo] = useState('');
5959
const [newPos, setNewPos] = useState('');
60+
const [newChannels, setNewChannels] = useState<string[]>(['slack']);
6061

6162
const posOptions = POSITIONS.map(p => ({ label: `${p.name} (${p.departmentName})`, value: p.id }));
6263
const departments = useMemo(() => {
@@ -290,11 +291,20 @@ export default function Employees() {
290291
<p className="text-[10px] text-text-muted">SOUL Ver</p>
291292
</div>
292293
</div>
294+
<div className="flex gap-2 mt-3">
295+
<Button variant="default" size="sm" onClick={() => { setSelected(null); navigate(`/agents/${agent.id}`); }}>View Agent</Button>
296+
<Button variant="default" size="sm" onClick={() => { setSelected(null); navigate(`/agents/${agent.id}/soul`); }}>Edit SOUL</Button>
297+
</div>
293298
</div>
294299
) : (
295-
<div className="rounded-lg bg-amber-500/5 border border-amber-500/20 p-4 text-center">
296-
<p className="text-sm text-amber-400 mb-1">No agent bound</p>
297-
<p className="text-xs text-text-muted">Assign to a position to auto-provision, or use Bulk Provision from Positions page.</p>
300+
<div className="rounded-lg bg-amber-500/5 border border-amber-500/20 p-4">
301+
<p className="text-sm text-amber-400 mb-2">No agent bound</p>
302+
<p className="text-xs text-text-muted mb-3">This employee doesn't have a personal AI agent yet. Create one to enable AI-assisted work.</p>
303+
<Button variant="primary" size="sm" onClick={() => {
304+
setSelected(null);
305+
navigate('/agents');
306+
// Pre-fill would require state sharing; navigate to Agent Factory instead
307+
}}><Bot size={14} /> Create Agent for {selected.name.split(' ')[0]}</Button>
298308
</div>
299309
)}
300310

@@ -356,19 +366,31 @@ export default function Employees() {
356366
name: newName, employeeNo: newNo || `EMP-${Date.now()}`,
357367
positionId: newPos, positionName: pos?.name || '',
358368
departmentId: pos?.departmentId || '', departmentName: pos?.departmentName || '',
359-
channels: [], agentId: null, agentStatus: 'idle',
369+
channels: newChannels, agentId: null, agentStatus: 'idle',
360370
} as any);
361371
}
362-
setShowCreate(false); setNewName(''); setNewNo(''); setNewPos('');
363-
}}>Add</Button></div>}
372+
setShowCreate(false); setNewName(''); setNewNo(''); setNewPos(''); setNewChannels(['slack']);
373+
}}>Add & Auto-Provision</Button></div>}
364374
>
365375
<div className="space-y-4">
366376
<Input label="Name" value={newName} onChange={setNewName} />
367377
<Input label="Employee No" value={newNo} onChange={setNewNo} placeholder="EMP-022" />
368378
<Select label="Position" value={newPos} onChange={setNewPos} options={posOptions} placeholder="Select position" />
379+
<div>
380+
<label className="block text-xs font-medium text-text-secondary mb-1.5">IM Channels</label>
381+
<div className="flex flex-wrap gap-2">
382+
{['slack', 'discord', 'telegram', 'whatsapp', 'feishu'].map(ch => (
383+
<button key={ch} onClick={() => setNewChannels(prev => prev.includes(ch) ? prev.filter(c => c !== ch) : [...prev, ch])}
384+
className={`rounded-lg px-3 py-1.5 text-xs font-medium border transition-colors ${newChannels.includes(ch) ? 'bg-primary/10 border-primary/40 text-primary-light' : 'border-dark-border text-text-muted hover:border-text-muted'}`}>
385+
{CHANNEL_LABELS[ch as ChannelType]}
386+
</button>
387+
))}
388+
</div>
389+
<p className="text-[10px] text-text-muted mt-1">Select which IM platforms this employee will use</p>
390+
</div>
369391
<div className="rounded-lg bg-primary/5 border border-primary/20 p-3 text-xs text-text-secondary">
370392
<Zap size={14} className="inline mr-1 text-primary-light" />
371-
A personal 1:1 agent will be auto-provisioned based on the position's SOUL template, default skills, and channel settings.
393+
A personal 1:1 agent will be auto-provisioned based on the position's SOUL template, default skills, and channel settings. The employee can start chatting immediately after IM pairing.
372394
</div>
373395
</div>
374396
</Modal>

enterprise/admin-console/src/pages/Organization/Positions.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export default function Positions() {
1919
const [newDept, setNewDept] = useState('');
2020
const [newSoul, setNewSoul] = useState('');
2121
const [newChannel, setNewChannel] = useState('slack');
22+
const [newTools, setNewTools] = useState<string[]>(['web_search']);
23+
const [newSkills, setNewSkills] = useState<string[]>([]);
2224

2325
const deptOptions = DEPARTMENTS.filter(d => !d.parentId).map(d => ({ label: d.name, value: d.id }));
2426
const totalMembers = POSITIONS.reduce((s, p) => s + p.memberCount, 0);
@@ -179,15 +181,15 @@ export default function Positions() {
179181
departmentId: newDept,
180182
departmentName: dept?.name || '',
181183
soulTemplate: newSoul,
182-
defaultSkills: [],
184+
defaultSkills: newSkills,
183185
defaultKnowledge: [],
184-
toolAllowlist: ['web_search'],
186+
toolAllowlist: newTools,
185187
defaultChannel: newChannel as any,
186188
memberCount: 0,
187189
createdAt: new Date().toISOString(),
188190
});
189191
}
190-
setShowCreate(false); setNewName(''); setNewDept(''); setNewSoul(''); setNewChannel('slack');
192+
setShowCreate(false); setNewName(''); setNewDept(''); setNewSoul(''); setNewChannel('slack'); setNewTools(['web_search']); setNewSkills([]);
191193
}}>Create</Button></div>}
192194
>
193195
<div className="space-y-4">
@@ -198,7 +200,31 @@ export default function Positions() {
198200
{ label: 'WhatsApp', value: 'whatsapp' }, { label: 'Discord', value: 'discord' },
199201
{ label: 'Feishu', value: 'feishu' }, { label: 'DingTalk', value: 'dingtalk' },
200202
]} />
201-
<Textarea label="SOUL Template" value={newSoul} onChange={setNewSoul} rows={8} placeholder="You are a ... specializing in ..." description="Define the default persona and behavior rules for agents in this position" />
203+
<div>
204+
<label className="block text-xs font-medium text-text-secondary mb-1.5">Tool Permissions</label>
205+
<div className="flex flex-wrap gap-2">
206+
{['web_search', 'browser', 'file', 'file_write', 'shell', 'code_execution'].map(tool => (
207+
<button key={tool} onClick={() => setNewTools(prev => prev.includes(tool) ? prev.filter(t => t !== tool) : [...prev, tool])}
208+
className={`rounded-lg px-3 py-1.5 text-xs font-medium border transition-colors ${newTools.includes(tool) ? 'bg-primary/10 border-primary/40 text-primary-light' : 'border-dark-border text-text-muted hover:border-text-muted'}`}>
209+
{tool}
210+
</button>
211+
))}
212+
</div>
213+
<p className="text-[10px] text-text-muted mt-1">Tools this position's agents are allowed to use (Plan A enforcement)</p>
214+
</div>
215+
<div>
216+
<label className="block text-xs font-medium text-text-secondary mb-1.5">Default Skills</label>
217+
<div className="flex flex-wrap gap-2">
218+
{['web_search', 'browser', 'excel-gen', 'email-send', 'calendar-check', 'crm-query', 'google-docs', 'notion-sync', 'sap-connector'].map(skill => (
219+
<button key={skill} onClick={() => setNewSkills(prev => prev.includes(skill) ? prev.filter(s => s !== skill) : [...prev, skill])}
220+
className={`rounded-lg px-3 py-1.5 text-xs font-medium border transition-colors ${newSkills.includes(skill) ? 'bg-success/10 border-success/40 text-success' : 'border-dark-border text-text-muted hover:border-text-muted'}`}>
221+
{skill}
222+
</button>
223+
))}
224+
</div>
225+
<p className="text-[10px] text-text-muted mt-1">Skills auto-installed for agents in this position</p>
226+
</div>
227+
<Textarea label="SOUL Template" value={newSoul} onChange={setNewSoul} rows={6} placeholder="You are a ... specializing in ..." description="Define the default persona and behavior rules for agents in this position" />
202228
</div>
203229
</Modal>
204230
</div>

enterprise/admin-console/src/pages/Playground.tsx

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { useState, useEffect, useCallback } from 'react';
1+
import { useState, useEffect, useCallback, useMemo } from 'react';
22
import { Send, User, Bot, Shield, Eye, Terminal, Loader } from 'lucide-react';
33
import ReactMarkdown from 'react-markdown';
44
import { Card, Badge, Button, PageHeader, Select } from '../components/ui';
5-
import { usePlaygroundProfiles } from '../hooks/useApi';
5+
import { usePlaygroundProfiles, useAgents, useEmployees, usePositions } from '../hooks/useApi';
66
import { api } from '../api/client';
77

8-
interface ChatMessage { role: 'user' | 'assistant' | 'system'; content: string; timestamp: string; }
9-
108
const STORAGE_KEY = 'openclaw_playground_chat';
119

1210
function loadMessages(tenantId: string): ChatMessage[] {
@@ -22,38 +20,69 @@ function saveMessages(tenantId: string, messages: ChatMessage[]) {
2220

2321
interface ChatMessage { role: 'user' | 'assistant' | 'system'; content: string; timestamp: string; }
2422

25-
const TENANT_OPTIONS = [
26-
{ label: 'Sarah Chen — Intern (WhatsApp)', value: 'wa__intern_sarah' },
27-
{ label: 'Alex Wang — Senior Engineer (Telegram)', value: 'tg__engineer_alex' },
28-
{ label: 'Jordan Lee — IT Admin (Discord)', value: 'dc__admin_jordan' },
29-
{ label: 'Carol Zhang — Finance Analyst (Slack)', value: 'sl__finance_carol' },
30-
];
31-
3223
export default function Playground() {
3324
const { data: profiles } = usePlaygroundProfiles();
34-
const [tenantId, setTenantId] = useState(TENANT_OPTIONS[0].value);
35-
const [messages, setMessages] = useState<ChatMessage[]>(() => loadMessages(TENANT_OPTIONS[0].value));
25+
const { data: agents = [] } = useAgents();
26+
const { data: employees = [] } = useEmployees();
27+
const { data: positions = [] } = usePositions();
28+
29+
// Build tenant options from real employees + agents
30+
const tenantOptions = useMemo(() => {
31+
const channelShort: Record<string, string> = { whatsapp: 'wa', telegram: 'tg', discord: 'dc', slack: 'sl', feishu: 'fs' };
32+
const opts = employees
33+
.filter(e => e.agentId)
34+
.map(e => {
35+
const agent = agents.find(a => a.id === e.agentId);
36+
const ch = e.channels?.[0] || 'slack';
37+
const chKey = channelShort[ch] || ch.slice(0, 2);
38+
return {
39+
label: `${e.name}${e.positionName} (${ch})`,
40+
value: `${chKey}__${e.id}`,
41+
};
42+
});
43+
// Fallback if no employees loaded yet
44+
if (opts.length === 0) {
45+
return [
46+
{ label: 'Carol Zhang — Finance Analyst (Slack)', value: 'sl__emp-carol' },
47+
{ label: 'Wang Wu — SDE (Telegram)', value: 'tg__emp-w5' },
48+
{ label: 'Zhang San — SA (Discord)', value: 'dc__emp-z3' },
49+
];
50+
}
51+
return opts;
52+
}, [employees, agents]);
53+
54+
const [tenantId, setTenantId] = useState('');
55+
const [messages, setMessages] = useState<ChatMessage[]>([]);
3656
const [inputValue, setInputValue] = useState('');
3757
const [lastPlanE, setLastPlanE] = useState('No messages processed yet');
3858
const [mode, setMode] = useState<'simulate' | 'live'>('simulate');
3959
const [sending, setSending] = useState(false);
4060

61+
// Initialize tenantId when options load
62+
useEffect(() => {
63+
if (tenantOptions.length > 0 && !tenantId) {
64+
setTenantId(tenantOptions[0].value);
65+
}
66+
}, [tenantOptions, tenantId]);
67+
4168
const profile = profiles?.[tenantId] || { role: 'loading', tools: [], planA: '', planE: '' };
4269
const profileLoaded = !!profiles?.[tenantId];
4370

4471
// Persist messages
45-
useEffect(() => { saveMessages(tenantId, messages); }, [messages, tenantId]);
72+
useEffect(() => { if (tenantId) saveMessages(tenantId, messages); }, [messages, tenantId]);
4673

4774
useEffect(() => {
48-
if (!profileLoaded) return;
75+
if (!tenantId) return;
4976
const saved = loadMessages(tenantId);
5077
if (saved.length > 0) {
5178
setMessages(saved);
5279
} else {
53-
setMessages([{ role: 'system', content: `🔒 Tenant context loaded: ${profile.role} role, ${profile.tools.length} tools`, timestamp: '' }]);
80+
const p = profiles?.[tenantId];
81+
const label = tenantOptions.find(o => o.value === tenantId)?.label || tenantId;
82+
setMessages([{ role: 'system', content: `🔒 Tenant context loaded: ${label}${p?.role || 'unknown'} role, ${p?.tools?.length || 0} tools`, timestamp: '' }]);
5483
}
5584
setLastPlanE('No messages processed yet');
56-
}, [tenantId, profileLoaded]);
85+
}, [tenantId, profiles]);
5786

5887
const handleSend = async () => {
5988
if (!inputValue.trim() || sending) return;
@@ -95,7 +124,7 @@ export default function Playground() {
95124
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
96125
<Card>
97126
<div className="mb-4 space-y-3">
98-
<Select label="Tenant Context" value={tenantId} onChange={v => setTenantId(v)} options={TENANT_OPTIONS} />
127+
<Select label="Tenant Context" value={tenantId} onChange={v => setTenantId(v)} options={tenantOptions} />
99128
<div className="flex items-center gap-3">
100129
<span className="text-xs text-text-muted">Mode:</span>
101130
<button onClick={() => setMode('simulate')} className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'simulate' ? 'bg-primary/10 text-primary-light' : 'text-text-muted hover:bg-dark-hover'}`}>Simulate</button>

enterprise/admin-console/src/pages/portal/Chat.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,11 @@ export default function PortalChat() {
131131
<p className="text-sm whitespace-pre-wrap">{msg.content}</p>
132132
)}
133133
<p className={`text-[10px] mt-1.5 ${msg.role === 'user' ? 'text-white/60' : 'text-text-muted'}`}>
134+
{msg.role === 'user' && '✓ '}
134135
{new Date(msg.timestamp).toLocaleTimeString()}
135136
{msg.source === 'agentcore' && ' · AgentCore'}
136137
{msg.source === 'fallback' && ' · offline'}
138+
{msg.source === 'warmup' && ' · warming up'}
137139
{msg.model && ` · ${msg.model.split('/').pop()?.split(':')[0] || ''}`}
138140
</p>
139141
</div>
@@ -176,9 +178,14 @@ export default function PortalChat() {
176178
<Send size={18} />
177179
</button>
178180
</div>
179-
<p className="text-[10px] text-text-muted mt-2 text-center">
180-
Messages are stored locally. Your agent runs on AWS Bedrock via AgentCore.
181-
</p>
181+
<div className="flex items-center justify-between mt-2">
182+
<p className="text-[10px] text-text-muted">
183+
Press Enter to send · Messages stored locally
184+
</p>
185+
<p className="text-[10px] text-text-muted">
186+
Powered by AWS Bedrock via AgentCore
187+
</p>
188+
</div>
182189
</div>
183190
</div>
184191
);

0 commit comments

Comments
 (0)