@everfur/sdk 0.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/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/animations/package.json +8 -0
- package/chat/package.json +8 -0
- package/client/package.json +8 -0
- package/core/package.json +8 -0
- package/dist/CameraPort-Cv31Pz7g.d.cts +29 -0
- package/dist/CameraPort-Cv31Pz7g.d.ts +29 -0
- package/dist/ChatController-CKdBvPj2.d.ts +146 -0
- package/dist/ChatController-CpUMvvZf.d.cts +146 -0
- package/dist/EverfurResult-D92-uL82.d.cts +240 -0
- package/dist/EverfurResult-D92-uL82.d.ts +240 -0
- package/dist/FilePort-BabWrv7I.d.cts +22 -0
- package/dist/FilePort-BabWrv7I.d.ts +22 -0
- package/dist/PhotoController-BItt5M7u.d.cts +43 -0
- package/dist/PhotoController-D8zMTdcW.d.ts +43 -0
- package/dist/TelemetryPort-BDNr00hu.d.cts +12 -0
- package/dist/TelemetryPort-BDNr00hu.d.ts +12 -0
- package/dist/animations/index.cjs +1997 -0
- package/dist/animations/index.d.cts +517 -0
- package/dist/animations/index.d.ts +517 -0
- package/dist/animations/index.js +1972 -0
- package/dist/cacheEpoch-DknKn3S0.d.cts +36 -0
- package/dist/cacheEpoch-DknKn3S0.d.ts +36 -0
- package/dist/chat/index.cjs +1170 -0
- package/dist/chat/index.d.cts +59 -0
- package/dist/chat/index.d.ts +59 -0
- package/dist/chat/index.js +1167 -0
- package/dist/client/index.cjs +2317 -0
- package/dist/client/index.d.cts +65 -0
- package/dist/client/index.d.ts +65 -0
- package/dist/client/index.js +2217 -0
- package/dist/config-BSjBdxrZ.d.cts +501 -0
- package/dist/config-CiJ0PVBB.d.ts +501 -0
- package/dist/core/index.cjs +3175 -0
- package/dist/core/index.d.cts +70 -0
- package/dist/core/index.d.ts +70 -0
- package/dist/core/index.js +3163 -0
- package/dist/identity-Brl-lDd6.d.cts +91 -0
- package/dist/identity-DK9zORrG.d.ts +91 -0
- package/dist/ids-CJ1S6adf.d.cts +46 -0
- package/dist/ids-CJ1S6adf.d.ts +46 -0
- package/dist/index.cjs +4488 -0
- package/dist/index.d.cts +156 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +4464 -0
- package/dist/petsRepository-BEGb97M9.d.cts +326 -0
- package/dist/petsRepository-Bu18r2kK.d.ts +326 -0
- package/dist/photo/index.cjs +2189 -0
- package/dist/photo/index.d.cts +43 -0
- package/dist/photo/index.d.ts +43 -0
- package/dist/photo/index.js +2186 -0
- package/dist/projection-CeIUsbSk.d.cts +8 -0
- package/dist/projection-CeIUsbSk.d.ts +8 -0
- package/dist/records/index.cjs +2840 -0
- package/dist/records/index.d.cts +224 -0
- package/dist/records/index.d.ts +224 -0
- package/dist/records/index.js +2834 -0
- package/dist/requestFunnel-DuUH-kAe.d.cts +28 -0
- package/dist/requestFunnel-dio5OmR9.d.ts +28 -0
- package/dist/resolve-Dq_4_agU.d.cts +86 -0
- package/dist/resolve-Dq_4_agU.d.ts +86 -0
- package/dist/runtime-BgQnA594.d.cts +349 -0
- package/dist/runtime-CBA-LvdM.d.ts +349 -0
- package/dist/server/index.cjs +533 -0
- package/dist/server/index.d.cts +48 -0
- package/dist/server/index.d.ts +48 -0
- package/dist/server/index.js +530 -0
- package/dist/testing/index.cjs +825 -0
- package/dist/testing/index.d.cts +113 -0
- package/dist/testing/index.d.ts +113 -0
- package/dist/testing/index.js +822 -0
- package/dist/testing/rn/index.cjs +449 -0
- package/dist/testing/rn/index.d.cts +49 -0
- package/dist/testing/rn/index.d.ts +49 -0
- package/dist/testing/rn/index.js +444 -0
- package/dist/video/index.cjs +1941 -0
- package/dist/video/index.d.cts +66 -0
- package/dist/video/index.d.ts +66 -0
- package/dist/video/index.js +1938 -0
- package/package.json +311 -0
- package/photo/package.json +8 -0
- package/records/package.json +8 -0
- package/server/device-blocked.cjs +15 -0
- package/server/package.json +9 -0
- package/testing/package.json +8 -0
- package/testing/rn/package.json +8 -0
- package/video/package.json +8 -0
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
import { createContext, useState, useEffect, useCallback, useSyncExternalStore, useMemo, useContext, useRef } from 'react';
|
|
2
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
3
|
+
import { KeyboardAvoidingView, View, Text, Platform, TextInput, Pressable, ActivityIndicator, ScrollView, StyleSheet, Animated } from 'react-native';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/rn/chat/useChat.ts
|
|
7
|
+
var CONTRACT_RETRYABILITY = Object.freeze([
|
|
8
|
+
Object.freeze({ wireCode: "AccessDeniedException", envelope: "http_envelope", httpStatus: 403, retryable: false }),
|
|
9
|
+
Object.freeze({ wireCode: "AuthorizationException", envelope: "http_envelope", httpStatus: 401, retryable: false }),
|
|
10
|
+
Object.freeze({ wireCode: "ConflictException", envelope: "http_envelope", httpStatus: 409, retryable: true }),
|
|
11
|
+
Object.freeze({ wireCode: "DAILY_QUOTA_EXCEEDED", envelope: "http_envelope", httpStatus: 429, retryable: true }),
|
|
12
|
+
Object.freeze({ wireCode: "IDEMPOTENCY_KEY_IN_FLIGHT", envelope: "http_envelope", httpStatus: 409, retryable: true }),
|
|
13
|
+
Object.freeze({ wireCode: "IDEMPOTENCY_KEY_REUSED_DIFFERENT_REQUEST", envelope: "http_envelope", httpStatus: 409, retryable: false }),
|
|
14
|
+
Object.freeze({ wireCode: "InternalFailureException", envelope: "http_envelope", httpStatus: 500, retryable: true }),
|
|
15
|
+
Object.freeze({ wireCode: "NotImplementedException", envelope: "http_envelope", httpStatus: 501, retryable: false }),
|
|
16
|
+
Object.freeze({ wireCode: "PaymentRequiredException", envelope: "http_envelope", httpStatus: 402, retryable: false }),
|
|
17
|
+
Object.freeze({ wireCode: "ResourceNotFoundException", envelope: "http_envelope", httpStatus: 404, retryable: false }),
|
|
18
|
+
Object.freeze({ wireCode: "ServiceUnavailableException", envelope: "http_envelope", httpStatus: 503, retryable: true }),
|
|
19
|
+
Object.freeze({ wireCode: "TIER_UPGRADE_REQUIRED", envelope: "http_envelope", httpStatus: 402, retryable: false }),
|
|
20
|
+
Object.freeze({ wireCode: "ThrottlingException", envelope: "http_envelope", httpStatus: 429, retryable: true }),
|
|
21
|
+
Object.freeze({ wireCode: "ValidationException", envelope: "http_envelope", httpStatus: 400, retryable: false }),
|
|
22
|
+
Object.freeze({ wireCode: "accessDenied", envelope: "http_envelope", httpStatus: 403, retryable: false }),
|
|
23
|
+
Object.freeze({ wireCode: "authExpired", envelope: "http_envelope", httpStatus: 401, retryable: true }),
|
|
24
|
+
Object.freeze({ wireCode: "internal_error", envelope: "sse_frame", httpStatus: 200, retryable: false }),
|
|
25
|
+
Object.freeze({ wireCode: "invalidRequest", envelope: "http_envelope", httpStatus: 400, retryable: false }),
|
|
26
|
+
Object.freeze({ wireCode: "not_found", envelope: "sse_frame", httpStatus: 200, retryable: false }),
|
|
27
|
+
Object.freeze({ wireCode: "session_required", envelope: "http_envelope", httpStatus: 403, retryable: false }),
|
|
28
|
+
Object.freeze({ wireCode: "timeout", envelope: "sse_frame", httpStatus: 200, retryable: true }),
|
|
29
|
+
Object.freeze({ wireCode: "unavailable", envelope: "http_envelope", httpStatus: 503, retryable: true })
|
|
30
|
+
]);
|
|
31
|
+
Object.freeze(
|
|
32
|
+
CONTRACT_RETRYABILITY.map((row) => row.wireCode)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// src/client/errors/contract.ts
|
|
36
|
+
var NORMALIZED_FOLD = Object.freeze({
|
|
37
|
+
// the default status-mapped envelope
|
|
38
|
+
ValidationException: "validationFailed",
|
|
39
|
+
AuthorizationException: "authRejected",
|
|
40
|
+
PaymentRequiredException: "paymentRequired",
|
|
41
|
+
AccessDeniedException: "accessDenied",
|
|
42
|
+
ResourceNotFoundException: "resourceNotFound",
|
|
43
|
+
ConflictException: "conflict",
|
|
44
|
+
ThrottlingException: "rateLimited",
|
|
45
|
+
InternalFailureException: "internalError",
|
|
46
|
+
NotImplementedException: "notImplemented",
|
|
47
|
+
ServiceUnavailableException: "serviceUnavailable",
|
|
48
|
+
// dict-detail codes on the widget surface
|
|
49
|
+
authExpired: "authExpired",
|
|
50
|
+
unavailable: "serviceUnavailable",
|
|
51
|
+
session_required: "sessionRequired",
|
|
52
|
+
accessDenied: "accessDenied",
|
|
53
|
+
invalidRequest: "sdkUpdateRequired",
|
|
54
|
+
// typed codes
|
|
55
|
+
IDEMPOTENCY_KEY_IN_FLIGHT: "idempotencyInFlight",
|
|
56
|
+
IDEMPOTENCY_KEY_REUSED_DIFFERENT_REQUEST: "idempotencyConflict",
|
|
57
|
+
DAILY_QUOTA_EXCEEDED: "quotaExceeded",
|
|
58
|
+
TIER_UPGRADE_REQUIRED: "paymentRequired",
|
|
59
|
+
// SSE error frames
|
|
60
|
+
not_found: "conversationNotFound",
|
|
61
|
+
timeout: "streamTimeout",
|
|
62
|
+
internal_error: "internalError"
|
|
63
|
+
});
|
|
64
|
+
var CONTRACT_ERROR_ROWS = Object.freeze(
|
|
65
|
+
CONTRACT_RETRYABILITY.map(
|
|
66
|
+
(generated) => Object.freeze({
|
|
67
|
+
wireCode: generated.wireCode,
|
|
68
|
+
envelope: generated.envelope,
|
|
69
|
+
retryable: generated.retryable,
|
|
70
|
+
normalized: NORMALIZED_FOLD[generated.wireCode]
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
var BY_UPPER = new Map(
|
|
75
|
+
CONTRACT_ERROR_ROWS.map((r) => [r.wireCode.toUpperCase(), r])
|
|
76
|
+
);
|
|
77
|
+
function contractErrorFor(wireCode) {
|
|
78
|
+
if (typeof wireCode !== "string") return void 0;
|
|
79
|
+
const key = wireCode.trim().toUpperCase();
|
|
80
|
+
return key.length > 0 ? BY_UPPER.get(key) : void 0;
|
|
81
|
+
}
|
|
82
|
+
var CATEGORY_OF = Object.freeze({
|
|
83
|
+
// the caller's sign-in state is wrong and a credential change is the remedy
|
|
84
|
+
authExpired: "authentication",
|
|
85
|
+
authRejected: "authentication",
|
|
86
|
+
sessionRequired: "authentication",
|
|
87
|
+
// signed in, correctly identified, and still not allowed
|
|
88
|
+
accessDenied: "authorization",
|
|
89
|
+
// a plan gate, not a permission failure: the remedy is commercial
|
|
90
|
+
paymentRequired: "entitlement",
|
|
91
|
+
// slow down / come back later
|
|
92
|
+
quotaExceeded: "rateLimit",
|
|
93
|
+
rateLimited: "rateLimit",
|
|
94
|
+
// the request itself was wrong; retrying it unchanged can never succeed
|
|
95
|
+
validationFailed: "invalidRequest",
|
|
96
|
+
sdkUpdateRequired: "invalidRequest",
|
|
97
|
+
// the target is gone, hidden, or was never visible to this caller
|
|
98
|
+
conversationNotFound: "notFound",
|
|
99
|
+
petNotFound: "notFound",
|
|
100
|
+
resourceNotFound: "notFound",
|
|
101
|
+
// something else already holds the thing
|
|
102
|
+
conflict: "conflict",
|
|
103
|
+
idempotencyConflict: "conflict",
|
|
104
|
+
idempotencyInFlight: "conflict",
|
|
105
|
+
// the pipe, not the request: timeouts, drops, a briefly unreachable dependency
|
|
106
|
+
streamTimeout: "connection",
|
|
107
|
+
streamIncomplete: "connection",
|
|
108
|
+
upstreamLlmTimeout: "connection",
|
|
109
|
+
serviceUnavailable: "connection",
|
|
110
|
+
// the server admits fault
|
|
111
|
+
internalError: "server",
|
|
112
|
+
notImplemented: "server",
|
|
113
|
+
mediaUploadFailed: "media",
|
|
114
|
+
moderationBlocked: "policy",
|
|
115
|
+
unknown: "unknown"
|
|
116
|
+
});
|
|
117
|
+
function categoryFor(code) {
|
|
118
|
+
return CATEGORY_OF[code];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/client/errors/EverfurError.ts
|
|
122
|
+
var USER_FACING_COPY = Object.freeze({
|
|
123
|
+
authExpired: "Please sign in again to continue.",
|
|
124
|
+
authRejected: "Please sign in again to continue.",
|
|
125
|
+
sessionRequired: "This app needs to be updated to keep using Everfur.",
|
|
126
|
+
accessDenied: "This feature isn't available on your account right now.",
|
|
127
|
+
paymentRequired: "This is part of a paid plan.",
|
|
128
|
+
quotaExceeded: "You have used today's messages. This resets tomorrow.",
|
|
129
|
+
rateLimited: "You are sending messages quickly. Try again in a few seconds.",
|
|
130
|
+
validationFailed: "Something in that request was not right. Check the details and try again.",
|
|
131
|
+
sdkUpdateRequired: "This app needs to be updated to keep using Everfur.",
|
|
132
|
+
conflict: "That is already running. Wait for it to finish, then try again.",
|
|
133
|
+
idempotencyConflict: "That message was already delivered.",
|
|
134
|
+
idempotencyInFlight: "That is still processing. Give it a moment.",
|
|
135
|
+
conversationNotFound: "This conversation is no longer available.",
|
|
136
|
+
petNotFound: "We could not find that pet. Please pick a pet and try again.",
|
|
137
|
+
resourceNotFound: "We could not find that.",
|
|
138
|
+
streamTimeout: "The response timed out. Tap retry to try again.",
|
|
139
|
+
mediaUploadFailed: "We could not upload that photo. Please attach it again.",
|
|
140
|
+
moderationBlocked: "This request cannot be answered here.",
|
|
141
|
+
upstreamLlmTimeout: "The response timed out. Tap retry to try again.",
|
|
142
|
+
internalError: "We hit a snag on our end. Please try again.",
|
|
143
|
+
notImplemented: "That is not available yet.",
|
|
144
|
+
serviceUnavailable: "That service is briefly unavailable. Try again shortly.",
|
|
145
|
+
streamIncomplete: "The connection dropped mid-answer. Tap retry to continue.",
|
|
146
|
+
unknown: "Something went wrong. Please try again."
|
|
147
|
+
});
|
|
148
|
+
function userFacingError(code) {
|
|
149
|
+
return USER_FACING_COPY[code];
|
|
150
|
+
}
|
|
151
|
+
function defineHidden(target, key, value) {
|
|
152
|
+
Object.defineProperty(target, key, {
|
|
153
|
+
value,
|
|
154
|
+
enumerable: false,
|
|
155
|
+
writable: false,
|
|
156
|
+
configurable: false
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function makeEverfurError(code, opts) {
|
|
160
|
+
contractErrorFor(opts?.wireCode);
|
|
161
|
+
const error = {
|
|
162
|
+
code,
|
|
163
|
+
category: categoryFor(code),
|
|
164
|
+
retryable: opts?.retryable,
|
|
165
|
+
displayMessage: userFacingError(code)
|
|
166
|
+
};
|
|
167
|
+
if (opts?.httpStatus !== void 0) error.httpStatus = opts.httpStatus;
|
|
168
|
+
if (opts?.requestId) error.requestId = opts.requestId;
|
|
169
|
+
if (opts?.wireMessage) defineHidden(error, "wireMessage", opts.wireMessage);
|
|
170
|
+
if (opts?.raw !== void 0 && opts.raw !== null) defineHidden(error, "raw", opts.raw);
|
|
171
|
+
return Object.freeze(error);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/client/errors/EverfurResult.ts
|
|
175
|
+
function ok(value, warning) {
|
|
176
|
+
return Object.freeze(
|
|
177
|
+
{ ok: true, value }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
function err(error) {
|
|
181
|
+
return Object.freeze({ ok: false, error });
|
|
182
|
+
}
|
|
183
|
+
var EverfurConfigError = class _EverfurConfigError extends Error {
|
|
184
|
+
constructor(kind, message) {
|
|
185
|
+
super(message);
|
|
186
|
+
this.name = "EverfurConfigError";
|
|
187
|
+
this.kind = kind;
|
|
188
|
+
Object.setPrototypeOf(this, _EverfurConfigError.prototype);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/client/protocol/frames.ts
|
|
193
|
+
function assertNever(x) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`assertNever: unhandled discriminated-union member ${JSON.stringify(x)}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/core/chat/ChatController.ts
|
|
200
|
+
var MESSAGE_MAX_CHARS = 4e3;
|
|
201
|
+
|
|
202
|
+
// src/client/entitlements/resolve.ts
|
|
203
|
+
var REQUIRES = Object.freeze({
|
|
204
|
+
chat: Object.freeze(["chat.message.create"]),
|
|
205
|
+
records: Object.freeze(["records.upload.create", "records.record.read"]),
|
|
206
|
+
video: Object.freeze(["video.gait.execute"]),
|
|
207
|
+
photo: Object.freeze(["photo.checkup.execute"]),
|
|
208
|
+
animations: Object.freeze(["animations.state.read"])
|
|
209
|
+
});
|
|
210
|
+
function requires(name) {
|
|
211
|
+
return REQUIRES[name];
|
|
212
|
+
}
|
|
213
|
+
function decideCapability(name, decisions, opts) {
|
|
214
|
+
if (opts?.isPending === true) {
|
|
215
|
+
return freezeDecision({ name, enabled: false, isPending: true, limits: {} });
|
|
216
|
+
}
|
|
217
|
+
const keys = requires(name);
|
|
218
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
219
|
+
for (const decision of decisions) byResource.set(decision.resource, decision);
|
|
220
|
+
const limits = mergeLimits(keys, byResource);
|
|
221
|
+
const missing = keys.filter((key) => byResource.get(key)?.granted !== true);
|
|
222
|
+
if (missing.length === 0) {
|
|
223
|
+
return freezeDecision({ name, enabled: true, isPending: false, limits });
|
|
224
|
+
}
|
|
225
|
+
return freezeDecision({
|
|
226
|
+
name,
|
|
227
|
+
enabled: false,
|
|
228
|
+
isPending: false,
|
|
229
|
+
limits,
|
|
230
|
+
disabled: disabledFor(keys, byResource)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function mergeLimits(keys, byResource) {
|
|
234
|
+
const out = {};
|
|
235
|
+
for (const key of keys) {
|
|
236
|
+
const decisionLimits = byResource.get(key)?.limits;
|
|
237
|
+
if (!decisionLimits) continue;
|
|
238
|
+
for (const [limitKey, limitValue] of Object.entries(decisionLimits)) {
|
|
239
|
+
const existing = out[limitKey];
|
|
240
|
+
out[limitKey] = existing === void 0 ? limitValue : Math.min(existing, limitValue);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
function disabledFor(keys, byResource) {
|
|
246
|
+
for (const key of keys) {
|
|
247
|
+
const explicit = byResource.get(key)?.disabled;
|
|
248
|
+
if (explicit) {
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
reason: explicit.reason,
|
|
251
|
+
level: explicit.level,
|
|
252
|
+
recoverable: explicit.recoverable
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return Object.freeze({
|
|
257
|
+
reason: userFacingError("accessDenied"),
|
|
258
|
+
level: "user_entitlement",
|
|
259
|
+
recoverable: false
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function freezeDecision(d) {
|
|
263
|
+
const base = {
|
|
264
|
+
name: d.name,
|
|
265
|
+
enabled: d.enabled,
|
|
266
|
+
isPending: d.isPending,
|
|
267
|
+
limits: Object.freeze({ ...d.limits })
|
|
268
|
+
};
|
|
269
|
+
if (d.disabled !== void 0) base.disabled = d.disabled;
|
|
270
|
+
return Object.freeze(base);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/client/theme/tokens.ts
|
|
274
|
+
function deepFreeze(value) {
|
|
275
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
276
|
+
Object.freeze(value);
|
|
277
|
+
for (const key of Object.keys(value)) {
|
|
278
|
+
deepFreeze(value[key]);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
var SPACE = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24, xxl: 32 };
|
|
284
|
+
var RADIUS = { tile: 5, xs: 6, sm: 10, md: 14, lg: 20, xl: 28, pill: 500 };
|
|
285
|
+
var TYPE_SIZE = {
|
|
286
|
+
nano: 8,
|
|
287
|
+
small: 9,
|
|
288
|
+
micro: 10,
|
|
289
|
+
tiny: 11,
|
|
290
|
+
caption: 12,
|
|
291
|
+
bodyTight: 13,
|
|
292
|
+
body: 14,
|
|
293
|
+
subtitle: 16,
|
|
294
|
+
heading: 18,
|
|
295
|
+
verdict: 24
|
|
296
|
+
};
|
|
297
|
+
var LINE_HEIGHT = { tight: 1.15, snug: 1.3, normal: 1.4, relaxed: 1.55 };
|
|
298
|
+
var WEIGHT = { regular: "400", medium: "500", bold: "700" };
|
|
299
|
+
var LETTER_SPACING = {
|
|
300
|
+
eyebrow: 1,
|
|
301
|
+
monoWide: 0.9,
|
|
302
|
+
headerBrand: -0.42,
|
|
303
|
+
tight: -0.35
|
|
304
|
+
};
|
|
305
|
+
function baseType() {
|
|
306
|
+
return {
|
|
307
|
+
family: "System",
|
|
308
|
+
mono: "monospace",
|
|
309
|
+
fontUrl: null,
|
|
310
|
+
size: TYPE_SIZE,
|
|
311
|
+
lineHeight: LINE_HEIGHT,
|
|
312
|
+
weight: WEIGHT,
|
|
313
|
+
letterSpacing: LETTER_SPACING
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
var MOTION = {
|
|
317
|
+
duration: { instant: 80, fast: 140, normal: 220, slow: 320 },
|
|
318
|
+
spring: { gentle: { damping: 18, stiffness: 200 } },
|
|
319
|
+
easing: { standard: "cubic-bezier(0.4, 0, 0.2, 1)" },
|
|
320
|
+
reducedMotion: false
|
|
321
|
+
};
|
|
322
|
+
var LIGHT_COLOR = {
|
|
323
|
+
brand: "#10462E",
|
|
324
|
+
brandSoft: "rgba(16,70,46,0.10)",
|
|
325
|
+
brandSofter: "rgba(16,70,46,0.15)",
|
|
326
|
+
brandBorder: "rgba(16,70,46,0.18)",
|
|
327
|
+
borderHairlineGreen: "rgba(16,70,46,0.25)",
|
|
328
|
+
surface: "#FFFFFF",
|
|
329
|
+
surfaceCream: "#FDFBF8",
|
|
330
|
+
surfaceSubtle: "#EFE9E3",
|
|
331
|
+
background: "#FFFFFF",
|
|
332
|
+
borderRingCream: "#D8D2C8",
|
|
333
|
+
text: {
|
|
334
|
+
primary: "#111111",
|
|
335
|
+
secondary: "#0E462E",
|
|
336
|
+
muted: "#6E6660",
|
|
337
|
+
placeholder: "rgba(0,0,0,0.30)",
|
|
338
|
+
onBrand: "#FFFFFF",
|
|
339
|
+
metaCool: "#3D3D3D"
|
|
340
|
+
},
|
|
341
|
+
primary: "#10462E",
|
|
342
|
+
onPrimary: "#FFFFFF",
|
|
343
|
+
accent: "#10462E",
|
|
344
|
+
onAccent: "#FFFFFF",
|
|
345
|
+
accentEdge: "#07C168",
|
|
346
|
+
success: "#07C168",
|
|
347
|
+
danger: "#B91C1C",
|
|
348
|
+
errorRed: "#FF1B00",
|
|
349
|
+
warningStrong: "#A45610",
|
|
350
|
+
cautionAccent: "#D9821D",
|
|
351
|
+
riskConcern: "#EFA700",
|
|
352
|
+
badgeBgPositive: "rgba(7,193,104,0.12)",
|
|
353
|
+
badgeBgCaution: "rgba(217,130,29,0.14)",
|
|
354
|
+
badgeBgCritical: "rgba(185,28,28,0.12)",
|
|
355
|
+
badgeBgNeutral: "rgba(110,102,96,0.12)",
|
|
356
|
+
bubbleUser: "#10462E",
|
|
357
|
+
bubbleAssistant: "#FFFFFF",
|
|
358
|
+
skeleton: "#EFE9E3",
|
|
359
|
+
scanButtonPink: "#FFC3BC",
|
|
360
|
+
checkupCardLowRiskBg: "rgba(7,193,104,0.10)",
|
|
361
|
+
checkupCardConcernBg: "rgba(239,167,0,0.10)"
|
|
362
|
+
};
|
|
363
|
+
var EVERFUR_LIGHT = deepFreeze({
|
|
364
|
+
brightness: "light",
|
|
365
|
+
meta: { displayName: "Everfur", logoUrl: null, placeholder: null, hidePoweredBy: false },
|
|
366
|
+
color: LIGHT_COLOR,
|
|
367
|
+
space: SPACE,
|
|
368
|
+
radius: RADIUS,
|
|
369
|
+
type: baseType(),
|
|
370
|
+
motion: MOTION
|
|
371
|
+
});
|
|
372
|
+
var DARK_COLOR = {
|
|
373
|
+
brand: "#6BD39A",
|
|
374
|
+
brandSoft: "rgba(107,211,154,0.12)",
|
|
375
|
+
brandSofter: "rgba(107,211,154,0.18)",
|
|
376
|
+
brandBorder: "rgba(107,211,154,0.22)",
|
|
377
|
+
borderHairlineGreen: "rgba(107,211,154,0.28)",
|
|
378
|
+
surface: "#1C1F23",
|
|
379
|
+
surfaceCream: "#23272C",
|
|
380
|
+
surfaceSubtle: "#23272C",
|
|
381
|
+
background: "#15171A",
|
|
382
|
+
borderRingCream: "#3A3F45",
|
|
383
|
+
text: {
|
|
384
|
+
primary: "#F5F3F0",
|
|
385
|
+
secondary: "#A9E4C4",
|
|
386
|
+
muted: "#9A938C",
|
|
387
|
+
placeholder: "rgba(255,255,255,0.35)",
|
|
388
|
+
onBrand: "#0E1F17",
|
|
389
|
+
metaCool: "#C9C9C9"
|
|
390
|
+
},
|
|
391
|
+
primary: "#6BD39A",
|
|
392
|
+
onPrimary: "#0E1F17",
|
|
393
|
+
accent: "#6BD39A",
|
|
394
|
+
onAccent: "#0E1F17",
|
|
395
|
+
accentEdge: "#34D888",
|
|
396
|
+
success: "#07C168",
|
|
397
|
+
danger: "#B91C1C",
|
|
398
|
+
errorRed: "#FF1B00",
|
|
399
|
+
warningStrong: "#A45610",
|
|
400
|
+
cautionAccent: "#D9821D",
|
|
401
|
+
riskConcern: "#EFA700",
|
|
402
|
+
badgeBgPositive: "rgba(7,193,104,0.18)",
|
|
403
|
+
badgeBgCaution: "rgba(217,130,29,0.20)",
|
|
404
|
+
badgeBgCritical: "rgba(185,28,28,0.22)",
|
|
405
|
+
badgeBgNeutral: "rgba(255,255,255,0.10)",
|
|
406
|
+
bubbleUser: "#6BD39A",
|
|
407
|
+
bubbleAssistant: "#1C1F23",
|
|
408
|
+
skeleton: "#23272C",
|
|
409
|
+
scanButtonPink: "#FFC3BC",
|
|
410
|
+
checkupCardLowRiskBg: "rgba(7,193,104,0.16)",
|
|
411
|
+
checkupCardConcernBg: "rgba(239,167,0,0.16)"
|
|
412
|
+
};
|
|
413
|
+
deepFreeze({
|
|
414
|
+
brightness: "dark",
|
|
415
|
+
meta: { displayName: "Everfur", logoUrl: null, placeholder: null, hidePoweredBy: false },
|
|
416
|
+
color: DARK_COLOR,
|
|
417
|
+
space: SPACE,
|
|
418
|
+
radius: RADIUS,
|
|
419
|
+
type: baseType(),
|
|
420
|
+
motion: MOTION
|
|
421
|
+
});
|
|
422
|
+
var EVERFUR_THEME_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("@everfur/sdk#EverfurThemeContext@v1");
|
|
423
|
+
var globalThemeHost = globalThis;
|
|
424
|
+
var EverfurThemeContext = globalThemeHost[EVERFUR_THEME_CONTEXT_SYMBOL] ?? (globalThemeHost[EVERFUR_THEME_CONTEXT_SYMBOL] = createContext(EVERFUR_LIGHT));
|
|
425
|
+
EverfurThemeContext.displayName ??= "EverfurThemeContext";
|
|
426
|
+
function useEverfurTheme() {
|
|
427
|
+
return useContext(EverfurThemeContext);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/rn/facade.ts
|
|
431
|
+
var EVERFUR_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("@everfur/sdk#EverfurContext@v1");
|
|
432
|
+
var globalContextHost = globalThis;
|
|
433
|
+
var EverfurContext = globalContextHost[EVERFUR_CONTEXT_SYMBOL] ?? (globalContextHost[EVERFUR_CONTEXT_SYMBOL] = createContext(null));
|
|
434
|
+
EverfurContext.displayName ??= "EverfurContext";
|
|
435
|
+
function useEverfurContext() {
|
|
436
|
+
const ctx = useContext(EverfurContext);
|
|
437
|
+
if (ctx === null) {
|
|
438
|
+
throw new EverfurConfigError(
|
|
439
|
+
"not-initialized",
|
|
440
|
+
"Everfur hooks must be called inside <EverfurProvider>. Wrap your tree once, high."
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return ctx;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/rn/chat/useChat.ts
|
|
447
|
+
var BOOTSTRAP_SNAPSHOT = Object.freeze({
|
|
448
|
+
status: "idle",
|
|
449
|
+
messages: Object.freeze([]),
|
|
450
|
+
conversationId: null,
|
|
451
|
+
error: null,
|
|
452
|
+
isStreaming: false,
|
|
453
|
+
isBootstrapping: true,
|
|
454
|
+
suggestedPrompts: Object.freeze([])
|
|
455
|
+
});
|
|
456
|
+
function useEverfurChat(opts) {
|
|
457
|
+
const { runtime, userRef, activePet } = useEverfurContext();
|
|
458
|
+
const petRef = opts?.petRef ?? activePet ?? null;
|
|
459
|
+
const conversationId = opts?.conversationId ?? null;
|
|
460
|
+
const [controller, setController] = useState(null);
|
|
461
|
+
useEffect(() => {
|
|
462
|
+
const acquired = runtime.registry.acquireChat({ userRef, petRef, conversationId });
|
|
463
|
+
setController(acquired);
|
|
464
|
+
return () => {
|
|
465
|
+
setController((current) => current === acquired ? null : current);
|
|
466
|
+
runtime.registry.releaseChat(acquired);
|
|
467
|
+
};
|
|
468
|
+
}, [runtime, userRef, petRef, conversationId]);
|
|
469
|
+
const subscribe = useCallback(
|
|
470
|
+
(onChange) => controller ? controller.subscribe(onChange) : NOOP_UNSUBSCRIBE,
|
|
471
|
+
[controller]
|
|
472
|
+
);
|
|
473
|
+
const getSnapshot = useCallback(
|
|
474
|
+
() => controller ? controller.getSnapshot() : BOOTSTRAP_SNAPSHOT,
|
|
475
|
+
[controller]
|
|
476
|
+
);
|
|
477
|
+
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
478
|
+
const send = useCallback(
|
|
479
|
+
(text) => controller ? driveToResult(controller, controller.send(text)) : Promise.resolve(notReadyError()),
|
|
480
|
+
[controller]
|
|
481
|
+
);
|
|
482
|
+
const retryLast = useCallback(
|
|
483
|
+
() => controller ? driveToResult(controller, controller.retryLast()) : Promise.resolve(notReadyError()),
|
|
484
|
+
[controller]
|
|
485
|
+
);
|
|
486
|
+
const stop = useCallback(() => {
|
|
487
|
+
controller?.stop();
|
|
488
|
+
}, [controller]);
|
|
489
|
+
return useMemo(
|
|
490
|
+
() => ({
|
|
491
|
+
status: snapshot.status,
|
|
492
|
+
messages: snapshot.messages,
|
|
493
|
+
conversationId: snapshot.conversationId,
|
|
494
|
+
error: snapshot.error,
|
|
495
|
+
isStreaming: snapshot.isStreaming,
|
|
496
|
+
isBootstrapping: snapshot.isBootstrapping,
|
|
497
|
+
suggestedPrompts: snapshot.suggestedPrompts,
|
|
498
|
+
send,
|
|
499
|
+
retryLast,
|
|
500
|
+
stop
|
|
501
|
+
}),
|
|
502
|
+
[snapshot, send, retryLast, stop]
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
var NOOP_UNSUBSCRIBE = () => {
|
|
506
|
+
};
|
|
507
|
+
async function driveToResult(controller, iterable) {
|
|
508
|
+
for await (const _frame of iterable) {
|
|
509
|
+
}
|
|
510
|
+
const snapshot = controller.getSnapshot();
|
|
511
|
+
if (snapshot.status === "error" && snapshot.error !== null) return err(snapshot.error);
|
|
512
|
+
return ok(lastAssistantResponseId(snapshot.messages));
|
|
513
|
+
}
|
|
514
|
+
function lastAssistantResponseId(messages) {
|
|
515
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
516
|
+
const message = messages[i];
|
|
517
|
+
if (message && message.role === "assistant" && message.responseId !== void 0) {
|
|
518
|
+
return message.responseId;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
function notReadyError() {
|
|
524
|
+
return err(
|
|
525
|
+
makeEverfurError("internalError", { retryable: true })
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
function useCapability(name) {
|
|
529
|
+
const { runtime } = useEverfurContext();
|
|
530
|
+
const store = runtime.entitlements;
|
|
531
|
+
const snapshot = useSyncExternalStore(
|
|
532
|
+
store.subscribe,
|
|
533
|
+
store.getSnapshot,
|
|
534
|
+
store.getSnapshot
|
|
535
|
+
);
|
|
536
|
+
return useMemo(
|
|
537
|
+
() => decideCapability(name, snapshot.decisions, { isPending: snapshot.isPending }),
|
|
538
|
+
[name, snapshot]
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
var MIN_OFFSTATE_HEIGHT = 48;
|
|
542
|
+
function selectView(decision) {
|
|
543
|
+
if (decision.isPending) return "pending";
|
|
544
|
+
return decision.enabled ? "enabled" : "disabled";
|
|
545
|
+
}
|
|
546
|
+
function CapabilityGate(props) {
|
|
547
|
+
const { capability, children, renderDisabled, renderPending, style } = props;
|
|
548
|
+
const decision = useCapability(capability);
|
|
549
|
+
const view = useMemo(() => selectView(decision), [decision]);
|
|
550
|
+
switch (view) {
|
|
551
|
+
case "enabled":
|
|
552
|
+
return children;
|
|
553
|
+
case "pending":
|
|
554
|
+
return renderPending ? renderPending() : /* @__PURE__ */ jsx(PendingSkeleton, { capability, style });
|
|
555
|
+
case "disabled":
|
|
556
|
+
return renderDisabled ? renderDisabled(decision) : /* @__PURE__ */ jsx(OffState, { decision, style });
|
|
557
|
+
default:
|
|
558
|
+
return assertNever(view);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function PendingSkeleton({
|
|
562
|
+
capability,
|
|
563
|
+
style
|
|
564
|
+
}) {
|
|
565
|
+
const theme = useEverfurTheme();
|
|
566
|
+
const styles = useMemo(() => makeStyles(theme), [theme]);
|
|
567
|
+
return /* @__PURE__ */ jsx(
|
|
568
|
+
View,
|
|
569
|
+
{
|
|
570
|
+
style: [styles.offstate, style],
|
|
571
|
+
accessible: true,
|
|
572
|
+
accessibilityState: { busy: true },
|
|
573
|
+
accessibilityLabel: `Loading ${capability}`,
|
|
574
|
+
children: /* @__PURE__ */ jsx(View, { style: styles.skeletonBar })
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
function OffState({
|
|
579
|
+
decision,
|
|
580
|
+
style
|
|
581
|
+
}) {
|
|
582
|
+
const theme = useEverfurTheme();
|
|
583
|
+
const styles = useMemo(() => makeStyles(theme), [theme]);
|
|
584
|
+
const message = decision.disabled?.reason ?? "";
|
|
585
|
+
return /* @__PURE__ */ jsx(
|
|
586
|
+
View,
|
|
587
|
+
{
|
|
588
|
+
style: [styles.offstate, style],
|
|
589
|
+
accessible: true,
|
|
590
|
+
accessibilityRole: "text",
|
|
591
|
+
accessibilityLabel: message,
|
|
592
|
+
children: message.length > 0 ? /* @__PURE__ */ jsx(Text, { style: styles.offstateText, children: message }) : null
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
function makeStyles(theme) {
|
|
597
|
+
const { color, space, radius, type } = theme;
|
|
598
|
+
return StyleSheet.create({
|
|
599
|
+
// A deliberate cream card with a soft brand hairline, never a bare grey slab.
|
|
600
|
+
offstate: {
|
|
601
|
+
minHeight: MIN_OFFSTATE_HEIGHT,
|
|
602
|
+
justifyContent: "center",
|
|
603
|
+
paddingHorizontal: space.lg,
|
|
604
|
+
paddingVertical: space.md,
|
|
605
|
+
backgroundColor: color.surfaceCream,
|
|
606
|
+
borderRadius: radius.md,
|
|
607
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
608
|
+
borderColor: color.brandBorder
|
|
609
|
+
},
|
|
610
|
+
offstateText: {
|
|
611
|
+
fontFamily: type.family,
|
|
612
|
+
fontSize: type.size.body,
|
|
613
|
+
lineHeight: Math.round(type.size.body * type.lineHeight.normal),
|
|
614
|
+
color: color.text.muted
|
|
615
|
+
},
|
|
616
|
+
// A soft green shimmer bar (brand wash) that stays legible on the cream card in both light and dark,
|
|
617
|
+
// where the neutral `skeleton` token would collide with the dark cream surface.
|
|
618
|
+
skeletonBar: {
|
|
619
|
+
height: space.md,
|
|
620
|
+
borderRadius: radius.xs,
|
|
621
|
+
backgroundColor: color.brandSofter
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
var CLINICAL_DISCLAIMER = Object.freeze({
|
|
626
|
+
title: "For educational use only",
|
|
627
|
+
body: "Everfur is for educational purposes only. It helps you track visible changes through AI checkups. It does not diagnose, treat, prescribe, or replace veterinary care. For urgent symptoms or emergencies, contact a veterinarian immediately."
|
|
628
|
+
});
|
|
629
|
+
function ClinicalDisclaimer(props) {
|
|
630
|
+
const theme = useEverfurTheme();
|
|
631
|
+
const styles = useMemo(() => makeStyles2(theme), [theme]);
|
|
632
|
+
return /* @__PURE__ */ jsxs(
|
|
633
|
+
View,
|
|
634
|
+
{
|
|
635
|
+
style: [styles.container, props.style],
|
|
636
|
+
accessible: true,
|
|
637
|
+
accessibilityRole: "text",
|
|
638
|
+
accessibilityLabel: `${CLINICAL_DISCLAIMER.title}. ${CLINICAL_DISCLAIMER.body}`,
|
|
639
|
+
children: [
|
|
640
|
+
/* @__PURE__ */ jsx(Text, { style: styles.title, children: CLINICAL_DISCLAIMER.title }),
|
|
641
|
+
/* @__PURE__ */ jsx(Text, { style: styles.body, children: CLINICAL_DISCLAIMER.body })
|
|
642
|
+
]
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
function makeStyles2(theme) {
|
|
647
|
+
const { color, space, type } = theme;
|
|
648
|
+
return StyleSheet.create({
|
|
649
|
+
container: {
|
|
650
|
+
paddingHorizontal: space.lg,
|
|
651
|
+
paddingVertical: space.md,
|
|
652
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
653
|
+
borderTopColor: color.borderHairlineGreen,
|
|
654
|
+
backgroundColor: color.surfaceCream,
|
|
655
|
+
gap: space.xs
|
|
656
|
+
},
|
|
657
|
+
title: {
|
|
658
|
+
fontFamily: type.family,
|
|
659
|
+
fontSize: type.size.caption,
|
|
660
|
+
lineHeight: Math.round(type.size.caption * type.lineHeight.snug),
|
|
661
|
+
color: color.text.secondary,
|
|
662
|
+
fontWeight: type.weight.bold,
|
|
663
|
+
letterSpacing: type.letterSpacing.eyebrow
|
|
664
|
+
},
|
|
665
|
+
body: {
|
|
666
|
+
fontFamily: type.family,
|
|
667
|
+
fontSize: type.size.caption,
|
|
668
|
+
lineHeight: Math.round(type.size.caption * type.lineHeight.snug),
|
|
669
|
+
color: color.text.muted,
|
|
670
|
+
fontWeight: type.weight.regular
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
var INLINE_TOKEN = /\*\*([^*]+?)\*\*|\*([^*]+?)\*|`([^`]+?)`/g;
|
|
675
|
+
function renderInlineMarkdown(text, styles) {
|
|
676
|
+
if (!text.includes("*") && !text.includes("`")) return text;
|
|
677
|
+
const nodes = [];
|
|
678
|
+
let cursor = 0;
|
|
679
|
+
let key = 0;
|
|
680
|
+
INLINE_TOKEN.lastIndex = 0;
|
|
681
|
+
for (let match = INLINE_TOKEN.exec(text); match !== null; match = INLINE_TOKEN.exec(text)) {
|
|
682
|
+
if (match.index > cursor) nodes.push(text.slice(cursor, match.index));
|
|
683
|
+
const bold = match[1];
|
|
684
|
+
const italic = match[2];
|
|
685
|
+
const code = match[3];
|
|
686
|
+
if (bold !== void 0) {
|
|
687
|
+
nodes.push(/* @__PURE__ */ jsx(Text, { style: styles.bold, children: bold }, key++));
|
|
688
|
+
} else if (italic !== void 0) {
|
|
689
|
+
nodes.push(styles.italic ? /* @__PURE__ */ jsx(Text, { style: styles.italic, children: italic }, key++) : italic);
|
|
690
|
+
} else if (code !== void 0) {
|
|
691
|
+
nodes.push(styles.code ? /* @__PURE__ */ jsx(Text, { style: styles.code, children: code }, key++) : code);
|
|
692
|
+
}
|
|
693
|
+
cursor = match.index + match[0].length;
|
|
694
|
+
}
|
|
695
|
+
if (cursor < text.length) nodes.push(text.slice(cursor));
|
|
696
|
+
return nodes;
|
|
697
|
+
}
|
|
698
|
+
var DOT_SIZE = 6;
|
|
699
|
+
var LOOP_MS = 520;
|
|
700
|
+
var STAGGER_MS = Math.round(LOOP_MS * 0.18);
|
|
701
|
+
var OPACITY_REST = 0.44;
|
|
702
|
+
var OPACITY_STATIC = 0.5;
|
|
703
|
+
var TRI_IN = [0, 0.5, 1];
|
|
704
|
+
var DOT_INDICES = [0, 1, 2];
|
|
705
|
+
function TypingIndicator() {
|
|
706
|
+
const theme = useEverfurTheme();
|
|
707
|
+
const styles = useMemo(() => makeStyles3(theme), [theme]);
|
|
708
|
+
const animate = !theme.motion.reducedMotion;
|
|
709
|
+
return /* @__PURE__ */ jsx(
|
|
710
|
+
View,
|
|
711
|
+
{
|
|
712
|
+
style: styles.card,
|
|
713
|
+
accessible: true,
|
|
714
|
+
accessibilityRole: "text",
|
|
715
|
+
accessibilityLabel: "Assistant is typing",
|
|
716
|
+
accessibilityLiveRegion: "polite",
|
|
717
|
+
accessibilityState: { busy: true },
|
|
718
|
+
children: DOT_INDICES.map(
|
|
719
|
+
(index) => animate ? /* @__PURE__ */ jsx(AnimatedDot, { index, style: styles.dot }, index) : /* @__PURE__ */ jsx(View, { style: [styles.dot, styles.dotStatic] }, index)
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
function AnimatedDot({ index, style }) {
|
|
725
|
+
const t = useRef(new Animated.Value(0)).current;
|
|
726
|
+
useEffect(() => {
|
|
727
|
+
const anim = Animated.sequence([
|
|
728
|
+
Animated.delay(index * STAGGER_MS),
|
|
729
|
+
Animated.loop(Animated.timing(t, { toValue: 1, duration: LOOP_MS, useNativeDriver: true }))
|
|
730
|
+
]);
|
|
731
|
+
anim.start();
|
|
732
|
+
return () => anim.stop();
|
|
733
|
+
}, [t, index]);
|
|
734
|
+
const opacity = t.interpolate({ inputRange: TRI_IN, outputRange: [OPACITY_REST, 1, OPACITY_REST] });
|
|
735
|
+
const translateY = t.interpolate({ inputRange: TRI_IN, outputRange: [0, -3.5, 0] });
|
|
736
|
+
return /* @__PURE__ */ jsx(Animated.View, { style: [style, { opacity, transform: [{ translateY }] }] });
|
|
737
|
+
}
|
|
738
|
+
function makeStyles3(theme) {
|
|
739
|
+
const c = theme.color;
|
|
740
|
+
const s = theme.space;
|
|
741
|
+
const r = theme.radius;
|
|
742
|
+
return StyleSheet.create({
|
|
743
|
+
card: {
|
|
744
|
+
alignSelf: "flex-start",
|
|
745
|
+
flexDirection: "row",
|
|
746
|
+
alignItems: "center",
|
|
747
|
+
gap: s.xs,
|
|
748
|
+
paddingHorizontal: s.md,
|
|
749
|
+
paddingVertical: s.sm,
|
|
750
|
+
backgroundColor: c.surfaceCream,
|
|
751
|
+
borderRadius: r.md,
|
|
752
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
753
|
+
borderColor: c.brandBorder
|
|
754
|
+
},
|
|
755
|
+
dot: { width: DOT_SIZE, height: DOT_SIZE, borderRadius: r.pill, backgroundColor: c.brand },
|
|
756
|
+
dotStatic: { opacity: OPACITY_STATIC }
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
var SPINNER_DELAY_MS = 300;
|
|
760
|
+
var DEFAULT_PLACEHOLDER = "Message";
|
|
761
|
+
var DEFAULT_SEND_LABEL = "Send";
|
|
762
|
+
var DEFAULT_STOP_LABEL = "Stop";
|
|
763
|
+
var FOLLOW_UPS_LABEL = "Follow up questions";
|
|
764
|
+
var POWERED_BY_LABEL = "powered by everfur";
|
|
765
|
+
var MIN_SURFACE_HEIGHT = 120;
|
|
766
|
+
var MIN_TOUCH_TARGET = 44;
|
|
767
|
+
var INPUT_MAX_HEIGHT = 120;
|
|
768
|
+
var EMPTY_GLYPH_SIZE = 40;
|
|
769
|
+
var BUBBLE_MAX_WIDTH = "82%";
|
|
770
|
+
var COMPOSER_BORDER_WIDTH = 1.25;
|
|
771
|
+
var BUBBLE_EDGE_WIDTH = 0.5;
|
|
772
|
+
var PENDING_OPACITY = 0.6;
|
|
773
|
+
var COMPOSER_SHADOW_OPACITY = 0.1;
|
|
774
|
+
var COMPOSER_SHADOW_OFFSET_Y = 2;
|
|
775
|
+
var COMPOSER_SHADOW_ELEVATION = 2;
|
|
776
|
+
function EverfurChat(props) {
|
|
777
|
+
const { petRef, conversationId, style } = props;
|
|
778
|
+
return /* @__PURE__ */ jsx(CapabilityGate, { capability: "chat", style, children: /* @__PURE__ */ jsx(ChatBody, { petRef, conversationId, style }) });
|
|
779
|
+
}
|
|
780
|
+
function selectView2(state) {
|
|
781
|
+
if (state.isBootstrapping) return "loading";
|
|
782
|
+
if (state.status === "error" && state.error) return "error";
|
|
783
|
+
if (state.messages.length === 0) return "empty";
|
|
784
|
+
return "populated";
|
|
785
|
+
}
|
|
786
|
+
function ChatBody({
|
|
787
|
+
petRef,
|
|
788
|
+
conversationId,
|
|
789
|
+
style
|
|
790
|
+
}) {
|
|
791
|
+
const { theme, styles } = useChatTheme();
|
|
792
|
+
const chatOpts = useMemo(
|
|
793
|
+
() => buildChatOpts(petRef, conversationId),
|
|
794
|
+
[petRef, conversationId]
|
|
795
|
+
);
|
|
796
|
+
const chat = useEverfurChat(chatOpts);
|
|
797
|
+
const view = selectView2(chat);
|
|
798
|
+
const insets = useSafeAreaInsets();
|
|
799
|
+
const onSelectPrompt = useCallback(
|
|
800
|
+
(prompt) => {
|
|
801
|
+
void chat.send(prompt);
|
|
802
|
+
},
|
|
803
|
+
[chat.send]
|
|
804
|
+
);
|
|
805
|
+
return (
|
|
806
|
+
// KeyboardAvoidingView so the composer + disclaimer lift above the software keyboard while the user types.
|
|
807
|
+
/* @__PURE__ */ jsxs(
|
|
808
|
+
KeyboardAvoidingView,
|
|
809
|
+
{
|
|
810
|
+
style: [styles.container, { paddingTop: insets.top }, style],
|
|
811
|
+
behavior: Platform.OS === "ios" ? "padding" : void 0,
|
|
812
|
+
children: [
|
|
813
|
+
/* @__PURE__ */ jsx(View, { style: styles.body, children: renderView(view, chat, onSelectPrompt) }),
|
|
814
|
+
/* @__PURE__ */ jsx(Composer, { send: chat.send, stop: chat.stop, isStreaming: chat.isStreaming }),
|
|
815
|
+
/* @__PURE__ */ jsx(ClinicalDisclaimer, {}),
|
|
816
|
+
theme.meta.hidePoweredBy ? null : /* @__PURE__ */ jsx(Text, { style: styles.poweredBy, accessibilityRole: "text", accessibilityLabel: POWERED_BY_LABEL, children: POWERED_BY_LABEL })
|
|
817
|
+
]
|
|
818
|
+
}
|
|
819
|
+
)
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
function Composer({
|
|
823
|
+
send,
|
|
824
|
+
stop,
|
|
825
|
+
isStreaming
|
|
826
|
+
}) {
|
|
827
|
+
const { theme, styles } = useChatTheme();
|
|
828
|
+
const [draft, setDraft] = useState("");
|
|
829
|
+
const placeholder = theme.meta.placeholder ?? DEFAULT_PLACEHOLDER;
|
|
830
|
+
const onSend = useCallback(() => {
|
|
831
|
+
if (draft.trim().length === 0) return;
|
|
832
|
+
if ([...draft].length > MESSAGE_MAX_CHARS) return;
|
|
833
|
+
void send(draft);
|
|
834
|
+
setDraft("");
|
|
835
|
+
}, [draft, send]);
|
|
836
|
+
const onStop = useCallback(() => {
|
|
837
|
+
stop();
|
|
838
|
+
}, [stop]);
|
|
839
|
+
const isOverLimit = useMemo(() => [...draft].length > MESSAGE_MAX_CHARS, [draft]);
|
|
840
|
+
return /* @__PURE__ */ jsxs(View, { style: styles.composer, children: [
|
|
841
|
+
/* @__PURE__ */ jsx(
|
|
842
|
+
TextInput,
|
|
843
|
+
{
|
|
844
|
+
style: styles.input,
|
|
845
|
+
value: draft,
|
|
846
|
+
onChangeText: setDraft,
|
|
847
|
+
placeholder,
|
|
848
|
+
placeholderTextColor: theme.color.text.placeholder,
|
|
849
|
+
accessibilityLabel: placeholder,
|
|
850
|
+
onSubmitEditing: onSend,
|
|
851
|
+
returnKeyType: "send",
|
|
852
|
+
blurOnSubmit: false,
|
|
853
|
+
multiline: true,
|
|
854
|
+
editable: true
|
|
855
|
+
}
|
|
856
|
+
),
|
|
857
|
+
isStreaming ? /* @__PURE__ */ jsx(
|
|
858
|
+
Pressable,
|
|
859
|
+
{
|
|
860
|
+
onPress: onStop,
|
|
861
|
+
accessibilityRole: "button",
|
|
862
|
+
accessibilityLabel: DEFAULT_STOP_LABEL,
|
|
863
|
+
style: styles.actionButton,
|
|
864
|
+
children: /* @__PURE__ */ jsx(Text, { style: styles.actionLabel, children: DEFAULT_STOP_LABEL })
|
|
865
|
+
}
|
|
866
|
+
) : /* @__PURE__ */ jsx(
|
|
867
|
+
Pressable,
|
|
868
|
+
{
|
|
869
|
+
onPress: onSend,
|
|
870
|
+
disabled: isOverLimit,
|
|
871
|
+
accessibilityRole: "button",
|
|
872
|
+
accessibilityLabel: DEFAULT_SEND_LABEL,
|
|
873
|
+
accessibilityState: { disabled: isOverLimit },
|
|
874
|
+
style: [styles.actionButton, isOverLimit ? styles.actionButtonDisabled : null],
|
|
875
|
+
children: /* @__PURE__ */ jsx(Text, { style: [styles.actionLabel, isOverLimit ? styles.actionLabelDisabled : null], children: DEFAULT_SEND_LABEL })
|
|
876
|
+
}
|
|
877
|
+
)
|
|
878
|
+
] });
|
|
879
|
+
}
|
|
880
|
+
function renderView(view, chat, onSelectPrompt) {
|
|
881
|
+
switch (view) {
|
|
882
|
+
case "loading":
|
|
883
|
+
return /* @__PURE__ */ jsx(LoadingState, {});
|
|
884
|
+
case "empty":
|
|
885
|
+
return /* @__PURE__ */ jsx(EmptyState, { prompts: chat.suggestedPrompts, onSelectPrompt });
|
|
886
|
+
case "error":
|
|
887
|
+
return /* @__PURE__ */ jsx(ErrorState, { error: chat.error, onRetry: chat.retryLast });
|
|
888
|
+
case "populated":
|
|
889
|
+
return /* @__PURE__ */ jsx(
|
|
890
|
+
PopulatedState,
|
|
891
|
+
{
|
|
892
|
+
messages: chat.messages,
|
|
893
|
+
prompts: chat.suggestedPrompts,
|
|
894
|
+
onSelectPrompt,
|
|
895
|
+
isStreaming: chat.isStreaming
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
default:
|
|
899
|
+
return assertNever(view);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function buildChatOpts(petRef, conversationId) {
|
|
903
|
+
const opts = {};
|
|
904
|
+
if (petRef !== void 0) opts.petRef = petRef;
|
|
905
|
+
if (conversationId !== void 0) opts.conversationId = conversationId;
|
|
906
|
+
return opts;
|
|
907
|
+
}
|
|
908
|
+
function LoadingState() {
|
|
909
|
+
const { theme, styles } = useChatTheme();
|
|
910
|
+
const [visible, setVisible] = useState(false);
|
|
911
|
+
useEffect(() => {
|
|
912
|
+
const id = setTimeout(() => setVisible(true), SPINNER_DELAY_MS);
|
|
913
|
+
return () => clearTimeout(id);
|
|
914
|
+
}, []);
|
|
915
|
+
return /* @__PURE__ */ jsx(View, { style: styles.centered, accessible: true, accessibilityState: { busy: true }, accessibilityLabel: "Loading", children: visible ? /* @__PURE__ */ jsx(ActivityIndicator, { color: theme.color.brand }) : null });
|
|
916
|
+
}
|
|
917
|
+
function EmptyState({
|
|
918
|
+
prompts,
|
|
919
|
+
onSelectPrompt
|
|
920
|
+
}) {
|
|
921
|
+
const { styles } = useChatTheme();
|
|
922
|
+
return /* @__PURE__ */ jsxs(ScrollView, { contentContainerStyle: styles.emptyContent, keyboardShouldPersistTaps: "handled", children: [
|
|
923
|
+
/* @__PURE__ */ jsx(View, { style: styles.emptyGlyphWrap, accessible: true, accessibilityRole: "summary", accessibilityLabel: "No messages yet", children: /* @__PURE__ */ jsx(View, { style: styles.emptyGlyph }) }),
|
|
924
|
+
/* @__PURE__ */ jsx(FollowUps, { prompts, onSelectPrompt })
|
|
925
|
+
] });
|
|
926
|
+
}
|
|
927
|
+
function FollowUps({
|
|
928
|
+
prompts,
|
|
929
|
+
onSelectPrompt
|
|
930
|
+
}) {
|
|
931
|
+
const { styles } = useChatTheme();
|
|
932
|
+
if (prompts.length === 0) return null;
|
|
933
|
+
return /* @__PURE__ */ jsxs(View, { style: styles.followUps, children: [
|
|
934
|
+
/* @__PURE__ */ jsx(Text, { style: styles.followUpsLabel, accessibilityRole: "header", children: FOLLOW_UPS_LABEL }),
|
|
935
|
+
prompts.map((prompt, index) => /* @__PURE__ */ jsx(
|
|
936
|
+
Pressable,
|
|
937
|
+
{
|
|
938
|
+
onPress: () => onSelectPrompt(prompt),
|
|
939
|
+
accessibilityRole: "button",
|
|
940
|
+
accessibilityLabel: prompt,
|
|
941
|
+
style: [styles.followUpRow, index > 0 ? styles.followUpDivider : null],
|
|
942
|
+
children: /* @__PURE__ */ jsx(Text, { style: styles.followUpText, children: prompt })
|
|
943
|
+
},
|
|
944
|
+
prompt
|
|
945
|
+
))
|
|
946
|
+
] });
|
|
947
|
+
}
|
|
948
|
+
function ErrorState({ error, onRetry }) {
|
|
949
|
+
const { styles } = useChatTheme();
|
|
950
|
+
return /* @__PURE__ */ jsxs(View, { style: styles.centered, accessible: true, accessibilityRole: "alert", children: [
|
|
951
|
+
/* @__PURE__ */ jsx(Text, { style: styles.errorText, children: error.displayMessage }),
|
|
952
|
+
error.retryable ? /* @__PURE__ */ jsx(
|
|
953
|
+
Pressable,
|
|
954
|
+
{
|
|
955
|
+
onPress: onRetry,
|
|
956
|
+
accessibilityRole: "button",
|
|
957
|
+
accessibilityLabel: "Try again",
|
|
958
|
+
style: styles.retryButton,
|
|
959
|
+
children: /* @__PURE__ */ jsx(Text, { style: styles.retryLabel, children: "Try again" })
|
|
960
|
+
}
|
|
961
|
+
) : null
|
|
962
|
+
] });
|
|
963
|
+
}
|
|
964
|
+
function PopulatedState({
|
|
965
|
+
messages,
|
|
966
|
+
prompts,
|
|
967
|
+
onSelectPrompt,
|
|
968
|
+
isStreaming
|
|
969
|
+
}) {
|
|
970
|
+
const { styles } = useChatTheme();
|
|
971
|
+
const lastMessage = messages[messages.length - 1];
|
|
972
|
+
const awaitingAssistant = isStreaming && (lastMessage === void 0 || lastMessage.role === "user");
|
|
973
|
+
return /* @__PURE__ */ jsxs(
|
|
974
|
+
ScrollView,
|
|
975
|
+
{
|
|
976
|
+
style: styles.list,
|
|
977
|
+
contentContainerStyle: styles.listContent,
|
|
978
|
+
accessibilityLabel: "Conversation",
|
|
979
|
+
keyboardShouldPersistTaps: "handled",
|
|
980
|
+
children: [
|
|
981
|
+
messages.map((message) => /* @__PURE__ */ jsx(MessageRow, { message }, message.id)),
|
|
982
|
+
awaitingAssistant ? /* @__PURE__ */ jsx(TypingIndicator, {}) : null,
|
|
983
|
+
!isStreaming ? /* @__PURE__ */ jsx(FollowUps, { prompts, onSelectPrompt }) : null
|
|
984
|
+
]
|
|
985
|
+
}
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
function MessageRow({ message }) {
|
|
989
|
+
const { styles } = useChatTheme();
|
|
990
|
+
const isUser = message.role === "user";
|
|
991
|
+
const sender = isUser ? "You" : "Assistant";
|
|
992
|
+
return /* @__PURE__ */ jsx(
|
|
993
|
+
View,
|
|
994
|
+
{
|
|
995
|
+
style: [styles.row, isUser ? styles.rowUser : styles.rowAssistant],
|
|
996
|
+
accessible: true,
|
|
997
|
+
accessibilityRole: "text",
|
|
998
|
+
accessibilityLabel: `${sender}: ${message.text}`,
|
|
999
|
+
accessibilityState: message.pending === true ? { busy: true } : void 0,
|
|
1000
|
+
children: /* @__PURE__ */ jsx(View, { style: [styles.bubble, isUser ? styles.bubbleUser : styles.bubbleAssistant, message.pending ? styles.bubblePending : null], children: /* @__PURE__ */ jsx(Text, { style: isUser ? styles.bubbleTextUser : styles.bubbleTextAssistant, children: isUser ? message.text : renderInlineMarkdown(message.text, {
|
|
1001
|
+
bold: styles.bubbleBold,
|
|
1002
|
+
italic: styles.bubbleItalic,
|
|
1003
|
+
code: styles.bubbleCode
|
|
1004
|
+
}) }) })
|
|
1005
|
+
}
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
function useChatTheme() {
|
|
1009
|
+
const theme = useEverfurTheme();
|
|
1010
|
+
const styles = useMemo(() => makeStyles4(theme), [theme]);
|
|
1011
|
+
return { theme, styles };
|
|
1012
|
+
}
|
|
1013
|
+
function makeStyles4(theme) {
|
|
1014
|
+
const c = theme.color;
|
|
1015
|
+
const s = theme.space;
|
|
1016
|
+
const r = theme.radius;
|
|
1017
|
+
const t = theme.type;
|
|
1018
|
+
const weightLabel = t.weight.medium;
|
|
1019
|
+
return StyleSheet.create({
|
|
1020
|
+
// Cream ground so the branded bubbles / bare assistant text sit on an Everfur surface (host `style` still wins).
|
|
1021
|
+
container: { flex: 1, minHeight: MIN_SURFACE_HEIGHT, backgroundColor: c.surfaceCream },
|
|
1022
|
+
body: { flex: 1 },
|
|
1023
|
+
centered: { minHeight: MIN_SURFACE_HEIGHT, flexGrow: 1, alignItems: "center", justifyContent: "center", padding: s.xl },
|
|
1024
|
+
// Deliberate empty-state placeholder: a soft brand-wash disc (never a blank region).
|
|
1025
|
+
emptyGlyph: {
|
|
1026
|
+
width: EMPTY_GLYPH_SIZE,
|
|
1027
|
+
height: EMPTY_GLYPH_SIZE,
|
|
1028
|
+
borderRadius: r.pill,
|
|
1029
|
+
backgroundColor: c.brandSoft,
|
|
1030
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
1031
|
+
borderColor: c.brandBorder
|
|
1032
|
+
},
|
|
1033
|
+
// Empty state scrolls (glyph centered, then the follow-up list) so nothing overflows a short surface.
|
|
1034
|
+
emptyContent: { flexGrow: 1, justifyContent: "center", padding: s.lg, gap: s.lg },
|
|
1035
|
+
emptyGlyphWrap: { alignItems: "center", justifyContent: "center", paddingVertical: s.xl },
|
|
1036
|
+
errorText: {
|
|
1037
|
+
fontFamily: t.family,
|
|
1038
|
+
fontSize: t.size.subtitle,
|
|
1039
|
+
lineHeight: t.size.subtitle * t.lineHeight.snug,
|
|
1040
|
+
color: c.danger,
|
|
1041
|
+
textAlign: "center",
|
|
1042
|
+
marginBottom: s.md
|
|
1043
|
+
},
|
|
1044
|
+
// Brand-green pill retry CTA with the WCAG-legible on-brand foreground.
|
|
1045
|
+
retryButton: {
|
|
1046
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
1047
|
+
minWidth: MIN_TOUCH_TARGET,
|
|
1048
|
+
alignItems: "center",
|
|
1049
|
+
justifyContent: "center",
|
|
1050
|
+
paddingHorizontal: s.xl,
|
|
1051
|
+
paddingVertical: s.sm,
|
|
1052
|
+
borderRadius: r.pill,
|
|
1053
|
+
backgroundColor: c.brand
|
|
1054
|
+
},
|
|
1055
|
+
retryLabel: { fontFamily: t.family, color: c.onPrimary, fontSize: t.size.body, fontWeight: weightLabel },
|
|
1056
|
+
list: { flex: 1 },
|
|
1057
|
+
listContent: { padding: s.lg, gap: s.md },
|
|
1058
|
+
row: { flexDirection: "row" },
|
|
1059
|
+
rowUser: { justifyContent: "flex-end" },
|
|
1060
|
+
rowAssistant: { justifyContent: "flex-start" },
|
|
1061
|
+
bubble: { maxWidth: BUBBLE_MAX_WIDTH, paddingHorizontal: s.md, paddingVertical: s.sm, borderRadius: r.lg },
|
|
1062
|
+
// User = brand accent fill + 0.5px accent edge, tail corner tightened (the mobile asymmetry).
|
|
1063
|
+
bubbleUser: {
|
|
1064
|
+
backgroundColor: c.bubbleUser,
|
|
1065
|
+
borderBottomRightRadius: r.tile,
|
|
1066
|
+
borderWidth: BUBBLE_EDGE_WIDTH,
|
|
1067
|
+
borderColor: c.accentEdge
|
|
1068
|
+
},
|
|
1069
|
+
// Assistant = bare text block (no fill, flush-left) so the reply reads as plain, on-surface prose.
|
|
1070
|
+
bubbleAssistant: { paddingHorizontal: 0 },
|
|
1071
|
+
bubblePending: { opacity: PENDING_OPACITY },
|
|
1072
|
+
bubbleTextUser: {
|
|
1073
|
+
fontFamily: t.family,
|
|
1074
|
+
color: c.onAccent,
|
|
1075
|
+
fontSize: t.size.body,
|
|
1076
|
+
lineHeight: t.size.body * t.lineHeight.normal
|
|
1077
|
+
},
|
|
1078
|
+
bubbleTextAssistant: {
|
|
1079
|
+
fontFamily: t.family,
|
|
1080
|
+
color: c.text.primary,
|
|
1081
|
+
fontSize: t.size.body,
|
|
1082
|
+
lineHeight: t.size.body * t.lineHeight.normal
|
|
1083
|
+
},
|
|
1084
|
+
// Inline-markdown runs inside the assistant reply (nested <Text>): bold entities + optional italic / code.
|
|
1085
|
+
bubbleBold: { fontWeight: t.weight.bold },
|
|
1086
|
+
bubbleItalic: { fontStyle: "italic" },
|
|
1087
|
+
bubbleCode: { fontFamily: t.mono },
|
|
1088
|
+
// "Follow up questions": a labeled, list-with-dividers block of tappable, ready-to-send backend prompts.
|
|
1089
|
+
followUps: { marginTop: s.sm, borderRadius: r.md, backgroundColor: c.brandSoft, borderWidth: StyleSheet.hairlineWidth, borderColor: c.brandBorder },
|
|
1090
|
+
followUpsLabel: {
|
|
1091
|
+
fontFamily: t.family,
|
|
1092
|
+
color: c.text.secondary,
|
|
1093
|
+
fontSize: t.size.caption,
|
|
1094
|
+
fontWeight: weightLabel,
|
|
1095
|
+
letterSpacing: t.letterSpacing.eyebrow,
|
|
1096
|
+
textTransform: "uppercase",
|
|
1097
|
+
paddingHorizontal: s.md,
|
|
1098
|
+
paddingTop: s.md,
|
|
1099
|
+
paddingBottom: s.sm
|
|
1100
|
+
},
|
|
1101
|
+
followUpRow: { minHeight: MIN_TOUCH_TARGET, justifyContent: "center", paddingHorizontal: s.md, paddingVertical: s.sm },
|
|
1102
|
+
followUpDivider: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: c.brandBorder },
|
|
1103
|
+
followUpText: {
|
|
1104
|
+
fontFamily: t.family,
|
|
1105
|
+
color: c.text.primary,
|
|
1106
|
+
fontSize: t.size.body,
|
|
1107
|
+
lineHeight: t.size.body * t.lineHeight.snug
|
|
1108
|
+
},
|
|
1109
|
+
// Composer = inset cream pill with a brand border and a soft brand-tinted lift.
|
|
1110
|
+
composer: {
|
|
1111
|
+
flexDirection: "row",
|
|
1112
|
+
alignItems: "flex-end",
|
|
1113
|
+
gap: s.sm,
|
|
1114
|
+
marginHorizontal: s.md,
|
|
1115
|
+
marginTop: s.xs,
|
|
1116
|
+
marginBottom: s.sm,
|
|
1117
|
+
paddingHorizontal: s.md,
|
|
1118
|
+
paddingVertical: s.sm,
|
|
1119
|
+
backgroundColor: c.surfaceCream,
|
|
1120
|
+
borderWidth: COMPOSER_BORDER_WIDTH,
|
|
1121
|
+
borderColor: c.brandBorder,
|
|
1122
|
+
borderRadius: r.pill,
|
|
1123
|
+
shadowColor: c.brand,
|
|
1124
|
+
shadowOpacity: COMPOSER_SHADOW_OPACITY,
|
|
1125
|
+
shadowRadius: s.sm,
|
|
1126
|
+
shadowOffset: { width: 0, height: COMPOSER_SHADOW_OFFSET_Y },
|
|
1127
|
+
elevation: COMPOSER_SHADOW_ELEVATION
|
|
1128
|
+
},
|
|
1129
|
+
// Bare input inside the pill (the composer owns the border / fill); placeholder color set on the element.
|
|
1130
|
+
input: {
|
|
1131
|
+
flex: 1,
|
|
1132
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
1133
|
+
maxHeight: INPUT_MAX_HEIGHT,
|
|
1134
|
+
paddingHorizontal: s.md,
|
|
1135
|
+
paddingVertical: s.sm,
|
|
1136
|
+
fontFamily: t.family,
|
|
1137
|
+
fontSize: t.size.body,
|
|
1138
|
+
lineHeight: t.size.body * t.lineHeight.normal,
|
|
1139
|
+
color: c.text.primary
|
|
1140
|
+
},
|
|
1141
|
+
// Brand-green pill send / stop control.
|
|
1142
|
+
actionButton: {
|
|
1143
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
1144
|
+
minWidth: MIN_TOUCH_TARGET,
|
|
1145
|
+
alignItems: "center",
|
|
1146
|
+
justifyContent: "center",
|
|
1147
|
+
paddingHorizontal: s.lg,
|
|
1148
|
+
borderRadius: r.pill,
|
|
1149
|
+
backgroundColor: c.brand
|
|
1150
|
+
},
|
|
1151
|
+
actionLabel: { fontFamily: t.family, color: c.onPrimary, fontSize: t.size.body, fontWeight: weightLabel },
|
|
1152
|
+
// The over-length state. Opacity alone would carry the meaning by appearance only, so the control also
|
|
1153
|
+
// sets `disabled` and `accessibilityState.disabled`, which is what a screen reader announces.
|
|
1154
|
+
actionButtonDisabled: { opacity: 0.45 },
|
|
1155
|
+
actionLabelDisabled: { color: c.onPrimary },
|
|
1156
|
+
// Everfur's own attribution: small, muted, centered under the disclaimer.
|
|
1157
|
+
poweredBy: {
|
|
1158
|
+
fontFamily: t.family,
|
|
1159
|
+
color: c.text.muted,
|
|
1160
|
+
fontSize: t.size.micro,
|
|
1161
|
+
textAlign: "center",
|
|
1162
|
+
paddingBottom: s.xs
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
export { EverfurChat, useEverfurChat };
|