@contentgrowth/llm-service 0.9.3 → 0.9.5
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.
|
@@ -149,7 +149,7 @@ function ChatHeader({
|
|
|
149
149
|
|
|
150
150
|
// src/ui/react/components/ChatInputArea.tsx
|
|
151
151
|
import { useState as useState3, useRef as useRef3, useImperativeHandle, forwardRef, useEffect as useEffect3, useCallback as useCallback3, useLayoutEffect } from "react";
|
|
152
|
-
import { MicrophoneIcon, StopIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
|
152
|
+
import { MicrophoneIcon, StopIcon, PaperAirplaneIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
|
153
153
|
|
|
154
154
|
// src/ui/react/hooks/useSpeechRecognition.ts
|
|
155
155
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
@@ -161,6 +161,12 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
|
|
|
161
161
|
const recognitionRef = useRef(null);
|
|
162
162
|
const isSimulatingRef = useRef(false);
|
|
163
163
|
const simulationTimeoutRef = useRef(null);
|
|
164
|
+
const onResultRef = useRef(onResult);
|
|
165
|
+
const onEndRef = useRef(onEnd);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
onResultRef.current = onResult;
|
|
168
|
+
onEndRef.current = onEnd;
|
|
169
|
+
}, [onResult, onEnd]);
|
|
164
170
|
useEffect(() => {
|
|
165
171
|
if (typeof window !== "undefined") {
|
|
166
172
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
@@ -179,7 +185,7 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
|
|
|
179
185
|
return;
|
|
180
186
|
}
|
|
181
187
|
setIsListening(false);
|
|
182
|
-
if (
|
|
188
|
+
if (onEndRef.current) onEndRef.current();
|
|
183
189
|
};
|
|
184
190
|
recognition.onresult = (event) => {
|
|
185
191
|
let interimTranscript = "";
|
|
@@ -188,10 +194,10 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
|
|
|
188
194
|
const result = event.results[i];
|
|
189
195
|
if (result.isFinal) {
|
|
190
196
|
finalTranscript += result[0].transcript;
|
|
191
|
-
if (
|
|
197
|
+
if (onResultRef.current) onResultRef.current(finalTranscript, true);
|
|
192
198
|
} else {
|
|
193
199
|
interimTranscript += result[0].transcript;
|
|
194
|
-
if (
|
|
200
|
+
if (onResultRef.current) onResultRef.current(interimTranscript, false);
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
setTranscript((prev) => prev + finalTranscript);
|
|
@@ -206,10 +212,10 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
|
|
|
206
212
|
simulationTimeoutRef.current = setTimeout(() => {
|
|
207
213
|
const mockText = "This is a simulated voice input for testing.";
|
|
208
214
|
setTranscript((prev) => prev + (prev ? " " : "") + mockText);
|
|
209
|
-
if (
|
|
215
|
+
if (onResultRef.current) onResultRef.current(mockText, true);
|
|
210
216
|
isSimulatingRef.current = false;
|
|
211
217
|
setIsListening(false);
|
|
212
|
-
if (
|
|
218
|
+
if (onEndRef.current) onEndRef.current();
|
|
213
219
|
simulationTimeoutRef.current = null;
|
|
214
220
|
}, 3e3);
|
|
215
221
|
return;
|
|
@@ -226,9 +232,11 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
|
|
|
226
232
|
clearTimeout(simulationTimeoutRef.current);
|
|
227
233
|
simulationTimeoutRef.current = null;
|
|
228
234
|
}
|
|
229
|
-
recognitionRef.current
|
|
235
|
+
if (recognitionRef.current) {
|
|
236
|
+
recognitionRef.current.stop();
|
|
237
|
+
}
|
|
230
238
|
};
|
|
231
|
-
}, [
|
|
239
|
+
}, [language]);
|
|
232
240
|
const start = useCallback(() => {
|
|
233
241
|
if (recognitionRef.current && !isListening) {
|
|
234
242
|
try {
|
|
@@ -248,10 +256,10 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
|
|
|
248
256
|
}
|
|
249
257
|
const mockText = "This is a simulated voice input for testing.";
|
|
250
258
|
setTranscript((prev) => prev + (prev ? " " : "") + mockText);
|
|
251
|
-
if (
|
|
259
|
+
if (onResultRef.current) onResultRef.current(mockText, true);
|
|
252
260
|
isSimulatingRef.current = false;
|
|
253
261
|
setIsListening(false);
|
|
254
|
-
if (
|
|
262
|
+
if (onEndRef.current) onEndRef.current();
|
|
255
263
|
return;
|
|
256
264
|
}
|
|
257
265
|
if (recognitionRef.current && isListening) {
|
|
@@ -387,6 +395,42 @@ var ChatInputArea = forwardRef(({
|
|
|
387
395
|
const [isTranscribing, setIsTranscribing] = useState3(false);
|
|
388
396
|
const [voiceError, setVoiceError] = useState3(null);
|
|
389
397
|
const [isFocused, setIsFocused] = useState3(false);
|
|
398
|
+
const [showDebug, setShowDebug] = useState3(false);
|
|
399
|
+
const [logs, setLogs] = useState3([]);
|
|
400
|
+
const tapCountRef = useRef3({ count: 0, lastTap: 0 });
|
|
401
|
+
useEffect3(() => {
|
|
402
|
+
const originalLog = console.log;
|
|
403
|
+
const originalWarn = console.warn;
|
|
404
|
+
const originalError = console.error;
|
|
405
|
+
const addLog = (type, args) => {
|
|
406
|
+
try {
|
|
407
|
+
const msg = args.map((arg) => {
|
|
408
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
|
409
|
+
if (typeof arg === "object") return JSON.stringify(arg);
|
|
410
|
+
return String(arg);
|
|
411
|
+
}).join(" ");
|
|
412
|
+
setLogs((prev) => [`[${type}] ${msg}`, ...prev].slice(0, 50));
|
|
413
|
+
} catch (e) {
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
console.log = (...args) => {
|
|
417
|
+
originalLog(...args);
|
|
418
|
+
addLog("LOG", args);
|
|
419
|
+
};
|
|
420
|
+
console.warn = (...args) => {
|
|
421
|
+
originalWarn(...args);
|
|
422
|
+
addLog("WRN", args);
|
|
423
|
+
};
|
|
424
|
+
console.error = (...args) => {
|
|
425
|
+
originalError(...args);
|
|
426
|
+
addLog("ERR", args);
|
|
427
|
+
};
|
|
428
|
+
return () => {
|
|
429
|
+
console.log = originalLog;
|
|
430
|
+
console.warn = originalWarn;
|
|
431
|
+
console.error = originalError;
|
|
432
|
+
};
|
|
433
|
+
}, []);
|
|
390
434
|
const textareaRef = useRef3(null);
|
|
391
435
|
const measurementRef = useRef3(null);
|
|
392
436
|
const pendingSelectionRef = useRef3(null);
|
|
@@ -456,6 +500,12 @@ var ChatInputArea = forwardRef(({
|
|
|
456
500
|
(_b2 = (_a2 = voiceConfigRef.current) == null ? void 0 : _a2.onVoiceEnd) == null ? void 0 : _b2.call(_a2);
|
|
457
501
|
}, []);
|
|
458
502
|
const nativeSpeech = useSpeechRecognition(handleVoiceResult, handleVoiceEnd, voiceConfig == null ? void 0 : voiceConfig.language);
|
|
503
|
+
useEffect3(() => {
|
|
504
|
+
if (nativeSpeech.error) {
|
|
505
|
+
setVoiceError(nativeSpeech.error);
|
|
506
|
+
console.error("[ChatInputArea] Native Speech Error:", nativeSpeech.error);
|
|
507
|
+
}
|
|
508
|
+
}, [nativeSpeech.error]);
|
|
459
509
|
const customRecorder = useAudioRecorder(async (blob) => {
|
|
460
510
|
var _a2, _b2, _c2;
|
|
461
511
|
setVoiceTrigger(null);
|
|
@@ -569,20 +619,40 @@ var ChatInputArea = forwardRef(({
|
|
|
569
619
|
if (!showInputForm) {
|
|
570
620
|
return null;
|
|
571
621
|
}
|
|
572
|
-
return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col w-full", children: [
|
|
622
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col w-full relative", children: [
|
|
623
|
+
showDebug && /* @__PURE__ */ jsxs3("div", { className: "absolute bottom-full left-0 right-0 mb-2 p-2 bg-black/80 text-green-400 text-xs font-mono h-48 overflow-y-auto rounded z-50 pointer-events-auto", children: [
|
|
624
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex justify-between items-center bg-gray-800 p-1 mb-1", children: [
|
|
625
|
+
/* @__PURE__ */ jsx5("span", { children: "Debug Logs" }),
|
|
626
|
+
/* @__PURE__ */ jsx5("button", { onClick: () => setShowDebug(false), className: "text-white hover:text-red-400", children: /* @__PURE__ */ jsx5(XMarkIcon, { className: "w-4 h-4" }) })
|
|
627
|
+
] }),
|
|
628
|
+
logs.map((log, i) => /* @__PURE__ */ jsx5("div", { className: "mb-0.5 border-b border-gray-700/50 pb-0.5 break-all", children: log }, i)),
|
|
629
|
+
logs.length === 0 && /* @__PURE__ */ jsx5("div", { children: "No logs yet..." })
|
|
630
|
+
] }),
|
|
573
631
|
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
|
|
574
632
|
voiceConfig && /* @__PURE__ */ jsx5(
|
|
575
633
|
"button",
|
|
576
634
|
{
|
|
577
635
|
type: "button",
|
|
578
636
|
onClick: () => {
|
|
637
|
+
const now = Date.now();
|
|
638
|
+
if (now - tapCountRef.current.lastTap < 500) {
|
|
639
|
+
tapCountRef.current.count++;
|
|
640
|
+
} else {
|
|
641
|
+
tapCountRef.current.count = 1;
|
|
642
|
+
}
|
|
643
|
+
tapCountRef.current.lastTap = now;
|
|
644
|
+
if (tapCountRef.current.count >= 5) {
|
|
645
|
+
setShowDebug((prev) => !prev);
|
|
646
|
+
tapCountRef.current.count = 0;
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
579
649
|
if (voiceTrigger) {
|
|
580
650
|
stopRecording();
|
|
581
651
|
} else if (!isTranscribing) {
|
|
582
652
|
startRecording("click");
|
|
583
653
|
}
|
|
584
654
|
},
|
|
585
|
-
className: `mb-1 p-2 rounded-full transition-all duration-300 flex-shrink-0 border ${
|
|
655
|
+
className: `mb-1 p-2 rounded-full transition-all duration-300 flex-shrink-0 border ${isTranscribing ? "text-white border-indigo-500 bg-indigo-600 scale-110 shadow-lg" : voiceTrigger ? "text-white border-orange-400 bg-orange-500 scale-110 shadow-lg" : "text-gray-500 border-gray-300 bg-white hover:text-gray-700 hover:bg-gray-100"} ${voiceTrigger ? "animate-pulse" : ""} ${isTranscribing ? "cursor-wait" : ""}`,
|
|
586
656
|
disabled: isTranscribing,
|
|
587
657
|
title: isTranscribing ? "Transcribing..." : voiceTrigger ? "Stop Recording" : "Start Voice Input",
|
|
588
658
|
children: isTranscribing ? /* @__PURE__ */ jsx5("div", { className: "animate-spin w-5 h-5 flex items-center justify-center", children: /* @__PURE__ */ jsxs3("svg", { className: "w-5 h-5 text-white", viewBox: "0 0 24 24", children: [
|
|
@@ -685,17 +755,17 @@ var ChatInputArea = forwardRef(({
|
|
|
685
755
|
)
|
|
686
756
|
] }),
|
|
687
757
|
inputHint && /* @__PURE__ */ jsx5("div", { className: "text-sm text-red-500 bg-red-50 py-1 px-4 rounded-lg mt-1", children: inputHint }),
|
|
688
|
-
/* @__PURE__ */ jsx5("div", { className: "ml-[46px] mb-2 mt-0.5 min-h-[0.75rem]", style: { marginLeft: "48px" }, children: /* @__PURE__ */ jsx5("p", { className: `text-[10px] leading-tight transition-all duration-200 ${voiceError ? "text-red-500" :
|
|
758
|
+
/* @__PURE__ */ jsx5("div", { className: "ml-[46px] mb-2 mt-0.5 min-h-[0.75rem]", style: { marginLeft: "48px" }, children: /* @__PURE__ */ jsx5("p", { className: `text-[10px] leading-tight transition-all duration-200 ${voiceError ? "text-red-500" : isTranscribing ? "text-indigo-600 font-bold" : voiceTrigger ? "text-orange-600 font-medium" : "text-gray-400"}`, children: voiceError ? /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-1 font-semibold italic", children: [
|
|
689
759
|
"Error: ",
|
|
690
760
|
voiceError
|
|
691
|
-
] }) : isTranscribing ? "Transcribing, please wait..." : voiceTrigger ? "Listening... tap mic icon again to stop" : hintText || (voiceConfig ? "Type in text or tap mic icon to talk" : "Type your message...") }) })
|
|
761
|
+
] }) : isTranscribing ? "Transcribing... please wait" : voiceTrigger ? "Transcribing, please wait..." : voiceTrigger ? "Listening... tap mic icon again to stop" : hintText || (voiceConfig ? "Type in text or tap mic icon to talk" : "Type your message...") }) })
|
|
692
762
|
] });
|
|
693
763
|
});
|
|
694
764
|
ChatInputArea.displayName = "ChatInputArea";
|
|
695
765
|
|
|
696
766
|
// src/ui/react/components/TapToTalk.tsx
|
|
697
|
-
import { useState as useState4, useCallback as useCallback4 } from "react";
|
|
698
|
-
import { MicrophoneIcon as MicrophoneIcon2 } from "@heroicons/react/24/outline";
|
|
767
|
+
import React3, { useState as useState4, useCallback as useCallback4, useRef as useRef4 } from "react";
|
|
768
|
+
import { MicrophoneIcon as MicrophoneIcon2, XMarkIcon as XMarkIcon2 } from "@heroicons/react/24/outline";
|
|
699
769
|
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
700
770
|
var TapToTalk = ({
|
|
701
771
|
onResult,
|
|
@@ -712,6 +782,42 @@ var TapToTalk = ({
|
|
|
712
782
|
const [isTranscribing, setIsTranscribing] = useState4(false);
|
|
713
783
|
const [voiceTrigger, setVoiceTrigger] = useState4(null);
|
|
714
784
|
const [errorMsg, setErrorMsg] = useState4(null);
|
|
785
|
+
const [showDebug, setShowDebug] = useState4(false);
|
|
786
|
+
const [logs, setLogs] = useState4([]);
|
|
787
|
+
const tapCountRef = useRef4({ count: 0, lastTap: 0 });
|
|
788
|
+
React3.useEffect(() => {
|
|
789
|
+
const originalLog = console.log;
|
|
790
|
+
const originalWarn = console.warn;
|
|
791
|
+
const originalError = console.error;
|
|
792
|
+
const addLog = (type, args) => {
|
|
793
|
+
try {
|
|
794
|
+
const msg = args.map((arg) => {
|
|
795
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
|
796
|
+
if (typeof arg === "object") return JSON.stringify(arg);
|
|
797
|
+
return String(arg);
|
|
798
|
+
}).join(" ");
|
|
799
|
+
setLogs((prev) => [`[${type}] ${msg}`, ...prev].slice(0, 50));
|
|
800
|
+
} catch (e) {
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
console.log = (...args) => {
|
|
804
|
+
originalLog(...args);
|
|
805
|
+
addLog("LOG", args);
|
|
806
|
+
};
|
|
807
|
+
console.warn = (...args) => {
|
|
808
|
+
originalWarn(...args);
|
|
809
|
+
addLog("WRN", args);
|
|
810
|
+
};
|
|
811
|
+
console.error = (...args) => {
|
|
812
|
+
originalError(...args);
|
|
813
|
+
addLog("ERR", args);
|
|
814
|
+
};
|
|
815
|
+
return () => {
|
|
816
|
+
console.log = originalLog;
|
|
817
|
+
console.warn = originalWarn;
|
|
818
|
+
console.error = originalError;
|
|
819
|
+
};
|
|
820
|
+
}, []);
|
|
715
821
|
const handleVoiceResult = useCallback4((text, isFinal) => {
|
|
716
822
|
if (isFinal) {
|
|
717
823
|
onResult(text);
|
|
@@ -723,6 +829,12 @@ var TapToTalk = ({
|
|
|
723
829
|
setVoiceTrigger(null);
|
|
724
830
|
}, []);
|
|
725
831
|
const nativeSpeech = useSpeechRecognition(handleVoiceResult, handleVoiceEnd, voiceConfig == null ? void 0 : voiceConfig.language);
|
|
832
|
+
React3.useEffect(() => {
|
|
833
|
+
if (nativeSpeech.error) {
|
|
834
|
+
setErrorMsg(nativeSpeech.error);
|
|
835
|
+
console.error("[TapToTalk] Native Speech Error:", nativeSpeech.error);
|
|
836
|
+
}
|
|
837
|
+
}, [nativeSpeech.error]);
|
|
726
838
|
const customRecorder = useAudioRecorder(async (blob) => {
|
|
727
839
|
setVoiceTrigger(null);
|
|
728
840
|
setIsTranscribing(true);
|
|
@@ -751,6 +863,18 @@ var TapToTalk = ({
|
|
|
751
863
|
const isListening = !!voiceTrigger || nativeSpeech.isListening || customRecorder.isRecording;
|
|
752
864
|
const isActive = isListening || isTranscribing;
|
|
753
865
|
const toggleVoice = async () => {
|
|
866
|
+
const now = Date.now();
|
|
867
|
+
if (now - tapCountRef.current.lastTap < 500) {
|
|
868
|
+
tapCountRef.current.count++;
|
|
869
|
+
} else {
|
|
870
|
+
tapCountRef.current.count = 1;
|
|
871
|
+
}
|
|
872
|
+
tapCountRef.current.lastTap = now;
|
|
873
|
+
if (tapCountRef.current.count >= 5) {
|
|
874
|
+
setShowDebug((prev) => !prev);
|
|
875
|
+
tapCountRef.current.count = 0;
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
754
878
|
if (isActive) {
|
|
755
879
|
if (isTranscribing && !isListening) return;
|
|
756
880
|
if ((voiceConfig == null ? void 0 : voiceConfig.mode) === "native") {
|
|
@@ -788,30 +912,43 @@ var TapToTalk = ({
|
|
|
788
912
|
label = "Listening ... Tap to stop";
|
|
789
913
|
Icon = /* @__PURE__ */ jsx6(MicrophoneIcon2, { className: "h-5 w-5 animate-pulse" });
|
|
790
914
|
} else if (isTranscribing) {
|
|
791
|
-
bgColor = "bg-
|
|
915
|
+
bgColor = "bg-indigo-600";
|
|
792
916
|
label = "Transcribing ...";
|
|
793
917
|
Icon = /* @__PURE__ */ jsxs4("svg", { className: "animate-spin h-5 w-5 text-white", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [
|
|
794
918
|
/* @__PURE__ */ jsx6("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
|
|
795
919
|
/* @__PURE__ */ jsx6("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })
|
|
796
920
|
] });
|
|
797
921
|
}
|
|
798
|
-
return /* @__PURE__ */ jsxs4(
|
|
799
|
-
"
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
922
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col w-full relative", children: [
|
|
923
|
+
showDebug && /* @__PURE__ */ jsxs4("div", { className: "absolute bottom-full left-0 right-0 mb-2 p-2 bg-black/80 text-green-400 text-xs font-mono h-48 overflow-y-auto rounded z-50 pointer-events-auto", children: [
|
|
924
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex justify-between items-center bg-gray-800 p-1 mb-1", children: [
|
|
925
|
+
/* @__PURE__ */ jsx6("span", { children: "Debug Logs" }),
|
|
926
|
+
/* @__PURE__ */ jsx6("button", { onClick: (e) => {
|
|
927
|
+
e.stopPropagation();
|
|
928
|
+
setShowDebug(false);
|
|
929
|
+
}, className: "text-white hover:text-red-400", children: /* @__PURE__ */ jsx6(XMarkIcon2, { className: "w-4 h-4" }) })
|
|
930
|
+
] }),
|
|
931
|
+
logs.map((log, i) => /* @__PURE__ */ jsx6("div", { className: "mb-0.5 border-b border-gray-700/50 pb-0.5 break-all", children: log }, i)),
|
|
932
|
+
logs.length === 0 && /* @__PURE__ */ jsx6("div", { children: "No logs yet..." })
|
|
933
|
+
] }),
|
|
934
|
+
/* @__PURE__ */ jsxs4(
|
|
935
|
+
"button",
|
|
936
|
+
{
|
|
937
|
+
onClick: toggleVoice,
|
|
938
|
+
disabled: disabled || isTranscribing && !isListening,
|
|
939
|
+
className: `flex items-center justify-center gap-3 px-6 py-3 rounded-xl transition-all duration-300 w-full font-medium shadow-md active:scale-[0.98]
|
|
804
940
|
${bgColor} text-white
|
|
805
941
|
${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
|
|
806
942
|
${className}`,
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
943
|
+
title: label,
|
|
944
|
+
children: [
|
|
945
|
+
/* @__PURE__ */ jsx6("div", { className: "flex items-center justify-center shrink-0", children: Icon }),
|
|
946
|
+
/* @__PURE__ */ jsx6("span", { className: "truncate", children: label }),
|
|
947
|
+
errorMsg && /* @__PURE__ */ jsx6("span", { className: "text-[10px] bg-white/20 px-1.5 py-0.5 rounded text-red-100 animate-in fade-in slide-in-from-right-1", children: errorMsg })
|
|
948
|
+
]
|
|
949
|
+
}
|
|
950
|
+
)
|
|
951
|
+
] });
|
|
815
952
|
};
|
|
816
953
|
|
|
817
954
|
// src/ui/react/components/ChatMessageList.tsx
|