NeonClary Cursor commited on
Commit
bb08a9f
·
1 Parent(s): beaa024

feat(transparency): Download as .txt button in Credential Summary

Browse files

Mirrors 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}>&times;</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}>&times;</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
+ }