chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { MessageActions } from './message-actions'
|
||||
@@ -0,0 +1,294 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
Check,
|
||||
ChipModal,
|
||||
ChipModalBody,
|
||||
ChipModalField,
|
||||
ChipModalFooter,
|
||||
ChipModalHeader,
|
||||
cn,
|
||||
Duplicate,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
Tooltip,
|
||||
toast,
|
||||
} from '@sim/emcn'
|
||||
import { GitBranch } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
|
||||
import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback'
|
||||
import { useForkMothershipChat } from '@/hooks/queries/mothership-chats'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
|
||||
const SPECIAL_TAGS = 'thinking|options|usage_upgrade|credential|mothership-error|file'
|
||||
|
||||
function toPlainText(raw: string): string {
|
||||
return (
|
||||
raw
|
||||
// Strip special tags and their contents
|
||||
.replace(new RegExp(`<\\/?(${SPECIAL_TAGS})(?:>[\\s\\S]*?<\\/(${SPECIAL_TAGS})>|>)`, 'g'), '')
|
||||
// Strip markdown
|
||||
.replace(/^#{1,6}\s+/gm, '')
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1')
|
||||
.replace(/\*(.+?)\*/g, '$1')
|
||||
.replace(/`{3}[\s\S]*?`{3}/g, '')
|
||||
.replace(/`(.+?)`/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
.replace(/^[>\-*]\s+/gm, '')
|
||||
.replace(/!\[[^\]]*\]\([^)]+\)/g, '')
|
||||
// Normalize whitespace
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
|
||||
const ICON_CLASS = 'size-[14px]'
|
||||
const BUTTON_CLASS =
|
||||
'flex size-[26px] items-center justify-center rounded-[6px] text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-hover)] focus-visible:outline-none'
|
||||
|
||||
interface MessageActionsProps {
|
||||
content: string
|
||||
userQuery?: string
|
||||
requestId?: string
|
||||
messageId?: string
|
||||
}
|
||||
|
||||
export const MessageActions = memo(function MessageActions({
|
||||
content,
|
||||
userQuery,
|
||||
requestId,
|
||||
messageId,
|
||||
}: MessageActionsProps) {
|
||||
const router = useRouter()
|
||||
const params = useParams<{ workspaceId: string }>()
|
||||
const { chatId } = useChatSurface()
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [copiedRequestId, setCopiedRequestId] = useState(false)
|
||||
const [pendingFeedback, setPendingFeedback] = useState<'up' | 'down' | null>(null)
|
||||
const [feedbackText, setFeedbackText] = useState('')
|
||||
const resetTimeoutRef = useRef<number | null>(null)
|
||||
const requestIdTimeoutRef = useRef<number | null>(null)
|
||||
const submitFeedback = useSubmitCopilotFeedback()
|
||||
const forkChat = useForkMothershipChat(params.workspaceId)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (resetTimeoutRef.current !== null) {
|
||||
window.clearTimeout(resetTimeoutRef.current)
|
||||
}
|
||||
if (requestIdTimeoutRef.current !== null) {
|
||||
window.clearTimeout(requestIdTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
if (!content) return
|
||||
const text = toPlainText(content)
|
||||
if (!text) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
if (resetTimeoutRef.current !== null) {
|
||||
window.clearTimeout(resetTimeoutRef.current)
|
||||
}
|
||||
resetTimeoutRef.current = window.setTimeout(() => setCopied(false), 1500)
|
||||
} catch {
|
||||
/* clipboard unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
const copyRequestId = async () => {
|
||||
if (!requestId) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(requestId)
|
||||
setCopiedRequestId(true)
|
||||
if (requestIdTimeoutRef.current !== null) {
|
||||
window.clearTimeout(requestIdTimeoutRef.current)
|
||||
}
|
||||
requestIdTimeoutRef.current = window.setTimeout(() => setCopiedRequestId(false), 1500)
|
||||
} catch {
|
||||
/* clipboard unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
const handleFeedbackClick = (type: 'up' | 'down') => {
|
||||
if (chatId && userQuery) {
|
||||
setPendingFeedback(type)
|
||||
setFeedbackText('')
|
||||
setCopiedRequestId(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitFeedback = () => {
|
||||
if (!pendingFeedback || !chatId || !userQuery) return
|
||||
const text = feedbackText.trim()
|
||||
if (!text) {
|
||||
setPendingFeedback(null)
|
||||
setFeedbackText('')
|
||||
return
|
||||
}
|
||||
submitFeedback.mutate({
|
||||
chatId,
|
||||
userQuery,
|
||||
agentResponse: content,
|
||||
isPositiveFeedback: pendingFeedback === 'up',
|
||||
feedback: text,
|
||||
})
|
||||
setPendingFeedback(null)
|
||||
setFeedbackText('')
|
||||
}
|
||||
|
||||
const handleModalClose = (open: boolean) => {
|
||||
if (!open) {
|
||||
setPendingFeedback(null)
|
||||
setFeedbackText('')
|
||||
setCopiedRequestId(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFork = async () => {
|
||||
if (!chatId || !messageId || forkChat.isPending) return
|
||||
try {
|
||||
const result = await forkChat.mutateAsync({ chatId, upToMessageId: messageId })
|
||||
useFolderStore.getState().clearChatSelection()
|
||||
router.push(`/workspace/${params.workspaceId}/chat/${result.id}`)
|
||||
} catch {
|
||||
toast.error('Failed to fork chat')
|
||||
}
|
||||
}
|
||||
|
||||
const hasContent = Boolean(content)
|
||||
const canSubmitFeedback = Boolean(chatId && userQuery)
|
||||
const canFork = false
|
||||
if (!hasContent && !canSubmitFeedback && !canFork) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex items-center gap-0.5'>
|
||||
{hasContent && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Copy message'
|
||||
onClick={copyToClipboard}
|
||||
className={BUTTON_CLASS}
|
||||
>
|
||||
{copied ? <Check className={ICON_CLASS} /> : <Duplicate className={ICON_CLASS} />}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
{copied ? 'Copied message' : 'Copy message'}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
{canSubmitFeedback && (
|
||||
<>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Like'
|
||||
onClick={() => handleFeedbackClick('up')}
|
||||
className={BUTTON_CLASS}
|
||||
>
|
||||
<ThumbsUp className={ICON_CLASS} />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>Good response</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Dislike'
|
||||
onClick={() => handleFeedbackClick('down')}
|
||||
className={BUTTON_CLASS}
|
||||
>
|
||||
<ThumbsDown className={ICON_CLASS} />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>Bad response</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</>
|
||||
)}
|
||||
{canFork && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Fork from here'
|
||||
onClick={handleFork}
|
||||
disabled={forkChat.isPending}
|
||||
className={cn(BUTTON_CLASS, forkChat.isPending && 'cursor-not-allowed opacity-50')}
|
||||
>
|
||||
<GitBranch className={ICON_CLASS} />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>Fork from here</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ChipModal
|
||||
open={pendingFeedback !== null}
|
||||
onOpenChange={handleModalClose}
|
||||
srTitle='Give feedback'
|
||||
>
|
||||
<ChipModalHeader onClose={() => handleModalClose(false)}>Give feedback</ChipModalHeader>
|
||||
<ChipModalBody>
|
||||
<div className='flex items-start justify-between gap-2 px-2'>
|
||||
<p className='font-medium text-[var(--text-secondary)] text-sm'>
|
||||
{pendingFeedback === 'up' ? 'What did you like?' : 'What could be improved?'}
|
||||
</p>
|
||||
{pendingFeedback === 'down' && requestId && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Copy request ID'
|
||||
onClick={copyRequestId}
|
||||
className='flex size-[22px] shrink-0 items-center justify-center rounded-full text-[var(--text-icon)] transition-colors hover-hover:bg-[var(--surface-hover)] focus-visible:outline-none'
|
||||
>
|
||||
{copiedRequestId ? (
|
||||
<Check className='size-[14px]' />
|
||||
) : (
|
||||
<Duplicate className='size-[14px]' />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
{copiedRequestId ? 'Copied request ID' : 'Copy request ID'}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</div>
|
||||
<ChipModalField
|
||||
type='textarea'
|
||||
title='Feedback'
|
||||
value={feedbackText}
|
||||
onChange={setFeedbackText}
|
||||
rows={6}
|
||||
minHeight={140}
|
||||
resizable
|
||||
placeholder={
|
||||
pendingFeedback === 'up'
|
||||
? 'Tell us what was helpful...'
|
||||
: 'Tell us what went wrong...'
|
||||
}
|
||||
/>
|
||||
</ChipModalBody>
|
||||
<ChipModalFooter
|
||||
onCancel={() => handleModalClose(false)}
|
||||
primaryAction={{
|
||||
label: 'Submit',
|
||||
onClick: handleSubmitFeedback,
|
||||
}}
|
||||
/>
|
||||
</ChipModal>
|
||||
</>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user