138 lines
4.0 KiB
TypeScript
138 lines
4.0 KiB
TypeScript
/**
|
|
* 圆桌规划共享常量与小工具函数。
|
|
*
|
|
* 这里只放与具体 React 状态无关的纯数据 / 纯函数。复杂的 dialogue 管理、
|
|
* 编排循环、API 状态等都在 `hooks/` 与 `components/` 里维护。
|
|
*/
|
|
import type { IntentSummary } from "@/roundtable-planning/api/intent";
|
|
|
|
/** 指挥员(人类)所在的固定 seat id。Agent 席位从 2 开始排。 */
|
|
export const COMMANDER_SEAT_ID = 1;
|
|
|
|
/** 编排循环最大轮次。超出后 toast 强停以防死循环。 */
|
|
export const MAX_CYCLES = 8;
|
|
|
|
/** Step 2 角色面板的一行。 */
|
|
export type RoleEntry = {
|
|
id: number;
|
|
name: string;
|
|
agentId?: string;
|
|
type: "human" | "agent";
|
|
status: string;
|
|
};
|
|
|
|
/** 用户在推荐弹窗选定的一个研讨席位。 */
|
|
export type SelectedAgent = { agent_id: string; name: string };
|
|
|
|
/** 研讨席位的头像配色轮换池。 */
|
|
export const FALLBACK_AVATAR_TYPES = [
|
|
"blue",
|
|
"purple",
|
|
"emerald",
|
|
"amber",
|
|
"pink",
|
|
] as const;
|
|
|
|
export function getAvatarTypeFor(_agentId: string, index: number): string {
|
|
return FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length];
|
|
}
|
|
|
|
export function getDisplayNameFor(agent: SelectedAgent): string {
|
|
return agent.name;
|
|
}
|
|
|
|
/**
|
|
* 右侧角色面板的模板列表。指挥员固定 id=1,agents 按用户在推荐弹窗里的
|
|
* 选择顺序排 id=2..N+1,与可视化圆桌 + agentNameToRole 保持一致。
|
|
*/
|
|
export function buildRoleTemplates(
|
|
agents: SelectedAgent[],
|
|
): ReadonlyArray<Omit<RoleEntry, "status">> {
|
|
const out: Omit<RoleEntry, "status">[] = [
|
|
{ id: COMMANDER_SEAT_ID, name: "指挥员 / 主持人", type: "human" },
|
|
];
|
|
agents.forEach((a, i) => {
|
|
out.push({
|
|
id: i + 2,
|
|
name: getDisplayNameFor(a),
|
|
agentId: a.agent_id,
|
|
type: "agent",
|
|
});
|
|
});
|
|
return out;
|
|
}
|
|
|
|
export function makeRolesFromTemplates(
|
|
templates: ReadonlyArray<Omit<RoleEntry, "status">>,
|
|
defaultStatus: string,
|
|
overrides: Record<number, string> = {},
|
|
): RoleEntry[] {
|
|
return templates.map((t) => ({
|
|
...t,
|
|
status: overrides[t.id] ?? defaultStatus,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* `agent_name`(小写)→ bubble meta 映射。流式输出时根据它反查
|
|
* speakerId / 头像 / 中文名,保证可视化圆桌的高亮和气泡渲染对齐。
|
|
*/
|
|
export function buildAgentNameToRole(
|
|
agents: SelectedAgent[],
|
|
): Record<string, { speakerId: number; avatarType: string; displayName: string }> {
|
|
const out: Record<
|
|
string,
|
|
{ speakerId: number; avatarType: string; displayName: string }
|
|
> = {};
|
|
agents.forEach((a, i) => {
|
|
out[a.agent_id] = {
|
|
speakerId: i + 2,
|
|
avatarType: getAvatarTypeFor(a.agent_id, i),
|
|
displayName: getDisplayNameFor(a),
|
|
};
|
|
});
|
|
return out;
|
|
}
|
|
|
|
export function nowTime(): string {
|
|
return new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
}
|
|
|
|
export function newCoordinatorName(): string {
|
|
return `roundtable-coordinator-${Date.now().toString(36)}`;
|
|
}
|
|
|
|
/**
|
|
* 后端 intent agent 输出的 `[INTENT_READY]\n\`\`\`json {...}\`\`\`` 块。
|
|
* 前端用同样形状的正则把摘要从对话气泡里抽出来,渲染成绿色摘要卡片。
|
|
*/
|
|
export const INTENT_READY_RE = /\[INTENT_READY\]\s*```json\s*(\{[\s\S]*?\})\s*```/;
|
|
|
|
/**
|
|
* 把含 [INTENT_READY] 标记的气泡正文拆成前 / 摘要 / 后三段。
|
|
* 摘要 JSON 格式异常时整体保留在 `before`,用户仍能看到原文。
|
|
*/
|
|
export function splitIntentReadyBlock(text: string): {
|
|
before: string;
|
|
summary: IntentSummary | null;
|
|
after: string;
|
|
} {
|
|
if (!text) return { before: text, summary: null, after: "" };
|
|
const m = INTENT_READY_RE.exec(text);
|
|
if (!m) return { before: text, summary: null, after: "" };
|
|
let summary: IntentSummary | null = null;
|
|
try {
|
|
const parsed = JSON.parse(m[1]);
|
|
if (parsed && typeof parsed === "object" && typeof parsed.objective === "string") {
|
|
summary = parsed as IntentSummary;
|
|
}
|
|
} catch {
|
|
return { before: text, summary: null, after: "" };
|
|
}
|
|
return {
|
|
before: text.slice(0, m.index).trimEnd(),
|
|
summary,
|
|
after: text.slice(m.index + m[0].length).trimStart(),
|
|
};
|
|
}
|