@allshift/ui 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +225 -2
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +222 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -40,9 +40,11 @@ __export(index_exports, {
|
|
|
40
40
|
LiveStat: () => LiveStat,
|
|
41
41
|
PageTransition: () => PageTransition,
|
|
42
42
|
ScrollGenerateCard: () => ScrollGenerateCard,
|
|
43
|
+
ShiftBar: () => ShiftBar,
|
|
43
44
|
ShiftCard: () => ShiftCard,
|
|
44
45
|
TiltCard: () => TiltCard,
|
|
45
|
-
useGenerate: () => useGenerate
|
|
46
|
+
useGenerate: () => useGenerate,
|
|
47
|
+
useHeyShift: () => useHeyShift
|
|
46
48
|
});
|
|
47
49
|
module.exports = __toCommonJS(index_exports);
|
|
48
50
|
|
|
@@ -822,6 +824,225 @@ function PageTransition({ children, className }) {
|
|
|
822
824
|
pathname
|
|
823
825
|
);
|
|
824
826
|
}
|
|
827
|
+
|
|
828
|
+
// src/voice/useHeyShift.ts
|
|
829
|
+
var import_react10 = require("react");
|
|
830
|
+
function getSpeechRecognitionCtor() {
|
|
831
|
+
const w = window;
|
|
832
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
833
|
+
}
|
|
834
|
+
function useHeyShift(config = {}) {
|
|
835
|
+
const {
|
|
836
|
+
wakeWord = "hey shift",
|
|
837
|
+
language = "en-US",
|
|
838
|
+
silenceMs = 1500,
|
|
839
|
+
onCommand,
|
|
840
|
+
onStateChange
|
|
841
|
+
} = config;
|
|
842
|
+
const [state, setState] = (0, import_react10.useState)("idle");
|
|
843
|
+
const [transcript, setTranscript] = (0, import_react10.useState)("");
|
|
844
|
+
const [isSupported, setIsSupported] = (0, import_react10.useState)(false);
|
|
845
|
+
const recRef = (0, import_react10.useRef)(null);
|
|
846
|
+
const silenceTimerRef = (0, import_react10.useRef)(null);
|
|
847
|
+
const activeRef = (0, import_react10.useRef)(false);
|
|
848
|
+
const accumulatedRef = (0, import_react10.useRef)("");
|
|
849
|
+
const stateRef = (0, import_react10.useRef)("idle");
|
|
850
|
+
const updateState = (0, import_react10.useCallback)((s) => {
|
|
851
|
+
stateRef.current = s;
|
|
852
|
+
setState(s);
|
|
853
|
+
onStateChange?.(s);
|
|
854
|
+
}, [onStateChange]);
|
|
855
|
+
(0, import_react10.useEffect)(() => {
|
|
856
|
+
const SR = getSpeechRecognitionCtor();
|
|
857
|
+
setIsSupported(!!(SR && window.speechSynthesis));
|
|
858
|
+
}, []);
|
|
859
|
+
const getVoice = (0, import_react10.useCallback)(() => {
|
|
860
|
+
const voices = window.speechSynthesis?.getVoices() ?? [];
|
|
861
|
+
return voices.find((v) => v.name.toLowerCase().includes("will")) ?? voices.find((v) => v.name.toLowerCase().includes("daniel")) ?? voices.find((v) => v.lang.startsWith("en") && v.name.toLowerCase().includes("male")) ?? voices.find((v) => v.lang.startsWith("en")) ?? voices[0] ?? null;
|
|
862
|
+
}, []);
|
|
863
|
+
const speak = (0, import_react10.useCallback)(async (text) => {
|
|
864
|
+
if (!window.speechSynthesis) return;
|
|
865
|
+
window.speechSynthesis.cancel();
|
|
866
|
+
return new Promise((resolve) => {
|
|
867
|
+
const utter = new SpeechSynthesisUtterance(text);
|
|
868
|
+
const setVoiceAndSpeak = () => {
|
|
869
|
+
const voice = getVoice();
|
|
870
|
+
if (voice) utter.voice = voice;
|
|
871
|
+
utter.rate = 1.05;
|
|
872
|
+
utter.pitch = 0.95;
|
|
873
|
+
updateState("speaking");
|
|
874
|
+
utter.onend = () => {
|
|
875
|
+
updateState("idle");
|
|
876
|
+
resolve();
|
|
877
|
+
};
|
|
878
|
+
utter.onerror = () => {
|
|
879
|
+
updateState("idle");
|
|
880
|
+
resolve();
|
|
881
|
+
};
|
|
882
|
+
window.speechSynthesis.speak(utter);
|
|
883
|
+
};
|
|
884
|
+
if (window.speechSynthesis.getVoices().length > 0) {
|
|
885
|
+
setVoiceAndSpeak();
|
|
886
|
+
} else {
|
|
887
|
+
window.speechSynthesis.onvoiceschanged = () => {
|
|
888
|
+
setVoiceAndSpeak();
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
}, [getVoice, updateState]);
|
|
893
|
+
const cancelSpeech = (0, import_react10.useCallback)(() => {
|
|
894
|
+
window.speechSynthesis?.cancel();
|
|
895
|
+
if (stateRef.current === "speaking") updateState("idle");
|
|
896
|
+
}, [updateState]);
|
|
897
|
+
const resetSilenceTimer = (0, import_react10.useCallback)((text) => {
|
|
898
|
+
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
|
|
899
|
+
silenceTimerRef.current = setTimeout(async () => {
|
|
900
|
+
if (!activeRef.current || !text.trim()) return;
|
|
901
|
+
activeRef.current = false;
|
|
902
|
+
accumulatedRef.current = "";
|
|
903
|
+
setTranscript("");
|
|
904
|
+
updateState("thinking");
|
|
905
|
+
try {
|
|
906
|
+
const response = await onCommand?.(text.trim());
|
|
907
|
+
if (response) {
|
|
908
|
+
await speak(response);
|
|
909
|
+
} else {
|
|
910
|
+
updateState("idle");
|
|
911
|
+
}
|
|
912
|
+
} catch {
|
|
913
|
+
updateState("error");
|
|
914
|
+
setTimeout(() => updateState("idle"), 2e3);
|
|
915
|
+
}
|
|
916
|
+
}, silenceMs);
|
|
917
|
+
}, [silenceMs, onCommand, speak, updateState]);
|
|
918
|
+
const startListening = (0, import_react10.useCallback)(() => {
|
|
919
|
+
const SR = getSpeechRecognitionCtor();
|
|
920
|
+
if (!SR || recRef.current) return;
|
|
921
|
+
const rec = new SR();
|
|
922
|
+
rec.continuous = true;
|
|
923
|
+
rec.interimResults = true;
|
|
924
|
+
rec.lang = language;
|
|
925
|
+
rec.onresult = (event) => {
|
|
926
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
927
|
+
const t = event.results[i][0].transcript.trim().toLowerCase();
|
|
928
|
+
const isFinal = event.results[i].isFinal;
|
|
929
|
+
if (!activeRef.current) {
|
|
930
|
+
if (t.includes(wakeWord.toLowerCase())) {
|
|
931
|
+
activeRef.current = true;
|
|
932
|
+
accumulatedRef.current = "";
|
|
933
|
+
setTranscript("");
|
|
934
|
+
updateState("wake");
|
|
935
|
+
setTimeout(() => {
|
|
936
|
+
if (activeRef.current) updateState("listening");
|
|
937
|
+
}, 300);
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
const raw = event.results[i][0].transcript;
|
|
941
|
+
if (isFinal) {
|
|
942
|
+
accumulatedRef.current += " " + raw;
|
|
943
|
+
}
|
|
944
|
+
const display = (accumulatedRef.current + " " + (isFinal ? "" : raw)).trim();
|
|
945
|
+
setTranscript(display);
|
|
946
|
+
resetSilenceTimer(display);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
rec.onend = () => {
|
|
951
|
+
if (recRef.current) {
|
|
952
|
+
setTimeout(() => {
|
|
953
|
+
recRef.current?.start();
|
|
954
|
+
}, 150);
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
rec.onerror = (event) => {
|
|
958
|
+
if (event.error === "no-speech") return;
|
|
959
|
+
if (event.error === "not-allowed") updateState("error");
|
|
960
|
+
};
|
|
961
|
+
rec.start();
|
|
962
|
+
recRef.current = rec;
|
|
963
|
+
}, [language, wakeWord, resetSilenceTimer, updateState]);
|
|
964
|
+
const stopListening = (0, import_react10.useCallback)(() => {
|
|
965
|
+
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
|
|
966
|
+
const rec = recRef.current;
|
|
967
|
+
recRef.current = null;
|
|
968
|
+
rec?.stop();
|
|
969
|
+
activeRef.current = false;
|
|
970
|
+
accumulatedRef.current = "";
|
|
971
|
+
setTranscript("");
|
|
972
|
+
updateState("idle");
|
|
973
|
+
}, [updateState]);
|
|
974
|
+
(0, import_react10.useEffect)(() => {
|
|
975
|
+
return () => {
|
|
976
|
+
const rec = recRef.current;
|
|
977
|
+
recRef.current = null;
|
|
978
|
+
rec?.stop();
|
|
979
|
+
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
|
|
980
|
+
window.speechSynthesis?.cancel();
|
|
981
|
+
};
|
|
982
|
+
}, []);
|
|
983
|
+
return { state, transcript, startListening, stopListening, speak, cancelSpeech, isSupported };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/voice/ShiftBar.tsx
|
|
987
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
988
|
+
var COLORS = {
|
|
989
|
+
idle: "rgba(255,255,255,0.15)",
|
|
990
|
+
wake: "#0ed882",
|
|
991
|
+
listening: "#0ed882",
|
|
992
|
+
thinking: "#22d3ee",
|
|
993
|
+
speaking: "#a78bfa",
|
|
994
|
+
error: "#f87171"
|
|
995
|
+
};
|
|
996
|
+
function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
|
|
997
|
+
const color = COLORS[state];
|
|
998
|
+
const isAnimated = state !== "idle" && state !== "error";
|
|
999
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
|
|
1000
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("style", { children: `
|
|
1001
|
+
@keyframes _sb_pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.35;transform:scale(1.45)} }
|
|
1002
|
+
@keyframes _sb_wave { 0%,100%{transform:scaleY(1)} 50%{transform:scaleY(1.9)} }
|
|
1003
|
+
@keyframes _sb_spin { to{transform:rotate(360deg)} }
|
|
1004
|
+
` }),
|
|
1005
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1006
|
+
"span",
|
|
1007
|
+
{
|
|
1008
|
+
className,
|
|
1009
|
+
role: "status",
|
|
1010
|
+
"aria-label": `Shift: ${state}`,
|
|
1011
|
+
style: { display: "inline-flex", alignItems: "center", gap: Math.round(size * 0.6) },
|
|
1012
|
+
children: Array.from({ length: dots }).map((_, i) => {
|
|
1013
|
+
let animation;
|
|
1014
|
+
if (isAnimated) {
|
|
1015
|
+
if (state === "thinking") {
|
|
1016
|
+
animation = `_sb_spin 1s linear infinite`;
|
|
1017
|
+
} else if (state === "speaking") {
|
|
1018
|
+
animation = `_sb_wave .7s ease-in-out ${i * 0.12}s infinite`;
|
|
1019
|
+
} else {
|
|
1020
|
+
animation = `_sb_pulse 1.2s ease-in-out ${i * 0.18}s infinite`;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1024
|
+
"span",
|
|
1025
|
+
{
|
|
1026
|
+
style: {
|
|
1027
|
+
display: "inline-block",
|
|
1028
|
+
width: state === "thinking" ? size * 1.6 : size,
|
|
1029
|
+
height: size,
|
|
1030
|
+
borderRadius: state === "thinking" ? "50%" : "50%",
|
|
1031
|
+
background: state === "thinking" ? "transparent" : color,
|
|
1032
|
+
border: state === "thinking" ? `2px solid ${color}` : "none",
|
|
1033
|
+
borderTopColor: state === "thinking" ? "transparent" : void 0,
|
|
1034
|
+
transition: "background .3s ease, border-color .3s ease",
|
|
1035
|
+
animation,
|
|
1036
|
+
transformOrigin: "50% 50%"
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
i
|
|
1040
|
+
);
|
|
1041
|
+
})
|
|
1042
|
+
}
|
|
1043
|
+
)
|
|
1044
|
+
] });
|
|
1045
|
+
}
|
|
825
1046
|
// Annotate the CommonJS export names for ESM import in node:
|
|
826
1047
|
0 && (module.exports = {
|
|
827
1048
|
ACCENT_RGB,
|
|
@@ -833,7 +1054,9 @@ function PageTransition({ children, className }) {
|
|
|
833
1054
|
LiveStat,
|
|
834
1055
|
PageTransition,
|
|
835
1056
|
ScrollGenerateCard,
|
|
1057
|
+
ShiftBar,
|
|
836
1058
|
ShiftCard,
|
|
837
1059
|
TiltCard,
|
|
838
|
-
useGenerate
|
|
1060
|
+
useGenerate,
|
|
1061
|
+
useHeyShift
|
|
839
1062
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -263,4 +263,33 @@ interface PageTransitionProps {
|
|
|
263
263
|
*/
|
|
264
264
|
declare function PageTransition({ children, className }: PageTransitionProps): react.JSX.Element;
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
type ShiftBarState = 'idle' | 'wake' | 'listening' | 'thinking' | 'speaking' | 'error';
|
|
267
|
+
interface HeyShiftConfig {
|
|
268
|
+
wakeWord?: string;
|
|
269
|
+
language?: string;
|
|
270
|
+
silenceMs?: number;
|
|
271
|
+
onCommand?: (text: string) => Promise<string | void>;
|
|
272
|
+
onStateChange?: (state: ShiftBarState) => void;
|
|
273
|
+
}
|
|
274
|
+
interface HeyShiftReturn {
|
|
275
|
+
state: ShiftBarState;
|
|
276
|
+
transcript: string;
|
|
277
|
+
startListening: () => void;
|
|
278
|
+
stopListening: () => void;
|
|
279
|
+
speak: (text: string) => Promise<void>;
|
|
280
|
+
cancelSpeech: () => void;
|
|
281
|
+
isSupported: boolean;
|
|
282
|
+
}
|
|
283
|
+
declare function useHeyShift(config?: HeyShiftConfig): HeyShiftReturn;
|
|
284
|
+
|
|
285
|
+
interface ShiftBarProps {
|
|
286
|
+
state: ShiftBarState;
|
|
287
|
+
/** Number of dots. Default 3 for wave effect. */
|
|
288
|
+
dots?: number;
|
|
289
|
+
/** Dot size in px. Default 7. */
|
|
290
|
+
size?: number;
|
|
291
|
+
className?: string;
|
|
292
|
+
}
|
|
293
|
+
declare function ShiftBar({ state, dots, size, className }: ShiftBarProps): react.JSX.Element;
|
|
294
|
+
|
|
295
|
+
export { ACCENT_RGB, type Accent, CanvasLauncher, type CanvasLauncherProps, CanvasTransition, type CanvasTransitionProps, type GenPhase, GenerativeView, type GenerativeViewProps, type HeyShiftConfig, type HeyShiftReturn, HolographicShine, type HolographicShineProps, type LauncherGroup, type LauncherItem, LiveCard, type LiveCardProps, LiveStat, type LiveStatProps, PageTransition, type PageTransitionProps, ScrollGenerateCard, type ScrollGenerateCardProps, ShiftBar, type ShiftBarProps, type ShiftBarState, ShiftCard, type ShiftCardProps, TiltCard, type TiltCardProps, type UseGenerateOptions, useGenerate, useHeyShift };
|
package/dist/index.d.ts
CHANGED
|
@@ -263,4 +263,33 @@ interface PageTransitionProps {
|
|
|
263
263
|
*/
|
|
264
264
|
declare function PageTransition({ children, className }: PageTransitionProps): react.JSX.Element;
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
type ShiftBarState = 'idle' | 'wake' | 'listening' | 'thinking' | 'speaking' | 'error';
|
|
267
|
+
interface HeyShiftConfig {
|
|
268
|
+
wakeWord?: string;
|
|
269
|
+
language?: string;
|
|
270
|
+
silenceMs?: number;
|
|
271
|
+
onCommand?: (text: string) => Promise<string | void>;
|
|
272
|
+
onStateChange?: (state: ShiftBarState) => void;
|
|
273
|
+
}
|
|
274
|
+
interface HeyShiftReturn {
|
|
275
|
+
state: ShiftBarState;
|
|
276
|
+
transcript: string;
|
|
277
|
+
startListening: () => void;
|
|
278
|
+
stopListening: () => void;
|
|
279
|
+
speak: (text: string) => Promise<void>;
|
|
280
|
+
cancelSpeech: () => void;
|
|
281
|
+
isSupported: boolean;
|
|
282
|
+
}
|
|
283
|
+
declare function useHeyShift(config?: HeyShiftConfig): HeyShiftReturn;
|
|
284
|
+
|
|
285
|
+
interface ShiftBarProps {
|
|
286
|
+
state: ShiftBarState;
|
|
287
|
+
/** Number of dots. Default 3 for wave effect. */
|
|
288
|
+
dots?: number;
|
|
289
|
+
/** Dot size in px. Default 7. */
|
|
290
|
+
size?: number;
|
|
291
|
+
className?: string;
|
|
292
|
+
}
|
|
293
|
+
declare function ShiftBar({ state, dots, size, className }: ShiftBarProps): react.JSX.Element;
|
|
294
|
+
|
|
295
|
+
export { ACCENT_RGB, type Accent, CanvasLauncher, type CanvasLauncherProps, CanvasTransition, type CanvasTransitionProps, type GenPhase, GenerativeView, type GenerativeViewProps, type HeyShiftConfig, type HeyShiftReturn, HolographicShine, type HolographicShineProps, type LauncherGroup, type LauncherItem, LiveCard, type LiveCardProps, LiveStat, type LiveStatProps, PageTransition, type PageTransitionProps, ScrollGenerateCard, type ScrollGenerateCardProps, ShiftBar, type ShiftBarProps, type ShiftBarState, ShiftCard, type ShiftCardProps, TiltCard, type TiltCardProps, type UseGenerateOptions, useGenerate, useHeyShift };
|
package/dist/index.js
CHANGED
|
@@ -792,6 +792,225 @@ function PageTransition({ children, className }) {
|
|
|
792
792
|
pathname
|
|
793
793
|
);
|
|
794
794
|
}
|
|
795
|
+
|
|
796
|
+
// src/voice/useHeyShift.ts
|
|
797
|
+
import { useCallback, useEffect as useEffect6, useRef as useRef6, useState as useState6 } from "react";
|
|
798
|
+
function getSpeechRecognitionCtor() {
|
|
799
|
+
const w = window;
|
|
800
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
801
|
+
}
|
|
802
|
+
function useHeyShift(config = {}) {
|
|
803
|
+
const {
|
|
804
|
+
wakeWord = "hey shift",
|
|
805
|
+
language = "en-US",
|
|
806
|
+
silenceMs = 1500,
|
|
807
|
+
onCommand,
|
|
808
|
+
onStateChange
|
|
809
|
+
} = config;
|
|
810
|
+
const [state, setState] = useState6("idle");
|
|
811
|
+
const [transcript, setTranscript] = useState6("");
|
|
812
|
+
const [isSupported, setIsSupported] = useState6(false);
|
|
813
|
+
const recRef = useRef6(null);
|
|
814
|
+
const silenceTimerRef = useRef6(null);
|
|
815
|
+
const activeRef = useRef6(false);
|
|
816
|
+
const accumulatedRef = useRef6("");
|
|
817
|
+
const stateRef = useRef6("idle");
|
|
818
|
+
const updateState = useCallback((s) => {
|
|
819
|
+
stateRef.current = s;
|
|
820
|
+
setState(s);
|
|
821
|
+
onStateChange?.(s);
|
|
822
|
+
}, [onStateChange]);
|
|
823
|
+
useEffect6(() => {
|
|
824
|
+
const SR = getSpeechRecognitionCtor();
|
|
825
|
+
setIsSupported(!!(SR && window.speechSynthesis));
|
|
826
|
+
}, []);
|
|
827
|
+
const getVoice = useCallback(() => {
|
|
828
|
+
const voices = window.speechSynthesis?.getVoices() ?? [];
|
|
829
|
+
return voices.find((v) => v.name.toLowerCase().includes("will")) ?? voices.find((v) => v.name.toLowerCase().includes("daniel")) ?? voices.find((v) => v.lang.startsWith("en") && v.name.toLowerCase().includes("male")) ?? voices.find((v) => v.lang.startsWith("en")) ?? voices[0] ?? null;
|
|
830
|
+
}, []);
|
|
831
|
+
const speak = useCallback(async (text) => {
|
|
832
|
+
if (!window.speechSynthesis) return;
|
|
833
|
+
window.speechSynthesis.cancel();
|
|
834
|
+
return new Promise((resolve) => {
|
|
835
|
+
const utter = new SpeechSynthesisUtterance(text);
|
|
836
|
+
const setVoiceAndSpeak = () => {
|
|
837
|
+
const voice = getVoice();
|
|
838
|
+
if (voice) utter.voice = voice;
|
|
839
|
+
utter.rate = 1.05;
|
|
840
|
+
utter.pitch = 0.95;
|
|
841
|
+
updateState("speaking");
|
|
842
|
+
utter.onend = () => {
|
|
843
|
+
updateState("idle");
|
|
844
|
+
resolve();
|
|
845
|
+
};
|
|
846
|
+
utter.onerror = () => {
|
|
847
|
+
updateState("idle");
|
|
848
|
+
resolve();
|
|
849
|
+
};
|
|
850
|
+
window.speechSynthesis.speak(utter);
|
|
851
|
+
};
|
|
852
|
+
if (window.speechSynthesis.getVoices().length > 0) {
|
|
853
|
+
setVoiceAndSpeak();
|
|
854
|
+
} else {
|
|
855
|
+
window.speechSynthesis.onvoiceschanged = () => {
|
|
856
|
+
setVoiceAndSpeak();
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}, [getVoice, updateState]);
|
|
861
|
+
const cancelSpeech = useCallback(() => {
|
|
862
|
+
window.speechSynthesis?.cancel();
|
|
863
|
+
if (stateRef.current === "speaking") updateState("idle");
|
|
864
|
+
}, [updateState]);
|
|
865
|
+
const resetSilenceTimer = useCallback((text) => {
|
|
866
|
+
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
|
|
867
|
+
silenceTimerRef.current = setTimeout(async () => {
|
|
868
|
+
if (!activeRef.current || !text.trim()) return;
|
|
869
|
+
activeRef.current = false;
|
|
870
|
+
accumulatedRef.current = "";
|
|
871
|
+
setTranscript("");
|
|
872
|
+
updateState("thinking");
|
|
873
|
+
try {
|
|
874
|
+
const response = await onCommand?.(text.trim());
|
|
875
|
+
if (response) {
|
|
876
|
+
await speak(response);
|
|
877
|
+
} else {
|
|
878
|
+
updateState("idle");
|
|
879
|
+
}
|
|
880
|
+
} catch {
|
|
881
|
+
updateState("error");
|
|
882
|
+
setTimeout(() => updateState("idle"), 2e3);
|
|
883
|
+
}
|
|
884
|
+
}, silenceMs);
|
|
885
|
+
}, [silenceMs, onCommand, speak, updateState]);
|
|
886
|
+
const startListening = useCallback(() => {
|
|
887
|
+
const SR = getSpeechRecognitionCtor();
|
|
888
|
+
if (!SR || recRef.current) return;
|
|
889
|
+
const rec = new SR();
|
|
890
|
+
rec.continuous = true;
|
|
891
|
+
rec.interimResults = true;
|
|
892
|
+
rec.lang = language;
|
|
893
|
+
rec.onresult = (event) => {
|
|
894
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
895
|
+
const t = event.results[i][0].transcript.trim().toLowerCase();
|
|
896
|
+
const isFinal = event.results[i].isFinal;
|
|
897
|
+
if (!activeRef.current) {
|
|
898
|
+
if (t.includes(wakeWord.toLowerCase())) {
|
|
899
|
+
activeRef.current = true;
|
|
900
|
+
accumulatedRef.current = "";
|
|
901
|
+
setTranscript("");
|
|
902
|
+
updateState("wake");
|
|
903
|
+
setTimeout(() => {
|
|
904
|
+
if (activeRef.current) updateState("listening");
|
|
905
|
+
}, 300);
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
const raw = event.results[i][0].transcript;
|
|
909
|
+
if (isFinal) {
|
|
910
|
+
accumulatedRef.current += " " + raw;
|
|
911
|
+
}
|
|
912
|
+
const display = (accumulatedRef.current + " " + (isFinal ? "" : raw)).trim();
|
|
913
|
+
setTranscript(display);
|
|
914
|
+
resetSilenceTimer(display);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
rec.onend = () => {
|
|
919
|
+
if (recRef.current) {
|
|
920
|
+
setTimeout(() => {
|
|
921
|
+
recRef.current?.start();
|
|
922
|
+
}, 150);
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
rec.onerror = (event) => {
|
|
926
|
+
if (event.error === "no-speech") return;
|
|
927
|
+
if (event.error === "not-allowed") updateState("error");
|
|
928
|
+
};
|
|
929
|
+
rec.start();
|
|
930
|
+
recRef.current = rec;
|
|
931
|
+
}, [language, wakeWord, resetSilenceTimer, updateState]);
|
|
932
|
+
const stopListening = useCallback(() => {
|
|
933
|
+
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
|
|
934
|
+
const rec = recRef.current;
|
|
935
|
+
recRef.current = null;
|
|
936
|
+
rec?.stop();
|
|
937
|
+
activeRef.current = false;
|
|
938
|
+
accumulatedRef.current = "";
|
|
939
|
+
setTranscript("");
|
|
940
|
+
updateState("idle");
|
|
941
|
+
}, [updateState]);
|
|
942
|
+
useEffect6(() => {
|
|
943
|
+
return () => {
|
|
944
|
+
const rec = recRef.current;
|
|
945
|
+
recRef.current = null;
|
|
946
|
+
rec?.stop();
|
|
947
|
+
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
|
|
948
|
+
window.speechSynthesis?.cancel();
|
|
949
|
+
};
|
|
950
|
+
}, []);
|
|
951
|
+
return { state, transcript, startListening, stopListening, speak, cancelSpeech, isSupported };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/voice/ShiftBar.tsx
|
|
955
|
+
import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
956
|
+
var COLORS = {
|
|
957
|
+
idle: "rgba(255,255,255,0.15)",
|
|
958
|
+
wake: "#0ed882",
|
|
959
|
+
listening: "#0ed882",
|
|
960
|
+
thinking: "#22d3ee",
|
|
961
|
+
speaking: "#a78bfa",
|
|
962
|
+
error: "#f87171"
|
|
963
|
+
};
|
|
964
|
+
function ShiftBar({ state, dots = 3, size = 7, className = "" }) {
|
|
965
|
+
const color = COLORS[state];
|
|
966
|
+
const isAnimated = state !== "idle" && state !== "error";
|
|
967
|
+
return /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
968
|
+
/* @__PURE__ */ jsx11("style", { children: `
|
|
969
|
+
@keyframes _sb_pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.35;transform:scale(1.45)} }
|
|
970
|
+
@keyframes _sb_wave { 0%,100%{transform:scaleY(1)} 50%{transform:scaleY(1.9)} }
|
|
971
|
+
@keyframes _sb_spin { to{transform:rotate(360deg)} }
|
|
972
|
+
` }),
|
|
973
|
+
/* @__PURE__ */ jsx11(
|
|
974
|
+
"span",
|
|
975
|
+
{
|
|
976
|
+
className,
|
|
977
|
+
role: "status",
|
|
978
|
+
"aria-label": `Shift: ${state}`,
|
|
979
|
+
style: { display: "inline-flex", alignItems: "center", gap: Math.round(size * 0.6) },
|
|
980
|
+
children: Array.from({ length: dots }).map((_, i) => {
|
|
981
|
+
let animation;
|
|
982
|
+
if (isAnimated) {
|
|
983
|
+
if (state === "thinking") {
|
|
984
|
+
animation = `_sb_spin 1s linear infinite`;
|
|
985
|
+
} else if (state === "speaking") {
|
|
986
|
+
animation = `_sb_wave .7s ease-in-out ${i * 0.12}s infinite`;
|
|
987
|
+
} else {
|
|
988
|
+
animation = `_sb_pulse 1.2s ease-in-out ${i * 0.18}s infinite`;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return /* @__PURE__ */ jsx11(
|
|
992
|
+
"span",
|
|
993
|
+
{
|
|
994
|
+
style: {
|
|
995
|
+
display: "inline-block",
|
|
996
|
+
width: state === "thinking" ? size * 1.6 : size,
|
|
997
|
+
height: size,
|
|
998
|
+
borderRadius: state === "thinking" ? "50%" : "50%",
|
|
999
|
+
background: state === "thinking" ? "transparent" : color,
|
|
1000
|
+
border: state === "thinking" ? `2px solid ${color}` : "none",
|
|
1001
|
+
borderTopColor: state === "thinking" ? "transparent" : void 0,
|
|
1002
|
+
transition: "background .3s ease, border-color .3s ease",
|
|
1003
|
+
animation,
|
|
1004
|
+
transformOrigin: "50% 50%"
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
i
|
|
1008
|
+
);
|
|
1009
|
+
})
|
|
1010
|
+
}
|
|
1011
|
+
)
|
|
1012
|
+
] });
|
|
1013
|
+
}
|
|
795
1014
|
export {
|
|
796
1015
|
ACCENT_RGB,
|
|
797
1016
|
CanvasLauncher,
|
|
@@ -802,7 +1021,9 @@ export {
|
|
|
802
1021
|
LiveStat,
|
|
803
1022
|
PageTransition,
|
|
804
1023
|
ScrollGenerateCard,
|
|
1024
|
+
ShiftBar,
|
|
805
1025
|
ShiftCard,
|
|
806
1026
|
TiltCard,
|
|
807
|
-
useGenerate
|
|
1027
|
+
useGenerate,
|
|
1028
|
+
useHeyShift
|
|
808
1029
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allshift/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shift family shared FEEL layer — glass morphism, 3D tilt physics, holographic shine, scroll-triggered generation, and the living-canvas engine (LiveCard, LiveStat, CanvasTransition, CanvasLauncher). Brand-neutral by design.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|