@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.
Files changed (131) hide show
  1. package/dist/cli.js +11 -9
  2. package/dist/components/Buttons/AjaxButtonCard/ui/AjaxButtonCard.d.ts +2 -2
  3. package/dist/components/Buttons/AjaxButtonCard/ui/AjaxButtonCard.d.ts.map +1 -1
  4. package/dist/components/Chats/ChatCard/ui/components/Markdown.d.ts +0 -1
  5. package/dist/components/Chats/ChatCard/ui/components/Markdown.d.ts.map +1 -1
  6. package/dist/components/Containers/AjaxGroupCard/ui/AjaxGroupCard.d.ts +2 -2
  7. package/dist/components/Containers/AjaxGroupCard/ui/AjaxGroupCard.d.ts.map +1 -1
  8. package/dist/components/Containers/SequenceCard/ui/SequenceCard.d.ts.map +1 -1
  9. package/dist/components/Containers/UnionCard/ui/UnionCard.d.ts.map +1 -1
  10. package/dist/components/PieCard/index.d.ts.map +1 -1
  11. package/dist/components/PieRoot/index.d.ts +1 -2
  12. package/dist/components/PieRoot/index.d.ts.map +1 -1
  13. package/dist/components/PieRoot/types/index.d.ts +3 -0
  14. package/dist/components/PieRoot/types/index.d.ts.map +1 -1
  15. package/dist/components/PieTelegramRoot/index.d.ts.map +1 -1
  16. package/dist/components/UI/index.d.ts.map +1 -1
  17. package/dist/components/index.esm.js +14 -26
  18. package/dist/components/index.js +23 -36
  19. package/dist/index.d.ts +1 -3
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.esm.js +20 -32
  22. package/dist/index.js +33 -46
  23. package/dist/providers/CentrifugeIOInitProvider.d.ts.map +1 -1
  24. package/dist/providers/SocketIOInitProvider.d.ts.map +1 -1
  25. package/dist/types/index.d.ts +7 -0
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/util/ajaxCommonUtils.d.ts.map +1 -1
  28. package/dist/util/centrifuge.d.ts +1 -1
  29. package/dist/util/centrifuge.d.ts.map +1 -1
  30. package/dist/util/initializeComponents.d.ts.map +1 -1
  31. package/dist/util/pieConfig.d.ts +12 -0
  32. package/dist/util/pieConfig.d.ts.map +1 -0
  33. package/dist/util/registry.d.ts.map +1 -1
  34. package/dist/util/socket.d.ts +1 -1
  35. package/dist/util/socket.d.ts.map +1 -1
  36. package/dist/util/useIsSupported.d.ts +1 -1
  37. package/dist/util/useIsSupported.d.ts.map +1 -1
  38. package/dist/util/useWebApp.d.ts +2 -2
  39. package/dist/util/useWebApp.d.ts.map +1 -1
  40. package/dist/util/waitForSidAvailable.d.ts +1 -2
  41. package/dist/util/waitForSidAvailable.d.ts.map +1 -1
  42. package/dist/util/webrtcClient.d.ts.map +1 -1
  43. package/package.json +21 -18
  44. package/src/components/Buttons/AjaxButtonCard/index.ts +1 -0
  45. package/src/components/Buttons/AjaxButtonCard/types/index.ts +17 -0
  46. package/src/components/Buttons/AjaxButtonCard/ui/AjaxButtonCard.tsx +38 -0
  47. package/src/components/Chats/ChatCard/index.ts +1 -0
  48. package/src/components/Chats/ChatCard/types/annyang.d.ts +11 -0
  49. package/src/components/Chats/ChatCard/types/index.ts +59 -0
  50. package/src/components/Chats/ChatCard/ui/ChatCard.tsx +130 -0
  51. package/src/components/Chats/ChatCard/ui/components/AttachFileButton.tsx +46 -0
  52. package/src/components/Chats/ChatCard/ui/components/AttachedFileView.tsx +29 -0
  53. package/src/components/Chats/ChatCard/ui/components/ChatCardInput.tsx +177 -0
  54. package/src/components/Chats/ChatCard/ui/components/ChatOption.tsx +25 -0
  55. package/src/components/Chats/ChatCard/ui/components/Markdown.tsx +25 -0
  56. package/src/components/Chats/ChatCard/ui/components/MessageAvatar.tsx +17 -0
  57. package/src/components/Chats/ChatCard/ui/components/MessageCard.tsx +80 -0
  58. package/src/components/Chats/ChatCard/ui/components/MessageContent.tsx +27 -0
  59. package/src/components/Chats/ChatCard/ui/components/MessagesBoard.tsx +61 -0
  60. package/src/components/Chats/ChatCard/ui/components/Options.tsx +20 -0
  61. package/src/components/Chats/ChatCard/ui/components/ResizableTextarea.tsx +59 -0
  62. package/src/components/Chats/ChatCard/ui/components/SendButton.tsx +37 -0
  63. package/src/components/Chats/ChatCard/ui/components/VoiceListeningButton.tsx +35 -0
  64. package/src/components/Chats/ChatCard/ui/components/icons/AttachFileIcon.tsx +18 -0
  65. package/src/components/Chats/ChatCard/ui/components/icons/AttachedFileIcon.tsx +18 -0
  66. package/src/components/Chats/ChatCard/ui/components/icons/CancelFileIcon.tsx +14 -0
  67. package/src/components/Chats/ChatCard/ui/components/icons/DefaultAvatar.tsx +10 -0
  68. package/src/components/Chats/ChatCard/ui/components/icons/SendIcon.tsx +18 -0
  69. package/src/components/Chats/ChatCard/ui/components/icons/VoiceRecordIcon.tsx +15 -0
  70. package/src/components/Containers/AjaxGroupCard/index.ts +1 -0
  71. package/src/components/Containers/AjaxGroupCard/types/index.ts +17 -0
  72. package/src/components/Containers/AjaxGroupCard/ui/AjaxGroupCard.tsx +96 -0
  73. package/src/components/Containers/SequenceCard/index.ts +1 -0
  74. package/src/components/Containers/SequenceCard/types/index.ts +10 -0
  75. package/src/components/Containers/SequenceCard/ui/SequenceCard.tsx +32 -0
  76. package/src/components/Containers/UnionCard/index.ts +1 -0
  77. package/src/components/Containers/UnionCard/types/index.ts +8 -0
  78. package/src/components/Containers/UnionCard/ui/UnionCard.tsx +27 -0
  79. package/src/components/PieCard/index.tsx +149 -0
  80. package/src/components/PieCard/types/index.ts +18 -0
  81. package/src/components/PieRoot/index.tsx +154 -0
  82. package/src/components/PieRoot/types/index.ts +14 -0
  83. package/src/components/PieTelegramRoot/index.tsx +161 -0
  84. package/src/components/UI/index.tsx +70 -0
  85. package/src/components/index.ts +6 -0
  86. package/src/index.ts +15 -0
  87. package/src/providers/CentrifugeIOInitProvider.tsx +42 -0
  88. package/src/providers/SocketIOInitProvider.tsx +52 -0
  89. package/src/types/index.ts +139 -0
  90. package/src/util/ajaxCommonUtils.ts +137 -0
  91. package/src/util/centrifuge.ts +33 -0
  92. package/src/util/fallback.tsx +6 -0
  93. package/src/util/initializeComponents.ts +84 -0
  94. package/src/util/lazy.ts +25 -0
  95. package/src/util/mitt.ts +11 -0
  96. package/src/util/pieConfig.ts +43 -0
  97. package/src/util/registry.ts +81 -0
  98. package/src/util/socket.ts +24 -0
  99. package/src/util/sx2radium.ts +15 -0
  100. package/src/util/tailwindCommonUtils.ts +6 -0
  101. package/src/util/useIsSupported.ts +17 -0
  102. package/src/util/useOpenAIWebRTC.ts +176 -0
  103. package/src/util/useWebApp.ts +32 -0
  104. package/src/util/waitForSidAvailable.ts +21 -0
  105. package/src/util/webrtcClient.ts +247 -0
  106. package/dist/cli.d.ts +0 -3
  107. package/dist/cli.d.ts.map +0 -1
  108. package/dist/components/PieBaseRoot/index.d.ts +0 -5
  109. package/dist/components/PieBaseRoot/index.d.ts.map +0 -1
  110. package/dist/components/PieBaseRoot/types/index.d.ts +0 -6
  111. package/dist/components/PieBaseRoot/types/index.d.ts.map +0 -1
  112. package/dist/components/PieStaticRoot/index.d.ts +0 -5
  113. package/dist/components/PieStaticRoot/index.d.ts.map +0 -1
  114. package/dist/components/PieStaticRoot/types/index.d.ts +0 -7
  115. package/dist/components/PieStaticRoot/types/index.d.ts.map +0 -1
  116. package/dist/config/constant.d.ts +0 -10
  117. package/dist/config/constant.d.ts.map +0 -1
  118. package/dist/util/axiosWithCache.d.ts +0 -3
  119. package/dist/util/axiosWithCache.d.ts.map +0 -1
  120. package/dist/util/globalForm.d.ts +0 -3
  121. package/dist/util/globalForm.d.ts.map +0 -1
  122. package/dist/util/queryClient.d.ts +0 -3
  123. package/dist/util/queryClient.d.ts.map +0 -1
  124. package/dist/util/useBodyStyles.d.ts +0 -5
  125. package/dist/util/useBodyStyles.d.ts.map +0 -1
  126. package/dist/util/useImage.d.ts +0 -5
  127. package/dist/util/useImage.d.ts.map +0 -1
  128. package/dist/util/useScreenSize.d.ts +0 -6
  129. package/dist/util/useScreenSize.d.ts.map +0 -1
  130. package/dist/util/useTitle.d.ts +0 -2
  131. 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
+ }
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ import mitt from 'mitt'
2
+ import { createContext } from 'react'
3
+
4
+ export const emitter = mitt()
5
+ export type MittEvents = {
6
+ [key: string]: any
7
+ }
8
+
9
+ const MittContext = createContext(emitter)
10
+
11
+ export default MittContext
@@ -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,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -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
+ }