Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

300 lines
9.8 KiB
TypeScript

import type {
PBLAgent,
PBLChatMessage as LegacyPBLChatMessage,
PBLIssue,
PBLProjectConfig,
} from '@/lib/pbl/types';
import type { PBLChatMessage, PBLMilestoneStatus, PBLProjectV2, PBLRole } from './types';
const COMPAT_INSTRUCTOR_ROLE_ID = 'role-compat-instructor';
const COMPAT_LEARNER_AGENT_NAME = 'Learner';
export function projectV2ToLegacyProjectConfig(project: PBLProjectV2): PBLProjectConfig {
const instructor = project.roles.find((role) => role.type === 'instructor') ?? project.roles[0];
const instructorName = instructor?.name || 'Instructor';
const issues = project.milestones.map((milestone, index): PBLIssue => {
const activeTask =
milestone.microtasks.find((task) => task.status === 'in_progress') ??
milestone.microtasks.find((task) => task.status === 'todo') ??
milestone.microtasks[0];
return {
id: legacyIssueId(milestone.id),
title: milestone.title || `Stage ${index + 1}`,
description: milestone.description || activeTask?.description || '',
person_in_charge: COMPAT_LEARNER_AGENT_NAME,
participants: [instructorName],
notes: (milestone.documents ?? [])
.map((document) => document.content)
.filter(Boolean)
.join('\n\n'),
parent_issue: null,
index,
is_done: milestone.status === 'completed',
is_active: milestone.status === 'active',
generated_questions: [milestone.briefing, ...(activeTask?.hints ?? [])]
.filter(Boolean)
.join('\n'),
question_agent_name: instructorName,
judge_agent_name: instructorName,
};
});
return {
projectInfo: {
title: project.title,
description: project.description,
},
agents: [
compatInstructorAgent(instructorName),
{
name: COMPAT_LEARNER_AGENT_NAME,
actor_role: 'Learner',
role_division: 'development',
system_prompt: '',
default_mode: 'idle',
delay_time: 0,
env: {},
is_user_role: true,
is_active: true,
is_system_agent: false,
},
],
issueboard: {
agent_ids: [instructorName],
issues,
current_issue_id: issues.find((issue) => issue.is_active)?.id ?? issues[0]?.id ?? null,
},
chat: {
messages: [],
},
selectedRole: hasStartedProject(project) ? COMPAT_LEARNER_AGENT_NAME : null,
};
}
export function upgradeLegacyPBLConfigToProjectV2(config: PBLProjectConfig): PBLProjectV2 {
const now = new Date().toISOString();
const language = detectLegacyLanguage(config);
const instructorName = inferInstructorName(config);
const instructorRole: PBLRole = {
id: COMPAT_INSTRUCTOR_ROLE_ID,
type: 'instructor',
name: instructorName,
description: 'Guides the learner through the upgraded legacy PBL project.',
};
const orderedIssues = config.issueboard.issues.slice().sort((a, b) => a.index - b.index);
const activeIssueId = inferActiveIssueId(config);
const allDone = orderedIssues.length > 0 && orderedIssues.every((issue) => issue.is_done);
const hasLegacyRuntime =
!!config.selectedRole ||
config.chat.messages.length > 0 ||
orderedIssues.some((issue) => issue.is_done);
return {
uiPhase: allDone ? 'completed' : hasLegacyRuntime ? 'workspace' : 'hero',
title: config.projectInfo.title || 'Project',
description: config.projectInfo.description || '',
proficiency: '',
language,
tags: [],
status: allDone ? 'completed' : 'active',
roles: [instructorRole],
milestones: orderedIssues.map((issue, index) => {
const status = legacyIssueStatus(issue, index, orderedIssues, activeIssueId);
return {
id: legacyMilestoneId(issue.id),
title: issue.title || `Task ${index + 1}`,
description: issue.description || issue.notes || undefined,
status,
order: index,
microtasks: [
{
id: legacyMicrotaskId(issue.id),
title: issue.title || `Task ${index + 1}`,
description: legacyMicrotaskDescription(issue),
status:
status === 'completed' ? 'completed' : status === 'active' ? 'in_progress' : 'todo',
assignee: 'user',
hints: issue.generated_questions ? [issue.generated_questions] : [],
order: 0,
},
],
documents: issue.notes
? [
{
id: `doc_${issue.id}`,
title: 'Legacy issue notes',
content: issue.notes,
docType: 'reference',
},
]
: [],
briefing: issue.generated_questions || issue.description || issue.title,
completionCriteria: legacyCompletionCriteria(language),
debrief: legacyDebrief(language),
};
}),
submissions: [],
evaluations: [],
threads: [
{
agentId: instructorRole.id,
messages: config.chat.messages.map((message) => legacyChatMessage(message, config)),
},
],
engagementEvents: [],
createdAt: now,
updatedAt: now,
};
}
export function isEmptyLegacyPBLConfig(config: PBLProjectConfig): boolean {
return (
config.projectInfo.title === '' &&
config.projectInfo.description === '' &&
config.agents.length === 0 &&
config.issueboard.issues.length === 0 &&
config.chat.messages.length === 0
);
}
function compatInstructorAgent(name: string): PBLAgent {
return {
name,
actor_role: 'Instructor',
role_division: 'management',
system_prompt: '',
default_mode: 'idle',
delay_time: 0,
env: {},
is_user_role: false,
is_active: true,
is_system_agent: true,
};
}
function hasStartedProject(project: PBLProjectV2): boolean {
return (
project.uiPhase !== 'hero' ||
project.threads.some((thread) => thread.messages.length > 0) ||
project.submissions.length > 0 ||
project.evaluations.length > 0 ||
project.engagementEvents.length > 0 ||
project.milestones.some(
(milestone) =>
milestone.status === 'completed' ||
milestone.microtasks.some(
(microtask) => microtask.status === 'completed' || microtask.status === 'skipped',
),
)
);
}
function legacyIssueId(milestoneId: string): string {
return `compat_issue_${milestoneId}`;
}
function legacyMilestoneId(issueId: string): string {
return `legacy_ms_${issueId}`;
}
function legacyMicrotaskId(issueId: string): string {
return `legacy_mt_${issueId}`;
}
function legacyIssueStatus(
issue: PBLProjectConfig['issueboard']['issues'][number],
index: number,
issues: PBLProjectConfig['issueboard']['issues'],
activeIssueId: string | null,
): PBLMilestoneStatus {
if (issue.is_done) return 'completed';
if (issue.id === activeIssueId) return 'active';
const firstIncomplete = issues.find((candidate) => !candidate.is_done);
if (!activeIssueId && (firstIncomplete ? issue.id === firstIncomplete.id : index === 0)) {
return 'active';
}
return 'locked';
}
function legacyMicrotaskDescription(
issue: PBLProjectConfig['issueboard']['issues'][number],
): string | undefined {
return [issue.description, issue.notes ? `Notes: ${issue.notes}` : ''].filter(Boolean).join('\n');
}
function legacyChatMessage(
message: LegacyPBLChatMessage,
config: PBLProjectConfig,
): PBLChatMessage {
const isUser = isLegacyUserMessage(message, config);
return {
id: message.id,
agentId: isUser ? undefined : COMPAT_INSTRUCTOR_ROLE_ID,
roleType: isUser ? 'user' : 'instructor',
content: message.message,
ts: new Date(message.timestamp || Date.now()).toISOString(),
};
}
function isLegacyUserMessage(message: LegacyPBLChatMessage, config: PBLProjectConfig): boolean {
const selectedRole =
config.selectedRole?.trim() || config.agents.find((agent) => agent.is_user_role)?.name?.trim();
if (selectedRole) return message.agent_name === selectedRole;
const agentNames = new Set(config.agents.map((agent) => agent.name));
return !agentNames.has(message.agent_name);
}
function inferInstructorName(config: PBLProjectConfig): string {
const activeIssue =
config.issueboard.issues.find((issue) => issue.is_active && !issue.is_done) ??
config.issueboard.issues.find(
(issue) => issue.id === config.issueboard.current_issue_id && !issue.is_done,
);
if (activeIssue?.question_agent_name) return activeIssue.question_agent_name;
const questionAgent = config.agents.find((agent) =>
agent.name.toLowerCase().includes('question'),
);
return questionAgent?.name || 'Instructor';
}
function inferActiveIssueId(config: PBLProjectConfig): string | null {
const issues = config.issueboard.issues;
return (
issues.find((issue) => issue.is_active && !issue.is_done)?.id ??
issues.find((issue) => issue.id === config.issueboard.current_issue_id && !issue.is_done)?.id ??
null
);
}
function detectLegacyLanguage(config: PBLProjectConfig): string {
const sample = [
config.projectInfo.title,
config.projectInfo.description,
...config.issueboard.issues.flatMap((issue) => [
issue.title,
issue.description,
issue.notes,
issue.generated_questions,
]),
].join('\n');
if (/[\u3040-\u30ff]/.test(sample)) return 'ja-JP';
if (/[\uac00-\ud7af]/.test(sample)) return 'ko-KR';
if (/[\u0600-\u06ff]/.test(sample)) return 'ar-SA';
if (/[\u0400-\u04ff]/.test(sample)) return 'ru-RU';
if (/[\u3400-\u9fff]/.test(sample)) return 'zh-CN';
return 'en-US';
}
function legacyCompletionCriteria(language: string): string {
return language.startsWith('zh')
? '学习者完成该任务,并能解释自己的解决思路。'
: 'The learner completes this task and can explain their reasoning.';
}
function legacyDebrief(language: string): string {
return language.startsWith('zh')
? '总结本任务的关键收获,并准备进入下一步。'
: 'Summarize the key takeaways from this task and prepare for the next step.';
}