Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f95efed
Added in some of the changes for momentary buttons.
johncrawley Dec 10, 2024
a867604
Fixed button input, but state does not progress to momentary paused
johncrawley Dec 12, 2024
d680fda
Removed MomentaryRecording state, momentary paused does not resume af…
johncrawley Dec 19, 2024
b164c0b
Fixed momentary pause button not showing after interrupt button pressed.
johncrawley Dec 20, 2024
28d35a0
Working on submitting transcriptions from momentary paused mode, and …
johncrawley Dec 28, 2024
73d23a9
Fixed momentary mode not engaging after interruption, but momentary m…
johncrawley Dec 30, 2024
6789193
Momentary paused state submits collected speech, but does not return …
johncrawley Jan 2, 2025
cf1804e
momentary paused icon (temp) returns after pi finishes speaking. Usin…
johncrawley Jan 9, 2025
cff3af4
Working momentary mode basic operation, but interrupts are currently …
johncrawley Jan 13, 2025
50539a1
Fixed bad state after interrupt button pressed while momentary mode i…
johncrawley Jan 14, 2025
4e828a6
Fixed transcript not submitting if momentary pause is triggered durin…
johncrawley Jan 16, 2025
5f1e688
Clean-up of redundant methods.
johncrawley Jan 17, 2025
c800d59
Cleaned up the button module.
johncrawley Jan 20, 2025
1fc35d4
Ensured that glow animation is restarted when momentary button is pre…
johncrawley Jan 21, 2025
d7ecc7b
Making sure animation stops in immersive mode with momentary paused, …
johncrawley Jan 22, 2025
78dc75e
Mouse-out from the call button now pauses momentary mode if it was ac…
johncrawley Jan 27, 2025
613d2f8
Renamed ButtonUpdater to ButtonHelper, have more methods in ButtonMod…
johncrawley Jan 27, 2025
5cd69d3
Fixed slow recording stop after momentary pause enabled and no speech…
johncrawley Jan 28, 2025
bf95ec2
Added prompt and tooltip changes for momentary states.
johncrawley Jan 28, 2025
e52d821
Added momentary mode interruption when the interrupt button is not di…
johncrawley Jan 29, 2025
341943b
Updated prompt to thinking after momentary pause after user has spoken.
johncrawley Jan 29, 2025
0cfbc82
Updated ButtonHelper to only configure listeners once, instead of eve…
johncrawley Feb 1, 2025
0914bd5
Working touch events and with cached icon creation, but regression wi…
johncrawley Feb 2, 2025
98cd386
Long-press and long-release button actions are now working as expecte…
johncrawley Feb 3, 2025
86849bc
Simplified the updateCallButton method signature.
johncrawley Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
"message": "End hands-free conversation",
"description": "The tooltip to display on the call button when a call is in progress."
},
"momentaryCallPaused": {
"message": "Hold to speak, or click to return to hands-free mode",
"description": "The tooltip to display on the call button when a call is in progress and momentary mode is paused."
},
"momentaryCallListening": {
"message": "Release when finished speaking",
"description": "The tooltip to display on the call button when a call is in progress and momentary mode is active."
},
"callInterruptible": {
"message": "Tap to interrupt",
"description": "The tooltip to display on the call button when a call is in progress and manual interruption is possible."
Expand Down Expand Up @@ -198,6 +206,16 @@
"example": "Pi"
}
}
},
"microphoneIsMuted": {
"message": "The microphone is muted...",
"description": "Message displayed when the 'momentary' mode is enabled but paused.",
"placeholders": {
"chatbot": {
"content": "$1",
"example": "Pi"
}
}
},
"toggleThemeToDarkMode": {
"message": "Switch to dark mode",
Expand Down
155 changes: 79 additions & 76 deletions src/ButtonModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import callIconSVG from "./icons/call.svg";
import callStartingIconSVG from "./icons/call-starting.svg";
import hangupIconSVG from "./icons/hangup.svg";
import interruptIconSVG from "./icons/interrupt.svg";
import momentaryPausedIconSVG from "./icons/momentary_paused.svg";
import momentaryListeningIconSVG from "./icons/momentary_listening.svg";
import hangupMincedIconSVG from "./icons/hangup-minced.svg";
import lockIconSVG from "./icons/lock.svg";
import unlockIconSVG from "./icons/unlock.svg";
Expand All @@ -23,6 +25,7 @@ import { IconModule } from "./icons/IconModule.ts";
import { ImmersionStateChecker } from "./ImmersionServiceLite.ts";
import { GlowColorUpdater } from "./buttons/GlowColorUpdater.js";
import { openSettings } from "./popup/popupopener.ts";
import { ButtonHelper } from "./buttons/ButtonHelper.js";

class ButtonModule {
/**
Expand All @@ -31,20 +34,17 @@ class ButtonModule {
*/
constructor(chatbot) {
this.icons = new IconModule();
this.buttonHelper = new ButtonHelper();
this.userPreferences = UserPreferenceModule.getInstance();
this.chatbot = chatbot;
this.immersionService = new ImmersionService(chatbot);
this.glowColorUpdater = new GlowColorUpdater();
this.sayPiActor = StateMachineService.actor; // the Say, Pi state machine
this.screenLockActor = StateMachineService.screenLockActor;
// Binding methods to the current instance
this.registerOtherEvents();

// track the frequency of bug #26
this.submissionsWithoutAnError = 0;

// track whether a call is active, so that new button instances can be initialized correctly
this.callIsActive = false;
}

registerOtherEvents() {
Expand Down Expand Up @@ -189,7 +189,7 @@ class ButtonModule {
button.id = id;
button.type = "button";
button.className = `saypi-control-button rounded-full bg-cream-550 enabled:hover:bg-cream-650 tooltip mini ${className}`;
button.setAttribute("aria-label", label);
this.setAriaLabelOf(button, label);

const svgElement = createSVGElement(icon);
button.appendChild(svgElement);
Expand All @@ -204,7 +204,7 @@ class ButtonModule {
createExitButton(container, position = 0) {
const button = this.createIconButton({
id: 'saypi-exit-button',
label: getMessage("exitImmersiveModeLong"),
label: "exitImmersiveModeLong",
icon: exitIconSVG,
onClick: () => ImmersionService.exitImmersiveMode(),
className: 'saypi-exit-button'
Expand All @@ -217,7 +217,7 @@ class ButtonModule {
createEnterButton(container, position = 0) {
const button = this.createIconButton({
id: 'saypi-enter-button',
label: getMessage("enterImmersiveModeLong"),
label: "enterImmersiveModeLong",
icon: maximizeIconSVG,
onClick: () => this.immersionService.enterImmersiveMode(),
className: 'saypi-enter-button'
Expand All @@ -234,9 +234,10 @@ class ButtonModule {
*/
createControlButton(options) {
const { shortLabel, longLabel = shortLabel, icon, onClick, className = '' } = options;

const button = createElement("a", {
className: `${className} maxi saypi-control-button tooltip flex h-16 w-16 flex-col items-center justify-center rounded-xl text-neutral-900 hover:bg-neutral-50-hover hover:text-neutral-900-hover active:bg-neutral-50-tap active:text-neutral-900-tap gap-0.5`,
ariaLabel: longLabel,
ariaLabel: getMessage(longLabel),
onclick: onClick,
});

Expand All @@ -245,7 +246,7 @@ class ButtonModule {

const labelDiv = createElement("div", {
className: "t-label",
textContent: shortLabel,
textContent: getMessage(shortLabel),
}, );
button.appendChild(labelDiv);

Expand All @@ -254,8 +255,8 @@ class ButtonModule {

createImmersiveModeButton(container, position = 0) {
const button = this.createControlButton({
shortLabel: getMessage("enterImmersiveModeShort"),
longLabel: getMessage("enterImmersiveModeLong"),
shortLabel: "enterImmersiveModeShort",
longLabel: "enterImmersiveModeLong",
icon: immersiveIconSVG,
onClick: () => this.immersionService.enterImmersiveMode(),
className: 'immersive-mode-button'
Expand All @@ -266,9 +267,8 @@ class ButtonModule {
}

createSettingsButton(container, position = 0) {
const label = getMessage("extensionSettings");
const button = this.createControlButton({
shortLabel: label,
shortLabel: "extensionSettings",
icon: settingsIconSVG,
onClick: () => openSettings(),
className: 'settings-button'
Expand All @@ -281,7 +281,7 @@ class ButtonModule {
createMiniSettingsButton(container, position = 0) {
const button = this.createIconButton({
id: 'saypi-settingsButton',
label: getMessage("extensionSettings"),
label: "extensionSettings",
icon: settingsIconSVG,
onClick: () => openSettings(),
className: 'settings-button'
Expand All @@ -297,30 +297,20 @@ class ButtonModule {
button.type = "button";
button.classList.add("call-button", "saypi-button", "tooltip");
button.classList.add(...this.chatbot.getExtraCallButtonClasses());
if (this.callIsActive) {
if (this.buttonHelper.isCallActive()) {
this.callActive(button);
} else {
this.callInactive(button);
}

addChild(container, button, position);
if (this.callIsActive) {
if (this.buttonHelper.isCallActive()) {
// if the call is active, start the glow animation once added to the DOM
AnimationModule.startAnimation("glow");
}
return button;
}

updateCallButtonColor(color) {
const callButton = document.getElementById("saypi-callButton");
// find first path element descendant of the call button's svg element child
const path = callButton?.querySelector("svg path");
if (path) {
// set the fill color of the path element
path.style.fill = color;
}
}

/**
*
* @param { isSpeech: number; notSpeech: number } probabilities
Expand All @@ -329,71 +319,80 @@ class ButtonModule {
this.glowColorUpdater.updateGlowColor(probabilities.isSpeech);
}

updateCallButton(callButton, svgIcon, label, onClick, isActive = false) {
if (!callButton) {
callButton = document.getElementById("saypi-callButton");
}
if (callButton) {
// Remove all existing child nodes
while (callButton.firstChild) {
callButton.removeChild(callButton.firstChild);
}

const svgElement = createSVGElement(svgIcon);
callButton.appendChild(svgElement);

callButton.setAttribute("aria-label", label);
callButton.onclick = onClick;
callButton.classList.toggle("active", isActive);
}
this.callIsActive = isActive;
}

callStarting(callButton) {
const label = getMessage("callStarting");
this.updateCallButton(callButton, callStartingIconSVG, label, () =>
this.sayPiActor.send("saypi:hangup")
);
this.buttonHelper.updateCallButton({
button: callButton,
icon: callStartingIconSVG,
label: "callStarting",
clickEventName: "saypi:hangup",
});
}

callActive(callButton) {
const label = getMessage("callInProgress");
this.updateCallButton(
callButton,
hangupIconSVG,
label,
() => this.sayPiActor.send("saypi:hangup"),
true
);
this.buttonHelper.updateCallButton({
button: callButton,
icon: hangupIconSVG,
label: "callInProgress",
clickEventName: "saypi:hangup",
longPressEventName: "saypi:momentaryListen",
isCallActive: true,
});
}

callMomentary(callButton) {
this.buttonHelper.updateCallButton({
button: callButton,
icon: momentaryListeningIconSVG,
label: "momentaryCallListening",
clickEventName: "saypi:momentaryStop",
longReleaseEventName: "saypi:momentaryPause",

isCallActive: true,
});
}

pauseMomentary(callButton) {
this.buttonHelper.updateCallButton({
button: callButton,
icon: momentaryPausedIconSVG,
label: "momentaryCallPaused",
clickEventName: "saypi:momentaryStop",
longPressEventName: "saypi:momentaryListen",
isCallActive: true,
});
}

callInterruptible(callButton) {
const handsFreeInterruptEnabled =
this.userPreferences.getCachedAllowInterruptions();

if (!handsFreeInterruptEnabled) {
const label = getMessage("callInterruptible");
this.updateCallButton(
callButton,
interruptIconSVG,
label,
() => {
this.sayPiActor.send("saypi:interrupt");
},
true
);
this.buttonHelper.updateCallButton({
button: callButton,
icon: interruptIconSVG,
label: "callInterruptible",
clickEventName: "saypi:interrupt",
isCallActive: true,
});
}
}

callInactive(callButton) {
const label = getMessage("callNotStarted", this.chatbot.getName());
this.updateCallButton(callButton, callIconSVG, label, () =>
this.sayPiActor.send("saypi:call")
);
this.buttonHelper.updateCallButton({
button: callButton,
icon: callIconSVG,
label: "callNotStarted",
labelArgument: this.chatbot.getName(),
clickEventName: "saypi:call",
});
}

callError(callButton) {
const label = getMessage("callError");
this.updateCallButton(callButton, hangupMincedIconSVG, label, null);
this.buttonHelper.updateCallButton({
button: callButton,
icon: hangupMincedIconSVG,
label: "callError",
});
}

disableCallButton() {
Expand Down Expand Up @@ -435,14 +434,18 @@ class ButtonModule {
return button;
}

setAriaLabelOf(button, labelName) {
const label = getMessage(labelName);
button.setAttribute("aria-label", label);
}

createUnlockButton(container) {
const label = getMessage("unlockButton");
const button = document.createElement("button");
button.id = "saypi-unlockButton";
button.type = "button";
button.className =
"lock-button saypi-control-button rounded-full bg-cream-550 enabled:hover:bg-cream-650 tooltip";
button.setAttribute("aria-label", label);
this.setAriaLabelOf(button, "unlockButton");
button.appendChild(createSVGElement(unlockIconSVG));
if (container) {
container.appendChild(button);
Expand Down
16 changes: 15 additions & 1 deletion src/FullscreenModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ import { isMobileDevice } from "./UserAgentModule";
const focusActor = interpret(focusMachine);
const tickInterval = 1000;
var ticker: NodeJS.Timeout;
const userInputEvents = ["mousemove", "click", "keypress"];
const userInputEvents = ["mousemove", "keypress"];

function handleUserInput() {
focusActor.send({ type: "blur" });
}

function handleMouseDown() {
if(document.fullscreenEnabled) {
focusActor.send({ type: "pause" });
}
}

function handleMouseUp() {
if(document.fullscreenEnabled) {
focusActor.send({ type: "resume" });
}
}

function startFocusModeListener() {
focusActor.start();
ticker = setInterval(() => {
Expand All @@ -23,6 +35,8 @@ function startFocusModeListener() {
for (const event of userInputEvents) {
document.addEventListener(event, handleUserInput);
}
document.addEventListener("mousedown", handleMouseDown);
document.addEventListener("mouseup", handleMouseUp);
}

function stopFocusModeListener() {
Expand Down
4 changes: 4 additions & 0 deletions src/audio/AudioModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ export default class AudioModule {
// soft stop recording
inputActor.send("stopRequested");
});
EventBus.on("audio:quickStopRecording", function (e) {
// soft stop recording but quickly
inputActor.send("quickStopRequested");
});
// audio input (recording) events (pass media recorder events -> audio input machine actor)
EventBus.on("audio:dataavailable", (detail) => {
inputActor.send({ type: "dataAvailable", ...detail });
Expand Down
Loading