File size: 7,229 Bytes
0bb4dfa
11bf9b7
0bb4dfa
 
 
 
 
 
 
 
 
 
 
 
d58dc6d
0bb4dfa
 
af5d1df
 
0bb4dfa
af5d1df
 
 
 
 
 
0bb4dfa
 
 
 
53edcb8
 
 
 
 
 
 
 
 
0bb4dfa
af5d1df
 
 
 
 
 
 
 
 
 
0bb4dfa
 
 
 
 
 
 
 
 
d58dc6d
0bb4dfa
 
 
 
 
 
 
 
 
d58dc6d
0bb4dfa
4944128
11bf9b7
0bb4dfa
d58dc6d
 
 
 
 
 
 
 
4944128
 
 
 
 
 
 
 
 
 
0bb4dfa
 
 
11bf9b7
 
 
0bb4dfa
 
4944128
 
 
 
 
 
0bb4dfa
 
4944128
0bb4dfa
4944128
0bb4dfa
 
 
4944128
 
 
 
 
 
 
11bf9b7
 
 
 
 
 
 
 
 
 
 
 
0bb4dfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11bf9b7
 
 
 
 
 
 
 
 
0bb4dfa
11bf9b7
 
 
 
 
 
 
 
 
 
4944128
 
 
 
 
 
 
 
d58dc6d
11bf9b7
4944128
 
 
 
 
 
 
 
 
11bf9b7
 
 
0bb4dfa
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
import React, { useState } from 'react';
import { ChevronDown, ChevronRight, User, X } from 'lucide-react';

/**
 * Replaces LLMChats3's LLMSelector. Lists the user's currently selected
 * participants with:
 *   - Toggle slider (on/off, doesn't deselect)
 *   - Accordion showing the LLM and the persona prompt
 *   - "Remove" button when the participant is off (does deselect)
 */
export default function ParticipantSidebar({
  participants,
  enabledMap,
  modelAssignments,
  neonPromptByModelId = {},
  onToggleEnabled,
  onRemove,
  autoSelectMode,
  maxParticipants,
}) {
  // In auto-select mode with no chat in progress, the sidebar shows a
  // placeholder explaining the deferred selection. Once the chat
  // starts, App.js populates `participants` with the LLM-chosen set
  // and the regular cards render normally.
  const showAutoPlaceholder = autoSelectMode && participants.length === 0;

  return (
    <aside className="sidebar ccai-sidebar">
      <div className="ccai-sidebar-header">
        <h2 className="sidebar-title">Participants</h2>
        {(showAutoPlaceholder || participants.length === 0) && (
          <div className="ccai-sidebar-help">
            {showAutoPlaceholder ? (
              <em>Auto-select is on.</em>
            ) : (
              <em>Use the Participants dropdown in the header to add some.</em>
            )}
          </div>
        )}
      </div>
      {showAutoPlaceholder && (
        <div className="ccai-sidebar-autoselect-empty">
          <strong>Auto-select: {maxParticipants} participants</strong>
          <div style={{ marginTop: 4 }}>
            When you start the chat, the orchestrator will pick the
            {' '}<strong>{maxParticipants}</strong> participants whose
            expertise best fits your question.
          </div>
        </div>
      )}
      {participants.map((p) => {
        const enabled = enabledMap[p.participant_id] !== false;
        const modelOverride = modelAssignments[p.participant_id];
        return (
          <ParticipantCard
            key={p.participant_id}
            participant={p}
            enabled={enabled}
            modelOverride={modelOverride}
            neonPromptByModelId={neonPromptByModelId}
            onToggleEnabled={() => onToggleEnabled(p.participant_id, !enabled)}
            onRemove={() => onRemove(p.participant_id)}
          />
        );
      })}
    </aside>
  );
}

function ParticipantCard({ participant, enabled, modelOverride, neonPromptByModelId, onToggleEnabled, onRemove }) {
  const [open, setOpen] = useState(false);
  const [promptExpanded, setPromptExpanded] = useState(false);
  const isHuman = participant.kind === 'human';

  const effectiveModelId = modelOverride
    || participant.default_model_id
    || (participant.kind === 'neon' ? participant.participant_id : '');
  const personaPrompt = (effectiveModelId.startsWith('neon:')
    && neonPromptByModelId[effectiveModelId])
    || participant.role_prompt
    || '';

  const PROMPT_PREVIEW_CHARS = 280;
  const promptIsLong = personaPrompt.length > PROMPT_PREVIEW_CHARS;

  const handleToggleOpen = () => {
    setOpen((wasOpen) => {
      if (wasOpen) setPromptExpanded(false);
      return !wasOpen;
    });
  };

  return (
    <div
      className={
        'ccai-participant-card'
        + (enabled ? '' : ' ccai-participant-card-off')
        + (isHuman ? ' ccai-participant-card-human' : '')
      }
    >
      <div
        className={
          'ccai-participant-row'
          + (open ? ' ccai-participant-row--expanded' : '')
        }
      >
        <button
          className="ccai-accordion-chevron"
          onClick={handleToggleOpen}
          aria-label="Toggle participant details"
          aria-expanded={open}
        >
          {open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
        </button>
        <div
          className={
            'ccai-participant-name'
            + (open ? ' ccai-participant-name--expanded' : '')
          }
          title={!open ? participant.name : undefined}
        >
          {isHuman && (
            <User
              size={12}
              strokeWidth={2.5}
              style={{ marginRight: 4, verticalAlign: '-2px' }}
            />
          )}
          {participant.name}
          {isHuman && (
            <span className="ccai-participant-human-tag">Human</span>
          )}
        </div>
        <div className="ccai-participant-controls">
          {enabled ? (
            <label className="ccai-toggle" title="Toggle participation">
              <input
                type="checkbox"
                checked={true}
                onChange={onToggleEnabled}
              />
              <span className="ccai-toggle-slider"></span>
            </label>
          ) : (
            <button
              className="btn-sm ccai-remove-btn"
              onClick={onRemove}
              title="Remove from this conversation"
            >
              <X size={12} /> Remove
            </button>
          )}
        </div>
      </div>
      {!enabled && (
        <div className="ccai-participant-row ccai-participant-row-secondary">
          <button
            className="btn-sm btn-outline ccai-reenable-btn"
            onClick={onToggleEnabled}
          >
            Re-enable
          </button>
        </div>
      )}
      {open && (
        <div className="ccai-participant-body">
          {isHuman ? (
            <div className="ccai-participant-field">
              <div className="ccai-participant-field-label">Role</div>
              <div className="ccai-participant-field-value">
                In-the-loop human participant. The orchestrator pauses
                for your input when it's your turn. Edit your name and
                credential summary from the "Human:&nbsp;…" button in
                the header.
              </div>
            </div>
          ) : (
            <>
              <div className="ccai-participant-field">
                <div className="ccai-participant-field-label">LLM</div>
                <div className="ccai-participant-field-value">
                  {modelOverride || participant.default_model_id || participant.model_display || ''}
                </div>
              </div>
              <div className="ccai-participant-field">
                <div className="ccai-participant-field-label">Persona prompt</div>
                <pre
                  className={
                    'ccai-participant-prompt'
                    + (promptIsLong && !promptExpanded
                      ? ' ccai-participant-prompt--preview'
                      : '')
                  }
                >
                  {personaPrompt || '(no prompt set)'}
                </pre>
                {promptIsLong && (
                  <button
                    type="button"
                    className="ccai-participant-prompt-toggle"
                    onClick={() => setPromptExpanded(v => !v)}
                  >
                    {promptExpanded ? 'Show less' : 'Show full prompt'}
                  </button>
                )}
              </div>
            </>
          )}
        </div>
      )}
    </div>
  );
}