fix(chat): déduplication des progress intermédiaires dans la timeline (#192)
Les progress « started » d'une même entité s'empilaient en plusieurs turns et n'étaient dédupliqués qu'a posteriori par scan linéaire du tableau de turns, d'où des doublons visibles et un affichage retardé (re-scan par chunk). appendProgress opérait directement sur turns[] (chaque « started » y était poussé puis retrouvé/remplacé par recherche séquentielle). Désormais: - ChatViewState (turns + entityProgressMap): la Map indexée par « entité\0bucket » (started/completed) est l'unique source de vérité pour les progress en cours; - les progress « started » ne deviennent des turns qu'au flush (completeChatViewProgress sur final/error), supprimant la duplication et rendant l'affichage immédiat; - « completed » merge depuis l'entrée running de la Map puis la retire, au lieu de scanner le tableau; - turnsIndicateBusy s'appuie sur la taille de la Map (état vivant) en plus du scan des turns. Tests: déduplication des progress par entité (started répétés → une seule entrée, flush au final), fusion « completed » depuis la Map, état busy reflété par la Map, et rendu non dupliqué sur l'enchaînement started/completed/final.
This commit is contained in:
@ -55,6 +55,7 @@ type ChatTurn =
|
||||
|
||||
interface CachedChatView {
|
||||
turns: ChatTurn[];
|
||||
entityProgressMap: Map<string, ReplyProgress>;
|
||||
chunkCount: number;
|
||||
}
|
||||
|
||||
@ -262,42 +263,57 @@ function progressDisplayKey(progress: ReplyProgress): string {
|
||||
return progressKey(progress);
|
||||
}
|
||||
|
||||
function appendProgress(turns: ChatTurn[], progress: ReplyProgress): ChatTurn[] {
|
||||
interface ChatViewState {
|
||||
turns: ChatTurn[];
|
||||
entityProgressMap: Map<string, ReplyProgress>;
|
||||
}
|
||||
|
||||
function emptyChatViewState(): ChatViewState {
|
||||
return { turns: [], entityProgressMap: new Map() };
|
||||
}
|
||||
|
||||
function appendProgress(state: ChatViewState, progress: ReplyProgress): ChatViewState {
|
||||
const entity = progressEntityKey(progress);
|
||||
if (entity && progress.stage === "completed") {
|
||||
let runningIndex = -1;
|
||||
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
||||
const turn = turns[index];
|
||||
if (
|
||||
turn.role === "progress" &&
|
||||
progressEntityKey(turn.progress) === entity &&
|
||||
(turn.progress.stage === "started" || turn.progress.stage === "delta")
|
||||
) {
|
||||
runningIndex = index;
|
||||
break;
|
||||
}
|
||||
if (entity) {
|
||||
const bucket = progressStageBucket(progress);
|
||||
const key = `${entity}\u0000${bucket}`;
|
||||
if (bucket === "started") {
|
||||
const nextMap = new Map(state.entityProgressMap);
|
||||
nextMap.set(key, progress);
|
||||
return { turns: state.turns, entityProgressMap: nextMap };
|
||||
}
|
||||
if (runningIndex >= 0) {
|
||||
const next = [...turns];
|
||||
const current = next[runningIndex];
|
||||
if (current.role === "progress") {
|
||||
next[runningIndex] = {
|
||||
role: "progress",
|
||||
progress: mergeCompletedProgress(current.progress, progress),
|
||||
};
|
||||
if (bucket === "completed") {
|
||||
const runningKey = `${entity}\u0000started`;
|
||||
const running = state.entityProgressMap.get(runningKey);
|
||||
const nextMap = new Map(state.entityProgressMap);
|
||||
nextMap.delete(runningKey);
|
||||
if (
|
||||
!running &&
|
||||
state.turns.some(
|
||||
(turn) => turn.role === "progress" && progressDisplayKey(turn.progress) === key,
|
||||
)
|
||||
) {
|
||||
return { turns: state.turns, entityProgressMap: nextMap };
|
||||
}
|
||||
return next;
|
||||
const completed = running ? mergeCompletedProgress(running, progress) : progress;
|
||||
return {
|
||||
turns: [...state.turns, { role: "progress", progress: completed }],
|
||||
entityProgressMap: nextMap,
|
||||
};
|
||||
}
|
||||
}
|
||||
const key = progressDisplayKey(progress);
|
||||
if (
|
||||
turns.some(
|
||||
state.turns.some(
|
||||
(turn) => turn.role === "progress" && progressDisplayKey(turn.progress) === key,
|
||||
)
|
||||
) {
|
||||
return turns;
|
||||
return state;
|
||||
}
|
||||
return [...turns, { role: "progress", progress }];
|
||||
return {
|
||||
turns: [...state.turns, { role: "progress", progress }],
|
||||
entityProgressMap: state.entityProgressMap,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeCompletedProgress(
|
||||
@ -352,13 +368,25 @@ function completeRunningProgress(turns: ChatTurn[]): ChatTurn[] {
|
||||
});
|
||||
}
|
||||
|
||||
function completeChatViewProgress(state: ChatViewState): ChatViewState {
|
||||
const activeTurns = Array.from(state.entityProgressMap.values()).map((progress) => ({
|
||||
role: "progress" as const,
|
||||
progress,
|
||||
}));
|
||||
return {
|
||||
turns: completeRunningProgress([...state.turns, ...activeTurns]),
|
||||
entityProgressMap: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
function isRecoverableNoResponseTurn(turn: ChatTurn | undefined): boolean {
|
||||
return turn?.role === "error" && turn.text === EMPTY_MODEL_RESPONSE_MESSAGE;
|
||||
}
|
||||
|
||||
function turnsIndicateBusy(turns: ChatTurn[]): boolean {
|
||||
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
||||
const turn = turns[index];
|
||||
function turnsIndicateBusy(state: ChatViewState): boolean {
|
||||
if (state.entityProgressMap.size > 0) return true;
|
||||
for (let index = state.turns.length - 1; index >= 0; index -= 1) {
|
||||
const turn = state.turns[index];
|
||||
if (turn.role === "final" || turn.role === "error") return false;
|
||||
if (turn.role === "agent" && turn.pending) return true;
|
||||
if (
|
||||
@ -490,9 +518,13 @@ function formatSlashCommandHelp(commands: SlashCommand[]): string {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function foldChunk(turns: ChatTurn[], raw: unknown): ChatTurn[] {
|
||||
function foldChunk(state: ChatViewState, raw: unknown): ChatViewState {
|
||||
const { turns } = state;
|
||||
if (!isReplyRecord(raw)) {
|
||||
return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
|
||||
return {
|
||||
turns: [...turns, { role: "unknown", text: unknownChunkLabel(raw) }],
|
||||
entityProgressMap: state.entityProgressMap,
|
||||
};
|
||||
}
|
||||
const last = turns[turns.length - 1];
|
||||
if (
|
||||
@ -501,44 +533,63 @@ function foldChunk(turns: ChatTurn[], raw: unknown): ChatTurn[] {
|
||||
raw.kind !== "UserPrompt"
|
||||
) {
|
||||
if (isRecoverableNoResponseTurn(last) && raw.kind !== "error") {
|
||||
return foldChunk(turns.slice(0, -1), raw);
|
||||
return foldChunk({ ...state, turns: turns.slice(0, -1) }, raw);
|
||||
}
|
||||
return turns;
|
||||
return state;
|
||||
}
|
||||
switch (raw.kind) {
|
||||
case "textDelta":
|
||||
return appendAgentDelta(turns, String(raw.text ?? ""));
|
||||
return {
|
||||
turns: appendAgentDelta(turns, String(raw.text ?? "")),
|
||||
entityProgressMap: state.entityProgressMap,
|
||||
};
|
||||
case "toolActivity":
|
||||
return [...turns, { role: "tool", label: String(raw.label ?? "Activité") }];
|
||||
return {
|
||||
turns: [...turns, { role: "tool", label: String(raw.label ?? "Activité") }],
|
||||
entityProgressMap: state.entityProgressMap,
|
||||
};
|
||||
case "progress": {
|
||||
const progress = normalizeProgress(raw.progress);
|
||||
if (!progress) return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
|
||||
return appendProgress(turns, progress);
|
||||
if (!progress) {
|
||||
return {
|
||||
turns: [...turns, { role: "unknown", text: unknownChunkLabel(raw) }],
|
||||
entityProgressMap: state.entityProgressMap,
|
||||
};
|
||||
}
|
||||
return appendProgress(state, progress);
|
||||
}
|
||||
case "final": {
|
||||
const content = String(raw.content ?? "");
|
||||
const next = completeRunningProgress(turns);
|
||||
const completedState = completeChatViewProgress(state);
|
||||
const next = [...completedState.turns];
|
||||
const last = next[next.length - 1];
|
||||
if (last?.role === "agent") next[next.length - 1] = { ...last, pending: false };
|
||||
next.push({ role: "final", text: content });
|
||||
return next;
|
||||
return { turns: next, entityProgressMap: completedState.entityProgressMap };
|
||||
}
|
||||
case "error": {
|
||||
const next = completeRunningProgress(turns);
|
||||
const completedState = completeChatViewProgress(state);
|
||||
const next = [...completedState.turns];
|
||||
const last = next[next.length - 1];
|
||||
if (last?.role === "agent") next[next.length - 1] = { ...last, pending: false };
|
||||
next.push({ role: "error", text: String(raw.message ?? "Erreur agent") });
|
||||
return next;
|
||||
return { turns: next, entityProgressMap: completedState.entityProgressMap };
|
||||
}
|
||||
case "userPrompt":
|
||||
case "UserPrompt":
|
||||
return appendUserPrompt(
|
||||
turns,
|
||||
String(raw.text ?? raw.prompt ?? ""),
|
||||
attachmentLabels(raw.attachments),
|
||||
);
|
||||
return {
|
||||
turns: appendUserPrompt(
|
||||
turns,
|
||||
String(raw.text ?? raw.prompt ?? ""),
|
||||
attachmentLabels(raw.attachments),
|
||||
),
|
||||
entityProgressMap: state.entityProgressMap,
|
||||
};
|
||||
default:
|
||||
return [...turns, { role: "unknown", text: unknownChunkLabel(raw) }];
|
||||
return {
|
||||
turns: [...turns, { role: "unknown", text: unknownChunkLabel(raw) }],
|
||||
entityProgressMap: state.entityProgressMap,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -560,6 +611,7 @@ export function CustomAgentChatView({
|
||||
const [turns, setTurns] = useState<ChatTurn[]>(
|
||||
() => (sessionId ? (chatSessionViewCache.get(sessionId)?.turns ?? []) : []),
|
||||
);
|
||||
const [entityProgressMap, setEntityProgressMap] = useState<Map<string, ReplyProgress>>(new Map());
|
||||
const [currentSession, setCurrentSession] = useState(sessionId);
|
||||
const [externalSessionId, setExternalSessionId] = useState(sessionId);
|
||||
const [draft, setDraft] = useState("");
|
||||
@ -575,6 +627,8 @@ export function CustomAgentChatView({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const composerRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const turnsRef = useRef(turns);
|
||||
const entityProgressMapRef = useRef(entityProgressMap);
|
||||
const sessionRef = useRef<string | null>(sessionId);
|
||||
sessionRef.current = currentSession;
|
||||
const scrollbackChunkCountRef = useRef(
|
||||
@ -597,6 +651,25 @@ export function CustomAgentChatView({
|
||||
const onConversationIdRef = useRef(onConversationId);
|
||||
onConversationIdRef.current = onConversationId;
|
||||
|
||||
const applyChatViewState = useCallback((next: ChatViewState) => {
|
||||
turnsRef.current = next.turns;
|
||||
entityProgressMapRef.current = next.entityProgressMap;
|
||||
setTurns(next.turns);
|
||||
setEntityProgressMap(next.entityProgressMap);
|
||||
}, []);
|
||||
|
||||
const updateChatViewState = useCallback(
|
||||
(updater: (prev: ChatViewState) => ChatViewState) => {
|
||||
applyChatViewState(
|
||||
updater({
|
||||
turns: turnsRef.current,
|
||||
entityProgressMap: entityProgressMapRef.current,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[applyChatViewState],
|
||||
);
|
||||
|
||||
const supported = Boolean(
|
||||
profile.structuredAdapter &&
|
||||
agent.launchAgentChat &&
|
||||
@ -610,6 +683,16 @@ export function CustomAgentChatView({
|
||||
() => (layoutId ? { projectId, layoutId, nodeId } : null),
|
||||
[layoutId, nodeId, projectId],
|
||||
);
|
||||
const displayTurns = useMemo<ChatTurn[]>(
|
||||
() => [
|
||||
...turns,
|
||||
...Array.from(entityProgressMap.values()).map((progress) => ({
|
||||
role: "progress" as const,
|
||||
progress,
|
||||
})),
|
||||
],
|
||||
[entityProgressMap, turns],
|
||||
);
|
||||
|
||||
const clearPersistedDraft = useCallback(async () => {
|
||||
if (!draftScope) return;
|
||||
@ -645,19 +728,23 @@ export function CustomAgentChatView({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const receive = useCallback((chunk: ReplyChunk) => {
|
||||
scrollbackChunkCountRef.current += 1;
|
||||
setTurns((prev) => foldChunk(prev, chunk));
|
||||
if (chunk.kind === "final" || chunk.kind === "error") setBusy(false);
|
||||
}, []);
|
||||
const receive = useCallback(
|
||||
(chunk: ReplyChunk) => {
|
||||
scrollbackChunkCountRef.current += 1;
|
||||
updateChatViewState((prev) => foldChunk(prev, chunk));
|
||||
if (chunk.kind === "final" || chunk.kind === "error") setBusy(false);
|
||||
},
|
||||
[updateChatViewState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentSession) return;
|
||||
chatSessionViewCache.set(currentSession, {
|
||||
turns,
|
||||
entityProgressMap,
|
||||
chunkCount: scrollbackChunkCountRef.current,
|
||||
});
|
||||
}, [currentSession, turns]);
|
||||
}, [currentSession, entityProgressMap, turns]);
|
||||
|
||||
useEffect(() => {
|
||||
draftSaveSeqRef.current += 1;
|
||||
@ -807,13 +894,13 @@ export function CustomAgentChatView({
|
||||
setCurrentSession(reattached.sessionId);
|
||||
if (options.applyScrollback) {
|
||||
scrollbackChunkCountRef.current = reattached.scrollback.length;
|
||||
const replayed = reattached.scrollback.reduce(foldChunk, [] as ChatTurn[]);
|
||||
setTurns(replayed);
|
||||
const replayed = reattached.scrollback.reduce(foldChunk, emptyChatViewState());
|
||||
applyChatViewState(replayed);
|
||||
setBusy(turnsIndicateBusy(replayed));
|
||||
}
|
||||
return reattached.sessionId;
|
||||
},
|
||||
[agent, receive],
|
||||
[agent, applyChatViewState, receive],
|
||||
);
|
||||
|
||||
const recoverStructuredSession = useCallback(
|
||||
@ -920,7 +1007,7 @@ export function CustomAgentChatView({
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
}, [turns]);
|
||||
}, [displayTurns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selfEmittedSessionIdRef.current === sessionId) {
|
||||
@ -953,7 +1040,10 @@ export function CustomAgentChatView({
|
||||
const cached = chatSessionViewCache.get(externalSessionId);
|
||||
if (cached) {
|
||||
scrollbackChunkCountRef.current = cached.chunkCount;
|
||||
setTurns(cached.turns);
|
||||
applyChatViewState({
|
||||
turns: cached.turns,
|
||||
entityProgressMap: cached.entityProgressMap ?? new Map(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
await reattachStructuredSession(externalSessionId, { applyScrollback: true });
|
||||
@ -1014,6 +1104,7 @@ export function CustomAgentChatView({
|
||||
reattachStructuredSession,
|
||||
recoverStructuredSession,
|
||||
publishSessionId,
|
||||
applyChatViewState,
|
||||
]);
|
||||
|
||||
const canSend = useMemo(
|
||||
@ -1066,7 +1157,7 @@ export function CustomAgentChatView({
|
||||
24,
|
||||
80,
|
||||
);
|
||||
setTurns([]);
|
||||
applyChatViewState(emptyChatViewState());
|
||||
setCurrentSession(null);
|
||||
publishSessionId(null);
|
||||
onConversationIdRef.current(null);
|
||||
@ -1093,7 +1184,10 @@ export function CustomAgentChatView({
|
||||
});
|
||||
}
|
||||
|
||||
setTurns([{ role: "tool", label: `Profil changé vers ${selected.name}.` }]);
|
||||
applyChatViewState({
|
||||
turns: [{ role: "tool", label: `Profil changé vers ${selected.name}.` }],
|
||||
entityProgressMap: new Map(),
|
||||
});
|
||||
setProfileDialog(null);
|
||||
} catch (e) {
|
||||
setProfileDialog((prev) =>
|
||||
@ -1125,14 +1219,14 @@ export function CustomAgentChatView({
|
||||
});
|
||||
if (result.effect.kind === "help") {
|
||||
const helpText = formatSlashCommandHelp(result.effect.commands);
|
||||
setTurns((prev) => [
|
||||
...prev,
|
||||
{ role: "agent", text: helpText },
|
||||
]);
|
||||
updateChatViewState((prev) => ({
|
||||
turns: [...prev.turns, { role: "agent", text: helpText }],
|
||||
entityProgressMap: prev.entityProgressMap,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (result.effect.kind === "cleanConversation") {
|
||||
setTurns([]);
|
||||
applyChatViewState(emptyChatViewState());
|
||||
return;
|
||||
}
|
||||
if (result.effect.kind === "profileSwitch") {
|
||||
@ -1145,16 +1239,22 @@ export function CustomAgentChatView({
|
||||
result.effect.commandId,
|
||||
...result.effect.arguments,
|
||||
);
|
||||
setTurns((prev) => [
|
||||
...prev,
|
||||
{ role: "tool", label: `Commande plugin ${result.command.name} exécutée.` },
|
||||
]);
|
||||
updateChatViewState((prev) => ({
|
||||
turns: [
|
||||
...prev.turns,
|
||||
{ role: "tool", label: `Commande plugin ${result.command.name} exécutée.` },
|
||||
],
|
||||
entityProgressMap: prev.entityProgressMap,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `Commande ${invocation.name} échouée: ${describe(e)}`;
|
||||
setError(message);
|
||||
setTurns((prev) => [...prev, { role: "error", text: message }]);
|
||||
updateChatViewState((prev) => ({
|
||||
turns: [...prev.turns, { role: "error", text: message }],
|
||||
entityProgressMap: prev.entityProgressMap,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1233,14 +1333,17 @@ export function CustomAgentChatView({
|
||||
void clearPersistedDraft();
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
setTurns((prev) => [
|
||||
...prev,
|
||||
{
|
||||
role: "user",
|
||||
text: displayText,
|
||||
...(attachmentLabels.length > 0 ? { attachments: attachmentLabels } : {}),
|
||||
},
|
||||
]);
|
||||
updateChatViewState((prev) => ({
|
||||
turns: [
|
||||
...prev.turns,
|
||||
{
|
||||
role: "user",
|
||||
text: displayText,
|
||||
...(attachmentLabels.length > 0 ? { attachments: attachmentLabels } : {}),
|
||||
},
|
||||
],
|
||||
entityProgressMap: prev.entityProgressMap,
|
||||
}));
|
||||
const sendTurn = (sid: string) =>
|
||||
attachmentInputs.length > 0
|
||||
? agent.sendAgentChat!(sid, text, receive, { attachments: attachmentInputs })
|
||||
@ -1267,13 +1370,19 @@ export function CustomAgentChatView({
|
||||
} catch (recoveryError) {
|
||||
setBusy(false);
|
||||
setError(describe(recoveryError));
|
||||
setTurns((prev) => [...prev, { role: "error", text: describe(recoveryError) }]);
|
||||
updateChatViewState((prev) => ({
|
||||
turns: [...prev.turns, { role: "error", text: describe(recoveryError) }],
|
||||
entityProgressMap: prev.entityProgressMap,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
setBusy(false);
|
||||
setError(describe(e));
|
||||
setTurns((prev) => [...prev, { role: "error", text: describe(e) }]);
|
||||
updateChatViewState((prev) => ({
|
||||
turns: [...prev.turns, { role: "error", text: describe(e) }],
|
||||
entityProgressMap: prev.entityProgressMap,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1288,33 +1397,48 @@ export function CustomAgentChatView({
|
||||
setOpening(false);
|
||||
setCurrentSession(null);
|
||||
publishSessionId(null);
|
||||
setTurns((prev) => [
|
||||
...completeRunningProgress(prev),
|
||||
{
|
||||
role: "tool",
|
||||
label:
|
||||
stopped.kind === "structured"
|
||||
? "Agent arrêté. Cascade d'arrêt demandée pour les tâches et appels MCP liés."
|
||||
: "Agent arrêté.",
|
||||
},
|
||||
]);
|
||||
updateChatViewState((prev) => {
|
||||
const completed = completeChatViewProgress(prev);
|
||||
return {
|
||||
turns: [
|
||||
...completed.turns,
|
||||
{
|
||||
role: "tool",
|
||||
label:
|
||||
stopped.kind === "structured"
|
||||
? "Agent arrêté. Cascade d'arrêt demandée pour les tâches et appels MCP liés."
|
||||
: "Agent arrêté.",
|
||||
},
|
||||
],
|
||||
entityProgressMap: completed.entityProgressMap,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
if (isNotFound(e)) {
|
||||
setBusy(false);
|
||||
setOpening(false);
|
||||
setCurrentSession(null);
|
||||
publishSessionId(null);
|
||||
setTurns((prev) => [
|
||||
...completeRunningProgress(prev),
|
||||
{ role: "tool", label: "Session déjà arrêtée." },
|
||||
]);
|
||||
updateChatViewState((prev) => {
|
||||
const completed = completeChatViewProgress(prev);
|
||||
return {
|
||||
turns: [
|
||||
...completed.turns,
|
||||
{ role: "tool", label: "Session déjà arrêtée." },
|
||||
],
|
||||
entityProgressMap: completed.entityProgressMap,
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
setError(describe(e));
|
||||
setTurns((prev) => [
|
||||
...prev,
|
||||
{ role: "error", text: `Arrêt de l'agent échoué: ${describe(e)}` },
|
||||
]);
|
||||
updateChatViewState((prev) => ({
|
||||
turns: [
|
||||
...prev.turns,
|
||||
{ role: "error", text: `Arrêt de l'agent échoué: ${describe(e)}` },
|
||||
],
|
||||
entityProgressMap: prev.entityProgressMap,
|
||||
}));
|
||||
} finally {
|
||||
setStopping(false);
|
||||
}
|
||||
@ -1449,19 +1573,19 @@ export function CustomAgentChatView({
|
||||
data-testid="custom-agent-chat-scroll"
|
||||
className="flex min-h-0 flex-1 basis-0 flex-col gap-2 overflow-x-hidden overflow-y-auto px-3 py-3"
|
||||
>
|
||||
{opening && turns.length === 0 ? (
|
||||
{opening && displayTurns.length === 0 ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted">
|
||||
<Spinner size={14} />
|
||||
<span>
|
||||
{externalSessionId ? "Reprise en cours..." : "Ouverture de la session structurée…"}
|
||||
</span>
|
||||
</div>
|
||||
) : turns.length === 0 ? (
|
||||
) : displayTurns.length === 0 ? (
|
||||
<p className="m-auto text-xs text-muted">
|
||||
Envoyez un message pour démarrer la conversation structurée.
|
||||
</p>
|
||||
) : (
|
||||
turns.map((turn, index) => (
|
||||
displayTurns.map((turn, index) => (
|
||||
<ChatBubble key={index} turn={turn} requesterName={agentName} />
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user