diff --git a/apps/webclaw/src/components/attachment-button.tsx b/apps/webclaw/src/components/attachment-button.tsx index aca2ddb..128ba22 100644 --- a/apps/webclaw/src/components/attachment-button.tsx +++ b/apps/webclaw/src/components/attachment-button.tsx @@ -5,7 +5,7 @@ import { HugeiconsIcon } from '@hugeicons/react' import { PlusSignIcon } from '@hugeicons/core-free-icons' import { Button } from '@/components/ui/button' -import { cn } from '@/lib/utils' +import { cn, randomUUID } from '@/lib/utils' /** Maximum file size before compression (10MB) */ const MAX_FILE_SIZE = 10 * 1024 * 1024 @@ -202,7 +202,7 @@ export function AttachmentButton({ // Reset input to allow selecting the same file again event.target.value = '' - const id = crypto.randomUUID() + const id = randomUUID() // Validate file type if (!isAcceptedImage(file)) { diff --git a/apps/webclaw/src/lib/utils.ts b/apps/webclaw/src/lib/utils.ts index 914ec86..0bed9a8 100644 --- a/apps/webclaw/src/lib/utils.ts +++ b/apps/webclaw/src/lib/utils.ts @@ -5,3 +5,23 @@ import type { ClassValue } from 'clsx' export function cn(...inputs: Array) { return twMerge(clsx(inputs)) } + +export function randomUUID() { + // Check if we're in a secure context with crypto.randomUUID available + if ( + typeof window !== 'undefined' && + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + window.crypto && + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + window.crypto.randomUUID + ) { + return window.crypto.randomUUID() + } + + // Fallback for insecure contexts (e.g. HTTP on LAN) + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0 + const v = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} diff --git a/apps/webclaw/src/screens/chat/chat-screen-utils.ts b/apps/webclaw/src/screens/chat/chat-screen-utils.ts index 59c7b99..f884713 100644 --- a/apps/webclaw/src/screens/chat/chat-screen-utils.ts +++ b/apps/webclaw/src/screens/chat/chat-screen-utils.ts @@ -1,5 +1,6 @@ import type { GatewayMessage } from './types' import type { AttachmentFile } from '@/components/attachment-button' +import { randomUUID } from '@/lib/utils' type OptimisticMessagePayload = { clientId: string @@ -11,7 +12,7 @@ export function createOptimisticMessage( body: string, attachments?: Array, ): OptimisticMessagePayload { - const clientId = crypto.randomUUID() + const clientId = randomUUID() const optimisticId = `opt-${clientId}` const timestamp = Date.now() diff --git a/apps/webclaw/src/screens/chat/chat-screen.tsx b/apps/webclaw/src/screens/chat/chat-screen.tsx index f1774b5..387f907 100644 --- a/apps/webclaw/src/screens/chat/chat-screen.tsx +++ b/apps/webclaw/src/screens/chat/chat-screen.tsx @@ -44,7 +44,7 @@ import type { AttachmentFile } from '@/components/attachment-button' import type { ChatComposerHelpers } from './components/chat-composer' import { useExport } from '@/hooks/use-export' import { useChatSettings } from '@/hooks/use-chat-settings' -import { cn } from '@/lib/utils' +import { cn, randomUUID } from '@/lib/utils' type ChatScreenProps = { activeFriendlyId: string @@ -270,7 +270,7 @@ export function ChatScreen({ friendlyId, message: body, thinking: settings.thinkingLevel, - idempotencyKey: crypto.randomUUID(), + idempotencyKey: randomUUID(), attachments: attachmentsPayload, }), })