Files
IdeA/frontend/src/adapters/conversationNormalization.ts
Blomios c19e375849 feat(frontend): viewer humain fil-par-paire en lecture seule (LS7)
Frontend pur, lecture seule, consomme read_conversation_page (LS6) ; aucun changement backend.
- adapters : conversation.ts (gateway) + conversationNormalization.ts.
- features/conversations : useConversationThread, ConversationViewer, index.
- domain (types LS7), ports (port + Gateways.conversation), adapters/index, mock (+ clé inventaire).
- features/workstate : ProjectWorkStatePanel prop onOpenConversation.
- features/projects : ProjectsView swap viewer.
- tests (QA, verts) : conversationNormalization, mock/conversationGateway,
  useConversationThread, ConversationViewer, ProjectsView.ls7. tsc 0, vitest 443/443 (+36).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:30:51 +02:00

68 lines
2.4 KiB
TypeScript

/**
* Wire → domain normalization for the conversation transcript read (LS7).
*
* Mirrors the LS6 backend DTO (`crates/app-tauri/src/dto.rs`): `TurnPageDto`
* (camelCase `turns` / `hasMore` / `nextAnchor`), `TurnViewDto`
* (`id` / `atMs` / `role` / `source` / `text` / `textLen`) and the serde-tagged
* `TurnSourceDto` (`{ kind: "human" }` | `{ kind: "agent", agentId }`). The
* `source` mapping is calqued on `workStateNormalization.ts`.
*/
import type { TurnPage, TurnRole, TurnSource, TurnView } from "@/domain";
type RecordLike = Record<string, unknown>;
function isRecord(value: unknown): value is RecordLike {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function stringValue(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function numberValue(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function arrayValue(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
/** Mirror of LS6 `TurnSourceDto` (serde tag `kind`, camelCase `agentId`). */
function normalizeSource(value: unknown): TurnSource {
if (!isRecord(value)) return { kind: "human" };
if (value.kind === "agent") {
return { kind: "agent", agentId: stringValue(value.agentId, "unknown") };
}
return { kind: "human" };
}
/** Mirror of the camelCase-serialized `TurnRole` (`prompt`/`response`/`toolActivity`). */
function normalizeRole(value: unknown): TurnRole {
return value === "response" || value === "toolActivity" ? value : "prompt";
}
function normalizeTurn(value: unknown, index: number): TurnView {
const turn = isRecord(value) ? value : {};
const text = stringValue(turn.text);
return {
id: stringValue(turn.id, `turn-${index}`),
atMs: numberValue(turn.atMs, index),
role: normalizeRole(turn.role),
source: normalizeSource(turn.source),
text,
textLen: numberValue(turn.textLen, text.length),
};
}
/** Maps a raw `TurnPageDto` payload to the domain {@link TurnPage}. */
export function normalizeTurnPage(value: unknown): TurnPage {
const page = isRecord(value) ? value : {};
const nextAnchor = page.nextAnchor;
return {
turns: arrayValue(page.turns).map(normalizeTurn),
hasMore: page.hasMore === true,
...(typeof nextAnchor === "string" ? { nextAnchor } : {}),
};
}