@layla-network/sdk 0.1.2 → 0.1.4
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.cjs +219 -8
- package/dist/index.d.cts +55 -2
- package/dist/index.d.ts +55 -2
- package/dist/index.js +216 -7
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -27,7 +27,9 @@ __export(index_exports, {
|
|
|
27
27
|
LaylaBridgeUnavailableError: () => LaylaBridgeUnavailableError,
|
|
28
28
|
LaylaError: () => LaylaError,
|
|
29
29
|
LaylaSDK: () => LaylaSDK,
|
|
30
|
-
default: () => client_default
|
|
30
|
+
default: () => client_default,
|
|
31
|
+
installLaylaMock: () => installLaylaMock,
|
|
32
|
+
makeMockCharacter: () => makeMockCharacter
|
|
31
33
|
});
|
|
32
34
|
module.exports = __toCommonJS(index_exports);
|
|
33
35
|
|
|
@@ -536,13 +538,15 @@ function oneShot(command, responseEvent, extract, signal) {
|
|
|
536
538
|
|
|
537
539
|
// src/resources/characters.ts
|
|
538
540
|
var Characters = class {
|
|
539
|
-
|
|
540
|
-
* Ask the native host for the available character cards. Resolves once with
|
|
541
|
-
* the host's `on_get_characters_response` payload, or rejects on error/abort.
|
|
542
|
-
*/
|
|
543
|
-
list(options = {}) {
|
|
541
|
+
list(offset = 0, range = 10, options = {}) {
|
|
544
542
|
return oneShot(
|
|
545
|
-
{
|
|
543
|
+
{
|
|
544
|
+
cmd: "get_characters",
|
|
545
|
+
data: {
|
|
546
|
+
offset,
|
|
547
|
+
limit: range
|
|
548
|
+
}
|
|
549
|
+
},
|
|
546
550
|
"on_get_characters_response",
|
|
547
551
|
(event) => {
|
|
548
552
|
const data = event.data;
|
|
@@ -578,6 +582,211 @@ var LaylaSDK = class {
|
|
|
578
582
|
}
|
|
579
583
|
};
|
|
580
584
|
var client_default = LaylaSDK;
|
|
585
|
+
|
|
586
|
+
// src/mock.ts
|
|
587
|
+
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
588
|
+
function makeMockCharacter(name, overrides = {}) {
|
|
589
|
+
return {
|
|
590
|
+
id: `mock-${name.toLowerCase()}`,
|
|
591
|
+
data: {
|
|
592
|
+
spec: "chara_card_v2",
|
|
593
|
+
spec_version: "2.0",
|
|
594
|
+
data: {
|
|
595
|
+
name,
|
|
596
|
+
description: `${name} is a mock character for local testing.`,
|
|
597
|
+
personality: "friendly, helpful",
|
|
598
|
+
scenario: "",
|
|
599
|
+
first_mes: `Hi, I'm ${name}.`,
|
|
600
|
+
mes_example: "",
|
|
601
|
+
creator_notes: "Generated by layla mock.",
|
|
602
|
+
system_prompt: "",
|
|
603
|
+
post_history_instructions: "",
|
|
604
|
+
alternate_greetings: [],
|
|
605
|
+
tags: ["mock"],
|
|
606
|
+
creator: "layla-mock",
|
|
607
|
+
character_version: "1.0",
|
|
608
|
+
extensions: {},
|
|
609
|
+
...overrides
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function installLaylaMock(options = {}) {
|
|
615
|
+
if (typeof window === "undefined") {
|
|
616
|
+
throw new Error(
|
|
617
|
+
"installLaylaMock needs a browser-like environment (window). In Node, run your test under jsdom or happy-dom."
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
const latencyMs = options.latencyMs ?? 150;
|
|
621
|
+
const tokenDelayMs = options.tokenDelayMs ?? 40;
|
|
622
|
+
const errorRate = options.errorRate ?? 0;
|
|
623
|
+
const characters = options.characters ?? [makeMockCharacter("Aria"), makeMockCharacter("Kai")];
|
|
624
|
+
let current = null;
|
|
625
|
+
const log = (...a) => {
|
|
626
|
+
if (options.debug) console.log("[layla-mock]", ...a);
|
|
627
|
+
};
|
|
628
|
+
const emit = (event) => {
|
|
629
|
+
log("RN->web", event);
|
|
630
|
+
window.dispatchEvent(new MessageEvent("message", { data: JSON.stringify(event) }));
|
|
631
|
+
};
|
|
632
|
+
const emitError = (message) => emit({ event: "on_error", data: { message } });
|
|
633
|
+
const shouldError = () => errorRate > 0 && Math.random() < errorRate;
|
|
634
|
+
const tokenize = (text) => text.match(/\S+\s*/g) ?? (text ? [text] : []);
|
|
635
|
+
const defaultReply = (messages) => {
|
|
636
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
637
|
+
const said = lastUser?.content ?? "(nothing)";
|
|
638
|
+
return `This is a mock Layla response. You said: "${said}". Tokens stream one at a time so you can exercise your streaming UI, cancellation, and the final-content promise.`;
|
|
639
|
+
};
|
|
640
|
+
async function* tokenSource(messages) {
|
|
641
|
+
const produced = options.respond ? await options.respond(messages) : defaultReply(messages);
|
|
642
|
+
if (typeof produced === "string") {
|
|
643
|
+
yield* tokenize(produced);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (Array.isArray(produced)) {
|
|
647
|
+
yield* produced;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (produced && typeof produced === "object") {
|
|
651
|
+
if (Symbol.asyncIterator in produced) {
|
|
652
|
+
yield* produced;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (Symbol.iterator in produced) {
|
|
656
|
+
yield* produced;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async function handleSend(messages) {
|
|
661
|
+
const gen = { cancelled: false };
|
|
662
|
+
current = gen;
|
|
663
|
+
try {
|
|
664
|
+
await delay(latencyMs);
|
|
665
|
+
if (gen.cancelled) return;
|
|
666
|
+
if (shouldError()) {
|
|
667
|
+
emitError("Simulated model error");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
let snapshot = "";
|
|
671
|
+
for await (const delta of tokenSource(messages)) {
|
|
672
|
+
if (gen.cancelled) return;
|
|
673
|
+
snapshot += delta;
|
|
674
|
+
emit({ event: "on_message", data: { msg: snapshot, delta } });
|
|
675
|
+
await delay(tokenDelayMs);
|
|
676
|
+
if (gen.cancelled) return;
|
|
677
|
+
}
|
|
678
|
+
emit({ event: "on_message_end" });
|
|
679
|
+
} finally {
|
|
680
|
+
if (current === gen) current = null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function handleCancel() {
|
|
684
|
+
if (current && !current.cancelled) {
|
|
685
|
+
current.cancelled = true;
|
|
686
|
+
queueMicrotask(() => emit({ event: "on_message_end" }));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async function handleGetCharacters(data) {
|
|
690
|
+
await delay(latencyMs);
|
|
691
|
+
if (shouldError()) {
|
|
692
|
+
emitError("Simulated characters error");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const { offset, limit } = data;
|
|
696
|
+
emit({
|
|
697
|
+
event: "on_get_characters_response",
|
|
698
|
+
data: characters.slice(offset, offset + limit)
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
async function handleGetCharacterImage(data) {
|
|
702
|
+
await delay(latencyMs);
|
|
703
|
+
if (shouldError()) {
|
|
704
|
+
emitError("Simulated character image error");
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
emit({
|
|
708
|
+
event: "on_get_character_image_response",
|
|
709
|
+
data: {
|
|
710
|
+
character_id: data.character_id,
|
|
711
|
+
image_data_base64: "https://picsum.photos/id/237/200/300"
|
|
712
|
+
// we return a url for mock, since we usually use this as the "src" of an <img> tag, it should work fine as a mock
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
async function handleGenerateImage(_) {
|
|
717
|
+
await delay(latencyMs);
|
|
718
|
+
if (shouldError()) {
|
|
719
|
+
emitError("Simulated image generation error");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const totalSteps = 5;
|
|
723
|
+
for (let step = 1; step <= totalSteps; step++) {
|
|
724
|
+
await delay(latencyMs);
|
|
725
|
+
emit({
|
|
726
|
+
event: "on_generate_image_progress",
|
|
727
|
+
data: {
|
|
728
|
+
status: `Generating image... (${step}/${totalSteps})`,
|
|
729
|
+
steps: step,
|
|
730
|
+
total_steps: totalSteps
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
emit({
|
|
735
|
+
event: "on_generate_image_response",
|
|
736
|
+
data: {
|
|
737
|
+
image_data_base64: "https://picsum.photos/200/300"
|
|
738
|
+
// Placeholder image URL for the mock
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
const fakeBridge = {
|
|
743
|
+
postMessage(raw) {
|
|
744
|
+
let msg;
|
|
745
|
+
try {
|
|
746
|
+
msg = JSON.parse(raw);
|
|
747
|
+
} catch {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
log("web->RN", msg);
|
|
751
|
+
switch (msg.cmd) {
|
|
752
|
+
case "send_message":
|
|
753
|
+
void handleSend(msg.data);
|
|
754
|
+
break;
|
|
755
|
+
case "cancel":
|
|
756
|
+
handleCancel();
|
|
757
|
+
break;
|
|
758
|
+
case "get_characters":
|
|
759
|
+
void handleGetCharacters(msg.data);
|
|
760
|
+
break;
|
|
761
|
+
case "get_character_image":
|
|
762
|
+
void handleGetCharacterImage(msg.data);
|
|
763
|
+
break;
|
|
764
|
+
case "generate_image":
|
|
765
|
+
void handleGenerateImage(msg.data);
|
|
766
|
+
break;
|
|
767
|
+
default:
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
const previous = window.ReactNativeWebView;
|
|
773
|
+
if (previous) {
|
|
774
|
+
log("warning: replacing an existing window.ReactNativeWebView");
|
|
775
|
+
}
|
|
776
|
+
window.ReactNativeWebView = fakeBridge;
|
|
777
|
+
log("installed");
|
|
778
|
+
let installed = true;
|
|
779
|
+
return {
|
|
780
|
+
uninstall() {
|
|
781
|
+
if (!installed) return;
|
|
782
|
+
installed = false;
|
|
783
|
+
if (current) current.cancelled = true;
|
|
784
|
+
if (previous) window.ReactNativeWebView = previous;
|
|
785
|
+
else delete window.ReactNativeWebView;
|
|
786
|
+
log("uninstalled");
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
581
790
|
// Annotate the CommonJS export names for ESM import in node:
|
|
582
791
|
0 && (module.exports = {
|
|
583
792
|
ChatCompletionStream,
|
|
@@ -586,5 +795,7 @@ var client_default = LaylaSDK;
|
|
|
586
795
|
LaylaAbortError,
|
|
587
796
|
LaylaBridgeUnavailableError,
|
|
588
797
|
LaylaError,
|
|
589
|
-
LaylaSDK
|
|
798
|
+
LaylaSDK,
|
|
799
|
+
installLaylaMock,
|
|
800
|
+
makeMockCharacter
|
|
590
801
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -86,6 +86,10 @@ interface LaylaApiSendMessage {
|
|
|
86
86
|
/** Ask the host for the list of available character cards (a one-shot request). */
|
|
87
87
|
interface LaylaApiGetCharacters {
|
|
88
88
|
cmd: 'get_characters';
|
|
89
|
+
data: {
|
|
90
|
+
offset: number;
|
|
91
|
+
limit: number;
|
|
92
|
+
};
|
|
89
93
|
}
|
|
90
94
|
/** Ask the host for a character image identified by <characterId>. */
|
|
91
95
|
interface LaylaApiGetCharacterImage {
|
|
@@ -409,7 +413,7 @@ declare class Characters {
|
|
|
409
413
|
* Ask the native host for the available character cards. Resolves once with
|
|
410
414
|
* the host's `on_get_characters_response` payload, or rejects on error/abort.
|
|
411
415
|
*/
|
|
412
|
-
list(options?: RequestOptions): Promise<LaylaCharacter[]>;
|
|
416
|
+
list(offset?: number, range?: number, options?: RequestOptions): Promise<LaylaCharacter[]>;
|
|
413
417
|
/**
|
|
414
418
|
* Ask the native host for a character portrait. Resolves to a ready-to-use
|
|
415
419
|
* image src string, or null when the character has no image.
|
|
@@ -435,4 +439,53 @@ declare class LaylaSDK {
|
|
|
435
439
|
constructor(_options?: LaylaSDKOptions);
|
|
436
440
|
}
|
|
437
441
|
|
|
438
|
-
|
|
442
|
+
/**
|
|
443
|
+
* mock.ts
|
|
444
|
+
* -------
|
|
445
|
+
* A fake Layla native host for running the SDK OUTSIDE the app (Vite dev server,
|
|
446
|
+
* Storybook, a browser test, etc.). It installs a stand-in
|
|
447
|
+
* `window.ReactNativeWebView` and answers the web->RN commands by dispatching the
|
|
448
|
+
* same `MessageEvent`s the real host would, so the SDK's bridge, queue,
|
|
449
|
+
* streaming, and cancel paths all run for real against it.
|
|
450
|
+
*
|
|
451
|
+
* Usage (Vite — install in dev before you use the SDK):
|
|
452
|
+
*
|
|
453
|
+
* // main.ts
|
|
454
|
+
* if (import.meta.env.DEV) {
|
|
455
|
+
* const { installLaylaMock } = await import('layla-sdk/mock');
|
|
456
|
+
* installLaylaMock({ debug: true });
|
|
457
|
+
* }
|
|
458
|
+
*
|
|
459
|
+
* Install BEFORE the SDK posts its first message; otherwise the bridge sees no
|
|
460
|
+
* host and rejects with LaylaBridgeUnavailableError.
|
|
461
|
+
*/
|
|
462
|
+
|
|
463
|
+
type MockReply = string | string[] | Iterable<string> | AsyncIterable<string>;
|
|
464
|
+
interface LaylaMockOptions {
|
|
465
|
+
/**
|
|
466
|
+
* Produce the assistant reply for a `send_message`. Return a string (which the
|
|
467
|
+
* mock tokenises and streams), an array of pre-split tokens, or an
|
|
468
|
+
* (async) iterable of tokens for full control over timing/content. May be
|
|
469
|
+
* async. Defaults to a canned reply that echoes the last user message.
|
|
470
|
+
*/
|
|
471
|
+
respond?: (messages: LaylaChatMessage[]) => MockReply | Promise<MockReply>;
|
|
472
|
+
/** Cards returned by `get_characters`. Defaults to two sample cards. */
|
|
473
|
+
characters?: LaylaCharacter[];
|
|
474
|
+
/** Delay before the first event of a response (simulated latency). Default 150ms. */
|
|
475
|
+
latencyMs?: number;
|
|
476
|
+
/** Delay between streamed tokens. Default 40ms. */
|
|
477
|
+
tokenDelayMs?: number;
|
|
478
|
+
/** 0..1 probability that a request fails with on_error, for testing error paths. Default 0. */
|
|
479
|
+
errorRate?: number;
|
|
480
|
+
/** Log every web->RN command and RN->web event to the console. */
|
|
481
|
+
debug?: boolean;
|
|
482
|
+
}
|
|
483
|
+
interface LaylaMockHandle {
|
|
484
|
+
/** Remove the fake bridge and restore whatever was there before. */
|
|
485
|
+
uninstall(): void;
|
|
486
|
+
}
|
|
487
|
+
/** Build a valid Character Card V2 with sensible mock defaults. */
|
|
488
|
+
declare function makeMockCharacter(name: string, overrides?: Partial<TavernCardV2['data']>): LaylaCharacter;
|
|
489
|
+
declare function installLaylaMock(options?: LaylaMockOptions): LaylaMockHandle;
|
|
490
|
+
|
|
491
|
+
export { type ChatCompletion, type ChatCompletionChunk, type ChatCompletionCreateParamsBase, type ChatCompletionCreateParamsNonStreaming, type ChatCompletionCreateParamsStreaming, ChatCompletionStream, Images, LaylaSDK as Layla, LaylaAbortError, type LaylaApiCancel, type LaylaApiEvent, type LaylaApiEvent_onError, type LaylaApiEvent_onGenerateImageResponse, type LaylaApiEvent_onGetCharacterImageResponse, type LaylaApiEvent_onGetCharactersResponse, type LaylaApiEvent_onMsg, type LaylaApiEvent_onMsgEnd, type LaylaApiGenerateImage, type LaylaApiGetCharacterImage, type LaylaApiGetCharacters, type LaylaApiRequest, type LaylaApiSendMessage, LaylaBridgeUnavailableError, type LaylaCharacter, type LaylaChatMessage, type LaylaChatRole, LaylaError, LaylaSDK, type LaylaSDKOptions, type RequestOptions, type TavernCardV2, type TavernCharacterBook, LaylaSDK as default, installLaylaMock, makeMockCharacter };
|
package/dist/index.d.ts
CHANGED
|
@@ -86,6 +86,10 @@ interface LaylaApiSendMessage {
|
|
|
86
86
|
/** Ask the host for the list of available character cards (a one-shot request). */
|
|
87
87
|
interface LaylaApiGetCharacters {
|
|
88
88
|
cmd: 'get_characters';
|
|
89
|
+
data: {
|
|
90
|
+
offset: number;
|
|
91
|
+
limit: number;
|
|
92
|
+
};
|
|
89
93
|
}
|
|
90
94
|
/** Ask the host for a character image identified by <characterId>. */
|
|
91
95
|
interface LaylaApiGetCharacterImage {
|
|
@@ -409,7 +413,7 @@ declare class Characters {
|
|
|
409
413
|
* Ask the native host for the available character cards. Resolves once with
|
|
410
414
|
* the host's `on_get_characters_response` payload, or rejects on error/abort.
|
|
411
415
|
*/
|
|
412
|
-
list(options?: RequestOptions): Promise<LaylaCharacter[]>;
|
|
416
|
+
list(offset?: number, range?: number, options?: RequestOptions): Promise<LaylaCharacter[]>;
|
|
413
417
|
/**
|
|
414
418
|
* Ask the native host for a character portrait. Resolves to a ready-to-use
|
|
415
419
|
* image src string, or null when the character has no image.
|
|
@@ -435,4 +439,53 @@ declare class LaylaSDK {
|
|
|
435
439
|
constructor(_options?: LaylaSDKOptions);
|
|
436
440
|
}
|
|
437
441
|
|
|
438
|
-
|
|
442
|
+
/**
|
|
443
|
+
* mock.ts
|
|
444
|
+
* -------
|
|
445
|
+
* A fake Layla native host for running the SDK OUTSIDE the app (Vite dev server,
|
|
446
|
+
* Storybook, a browser test, etc.). It installs a stand-in
|
|
447
|
+
* `window.ReactNativeWebView` and answers the web->RN commands by dispatching the
|
|
448
|
+
* same `MessageEvent`s the real host would, so the SDK's bridge, queue,
|
|
449
|
+
* streaming, and cancel paths all run for real against it.
|
|
450
|
+
*
|
|
451
|
+
* Usage (Vite — install in dev before you use the SDK):
|
|
452
|
+
*
|
|
453
|
+
* // main.ts
|
|
454
|
+
* if (import.meta.env.DEV) {
|
|
455
|
+
* const { installLaylaMock } = await import('layla-sdk/mock');
|
|
456
|
+
* installLaylaMock({ debug: true });
|
|
457
|
+
* }
|
|
458
|
+
*
|
|
459
|
+
* Install BEFORE the SDK posts its first message; otherwise the bridge sees no
|
|
460
|
+
* host and rejects with LaylaBridgeUnavailableError.
|
|
461
|
+
*/
|
|
462
|
+
|
|
463
|
+
type MockReply = string | string[] | Iterable<string> | AsyncIterable<string>;
|
|
464
|
+
interface LaylaMockOptions {
|
|
465
|
+
/**
|
|
466
|
+
* Produce the assistant reply for a `send_message`. Return a string (which the
|
|
467
|
+
* mock tokenises and streams), an array of pre-split tokens, or an
|
|
468
|
+
* (async) iterable of tokens for full control over timing/content. May be
|
|
469
|
+
* async. Defaults to a canned reply that echoes the last user message.
|
|
470
|
+
*/
|
|
471
|
+
respond?: (messages: LaylaChatMessage[]) => MockReply | Promise<MockReply>;
|
|
472
|
+
/** Cards returned by `get_characters`. Defaults to two sample cards. */
|
|
473
|
+
characters?: LaylaCharacter[];
|
|
474
|
+
/** Delay before the first event of a response (simulated latency). Default 150ms. */
|
|
475
|
+
latencyMs?: number;
|
|
476
|
+
/** Delay between streamed tokens. Default 40ms. */
|
|
477
|
+
tokenDelayMs?: number;
|
|
478
|
+
/** 0..1 probability that a request fails with on_error, for testing error paths. Default 0. */
|
|
479
|
+
errorRate?: number;
|
|
480
|
+
/** Log every web->RN command and RN->web event to the console. */
|
|
481
|
+
debug?: boolean;
|
|
482
|
+
}
|
|
483
|
+
interface LaylaMockHandle {
|
|
484
|
+
/** Remove the fake bridge and restore whatever was there before. */
|
|
485
|
+
uninstall(): void;
|
|
486
|
+
}
|
|
487
|
+
/** Build a valid Character Card V2 with sensible mock defaults. */
|
|
488
|
+
declare function makeMockCharacter(name: string, overrides?: Partial<TavernCardV2['data']>): LaylaCharacter;
|
|
489
|
+
declare function installLaylaMock(options?: LaylaMockOptions): LaylaMockHandle;
|
|
490
|
+
|
|
491
|
+
export { type ChatCompletion, type ChatCompletionChunk, type ChatCompletionCreateParamsBase, type ChatCompletionCreateParamsNonStreaming, type ChatCompletionCreateParamsStreaming, ChatCompletionStream, Images, LaylaSDK as Layla, LaylaAbortError, type LaylaApiCancel, type LaylaApiEvent, type LaylaApiEvent_onError, type LaylaApiEvent_onGenerateImageResponse, type LaylaApiEvent_onGetCharacterImageResponse, type LaylaApiEvent_onGetCharactersResponse, type LaylaApiEvent_onMsg, type LaylaApiEvent_onMsgEnd, type LaylaApiGenerateImage, type LaylaApiGetCharacterImage, type LaylaApiGetCharacters, type LaylaApiRequest, type LaylaApiSendMessage, LaylaBridgeUnavailableError, type LaylaCharacter, type LaylaChatMessage, type LaylaChatRole, LaylaError, LaylaSDK, type LaylaSDKOptions, type RequestOptions, type TavernCardV2, type TavernCharacterBook, LaylaSDK as default, installLaylaMock, makeMockCharacter };
|
package/dist/index.js
CHANGED
|
@@ -503,13 +503,15 @@ function oneShot(command, responseEvent, extract, signal) {
|
|
|
503
503
|
|
|
504
504
|
// src/resources/characters.ts
|
|
505
505
|
var Characters = class {
|
|
506
|
-
|
|
507
|
-
* Ask the native host for the available character cards. Resolves once with
|
|
508
|
-
* the host's `on_get_characters_response` payload, or rejects on error/abort.
|
|
509
|
-
*/
|
|
510
|
-
list(options = {}) {
|
|
506
|
+
list(offset = 0, range = 10, options = {}) {
|
|
511
507
|
return oneShot(
|
|
512
|
-
{
|
|
508
|
+
{
|
|
509
|
+
cmd: "get_characters",
|
|
510
|
+
data: {
|
|
511
|
+
offset,
|
|
512
|
+
limit: range
|
|
513
|
+
}
|
|
514
|
+
},
|
|
513
515
|
"on_get_characters_response",
|
|
514
516
|
(event) => {
|
|
515
517
|
const data = event.data;
|
|
@@ -545,6 +547,211 @@ var LaylaSDK = class {
|
|
|
545
547
|
}
|
|
546
548
|
};
|
|
547
549
|
var client_default = LaylaSDK;
|
|
550
|
+
|
|
551
|
+
// src/mock.ts
|
|
552
|
+
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
553
|
+
function makeMockCharacter(name, overrides = {}) {
|
|
554
|
+
return {
|
|
555
|
+
id: `mock-${name.toLowerCase()}`,
|
|
556
|
+
data: {
|
|
557
|
+
spec: "chara_card_v2",
|
|
558
|
+
spec_version: "2.0",
|
|
559
|
+
data: {
|
|
560
|
+
name,
|
|
561
|
+
description: `${name} is a mock character for local testing.`,
|
|
562
|
+
personality: "friendly, helpful",
|
|
563
|
+
scenario: "",
|
|
564
|
+
first_mes: `Hi, I'm ${name}.`,
|
|
565
|
+
mes_example: "",
|
|
566
|
+
creator_notes: "Generated by layla mock.",
|
|
567
|
+
system_prompt: "",
|
|
568
|
+
post_history_instructions: "",
|
|
569
|
+
alternate_greetings: [],
|
|
570
|
+
tags: ["mock"],
|
|
571
|
+
creator: "layla-mock",
|
|
572
|
+
character_version: "1.0",
|
|
573
|
+
extensions: {},
|
|
574
|
+
...overrides
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function installLaylaMock(options = {}) {
|
|
580
|
+
if (typeof window === "undefined") {
|
|
581
|
+
throw new Error(
|
|
582
|
+
"installLaylaMock needs a browser-like environment (window). In Node, run your test under jsdom or happy-dom."
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const latencyMs = options.latencyMs ?? 150;
|
|
586
|
+
const tokenDelayMs = options.tokenDelayMs ?? 40;
|
|
587
|
+
const errorRate = options.errorRate ?? 0;
|
|
588
|
+
const characters = options.characters ?? [makeMockCharacter("Aria"), makeMockCharacter("Kai")];
|
|
589
|
+
let current = null;
|
|
590
|
+
const log = (...a) => {
|
|
591
|
+
if (options.debug) console.log("[layla-mock]", ...a);
|
|
592
|
+
};
|
|
593
|
+
const emit = (event) => {
|
|
594
|
+
log("RN->web", event);
|
|
595
|
+
window.dispatchEvent(new MessageEvent("message", { data: JSON.stringify(event) }));
|
|
596
|
+
};
|
|
597
|
+
const emitError = (message) => emit({ event: "on_error", data: { message } });
|
|
598
|
+
const shouldError = () => errorRate > 0 && Math.random() < errorRate;
|
|
599
|
+
const tokenize = (text) => text.match(/\S+\s*/g) ?? (text ? [text] : []);
|
|
600
|
+
const defaultReply = (messages) => {
|
|
601
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
602
|
+
const said = lastUser?.content ?? "(nothing)";
|
|
603
|
+
return `This is a mock Layla response. You said: "${said}". Tokens stream one at a time so you can exercise your streaming UI, cancellation, and the final-content promise.`;
|
|
604
|
+
};
|
|
605
|
+
async function* tokenSource(messages) {
|
|
606
|
+
const produced = options.respond ? await options.respond(messages) : defaultReply(messages);
|
|
607
|
+
if (typeof produced === "string") {
|
|
608
|
+
yield* tokenize(produced);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (Array.isArray(produced)) {
|
|
612
|
+
yield* produced;
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (produced && typeof produced === "object") {
|
|
616
|
+
if (Symbol.asyncIterator in produced) {
|
|
617
|
+
yield* produced;
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (Symbol.iterator in produced) {
|
|
621
|
+
yield* produced;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async function handleSend(messages) {
|
|
626
|
+
const gen = { cancelled: false };
|
|
627
|
+
current = gen;
|
|
628
|
+
try {
|
|
629
|
+
await delay(latencyMs);
|
|
630
|
+
if (gen.cancelled) return;
|
|
631
|
+
if (shouldError()) {
|
|
632
|
+
emitError("Simulated model error");
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
let snapshot = "";
|
|
636
|
+
for await (const delta of tokenSource(messages)) {
|
|
637
|
+
if (gen.cancelled) return;
|
|
638
|
+
snapshot += delta;
|
|
639
|
+
emit({ event: "on_message", data: { msg: snapshot, delta } });
|
|
640
|
+
await delay(tokenDelayMs);
|
|
641
|
+
if (gen.cancelled) return;
|
|
642
|
+
}
|
|
643
|
+
emit({ event: "on_message_end" });
|
|
644
|
+
} finally {
|
|
645
|
+
if (current === gen) current = null;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function handleCancel() {
|
|
649
|
+
if (current && !current.cancelled) {
|
|
650
|
+
current.cancelled = true;
|
|
651
|
+
queueMicrotask(() => emit({ event: "on_message_end" }));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function handleGetCharacters(data) {
|
|
655
|
+
await delay(latencyMs);
|
|
656
|
+
if (shouldError()) {
|
|
657
|
+
emitError("Simulated characters error");
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const { offset, limit } = data;
|
|
661
|
+
emit({
|
|
662
|
+
event: "on_get_characters_response",
|
|
663
|
+
data: characters.slice(offset, offset + limit)
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
async function handleGetCharacterImage(data) {
|
|
667
|
+
await delay(latencyMs);
|
|
668
|
+
if (shouldError()) {
|
|
669
|
+
emitError("Simulated character image error");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
emit({
|
|
673
|
+
event: "on_get_character_image_response",
|
|
674
|
+
data: {
|
|
675
|
+
character_id: data.character_id,
|
|
676
|
+
image_data_base64: "https://picsum.photos/id/237/200/300"
|
|
677
|
+
// we return a url for mock, since we usually use this as the "src" of an <img> tag, it should work fine as a mock
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async function handleGenerateImage(_) {
|
|
682
|
+
await delay(latencyMs);
|
|
683
|
+
if (shouldError()) {
|
|
684
|
+
emitError("Simulated image generation error");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const totalSteps = 5;
|
|
688
|
+
for (let step = 1; step <= totalSteps; step++) {
|
|
689
|
+
await delay(latencyMs);
|
|
690
|
+
emit({
|
|
691
|
+
event: "on_generate_image_progress",
|
|
692
|
+
data: {
|
|
693
|
+
status: `Generating image... (${step}/${totalSteps})`,
|
|
694
|
+
steps: step,
|
|
695
|
+
total_steps: totalSteps
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
emit({
|
|
700
|
+
event: "on_generate_image_response",
|
|
701
|
+
data: {
|
|
702
|
+
image_data_base64: "https://picsum.photos/200/300"
|
|
703
|
+
// Placeholder image URL for the mock
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
const fakeBridge = {
|
|
708
|
+
postMessage(raw) {
|
|
709
|
+
let msg;
|
|
710
|
+
try {
|
|
711
|
+
msg = JSON.parse(raw);
|
|
712
|
+
} catch {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
log("web->RN", msg);
|
|
716
|
+
switch (msg.cmd) {
|
|
717
|
+
case "send_message":
|
|
718
|
+
void handleSend(msg.data);
|
|
719
|
+
break;
|
|
720
|
+
case "cancel":
|
|
721
|
+
handleCancel();
|
|
722
|
+
break;
|
|
723
|
+
case "get_characters":
|
|
724
|
+
void handleGetCharacters(msg.data);
|
|
725
|
+
break;
|
|
726
|
+
case "get_character_image":
|
|
727
|
+
void handleGetCharacterImage(msg.data);
|
|
728
|
+
break;
|
|
729
|
+
case "generate_image":
|
|
730
|
+
void handleGenerateImage(msg.data);
|
|
731
|
+
break;
|
|
732
|
+
default:
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
const previous = window.ReactNativeWebView;
|
|
738
|
+
if (previous) {
|
|
739
|
+
log("warning: replacing an existing window.ReactNativeWebView");
|
|
740
|
+
}
|
|
741
|
+
window.ReactNativeWebView = fakeBridge;
|
|
742
|
+
log("installed");
|
|
743
|
+
let installed = true;
|
|
744
|
+
return {
|
|
745
|
+
uninstall() {
|
|
746
|
+
if (!installed) return;
|
|
747
|
+
installed = false;
|
|
748
|
+
if (current) current.cancelled = true;
|
|
749
|
+
if (previous) window.ReactNativeWebView = previous;
|
|
750
|
+
else delete window.ReactNativeWebView;
|
|
751
|
+
log("uninstalled");
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
548
755
|
export {
|
|
549
756
|
ChatCompletionStream,
|
|
550
757
|
Images,
|
|
@@ -553,5 +760,7 @@ export {
|
|
|
553
760
|
LaylaBridgeUnavailableError,
|
|
554
761
|
LaylaError,
|
|
555
762
|
LaylaSDK,
|
|
556
|
-
client_default as default
|
|
763
|
+
client_default as default,
|
|
764
|
+
installLaylaMock,
|
|
765
|
+
makeMockCharacter
|
|
557
766
|
};
|