Skip to content

Commit b1d1e55

Browse files
authored
chore: chat stub (#83)
1 parent 5c671de commit b1d1e55

File tree

8 files changed

+179
-0
lines changed

8 files changed

+179
-0
lines changed

frontend/src/components/body.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createComponent, Router, Shade } from '@furystack/shades'
22
import { cssVariableTheme } from '@furystack/shades-common-components'
33
import { Init, Login, Offline } from '../pages/index.js'
44
import { SessionService } from '../services/session.js'
5+
import { chatRoutes } from './routes/chat-routes.js'
56
import { dashboardRoutes } from './routes/dashboard-routes.js'
67
import { entityRoutes } from './routes/entity-routes.js'
78
import { fileBrowserRoutes } from './routes/file-browser-routes.js'
@@ -28,6 +29,7 @@ export const Body = Shade<{ style?: Partial<CSSStyleDeclaration> }>({
2829
...movieRoutes,
2930
...(hasAdminRole ? [...entityRoutes, ...fileBrowserRoutes, ...iotRoutes] : []),
3031
...dashboardRoutes,
32+
...chatRoutes,
3133
]}
3234
/>
3335
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createComponent, Shade } from '@furystack/shades'
2+
import { AppBarLink } from '@furystack/shades-common-components'
3+
import { SessionService } from '../../services/session.js'
4+
5+
export const ChatIcon = Shade({
6+
shadowDomName: 'shade-app-chat-icon',
7+
render: ({ injector, useObservable }) => {
8+
const session = injector.getInstance(SessionService)
9+
const [sessionState] = useObservable('sessionState', session.state)
10+
11+
if (sessionState !== 'authenticated') {
12+
return null
13+
}
14+
15+
return (
16+
<AppBarLink title="Chat" href="/chat">
17+
{sessionState === 'authenticated' ? '💬' : '🔒 Login to chat'}
18+
</AppBarLink>
19+
)
20+
},
21+
})

frontend/src/components/header.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createComponent, Shade } from '@furystack/shades'
22
import { AppBar, AppBarLink, Button } from '@furystack/shades-common-components'
33
import { environmentOptions } from '../environment-options.js'
44
import { SessionService } from '../services/session.js'
5+
import { ChatIcon } from './chat/chat-icon.js'
56
import { PiRatCommandPalette } from './command-palette/index.js'
67
import { GithubLogo } from './github-logo/index.js'
78
import { ThemeSwitch } from './theme-switch/index.js'
@@ -65,6 +66,7 @@ export const Header = Shade<HeaderProps>({
6566
>
6667
<GithubLogo style={{ height: '1rem' }} />
6768
</Button>
69+
<ChatIcon />
6870
{sessionState === 'authenticated' ? (
6971
<Button variant="outlined" onclick={() => injector.getInstance(SessionService).logout()}>
7072
Log Out
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createComponent } from '@furystack/shades'
2+
import { PiRatLazyLoad } from '../pirat-lazy-load.js'
3+
import { onLeave, onVisit } from './route-animations.js'
4+
5+
export const chatPageRoute = {
6+
url: '/chat',
7+
onVisit,
8+
onLeave,
9+
component: () => {
10+
return (
11+
<PiRatLazyLoad
12+
component={async () => {
13+
const { ChatPage } = await import('../../pages/chat/index.js')
14+
return <ChatPage />
15+
}}
16+
/>
17+
)
18+
},
19+
}
20+
21+
export const chatRoutes = [chatPageRoute] as const

frontend/src/pages/chat/index.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { createComponent, Shade } from '@furystack/shades'
2+
import { Button } from '@furystack/shades-common-components'
3+
import { SpeechRecognitionService } from './speech-recognition-service.js'
4+
import { SpeechSynthesisService } from './speech-synthesis-service.js'
5+
6+
export const ChatPage = Shade({
7+
shadowDomName: 'shade-app-chat-page',
8+
style: {
9+
marginTop: '64px',
10+
display: 'flex',
11+
width: '100%',
12+
flexWrap: 'wrap',
13+
justifyContent: 'center',
14+
},
15+
render: ({ injector }) => {
16+
const speechSynthesis = injector.getInstance(SpeechSynthesisService)
17+
18+
const speechRecognizer = injector.getInstance(SpeechRecognitionService)
19+
20+
return (
21+
<div>
22+
<h1>Chat Page</h1>
23+
<p>This is the chat page where users can interact with each other.</p>
24+
<Button
25+
onclick={() =>
26+
speechSynthesis.speak(
27+
'Továbbra is gondoskodunk gépjárműve biztosítási védelméről. Amennyiben elégedett a szolgáltatásunkkal, Önnek nincs teendője.',
28+
)
29+
}
30+
>
31+
💬 Speak
32+
</Button>
33+
34+
<Button
35+
onclick={async () => {
36+
const result = await speechRecognizer.recognizeSpeech()
37+
speechSynthesis.speak(`Azt mondtad: ${result}`)
38+
console.log('Recognized speech:', result)
39+
}}
40+
>
41+
🎤 Speak
42+
</Button>
43+
</div>
44+
)
45+
},
46+
})
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Injectable } from '@furystack/inject'
2+
import { Lock } from 'semaphore-async-await'
3+
4+
type SpeechRecognitionEvent = {
5+
results: SpeechRecognitionResultList
6+
}
7+
8+
type SpeechRecognitionResultList = {
9+
[index: number]: SpeechRecognitionResult
10+
length: number
11+
}
12+
13+
type SpeechRecognitionError = {
14+
error: string
15+
message: string
16+
}
17+
18+
declare class webkitSpeechRecognition {
19+
continuous: boolean
20+
interimResults: boolean
21+
lang: string
22+
public start(): void
23+
public stop(): void
24+
public onresult: (event: SpeechRecognitionEvent) => void
25+
public onerror: (event: SpeechRecognitionError) => void
26+
public onend: () => void
27+
}
28+
29+
@Injectable({ lifetime: 'singleton' })
30+
export class SpeechRecognitionService {
31+
public lock = new Lock()
32+
33+
public async recognizeSpeech(): Promise<string> {
34+
try {
35+
await this.lock.acquire()
36+
37+
const speechRecognition = new webkitSpeechRecognition()
38+
39+
return new Promise((resolve, reject) => {
40+
if (!speechRecognition) {
41+
reject(new Error('Speech recognition is not supported in this browser.'))
42+
return
43+
}
44+
45+
speechRecognition.lang = 'hu-HU'
46+
47+
speechRecognition.onresult = (event) => {
48+
if (event.results.length > 0) {
49+
resolve(event.results[0][0].transcript)
50+
} else {
51+
reject(new Error('No speech recognized.'))
52+
}
53+
}
54+
55+
speechRecognition.onerror = (event: SpeechRecognitionError) => {
56+
reject(new Error(`Speech recognition error: ${event.error}`))
57+
}
58+
59+
speechRecognition.onend = () => {
60+
console.log('Speech recognition ended.')
61+
}
62+
63+
speechRecognition.start()
64+
})
65+
} finally {
66+
this.lock.release()
67+
}
68+
}
69+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Injectable } from '@furystack/inject'
2+
3+
@Injectable({ lifetime: 'singleton' })
4+
export class SpeechSynthesisService {
5+
public speak(text: string) {
6+
if (window.speechSynthesis) {
7+
const utterance = new SpeechSynthesisUtterance(text)
8+
utterance.lang = 'hu-HU'
9+
utterance.voice = window.speechSynthesis.getVoices().find((voice) => voice.lang === 'hu-HU') || null
10+
window.speechSynthesis.speak(utterance)
11+
} else {
12+
console.warn('Speech synthesis is not supported in this browser.')
13+
}
14+
}
15+
}

frontend/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export default defineConfig(async () => {
1212
uploadToken: process.env.CODECOV_TOKEN,
1313
}),
1414
],
15+
server: {
16+
host: true,
17+
},
1518
build: {
1619
rollupOptions: {
1720
external: ['vitest'],

0 commit comments

Comments
 (0)