diff --git a/dailyjs/live-transcription/.babelrc b/dailyjs/live-transcription/.babelrc new file mode 100644 index 0000000..a6f4434 --- /dev/null +++ b/dailyjs/live-transcription/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["inline-react-svg"] +} diff --git a/dailyjs/live-transcription/README.md b/dailyjs/live-transcription/README.md new file mode 100644 index 0000000..9840d4a --- /dev/null +++ b/dailyjs/live-transcription/README.md @@ -0,0 +1,40 @@ +# Live Transcription + +![Live Transcription](./image.png) + +### Live example + +**[See it in action here ➡️](https://dailyjs-live-transcription.vercel.app)** + +--- + +## What does this demo do? + +- Use [sendAppMessage](https://docs.daily.co/reference#%EF%B8%8F-sendappmessage) to send messages +- Listen for incoming messages using the call object `app-message` event +- Extend the basic call demo with a chat provider and aside +- Show a notification bubble on chat tray button when a new message is received +- Demonstrate how to play a sound whenever a message is received + +Please note: this demo is not currently mobile optimised + +### Getting started + +``` +# set both DAILY_API_KEY and DAILY_DOMAIN +mv env.example .env.local + +yarn +yarn workspace @dailyjs/live-transcription dev +``` + +## How does this example work? + +In this example we extend the [basic call demo](../basic-call) with the ability to generate transcription of the meeting in real time and log that in a side panel. + +We pass a custom tray object, a custom app object (wrapping the original in a new `TranscriptionProvider`) as well as add our `TranscriptionAside` panel. We also symlink both the `public` and `pages/api` folders from the basic call. + + +## Deploy your own on Vercel + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples) diff --git a/dailyjs/live-transcription/components/App/App.js b/dailyjs/live-transcription/components/App/App.js new file mode 100644 index 0000000..b23d9ce --- /dev/null +++ b/dailyjs/live-transcription/components/App/App.js @@ -0,0 +1,13 @@ +import React from 'react'; + +import App from '@dailyjs/basic-call/components/App'; +import { TranscriptionProvider } from '../../contexts/TranscriptionProvider'; + +// Extend our basic call app component with the Live Transcription context +export const AppWithTranscription = () => ( + + + +); + +export default AppWithTranscription; diff --git a/dailyjs/live-transcription/components/App/index.js b/dailyjs/live-transcription/components/App/index.js new file mode 100644 index 0000000..a1726c9 --- /dev/null +++ b/dailyjs/live-transcription/components/App/index.js @@ -0,0 +1 @@ +export { AppWithTranscription as default } from './App'; diff --git a/dailyjs/live-transcription/components/TranscriptionAside/TranscriptionAside.js b/dailyjs/live-transcription/components/TranscriptionAside/TranscriptionAside.js new file mode 100644 index 0000000..00a4ff3 --- /dev/null +++ b/dailyjs/live-transcription/components/TranscriptionAside/TranscriptionAside.js @@ -0,0 +1,91 @@ +import React, { useEffect, useRef, useState } from 'react'; +import Aside from '@dailyjs/shared/components/Aside'; +import { Button } from '@dailyjs/shared/components/Button'; +import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; +import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; +import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; +import { useTranscription } from '../../contexts/TranscriptionProvider'; + +export const TRANSCRIPTION_ASIDE = 'transcription'; + +export const TranscriptionAside = () => { + const { callObject } = useCallState(); + const { showAside, setShowAside } = useUIState(); + const { _sendMessage, transcriptionHistory } = useTranscription(); + const { _allParticipants, isOwner } = useParticipants(); + + const msgWindowRef = useRef(); + + + useEffect(() => { + if (msgWindowRef.current) { + msgWindowRef.current.scrollTop = msgWindowRef.current.scrollHeight; + } + }, [transcriptionHistory?.length]); + + if (!showAside || showAside !== TRANSCRIPTION_ASIDE) { + return null; + } + + function startTranscription() { + callObject.startTranscription(); + } + + function stopTranscription() { + callObject.stopTranscription(); + } + + return ( + + ); +}; + +export default TranscriptionAside; diff --git a/dailyjs/live-transcription/components/TranscriptionAside/index.js b/dailyjs/live-transcription/components/TranscriptionAside/index.js new file mode 100644 index 0000000..c9ef5c2 --- /dev/null +++ b/dailyjs/live-transcription/components/TranscriptionAside/index.js @@ -0,0 +1 @@ +export { TranscriptionAside as default } from './TranscriptionAside'; diff --git a/dailyjs/live-transcription/components/Tray/Tray.js b/dailyjs/live-transcription/components/Tray/Tray.js new file mode 100644 index 0000000..5da7e15 --- /dev/null +++ b/dailyjs/live-transcription/components/Tray/Tray.js @@ -0,0 +1,26 @@ +import React from 'react'; + +import { TrayButton } from '@dailyjs/shared/components/Tray'; +import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; +import { ReactComponent as IconTranscription } from '@dailyjs/shared/icons/chat-md.svg'; +import { useTranscription } from '../../contexts/TranscriptionProvider'; +import { TRANSCRIPTION_ASIDE } from '../TranscriptionAside/TranscriptionAside'; + +export const Tray = () => { + const { toggleAside } = useUIState(); + const { hasNewMessages } = useTranscription(); + + return ( + { + toggleAside(TRANSCRIPTION_ASIDE); + }} + > + + + ); +}; + +export default Tray; diff --git a/dailyjs/live-transcription/components/Tray/index.js b/dailyjs/live-transcription/components/Tray/index.js new file mode 100644 index 0000000..100bcc8 --- /dev/null +++ b/dailyjs/live-transcription/components/Tray/index.js @@ -0,0 +1 @@ +export { Tray as default } from './Tray'; diff --git a/dailyjs/live-transcription/contexts/TranscriptionProvider.js b/dailyjs/live-transcription/contexts/TranscriptionProvider.js new file mode 100644 index 0000000..41a75e7 --- /dev/null +++ b/dailyjs/live-transcription/contexts/TranscriptionProvider.js @@ -0,0 +1,69 @@ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; +import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; +import { nanoid } from 'nanoid'; +import PropTypes from 'prop-types'; + +export const TranscriptionContext = createContext(); + +export const TranscriptionProvider = ({ children }) => { + const { callObject } = useCallState(); + const [transcriptionHistory, setTranscriptionHistory] = useState([]); + const [hasNewMessages, setHasNewMessages] = useState(false); + + const handleNewMessage = useCallback( + (e) => { + if (e.fromId === 'transcription' && e.data?.is_final) { + console.log(`${JSON.stringify(e.data)}: ${JSON.stringify(callObject.participants())}`) + + const sender = e.data.user_name + ? e.data.user_name + : 'Guest'; + + + setTranscriptionHistory((oldState) => [ + ...oldState, + { sender, message: e.data.text, id: nanoid() }, + ]); + } + + setHasNewMessages(true); + }, + [callObject] + ); + + useEffect(() => { + if (!callObject) { + return false; + } + + console.log(`💬 Transcription provider listening for app messages`); + + callObject.on('app-message', handleNewMessage); + + return () => callObject.off('app-message', handleNewMessage); + }, [callObject, handleNewMessage]); + + return ( + + {children} + + ); +}; + +TranscriptionProvider.propTypes = { + children: PropTypes.node, +}; + +export const useTranscription = () => useContext(TranscriptionContext); diff --git a/dailyjs/live-transcription/env.example b/dailyjs/live-transcription/env.example new file mode 100644 index 0000000..5ab7e03 --- /dev/null +++ b/dailyjs/live-transcription/env.example @@ -0,0 +1,8 @@ +# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain' +DAILY_DOMAIN= + +# Obtained from https://dashboard.daily.co/developers +DAILY_API_KEY= + +# Daily REST API endpoint +DAILY_REST_DOMAIN=https://api.daily.co/v1 diff --git a/dailyjs/live-transcription/hooks/useMessageSound.js b/dailyjs/live-transcription/hooks/useMessageSound.js new file mode 100644 index 0000000..3324693 --- /dev/null +++ b/dailyjs/live-transcription/hooks/useMessageSound.js @@ -0,0 +1,19 @@ +import { useEffect, useMemo } from 'react'; + +import { useSound } from '@dailyjs/shared/hooks/useSound'; +import { debounce } from 'debounce'; + +/** + * Convenience hook to play `join.mp3` when participants join the call + */ +export const useMessageSound = () => { + const { load, play } = useSound('assets/message.mp3'); + + useEffect(() => { + load(); + }, [load]); + + return useMemo(() => debounce(() => play(), 5000, true), [play]); +}; + +export default useMessageSound; diff --git a/dailyjs/live-transcription/index.js b/dailyjs/live-transcription/index.js new file mode 100644 index 0000000..9044efc --- /dev/null +++ b/dailyjs/live-transcription/index.js @@ -0,0 +1 @@ +// Note: I am here because next-transpile-modules requires a mainfile diff --git a/dailyjs/live-transcription/next.config.js b/dailyjs/live-transcription/next.config.js new file mode 100644 index 0000000..9a0a6ee --- /dev/null +++ b/dailyjs/live-transcription/next.config.js @@ -0,0 +1,13 @@ +const withPlugins = require('next-compose-plugins'); +const withTM = require('next-transpile-modules')([ + '@dailyjs/shared', + '@dailyjs/basic-call', +]); + +const packageJson = require('./package.json'); + +module.exports = withPlugins([withTM], { + env: { + PROJECT_TITLE: packageJson.description, + }, +}); diff --git a/dailyjs/live-transcription/package.json b/dailyjs/live-transcription/package.json new file mode 100644 index 0000000..e271ed8 --- /dev/null +++ b/dailyjs/live-transcription/package.json @@ -0,0 +1,25 @@ +{ + "name": "@dailyjs/live-transcription", + "description": "Basic Call + Transcription Example", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@dailyjs/shared": "*", + "@dailyjs/basic-call": "*", + "next": "^11.0.0", + "pluralize": "^8.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "babel-plugin-module-resolver": "^4.1.0", + "next-compose-plugins": "^2.2.1", + "next-transpile-modules": "^8.0.0" + } +} diff --git a/dailyjs/live-transcription/pages/_app.js b/dailyjs/live-transcription/pages/_app.js new file mode 100644 index 0000000..f64e96a --- /dev/null +++ b/dailyjs/live-transcription/pages/_app.js @@ -0,0 +1,12 @@ +import React from 'react'; +import App from '@dailyjs/basic-call/pages/_app'; +import AppWithTranscription from '../components/App'; + +import TranscriptionAside from '../components/TranscriptionAside'; +import Tray from '../components/Tray'; + +App.asides = [TranscriptionAside]; +App.customAppComponent = ; +App.customTrayComponent = ; + +export default App; diff --git a/dailyjs/live-transcription/pages/api b/dailyjs/live-transcription/pages/api new file mode 120000 index 0000000..999f604 --- /dev/null +++ b/dailyjs/live-transcription/pages/api @@ -0,0 +1 @@ +../../basic-call/pages/api \ No newline at end of file diff --git a/dailyjs/live-transcription/pages/index.js b/dailyjs/live-transcription/pages/index.js new file mode 100644 index 0000000..d25e77e --- /dev/null +++ b/dailyjs/live-transcription/pages/index.js @@ -0,0 +1,13 @@ +import Index from '@dailyjs/basic-call/pages'; +import getDemoProps from '@dailyjs/shared/lib/demoProps'; + +export async function getStaticProps() { + const defaultProps = getDemoProps(); + + // Pass through domain as prop + return { + props: defaultProps, + }; +} + +export default Index; diff --git a/dailyjs/live-transcription/public/assets/daily-logo-dark.svg b/dailyjs/live-transcription/public/assets/daily-logo-dark.svg new file mode 100644 index 0000000..ef3a565 --- /dev/null +++ b/dailyjs/live-transcription/public/assets/daily-logo-dark.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/dailyjs/live-transcription/public/assets/daily-logo.svg b/dailyjs/live-transcription/public/assets/daily-logo.svg new file mode 100644 index 0000000..534a18a --- /dev/null +++ b/dailyjs/live-transcription/public/assets/daily-logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/dailyjs/live-transcription/public/assets/join.mp3 b/dailyjs/live-transcription/public/assets/join.mp3 new file mode 100644 index 0000000..7657915 Binary files /dev/null and b/dailyjs/live-transcription/public/assets/join.mp3 differ diff --git a/dailyjs/live-transcription/public/assets/message.mp3 b/dailyjs/live-transcription/public/assets/message.mp3 new file mode 100644 index 0000000..a067315 Binary files /dev/null and b/dailyjs/live-transcription/public/assets/message.mp3 differ diff --git a/dailyjs/live-transcription/public/assets/pattern-bg.png b/dailyjs/live-transcription/public/assets/pattern-bg.png new file mode 100644 index 0000000..01e0d0d Binary files /dev/null and b/dailyjs/live-transcription/public/assets/pattern-bg.png differ