@allshift/ui 0.3.1 → 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 +226 -2
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +224 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
"use strict";
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
@@ -39,9 +40,11 @@ __export(index_exports, {
|
|
|
39
40
|
LiveStat: () => LiveStat,
|
|
40
41
|
PageTransition: () => PageTransition,
|
|
41
42
|
ScrollGenerateCard: () => ScrollGenerateCard,
|
|
43
|
+
ShiftBar: () => ShiftBar,
|
|
42
44
|
ShiftCard: () => ShiftCard,
|
|
43
45
|
TiltCard: () => TiltCard,
|
|
44
|
-
useGenerate: () => useGenerate
|
|
46
|
+
useGenerate: () => useGenerate,
|
|
47
|
+
useHeyShift: () => useHeyShift
|
|
45
48
|
});
|
|
46
49
|
module.exports = __toCommonJS(index_exports);
|
|
47
50
|
|
|
@@ -821,6 +824,225 @@ function PageTransition({ children, className }) {
|
|
|
821
824
|
pathname
|
|
822
825
|
);
|
|
823
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
|
+
}
|
|
824
1046
|
// Annotate the CommonJS export names for ESM import in node:
|
|
825
1047
|
0 && (module.exports = {
|
|
826
1048
|
ACCENT_RGB,
|
|
@@ -832,7 +1054,9 @@ function PageTransition({ children, className }) {
|
|
|
832
1054
|
LiveStat,
|
|
833
1055
|
PageTransition,
|
|
834
1056
|
ScrollGenerateCard,
|
|
1057
|
+
ShiftBar,
|
|
835
1058
|
ShiftCard,
|
|
836
1059
|
TiltCard,
|
|
837
|
-
useGenerate
|
|
1060
|
+
useGenerate,
|
|
1061
|
+
useHeyShift
|
|
838
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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
// src/components/ShiftCard.tsx
|
|
2
4
|
import { motion } from "framer-motion";
|
|
3
5
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -790,6 +792,225 @@ function PageTransition({ children, className }) {
|
|
|
790
792
|
pathname
|
|
791
793
|
);
|
|
792
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
|
+
}
|
|
793
1014
|
export {
|
|
794
1015
|
ACCENT_RGB,
|
|
795
1016
|
CanvasLauncher,
|
|
@@ -800,7 +1021,9 @@ export {
|
|
|
800
1021
|
LiveStat,
|
|
801
1022
|
PageTransition,
|
|
802
1023
|
ScrollGenerateCard,
|
|
1024
|
+
ShiftBar,
|
|
803
1025
|
ShiftCard,
|
|
804
1026
|
TiltCard,
|
|
805
|
-
useGenerate
|
|
1027
|
+
useGenerate,
|
|
1028
|
+
useHeyShift
|
|
806
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,
|