File size: 4,620 Bytes
f8b5d42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { createContext, useContext, useEffect, useState } from "react";
import System from "@/models/system";
import Appearance from "@/models/appearance";

const ASSISTANT_MESSAGE_COMPLETE_EVENT = "ASSISTANT_MESSAGE_COMPLETE_EVENT";
const TTSProviderContext = createContext();

/**
 * This component is used to provide the TTS provider context to the application.
 *
 * TODO: This context provider simply wraps around the System.keys() call to get the TTS provider settings.
 * However, we use .keys() in a ton of places and it might make more sense to make a generalized hook that
 * can be used anywhere we need to get _any_ setting from the System by just grabbing keys() and reusing it
 * as a hook where needed.
 *
 * For now, since TTSButtons are rendered on every message, we can save a ton of requests by just using this
 * hook where for now so we can recycle the TTS settings in the chat container.
 */
export function TTSProvider({ children }) {
  const [settings, setSettings] = useState({});
  const [provider, setProvider] = useState("native");
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function getSettings() {
      const _settings = await System.keys();
      setProvider(_settings?.TextToSpeechProvider ?? "native");
      setSettings(_settings);
      setLoading(false);
    }
    getSettings();
  }, []);

  return (
    <TTSProviderContext.Provider
      value={{
        settings,
        provider,
        loading,
      }}
    >
      {children}
    </TTSProviderContext.Provider>
  );
}

/**
 * This hook is used to get the TTS provider settings easily without
 * having to refetch the settings from the System.keys() call each component mount.
 *
 * @returns {{settings: {TTSPiperTTSVoiceModel: string|null}, provider: string, loading: boolean}} The TTS provider settings.
 */
export function useTTSProvider() {
  const context = useContext(TTSProviderContext);
  if (!context)
    throw new Error("useTTSProvider must be used within a TTSProvider");
  return context;
}

/**
 * This function will emit the ASSISTANT_MESSAGE_COMPLETE_EVENT event.
 *
 * This event is used to notify the TTSProvider that a message has been fully generated and that the TTS response
 * should be played if the user setting is enabled.
 *
 * @param {string} chatId - The chatId of the message that has been fully generated.
 */
export function emitAssistantMessageCompleteEvent(chatId) {
  window.dispatchEvent(
    new CustomEvent(ASSISTANT_MESSAGE_COMPLETE_EVENT, { detail: { chatId } })
  );
}

/**
 * This hook will establish a listener for the ASSISTANT_MESSAGE_COMPLETE_EVENT event.
 * When the event is triggered, the hook will attempt to play the TTS response for the given chatId.
 * It will attempt to play the TTS response for the given chatId until it is successful or the maximum number of attempts
 * is reached.
 *
 * This is accomplished by looking for a button with the data-auto-play-chat-id attribute that matches the chatId.
 */
export function useWatchForAutoPlayAssistantTTSResponse() {
  const autoPlayAssistantTtsResponse = Appearance.get(
    "autoPlayAssistantTtsResponse"
  );

  function handleAutoPlayTTSEvent(event) {
    let autoPlayAttempts = 0;
    const { chatId } = event.detail;

    /**
     * Attempt to play the TTS response for the given chatId.
     * This is a recursive function that will attempt to play the TTS response
     * for the given chatId until it is successful or the maximum number of attempts
     * is reached.
     * @returns {boolean} true if the TTS response was played, false otherwise.
     */
    function attemptToPlay() {
      const playBtn = document.querySelector(
        `[data-auto-play-chat-id="${chatId}"]`
      );
      if (!playBtn) {
        autoPlayAttempts++;
        if (autoPlayAttempts > 3) return false;
        setTimeout(() => {
          attemptToPlay();
        }, 1000 * autoPlayAttempts);
        return false;
      }
      playBtn.click();
      return true;
    }
    setTimeout(() => {
      attemptToPlay();
    }, 800);
  }

  // Only bother to listen for these events if the user has autoPlayAssistantTtsResponse
  // setting enabled.
  useEffect(() => {
    if (autoPlayAssistantTtsResponse) {
      window.addEventListener(
        ASSISTANT_MESSAGE_COMPLETE_EVENT,
        handleAutoPlayTTSEvent
      );
      return () => {
        window.removeEventListener(
          ASSISTANT_MESSAGE_COMPLETE_EVENT,
          handleAutoPlayTTSEvent
        );
      };
    } else {
      console.log("Assistant TTS auto-play is disabled");
    }
  }, [autoPlayAssistantTtsResponse]);
}