Skip to content

Commit b0cb95b

Browse files
authored
feat: mothership/copilot feedback (#3940)
* feat: mothership/copilot feedback * fix(feedback): remove mutation object from useCallback deps
1 parent 6d00d6b commit b0cb95b

File tree

12 files changed

+535
-313
lines changed

12 files changed

+535
-313
lines changed
Lines changed: 141 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,59 @@
11
'use client'
22

33
import { useCallback, useEffect, useRef, useState } from 'react'
4-
import { Check, Copy, Ellipsis, Hash } from 'lucide-react'
54
import {
6-
DropdownMenu,
7-
DropdownMenuContent,
8-
DropdownMenuItem,
9-
DropdownMenuTrigger,
5+
Button,
6+
Check,
7+
Copy,
8+
Modal,
9+
ModalBody,
10+
ModalContent,
11+
ModalFooter,
12+
ModalHeader,
13+
Textarea,
14+
ThumbsDown,
15+
ThumbsUp,
1016
} from '@/components/emcn'
17+
import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback'
18+
19+
const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file'
20+
21+
function toPlainText(raw: string): string {
22+
return (
23+
raw
24+
// Strip special tags and their contents
25+
.replace(new RegExp(`<\\/?(${SPECIAL_TAGS})(?:>[\\s\\S]*?<\\/(${SPECIAL_TAGS})>|>)`, 'g'), '')
26+
// Strip markdown
27+
.replace(/^#{1,6}\s+/gm, '')
28+
.replace(/\*\*(.+?)\*\*/g, '$1')
29+
.replace(/\*(.+?)\*/g, '$1')
30+
.replace(/`{3}[\s\S]*?`{3}/g, '')
31+
.replace(/`(.+?)`/g, '$1')
32+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
33+
.replace(/^[>\-*]\s+/gm, '')
34+
.replace(/!\[[^\]]*\]\([^)]+\)/g, '')
35+
// Normalize whitespace
36+
.replace(/\n{3,}/g, '\n\n')
37+
.trim()
38+
)
39+
}
40+
41+
const ICON_CLASS = 'h-[14px] w-[14px]'
42+
const BUTTON_CLASS =
43+
'flex h-[26px] w-[26px] items-center justify-center rounded-[6px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-hover)] focus-visible:outline-none'
1144

1245
interface MessageActionsProps {
1346
content: string
14-
requestId?: string
47+
chatId?: string
48+
userQuery?: string
1549
}
1650

17-
export function MessageActions({ content, requestId }: MessageActionsProps) {
18-
const [copied, setCopied] = useState<'message' | 'request' | null>(null)
51+
export function MessageActions({ content, chatId, userQuery }: MessageActionsProps) {
52+
const [copied, setCopied] = useState(false)
53+
const [pendingFeedback, setPendingFeedback] = useState<'up' | 'down' | null>(null)
54+
const [feedbackText, setFeedbackText] = useState('')
1955
const resetTimeoutRef = useRef<number | null>(null)
56+
const submitFeedback = useSubmitCopilotFeedback()
2057

2158
useEffect(() => {
2259
return () => {
@@ -26,59 +63,119 @@ export function MessageActions({ content, requestId }: MessageActionsProps) {
2663
}
2764
}, [])
2865

29-
const copyToClipboard = useCallback(async (text: string, type: 'message' | 'request') => {
66+
const copyToClipboard = useCallback(async () => {
67+
if (!content) return
68+
const text = toPlainText(content)
69+
if (!text) return
3070
try {
3171
await navigator.clipboard.writeText(text)
32-
setCopied(type)
72+
setCopied(true)
3373
if (resetTimeoutRef.current !== null) {
3474
window.clearTimeout(resetTimeoutRef.current)
3575
}
36-
resetTimeoutRef.current = window.setTimeout(() => setCopied(null), 1500)
76+
resetTimeoutRef.current = window.setTimeout(() => setCopied(false), 1500)
3777
} catch {
78+
/* clipboard unavailable */
79+
}
80+
}, [content])
81+
82+
const handleFeedbackClick = useCallback(
83+
(type: 'up' | 'down') => {
84+
if (chatId && userQuery) {
85+
setPendingFeedback(type)
86+
setFeedbackText('')
87+
}
88+
},
89+
[chatId, userQuery]
90+
)
91+
92+
const handleSubmitFeedback = useCallback(() => {
93+
if (!pendingFeedback || !chatId || !userQuery) return
94+
const text = feedbackText.trim()
95+
if (!text) {
96+
setPendingFeedback(null)
97+
setFeedbackText('')
3898
return
3999
}
100+
submitFeedback.mutate({
101+
chatId,
102+
userQuery,
103+
agentResponse: content,
104+
isPositiveFeedback: pendingFeedback === 'up',
105+
feedback: text,
106+
})
107+
setPendingFeedback(null)
108+
setFeedbackText('')
109+
}, [pendingFeedback, chatId, userQuery, content, feedbackText])
110+
111+
const handleModalClose = useCallback((open: boolean) => {
112+
if (!open) {
113+
setPendingFeedback(null)
114+
setFeedbackText('')
115+
}
40116
}, [])
41117

42-
if (!content && !requestId) {
43-
return null
44-
}
118+
if (!content) return null
45119

46120
return (
47-
<DropdownMenu modal={false}>
48-
<DropdownMenuTrigger asChild>
121+
<>
122+
<div className='flex items-center gap-0.5'>
49123
<button
50124
type='button'
51-
aria-label='More options'
52-
className='flex h-5 w-5 items-center justify-center rounded-sm text-[var(--text-icon)] opacity-0 transition-colors transition-opacity hover-hover:bg-[var(--surface-3)] hover-hover:text-[var(--text-primary)] focus-visible:opacity-100 focus-visible:outline-none group-hover/msg:opacity-100 data-[state=open]:opacity-100'
53-
onClick={(event) => event.stopPropagation()}
125+
aria-label='Copy message'
126+
onClick={copyToClipboard}
127+
className={BUTTON_CLASS}
54128
>
55-
<Ellipsis className='h-3 w-3' strokeWidth={2} />
129+
{copied ? <Check className={ICON_CLASS} /> : <Copy className={ICON_CLASS} />}
56130
</button>
57-
</DropdownMenuTrigger>
58-
<DropdownMenuContent align='end' side='top' sideOffset={4}>
59-
<DropdownMenuItem
60-
disabled={!content}
61-
onSelect={(event) => {
62-
event.stopPropagation()
63-
void copyToClipboard(content, 'message')
64-
}}
131+
<button
132+
type='button'
133+
aria-label='Like'
134+
onClick={() => handleFeedbackClick('up')}
135+
className={BUTTON_CLASS}
65136
>
66-
{copied === 'message' ? <Check /> : <Copy />}
67-
<span>Copy Message</span>
68-
</DropdownMenuItem>
69-
<DropdownMenuItem
70-
disabled={!requestId}
71-
onSelect={(event) => {
72-
event.stopPropagation()
73-
if (requestId) {
74-
void copyToClipboard(requestId, 'request')
75-
}
76-
}}
137+
<ThumbsUp className={ICON_CLASS} />
138+
</button>
139+
<button
140+
type='button'
141+
aria-label='Dislike'
142+
onClick={() => handleFeedbackClick('down')}
143+
className={BUTTON_CLASS}
77144
>
78-
{copied === 'request' ? <Check /> : <Hash />}
79-
<span>Copy Request ID</span>
80-
</DropdownMenuItem>
81-
</DropdownMenuContent>
82-
</DropdownMenu>
145+
<ThumbsDown className={ICON_CLASS} />
146+
</button>
147+
</div>
148+
149+
<Modal open={pendingFeedback !== null} onOpenChange={handleModalClose}>
150+
<ModalContent size='sm'>
151+
<ModalHeader>Give feedback</ModalHeader>
152+
<ModalBody>
153+
<div className='flex flex-col gap-2'>
154+
<p className='font-medium text-[var(--text-secondary)] text-sm'>
155+
{pendingFeedback === 'up' ? 'What did you like?' : 'What could be improved?'}
156+
</p>
157+
<Textarea
158+
placeholder={
159+
pendingFeedback === 'up'
160+
? 'Tell us what was helpful...'
161+
: 'Tell us what went wrong...'
162+
}
163+
value={feedbackText}
164+
onChange={(e) => setFeedbackText(e.target.value)}
165+
rows={3}
166+
/>
167+
</div>
168+
</ModalBody>
169+
<ModalFooter>
170+
<Button variant='default' onClick={() => handleModalClose(false)}>
171+
Cancel
172+
</Button>
173+
<Button variant='primary' onClick={handleSubmitFeedback}>
174+
Submit
175+
</Button>
176+
</ModalFooter>
177+
</ModalContent>
178+
</Modal>
179+
</>
83180
)
84181
}

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,9 @@ function MothershipErrorDisplay({ data }: { data: MothershipErrorTagData }) {
473473
const detail = data.code ? `${data.message} (${data.code})` : data.message
474474

475475
return (
476-
<span className='animate-stream-fade-in font-base text-[13px] text-[var(--text-secondary)] italic leading-[20px]'>
476+
<p className='animate-stream-fade-in font-base text-[13px] text-[var(--text-secondary)] italic leading-[20px]'>
477477
{detail}
478-
</span>
478+
</p>
479479
)
480480
}
481481

apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface MothershipChatProps {
3535
onSendQueuedMessage: (id: string) => Promise<void>
3636
onEditQueuedMessage: (id: string) => void
3737
userId?: string
38+
chatId?: string
3839
onContextAdd?: (context: ChatContext) => void
3940
editValue?: string
4041
onEditValueConsumed?: () => void
@@ -53,7 +54,7 @@ const LAYOUT_STYLES = {
5354
userRow: 'flex flex-col items-end gap-[6px] pt-3',
5455
attachmentWidth: 'max-w-[70%]',
5556
userBubble: 'max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2',
56-
assistantRow: 'group/msg relative pb-5',
57+
assistantRow: 'group/msg',
5758
footer: 'flex-shrink-0 px-[24px] pb-[16px]',
5859
footerInner: 'mx-auto max-w-[42rem]',
5960
},
@@ -63,7 +64,7 @@ const LAYOUT_STYLES = {
6364
userRow: 'flex flex-col items-end gap-[6px] pt-2',
6465
attachmentWidth: 'max-w-[85%]',
6566
userBubble: 'max-w-[85%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3 py-2',
66-
assistantRow: 'group/msg relative pb-3',
67+
assistantRow: 'group/msg',
6768
footer: 'flex-shrink-0 px-3 pb-3',
6869
footerInner: '',
6970
},
@@ -80,6 +81,7 @@ export function MothershipChat({
8081
onSendQueuedMessage,
8182
onEditQueuedMessage,
8283
userId,
84+
chatId,
8385
onContextAdd,
8486
editValue,
8587
onEditValueConsumed,
@@ -147,20 +149,28 @@ export function MothershipChat({
147149
}
148150

149151
const isLastMessage = index === messages.length - 1
152+
const precedingUserMsg = [...messages]
153+
.slice(0, index)
154+
.reverse()
155+
.find((m) => m.role === 'user')
150156

151157
return (
152158
<div key={msg.id} className={styles.assistantRow}>
153-
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
154-
<div className='absolute right-0 bottom-0 z-10'>
155-
<MessageActions content={msg.content} requestId={msg.requestId} />
156-
</div>
157-
)}
158159
<MessageContent
159160
blocks={msg.contentBlocks || []}
160161
fallbackContent={msg.content}
161162
isStreaming={isThisStreaming}
162163
onOptionSelect={isLastMessage ? onSubmit : undefined}
163164
/>
165+
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
166+
<div className='mt-2.5'>
167+
<MessageActions
168+
content={msg.content}
169+
chatId={chatId}
170+
userQuery={precedingUserMsg?.content}
171+
/>
172+
</div>
173+
)}
164174
</div>
165175
)
166176
})}

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export const MothershipView = memo(
115115
<div
116116
ref={ref}
117117
className={cn(
118-
'relative z-10 flex h-full flex-col overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]',
118+
'relative z-10 flex h-full flex-col overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]',
119119
isCollapsed ? 'w-0 min-w-0 border-l-0' : 'w-1/2 border-l',
120120
className
121121
)}

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ export function Home({ chatId }: HomeProps = {}) {
348348
onSendQueuedMessage={sendNow}
349349
onEditQueuedMessage={handleEditQueuedMessage}
350350
userId={session?.user?.id}
351+
chatId={resolvedChatId}
351352
onContextAdd={handleContextAdd}
352353
editValue={editingInputValue}
353354
onEditValueConsumed={clearEditingValue}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
839839
onSendQueuedMessage={copilotSendNow}
840840
onEditQueuedMessage={handleCopilotEditQueuedMessage}
841841
userId={session?.user?.id}
842+
chatId={copilotResolvedChatId}
842843
editValue={copilotEditingInputValue}
843844
onEditValueConsumed={clearCopilotEditingValue}
844845
layout='copilot-view'

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/help-modal/help-modal.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
Button,
1313
Combobox,
1414
Input,
15-
Label,
1615
Modal,
1716
ModalBody,
1817
ModalContent,
@@ -432,7 +431,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
432431
<div ref={scrollContainerRef} className='min-h-0 flex-1 overflow-y-auto'>
433432
<div className='space-y-3'>
434433
<div className='flex flex-col gap-2'>
435-
<Label htmlFor='type'>Request</Label>
434+
<p className='font-medium text-[var(--text-secondary)] text-sm'>Request</p>
436435
<Combobox
437436
id='type'
438437
options={REQUEST_TYPE_OPTIONS}
@@ -447,7 +446,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
447446
</div>
448447

449448
<div className='flex flex-col gap-2'>
450-
<Label htmlFor='subject'>Subject</Label>
449+
<p className='font-medium text-[var(--text-secondary)] text-sm'>Subject</p>
451450
<Input
452451
id='subject'
453452
placeholder='Brief description of your request'
@@ -457,7 +456,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
457456
</div>
458457

459458
<div className='flex flex-col gap-2'>
460-
<Label htmlFor='message'>Message</Label>
459+
<p className='font-medium text-[var(--text-secondary)] text-sm'>Message</p>
461460
<Textarea
462461
id='message'
463462
placeholder='Please provide details about your request...'
@@ -468,7 +467,9 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
468467
</div>
469468

470469
<div className='flex flex-col gap-2'>
471-
<Label>Attach Images (Optional)</Label>
470+
<p className='font-medium text-[var(--text-secondary)] text-sm'>
471+
Attach Images (Optional)
472+
</p>
472473
<Button
473474
type='button'
474475
variant='default'
@@ -505,7 +506,9 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
505506

506507
{images.length > 0 && (
507508
<div className='space-y-2'>
508-
<Label>Uploaded Images</Label>
509+
<p className='font-medium text-[var(--text-secondary)] text-sm'>
510+
Uploaded Images
511+
</p>
509512
<div className='grid grid-cols-2 gap-3'>
510513
{images.map((image, index) => (
511514
<div

0 commit comments

Comments
 (0)