@kontextso/sdk-react-native 3.0.7-rc.2 → 4.0.0-rc.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/index.d.mts +167 -4
- package/dist/index.d.ts +167 -4
- package/dist/index.js +468 -615
- package/dist/index.mjs +466 -612
- package/package.json +7 -9
- package/src/AbstractStream.ts +141 -0
- package/src/Configuration.ts +129 -0
- package/src/InlineAd.tsx +168 -0
- package/src/KontextAds.ts +40 -0
- package/src/Logger.ts +95 -0
- package/src/NativeStream.ts +20 -0
- package/src/Preload.ts +150 -0
- package/src/Session.ts +104 -0
- package/src/index.ts +2 -4
- package/src/utils/device.ts +89 -0
- package/src/utils/request.ts +73 -0
- package/src/utils/sdk.ts +16 -0
- package/src/utils/validation.ts +59 -0
- package/android/build.gradle +0 -88
- package/android/gradle.properties +0 -5
- package/android/src/main/AndroidManifest.xml +0 -2
- package/android/src/main/java/so/kontext/react/RNKontextModuleImpl.kt +0 -21
- package/android/src/newarch/java/so/kontext/react/RNKontextModule.kt +0 -22
- package/android/src/newarch/java/so/kontext/react/RNKontextPackage.kt +0 -32
- package/android/src/oldarch/java/so/kontext/react/RNKontextModule.kt +0 -25
- package/android/src/oldarch/java/so/kontext/react/RNKontextPacakge.kt +0 -16
- package/ios/KontextSDK.swift +0 -26
- package/ios/RNKontext.h +0 -13
- package/ios/RNKontext.mm +0 -36
- package/src/__tests__/util.test.ts +0 -9
- package/src/context/AdsProvider.tsx +0 -119
- package/src/formats/Format.tsx +0 -591
- package/src/formats/InlineAd.tsx +0 -8
- package/src/frame-webview.tsx +0 -43
package/src/Preload.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Message } from '@kontextso/sdk-common'
|
|
2
|
+
import type{ Session } from './Session'
|
|
3
|
+
import { fetchWithRetry } from './utils/request'
|
|
4
|
+
|
|
5
|
+
type Bid = {
|
|
6
|
+
bidId: string
|
|
7
|
+
code: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface PreloadResponse {
|
|
11
|
+
bids?: Bid[]
|
|
12
|
+
sessionId?: string
|
|
13
|
+
skipCode?: string
|
|
14
|
+
skip?: boolean
|
|
15
|
+
|
|
16
|
+
// Error state
|
|
17
|
+
error?: string
|
|
18
|
+
errCode?: string
|
|
19
|
+
permanent?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Preload {
|
|
23
|
+
public readonly session: Session
|
|
24
|
+
|
|
25
|
+
private messages: Message[] = []
|
|
26
|
+
private abortController: AbortController
|
|
27
|
+
private running = false
|
|
28
|
+
private bid: Bid | null = null
|
|
29
|
+
|
|
30
|
+
constructor(session: Session, messages: Message[]) {
|
|
31
|
+
this.session = session
|
|
32
|
+
this.abortController = new AbortController()
|
|
33
|
+
this.setMessages(messages)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private setMessages (messages: Message[]) {
|
|
37
|
+
this.messages = messages
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
hasBid () {
|
|
41
|
+
return this.bid !== null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getBid () {
|
|
45
|
+
return this.bid
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private setBid (bid: Bid) {
|
|
49
|
+
this.bid = bid
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
cancel () {
|
|
53
|
+
this.abortController.abort()
|
|
54
|
+
this.running = false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
isRunning () {
|
|
58
|
+
return this.running
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
private async getDevice () {
|
|
63
|
+
try {
|
|
64
|
+
return await getDevice()
|
|
65
|
+
} catch (error) {
|
|
66
|
+
this.session.logger.error('Error getting device:', error)
|
|
67
|
+
return {} as DeviceConfig
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
private async getPreloadBody () {
|
|
73
|
+
const config = this.session.config
|
|
74
|
+
const preloadBody: Record<string, any> = {
|
|
75
|
+
messages: this.messages,
|
|
76
|
+
sessionId: this.session.getSessionId(),
|
|
77
|
+
publisherToken: config.publisherToken,
|
|
78
|
+
userId: config.userId,
|
|
79
|
+
userEmail: config.userEmail,
|
|
80
|
+
conversationId: config.conversationId,
|
|
81
|
+
character: config.character,
|
|
82
|
+
enabledPlacementCodes: [config.placementCode],
|
|
83
|
+
variantId: config.variantId,
|
|
84
|
+
regulatory: config.regulatory,
|
|
85
|
+
sdk: this.session.sdk,
|
|
86
|
+
}
|
|
87
|
+
return preloadBody
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private canRequestAd () {
|
|
91
|
+
if (this.session.isSessionDisabled()) {
|
|
92
|
+
return {
|
|
93
|
+
status: 'error',
|
|
94
|
+
message: 'Session is disabled',
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!this.messages.length) {
|
|
98
|
+
return {
|
|
99
|
+
status: 'error',
|
|
100
|
+
message: 'No messages',
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async requestAd () {
|
|
107
|
+
this.running = true
|
|
108
|
+
const cannotRequestAdReason = this.canRequestAd()
|
|
109
|
+
if (cannotRequestAdReason) {
|
|
110
|
+
return cannotRequestAdReason
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const config = this.session.config
|
|
114
|
+
const preloadBody = await this.getPreloadBody()
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetchWithRetry(`${config.adServerUrl}/preload`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
body: JSON.stringify(preloadBody),
|
|
120
|
+
headers: {
|
|
121
|
+
'Kontextso-Is-Disabled': '0',
|
|
122
|
+
'Kontextso-Publisher-Token': config.publisherToken,
|
|
123
|
+
},
|
|
124
|
+
timeout: 16000,
|
|
125
|
+
abortController: this.abortController,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const jsonResponse = await response.json() as PreloadResponse
|
|
129
|
+
|
|
130
|
+
const bid = jsonResponse.bids?.[0] ?? null
|
|
131
|
+
if (jsonResponse.sessionId) {
|
|
132
|
+
this.session.setSessionId(jsonResponse.sessionId)
|
|
133
|
+
}
|
|
134
|
+
if (bid) {
|
|
135
|
+
this.setBid(bid)
|
|
136
|
+
}
|
|
137
|
+
this.session.updateBids()
|
|
138
|
+
return {
|
|
139
|
+
status: 'success',
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.session.logger.error('Error requesting ad:', error)
|
|
143
|
+
return {
|
|
144
|
+
status: 'error',
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
this.running = false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/Session.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Message } from '@kontextso/sdk-common'
|
|
2
|
+
import type { Configuration } from './Configuration'
|
|
3
|
+
import { Logger } from './Logger'
|
|
4
|
+
import { Preload } from './Preload'
|
|
5
|
+
import type { SDKConfig } from './utils/sdk'
|
|
6
|
+
|
|
7
|
+
export class Session {
|
|
8
|
+
public readonly config: Configuration
|
|
9
|
+
public readonly logger: Logger
|
|
10
|
+
public readonly sdk: SDKConfig
|
|
11
|
+
|
|
12
|
+
private sessionId: string | null = null
|
|
13
|
+
private sessionDisabled = false
|
|
14
|
+
private messages: Message[] = []
|
|
15
|
+
private bids: {
|
|
16
|
+
bidId: string
|
|
17
|
+
messageId: string
|
|
18
|
+
}[] = []
|
|
19
|
+
private onUpdateBidsCallback: (() => void) | null = null
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// only one preload instance is allowed at a time
|
|
23
|
+
private preloadInstance: Preload | null = null
|
|
24
|
+
|
|
25
|
+
constructor(config: Configuration, { sdk }: { sdk: SDKConfig }) {
|
|
26
|
+
this.config = config
|
|
27
|
+
this.logger = new Logger()
|
|
28
|
+
this.sdk = sdk
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setOnUpdateBids (callback: () => void) {
|
|
32
|
+
this.onUpdateBidsCallback = callback
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isSessionDisabled () {
|
|
36
|
+
return this.sessionDisabled
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setSessionDisabled (disabled: boolean) {
|
|
40
|
+
this.sessionDisabled = disabled
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getSessionId () {
|
|
44
|
+
return this.sessionId
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setSessionId (sessionId: string) {
|
|
48
|
+
this.logger.info('Session ID set:', sessionId)
|
|
49
|
+
this.sessionId = sessionId
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addMessage (message: Message) {
|
|
53
|
+
this.messages.push(message)
|
|
54
|
+
this.updateBids()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getMessages () {
|
|
58
|
+
return this.messages
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public updateBids() {
|
|
62
|
+
if (!this.preloadInstance?.hasBid()) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const bid = this.preloadInstance.getBid()
|
|
66
|
+
if (!bid) {
|
|
67
|
+
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const lastMessage = this.messages[this.messages.length - 1]
|
|
71
|
+
if (!lastMessage || lastMessage.role !== 'assistant') {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const assignedBid = this.bids.find((b) => {
|
|
75
|
+
if (b.bidId === bid?.bidId) {
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
if (b.messageId === lastMessage.id) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
return false
|
|
82
|
+
})
|
|
83
|
+
if (assignedBid) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
this.bids.push({
|
|
87
|
+
bidId: bid.bidId,
|
|
88
|
+
messageId: lastMessage.id,
|
|
89
|
+
})
|
|
90
|
+
this.onUpdateBidsCallback?.()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
preload () {
|
|
94
|
+
if (this.preloadInstance?.isRunning()) {
|
|
95
|
+
this.preloadInstance.cancel()
|
|
96
|
+
}
|
|
97
|
+
this.preloadInstance = new Preload(this, [...this.messages])
|
|
98
|
+
return this.preloadInstance
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getLastBid () {
|
|
102
|
+
return this.bids[this.bids.length - 1]
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
|
|
2
|
+
import { detectDeviceType, detectOS, parseUserAgent } from '@kontextso/utils/user-agent'
|
|
3
|
+
|
|
4
|
+
interface DeviceHardware {
|
|
5
|
+
type: 'handset' | 'tablet' | 'desktop' | 'tv' | 'other'
|
|
6
|
+
bootTime?: number
|
|
7
|
+
brand?: string
|
|
8
|
+
model?: string
|
|
9
|
+
sdCardAvailable?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface OperatingSystem {
|
|
13
|
+
name: string
|
|
14
|
+
version: string
|
|
15
|
+
locale: string
|
|
16
|
+
timezone: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ScreenInfo {
|
|
20
|
+
darkMode: boolean
|
|
21
|
+
dpr: number
|
|
22
|
+
height: number
|
|
23
|
+
width: number
|
|
24
|
+
orientation?: 'portrait' | 'landscape'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AudioInfo {
|
|
28
|
+
volume?: number
|
|
29
|
+
muted?: boolean
|
|
30
|
+
outputPluggedIn?: boolean
|
|
31
|
+
outputType?: Array<'wired' | 'hdmi' | 'bluetooth' | 'usb' | 'other'>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface NetworkInfo {
|
|
35
|
+
carrier?: string
|
|
36
|
+
detail?: string
|
|
37
|
+
type?: 'wifi' | 'cellular' | 'ethernet' | 'other'
|
|
38
|
+
userAgent?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface PowerInfo {
|
|
42
|
+
batteryLevel?: number
|
|
43
|
+
batteryState?: 'charging' | 'full' | 'unplugged' | 'unknown'
|
|
44
|
+
lowPowerMode?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DeviceConfig {
|
|
48
|
+
hardware: DeviceHardware
|
|
49
|
+
os: OperatingSystem
|
|
50
|
+
screen: ScreenInfo
|
|
51
|
+
audio?: AudioInfo
|
|
52
|
+
network?: NetworkInfo
|
|
53
|
+
power?: PowerInfo
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const getDevice = async (): Promise<DeviceConfig> => {
|
|
57
|
+
const userAgent = navigator.userAgent
|
|
58
|
+
const parsedUserAgent = parseUserAgent(userAgent)
|
|
59
|
+
const parsedDevice = parsedUserAgent?.getDevice()
|
|
60
|
+
const parsedOS = parsedUserAgent?.getOS()
|
|
61
|
+
// @ts-expect-error Not in types
|
|
62
|
+
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
network: {
|
|
66
|
+
userAgent,
|
|
67
|
+
detail: connection?.effectiveType,
|
|
68
|
+
type: connection?.type,
|
|
69
|
+
},
|
|
70
|
+
screen: {
|
|
71
|
+
width: screen.width,
|
|
72
|
+
height: screen.height,
|
|
73
|
+
dpr: window.devicePixelRatio || 1,
|
|
74
|
+
orientation: screen.orientation?.type?.includes('portrait') ? 'portrait' : 'landscape',
|
|
75
|
+
darkMode: window.matchMedia?.('(prefers-color-scheme: dark)').matches,
|
|
76
|
+
},
|
|
77
|
+
hardware: {
|
|
78
|
+
type: detectDeviceType(parsedUserAgent),
|
|
79
|
+
brand: parsedDevice?.vendor,
|
|
80
|
+
model: parsedDevice?.model,
|
|
81
|
+
},
|
|
82
|
+
os: {
|
|
83
|
+
name: detectOS(parsedUserAgent),
|
|
84
|
+
version: parsedOS?.version || '',
|
|
85
|
+
locale: navigator.language || 'en-US',
|
|
86
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request function with retry and exponential backoff
|
|
3
|
+
*/
|
|
4
|
+
export async function fetchWithRetry(
|
|
5
|
+
url: string,
|
|
6
|
+
options: RequestInit & {
|
|
7
|
+
retry?: {
|
|
8
|
+
maxRetries?: number
|
|
9
|
+
baseDelay?: number
|
|
10
|
+
backoffFactor?: number
|
|
11
|
+
}
|
|
12
|
+
timeout?: number
|
|
13
|
+
abortController?: AbortController
|
|
14
|
+
} = {}
|
|
15
|
+
): Promise<Response> {
|
|
16
|
+
const { retry = {}, timeout, abortController: abortControllerOption, ...fetchOptions } = options
|
|
17
|
+
|
|
18
|
+
const maxRetries = retry.maxRetries ?? 3
|
|
19
|
+
const baseDelay = retry.baseDelay ?? 1000
|
|
20
|
+
const backoffFactor = retry.backoffFactor ?? 2
|
|
21
|
+
|
|
22
|
+
const abortController = abortControllerOption || new AbortController()
|
|
23
|
+
fetchOptions.signal = abortController.signal
|
|
24
|
+
|
|
25
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
26
|
+
if (timeout) {
|
|
27
|
+
timeoutId = setTimeout(() => abortController.abort('timeout'), timeout)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let lastError: Error | null = null
|
|
31
|
+
|
|
32
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(url, fetchOptions)
|
|
35
|
+
|
|
36
|
+
// Throw only re-tryable errors, otherwise return the response to let the caller handle it
|
|
37
|
+
if (response.status >= 500 || response.status === 429) {
|
|
38
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
39
|
+
;(error as any).status = response.status
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return response
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (abortController.signal.aborted) {
|
|
46
|
+
throw new Error(`Fetch aborted: ${abortController.signal.reason}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lastError = error instanceof Error ? error : new Error('Unknown error')
|
|
50
|
+
|
|
51
|
+
// Retry on network errors and 5xx status codes
|
|
52
|
+
const shouldRetry =
|
|
53
|
+
error instanceof TypeError || // Network error
|
|
54
|
+
(error as any).status >= 500 || // Server error
|
|
55
|
+
(error as any).status === 429 // Rate limited
|
|
56
|
+
|
|
57
|
+
// Don't retry on last attempt or if error shouldn't be retried
|
|
58
|
+
if (attempt === maxRetries || !shouldRetry) {
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Wait before retry (exponential backoff)
|
|
63
|
+
const delay = baseDelay * backoffFactor ** attempt
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
65
|
+
} finally {
|
|
66
|
+
if (timeoutId) {
|
|
67
|
+
clearTimeout(timeoutId)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw lastError
|
|
73
|
+
}
|
package/src/utils/sdk.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
import { Platform } from '@kontextso/utils/user-agent'
|
|
3
|
+
import type { SDK } from '@kontextso/sdk-common'
|
|
4
|
+
import { version } from '../../package.json'
|
|
5
|
+
|
|
6
|
+
export interface SDKConfig {
|
|
7
|
+
name: Exclude<SDK, 'sdk'>
|
|
8
|
+
platform: 'ios' | 'android' | 'web'
|
|
9
|
+
version: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const getSdk = async (): Promise<SDKConfig> => ({
|
|
13
|
+
name: 'sdk-js',
|
|
14
|
+
platform: Platform.Web,
|
|
15
|
+
version,
|
|
16
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { FetchAdParams } from "./interface"
|
|
2
|
+
|
|
3
|
+
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
4
|
+
|
|
5
|
+
export const validateFetchAdSettings = (settings: FetchAdParams) => {
|
|
6
|
+
if (!settings.adServerUrl) {
|
|
7
|
+
throw new Error('Server URL is required.')
|
|
8
|
+
}
|
|
9
|
+
if (!settings.publisherToken) {
|
|
10
|
+
throw new Error('Publisher token is required.')
|
|
11
|
+
}
|
|
12
|
+
if (!settings.conversationId) {
|
|
13
|
+
throw new Error('Conversation ID is required.')
|
|
14
|
+
}
|
|
15
|
+
if (typeof settings.conversationId !== 'string') {
|
|
16
|
+
throw new Error('Conversation ID must be a string.')
|
|
17
|
+
}
|
|
18
|
+
if (!settings.userId) {
|
|
19
|
+
throw new Error('User ID is required.')
|
|
20
|
+
}
|
|
21
|
+
if (typeof settings.userId !== 'string') {
|
|
22
|
+
throw new Error('User ID must be a string.')
|
|
23
|
+
}
|
|
24
|
+
if (!settings.enabledPlacementCodes || !Array.isArray(settings.enabledPlacementCodes) || settings.enabledPlacementCodes.length === 0) {
|
|
25
|
+
throw new Error('Placement code is required.')
|
|
26
|
+
}
|
|
27
|
+
if (!settings.messages || !Array.isArray(settings.messages) || settings.messages.length === 0) {
|
|
28
|
+
throw new Error('Messages are required.')
|
|
29
|
+
}
|
|
30
|
+
settings.messages.forEach((message: any) => {
|
|
31
|
+
if (!message.id) {
|
|
32
|
+
throw new Error('Message ID is required.')
|
|
33
|
+
}
|
|
34
|
+
if (typeof message.id !== 'string') {
|
|
35
|
+
throw new Error('Message ID must be a string.')
|
|
36
|
+
}
|
|
37
|
+
if (!message.content) {
|
|
38
|
+
throw new Error('Message content is required.')
|
|
39
|
+
}
|
|
40
|
+
if (!message.createdAt) {
|
|
41
|
+
throw new Error('Message createdAt is required.')
|
|
42
|
+
}
|
|
43
|
+
if (typeof message.createdAt !== 'string' && !(message.createdAt instanceof Date)) {
|
|
44
|
+
throw new Error('Message createdAt must be string or Date object.')
|
|
45
|
+
}
|
|
46
|
+
if (typeof message.createdAt === 'string' && !DATE_REGEX.test(message.createdAt)) {
|
|
47
|
+
throw new Error('Message createdAt must be a valid ISO 8601 date string.')
|
|
48
|
+
}
|
|
49
|
+
if (message.createdAt instanceof Date && Number.isNaN(message.createdAt.getTime())) {
|
|
50
|
+
throw new Error('Message createdAt must be a valid Date object.')
|
|
51
|
+
}
|
|
52
|
+
if (!message.role) {
|
|
53
|
+
throw new Error('Message role is required.')
|
|
54
|
+
}
|
|
55
|
+
if (message.role !== 'assistant' && message.role !== 'user') {
|
|
56
|
+
throw new Error('Message role must be either "assistant" or "user".')
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
package/android/build.gradle
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
buildscript {
|
|
2
|
-
ext.getExtOrDefault = {name ->
|
|
3
|
-
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['RNKontext_' + name]
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
repositories {
|
|
7
|
-
google()
|
|
8
|
-
mavenCentral()
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
dependencies {
|
|
12
|
-
classpath "com.android.tools.build:gradle:8.7.2"
|
|
13
|
-
// noinspection DifferentKotlinGradleVersion
|
|
14
|
-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
apply plugin: "com.android.library"
|
|
20
|
-
apply plugin: "kotlin-android"
|
|
21
|
-
|
|
22
|
-
apply plugin: "com.facebook.react"
|
|
23
|
-
|
|
24
|
-
def isNewArchitectureEnabled() {
|
|
25
|
-
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
def getExtOrIntegerDefault(name) {
|
|
29
|
-
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["RNKontext_" + name]).toInteger()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
android {
|
|
33
|
-
namespace "so.kontext.react"
|
|
34
|
-
|
|
35
|
-
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
36
|
-
|
|
37
|
-
defaultConfig {
|
|
38
|
-
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
39
|
-
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
40
|
-
// Expose the new-arch flag to runtime and build-time Kotlin/Java code
|
|
41
|
-
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
buildFeatures {
|
|
45
|
-
buildConfig true
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
buildTypes {
|
|
49
|
-
release {
|
|
50
|
-
minifyEnabled false
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
lintOptions {
|
|
55
|
-
disable "GradleCompatible"
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
compileOptions {
|
|
59
|
-
sourceCompatibility JavaVersion.VERSION_1_8
|
|
60
|
-
targetCompatibility JavaVersion.VERSION_1_8
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
sourceSets {
|
|
64
|
-
main {
|
|
65
|
-
java.srcDirs += [
|
|
66
|
-
"generated/java",
|
|
67
|
-
"generated/jni"
|
|
68
|
-
]
|
|
69
|
-
if (isNewArchitectureEnabled()) {
|
|
70
|
-
java.srcDirs += ['src/newarch/java']
|
|
71
|
-
} else {
|
|
72
|
-
java.srcDirs += ['src/oldarch/java']
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
repositories {
|
|
79
|
-
mavenCentral()
|
|
80
|
-
google()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
84
|
-
|
|
85
|
-
dependencies {
|
|
86
|
-
implementation "com.facebook.react:react-android"
|
|
87
|
-
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
88
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
package so.kontext.react
|
|
2
|
-
|
|
3
|
-
import com.facebook.react.bridge.Promise
|
|
4
|
-
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
-
import android.content.Context
|
|
6
|
-
import android.media.AudioManager
|
|
7
|
-
|
|
8
|
-
class RNKontextModuleImpl(private val reactContext: ReactApplicationContext) {
|
|
9
|
-
fun isSoundOn(promise: Promise?) {
|
|
10
|
-
val audioManager = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
11
|
-
val current = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
|
12
|
-
val max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
|
13
|
-
val volume = if (max > 0) current.toFloat() / max.toFloat() else 0f
|
|
14
|
-
|
|
15
|
-
promise?.resolve(volume > MINIMAL_VOLUME_THRESHOLD)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
companion object {
|
|
19
|
-
private const val MINIMAL_VOLUME_THRESHOLD = 0.0f
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
package so.kontext.react
|
|
2
|
-
|
|
3
|
-
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
-
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
-
import com.facebook.react.bridge.Promise
|
|
6
|
-
|
|
7
|
-
@ReactModule(name = RNKontextModule.NAME)
|
|
8
|
-
class RNKontextModule(reactContext: ReactApplicationContext) :
|
|
9
|
-
NativeRNKontextSpec(reactContext) {
|
|
10
|
-
|
|
11
|
-
private val impl = RNKontextModuleImpl(reactContext)
|
|
12
|
-
|
|
13
|
-
override fun getName(): String = NAME
|
|
14
|
-
|
|
15
|
-
override fun isSoundOn(promise: Promise?) {
|
|
16
|
-
impl.isSoundOn(promise)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
companion object {
|
|
20
|
-
const val NAME = "RNKontext"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
package so.kontext.react
|
|
2
|
-
|
|
3
|
-
import com.facebook.react.BaseReactPackage
|
|
4
|
-
import com.facebook.react.bridge.NativeModule
|
|
5
|
-
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
-
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
-
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
-
|
|
9
|
-
class RNKontextPackage : BaseReactPackage() {
|
|
10
|
-
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
11
|
-
return if (name == RNKontextModule.NAME) {
|
|
12
|
-
RNKontextModule(reactContext)
|
|
13
|
-
} else {
|
|
14
|
-
null
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
19
|
-
return ReactModuleInfoProvider {
|
|
20
|
-
mapOf(
|
|
21
|
-
RNKontextModule.NAME to ReactModuleInfo(
|
|
22
|
-
RNKontextModule.NAME,
|
|
23
|
-
RNKontextModule.NAME,
|
|
24
|
-
false, // canOverrideExistingModule
|
|
25
|
-
false, // needsEagerInit
|
|
26
|
-
false, // isCxxModule
|
|
27
|
-
true // isTurboModule
|
|
28
|
-
)
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|