@usecrow/ui 0.1.57 → 0.1.59
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 +398 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -36
- package/dist/index.d.ts +16 -36
- package/dist/index.js +398 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -937,18 +937,21 @@ function useConversations({ productId, apiUrl = "" }) {
|
|
|
937
937
|
const [isLoadingHistory, setIsLoadingHistory] = React3.useState(false);
|
|
938
938
|
const loadConversations = React3.useCallback(async () => {
|
|
939
939
|
const token = window.__crow_identity_token;
|
|
940
|
-
if (!token) return;
|
|
940
|
+
if (!token) return [];
|
|
941
941
|
try {
|
|
942
942
|
const res = await fetch(
|
|
943
943
|
`${apiUrl}/api/chat/conversations?product_id=${productId}&identity_token=${encodeURIComponent(token)}`
|
|
944
944
|
);
|
|
945
945
|
if (res.ok) {
|
|
946
946
|
const data = await res.json();
|
|
947
|
-
|
|
947
|
+
const convs = data.conversations || [];
|
|
948
|
+
setConversations(convs);
|
|
949
|
+
return convs;
|
|
948
950
|
}
|
|
949
951
|
} catch (error) {
|
|
950
952
|
console.error("[Crow] Failed to load conversations:", error);
|
|
951
953
|
}
|
|
954
|
+
return [];
|
|
952
955
|
}, [apiUrl, productId]);
|
|
953
956
|
const loadConversationHistory = React3.useCallback(
|
|
954
957
|
async (conversationId) => {
|
|
@@ -1769,6 +1772,186 @@ function usePreviewCopilotStyles(previewStyles) {
|
|
|
1769
1772
|
styles: mergeCopilotStyles(void 0, previewStyles)
|
|
1770
1773
|
};
|
|
1771
1774
|
}
|
|
1775
|
+
function useTTSOutput({
|
|
1776
|
+
backendUrl,
|
|
1777
|
+
voiceId = "YTpq7expH9539ERJ"
|
|
1778
|
+
}) {
|
|
1779
|
+
const [isSpeaking, setIsSpeaking] = React3.useState(false);
|
|
1780
|
+
const [error, setError] = React3.useState(null);
|
|
1781
|
+
const wsRef = React3.useRef(null);
|
|
1782
|
+
const audioContextRef = React3.useRef(null);
|
|
1783
|
+
const nextTimeRef = React3.useRef(0);
|
|
1784
|
+
const streamCompleteRef = React3.useRef(false);
|
|
1785
|
+
const completionCheckIntervalRef = React3.useRef(null);
|
|
1786
|
+
const cleanupAudioContext = React3.useCallback(() => {
|
|
1787
|
+
setIsSpeaking(false);
|
|
1788
|
+
if (audioContextRef.current && audioContextRef.current.state !== "closed") {
|
|
1789
|
+
audioContextRef.current.close();
|
|
1790
|
+
audioContextRef.current = null;
|
|
1791
|
+
}
|
|
1792
|
+
if (completionCheckIntervalRef.current) {
|
|
1793
|
+
clearInterval(completionCheckIntervalRef.current);
|
|
1794
|
+
completionCheckIntervalRef.current = null;
|
|
1795
|
+
}
|
|
1796
|
+
}, []);
|
|
1797
|
+
const closeWebSocket = React3.useCallback(() => {
|
|
1798
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
1799
|
+
try {
|
|
1800
|
+
wsRef.current.send(JSON.stringify({ type: "stop" }));
|
|
1801
|
+
wsRef.current.close();
|
|
1802
|
+
} catch (e) {
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
wsRef.current = null;
|
|
1806
|
+
}, []);
|
|
1807
|
+
const cleanupTTS = React3.useCallback(() => {
|
|
1808
|
+
setIsSpeaking(false);
|
|
1809
|
+
setError(null);
|
|
1810
|
+
closeWebSocket();
|
|
1811
|
+
cleanupAudioContext();
|
|
1812
|
+
}, [closeWebSocket, cleanupAudioContext]);
|
|
1813
|
+
const waitForAudioComplete = React3.useCallback(() => {
|
|
1814
|
+
if (completionCheckIntervalRef.current) {
|
|
1815
|
+
clearInterval(completionCheckIntervalRef.current);
|
|
1816
|
+
}
|
|
1817
|
+
completionCheckIntervalRef.current = setInterval(() => {
|
|
1818
|
+
if (!audioContextRef.current) {
|
|
1819
|
+
if (completionCheckIntervalRef.current) {
|
|
1820
|
+
clearInterval(completionCheckIntervalRef.current);
|
|
1821
|
+
completionCheckIntervalRef.current = null;
|
|
1822
|
+
}
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const now = audioContextRef.current.currentTime;
|
|
1826
|
+
if (now >= nextTimeRef.current) {
|
|
1827
|
+
if (completionCheckIntervalRef.current) {
|
|
1828
|
+
clearInterval(completionCheckIntervalRef.current);
|
|
1829
|
+
completionCheckIntervalRef.current = null;
|
|
1830
|
+
}
|
|
1831
|
+
cleanupAudioContext();
|
|
1832
|
+
}
|
|
1833
|
+
}, 100);
|
|
1834
|
+
}, [cleanupAudioContext]);
|
|
1835
|
+
const playAudioChunk = React3.useCallback((base64Audio) => {
|
|
1836
|
+
if (!audioContextRef.current || audioContextRef.current.state === "closed") {
|
|
1837
|
+
console.error("TTS: AudioContext not available");
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
try {
|
|
1841
|
+
const binary = atob(base64Audio);
|
|
1842
|
+
const bytes = new Uint8Array(binary.length);
|
|
1843
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1844
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1845
|
+
}
|
|
1846
|
+
const pcm16 = new Int16Array(bytes.buffer);
|
|
1847
|
+
const float32 = new Float32Array(pcm16.length);
|
|
1848
|
+
for (let i = 0; i < pcm16.length; i++) {
|
|
1849
|
+
float32[i] = pcm16[i] / 32768;
|
|
1850
|
+
}
|
|
1851
|
+
const buffer = audioContextRef.current.createBuffer(1, float32.length, 48e3);
|
|
1852
|
+
buffer.getChannelData(0).set(float32);
|
|
1853
|
+
const source = audioContextRef.current.createBufferSource();
|
|
1854
|
+
source.buffer = buffer;
|
|
1855
|
+
source.connect(audioContextRef.current.destination);
|
|
1856
|
+
const now = audioContextRef.current.currentTime;
|
|
1857
|
+
if (nextTimeRef.current < now) {
|
|
1858
|
+
nextTimeRef.current = now;
|
|
1859
|
+
}
|
|
1860
|
+
source.start(nextTimeRef.current);
|
|
1861
|
+
nextTimeRef.current += buffer.duration;
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
console.error("TTS: Error playing audio chunk:", err);
|
|
1864
|
+
setError(err instanceof Error ? err.message : "Failed to play audio chunk");
|
|
1865
|
+
}
|
|
1866
|
+
}, []);
|
|
1867
|
+
const speak = React3.useCallback(
|
|
1868
|
+
(text) => {
|
|
1869
|
+
console.log("[TTS Hook] speak called with:", text.substring(0, 50), "backendUrl:", backendUrl);
|
|
1870
|
+
if (!text.trim()) {
|
|
1871
|
+
console.log("[TTS Hook] No text to speak");
|
|
1872
|
+
setError("No text to speak");
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
if (isSpeaking || wsRef.current) {
|
|
1876
|
+
console.log("[TTS Hook] Already playing");
|
|
1877
|
+
setError("Already playing, stop first");
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
setError(null);
|
|
1881
|
+
nextTimeRef.current = 0;
|
|
1882
|
+
streamCompleteRef.current = false;
|
|
1883
|
+
try {
|
|
1884
|
+
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)({
|
|
1885
|
+
sampleRate: 48e3
|
|
1886
|
+
});
|
|
1887
|
+
const url = backendUrl.startsWith("http") ? backendUrl.replace(/^http/, "ws") : backendUrl;
|
|
1888
|
+
const wsUrl = `${url}/api/tts/stream`;
|
|
1889
|
+
console.log("[TTS Hook] Connecting to:", wsUrl);
|
|
1890
|
+
const ws = new WebSocket(wsUrl);
|
|
1891
|
+
wsRef.current = ws;
|
|
1892
|
+
ws.onopen = () => {
|
|
1893
|
+
ws.send(
|
|
1894
|
+
JSON.stringify({
|
|
1895
|
+
type: "setup",
|
|
1896
|
+
voice_id: voiceId,
|
|
1897
|
+
output_format: "pcm"
|
|
1898
|
+
})
|
|
1899
|
+
);
|
|
1900
|
+
};
|
|
1901
|
+
ws.onmessage = (event) => {
|
|
1902
|
+
const msg = JSON.parse(event.data);
|
|
1903
|
+
if (msg.type === "ready") {
|
|
1904
|
+
ws.send(JSON.stringify({ type: "text", text }));
|
|
1905
|
+
ws.send(JSON.stringify({ type: "end_of_stream" }));
|
|
1906
|
+
} else if (msg.type === "audio") {
|
|
1907
|
+
playAudioChunk(msg.audio);
|
|
1908
|
+
} else if (msg.type === "done") {
|
|
1909
|
+
streamCompleteRef.current = true;
|
|
1910
|
+
closeWebSocket();
|
|
1911
|
+
waitForAudioComplete();
|
|
1912
|
+
} else if (msg.type === "error") {
|
|
1913
|
+
setError(msg.message || "TTS error");
|
|
1914
|
+
cleanupTTS();
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
ws.onerror = () => {
|
|
1918
|
+
setError("WebSocket error");
|
|
1919
|
+
cleanupTTS();
|
|
1920
|
+
};
|
|
1921
|
+
ws.onclose = () => {
|
|
1922
|
+
wsRef.current = null;
|
|
1923
|
+
};
|
|
1924
|
+
setIsSpeaking(true);
|
|
1925
|
+
} catch (err) {
|
|
1926
|
+
setError(err instanceof Error ? err.message : "Failed to start TTS");
|
|
1927
|
+
cleanupTTS();
|
|
1928
|
+
}
|
|
1929
|
+
},
|
|
1930
|
+
[
|
|
1931
|
+
isSpeaking,
|
|
1932
|
+
backendUrl,
|
|
1933
|
+
voiceId,
|
|
1934
|
+
playAudioChunk,
|
|
1935
|
+
closeWebSocket,
|
|
1936
|
+
waitForAudioComplete,
|
|
1937
|
+
cleanupTTS
|
|
1938
|
+
]
|
|
1939
|
+
);
|
|
1940
|
+
const stop = React3.useCallback(() => {
|
|
1941
|
+
cleanupTTS();
|
|
1942
|
+
}, [cleanupTTS]);
|
|
1943
|
+
React3.useEffect(() => {
|
|
1944
|
+
return () => {
|
|
1945
|
+
cleanupTTS();
|
|
1946
|
+
};
|
|
1947
|
+
}, [cleanupTTS]);
|
|
1948
|
+
return {
|
|
1949
|
+
speak,
|
|
1950
|
+
stop,
|
|
1951
|
+
isSpeaking,
|
|
1952
|
+
error
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1772
1955
|
var WidgetStyleContext = React3.createContext(null);
|
|
1773
1956
|
function WidgetStyleProvider({
|
|
1774
1957
|
children,
|
|
@@ -2756,80 +2939,176 @@ var ModelSelector = ({
|
|
|
2756
2939
|
] }, provider)) })
|
|
2757
2940
|
] });
|
|
2758
2941
|
};
|
|
2759
|
-
var
|
|
2760
|
-
if (typeof window === "undefined") return
|
|
2761
|
-
return
|
|
2942
|
+
var isMediaRecorderSupported = () => {
|
|
2943
|
+
if (typeof window === "undefined") return false;
|
|
2944
|
+
return !!(navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === "function" && (window.AudioContext || window.webkitAudioContext));
|
|
2762
2945
|
};
|
|
2763
|
-
function useVoiceInput(options
|
|
2764
|
-
const {
|
|
2765
|
-
const [supported] = React3.useState(() =>
|
|
2946
|
+
function useVoiceInput(options) {
|
|
2947
|
+
const { backendUrl, silenceTimeoutMs } = options;
|
|
2948
|
+
const [supported] = React3.useState(() => isMediaRecorderSupported());
|
|
2766
2949
|
const [isRecording, setIsRecording] = React3.useState(false);
|
|
2767
2950
|
const [transcript, setTranscript] = React3.useState("");
|
|
2768
|
-
const
|
|
2951
|
+
const [error, setError] = React3.useState(null);
|
|
2952
|
+
const wsRef = React3.useRef(null);
|
|
2953
|
+
const streamRef = React3.useRef(null);
|
|
2954
|
+
const audioContextRef = React3.useRef(null);
|
|
2955
|
+
const processorRef = React3.useRef(null);
|
|
2769
2956
|
const silenceTimerRef = React3.useRef(null);
|
|
2770
|
-
const
|
|
2957
|
+
const transcriptRef = React3.useRef("");
|
|
2958
|
+
const interimRef = React3.useRef("");
|
|
2959
|
+
const isRecordingRef = React3.useRef(false);
|
|
2771
2960
|
const clearSilenceTimer = React3.useCallback(() => {
|
|
2772
2961
|
if (silenceTimerRef.current) {
|
|
2773
2962
|
clearTimeout(silenceTimerRef.current);
|
|
2774
2963
|
silenceTimerRef.current = null;
|
|
2775
2964
|
}
|
|
2776
2965
|
}, []);
|
|
2777
|
-
const
|
|
2966
|
+
const cleanup = React3.useCallback(() => {
|
|
2778
2967
|
clearSilenceTimer();
|
|
2779
|
-
|
|
2780
|
-
|
|
2968
|
+
isRecordingRef.current = false;
|
|
2969
|
+
if (interimRef.current) {
|
|
2970
|
+
transcriptRef.current += interimRef.current + " ";
|
|
2971
|
+
setTranscript(transcriptRef.current.trim());
|
|
2972
|
+
interimRef.current = "";
|
|
2973
|
+
}
|
|
2974
|
+
if (wsRef.current) {
|
|
2975
|
+
try {
|
|
2976
|
+
if (wsRef.current.readyState === WebSocket.OPEN) {
|
|
2977
|
+
wsRef.current.send(JSON.stringify({ type: "stop" }));
|
|
2978
|
+
}
|
|
2979
|
+
wsRef.current.close();
|
|
2980
|
+
} catch (e) {
|
|
2981
|
+
}
|
|
2982
|
+
wsRef.current = null;
|
|
2983
|
+
}
|
|
2984
|
+
if (processorRef.current) {
|
|
2985
|
+
processorRef.current.disconnect();
|
|
2986
|
+
processorRef.current = null;
|
|
2987
|
+
}
|
|
2988
|
+
if (audioContextRef.current) {
|
|
2989
|
+
audioContextRef.current.close();
|
|
2990
|
+
audioContextRef.current = null;
|
|
2781
2991
|
}
|
|
2992
|
+
if (streamRef.current) {
|
|
2993
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
2994
|
+
streamRef.current = null;
|
|
2995
|
+
}
|
|
2996
|
+
setIsRecording(false);
|
|
2782
2997
|
}, [clearSilenceTimer]);
|
|
2998
|
+
const stop = React3.useCallback(() => {
|
|
2999
|
+
cleanup();
|
|
3000
|
+
}, [cleanup]);
|
|
2783
3001
|
const clear = React3.useCallback(() => {
|
|
2784
3002
|
setTranscript("");
|
|
2785
|
-
|
|
3003
|
+
transcriptRef.current = "";
|
|
3004
|
+
setError(null);
|
|
2786
3005
|
}, []);
|
|
2787
|
-
const
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
let final = "";
|
|
2802
|
-
for (let i = 0; i < event.results.length; i++) {
|
|
2803
|
-
const result = event.results[i];
|
|
2804
|
-
if (result.isFinal) {
|
|
2805
|
-
final += result[0].transcript;
|
|
2806
|
-
} else {
|
|
2807
|
-
interim += result[0].transcript;
|
|
2808
|
-
}
|
|
3006
|
+
const startAudioCapture = React3.useCallback(() => {
|
|
3007
|
+
if (!streamRef.current || !wsRef.current) return;
|
|
3008
|
+
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24e3 });
|
|
3009
|
+
const source = audioContextRef.current.createMediaStreamSource(
|
|
3010
|
+
streamRef.current
|
|
3011
|
+
);
|
|
3012
|
+
processorRef.current = audioContextRef.current.createScriptProcessor(
|
|
3013
|
+
4096,
|
|
3014
|
+
1,
|
|
3015
|
+
1
|
|
3016
|
+
);
|
|
3017
|
+
processorRef.current.onaudioprocess = (event) => {
|
|
3018
|
+
if (!isRecordingRef.current || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
|
|
3019
|
+
return;
|
|
2809
3020
|
}
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
stop();
|
|
2816
|
-
}, silenceTimeoutMs);
|
|
3021
|
+
const inputData = event.inputBuffer.getChannelData(0);
|
|
3022
|
+
const pcm16 = new Int16Array(inputData.length);
|
|
3023
|
+
for (let i = 0; i < inputData.length; i++) {
|
|
3024
|
+
const s = Math.max(-1, Math.min(1, inputData[i]));
|
|
3025
|
+
pcm16[i] = s < 0 ? s * 32768 : s * 32767;
|
|
2817
3026
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
3027
|
+
const bytes = new Uint8Array(pcm16.buffer);
|
|
3028
|
+
let binary = "";
|
|
3029
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
3030
|
+
binary += String.fromCharCode(bytes[i]);
|
|
2822
3031
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
setIsRecording(false);
|
|
2827
|
-
recognitionRef.current = null;
|
|
3032
|
+
wsRef.current.send(
|
|
3033
|
+
JSON.stringify({ type: "audio", data: btoa(binary) })
|
|
3034
|
+
);
|
|
2828
3035
|
};
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
3036
|
+
source.connect(processorRef.current);
|
|
3037
|
+
processorRef.current.connect(audioContextRef.current.destination);
|
|
3038
|
+
}, []);
|
|
3039
|
+
const start = React3.useCallback(async () => {
|
|
3040
|
+
if (!supported) {
|
|
3041
|
+
setError("Audio recording not supported in this browser");
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
setError(null);
|
|
3045
|
+
transcriptRef.current = "";
|
|
3046
|
+
setTranscript("");
|
|
3047
|
+
try {
|
|
3048
|
+
streamRef.current = await navigator.mediaDevices.getUserMedia({
|
|
3049
|
+
audio: {
|
|
3050
|
+
echoCancellation: true,
|
|
3051
|
+
noiseSuppression: true,
|
|
3052
|
+
sampleRate: 24e3
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
const wsProtocol = backendUrl.startsWith("https") ? "wss" : "ws";
|
|
3056
|
+
const wsHost = backendUrl.replace(/^https?:\/\//, "");
|
|
3057
|
+
const wsUrl = `${wsProtocol}://${wsHost}/api/stt/stream`;
|
|
3058
|
+
wsRef.current = new WebSocket(wsUrl);
|
|
3059
|
+
wsRef.current.onopen = () => {
|
|
3060
|
+
wsRef.current?.send(JSON.stringify({ type: "setup" }));
|
|
3061
|
+
};
|
|
3062
|
+
wsRef.current.onmessage = (event) => {
|
|
3063
|
+
const msg = JSON.parse(event.data);
|
|
3064
|
+
if (msg.type === "ready") {
|
|
3065
|
+
startAudioCapture();
|
|
3066
|
+
isRecordingRef.current = true;
|
|
3067
|
+
setIsRecording(true);
|
|
3068
|
+
} else if (msg.type === "transcript") {
|
|
3069
|
+
if (msg.is_final && msg.text) {
|
|
3070
|
+
transcriptRef.current += msg.text + " ";
|
|
3071
|
+
interimRef.current = "";
|
|
3072
|
+
setTranscript(transcriptRef.current.trim());
|
|
3073
|
+
if (silenceTimeoutMs) {
|
|
3074
|
+
clearSilenceTimer();
|
|
3075
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
3076
|
+
stop();
|
|
3077
|
+
}, silenceTimeoutMs);
|
|
3078
|
+
}
|
|
3079
|
+
} else if (!msg.is_final && msg.text) {
|
|
3080
|
+
interimRef.current = msg.text;
|
|
3081
|
+
setTranscript((transcriptRef.current + msg.text).trim());
|
|
3082
|
+
}
|
|
3083
|
+
} else if (msg.type === "error") {
|
|
3084
|
+
setError(msg.message || "STT error");
|
|
3085
|
+
cleanup();
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
wsRef.current.onerror = () => {
|
|
3089
|
+
setError("WebSocket connection error");
|
|
3090
|
+
cleanup();
|
|
3091
|
+
};
|
|
3092
|
+
wsRef.current.onclose = () => {
|
|
3093
|
+
if (isRecordingRef.current) {
|
|
3094
|
+
cleanup();
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
} catch (err) {
|
|
3098
|
+
setError(
|
|
3099
|
+
err instanceof Error ? err.message : "Failed to start recording"
|
|
3100
|
+
);
|
|
3101
|
+
cleanup();
|
|
3102
|
+
}
|
|
3103
|
+
}, [
|
|
3104
|
+
supported,
|
|
3105
|
+
backendUrl,
|
|
3106
|
+
startAudioCapture,
|
|
3107
|
+
silenceTimeoutMs,
|
|
3108
|
+
clearSilenceTimer,
|
|
3109
|
+
stop,
|
|
3110
|
+
cleanup
|
|
3111
|
+
]);
|
|
2833
3112
|
const toggle = React3.useCallback(() => {
|
|
2834
3113
|
if (isRecording) {
|
|
2835
3114
|
stop();
|
|
@@ -2839,13 +3118,19 @@ function useVoiceInput(options = {}) {
|
|
|
2839
3118
|
}, [isRecording, start, stop]);
|
|
2840
3119
|
React3.useEffect(() => {
|
|
2841
3120
|
return () => {
|
|
2842
|
-
|
|
2843
|
-
if (recognitionRef.current) {
|
|
2844
|
-
recognitionRef.current.abort();
|
|
2845
|
-
}
|
|
3121
|
+
cleanup();
|
|
2846
3122
|
};
|
|
2847
|
-
}, [
|
|
2848
|
-
return {
|
|
3123
|
+
}, [cleanup]);
|
|
3124
|
+
return {
|
|
3125
|
+
supported,
|
|
3126
|
+
isRecording,
|
|
3127
|
+
transcript,
|
|
3128
|
+
error,
|
|
3129
|
+
start,
|
|
3130
|
+
stop,
|
|
3131
|
+
toggle,
|
|
3132
|
+
clear
|
|
3133
|
+
};
|
|
2849
3134
|
}
|
|
2850
3135
|
var Textarea = React3__default.default.forwardRef(
|
|
2851
3136
|
({ className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3039,11 +3324,23 @@ var PromptInputBox = React3__default.default.forwardRef(
|
|
|
3039
3324
|
selectedModel = "gpt-4o",
|
|
3040
3325
|
onModelChange,
|
|
3041
3326
|
availableModels = [],
|
|
3042
|
-
highlighted = false
|
|
3327
|
+
highlighted = false,
|
|
3328
|
+
backendUrl = "",
|
|
3329
|
+
triggerVoiceRecording = 0
|
|
3043
3330
|
}, ref) => {
|
|
3044
3331
|
const [input, setInput] = React3__default.default.useState("");
|
|
3045
3332
|
const promptBoxRef = React3__default.default.useRef(null);
|
|
3046
|
-
const voice = useVoiceInput();
|
|
3333
|
+
const voice = useVoiceInput({ backendUrl, silenceTimeoutMs: 1500 });
|
|
3334
|
+
const lastTriggerRef = React3__default.default.useRef(0);
|
|
3335
|
+
const voiceRef = React3__default.default.useRef(voice);
|
|
3336
|
+
voiceRef.current = voice;
|
|
3337
|
+
React3__default.default.useEffect(() => {
|
|
3338
|
+
if (triggerVoiceRecording > 0 && triggerVoiceRecording !== lastTriggerRef.current) {
|
|
3339
|
+
console.log("[Voice] Auto-starting recording from trigger");
|
|
3340
|
+
voiceRef.current.start();
|
|
3341
|
+
}
|
|
3342
|
+
lastTriggerRef.current = triggerVoiceRecording;
|
|
3343
|
+
}, [triggerVoiceRecording]);
|
|
3047
3344
|
React3__default.default.useEffect(() => {
|
|
3048
3345
|
if (voice.isRecording && voice.transcript) {
|
|
3049
3346
|
setInput(voice.transcript);
|
|
@@ -3052,11 +3349,16 @@ var PromptInputBox = React3__default.default.forwardRef(
|
|
|
3052
3349
|
const wasRecordingRef = React3__default.default.useRef(false);
|
|
3053
3350
|
React3__default.default.useEffect(() => {
|
|
3054
3351
|
if (wasRecordingRef.current && !voice.isRecording && voice.transcript) {
|
|
3055
|
-
|
|
3352
|
+
const messageToSend = voice.transcript.trim();
|
|
3353
|
+
if (messageToSend) {
|
|
3354
|
+
console.log("[Voice] Auto-sending:", messageToSend);
|
|
3355
|
+
onSend(messageToSend);
|
|
3356
|
+
setInput("");
|
|
3357
|
+
}
|
|
3056
3358
|
voice.clear();
|
|
3057
3359
|
}
|
|
3058
3360
|
wasRecordingRef.current = voice.isRecording;
|
|
3059
|
-
}, [voice.isRecording, voice.transcript, voice.clear]);
|
|
3361
|
+
}, [voice.isRecording, voice.transcript, voice.clear, onSend]);
|
|
3060
3362
|
const handleSubmit = () => {
|
|
3061
3363
|
if (input.trim()) {
|
|
3062
3364
|
if (voice.isRecording) {
|
|
@@ -3667,6 +3969,25 @@ function CrowWidget({
|
|
|
3667
3969
|
setShouldRestoreHistory(true);
|
|
3668
3970
|
}
|
|
3669
3971
|
});
|
|
3972
|
+
const tts = useTTSOutput({ backendUrl: apiUrl });
|
|
3973
|
+
const ttsRef = React3.useRef(tts);
|
|
3974
|
+
ttsRef.current = tts;
|
|
3975
|
+
const wasLoadingRef = React3.useRef(false);
|
|
3976
|
+
React3.useEffect(() => {
|
|
3977
|
+
console.log("[Crow TTS] isLoading changed:", chat.isLoading, "wasLoading:", wasLoadingRef.current);
|
|
3978
|
+
if (wasLoadingRef.current && !chat.isLoading) {
|
|
3979
|
+
const lastMessage = [...chat.messages].reverse().find((m) => m.isBot);
|
|
3980
|
+
console.log("[Crow TTS] Last bot message:", lastMessage?.content?.substring(0, 50));
|
|
3981
|
+
if (lastMessage?.content) {
|
|
3982
|
+
const textToSpeak = lastMessage.content.replace(/\*\*/g, "").replace(/\*/g, "").replace(/`[^`]+`/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
|
|
3983
|
+
if (textToSpeak) {
|
|
3984
|
+
console.log("[Crow TTS] Speaking:", textToSpeak.substring(0, 50));
|
|
3985
|
+
ttsRef.current.speak(textToSpeak);
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
wasLoadingRef.current = chat.isLoading;
|
|
3990
|
+
}, [chat.isLoading, chat.messages]);
|
|
3670
3991
|
React3.useEffect(() => {
|
|
3671
3992
|
if (initialSuggestions.length > 0 && chat.suggestedActions.length === 0) {
|
|
3672
3993
|
chat.setSuggestedActions(initialSuggestions);
|
|
@@ -3706,7 +4027,15 @@ function CrowWidget({
|
|
|
3706
4027
|
const { executeClientTool } = useCrowAPI({
|
|
3707
4028
|
onIdentified: async () => {
|
|
3708
4029
|
setIsVerifiedUser(true);
|
|
3709
|
-
await conversations.loadConversations();
|
|
4030
|
+
const convs = await conversations.loadConversations();
|
|
4031
|
+
if (convs.length > 0) {
|
|
4032
|
+
const mostRecent = convs[0];
|
|
4033
|
+
const historyMessages = await conversations.loadConversationHistory(mostRecent.id);
|
|
4034
|
+
if (historyMessages.length > 0) {
|
|
4035
|
+
chat.loadMessages(historyMessages);
|
|
4036
|
+
chat.setConversationId(mostRecent.id);
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
3710
4039
|
},
|
|
3711
4040
|
onReset: () => {
|
|
3712
4041
|
setIsVerifiedUser(false);
|
|
@@ -4061,7 +4390,8 @@ function CrowWidget({
|
|
|
4061
4390
|
isLoading: chat.isLoading,
|
|
4062
4391
|
showStopButton: isBrowserUseActive || !!askUserResolver || !!pendingConfirmation,
|
|
4063
4392
|
highlighted: !!askUserResolver,
|
|
4064
|
-
className: "crow-backdrop-blur-md"
|
|
4393
|
+
className: "crow-backdrop-blur-md",
|
|
4394
|
+
backendUrl: apiUrl
|
|
4065
4395
|
}
|
|
4066
4396
|
)
|
|
4067
4397
|
] })
|