Skip to content

Commit 8592e87

Browse files
bokelleyclaude
andcommitted
Merge main and resolve conflict in admin-org-detail.html
Keep both the Escape key handler from main and the link_domain query parameter handler from this branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
2 parents cde3743 + 4ab2e14 commit 8592e87

File tree

9 files changed

+665
-30
lines changed

9 files changed

+665
-30
lines changed

.changeset/fix-feedback-modal.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
Fix feedback modal not displaying saved feedback when viewing threads

.changeset/spicy-animals-fail.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.changeset/tired-rockets-lie.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

server/public/admin-addie.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3146,6 +3146,12 @@ <h4>Pattern Identified</h4>
31463146
document.getElementById('thread-feedback-panel').style.display = 'block';
31473147
clearThreadFeedback();
31483148

3149+
// Load existing feedback from last assistant message if present
3150+
const lastAssistantMsg = [...data.messages].reverse().find(m => m.role === 'assistant');
3151+
if (lastAssistantMsg && lastAssistantMsg.rating) {
3152+
loadExistingFeedback(lastAssistantMsg);
3153+
}
3154+
31493155
// Show actions panel
31503156
const actionsEl = document.getElementById('conversation-actions');
31513157
actionsEl.style.display = 'block';
@@ -3703,6 +3709,36 @@ <h4>Pattern Identified</h4>
37033709
document.getElementById('thread-feedback-notes').value = '';
37043710
}
37053711

3712+
function loadExistingFeedback(message) {
3713+
// Set sentiment based on rating (5 = positive, 1 = negative)
3714+
// Rating 3 is intentionally neutral and leaves sentiment unset
3715+
if (message.rating >= 4) {
3716+
setThreadRating('positive');
3717+
} else if (message.rating <= 2) {
3718+
setThreadRating('negative');
3719+
}
3720+
3721+
// Load feedback tags (validate against known tags for security)
3722+
const validTags = ['accurate', 'helpful', 'well_cited', 'good_tone', 'inaccurate', 'missing_info', 'wrong_source', 'too_verbose', 'too_brief', 'wrong_tone'];
3723+
if (message.feedback_tags && Array.isArray(message.feedback_tags)) {
3724+
message.feedback_tags.forEach(tag => {
3725+
if (validTags.includes(tag)) {
3726+
const tagEl = document.querySelector(`#thread-feedback-tags .feedback-tag[data-tag="${tag}"]`);
3727+
if (tagEl) {
3728+
tagEl.classList.add('selected');
3729+
threadFeedbackState.tags.push(tag);
3730+
}
3731+
}
3732+
});
3733+
}
3734+
3735+
// Load notes (rating_notes contains the text feedback)
3736+
if (message.rating_notes) {
3737+
document.getElementById('thread-feedback-notes').value = message.rating_notes;
3738+
threadFeedbackState.notes = message.rating_notes;
3739+
}
3740+
}
3741+
37063742
async function saveThreadFeedback() {
37073743
const notes = document.getElementById('thread-feedback-notes').value;
37083744
threadFeedbackState.notes = notes;

server/public/admin-org-detail.html

Lines changed: 191 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@
174174
color: var(--color-text-muted);
175175
margin-left: var(--space-2);
176176
}
177+
.member-name-link {
178+
color: var(--color-brand);
179+
text-decoration: none;
180+
cursor: pointer;
181+
}
182+
.member-name-link:hover {
183+
text-decoration: underline;
184+
}
177185

178186
/* Working groups */
179187
.wg-badge {
@@ -1009,6 +1017,19 @@ <h2>Add Domain</h2>
10091017
</div>
10101018
</div>
10111019

1020+
<!-- User Context Modal -->
1021+
<div id="userContextModal" class="modal">
1022+
<div class="modal-content" style="max-width: 700px;">
1023+
<div class="modal-header">
1024+
<h2 id="userContextModalTitle">Member Context</h2>
1025+
<button class="modal-close" onclick="closeUserContextModal()">&times;</button>
1026+
</div>
1027+
<div id="userContextModalBody" style="padding: var(--space-4);">
1028+
<!-- Content populated by JavaScript -->
1029+
</div>
1030+
</div>
1031+
</div>
1032+
10121033
<script>
10131034
let orgData = null;
10141035
let stakeholders = [];
@@ -1168,11 +1189,16 @@ <h2>Add Domain</h2>
11681189
count.textContent = `(${orgData.members.length})`;
11691190
list.innerHTML = orgData.members.map(member => {
11701191
const name = [member.firstName, member.lastName].filter(Boolean).join(' ') || member.email;
1192+
const escapedName = escapeHtml(name);
1193+
const escapedEmail = escapeHtml(member.email);
1194+
const memberId = member.id;
11711195
return `
11721196
<li>
1173-
${name}
1174-
<span class="member-role">${member.role}</span>
1175-
<br><span style="font-size: var(--text-xs); color: var(--color-text-muted);">${member.email}</span>
1197+
${memberId
1198+
? `<a href="#" class="member-name-link" onclick="showUserContext('${escapeHtml(memberId)}', 'workos', '${escapedName}'); return false;">${escapedName}</a>`
1199+
: escapedName}
1200+
<span class="member-role">${escapeHtml(member.role)}</span>
1201+
<br><span style="font-size: var(--text-xs); color: var(--color-text-muted);">${escapedEmail}</span>
11761202
</li>
11771203
`;
11781204
}).join('');
@@ -1947,13 +1973,175 @@ <h2>Add Domain</h2>
19471973
}
19481974
}
19491975

1976+
// ========================================
1977+
// USER CONTEXT MODAL
1978+
// ========================================
1979+
1980+
async function showUserContext(userId, type, userName) {
1981+
const modal = document.getElementById('userContextModal');
1982+
const modalTitle = document.getElementById('userContextModalTitle');
1983+
const modalBody = document.getElementById('userContextModalBody');
1984+
1985+
modalTitle.textContent = `Context: ${userName}`;
1986+
modalBody.innerHTML = '<div style="text-align: center; padding: var(--space-8);"><p>Loading context...</p></div>';
1987+
modal.style.display = 'block';
1988+
1989+
try {
1990+
const response = await fetch(`/api/admin/users/${userId}/context?type=${type}`);
1991+
if (!response.ok) {
1992+
throw new Error('Failed to fetch context');
1993+
}
1994+
const context = await response.json();
1995+
modalBody.innerHTML = renderUserContext(context, userId);
1996+
} catch (error) {
1997+
console.error('Error fetching context:', error);
1998+
modalBody.innerHTML = `
1999+
<div style="text-align: center; padding: var(--space-8); color: var(--color-error-600);">
2000+
<p>Failed to load context.</p>
2001+
<button class="btn btn-secondary" onclick="showUserContext('${escapeHtml(userId)}', '${escapeHtml(type)}', '${escapeHtml(userName)}')">Retry</button>
2002+
</div>
2003+
`;
2004+
}
2005+
}
2006+
2007+
function closeUserContextModal() {
2008+
document.getElementById('userContextModal').style.display = 'none';
2009+
}
2010+
2011+
function renderUserContext(context, userId) {
2012+
let html = '';
2013+
2014+
// Addie's Goal Section
2015+
if (context.addie_goal) {
2016+
html += '<div style="background: var(--color-primary-50); border-radius: var(--radius-md); padding: var(--space-4); margin-bottom: var(--space-4);">';
2017+
html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Addie\'s Goal</h3>';
2018+
html += `<div style="font-weight: var(--font-medium); color: var(--color-text-heading);">${escapeHtml(context.addie_goal.goal_name || context.addie_goal.goal_key)}</div>`;
2019+
if (context.addie_goal.reasoning) {
2020+
html += `<p style="font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: var(--space-1);">${escapeHtml(context.addie_goal.reasoning)}</p>`;
2021+
}
2022+
html += '</div>';
2023+
}
2024+
2025+
// Identity Section
2026+
html += '<div style="margin-bottom: var(--space-4);">';
2027+
html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Identity</h3>';
2028+
html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-3);">';
2029+
2030+
if (context.workos_user) {
2031+
html += renderContextItem('Email', context.workos_user.email);
2032+
const name = `${context.workos_user.first_name || ''} ${context.workos_user.last_name || ''}`.trim();
2033+
if (name) html += renderContextItem('Name', name);
2034+
}
2035+
2036+
if (context.slack_user && !context.workos_user) {
2037+
html += renderContextItem('Email', context.slack_user.email || 'Not set');
2038+
html += renderContextItem('Slack Name', context.slack_user.display_name || context.slack_user.real_name || 'Not set');
2039+
}
2040+
2041+
// Status badges
2042+
const statusItems = [];
2043+
if (context.is_member) statusItems.push('<span style="display: inline-block; padding: 2px 8px; background: var(--color-success-100); color: var(--color-success-700); border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-medium);">Member</span>');
2044+
if (context.slack_linked) statusItems.push('<span style="display: inline-block; padding: 2px 8px; background: #4A154B20; color: #4A154B; border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-medium);">Slack</span>');
2045+
2046+
if (statusItems.length > 0) {
2047+
html += `<div style="background: var(--color-gray-50); padding: var(--space-3); border-radius: var(--radius-md); grid-column: 1 / -1;">
2048+
<div style="font-size: var(--text-xs); color: var(--color-text-muted); margin-bottom: var(--space-1);">Status</div>
2049+
<div style="display: flex; gap: var(--space-1);">${statusItems.join(' ')}</div>
2050+
</div>`;
2051+
}
2052+
2053+
html += '</div></div>';
2054+
2055+
// Organization Section
2056+
if (context.organization) {
2057+
html += '<div style="margin-bottom: var(--space-4);">';
2058+
html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Organization</h3>';
2059+
html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-3);">';
2060+
html += renderContextItem('Company', context.organization.name);
2061+
const subStatus = context.organization.subscription_status || 'None';
2062+
html += renderContextItem('Subscription', subStatus);
2063+
html += '</div></div>';
2064+
}
2065+
2066+
// Activity Section
2067+
html += '<div style="margin-bottom: var(--space-4);">';
2068+
html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Activity</h3>';
2069+
html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-3);">';
2070+
2071+
if (context.engagement) {
2072+
html += renderContextItem('Dashboard Logins (30d)', context.engagement.login_count_30d.toString());
2073+
if (context.engagement.last_login) {
2074+
html += renderContextItem('Last Login', new Date(context.engagement.last_login).toLocaleDateString());
2075+
}
2076+
html += renderContextItem('Email Clicks (30d)', context.engagement.email_click_count_30d.toString());
2077+
}
2078+
2079+
if (context.slack_activity) {
2080+
html += renderContextItem('Slack Messages (30d)', context.slack_activity.total_messages_30d.toString());
2081+
html += renderContextItem('Reactions Given', context.slack_activity.total_reactions_30d.toString());
2082+
if (context.slack_activity.last_activity_at) {
2083+
html += renderContextItem('Last Slack Activity', new Date(context.slack_activity.last_activity_at).toLocaleDateString());
2084+
}
2085+
}
2086+
2087+
html += '</div></div>';
2088+
2089+
// Working Groups Section
2090+
if (context.working_groups && context.working_groups.length > 0) {
2091+
html += '<div style="margin-bottom: var(--space-4);">';
2092+
html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Working Groups</h3>';
2093+
html += '<div style="display: flex; flex-wrap: wrap; gap: var(--space-1);">';
2094+
context.working_groups.forEach(wg => {
2095+
html += `<span style="display: inline-block; padding: 2px 8px; background: var(--color-primary-100); color: var(--color-primary-700); border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-medium);">${escapeHtml(wg.name)}${wg.is_leader ? ' (Leader)' : ''}</span>`;
2096+
});
2097+
html += '</div></div>';
2098+
}
2099+
2100+
// Insights Section
2101+
if (context.insights && context.insights.length > 0) {
2102+
html += '<div style="margin-bottom: var(--space-4);">';
2103+
html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Insights</h3>';
2104+
html += '<div style="max-height: 150px; overflow-y: auto;">';
2105+
context.insights.forEach(insight => {
2106+
html += `<div style="background: var(--color-gray-50); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2);">
2107+
<div style="font-weight: var(--font-medium); font-size: var(--text-xs); color: var(--color-text-heading);">${escapeHtml(insight.type_name || 'Insight')}</div>
2108+
<div style="font-size: var(--text-sm); color: var(--color-text-primary);">${escapeHtml(insight.value)}</div>
2109+
</div>`;
2110+
});
2111+
html += '</div></div>';
2112+
}
2113+
2114+
// Link to full user view
2115+
html += `<div style="margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--color-gray-200);">
2116+
<a href="/admin/users" class="btn btn-secondary" style="text-decoration: none;">View in Users Page</a>
2117+
</div>`;
2118+
2119+
return html;
2120+
}
2121+
2122+
function renderContextItem(label, value) {
2123+
return `
2124+
<div style="background: var(--color-gray-50); padding: var(--space-3); border-radius: var(--radius-md);">
2125+
<div style="font-size: var(--text-xs); color: var(--color-text-muted); margin-bottom: var(--space-1);">${escapeHtml(label)}</div>
2126+
<div style="font-size: var(--text-sm); color: var(--color-text-heading); font-weight: var(--font-medium);">${escapeHtml(value)}</div>
2127+
</div>
2128+
`;
2129+
}
2130+
19502131
// Close modal when clicking outside
19512132
window.onclick = function(event) {
19522133
if (event.target.classList.contains('modal')) {
19532134
event.target.style.display = 'none';
19542135
}
19552136
}
19562137

2138+
// Close modals on Escape key
2139+
document.addEventListener('keydown', function(e) {
2140+
if (e.key === 'Escape') {
2141+
closeUserContextModal();
2142+
}
2143+
});
2144+
19572145
// Check for link_domain query parameter
19582146
async function checkLinkDomainParam() {
19592147
const params = new URLSearchParams(window.location.search);

server/public/admin-users.html

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@
153153
font-size: var(--text-sm);
154154
color: var(--color-text-muted);
155155
}
156+
a.user-org {
157+
color: var(--color-brand);
158+
text-decoration: none;
159+
}
160+
a.user-org:hover {
161+
text-decoration: underline;
162+
}
156163
.group-badges {
157164
display: flex;
158165
flex-wrap: wrap;
@@ -1385,7 +1392,9 @@ <h2 id="contextModalTitle">Member Context</h2>
13851392
<div class="user-email">${escapeHtml(displayEmail)}</div>
13861393
</td>
13871394
<td>
1388-
<span class="user-org">${escapeHtml(displayOrg) || '<em style="color: var(--color-text-muted);">No org</em>'}</span>
1395+
${user.org_id && displayOrg
1396+
? `<a href="/admin/organizations/${encodeURIComponent(user.org_id)}" class="user-org">${escapeHtml(displayOrg)}</a>`
1397+
: '<span class="user-org"><em style="color: var(--color-text-muted);">No org</em></span>'}
13891398
</td>
13901399
<td>
13911400
${engagementHtml}

0 commit comments

Comments
 (0)