Skip to content

Commit 113c623

Browse files
lambasuclaude
andcommitted
Add Group Intelligence prototype (Prototype 3)
Classifier-driven agent triggering without @mention: Jira detects a relevant message mid-standup (82% confidence), sends a targeted private message to the user with Yes/Skip actions, and on confirm posts a group response with the ticket link card. Includes: contact 35 (Northwind sprint sync), seeded standup messages, ClassifierBadge, MonitoringIndicator, targeted action buttons, and scripted group-response flow in ChatView. FRE updated with Group Intelligence narrative. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 285422f commit 113c623

6 files changed

Lines changed: 335 additions & 31 deletions

File tree

src/App.jsx

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import './App.css'
1010

1111
export default function App() {
1212
const [activeView, setActiveView] = useState('chat') // 'chat' | 'activity'
13-
const [activeChatId, setActiveChatId] = useState(23)
14-
const [readChatIds, setReadChatIds] = useState(() => new Set([23]))
13+
const [activeChatId, setActiveChatId] = useState(35)
14+
const [readChatIds, setReadChatIds] = useState(() => new Set([35]))
1515
const [sessions, setSessions] = useState(initialSessions)
1616
const [dynamicSessionMessages, setDynamicSessionMessages] = useState({})
1717
// Activity feed: persist which events the user has opened so unread decorations clear.
@@ -115,49 +115,51 @@ export default function App() {
115115
</div>
116116
{showFre && (
117117
<FreModal
118-
title="Day-one agent onboarding"
119-
subtitle="An agent added to a group chat already knows who's in the room, what they're working on, and what it can dobefore anyone says a word."
118+
title="Group intelligence for Teams agents"
119+
subtitle="Agents monitor group conversations and decide when to engagewithout waiting to be @mentioned."
120120
onDismiss={dismissFre}
121121
>
122122
<h3 className="fre-section-title">Today</h3>
123123
<p>
124-
When you add an agent to a group chat, the first thing you have to
125-
do is brief it. Who's on the team. What the project is. What tools
126-
you use. What decisions have already been made. It's a tax the team
127-
pays every time — and it scales badly as chats get longer and
128-
teams get larger.
124+
Agents in Teams only respond when explicitly @mentioned. In a
125+
real standup or sprint sync, the right information often surfaces
126+
mid-conversation — not because someone called for it, but because
127+
context made it relevant. Requiring an @mention means value gets
128+
missed every time someone forgets to invoke the right agent.
129129
</p>
130130

131131
<h3 className="fre-section-title">Problem</h3>
132132
<p>
133-
Agents are context-blind on arrival. They can be capable once
134-
grounded, but grounding them is manual, inconsistent, and
135-
invisible to everyone else in the chat. The agent that arrives
136-
asking "how can I help?" puts the burden on the team to narrate
137-
work that's already been done.
133+
Teams with multiple agents create a coordination burden: users
134+
must remember which agent owns which domain, and manually invoke
135+
each one at the right moment. Agents are passive by default —
136+
capable only when called.
138137
</p>
139138

140139
<h3 className="fre-section-title">Solution</h3>
141140
<p>
142-
When an agent joins a group chat, it reads the thread first. It
143-
surfaces what it learned — stakeholders and their roles, tools
144-
already in use, files shared, open decisions — in a single
145-
structured welcome message. Every action it proposes is grounded
146-
in the actual work happening in the chat, not a generic prompt.
141+
A classifier-driven decision layer runs continuously on group chat
142+
messages. Each agent registers its own topic scope. When a message
143+
arrives, the classifier scores it for relevance — no LLM required.
144+
High confidence: agent responds directly. "Maybe" band: agent sends
145+
a <strong>targeted message</strong> (visible only to the relevant
146+
user) asking for confirmation before engaging the group.
147147
</p>
148148
<p>
149-
Open the <strong>Northwind launch</strong> chat in the left panel.
150-
Scroll to the bottom: Claude was just added and sent its first
151-
message — already knowing the team, the tools, and the one
152-
launch-blocking ticket that needs attention right now.
149+
Open the <strong>Northwind sprint sync</strong> chat. Scroll to the
150+
bottom: Olivia @mentioned Jira earlier to create a ticket. A few
151+
messages later, James asks about the bug without any @mention. Jira's
152+
classifier detected the relevance — confidence 82% — and sent James
153+
a private targeted message asking whether to respond to the group.
153154
</p>
154155

155156
<h3 className="fre-section-title">What this unlocks</h3>
156157
<p>
157-
Zero-briefing agent adoption. The team doesn't stop their work to
158-
onboard a new tool — the tool onboards itself. And because the
159-
agent's first message shows its reasoning, the team can verify
160-
what it knows before trusting what it does.
158+
Agents become ambient collaborators that surface help at the right
159+
moment without spamming or requiring user-initiated prompts. The
160+
targeted message pattern gives users control over when the agent
161+
engages — graceful uncertainty handling that keeps the experience
162+
from feeling intrusive.
161163
</p>
162164
</FreModal>
163165
)}

src/components/ChatView.css

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,3 +823,143 @@
823823
color: #424242;
824824
font-weight: 500;
825825
}
826+
827+
/* ── Classifier badge (group intelligence targeted messages) ─────────── */
828+
.classifier-badge {
829+
background: #F0F4FF;
830+
border: 1px solid #C8D0F0;
831+
border-radius: 6px;
832+
padding: 8px 10px;
833+
margin-bottom: 8px;
834+
display: flex;
835+
flex-direction: column;
836+
gap: 5px;
837+
}
838+
839+
.classifier-badge-header {
840+
display: flex;
841+
align-items: center;
842+
gap: 6px;
843+
}
844+
845+
.classifier-icon {
846+
color: #5B5FC7;
847+
flex-shrink: 0;
848+
}
849+
850+
.classifier-label {
851+
font-size: 11px;
852+
font-weight: 600;
853+
color: #5B5FC7;
854+
flex: 1;
855+
}
856+
857+
.classifier-confidence {
858+
font-size: 11px;
859+
font-weight: 700;
860+
}
861+
862+
.classifier-bar-track {
863+
height: 3px;
864+
background: #D8DCF0;
865+
border-radius: 2px;
866+
overflow: hidden;
867+
}
868+
869+
.classifier-bar-fill {
870+
height: 100%;
871+
border-radius: 2px;
872+
transition: width 0.3s ease;
873+
}
874+
875+
.classifier-meta {
876+
font-size: 11px;
877+
color: #707070;
878+
display: flex;
879+
flex-wrap: wrap;
880+
gap: 3px;
881+
align-items: baseline;
882+
}
883+
884+
.classifier-keyword {
885+
color: #4448B3;
886+
font-weight: 500;
887+
}
888+
889+
.classifier-context {
890+
color: #888;
891+
}
892+
893+
/* ── Targeted action buttons ─────────────────────────────────────────── */
894+
.targeted-actions {
895+
display: flex;
896+
gap: 8px;
897+
margin-top: 10px;
898+
}
899+
900+
.targeted-action-btn {
901+
padding: 5px 14px;
902+
border-radius: 4px;
903+
font-size: 13px;
904+
font-weight: 500;
905+
font-family: inherit;
906+
cursor: pointer;
907+
border: 1px solid transparent;
908+
transition: background 0.1s, border-color 0.1s;
909+
}
910+
911+
.targeted-action-btn-primary {
912+
background: #5B5FC7;
913+
color: #fff;
914+
border-color: #5B5FC7;
915+
}
916+
917+
.targeted-action-btn-primary:hover {
918+
background: #4448B3;
919+
border-color: #4448B3;
920+
}
921+
922+
.targeted-action-btn-secondary {
923+
background: transparent;
924+
color: #424242;
925+
border-color: #C8C8C8;
926+
}
927+
928+
.targeted-action-btn-secondary:hover {
929+
background: #F0F0F0;
930+
}
931+
932+
/* ── Agent monitoring indicator ──────────────────────────────────────── */
933+
.monitoring-indicator {
934+
display: flex;
935+
align-items: center;
936+
gap: 6px;
937+
padding: 5px 16px;
938+
background: #F5F5FF;
939+
border-top: 1px solid #E8E8F0;
940+
font-size: 11.5px;
941+
color: #616161;
942+
}
943+
944+
.monitoring-dot {
945+
width: 6px;
946+
height: 6px;
947+
border-radius: 50%;
948+
background: #5B5FC7;
949+
flex-shrink: 0;
950+
animation: monitoring-pulse 2.5s ease-in-out infinite;
951+
}
952+
953+
@keyframes monitoring-pulse {
954+
0%, 100% { opacity: 1; }
955+
50% { opacity: 0.35; }
956+
}
957+
958+
.monitoring-agents {
959+
font-weight: 600;
960+
color: #5B5FC7;
961+
}
962+
963+
.monitoring-label {
964+
color: #888;
965+
}

src/components/ChatView.jsx

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ export default function ChatView({
122122
const [threadRailOpen, setThreadRailOpen] = useState(false)
123123
const [highlightMessageId, setHighlightMessageId] = useState(null)
124124
const [activeTab, setActiveTab] = useState('chat')
125+
// Group intelligence demo flow (chat 35): track whether the targeted
126+
// message has been acted on so we can replace it with the group response.
127+
const [groupIntelAction, setGroupIntelAction] = useState(null) // null | 'confirmed' | 'skipped'
125128
const messagesEndRef = useRef(null)
126129

127130
// Reset per-chat ephemeral state when activeChatId changes. Using the
@@ -144,6 +147,7 @@ export default function ChatView({
144147
setThreadRailOpen(false)
145148
setHighlightMessageId(null)
146149
setActiveTab('chat')
150+
setGroupIntelAction(null)
147151
const intentMatches = navIntent && navIntent.chatId === activeChatId
148152
const intentHasSession = intentMatches && 'sessionId' in navIntent
149153
if (intentHasSession) {
@@ -537,6 +541,42 @@ export default function ChatView({
537541
const contextBrief = activeContact.contextBriefId ? contextBriefs[activeContact.contextBriefId] : null
538542
const pinnedTab = contextBrief ? { label: contextBrief.filename } : null
539543

544+
// Group intelligence: monitoring agents for this chat (ids → contact objects).
545+
const monitoringAgentIds = activeContact.monitoringAgents || []
546+
const monitoringAgents = monitoringAgentIds.map(id => contacts.find(c => c.id === id)).filter(Boolean)
547+
548+
const handleTargetedAction = (action) => {
549+
if (action.action === 'skip') {
550+
setGroupIntelAction('skipped')
551+
return
552+
}
553+
// 'confirm' — Jira responds to the group.
554+
setGroupIntelAction('confirmed')
555+
const jira = contacts.find(c => c.id === 4)
556+
setMainTypingAgentId(activeChatId)
557+
setTimeout(() => {
558+
setMainTypingAgentId(prev => prev === activeChatId ? null : prev)
559+
setExtraMessages(prev => ({
560+
...prev,
561+
[activeChatId]: [
562+
...(prev[activeChatId] || []),
563+
{
564+
id: `gi-response-${Date.now()}`,
565+
senderId: 4,
566+
text: 'Yes — JIRA-4593 is tracked. Created earlier today, assigned to Kevin Park, P1, due Friday. Kevin has a fix in draft.',
567+
link: {
568+
source: 'jira',
569+
title: 'JIRA-4593 — Guest tenant blank page on expired token re-auth',
570+
subtitle: 'In Progress · Kevin Park · P1 · Due Apr 25',
571+
url: '#',
572+
},
573+
time: nowTimeStr(),
574+
},
575+
],
576+
}))
577+
}, 1800)
578+
}
579+
540580
return (
541581
<div className="chat-view">
542582
<div className="chat-view-main">
@@ -606,7 +646,13 @@ export default function ChatView({
606646
Recent context from the conversation has been shared with this session.
607647
</div>
608648
)}
609-
{messages.map((msg) => {
649+
{messages
650+
.filter(msg => {
651+
// Hide the targeted message once the user has acted on it.
652+
if (msg.targetedActions && groupIntelAction !== null) return false
653+
return true
654+
})
655+
.map((msg) => {
610656
const isThreaded = isGroup && msg.replies?.length > 0
611657
return (
612658
<MessageRow
@@ -622,6 +668,7 @@ export default function ChatView({
622668
setThreadRailOpen(true)
623669
}
624670
} : openJiraThread}
671+
onTargetedAction={msg.targetedActions ? handleTargetedAction : undefined}
625672
/>
626673
)
627674
})}
@@ -631,10 +678,18 @@ export default function ChatView({
631678
</div>
632679
)}
633680

681+
{monitoringAgents.length > 0 && (
682+
<div className="monitoring-indicator">
683+
<div className="monitoring-dot" />
684+
<span className="monitoring-agents">{monitoringAgents.map(a => a.name).join(' · ')}</span>
685+
<span className="monitoring-label">are monitoring this conversation</span>
686+
</div>
687+
)}
688+
634689
<div className="chat-compose-area">
635690
{mainTypingAgentId === activeChatId && (
636691
<TypingIndicator
637-
contact={activeContact}
692+
contact={contacts.find(c => c.id === 4) || activeContact}
638693
className="chat-compose-typing"
639694
/>
640695
)}

0 commit comments

Comments
 (0)