@rimori/client 1.4.0 → 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.
Files changed (133) hide show
  1. package/README.md +77 -71
  2. package/dist/cli/scripts/init/dev-registration.d.ts +1 -1
  3. package/dist/cli/scripts/init/dev-registration.js +4 -4
  4. package/dist/cli/scripts/init/main.js +1 -1
  5. package/dist/cli/scripts/init/package-setup.d.ts +1 -1
  6. package/dist/cli/scripts/init/package-setup.js +3 -3
  7. package/dist/cli/scripts/init/router-transformer.js +19 -12
  8. package/dist/cli/scripts/init/vite-config.d.ts +2 -2
  9. package/dist/cli/scripts/init/vite-config.js +2 -2
  10. package/dist/cli/scripts/release/release-config-upload.js +9 -9
  11. package/dist/cli/scripts/release/release-db-update.d.ts +1 -1
  12. package/dist/cli/scripts/release/release-db-update.js +9 -9
  13. package/dist/cli/scripts/release/release-file-upload.js +1 -1
  14. package/dist/cli/scripts/release/release.js +2 -2
  15. package/dist/components/CRUDModal.d.ts +1 -1
  16. package/dist/components/CRUDModal.js +3 -3
  17. package/dist/components/MarkdownEditor.js +16 -16
  18. package/dist/components/Spinner.js +2 -2
  19. package/dist/components/ai/Assistant.js +7 -8
  20. package/dist/components/ai/Avatar.d.ts +2 -2
  21. package/dist/components/ai/Avatar.js +10 -5
  22. package/dist/components/ai/EmbeddedAssistent/AudioInputField.js +5 -6
  23. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.d.ts +1 -1
  24. package/dist/components/ai/EmbeddedAssistent/CircleAudioAvatar.js +1 -2
  25. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +1 -2
  26. package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +4 -2
  27. package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +2 -3
  28. package/dist/components/audio/Playbutton.js +10 -7
  29. package/dist/components/components/ContextMenu.d.ts +1 -1
  30. package/dist/components/components/ContextMenu.js +19 -16
  31. package/dist/components.d.ts +10 -10
  32. package/dist/components.js +10 -10
  33. package/dist/core/controller/AIController.d.ts +2 -2
  34. package/dist/core/controller/AIController.js +12 -12
  35. package/dist/core/controller/ExerciseController.d.ts +2 -2
  36. package/dist/core/controller/ExerciseController.js +2 -2
  37. package/dist/core/controller/ObjectController.js +5 -5
  38. package/dist/core/controller/SettingsController.d.ts +22 -7
  39. package/dist/core/controller/SettingsController.js +73 -8
  40. package/dist/core/controller/SharedContentController.d.ts +3 -3
  41. package/dist/core/controller/SharedContentController.js +38 -20
  42. package/dist/core/controller/VoiceController.js +6 -4
  43. package/dist/core/core.d.ts +15 -15
  44. package/dist/core/core.js +7 -7
  45. package/dist/fromRimori/EventBus.js +23 -23
  46. package/dist/fromRimori/PluginTypes.d.ts +4 -4
  47. package/dist/hooks/UseChatHook.d.ts +3 -3
  48. package/dist/hooks/UseChatHook.js +9 -3
  49. package/dist/index.d.ts +10 -10
  50. package/dist/index.js +9 -9
  51. package/dist/plugin/AccomplishmentHandler.d.ts +5 -5
  52. package/dist/plugin/AccomplishmentHandler.js +31 -27
  53. package/dist/plugin/AudioController.d.ts +1 -1
  54. package/dist/plugin/AudioController.js +6 -6
  55. package/dist/plugin/Logger.js +15 -13
  56. package/dist/plugin/PluginController.d.ts +7 -1
  57. package/dist/plugin/PluginController.js +32 -27
  58. package/dist/plugin/RimoriClient.d.ts +17 -18
  59. package/dist/plugin/RimoriClient.js +31 -31
  60. package/dist/plugin/StandaloneClient.d.ts +1 -1
  61. package/dist/plugin/StandaloneClient.js +35 -16
  62. package/dist/plugin/ThemeSetter.js +4 -4
  63. package/dist/providers/PluginProvider.js +44 -14
  64. package/dist/utils/Language.js +57 -57
  65. package/dist/utils/PluginUtils.js +3 -3
  66. package/dist/utils/difficultyConverter.d.ts +1 -1
  67. package/dist/utils/difficultyConverter.js +1 -1
  68. package/dist/utils/endpoint.js +2 -2
  69. package/dist/worker/WorkerSetup.d.ts +1 -1
  70. package/dist/worker/WorkerSetup.js +6 -6
  71. package/example/docs/devdocs.md +50 -40
  72. package/example/docs/overview.md +1 -1
  73. package/example/docs/userdocs.md +4 -1
  74. package/example/rimori.config.ts +51 -49
  75. package/example/worker/vite.config.ts +3 -3
  76. package/example/worker/worker.ts +2 -2
  77. package/package.json +14 -8
  78. package/prettier.config.js +1 -1
  79. package/src/cli/scripts/init/dev-registration.ts +5 -8
  80. package/src/cli/scripts/init/env-setup.ts +1 -1
  81. package/src/cli/scripts/init/file-operations.ts +1 -1
  82. package/src/cli/scripts/init/html-cleaner.ts +2 -5
  83. package/src/cli/scripts/init/main.ts +16 -13
  84. package/src/cli/scripts/init/package-setup.ts +11 -15
  85. package/src/cli/scripts/init/router-transformer.ts +40 -37
  86. package/src/cli/scripts/init/tailwind-config.ts +17 -26
  87. package/src/cli/scripts/init/vite-config.ts +3 -3
  88. package/src/cli/scripts/release/release-config-upload.ts +11 -11
  89. package/src/cli/scripts/release/release-db-update.ts +12 -12
  90. package/src/cli/scripts/release/release-file-upload.ts +2 -2
  91. package/src/cli/scripts/release/release.ts +4 -4
  92. package/src/cli/types/DatabaseTypes.ts +2 -10
  93. package/src/components/CRUDModal.tsx +64 -48
  94. package/src/components/MarkdownEditor.tsx +58 -27
  95. package/src/components/Spinner.tsx +24 -17
  96. package/src/components/ai/Assistant.tsx +70 -70
  97. package/src/components/ai/Avatar.tsx +17 -14
  98. package/src/components/ai/EmbeddedAssistent/AudioInputField.tsx +63 -54
  99. package/src/components/ai/EmbeddedAssistent/CircleAudioAvatar.tsx +14 -5
  100. package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +75 -74
  101. package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +3 -4
  102. package/src/components/ai/EmbeddedAssistent/VoiceRecoder.tsx +109 -94
  103. package/src/components/ai/utils.ts +4 -4
  104. package/src/components/audio/Playbutton.tsx +101 -93
  105. package/src/components/components/ContextMenu.tsx +47 -35
  106. package/src/components.ts +10 -10
  107. package/src/core/controller/AIController.ts +29 -19
  108. package/src/core/controller/ExerciseController.ts +16 -23
  109. package/src/core/controller/ObjectController.ts +15 -10
  110. package/src/core/controller/SettingsController.ts +89 -16
  111. package/src/core/controller/SharedContentController.ts +80 -44
  112. package/src/core/controller/VoiceController.ts +10 -8
  113. package/src/core/core.ts +15 -16
  114. package/src/fromRimori/EventBus.ts +76 -47
  115. package/src/fromRimori/PluginTypes.ts +26 -17
  116. package/src/fromRimori/readme.md +2 -2
  117. package/src/hooks/UseChatHook.ts +25 -15
  118. package/src/index.ts +10 -10
  119. package/src/plugin/AccomplishmentHandler.ts +53 -35
  120. package/src/plugin/AudioController.ts +18 -12
  121. package/src/plugin/Logger.ts +28 -21
  122. package/src/plugin/PluginController.ts +60 -44
  123. package/src/plugin/RimoriClient.ts +102 -72
  124. package/src/plugin/StandaloneClient.ts +51 -24
  125. package/src/plugin/ThemeSetter.ts +5 -5
  126. package/src/providers/PluginProvider.tsx +90 -36
  127. package/src/style.scss +3 -3
  128. package/src/utils/Language.ts +58 -58
  129. package/src/utils/PluginUtils.ts +16 -20
  130. package/src/utils/difficultyConverter.ts +2 -2
  131. package/src/utils/endpoint.ts +3 -2
  132. package/src/worker/WorkerSetup.ts +8 -9
  133. package/tsconfig.json +2 -4
@@ -1,64 +1,73 @@
1
1
  import React, { useState } from 'react';
2
2
  import { VoiceRecorder } from './VoiceRecoder';
3
- import { BiSolidRightArrow } from "react-icons/bi";
4
- import { HiMiniSpeakerXMark, HiMiniSpeakerWave } from "react-icons/hi2";
3
+ import { BiSolidRightArrow } from 'react-icons/bi';
4
+ import { HiMiniSpeakerXMark, HiMiniSpeakerWave } from 'react-icons/hi2';
5
5
 
6
6
  interface AudioInputFieldProps {
7
- onSubmit: (text: string) => void;
8
- onAudioControl?: (voice: boolean) => void;
9
- blockSubmission?: boolean;
7
+ onSubmit: (text: string) => void;
8
+ onAudioControl?: (voice: boolean) => void;
9
+ blockSubmission?: boolean;
10
10
  }
11
11
 
12
12
  export function AudioInputField({ onSubmit, onAudioControl, blockSubmission = false }: AudioInputFieldProps) {
13
- const [text, setText] = useState('');
14
- const [audioEnabled, setAudioEnabled] = useState(true);
13
+ const [text, setText] = useState('');
14
+ const [audioEnabled, setAudioEnabled] = useState(true);
15
15
 
16
- const handleSubmit = (manualText?: string) => {
17
- if (blockSubmission) return;
18
- const sendableText = manualText || text;
19
- if (sendableText.trim()) {
20
- onSubmit(sendableText);
21
- setTimeout(() => {
22
- setText('');
23
- }, 100);
24
- }
25
- };
16
+ const handleSubmit = (manualText?: string) => {
17
+ if (blockSubmission) return;
18
+ const sendableText = manualText || text;
19
+ if (sendableText.trim()) {
20
+ onSubmit(sendableText);
21
+ setTimeout(() => {
22
+ setText('');
23
+ }, 100);
24
+ }
25
+ };
26
26
 
27
- const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
28
- if (blockSubmission) return;
29
- if (e.key === 'Enter' && e.ctrlKey) {
30
- setText(text + '\n');
31
- } else if (e.key === 'Enter') {
32
- handleSubmit();
33
- }
34
- };
27
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
28
+ if (blockSubmission) return;
29
+ if (e.key === 'Enter' && e.ctrlKey) {
30
+ setText(text + '\n');
31
+ } else if (e.key === 'Enter') {
32
+ handleSubmit();
33
+ }
34
+ };
35
35
 
36
- return (
37
- <div className="flex items-center bg-gray-600 pt-2 pb-2 p-2">
38
- {onAudioControl && <button
39
- onClick={() => {
40
- onAudioControl(!audioEnabled);
41
- setAudioEnabled(!audioEnabled);
42
- }}
43
- className="cursor-default">
44
- {audioEnabled ? <HiMiniSpeakerWave className='w-9 h-9 cursor-pointer' /> : <HiMiniSpeakerXMark className='w-9 h-9 cursor-pointer' />}
45
- </button>}
46
- <VoiceRecorder onRecordingStatusChange={() => {}} onVoiceRecorded={(m: string) => {
47
- console.log('onVoiceRecorded', m);
48
- handleSubmit(m);
49
- }}
50
- />
51
- <textarea
52
- value={text}
53
- onChange={(e) => setText(e.target.value)}
54
- onKeyDown={handleKeyDown}
55
- className="flex-1 border-none rounded-lg p-2 text-gray-800 focus::outline-none"
56
- placeholder='Type a message...'
57
- disabled={blockSubmission}
58
- />
59
- <button onClick={() => handleSubmit()} className="cursor-default" disabled={blockSubmission}>
60
- <BiSolidRightArrow className='w-9 h-10 cursor-pointer' />
61
- </button>
62
- </div>
63
- );
64
- };
36
+ return (
37
+ <div className="flex items-center bg-gray-600 pt-2 pb-2 p-2">
38
+ {onAudioControl && (
39
+ <button
40
+ onClick={() => {
41
+ onAudioControl(!audioEnabled);
42
+ setAudioEnabled(!audioEnabled);
43
+ }}
44
+ className="cursor-default"
45
+ >
46
+ {audioEnabled ? (
47
+ <HiMiniSpeakerWave className="w-9 h-9 cursor-pointer" />
48
+ ) : (
49
+ <HiMiniSpeakerXMark className="w-9 h-9 cursor-pointer" />
50
+ )}
51
+ </button>
52
+ )}
53
+ <VoiceRecorder
54
+ onRecordingStatusChange={() => {}}
55
+ onVoiceRecorded={(m: string) => {
56
+ console.log('onVoiceRecorded', m);
57
+ handleSubmit(m);
58
+ }}
59
+ />
60
+ <textarea
61
+ value={text}
62
+ onChange={(e) => setText(e.target.value)}
63
+ onKeyDown={handleKeyDown}
64
+ className="flex-1 border-none rounded-lg p-2 text-gray-800 focus::outline-none"
65
+ placeholder="Type a message..."
66
+ disabled={blockSubmission}
67
+ />
68
+ <button onClick={() => handleSubmit()} className="cursor-default" disabled={blockSubmission}>
69
+ <BiSolidRightArrow className="w-9 h-10 cursor-pointer" />
70
+ </button>
71
+ </div>
72
+ );
73
+ }
@@ -8,7 +8,12 @@ interface CircleAudioAvatarProps {
8
8
  isDarkTheme?: boolean;
9
9
  }
10
10
 
11
- export function CircleAudioAvatar({ imageUrl, className, isDarkTheme = false, width = "150px" }: CircleAudioAvatarProps) {
11
+ export function CircleAudioAvatar({
12
+ imageUrl,
13
+ className,
14
+ isDarkTheme = false,
15
+ width = '150px',
16
+ }: CircleAudioAvatarProps) {
12
17
  const canvasRef = useRef<HTMLCanvasElement>(null);
13
18
  const currentLoudnessRef = useRef(0);
14
19
  const targetLoudnessRef = useRef(0);
@@ -31,7 +36,7 @@ export function CircleAudioAvatar({ imageUrl, className, isDarkTheme = false, wi
31
36
  if (currentLoudnessRef.current > targetLoudnessRef.current) {
32
37
  currentLoudnessRef.current = Math.max(
33
38
  targetLoudnessRef.current,
34
- currentLoudnessRef.current - decayRate * currentLoudnessRef.current
39
+ currentLoudnessRef.current - decayRate * currentLoudnessRef.current,
35
40
  );
36
41
  } else {
37
42
  currentLoudnessRef.current = targetLoudnessRef.current;
@@ -63,7 +68,12 @@ export function CircleAudioAvatar({ imageUrl, className, isDarkTheme = false, wi
63
68
  }
64
69
  }, [imageUrl]);
65
70
 
66
- const draw = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, image: HTMLImageElement, loudness: number) => {
71
+ const draw = (
72
+ ctx: CanvasRenderingContext2D,
73
+ canvas: HTMLCanvasElement,
74
+ image: HTMLImageElement,
75
+ loudness: number,
76
+ ) => {
67
77
  if (canvas && ctx) {
68
78
  ctx.clearRect(0, 0, canvas.width, canvas.height);
69
79
 
@@ -94,5 +104,4 @@ export function CircleAudioAvatar({ imageUrl, className, isDarkTheme = false, wi
94
104
  };
95
105
 
96
106
  return <canvas ref={canvasRef} className={className} width={500} height={500} style={{ width }} />;
97
- };
98
-
107
+ }
@@ -3,93 +3,94 @@ import { ChunkedAudioPlayer } from './Player';
3
3
  type VoiceBackend = (text: string, voice?: string, speed?: number) => Promise<Blob>;
4
4
 
5
5
  export class MessageSender {
6
- private player = new ChunkedAudioPlayer();
7
- private fetchedSentences = new Set<string>();
8
- private lastLoading = false;
9
- private voice: string;
10
- private model: string;
11
- private voiceBackend: VoiceBackend;
6
+ private player = new ChunkedAudioPlayer();
7
+ private fetchedSentences = new Set<string>();
8
+ private lastLoading = false;
9
+ private voice: string;
10
+ private voiceBackend: VoiceBackend;
12
11
 
13
- constructor(voiceBackend: VoiceBackend, voice: string = 'alloy', model = 'openai') {
14
- this.voiceBackend = voiceBackend;
15
- this.voice = voice;
16
- this.model = model;
12
+ constructor(voiceBackend: VoiceBackend, voice: string) {
13
+ if (voice?.split('_').length !== 2) {
14
+ throw new Error("Invalid voice id format '" + voice + "'. Voice id needs to look like <provider>_<voice_id>");
17
15
  }
16
+ this.voiceBackend = voiceBackend;
17
+ this.voice = voice;
18
+ }
18
19
 
19
- private getCompletedSentences(currentText: string, isLoading: boolean): string[] {
20
- // Split the text based on the following characters: .,?!
21
- // Only split on : when followed by a space
22
- const pattern = /(.+?[,.?!]|.+?:\s+|.+?\n+)/g;
23
- const result: string[] = [];
24
- let match;
25
- while ((match = pattern.exec(currentText)) !== null) {
26
- const sentence = match[0].trim();
27
- if (sentence.length > 0) {
28
- result.push(sentence);
29
- }
30
- }
31
- if (!isLoading) {
32
- const lastFullSentence = result[result.length - 1];
33
- const leftoverIndex = currentText.lastIndexOf(lastFullSentence) + lastFullSentence.length;
34
- if (leftoverIndex < currentText.length) {
35
- result.push(currentText.slice(leftoverIndex).trim());
36
- }
37
- }
38
- return result;
20
+ private getCompletedSentences(currentText: string, isLoading: boolean): string[] {
21
+ // Split the text based on the following characters: .,?!
22
+ // Only split on : when followed by a space
23
+ const pattern = /(.+?[,.?!]|.+?:\s+|.+?\n+)/g;
24
+ const result: string[] = [];
25
+ let match;
26
+ while ((match = pattern.exec(currentText)) !== null) {
27
+ const sentence = match[0].trim();
28
+ if (sentence.length > 0) {
29
+ result.push(sentence);
30
+ }
39
31
  }
32
+ if (!isLoading) {
33
+ const lastFullSentence = result[result.length - 1];
34
+ const leftoverIndex = currentText.lastIndexOf(lastFullSentence) + lastFullSentence.length;
35
+ if (leftoverIndex < currentText.length) {
36
+ result.push(currentText.slice(leftoverIndex).trim());
37
+ }
38
+ }
39
+ return result;
40
+ }
40
41
 
41
- public async handleNewText(currentText: string | undefined, isLoading: boolean) {
42
- if (!this.lastLoading && isLoading) {
43
- this.reset();
44
- }
45
- this.lastLoading = isLoading;
42
+ public async handleNewText(currentText: string | undefined, isLoading: boolean) {
43
+ if (!this.lastLoading && isLoading) {
44
+ this.reset();
45
+ }
46
+ this.lastLoading = isLoading;
46
47
 
47
- if (!currentText) {
48
- return;
49
- }
48
+ if (!currentText) {
49
+ return;
50
+ }
50
51
 
51
- const sentences = this.getCompletedSentences(currentText, isLoading);
52
+ const sentences = this.getCompletedSentences(currentText, isLoading);
52
53
 
53
- for (let i = 0; i < sentences.length; i++) {
54
- const sentence = sentences[i];
55
- if (!this.fetchedSentences.has(sentence)) {
56
- this.fetchedSentences.add(sentence);
57
- const audioData = await this.generateSpeech(sentence);
58
- await this.player.addChunk(audioData, i);
59
- }
60
- }
54
+ for (let i = 0; i < sentences.length; i++) {
55
+ const sentence = sentences[i];
56
+ if (!this.fetchedSentences.has(sentence)) {
57
+ this.fetchedSentences.add(sentence);
58
+ const audioData = await this.generateSpeech(sentence);
59
+ await this.player.addChunk(audioData, i);
60
+ }
61
61
  }
62
+ }
62
63
 
63
- private async generateSpeech(sentence: string): Promise<ArrayBuffer> {
64
- const blob = await this.voiceBackend(sentence, this.voice, 1.0);
65
- return await blob.arrayBuffer();
66
- }
64
+ private async generateSpeech(sentence: string): Promise<ArrayBuffer> {
65
+ const blob = await this.voiceBackend(sentence, this.voice, 1.0);
66
+ return await blob.arrayBuffer();
67
+ }
67
68
 
68
- public play() {
69
- this.player.playAgain();
70
- }
69
+ public play() {
70
+ this.player.playAgain();
71
+ }
71
72
 
72
- public stop() {
73
- this.player.stopPlayback();
74
- }
73
+ public stop() {
74
+ this.player.stopPlayback();
75
+ }
75
76
 
76
- private reset() {
77
- this.stop();
78
- this.fetchedSentences.clear();
79
- this.player.reset();
80
- }
77
+ private reset() {
78
+ this.stop();
79
+ this.fetchedSentences.clear();
80
+ this.player.reset();
81
+ }
81
82
 
82
- public setVolume(volume: number) {
83
- this.player.setVolume(volume);
84
- }
83
+ public setVolume(volume: number) {
84
+ this.player.setVolume(volume);
85
+ }
85
86
 
86
- public setOnLoudnessChange(callback: (value: number) => void) {
87
- this.player.setOnLoudnessChange((loudness) => {
88
- callback(loudness);
89
- });
90
- }
87
+ public setOnLoudnessChange(callback: (value: number) => void) {
88
+ this.player.setOnLoudnessChange((loudness) => {
89
+ callback(loudness);
90
+ });
91
+ }
91
92
 
92
- public setOnEndOfSpeech(callback: () => void) {
93
- this.player.setOnEndOfSpeech(callback);
94
- }
95
- }
93
+ public setOnEndOfSpeech(callback: () => void) {
94
+ this.player.setOnEndOfSpeech(callback);
95
+ }
96
+ }
@@ -1,5 +1,4 @@
1
1
  export class ChunkedAudioPlayer {
2
-
3
2
  private audioContext!: AudioContext;
4
3
  private chunkQueue: ArrayBuffer[] = [];
5
4
  private isPlaying = false;
@@ -9,10 +8,10 @@ export class ChunkedAudioPlayer {
9
8
  private isMonitoring = false;
10
9
  private handle = 0;
11
10
  private volume = 1.0;
12
- private loudnessCallback: (value: number) => void = () => { };
11
+ private loudnessCallback: (value: number) => void = () => {};
13
12
  private currentIndex = 0;
14
13
  private startedPlaying = false;
15
- private onEndOfSpeech: () => void = () => { };
14
+ private onEndOfSpeech: () => void = () => {};
16
15
 
17
16
  constructor() {
18
17
  this.init();
@@ -195,4 +194,4 @@ export class ChunkedAudioPlayer {
195
194
  public setOnEndOfSpeech(callback: () => void) {
196
195
  this.onEndOfSpeech = callback;
197
196
  }
198
- }
197
+ }
@@ -13,102 +13,117 @@ interface Props {
13
13
  onVoiceRecorded: (message: string) => void;
14
14
  }
15
15
 
16
- export const VoiceRecorder = forwardRef(({ onVoiceRecorded, iconSize, className, disabled, loading, onRecordingStatusChange, enablePushToTalk = false }: Props, ref) => {
17
- const [isRecording, setIsRecording] = useState(false);
18
- const [internalIsProcessing, setInternalIsProcessing] = useState(false);
19
- const audioControllerRef = useRef<AudioController | null>(null);
20
- const { ai, plugin } = useRimori();
21
-
22
- // Ref for latest onVoiceRecorded callback
23
- const onVoiceRecordedRef = useRef(onVoiceRecorded);
24
- useEffect(() => {
25
- onVoiceRecordedRef.current = onVoiceRecorded;
26
- }, [onVoiceRecorded]);
27
-
28
- const startRecording = async () => {
29
- try {
30
- if (!audioControllerRef.current) {
31
- audioControllerRef.current = new AudioController(plugin.pluginId);
32
- }
33
-
34
- await audioControllerRef.current.startRecording();
35
- setIsRecording(true);
36
- onRecordingStatusChange(true);
37
- } catch (error) {
38
- console.error('Failed to start recording:', error);
39
- // Handle permission denied or other errors
40
- }
41
- };
42
-
43
-
44
-
45
- const stopRecording = async () => {
46
- try {
47
- if (audioControllerRef.current && isRecording) {
48
- const audioResult = await audioControllerRef.current.stopRecording();
49
- // console.log("audioResult: ", audioResult);
50
-
51
- setInternalIsProcessing(true);
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
- const handleKeyUp = (event: KeyboardEvent) => {
92
- if (event.code === 'Space' && spacePressedRef.current) {
93
- spacePressedRef.current = false;
94
- stopRecording();
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
- return (
106
- <button className={"flex flex-row justify-center items-center rounded-full mx-auto disabled:opacity-50 " + className}
107
- onClick={isRecording ? stopRecording : startRecording}
108
- disabled={disabled || loading || internalIsProcessing}>
109
- {loading || internalIsProcessing ? <FaSpinner className="animate-spin" /> :
110
- <FaMicrophone size={iconSize} className={(isRecording ? "text-red-600" : "")} />
111
- }
112
- </button>
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
- messages.push({ id: '1', role: 'system', content: instructions.instructions });
11
+ messages.push({ id: '1', role: 'system', content: instructions.instructions });
12
12
  }
13
13
  if (instructions.userMessage) {
14
- messages.push({ id: '2', role: 'user', content: instructions.userMessage });
14
+ messages.push({ id: '2', role: 'user', content: instructions.userMessage });
15
15
  }
16
16
  if (instructions.assistantMessage) {
17
- messages.push({ id: '3', role: 'assistant', content: instructions.assistantMessage });
17
+ messages.push({ id: '3', role: 'assistant', content: instructions.assistantMessage });
18
18
  }
19
19
 
20
20
  return messages;
21
- }
21
+ }