+
-
-
-
PERSONA GENERATION
- {dirty && (
-
-
- UNSAVED CHANGES
-
+
{isTopology ? 'TOPOLOGY PERSONAS' : 'PERSONA GENERATION'}
+
+ {personas.length} PERSONA{personas.length === 1 ? '' : 'S'} · {llmHeavyCount} LLM-HEAVY
+ {isTopology
+ ? ` · TOPOLOGY ${topoName ? topoName.toUpperCase() : (topologyId ?? '').slice(0, 8)} · DEFAULT LANG ${languageDefault.toUpperCase()}`
+ : ' · GLOBAL POOL · FLEET (MACVLAN/IPVLAN) + SWARM-SHARD MAIL DECKIES'}
+
+
+
+
+ {([['all', 'ALL'], ['formal', 'FORMAL'], ['direct', 'DIRECT'],
+ ['casual', 'CASUAL'], ['technical', 'TECHNICAL'],
+ ['custom', 'CUSTOM']] as [FilterKey, string][]).map(
+ ([v, l]) => (
+
+ ),
)}
-
- GLOBAL POOL · FLEET (MACVLAN/IPVLAN) + SWARM-SHARD MAIL DECKIES
-
+
+
+
+
-
- Scope: personas listed here drive emailgen against{' '}
- non-MazeNET mail deckies (unihost MACVLAN/IPVLAN, SWARM
- shards). MazeNET topologies have their own per-topology persona
- list configured in the topology editor.
-
- {path && (
+ {isTopology ? (
+
+ Scope: personas listed here drive emailgen for the
+ mail deckies attached to this MazeNET topology only.
+ Unset language entries fall back to the topology's
+ default ({languageDefault.toUpperCase()}).
+
+ ) : (
+
+ Scope: personas listed here drive emailgen against{' '}
+ non-MazeNET mail deckies (unihost MACVLAN/IPVLAN, SWARM
+ shards). MazeNET topologies have their own per-topology persona
+ list configured in the topology editor.
+
+ )}
+ {path && !isTopology && (
FILE{' '}
{path}
)}
-
-
-
-
-
-
{error && (
-
+
-
-
- {loading ? (
-
- ) : personas.length === 0 ? (
-
- ) : (
-
-
-
- | NAME |
- EMAIL |
- ROLE |
- TONE |
- LANG |
- HOURS |
- REPLY |
- MANNERISMS |
- FLAGS |
- |
-
-
-
- {personas.map((p, idx) => (
-
- | {p.name} |
- {p.email} |
- {p.role} |
-
- {p.tone}
- |
-
-
- {(p.language ?? 'en').toUpperCase()}
-
- |
- {p.active_hours} |
- {p.reply_latency} |
-
- {p.mannerisms.length === 0
- ? '—'
- : `${p.mannerisms.length} item${p.mannerisms.length === 1 ? '' : 's'}`}
- |
-
- {p.uses_llms_heavily && (
-
- LLM-HEAVY
-
- )}
- |
-
-
-
- |
-
- ))}
-
-
- )}
-
-
- {modalOpen && (
- {
- if (e.target === e.currentTarget) closeModal();
- }}
- >
-
-
-
-
- {editingIdx === null ? 'ADD PERSONA' : 'EDIT PERSONA'}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {draftError && (
-
- )}
-
-
-
-
-
-
- )}
+ )}
+
+
+
+ {visible.length === 0 ? (
+
+
+
+ {personas.length === 0
+ ? (isTopology
+ ? 'NO PERSONAS ON THIS TOPOLOGY — ADD AT LEAST 2 SO THE EMAILGEN SCHEDULER CAN PICK SENDER+RECIPIENT'
+ : 'NO PERSONAS CONFIGURED — ADD AT LEAST 2 TO START THE EMAILGEN WORKER')
+ : 'NO PERSONAS MATCH CURRENT FILTER'}
+
+ {personas.length === 0 && (
+
+ )}
+
+ ) : (
+ visible.map((p, idx) => {
+ const realIdx = personas.indexOf(p);
+ return (
+
openEdit(realIdx)}
+ onRemove={() => removePersona(realIdx)}
+ />
+ );
+ })
+ )}
+
+
+
);
};
export default PersonaGeneration;
+
+// Topology-bound variant. Mounted at /topologies/:id/personas; the
+// route component reads the id off the URL so callers can `
`
+// straight in from the topology list / MazeNET toolbar.
+export const TopologyPersonaGeneration: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ if (!id) return null;
+ return
;
+};
diff --git a/decnet_web/src/components/TopologyList/TopologyList.tsx b/decnet_web/src/components/TopologyList/TopologyList.tsx
index 175ad74b..23ae45c9 100644
--- a/decnet_web/src/components/TopologyList/TopologyList.tsx
+++ b/decnet_web/src/components/TopologyList/TopologyList.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Network, Plus, Power, Trash2, UploadCloud, RefreshCw, Skull, Server, Cpu } from '../../icons';
+import { Network, Plus, Power, Trash2, UploadCloud, RefreshCw, Skull, Server, Cpu, Mail } from '../../icons';
import api from '../../utils/api';
import { useSwarmHosts } from '../../hooks/useSwarmHosts';
import { clearLayout } from '../MazeNET/useMazeLayoutStore';
@@ -223,6 +223,14 @@ const TopologyList: React.FC = () => {
{r.id}
e.stopPropagation()}>
+
{r.status === 'pending' && (