Integrate project classroom with teacher and friend consultations

This commit is contained in:
鲨鱼辣椒
2026-09-22 15:50:18 +08:00
parent e5d271bc45
commit 68e676cb62
28 changed files with 957 additions and 314 deletions

View File

@@ -447,7 +447,8 @@ async function installCodingFirstChatHost(
});
const teacherDefinition = {schema_version:1,teacher_id:'coding-teacher',name:'代码老师',description:'',avatar_id:'avatar-01',welcome_message:'一起理解代码',suggested_questions:['解释当前代码'],system_prompt:'教学',skills:[],model:{model_id:'model-a',reasoning_choice:{mode:'default'}},limits:{max_input_tokens:8000,max_output_tokens:1500}};
let teacherTopic: Record<string, unknown> | null = null;
const friendDefinition = {...teacherDefinition,teacher_id:'coding-friend',name:'小麦',welcome_message:'一起看看你的作品',suggested_questions:['想听听你对作品的第一印象']};
const consultationTopics: Record<string, Record<string, unknown>> = {};
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (
_event,
@@ -521,16 +522,23 @@ async function installCodingFirstChatHost(
}
if (path === '/api/coding/teacher/config') return respond({enabled:true,revision:1,published_version:1,definition:teacherDefinition});
if (path.endsWith('/teacher-topics') && method === 'GET') return respond({items:teacherTopic?[{id:teacherTopic.id,title:'老师话题'}]:[],lastSelectedTopicId:teacherTopic?.id??null});
if (path.endsWith('/teacher-topics') && method === 'POST') {
teacherTopic={schemaVersion:1,revision:0,id:'teacher-topic',accountId:'e2e',projectId:project.id,sourceConversationId:conversation.id,definition:teacherDefinition,version:1,createdAt:now,updatedAt:now,requests:[]};
return respond(teacherTopic,201);
if (path === '/api/coding/friend/config') return respond({enabled:true,revision:1,published_version:1,definition:friendDefinition});
const consultationRole = path.match(/\/(teacher|friend)-topics(?:\/|$)/)?.[1];
if (consultationRole) {
const topicBase = `/${consultationRole}-topics`;
const topicId = `${consultationRole}-topic`;
const currentTopic = consultationTopics[consultationRole];
if (path.endsWith(topicBase) && method === 'GET') return respond({items:currentTopic?[{id:currentTopic.id,title:consultationRole==='friend'?'朋友话题':'老师话题'}]:[],lastSelectedTopicId:currentTopic?.id??null});
if (path.endsWith(topicBase) && method === 'POST') {
consultationTopics[consultationRole]={schemaVersion:1,revision:0,id:topicId,accountId:'e2e',projectId:project.id,role:consultationRole,sourceConversationId:conversation.id,definition:consultationRole==='friend'?friendDefinition:teacherDefinition,version:1,createdAt:now,updatedAt:now,requests:[]};
return respond(consultationTopics[consultationRole],201);
}
if (path.endsWith(`${topicBase}/${topicId}/messages`) && method === 'POST') {
consultationTopics[consultationRole]={...currentTopic,revision:2,requests:[{id:body!.requestId,text:body!.text,references:body!.references??[],createdAt:now,sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,status:'completed',response:consultationRole==='friend'?'听你的描述,我有点好奇该从哪里开始玩。':'先理解状态如何随点击变化,再修改代码。'}]};
return respond(consultationTopics[consultationRole],202);
}
if (path.endsWith(`${topicBase}/${topicId}`)) return respond(currentTopic);
}
if (path.endsWith('/teacher-topics/teacher-topic/messages') && method === 'POST') {
teacherTopic={...teacherTopic,revision:2,requests:[{id:body!.requestId,text:body!.text,references:body!.references??[],createdAt:now,sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,status:'completed',response:'先理解状态如何随点击变化,再修改代码。'}]};
return respond(teacherTopic,202);
}
if (path.endsWith('/teacher-topics/teacher-topic')) return respond(teacherTopic);
if (path === '/api/coding/projects') {
return respond({ projects: [project], activeProjectId: project.id });
}
@@ -1708,7 +1716,7 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
});
test('project teacher side chat returns advice to the coding draft without submitting it', async ({launchElectronApp}) => {
test('project consultations preserve student drafts and switch between work and chat without submitting advice', async ({launchElectronApp}) => {
const electronApp=await launchElectronApp({skipSetup:true});let page=await getStableWindow(electronApp);
const connection=await page.evaluate(async()=>({token:await window.electron.ipcRenderer.invoke('hostapi:token') as string,baseUrl:await window.electron.ipcRenderer.invoke('hostapi:base-url') as string}));
await installCodingFirstChatHost(electronApp,connection,true);await settleSnapshot(electronApp);await disableCodingEventSource(page);
@@ -1717,22 +1725,48 @@ test('project teacher side chat returns advice to the coding draft without submi
await page.getByTestId('ai-module-option-programming').click();await page.evaluate(()=>{window.location.hash='/chat';});
await expect(page.getByTestId('project-conversations')).toBeVisible();
await expect(page.getByTestId('coding-conversation-sidebar')).toHaveCount(0);
await expect(page.getByRole('tab',{name:'操作对话',exact:true})).toHaveAttribute('aria-selected','true');
await expect(page.getByRole('button',{name:/项目共识|记一下|记入共识/})).toHaveCount(0);
const composer=page.getByTestId('coding-message-composer').getByRole('textbox');
await composer.fill('保留我的草稿');
await page.getByRole('button',{name:'问老师',exact:true}).click();
await page.getByRole('tab',{name:'作品',exact:true}).click();
await expect(page.getByRole('tabpanel',{name:'作品',exact:true})).toBeVisible();
await expect(composer).toBeHidden();
await page.getByRole('tab',{name:'操作对话',exact:true}).click();
await expect(composer).toHaveValue('保留我的草稿');
await page.getByRole('button',{name:'老师',exact:true}).click();
const teacher=page.getByTestId('teacher-chat-panel');
await expect(teacher.getByText('一起理解代码')).toBeVisible();
await teacher.getByRole('button',{name:'解释当前代码',exact:true}).click();
await expect(teacher.getByRole('textbox',{name:'向老师提问'})).toHaveValue('解释当前代码');
expect((await readState(electronApp)).captured.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(0);
await teacher.getByRole('textbox',{name:'向老师提问'}).fill('帮我理解当前代码');
await teacher.getByRole('button',{name:'提问',exact:true}).click();
await expect(teacher.getByText('先理解状态如何随点击变化,再修改代码。')).toBeVisible();
await teacher.getByRole('button',{name:'带回主会话草稿'}).click();
await expect(composer).toHaveValue('保留我的草稿\n\n先理解状态如何随点击变化,再修改代码。');
await expect(teacher.getByRole('button',{name:'带回主会话草稿'})).toHaveCount(0);
await teacher.getByRole('button',{name:'我去试一试'}).click();
await expect(composer).toHaveValue('保留我的草稿');
await teacher.getByRole('textbox',{name:'向老师提问'}).fill('还没说完的困惑');
await page.getByRole('button',{name:'朋友',exact:true}).click();
await expect(teacher).toHaveAttribute('data-role','friend');
await expect(teacher.getByText('一起看看你的作品')).toBeVisible();
await expect(teacher.getByRole('textbox',{name:'向朋友提问'})).toHaveValue('');
await expect(teacher.getByText('先理解状态如何随点击变化,再修改代码。')).toHaveCount(0);
await teacher.getByRole('textbox',{name:'向朋友提问'}).fill('你觉得哪里有意思?');
await teacher.getByRole('button',{name:'关闭朋友'}).click();
await expect(teacher).toHaveCount(0);
await page.getByRole('button',{name:'朋友',exact:true}).click();
await expect(teacher.getByRole('textbox',{name:'向朋友提问'})).toHaveValue('你觉得哪里有意思?');
await page.getByRole('button',{name:'老师',exact:true}).click();
await expect(teacher.getByRole('textbox',{name:'向老师提问'})).toHaveValue('还没说完的困惑');
await expect(teacher.getByText('先理解状态如何随点击变化,再修改代码。')).toBeVisible();
const requests=(await readState(electronApp)).captured;
expect(requests.filter(item=>item.path.endsWith('/prompt')&&item.method==='POST')).toHaveLength(0);
expect(requests.filter(item=>item.path.endsWith('/messages')&&item.method==='POST')).toHaveLength(1);
await page.screenshot({path:test.info().outputPath('project-teacher-side-chat.png')});
await teacher.getByRole('button',{name:'关闭老师'}).click();await expect(teacher).toHaveCount(0);
await page.getByRole('button',{name:'问老师',exact:true}).click();
await page.getByRole('button',{name:'老师',exact:true}).click();
await expect(page.getByText('先理解状态如何随点击变化,再修改代码。',{exact:true})).toBeVisible();
await expect(teacher.getByRole('textbox',{name:'向老师提问'})).toHaveValue('还没说完的困惑');
} finally {await releaseSnapshot(electronApp);}
});