Spaces:
Sleeping
Sleeping
NeonClary Cursor commited on
Commit ·
bb08a9f
1
Parent(s): beaa024
feat(transparency): Download as .txt button in Credential Summary
Browse filesMirrors the existing PromptCatalogModal download affordance so the two transparency modals look like a matched set. Button sits to the right of Refresh in the Credential Summary header, disabled until a summary has been generated. The downloaded .txt uses the same banner/separator style as the prompt-catalog dump and includes the question, each participant's name, credibility score, expertise, style, and bias-to-watch.
Co-authored-by: Cursor <cursoragent@cursor.com>
frontend/src/components/CredentialSummaryModal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
import React from 'react';
|
|
|
|
| 2 |
|
| 3 |
/**
|
| 4 |
* Read-only modal that surfaces the orchestrator-generated Credential
|
|
@@ -19,11 +20,38 @@ export default function CredentialSummaryModal({
|
|
| 19 |
onClose,
|
| 20 |
onRefresh,
|
| 21 |
}) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
if (!isOpen) return null;
|
| 23 |
|
| 24 |
const credentials = (data && data.credentials) || [];
|
| 25 |
const question = data?.question || '';
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
return (
|
| 28 |
<div className="ccai-credentials-overlay">
|
| 29 |
<div className="ccai-credentials-card">
|
|
@@ -45,6 +73,15 @@ export default function CredentialSummaryModal({
|
|
| 45 |
Refresh
|
| 46 |
</button>
|
| 47 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
<button className="modal-close" onClick={onClose}>×</button>
|
| 49 |
</div>
|
| 50 |
|
|
@@ -116,3 +153,46 @@ function toScore(value) {
|
|
| 116 |
if (Number.isNaN(n)) return null;
|
| 117 |
return Math.max(0, Math.min(1, n));
|
| 118 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useMemo } from 'react';
|
| 2 |
+
import { Download } from 'lucide-react';
|
| 3 |
|
| 4 |
/**
|
| 5 |
* Read-only modal that surfaces the orchestrator-generated Credential
|
|
|
|
| 20 |
onClose,
|
| 21 |
onRefresh,
|
| 22 |
}) {
|
| 23 |
+
// Hooks must run on every render, so the filename memo lives ABOVE
|
| 24 |
+
// the early return. The dependency on `isOpen` regenerates the
|
| 25 |
+
// timestamp each time the modal opens (matches PromptCatalogModal).
|
| 26 |
+
const filename = useMemo(() => {
|
| 27 |
+
const now = new Date();
|
| 28 |
+
const pad = (n) => String(n).padStart(2, '0');
|
| 29 |
+
return (
|
| 30 |
+
'ccai-credentials-'
|
| 31 |
+
+ `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
|
| 32 |
+
+ `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
| 33 |
+
+ '.txt'
|
| 34 |
+
);
|
| 35 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 36 |
+
}, [isOpen]);
|
| 37 |
+
|
| 38 |
if (!isOpen) return null;
|
| 39 |
|
| 40 |
const credentials = (data && data.credentials) || [];
|
| 41 |
const question = data?.question || '';
|
| 42 |
|
| 43 |
+
const handleDownload = () => {
|
| 44 |
+
if (!credentials.length) return;
|
| 45 |
+
const txt = renderCredentialsAsText(question, credentials);
|
| 46 |
+
const blob = new Blob([txt], { type: 'text/plain;charset=utf-8' });
|
| 47 |
+
const url = URL.createObjectURL(blob);
|
| 48 |
+
const a = document.createElement('a');
|
| 49 |
+
a.href = url;
|
| 50 |
+
a.download = filename;
|
| 51 |
+
a.click();
|
| 52 |
+
URL.revokeObjectURL(url);
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
return (
|
| 56 |
<div className="ccai-credentials-overlay">
|
| 57 |
<div className="ccai-credentials-card">
|
|
|
|
| 73 |
Refresh
|
| 74 |
</button>
|
| 75 |
)}
|
| 76 |
+
<button
|
| 77 |
+
className="btn-sm btn-outline"
|
| 78 |
+
onClick={handleDownload}
|
| 79 |
+
disabled={credentials.length === 0}
|
| 80 |
+
title="Download the credential summary as a .txt file"
|
| 81 |
+
>
|
| 82 |
+
<Download size={14} style={{ marginRight: 4 }} />
|
| 83 |
+
Download as .txt
|
| 84 |
+
</button>
|
| 85 |
<button className="modal-close" onClick={onClose}>×</button>
|
| 86 |
</div>
|
| 87 |
|
|
|
|
| 153 |
if (Number.isNaN(n)) return null;
|
| 154 |
return Math.max(0, Math.min(1, n));
|
| 155 |
}
|
| 156 |
+
|
| 157 |
+
/**
|
| 158 |
+
* Flat human-readable .txt dump used by the Download button. Same
|
| 159 |
+
* banner/separator style as PromptCatalogModal.renderCatalogAsText so
|
| 160 |
+
* the two transparency exports look like a matched set.
|
| 161 |
+
*/
|
| 162 |
+
function renderCredentialsAsText(question, credentials) {
|
| 163 |
+
const now = new Date().toISOString();
|
| 164 |
+
const lines = [];
|
| 165 |
+
const banner = '═'.repeat(64);
|
| 166 |
+
lines.push(banner);
|
| 167 |
+
lines.push('Collaborative Conversational AI (CCAI) Demo — Credential Summary');
|
| 168 |
+
lines.push(`Generated: ${now}`);
|
| 169 |
+
lines.push(banner);
|
| 170 |
+
lines.push('');
|
| 171 |
+
|
| 172 |
+
if (question) {
|
| 173 |
+
lines.push('Question:');
|
| 174 |
+
for (const ln of String(question).split('\n')) {
|
| 175 |
+
lines.push(' ' + ln);
|
| 176 |
+
}
|
| 177 |
+
lines.push('');
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
const sep = '─'.repeat(12);
|
| 181 |
+
lines.push(`${sep} Participants ${sep}`);
|
| 182 |
+
lines.push('');
|
| 183 |
+
|
| 184 |
+
for (const cred of credentials) {
|
| 185 |
+
const score = toScore(cred.credibility_for_question);
|
| 186 |
+
const name = cred.name || cred.participant_id || '(unknown)';
|
| 187 |
+
lines.push(`## ${name}`);
|
| 188 |
+
if (score !== null) {
|
| 189 |
+
lines.push(`Credibility: ${score.toFixed(2)} of 1.00`);
|
| 190 |
+
}
|
| 191 |
+
if (cred.expertise) lines.push(`Expertise: ${cred.expertise}`);
|
| 192 |
+
if (cred.personality) lines.push(`Style: ${cred.personality}`);
|
| 193 |
+
if (cred.bias_to_watch) lines.push(`Bias to watch: ${cred.bias_to_watch}`);
|
| 194 |
+
lines.push('');
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
return lines.join('\n');
|
| 198 |
+
}
|