Skip to content

Commit 63408f5

Browse files
authored
fix webapp copy transcript uses resolved speaker names instead of generic labels (#5533)
## Summary - Copy transcript now uses tagged person names (e.g. "Marcus") instead of generic "Speaker 0/1/2" - Applies to both the conversations webapp and the memories chat context ## Demo **Before** https://github.com/user-attachments/assets/79b73251-2f77-4aaa-8c7e-cd8cbf7436fd **After** https://github.com/user-attachments/assets/5ca18c9a-3fbf-41ee-a527-98da9738577f 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents d177d49 + 45096c3 commit 63408f5

File tree

4 files changed

+110
-73
lines changed

4 files changed

+110
-73
lines changed

web/app/src/components/conversations/ConversationActionsMenu.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,36 @@ import {
1313
import { cn } from '@/lib/utils';
1414
import { deleteConversation, reprocessConversation } from '@/lib/api';
1515
import type { Conversation, TranscriptSegment } from '@/types/conversation';
16+
import type { Person } from '@/types/user';
1617
import { MixpanelManager } from '@/lib/analytics/mixpanel';
1718

1819
interface ConversationActionsMenuProps {
1920
conversation: Conversation;
21+
people?: Person[];
2022
onConversationUpdate?: (conversation: Conversation) => void;
2123
onDelete?: () => void;
2224
className?: string;
2325
}
2426

2527
/**
26-
* Generate transcript text from segments
28+
* Generate transcript text from segments, resolving speaker names from people list
2729
*/
28-
function generateTranscript(segments: TranscriptSegment[]): string {
30+
function generateTranscript(segments: TranscriptSegment[], people?: Person[]): string {
2931
if (!segments || segments.length === 0) return '';
3032

3133
return segments
3234
.map((segment) => {
33-
const speaker = segment.is_user ? 'You' : `Speaker ${segment.speaker_id}`;
35+
let speaker: string;
36+
if (segment.is_user) {
37+
speaker = 'You';
38+
} else if (segment.person_id && people) {
39+
const person = people.find((p) => p.id === segment.person_id);
40+
speaker = person ? person.name : (segment.speaker_name || `Speaker ${segment.speaker_id}`);
41+
} else if (segment.speaker_name) {
42+
speaker = segment.speaker_name;
43+
} else {
44+
speaker = `Speaker ${segment.speaker_id}`;
45+
}
3446
return `${speaker}: ${segment.text}`;
3547
})
3648
.join('\n\n');
@@ -48,6 +60,7 @@ function getSummaryContent(conversation: Conversation): string {
4860

4961
export function ConversationActionsMenu({
5062
conversation,
63+
people,
5164
onConversationUpdate,
5265
onDelete,
5366
className,
@@ -75,7 +88,7 @@ export function ConversationActionsMenu({
7588
}, [isOpen]);
7689

7790
const handleCopyTranscript = async () => {
78-
const transcript = generateTranscript(conversation.transcript_segments);
91+
const transcript = generateTranscript(conversation.transcript_segments, people);
7992
if (!transcript) return;
8093

8194
await navigator.clipboard.writeText(transcript);

web/app/src/components/conversations/ConversationDetailPanel.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,7 @@ export function ConversationDetailPanel({
647647
{/* Actions Menu */}
648648
<ConversationActionsMenu
649649
conversation={conversation}
650+
people={people}
650651
onConversationUpdate={onConversationUpdate}
651652
onDelete={handleDelete}
652653
/>

web/frontend/src/components/memories/chat/chat.tsx

Lines changed: 91 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
'use client';
22

3-
import { useState, useRef, useEffect } from 'react';
4-
import { TranscriptSegment } from '@/src/types/memory.types';
3+
import { useState, useRef, useEffect, useMemo } from 'react';
4+
import { TranscriptSegment, Person } from '@/src/types/memory.types';
55
import chatWithMemory from '@/src/actions/memories/chat-with-memory';
66
import { Send, UserCircle, Message, ArrowDown } from 'iconoir-react';
77
import Markdown from 'markdown-to-jsx';
88

99
interface ChatProps {
1010
transcript: TranscriptSegment[];
11+
people?: Person[];
1112
onClearChatRef?: (clearFn: () => void) => void;
1213
onMessagesChange?: (hasMessages: boolean) => void;
1314
}
@@ -17,7 +18,12 @@ interface ChatMessage {
1718
content: string;
1819
}
1920

20-
export default function Chat({ transcript, onClearChatRef, onMessagesChange }: ChatProps) {
21+
export default function Chat({
22+
transcript,
23+
people,
24+
onClearChatRef,
25+
onMessagesChange,
26+
}: ChatProps) {
2127
const [messages, setMessages] = useState<ChatMessage[]>([]);
2228
const [input, setInput] = useState('');
2329
const [isLoading, setIsLoading] = useState(false);
@@ -50,13 +56,25 @@ export default function Chat({ transcript, onClearChatRef, onMessagesChange }: C
5056
};
5157
}, []);
5258

53-
// Convert transcript segments to a readable string
54-
const transcriptText = transcript
55-
.map((segment) => {
56-
const speaker = segment.is_user ? 'Owner' : `Speaker ${segment.speaker_id}`;
57-
return `${speaker}: ${segment.text}`;
58-
})
59-
.join('\n\n');
59+
// Convert transcript segments to a readable string, resolving person names
60+
const transcriptText = useMemo(
61+
() =>
62+
transcript
63+
.map((segment) => {
64+
let speaker: string;
65+
if (segment.is_user) {
66+
speaker = 'Owner';
67+
} else if (segment.person_id && people) {
68+
const person = people.find((p) => p.id === segment.person_id);
69+
speaker = person ? person.name : `Speaker ${segment.speaker_id}`;
70+
} else {
71+
speaker = `Speaker ${segment.speaker_id}`;
72+
}
73+
return `${speaker}: ${segment.text}`;
74+
})
75+
.join('\n\n'),
76+
[transcript, people],
77+
);
6078

6179
const scrollToBottom = (smooth = true) => {
6280
if (messagesContainerRef.current) {
@@ -181,15 +199,17 @@ export default function Chat({ transcript, onClearChatRef, onMessagesChange }: C
181199
<div
182200
ref={messagesContainerRef}
183201
onScroll={handleScroll}
184-
className={`chat-messages-container relative overflow-y-auto ${messages.length === 0 ? 'pb-2 pt-4 px-4 md:pt-6 md:px-6' : 'p-4 md:p-6'}`}
202+
className={`chat-messages-container relative overflow-y-auto ${
203+
messages.length === 0 ? 'px-4 pb-2 pt-4 md:px-6 md:pt-6' : 'p-4 md:p-6'
204+
}`}
185205
style={{
186206
height: '400px',
187207
overflowY: 'auto',
188208
WebkitOverflowScrolling: 'touch',
189209
scrollBehavior: 'smooth',
190210
// Custom scrollbar styling for Firefox
191211
scrollbarWidth: 'thin',
192-
scrollbarColor: '#3f3f46 transparent'
212+
scrollbarColor: '#3f3f46 transparent',
193213
}}
194214
>
195215
<div className={messages.length === 0 ? 'space-y-0' : 'space-y-6'}>
@@ -205,7 +225,9 @@ export default function Chat({ transcript, onClearChatRef, onMessagesChange }: C
205225
<div className="flex flex-col gap-1">
206226
<div className="max-w-[85%] rounded-2xl bg-zinc-800/80 px-4 py-3 text-gray-100 shadow-lg">
207227
<p className="text-sm leading-relaxed md:text-base">
208-
Hi! I can help you explore this conversation. Ask me questions about the transcript, key points, or any details you'd like to know more about.
228+
Hi! I can help you explore this conversation. Ask me questions
229+
about the transcript, key points, or any details you'd like to
230+
know more about.
209231
</p>
210232
</div>
211233
</div>
@@ -247,7 +269,8 @@ export default function Chat({ transcript, onClearChatRef, onMessagesChange }: C
247269
...prev,
248270
{
249271
role: 'assistant',
250-
content: 'Sorry, I encountered an error. Please try again.',
272+
content:
273+
'Sorry, I encountered an error. Please try again.',
251274
},
252275
]);
253276
}
@@ -274,73 +297,73 @@ export default function Chat({ transcript, onClearChatRef, onMessagesChange }: C
274297
</>
275298
)}
276299
{messages.map((message, index) => (
300+
<div
301+
key={index}
302+
className={`flex gap-4 ${
303+
message.role === 'user' ? 'flex-row-reverse' : 'flex-row'
304+
}`}
305+
>
306+
{/* Avatar */}
277307
<div
278-
key={index}
279-
className={`flex gap-4 ${
280-
message.role === 'user' ? 'flex-row-reverse' : 'flex-row'
308+
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${
309+
message.role === 'user'
310+
? 'bg-gradient-to-br from-blue-500 to-blue-600'
311+
: 'bg-gradient-to-br from-purple-500 to-purple-600'
312+
}`}
313+
>
314+
{message.role === 'user' ? (
315+
<UserCircle className="h-5 w-5 text-white" />
316+
) : (
317+
<Message className="h-5 w-5 text-white" />
318+
)}
319+
</div>
320+
321+
{/* Message Content */}
322+
<div
323+
className={`flex min-w-0 flex-1 flex-col gap-1 ${
324+
message.role === 'user' ? 'items-end' : 'items-start'
281325
}`}
282326
>
283-
{/* Avatar */}
284327
<div
285-
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${
328+
className={`max-w-[85%] rounded-2xl px-4 py-3 ${
286329
message.role === 'user'
287-
? 'bg-gradient-to-br from-blue-500 to-blue-600'
288-
: 'bg-gradient-to-br from-purple-500 to-purple-600'
330+
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg'
331+
: 'bg-zinc-800/80 text-gray-100 shadow-lg'
289332
}`}
290333
>
291-
{message.role === 'user' ? (
292-
<UserCircle className="h-5 w-5 text-white" />
334+
{message.role === 'assistant' ? (
335+
<div className="prose prose-sm max-w-none text-gray-100 dark:prose-invert prose-headings:text-gray-100 prose-p:leading-relaxed prose-p:text-gray-100 prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline prose-blockquote:border-l-blue-500 prose-blockquote:text-gray-100 prose-strong:text-gray-100 prose-code:text-blue-300 prose-pre:bg-zinc-900 prose-pre:text-gray-200 prose-ol:text-gray-100 prose-ul:text-gray-100 prose-li:text-gray-100">
336+
<Markdown>{message.content}</Markdown>
337+
</div>
293338
) : (
294-
<Message className="h-5 w-5 text-white" />
339+
<p className="whitespace-pre-wrap text-sm leading-relaxed md:text-base">
340+
{message.content}
341+
</p>
295342
)}
296343
</div>
297-
298-
{/* Message Content */}
299-
<div
300-
className={`flex min-w-0 flex-1 flex-col gap-1 ${
301-
message.role === 'user' ? 'items-end' : 'items-start'
302-
}`}
303-
>
304-
<div
305-
className={`max-w-[85%] rounded-2xl px-4 py-3 ${
306-
message.role === 'user'
307-
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg'
308-
: 'bg-zinc-800/80 text-gray-100 shadow-lg'
309-
}`}
310-
>
311-
{message.role === 'assistant' ? (
312-
<div className="prose prose-sm max-w-none dark:prose-invert prose-headings:text-gray-100 prose-p:text-gray-100 prose-p:leading-relaxed prose-strong:text-gray-100 prose-ul:text-gray-100 prose-ol:text-gray-100 prose-li:text-gray-100 prose-code:text-blue-300 prose-pre:bg-zinc-900 prose-pre:text-gray-200 prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline prose-blockquote:text-gray-100 prose-blockquote:border-l-blue-500 text-gray-100">
313-
<Markdown>{message.content}</Markdown>
314-
</div>
315-
) : (
316-
<p className="whitespace-pre-wrap text-sm leading-relaxed md:text-base">
317-
{message.content}
318-
</p>
319-
)}
320-
</div>
321-
</div>
322344
</div>
323-
))}
324-
{isLoading && (
325-
<div className="flex gap-4">
326-
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-purple-600">
327-
<Message className="h-5 w-5 text-white" />
328-
</div>
329-
<div className="flex flex-col gap-1">
330-
<div className="rounded-2xl bg-zinc-800/80 px-4 py-3 shadow-lg">
331-
<div className="flex items-center gap-2">
332-
<div className="flex gap-1">
333-
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.3s]"></div>
334-
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.15s]"></div>
335-
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400"></div>
336-
</div>
337-
<span className="text-sm text-gray-400">Thinking...</span>
345+
</div>
346+
))}
347+
{isLoading && (
348+
<div className="flex gap-4">
349+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-purple-600">
350+
<Message className="h-5 w-5 text-white" />
351+
</div>
352+
<div className="flex flex-col gap-1">
353+
<div className="rounded-2xl bg-zinc-800/80 px-4 py-3 shadow-lg">
354+
<div className="flex items-center gap-2">
355+
<div className="flex gap-1">
356+
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.3s]"></div>
357+
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400 [animation-delay:-0.15s]"></div>
358+
<div className="h-2 w-2 animate-bounce rounded-full bg-gray-400"></div>
338359
</div>
360+
<span className="text-sm text-gray-400">Thinking...</span>
339361
</div>
340362
</div>
341363
</div>
342-
)}
343-
<div ref={messagesEndRef} />
364+
</div>
365+
)}
366+
<div ref={messagesEndRef} />
344367
</div>
345368

346369
{/* Scroll to bottom button */}
@@ -365,13 +388,13 @@ export default function Chat({ transcript, onClearChatRef, onMessagesChange }: C
365388
onChange={(e) => setInput(e.target.value)}
366389
onKeyDown={handleKeyDown}
367390
placeholder="Ask a question about this conversation..."
368-
className="w-full resize-none rounded-xl border border-zinc-700/50 bg-zinc-900/80 px-3 py-2.5 text-sm text-white placeholder:text-gray-500 focus:border-blue-500/50 focus:bg-zinc-900 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all md:px-4 md:py-3 md:text-base"
391+
className="w-full resize-none rounded-xl border border-zinc-700/50 bg-zinc-900/80 px-3 py-2.5 text-sm text-white transition-all placeholder:text-gray-500 focus:border-blue-500/50 focus:bg-zinc-900 focus:outline-none focus:ring-2 focus:ring-blue-500/20 md:px-4 md:py-3 md:text-base"
369392
rows={1}
370393
disabled={isLoading}
371394
style={{
372395
minHeight: '44px',
373396
maxHeight: '120px',
374-
overflow: 'hidden'
397+
overflow: 'hidden',
375398
}}
376399
/>
377400
</div>
@@ -390,4 +413,3 @@ export default function Chat({ transcript, onClearChatRef, onMessagesChange }: C
390413
</div>
391414
);
392415
}
393-

web/frontend/src/components/memories/summary/memory-with-tabs.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export default function MemoryWithTabs({ memory }: MemoryWithTabsProps) {
6161
<div style={{ display: currentTab === 'chat' ? 'block' : 'none' }}>
6262
<Chat
6363
transcript={memory.transcript_segments}
64+
people={memory.people}
6465
onClearChatRef={handleClearChatRef}
6566
onMessagesChange={setHasMessages}
6667
/>

0 commit comments

Comments
 (0)