chore: establish learning module baseline
This commit is contained in:
commit
2d04197f3f
2409 files changed
+533050
No files matched your search
@@ -0,0 +1,170 @@
|
||||
import { produce } from 'immer';
|
||||
import type { GeneratedAgentConfig } from '@/lib/types/stage';
|
||||
import { AGENT_DEFAULT_AVATARS, AGENT_COLOR_PALETTE } from '@/lib/constants/agent-defaults';
|
||||
|
||||
// Cap undo history so long editing sessions don't grow memory unbounded.
|
||||
export const MAX_HISTORY = 50;
|
||||
|
||||
export type AgentRoster = GeneratedAgentConfig[];
|
||||
|
||||
export interface AgentConfigPatch {
|
||||
name?: string;
|
||||
role?: string;
|
||||
persona?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
export type AgentEditOperation =
|
||||
| { type: 'agent.add'; agent: GeneratedAgentConfig }
|
||||
| { type: 'agent.update'; id: string; patch: AgentConfigPatch }
|
||||
| { type: 'agent.delete'; id: string }
|
||||
| { type: 'agent.reorder'; id: string; index: number };
|
||||
|
||||
export interface AgentRosterHistory {
|
||||
past: AgentRoster[];
|
||||
present: AgentRoster;
|
||||
future: AgentRoster[];
|
||||
}
|
||||
|
||||
/** Maps a role string to its priority number. teacher=10, assistant=7, else=5. */
|
||||
export function priorityForRole(role: string): number {
|
||||
if (role === 'teacher') return 10;
|
||||
if (role === 'assistant') return 7;
|
||||
return 5;
|
||||
}
|
||||
|
||||
/** Returns a sensible display name for a role. */
|
||||
function nameForRole(role: string): string {
|
||||
if (role === 'teacher') return 'Teacher';
|
||||
if (role === 'assistant') return 'Assistant';
|
||||
return 'Student';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new GeneratedAgentConfig with defaults derived from role + index.
|
||||
* avatar cycles through AGENT_DEFAULT_AVATARS, color through AGENT_COLOR_PALETTE.
|
||||
*/
|
||||
export function createAgentConfig(role: string, index: number, id: string): GeneratedAgentConfig {
|
||||
return {
|
||||
id,
|
||||
name: nameForRole(role),
|
||||
role,
|
||||
persona: '',
|
||||
avatar: AGENT_DEFAULT_AVATARS[index % AGENT_DEFAULT_AVATARS.length],
|
||||
color: AGENT_COLOR_PALETTE[index % AGENT_COLOR_PALETTE.length],
|
||||
priority: priorityForRole(role),
|
||||
};
|
||||
}
|
||||
|
||||
/** Count how many agents in the roster have role 'teacher'. */
|
||||
export function teacherCount(roster: AgentRoster): number {
|
||||
return roster.filter((a) => a.role === 'teacher').length;
|
||||
}
|
||||
|
||||
export function applyAgentEditOperation(roster: AgentRoster, op: AgentEditOperation): AgentRoster;
|
||||
export function applyAgentEditOperation(
|
||||
history: AgentRosterHistory,
|
||||
op: AgentEditOperation,
|
||||
): AgentRosterHistory;
|
||||
export function applyAgentEditOperation(
|
||||
target: AgentRoster | AgentRosterHistory,
|
||||
op: AgentEditOperation,
|
||||
): AgentRoster | AgentRosterHistory {
|
||||
if (isAgentRosterHistory(target)) {
|
||||
const next = applyOperationToRoster(target.present, op);
|
||||
// If the roster didn't change (e.g. update against missing id), skip history push.
|
||||
if (next === target.present) return target;
|
||||
|
||||
return {
|
||||
past: capHistory([...target.past, target.present]),
|
||||
present: next,
|
||||
future: [],
|
||||
};
|
||||
}
|
||||
|
||||
return applyOperationToRoster(target, op);
|
||||
}
|
||||
|
||||
export function undoAgentEditOperation(h: AgentRosterHistory): AgentRosterHistory {
|
||||
if (h.past.length === 0) return h;
|
||||
|
||||
const previous = h.past[h.past.length - 1];
|
||||
return {
|
||||
past: h.past.slice(0, -1),
|
||||
present: previous,
|
||||
future: [h.present, ...h.future],
|
||||
};
|
||||
}
|
||||
|
||||
export function redoAgentEditOperation(h: AgentRosterHistory): AgentRosterHistory {
|
||||
if (h.future.length === 0) return h;
|
||||
|
||||
const next = h.future[0];
|
||||
return {
|
||||
past: capHistory([...h.past, h.present]),
|
||||
present: next,
|
||||
future: h.future.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
function applyOperationToRoster(roster: AgentRoster, op: AgentEditOperation): AgentRoster {
|
||||
return produce(roster, (draft) => {
|
||||
switch (op.type) {
|
||||
case 'agent.add': {
|
||||
draft.push({ ...op.agent });
|
||||
return;
|
||||
}
|
||||
case 'agent.update': {
|
||||
const agent = draft.find((a) => a.id === op.id);
|
||||
if (!agent) return;
|
||||
|
||||
// Guard: role change that would drop teacher count to 0.
|
||||
if (op.patch.role !== undefined && op.patch.role !== agent.role) {
|
||||
if (agent.role === 'teacher' && teacherCount(roster) <= 1) {
|
||||
throw new Error('LAST_TEACHER');
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(agent, op.patch);
|
||||
|
||||
// Recompute priority when role changes.
|
||||
if (op.patch.role !== undefined) {
|
||||
agent.priority = priorityForRole(agent.role);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'agent.delete': {
|
||||
const idx = draft.findIndex((a) => a.id === op.id);
|
||||
if (idx === -1) return;
|
||||
const agent = draft[idx];
|
||||
|
||||
// Guard: cannot delete the last teacher.
|
||||
if (agent.role === 'teacher' && teacherCount(roster) <= 1) {
|
||||
throw new Error('LAST_TEACHER');
|
||||
}
|
||||
|
||||
draft.splice(idx, 1);
|
||||
return;
|
||||
}
|
||||
case 'agent.reorder': {
|
||||
const currentIndex = draft.findIndex((a) => a.id === op.id);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const [agent] = draft.splice(currentIndex, 1);
|
||||
const nextIndex = Math.max(0, Math.min(op.index, draft.length));
|
||||
draft.splice(nextIndex, 0, agent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isAgentRosterHistory(
|
||||
target: AgentRoster | AgentRosterHistory,
|
||||
): target is AgentRosterHistory {
|
||||
return 'present' in target && 'past' in target && 'future' in target;
|
||||
}
|
||||
|
||||
function capHistory(past: AgentRoster[]): AgentRoster[] {
|
||||
return past.length > MAX_HISTORY ? past.slice(past.length - MAX_HISTORY) : past;
|
||||
}
|
||||
Reference in new issue
Block a user