import React, { useMemo, useState, useEffect } from 'react'; import { Download, Edit2, Check, X, User } from 'lucide-react'; /** * Read-only modal that surfaces the orchestrator-generated Credential * Summary - the per-participant assessment of expertise, debating * style, credibility on this question, and biases to watch. * * Built concurrently during Phase 1 (as each initial opinion lands). * Rebuilt only if a participant's backing LLM model changes. The modal * pulls a fresh snapshot via GET * /api/chat/{id}/credentials each time it's opened, so the user sees * the latest version regardless of when they peek. * * Layout mirrors ChatTableView (overlay + card + close button) for * consistency with the existing transparency surfaces. */ export default function CredentialSummaryModal({ isOpen, data, onClose, onRefresh, humanParticipantId, onEditHumanCredential, }) { // Hooks must run on every render, so the filename memo lives ABOVE // the early return. The dependency on `isOpen` regenerates the // timestamp each time the modal opens (matches PromptCatalogModal). const filename = useMemo(() => { const now = new Date(); const pad = (n) => String(n).padStart(2, '0'); return ( 'ccai-credentials-' + `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}` + '.txt' ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]); if (!isOpen) return null; const credentials = (data && data.credentials) || []; const question = data?.question || ''; const handleDownload = () => { if (!credentials.length) return; const txt = renderCredentialsAsText(question, credentials); const blob = new Blob([txt], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }; return (

Credential Summary

The orchestrator's neutral assessment of each participant. Built during Phase 1; updated only if a participant's model changes.
{onRefresh && ( )}
{question && (
Question:
{question}
)}
{credentials.length === 0 ? (
No Credential Summary has been generated yet. The orchestrator builds it after Phase 1 (initial opinions).
) : ( credentials.map((c) => { const isHuman = !!humanParticipantId && c.participant_id === humanParticipantId; return ( ); }) )}
); } function CredentialCard({ cred, isHuman, onEdit }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(() => ({ name: cred.name || '', expertise: cred.expertise || '', personality: cred.personality || '', credibility_for_question: cred.credibility_for_question !== undefined ? cred.credibility_for_question : 0.5, bias_to_watch: cred.bias_to_watch || '', })); // Reset the draft whenever the underlying credential payload // changes (e.g. a Phase-3 refresh from the SSE stream). useEffect(() => { setDraft({ name: cred.name || '', expertise: cred.expertise || '', personality: cred.personality || '', credibility_for_question: cred.credibility_for_question !== undefined ? cred.credibility_for_question : 0.5, bias_to_watch: cred.bias_to_watch || '', }); }, [cred]); const score = toScore(cred.credibility_for_question); if (isHuman && editing) { return (
setDraft(d => ({ ...d, name: e.target.value }))} /> Human
setDraft(d => ({ ...d, expertise: v }))} /> setDraft(d => ({ ...d, personality: v }))} /> setDraft(d => ({ ...d, credibility_for_question: v }))} /> setDraft(d => ({ ...d, bias_to_watch: v }))} />
); } return (
{isHuman && ( )} {cred.name || cred.participant_id} {isHuman && ( Human )}
{score !== null && (
Credibility
{score.toFixed(2)}
)} {isHuman && onEdit && ( )}
); } function EditableRow({ label, value, onChange }) { return (
{label}