@tavus/cvi-ui 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -17
- package/dist/index.js +352 -33
- package/package.json +40 -39
package/dist/index.js
CHANGED
|
@@ -289,17 +289,52 @@ async function updateDependencies(dependencies, config, options) {
|
|
|
289
289
|
//#region src/templates/tsx/components/audio-wave.json
|
|
290
290
|
var audio_wave_default$1 = {
|
|
291
291
|
type: "components",
|
|
292
|
-
content: "import React, { useCallback, useRef, memo } from
|
|
292
|
+
content: "import React, { useCallback, useRef, memo } from 'react';\nimport { useActiveSpeakerId } from '@daily-co/daily-react';\nimport { useAudioLevelObserver } from '@daily-co/daily-react';\nimport styles from './audio-wave.module.css';\n\nexport const AudioWave = memo(({ id }: { id: string }) => {\n const activeSpeakerId = useActiveSpeakerId();\n const isActiveSpeaker = activeSpeakerId === id;\n\n const leftBarRef = useRef<HTMLDivElement>(null);\n const centerBarRef = useRef<HTMLDivElement>(null);\n const rightBarRef = useRef<HTMLDivElement>(null);\n const animationFrameRef = useRef<number | undefined>(undefined);\n\n useAudioLevelObserver(\n id,\n useCallback((volume) => {\n // Cancel any pending animation frame to prevent accumulation\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n\n // Use requestAnimationFrame to batch DOM updates\n animationFrameRef.current = requestAnimationFrame(() => {\n const scaledVolume = Number(Math.max(0.01, volume).toFixed(2));\n if (leftBarRef.current && centerBarRef.current && rightBarRef.current) {\n leftBarRef.current.style.height = `${20 + scaledVolume * 40}%`;\n centerBarRef.current.style.height = `${20 + scaledVolume * 130}%`;\n rightBarRef.current.style.height = `${20 + scaledVolume * 40}%`;\n }\n });\n }, [])\n );\n\n return (\n <div className={styles.container}>\n <div className={styles.waveContainer}>\n <div\n ref={leftBarRef}\n className={`${styles.bar} ${!isActiveSpeaker ? styles.barInactive : ''}`}\n />\n <div\n ref={centerBarRef}\n className={`${styles.bar} ${!isActiveSpeaker ? styles.barInactive : ''}`}\n />\n <div\n ref={rightBarRef}\n className={`${styles.bar} ${!isActiveSpeaker ? styles.barInactive : ''}`}\n />\n </div>\n </div>\n );\n});\n\nAudioWave.displayName = 'AudioWave';\n",
|
|
293
293
|
styles: ".container {\n overflow: hidden;\n border: 1px solid white;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: 9999px;\n will-change: transform;\n transform: translateZ(0);\n}\n\n.waveContainer {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 0.125rem;\n width: 100%;\n height: 100%;\n}\n\n.bar {\n width: 0.25rem;\n height: 0.25rem;\n background-color: white;\n border-radius: 9999px;\n transition: height 200ms ease-out;\n will-change: height;\n transform: translateZ(0);\n}\n\n.barInactive {\n width: 0.25rem !important;\n height: 0.25rem !important;\n}\n"
|
|
294
294
|
};
|
|
295
295
|
//#endregion
|
|
296
|
+
//#region src/templates/tsx/components/chat.json
|
|
297
|
+
var chat_default$1 = {
|
|
298
|
+
type: "components",
|
|
299
|
+
content: "import React, {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useChat, type ChatMessage } from '../../hooks/use-chat';\nimport styles from './chat.module.css';\n\ntype ChatContextValue = {\n isOpen: boolean;\n toggle: () => void;\n close: () => void;\n messages: ChatMessage[];\n sendMessage: (text: string) => void;\n conversationId: string | null;\n};\n\nconst ChatContext = createContext<ChatContextValue | null>(null);\n\nexport const ChatProvider = ({\n children,\n defaultOpen = false,\n}: {\n children: React.ReactNode;\n defaultOpen?: boolean;\n}) => {\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const { messages, sendMessage, conversationId } = useChat();\n const toggle = useCallback(() => setIsOpen((v) => !v), []);\n const close = useCallback(() => setIsOpen(false), []);\n const value = useMemo(\n () => ({ isOpen, toggle, close, messages, sendMessage, conversationId }),\n [isOpen, toggle, close, messages, sendMessage, conversationId]\n );\n return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;\n};\n\nconst useChatContext = (): ChatContextValue => {\n const ctx = useContext(ChatContext);\n if (!ctx) {\n throw new Error('Chat components must be used within <ChatProvider>');\n }\n return ctx;\n};\n\nexport const ChatButton = memo(() => {\n const { isOpen, toggle } = useChatContext();\n return (\n <button\n type=\"button\"\n onClick={toggle}\n aria-pressed={isOpen}\n aria-label={isOpen ? 'Close chat' : 'Open chat'}\n className={`${styles.chatButton} ${isOpen ? styles.chatButtonActive : ''}`}\n >\n <span className={styles.chatButtonIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path\n d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </span>\n </button>\n );\n});\n\nChatButton.displayName = 'ChatButton';\n\nconst MessageBubble = memo(({ message }: { message: ChatMessage }) => {\n const isUser = message.role === 'user';\n return (\n <li\n className={`${styles.messageRow} ${isUser ? styles.messageRowUser : styles.messageRowReplica}`}\n >\n <span\n className={`${styles.messageRole} ${\n isUser ? styles.messageRoleUser : styles.messageRoleReplica\n }`}\n >\n {isUser ? 'You' : 'Replica'}\n </span>\n <span\n className={`${styles.messageBubble} ${\n isUser ? styles.messageBubbleUser : styles.messageBubbleReplica\n }`}\n >\n {message.text}\n </span>\n </li>\n );\n});\n\nMessageBubble.displayName = 'MessageBubble';\n\nexport const ChatPanel = memo(() => {\n const { isOpen, close, messages, sendMessage, conversationId } = useChatContext();\n const [draft, setDraft] = useState('');\n const listRef = useRef<HTMLUListElement | null>(null);\n\n useEffect(() => {\n const el = listRef.current;\n if (el) {\n el.scrollTop = el.scrollHeight;\n }\n }, [messages.length]);\n\n const submit = useCallback(() => {\n const trimmed = draft.trim();\n if (!trimmed || !conversationId) {\n return;\n }\n sendMessage(trimmed);\n setDraft('');\n }, [draft, sendMessage, conversationId]);\n\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n submit();\n }\n },\n [submit]\n );\n\n const canSend = !!draft.trim() && !!conversationId;\n\n return (\n <aside\n className={`${styles.panel} ${isOpen ? styles.panelOpen : ''}`}\n aria-hidden={!isOpen}\n inert={!isOpen}\n >\n <header className={styles.header}>\n <span>Chat</span>\n <button\n type=\"button\"\n className={styles.closeButton}\n onClick={close}\n aria-label=\"Close chat\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path\n d=\"M18 6L6 18M6 6l12 12\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n </button>\n </header>\n\n <ul ref={listRef} className={styles.messageList} role=\"log\" aria-live=\"polite\">\n {messages.length === 0 ? (\n <li className={styles.empty}>\n {conversationId ? 'Send a message to start the conversation.' : 'Connecting…'}\n </li>\n ) : (\n messages.map((m) => <MessageBubble key={m.id} message={m} />)\n )}\n </ul>\n\n <form\n className={styles.composer}\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n <textarea\n className={styles.composerInput}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder={conversationId ? 'Type a message…' : 'Connecting…'}\n disabled={!conversationId}\n rows={1}\n aria-label=\"Message\"\n />\n <button\n type=\"submit\"\n className={styles.sendButton}\n disabled={!canSend}\n aria-label=\"Send message\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path\n d=\"M3 12l18-9-9 18-2-7-7-2z\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n />\n </svg>\n </button>\n </form>\n </aside>\n );\n});\n\nChatPanel.displayName = 'ChatPanel';\n",
|
|
300
|
+
styles: ".chatButton {\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n color: #000;\n cursor: pointer;\n}\n\n.chatButtonActive {\n background-color: white;\n color: #020617;\n}\n\n@container conversation (max-width: 400px) {\n .chatButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n}\n\n.chatButtonIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.panel {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n max-width: 100%;\n z-index: 25;\n display: flex;\n flex-direction: column;\n background-color: rgba(2, 6, 23, 0.85);\n backdrop-filter: blur(8px);\n color: white;\n transform: translateX(100%);\n transition: transform 200ms ease;\n pointer-events: none;\n}\n\n.panelOpen {\n transform: translateX(0);\n pointer-events: auto;\n}\n\n@container conversation (min-width: 768px) {\n .panel {\n width: 24rem;\n }\n}\n\n.header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1.25rem;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n font-weight: 600;\n font-size: 1rem;\n}\n\n.closeButton {\n height: 2rem;\n width: 2rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n background: transparent;\n border: none;\n color: white;\n cursor: pointer;\n}\n\n.closeButton:hover {\n background-color: rgba(255, 255, 255, 0.1);\n}\n\n.messageList {\n flex: 1;\n overflow-y: auto;\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n list-style: none;\n margin: 0;\n}\n\n.messageRow {\n display: flex;\n flex-direction: column;\n max-width: 85%;\n gap: 0.25rem;\n}\n\n.messageRowUser {\n align-self: flex-end;\n align-items: flex-end;\n}\n\n.messageRowReplica {\n align-self: flex-start;\n align-items: flex-start;\n}\n\n.messageRole {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n opacity: 0.7;\n}\n\n.messageRoleReplica {\n color: #93c5fd;\n}\n\n.messageRoleUser {\n color: #fcd34d;\n}\n\n.messageBubble {\n padding: 0.625rem 0.875rem;\n border-radius: 1rem;\n font-size: 0.95rem;\n line-height: 1.4;\n word-break: break-word;\n}\n\n.messageBubbleUser {\n background-color: rgba(252, 211, 77, 0.18);\n border: 1px solid rgba(252, 211, 77, 0.35);\n border-bottom-right-radius: 0.25rem;\n text-align: end;\n}\n\n.messageBubbleReplica {\n background-color: rgba(147, 197, 253, 0.18);\n border: 1px solid rgba(147, 197, 253, 0.35);\n border-bottom-left-radius: 0.25rem;\n text-align: start;\n}\n\n.composer {\n display: flex;\n gap: 0.5rem;\n padding: 0.75rem;\n border-top: 1px solid rgba(255, 255, 255, 0.1);\n padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0));\n}\n\n.composerInput {\n flex: 1;\n resize: none;\n min-height: 2.5rem;\n max-height: 8rem;\n padding: 0.5rem 0.75rem;\n border-radius: 0.75rem;\n background-color: rgba(255, 255, 255, 0.08);\n border: 1px solid rgba(255, 255, 255, 0.15);\n color: white;\n font-family: inherit;\n font-size: 0.95rem;\n line-height: 1.4;\n outline: none;\n}\n\n.composerInput:focus {\n border-color: rgba(255, 255, 255, 0.4);\n}\n\n.composerInput:disabled {\n opacity: 0.5;\n}\n\n.sendButton {\n height: 2.5rem;\n width: 2.5rem;\n border-radius: 9999px;\n background-color: white;\n color: #020617;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.sendButton:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.empty {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 2rem;\n text-align: center;\n color: rgba(255, 255, 255, 0.5);\n font-size: 0.9rem;\n}\n",
|
|
301
|
+
componentsDependencies: ["use-chat"]
|
|
302
|
+
};
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/templates/tsx/components/closed-captions.json
|
|
305
|
+
var closed_captions_default$1 = {
|
|
306
|
+
type: "components",
|
|
307
|
+
content: "import React, { createContext, memo, useCallback, useContext, useMemo, useState } from 'react';\nimport { useClosedCaption } from '../../hooks/use-closed-caption';\nimport styles from './closed-captions.module.css';\n\ntype ClosedCaptionsContextValue = {\n isEnabled: boolean;\n toggle: () => void;\n};\n\nconst ClosedCaptionsContext = createContext<ClosedCaptionsContextValue | null>(null);\n\nexport const ClosedCaptionsProvider = ({\n children,\n defaultEnabled = false,\n}: {\n children: React.ReactNode;\n defaultEnabled?: boolean;\n}) => {\n const [isEnabled, setIsEnabled] = useState(defaultEnabled);\n const toggle = useCallback(() => setIsEnabled((v) => !v), []);\n const value = useMemo(() => ({ isEnabled, toggle }), [isEnabled, toggle]);\n return <ClosedCaptionsContext.Provider value={value}>{children}</ClosedCaptionsContext.Provider>;\n};\n\nexport const useClosedCaptionsContext = (): ClosedCaptionsContextValue => {\n const ctx = useContext(ClosedCaptionsContext);\n if (!ctx) {\n throw new Error('ClosedCaptions components must be used within <ClosedCaptionsProvider>');\n }\n return ctx;\n};\n\nexport const ClosedCaptionsButton = memo(() => {\n const { isEnabled, toggle } = useClosedCaptionsContext();\n\n return (\n <button\n type=\"button\"\n onClick={toggle}\n aria-pressed={isEnabled}\n className={`${styles.captionButton} ${isEnabled ? styles.captionButtonActive : ''}`}\n >\n <span className={styles.captionButtonIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label={isEnabled ? 'Closed Captions On' : 'Closed Captions Off'}\n >\n <rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\" stroke=\"currentColor\" strokeWidth=\"2\" />\n <path\n d=\"M10 11C10 10.4477 9.55228 10 9 10H8C7.44772 10 7 10.4477 7 11V13C7 13.5523 7.44772 14 8 14H9C9.55228 14 10 13.5523 10 13\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n <path\n d=\"M17 11C17 10.4477 16.5523 10 16 10H15C14.4477 10 14 10.4477 14 11V13C14 13.5523 14.4477 14 15 14H16C16.5523 14 17 13.5523 17 13\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n </span>\n <span className={styles.srOnly}>Closed Captions</span>\n </button>\n );\n});\n\nClosedCaptionsButton.displayName = 'ClosedCaptionsButton';\n\nexport const ClosedCaptions = memo(() => {\n const { isEnabled } = useClosedCaptionsContext();\n const caption = useClosedCaption();\n\n if (!isEnabled || !caption || !caption.text) {\n return null;\n }\n\n return (\n <div className={styles.container} role=\"status\" aria-live=\"polite\">\n <span\n className={`${styles.role} ${caption.role === 'replica' ? styles.roleReplica : styles.roleUser}`}\n >\n {caption.role === 'replica' ? 'Replica' : 'You'}\n </span>\n <span className={styles.text}>\n <span className={styles.textInner}>{caption.text}</span>\n </span>\n </div>\n );\n});\n\nClosedCaptions.displayName = 'ClosedCaptions';\n",
|
|
308
|
+
styles: ".captionButton {\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n color: #000;\n cursor: pointer;\n}\n\n.captionButtonActive {\n background-color: white;\n color: #020617;\n}\n\n@container conversation (max-width: 400px) {\n .captionButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n}\n\n.captionButtonIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n.container {\n position: absolute;\n bottom: 5.5rem;\n left: 50%;\n transform: translateX(-50%);\n z-index: 15;\n width: min(85%, 40rem);\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.2rem;\n padding: 0.625rem 1rem;\n border-radius: 0.625rem;\n background-color: rgba(2, 6, 23, 0.7);\n backdrop-filter: blur(8px);\n color: white;\n font-size: 0.95rem;\n line-height: 1.4;\n text-align: center;\n pointer-events: none;\n --cc-line-height: 1.4;\n --cc-max-lines: 3;\n}\n\n.role {\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n opacity: 0.8;\n}\n\n.roleReplica {\n color: #93c5fd;\n}\n\n.roleUser {\n color: #fcd34d;\n}\n\n.text {\n display: flex;\n align-items: flex-end;\n justify-content: center;\n width: 100%;\n max-height: calc(var(--cc-line-height) * var(--cc-max-lines) * 1em);\n line-height: var(--cc-line-height);\n overflow: hidden;\n}\n\n.textInner {\n display: block;\n word-break: break-word;\n}\n",
|
|
309
|
+
componentsDependencies: ["use-closed-caption"]
|
|
310
|
+
};
|
|
311
|
+
//#endregion
|
|
296
312
|
//#region src/templates/tsx/components/conversation-01.json
|
|
297
313
|
var conversation_01_default$1 = {
|
|
298
314
|
type: "components",
|
|
299
|
-
content: "import React, {
|
|
300
|
-
styles: ".container {\n position: relative;\n width: 100%;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n aspect-ratio: 9/16;\n overflow: hidden;\n border-radius: 0.5rem;\n max-height: 90vh;\n
|
|
315
|
+
content: "import React, { memo, useCallback, useEffect, useRef, useState } from 'react';\nimport {\n DailyAudioTrack,\n DailyVideo,\n useDevices,\n useLocalSessionId,\n useMeetingState,\n useScreenVideoTrack,\n useVideoTrack,\n} from '@daily-co/daily-react';\nimport { MicSelectBtn, CameraSelectBtn, ScreenShareButton } from '../device-select';\nimport { ClosedCaptions, ClosedCaptionsButton, ClosedCaptionsProvider } from '../closed-captions';\nimport { ChatButton, ChatPanel, ChatProvider } from '../chat';\nimport { ConnectingState, LeavingState } from '../conversation-status';\nimport { useLocalScreenshare } from '../../hooks/use-local-screenshare';\nimport { useReplicaIDs } from '../../hooks/use-replica-ids';\nimport { useCVICall } from '../../hooks/use-cvi-call';\nimport { AudioWave } from '../audio-wave';\n\nimport styles from './conversation.module.css';\n\ninterface ConversationProps {\n onLeave: () => void;\n conversationUrl: string;\n}\n\nconst VideoPreview = React.memo(({ id }: { id: string }) => {\n const videoState = useVideoTrack(id);\n\n return (\n <div\n className={`${styles.previewVideoContainer} ${videoState.isOff ? styles.previewVideoContainerHidden : ''}`}\n >\n <DailyVideo\n automirror\n sessionId={id}\n type=\"video\"\n fit=\"cover\"\n className={`${styles.previewVideo} ${videoState.isOff ? styles.previewVideoHidden : ''}`}\n />\n <div className={styles.audioWaveContainer}>\n <AudioWave id={id} />\n </div>\n </div>\n );\n});\n\nconst PreviewVideos = React.memo(() => {\n const localId = useLocalSessionId();\n const { isScreenSharing } = useLocalScreenshare();\n const replicaIds = useReplicaIDs();\n const replicaId = replicaIds[0];\n\n return (\n <>\n {isScreenSharing && <VideoPreview id={replicaId} />}\n <VideoPreview id={localId} />\n </>\n );\n});\n\nconst SelfView = React.memo(() => (\n <div className={styles.selfViewContainer}>\n <PreviewVideos />\n </div>\n));\n\nconst MainVideo = React.memo(() => {\n const replicaIds = useReplicaIDs();\n const localId = useLocalSessionId();\n const videoState = useVideoTrack(replicaIds[0]);\n const screenVideoState = useScreenVideoTrack(localId);\n const meetingState = useMeetingState();\n const isScreenSharing = !screenVideoState.isOff;\n const replicaId = replicaIds[0];\n const [hasReplicaConnected, setHasReplicaConnected] = useState(false);\n\n useEffect(() => {\n if (replicaId && videoState.state === 'playable') {\n setHasReplicaConnected(true);\n }\n }, [replicaId, videoState.state]);\n\n if (meetingState === 'left-meeting' || meetingState === 'error') {\n return <LeavingState />;\n }\n\n if (!hasReplicaConnected) {\n return <ConnectingState />;\n }\n\n if (!replicaId) {\n return <ConnectingState />;\n }\n\n return (\n <div\n className={`${styles.mainVideoContainer} ${isScreenSharing ? styles.mainVideoContainerScreenSharing : ''}`}\n >\n <DailyVideo\n automirror\n sessionId={isScreenSharing ? localId : replicaId}\n type={isScreenSharing ? 'screenVideo' : 'video'}\n className={`${styles.mainVideo}\n ${isScreenSharing ? styles.mainVideoScreenSharing : ''}\n ${videoState.isOff ? styles.mainVideoHidden : ''}`}\n />\n <DailyAudioTrack sessionId={replicaId} />\n </div>\n );\n});\n\nconst MoreMenu = memo(() => {\n const [isOpen, setIsOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!isOpen) {\n return;\n }\n const handlePointerDown = (e: PointerEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) {\n setIsOpen(false);\n }\n };\n const handleKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n setIsOpen(false);\n }\n };\n document.addEventListener('pointerdown', handlePointerDown);\n document.addEventListener('keydown', handleKey);\n return () => {\n document.removeEventListener('pointerdown', handlePointerDown);\n document.removeEventListener('keydown', handleKey);\n };\n }, [isOpen]);\n\n return (\n <div ref={ref} className={styles.moreMenu}>\n <button\n type=\"button\"\n onClick={() => setIsOpen((v) => !v)}\n aria-pressed={isOpen}\n aria-label={isOpen ? 'Close more controls' : 'More controls'}\n aria-haspopup=\"true\"\n aria-expanded={isOpen}\n className={`${styles.moreButton} ${isOpen ? styles.moreButtonActive : ''}`}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <circle cx=\"5\" cy=\"12\" r=\"1.75\" fill=\"currentColor\" />\n <circle cx=\"12\" cy=\"12\" r=\"1.75\" fill=\"currentColor\" />\n <circle cx=\"19\" cy=\"12\" r=\"1.75\" fill=\"currentColor\" />\n </svg>\n </button>\n {isOpen && (\n <div className={styles.morePopover} role=\"menu\">\n <ScreenShareButton />\n <ClosedCaptionsButton />\n </div>\n )}\n </div>\n );\n});\n\nMoreMenu.displayName = 'MoreMenu';\n\nexport const Conversation = React.memo(({ onLeave, conversationUrl }: ConversationProps) => {\n const { joinCall, leaveCall } = useCVICall();\n const meetingState = useMeetingState();\n const { hasMicError } = useDevices();\n\n useEffect(() => {\n if (meetingState === 'error') {\n onLeave();\n }\n }, [meetingState, onLeave]);\n\n useEffect(() => {\n joinCall({ url: conversationUrl });\n }, []);\n\n const handleLeave = useCallback(() => {\n leaveCall();\n onLeave();\n }, [leaveCall, onLeave]);\n\n return (\n <ClosedCaptionsProvider>\n <ChatProvider>\n <div className={styles.containerWrapper}>\n <div className={styles.container}>\n <div className={styles.videoContainer}>\n {hasMicError && (\n <div className={styles.errorContainer}>\n <p>\n Camera or microphone access denied. Please check your settings and try again.\n </p>\n </div>\n )}\n\n <div className={styles.mainVideoContainer}>\n <MainVideo />\n </div>\n\n <SelfView />\n\n <ClosedCaptions />\n </div>\n\n <ChatPanel />\n\n <div\n className={`${styles.footer} ${meetingState === 'left-meeting' ? styles.footerLeaving : ''}`}\n aria-hidden={meetingState === 'left-meeting'}\n >\n <div className={styles.footerControls}>\n <MicSelectBtn />\n <CameraSelectBtn />\n <MoreMenu />\n <ChatButton />\n <button type=\"button\" className={styles.leaveButton} onClick={handleLeave}>\n <span className={styles.leaveButtonIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Leave Call\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </span>\n </button>\n </div>\n </div>\n </div>\n </div>\n </ChatProvider>\n </ClosedCaptionsProvider>\n );\n});\n",
|
|
316
|
+
styles: ".containerWrapper {\n width: 100%;\n container-type: inline-size;\n container-name: conversation;\n}\n\n.container {\n position: relative;\n width: 100%;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n aspect-ratio: 9/16;\n overflow: hidden;\n border-radius: 0.5rem;\n max-height: 90vh;\n background: linear-gradient(135deg, #4b5563 0%, #1f2937 100%);\n background-size: 400% 400%;\n animation: gradient 15s ease infinite;\n}\n\n/* Aspect ratio tracks the wrapper's actual width: portrait when narrow\n (e.g. embedded in a 400px modal), landscape when wide. */\n@container conversation (min-width: 768px) {\n .container {\n aspect-ratio: 16/9;\n }\n}\n\n.errorContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(248, 250, 252, 0.08);\n color: white;\n height: 100%;\n font-size: 1.5rem;\n font-weight: 600;\n text-align: center;\n}\n\n.videoContainer {\n position: relative;\n z-index: 5;\n width: 100%;\n height: 100%;\n}\n\n.footer {\n position: absolute;\n bottom: 1.5rem;\n left: 0;\n right: 0;\n z-index: 20;\n transition:\n opacity 0.3s ease,\n transform 0.3s ease;\n}\n\n.footerLeaving {\n opacity: 0;\n transform: translateY(20px);\n pointer-events: none;\n}\n\n.footerControls {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 16px;\n}\n\n.moreMenu {\n display: block;\n position: relative;\n}\n\n.moreButton {\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n color: #000;\n cursor: pointer;\n}\n\n.moreButtonActive {\n background-color: white;\n color: #020617;\n}\n\n.morePopover {\n position: absolute;\n bottom: calc(100% + 0.5rem);\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n padding: 0.5rem;\n border-radius: 1rem;\n backdrop-filter: blur(16px);\n background-color: rgba(162, 162, 162, 0.2);\n z-index: 30;\n}\n\n.leaveButton {\n background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);\n color: white;\n border: none;\n font-size: 16px;\n cursor: pointer;\n transition: all 0.3s ease;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: #ef4444;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.leaveButton:hover {\n opacity: 0.8;\n}\n\n.leaveButtonIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n@container conversation (max-width: 400px) {\n .footerControls {\n gap: 8px;\n }\n .moreButton,\n .leaveButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n}\n\n/* ReplicaVideo styles */\n.mainVideoContainer {\n background: transparent;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.mainVideoContainerScreenSharing {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.mainVideo {\n position: absolute;\n inset: 0;\n object-position: center;\n object-fit: cover !important;\n height: 100%;\n width: 100%;\n transition: all 0.3s ease;\n}\n\n.mainVideoScreenSharing {\n object-fit: contain !important;\n}\n\n.mainVideoHidden {\n display: none;\n}\n\n/* PreviewVideo styles */\n.previewVideoContainer {\n position: relative;\n background: rgba(2, 6, 23, 0.3);\n aspect-ratio: 1/1;\n width: 6rem;\n border-radius: 0.75rem;\n overflow: hidden;\n z-index: 10;\n}\n\n@container conversation (min-width: 768px) {\n .previewVideoContainer {\n width: 7rem;\n }\n}\n\n@container conversation (min-width: 1024px) {\n .previewVideoContainer {\n width: 8rem;\n }\n}\n\n.previewVideoContainerHidden {\n background: transparent;\n display: none;\n}\n\n.previewVideo {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.previewVideoHidden {\n display: none;\n}\n\n/* Main video container */\n.mainVideoContainer {\n width: 100%;\n height: 100%;\n}\n\n/* Self view container — pinned to top-right so captions own the bottom band. */\n.selfViewContainer {\n position: absolute;\n top: 1rem;\n left: 1rem;\n display: flex;\n align-items: flex-end;\n flex-direction: column;\n gap: 0.5rem;\n z-index: 15;\n}\n\n.selfViewContainer video {\n object-position: center center;\n object-fit: cover;\n}\n\n/* Waiting message container */\n/* Start of Selection */\n.waitingContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n color: white;\n height: 100%;\n font-size: 1.5rem;\n font-weight: 600;\n}\n\n@keyframes gradient {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n/* End of Selection */\n\n.audioWaveContainer {\n position: absolute;\n bottom: 0.5rem;\n right: 0.5rem;\n}\n",
|
|
317
|
+
componentsDependencies: [
|
|
318
|
+
"device-select",
|
|
319
|
+
"closed-captions",
|
|
320
|
+
"chat",
|
|
321
|
+
"conversation-status",
|
|
322
|
+
"use-local-screenshare",
|
|
323
|
+
"use-replica-ids",
|
|
324
|
+
"use-cvi-call",
|
|
325
|
+
"audio-wave",
|
|
326
|
+
"cvi-provider"
|
|
327
|
+
]
|
|
328
|
+
};
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/templates/tsx/components/conversation-02.json
|
|
331
|
+
var conversation_02_default$1 = {
|
|
332
|
+
type: "components",
|
|
333
|
+
content: "import React, { useCallback, useEffect, useState } from 'react';\nimport {\n DailyAudioTrack,\n DailyVideo,\n useDevices,\n useLocalSessionId,\n useMeetingState,\n useScreenVideoTrack,\n useVideoTrack,\n} from '@daily-co/daily-react';\nimport { MicSelectBtn, CameraSelectBtn, ScreenShareButton } from '../device-select';\nimport { ConnectingState, LeavingState } from '../conversation-status';\nimport { useLocalScreenshare } from '../../hooks/use-local-screenshare';\nimport { useReplicaIDs } from '../../hooks/use-replica-ids';\nimport { useCVICall } from '../../hooks/use-cvi-call';\nimport { AudioWave } from '../audio-wave';\n\nimport styles from './conversation.module.css';\n\ninterface ConversationProps {\n onLeave: () => void;\n conversationUrl: string;\n}\n\nconst VideoPreview = React.memo(({ id }: { id: string }) => {\n const videoState = useVideoTrack(id);\n const widthVideo = videoState.track?.getSettings()?.width;\n const heightVideo = videoState.track?.getSettings()?.height;\n const isVertical = widthVideo && heightVideo ? widthVideo < heightVideo : false;\n\n return (\n <div\n className={`${styles.previewVideoContainer} ${isVertical ? styles.previewVideoContainerVertical : ''} ${videoState.isOff ? styles.previewVideoContainerHidden : ''}`}\n >\n <DailyVideo\n automirror\n sessionId={id}\n type=\"video\"\n className={`${styles.previewVideo} ${isVertical ? styles.previewVideoVertical : ''} ${videoState.isOff ? styles.previewVideoHidden : ''}`}\n />\n <div className={styles.audioWaveContainer}>\n <AudioWave id={id} />\n </div>\n </div>\n );\n});\n\nconst PreviewVideos = React.memo(() => {\n const localId = useLocalSessionId();\n const { isScreenSharing } = useLocalScreenshare();\n const replicaIds = useReplicaIDs();\n const replicaId = replicaIds[0];\n\n return (\n <>\n {isScreenSharing && <VideoPreview id={replicaId} />}\n <VideoPreview id={localId} />\n </>\n );\n});\n\nconst MainVideo = React.memo(() => {\n const replicaIds = useReplicaIDs();\n const localId = useLocalSessionId();\n const videoState = useVideoTrack(replicaIds[0]);\n const screenVideoState = useScreenVideoTrack(localId);\n const meetingState = useMeetingState();\n const isScreenSharing = !screenVideoState.isOff;\n // This is one-to-one call, so we can use the first replica id\n const replicaId = replicaIds[0];\n const [hasReplicaConnected, setHasReplicaConnected] = useState(false);\n\n useEffect(() => {\n if (replicaId && videoState.state === 'playable') {\n setHasReplicaConnected(true);\n }\n }, [replicaId, videoState.state]);\n\n if (meetingState === 'left-meeting' || meetingState === 'error') {\n return <LeavingState />;\n }\n\n if (!hasReplicaConnected) {\n return <ConnectingState />;\n }\n\n if (!replicaId) {\n return <ConnectingState />;\n }\n\n // Switching between replica video and screen sharing video\n return (\n <div\n className={`${styles.mainVideoContainer} ${isScreenSharing ? styles.mainVideoContainerScreenSharing : ''}`}\n >\n <DailyVideo\n automirror\n sessionId={isScreenSharing ? localId : replicaId}\n type={isScreenSharing ? 'screenVideo' : 'video'}\n className={`${styles.mainVideo}\n ${isScreenSharing ? styles.mainVideoScreenSharing : ''}\n ${videoState.isOff ? styles.mainVideoHidden : ''}`}\n />\n <DailyAudioTrack sessionId={replicaId} />\n </div>\n );\n});\n\nexport const Conversation = React.memo(({ onLeave, conversationUrl }: ConversationProps) => {\n const { joinCall, leaveCall } = useCVICall();\n const meetingState = useMeetingState();\n const { hasMicError } = useDevices();\n\n useEffect(() => {\n if (meetingState === 'error') {\n onLeave();\n }\n }, [meetingState, onLeave]);\n\n // Initialize call when conversation is available\n useEffect(() => {\n joinCall({ url: conversationUrl });\n }, []);\n\n const handleLeave = useCallback(() => {\n leaveCall();\n onLeave();\n }, [leaveCall, onLeave]);\n\n return (\n <div className={styles.containerWrapper}>\n <div className={styles.container}>\n <div className={styles.videoContainer}>\n {hasMicError && (\n <div className={styles.errorContainer}>\n <p>Camera or microphone access denied. Please check your settings and try again.</p>\n </div>\n )}\n\n {/* Main video */}\n <div className={styles.mainVideoContainer}>\n <MainVideo />\n </div>\n\n {/* Self view */}\n <div className={styles.selfViewContainer}>\n <PreviewVideos />\n </div>\n </div>\n\n <div\n className={`${styles.footer} ${meetingState === 'left-meeting' ? styles.footerLeaving : ''}`}\n aria-hidden={meetingState === 'left-meeting'}\n >\n <div className={styles.footerControls}>\n <MicSelectBtn />\n <CameraSelectBtn />\n <ScreenShareButton />\n <button type=\"button\" className={styles.leaveButton} onClick={handleLeave}>\n <span className={styles.leaveButtonIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Leave Call\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </span>\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n});\n",
|
|
334
|
+
styles: ".containerWrapper {\n width: 100%;\n container-type: inline-size;\n container-name: conversation;\n}\n\n.container {\n position: relative;\n width: 100%;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n aspect-ratio: 9/16;\n overflow: hidden;\n border-radius: 0.5rem;\n max-height: 90vh;\n background: linear-gradient(135deg, #4b5563 0%, #1f2937 100%);\n background-size: 400% 400%;\n animation: gradient 15s ease infinite;\n}\n\n/* Aspect ratio tracks the wrapper's actual width: portrait when narrow,\n landscape when wide. */\n@container conversation (min-width: 768px) {\n .container {\n aspect-ratio: 16/9;\n }\n}\n\n.errorContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(248, 250, 252, 0.08);\n color: white;\n height: 100%;\n font-size: 1.5rem;\n font-weight: 600;\n text-align: center;\n}\n\n.videoContainer {\n position: relative;\n z-index: 5;\n width: 100%;\n height: 100%;\n}\n\n.footer {\n position: absolute;\n bottom: 1.5rem;\n left: 0;\n right: 0;\n z-index: 20;\n transition:\n opacity 0.3s ease,\n transform 0.3s ease;\n}\n\n.footerLeaving {\n opacity: 0;\n transform: translateY(20px);\n pointer-events: none;\n}\n\n.footerControls {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 16px;\n}\n\n.leaveButton {\n background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);\n color: white;\n border: none;\n font-size: 16px;\n cursor: pointer;\n transition: all 0.3s ease;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: #ef4444;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.leaveButton:hover {\n opacity: 0.8;\n}\n\n.leaveButtonIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* ReplicaVideo styles */\n.mainVideoContainer {\n background: transparent;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.mainVideoContainerScreenSharing {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.mainVideo {\n position: absolute;\n inset: 0;\n object-position: center;\n object-fit: cover !important;\n height: 100%;\n width: 100%;\n transition: all 0.3s ease;\n}\n\n.mainVideoScreenSharing {\n object-fit: contain !important;\n}\n\n.mainVideoHidden {\n display: none;\n}\n\n/* PreviewVideo styles */\n.previewVideoContainer {\n position: relative;\n background: rgba(2, 6, 23, 0.3);\n aspect-ratio: 16/9;\n width: 13rem;\n border-radius: 1rem;\n overflow: hidden;\n max-height: 140px;\n z-index: 10;\n}\n\n@container conversation (min-width: 768px) {\n .previewVideoContainer {\n width: 15rem;\n max-height: 160px;\n }\n}\n\n@container conversation (min-width: 1024px) {\n .previewVideoContainer {\n width: 18rem;\n max-height: 200px;\n }\n}\n\n.previewVideoContainerVertical {\n height: 40.5rem;\n width: 6rem;\n}\n\n.previewVideoContainerHidden {\n background: transparent;\n display: none;\n}\n\n.previewVideo {\n width: 100%;\n height: auto;\n max-height: 120px;\n}\n\n@container conversation (min-width: 768px) {\n .previewVideo {\n max-height: 100%;\n }\n}\n\n.previewVideoVertical {\n height: 40.5rem;\n width: 6rem;\n object-fit: cover;\n}\n\n.previewVideoHidden {\n display: none;\n}\n\n/* Main video container */\n.mainVideoContainer {\n width: 100%;\n height: 100%;\n}\n\n/* Self view container */\n.selfViewContainer {\n position: absolute;\n bottom: 5.5rem;\n left: 1rem;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n@container conversation (min-width: 1024px) {\n .selfViewContainer {\n bottom: 1rem;\n }\n}\n\n/* Waiting message container */\n/* Start of Selection */\n.waitingContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n color: white;\n height: 100%;\n font-size: 1.5rem;\n font-weight: 600;\n}\n\n@keyframes gradient {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n/* End of Selection */\n\n.audioWaveContainer {\n position: absolute;\n bottom: 0.5rem;\n right: 0.5rem;\n}\n",
|
|
301
335
|
componentsDependencies: [
|
|
302
336
|
"device-select",
|
|
337
|
+
"conversation-status",
|
|
303
338
|
"use-local-screenshare",
|
|
304
339
|
"use-replica-ids",
|
|
305
340
|
"use-cvi-call",
|
|
@@ -308,18 +343,26 @@ var conversation_01_default$1 = {
|
|
|
308
343
|
]
|
|
309
344
|
};
|
|
310
345
|
//#endregion
|
|
346
|
+
//#region src/templates/tsx/components/conversation-status.json
|
|
347
|
+
var conversation_status_default$1 = {
|
|
348
|
+
type: "components",
|
|
349
|
+
content: "import React, { memo } from 'react';\nimport styles from './conversation-status.module.css';\n\nconst GRID_SIZE = 144;\nconst CONNECT_STAGGER_S = 1.8;\nconst LEAVE_STAGGER_S = 1.0;\n\n// Pre-shuffled positions, computed once at module load. Each square gets a\n// stable position in the random fill order so re-renders never re-shuffle.\nconst SHUFFLED_ORDER: number[] = (() => {\n const order = Array.from({ length: GRID_SIZE }, (_, i) => i);\n for (let i = order.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [order[i], order[j]] = [order[j], order[i]];\n }\n return order;\n})();\n\n// Per-square target opacity, ranged 0.25–0.75, gives the grid a dithered\n// look so cells don't all read at the same brightness when filled.\nconst TARGET_OPACITY: number[] = Array.from(\n { length: GRID_SIZE },\n () => 0.25 + Math.random() * 0.5\n);\n\n// Per-square Bayer pattern offset (0–7px on each axis) so the dither\n// patterns don't line up across cells — adds visual noise to the grid.\nconst DITHER_OFFSETS: string[] = Array.from({ length: GRID_SIZE }, () => {\n const x = Math.floor(Math.random() * 8);\n const y = Math.floor(Math.random() * 8);\n return `${x}px ${y}px`;\n});\n\nconst Grid = ({ leaving = false }: { leaving?: boolean }) => {\n const squareClass = leaving ? styles.squareLeaving : styles.squareConnecting;\n const totalStagger = leaving ? LEAVE_STAGGER_S : CONNECT_STAGGER_S;\n return (\n <div className={styles.grid} aria-hidden=\"true\">\n {Array.from({ length: GRID_SIZE }, (_, i) => {\n const delay = (SHUFFLED_ORDER[i] / GRID_SIZE) * totalStagger;\n const style = {\n '--fill-delay': `${delay.toFixed(3)}s`,\n '--target-opacity': TARGET_OPACITY[i].toFixed(2),\n '--dither-offset': DITHER_OFFSETS[i],\n } as React.CSSProperties;\n return <span key={i} className={`${styles.square} ${squareClass}`} style={style} />;\n })}\n </div>\n );\n};\n\nconst Dots = () => (\n <>\n <span className={styles.dot}>.</span>\n <span className={styles.dot}>.</span>\n <span className={styles.dot}>.</span>\n </>\n);\n\nexport const ConnectingState = memo(() => (\n <div className={styles.statusContainer} role=\"status\" aria-live=\"polite\">\n <Grid />\n <div className={`${styles.label} ${styles.labelConnecting}`}>\n <span className={styles.labelText}>\n Connecting\n <Dots />\n </span>\n </div>\n </div>\n));\n\nConnectingState.displayName = 'ConnectingState';\n\nexport const LeavingState = memo(() => (\n <div className={styles.statusContainer} role=\"status\" aria-live=\"polite\">\n <Grid leaving />\n <div className={`${styles.label} ${styles.labelLeaving}`}>\n <span className={styles.labelText}>\n Leaving\n <Dots />\n </span>\n </div>\n </div>\n));\n\nLeavingState.displayName = 'LeavingState';\n",
|
|
350
|
+
styles: ".statusContainer {\n position: relative;\n width: 100%;\n height: 100%;\n background: #0f0f13;\n overflow: hidden;\n}\n\n.grid {\n position: absolute;\n inset: 0;\n display: grid;\n grid-template-columns: repeat(9, 1fr);\n grid-template-rows: repeat(16, 1fr);\n gap: 0.25rem;\n padding: 0.25rem;\n box-sizing: border-box;\n}\n\n@container conversation (min-width: 768px) {\n .grid {\n grid-template-columns: repeat(16, 1fr);\n grid-template-rows: repeat(9, 1fr);\n }\n}\n\n.square {\n border-radius: 0.25rem;\n background-color: rgba(255, 255, 255, 0.04);\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);\n transform-origin: center;\n opacity: 0;\n}\n\n.squareConnecting {\n background-color: #412174;\n background-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8' shape-rendering='crispEdges'><rect x='4' y='0' width='4' height='4' fill='black' fill-opacity='0.45'/><rect x='0' y='4' width='4' height='4' fill='black' fill-opacity='0.65'/><rect x='4' y='4' width='4' height='4' fill='black' fill-opacity='0.25'/></svg>\");\n background-size: 8px 8px;\n background-repeat: repeat;\n background-position: var(--dither-offset, 0 0);\n animation:\n squareFillIn 0.55s ease-out var(--fill-delay, 0s) forwards,\n squareShimmer 2.6s ease-in-out calc(var(--fill-delay, 0s) + 0.55s) infinite;\n}\n\n@keyframes squareFillIn {\n from {\n transform: scale(0.7);\n opacity: 0;\n }\n to {\n transform: scale(1);\n opacity: var(--target-opacity, 1);\n }\n}\n\n@keyframes squareShimmer {\n 0%,\n 100% {\n opacity: var(--target-opacity, 1);\n }\n 50% {\n opacity: calc(var(--target-opacity, 1) * 0.2);\n }\n}\n\n.squareLeaving {\n background-color: #683d1b;\n background-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8' shape-rendering='crispEdges'><rect x='4' y='0' width='4' height='4' fill='black' fill-opacity='0.45'/><rect x='0' y='4' width='4' height='4' fill='black' fill-opacity='0.65'/><rect x='4' y='4' width='4' height='4' fill='black' fill-opacity='0.25'/></svg>\");\n background-size: 8px 8px;\n background-repeat: repeat;\n background-position: var(--dither-offset, 0 0);\n opacity: var(--target-opacity, 1);\n transform: scale(1);\n animation:\n squareFadeOut 3s ease-out var(--fill-delay, 0s) forwards,\n squareShimmer 2.6s ease-in-out calc(var(--fill-delay, 0s) + 0.55s) infinite;\n}\n\n@keyframes squareFadeOut {\n from {\n opacity: var(--target-opacity, 1);\n transform: scale(1);\n }\n to {\n transform: scale(0.7);\n opacity: 0;\n }\n}\n\n.label {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2;\n pointer-events: none;\n font-size: 1rem;\n font-weight: 500;\n}\n\n.labelConnecting {\n color: #ffffff;\n}\n\n.labelLeaving {\n color: #ffffff;\n}\n\n.labelText {\n display: inline-flex;\n padding: 0.5rem 1.25rem;\n border-radius: 0.25rem;\n background-color: rgba(15, 23, 42, 0.65);\n backdrop-filter: blur(10px);\n}\n\n.dot {\n display: inline-block;\n opacity: 0;\n animation: dotFade 1.4s ease-in-out infinite;\n}\n\n.dot:nth-child(1) {\n animation-delay: 0s;\n}\n.dot:nth-child(2) {\n animation-delay: 0.2s;\n}\n.dot:nth-child(3) {\n animation-delay: 0.4s;\n}\n\n@keyframes dotFade {\n 0%,\n 80%,\n 100% {\n opacity: 0;\n }\n 40% {\n opacity: 1;\n }\n}\n",
|
|
351
|
+
componentsDependencies: ["cvi-provider"]
|
|
352
|
+
};
|
|
353
|
+
//#endregion
|
|
311
354
|
//#region src/templates/tsx/components/cvi-provider.json
|
|
312
355
|
var cvi_provider_default$1 = {
|
|
313
356
|
type: "components",
|
|
314
|
-
content: "import { DailyProvider } from
|
|
357
|
+
content: "import { DailyProvider } from '@daily-co/daily-react';\n\nexport const CVIProvider = ({ children }: { children: React.ReactNode }) => {\n return <DailyProvider>{children}</DailyProvider>;\n};\n",
|
|
315
358
|
styles: ""
|
|
316
359
|
};
|
|
317
360
|
//#endregion
|
|
318
361
|
//#region src/templates/tsx/components/device-select.json
|
|
319
362
|
var device_select_default$1 = {
|
|
320
363
|
type: "components",
|
|
321
|
-
content: "import React, { memo } from \"react\";\nimport { useDevices } from \"@daily-co/daily-react\";\nimport { useLocalCamera } from \"../../hooks/use-local-camera\";\nimport { useLocalMicrophone } from \"../../hooks/use-local-microphone\";\nimport { useLocalScreenshare } from \"../../hooks/use-local-screenshare\";\nimport styles from \"./device-select.module.css\";\n\nexport const SelectDevice = ({\n value,\n devices,\n disabled,\n onChange,\n}: {\n value: string | undefined;\n devices: { device: MediaDeviceInfo }[];\n disabled: boolean;\n onChange: (value: string) => void;\n}) => {\n return (\n <div className={styles.selectDeviceContainer}>\n <select\n value={value}\n onChange={(e) => onChange(e.target.value)}\n disabled={disabled}\n className={styles.selectDevice}\n >\n {devices.map(({ device }) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))}\n </select>\n <span className={styles.selectArrow}>\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M4 6L8 10L12 6\" stroke=\"#fff\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n </span>\n </div>\n );\n};\n\nexport const MicSelectBtn = memo(() => {\n const { onToggleMicrophone, isMicReady, isMicMuted } = useLocalMicrophone();\n const { microphones, currentMic, setMicrophone } = useDevices();\n\n return (\n <div className={styles.deviceButtonContainer}>\n <button\n type=\"button\"\n onClick={onToggleMicrophone}\n disabled={!isMicReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isMicMuted || !isMicReady ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone Muted\"\n >\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 18.1339 14.0992 18.1339 14.0992M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"1.66667\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <rect\n x=\"1.30225\"\n y=\"3\"\n width=\"26\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 1.30225 3)\"\n fill=\"#020617\"\n />\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M9 9.39031V11C9 11.7956 9.31607 12.5587 9.87868 13.1213C10.4413 13.6839 11.2044 14 12 14C12.7956 14 13.5587 13.6839 14.1213 13.1213C14.2829 12.9597 14.4242 12.7816 14.5435 12.5908L9 9.39031ZM15 7.71466V6C15 5.20435 14.6839 4.44129 14.1213 3.87868C13.5587 3.31607 12.7956 3 12 3C11.2044 3 10.4413 3.31607 9.87868 3.87868C9.69528 4.06208 9.53807 4.26678 9.40948 4.48697L15 7.71466Z\"\n fill=\"#020617\"\n />\n <path\n d=\"M9 9.39031L9.41667 8.66862C9.15883 8.51976 8.84117 8.51976 8.58333 8.66862C8.3255 8.81748 8.16667 9.09259 8.16667 9.39031H9ZM9.87868 13.1213L9.28942 13.7106H9.28942L9.87868 13.1213ZM14.1213 13.1213L14.7106 13.7106L14.7106 13.7106L14.1213 13.1213ZM14.5435 12.5908L15.25 13.0327C15.3699 12.841 15.4068 12.6088 15.3521 12.3894C15.2974 12.17 15.156 11.9822 14.9601 11.8692L14.5435 12.5908ZM15 7.71466L14.5833 8.43635C14.8412 8.58521 15.1588 8.58521 15.4167 8.43635C15.6745 8.28749 15.8333 8.01238 15.8333 7.71466H15ZM14.1213 3.87868L14.7106 3.28942L14.7106 3.28942L14.1213 3.87868ZM9.87868 3.87868L9.28942 3.28942L9.28942 3.28942L9.87868 3.87868ZM9.40948 4.48697L8.68988 4.06671C8.57806 4.25818 8.54715 4.48633 8.604 4.70065C8.66086 4.91497 8.80078 5.09779 8.99281 5.20866L9.40948 4.48697ZM9.83333 11V9.39031H8.16667V11H9.83333ZM10.4679 12.5321C10.0616 12.1257 9.83333 11.5746 9.83333 11H8.16667C8.16667 12.0167 8.57053 12.9917 9.28942 13.7106L10.4679 12.5321ZM12 13.1667C11.4254 13.1667 10.8743 12.9384 10.4679 12.5321L9.28942 13.7106C10.0083 14.4295 10.9833 14.8333 12 14.8333V13.1667ZM13.5321 12.5321C13.1257 12.9384 12.5746 13.1667 12 13.1667V14.8333C13.0167 14.8333 13.9917 14.4295 14.7106 13.7106L13.5321 12.5321ZM13.837 12.149C13.7508 12.2867 13.6488 12.4153 13.5321 12.5321L14.7106 13.7106C14.917 13.5041 15.0976 13.2764 15.25 13.0327L13.837 12.149ZM14.9601 11.8692L9.41667 8.66862L8.58333 10.112L14.1268 13.3125L14.9601 11.8692ZM14.1667 6V7.71466H15.8333V6H14.1667ZM13.5321 4.46794C13.9384 4.87426 14.1667 5.42536 14.1667 6H15.8333C15.8333 4.98334 15.4295 4.00831 14.7106 3.28942L13.5321 4.46794ZM12 3.83333C12.5746 3.83333 13.1257 4.06161 13.5321 4.46794L14.7106 3.28942C13.9917 2.57053 13.0167 2.16667 12 2.16667V3.83333ZM10.4679 4.46794C10.8743 4.06161 11.4254 3.83333 12 3.83333V2.16667C10.9833 2.16667 10.0083 2.57053 9.28942 3.28942L10.4679 4.46794ZM10.1291 4.90724C10.2219 4.74824 10.3354 4.60042 10.4679 4.46794L9.28942 3.28942C9.05511 3.52374 8.85421 3.78533 8.68988 4.06671L10.1291 4.90724ZM8.99281 5.20866L14.5833 8.43635L15.4167 6.99298L9.82615 3.76529L8.99281 5.20866Z\"\n fill=\"#020617\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone\"\n >\n <path\n d=\"M9 6C9 5.20435 9.31607 4.44129 9.87868 3.87868C10.4413 3.31607 11.2044 3 12 3C12.7956 3 13.5587 3.31607 14.1213 3.87868C14.6839 4.44129 15 5.20435 15 6V11C15 11.7956 14.6839 12.5587 14.1213 13.1213C13.5587 13.6839 12.7956 14 12 14C11.2044 14 10.4413 13.6839 9.87868 13.1213C9.31607 12.5587 9 11.7956 9 11V6Z\"\n fill=\"#020617\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 19 12.8565 19 11V10M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Microphone</span>\n </button>\n <SelectDevice\n value={currentMic?.device?.deviceId}\n devices={microphones || []}\n disabled={!isMicReady}\n onChange={(val) => setMicrophone(val)}\n />\n </div>\n );\n});\n\nMicSelectBtn.displayName = \"MicSelectBtn\";\n\nexport const CameraSelectBtn = memo(() => {\n const { onToggleCamera, isCamReady, isCamMuted } = useLocalCamera();\n const { currentCam, cameras, setCamera } = useDevices();\n\n\n return (\n <div className={styles.deviceButtonContainer}>\n <button\n type=\"button\"\n onClick={onToggleCamera}\n disabled={!isCamReady || !currentCam}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isCamMuted ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera Muted\"\n >\n <g clipPath=\"url(#clip0_7082_14220)\">\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M3.19874 5.60093C3.08628 5.68537 2.97928 5.77808 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V15.048C17.7787 14.8204 17.5304 14.6189 17.2595 14.4485L3.19874 5.60093ZM22 12.655V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H8.7412L22 12.655Z\"\n fill=\"#020617\"\n />\n <rect\n x=\"0.777222\"\n y=\"2.64844\"\n width=\"26.7988\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 0.777222 2.64844)\"\n fill=\"#020617\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_7082_14220\">\n <rect width=\"24\" height=\"24\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera\"\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M5 5C4.20435 5 3.44129 5.31607 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V14.414L20.293 16.707C20.4329 16.8468 20.611 16.942 20.805 16.9806C20.9989 17.0192 21.2 16.9993 21.3827 16.9237C21.5654 16.848 21.7215 16.7199 21.8314 16.5555C21.9413 16.391 22 16.1978 22 16V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H5Z\"\n fill=\"#020617\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Camera</span>\n </button>\n <SelectDevice\n value={currentCam?.device?.deviceId}\n devices={cameras || []}\n disabled={!isCamReady}\n onChange={(val) => setCamera(val)}\n />\n </div>\n );\n});\n\nCameraSelectBtn.displayName = \"CameraSelectBtn\";\n\nexport const ScreenShareButton = memo(() => {\n const { onToggleScreenshare, isScreenSharing } = useLocalScreenshare();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleScreenshare}\n className={`${styles.deviceButtonContainer} ${styles.screenShareButton}`}\n >\n <span>\n {isScreenSharing ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M21.0008 19C21.2557 19.0003 21.5009 19.0979 21.6862 19.2728C21.8715 19.4478 21.9831 19.687 21.998 19.9414C22.013 20.1958 21.9302 20.4464 21.7666 20.6418C21.603 20.8373 21.3709 20.9629 21.1178 20.993L21.0008 21H3.00085C2.74597 20.9997 2.50081 20.9021 2.31548 20.7272C2.13014 20.5522 2.01861 20.313 2.00367 20.0586C1.98874 19.8042 2.07152 19.5536 2.23511 19.3582C2.3987 19.1627 2.63075 19.0371 2.88385 19.007L3.00085 19H21.0008ZM19.0008 4C19.5054 3.99984 19.9914 4.19041 20.3614 4.5335C20.7314 4.87659 20.958 5.34684 20.9958 5.85L21.0008 6V16C21.001 16.5046 20.8104 16.9906 20.4673 17.3605C20.1243 17.7305 19.654 17.9572 19.1508 17.995L19.0008 18H5.00085C4.49627 18.0002 4.01028 17.8096 3.6403 17.4665C3.27032 17.1234 3.04369 16.6532 3.00585 16.15L3.00085 16V6C3.00069 5.49542 3.19125 5.00943 3.53434 4.63945C3.87743 4.26947 4.34769 4.04284 4.85085 4.005L5.00085 4H19.0008Z\"\n fill=\"#2D65FF\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M20.9999 19C21.2547 19.0003 21.4999 19.0979 21.6852 19.2728C21.8706 19.4478 21.9821 19.687 21.997 19.9414C22.012 20.1958 21.9292 20.4464 21.7656 20.6418C21.602 20.8373 21.37 20.9629 21.1169 20.993L20.9999 21H2.99987C2.74499 20.9997 2.49984 20.9021 2.3145 20.7272C2.12916 20.5522 2.01763 20.313 2.0027 20.0586C1.98776 19.8042 2.07054 19.5536 2.23413 19.3582C2.39772 19.1627 2.62977 19.0371 2.88287 19.007L2.99987 19H20.9999ZM18.9999 4C19.5044 3.99984 19.9904 4.19041 20.3604 4.5335C20.7304 4.87659 20.957 5.34684 20.9949 5.85L20.9999 6V16C21 16.5046 20.8095 16.9906 20.4664 17.3605C20.1233 17.7305 19.653 17.9572 19.1499 17.995L18.9999 18H4.99987C4.49529 18.0002 4.0093 17.8096 3.63932 17.4665C3.26934 17.1234 3.04271 16.6532 3.00487 16.15L2.99987 16V6C2.99971 5.49542 3.19028 5.00943 3.53337 4.63945C3.87646 4.26947 4.34671 4.04284 4.84987 4.005L4.99987 4H18.9999Z\"\n fill=\"white\"\n />\n </svg>\n )}\n </span>\n </button>\n );\n});\n\nScreenShareButton.displayName = \"ScreenShareButton\";",
|
|
322
|
-
styles: "/* SelectDevice styles */\n.selectDevice {\n height: 3rem;\n width: 5.5rem;\n border-radius: 9999px;\n background-color: rgba(255, 255, 255, 0.2);\n padding: 0 0.75rem;\n border: 1px solid rgba(255, 255, 255, 0.2);\n backdrop-filter: blur(10px);\n color: transparent;\n padding-right: 2rem; /* space for arrow */\n box-sizing: border-box;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n cursor: pointer;\n}\n\n.selectDevice::-ms-expand {\n display: none;\n}\n\n.selectDevice option {\n color: transparent;\n background-color: transparent;\n}\n\n.selectDevice:focus {\n outline: none;\n}\n\n/* Device button container styles */\n.deviceButtonContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n}\n\n/* Device button styles */\n.deviceButton {\n position: absolute;\n left: 0;\n top: 0;\n z-index: 10;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n cursor: pointer;\n}\n\n.deviceButtonIcon {\n display: flex;\n}\n\n.deviceButton:disabled {\n opacity: 0.5;\n}\n\n/* Screen reader only text */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n/* Controls container */\n.controlsContainer {\n display: flex;\n gap: 1rem;\n justify-content: center;\n align-items: center;\n}\n\n.selectDeviceContainer {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.selectArrow {\n position: absolute;\n right: 1rem;\n pointer-events: none;\n display: flex;\n align-items: center;\n height: 100%;\n}\n\n.screenShareButton {\n cursor: pointer;\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n}\n",
|
|
364
|
+
content: "import React, { memo } from 'react';\nimport { useDevices } from '@daily-co/daily-react';\nimport { useLocalCamera } from '../../hooks/use-local-camera';\nimport { useLocalMicrophone } from '../../hooks/use-local-microphone';\nimport { useLocalScreenshare } from '../../hooks/use-local-screenshare';\nimport styles from './device-select.module.css';\n\nexport const SelectDevice = ({\n value,\n devices,\n disabled,\n onChange,\n}: {\n value: string | undefined;\n devices: { device: MediaDeviceInfo }[];\n disabled: boolean;\n onChange: (value: string) => void;\n}) => {\n return (\n <div className={styles.selectDeviceContainer}>\n <select\n value={value}\n onChange={(e) => onChange(e.target.value)}\n disabled={disabled}\n className={styles.selectDevice}\n >\n {devices.map(({ device }) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))}\n </select>\n <span className={styles.selectArrow}>\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M4 6L8 10L12 6\"\n stroke=\"#fff\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </span>\n </div>\n );\n};\n\nexport const MicSelectBtn = memo(() => {\n const { onToggleMicrophone, isMicReady, isMicMuted } = useLocalMicrophone();\n const { microphones, currentMic, setMicrophone } = useDevices();\n\n return (\n <div className={styles.deviceButtonContainer}>\n <button\n type=\"button\"\n onClick={onToggleMicrophone}\n disabled={!isMicReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isMicMuted || !isMicReady ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone Muted\"\n >\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 18.1339 14.0992 18.1339 14.0992M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"1.66667\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <rect\n x=\"1.30225\"\n y=\"3\"\n width=\"26\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 1.30225 3)\"\n fill=\"#020617\"\n />\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M9 9.39031V11C9 11.7956 9.31607 12.5587 9.87868 13.1213C10.4413 13.6839 11.2044 14 12 14C12.7956 14 13.5587 13.6839 14.1213 13.1213C14.2829 12.9597 14.4242 12.7816 14.5435 12.5908L9 9.39031ZM15 7.71466V6C15 5.20435 14.6839 4.44129 14.1213 3.87868C13.5587 3.31607 12.7956 3 12 3C11.2044 3 10.4413 3.31607 9.87868 3.87868C9.69528 4.06208 9.53807 4.26678 9.40948 4.48697L15 7.71466Z\"\n fill=\"#020617\"\n />\n <path\n d=\"M9 9.39031L9.41667 8.66862C9.15883 8.51976 8.84117 8.51976 8.58333 8.66862C8.3255 8.81748 8.16667 9.09259 8.16667 9.39031H9ZM9.87868 13.1213L9.28942 13.7106H9.28942L9.87868 13.1213ZM14.1213 13.1213L14.7106 13.7106L14.7106 13.7106L14.1213 13.1213ZM14.5435 12.5908L15.25 13.0327C15.3699 12.841 15.4068 12.6088 15.3521 12.3894C15.2974 12.17 15.156 11.9822 14.9601 11.8692L14.5435 12.5908ZM15 7.71466L14.5833 8.43635C14.8412 8.58521 15.1588 8.58521 15.4167 8.43635C15.6745 8.28749 15.8333 8.01238 15.8333 7.71466H15ZM14.1213 3.87868L14.7106 3.28942L14.7106 3.28942L14.1213 3.87868ZM9.87868 3.87868L9.28942 3.28942L9.28942 3.28942L9.87868 3.87868ZM9.40948 4.48697L8.68988 4.06671C8.57806 4.25818 8.54715 4.48633 8.604 4.70065C8.66086 4.91497 8.80078 5.09779 8.99281 5.20866L9.40948 4.48697ZM9.83333 11V9.39031H8.16667V11H9.83333ZM10.4679 12.5321C10.0616 12.1257 9.83333 11.5746 9.83333 11H8.16667C8.16667 12.0167 8.57053 12.9917 9.28942 13.7106L10.4679 12.5321ZM12 13.1667C11.4254 13.1667 10.8743 12.9384 10.4679 12.5321L9.28942 13.7106C10.0083 14.4295 10.9833 14.8333 12 14.8333V13.1667ZM13.5321 12.5321C13.1257 12.9384 12.5746 13.1667 12 13.1667V14.8333C13.0167 14.8333 13.9917 14.4295 14.7106 13.7106L13.5321 12.5321ZM13.837 12.149C13.7508 12.2867 13.6488 12.4153 13.5321 12.5321L14.7106 13.7106C14.917 13.5041 15.0976 13.2764 15.25 13.0327L13.837 12.149ZM14.9601 11.8692L9.41667 8.66862L8.58333 10.112L14.1268 13.3125L14.9601 11.8692ZM14.1667 6V7.71466H15.8333V6H14.1667ZM13.5321 4.46794C13.9384 4.87426 14.1667 5.42536 14.1667 6H15.8333C15.8333 4.98334 15.4295 4.00831 14.7106 3.28942L13.5321 4.46794ZM12 3.83333C12.5746 3.83333 13.1257 4.06161 13.5321 4.46794L14.7106 3.28942C13.9917 2.57053 13.0167 2.16667 12 2.16667V3.83333ZM10.4679 4.46794C10.8743 4.06161 11.4254 3.83333 12 3.83333V2.16667C10.9833 2.16667 10.0083 2.57053 9.28942 3.28942L10.4679 4.46794ZM10.1291 4.90724C10.2219 4.74824 10.3354 4.60042 10.4679 4.46794L9.28942 3.28942C9.05511 3.52374 8.85421 3.78533 8.68988 4.06671L10.1291 4.90724ZM8.99281 5.20866L14.5833 8.43635L15.4167 6.99298L9.82615 3.76529L8.99281 5.20866Z\"\n fill=\"#020617\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone\"\n >\n <path\n d=\"M9 6C9 5.20435 9.31607 4.44129 9.87868 3.87868C10.4413 3.31607 11.2044 3 12 3C12.7956 3 13.5587 3.31607 14.1213 3.87868C14.6839 4.44129 15 5.20435 15 6V11C15 11.7956 14.6839 12.5587 14.1213 13.1213C13.5587 13.6839 12.7956 14 12 14C11.2044 14 10.4413 13.6839 9.87868 13.1213C9.31607 12.5587 9 11.7956 9 11V6Z\"\n fill=\"#020617\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 19 12.8565 19 11V10M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Microphone</span>\n </button>\n <SelectDevice\n value={currentMic?.device?.deviceId}\n devices={microphones || []}\n disabled={!isMicReady}\n onChange={(val) => setMicrophone(val)}\n />\n </div>\n );\n});\n\nMicSelectBtn.displayName = 'MicSelectBtn';\n\nexport const CameraSelectBtn = memo(() => {\n const { onToggleCamera, isCamReady, isCamMuted } = useLocalCamera();\n const { currentCam, cameras, setCamera } = useDevices();\n\n return (\n <div className={styles.deviceButtonContainer}>\n <button\n type=\"button\"\n onClick={onToggleCamera}\n disabled={!isCamReady || !currentCam}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isCamMuted ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera Muted\"\n >\n <g clipPath=\"url(#clip0_7082_14220)\">\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M3.19874 5.60093C3.08628 5.68537 2.97928 5.77808 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V15.048C17.7787 14.8204 17.5304 14.6189 17.2595 14.4485L3.19874 5.60093ZM22 12.655V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H8.7412L22 12.655Z\"\n fill=\"#020617\"\n />\n <rect\n x=\"0.777222\"\n y=\"2.64844\"\n width=\"26.7988\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 0.777222 2.64844)\"\n fill=\"#020617\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_7082_14220\">\n <rect width=\"24\" height=\"24\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera\"\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M5 5C4.20435 5 3.44129 5.31607 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V14.414L20.293 16.707C20.4329 16.8468 20.611 16.942 20.805 16.9806C20.9989 17.0192 21.2 16.9993 21.3827 16.9237C21.5654 16.848 21.7215 16.7199 21.8314 16.5555C21.9413 16.391 22 16.1978 22 16V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H5Z\"\n fill=\"#020617\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Camera</span>\n </button>\n <SelectDevice\n value={currentCam?.device?.deviceId}\n devices={cameras || []}\n disabled={!isCamReady}\n onChange={(val) => setCamera(val)}\n />\n </div>\n );\n});\n\nCameraSelectBtn.displayName = 'CameraSelectBtn';\n\nexport const ScreenShareButton = memo(() => {\n const { onToggleScreenshare, isScreenSharing } = useLocalScreenshare();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleScreenshare}\n className={`${styles.deviceButtonContainer} ${styles.screenShareButton}`}\n >\n <span>\n {isScreenSharing ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M21.0008 19C21.2557 19.0003 21.5009 19.0979 21.6862 19.2728C21.8715 19.4478 21.9831 19.687 21.998 19.9414C22.013 20.1958 21.9302 20.4464 21.7666 20.6418C21.603 20.8373 21.3709 20.9629 21.1178 20.993L21.0008 21H3.00085C2.74597 20.9997 2.50081 20.9021 2.31548 20.7272C2.13014 20.5522 2.01861 20.313 2.00367 20.0586C1.98874 19.8042 2.07152 19.5536 2.23511 19.3582C2.3987 19.1627 2.63075 19.0371 2.88385 19.007L3.00085 19H21.0008ZM19.0008 4C19.5054 3.99984 19.9914 4.19041 20.3614 4.5335C20.7314 4.87659 20.958 5.34684 20.9958 5.85L21.0008 6V16C21.001 16.5046 20.8104 16.9906 20.4673 17.3605C20.1243 17.7305 19.654 17.9572 19.1508 17.995L19.0008 18H5.00085C4.49627 18.0002 4.01028 17.8096 3.6403 17.4665C3.27032 17.1234 3.04369 16.6532 3.00585 16.15L3.00085 16V6C3.00069 5.49542 3.19125 5.00943 3.53434 4.63945C3.87743 4.26947 4.34769 4.04284 4.85085 4.005L5.00085 4H19.0008Z\"\n fill=\"#2D65FF\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M20.9999 19C21.2547 19.0003 21.4999 19.0979 21.6852 19.2728C21.8706 19.4478 21.9821 19.687 21.997 19.9414C22.012 20.1958 21.9292 20.4464 21.7656 20.6418C21.602 20.8373 21.37 20.9629 21.1169 20.993L20.9999 21H2.99987C2.74499 20.9997 2.49984 20.9021 2.3145 20.7272C2.12916 20.5522 2.01763 20.313 2.0027 20.0586C1.98776 19.8042 2.07054 19.5536 2.23413 19.3582C2.39772 19.1627 2.62977 19.0371 2.88287 19.007L2.99987 19H20.9999ZM18.9999 4C19.5044 3.99984 19.9904 4.19041 20.3604 4.5335C20.7304 4.87659 20.957 5.34684 20.9949 5.85L20.9999 6V16C21 16.5046 20.8095 16.9906 20.4664 17.3605C20.1233 17.7305 19.653 17.9572 19.1499 17.995L18.9999 18H4.99987C4.49529 18.0002 4.0093 17.8096 3.63932 17.4665C3.26934 17.1234 3.04271 16.6532 3.00487 16.15L2.99987 16V6C2.99971 5.49542 3.19028 5.00943 3.53337 4.63945C3.87646 4.26947 4.34671 4.04284 4.84987 4.005L4.99987 4H18.9999Z\"\n fill=\"currentColor\"\n />\n </svg>\n )}\n </span>\n </button>\n );\n});\n\nScreenShareButton.displayName = 'ScreenShareButton';\n",
|
|
365
|
+
styles: "/* SelectDevice styles */\n.selectDevice {\n height: 3rem;\n width: 5.5rem;\n border-radius: 9999px;\n background-color: rgba(255, 255, 255, 0.2);\n padding: 0 0.75rem;\n border: 1px solid rgba(255, 255, 255, 0.2);\n backdrop-filter: blur(10px);\n color: transparent;\n padding-right: 2rem; /* space for arrow */\n box-sizing: border-box;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n cursor: pointer;\n}\n\n.selectDevice::-ms-expand {\n display: none;\n}\n\n.selectDevice option {\n color: transparent;\n background-color: transparent;\n}\n\n.selectDevice:focus {\n outline: none;\n}\n\n/* Device button container styles */\n.deviceButtonContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n}\n\n/* Device button styles */\n.deviceButton {\n position: absolute;\n left: 0;\n top: 0;\n z-index: 10;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n cursor: pointer;\n}\n\n.deviceButtonIcon {\n display: flex;\n}\n\n.deviceButton:disabled {\n opacity: 0.5;\n}\n\n/* Screen reader only text */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n/* Controls container */\n.controlsContainer {\n display: flex;\n gap: 1rem;\n justify-content: center;\n align-items: center;\n}\n\n.selectDeviceContainer {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.selectArrow {\n position: absolute;\n right: 1rem;\n pointer-events: none;\n display: flex;\n align-items: center;\n height: 100%;\n}\n\n.screenShareButton {\n cursor: pointer;\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n color: #000;\n}\n\n@container conversation (max-width: 400px) {\n .selectDevice {\n height: 2.5rem;\n width: 4.5rem;\n padding-right: 1.5rem;\n }\n .deviceButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n .screenShareButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n .selectArrow {\n right: 0.625rem;\n }\n}\n",
|
|
323
366
|
componentsDependencies: [
|
|
324
367
|
"use-local-camera",
|
|
325
368
|
"use-local-microphone",
|
|
@@ -330,8 +373,8 @@ var device_select_default$1 = {
|
|
|
330
373
|
//#region src/templates/tsx/components/hair-check-01.json
|
|
331
374
|
var hair_check_01_default$1 = {
|
|
332
375
|
type: "components",
|
|
333
|
-
content: "import React, { memo, useEffect } from
|
|
334
|
-
styles: "/* Button Component */\n/* Start of Selection */\n.buttonCancel {\n padding: 1rem;\n border: 1px solid rgba(255, 255, 255, 0.1);\n background-color: rgba(239, 68, 68, 0.8);\n border-radius: 9999px;\n color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n outline: none;\n}\n\n.buttonCancel:hover {\n background-color: rgba(239, 68, 68, 1);\n}\n/* End of Selection */\n\n/* ButtonJoin Component */\n.buttonJoin {\n padding: 5px;\n border: 1px solid rgba(255, 255, 255, 0.1);\n background-color: rgba(255, 255, 255, 0.1);\n border-radius: 9999px;\n color: white;\n height: 3.625rem;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 500;\n cursor: pointer;\n}\n\n.buttonJoinInner {\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: #32c75c;\n border-radius: 9999px;\n padding: 1rem;\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n height: 3rem;\n transition: background-color 0.2s;\n font-weight: 500;\n min-width: 8.5rem;\n}\n\n.buttonJoin:hover .buttonJoinInner {\n background-color: rgba(62, 192, 97, 0.9);\n}\n\n.buttonJoin:disabled .buttonJoinInner {\n background-color: rgba(34, 197, 94, 0.5);\n cursor: not-allowed;\n}\n\n.buttonJoinDesktop {\n display: none;\n}\n\n@
|
|
376
|
+
content: "import React, { memo, useEffect } from 'react';\nimport { DailyVideo, useDaily } from '@daily-co/daily-react';\nimport { CameraSelectBtn, MicSelectBtn } from '../device-select';\nimport { useStartHaircheck } from '../../hooks/use-start-haircheck';\nimport { useLocalCamera } from '../../hooks/use-local-camera';\n\nimport styles from './hair-check.module.css';\n\nconst JoinBtn = ({\n onClick,\n disabled,\n className,\n loading,\n}: {\n loading?: boolean;\n onClick: () => void;\n disabled?: boolean;\n className?: string;\n}) => {\n return (\n <button\n className={`${styles.buttonJoin} ${className || ''}`}\n type=\"button\"\n onClick={onClick}\n disabled={disabled || loading}\n >\n <div className={styles.buttonJoinInner}>{loading ? 'Joining...' : 'Join Video Chat'}</div>\n </button>\n );\n};\n\nexport const HairCheck = memo(\n ({\n isJoinBtnLoading = false,\n onJoin,\n onCancel,\n }: {\n isJoinBtnLoading?: boolean;\n onJoin: () => void;\n onCancel?: () => void;\n }) => {\n const daily = useDaily();\n const { localSessionId, isCamMuted } = useLocalCamera();\n\n const {\n isPermissionsPrompt,\n isPermissionsLoading,\n isPermissionsGranted,\n isPermissionsDenied,\n requestPermissions,\n } = useStartHaircheck();\n\n useEffect(() => {\n requestPermissions();\n }, []);\n\n const onCancelHairCheck = () => {\n if (daily) {\n daily.leave();\n }\n onCancel?.();\n };\n\n const getDescription = () => {\n if (isPermissionsPrompt) {\n return 'Make sure your camera and mic are ready!';\n }\n if (isPermissionsLoading) {\n return 'Getting your camera and mic ready...';\n }\n if (isPermissionsDenied) {\n return 'Camera and mic access denied. Allow permissions to continue.';\n }\n return \"You're all set! Your device is ready.\";\n };\n return (\n <div className={styles.haircheckBlockWrapper}>\n <div className={styles.haircheckBlock}>\n {isPermissionsGranted && !isCamMuted ? (\n <DailyVideo\n type=\"video\"\n sessionId={localSessionId}\n mirror\n className={styles.dailyVideo}\n />\n ) : (\n <div className={styles.haircheckUserPlaceholder}>\n <span className={styles.haircheckUserIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"88\"\n height=\"89\"\n viewBox=\"0 0 88 89\"\n fill=\"none\"\n aria-label=\"Haircheck User\"\n role=\"img\"\n >\n <path\n d=\"M44 48.6406C17.952 48.6406 8.80005 61.8406 8.80005 70.6406V83.8406H79.2001V70.6406C79.2001 61.8406 70.0481 48.6406 44 48.6406Z\"\n fill=\"url(#paint0_linear_7135_21737)\"\n />\n <path\n d=\"M44 44.2406C54.9352 44.2406 63.7999 35.3759 63.7999 24.4406C63.7999 13.5054 54.9352 4.64062 44 4.64062C33.0647 4.64062 24.2 13.5054 24.2 24.4406C24.2 35.3759 33.0647 44.2406 44 44.2406Z\"\n fill=\"url(#paint1_linear_7135_21737)\"\n />\n <defs>\n <linearGradient\n id=\"paint0_linear_7135_21737\"\n x1=\"36.5001\"\n y1=\"43\"\n x2=\"44.0001\"\n y2=\"97.5\"\n gradientUnits=\"userSpaceOnUse\"\n >\n <stop stopColor=\"white\" />\n <stop offset=\"1\" stopColor=\"white\" stopOpacity=\"0\" />\n </linearGradient>\n <linearGradient\n id=\"paint1_linear_7135_21737\"\n x1=\"44\"\n y1=\"4.64062\"\n x2=\"44\"\n y2=\"44.2406\"\n gradientUnits=\"userSpaceOnUse\"\n >\n <stop stopColor=\"white\" />\n <stop offset=\"1\" stopColor=\"white\" stopOpacity=\"0\" />\n </linearGradient>\n </defs>\n </svg>\n </span>\n </div>\n )}\n\n <div className={styles.haircheckSidebar}>\n <div className={styles.haircheckSidebarContent}>\n {isPermissionsDenied ? (\n <button\n type=\"button\"\n onClick={onCancelHairCheck}\n className={`${styles.buttonCancel} ${styles.buttonJoinMobile}`}\n >\n Cancel\n </button>\n ) : (\n <JoinBtn\n loading={isJoinBtnLoading}\n disabled={!isPermissionsGranted}\n className={styles.buttonJoinMobile}\n onClick={onJoin}\n />\n )}\n <div />\n <div className={styles.haircheckContent}>\n <div className={styles.haircheckDescription}>{getDescription()}</div>\n {isPermissionsDenied ? (\n <button\n type=\"button\"\n onClick={onCancelHairCheck}\n className={`${styles.buttonCancel} ${styles.buttonJoinDesktop}`}\n >\n Cancel\n </button>\n ) : (\n <JoinBtn\n loading={isJoinBtnLoading}\n disabled={!isPermissionsGranted}\n className={styles.buttonJoinDesktop}\n onClick={onJoin}\n />\n )}\n </div>\n <div className={styles.haircheckControls}>\n <MicSelectBtn />\n <CameraSelectBtn />\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n }\n);\n\nHairCheck.displayName = 'HairCheck';\n",
|
|
377
|
+
styles: "/* Button Component */\n/* Start of Selection */\n.buttonCancel {\n padding: 1rem;\n border: 1px solid rgba(255, 255, 255, 0.1);\n background-color: rgba(239, 68, 68, 0.8);\n border-radius: 9999px;\n color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n outline: none;\n}\n\n.buttonCancel:hover {\n background-color: rgba(239, 68, 68, 1);\n}\n/* End of Selection */\n\n/* ButtonJoin Component */\n.buttonJoin {\n padding: 5px;\n border: 1px solid rgba(255, 255, 255, 0.1);\n background-color: rgba(255, 255, 255, 0.1);\n border-radius: 9999px;\n color: white;\n height: 3.625rem;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 500;\n cursor: pointer;\n}\n\n.buttonJoinInner {\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: #32c75c;\n border-radius: 9999px;\n padding: 1rem;\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n height: 3rem;\n transition: background-color 0.2s;\n font-weight: 500;\n min-width: 8.5rem;\n}\n\n.buttonJoin:hover .buttonJoinInner {\n background-color: rgba(62, 192, 97, 0.9);\n}\n\n.buttonJoin:disabled .buttonJoinInner {\n background-color: rgba(34, 197, 94, 0.5);\n cursor: not-allowed;\n}\n\n.buttonJoinDesktop {\n display: none;\n}\n\n@container haircheck (min-width: 768px) {\n .buttonJoinDesktop {\n display: flex;\n }\n}\n\n.buttonJoinMobile {\n position: absolute;\n top: -5.125rem;\n left: 50%;\n transform: translateX(-50%);\n z-index: 20;\n}\n\n@container haircheck (min-width: 768px) {\n .buttonJoinMobile {\n display: none;\n }\n}\n\n/* HaircheckScreen Component */\n.haircheckBlockWrapper {\n width: 100%;\n container-type: inline-size;\n container-name: haircheck;\n}\n\n.haircheckBlock {\n position: relative;\n width: 100%;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n aspect-ratio: 9/16;\n overflow: hidden;\n border-radius: 0.5rem;\n max-height: 90vh;\n}\n\n/* Aspect ratio tracks the wrapper's actual width: portrait when narrow,\n landscape when wide. */\n@container haircheck (min-width: 768px) {\n .haircheckBlock {\n aspect-ratio: 16/9;\n }\n}\n\n.dailyVideo {\n width: 100%;\n height: 100%;\n object-fit: cover !important;\n position: absolute;\n inset: 0;\n transition: opacity 0.3s ease-in-out;\n}\n\n.haircheckUserPlaceholder {\n position: absolute;\n inset: 0;\n background: linear-gradient(135deg, #4b5563 0%, #1f2937 100%);\n z-index: 5;\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.haircheckUserIcon {\n width: 12.5rem;\n height: 12.5rem;\n border-radius: 9999px;\n background-color: rgba(255, 255, 255, 0.1);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.haircheckSidebar {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n background-color: rgba(2, 6, 23, 0.45);\n backdrop-filter: blur(20px);\n border-left: 1px solid rgba(255, 255, 255, 0.2);\n z-index: 5;\n}\n\n@container haircheck (min-width: 768px) {\n .haircheckSidebar {\n left: auto;\n top: 0;\n bottom: 0;\n width: 17.5rem;\n }\n}\n\n@container haircheck (min-width: 1024px) {\n .haircheckSidebar {\n width: 23rem;\n }\n}\n\n.haircheckSidebarContent {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n padding: 1.5rem 1.25rem;\n gap: 1.5rem;\n width: 100%;\n height: 100%;\n}\n\n@container haircheck (min-width: 768px) {\n .haircheckSidebarContent {\n padding: 1rem 1.25rem;\n }\n}\n\n@container haircheck (min-width: 1024px) {\n .haircheckSidebarContent {\n padding: 2rem 1.25rem;\n }\n}\n\n.haircheckContent {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 2rem;\n}\n\n.haircheckDescription {\n color: #ffffff;\n font-size: 1.125rem;\n font-weight: 500;\n}\n\n@container haircheck (min-width: 768px) {\n .haircheckDescription {\n font-size: 1.25rem;\n }\n}\n\n.haircheckControls {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: 1rem;\n}\n",
|
|
335
378
|
componentsDependencies: [
|
|
336
379
|
"device-select",
|
|
337
380
|
"use-start-haircheck",
|
|
@@ -343,8 +386,8 @@ var hair_check_01_default$1 = {
|
|
|
343
386
|
//#region src/templates/tsx/components/media-controls.json
|
|
344
387
|
var media_controls_default$1 = {
|
|
345
388
|
type: "components",
|
|
346
|
-
content: "import React, { memo } from \"react\";\nimport { useLocalCamera } from \"../../hooks/use-local-camera\";\nimport { useLocalMicrophone } from \"../../hooks/use-local-microphone\";\nimport { useLocalScreenshare } from \"../../hooks/use-local-screenshare\";\n\nimport styles from \"./media-controls.module.css\";\n\nexport const MicToggleButton = memo(() => {\n const { onToggleMicrophone, isMicReady, isMicMuted } = useLocalMicrophone();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleMicrophone}\n disabled={!isMicReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isMicMuted || !isMicReady ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone Muted\"\n >\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 18.1339 14.0992 18.1339 14.0992M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"1.66667\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <rect\n x=\"1.30225\"\n y=\"3\"\n width=\"26\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 1.30225 3)\"\n fill=\"#020617\"\n />\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M9 9.39031V11C9 11.7956 9.31607 12.5587 9.87868 13.1213C10.4413 13.6839 11.2044 14 12 14C12.7956 14 13.5587 13.6839 14.1213 13.1213C14.2829 12.9597 14.4242 12.7816 14.5435 12.5908L9 9.39031ZM15 7.71466V6C15 5.20435 14.6839 4.44129 14.1213 3.87868C13.5587 3.31607 12.7956 3 12 3C11.2044 3 10.4413 3.31607 9.87868 3.87868C9.69528 4.06208 9.53807 4.26678 9.40948 4.48697L15 7.71466Z\"\n fill=\"#020617\"\n />\n <path\n d=\"M9 9.39031L9.41667 8.66862C9.15883 8.51976 8.84117 8.51976 8.58333 8.66862C8.3255 8.81748 8.16667 9.09259 8.16667 9.39031H9ZM9.87868 13.1213L9.28942 13.7106H9.28942L9.87868 13.1213ZM14.1213 13.1213L14.7106 13.7106L14.7106 13.7106L14.1213 13.1213ZM14.5435 12.5908L15.25 13.0327C15.3699 12.841 15.4068 12.6088 15.3521 12.3894C15.2974 12.17 15.156 11.9822 14.9601 11.8692L14.5435 12.5908ZM15 7.71466L14.5833 8.43635C14.8412 8.58521 15.1588 8.58521 15.4167 8.43635C15.6745 8.28749 15.8333 8.01238 15.8333 7.71466H15ZM14.1213 3.87868L14.7106 3.28942L14.7106 3.28942L14.1213 3.87868ZM9.87868 3.87868L9.28942 3.28942L9.28942 3.28942L9.87868 3.87868ZM9.40948 4.48697L8.68988 4.06671C8.57806 4.25818 8.54715 4.48633 8.604 4.70065C8.66086 4.91497 8.80078 5.09779 8.99281 5.20866L9.40948 4.48697ZM9.83333 11V9.39031H8.16667V11H9.83333ZM10.4679 12.5321C10.0616 12.1257 9.83333 11.5746 9.83333 11H8.16667C8.16667 12.0167 8.57053 12.9917 9.28942 13.7106L10.4679 12.5321ZM12 13.1667C11.4254 13.1667 10.8743 12.9384 10.4679 12.5321L9.28942 13.7106C10.0083 14.4295 10.9833 14.8333 12 14.8333V13.1667ZM13.5321 12.5321C13.1257 12.9384 12.5746 13.1667 12 13.1667V14.8333C13.0167 14.8333 13.9917 14.4295 14.7106 13.7106L13.5321 12.5321ZM13.837 12.149C13.7508 12.2867 13.6488 12.4153 13.5321 12.5321L14.7106 13.7106C14.917 13.5041 15.0976 13.2764 15.25 13.0327L13.837 12.149ZM14.9601 11.8692L9.41667 8.66862L8.58333 10.112L14.1268 13.3125L14.9601 11.8692ZM14.1667 6V7.71466H15.8333V6H14.1667ZM13.5321 4.46794C13.9384 4.87426 14.1667 5.42536 14.1667 6H15.8333C15.8333 4.98334 15.4295 4.00831 14.7106 3.28942L13.5321 4.46794ZM12 3.83333C12.5746 3.83333 13.1257 4.06161 13.5321 4.46794L14.7106 3.28942C13.9917 2.57053 13.0167 2.16667 12 2.16667V3.83333ZM10.4679 4.46794C10.8743 4.06161 11.4254 3.83333 12 3.83333V2.16667C10.9833 2.16667 10.0083 2.57053 9.28942 3.28942L10.4679 4.46794ZM10.1291 4.90724C10.2219 4.74824 10.3354 4.60042 10.4679 4.46794L9.28942 3.28942C9.05511 3.52374 8.85421 3.78533 8.68988 4.06671L10.1291 4.90724ZM8.99281 5.20866L14.5833 8.43635L15.4167 6.99298L9.82615 3.76529L8.99281 5.20866Z\"\n fill=\"#020617\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone\"\n >\n <path\n d=\"M9 6C9 5.20435 9.31607 4.44129 9.87868 3.87868C10.4413 3.31607 11.2044 3 12 3C12.7956 3 13.5587 3.31607 14.1213 3.87868C14.6839 4.44129 15 5.20435 15 6V11C15 11.7956 14.6839 12.5587 14.1213 13.1213C13.5587 13.6839 12.7956 14 12 14C11.2044 14 10.4413 13.6839 9.87868 13.1213C9.31607 12.5587 9 11.7956 9 11V6Z\"\n fill=\"#020617\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 19 12.8565 19 11V10M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Microphone</span>\n </button>\n );\n});\n\nMicToggleButton.displayName = \"MicToggleButton\";\n\nexport const CameraToggleButton = memo(() => {\n const { onToggleCamera, isCamReady, isCamMuted } = useLocalCamera();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleCamera}\n disabled={!isCamReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isCamMuted ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera Muted\"\n >\n <g clipPath=\"url(#clip0_7082_14220)\">\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M3.19874 5.60093C3.08628 5.68537 2.97928 5.77808 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V15.048C17.7787 14.8204 17.5304 14.6189 17.2595 14.4485L3.19874 5.60093ZM22 12.655V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H8.7412L22 12.655Z\"\n fill=\"#020617\"\n />\n <rect\n x=\"0.777222\"\n y=\"2.64844\"\n width=\"26.7988\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 0.777222 2.64844)\"\n fill=\"#020617\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_7082_14220\">\n <rect width=\"24\" height=\"24\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera\"\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M5 5C4.20435 5 3.44129 5.31607 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V14.414L20.293 16.707C20.4329 16.8468 20.611 16.942 20.805 16.9806C20.9989 17.0192 21.2 16.9993 21.3827 16.9237C21.5654 16.848 21.7215 16.7199 21.8314 16.5555C21.9413 16.391 22 16.1978 22 16V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H5Z\"\n fill=\"#020617\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Camera</span>\n </button>\n );\n});\n\nCameraToggleButton.displayName = \"CameraToggleButton\";\n\nexport const ScreenShareButton = memo(() => {\n const { onToggleScreenshare, isScreenSharing } = useLocalScreenshare();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleScreenshare}\n className={`${styles.deviceButtonContainer} ${styles.screenShareButton}`}\n >\n <span>\n {isScreenSharing ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M21.0008 19C21.2557 19.0003 21.5009 19.0979 21.6862 19.2728C21.8715 19.4478 21.9831 19.687 21.998 19.9414C22.013 20.1958 21.9302 20.4464 21.7666 20.6418C21.603 20.8373 21.3709 20.9629 21.1178 20.993L21.0008 21H3.00085C2.74597 20.9997 2.50081 20.9021 2.31548 20.7272C2.13014 20.5522 2.01861 20.313 2.00367 20.0586C1.98874 19.8042 2.07152 19.5536 2.23511 19.3582C2.3987 19.1627 2.63075 19.0371 2.88385 19.007L3.00085 19H21.0008ZM19.0008 4C19.5054 3.99984 19.9914 4.19041 20.3614 4.5335C20.7314 4.87659 20.958 5.34684 20.9958 5.85L21.0008 6V16C21.001 16.5046 20.8104 16.9906 20.4673 17.3605C20.1243 17.7305 19.654 17.9572 19.1508 17.995L19.0008 18H5.00085C4.49627 18.0002 4.01028 17.8096 3.6403 17.4665C3.27032 17.1234 3.04369 16.6532 3.00585 16.15L3.00085 16V6C3.00069 5.49542 3.19125 5.00943 3.53434 4.63945C3.87743 4.26947 4.34769 4.04284 4.85085 4.005L5.00085 4H19.0008Z\"\n fill=\"#2D65FF\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M20.9999 19C21.2547 19.0003 21.4999 19.0979 21.6852 19.2728C21.8706 19.4478 21.9821 19.687 21.997 19.9414C22.012 20.1958 21.9292 20.4464 21.7656 20.6418C21.602 20.8373 21.37 20.9629 21.1169 20.993L20.9999 21H2.99987C2.74499 20.9997 2.49984 20.9021 2.3145 20.7272C2.12916 20.5522 2.01763 20.313 2.0027 20.0586C1.98776 19.8042 2.07054 19.5536 2.23413 19.3582C2.39772 19.1627 2.62977 19.0371 2.88287 19.007L2.99987 19H20.9999ZM18.9999 4C19.5044 3.99984 19.9904 4.19041 20.3604 4.5335C20.7304 4.87659 20.957 5.34684 20.9949 5.85L20.9999 6V16C21 16.5046 20.8095 16.9906 20.4664 17.3605C20.1233 17.7305 19.653 17.9572 19.1499 17.995L18.9999 18H4.99987C4.49529 18.0002 4.0093 17.8096 3.63932 17.4665C3.26934 17.1234 3.04271 16.6532 3.00487 16.15L2.99987 16V6C2.99971 5.49542 3.19028 5.00943 3.53337 4.63945C3.87646 4.26947 4.34671 4.04284 4.84987 4.005L4.99987 4H18.9999Z\"\n fill=\"white\"\n />\n </svg>\n )}\n </span>\n </button>\n );\n});\n\nScreenShareButton.displayName = \"ScreenShareButton\";",
|
|
347
|
-
styles: "/* Device button styles */\n.deviceButton {\n
|
|
389
|
+
content: "import React, { memo } from 'react';\nimport { useLocalCamera } from '../../hooks/use-local-camera';\nimport { useLocalMicrophone } from '../../hooks/use-local-microphone';\nimport { useLocalScreenshare } from '../../hooks/use-local-screenshare';\n\nimport styles from './media-controls.module.css';\n\nexport const MicToggleButton = memo(() => {\n const { onToggleMicrophone, isMicReady, isMicMuted } = useLocalMicrophone();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleMicrophone}\n disabled={!isMicReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isMicMuted || !isMicReady ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone Muted\"\n >\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 18.1339 14.0992 18.1339 14.0992M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"1.66667\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <rect\n x=\"1.30225\"\n y=\"3\"\n width=\"26\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 1.30225 3)\"\n fill=\"#020617\"\n />\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M9 9.39031V11C9 11.7956 9.31607 12.5587 9.87868 13.1213C10.4413 13.6839 11.2044 14 12 14C12.7956 14 13.5587 13.6839 14.1213 13.1213C14.2829 12.9597 14.4242 12.7816 14.5435 12.5908L9 9.39031ZM15 7.71466V6C15 5.20435 14.6839 4.44129 14.1213 3.87868C13.5587 3.31607 12.7956 3 12 3C11.2044 3 10.4413 3.31607 9.87868 3.87868C9.69528 4.06208 9.53807 4.26678 9.40948 4.48697L15 7.71466Z\"\n fill=\"#020617\"\n />\n <path\n d=\"M9 9.39031L9.41667 8.66862C9.15883 8.51976 8.84117 8.51976 8.58333 8.66862C8.3255 8.81748 8.16667 9.09259 8.16667 9.39031H9ZM9.87868 13.1213L9.28942 13.7106H9.28942L9.87868 13.1213ZM14.1213 13.1213L14.7106 13.7106L14.7106 13.7106L14.1213 13.1213ZM14.5435 12.5908L15.25 13.0327C15.3699 12.841 15.4068 12.6088 15.3521 12.3894C15.2974 12.17 15.156 11.9822 14.9601 11.8692L14.5435 12.5908ZM15 7.71466L14.5833 8.43635C14.8412 8.58521 15.1588 8.58521 15.4167 8.43635C15.6745 8.28749 15.8333 8.01238 15.8333 7.71466H15ZM14.1213 3.87868L14.7106 3.28942L14.7106 3.28942L14.1213 3.87868ZM9.87868 3.87868L9.28942 3.28942L9.28942 3.28942L9.87868 3.87868ZM9.40948 4.48697L8.68988 4.06671C8.57806 4.25818 8.54715 4.48633 8.604 4.70065C8.66086 4.91497 8.80078 5.09779 8.99281 5.20866L9.40948 4.48697ZM9.83333 11V9.39031H8.16667V11H9.83333ZM10.4679 12.5321C10.0616 12.1257 9.83333 11.5746 9.83333 11H8.16667C8.16667 12.0167 8.57053 12.9917 9.28942 13.7106L10.4679 12.5321ZM12 13.1667C11.4254 13.1667 10.8743 12.9384 10.4679 12.5321L9.28942 13.7106C10.0083 14.4295 10.9833 14.8333 12 14.8333V13.1667ZM13.5321 12.5321C13.1257 12.9384 12.5746 13.1667 12 13.1667V14.8333C13.0167 14.8333 13.9917 14.4295 14.7106 13.7106L13.5321 12.5321ZM13.837 12.149C13.7508 12.2867 13.6488 12.4153 13.5321 12.5321L14.7106 13.7106C14.917 13.5041 15.0976 13.2764 15.25 13.0327L13.837 12.149ZM14.9601 11.8692L9.41667 8.66862L8.58333 10.112L14.1268 13.3125L14.9601 11.8692ZM14.1667 6V7.71466H15.8333V6H14.1667ZM13.5321 4.46794C13.9384 4.87426 14.1667 5.42536 14.1667 6H15.8333C15.8333 4.98334 15.4295 4.00831 14.7106 3.28942L13.5321 4.46794ZM12 3.83333C12.5746 3.83333 13.1257 4.06161 13.5321 4.46794L14.7106 3.28942C13.9917 2.57053 13.0167 2.16667 12 2.16667V3.83333ZM10.4679 4.46794C10.8743 4.06161 11.4254 3.83333 12 3.83333V2.16667C10.9833 2.16667 10.0083 2.57053 9.28942 3.28942L10.4679 4.46794ZM10.1291 4.90724C10.2219 4.74824 10.3354 4.60042 10.4679 4.46794L9.28942 3.28942C9.05511 3.52374 8.85421 3.78533 8.68988 4.06671L10.1291 4.90724ZM8.99281 5.20866L14.5833 8.43635L15.4167 6.99298L9.82615 3.76529L8.99281 5.20866Z\"\n fill=\"#020617\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone\"\n >\n <path\n d=\"M9 6C9 5.20435 9.31607 4.44129 9.87868 3.87868C10.4413 3.31607 11.2044 3 12 3C12.7956 3 13.5587 3.31607 14.1213 3.87868C14.6839 4.44129 15 5.20435 15 6V11C15 11.7956 14.6839 12.5587 14.1213 13.1213C13.5587 13.6839 12.7956 14 12 14C11.2044 14 10.4413 13.6839 9.87868 13.1213C9.31607 12.5587 9 11.7956 9 11V6Z\"\n fill=\"#020617\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 19 12.8565 19 11V10M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Microphone</span>\n </button>\n );\n});\n\nMicToggleButton.displayName = 'MicToggleButton';\n\nexport const CameraToggleButton = memo(() => {\n const { onToggleCamera, isCamReady, isCamMuted } = useLocalCamera();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleCamera}\n disabled={!isCamReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isCamMuted ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera Muted\"\n >\n <g clipPath=\"url(#clip0_7082_14220)\">\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M3.19874 5.60093C3.08628 5.68537 2.97928 5.77808 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V15.048C17.7787 14.8204 17.5304 14.6189 17.2595 14.4485L3.19874 5.60093ZM22 12.655V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H8.7412L22 12.655Z\"\n fill=\"#020617\"\n />\n <rect\n x=\"0.777222\"\n y=\"2.64844\"\n width=\"26.7988\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 0.777222 2.64844)\"\n fill=\"#020617\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_7082_14220\">\n <rect width=\"24\" height=\"24\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera\"\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M5 5C4.20435 5 3.44129 5.31607 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V14.414L20.293 16.707C20.4329 16.8468 20.611 16.942 20.805 16.9806C20.9989 17.0192 21.2 16.9993 21.3827 16.9237C21.5654 16.848 21.7215 16.7199 21.8314 16.5555C21.9413 16.391 22 16.1978 22 16V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H5Z\"\n fill=\"#020617\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Camera</span>\n </button>\n );\n});\n\nCameraToggleButton.displayName = 'CameraToggleButton';\n\nexport const ScreenShareButton = memo(() => {\n const { onToggleScreenshare, isScreenSharing } = useLocalScreenshare();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleScreenshare}\n className={`${styles.deviceButtonContainer} ${styles.screenShareButton}`}\n >\n <span>\n {isScreenSharing ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M21.0008 19C21.2557 19.0003 21.5009 19.0979 21.6862 19.2728C21.8715 19.4478 21.9831 19.687 21.998 19.9414C22.013 20.1958 21.9302 20.4464 21.7666 20.6418C21.603 20.8373 21.3709 20.9629 21.1178 20.993L21.0008 21H3.00085C2.74597 20.9997 2.50081 20.9021 2.31548 20.7272C2.13014 20.5522 2.01861 20.313 2.00367 20.0586C1.98874 19.8042 2.07152 19.5536 2.23511 19.3582C2.3987 19.1627 2.63075 19.0371 2.88385 19.007L3.00085 19H21.0008ZM19.0008 4C19.5054 3.99984 19.9914 4.19041 20.3614 4.5335C20.7314 4.87659 20.958 5.34684 20.9958 5.85L21.0008 6V16C21.001 16.5046 20.8104 16.9906 20.4673 17.3605C20.1243 17.7305 19.654 17.9572 19.1508 17.995L19.0008 18H5.00085C4.49627 18.0002 4.01028 17.8096 3.6403 17.4665C3.27032 17.1234 3.04369 16.6532 3.00585 16.15L3.00085 16V6C3.00069 5.49542 3.19125 5.00943 3.53434 4.63945C3.87743 4.26947 4.34769 4.04284 4.85085 4.005L5.00085 4H19.0008Z\"\n fill=\"#2D65FF\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M20.9999 19C21.2547 19.0003 21.4999 19.0979 21.6852 19.2728C21.8706 19.4478 21.9821 19.687 21.997 19.9414C22.012 20.1958 21.9292 20.4464 21.7656 20.6418C21.602 20.8373 21.37 20.9629 21.1169 20.993L20.9999 21H2.99987C2.74499 20.9997 2.49984 20.9021 2.3145 20.7272C2.12916 20.5522 2.01763 20.313 2.0027 20.0586C1.98776 19.8042 2.07054 19.5536 2.23413 19.3582C2.39772 19.1627 2.62977 19.0371 2.88287 19.007L2.99987 19H20.9999ZM18.9999 4C19.5044 3.99984 19.9904 4.19041 20.3604 4.5335C20.7304 4.87659 20.957 5.34684 20.9949 5.85L20.9999 6V16C21 16.5046 20.8095 16.9906 20.4664 17.3605C20.1233 17.7305 19.653 17.9572 19.1499 17.995L18.9999 18H4.99987C4.49529 18.0002 4.0093 17.8096 3.63932 17.4665C3.26934 17.1234 3.04271 16.6532 3.00487 16.15L2.99987 16V6C2.99971 5.49542 3.19028 5.00943 3.53337 4.63945C3.87646 4.26947 4.34671 4.04284 4.84987 4.005L4.99987 4H18.9999Z\"\n fill=\"white\"\n />\n </svg>\n )}\n </span>\n </button>\n );\n});\n\nScreenShareButton.displayName = 'ScreenShareButton';\n",
|
|
390
|
+
styles: "/* Device button styles */\n.deviceButton {\n position: relative;\n z-index: 10;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n cursor: pointer;\n}\n\n.deviceButtonIcon {\n display: flex;\n}\n\n.deviceButton:disabled {\n opacity: 0.5;\n}\n\n/* Screen reader only text */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n.screenShareButton {\n cursor: pointer;\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n}\n",
|
|
348
391
|
componentsDependencies: [
|
|
349
392
|
"use-local-camera",
|
|
350
393
|
"use-local-microphone",
|
|
@@ -355,14 +398,30 @@ var media_controls_default$1 = {
|
|
|
355
398
|
//#region src/templates/tsx/hooks/cvi-events-hooks.json
|
|
356
399
|
var cvi_events_hooks_default$1 = {
|
|
357
400
|
type: "hooks",
|
|
358
|
-
content: "import { useCallback } from
|
|
401
|
+
content: "import { useCallback } from 'react';\nimport { useAppMessage, useDailyEvent } from '@daily-co/daily-react';\n\n// Every event broadcast by Tavus carries `seq` for global monotonic ordering\n// and `turn_idx` for grouping events by conversational turn.\n// See the Interactions Protocol docs (\"Event Ordering and Turn Tracking\").\ntype EventOrdering = {\n seq: number;\n turn_idx?: number;\n};\n\ntype AppMessageUtterance = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.utterance';\n conversation_id: string;\n inference_id: string;\n properties: {\n role: 'user' | 'replica';\n speech: string;\n visual_context: string;\n // Present only on user utterances when the persona uses Raven-1.\n user_audio_analysis?: string;\n user_visual_analysis?: string;\n };\n};\n\n// Streaming utterance event — emitted as either side speaks. Reflects what was\n// actually spoken/transcribed (vs. `conversation.utterance` role=replica which\n// contains the full intended LLM response, even if interrupted).\ntype AppMessageUtteranceStreaming = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.utterance.streaming';\n conversation_id: string;\n inference_id: string;\n properties: {\n role: 'user' | 'replica';\n content_index: number;\n speech: string;\n final?: boolean;\n is_interrupted?: boolean;\n };\n};\n\ntype AppMessageToolCall<T> = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.tool_call';\n conversation_id: string;\n inference_id: string;\n properties: T;\n};\n\ntype PerceptionFrame = {\n data: string;\n mime_type: string;\n};\n\ntype AppMessagePerceptionToolCall<T = unknown> = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.perception_tool_call';\n conversation_id: string;\n properties: {\n modality: 'vision' | 'audio';\n name: string;\n // For modality=\"audio\" this is a JSON string. For modality=\"vision\" this\n // is an object with the tool-defined fields. Caller chooses T accordingly.\n arguments: T;\n frames?: PerceptionFrame[];\n };\n};\n\ntype AppMessagePerceptionAnalysis = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.perception_analysis';\n conversation_id: string;\n properties: {\n analysis: string;\n };\n};\n\n// Canonical role-based speaking events (current Tavus schema). Use the `role`\n// field in `properties` to identify the speaker.\ntype AppMessageStartedSpeaking = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.started_speaking';\n conversation_id: string;\n inference_id: string;\n properties: {\n role: 'user' | 'replica';\n };\n};\n\ntype AppMessageStoppedSpeaking = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.stopped_speaking';\n conversation_id: string;\n inference_id: string;\n properties: {\n role: 'user' | 'replica';\n interrupted: boolean;\n // Speaking duration in seconds. Null if the start time could not be determined.\n duration: number | null;\n };\n};\n\n// Legacy per-role speaking events. Kept for backward compatibility with older\n// Tavus deployments that may still emit them.\ntype AppMessageReplicaStartedSpeaking = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.replica.started_speaking';\n inference_id: string;\n};\n\ntype AppMessageReplicaStoppedSpeaking = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.replica.stopped_speaking';\n inference_id: string;\n};\n\ntype AppMessageUserStartedSpeaking = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.user.started_speaking';\n inference_id: string;\n};\n\ntype AppMessageUserStoppedSpeaking = EventOrdering & {\n message_type: 'conversation';\n event_type: 'conversation.user.stopped_speaking';\n inference_id: string;\n};\n\ntype AppMessage<T> = {\n data:\n | AppMessageUtterance\n | AppMessageUtteranceStreaming\n | AppMessageToolCall<T>\n | AppMessagePerceptionToolCall<T>\n | AppMessagePerceptionAnalysis\n | AppMessageStartedSpeaking\n | AppMessageStoppedSpeaking\n | AppMessageReplicaStartedSpeaking\n | AppMessageReplicaStoppedSpeaking\n | AppMessageUserStartedSpeaking\n | AppMessageUserStoppedSpeaking;\n};\n\nexport function useObservableEvent<T>(callback: (event: AppMessage<T>['data']) => void): void {\n return useDailyEvent(\n 'app-message',\n useCallback(\n (event: AppMessage<T>) => {\n callback(event.data);\n },\n [callback]\n )\n );\n}\n\ntype AppMessageEcho = {\n message_type: 'conversation';\n event_type: 'conversation.echo';\n conversation_id: string;\n properties: {\n modality: 'audio' | 'text';\n text?: string;\n audio?: string;\n sample_rate?: number;\n inference_id?: string;\n done?: boolean;\n };\n};\n\ntype AppMessageRespond = {\n message_type: 'conversation';\n event_type: 'conversation.respond';\n conversation_id: string;\n properties: {\n text: string;\n };\n};\n\ntype AppMessageInterrupt = {\n message_type: 'conversation';\n event_type: 'conversation.interrupt';\n conversation_id: string;\n};\n\ntype AppMessageOverwriteLlmContext = {\n message_type: 'conversation';\n event_type: 'conversation.overwrite_llm_context';\n conversation_id: string;\n properties: {\n context: string;\n };\n};\n\ntype AppMessageAppendLlmContext = {\n message_type: 'conversation';\n event_type: 'conversation.append_llm_context';\n conversation_id: string;\n properties: {\n context: string;\n };\n};\n\ntype Sensitivity = 'superlow' | 'verylow' | 'low' | 'medium' | 'high' | 'auto';\n\ntype AppMessageSensitivity = {\n message_type: 'conversation';\n event_type: 'conversation.sensitivity';\n conversation_id: string;\n properties: {\n participant_pause_sensitivity: Sensitivity;\n participant_interrupt_sensitivity: Sensitivity;\n };\n};\n\ntype SendAppMessageProps =\n | AppMessageEcho\n | AppMessageRespond\n | AppMessageInterrupt\n | AppMessageOverwriteLlmContext\n | AppMessageAppendLlmContext\n | AppMessageSensitivity;\n\nexport function useSendAppMessage(): (message: SendAppMessageProps) => void {\n const sendAppMessage = useAppMessage();\n\n return useCallback(\n (message: SendAppMessageProps) => {\n sendAppMessage(message, '*');\n },\n [sendAppMessage]\n );\n}\n",
|
|
359
402
|
styles: ""
|
|
360
403
|
};
|
|
361
404
|
//#endregion
|
|
405
|
+
//#region src/templates/tsx/hooks/use-chat.json
|
|
406
|
+
var use_chat_default$1 = {
|
|
407
|
+
type: "hooks",
|
|
408
|
+
content: "import { useCallback, useMemo, useReducer } from 'react';\nimport { useObservableEvent, useSendAppMessage } from './cvi-events-hooks';\n\nexport type ChatRole = 'user' | 'replica';\n\nexport type ChatMessage = {\n id: string;\n role: ChatRole;\n text: string;\n createdAt: number;\n};\n\ntype UtteranceLike = {\n inference_id: string;\n conversation_id?: string;\n properties: { role: string; speech: string };\n};\n\nexport function makeMessageId(inferenceId: string, role: ChatRole): string {\n return `${inferenceId}:${role}`;\n}\n\nexport function applyUtterance(\n prev: ChatMessage[],\n event: UtteranceLike,\n now: number\n): ChatMessage[] {\n const speech = event.properties.speech;\n const role = event.properties.role;\n if (!speech || (role !== 'user' && role !== 'replica')) {\n return prev;\n }\n const id = makeMessageId(event.inference_id, role);\n\n let base = prev;\n if (role === 'user') {\n const trimmed = speech.trim();\n base = prev.filter(\n (m) => !(m.id.startsWith('local-') && m.role === 'user' && m.text.trim() === trimmed)\n );\n }\n\n const existingIdx = base.findIndex((m) => m.id === id);\n if (existingIdx >= 0) {\n const next = base.slice();\n next[existingIdx] = { ...next[existingIdx], text: speech };\n return next;\n }\n return [...base, { id, role, text: speech, createdAt: now }];\n}\n\nexport function appendOptimistic(\n prev: ChatMessage[],\n text: string,\n id: string,\n now: number\n): ChatMessage[] {\n return [...prev, { id, role: 'user', text, createdAt: now }];\n}\n\ntype ChatState = {\n messages: ChatMessage[];\n conversationId: string | null;\n};\n\ntype ChatAction =\n | { type: 'utterance'; event: UtteranceLike; now: number }\n | { type: 'optimistic'; text: string; id: string; now: number };\n\nfunction chatReducer(state: ChatState, action: ChatAction): ChatState {\n switch (action.type) {\n case 'utterance':\n return {\n messages: applyUtterance(state.messages, action.event, action.now),\n conversationId: state.conversationId ?? action.event.conversation_id ?? null,\n };\n case 'optimistic':\n return {\n ...state,\n messages: appendOptimistic(state.messages, action.text, action.id, action.now),\n };\n }\n}\n\nconst INITIAL_STATE: ChatState = { messages: [], conversationId: null };\n\nfunction generateLocalId(): string {\n const cryptoObj = (globalThis as { crypto?: Crypto }).crypto;\n if (cryptoObj?.randomUUID) {\n return `local-${cryptoObj.randomUUID()}`;\n }\n return `local-${Math.random().toString(36).slice(2)}-${Date.now()}`;\n}\n\nexport type UseChatReturn = {\n messages: ChatMessage[];\n conversationId: string | null;\n sendMessage: (text: string) => void;\n};\n\nexport function useChat(): UseChatReturn {\n const [state, dispatch] = useReducer(chatReducer, INITIAL_STATE);\n const sendAppMessage = useSendAppMessage();\n\n useObservableEvent<never>(\n useCallback((event) => {\n if (event.event_type === 'conversation.utterance') {\n dispatch({\n type: 'utterance',\n event,\n now: Date.now(),\n });\n }\n }, [])\n );\n\n const sendMessage = useCallback(\n (text: string) => {\n const trimmed = text.trim();\n if (!trimmed || !state.conversationId) {\n return;\n }\n const id = generateLocalId();\n dispatch({ type: 'optimistic', text: trimmed, id, now: Date.now() });\n sendAppMessage({\n message_type: 'conversation',\n event_type: 'conversation.respond',\n conversation_id: state.conversationId,\n properties: { text: trimmed },\n });\n },\n [state.conversationId, sendAppMessage]\n );\n\n return useMemo(\n () => ({\n messages: state.messages,\n conversationId: state.conversationId,\n sendMessage,\n }),\n [state.messages, state.conversationId, sendMessage]\n );\n}\n",
|
|
409
|
+
styles: "",
|
|
410
|
+
componentsDependencies: ["cvi-events-hooks"]
|
|
411
|
+
};
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/templates/tsx/hooks/use-closed-caption.json
|
|
414
|
+
var use_closed_caption_default$1 = {
|
|
415
|
+
type: "hooks",
|
|
416
|
+
content: "import { useCallback, useRef, useState } from 'react';\nimport { useObservableEvent } from './cvi-events-hooks';\n\nconst CAPTION_CLEAR_DELAY_MS = 2000;\n\nexport type ClosedCaption = {\n role: 'user' | 'replica';\n text: string;\n};\n\nexport const useClosedCaption = (): ClosedCaption | null => {\n const [caption, setCaption] = useState<ClosedCaption | null>(null);\n const clearTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const update = useCallback((next: ClosedCaption, final: boolean) => {\n setCaption(next);\n if (clearTimer.current !== null) {\n clearTimeout(clearTimer.current);\n clearTimer.current = null;\n }\n if (final) {\n clearTimer.current = setTimeout(() => {\n setCaption(null);\n clearTimer.current = null;\n }, CAPTION_CLEAR_DELAY_MS);\n }\n }, []);\n\n useObservableEvent<unknown>(\n useCallback(\n (event) => {\n if (event.event_type === 'conversation.utterance.streaming') {\n const { role, speech, final } = event.properties;\n if (role === 'user' || role === 'replica') {\n update({ role, text: speech }, final ?? false);\n }\n }\n },\n [update]\n )\n );\n\n return caption;\n};\n",
|
|
417
|
+
styles: "",
|
|
418
|
+
componentsDependencies: ["cvi-events-hooks"]
|
|
419
|
+
};
|
|
420
|
+
//#endregion
|
|
362
421
|
//#region src/templates/tsx/hooks/use-cvi-call.json
|
|
363
422
|
var use_cvi_call_default$1 = {
|
|
364
423
|
type: "hooks",
|
|
365
|
-
content: "import { useCallback } from 'react';\nimport { useDaily } from '@daily-co/daily-react';\n\nexport const useCVICall = (): {\n joinCall: (props: { url: string }) => void;\n leaveCall: () => void;\n} => {\n const daily = useDaily();\n\n const joinCall = useCallback(\n ({ url }: { url: string }) => {\n daily?.join({\n url: url,\n inputSettings: {\n audio: {\n processor: {\n type:
|
|
424
|
+
content: "import { useCallback } from 'react';\nimport { useDaily } from '@daily-co/daily-react';\n\nexport const useCVICall = (): {\n joinCall: (props: { url: string }) => void;\n leaveCall: () => void;\n} => {\n const daily = useDaily();\n\n const joinCall = useCallback(\n ({ url }: { url: string }) => {\n daily?.join({\n url: url,\n inputSettings: {\n audio: {\n processor: {\n type: 'noise-cancellation',\n },\n },\n },\n });\n },\n [daily]\n );\n\n const leaveCall = useCallback(() => {\n daily?.leave();\n }, [daily]);\n\n return { joinCall, leaveCall };\n};\n",
|
|
366
425
|
styles: ""
|
|
367
426
|
};
|
|
368
427
|
//#endregion
|
|
@@ -419,12 +478,18 @@ var use_start_haircheck_default$1 = {
|
|
|
419
478
|
//#region src/templates/tsx/index.ts
|
|
420
479
|
var tsx_exports = /* @__PURE__ */ __exportAll({
|
|
421
480
|
"audio-wave": () => audio_wave_default$1,
|
|
481
|
+
chat: () => chat_default$1,
|
|
482
|
+
"closed-captions": () => closed_captions_default$1,
|
|
422
483
|
"conversation-01": () => conversation_01_default$1,
|
|
484
|
+
"conversation-02": () => conversation_02_default$1,
|
|
485
|
+
"conversation-status": () => conversation_status_default$1,
|
|
423
486
|
"cvi-events-hooks": () => cvi_events_hooks_default$1,
|
|
424
487
|
"cvi-provider": () => cvi_provider_default$1,
|
|
425
488
|
"device-select": () => device_select_default$1,
|
|
426
489
|
"hair-check-01": () => hair_check_01_default$1,
|
|
427
490
|
"media-controls": () => media_controls_default$1,
|
|
491
|
+
"use-chat": () => use_chat_default$1,
|
|
492
|
+
"use-closed-caption": () => use_closed_caption_default$1,
|
|
428
493
|
"use-cvi-call": () => use_cvi_call_default$1,
|
|
429
494
|
"use-local-camera": () => use_local_camera_default$1,
|
|
430
495
|
"use-local-microphone": () => use_local_microphone_default$1,
|
|
@@ -442,13 +507,48 @@ var audio_wave_default = {
|
|
|
442
507
|
styles: ".container {\n overflow: hidden;\n border: 1px solid white;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: 9999px;\n will-change: transform;\n transform: translateZ(0);\n}\n\n.waveContainer {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 0.125rem;\n width: 100%;\n height: 100%;\n}\n\n.bar {\n width: 0.25rem;\n height: 0.25rem;\n background-color: white;\n border-radius: 9999px;\n transition: height 200ms ease-out;\n will-change: height;\n transform: translateZ(0);\n}\n\n.barInactive {\n width: 0.25rem !important;\n height: 0.25rem !important;\n}\n"
|
|
443
508
|
};
|
|
444
509
|
//#endregion
|
|
510
|
+
//#region src/templates/jsx/components/chat.json
|
|
511
|
+
var chat_default = {
|
|
512
|
+
type: "components",
|
|
513
|
+
content: "import React, {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useChat } from '../../hooks/use-chat';\nimport styles from './chat.module.css';\n\nconst ChatContext = createContext(null);\n\nexport const ChatProvider = ({ children, defaultOpen = false }) => {\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const { messages, sendMessage, conversationId } = useChat();\n const toggle = useCallback(() => setIsOpen((v) => !v), []);\n const close = useCallback(() => setIsOpen(false), []);\n const value = useMemo(\n () => ({ isOpen, toggle, close, messages, sendMessage, conversationId }),\n [isOpen, toggle, close, messages, sendMessage, conversationId]\n );\n return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;\n};\n\nconst useChatContext = () => {\n const ctx = useContext(ChatContext);\n if (!ctx) {\n throw new Error('Chat components must be used within <ChatProvider>');\n }\n return ctx;\n};\n\nexport const ChatButton = memo(() => {\n const { isOpen, toggle } = useChatContext();\n return (\n <button\n type=\"button\"\n onClick={toggle}\n aria-pressed={isOpen}\n aria-label={isOpen ? 'Close chat' : 'Open chat'}\n className={`${styles.chatButton} ${isOpen ? styles.chatButtonActive : ''}`}\n >\n <span className={styles.chatButtonIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path\n d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </span>\n </button>\n );\n});\n\nChatButton.displayName = 'ChatButton';\n\nconst MessageBubble = memo(({ message }) => {\n const isUser = message.role === 'user';\n return (\n <li\n className={`${styles.messageRow} ${isUser ? styles.messageRowUser : styles.messageRowReplica}`}\n >\n <span\n className={`${styles.messageRole} ${\n isUser ? styles.messageRoleUser : styles.messageRoleReplica\n }`}\n >\n {isUser ? 'You' : 'Replica'}\n </span>\n <span\n className={`${styles.messageBubble} ${\n isUser ? styles.messageBubbleUser : styles.messageBubbleReplica\n }`}\n >\n {message.text}\n </span>\n </li>\n );\n});\n\nMessageBubble.displayName = 'MessageBubble';\n\nexport const ChatPanel = memo(() => {\n const { isOpen, close, messages, sendMessage, conversationId } = useChatContext();\n const [draft, setDraft] = useState('');\n const listRef = useRef(null);\n\n useEffect(() => {\n const el = listRef.current;\n if (el) {\n el.scrollTop = el.scrollHeight;\n }\n }, [messages.length]);\n\n const submit = useCallback(() => {\n const trimmed = draft.trim();\n if (!trimmed || !conversationId) {\n return;\n }\n sendMessage(trimmed);\n setDraft('');\n }, [draft, sendMessage, conversationId]);\n\n const onKeyDown = useCallback(\n (e) => {\n if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n submit();\n }\n },\n [submit]\n );\n\n const canSend = !!draft.trim() && !!conversationId;\n\n return (\n <aside\n className={`${styles.panel} ${isOpen ? styles.panelOpen : ''}`}\n aria-hidden={!isOpen}\n inert={!isOpen}\n >\n <header className={styles.header}>\n <span>Chat</span>\n <button\n type=\"button\"\n className={styles.closeButton}\n onClick={close}\n aria-label=\"Close chat\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path\n d=\"M18 6L6 18M6 6l12 12\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n </button>\n </header>\n\n <ul ref={listRef} className={styles.messageList} role=\"log\" aria-live=\"polite\">\n {messages.length === 0 ? (\n <li className={styles.empty}>\n {conversationId ? 'Send a message to start the conversation.' : 'Connecting…'}\n </li>\n ) : (\n messages.map((m) => <MessageBubble key={m.id} message={m} />)\n )}\n </ul>\n\n <form\n className={styles.composer}\n onSubmit={(e) => {\n e.preventDefault();\n submit();\n }}\n >\n <textarea\n className={styles.composerInput}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder={conversationId ? 'Type a message…' : 'Connecting…'}\n disabled={!conversationId}\n rows={1}\n aria-label=\"Message\"\n />\n\n <button\n type=\"submit\"\n className={styles.sendButton}\n disabled={!canSend}\n aria-label=\"Send message\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path\n d=\"M3 12l18-9-9 18-2-7-7-2z\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n />\n </svg>\n </button>\n </form>\n </aside>\n );\n});\n\nChatPanel.displayName = 'ChatPanel';\n",
|
|
514
|
+
styles: ".chatButton {\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n color: #000;\n cursor: pointer;\n}\n\n.chatButtonActive {\n background-color: white;\n color: #020617;\n}\n\n@container conversation (max-width: 400px) {\n .chatButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n}\n\n.chatButtonIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.panel {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n max-width: 100%;\n z-index: 25;\n display: flex;\n flex-direction: column;\n background-color: rgba(2, 6, 23, 0.85);\n backdrop-filter: blur(8px);\n color: white;\n transform: translateX(100%);\n transition: transform 200ms ease;\n pointer-events: none;\n}\n\n.panelOpen {\n transform: translateX(0);\n pointer-events: auto;\n}\n\n@container conversation (min-width: 768px) {\n .panel {\n width: 24rem;\n }\n}\n\n.header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1.25rem;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n font-weight: 600;\n font-size: 1rem;\n}\n\n.closeButton {\n height: 2rem;\n width: 2rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n background: transparent;\n border: none;\n color: white;\n cursor: pointer;\n}\n\n.closeButton:hover {\n background-color: rgba(255, 255, 255, 0.1);\n}\n\n.messageList {\n flex: 1;\n overflow-y: auto;\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n list-style: none;\n margin: 0;\n}\n\n.messageRow {\n display: flex;\n flex-direction: column;\n max-width: 85%;\n gap: 0.25rem;\n}\n\n.messageRowUser {\n align-self: flex-end;\n align-items: flex-end;\n}\n\n.messageRowReplica {\n align-self: flex-start;\n align-items: flex-start;\n}\n\n.messageRole {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n opacity: 0.7;\n}\n\n.messageRoleReplica {\n color: #93c5fd;\n}\n\n.messageRoleUser {\n color: #fcd34d;\n}\n\n.messageBubble {\n padding: 0.625rem 0.875rem;\n border-radius: 1rem;\n font-size: 0.95rem;\n line-height: 1.4;\n word-break: break-word;\n}\n\n.messageBubbleUser {\n background-color: rgba(252, 211, 77, 0.18);\n border: 1px solid rgba(252, 211, 77, 0.35);\n border-bottom-right-radius: 0.25rem;\n text-align: end;\n}\n\n.messageBubbleReplica {\n background-color: rgba(147, 197, 253, 0.18);\n border: 1px solid rgba(147, 197, 253, 0.35);\n border-bottom-left-radius: 0.25rem;\n text-align: start;\n}\n\n.composer {\n display: flex;\n gap: 0.5rem;\n padding: 0.75rem;\n border-top: 1px solid rgba(255, 255, 255, 0.1);\n padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0));\n}\n\n.composerInput {\n flex: 1;\n resize: none;\n min-height: 2.5rem;\n max-height: 8rem;\n padding: 0.5rem 0.75rem;\n border-radius: 0.75rem;\n background-color: rgba(255, 255, 255, 0.08);\n border: 1px solid rgba(255, 255, 255, 0.15);\n color: white;\n font-family: inherit;\n font-size: 0.95rem;\n line-height: 1.4;\n outline: none;\n}\n\n.composerInput:focus {\n border-color: rgba(255, 255, 255, 0.4);\n}\n\n.composerInput:disabled {\n opacity: 0.5;\n}\n\n.sendButton {\n height: 2.5rem;\n width: 2.5rem;\n border-radius: 9999px;\n background-color: white;\n color: #020617;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.sendButton:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.empty {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 2rem;\n text-align: center;\n color: rgba(255, 255, 255, 0.5);\n font-size: 0.9rem;\n}\n",
|
|
515
|
+
componentsDependencies: ["use-chat"]
|
|
516
|
+
};
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/templates/jsx/components/closed-captions.json
|
|
519
|
+
var closed_captions_default = {
|
|
520
|
+
type: "components",
|
|
521
|
+
content: "import React, { createContext, memo, useCallback, useContext, useMemo, useState } from 'react';\nimport { useClosedCaption } from '../../hooks/use-closed-caption';\nimport styles from './closed-captions.module.css';\n\nconst ClosedCaptionsContext = createContext(null);\n\nexport const ClosedCaptionsProvider = ({ children, defaultEnabled = false }) => {\n const [isEnabled, setIsEnabled] = useState(defaultEnabled);\n const toggle = useCallback(() => setIsEnabled((v) => !v), []);\n const value = useMemo(() => ({ isEnabled, toggle }), [isEnabled, toggle]);\n return <ClosedCaptionsContext.Provider value={value}>{children}</ClosedCaptionsContext.Provider>;\n};\n\nexport const useClosedCaptionsContext = () => {\n const ctx = useContext(ClosedCaptionsContext);\n if (!ctx) {\n throw new Error('ClosedCaptions components must be used within <ClosedCaptionsProvider>');\n }\n return ctx;\n};\n\nexport const ClosedCaptionsButton = memo(() => {\n const { isEnabled, toggle } = useClosedCaptionsContext();\n\n return (\n <button\n type=\"button\"\n onClick={toggle}\n aria-pressed={isEnabled}\n className={`${styles.captionButton} ${isEnabled ? styles.captionButtonActive : ''}`}\n >\n <span className={styles.captionButtonIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label={isEnabled ? 'Closed Captions On' : 'Closed Captions Off'}\n >\n <rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\" stroke=\"currentColor\" strokeWidth=\"2\" />\n <path\n d=\"M10 11C10 10.4477 9.55228 10 9 10H8C7.44772 10 7 10.4477 7 11V13C7 13.5523 7.44772 14 8 14H9C9.55228 14 10 13.5523 10 13\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n\n <path\n d=\"M17 11C17 10.4477 16.5523 10 16 10H15C14.4477 10 14 10.4477 14 11V13C14 13.5523 14.4477 14 15 14H16C16.5523 14 17 13.5523 17 13\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n </span>\n <span className={styles.srOnly}>Closed Captions</span>\n </button>\n );\n});\n\nClosedCaptionsButton.displayName = 'ClosedCaptionsButton';\n\nexport const ClosedCaptions = memo(() => {\n const { isEnabled } = useClosedCaptionsContext();\n const caption = useClosedCaption();\n\n if (!isEnabled || !caption || !caption.text) {\n return null;\n }\n\n return (\n <div className={styles.container} role=\"status\" aria-live=\"polite\">\n <span\n className={`${styles.role} ${caption.role === 'replica' ? styles.roleReplica : styles.roleUser}`}\n >\n {caption.role === 'replica' ? 'Replica' : 'You'}\n </span>\n <span className={styles.text}>\n <span className={styles.textInner}>{caption.text}</span>\n </span>\n </div>\n );\n});\n\nClosedCaptions.displayName = 'ClosedCaptions';\n",
|
|
522
|
+
styles: ".captionButton {\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n color: #000;\n cursor: pointer;\n}\n\n.captionButtonActive {\n background-color: white;\n color: #020617;\n}\n\n@container conversation (max-width: 400px) {\n .captionButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n}\n\n.captionButtonIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n.container {\n position: absolute;\n bottom: 5.5rem;\n left: 50%;\n transform: translateX(-50%);\n z-index: 15;\n width: min(85%, 40rem);\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.2rem;\n padding: 0.625rem 1rem;\n border-radius: 0.625rem;\n background-color: rgba(2, 6, 23, 0.7);\n backdrop-filter: blur(8px);\n color: white;\n font-size: 0.95rem;\n line-height: 1.4;\n text-align: center;\n pointer-events: none;\n --cc-line-height: 1.4;\n --cc-max-lines: 3;\n}\n\n.role {\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n opacity: 0.8;\n}\n\n.roleReplica {\n color: #93c5fd;\n}\n\n.roleUser {\n color: #fcd34d;\n}\n\n.text {\n display: flex;\n align-items: flex-end;\n justify-content: center;\n width: 100%;\n max-height: calc(var(--cc-line-height) * var(--cc-max-lines) * 1em);\n line-height: var(--cc-line-height);\n overflow: hidden;\n}\n\n.textInner {\n display: block;\n word-break: break-word;\n}\n",
|
|
523
|
+
componentsDependencies: ["use-closed-caption"]
|
|
524
|
+
};
|
|
525
|
+
//#endregion
|
|
445
526
|
//#region src/templates/jsx/components/conversation-01.json
|
|
446
527
|
var conversation_01_default = {
|
|
447
528
|
type: "components",
|
|
448
|
-
content: "import React, {
|
|
449
|
-
styles: ".container {\n position: relative;\n width: 100%;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n aspect-ratio: 9/16;\n overflow: hidden;\n border-radius: 0.5rem;\n max-height: 90vh;\n
|
|
529
|
+
content: "import React, { memo, useCallback, useEffect, useRef, useState } from 'react';\nimport {\n DailyAudioTrack,\n DailyVideo,\n useDevices,\n useLocalSessionId,\n useMeetingState,\n useScreenVideoTrack,\n useVideoTrack,\n} from '@daily-co/daily-react';\nimport { MicSelectBtn, CameraSelectBtn, ScreenShareButton } from '../device-select';\nimport { ClosedCaptions, ClosedCaptionsButton, ClosedCaptionsProvider } from '../closed-captions';\nimport { ChatButton, ChatPanel, ChatProvider } from '../chat';\nimport { ConnectingState, LeavingState } from '../conversation-status';\nimport { useLocalScreenshare } from '../../hooks/use-local-screenshare';\nimport { useReplicaIDs } from '../../hooks/use-replica-ids';\nimport { useCVICall } from '../../hooks/use-cvi-call';\nimport { AudioWave } from '../audio-wave';\n\nimport styles from './conversation.module.css';\n\nconst VideoPreview = React.memo(({ id }) => {\n const videoState = useVideoTrack(id);\n\n return (\n <div\n className={`${styles.previewVideoContainer} ${videoState.isOff ? styles.previewVideoContainerHidden : ''}`}\n >\n <DailyVideo\n automirror\n sessionId={id}\n type=\"video\"\n fit=\"cover\"\n className={`${styles.previewVideo} ${videoState.isOff ? styles.previewVideoHidden : ''}`}\n />\n\n <div className={styles.audioWaveContainer}>\n <AudioWave id={id} />\n </div>\n </div>\n );\n});\n\nconst PreviewVideos = React.memo(() => {\n const localId = useLocalSessionId();\n const { isScreenSharing } = useLocalScreenshare();\n const replicaIds = useReplicaIDs();\n const replicaId = replicaIds[0];\n\n return (\n <>\n {isScreenSharing && <VideoPreview id={replicaId} />}\n <VideoPreview id={localId} />\n </>\n );\n});\n\nconst SelfView = React.memo(() => (\n <div className={styles.selfViewContainer}>\n <PreviewVideos />\n </div>\n));\n\nconst MainVideo = React.memo(() => {\n const replicaIds = useReplicaIDs();\n const localId = useLocalSessionId();\n const videoState = useVideoTrack(replicaIds[0]);\n const screenVideoState = useScreenVideoTrack(localId);\n const meetingState = useMeetingState();\n const isScreenSharing = !screenVideoState.isOff;\n const replicaId = replicaIds[0];\n const [hasReplicaConnected, setHasReplicaConnected] = useState(false);\n\n useEffect(() => {\n if (replicaId && videoState.state === 'playable') {\n setHasReplicaConnected(true);\n }\n }, [replicaId, videoState.state]);\n\n if (meetingState === 'left-meeting' || meetingState === 'error') {\n return <LeavingState />;\n }\n\n if (!hasReplicaConnected) {\n return <ConnectingState />;\n }\n\n if (!replicaId) {\n return <ConnectingState />;\n }\n\n return (\n <div\n className={`${styles.mainVideoContainer} ${isScreenSharing ? styles.mainVideoContainerScreenSharing : ''}`}\n >\n <DailyVideo\n automirror\n sessionId={isScreenSharing ? localId : replicaId}\n type={isScreenSharing ? 'screenVideo' : 'video'}\n className={`${styles.mainVideo}\n ${isScreenSharing ? styles.mainVideoScreenSharing : ''}\n ${videoState.isOff ? styles.mainVideoHidden : ''}`}\n />\n\n <DailyAudioTrack sessionId={replicaId} />\n </div>\n );\n});\n\nconst MoreMenu = memo(() => {\n const [isOpen, setIsOpen] = useState(false);\n const ref = useRef(null);\n\n useEffect(() => {\n if (!isOpen) {\n return;\n }\n const handlePointerDown = (e) => {\n if (ref.current && !ref.current.contains(e.target)) {\n setIsOpen(false);\n }\n };\n const handleKey = (e) => {\n if (e.key === 'Escape') {\n setIsOpen(false);\n }\n };\n document.addEventListener('pointerdown', handlePointerDown);\n document.addEventListener('keydown', handleKey);\n return () => {\n document.removeEventListener('pointerdown', handlePointerDown);\n document.removeEventListener('keydown', handleKey);\n };\n }, [isOpen]);\n\n return (\n <div ref={ref} className={styles.moreMenu}>\n <button\n type=\"button\"\n onClick={() => setIsOpen((v) => !v)}\n aria-pressed={isOpen}\n aria-label={isOpen ? 'Close more controls' : 'More controls'}\n aria-haspopup=\"true\"\n aria-expanded={isOpen}\n className={`${styles.moreButton} ${isOpen ? styles.moreButtonActive : ''}`}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <circle cx=\"5\" cy=\"12\" r=\"1.75\" fill=\"currentColor\" />\n <circle cx=\"12\" cy=\"12\" r=\"1.75\" fill=\"currentColor\" />\n <circle cx=\"19\" cy=\"12\" r=\"1.75\" fill=\"currentColor\" />\n </svg>\n </button>\n {isOpen && (\n <div className={styles.morePopover} role=\"menu\">\n <ScreenShareButton />\n <ClosedCaptionsButton />\n </div>\n )}\n </div>\n );\n});\n\nMoreMenu.displayName = 'MoreMenu';\n\nexport const Conversation = React.memo(({ onLeave, conversationUrl }) => {\n const { joinCall, leaveCall } = useCVICall();\n const meetingState = useMeetingState();\n const { hasMicError } = useDevices();\n\n useEffect(() => {\n if (meetingState === 'error') {\n onLeave();\n }\n }, [meetingState, onLeave]);\n\n useEffect(() => {\n joinCall({ url: conversationUrl });\n }, []);\n\n const handleLeave = useCallback(() => {\n leaveCall();\n onLeave();\n }, [leaveCall, onLeave]);\n\n return (\n <ClosedCaptionsProvider>\n <ChatProvider>\n <div className={styles.containerWrapper}>\n <div className={styles.container}>\n <div className={styles.videoContainer}>\n {hasMicError && (\n <div className={styles.errorContainer}>\n <p>\n Camera or microphone access denied. Please check your settings and try again.\n </p>\n </div>\n )}\n\n <div className={styles.mainVideoContainer}>\n <MainVideo />\n </div>\n\n <SelfView />\n\n <ClosedCaptions />\n </div>\n\n <ChatPanel />\n\n <div\n className={`${styles.footer} ${meetingState === 'left-meeting' ? styles.footerLeaving : ''}`}\n aria-hidden={meetingState === 'left-meeting'}\n >\n <div className={styles.footerControls}>\n <MicSelectBtn />\n <CameraSelectBtn />\n <MoreMenu />\n <ChatButton />\n <button type=\"button\" className={styles.leaveButton} onClick={handleLeave}>\n <span className={styles.leaveButtonIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Leave Call\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </span>\n </button>\n </div>\n </div>\n </div>\n </div>\n </ChatProvider>\n </ClosedCaptionsProvider>\n );\n});\n",
|
|
530
|
+
styles: ".containerWrapper {\n width: 100%;\n container-type: inline-size;\n container-name: conversation;\n}\n\n.container {\n position: relative;\n width: 100%;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n aspect-ratio: 9/16;\n overflow: hidden;\n border-radius: 0.5rem;\n max-height: 90vh;\n background: linear-gradient(135deg, #4b5563 0%, #1f2937 100%);\n background-size: 400% 400%;\n animation: gradient 15s ease infinite;\n}\n\n/* Aspect ratio tracks the wrapper's actual width: portrait when narrow\n (e.g. embedded in a 400px modal), landscape when wide. */\n@container conversation (min-width: 768px) {\n .container {\n aspect-ratio: 16/9;\n }\n}\n\n.errorContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(248, 250, 252, 0.08);\n color: white;\n height: 100%;\n font-size: 1.5rem;\n font-weight: 600;\n text-align: center;\n}\n\n.videoContainer {\n position: relative;\n z-index: 5;\n width: 100%;\n height: 100%;\n}\n\n.footer {\n position: absolute;\n bottom: 1.5rem;\n left: 0;\n right: 0;\n z-index: 20;\n transition:\n opacity 0.3s ease,\n transform 0.3s ease;\n}\n\n.footerLeaving {\n opacity: 0;\n transform: translateY(20px);\n pointer-events: none;\n}\n\n.footerControls {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 16px;\n}\n\n.moreMenu {\n display: block;\n position: relative;\n}\n\n.moreButton {\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n color: #000;\n cursor: pointer;\n}\n\n.moreButtonActive {\n background-color: white;\n color: #020617;\n}\n\n.morePopover {\n position: absolute;\n bottom: calc(100% + 0.5rem);\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n padding: 0.5rem;\n border-radius: 1rem;\n backdrop-filter: blur(16px);\n background-color: rgba(162, 162, 162, 0.2);\n z-index: 30;\n}\n\n.leaveButton {\n background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);\n color: white;\n border: none;\n font-size: 16px;\n cursor: pointer;\n transition: all 0.3s ease;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: #ef4444;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.leaveButton:hover {\n opacity: 0.8;\n}\n\n.leaveButtonIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n@container conversation (max-width: 400px) {\n .footerControls {\n gap: 8px;\n }\n .moreButton,\n .leaveButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n}\n\n/* ReplicaVideo styles */\n.mainVideoContainer {\n background: transparent;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.mainVideoContainerScreenSharing {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.mainVideo {\n position: absolute;\n inset: 0;\n object-position: center;\n object-fit: cover !important;\n height: 100%;\n width: 100%;\n transition: all 0.3s ease;\n}\n\n.mainVideoScreenSharing {\n object-fit: contain !important;\n}\n\n.mainVideoHidden {\n display: none;\n}\n\n/* PreviewVideo styles */\n.previewVideoContainer {\n position: relative;\n background: rgba(2, 6, 23, 0.3);\n aspect-ratio: 1/1;\n width: 6rem;\n border-radius: 0.75rem;\n overflow: hidden;\n z-index: 10;\n}\n\n@container conversation (min-width: 768px) {\n .previewVideoContainer {\n width: 7rem;\n }\n}\n\n@container conversation (min-width: 1024px) {\n .previewVideoContainer {\n width: 8rem;\n }\n}\n\n.previewVideoContainerHidden {\n background: transparent;\n display: none;\n}\n\n.previewVideo {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.previewVideoHidden {\n display: none;\n}\n\n/* Main video container */\n.mainVideoContainer {\n width: 100%;\n height: 100%;\n}\n\n/* Self view container — pinned to top-right so captions own the bottom band. */\n.selfViewContainer {\n position: absolute;\n top: 1rem;\n left: 1rem;\n display: flex;\n align-items: flex-end;\n flex-direction: column;\n gap: 0.5rem;\n z-index: 15;\n}\n\n.selfViewContainer video {\n object-position: center center;\n object-fit: cover;\n}\n\n/* Waiting message container */\n/* Start of Selection */\n.waitingContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n color: white;\n height: 100%;\n font-size: 1.5rem;\n font-weight: 600;\n}\n\n@keyframes gradient {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n/* End of Selection */\n\n.audioWaveContainer {\n position: absolute;\n bottom: 0.5rem;\n right: 0.5rem;\n}\n",
|
|
531
|
+
componentsDependencies: [
|
|
532
|
+
"device-select",
|
|
533
|
+
"closed-captions",
|
|
534
|
+
"chat",
|
|
535
|
+
"conversation-status",
|
|
536
|
+
"use-local-screenshare",
|
|
537
|
+
"use-replica-ids",
|
|
538
|
+
"use-cvi-call",
|
|
539
|
+
"audio-wave",
|
|
540
|
+
"cvi-provider"
|
|
541
|
+
]
|
|
542
|
+
};
|
|
543
|
+
//#endregion
|
|
544
|
+
//#region src/templates/jsx/components/conversation-02.json
|
|
545
|
+
var conversation_02_default = {
|
|
546
|
+
type: "components",
|
|
547
|
+
content: "import React, { useCallback, useEffect, useState } from 'react';\nimport {\n DailyAudioTrack,\n DailyVideo,\n useDevices,\n useLocalSessionId,\n useMeetingState,\n useScreenVideoTrack,\n useVideoTrack,\n} from '@daily-co/daily-react';\nimport { MicSelectBtn, CameraSelectBtn, ScreenShareButton } from '../device-select';\nimport { ConnectingState, LeavingState } from '../conversation-status';\nimport { useLocalScreenshare } from '../../hooks/use-local-screenshare';\nimport { useReplicaIDs } from '../../hooks/use-replica-ids';\nimport { useCVICall } from '../../hooks/use-cvi-call';\nimport { AudioWave } from '../audio-wave';\n\nimport styles from './conversation.module.css';\n\nconst VideoPreview = React.memo(({ id }) => {\n const videoState = useVideoTrack(id);\n const widthVideo = videoState.track?.getSettings()?.width;\n const heightVideo = videoState.track?.getSettings()?.height;\n const isVertical = widthVideo && heightVideo ? widthVideo < heightVideo : false;\n\n return (\n <div\n className={`${styles.previewVideoContainer} ${isVertical ? styles.previewVideoContainerVertical : ''} ${videoState.isOff ? styles.previewVideoContainerHidden : ''}`}\n >\n <DailyVideo\n automirror\n sessionId={id}\n type=\"video\"\n className={`${styles.previewVideo} ${isVertical ? styles.previewVideoVertical : ''} ${videoState.isOff ? styles.previewVideoHidden : ''}`}\n />\n\n <div className={styles.audioWaveContainer}>\n <AudioWave id={id} />\n </div>\n </div>\n );\n});\n\nconst PreviewVideos = React.memo(() => {\n const localId = useLocalSessionId();\n const { isScreenSharing } = useLocalScreenshare();\n const replicaIds = useReplicaIDs();\n const replicaId = replicaIds[0];\n\n return (\n <>\n {isScreenSharing && <VideoPreview id={replicaId} />}\n <VideoPreview id={localId} />\n </>\n );\n});\n\nconst MainVideo = React.memo(() => {\n const replicaIds = useReplicaIDs();\n const localId = useLocalSessionId();\n const videoState = useVideoTrack(replicaIds[0]);\n const screenVideoState = useScreenVideoTrack(localId);\n const meetingState = useMeetingState();\n const isScreenSharing = !screenVideoState.isOff;\n // This is one-to-one call, so we can use the first replica id\n const replicaId = replicaIds[0];\n const [hasReplicaConnected, setHasReplicaConnected] = useState(false);\n\n useEffect(() => {\n if (replicaId && videoState.state === 'playable') {\n setHasReplicaConnected(true);\n }\n }, [replicaId, videoState.state]);\n\n if (meetingState === 'left-meeting' || meetingState === 'error') {\n return <LeavingState />;\n }\n\n if (!hasReplicaConnected) {\n return <ConnectingState />;\n }\n\n if (!replicaId) {\n return <ConnectingState />;\n }\n\n // Switching between replica video and screen sharing video\n return (\n <div\n className={`${styles.mainVideoContainer} ${isScreenSharing ? styles.mainVideoContainerScreenSharing : ''}`}\n >\n <DailyVideo\n automirror\n sessionId={isScreenSharing ? localId : replicaId}\n type={isScreenSharing ? 'screenVideo' : 'video'}\n className={`${styles.mainVideo}\n ${isScreenSharing ? styles.mainVideoScreenSharing : ''}\n ${videoState.isOff ? styles.mainVideoHidden : ''}`}\n />\n\n <DailyAudioTrack sessionId={replicaId} />\n </div>\n );\n});\n\nexport const Conversation = React.memo(({ onLeave, conversationUrl }) => {\n const { joinCall, leaveCall } = useCVICall();\n const meetingState = useMeetingState();\n const { hasMicError } = useDevices();\n\n useEffect(() => {\n if (meetingState === 'error') {\n onLeave();\n }\n }, [meetingState, onLeave]);\n\n // Initialize call when conversation is available\n useEffect(() => {\n joinCall({ url: conversationUrl });\n }, []);\n\n const handleLeave = useCallback(() => {\n leaveCall();\n onLeave();\n }, [leaveCall, onLeave]);\n\n return (\n <div className={styles.containerWrapper}>\n <div className={styles.container}>\n <div className={styles.videoContainer}>\n {hasMicError && (\n <div className={styles.errorContainer}>\n <p>Camera or microphone access denied. Please check your settings and try again.</p>\n </div>\n )}\n\n {/* Main video */}\n <div className={styles.mainVideoContainer}>\n <MainVideo />\n </div>\n\n {/* Self view */}\n <div className={styles.selfViewContainer}>\n <PreviewVideos />\n </div>\n </div>\n\n <div\n className={`${styles.footer} ${meetingState === 'left-meeting' ? styles.footerLeaving : ''}`}\n aria-hidden={meetingState === 'left-meeting'}\n >\n <div className={styles.footerControls}>\n <MicSelectBtn />\n <CameraSelectBtn />\n <ScreenShareButton />\n <button type=\"button\" className={styles.leaveButton} onClick={handleLeave}>\n <span className={styles.leaveButtonIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Leave Call\"\n >\n <path\n d=\"M18 6L6 18M6 6L18 18\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </span>\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n});\n",
|
|
548
|
+
styles: ".containerWrapper {\n width: 100%;\n container-type: inline-size;\n container-name: conversation;\n}\n\n.container {\n position: relative;\n width: 100%;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n aspect-ratio: 9/16;\n overflow: hidden;\n border-radius: 0.5rem;\n max-height: 90vh;\n background: linear-gradient(135deg, #4b5563 0%, #1f2937 100%);\n background-size: 400% 400%;\n animation: gradient 15s ease infinite;\n}\n\n/* Aspect ratio tracks the wrapper's actual width: portrait when narrow,\n landscape when wide. */\n@container conversation (min-width: 768px) {\n .container {\n aspect-ratio: 16/9;\n }\n}\n\n.errorContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(248, 250, 252, 0.08);\n color: white;\n height: 100%;\n font-size: 1.5rem;\n font-weight: 600;\n text-align: center;\n}\n\n.videoContainer {\n position: relative;\n z-index: 5;\n width: 100%;\n height: 100%;\n}\n\n.footer {\n position: absolute;\n bottom: 1.5rem;\n left: 0;\n right: 0;\n z-index: 20;\n transition:\n opacity 0.3s ease,\n transform 0.3s ease;\n}\n\n.footerLeaving {\n opacity: 0;\n transform: translateY(20px);\n pointer-events: none;\n}\n\n.footerControls {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 16px;\n}\n\n.leaveButton {\n background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);\n color: white;\n border: none;\n font-size: 16px;\n cursor: pointer;\n transition: all 0.3s ease;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: #ef4444;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.leaveButton:hover {\n opacity: 0.8;\n}\n\n.leaveButtonIcon {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* ReplicaVideo styles */\n.mainVideoContainer {\n background: transparent;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.mainVideoContainerScreenSharing {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.mainVideo {\n position: absolute;\n inset: 0;\n object-position: center;\n object-fit: cover !important;\n height: 100%;\n width: 100%;\n transition: all 0.3s ease;\n}\n\n.mainVideoScreenSharing {\n object-fit: contain !important;\n}\n\n.mainVideoHidden {\n display: none;\n}\n\n/* PreviewVideo styles */\n.previewVideoContainer {\n position: relative;\n background: rgba(2, 6, 23, 0.3);\n aspect-ratio: 16/9;\n width: 13rem;\n border-radius: 1rem;\n overflow: hidden;\n max-height: 140px;\n z-index: 10;\n}\n\n@container conversation (min-width: 768px) {\n .previewVideoContainer {\n width: 15rem;\n max-height: 160px;\n }\n}\n\n@container conversation (min-width: 1024px) {\n .previewVideoContainer {\n width: 18rem;\n max-height: 200px;\n }\n}\n\n.previewVideoContainerVertical {\n height: 40.5rem;\n width: 6rem;\n}\n\n.previewVideoContainerHidden {\n background: transparent;\n display: none;\n}\n\n.previewVideo {\n width: 100%;\n height: auto;\n max-height: 120px;\n}\n\n@container conversation (min-width: 768px) {\n .previewVideo {\n max-height: 100%;\n }\n}\n\n.previewVideoVertical {\n height: 40.5rem;\n width: 6rem;\n object-fit: cover;\n}\n\n.previewVideoHidden {\n display: none;\n}\n\n/* Main video container */\n.mainVideoContainer {\n width: 100%;\n height: 100%;\n}\n\n/* Self view container */\n.selfViewContainer {\n position: absolute;\n bottom: 5.5rem;\n left: 1rem;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n@container conversation (min-width: 1024px) {\n .selfViewContainer {\n bottom: 1rem;\n }\n}\n\n/* Waiting message container */\n/* Start of Selection */\n.waitingContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n color: white;\n height: 100%;\n font-size: 1.5rem;\n font-weight: 600;\n}\n\n@keyframes gradient {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n/* End of Selection */\n\n.audioWaveContainer {\n position: absolute;\n bottom: 0.5rem;\n right: 0.5rem;\n}\n",
|
|
450
549
|
componentsDependencies: [
|
|
451
550
|
"device-select",
|
|
551
|
+
"conversation-status",
|
|
452
552
|
"use-local-screenshare",
|
|
453
553
|
"use-replica-ids",
|
|
454
554
|
"use-cvi-call",
|
|
@@ -457,6 +557,14 @@ var conversation_01_default = {
|
|
|
457
557
|
]
|
|
458
558
|
};
|
|
459
559
|
//#endregion
|
|
560
|
+
//#region src/templates/jsx/components/conversation-status.json
|
|
561
|
+
var conversation_status_default = {
|
|
562
|
+
type: "components",
|
|
563
|
+
content: "import React, { memo } from 'react';\nimport styles from './conversation-status.module.css';\n\nconst GRID_SIZE = 144;\nconst CONNECT_STAGGER_S = 1.8;\nconst LEAVE_STAGGER_S = 1.0;\n\n// Pre-shuffled positions, computed once at module load. Each square gets a\n// stable position in the random fill order so re-renders never re-shuffle.\nconst SHUFFLED_ORDER = (() => {\n const order = Array.from({ length: GRID_SIZE }, (_, i) => i);\n for (let i = order.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [order[i], order[j]] = [order[j], order[i]];\n }\n return order;\n})();\n\n// Per-square target opacity, ranged 0.25–0.75, gives the grid a dithered\n// look so cells don't all read at the same brightness when filled.\nconst TARGET_OPACITY = Array.from({ length: GRID_SIZE }, () => 0.25 + Math.random() * 0.5);\n\n// Per-square Bayer pattern offset (0–7px on each axis) so the dither\n// patterns don't line up across cells — adds visual noise to the grid.\nconst DITHER_OFFSETS = Array.from({ length: GRID_SIZE }, () => {\n const x = Math.floor(Math.random() * 8);\n const y = Math.floor(Math.random() * 8);\n return `${x}px ${y}px`;\n});\n\nconst Grid = ({ leaving = false }) => {\n const squareClass = leaving ? styles.squareLeaving : styles.squareConnecting;\n const totalStagger = leaving ? LEAVE_STAGGER_S : CONNECT_STAGGER_S;\n return (\n <div className={styles.grid} aria-hidden=\"true\">\n {Array.from({ length: GRID_SIZE }, (_, i) => {\n const delay = (SHUFFLED_ORDER[i] / GRID_SIZE) * totalStagger;\n const style = {\n '--fill-delay': `${delay.toFixed(3)}s`,\n '--target-opacity': TARGET_OPACITY[i].toFixed(2),\n '--dither-offset': DITHER_OFFSETS[i],\n };\n return <span key={i} className={`${styles.square} ${squareClass}`} style={style} />;\n })}\n </div>\n );\n};\n\nconst Dots = () => (\n <>\n <span className={styles.dot}>.</span>\n <span className={styles.dot}>.</span>\n <span className={styles.dot}>.</span>\n </>\n);\n\nexport const ConnectingState = memo(() => (\n <div className={styles.statusContainer} role=\"status\" aria-live=\"polite\">\n <Grid />\n <div className={`${styles.label} ${styles.labelConnecting}`}>\n <span className={styles.labelText}>\n Connecting\n <Dots />\n </span>\n </div>\n </div>\n));\n\nConnectingState.displayName = 'ConnectingState';\n\nexport const LeavingState = memo(() => (\n <div className={styles.statusContainer} role=\"status\" aria-live=\"polite\">\n <Grid leaving />\n <div className={`${styles.label} ${styles.labelLeaving}`}>\n <span className={styles.labelText}>\n Leaving\n <Dots />\n </span>\n </div>\n </div>\n));\n\nLeavingState.displayName = 'LeavingState';\n",
|
|
564
|
+
styles: ".statusContainer {\n position: relative;\n width: 100%;\n height: 100%;\n background: #0f0f13;\n overflow: hidden;\n}\n\n.grid {\n position: absolute;\n inset: 0;\n display: grid;\n grid-template-columns: repeat(9, 1fr);\n grid-template-rows: repeat(16, 1fr);\n gap: 0.25rem;\n padding: 0.25rem;\n box-sizing: border-box;\n}\n\n@container conversation (min-width: 768px) {\n .grid {\n grid-template-columns: repeat(16, 1fr);\n grid-template-rows: repeat(9, 1fr);\n }\n}\n\n.square {\n border-radius: 0.25rem;\n background-color: rgba(255, 255, 255, 0.04);\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);\n transform-origin: center;\n opacity: 0;\n}\n\n.squareConnecting {\n background-color: #412174;\n background-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8' shape-rendering='crispEdges'><rect x='4' y='0' width='4' height='4' fill='black' fill-opacity='0.45'/><rect x='0' y='4' width='4' height='4' fill='black' fill-opacity='0.65'/><rect x='4' y='4' width='4' height='4' fill='black' fill-opacity='0.25'/></svg>\");\n background-size: 8px 8px;\n background-repeat: repeat;\n background-position: var(--dither-offset, 0 0);\n animation:\n squareFillIn 0.55s ease-out var(--fill-delay, 0s) forwards,\n squareShimmer 2.6s ease-in-out calc(var(--fill-delay, 0s) + 0.55s) infinite;\n}\n\n@keyframes squareFillIn {\n from {\n transform: scale(0.7);\n opacity: 0;\n }\n to {\n transform: scale(1);\n opacity: var(--target-opacity, 1);\n }\n}\n\n@keyframes squareShimmer {\n 0%,\n 100% {\n opacity: var(--target-opacity, 1);\n }\n 50% {\n opacity: calc(var(--target-opacity, 1) * 0.2);\n }\n}\n\n.squareLeaving {\n background-color: #683d1b;\n background-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8' shape-rendering='crispEdges'><rect x='4' y='0' width='4' height='4' fill='black' fill-opacity='0.45'/><rect x='0' y='4' width='4' height='4' fill='black' fill-opacity='0.65'/><rect x='4' y='4' width='4' height='4' fill='black' fill-opacity='0.25'/></svg>\");\n background-size: 8px 8px;\n background-repeat: repeat;\n background-position: var(--dither-offset, 0 0);\n opacity: var(--target-opacity, 1);\n transform: scale(1);\n animation:\n squareFadeOut 3s ease-out var(--fill-delay, 0s) forwards,\n squareShimmer 2.6s ease-in-out calc(var(--fill-delay, 0s) + 0.55s) infinite;\n}\n\n@keyframes squareFadeOut {\n from {\n opacity: var(--target-opacity, 1);\n transform: scale(1);\n }\n to {\n transform: scale(0.7);\n opacity: 0;\n }\n}\n\n.label {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2;\n pointer-events: none;\n font-size: 1rem;\n font-weight: 500;\n}\n\n.labelConnecting {\n color: #ffffff;\n}\n\n.labelLeaving {\n color: #ffffff;\n}\n\n.labelText {\n display: inline-flex;\n padding: 0.5rem 1.25rem;\n border-radius: 0.25rem;\n background-color: rgba(15, 23, 42, 0.65);\n backdrop-filter: blur(10px);\n}\n\n.dot {\n display: inline-block;\n opacity: 0;\n animation: dotFade 1.4s ease-in-out infinite;\n}\n\n.dot:nth-child(1) {\n animation-delay: 0s;\n}\n.dot:nth-child(2) {\n animation-delay: 0.2s;\n}\n.dot:nth-child(3) {\n animation-delay: 0.4s;\n}\n\n@keyframes dotFade {\n 0%,\n 80%,\n 100% {\n opacity: 0;\n }\n 40% {\n opacity: 1;\n }\n}\n",
|
|
565
|
+
componentsDependencies: ["cvi-provider"]
|
|
566
|
+
};
|
|
567
|
+
//#endregion
|
|
460
568
|
//#region src/templates/jsx/components/cvi-provider.json
|
|
461
569
|
var cvi_provider_default = {
|
|
462
570
|
type: "components",
|
|
@@ -467,8 +575,8 @@ var cvi_provider_default = {
|
|
|
467
575
|
//#region src/templates/jsx/components/device-select.json
|
|
468
576
|
var device_select_default = {
|
|
469
577
|
type: "components",
|
|
470
|
-
content: "import React, { memo } from 'react';\nimport { useDevices } from '@daily-co/daily-react';\nimport { useLocalCamera } from '../../hooks/use-local-camera';\nimport { useLocalMicrophone } from '../../hooks/use-local-microphone';\nimport { useLocalScreenshare } from '../../hooks/use-local-screenshare';\nimport styles from './device-select.module.css';\n\nexport const SelectDevice = ({ value, devices, disabled, onChange }) => {\n return (\n <div className={styles.selectDeviceContainer}>\n <select\n value={value}\n onChange={(e) => onChange(e.target.value)}\n disabled={disabled}\n className={styles.selectDevice}\n >\n {devices.map(({ device }) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))}\n </select>\n <span className={styles.selectArrow}>\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M4 6L8 10L12 6\"\n stroke=\"#fff\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </span>\n </div>\n );\n};\n\nexport const MicSelectBtn = memo(() => {\n const { onToggleMicrophone, isMicReady, isMicMuted } = useLocalMicrophone();\n const { microphones, currentMic, setMicrophone } = useDevices();\n\n return (\n <div className={styles.deviceButtonContainer}>\n <button\n type=\"button\"\n onClick={onToggleMicrophone}\n disabled={!isMicReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isMicMuted || !isMicReady ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone Muted\"\n >\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 18.1339 14.0992 18.1339 14.0992M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"1.66667\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n\n <rect\n x=\"1.30225\"\n y=\"3\"\n width=\"26\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 1.30225 3)\"\n fill=\"#020617\"\n />\n\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M9 9.39031V11C9 11.7956 9.31607 12.5587 9.87868 13.1213C10.4413 13.6839 11.2044 14 12 14C12.7956 14 13.5587 13.6839 14.1213 13.1213C14.2829 12.9597 14.4242 12.7816 14.5435 12.5908L9 9.39031ZM15 7.71466V6C15 5.20435 14.6839 4.44129 14.1213 3.87868C13.5587 3.31607 12.7956 3 12 3C11.2044 3 10.4413 3.31607 9.87868 3.87868C9.69528 4.06208 9.53807 4.26678 9.40948 4.48697L15 7.71466Z\"\n fill=\"#020617\"\n />\n\n <path\n d=\"M9 9.39031L9.41667 8.66862C9.15883 8.51976 8.84117 8.51976 8.58333 8.66862C8.3255 8.81748 8.16667 9.09259 8.16667 9.39031H9ZM9.87868 13.1213L9.28942 13.7106H9.28942L9.87868 13.1213ZM14.1213 13.1213L14.7106 13.7106L14.7106 13.7106L14.1213 13.1213ZM14.5435 12.5908L15.25 13.0327C15.3699 12.841 15.4068 12.6088 15.3521 12.3894C15.2974 12.17 15.156 11.9822 14.9601 11.8692L14.5435 12.5908ZM15 7.71466L14.5833 8.43635C14.8412 8.58521 15.1588 8.58521 15.4167 8.43635C15.6745 8.28749 15.8333 8.01238 15.8333 7.71466H15ZM14.1213 3.87868L14.7106 3.28942L14.7106 3.28942L14.1213 3.87868ZM9.87868 3.87868L9.28942 3.28942L9.28942 3.28942L9.87868 3.87868ZM9.40948 4.48697L8.68988 4.06671C8.57806 4.25818 8.54715 4.48633 8.604 4.70065C8.66086 4.91497 8.80078 5.09779 8.99281 5.20866L9.40948 4.48697ZM9.83333 11V9.39031H8.16667V11H9.83333ZM10.4679 12.5321C10.0616 12.1257 9.83333 11.5746 9.83333 11H8.16667C8.16667 12.0167 8.57053 12.9917 9.28942 13.7106L10.4679 12.5321ZM12 13.1667C11.4254 13.1667 10.8743 12.9384 10.4679 12.5321L9.28942 13.7106C10.0083 14.4295 10.9833 14.8333 12 14.8333V13.1667ZM13.5321 12.5321C13.1257 12.9384 12.5746 13.1667 12 13.1667V14.8333C13.0167 14.8333 13.9917 14.4295 14.7106 13.7106L13.5321 12.5321ZM13.837 12.149C13.7508 12.2867 13.6488 12.4153 13.5321 12.5321L14.7106 13.7106C14.917 13.5041 15.0976 13.2764 15.25 13.0327L13.837 12.149ZM14.9601 11.8692L9.41667 8.66862L8.58333 10.112L14.1268 13.3125L14.9601 11.8692ZM14.1667 6V7.71466H15.8333V6H14.1667ZM13.5321 4.46794C13.9384 4.87426 14.1667 5.42536 14.1667 6H15.8333C15.8333 4.98334 15.4295 4.00831 14.7106 3.28942L13.5321 4.46794ZM12 3.83333C12.5746 3.83333 13.1257 4.06161 13.5321 4.46794L14.7106 3.28942C13.9917 2.57053 13.0167 2.16667 12 2.16667V3.83333ZM10.4679 4.46794C10.8743 4.06161 11.4254 3.83333 12 3.83333V2.16667C10.9833 2.16667 10.0083 2.57053 9.28942 3.28942L10.4679 4.46794ZM10.1291 4.90724C10.2219 4.74824 10.3354 4.60042 10.4679 4.46794L9.28942 3.28942C9.05511 3.52374 8.85421 3.78533 8.68988 4.06671L10.1291 4.90724ZM8.99281 5.20866L14.5833 8.43635L15.4167 6.99298L9.82615 3.76529L8.99281 5.20866Z\"\n fill=\"#020617\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone\"\n >\n <path\n d=\"M9 6C9 5.20435 9.31607 4.44129 9.87868 3.87868C10.4413 3.31607 11.2044 3 12 3C12.7956 3 13.5587 3.31607 14.1213 3.87868C14.6839 4.44129 15 5.20435 15 6V11C15 11.7956 14.6839 12.5587 14.1213 13.1213C13.5587 13.6839 12.7956 14 12 14C11.2044 14 10.4413 13.6839 9.87868 13.1213C9.31607 12.5587 9 11.7956 9 11V6Z\"\n fill=\"#020617\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 19 12.8565 19 11V10M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Microphone</span>\n </button>\n <SelectDevice\n value={currentMic?.device?.deviceId}\n devices={microphones || []}\n disabled={!isMicReady}\n onChange={(val) => setMicrophone(val)}\n />\n </div>\n );\n});\n\nMicSelectBtn.displayName = 'MicSelectBtn';\n\nexport const CameraSelectBtn = memo(() => {\n const { onToggleCamera, isCamReady, isCamMuted } = useLocalCamera();\n const { currentCam, cameras, setCamera } = useDevices();\n\n return (\n <div className={styles.deviceButtonContainer}>\n <button\n type=\"button\"\n onClick={onToggleCamera}\n disabled={!isCamReady || !currentCam}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isCamMuted ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera Muted\"\n >\n <g clipPath=\"url(#clip0_7082_14220)\">\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M3.19874 5.60093C3.08628 5.68537 2.97928 5.77808 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V15.048C17.7787 14.8204 17.5304 14.6189 17.2595 14.4485L3.19874 5.60093ZM22 12.655V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H8.7412L22 12.655Z\"\n fill=\"#020617\"\n />\n\n <rect\n x=\"0.777222\"\n y=\"2.64844\"\n width=\"26.7988\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 0.777222 2.64844)\"\n fill=\"#020617\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_7082_14220\">\n <rect width=\"24\" height=\"24\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera\"\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M5 5C4.20435 5 3.44129 5.31607 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V14.414L20.293 16.707C20.4329 16.8468 20.611 16.942 20.805 16.9806C20.9989 17.0192 21.2 16.9993 21.3827 16.9237C21.5654 16.848 21.7215 16.7199 21.8314 16.5555C21.9413 16.391 22 16.1978 22 16V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H5Z\"\n fill=\"#020617\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Camera</span>\n </button>\n <SelectDevice\n value={currentCam?.device?.deviceId}\n devices={cameras || []}\n disabled={!isCamReady}\n onChange={(val) => setCamera(val)}\n />\n </div>\n );\n});\n\nCameraSelectBtn.displayName = 'CameraSelectBtn';\n\nexport const ScreenShareButton = memo(() => {\n const { onToggleScreenshare, isScreenSharing } = useLocalScreenshare();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleScreenshare}\n className={`${styles.deviceButtonContainer} ${styles.screenShareButton}`}\n >\n <span>\n {isScreenSharing ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M21.0008 19C21.2557 19.0003 21.5009 19.0979 21.6862 19.2728C21.8715 19.4478 21.9831 19.687 21.998 19.9414C22.013 20.1958 21.9302 20.4464 21.7666 20.6418C21.603 20.8373 21.3709 20.9629 21.1178 20.993L21.0008 21H3.00085C2.74597 20.9997 2.50081 20.9021 2.31548 20.7272C2.13014 20.5522 2.01861 20.313 2.00367 20.0586C1.98874 19.8042 2.07152 19.5536 2.23511 19.3582C2.3987 19.1627 2.63075 19.0371 2.88385 19.007L3.00085 19H21.0008ZM19.0008 4C19.5054 3.99984 19.9914 4.19041 20.3614 4.5335C20.7314 4.87659 20.958 5.34684 20.9958 5.85L21.0008 6V16C21.001 16.5046 20.8104 16.9906 20.4673 17.3605C20.1243 17.7305 19.654 17.9572 19.1508 17.995L19.0008 18H5.00085C4.49627 18.0002 4.01028 17.8096 3.6403 17.4665C3.27032 17.1234 3.04369 16.6532 3.00585 16.15L3.00085 16V6C3.00069 5.49542 3.19125 5.00943 3.53434 4.63945C3.87743 4.26947 4.34769 4.04284 4.85085 4.005L5.00085 4H19.0008Z\"\n fill=\"#2D65FF\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M20.9999 19C21.2547 19.0003 21.4999 19.0979 21.6852 19.2728C21.8706 19.4478 21.9821 19.687 21.997 19.9414C22.012 20.1958 21.9292 20.4464 21.7656 20.6418C21.602 20.8373 21.37 20.9629 21.1169 20.993L20.9999 21H2.99987C2.74499 20.9997 2.49984 20.9021 2.3145 20.7272C2.12916 20.5522 2.01763 20.313 2.0027 20.0586C1.98776 19.8042 2.07054 19.5536 2.23413 19.3582C2.39772 19.1627 2.62977 19.0371 2.88287 19.007L2.99987 19H20.9999ZM18.9999 4C19.5044 3.99984 19.9904 4.19041 20.3604 4.5335C20.7304 4.87659 20.957 5.34684 20.9949 5.85L20.9999 6V16C21 16.5046 20.8095 16.9906 20.4664 17.3605C20.1233 17.7305 19.653 17.9572 19.1499 17.995L18.9999 18H4.99987C4.49529 18.0002 4.0093 17.8096 3.63932 17.4665C3.26934 17.1234 3.04271 16.6532 3.00487 16.15L2.99987 16V6C2.99971 5.49542 3.19028 5.00943 3.53337 4.63945C3.87646 4.26947 4.34671 4.04284 4.84987 4.005L4.99987 4H18.9999Z\"\n fill=\"white\"\n />\n </svg>\n )}\n </span>\n </button>\n );\n});\n\nScreenShareButton.displayName = 'ScreenShareButton';\n",
|
|
471
|
-
styles: "/* SelectDevice styles */\n.selectDevice {\n height: 3rem;\n width: 5.5rem;\n border-radius: 9999px;\n background-color: rgba(255, 255, 255, 0.2);\n padding: 0 0.75rem;\n border: 1px solid rgba(255, 255, 255, 0.2);\n backdrop-filter: blur(10px);\n color: transparent;\n padding-right: 2rem; /* space for arrow */\n box-sizing: border-box;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n cursor: pointer;\n}\n\n.selectDevice::-ms-expand {\n display: none;\n}\n\n.selectDevice option {\n color: transparent;\n background-color: transparent;\n}\n\n.selectDevice:focus {\n outline: none;\n}\n\n/* Device button container styles */\n.deviceButtonContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n}\n\n/* Device button styles */\n.deviceButton {\n position: absolute;\n left: 0;\n top: 0;\n z-index: 10;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n cursor: pointer;\n}\n\n.deviceButtonIcon {\n display: flex;\n}\n\n.deviceButton:disabled {\n opacity: 0.5;\n}\n\n/* Screen reader only text */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n/* Controls container */\n.controlsContainer {\n display: flex;\n gap: 1rem;\n justify-content: center;\n align-items: center;\n}\n\n.selectDeviceContainer {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.selectArrow {\n position: absolute;\n right: 1rem;\n pointer-events: none;\n display: flex;\n align-items: center;\n height: 100%;\n}\n\n.screenShareButton {\n cursor: pointer;\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n}\n",
|
|
578
|
+
content: "import React, { memo } from 'react';\nimport { useDevices } from '@daily-co/daily-react';\nimport { useLocalCamera } from '../../hooks/use-local-camera';\nimport { useLocalMicrophone } from '../../hooks/use-local-microphone';\nimport { useLocalScreenshare } from '../../hooks/use-local-screenshare';\nimport styles from './device-select.module.css';\n\nexport const SelectDevice = ({ value, devices, disabled, onChange }) => {\n return (\n <div className={styles.selectDeviceContainer}>\n <select\n value={value}\n onChange={(e) => onChange(e.target.value)}\n disabled={disabled}\n className={styles.selectDevice}\n >\n {devices.map(({ device }) => (\n <option key={device.deviceId} value={device.deviceId}>\n {device.label}\n </option>\n ))}\n </select>\n <span className={styles.selectArrow}>\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M4 6L8 10L12 6\"\n stroke=\"#fff\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </span>\n </div>\n );\n};\n\nexport const MicSelectBtn = memo(() => {\n const { onToggleMicrophone, isMicReady, isMicMuted } = useLocalMicrophone();\n const { microphones, currentMic, setMicrophone } = useDevices();\n\n return (\n <div className={styles.deviceButtonContainer}>\n <button\n type=\"button\"\n onClick={onToggleMicrophone}\n disabled={!isMicReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isMicMuted || !isMicReady ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone Muted\"\n >\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 18.1339 14.0992 18.1339 14.0992M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"1.66667\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n\n <rect\n x=\"1.30225\"\n y=\"3\"\n width=\"26\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 1.30225 3)\"\n fill=\"#020617\"\n />\n\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M9 9.39031V11C9 11.7956 9.31607 12.5587 9.87868 13.1213C10.4413 13.6839 11.2044 14 12 14C12.7956 14 13.5587 13.6839 14.1213 13.1213C14.2829 12.9597 14.4242 12.7816 14.5435 12.5908L9 9.39031ZM15 7.71466V6C15 5.20435 14.6839 4.44129 14.1213 3.87868C13.5587 3.31607 12.7956 3 12 3C11.2044 3 10.4413 3.31607 9.87868 3.87868C9.69528 4.06208 9.53807 4.26678 9.40948 4.48697L15 7.71466Z\"\n fill=\"#020617\"\n />\n\n <path\n d=\"M9 9.39031L9.41667 8.66862C9.15883 8.51976 8.84117 8.51976 8.58333 8.66862C8.3255 8.81748 8.16667 9.09259 8.16667 9.39031H9ZM9.87868 13.1213L9.28942 13.7106H9.28942L9.87868 13.1213ZM14.1213 13.1213L14.7106 13.7106L14.7106 13.7106L14.1213 13.1213ZM14.5435 12.5908L15.25 13.0327C15.3699 12.841 15.4068 12.6088 15.3521 12.3894C15.2974 12.17 15.156 11.9822 14.9601 11.8692L14.5435 12.5908ZM15 7.71466L14.5833 8.43635C14.8412 8.58521 15.1588 8.58521 15.4167 8.43635C15.6745 8.28749 15.8333 8.01238 15.8333 7.71466H15ZM14.1213 3.87868L14.7106 3.28942L14.7106 3.28942L14.1213 3.87868ZM9.87868 3.87868L9.28942 3.28942L9.28942 3.28942L9.87868 3.87868ZM9.40948 4.48697L8.68988 4.06671C8.57806 4.25818 8.54715 4.48633 8.604 4.70065C8.66086 4.91497 8.80078 5.09779 8.99281 5.20866L9.40948 4.48697ZM9.83333 11V9.39031H8.16667V11H9.83333ZM10.4679 12.5321C10.0616 12.1257 9.83333 11.5746 9.83333 11H8.16667C8.16667 12.0167 8.57053 12.9917 9.28942 13.7106L10.4679 12.5321ZM12 13.1667C11.4254 13.1667 10.8743 12.9384 10.4679 12.5321L9.28942 13.7106C10.0083 14.4295 10.9833 14.8333 12 14.8333V13.1667ZM13.5321 12.5321C13.1257 12.9384 12.5746 13.1667 12 13.1667V14.8333C13.0167 14.8333 13.9917 14.4295 14.7106 13.7106L13.5321 12.5321ZM13.837 12.149C13.7508 12.2867 13.6488 12.4153 13.5321 12.5321L14.7106 13.7106C14.917 13.5041 15.0976 13.2764 15.25 13.0327L13.837 12.149ZM14.9601 11.8692L9.41667 8.66862L8.58333 10.112L14.1268 13.3125L14.9601 11.8692ZM14.1667 6V7.71466H15.8333V6H14.1667ZM13.5321 4.46794C13.9384 4.87426 14.1667 5.42536 14.1667 6H15.8333C15.8333 4.98334 15.4295 4.00831 14.7106 3.28942L13.5321 4.46794ZM12 3.83333C12.5746 3.83333 13.1257 4.06161 13.5321 4.46794L14.7106 3.28942C13.9917 2.57053 13.0167 2.16667 12 2.16667V3.83333ZM10.4679 4.46794C10.8743 4.06161 11.4254 3.83333 12 3.83333V2.16667C10.9833 2.16667 10.0083 2.57053 9.28942 3.28942L10.4679 4.46794ZM10.1291 4.90724C10.2219 4.74824 10.3354 4.60042 10.4679 4.46794L9.28942 3.28942C9.05511 3.52374 8.85421 3.78533 8.68988 4.06671L10.1291 4.90724ZM8.99281 5.20866L14.5833 8.43635L15.4167 6.99298L9.82615 3.76529L8.99281 5.20866Z\"\n fill=\"#020617\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone\"\n >\n <path\n d=\"M9 6C9 5.20435 9.31607 4.44129 9.87868 3.87868C10.4413 3.31607 11.2044 3 12 3C12.7956 3 13.5587 3.31607 14.1213 3.87868C14.6839 4.44129 15 5.20435 15 6V11C15 11.7956 14.6839 12.5587 14.1213 13.1213C13.5587 13.6839 12.7956 14 12 14C11.2044 14 10.4413 13.6839 9.87868 13.1213C9.31607 12.5587 9 11.7956 9 11V6Z\"\n fill=\"#020617\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 19 12.8565 19 11V10M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Microphone</span>\n </button>\n <SelectDevice\n value={currentMic?.device?.deviceId}\n devices={microphones || []}\n disabled={!isMicReady}\n onChange={(val) => setMicrophone(val)}\n />\n </div>\n );\n});\n\nMicSelectBtn.displayName = 'MicSelectBtn';\n\nexport const CameraSelectBtn = memo(() => {\n const { onToggleCamera, isCamReady, isCamMuted } = useLocalCamera();\n const { currentCam, cameras, setCamera } = useDevices();\n\n return (\n <div className={styles.deviceButtonContainer}>\n <button\n type=\"button\"\n onClick={onToggleCamera}\n disabled={!isCamReady || !currentCam}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isCamMuted ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera Muted\"\n >\n <g clipPath=\"url(#clip0_7082_14220)\">\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M3.19874 5.60093C3.08628 5.68537 2.97928 5.77808 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V15.048C17.7787 14.8204 17.5304 14.6189 17.2595 14.4485L3.19874 5.60093ZM22 12.655V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H8.7412L22 12.655Z\"\n fill=\"#020617\"\n />\n\n <rect\n x=\"0.777222\"\n y=\"2.64844\"\n width=\"26.7988\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 0.777222 2.64844)\"\n fill=\"#020617\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_7082_14220\">\n <rect width=\"24\" height=\"24\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera\"\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M5 5C4.20435 5 3.44129 5.31607 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V14.414L20.293 16.707C20.4329 16.8468 20.611 16.942 20.805 16.9806C20.9989 17.0192 21.2 16.9993 21.3827 16.9237C21.5654 16.848 21.7215 16.7199 21.8314 16.5555C21.9413 16.391 22 16.1978 22 16V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H5Z\"\n fill=\"#020617\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Camera</span>\n </button>\n <SelectDevice\n value={currentCam?.device?.deviceId}\n devices={cameras || []}\n disabled={!isCamReady}\n onChange={(val) => setCamera(val)}\n />\n </div>\n );\n});\n\nCameraSelectBtn.displayName = 'CameraSelectBtn';\n\nexport const ScreenShareButton = memo(() => {\n const { onToggleScreenshare, isScreenSharing } = useLocalScreenshare();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleScreenshare}\n className={`${styles.deviceButtonContainer} ${styles.screenShareButton}`}\n >\n <span>\n {isScreenSharing ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M21.0008 19C21.2557 19.0003 21.5009 19.0979 21.6862 19.2728C21.8715 19.4478 21.9831 19.687 21.998 19.9414C22.013 20.1958 21.9302 20.4464 21.7666 20.6418C21.603 20.8373 21.3709 20.9629 21.1178 20.993L21.0008 21H3.00085C2.74597 20.9997 2.50081 20.9021 2.31548 20.7272C2.13014 20.5522 2.01861 20.313 2.00367 20.0586C1.98874 19.8042 2.07152 19.5536 2.23511 19.3582C2.3987 19.1627 2.63075 19.0371 2.88385 19.007L3.00085 19H21.0008ZM19.0008 4C19.5054 3.99984 19.9914 4.19041 20.3614 4.5335C20.7314 4.87659 20.958 5.34684 20.9958 5.85L21.0008 6V16C21.001 16.5046 20.8104 16.9906 20.4673 17.3605C20.1243 17.7305 19.654 17.9572 19.1508 17.995L19.0008 18H5.00085C4.49627 18.0002 4.01028 17.8096 3.6403 17.4665C3.27032 17.1234 3.04369 16.6532 3.00585 16.15L3.00085 16V6C3.00069 5.49542 3.19125 5.00943 3.53434 4.63945C3.87743 4.26947 4.34769 4.04284 4.85085 4.005L5.00085 4H19.0008Z\"\n fill=\"#2D65FF\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M20.9999 19C21.2547 19.0003 21.4999 19.0979 21.6852 19.2728C21.8706 19.4478 21.9821 19.687 21.997 19.9414C22.012 20.1958 21.9292 20.4464 21.7656 20.6418C21.602 20.8373 21.37 20.9629 21.1169 20.993L20.9999 21H2.99987C2.74499 20.9997 2.49984 20.9021 2.3145 20.7272C2.12916 20.5522 2.01763 20.313 2.0027 20.0586C1.98776 19.8042 2.07054 19.5536 2.23413 19.3582C2.39772 19.1627 2.62977 19.0371 2.88287 19.007L2.99987 19H20.9999ZM18.9999 4C19.5044 3.99984 19.9904 4.19041 20.3604 4.5335C20.7304 4.87659 20.957 5.34684 20.9949 5.85L20.9999 6V16C21 16.5046 20.8095 16.9906 20.4664 17.3605C20.1233 17.7305 19.653 17.9572 19.1499 17.995L18.9999 18H4.99987C4.49529 18.0002 4.0093 17.8096 3.63932 17.4665C3.26934 17.1234 3.04271 16.6532 3.00487 16.15L2.99987 16V6C2.99971 5.49542 3.19028 5.00943 3.53337 4.63945C3.87646 4.26947 4.34671 4.04284 4.84987 4.005L4.99987 4H18.9999Z\"\n fill=\"currentColor\"\n />\n </svg>\n )}\n </span>\n </button>\n );\n});\n\nScreenShareButton.displayName = 'ScreenShareButton';\n",
|
|
579
|
+
styles: "/* SelectDevice styles */\n.selectDevice {\n height: 3rem;\n width: 5.5rem;\n border-radius: 9999px;\n background-color: rgba(255, 255, 255, 0.2);\n padding: 0 0.75rem;\n border: 1px solid rgba(255, 255, 255, 0.2);\n backdrop-filter: blur(10px);\n color: transparent;\n padding-right: 2rem; /* space for arrow */\n box-sizing: border-box;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n cursor: pointer;\n}\n\n.selectDevice::-ms-expand {\n display: none;\n}\n\n.selectDevice option {\n color: transparent;\n background-color: transparent;\n}\n\n.selectDevice:focus {\n outline: none;\n}\n\n/* Device button container styles */\n.deviceButtonContainer {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n}\n\n/* Device button styles */\n.deviceButton {\n position: absolute;\n left: 0;\n top: 0;\n z-index: 10;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n cursor: pointer;\n}\n\n.deviceButtonIcon {\n display: flex;\n}\n\n.deviceButton:disabled {\n opacity: 0.5;\n}\n\n/* Screen reader only text */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n/* Controls container */\n.controlsContainer {\n display: flex;\n gap: 1rem;\n justify-content: center;\n align-items: center;\n}\n\n.selectDeviceContainer {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.selectArrow {\n position: absolute;\n right: 1rem;\n pointer-events: none;\n display: flex;\n align-items: center;\n height: 100%;\n}\n\n.screenShareButton {\n cursor: pointer;\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n color: #000;\n}\n\n@container conversation (max-width: 400px) {\n .selectDevice {\n height: 2.5rem;\n width: 4.5rem;\n padding-right: 1.5rem;\n }\n .deviceButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n .screenShareButton {\n height: 2.5rem;\n width: 2.5rem;\n }\n .selectArrow {\n right: 0.625rem;\n }\n}\n",
|
|
472
580
|
componentsDependencies: [
|
|
473
581
|
"use-local-camera",
|
|
474
582
|
"use-local-microphone",
|
|
@@ -479,8 +587,8 @@ var device_select_default = {
|
|
|
479
587
|
//#region src/templates/jsx/components/hair-check-01.json
|
|
480
588
|
var hair_check_01_default = {
|
|
481
589
|
type: "components",
|
|
482
|
-
content: "import React, { memo, useEffect } from 'react';\nimport { DailyVideo, useDaily } from '@daily-co/daily-react';\nimport { CameraSelectBtn, MicSelectBtn } from '../device-select';\nimport { useStartHaircheck } from '../../hooks/use-start-haircheck';\nimport { useLocalCamera } from '../../hooks/use-local-camera';\n\nimport styles from './hair-check.module.css';\n\nconst JoinBtn = ({ onClick, disabled, className, loading }) => {\n return (\n <button\n className={`${styles.buttonJoin} ${className || ''}`}\n type=\"button\"\n onClick={onClick}\n disabled={disabled || loading}\n >\n <div className={styles.buttonJoinInner}>{loading ? 'Joining...' : 'Join Video Chat'}</div>\n </button>\n );\n};\n\nexport const HairCheck = memo(({ isJoinBtnLoading = false, onJoin, onCancel }) => {\n const daily = useDaily();\n const { localSessionId, isCamMuted } = useLocalCamera();\n\n const {\n isPermissionsPrompt,\n isPermissionsLoading,\n isPermissionsGranted,\n isPermissionsDenied,\n requestPermissions,\n } = useStartHaircheck();\n\n useEffect(() => {\n requestPermissions();\n }, []);\n\n const onCancelHairCheck = () => {\n if (daily) {\n daily.leave();\n }\n onCancel?.();\n };\n\n const getDescription = () => {\n if (isPermissionsPrompt) {\n return 'Make sure your camera and mic are ready!';\n }\n if (isPermissionsLoading) {\n return 'Getting your camera and mic ready...';\n }\n if (isPermissionsDenied) {\n return 'Camera and mic access denied. Allow permissions to continue.';\n }\n return \"You're all set! Your device is ready.\";\n };\n return (\n <div className={styles.
|
|
483
|
-
styles: "/* Button Component */\n/* Start of Selection */\n.buttonCancel {\n padding: 1rem;\n border: 1px solid rgba(255, 255, 255, 0.1);\n background-color: rgba(239, 68, 68, 0.8);\n border-radius: 9999px;\n color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n outline: none;\n}\n\n.buttonCancel:hover {\n background-color: rgba(239, 68, 68, 1);\n}\n/* End of Selection */\n\n/* ButtonJoin Component */\n.buttonJoin {\n padding: 5px;\n border: 1px solid rgba(255, 255, 255, 0.1);\n background-color: rgba(255, 255, 255, 0.1);\n border-radius: 9999px;\n color: white;\n height: 3.625rem;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 500;\n cursor: pointer;\n}\n\n.buttonJoinInner {\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: #32c75c;\n border-radius: 9999px;\n padding: 1rem;\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n height: 3rem;\n transition: background-color 0.2s;\n font-weight: 500;\n min-width: 8.5rem;\n}\n\n.buttonJoin:hover .buttonJoinInner {\n background-color: rgba(62, 192, 97, 0.9);\n}\n\n.buttonJoin:disabled .buttonJoinInner {\n background-color: rgba(34, 197, 94, 0.5);\n cursor: not-allowed;\n}\n\n.buttonJoinDesktop {\n display: none;\n}\n\n@
|
|
590
|
+
content: "import React, { memo, useEffect } from 'react';\nimport { DailyVideo, useDaily } from '@daily-co/daily-react';\nimport { CameraSelectBtn, MicSelectBtn } from '../device-select';\nimport { useStartHaircheck } from '../../hooks/use-start-haircheck';\nimport { useLocalCamera } from '../../hooks/use-local-camera';\n\nimport styles from './hair-check.module.css';\n\nconst JoinBtn = ({ onClick, disabled, className, loading }) => {\n return (\n <button\n className={`${styles.buttonJoin} ${className || ''}`}\n type=\"button\"\n onClick={onClick}\n disabled={disabled || loading}\n >\n <div className={styles.buttonJoinInner}>{loading ? 'Joining...' : 'Join Video Chat'}</div>\n </button>\n );\n};\n\nexport const HairCheck = memo(({ isJoinBtnLoading = false, onJoin, onCancel }) => {\n const daily = useDaily();\n const { localSessionId, isCamMuted } = useLocalCamera();\n\n const {\n isPermissionsPrompt,\n isPermissionsLoading,\n isPermissionsGranted,\n isPermissionsDenied,\n requestPermissions,\n } = useStartHaircheck();\n\n useEffect(() => {\n requestPermissions();\n }, []);\n\n const onCancelHairCheck = () => {\n if (daily) {\n daily.leave();\n }\n onCancel?.();\n };\n\n const getDescription = () => {\n if (isPermissionsPrompt) {\n return 'Make sure your camera and mic are ready!';\n }\n if (isPermissionsLoading) {\n return 'Getting your camera and mic ready...';\n }\n if (isPermissionsDenied) {\n return 'Camera and mic access denied. Allow permissions to continue.';\n }\n return \"You're all set! Your device is ready.\";\n };\n return (\n <div className={styles.haircheckBlockWrapper}>\n <div className={styles.haircheckBlock}>\n {isPermissionsGranted && !isCamMuted ? (\n <DailyVideo\n type=\"video\"\n sessionId={localSessionId}\n mirror\n className={styles.dailyVideo}\n />\n ) : (\n <div className={styles.haircheckUserPlaceholder}>\n <span className={styles.haircheckUserIcon}>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"88\"\n height=\"89\"\n viewBox=\"0 0 88 89\"\n fill=\"none\"\n aria-label=\"Haircheck User\"\n role=\"img\"\n >\n <path\n d=\"M44 48.6406C17.952 48.6406 8.80005 61.8406 8.80005 70.6406V83.8406H79.2001V70.6406C79.2001 61.8406 70.0481 48.6406 44 48.6406Z\"\n fill=\"url(#paint0_linear_7135_21737)\"\n />\n\n <path\n d=\"M44 44.2406C54.9352 44.2406 63.7999 35.3759 63.7999 24.4406C63.7999 13.5054 54.9352 4.64062 44 4.64062C33.0647 4.64062 24.2 13.5054 24.2 24.4406C24.2 35.3759 33.0647 44.2406 44 44.2406Z\"\n fill=\"url(#paint1_linear_7135_21737)\"\n />\n\n <defs>\n <linearGradient\n id=\"paint0_linear_7135_21737\"\n x1=\"36.5001\"\n y1=\"43\"\n x2=\"44.0001\"\n y2=\"97.5\"\n gradientUnits=\"userSpaceOnUse\"\n >\n <stop stopColor=\"white\" />\n <stop offset=\"1\" stopColor=\"white\" stopOpacity=\"0\" />\n </linearGradient>\n <linearGradient\n id=\"paint1_linear_7135_21737\"\n x1=\"44\"\n y1=\"4.64062\"\n x2=\"44\"\n y2=\"44.2406\"\n gradientUnits=\"userSpaceOnUse\"\n >\n <stop stopColor=\"white\" />\n <stop offset=\"1\" stopColor=\"white\" stopOpacity=\"0\" />\n </linearGradient>\n </defs>\n </svg>\n </span>\n </div>\n )}\n\n <div className={styles.haircheckSidebar}>\n <div className={styles.haircheckSidebarContent}>\n {isPermissionsDenied ? (\n <button\n type=\"button\"\n onClick={onCancelHairCheck}\n className={`${styles.buttonCancel} ${styles.buttonJoinMobile}`}\n >\n Cancel\n </button>\n ) : (\n <JoinBtn\n loading={isJoinBtnLoading}\n disabled={!isPermissionsGranted}\n className={styles.buttonJoinMobile}\n onClick={onJoin}\n />\n )}\n <div />\n <div className={styles.haircheckContent}>\n <div className={styles.haircheckDescription}>{getDescription()}</div>\n {isPermissionsDenied ? (\n <button\n type=\"button\"\n onClick={onCancelHairCheck}\n className={`${styles.buttonCancel} ${styles.buttonJoinDesktop}`}\n >\n Cancel\n </button>\n ) : (\n <JoinBtn\n loading={isJoinBtnLoading}\n disabled={!isPermissionsGranted}\n className={styles.buttonJoinDesktop}\n onClick={onJoin}\n />\n )}\n </div>\n <div className={styles.haircheckControls}>\n <MicSelectBtn />\n <CameraSelectBtn />\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n});\n\nHairCheck.displayName = 'HairCheck';\n",
|
|
591
|
+
styles: "/* Button Component */\n/* Start of Selection */\n.buttonCancel {\n padding: 1rem;\n border: 1px solid rgba(255, 255, 255, 0.1);\n background-color: rgba(239, 68, 68, 0.8);\n border-radius: 9999px;\n color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n outline: none;\n}\n\n.buttonCancel:hover {\n background-color: rgba(239, 68, 68, 1);\n}\n/* End of Selection */\n\n/* ButtonJoin Component */\n.buttonJoin {\n padding: 5px;\n border: 1px solid rgba(255, 255, 255, 0.1);\n background-color: rgba(255, 255, 255, 0.1);\n border-radius: 9999px;\n color: white;\n height: 3.625rem;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 500;\n cursor: pointer;\n}\n\n.buttonJoinInner {\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: #32c75c;\n border-radius: 9999px;\n padding: 1rem;\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n height: 3rem;\n transition: background-color 0.2s;\n font-weight: 500;\n min-width: 8.5rem;\n}\n\n.buttonJoin:hover .buttonJoinInner {\n background-color: rgba(62, 192, 97, 0.9);\n}\n\n.buttonJoin:disabled .buttonJoinInner {\n background-color: rgba(34, 197, 94, 0.5);\n cursor: not-allowed;\n}\n\n.buttonJoinDesktop {\n display: none;\n}\n\n@container haircheck (min-width: 768px) {\n .buttonJoinDesktop {\n display: flex;\n }\n}\n\n.buttonJoinMobile {\n position: absolute;\n top: -5.125rem;\n left: 50%;\n transform: translateX(-50%);\n z-index: 20;\n}\n\n@container haircheck (min-width: 768px) {\n .buttonJoinMobile {\n display: none;\n }\n}\n\n/* HaircheckScreen Component */\n.haircheckBlockWrapper {\n width: 100%;\n container-type: inline-size;\n container-name: haircheck;\n}\n\n.haircheckBlock {\n position: relative;\n width: 100%;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n aspect-ratio: 9/16;\n overflow: hidden;\n border-radius: 0.5rem;\n max-height: 90vh;\n}\n\n/* Aspect ratio tracks the wrapper's actual width: portrait when narrow,\n landscape when wide. */\n@container haircheck (min-width: 768px) {\n .haircheckBlock {\n aspect-ratio: 16/9;\n }\n}\n\n.dailyVideo {\n width: 100%;\n height: 100%;\n object-fit: cover !important;\n position: absolute;\n inset: 0;\n transition: opacity 0.3s ease-in-out;\n}\n\n.haircheckUserPlaceholder {\n position: absolute;\n inset: 0;\n background: linear-gradient(135deg, #4b5563 0%, #1f2937 100%);\n z-index: 5;\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.haircheckUserIcon {\n width: 12.5rem;\n height: 12.5rem;\n border-radius: 9999px;\n background-color: rgba(255, 255, 255, 0.1);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.haircheckSidebar {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n background-color: rgba(2, 6, 23, 0.45);\n backdrop-filter: blur(20px);\n border-left: 1px solid rgba(255, 255, 255, 0.2);\n z-index: 5;\n}\n\n@container haircheck (min-width: 768px) {\n .haircheckSidebar {\n left: auto;\n top: 0;\n bottom: 0;\n width: 17.5rem;\n }\n}\n\n@container haircheck (min-width: 1024px) {\n .haircheckSidebar {\n width: 23rem;\n }\n}\n\n.haircheckSidebarContent {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n padding: 1.5rem 1.25rem;\n gap: 1.5rem;\n width: 100%;\n height: 100%;\n}\n\n@container haircheck (min-width: 768px) {\n .haircheckSidebarContent {\n padding: 1rem 1.25rem;\n }\n}\n\n@container haircheck (min-width: 1024px) {\n .haircheckSidebarContent {\n padding: 2rem 1.25rem;\n }\n}\n\n.haircheckContent {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 2rem;\n}\n\n.haircheckDescription {\n color: #ffffff;\n font-size: 1.125rem;\n font-weight: 500;\n}\n\n@container haircheck (min-width: 768px) {\n .haircheckDescription {\n font-size: 1.25rem;\n }\n}\n\n.haircheckControls {\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n gap: 1rem;\n}\n",
|
|
484
592
|
componentsDependencies: [
|
|
485
593
|
"device-select",
|
|
486
594
|
"use-start-haircheck",
|
|
@@ -493,7 +601,7 @@ var hair_check_01_default = {
|
|
|
493
601
|
var media_controls_default = {
|
|
494
602
|
type: "components",
|
|
495
603
|
content: "import React, { memo } from 'react';\nimport { useLocalCamera } from '../../hooks/use-local-camera';\nimport { useLocalMicrophone } from '../../hooks/use-local-microphone';\nimport { useLocalScreenshare } from '../../hooks/use-local-screenshare';\n\nimport styles from './media-controls.module.css';\n\nexport const MicToggleButton = memo(() => {\n const { onToggleMicrophone, isMicReady, isMicMuted } = useLocalMicrophone();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleMicrophone}\n disabled={!isMicReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isMicMuted || !isMicReady ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone Muted\"\n >\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 18.1339 14.0992 18.1339 14.0992M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"1.66667\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n\n <rect\n x=\"1.30225\"\n y=\"3\"\n width=\"26\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 1.30225 3)\"\n fill=\"#020617\"\n />\n\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M9 9.39031V11C9 11.7956 9.31607 12.5587 9.87868 13.1213C10.4413 13.6839 11.2044 14 12 14C12.7956 14 13.5587 13.6839 14.1213 13.1213C14.2829 12.9597 14.4242 12.7816 14.5435 12.5908L9 9.39031ZM15 7.71466V6C15 5.20435 14.6839 4.44129 14.1213 3.87868C13.5587 3.31607 12.7956 3 12 3C11.2044 3 10.4413 3.31607 9.87868 3.87868C9.69528 4.06208 9.53807 4.26678 9.40948 4.48697L15 7.71466Z\"\n fill=\"#020617\"\n />\n\n <path\n d=\"M9 9.39031L9.41667 8.66862C9.15883 8.51976 8.84117 8.51976 8.58333 8.66862C8.3255 8.81748 8.16667 9.09259 8.16667 9.39031H9ZM9.87868 13.1213L9.28942 13.7106H9.28942L9.87868 13.1213ZM14.1213 13.1213L14.7106 13.7106L14.7106 13.7106L14.1213 13.1213ZM14.5435 12.5908L15.25 13.0327C15.3699 12.841 15.4068 12.6088 15.3521 12.3894C15.2974 12.17 15.156 11.9822 14.9601 11.8692L14.5435 12.5908ZM15 7.71466L14.5833 8.43635C14.8412 8.58521 15.1588 8.58521 15.4167 8.43635C15.6745 8.28749 15.8333 8.01238 15.8333 7.71466H15ZM14.1213 3.87868L14.7106 3.28942L14.7106 3.28942L14.1213 3.87868ZM9.87868 3.87868L9.28942 3.28942L9.28942 3.28942L9.87868 3.87868ZM9.40948 4.48697L8.68988 4.06671C8.57806 4.25818 8.54715 4.48633 8.604 4.70065C8.66086 4.91497 8.80078 5.09779 8.99281 5.20866L9.40948 4.48697ZM9.83333 11V9.39031H8.16667V11H9.83333ZM10.4679 12.5321C10.0616 12.1257 9.83333 11.5746 9.83333 11H8.16667C8.16667 12.0167 8.57053 12.9917 9.28942 13.7106L10.4679 12.5321ZM12 13.1667C11.4254 13.1667 10.8743 12.9384 10.4679 12.5321L9.28942 13.7106C10.0083 14.4295 10.9833 14.8333 12 14.8333V13.1667ZM13.5321 12.5321C13.1257 12.9384 12.5746 13.1667 12 13.1667V14.8333C13.0167 14.8333 13.9917 14.4295 14.7106 13.7106L13.5321 12.5321ZM13.837 12.149C13.7508 12.2867 13.6488 12.4153 13.5321 12.5321L14.7106 13.7106C14.917 13.5041 15.0976 13.2764 15.25 13.0327L13.837 12.149ZM14.9601 11.8692L9.41667 8.66862L8.58333 10.112L14.1268 13.3125L14.9601 11.8692ZM14.1667 6V7.71466H15.8333V6H14.1667ZM13.5321 4.46794C13.9384 4.87426 14.1667 5.42536 14.1667 6H15.8333C15.8333 4.98334 15.4295 4.00831 14.7106 3.28942L13.5321 4.46794ZM12 3.83333C12.5746 3.83333 13.1257 4.06161 13.5321 4.46794L14.7106 3.28942C13.9917 2.57053 13.0167 2.16667 12 2.16667V3.83333ZM10.4679 4.46794C10.8743 4.06161 11.4254 3.83333 12 3.83333V2.16667C10.9833 2.16667 10.0083 2.57053 9.28942 3.28942L10.4679 4.46794ZM10.1291 4.90724C10.2219 4.74824 10.3354 4.60042 10.4679 4.46794L9.28942 3.28942C9.05511 3.52374 8.85421 3.78533 8.68988 4.06671L10.1291 4.90724ZM8.99281 5.20866L14.5833 8.43635L15.4167 6.99298L9.82615 3.76529L8.99281 5.20866Z\"\n fill=\"#020617\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Microphone\"\n >\n <path\n d=\"M9 6C9 5.20435 9.31607 4.44129 9.87868 3.87868C10.4413 3.31607 11.2044 3 12 3C12.7956 3 13.5587 3.31607 14.1213 3.87868C14.6839 4.44129 15 5.20435 15 6V11C15 11.7956 14.6839 12.5587 14.1213 13.1213C13.5587 13.6839 12.7956 14 12 14C11.2044 14 10.4413 13.6839 9.87868 13.1213C9.31607 12.5587 9 11.7956 9 11V6Z\"\n fill=\"#020617\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n\n <path\n d=\"M12 18C10.1435 18 8.36301 17.2625 7.05025 15.9497C5.7375 14.637 5 12.8565 5 11V10M12 18C13.8565 18 15.637 17.2625 16.9497 15.9497C18.2625 14.637 19 12.8565 19 11V10M12 18V21\"\n stroke=\"#020617\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Microphone</span>\n </button>\n );\n});\n\nMicToggleButton.displayName = 'MicToggleButton';\n\nexport const CameraToggleButton = memo(() => {\n const { onToggleCamera, isCamReady, isCamMuted } = useLocalCamera();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleCamera}\n disabled={!isCamReady}\n className={styles.deviceButton}\n >\n <span className={styles.deviceButtonIcon}>\n {isCamMuted ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera Muted\"\n >\n <g clipPath=\"url(#clip0_7082_14220)\">\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M3.19874 5.60093C3.08628 5.68537 2.97928 5.77808 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V15.048C17.7787 14.8204 17.5304 14.6189 17.2595 14.4485L3.19874 5.60093ZM22 12.655V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H8.7412L22 12.655Z\"\n fill=\"#020617\"\n />\n\n <rect\n x=\"0.777222\"\n y=\"2.64844\"\n width=\"26.7988\"\n height=\"2.24738\"\n rx=\"1.12369\"\n transform=\"rotate(30 0.777222 2.64844)\"\n fill=\"#020617\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_7082_14220\">\n <rect width=\"24\" height=\"24\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Camera\"\n >\n <path\n fillRule=\"evenodd\"\n clipRule=\"evenodd\"\n d=\"M5 5C4.20435 5 3.44129 5.31607 2.87868 5.87868C2.31607 6.44129 2 7.20435 2 8V16C2 16.7956 2.31607 17.5587 2.87868 18.1213C3.44129 18.6839 4.20435 19 5 19H15C15.7956 19 16.5587 18.6839 17.1213 18.1213C17.6839 17.5587 18 16.7956 18 16V14.414L20.293 16.707C20.4329 16.8468 20.611 16.942 20.805 16.9806C20.9989 17.0192 21.2 16.9993 21.3827 16.9237C21.5654 16.848 21.7215 16.7199 21.8314 16.5555C21.9413 16.391 22 16.1978 22 16V8C22 7.80225 21.9413 7.60895 21.8314 7.44454C21.7215 7.28013 21.5654 7.15199 21.3827 7.07632C21.2 7.00065 20.9989 6.98085 20.805 7.01942C20.611 7.05798 20.4329 7.15319 20.293 7.293L18 9.586V8C18 7.20435 17.6839 6.44129 17.1213 5.87868C16.5587 5.31607 15.7956 5 15 5H5Z\"\n fill=\"#020617\"\n />\n </svg>\n )}\n </span>\n <span className={styles.srOnly}>Camera</span>\n </button>\n );\n});\n\nCameraToggleButton.displayName = 'CameraToggleButton';\n\nexport const ScreenShareButton = memo(() => {\n const { onToggleScreenshare, isScreenSharing } = useLocalScreenshare();\n\n return (\n <button\n type=\"button\"\n onClick={onToggleScreenshare}\n className={`${styles.deviceButtonContainer} ${styles.screenShareButton}`}\n >\n <span>\n {isScreenSharing ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M21.0008 19C21.2557 19.0003 21.5009 19.0979 21.6862 19.2728C21.8715 19.4478 21.9831 19.687 21.998 19.9414C22.013 20.1958 21.9302 20.4464 21.7666 20.6418C21.603 20.8373 21.3709 20.9629 21.1178 20.993L21.0008 21H3.00085C2.74597 20.9997 2.50081 20.9021 2.31548 20.7272C2.13014 20.5522 2.01861 20.313 2.00367 20.0586C1.98874 19.8042 2.07152 19.5536 2.23511 19.3582C2.3987 19.1627 2.63075 19.0371 2.88385 19.007L3.00085 19H21.0008ZM19.0008 4C19.5054 3.99984 19.9914 4.19041 20.3614 4.5335C20.7314 4.87659 20.958 5.34684 20.9958 5.85L21.0008 6V16C21.001 16.5046 20.8104 16.9906 20.4673 17.3605C20.1243 17.7305 19.654 17.9572 19.1508 17.995L19.0008 18H5.00085C4.49627 18.0002 4.01028 17.8096 3.6403 17.4665C3.27032 17.1234 3.04369 16.6532 3.00585 16.15L3.00085 16V6C3.00069 5.49542 3.19125 5.00943 3.53434 4.63945C3.87743 4.26947 4.34769 4.04284 4.85085 4.005L5.00085 4H19.0008Z\"\n fill=\"#2D65FF\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Screen Share\"\n >\n <path\n d=\"M20.9999 19C21.2547 19.0003 21.4999 19.0979 21.6852 19.2728C21.8706 19.4478 21.9821 19.687 21.997 19.9414C22.012 20.1958 21.9292 20.4464 21.7656 20.6418C21.602 20.8373 21.37 20.9629 21.1169 20.993L20.9999 21H2.99987C2.74499 20.9997 2.49984 20.9021 2.3145 20.7272C2.12916 20.5522 2.01763 20.313 2.0027 20.0586C1.98776 19.8042 2.07054 19.5536 2.23413 19.3582C2.39772 19.1627 2.62977 19.0371 2.88287 19.007L2.99987 19H20.9999ZM18.9999 4C19.5044 3.99984 19.9904 4.19041 20.3604 4.5335C20.7304 4.87659 20.957 5.34684 20.9949 5.85L20.9999 6V16C21 16.5046 20.8095 16.9906 20.4664 17.3605C20.1233 17.7305 19.653 17.9572 19.1499 17.995L18.9999 18H4.99987C4.49529 18.0002 4.0093 17.8096 3.63932 17.4665C3.26934 17.1234 3.04271 16.6532 3.00487 16.15L2.99987 16V6C2.99971 5.49542 3.19028 5.00943 3.53337 4.63945C3.87646 4.26947 4.34671 4.04284 4.84987 4.005L4.99987 4H18.9999Z\"\n fill=\"white\"\n />\n </svg>\n )}\n </span>\n </button>\n );\n});\n\nScreenShareButton.displayName = 'ScreenShareButton';\n",
|
|
496
|
-
styles: "/* Device button styles */\n.deviceButton {\n
|
|
604
|
+
styles: "/* Device button styles */\n.deviceButton {\n position: relative;\n z-index: 10;\n height: 3rem;\n width: 3rem;\n border-radius: 9999px;\n background-color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1px solid transparent;\n cursor: pointer;\n}\n\n.deviceButtonIcon {\n display: flex;\n}\n\n.deviceButton:disabled {\n opacity: 0.5;\n}\n\n/* Screen reader only text */\n.srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n\n.screenShareButton {\n cursor: pointer;\n position: relative;\n height: 3rem;\n width: 3rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n backdrop-filter: blur(4px);\n background-color: rgba(255, 255, 255, 0.2);\n border: 1px solid rgba(255, 255, 255, 0.2);\n}\n",
|
|
497
605
|
componentsDependencies: [
|
|
498
606
|
"use-local-camera",
|
|
499
607
|
"use-local-microphone",
|
|
@@ -504,10 +612,26 @@ var media_controls_default = {
|
|
|
504
612
|
//#region src/templates/jsx/hooks/cvi-events-hooks.json
|
|
505
613
|
var cvi_events_hooks_default = {
|
|
506
614
|
type: "hooks",
|
|
507
|
-
content: "import { useCallback } from 'react';\nimport { useAppMessage, useDailyEvent } from '@daily-co/daily-react';\n\n// Every event broadcast by Tavus carries `seq` for global monotonic ordering\n// and `turn_idx` for grouping events by conversational turn.\n// See the Interactions Protocol docs (\"Event Ordering and Turn Tracking\").\n\n// Streaming utterance event — emitted as
|
|
615
|
+
content: "import { useCallback } from 'react';\nimport { useAppMessage, useDailyEvent } from '@daily-co/daily-react';\n\n// Every event broadcast by Tavus carries `seq` for global monotonic ordering\n// and `turn_idx` for grouping events by conversational turn.\n// See the Interactions Protocol docs (\"Event Ordering and Turn Tracking\").\n\n// Streaming utterance event — emitted as either side speaks. Reflects what was\n// actually spoken/transcribed (vs. `conversation.utterance` role=replica which\n// contains the full intended LLM response, even if interrupted).\n\n// Canonical role-based speaking events (current Tavus schema). Use the `role`\n// field in `properties` to identify the speaker.\n\n// Legacy per-role speaking events. Kept for backward compatibility with older\n// Tavus deployments that may still emit them.\n\nexport function useObservableEvent(callback) {\n return useDailyEvent(\n 'app-message',\n useCallback(\n (event) => {\n callback(event.data);\n },\n [callback]\n )\n );\n}\n\nexport function useSendAppMessage() {\n const sendAppMessage = useAppMessage();\n\n return useCallback(\n (message) => {\n sendAppMessage(message, '*');\n },\n [sendAppMessage]\n );\n}\n",
|
|
508
616
|
styles: ""
|
|
509
617
|
};
|
|
510
618
|
//#endregion
|
|
619
|
+
//#region src/templates/jsx/hooks/use-chat.json
|
|
620
|
+
var use_chat_default = {
|
|
621
|
+
type: "hooks",
|
|
622
|
+
content: "import { useCallback, useMemo, useReducer } from 'react';\nimport { useObservableEvent, useSendAppMessage } from './cvi-events-hooks';\n\nexport function makeMessageId(inferenceId, role) {\n return `${inferenceId}:${role}`;\n}\n\nexport function applyUtterance(prev, event, now) {\n const speech = event.properties.speech;\n const role = event.properties.role;\n if (!speech || (role !== 'user' && role !== 'replica')) {\n return prev;\n }\n const id = makeMessageId(event.inference_id, role);\n\n let base = prev;\n if (role === 'user') {\n const trimmed = speech.trim();\n base = prev.filter(\n (m) => !(m.id.startsWith('local-') && m.role === 'user' && m.text.trim() === trimmed)\n );\n }\n\n const existingIdx = base.findIndex((m) => m.id === id);\n if (existingIdx >= 0) {\n const next = base.slice();\n next[existingIdx] = { ...next[existingIdx], text: speech };\n return next;\n }\n return [...base, { id, role, text: speech, createdAt: now }];\n}\n\nexport function appendOptimistic(prev, text, id, now) {\n return [...prev, { id, role: 'user', text, createdAt: now }];\n}\n\nfunction chatReducer(state, action) {\n switch (action.type) {\n case 'utterance':\n return {\n messages: applyUtterance(state.messages, action.event, action.now),\n conversationId: state.conversationId ?? action.event.conversation_id ?? null,\n };\n case 'optimistic':\n return {\n ...state,\n messages: appendOptimistic(state.messages, action.text, action.id, action.now),\n };\n }\n}\n\nconst INITIAL_STATE = { messages: [], conversationId: null };\n\nfunction generateLocalId() {\n const cryptoObj = globalThis.crypto;\n if (cryptoObj?.randomUUID) {\n return `local-${cryptoObj.randomUUID()}`;\n }\n return `local-${Math.random().toString(36).slice(2)}-${Date.now()}`;\n}\n\nexport function useChat() {\n const [state, dispatch] = useReducer(chatReducer, INITIAL_STATE);\n const sendAppMessage = useSendAppMessage();\n\n useObservableEvent(\n useCallback((event) => {\n if (event.event_type === 'conversation.utterance') {\n dispatch({\n type: 'utterance',\n event,\n now: Date.now(),\n });\n }\n }, [])\n );\n\n const sendMessage = useCallback(\n (text) => {\n const trimmed = text.trim();\n if (!trimmed || !state.conversationId) {\n return;\n }\n const id = generateLocalId();\n dispatch({ type: 'optimistic', text: trimmed, id, now: Date.now() });\n sendAppMessage({\n message_type: 'conversation',\n event_type: 'conversation.respond',\n conversation_id: state.conversationId,\n properties: { text: trimmed },\n });\n },\n [state.conversationId, sendAppMessage]\n );\n\n return useMemo(\n () => ({\n messages: state.messages,\n conversationId: state.conversationId,\n sendMessage,\n }),\n [state.messages, state.conversationId, sendMessage]\n );\n}\n",
|
|
623
|
+
styles: "",
|
|
624
|
+
componentsDependencies: ["cvi-events-hooks"]
|
|
625
|
+
};
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/templates/jsx/hooks/use-closed-caption.json
|
|
628
|
+
var use_closed_caption_default = {
|
|
629
|
+
type: "hooks",
|
|
630
|
+
content: "import { useCallback, useRef, useState } from 'react';\nimport { useObservableEvent } from './cvi-events-hooks';\n\nconst CAPTION_CLEAR_DELAY_MS = 2000;\n\nexport const useClosedCaption = () => {\n const [caption, setCaption] = useState(null);\n const clearTimer = useRef(null);\n\n const update = useCallback((next, final) => {\n setCaption(next);\n if (clearTimer.current !== null) {\n clearTimeout(clearTimer.current);\n clearTimer.current = null;\n }\n if (final) {\n clearTimer.current = setTimeout(() => {\n setCaption(null);\n clearTimer.current = null;\n }, CAPTION_CLEAR_DELAY_MS);\n }\n }, []);\n\n useObservableEvent(\n useCallback(\n (event) => {\n if (event.event_type === 'conversation.utterance.streaming') {\n const { role, speech, final } = event.properties;\n if (role === 'user' || role === 'replica') {\n update({ role, text: speech }, final ?? false);\n }\n }\n },\n [update]\n )\n );\n\n return caption;\n};\n",
|
|
631
|
+
styles: "",
|
|
632
|
+
componentsDependencies: ["cvi-events-hooks"]
|
|
633
|
+
};
|
|
634
|
+
//#endregion
|
|
511
635
|
//#region src/templates/jsx/hooks/use-cvi-call.json
|
|
512
636
|
var use_cvi_call_default = {
|
|
513
637
|
type: "hooks",
|
|
@@ -567,12 +691,18 @@ var use_start_haircheck_default = {
|
|
|
567
691
|
//#region src/templates/jsx/index.ts
|
|
568
692
|
var jsx_exports = /* @__PURE__ */ __exportAll({
|
|
569
693
|
"audio-wave": () => audio_wave_default,
|
|
694
|
+
chat: () => chat_default,
|
|
695
|
+
"closed-captions": () => closed_captions_default,
|
|
570
696
|
"conversation-01": () => conversation_01_default,
|
|
697
|
+
"conversation-02": () => conversation_02_default,
|
|
698
|
+
"conversation-status": () => conversation_status_default,
|
|
571
699
|
"cvi-events-hooks": () => cvi_events_hooks_default,
|
|
572
700
|
"cvi-provider": () => cvi_provider_default,
|
|
573
701
|
"device-select": () => device_select_default,
|
|
574
702
|
"hair-check-01": () => hair_check_01_default,
|
|
575
703
|
"media-controls": () => media_controls_default,
|
|
704
|
+
"use-chat": () => use_chat_default,
|
|
705
|
+
"use-closed-caption": () => use_closed_caption_default,
|
|
576
706
|
"use-cvi-call": () => use_cvi_call_default,
|
|
577
707
|
"use-local-camera": () => use_local_camera_default,
|
|
578
708
|
"use-local-microphone": () => use_local_microphone_default,
|
|
@@ -583,9 +713,69 @@ var jsx_exports = /* @__PURE__ */ __exportAll({
|
|
|
583
713
|
"use-start-haircheck": () => use_start_haircheck_default
|
|
584
714
|
});
|
|
585
715
|
//#endregion
|
|
716
|
+
//#region src/templates/server/next-app.json
|
|
717
|
+
var next_app_default = {
|
|
718
|
+
type: "server",
|
|
719
|
+
framework: "next-app",
|
|
720
|
+
content: "// Server-side Tavus API for Next.js (App Router).\n// Reads TAVUS_API_KEY from server environment — never exposed to the client.\n// Get an API key: https://platform.tavus.io/api-keys\n\nconst TAVUS_API_BASE = 'https://tavusapi.com/v2';\n\ntype CreateBody = {\n action: 'create';\n // Forwarded verbatim to POST /v2/conversations. See the create-conversation\n // API reference for the full field list:\n // https://docs.tavus.io/api-reference/conversations/create-conversation\n params?: Record<string, unknown>;\n};\ntype EndBody = { action: 'end'; conversationId: string };\ntype Body = CreateBody | EndBody;\n\nexport async function POST(request: Request) {\n const apiKey = process.env.TAVUS_API_KEY;\n if (!apiKey) {\n return Response.json(\n { error: 'TAVUS_API_KEY is not set in the server environment.' },\n { status: 500 }\n );\n }\n\n const body = (await request.json()) as Body;\n\n if (body.action === 'create') {\n const res = await fetch(`${TAVUS_API_BASE}/conversations`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify(body.params ?? {}),\n });\n if (!res.ok) {\n return Response.json(\n { error: `Tavus create failed: ${res.status} ${await res.text()}` },\n { status: res.status }\n );\n }\n return Response.json(await res.json());\n }\n\n if (body.action === 'end') {\n const res = await fetch(`${TAVUS_API_BASE}/conversations/${body.conversationId}/end`, {\n method: 'POST',\n headers: { 'x-api-key': apiKey },\n });\n if (!res.ok) {\n return Response.json(\n { error: `Tavus end failed: ${res.status} ${await res.text()}` },\n { status: res.status }\n );\n }\n return new Response(null, { status: 204 });\n }\n\n return Response.json({ error: 'Unknown action' }, { status: 400 });\n}\n",
|
|
721
|
+
targetPath: "app/api/tavus/route.ts"
|
|
722
|
+
};
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/templates/server/next-pages.json
|
|
725
|
+
var next_pages_default = {
|
|
726
|
+
type: "server",
|
|
727
|
+
framework: "next-pages",
|
|
728
|
+
content: "// Server-side Tavus API for Next.js (Pages Router).\n// Reads TAVUS_API_KEY from server environment — never exposed to the client.\n// Get an API key: https://platform.tavus.io/api-keys\nimport type { NextApiRequest, NextApiResponse } from 'next';\n\nconst TAVUS_API_BASE = 'https://tavusapi.com/v2';\n\ntype Body =\n | { action: 'create'; params?: Record<string, unknown> }\n | { action: 'end'; conversationId: string };\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST');\n return res.status(405).end();\n }\n\n const apiKey = process.env.TAVUS_API_KEY;\n if (!apiKey) {\n return res.status(500).json({ error: 'TAVUS_API_KEY is not set in the server environment.' });\n }\n\n const body = req.body as Body;\n\n if (body.action === 'create') {\n const r = await fetch(`${TAVUS_API_BASE}/conversations`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify(body.params ?? {}),\n });\n if (!r.ok) {\n return res.status(r.status).json({ error: `Tavus create failed: ${await r.text()}` });\n }\n return res.status(200).json(await r.json());\n }\n\n if (body.action === 'end' && body.conversationId) {\n const r = await fetch(`${TAVUS_API_BASE}/conversations/${body.conversationId}/end`, {\n method: 'POST',\n headers: { 'x-api-key': apiKey },\n });\n if (!r.ok) {\n return res.status(r.status).json({ error: `Tavus end failed: ${await r.text()}` });\n }\n return res.status(204).end();\n }\n\n return res.status(400).json({ error: 'Unknown action' });\n}\n",
|
|
729
|
+
targetPath: "pages/api/tavus.ts"
|
|
730
|
+
};
|
|
731
|
+
//#endregion
|
|
732
|
+
//#region src/templates/server/remix.json
|
|
733
|
+
var remix_default = {
|
|
734
|
+
type: "server",
|
|
735
|
+
framework: "remix",
|
|
736
|
+
content: "// Server-side Tavus API for Remix (resource route).\n// Reads TAVUS_API_KEY from server environment — never exposed to the client.\n// Get an API key: https://platform.tavus.io/api-keys\nimport type { ActionFunctionArgs } from '@remix-run/node';\nimport { json } from '@remix-run/node';\n\nconst TAVUS_API_BASE = 'https://tavusapi.com/v2';\n\ntype Body =\n | { action: 'create'; params?: Record<string, unknown> }\n | { action: 'end'; conversationId: string };\n\nexport async function action({ request }: ActionFunctionArgs) {\n if (request.method !== 'POST') {\n throw json({ error: 'Method not allowed' }, { status: 405 });\n }\n\n const apiKey = process.env.TAVUS_API_KEY;\n if (!apiKey) {\n throw json({ error: 'TAVUS_API_KEY is not set in the server environment.' }, { status: 500 });\n }\n\n const body = (await request.json()) as Body;\n\n if (body.action === 'create') {\n const r = await fetch(`${TAVUS_API_BASE}/conversations`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify(body.params ?? {}),\n });\n if (!r.ok) {\n throw json({ error: `Tavus create failed: ${await r.text()}` }, { status: r.status });\n }\n return json(await r.json());\n }\n\n if (body.action === 'end' && body.conversationId) {\n const r = await fetch(`${TAVUS_API_BASE}/conversations/${body.conversationId}/end`, {\n method: 'POST',\n headers: { 'x-api-key': apiKey },\n });\n if (!r.ok) {\n throw json({ error: `Tavus end failed: ${await r.text()}` }, { status: r.status });\n }\n return new Response(null, { status: 204 });\n }\n\n throw json({ error: 'Unknown action' }, { status: 400 });\n}\n",
|
|
737
|
+
targetPath: "app/routes/api.tavus.ts"
|
|
738
|
+
};
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/templates/server/tanstack-start.json
|
|
741
|
+
var tanstack_start_default = {
|
|
742
|
+
type: "server",
|
|
743
|
+
framework: "tanstack-start",
|
|
744
|
+
content: "// Server-side Tavus API for TanStack Start (file-based API route).\n// Reads TAVUS_API_KEY from server environment — never exposed to the client.\n// Get an API key: https://platform.tavus.io/api-keys\nimport { createAPIFileRoute } from '@tanstack/start/api';\n\nconst TAVUS_API_BASE = 'https://tavusapi.com/v2';\n\ntype Body =\n | { action: 'create'; params?: Record<string, unknown> }\n | { action: 'end'; conversationId: string };\n\nexport const APIRoute = createAPIFileRoute('/api/tavus')({\n POST: async ({ request }) => {\n const apiKey = process.env.TAVUS_API_KEY;\n if (!apiKey) {\n return Response.json(\n { error: 'TAVUS_API_KEY is not set in the server environment.' },\n { status: 500 }\n );\n }\n\n const body = (await request.json()) as Body;\n\n if (body.action === 'create') {\n const r = await fetch(`${TAVUS_API_BASE}/conversations`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify(body.params ?? {}),\n });\n if (!r.ok) {\n return Response.json(\n { error: `Tavus create failed: ${await r.text()}` },\n { status: r.status }\n );\n }\n return Response.json(await r.json());\n }\n\n if (body.action === 'end' && body.conversationId) {\n const r = await fetch(`${TAVUS_API_BASE}/conversations/${body.conversationId}/end`, {\n method: 'POST',\n headers: { 'x-api-key': apiKey },\n });\n if (!r.ok) {\n return Response.json(\n { error: `Tavus end failed: ${await r.text()}` },\n { status: r.status }\n );\n }\n return new Response(null, { status: 204 });\n }\n\n return Response.json({ error: 'Unknown action' }, { status: 400 });\n },\n});\n",
|
|
745
|
+
targetPath: "app/routes/api/tavus.ts"
|
|
746
|
+
};
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/templates/server/index.ts
|
|
749
|
+
var server_exports = /* @__PURE__ */ __exportAll({
|
|
750
|
+
"next-app": () => next_app_default,
|
|
751
|
+
"next-pages": () => next_pages_default,
|
|
752
|
+
remix: () => remix_default,
|
|
753
|
+
"tanstack-start": () => tanstack_start_default
|
|
754
|
+
});
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region src/templates/lib/tavus-api-vite-ssr.json
|
|
757
|
+
var tavus_api_vite_ssr_default = {
|
|
758
|
+
type: "lib",
|
|
759
|
+
content: "// Runtime-agnostic Tavus API handler for Vite projects with a server.\n// Pair with `tavus-client.ts` (which calls POST /api/tavus from the browser).\n//\n// You wire `handleTavusRequest` into whatever middleware your server uses.\n// The function takes a standard `Request` and returns a `Response`, so it\n// works with any runtime that speaks Web fetch (h3, Hono, Cloudflare Workers,\n// Bun.serve, vike-server, Vinxi, etc.).\n//\n// Set `TAVUS_API_KEY` in your server environment — never on the client.\n// Get an API key: https://platform.tavus.io/api-keys\n//\n// ─── Wiring examples ──────────────────────────────────────────────────────\n//\n// Hono:\n// import { Hono } from 'hono';\n// import { handleTavusRequest } from './lib/tavus-api-vite-ssr';\n// const app = new Hono();\n// app.post('/api/tavus', (c) => handleTavusRequest(c.req.raw));\n//\n// h3 / Vinxi:\n// import { eventHandler, toWebRequest } from 'h3';\n// import { handleTavusRequest } from './lib/tavus-api-vite-ssr';\n// export default eventHandler((event) => handleTavusRequest(toWebRequest(event)));\n//\n// Express (with Web Fetch adapter, e.g. `@hattip/adapter-node`):\n// app.post('/api/tavus', async (req, res) => {\n// const webReq = toWebRequest(req); // your adapter\n// const webRes = await handleTavusRequest(webReq);\n// // pipe webRes back to `res`\n// });\n\nconst TAVUS_API_BASE = 'https://tavusapi.com/v2';\n\ntype Body =\n | { action: 'create'; params?: Record<string, unknown> }\n | { action: 'end'; conversationId: string };\n\nexport async function handleTavusRequest(request: Request): Promise<Response> {\n if (request.method !== 'POST') {\n return new Response('Method not allowed', {\n status: 405,\n headers: { Allow: 'POST' },\n });\n }\n\n const apiKey = process.env.TAVUS_API_KEY;\n if (!apiKey) {\n return Response.json(\n { error: 'TAVUS_API_KEY is not set in the server environment.' },\n { status: 500 }\n );\n }\n\n let body: Body;\n try {\n body = (await request.json()) as Body;\n } catch {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 });\n }\n\n if (body.action === 'create') {\n const r = await fetch(`${TAVUS_API_BASE}/conversations`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify(body.params ?? {}),\n });\n if (!r.ok) {\n return Response.json(\n { error: `Tavus create failed: ${await r.text()}` },\n { status: r.status }\n );\n }\n return Response.json(await r.json());\n }\n\n if (body.action === 'end' && body.conversationId) {\n const r = await fetch(`${TAVUS_API_BASE}/conversations/${body.conversationId}/end`, {\n method: 'POST',\n headers: { 'x-api-key': apiKey },\n });\n if (!r.ok) {\n return Response.json({ error: `Tavus end failed: ${await r.text()}` }, { status: r.status });\n }\n return new Response(null, { status: 204 });\n }\n\n return Response.json({ error: 'Unknown action' }, { status: 400 });\n}\n"
|
|
760
|
+
};
|
|
761
|
+
//#endregion
|
|
762
|
+
//#region src/templates/lib/tavus-client.json
|
|
763
|
+
var tavus_client_default = {
|
|
764
|
+
type: "lib",
|
|
765
|
+
content: "// Browser client that talks to your server-side /api/tavus route.\n// The Tavus API key stays on the server — see the matching server route\n// installed by `npx @tavus/cvi-ui add tavus-api`.\n//\n// Get an API key: https://platform.tavus.io/api-keys\n// Full create-conversation field reference:\n// https://docs.tavus.io/api-reference/conversations/create-conversation\n\nconst ENDPOINT = '/api/tavus';\n\n// Typed params for `POST /v2/conversations`. Mirrors the create-conversation\n// API; all fields optional here so you can omit anything that has a default\n// (e.g. `replica_id` is not required when your persona has a default replica).\n// Tavus returns 4xx if you pass nothing valid; that error propagates to you.\nexport type CreateConversationParams = {\n replica_id?: string;\n persona_id?: string;\n audio_only?: boolean;\n callback_url?: string;\n conversation_name?: string;\n conversational_context?: string;\n custom_greeting?: string;\n memory_stores?: string[];\n document_ids?: string[];\n document_retrieval_strategy?: 'speed' | 'quality' | 'balanced';\n document_tags?: string[];\n test_mode?: boolean;\n require_auth?: boolean;\n max_participants?: number;\n properties?: {\n max_call_duration?: number;\n participant_left_timeout?: number;\n participant_absent_timeout?: number;\n enable_recording?: boolean;\n enable_closed_captions?: boolean;\n apply_greenscreen?: boolean;\n require_auth?: boolean;\n language?: string;\n recording_storage?: Record<string, unknown>;\n // Forward-compat: pass any new `properties.*` field Tavus adds.\n [key: string]: unknown;\n };\n // Forward-compat: pass any new top-level field Tavus adds.\n [key: string]: unknown;\n};\n\n// The full Tavus response shape includes more than these two fields. We only\n// type what callers commonly need; widen as you need.\nexport type CreateConversationResponse = {\n conversation_id: string;\n conversation_url: string;\n [key: string]: unknown;\n};\n\nexport async function createTavusConversation(\n params: CreateConversationParams = {}\n): Promise<CreateConversationResponse> {\n const res = await fetch(ENDPOINT, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action: 'create', params }),\n });\n if (!res.ok) {\n throw new Error(`Tavus create failed: ${res.status} ${await res.text()}`);\n }\n return res.json();\n}\n\nexport async function endTavusConversation(conversationId: string): Promise<void> {\n const res = await fetch(ENDPOINT, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action: 'end', conversationId }),\n });\n if (!res.ok) {\n throw new Error(`Tavus end failed: ${res.status} ${await res.text()}`);\n }\n}\n"
|
|
766
|
+
};
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region src/templates/lib/index.ts
|
|
769
|
+
var lib_exports = /* @__PURE__ */ __exportAll({
|
|
770
|
+
"tavus-api-vite-ssr": () => tavus_api_vite_ssr_default,
|
|
771
|
+
"tavus-client": () => tavus_client_default
|
|
772
|
+
});
|
|
773
|
+
//#endregion
|
|
586
774
|
//#region src/templates/index.ts
|
|
587
775
|
var templates_exports = /* @__PURE__ */ __exportAll({
|
|
588
776
|
jsx: () => jsx_exports,
|
|
777
|
+
lib: () => lib_exports,
|
|
778
|
+
server: () => server_exports,
|
|
589
779
|
tsx: () => tsx_exports
|
|
590
780
|
});
|
|
591
781
|
//#endregion
|
|
@@ -595,10 +785,26 @@ const components = [
|
|
|
595
785
|
name: "audio-wave",
|
|
596
786
|
path: "components/audio-wave"
|
|
597
787
|
},
|
|
788
|
+
{
|
|
789
|
+
name: "chat",
|
|
790
|
+
path: "components/chat"
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: "closed-captions",
|
|
794
|
+
path: "components/closed-captions"
|
|
795
|
+
},
|
|
598
796
|
{
|
|
599
797
|
name: "conversation-01",
|
|
600
798
|
path: "components/conversation-01"
|
|
601
799
|
},
|
|
800
|
+
{
|
|
801
|
+
name: "conversation-02",
|
|
802
|
+
path: "components/conversation-02"
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
name: "conversation-status",
|
|
806
|
+
path: "components/conversation-status"
|
|
807
|
+
},
|
|
602
808
|
{
|
|
603
809
|
name: "cvi-provider",
|
|
604
810
|
path: "components/cvi-provider"
|
|
@@ -619,6 +825,14 @@ const components = [
|
|
|
619
825
|
name: "cvi-events-hooks",
|
|
620
826
|
path: "hooks/cvi-events-hooks"
|
|
621
827
|
},
|
|
828
|
+
{
|
|
829
|
+
name: "use-chat",
|
|
830
|
+
path: "hooks/use-chat"
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: "use-closed-caption",
|
|
834
|
+
path: "hooks/use-closed-caption"
|
|
835
|
+
},
|
|
622
836
|
{
|
|
623
837
|
name: "use-cvi-call",
|
|
624
838
|
path: "hooks/use-cvi-call"
|
|
@@ -678,7 +892,7 @@ function findComponentByName(name) {
|
|
|
678
892
|
return component;
|
|
679
893
|
}
|
|
680
894
|
async function updateFiles(files, config, options) {
|
|
681
|
-
if (!files?.length) return {
|
|
895
|
+
if (!((files?.length ?? 0) > 0 || (options.serverDependencies?.length ?? 0) > 0 || (options.libDependencies?.length ?? 0) > 0)) return {
|
|
682
896
|
filesCreated: [],
|
|
683
897
|
filesUpdated: [],
|
|
684
898
|
filesSkipped: []
|
|
@@ -752,6 +966,68 @@ async function updateFiles(files, config, options) {
|
|
|
752
966
|
if (existingTarget) filesUpdated.push(path$1.relative(config.resolvedPaths.cwd, targetPath));
|
|
753
967
|
else filesCreated.push(path$1.relative(config.resolvedPaths.cwd, targetPath));
|
|
754
968
|
}
|
|
969
|
+
for (const fwName of options.serverDependencies ?? []) {
|
|
970
|
+
const tpl = server_exports[fwName];
|
|
971
|
+
if (!tpl) {
|
|
972
|
+
logger.warn(`Server template for framework '${fwName}' not found`);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const targetPath = path$1.join(config.resolvedPaths.cwd, tpl.targetPath);
|
|
976
|
+
const targetDir = path$1.dirname(targetPath);
|
|
977
|
+
const existingTarget = existsSync(targetPath);
|
|
978
|
+
if (existingTarget && !options.overwrite) {
|
|
979
|
+
filesCreatedSpinner.stop();
|
|
980
|
+
if (options.rootSpinner) options.rootSpinner.stop();
|
|
981
|
+
const { overwrite } = await prompts({
|
|
982
|
+
type: "confirm",
|
|
983
|
+
name: "overwrite",
|
|
984
|
+
message: `The file ${highlighter.info(tpl.targetPath)} already exists. Would you like to overwrite?`,
|
|
985
|
+
initial: false
|
|
986
|
+
});
|
|
987
|
+
if (!overwrite) {
|
|
988
|
+
filesSkipped.push(tpl.targetPath);
|
|
989
|
+
if (options.rootSpinner) options.rootSpinner.start();
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
filesCreatedSpinner?.start();
|
|
993
|
+
if (options.rootSpinner) options.rootSpinner.start();
|
|
994
|
+
}
|
|
995
|
+
if (!existsSync(targetDir)) await promises.mkdir(targetDir, { recursive: true });
|
|
996
|
+
await promises.writeFile(targetPath, tpl.content);
|
|
997
|
+
if (existingTarget) filesUpdated.push(tpl.targetPath);
|
|
998
|
+
else filesCreated.push(tpl.targetPath);
|
|
999
|
+
}
|
|
1000
|
+
for (const libName of options.libDependencies ?? []) {
|
|
1001
|
+
const tpl = lib_exports[libName];
|
|
1002
|
+
if (!tpl) {
|
|
1003
|
+
logger.warn(`Lib template '${libName}' not found`);
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const targetDir = path$1.join(config.resolvedPaths.cwd, componentsPath, "lib");
|
|
1007
|
+
const targetPath = path$1.join(targetDir, `${libName}.ts`);
|
|
1008
|
+
const existingTarget = existsSync(targetPath);
|
|
1009
|
+
if (existingTarget && !options.overwrite) {
|
|
1010
|
+
filesCreatedSpinner.stop();
|
|
1011
|
+
if (options.rootSpinner) options.rootSpinner.stop();
|
|
1012
|
+
const { overwrite } = await prompts({
|
|
1013
|
+
type: "confirm",
|
|
1014
|
+
name: "overwrite",
|
|
1015
|
+
message: `The file ${highlighter.info(libName)} already exists. Would you like to overwrite?`,
|
|
1016
|
+
initial: false
|
|
1017
|
+
});
|
|
1018
|
+
if (!overwrite) {
|
|
1019
|
+
filesSkipped.push(path$1.relative(config.resolvedPaths.cwd, targetPath));
|
|
1020
|
+
if (options.rootSpinner) options.rootSpinner.start();
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
filesCreatedSpinner?.start();
|
|
1024
|
+
if (options.rootSpinner) options.rootSpinner.start();
|
|
1025
|
+
}
|
|
1026
|
+
if (!existsSync(targetDir)) await promises.mkdir(targetDir, { recursive: true });
|
|
1027
|
+
await promises.writeFile(targetPath, tpl.content);
|
|
1028
|
+
if (existingTarget) filesUpdated.push(path$1.relative(config.resolvedPaths.cwd, targetPath));
|
|
1029
|
+
else filesCreated.push(path$1.relative(config.resolvedPaths.cwd, targetPath));
|
|
1030
|
+
}
|
|
755
1031
|
if (!(filesCreated.length || filesUpdated.length) && !filesSkipped.length) filesCreatedSpinner?.info("No files updated.");
|
|
756
1032
|
if (filesCreated.length) {
|
|
757
1033
|
filesCreatedSpinner?.succeed(`Created ${filesCreated.length} ${filesCreated.length === 1 ? "file" : "files"}:`);
|
|
@@ -774,6 +1050,12 @@ async function updateFiles(files, config, options) {
|
|
|
774
1050
|
}
|
|
775
1051
|
//#endregion
|
|
776
1052
|
//#region src/utils/resolve-components-tree.ts
|
|
1053
|
+
const SERVER_FRAMEWORKS = [
|
|
1054
|
+
"next-app",
|
|
1055
|
+
"next-pages",
|
|
1056
|
+
"remix",
|
|
1057
|
+
"tanstack-start"
|
|
1058
|
+
];
|
|
777
1059
|
function findComponentTemplate(name) {
|
|
778
1060
|
let component = tsx_exports[name];
|
|
779
1061
|
if (!component) component = tsx_exports[getDefaultVersion(removeVersionSuffix(name))];
|
|
@@ -799,9 +1081,11 @@ function deduplicateComponents(components) {
|
|
|
799
1081
|
});
|
|
800
1082
|
return deduplicatedComponents;
|
|
801
1083
|
}
|
|
802
|
-
function resolveComponentsTree(components) {
|
|
1084
|
+
function resolveComponentsTree(components, framework) {
|
|
803
1085
|
const dependencies = /* @__PURE__ */ new Set();
|
|
804
|
-
const componentsDependencies = new Set(
|
|
1086
|
+
const componentsDependencies = /* @__PURE__ */ new Set();
|
|
1087
|
+
const serverDependencies = /* @__PURE__ */ new Set();
|
|
1088
|
+
const libDependencies = /* @__PURE__ */ new Set();
|
|
805
1089
|
function processComponent(componentName) {
|
|
806
1090
|
const component = findComponentTemplate(componentName);
|
|
807
1091
|
if (!component) return;
|
|
@@ -811,15 +1095,41 @@ function resolveComponentsTree(components) {
|
|
|
811
1095
|
processComponent(dep);
|
|
812
1096
|
});
|
|
813
1097
|
}
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1098
|
+
const tsxRequests = [];
|
|
1099
|
+
for (const raw of components) {
|
|
1100
|
+
if (raw === "tavus-api") {
|
|
1101
|
+
if (framework && SERVER_FRAMEWORKS.includes(framework)) {
|
|
1102
|
+
serverDependencies.add(framework);
|
|
1103
|
+
libDependencies.add("tavus-client");
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
throw new Error(`'tavus-api' requires a server-capable framework (one of: ${SERVER_FRAMEWORKS.join(", ")}). Detected framework: ${framework ?? "unknown"}. Calling the Tavus API from the browser would expose your TAVUS_API_KEY.\n\nIf your Vite project has a server runtime (Vinxi, Hono, vike-server, custom Express, Cloudflare Workers, Bun, ...), run \`add tavus-api-vite-ssr\` instead — it installs a runtime-agnostic Request handler you wire into your middleware.\n\nIf your project is purely client-side (no server), you must front the Tavus API with your own backend. We do not ship a browser-direct client because shipping the API key in client JS is unsafe in any deployed context.`);
|
|
1107
|
+
}
|
|
1108
|
+
if (raw === "tavus-api-vite-ssr") {
|
|
1109
|
+
libDependencies.add("tavus-api-vite-ssr");
|
|
1110
|
+
libDependencies.add("tavus-client");
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (raw in lib_exports) {
|
|
1114
|
+
libDependencies.add(raw);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (raw in server_exports) {
|
|
1118
|
+
serverDependencies.add(raw);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
tsxRequests.push(raw);
|
|
1122
|
+
}
|
|
1123
|
+
if (tsxRequests.length > 0) {
|
|
1124
|
+
const uniqueComponents = deduplicateComponents(tsxRequests);
|
|
1125
|
+
uniqueComponents.forEach((c) => componentsDependencies.add(c));
|
|
1126
|
+
uniqueComponents.forEach((c) => processComponent(c));
|
|
1127
|
+
}
|
|
820
1128
|
return {
|
|
821
1129
|
dependencies: Array.from(dependencies),
|
|
822
|
-
componentsDependencies: Array.from(componentsDependencies)
|
|
1130
|
+
componentsDependencies: Array.from(componentsDependencies),
|
|
1131
|
+
serverDependencies: Array.from(serverDependencies),
|
|
1132
|
+
libDependencies: Array.from(libDependencies)
|
|
823
1133
|
};
|
|
824
1134
|
}
|
|
825
1135
|
//#endregion
|
|
@@ -834,13 +1144,22 @@ async function addComponents(components, config, options) {
|
|
|
834
1144
|
return await addProjectComponents(components, config, options);
|
|
835
1145
|
}
|
|
836
1146
|
async function addProjectComponents(components, config, options) {
|
|
1147
|
+
const projectInfo = await getProjectInfo(config.resolvedPaths.cwd);
|
|
837
1148
|
const registrySpinner = spinner(`Checking components.`, { silent: options.silent })?.start();
|
|
838
|
-
|
|
1149
|
+
let tree;
|
|
1150
|
+
try {
|
|
1151
|
+
tree = resolveComponentsTree(components, projectInfo?.framework?.name);
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
registrySpinner?.fail();
|
|
1154
|
+
throw err;
|
|
1155
|
+
}
|
|
839
1156
|
registrySpinner?.succeed();
|
|
840
1157
|
if (!options.skipInstall) await updateDependencies(tree.dependencies, config, { silent: options.silent });
|
|
841
1158
|
await updateFiles(tree.componentsDependencies, config, {
|
|
842
1159
|
overwrite: options.overwrite,
|
|
843
|
-
silent: options.silent
|
|
1160
|
+
silent: options.silent,
|
|
1161
|
+
serverDependencies: tree.serverDependencies,
|
|
1162
|
+
libDependencies: tree.libDependencies
|
|
844
1163
|
});
|
|
845
1164
|
}
|
|
846
1165
|
//#endregion
|
|
@@ -1054,7 +1373,7 @@ const info = new Command().name("info").description("get information about your
|
|
|
1054
1373
|
});
|
|
1055
1374
|
//#endregion
|
|
1056
1375
|
//#region package.json
|
|
1057
|
-
var version = "0.0.
|
|
1376
|
+
var version = "0.0.3";
|
|
1058
1377
|
//#endregion
|
|
1059
1378
|
//#region src/index.ts
|
|
1060
1379
|
process.on("SIGINT", () => process.exit(0));
|