@piedata/pieui 1.0.1 → 1.1.0
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/dist/cli.js +11 -9
- package/dist/components/Buttons/AjaxButtonCard/ui/AjaxButtonCard.d.ts +2 -2
- package/dist/components/Buttons/AjaxButtonCard/ui/AjaxButtonCard.d.ts.map +1 -1
- package/dist/components/Chats/ChatCard/ui/components/Markdown.d.ts +0 -1
- package/dist/components/Chats/ChatCard/ui/components/Markdown.d.ts.map +1 -1
- package/dist/components/Containers/AjaxGroupCard/ui/AjaxGroupCard.d.ts +2 -2
- package/dist/components/Containers/AjaxGroupCard/ui/AjaxGroupCard.d.ts.map +1 -1
- package/dist/components/Containers/SequenceCard/ui/SequenceCard.d.ts.map +1 -1
- package/dist/components/Containers/UnionCard/ui/UnionCard.d.ts.map +1 -1
- package/dist/components/PieCard/index.d.ts.map +1 -1
- package/dist/components/PieRoot/index.d.ts +1 -2
- package/dist/components/PieRoot/index.d.ts.map +1 -1
- package/dist/components/PieRoot/types/index.d.ts +3 -0
- package/dist/components/PieRoot/types/index.d.ts.map +1 -1
- package/dist/components/PieTelegramRoot/index.d.ts.map +1 -1
- package/dist/components/UI/index.d.ts.map +1 -1
- package/dist/components/index.esm.js +14 -26
- package/dist/components/index.js +23 -36
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +20 -32
- package/dist/index.js +33 -46
- package/dist/providers/CentrifugeIOInitProvider.d.ts.map +1 -1
- package/dist/providers/SocketIOInitProvider.d.ts.map +1 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/util/ajaxCommonUtils.d.ts.map +1 -1
- package/dist/util/centrifuge.d.ts +1 -1
- package/dist/util/centrifuge.d.ts.map +1 -1
- package/dist/util/initializeComponents.d.ts.map +1 -1
- package/dist/util/pieConfig.d.ts +12 -0
- package/dist/util/pieConfig.d.ts.map +1 -0
- package/dist/util/registry.d.ts.map +1 -1
- package/dist/util/socket.d.ts +1 -1
- package/dist/util/socket.d.ts.map +1 -1
- package/dist/util/useIsSupported.d.ts +1 -1
- package/dist/util/useIsSupported.d.ts.map +1 -1
- package/dist/util/useWebApp.d.ts +2 -2
- package/dist/util/useWebApp.d.ts.map +1 -1
- package/dist/util/waitForSidAvailable.d.ts +1 -2
- package/dist/util/waitForSidAvailable.d.ts.map +1 -1
- package/dist/util/webrtcClient.d.ts.map +1 -1
- package/package.json +21 -18
- package/src/components/Buttons/AjaxButtonCard/index.ts +1 -0
- package/src/components/Buttons/AjaxButtonCard/types/index.ts +17 -0
- package/src/components/Buttons/AjaxButtonCard/ui/AjaxButtonCard.tsx +38 -0
- package/src/components/Chats/ChatCard/index.ts +1 -0
- package/src/components/Chats/ChatCard/types/annyang.d.ts +11 -0
- package/src/components/Chats/ChatCard/types/index.ts +59 -0
- package/src/components/Chats/ChatCard/ui/ChatCard.tsx +130 -0
- package/src/components/Chats/ChatCard/ui/components/AttachFileButton.tsx +46 -0
- package/src/components/Chats/ChatCard/ui/components/AttachedFileView.tsx +29 -0
- package/src/components/Chats/ChatCard/ui/components/ChatCardInput.tsx +177 -0
- package/src/components/Chats/ChatCard/ui/components/ChatOption.tsx +25 -0
- package/src/components/Chats/ChatCard/ui/components/Markdown.tsx +25 -0
- package/src/components/Chats/ChatCard/ui/components/MessageAvatar.tsx +17 -0
- package/src/components/Chats/ChatCard/ui/components/MessageCard.tsx +80 -0
- package/src/components/Chats/ChatCard/ui/components/MessageContent.tsx +27 -0
- package/src/components/Chats/ChatCard/ui/components/MessagesBoard.tsx +61 -0
- package/src/components/Chats/ChatCard/ui/components/Options.tsx +20 -0
- package/src/components/Chats/ChatCard/ui/components/ResizableTextarea.tsx +59 -0
- package/src/components/Chats/ChatCard/ui/components/SendButton.tsx +37 -0
- package/src/components/Chats/ChatCard/ui/components/VoiceListeningButton.tsx +35 -0
- package/src/components/Chats/ChatCard/ui/components/icons/AttachFileIcon.tsx +18 -0
- package/src/components/Chats/ChatCard/ui/components/icons/AttachedFileIcon.tsx +18 -0
- package/src/components/Chats/ChatCard/ui/components/icons/CancelFileIcon.tsx +14 -0
- package/src/components/Chats/ChatCard/ui/components/icons/DefaultAvatar.tsx +10 -0
- package/src/components/Chats/ChatCard/ui/components/icons/SendIcon.tsx +18 -0
- package/src/components/Chats/ChatCard/ui/components/icons/VoiceRecordIcon.tsx +15 -0
- package/src/components/Containers/AjaxGroupCard/index.ts +1 -0
- package/src/components/Containers/AjaxGroupCard/types/index.ts +17 -0
- package/src/components/Containers/AjaxGroupCard/ui/AjaxGroupCard.tsx +96 -0
- package/src/components/Containers/SequenceCard/index.ts +1 -0
- package/src/components/Containers/SequenceCard/types/index.ts +10 -0
- package/src/components/Containers/SequenceCard/ui/SequenceCard.tsx +32 -0
- package/src/components/Containers/UnionCard/index.ts +1 -0
- package/src/components/Containers/UnionCard/types/index.ts +8 -0
- package/src/components/Containers/UnionCard/ui/UnionCard.tsx +27 -0
- package/src/components/PieCard/index.tsx +149 -0
- package/src/components/PieCard/types/index.ts +18 -0
- package/src/components/PieRoot/index.tsx +154 -0
- package/src/components/PieRoot/types/index.ts +14 -0
- package/src/components/PieTelegramRoot/index.tsx +161 -0
- package/src/components/UI/index.tsx +70 -0
- package/src/components/index.ts +6 -0
- package/src/index.ts +15 -0
- package/src/providers/CentrifugeIOInitProvider.tsx +42 -0
- package/src/providers/SocketIOInitProvider.tsx +52 -0
- package/src/types/index.ts +139 -0
- package/src/util/ajaxCommonUtils.ts +137 -0
- package/src/util/centrifuge.ts +33 -0
- package/src/util/fallback.tsx +6 -0
- package/src/util/initializeComponents.ts +84 -0
- package/src/util/lazy.ts +25 -0
- package/src/util/mitt.ts +11 -0
- package/src/util/pieConfig.ts +43 -0
- package/src/util/registry.ts +81 -0
- package/src/util/socket.ts +24 -0
- package/src/util/sx2radium.ts +15 -0
- package/src/util/tailwindCommonUtils.ts +6 -0
- package/src/util/useIsSupported.ts +17 -0
- package/src/util/useOpenAIWebRTC.ts +176 -0
- package/src/util/useWebApp.ts +32 -0
- package/src/util/waitForSidAvailable.ts +21 -0
- package/src/util/webrtcClient.ts +247 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/components/PieBaseRoot/index.d.ts +0 -5
- package/dist/components/PieBaseRoot/index.d.ts.map +0 -1
- package/dist/components/PieBaseRoot/types/index.d.ts +0 -6
- package/dist/components/PieBaseRoot/types/index.d.ts.map +0 -1
- package/dist/components/PieStaticRoot/index.d.ts +0 -5
- package/dist/components/PieStaticRoot/index.d.ts.map +0 -1
- package/dist/components/PieStaticRoot/types/index.d.ts +0 -7
- package/dist/components/PieStaticRoot/types/index.d.ts.map +0 -1
- package/dist/config/constant.d.ts +0 -10
- package/dist/config/constant.d.ts.map +0 -1
- package/dist/util/axiosWithCache.d.ts +0 -3
- package/dist/util/axiosWithCache.d.ts.map +0 -1
- package/dist/util/globalForm.d.ts +0 -3
- package/dist/util/globalForm.d.ts.map +0 -1
- package/dist/util/queryClient.d.ts +0 -3
- package/dist/util/queryClient.d.ts.map +0 -1
- package/dist/util/useBodyStyles.d.ts +0 -5
- package/dist/util/useBodyStyles.d.ts.map +0 -1
- package/dist/util/useImage.d.ts +0 -5
- package/dist/util/useImage.d.ts.map +0 -1
- package/dist/util/useScreenSize.d.ts +0 -6
- package/dist/util/useScreenSize.d.ts.map +0 -1
- package/dist/util/useTitle.d.ts +0 -2
- package/dist/util/useTitle.d.ts.map +0 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { registerPieComponent } from './registry'
|
|
2
|
+
|
|
3
|
+
// Import all components that need registration
|
|
4
|
+
import SequenceCard from '../components/Containers/SequenceCard/ui/SequenceCard'
|
|
5
|
+
import UnionCard from '../components/Containers/UnionCard/ui/UnionCard'
|
|
6
|
+
import AjaxGroupCard from '../components/Containers/AjaxGroupCard/ui/AjaxGroupCard'
|
|
7
|
+
|
|
8
|
+
import AjaxButtonCard from '../components/Buttons/AjaxButtonCard/ui/AjaxButtonCard'
|
|
9
|
+
|
|
10
|
+
import ChatCard from '../components/Chats/ChatCard/ui/ChatCard'
|
|
11
|
+
|
|
12
|
+
let initialized = false
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize all PieUI components by registering them in the component registry.
|
|
16
|
+
* This function should be called once before using PieRoot or any dynamic components.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { initializePieComponents } from 'pieui'
|
|
21
|
+
*
|
|
22
|
+
* // Call this once in your app initialization
|
|
23
|
+
* initializePieComponents()
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function initializePieComponents(): void {
|
|
27
|
+
if (initialized) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Register all built-in components
|
|
32
|
+
registerPieComponent({
|
|
33
|
+
name: 'SequenceCard',
|
|
34
|
+
component: SequenceCard,
|
|
35
|
+
metadata: {
|
|
36
|
+
author: "PieData",
|
|
37
|
+
description: "Simple div with styles joining few components"
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
registerPieComponent({
|
|
42
|
+
name: 'UnionCard',
|
|
43
|
+
component: UnionCard,
|
|
44
|
+
metadata: {
|
|
45
|
+
author: "PieData",
|
|
46
|
+
description: "Renders one of many components"
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
registerPieComponent({
|
|
51
|
+
name: 'AjaxGroupCard',
|
|
52
|
+
component: AjaxGroupCard,
|
|
53
|
+
metadata: {
|
|
54
|
+
author: "PieData",
|
|
55
|
+
description: "Group card with AJAX support"
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
registerPieComponent({
|
|
60
|
+
name: 'AjaxButtonCard',
|
|
61
|
+
component: AjaxButtonCard,
|
|
62
|
+
metadata: {
|
|
63
|
+
author: "PieData",
|
|
64
|
+
description: "Button with AJAX support"
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
registerPieComponent({
|
|
69
|
+
name: 'ChatCard',
|
|
70
|
+
component: ChatCard,
|
|
71
|
+
metadata: {
|
|
72
|
+
author: "PieData",
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
initialized = true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if PieUI components have been initialized
|
|
81
|
+
*/
|
|
82
|
+
export function isPieComponentsInitialized(): boolean {
|
|
83
|
+
return initialized
|
|
84
|
+
}
|
package/src/util/lazy.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { lazy, ComponentType, LazyExoticComponent } from 'react'
|
|
2
|
+
|
|
3
|
+
const moduleCache = new Map<string, any>()
|
|
4
|
+
|
|
5
|
+
export function trackLazy<T extends ComponentType<any>>(
|
|
6
|
+
loader: () => Promise<{ default: T }>,
|
|
7
|
+
name: string,
|
|
8
|
+
): LazyExoticComponent<T> {
|
|
9
|
+
return lazy(() => {
|
|
10
|
+
if (moduleCache.has(name)) {
|
|
11
|
+
return moduleCache.get(name)!
|
|
12
|
+
}
|
|
13
|
+
const promise = loader().then((mod) => mod)
|
|
14
|
+
moduleCache.set(name, promise)
|
|
15
|
+
return promise
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const preloadComponent = async (name: string, loader?: () => Promise<any>) => {
|
|
20
|
+
if (!loader) return
|
|
21
|
+
if (moduleCache.has(name)) return moduleCache.get(name)
|
|
22
|
+
const promise = loader().then((mod) => mod)
|
|
23
|
+
moduleCache.set(name, promise)
|
|
24
|
+
return promise
|
|
25
|
+
}
|
package/src/util/mitt.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {createContext, useContext} from "react";
|
|
2
|
+
import {PieConfig} from "../types";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const PieConfigContext = createContext<PieConfig | null>(null)
|
|
6
|
+
|
|
7
|
+
export const usePieConfig = () => {
|
|
8
|
+
const context = useContext(PieConfigContext)
|
|
9
|
+
if (!context) {
|
|
10
|
+
throw new Error('usePieConfig must be used within PieConfigProvider')
|
|
11
|
+
}
|
|
12
|
+
return context
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Helper functions that match the old API
|
|
16
|
+
export const useApiServer = () => {
|
|
17
|
+
const { apiServer } = usePieConfig()
|
|
18
|
+
return apiServer
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getApiServer = useApiServer
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
export const useCentrifugeServer = () => {
|
|
25
|
+
const { centrifugeServer } = usePieConfig()
|
|
26
|
+
return centrifugeServer
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const getCentrifugeServer = useCentrifugeServer
|
|
30
|
+
|
|
31
|
+
export const useIsRenderingLogEnabled = () => {
|
|
32
|
+
const { enableRenderingLog } = usePieConfig()
|
|
33
|
+
return enableRenderingLog
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const isRenderingLogEnabled = useIsRenderingLogEnabled
|
|
37
|
+
|
|
38
|
+
export const usePageProcessor = () => {
|
|
39
|
+
const { pageProcessor } = usePieConfig()
|
|
40
|
+
return pageProcessor
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const getPageProcessor = usePageProcessor
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {ComponentMetadata, ComponentRegistration} from '../types'
|
|
2
|
+
import {trackLazy} from './lazy'
|
|
3
|
+
import {ComponentType} from 'react'
|
|
4
|
+
|
|
5
|
+
const registry = new Map<string, ComponentRegistration<any>>()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const normalizeRegistration = <TProps,>(
|
|
9
|
+
registration: ComponentRegistration<TProps>
|
|
10
|
+
): ComponentRegistration<TProps> => {
|
|
11
|
+
if (!registration.name) {
|
|
12
|
+
throw new Error('Component registration requires a name')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!registration.component && !registration.loader) {
|
|
16
|
+
throw new Error(`Component "${registration.name}" requires component or loader`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const entry: ComponentRegistration<TProps> = {
|
|
20
|
+
name: registration.name,
|
|
21
|
+
component: registration.component,
|
|
22
|
+
loader: registration.loader,
|
|
23
|
+
metadata: registration.metadata,
|
|
24
|
+
fallback: registration.fallback,
|
|
25
|
+
isLazy: false,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!entry.component && entry.loader) {
|
|
29
|
+
entry.component = trackLazy(
|
|
30
|
+
entry.loader,
|
|
31
|
+
registration.name
|
|
32
|
+
) as ComponentType<TProps>
|
|
33
|
+
entry.loader = undefined
|
|
34
|
+
entry.isLazy = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return entry
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export function registerPieComponent<TProps>(
|
|
42
|
+
registration: ComponentRegistration<TProps>
|
|
43
|
+
): ComponentType<TProps> | undefined {
|
|
44
|
+
const entry = normalizeRegistration(registration)
|
|
45
|
+
registry.set(entry.name, entry)
|
|
46
|
+
return entry.component
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
export const registerMultipleComponents = (
|
|
51
|
+
components: ComponentRegistration<any>[]
|
|
52
|
+
) => {
|
|
53
|
+
components.forEach((component) => registerPieComponent(component))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const unregisterComponent = (name: string) => {
|
|
57
|
+
registry.delete(name)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const hasComponent = (name: string) => {
|
|
61
|
+
return registry.has(name)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const getComponentMeta = (name: string): ComponentMetadata | undefined => {
|
|
65
|
+
return registry.get(name)?.metadata
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
export const getRegistryEntry = (
|
|
70
|
+
name: string
|
|
71
|
+
): ComponentRegistration<any> | undefined => {
|
|
72
|
+
return registry.get(name)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const getAllRegisteredComponents = (): string[] => {
|
|
76
|
+
return Array.from(registry.keys())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const getRegistrySize = (): number => {
|
|
80
|
+
return registry.size
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {io, Socket} from 'socket.io-client'
|
|
2
|
+
import { createContext } from 'react'
|
|
3
|
+
|
|
4
|
+
// export const socket = io(getApiServer(), {
|
|
5
|
+
// autoConnect: false,
|
|
6
|
+
// transports: ['websocket'],
|
|
7
|
+
// })
|
|
8
|
+
|
|
9
|
+
export const getSocket = (apiServer: string) => io(apiServer, {
|
|
10
|
+
autoConnect: false,
|
|
11
|
+
transports: ['websocket'],
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
const onPieInitEvent = (event) => {
|
|
16
|
+
window.sid = event.sid
|
|
17
|
+
console.log(`Connected: ${window.sid}`)
|
|
18
|
+
}
|
|
19
|
+
socket.on(`pieinit`, onPieInitEvent)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const SocketIOContext = createContext<Socket | null>(null)
|
|
23
|
+
|
|
24
|
+
export default SocketIOContext
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CSSProperties } from 'react'
|
|
2
|
+
import Radium from 'radium'
|
|
3
|
+
|
|
4
|
+
export function sx2radium(sx: Record<string, any> | CSSProperties | undefined): CSSProperties {
|
|
5
|
+
if (!sx) {
|
|
6
|
+
return {}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const copy = { ...sx }
|
|
10
|
+
if ('animationName' in copy && typeof copy.animationName === 'object') {
|
|
11
|
+
const uniqueAnimationName = 'radiumAnimation_' + Math.random().toString(36).substring(2, 8)
|
|
12
|
+
copy.animationName = Radium.keyframes(copy.animationName, uniqueAnimationName)
|
|
13
|
+
}
|
|
14
|
+
return copy
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useIsSupported(apiServer: string, name: string): boolean | null {
|
|
4
|
+
const [isSupported, setIsSupported] = useState<boolean | null>(null)
|
|
5
|
+
const [supportIsRequested, setSupportIsRequested] = useState(false)
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!supportIsRequested) {
|
|
8
|
+
setSupportIsRequested(true)
|
|
9
|
+
fetch(apiServer + `api/support/${name}`, { method: 'GET' })
|
|
10
|
+
.then((res) => res.json())
|
|
11
|
+
.then((res) => {
|
|
12
|
+
setIsSupported(res)
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
}, [])
|
|
16
|
+
return isSupported
|
|
17
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export type OpenAIEvent = {
|
|
4
|
+
type: string
|
|
5
|
+
event_id?: string
|
|
6
|
+
timestamp?: string
|
|
7
|
+
[key: string]: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type UseOpenAIWebRTCReturn = {
|
|
11
|
+
isSessionActive: boolean
|
|
12
|
+
startSession: (ephemeralKey: string, useMicrophone?: boolean) => Promise<void>
|
|
13
|
+
stopSession: () => void
|
|
14
|
+
sendTextMessage: (text: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Создаём немой аудио-трек для случаев без микрофона
|
|
18
|
+
function createSilentAudioTrack(): MediaStreamTrack {
|
|
19
|
+
const ctx = new AudioContext()
|
|
20
|
+
const oscillator = ctx.createOscillator()
|
|
21
|
+
const dst = oscillator.connect(ctx.createMediaStreamDestination())
|
|
22
|
+
oscillator.start()
|
|
23
|
+
|
|
24
|
+
const track = (dst as any).stream.getAudioTracks()[0]
|
|
25
|
+
track.enabled = false
|
|
26
|
+
return track
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function useOpenAIWebRTC(
|
|
30
|
+
audioElement: HTMLAudioElement | null = null,
|
|
31
|
+
onEvent?: (event: OpenAIEvent) => void
|
|
32
|
+
): UseOpenAIWebRTCReturn {
|
|
33
|
+
const [isSessionActive, setIsSessionActive] = useState<boolean>(false)
|
|
34
|
+
const peerConnection = useRef<RTCPeerConnection | null>(null)
|
|
35
|
+
const dataChannelRef = useRef<RTCDataChannel | null>(null)
|
|
36
|
+
const waitForDataChannelOpen = useRef<Promise<void> | null>(null)
|
|
37
|
+
|
|
38
|
+
const startSession = useCallback(async (ephemeralKey: string, useMicrophone: boolean = true) => {
|
|
39
|
+
if (peerConnection.current) stopSession()
|
|
40
|
+
|
|
41
|
+
const pc = new RTCPeerConnection()
|
|
42
|
+
|
|
43
|
+
// Обновляем isSessionActive по реальному состоянию соединения
|
|
44
|
+
pc.onconnectionstatechange = () => {
|
|
45
|
+
if (pc.connectionState === 'connected') {
|
|
46
|
+
setIsSessionActive(true)
|
|
47
|
+
} else if (['disconnected', 'failed', 'closed'].includes(pc.connectionState)) {
|
|
48
|
+
setIsSessionActive(false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pc.ontrack = (e: RTCTrackEvent) => {
|
|
53
|
+
if (audioElement) audioElement.srcObject = e.streams[0]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (useMicrophone) {
|
|
57
|
+
const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
58
|
+
pc.addTrack(mediaStream.getTracks()[0], mediaStream)
|
|
59
|
+
} else {
|
|
60
|
+
const silentTrack = createSilentAudioTrack()
|
|
61
|
+
pc.addTrack(silentTrack)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dc = pc.createDataChannel('oai-events')
|
|
65
|
+
dataChannelRef.current = dc
|
|
66
|
+
|
|
67
|
+
// Promise ждёт открытия DataChannel
|
|
68
|
+
waitForDataChannelOpen.current = new Promise<void>((resolve) => {
|
|
69
|
+
dc.addEventListener('open', () => {
|
|
70
|
+
resolve()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const offer = await pc.createOffer()
|
|
75
|
+
await pc.setLocalDescription(offer)
|
|
76
|
+
|
|
77
|
+
const baseUrl = 'https://api.openai.com/v1/realtime/calls'
|
|
78
|
+
const model = 'gpt-realtime'
|
|
79
|
+
const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
body: offer.sdp,
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${ephemeralKey}`,
|
|
84
|
+
'Content-Type': 'application/sdp',
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const sdp = await sdpResponse.text()
|
|
89
|
+
await pc.setRemoteDescription({ type: 'answer', sdp })
|
|
90
|
+
|
|
91
|
+
peerConnection.current = pc
|
|
92
|
+
|
|
93
|
+
// ждём открытия DataChannel, чтобы можно было безопасно отправлять сообщения
|
|
94
|
+
await waitForDataChannelOpen.current
|
|
95
|
+
}, [audioElement])
|
|
96
|
+
|
|
97
|
+
const stopSession = useCallback(() => {
|
|
98
|
+
const dc = dataChannelRef.current
|
|
99
|
+
if (dc) dc.close()
|
|
100
|
+
|
|
101
|
+
peerConnection.current?.getSenders()?.forEach(sender => sender.track?.stop())
|
|
102
|
+
peerConnection.current?.close()
|
|
103
|
+
|
|
104
|
+
setIsSessionActive(false)
|
|
105
|
+
peerConnection.current = null
|
|
106
|
+
dataChannelRef.current = null
|
|
107
|
+
waitForDataChannelOpen.current = null
|
|
108
|
+
}, [audioElement])
|
|
109
|
+
|
|
110
|
+
const sendClientEvent = useCallback((message: OpenAIEvent) => {
|
|
111
|
+
const dc = dataChannelRef.current
|
|
112
|
+
if (!dc) {
|
|
113
|
+
console.error('Data channel is not ready', message)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const timestamp = new Date().toLocaleTimeString()
|
|
118
|
+
message.event_id = message.event_id || crypto.randomUUID()
|
|
119
|
+
|
|
120
|
+
dc.send(JSON.stringify(message))
|
|
121
|
+
|
|
122
|
+
if (!message.timestamp) message.timestamp = timestamp
|
|
123
|
+
onEvent?.(message)
|
|
124
|
+
}, [onEvent, audioElement])
|
|
125
|
+
|
|
126
|
+
const sendTextMessage = useCallback((text: string) => {
|
|
127
|
+
const dc = dataChannelRef.current
|
|
128
|
+
if (!dc) {
|
|
129
|
+
console.error('Data channel is not ready')
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const send = () => {
|
|
134
|
+
const event: OpenAIEvent = {
|
|
135
|
+
type: 'conversation.item.create',
|
|
136
|
+
item: {
|
|
137
|
+
type: 'message',
|
|
138
|
+
role: 'user',
|
|
139
|
+
content: [{ type: 'input_text', text }],
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
sendClientEvent(event)
|
|
143
|
+
sendClientEvent({ type: 'response.create' })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Ждём открытия канала, если ещё не открылся
|
|
147
|
+
if (waitForDataChannelOpen.current) {
|
|
148
|
+
waitForDataChannelOpen.current.then(send)
|
|
149
|
+
} else {
|
|
150
|
+
send()
|
|
151
|
+
}
|
|
152
|
+
}, [onEvent, audioElement])
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const dc = dataChannelRef.current
|
|
156
|
+
if (!dc) return
|
|
157
|
+
|
|
158
|
+
const handleMessage = (e: MessageEvent) => {
|
|
159
|
+
const event: OpenAIEvent = JSON.parse(e.data)
|
|
160
|
+
if (!event.timestamp) event.timestamp = new Date().toLocaleTimeString()
|
|
161
|
+
onEvent?.(event)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
dc.addEventListener('message', handleMessage)
|
|
165
|
+
return () => {
|
|
166
|
+
dc.removeEventListener('message', handleMessage)
|
|
167
|
+
}
|
|
168
|
+
}, [onEvent, audioElement])
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
isSessionActive,
|
|
172
|
+
startSession,
|
|
173
|
+
stopSession,
|
|
174
|
+
sendTextMessage,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { getPageProcessor } from './pieConfig'
|
|
3
|
+
import { InitData, InitDataUnsafe, WebApp } from '../types'
|
|
4
|
+
|
|
5
|
+
export const useWebApp = (): WebApp | null => {
|
|
6
|
+
const [webApp, setWebApp] = useState<WebApp | null>(null)
|
|
7
|
+
const pageProcessor = getPageProcessor()
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (typeof window === 'undefined') return
|
|
11
|
+
|
|
12
|
+
const wApp = window.Telegram.WebApp
|
|
13
|
+
wApp.ready()
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
pageProcessor === 'telegram_expanded' &&
|
|
17
|
+
(wApp.platform === 'ios' || wApp.platform === 'android')
|
|
18
|
+
) {
|
|
19
|
+
wApp.expand()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setWebApp(wApp)
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
return webApp
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useInitData = (): readonly [InitDataUnsafe | undefined, InitData | undefined] => {
|
|
29
|
+
const webApp = useWebApp()
|
|
30
|
+
|
|
31
|
+
return [webApp?.initDataUnsafe, webApp?.initData] as const
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export default function waitForSidAvailable(interval = 1000) {
|
|
4
|
+
return new Promise<void>((resolve) => {
|
|
5
|
+
if (typeof window === 'undefined') {
|
|
6
|
+
// На сервере просто резолвим сразу или кидаем ошибку
|
|
7
|
+
resolve()
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const check = () => {
|
|
12
|
+
if (typeof window.sid !== 'undefined') {
|
|
13
|
+
resolve()
|
|
14
|
+
} else {
|
|
15
|
+
setTimeout(check, interval)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
check()
|
|
20
|
+
})
|
|
21
|
+
}
|