NeonClary Cursor commited on
Commit
478df90
·
1 Parent(s): af5d1df

feat(transparency): add "View current chat prompts" to settings

Browse files

New PromptCatalogModal under Settings -> Transparency lists every
prompt template the orchestrator and participants use, grouped by
phase, with a 1-2 sentence purpose for each, the runtime template
variables, and a copy-to-clipboard button per prompt. Powered by a
new GET /api/chat/prompts/catalog endpoint and a hand-curated
backend/app/services/prompts/catalog.py. Header includes a
"Download as .txt" button that produces a flat ccai-prompts-*.txt
dump.

Co-authored-by: Cursor <cursoragent@cursor.com>

backend/app/api/chat.py CHANGED
@@ -33,6 +33,7 @@ from app.services.models import (
33
  clamp_conversation_limits,
34
  )
35
  from app.services.auto_select import auto_select_participants
 
36
  from app.services.orchestrator import (
37
  create_session,
38
  get_session,
@@ -194,6 +195,24 @@ async def api_generate_role_freeform(req: GenerateRoleFreeformRequest):
194
  return result
195
 
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  # ---------------------------------------------------------------------------
198
  # Auto-select participants (LLM-based ranking for "Select N Automatically")
199
  # ---------------------------------------------------------------------------
 
33
  clamp_conversation_limits,
34
  )
35
  from app.services.auto_select import auto_select_participants
36
+ from app.services.prompts.catalog import build_prompt_catalog
37
  from app.services.orchestrator import (
38
  create_session,
39
  get_session,
 
195
  return result
196
 
197
 
198
+ # ---------------------------------------------------------------------------
199
+ # Prompt catalog (Transparency: "View current chat prompts")
200
+ # ---------------------------------------------------------------------------
201
+
202
+ @router.get("/chat/prompts/catalog")
203
+ async def api_chat_prompts_catalog():
204
+ """Return every prompt template the orchestrator and participants
205
+ use during a chat, grouped by phase, each with a short purpose and
206
+ a list of runtime template variables. Used by the
207
+ PromptCatalogModal in the settings menu's Transparency section.
208
+
209
+ Shape:
210
+ {"groups": [{"title": "...", "items": [{"name", "purpose",
211
+ "variables", "template"}]}, ...]}
212
+ """
213
+ return build_prompt_catalog()
214
+
215
+
216
  # ---------------------------------------------------------------------------
217
  # Auto-select participants (LLM-based ranking for "Select N Automatically")
218
  # ---------------------------------------------------------------------------
backend/app/services/prompts/catalog.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hand-curated catalog of every prompt template the CCAI orchestrator
2
+ and participants use, organized by the order they fire during a chat.
3
+
4
+ This backs GET /api/chat/prompts/catalog, which feeds the "View current
5
+ chat prompts" modal in the settings menu. The goal is user-facing
6
+ transparency: a non-technical user should be able to scroll the modal
7
+ and understand exactly what the orchestrator asks each participant to
8
+ do at each phase.
9
+
10
+ The catalog is intentionally explicit (rather than auto-discovered) so
11
+ the per-prompt `purpose` strings can stay readable and so the
12
+ ordering matches the live conversation flow even when prompts are
13
+ imported from different modules.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from typing import Any
19
+
20
+ from app.services.prompts import (
21
+ ADDRESSED_TO_PROMPT,
22
+ ALLIANCE_DETECTION_PROMPT,
23
+ AUTO_SELECT_PARTICIPANTS_PROMPT,
24
+ CONSENSUS_ALLIED_PROMPT,
25
+ CONSENSUS_SOLO_PROMPT,
26
+ CONSENSUS_STATUS_PROMPT,
27
+ CONTRIBUTION_SUMMARY_PROMPT,
28
+ CREDENTIAL_BUILD_PROMPT,
29
+ CREDENTIAL_REFRESH_PROMPT,
30
+ CRITIQUE_PROMPT,
31
+ FINALIZATION_PROMPT,
32
+ INITIAL_OPINION_PROMPT,
33
+ MAJORITY_REPORT_PROMPT,
34
+ NO_CONSENSUS_REPORT_PROMPT,
35
+ NO_REASONING_DIRECTIVE,
36
+ ORCHESTRATOR_BASE_DIRECTIVE,
37
+ PARTICIPANT_BASE_DIRECTIVE,
38
+ STATUS_ASSESSMENT_PROMPT,
39
+ TARGETED_FOLLOWUP_FROM_PARTICIPANT_PROMPT,
40
+ TARGETED_FOLLOWUP_PROMPT,
41
+ UNADDRESSED_FACTOR_PROMPT,
42
+ )
43
+
44
+
45
+ # Each entry: (constant_name, prompt_text, purpose). The frontend will
46
+ # also derive {var} placeholders from the prompt text directly, but
47
+ # `name` and `purpose` stay hand-curated for readability.
48
+ _PROMPTS_BY_NAME: dict[str, tuple[str, str]] = {
49
+ # ── Always-on directives ─────────────────────────────────────
50
+ "PARTICIPANT_BASE_DIRECTIVE": (
51
+ PARTICIPANT_BASE_DIRECTIVE,
52
+ "Appended to every participant's role prompt so they know they "
53
+ "are in a CCAI forum, who the other participants are, and that "
54
+ "the orchestrator (not them) controls the floor.",
55
+ ),
56
+ "NO_REASONING_DIRECTIVE": (
57
+ NO_REASONING_DIRECTIVE,
58
+ "Hard 'no reasoning, no meta-commentary' guard added to every "
59
+ "participant call so models don't leak chain-of-thought, "
60
+ "<think> tags, or scratchpad text into the chat.",
61
+ ),
62
+ "ORCHESTRATOR_BASE_DIRECTIVE": (
63
+ ORCHESTRATOR_BASE_DIRECTIVE,
64
+ "System message for every orchestrator-side LLM call. "
65
+ "Establishes neutrality and demands strict JSON for the "
66
+ "structured ones (status checks, alliance detection, etc.).",
67
+ ),
68
+
69
+ # ── Optional pre-chat: Select N Automatically ────────────────
70
+ "AUTO_SELECT_PARTICIPANTS_PROMPT": (
71
+ AUTO_SELECT_PARTICIPANTS_PROMPT,
72
+ "Used when the user enables 'Select N Automatically' in the "
73
+ "participants dropdown. The orchestrator LLM ranks every "
74
+ "available candidate persona by relevance to the question and "
75
+ "returns the top N just before /chat/start.",
76
+ ),
77
+
78
+ # ── Phase 1 ─────────────────────────────────────────────────
79
+ "INITIAL_OPINION_PROMPT": (
80
+ INITIAL_OPINION_PROMPT,
81
+ "Phase 1. Asks each participant for an independent first "
82
+ "opinion, with no awareness of what anyone else has said. "
83
+ "This independence is what makes the Credential Summary in "
84
+ "the next step meaningful.",
85
+ ),
86
+
87
+ # ── Credential Summary (build) ──────────────────────────────
88
+ "CREDENTIAL_BUILD_PROMPT": (
89
+ CREDENTIAL_BUILD_PROMPT,
90
+ "After Phase 1 the orchestrator builds the Credential Summary: "
91
+ "a neutral assessment of each participant's expertise, "
92
+ "personality, biases, and a 0-1 credibility score on this "
93
+ "specific question. Returned as strict JSON.",
94
+ ),
95
+
96
+ # ── Phase 2 ─────────────────────────────────────────────────
97
+ "CRITIQUE_PROMPT": (
98
+ CRITIQUE_PROMPT,
99
+ "Phase 2. Each participant gets one or more critique rounds "
100
+ "(count is tunable in Settings -> Conversation limits). They "
101
+ "address any open questions aimed at them first, then offer "
102
+ "constructive critique of others and may revise their opinion.",
103
+ ),
104
+
105
+ # ── Credential Summary (refresh) ────────────────────────────
106
+ "CREDENTIAL_REFRESH_PROMPT": (
107
+ CREDENTIAL_REFRESH_PROMPT,
108
+ "After the last critique round, the orchestrator refreshes "
109
+ "the Credential Summary because participants reveal a lot "
110
+ "more about themselves through critique than through their "
111
+ "opening pitch.",
112
+ ),
113
+
114
+ # ── Phase 3 ─────────────────────────────────────────────────
115
+ "STATUS_ASSESSMENT_PROMPT": (
116
+ STATUS_ASSESSMENT_PROMPT,
117
+ "Phase 3. The orchestrator decides whether the group needs "
118
+ "targeted follow-ups before finalization. It prefers to relay "
119
+ "unanswered participant-to-participant questions verbatim and "
120
+ "only synthesizes a new one if no real open thread exists.",
121
+ ),
122
+ "TARGETED_FOLLOWUP_PROMPT": (
123
+ TARGETED_FOLLOWUP_PROMPT,
124
+ "Used inside Phase 3 when the orchestrator is asking a "
125
+ "follow-up of its own initiative (no real open thread "
126
+ "existed). The participant sees this as 'The orchestrator "
127
+ "has a follow-up for you'.",
128
+ ),
129
+ "TARGETED_FOLLOWUP_FROM_PARTICIPANT_PROMPT": (
130
+ TARGETED_FOLLOWUP_FROM_PARTICIPANT_PROMPT,
131
+ "Used inside Phase 3 when the orchestrator is relaying a "
132
+ "previously-unanswered question verbatim from another "
133
+ "participant, attributed to the original asker.",
134
+ ),
135
+
136
+ # ── Phase 4 ─────────────────────────────────────────────────
137
+ "FINALIZATION_PROMPT": (
138
+ FINALIZATION_PROMPT,
139
+ "Phase 4. Each participant states a revised post-discussion "
140
+ "opinion OR endorses another participant's revised opinion "
141
+ "(with an optional caveat). Open threads aimed at them are "
142
+ "answered briefly at the top.",
143
+ ),
144
+
145
+ # ── Phase 5 ─────────────────────────────────────────────────
146
+ "ALLIANCE_DETECTION_PROMPT": (
147
+ ALLIANCE_DETECTION_PROMPT,
148
+ "Start of Phase 5. The orchestrator clusters participants "
149
+ "into 'alliance groups' based on their revised opinions, so "
150
+ "consensus prompts can address allies and solo voices "
151
+ "differently.",
152
+ ),
153
+ "ADDRESSED_TO_PROMPT": (
154
+ ADDRESSED_TO_PROMPT,
155
+ "Used after each Phase 5 message to classify whether it was "
156
+ "aimed at one specific other participant (a dyadic exchange) "
157
+ "or broadcast to the whole group. Drives the back-and-forth "
158
+ "routing and the 'A -> B' header on each chat bubble.",
159
+ ),
160
+ "CONSENSUS_ALLIED_PROMPT": (
161
+ CONSENSUS_ALLIED_PROMPT,
162
+ "Phase 5 prompt for participants who are part of an alliance. "
163
+ "They advocate for the group's shared stance and try to win "
164
+ "over participants from other groups, addressing ONE "
165
+ "specific participant when possible.",
166
+ ),
167
+ "CONSENSUS_SOLO_PROMPT": (
168
+ CONSENSUS_SOLO_PROMPT,
169
+ "Phase 5 prompt for participants who have no allies. They "
170
+ "pick one of three strategies: persuade the closest group, "
171
+ "switch sides, or propose a compromise.",
172
+ ),
173
+ "CONSENSUS_STATUS_PROMPT": (
174
+ CONSENSUS_STATUS_PROMPT,
175
+ "Used periodically during Phase 5. The orchestrator decides "
176
+ "whether the group has reached majority agreement, is still "
177
+ "shifting productively, or has stalled and needs intervention.",
178
+ ),
179
+
180
+ # ── Phase 6 (closure) ───────────────────────────────────────
181
+ "UNADDRESSED_FACTOR_PROMPT": (
182
+ UNADDRESSED_FACTOR_PROMPT,
183
+ "If Phase 5 stalls, the orchestrator looks for ONE important "
184
+ "factor the group hasn't adequately discussed and surfaces it "
185
+ "as a new consideration before re-running consensus.",
186
+ ),
187
+ "MAJORITY_REPORT_PROMPT": (
188
+ MAJORITY_REPORT_PROMPT,
189
+ "Final report when the group reaches majority agreement. The "
190
+ "orchestrator writes a neutral prose summary of the decision, "
191
+ "the strongest supporting reasons, and high-credibility "
192
+ "dissenting points.",
193
+ ),
194
+ "NO_CONSENSUS_REPORT_PROMPT": (
195
+ NO_CONSENSUS_REPORT_PROMPT,
196
+ "Final report when consensus retries are exhausted without "
197
+ "agreement. The orchestrator lays out the major opinions, "
198
+ "names supporters, and offers its own neutral recommendation "
199
+ "with a note that the user should ultimately decide.",
200
+ ),
201
+
202
+ # ── Utility / table view ────────────────────────────────────
203
+ "CONTRIBUTION_SUMMARY_PROMPT": (
204
+ CONTRIBUTION_SUMMARY_PROMPT,
205
+ "Run after the chat ends to populate the per-participant "
206
+ "summary column in the table view (and CSV export). Asks the "
207
+ "orchestrator to write a neutral 2-3 sentence recap of each "
208
+ "participant's overall contribution.",
209
+ ),
210
+ }
211
+
212
+
213
+ # Ordered grouping that mirrors the live phase flow. Names reference
214
+ # `_PROMPTS_BY_NAME` keys; the frontend renders groups in this order.
215
+ _GROUPS: list[tuple[str, list[str]]] = [
216
+ ("Always-on directives", [
217
+ "PARTICIPANT_BASE_DIRECTIVE",
218
+ "NO_REASONING_DIRECTIVE",
219
+ "ORCHESTRATOR_BASE_DIRECTIVE",
220
+ ]),
221
+ ("Optional pre-chat", [
222
+ "AUTO_SELECT_PARTICIPANTS_PROMPT",
223
+ ]),
224
+ ("Phase 1: Initial Opinions", [
225
+ "INITIAL_OPINION_PROMPT",
226
+ ]),
227
+ ("Credential Summary (build)", [
228
+ "CREDENTIAL_BUILD_PROMPT",
229
+ ]),
230
+ ("Phase 2: Critique", [
231
+ "CRITIQUE_PROMPT",
232
+ ]),
233
+ ("Credential Summary (refresh)", [
234
+ "CREDENTIAL_REFRESH_PROMPT",
235
+ ]),
236
+ ("Phase 3: Status assessment & targeted follow-ups", [
237
+ "STATUS_ASSESSMENT_PROMPT",
238
+ "TARGETED_FOLLOWUP_PROMPT",
239
+ "TARGETED_FOLLOWUP_FROM_PARTICIPANT_PROMPT",
240
+ ]),
241
+ ("Phase 4: Opinion Finalization", [
242
+ "FINALIZATION_PROMPT",
243
+ ]),
244
+ ("Phase 5: Consensus Gathering", [
245
+ "ALLIANCE_DETECTION_PROMPT",
246
+ "ADDRESSED_TO_PROMPT",
247
+ "CONSENSUS_ALLIED_PROMPT",
248
+ "CONSENSUS_SOLO_PROMPT",
249
+ "CONSENSUS_STATUS_PROMPT",
250
+ ]),
251
+ ("Phase 6: Closure", [
252
+ "UNADDRESSED_FACTOR_PROMPT",
253
+ "MAJORITY_REPORT_PROMPT",
254
+ "NO_CONSENSUS_REPORT_PROMPT",
255
+ ]),
256
+ ("Utility prompts", [
257
+ "CONTRIBUTION_SUMMARY_PROMPT",
258
+ ]),
259
+ ]
260
+
261
+
262
+ _VAR_RE = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
263
+
264
+
265
+ def _extract_variables(template: str) -> list[str]:
266
+ """Pull single-brace `{var}` placeholders out of a template,
267
+ preserving the first-occurrence order. Double-brace literals
268
+ (`{{` and `}}` from the JSON examples) are correctly ignored
269
+ because the regex only matches a single brace pair.
270
+ """
271
+ seen: set[str] = set()
272
+ out: list[str] = []
273
+ for m in _VAR_RE.finditer(template):
274
+ name = m.group(1)
275
+ if name in seen:
276
+ continue
277
+ seen.add(name)
278
+ out.append(name)
279
+ return out
280
+
281
+
282
+ def build_prompt_catalog() -> dict[str, Any]:
283
+ """Return the catalog as a JSON-serializable dict.
284
+
285
+ Shape:
286
+ {
287
+ "groups": [
288
+ {"title": "...", "items": [{"name", "purpose", "variables", "template"}, ...]},
289
+ ...
290
+ ]
291
+ }
292
+ """
293
+ groups_out: list[dict[str, Any]] = []
294
+ for group_title, names in _GROUPS:
295
+ items: list[dict[str, Any]] = []
296
+ for name in names:
297
+ if name not in _PROMPTS_BY_NAME:
298
+ # Defensive: skip a name typo rather than 500.
299
+ continue
300
+ template, purpose = _PROMPTS_BY_NAME[name]
301
+ items.append({
302
+ "name": name,
303
+ "purpose": purpose,
304
+ "variables": _extract_variables(template),
305
+ "template": template,
306
+ })
307
+ groups_out.append({"title": group_title, "items": items})
308
+ return {"groups": groups_out}
frontend/src/App.js CHANGED
@@ -7,6 +7,7 @@ import ExpertPersonaModal from './components/ExpertPersonaModal';
7
  import ChatTableView from './components/ChatTableView';
8
  import CredentialSummaryModal from './components/CredentialSummaryModal';
9
  import ConversationLimitsModal from './components/ConversationLimitsModal';
 
10
  import {
11
  fetchModels, fetchPersonas, fetchDemoQuestions,
12
  startChat, continueChat, getOrchestrator, setOrchestrator,
@@ -14,6 +15,7 @@ import {
14
  exportChat, exportApiLog, fetchTableView, fetchCredentials,
15
  fetchConversationLimitsDefaults,
16
  autoSelectParticipants,
 
17
  getRateLimitStatus,
18
  } from './utils/api';
19
  import * as storage from './utils/storage';
@@ -93,6 +95,10 @@ export default function App() {
93
  !!persisted.auto_select_mode,
94
  );
95
  const [priorManualSelection, setPriorManualSelection] = useState(null);
 
 
 
 
96
 
97
  const abortRef = useRef(null);
98
 
@@ -376,6 +382,20 @@ export default function App() {
376
  storage.setConversationLimits({});
377
  }, []);
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  // ─── Build start payload ────────────────────────────────────────
380
  // `participantsOverride`, if provided, replaces the
381
  // selectedParticipants-derived list (used by the auto-select flow
@@ -655,6 +675,7 @@ export default function App() {
655
  onShowTableView={handleShowTableView}
656
  onShowCredentials={handleShowCredentials}
657
  hasCredentials={!!sessionId}
 
658
  onShowConversationLimits={handleShowConversationLimits}
659
  conversationLimitsOverridden={Object.keys(limitsOverrides).length > 0}
660
  onDownloadChatTxt={handleDownloadTxt}
@@ -733,6 +754,11 @@ export default function App() {
733
  onChange={handleConversationLimitsChange}
734
  onResetAll={handleConversationLimitsResetAll}
735
  />
 
 
 
 
 
736
  </div>
737
  );
738
  }
 
7
  import ChatTableView from './components/ChatTableView';
8
  import CredentialSummaryModal from './components/CredentialSummaryModal';
9
  import ConversationLimitsModal from './components/ConversationLimitsModal';
10
+ import PromptCatalogModal from './components/PromptCatalogModal';
11
  import {
12
  fetchModels, fetchPersonas, fetchDemoQuestions,
13
  startChat, continueChat, getOrchestrator, setOrchestrator,
 
15
  exportChat, exportApiLog, fetchTableView, fetchCredentials,
16
  fetchConversationLimitsDefaults,
17
  autoSelectParticipants,
18
+ fetchPromptCatalog,
19
  getRateLimitStatus,
20
  } from './utils/api';
21
  import * as storage from './utils/storage';
 
95
  !!persisted.auto_select_mode,
96
  );
97
  const [priorManualSelection, setPriorManualSelection] = useState(null);
98
+ // Prompt catalog: lazily fetched on first open, then cached for the
99
+ // rest of the session. The catalog is static per backend deploy.
100
+ const [promptCatalog, setPromptCatalog] = useState(null);
101
+ const [promptCatalogOpen, setPromptCatalogOpen] = useState(false);
102
 
103
  const abortRef = useRef(null);
104
 
 
382
  storage.setConversationLimits({});
383
  }, []);
384
 
385
+ // ─── Prompt catalog (Transparency) ─────────────────────────────
386
+ const handleShowPromptCatalog = useCallback(async () => {
387
+ if (!promptCatalog) {
388
+ try {
389
+ const data = await fetchPromptCatalog();
390
+ setPromptCatalog(data);
391
+ } catch (err) {
392
+ console.error('Prompt catalog fetch failed:', err);
393
+ return;
394
+ }
395
+ }
396
+ setPromptCatalogOpen(true);
397
+ }, [promptCatalog]);
398
+
399
  // ─── Build start payload ────────────────────────────────────────
400
  // `participantsOverride`, if provided, replaces the
401
  // selectedParticipants-derived list (used by the auto-select flow
 
675
  onShowTableView={handleShowTableView}
676
  onShowCredentials={handleShowCredentials}
677
  hasCredentials={!!sessionId}
678
+ onShowPromptCatalog={handleShowPromptCatalog}
679
  onShowConversationLimits={handleShowConversationLimits}
680
  conversationLimitsOverridden={Object.keys(limitsOverrides).length > 0}
681
  onDownloadChatTxt={handleDownloadTxt}
 
754
  onChange={handleConversationLimitsChange}
755
  onResetAll={handleConversationLimitsResetAll}
756
  />
757
+ <PromptCatalogModal
758
+ isOpen={promptCatalogOpen}
759
+ catalog={promptCatalog}
760
+ onClose={() => setPromptCatalogOpen(false)}
761
+ />
762
  </div>
763
  );
764
  }
frontend/src/components/DevMenu.js CHANGED
@@ -2,6 +2,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react';
2
  import {
3
  ChevronRight, Download, Settings2, Search,
4
  Square, CheckSquare, UserPlus, Table2, ScrollText, SlidersHorizontal,
 
5
  } from 'lucide-react';
6
 
7
  /**
@@ -35,6 +36,7 @@ export default function DevMenu({
35
  onShowTableView,
36
  onShowCredentials,
37
  hasCredentials,
 
38
  onShowConversationLimits,
39
  conversationLimitsOverridden,
40
  onDownloadChatTxt,
@@ -227,6 +229,14 @@ export default function DevMenu({
227
  View Credential Summary…
228
  <ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5 }} />
229
  </button>
 
 
 
 
 
 
 
 
230
 
231
  <div className="dev-panel-divider" />
232
  <div className="dev-panel-label">Advanced</div>
 
2
  import {
3
  ChevronRight, Download, Settings2, Search,
4
  Square, CheckSquare, UserPlus, Table2, ScrollText, SlidersHorizontal,
5
+ BookOpen,
6
  } from 'lucide-react';
7
 
8
  /**
 
36
  onShowTableView,
37
  onShowCredentials,
38
  hasCredentials,
39
+ onShowPromptCatalog,
40
  onShowConversationLimits,
41
  conversationLimitsOverridden,
42
  onDownloadChatTxt,
 
229
  View Credential Summary…
230
  <ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5 }} />
231
  </button>
232
+ <button
233
+ onClick={() => { onShowPromptCatalog?.(); setOpen(false); }}
234
+ title="View every prompt template the orchestrator and participants use, grouped by phase."
235
+ >
236
+ <BookOpen size={14} className="dev-check-icon" />
237
+ View current chat prompts…
238
+ <ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5 }} />
239
+ </button>
240
 
241
  <div className="dev-panel-divider" />
242
  <div className="dev-panel-label">Advanced</div>
frontend/src/components/PromptCatalogModal.js ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Copy, Check, Download } from 'lucide-react';
3
+
4
+ /**
5
+ * Transparency modal that surfaces every prompt template the
6
+ * orchestrator and participants use during a chat, grouped by phase.
7
+ * Each item shows a humanized title, a 1-2 sentence purpose, the
8
+ * runtime template variables the backend interpolates, and the full
9
+ * template in a copy-able <pre> block. A "Download as .txt" button
10
+ * in the header dumps the whole catalog in a flat human-readable form.
11
+ *
12
+ * Same shell pattern as ConversationLimitsModal / CredentialSummaryModal.
13
+ */
14
+ export default function PromptCatalogModal({
15
+ isOpen,
16
+ catalog,
17
+ onClose,
18
+ }) {
19
+ // Note: hooks must run on every render, so this filename memo lives
20
+ // ABOVE the `if (!isOpen) return null` early return. The dependency
21
+ // on `isOpen` makes the filename timestamp regenerate each time the
22
+ // modal opens (i.e. each download gets a fresh stamp).
23
+ const filename = useMemo(() => {
24
+ const now = new Date();
25
+ const pad = (n) => String(n).padStart(2, '0');
26
+ return (
27
+ 'ccai-prompts-'
28
+ + `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
29
+ + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
30
+ + '.txt'
31
+ );
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ }, [isOpen]);
34
+
35
+ if (!isOpen) return null;
36
+
37
+ const handleDownload = () => {
38
+ if (!catalog) return;
39
+ const txt = renderCatalogAsText(catalog);
40
+ const blob = new Blob([txt], { type: 'text/plain;charset=utf-8' });
41
+ const url = URL.createObjectURL(blob);
42
+ const a = document.createElement('a');
43
+ a.href = url;
44
+ a.download = filename;
45
+ a.click();
46
+ URL.revokeObjectURL(url);
47
+ };
48
+
49
+ return (
50
+ <div className="ccai-credentials-overlay">
51
+ <div className="ccai-credentials-card">
52
+ <div className="ccai-credentials-header">
53
+ <div>
54
+ <h2>Current chat prompts</h2>
55
+ <div className="ccai-credentials-subtitle">
56
+ Every prompt template the orchestrator and participants
57
+ use during a chat, in conversation order. Variables in
58
+ {' '}<code>{'{braces}'}</code> are filled in at runtime.
59
+ </div>
60
+ </div>
61
+ <div className="ccai-tab-spacer" />
62
+ <button
63
+ className="btn-sm btn-outline"
64
+ onClick={handleDownload}
65
+ disabled={!catalog}
66
+ title="Download the full catalog as a .txt file"
67
+ >
68
+ <Download size={14} style={{ marginRight: 4 }} />
69
+ Download as .txt
70
+ </button>
71
+ <button className="modal-close" onClick={onClose}>&times;</button>
72
+ </div>
73
+
74
+ <div className="ccai-credentials-body">
75
+ {!catalog && (
76
+ <div className="ccai-credentials-empty">Loading prompts...</div>
77
+ )}
78
+ {catalog && (catalog.groups || []).map((g) => (
79
+ <div key={g.title} className="ccai-prompt-group">
80
+ <div className="ccai-prompt-group-title">{g.title}</div>
81
+ {(g.items || []).map((item) => (
82
+ <PromptItem key={item.name} item={item} />
83
+ ))}
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ function PromptItem({ item }) {
93
+ const [copied, setCopied] = useState(false);
94
+ const displayName = humanizeName(item.name);
95
+
96
+ const handleCopy = async () => {
97
+ try {
98
+ await navigator.clipboard.writeText(item.template || '');
99
+ setCopied(true);
100
+ setTimeout(() => setCopied(false), 1500);
101
+ } catch (err) {
102
+ console.warn('Clipboard copy failed:', err);
103
+ }
104
+ };
105
+
106
+ return (
107
+ <div className="ccai-prompt-item">
108
+ <div className="ccai-prompt-item-head">
109
+ <div className="ccai-prompt-item-title">{displayName}</div>
110
+ <button
111
+ className="ccai-prompt-copy-btn"
112
+ onClick={handleCopy}
113
+ title="Copy template to clipboard"
114
+ >
115
+ {copied ? <Check size={12} /> : <Copy size={12} />}
116
+ {copied ? 'Copied' : 'Copy'}
117
+ </button>
118
+ </div>
119
+ <div className="ccai-prompt-purpose">{item.purpose}</div>
120
+ {item.variables && item.variables.length > 0 && (
121
+ <div className="ccai-prompt-vars">
122
+ <strong>Variables:</strong>{' '}
123
+ {item.variables.map((v, i) => (
124
+ <span key={v}>
125
+ <code>{`{${v}}`}</code>
126
+ {i < item.variables.length - 1 ? ', ' : ''}
127
+ </span>
128
+ ))}
129
+ </div>
130
+ )}
131
+ <pre className="ccai-prompt-template">{item.template}</pre>
132
+ </div>
133
+ );
134
+ }
135
+
136
+ /**
137
+ * "INITIAL_OPINION_PROMPT" -> "Initial Opinion Prompt".
138
+ * Splits on underscores, lowercases everything, capitalizes each word.
139
+ * Drops nothing (e.g. "Prompt" suffix stays) so the displayed name
140
+ * still matches the constant a developer might grep for.
141
+ */
142
+ function humanizeName(name) {
143
+ if (!name) return '';
144
+ return name
145
+ .split('_')
146
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
147
+ .join(' ');
148
+ }
149
+
150
+ /**
151
+ * Flat human-readable .txt dump used by the Download button. Matches
152
+ * the spec format: title banner, per-group separator, per-item header
153
+ * with purpose + variables, then the indented template body.
154
+ */
155
+ function renderCatalogAsText(catalog) {
156
+ const now = new Date().toISOString();
157
+ const lines = [];
158
+ const banner = '═'.repeat(64);
159
+ lines.push(banner);
160
+ lines.push('CCAI Vibe Demo — Current chat prompts');
161
+ lines.push(`Generated: ${now}`);
162
+ lines.push(banner);
163
+ lines.push('');
164
+
165
+ for (const group of catalog.groups || []) {
166
+ const sep = '─'.repeat(12);
167
+ lines.push(`${sep} ${group.title} ${sep}`);
168
+ lines.push('');
169
+ for (const item of (group.items || [])) {
170
+ lines.push(`## ${humanizeName(item.name)}`);
171
+ lines.push(`Purpose: ${item.purpose}`);
172
+ if (item.variables && item.variables.length > 0) {
173
+ lines.push(
174
+ 'Variables: '
175
+ + item.variables.map(v => `{${v}}`).join(', '),
176
+ );
177
+ }
178
+ lines.push('');
179
+ // Indent each template line by 4 spaces so the body is
180
+ // visually distinct from the metadata in a plain-text viewer.
181
+ for (const ln of (item.template || '').split('\n')) {
182
+ lines.push(' ' + ln);
183
+ }
184
+ lines.push('');
185
+ }
186
+ }
187
+
188
+ return lines.join('\n');
189
+ }
frontend/src/styles/ccai.css CHANGED
@@ -1175,3 +1175,112 @@
1175
  color: var(--text-primary);
1176
  font-size: 13px;
1177
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1175
  color: var(--text-primary);
1176
  font-size: 13px;
1177
  }
1178
+
1179
+
1180
+ /* ── Prompt catalog modal ─────────────────────────────────────────
1181
+ Re-uses .ccai-credentials-overlay/.ccai-credentials-card shell.
1182
+ The classes below style the per-group sections and per-item
1183
+ purpose / variables / template blocks. */
1184
+
1185
+ .ccai-prompt-group {
1186
+ display: flex;
1187
+ flex-direction: column;
1188
+ gap: 10px;
1189
+ padding: 12px 14px;
1190
+ background: var(--bg-secondary);
1191
+ border: 1px solid var(--border-primary);
1192
+ border-radius: 10px;
1193
+ }
1194
+
1195
+ .ccai-prompt-group + .ccai-prompt-group {
1196
+ margin-top: 4px;
1197
+ }
1198
+
1199
+ .ccai-prompt-group-title {
1200
+ font-size: 13px;
1201
+ font-weight: 600;
1202
+ color: var(--text-primary);
1203
+ text-transform: uppercase;
1204
+ letter-spacing: 0.04em;
1205
+ border-bottom: 1px solid var(--border-primary);
1206
+ padding-bottom: 6px;
1207
+ }
1208
+
1209
+ .ccai-prompt-item {
1210
+ display: flex;
1211
+ flex-direction: column;
1212
+ gap: 6px;
1213
+ padding: 8px 0;
1214
+ }
1215
+
1216
+ .ccai-prompt-item + .ccai-prompt-item {
1217
+ border-top: 1px dashed var(--border-primary);
1218
+ padding-top: 12px;
1219
+ }
1220
+
1221
+ .ccai-prompt-item-head {
1222
+ display: flex;
1223
+ align-items: center;
1224
+ justify-content: space-between;
1225
+ gap: 12px;
1226
+ }
1227
+
1228
+ .ccai-prompt-item-title {
1229
+ font-size: 14px;
1230
+ font-weight: 600;
1231
+ color: var(--text-primary);
1232
+ }
1233
+
1234
+ .ccai-prompt-copy-btn {
1235
+ display: inline-flex;
1236
+ align-items: center;
1237
+ gap: 4px;
1238
+ font-size: 12px;
1239
+ padding: 3px 8px;
1240
+ background: transparent;
1241
+ border: 1px solid var(--border-primary);
1242
+ color: var(--text-secondary);
1243
+ border-radius: 6px;
1244
+ cursor: pointer;
1245
+ }
1246
+
1247
+ .ccai-prompt-copy-btn:hover {
1248
+ background: var(--card-bg);
1249
+ color: var(--text-primary);
1250
+ }
1251
+
1252
+ .ccai-prompt-purpose {
1253
+ font-size: 12.5px;
1254
+ color: var(--text-secondary);
1255
+ line-height: 1.45;
1256
+ }
1257
+
1258
+ .ccai-prompt-vars {
1259
+ font-size: 12px;
1260
+ color: var(--text-secondary);
1261
+ }
1262
+
1263
+ .ccai-prompt-vars code {
1264
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1265
+ background: var(--bg-primary);
1266
+ border: 1px solid var(--border-primary);
1267
+ border-radius: 4px;
1268
+ padding: 1px 4px;
1269
+ font-size: 11.5px;
1270
+ }
1271
+
1272
+ .ccai-prompt-template {
1273
+ background: var(--bg-primary);
1274
+ border: 1px solid var(--border-primary);
1275
+ border-radius: 6px;
1276
+ padding: 10px 12px;
1277
+ margin: 0;
1278
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1279
+ font-size: 12px;
1280
+ line-height: 1.45;
1281
+ color: var(--text-primary);
1282
+ white-space: pre-wrap;
1283
+ overflow-x: auto;
1284
+ max-height: 280px;
1285
+ overflow-y: auto;
1286
+ }
frontend/src/utils/api.js CHANGED
@@ -179,6 +179,17 @@ export async function fetchTableView(sessionId) {
179
  return resp.json();
180
  }
181
 
 
 
 
 
 
 
 
 
 
 
 
182
  /**
183
  * Ask the backend to pick the top `count` participants from the
184
  * candidate pool by relevance to the question. Used by the
 
179
  return resp.json();
180
  }
181
 
182
+ /**
183
+ * Fetch the catalog of every prompt template the orchestrator and
184
+ * participants use, grouped by phase and annotated with purpose and
185
+ * runtime variables. Backs the "View current chat prompts" modal.
186
+ */
187
+ export async function fetchPromptCatalog() {
188
+ const resp = await fetch(`${API_BASE}/api/chat/prompts/catalog`, { cache: 'no-store' });
189
+ if (!resp.ok) throw new Error('Failed to fetch prompt catalog');
190
+ return resp.json();
191
+ }
192
+
193
  /**
194
  * Ask the backend to pick the top `count` participants from the
195
  * candidate pool by relevance to the question. Used by the