Collapse teacher introductions after three operation replies
This commit is contained in:
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { teacherApi, teacherTopicsPath, type TeacherCatalogState } from '@/lib/coding-teacher';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { AppError } from '@/lib/error-model';
|
||||
import { useTeacherIntroductionRounds } from './use-teacher-introduction-rounds';
|
||||
import {
|
||||
TEACHER_CHECK_IN_INTERVAL_MS,
|
||||
type TeacherCheckInInput,
|
||||
@@ -346,6 +347,9 @@ export function useTeacherCompanion(options: Options) {
|
||||
// Opening one teacher consumes only that teacher's introduction, never their unread replies.
|
||||
if (options.teacherOpen && selectedAgent?.teacher_id) dismissWelcome(selectedAgent.teacher_id);
|
||||
}, [options.teacherOpen, selectedAgent?.teacher_id, dismissWelcome]);
|
||||
useTeacherIntroductionRounds(introductionKey, options.projectId,
|
||||
visibleState.agents.filter(agent => !introducedIds.includes(agent.teacher_id)).map(agent => agent.teacher_id),
|
||||
dismissIntroductions);
|
||||
const welcomes = options.projectId && !options.teacherOpen && !visibleState.loading
|
||||
? visibleState.agents.flatMap((agent, index) => introducedIds.includes(agent.teacher_id) ? [] : [{
|
||||
agentId: agent.teacher_id,
|
||||
|
||||
67
src/pages/Chat/use-teacher-introduction-rounds.ts
Normal file
67
src/pages/Chat/use-teacher-introduction-rounds.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useEffect } from 'react';
|
||||
import { codingConversationStore, type CodingConversationStoreState } from '@/stores/coding-conversations';
|
||||
|
||||
interface IntroductionProgress {
|
||||
since: number;
|
||||
runs: string[];
|
||||
}
|
||||
|
||||
function readProgress(key: string): Record<string, IntroductionProgress> {
|
||||
try {
|
||||
const value: unknown = JSON.parse(localStorage.getItem(key) ?? '{}');
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
return Object.fromEntries(Object.entries(value).filter(([, progress]) => (
|
||||
progress && typeof progress.since === 'number' && Number.isFinite(progress.since)
|
||||
&& Array.isArray(progress.runs) && progress.runs.length <= 3
|
||||
&& progress.runs.every((run: unknown) => typeof run === 'string')
|
||||
)));
|
||||
} catch { return {}; }
|
||||
}
|
||||
|
||||
/** Presentation receipts only: observe Main's settled runs, never dispatch or edit a conversation. */
|
||||
export function useTeacherIntroductionRounds(
|
||||
introductionKey: string | null,
|
||||
projectId: string | null,
|
||||
pendingAgentIds: string[],
|
||||
dismiss: (ids: string[]) => void,
|
||||
) {
|
||||
const agentIdsKey = JSON.stringify(pendingAgentIds);
|
||||
useEffect(() => {
|
||||
const agentIds: string[] = JSON.parse(agentIdsKey);
|
||||
if (!introductionKey || !projectId || !agentIds.length) return;
|
||||
const key = `${introductionKey}:rounds`;
|
||||
const saved = readProgress(key);
|
||||
const progress = Object.fromEntries(agentIds.map(id => [id, saved[id] ?? { since: Date.now(), runs: [] }]));
|
||||
const persist = () => {
|
||||
try { localStorage.setItem(key, JSON.stringify(progress)); } catch { /* Keep counting in memory. */ }
|
||||
};
|
||||
persist();
|
||||
const observe = (state: CodingConversationStoreState) => {
|
||||
let changed = false;
|
||||
for (const entry of Object.values(state.entriesByConversationId)) {
|
||||
const snapshot = entry.reducer.snapshot;
|
||||
if (entry.loadState !== 'live' || entry.reducer.invalidation || snapshot?.conversation.projectId !== projectId) continue;
|
||||
const run = snapshot.run;
|
||||
// A successful top-level prompt is one round, regardless of its tool calls or retries.
|
||||
if (run.status !== 'idle' || run.terminalReason !== 'completed' || run.mode !== 'prompt'
|
||||
|| !run.runId || run.startedAt === undefined) continue;
|
||||
const runKey = JSON.stringify([snapshot.conversation.id, run.runId]);
|
||||
for (const item of Object.values(progress)) {
|
||||
// Persist the starting time so loading old history cannot consume introductions.
|
||||
if (item.runs.length >= 3 || run.startedAt < item.since || item.runs.includes(runKey)) continue;
|
||||
item.runs.push(runKey);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) persist();
|
||||
const finished = agentIds.filter(id => progress[id].runs.length >= 3);
|
||||
if (finished.length) dismiss(finished);
|
||||
};
|
||||
// Include a completion delivered during a page reload, and observe all project conversations
|
||||
// so switching the selected operation chat cannot lose or double-count a completion.
|
||||
observe(codingConversationStore.getState());
|
||||
return codingConversationStore.subscribe((state, previous) => {
|
||||
if (state.entriesByConversationId !== previous.entriesByConversationId) observe(state);
|
||||
});
|
||||
}, [introductionKey, projectId, agentIdsKey, dismiss]);
|
||||
}
|
||||
Reference in New Issue
Block a user