Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

import { createElement, useMemo, useState } from 'react'
import { useParams } from 'next/navigation'
import { ArrowRight, ChevronDown, Expandable, ExpandableContent } from '@/components/emcn'
import {
ArrowRight,
ChevronDown,
Expandable,
ExpandableContent,
SecretReveal,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth'
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
Expand Down Expand Up @@ -47,9 +53,10 @@ export const CREDENTIAL_TAG_TYPES = [
export type CredentialTagType = (typeof CREDENTIAL_TAG_TYPES)[number]

export interface CredentialTagData {
value: string
value?: string
type: CredentialTagType
provider?: string
redacted?: boolean
}

export interface MothershipErrorTagData {
Expand Down Expand Up @@ -140,12 +147,15 @@ function isUsageUpgradeTagData(value: unknown): value is UsageUpgradeTagData {

function isCredentialTagData(value: unknown): value is CredentialTagData {
if (!isRecord(value)) return false
return (
typeof value.value === 'string' &&
typeof value.type === 'string' &&
(CREDENTIAL_TAG_TYPES as readonly string[]).includes(value.type) &&
(value.provider === undefined || typeof value.provider === 'string')
)
if (
typeof value.type !== 'string' ||
!(CREDENTIAL_TAG_TYPES as readonly string[]).includes(value.type)
) {
return false
}
if (value.provider !== undefined && typeof value.provider !== 'string') return false
if (value.redacted === true) return value.value === undefined || typeof value.value === 'string'
return typeof value.value === 'string'
}

function isMothershipErrorTagData(value: unknown): value is MothershipErrorTagData {
Expand Down Expand Up @@ -595,24 +605,30 @@ const LockIcon = (props: { className?: string }) => (
)

function CredentialDisplay({ data }: { data: CredentialTagData }) {
if (data.type !== 'link' || !data.provider) return null
if (data.type === 'link') {
if (!data.provider) return null
const Icon = getCredentialIcon(data.provider) ?? LockIcon
return (
<a
href={data.value}
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 rounded-lg border border-[var(--divider)] px-3 py-2.5 transition-colors hover-hover:bg-[var(--surface-5)]'
>
{createElement(Icon, { className: 'h-[16px] w-[16px] shrink-0' })}
<span className='flex-1 font-base text-[var(--text-body)] text-sm'>
Connect {data.provider}
</span>
<ArrowRight className='h-[16px] w-[16px] shrink-0 text-[var(--text-icon)]' />
</a>
)
}

const Icon = getCredentialIcon(data.provider) ?? LockIcon
if (data.type === 'sim_key') {
return <SecretReveal value={data.value} redacted={data.redacted || !data.value} />
}

return (
<a
href={data.value}
target='_blank'
rel='noopener noreferrer'
className='flex items-center gap-2 rounded-lg border border-[var(--divider)] px-3 py-2.5 transition-colors hover-hover:bg-[var(--surface-5)]'
>
{createElement(Icon, { className: 'h-[16px] w-[16px] shrink-0' })}
<span className='flex-1 font-base text-[var(--text-body)] text-sm'>
Connect {data.provider}
</span>
<ArrowRight className='h-[16px] w-[16px] shrink-0 text-[var(--text-icon)]' />
</a>
)
return null
}

function MothershipErrorDisplay({ data }: { data: MothershipErrorTagData }) {
Expand Down
19 changes: 15 additions & 4 deletions apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import type {
PersistedMessage,
} from '@/lib/copilot/chat/persisted-message'
import { normalizeMessage, withBlockTiming } from '@/lib/copilot/chat/persisted-message'
import {
captureRevealedSimKeys,
type RevealedSimKeysByMessage,
restoreRevealedSimKeysForMessage,
} from '@/lib/copilot/chat/sim-key-redaction'
import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome'
import { MOTHERSHIP_CHAT_API_PATH, STREAM_STORAGE_KEY } from '@/lib/copilot/constants'
import type {
Expand Down Expand Up @@ -1062,6 +1067,7 @@ export function useChat(
)
const [genericResourceData, setGenericResourceData] = useState<GenericResourceData | null>(null)
const onResourceEventRef = useRef(options?.onResourceEvent)
const revealedSimKeysRef = useRef<RevealedSimKeysByMessage>(new Map())
onResourceEventRef.current = options?.onResourceEvent
const apiPathRef = useRef(options?.apiPath ?? MOTHERSHIP_CHAT_API_PATH)
apiPathRef.current = options?.apiPath ?? MOTHERSHIP_CHAT_API_PATH
Expand Down Expand Up @@ -1383,10 +1389,10 @@ export function useChat(
}, [clearActiveTurn, clearQueueDispatchState, resetEphemeralPreviewState, setTransportIdle])

const { data: chatHistory } = useChatHistory(resolvedChatId)
const messages = useMemo(
() => chatHistory?.messages.map(toDisplayMessage) ?? pendingMessages,
[chatHistory, pendingMessages]
)
const messages = useMemo(() => {
const source = chatHistory?.messages.map(toDisplayMessage) ?? pendingMessages
return source.map((m) => restoreRevealedSimKeysForMessage(m, revealedSimKeysRef.current))
}, [chatHistory, pendingMessages])
const addResource = useCallback((resource: MothershipResource): boolean => {
if (resourcesRef.current.some((r) => r.type === resource.type && r.id === resource.id)) {
return false
Expand Down Expand Up @@ -1875,6 +1881,11 @@ export function useChat(
const flush = () => {
if (isStale()) return
streamingBlocksRef.current = [...blocks]
captureRevealedSimKeys(
revealedSimKeysRef.current,
[assistantId, streamRequestId],
runningText
)
const activeChatId = chatIdRef.current
if (!activeChatId) {
const snapshot: Partial<ChatMessage> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useState } from 'react'
import { createLogger } from '@sim/logger'
import { Check, Copy } from 'lucide-react'
import {
Button,
ButtonGroup,
Expand All @@ -13,6 +12,7 @@ import {
ModalContent,
ModalFooter,
ModalHeader,
SecretReveal,
} from '@/components/emcn'
import { type ApiKey, useCreateApiKey } from '@/hooks/queries/api-keys'

Expand Down Expand Up @@ -50,8 +50,6 @@ export function CreateApiKeyModal({
const [createError, setCreateError] = useState<string | null>(null)
const [newKey, setNewKey] = useState<ApiKey | null>(null)
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
const [copySuccess, setCopySuccess] = useState(false)

const createApiKeyMutation = useCreateApiKey()

const handleCreateKey = async () => {
Expand Down Expand Up @@ -105,12 +103,6 @@ export function CreateApiKeyModal({
setCreateError(null)
}

const copyToClipboard = (key: string) => {
navigator.clipboard.writeText(key)
setCopySuccess(true)
setTimeout(() => setCopySuccess(false), 2000)
}

return (
<>
{/* Create API Key Dialog */}
Expand Down Expand Up @@ -209,7 +201,6 @@ export function CreateApiKeyModal({
setShowNewKeyDialog(dialogOpen)
if (!dialogOpen) {
setNewKey(null)
setCopySuccess(false)
}
}}
>
Expand All @@ -223,27 +214,7 @@ export function CreateApiKeyModal({
</span>
</p>

{newKey && (
<div className='relative mt-2.5'>
<div className='flex h-9 items-center rounded-md border bg-[var(--surface-1)] px-2.5 pr-10'>
<code className='flex-1 truncate font-mono text-[var(--text-primary)] text-sm'>
{newKey.key}
</code>
</div>
<Button
variant='ghost'
className='-translate-y-1/2 absolute top-1/2 right-[4px] h-[28px] w-[28px] rounded-sm text-[var(--text-muted)] hover-hover:text-[var(--text-primary)]'
onClick={() => copyToClipboard(newKey.key)}
>
{copySuccess ? (
<Check className='h-[14px] w-[14px]' />
) : (
<Copy className='h-[14px] w-[14px]' />
)}
<span className='sr-only'>Copy to clipboard</span>
</Button>
</div>
)}
{newKey && <SecretReveal value={newKey.key} className='mt-2.5' />}
</ModalBody>
</ModalContent>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useMemo, useState } from 'react'
// import { useParams } from 'next/navigation'
import { createLogger } from '@sim/logger'
import { formatDate } from '@sim/utils/formatting'
import { Check, Copy, Plus, Search } from 'lucide-react'
import { Plus, Search } from 'lucide-react'
import {
Button,
Input as EmcnInput,
Expand All @@ -13,6 +13,7 @@ import {
ModalContent,
ModalFooter,
ModalHeader,
SecretReveal,
// Switch,
} from '@/components/emcn'
import { Input } from '@/components/ui'
Expand Down Expand Up @@ -58,7 +59,6 @@ export function Copilot() {
const [newKeyName, setNewKeyName] = useState('')
const [newKey, setNewKey] = useState<string | null>(null)
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
const [copySuccess, setCopySuccess] = useState(false)
const [deleteKey, setDeleteKey] = useState<CopilotKey | null>(null)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
Expand Down Expand Up @@ -115,12 +115,6 @@ export function Copilot() {
}
}

const copyToClipboard = (key: string) => {
navigator.clipboard.writeText(key)
setCopySuccess(true)
setTimeout(() => setCopySuccess(false), 2000)
}

const handleDeleteKey = async () => {
if (!deleteKey) return
try {
Expand Down Expand Up @@ -316,7 +310,6 @@ export function Copilot() {
setShowNewKeyDialog(open)
if (!open) {
setNewKey(null)
setCopySuccess(false)
}
}}
>
Expand All @@ -330,27 +323,7 @@ export function Copilot() {
</span>
</p>

{newKey && (
<div className='relative mt-2.5'>
<div className='flex h-9 items-center rounded-md border bg-[var(--surface-1)] px-2.5 pr-10'>
<code className='flex-1 truncate font-mono text-[var(--text-primary)] text-sm'>
{newKey}
</code>
</div>
<Button
variant='ghost'
className='-translate-y-1/2 absolute top-1/2 right-[4px] h-[28px] w-[28px] rounded-sm text-[var(--text-muted)] hover-hover:text-[var(--text-primary)]'
onClick={() => copyToClipboard(newKey)}
>
{copySuccess ? (
<Check className='h-[14px] w-[14px]' />
) : (
<Copy className='h-[14px] w-[14px]' />
)}
<span className='sr-only'>Copy to clipboard</span>
</Button>
</div>
)}
{newKey && <SecretReveal value={newKey} className='mt-2.5' />}
</ModalBody>
</ModalContent>
</Modal>
Expand Down
25 changes: 4 additions & 21 deletions apps/sim/components/emcn/components/code/copy-code-button.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,25 @@
'use client'

import { useCallback, useEffect, useRef, useState } from 'react'
import { Button, Check, Copy } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'

interface CopyCodeButtonProps {
code: string
className?: string
}

export function CopyCodeButton({ code, className }: CopyCodeButtonProps) {
const [copied, setCopied] = useState(false)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(code)
setCopied(true)
if (timerRef.current) clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => setCopied(false), 2000)
} catch {}
}, [code])

useEffect(
() => () => {
if (timerRef.current) clearTimeout(timerRef.current)
},
[]
)
const { copied, copy } = useCopyToClipboard()

return (
<Button
type='button'
variant='ghost'
onClick={handleCopy}
onClick={() => copy(code)}
className={cn('flex items-center gap-1 rounded px-1.5 py-0.5 text-xs', className)}
>
{copied ? <Check className='size-3.5' /> : <Copy className='size-3.5' />}
{copied ? <Check className='h-[14px] w-[14px]' /> : <Copy className='h-[14px] w-[14px]' />}
</Button>
)
}
1 change: 1 addition & 0 deletions apps/sim/components/emcn/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export {
SModalTrigger,
} from './s-modal/s-modal'
export { SecretInput, type SecretInputProps } from './secret-input/secret-input'
export { SecretReveal, type SecretRevealProps } from './secret-reveal/secret-reveal'
export { Skeleton } from './skeleton/skeleton'
export { Slider, type SliderProps } from './slider/slider'
export { Switch } from './switch/switch'
Expand Down
Loading
Loading