@rimori/client 1.3.1 → 1.4.3
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/.prettierignore +35 -0
- package/README.md +77 -71
- package/dist/cli/scripts/init/dev-registration.d.ts +1 -1
- package/dist/cli/scripts/init/dev-registration.js +4 -4
- package/dist/cli/scripts/init/main.js +1 -1
- package/dist/cli/scripts/init/package-setup.d.ts +1 -1
- package/dist/cli/scripts/init/package-setup.js +3 -3
- package/dist/cli/scripts/init/router-transformer.js +19 -12
- package/dist/cli/scripts/init/vite-config.d.ts +2 -2
- package/dist/cli/scripts/init/vite-config.js +2 -2
- package/dist/cli/scripts/release/release-config-upload.js +9 -9
- package/dist/cli/scripts/release/release-db-update.d.ts +1 -1
- package/dist/cli/scripts/release/release-db-update.js +9 -9
- package/dist/cli/scripts/release/release-file-upload.js +2 -2
- package/dist/cli/scripts/release/release.js +2 -2
- package/dist/cli/types/DatabaseTypes.d.ts +2 -2
- package/dist/components/CRUDModal.d.ts +1 -1
- package/dist/components/CRUDModal.js +3 -3
- package/dist/components/MarkdownEditor.js +16 -16
- package/dist/components/Spinner.js +2 -2
- package/dist/components/ai/Assistant.js +7 -8
- package/dist/components/ai/Avatar.d.ts +2 -2
- package/dist/components/ai/Avatar.js +14 -7
- package/dist/components/ai/EmbeddedAssistent/AudioInputField.js +5 -6
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +1 -1
- package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +1 -2
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +1 -2
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +4 -2
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +1 -1
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +2 -3
- package/dist/components/audio/Playbutton.js +10 -7
- package/dist/components/components/ContextMenu.d.ts +1 -1
- package/dist/components/components/ContextMenu.js +19 -16
- package/dist/components.d.ts +10 -10
- package/dist/components.js +10 -10
- package/dist/core/controller/AIController.d.ts +2 -2
- package/dist/core/controller/AIController.js +20 -18
- package/dist/core/controller/ExerciseController.d.ts +52 -0
- package/dist/core/controller/ExerciseController.js +73 -0
- package/dist/core/controller/ObjectController.js +5 -5
- package/dist/core/controller/SettingsController.d.ts +22 -7
- package/dist/core/controller/SettingsController.js +73 -8
- package/dist/core/controller/SharedContentController.d.ts +3 -3
- package/dist/core/controller/SharedContentController.js +38 -20
- package/dist/core/controller/VoiceController.js +6 -4
- package/dist/core/core.d.ts +15 -14
- package/dist/core/core.js +7 -7
- package/dist/fromRimori/EventBus.js +23 -23
- package/dist/fromRimori/PluginTypes.d.ts +4 -4
- package/dist/hooks/UseChatHook.d.ts +3 -3
- package/dist/hooks/UseChatHook.js +9 -3
- package/dist/index.d.ts +10 -10
- package/dist/index.js +9 -9
- package/dist/plugin/AccomplishmentHandler.d.ts +5 -5
- package/dist/plugin/AccomplishmentHandler.js +31 -27
- package/dist/plugin/AudioController.d.ts +1 -1
- package/dist/plugin/AudioController.js +6 -6
- package/dist/plugin/Logger.d.ts +5 -0
- package/dist/plugin/Logger.js +65 -13
- package/dist/plugin/PluginController.d.ts +7 -1
- package/dist/plugin/PluginController.js +32 -27
- package/dist/plugin/RimoriClient.d.ts +39 -14
- package/dist/plugin/RimoriClient.js +60 -31
- package/dist/plugin/StandaloneClient.d.ts +1 -1
- package/dist/plugin/StandaloneClient.js +35 -16
- package/dist/plugin/ThemeSetter.js +4 -4
- package/dist/providers/PluginProvider.js +44 -14
- package/dist/utils/Language.js +57 -57
- package/dist/utils/PluginUtils.js +3 -3
- package/dist/utils/difficultyConverter.d.ts +1 -1
- package/dist/utils/difficultyConverter.js +1 -1
- package/dist/utils/endpoint.js +2 -2
- package/dist/worker/WorkerSetup.d.ts +1 -1
- package/dist/worker/WorkerSetup.js +6 -6
- package/eslint.config.js +53 -0
- package/example/docs/devdocs.md +50 -40
- package/example/docs/overview.md +1 -1
- package/example/docs/userdocs.md +4 -1
- package/example/rimori.config.ts +51 -49
- package/example/worker/vite.config.ts +3 -3
- package/example/worker/worker.ts +2 -2
- package/package.json +17 -4
- package/prettier.config.js +8 -0
- package/src/cli/scripts/init/dev-registration.ts +5 -8
- package/src/cli/scripts/init/env-setup.ts +1 -1
- package/src/cli/scripts/init/file-operations.ts +1 -1
- package/src/cli/scripts/init/html-cleaner.ts +2 -5
- package/src/cli/scripts/init/main.ts +16 -13
- package/src/cli/scripts/init/package-setup.ts +11 -15
- package/src/cli/scripts/init/router-transformer.ts +40 -37
- package/src/cli/scripts/init/tailwind-config.ts +17 -26
- package/src/cli/scripts/init/vite-config.ts +3 -3
- package/src/cli/scripts/release/release-config-upload.ts +11 -11
- package/src/cli/scripts/release/release-db-update.ts +12 -12
- package/src/cli/scripts/release/release-file-upload.ts +3 -3
- package/src/cli/scripts/release/release.ts +4 -4
- package/src/cli/types/DatabaseTypes.ts +7 -8
- package/src/components/CRUDModal.tsx +64 -48
- package/src/components/MarkdownEditor.tsx +58 -27
- package/src/components/Spinner.tsx +24 -17
- package/src/components/ai/Assistant.tsx +70 -70
- package/src/components/ai/Avatar.tsx +20 -16
- package/src/components/ai/EmbeddedAssistent/AudioInputField.tsx +63 -54
- package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +14 -5
- package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +75 -74
- package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +177 -178
- package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +109 -94
- package/src/components/ai/utils.ts +4 -4
- package/src/components/audio/Playbutton.tsx +101 -93
- package/src/components/components/ContextMenu.tsx +47 -35
- package/src/components.ts +10 -10
- package/src/core/controller/AIController.ts +62 -50
- package/src/core/controller/ExerciseController.ts +98 -0
- package/src/core/controller/ObjectController.ts +15 -10
- package/src/core/controller/SettingsController.ts +89 -16
- package/src/core/controller/SharedContentController.ts +80 -44
- package/src/core/controller/VoiceController.ts +10 -8
- package/src/core/core.ts +15 -15
- package/src/fromRimori/EventBus.ts +76 -47
- package/src/fromRimori/PluginTypes.ts +26 -17
- package/src/fromRimori/readme.md +2 -2
- package/src/hooks/UseChatHook.ts +25 -15
- package/src/index.ts +10 -10
- package/src/plugin/AccomplishmentHandler.ts +53 -35
- package/src/plugin/AudioController.ts +18 -12
- package/src/plugin/Logger.ts +77 -19
- package/src/plugin/PluginController.ts +60 -44
- package/src/plugin/RimoriClient.ts +133 -69
- package/src/plugin/StandaloneClient.ts +51 -24
- package/src/plugin/ThemeSetter.ts +5 -5
- package/src/providers/PluginProvider.tsx +90 -36
- package/src/style.scss +3 -3
- package/src/utils/Language.ts +58 -58
- package/src/utils/PluginUtils.ts +16 -20
- package/src/utils/difficultyConverter.ts +2 -2
- package/src/utils/endpoint.ts +3 -2
- package/src/worker/WorkerSetup.ts +8 -9
- package/tsconfig.json +2 -4
- package/dist/components/LoggerExample.d.ts +0 -6
- package/dist/components/LoggerExample.js +0 -79
- package/dist/core/controller/AudioController.d.ts +0 -0
- package/dist/core/controller/AudioController.js +0 -1
- package/dist/hooks/UseLogger.d.ts +0 -30
- package/dist/hooks/UseLogger.js +0 -122
- package/dist/plugin/LoggerExample.d.ts +0 -16
- package/dist/plugin/LoggerExample.js +0 -140
- package/dist/utils/audioFormats.d.ts +0 -26
- package/dist/utils/audioFormats.js +0 -67
|
@@ -13,102 +13,117 @@ interface Props {
|
|
|
13
13
|
onVoiceRecorded: (message: string) => void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export const VoiceRecorder = forwardRef(
|
|
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
|
-
// Play the recorded audio from the Blob
|
|
54
|
-
// const blobUrl = URL.createObjectURL(audioResult.recording);
|
|
55
|
-
// const audioRef = new Audio(blobUrl);
|
|
56
|
-
// audioRef.onended = () => URL.revokeObjectURL(blobUrl);
|
|
57
|
-
// audioRef.play().catch((e) => console.error('Playback error:', e));
|
|
58
|
-
|
|
59
|
-
// console.log("audioBlob: ", audioResult.recording);
|
|
60
|
-
const text = await ai.getTextFromVoice(audioResult.recording);
|
|
61
|
-
// console.log("stt result", text);
|
|
62
|
-
// throw new Error("test");
|
|
63
|
-
setInternalIsProcessing(false);
|
|
64
|
-
onVoiceRecordedRef.current(text);
|
|
65
|
-
}
|
|
66
|
-
} catch (error) {
|
|
67
|
-
console.error('Failed to stop recording:', error);
|
|
68
|
-
} finally {
|
|
69
|
-
setIsRecording(false);
|
|
70
|
-
onRecordingStatusChange(false);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
useImperativeHandle(ref, () => ({
|
|
75
|
-
startRecording,
|
|
76
|
-
stopRecording,
|
|
77
|
-
}));
|
|
78
|
-
|
|
79
|
-
// push to talk feature
|
|
80
|
-
const spacePressedRef = useRef(false);
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (!enablePushToTalk) return;
|
|
84
|
-
|
|
85
|
-
const handleKeyDown = async (event: KeyboardEvent) => {
|
|
86
|
-
if (event.code === 'Space' && !spacePressedRef.current) {
|
|
87
|
-
spacePressedRef.current = true;
|
|
88
|
-
await startRecording();
|
|
16
|
+
export const VoiceRecorder = forwardRef(
|
|
17
|
+
(
|
|
18
|
+
{
|
|
19
|
+
onVoiceRecorded,
|
|
20
|
+
iconSize,
|
|
21
|
+
className,
|
|
22
|
+
disabled,
|
|
23
|
+
loading,
|
|
24
|
+
onRecordingStatusChange,
|
|
25
|
+
enablePushToTalk = false,
|
|
26
|
+
}: Props,
|
|
27
|
+
ref,
|
|
28
|
+
) => {
|
|
29
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
30
|
+
const [internalIsProcessing, setInternalIsProcessing] = useState(false);
|
|
31
|
+
const audioControllerRef = useRef<AudioController | null>(null);
|
|
32
|
+
const { ai, plugin } = useRimori();
|
|
33
|
+
|
|
34
|
+
// Ref for latest onVoiceRecorded callback
|
|
35
|
+
const onVoiceRecordedRef = useRef(onVoiceRecorded);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
onVoiceRecordedRef.current = onVoiceRecorded;
|
|
38
|
+
}, [onVoiceRecorded]);
|
|
39
|
+
|
|
40
|
+
const startRecording = async () => {
|
|
41
|
+
try {
|
|
42
|
+
if (!audioControllerRef.current) {
|
|
43
|
+
audioControllerRef.current = new AudioController(plugin.pluginId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await audioControllerRef.current.startRecording();
|
|
47
|
+
setIsRecording(true);
|
|
48
|
+
onRecordingStatusChange(true);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Failed to start recording:', error);
|
|
51
|
+
// Handle permission denied or other errors
|
|
89
52
|
}
|
|
90
53
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
54
|
+
|
|
55
|
+
const stopRecording = async () => {
|
|
56
|
+
try {
|
|
57
|
+
if (audioControllerRef.current && isRecording) {
|
|
58
|
+
const audioResult = await audioControllerRef.current.stopRecording();
|
|
59
|
+
// console.log("audioResult: ", audioResult);
|
|
60
|
+
|
|
61
|
+
setInternalIsProcessing(true);
|
|
62
|
+
|
|
63
|
+
// Play the recorded audio from the Blob
|
|
64
|
+
// const blobUrl = URL.createObjectURL(audioResult.recording);
|
|
65
|
+
// const audioRef = new Audio(blobUrl);
|
|
66
|
+
// audioRef.onended = () => URL.revokeObjectURL(blobUrl);
|
|
67
|
+
// audioRef.play().catch((e) => console.error('Playback error:', e));
|
|
68
|
+
|
|
69
|
+
// console.log("audioBlob: ", audioResult.recording);
|
|
70
|
+
const text = await ai.getTextFromVoice(audioResult.recording);
|
|
71
|
+
// console.log("stt result", text);
|
|
72
|
+
// throw new Error("test");
|
|
73
|
+
setInternalIsProcessing(false);
|
|
74
|
+
onVoiceRecordedRef.current(text);
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to stop recording:', error);
|
|
78
|
+
} finally {
|
|
79
|
+
setIsRecording(false);
|
|
80
|
+
onRecordingStatusChange(false);
|
|
95
81
|
}
|
|
96
82
|
};
|
|
97
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
98
|
-
window.addEventListener('keyup', handleKeyUp);
|
|
99
|
-
return () => {
|
|
100
|
-
window.removeEventListener('keydown', handleKeyDown);
|
|
101
|
-
window.removeEventListener('keyup', handleKeyUp);
|
|
102
|
-
};
|
|
103
|
-
}, [enablePushToTalk]);
|
|
104
83
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
84
|
+
useImperativeHandle(ref, () => ({
|
|
85
|
+
startRecording,
|
|
86
|
+
stopRecording,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// push to talk feature
|
|
90
|
+
const spacePressedRef = useRef(false);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!enablePushToTalk) return;
|
|
94
|
+
|
|
95
|
+
const handleKeyDown = async (event: KeyboardEvent) => {
|
|
96
|
+
if (event.code === 'Space' && !spacePressedRef.current) {
|
|
97
|
+
spacePressedRef.current = true;
|
|
98
|
+
await startRecording();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
102
|
+
if (event.code === 'Space' && spacePressedRef.current) {
|
|
103
|
+
spacePressedRef.current = false;
|
|
104
|
+
stopRecording();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
108
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
109
|
+
return () => {
|
|
110
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
111
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
112
|
+
};
|
|
113
|
+
}, [enablePushToTalk]);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<button
|
|
117
|
+
className={'flex flex-row justify-center items-center rounded-full mx-auto disabled:opacity-50 ' + className}
|
|
118
|
+
onClick={isRecording ? stopRecording : startRecording}
|
|
119
|
+
disabled={disabled || loading || internalIsProcessing}
|
|
120
|
+
>
|
|
121
|
+
{loading || internalIsProcessing ? (
|
|
122
|
+
<FaSpinner className="animate-spin" />
|
|
123
|
+
) : (
|
|
124
|
+
<FaMicrophone size={iconSize} className={isRecording ? 'text-red-600' : ''} />
|
|
125
|
+
)}
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
@@ -8,14 +8,14 @@ export function getFirstMessages(instructions: FirstMessages): any[] {
|
|
|
8
8
|
const messages = [];
|
|
9
9
|
|
|
10
10
|
if (instructions.instructions) {
|
|
11
|
-
|
|
11
|
+
messages.push({ id: '1', role: 'system', content: instructions.instructions });
|
|
12
12
|
}
|
|
13
13
|
if (instructions.userMessage) {
|
|
14
|
-
|
|
14
|
+
messages.push({ id: '2', role: 'user', content: instructions.userMessage });
|
|
15
15
|
}
|
|
16
16
|
if (instructions.assistantMessage) {
|
|
17
|
-
|
|
17
|
+
messages.push({ id: '3', role: 'assistant', content: instructions.assistantMessage });
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
return messages;
|
|
21
|
-
}
|
|
21
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { FaPlayCircle, FaStopCircle } from
|
|
3
|
-
import { useRimori } from
|
|
2
|
+
import { FaPlayCircle, FaStopCircle } from 'react-icons/fa';
|
|
3
|
+
import { useRimori } from '../../providers/PluginProvider';
|
|
4
4
|
import { Spinner } from '../Spinner';
|
|
5
5
|
import { EventBus } from '../../fromRimori/EventBus';
|
|
6
6
|
|
|
7
7
|
type AudioPlayerProps = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
text: string;
|
|
9
|
+
voice?: string;
|
|
10
|
+
language?: string;
|
|
11
|
+
hide?: boolean;
|
|
12
|
+
playOnMount?: boolean;
|
|
13
|
+
initialSpeed?: number;
|
|
14
|
+
enableSpeedAdjustment?: boolean;
|
|
15
|
+
playListenerEvent?: string;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
export const AudioPlayOptions = [0.8, 0.9, 1.0, 1.1, 1.2, 1.5];
|
|
@@ -21,98 +21,106 @@ export type AudioPlayOptionType = 0.8 | 0.9 | 1.0 | 1.1 | 1.2 | 1.5;
|
|
|
21
21
|
let isFetchingAudio = false;
|
|
22
22
|
|
|
23
23
|
export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
text,
|
|
25
|
+
voice,
|
|
26
|
+
language,
|
|
27
|
+
hide,
|
|
28
|
+
playListenerEvent,
|
|
29
|
+
initialSpeed = 1.0,
|
|
30
|
+
playOnMount = false,
|
|
31
|
+
enableSpeedAdjustment = false,
|
|
32
32
|
}) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
|
34
|
+
const [speed, setSpeed] = useState(initialSpeed);
|
|
35
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
36
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
37
|
+
const { ai } = useRimori();
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!playListenerEvent) return;
|
|
41
|
+
EventBus.on(playListenerEvent, () => togglePlayback());
|
|
42
|
+
}, [playListenerEvent]);
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
}, [text]);
|
|
50
|
-
|
|
51
|
-
// Function to generate audio from text using API
|
|
52
|
-
const generateAudio = async () => {
|
|
53
|
-
setIsLoading(true);
|
|
54
|
-
|
|
55
|
-
const blob = await ai.getVoice(text, voice || (language ? "aws_default" : "openai_alloy"), 1, language);
|
|
56
|
-
setAudioUrl(URL.createObjectURL(blob));
|
|
57
|
-
setIsLoading(false);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
audioUrl && setAudioUrl(null);
|
|
46
|
+
return () => {
|
|
47
|
+
audioUrl && URL.revokeObjectURL(audioUrl);
|
|
58
48
|
};
|
|
49
|
+
}, [text]);
|
|
59
50
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const audio = new Audio(audioUrl);
|
|
64
|
-
audio.playbackRate = speed;
|
|
65
|
-
audio.play().then(() => {
|
|
66
|
-
audio.onended = () => {
|
|
67
|
-
setIsPlaying(false);
|
|
68
|
-
isFetchingAudio = false;
|
|
69
|
-
};
|
|
70
|
-
}).catch(e => {
|
|
71
|
-
console.warn("Error playing audio:", e);
|
|
72
|
-
setIsPlaying(false);
|
|
73
|
-
});
|
|
51
|
+
// Function to generate audio from text using API
|
|
52
|
+
const generateAudio = async () => {
|
|
53
|
+
setIsLoading(true);
|
|
74
54
|
|
|
75
|
-
|
|
76
|
-
|
|
55
|
+
const blob = await ai.getVoice(text, voice || (language ? 'aws_default' : 'openai_alloy'), 1, language);
|
|
56
|
+
setAudioUrl(URL.createObjectURL(blob));
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Effect to play audio when audioUrl changes and play state is true
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!audioUrl || !isPlaying) return;
|
|
63
|
+
const audio = new Audio(audioUrl);
|
|
64
|
+
audio.playbackRate = speed;
|
|
65
|
+
audio
|
|
66
|
+
.play()
|
|
67
|
+
.then(() => {
|
|
68
|
+
audio.onended = () => {
|
|
69
|
+
setIsPlaying(false);
|
|
70
|
+
isFetchingAudio = false;
|
|
77
71
|
};
|
|
78
|
-
|
|
72
|
+
})
|
|
73
|
+
.catch((e) => {
|
|
74
|
+
console.warn('Error playing audio:', e);
|
|
75
|
+
setIsPlaying(false);
|
|
76
|
+
});
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
generateAudio().then(() => setIsPlaying(true));
|
|
83
|
-
} else {
|
|
84
|
-
setIsPlaying((prev) => !prev);
|
|
85
|
-
}
|
|
78
|
+
return () => {
|
|
79
|
+
audio.pause();
|
|
86
80
|
};
|
|
81
|
+
}, [audioUrl, isPlaying, speed]);
|
|
82
|
+
|
|
83
|
+
const togglePlayback = () => {
|
|
84
|
+
if (!isPlaying && !audioUrl) {
|
|
85
|
+
generateAudio().then(() => setIsPlaying(true));
|
|
86
|
+
} else {
|
|
87
|
+
setIsPlaying((prev) => !prev);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!playOnMount || isFetchingAudio) return;
|
|
93
|
+
isFetchingAudio = true;
|
|
94
|
+
// console.log("playOnMount", playOnMount);
|
|
95
|
+
togglePlayback();
|
|
96
|
+
}, [playOnMount]);
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
98
|
+
return (
|
|
99
|
+
<div className="group relative">
|
|
100
|
+
<div className="flex flex-row items-end">
|
|
101
|
+
{!hide && (
|
|
102
|
+
<button className="text-gray-500" onClick={togglePlayback} disabled={isLoading}>
|
|
103
|
+
{isLoading ? <Spinner /> : isPlaying ? <FaStopCircle size={'25px'} /> : <FaPlayCircle size={'25px'} />}
|
|
104
|
+
</button>
|
|
105
|
+
)}
|
|
106
|
+
{enableSpeedAdjustment && (
|
|
107
|
+
<div className="ml-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-row text-sm text-gray-500">
|
|
108
|
+
<span className="pr-1">Speed: </span>
|
|
109
|
+
<select
|
|
110
|
+
value={speed}
|
|
111
|
+
className="appearance-none cursor-pointer pr-0 p-0 rounded shadow leading-tight focus:outline-none focus:bg-gray-800 focus:ring bg-transparent border-0"
|
|
112
|
+
onChange={(e) => setSpeed(parseFloat(e.target.value))}
|
|
113
|
+
disabled={isLoading}
|
|
114
|
+
>
|
|
115
|
+
{AudioPlayOptions.map((s) => (
|
|
116
|
+
<option key={s} value={s}>
|
|
117
|
+
{s}
|
|
118
|
+
</option>
|
|
119
|
+
))}
|
|
120
|
+
</select>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
118
126
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from
|
|
2
|
-
import { EventBus } from
|
|
3
|
-
import { RimoriClient } from
|
|
4
|
-
import { MenuEntry } from
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { EventBus } from '../../fromRimori/EventBus';
|
|
3
|
+
import { RimoriClient } from '../../plugin/RimoriClient';
|
|
4
|
+
import { MenuEntry } from '../../fromRimori/PluginTypes';
|
|
5
5
|
|
|
6
6
|
export interface Position {
|
|
7
|
-
x: number
|
|
8
|
-
y: number
|
|
9
|
-
text?: string
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
text?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
@@ -35,7 +35,7 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
35
35
|
const rect = range.getBoundingClientRect();
|
|
36
36
|
|
|
37
37
|
// Center horizontally over the selected text, accounting for menu width
|
|
38
|
-
const centerX = rect.left +
|
|
38
|
+
const centerX = rect.left + rect.width / 2 - menuWidth / 2;
|
|
39
39
|
|
|
40
40
|
// Position 12px below where the text ends vertically
|
|
41
41
|
const textEndY = rect.bottom + 12;
|
|
@@ -44,12 +44,14 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
useEffect(() => {
|
|
47
|
-
const actions = client.plugin
|
|
47
|
+
const actions = client.plugin
|
|
48
|
+
.getPluginInfo()
|
|
49
|
+
.installedPlugins.flatMap((p) => p.context_menu_actions)
|
|
50
|
+
.filter(Boolean);
|
|
48
51
|
setActions(actions);
|
|
49
52
|
setOpenOnTextSelect(client.plugin.getUserInfo().context_menu_on_select);
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
EventBus.on<{ actions: MenuEntry[] }>("global.contextMenu.createActions", ({ data }) => {
|
|
54
|
+
EventBus.on<{ actions: MenuEntry[] }>('global.contextMenu.createActions', ({ data }) => {
|
|
53
55
|
setActions([...data.actions, ...actions]);
|
|
54
56
|
});
|
|
55
57
|
}, []);
|
|
@@ -84,10 +86,9 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
84
86
|
|
|
85
87
|
// Prevent context menu on textarea or text input selection
|
|
86
88
|
const target = e.target as HTMLElement;
|
|
87
|
-
const isTextInput =
|
|
88
|
-
|
|
89
|
-
(target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text')
|
|
90
|
-
);
|
|
89
|
+
const isTextInput =
|
|
90
|
+
target &&
|
|
91
|
+
(target.tagName === 'TEXTAREA' || (target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text'));
|
|
91
92
|
if (isTextInput) {
|
|
92
93
|
setIsOpen(false);
|
|
93
94
|
return;
|
|
@@ -123,16 +124,16 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
123
124
|
}
|
|
124
125
|
};
|
|
125
126
|
|
|
126
|
-
document.addEventListener(
|
|
127
|
-
window.addEventListener(
|
|
128
|
-
document.addEventListener(
|
|
129
|
-
document.addEventListener(
|
|
127
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
128
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
129
|
+
document.addEventListener('contextmenu', handleMouseUp);
|
|
130
|
+
document.addEventListener('selectionchange', handleSelectionChange);
|
|
130
131
|
|
|
131
132
|
return () => {
|
|
132
|
-
document.removeEventListener(
|
|
133
|
-
window.removeEventListener(
|
|
134
|
-
document.removeEventListener(
|
|
135
|
-
document.removeEventListener(
|
|
133
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
134
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
135
|
+
document.removeEventListener('contextmenu', handleMouseUp);
|
|
136
|
+
document.removeEventListener('selectionchange', handleSelectionChange);
|
|
136
137
|
};
|
|
137
138
|
}, [openOnTextSelect, isOpen, position.text]);
|
|
138
139
|
|
|
@@ -144,24 +145,35 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
144
145
|
<div
|
|
145
146
|
ref={menuRef}
|
|
146
147
|
className="fixed bg-gray-400 dark:bg-gray-700 shadow-lg border border-gray-400 rounded-md overflow-hidden dark:text-white z-50"
|
|
147
|
-
style={{ top: position.y, left: position.x }}
|
|
148
|
+
style={{ top: position.y, left: position.x }}
|
|
149
|
+
>
|
|
148
150
|
{actions.map((action, index) => (
|
|
149
|
-
<MenuEntryItem
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
<MenuEntryItem
|
|
152
|
+
key={index}
|
|
153
|
+
icon={action.icon}
|
|
154
|
+
text={action.text}
|
|
155
|
+
onClick={() => {
|
|
156
|
+
setIsOpen(false);
|
|
157
|
+
window.getSelection()?.removeAllRanges();
|
|
158
|
+
client.event.emitSidebarAction(action.plugin_id, action.action_key, position.text);
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
154
161
|
))}
|
|
155
162
|
</div>
|
|
156
163
|
);
|
|
157
164
|
};
|
|
158
165
|
|
|
159
|
-
function MenuEntryItem(props: { icon: React.ReactNode
|
|
160
|
-
return
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
function MenuEntryItem(props: { icon: React.ReactNode; text: string; onClick: () => void }) {
|
|
167
|
+
return (
|
|
168
|
+
<button
|
|
169
|
+
onClick={props.onClick}
|
|
170
|
+
className="px-4 py-2 text-left hover:bg-gray-500 dark:hover:bg-gray-600 w-full flex flex-row"
|
|
171
|
+
>
|
|
172
|
+
<span className="flex-grow">{props.icon}</span>
|
|
173
|
+
<span className="flex-grow">{props.text}</span>
|
|
174
|
+
{/* <span className="text-sm">Ctrl+Shift+xxxx</span> */}
|
|
175
|
+
</button>
|
|
176
|
+
);
|
|
165
177
|
}
|
|
166
178
|
|
|
167
179
|
export default ContextMenu;
|
package/src/components.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// React components and hooks exports
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
9
|
-
export * from
|
|
10
|
-
export * from
|
|
11
|
-
export * from
|
|
2
|
+
export * from './components/ai/Assistant';
|
|
3
|
+
export * from './components/ai/Avatar';
|
|
4
|
+
export * from './components/ai/EmbeddedAssistent/VoiceRecoder';
|
|
5
|
+
export * from './components/audio/Playbutton';
|
|
6
|
+
export * from './components/CRUDModal';
|
|
7
|
+
export * from './components/MarkdownEditor';
|
|
8
|
+
export * from './components/Spinner';
|
|
9
|
+
export * from './hooks/UseChatHook';
|
|
10
|
+
export * from './plugin/ThemeSetter';
|
|
11
|
+
export * from './providers/PluginProvider';
|