@rimori/react-client 0.3.0-next.8 → 0.4.0-next.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/components/MarkdownEditor.d.ts +8 -0
- package/dist/components/MarkdownEditor.js +48 -0
- package/dist/components/Spinner.d.ts +1 -0
- package/dist/components/Spinner.js +1 -0
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +11 -0
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +95 -0
- package/dist/plugin/ThemeSetter.d.ts +2 -0
- package/dist/plugin/ThemeSetter.js +31 -0
- package/dist/utils/FullscreenUtils.d.ts +2 -0
- package/dist/utils/FullscreenUtils.js +23 -0
- package/package.json +3 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Markdown } from 'tiptap-markdown';
|
|
3
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
4
|
+
import { PiCodeBlock } from 'react-icons/pi';
|
|
5
|
+
import { TbBlockquote } from 'react-icons/tb';
|
|
6
|
+
import { GoListOrdered } from 'react-icons/go';
|
|
7
|
+
import { AiOutlineUnorderedList } from 'react-icons/ai';
|
|
8
|
+
import { EditorProvider, useCurrentEditor } from '@tiptap/react';
|
|
9
|
+
import { LuHeading1, LuHeading2, LuHeading3 } from 'react-icons/lu';
|
|
10
|
+
import { FaBold, FaCode, FaItalic, FaParagraph, FaStrikethrough } from 'react-icons/fa';
|
|
11
|
+
const EditorButton = ({ action, isActive, label, disabled }) => {
|
|
12
|
+
const { editor } = useCurrentEditor();
|
|
13
|
+
if (!editor) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (action.includes('heading')) {
|
|
17
|
+
const level = parseInt(action[action.length - 1]);
|
|
18
|
+
return (_jsx("button", { onClick: () => editor.chain().focus().toggleHeading({ level: level }).run(), className: `pl-2 ${isActive ? 'is-active' : ''}`, children: label }));
|
|
19
|
+
}
|
|
20
|
+
return (_jsx("button", { onClick: () => editor.chain().focus()[action]().run(), disabled: disabled ? !editor.can().chain().focus()[action]().run() : false, className: `pl-2 ${isActive ? 'is-active' : ''}`, children: label }));
|
|
21
|
+
};
|
|
22
|
+
const MenuBar = () => {
|
|
23
|
+
const { editor } = useCurrentEditor();
|
|
24
|
+
if (!editor) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return (_jsxs("div", { className: "bg-gray-400 dark:bg-gray-800 dark:text-white text-lg flex flex-row flex-wrap items-center p-1", children: [_jsx(EditorButton, { action: "toggleBold", isActive: editor.isActive('bold'), label: _jsx(FaBold, {}), disabled: true }), _jsx(EditorButton, { action: "toggleItalic", isActive: editor.isActive('italic'), label: _jsx(FaItalic, {}), disabled: true }), _jsx(EditorButton, { action: "toggleStrike", isActive: editor.isActive('strike'), label: _jsx(FaStrikethrough, {}), disabled: true }), _jsx(EditorButton, { action: "toggleCode", isActive: editor.isActive('code'), label: _jsx(FaCode, {}), disabled: true }), _jsx(EditorButton, { action: "setParagraph", isActive: editor.isActive('paragraph'), label: _jsx(FaParagraph, {}) }), _jsx(EditorButton, { action: "setHeading1", isActive: editor.isActive('heading', { level: 1 }), label: _jsx(LuHeading1, { size: '24px' }) }), _jsx(EditorButton, { action: "setHeading2", isActive: editor.isActive('heading', { level: 2 }), label: _jsx(LuHeading2, { size: '24px' }) }), _jsx(EditorButton, { action: "setHeading3", isActive: editor.isActive('heading', { level: 3 }), label: _jsx(LuHeading3, { size: '24px' }) }), _jsx(EditorButton, { action: "toggleBulletList", isActive: editor.isActive('bulletList'), label: _jsx(AiOutlineUnorderedList, { size: '24px' }) }), _jsx(EditorButton, { action: "toggleOrderedList", isActive: editor.isActive('orderedList'), label: _jsx(GoListOrdered, { size: '24px' }) }), _jsx(EditorButton, { action: "toggleCodeBlock", isActive: editor.isActive('codeBlock'), label: _jsx(PiCodeBlock, { size: '24px' }) }), _jsx(EditorButton, { action: "toggleBlockquote", isActive: editor.isActive('blockquote'), label: _jsx(TbBlockquote, { size: '24px' }) })] }));
|
|
28
|
+
};
|
|
29
|
+
const extensions = [
|
|
30
|
+
StarterKit.configure({
|
|
31
|
+
bulletList: {
|
|
32
|
+
HTMLAttributes: {
|
|
33
|
+
class: 'list-disc list-inside dark:text-white p-1 mt-1 [&_li]:mb-1 [&_p]:inline m-0',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
orderedList: {
|
|
37
|
+
HTMLAttributes: {
|
|
38
|
+
className: 'list-decimal list-inside dark:text-white p-1 mt-1 [&_li]:mb-1 [&_p]:inline m-0',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
Markdown,
|
|
43
|
+
];
|
|
44
|
+
export const MarkdownEditor = (props) => {
|
|
45
|
+
return (_jsx("div", { className: 'text-md border border-gray-800 overflow-hidden ' + props.className, style: { borderWidth: props.editable ? 1 : 0 }, children: _jsx(EditorProvider, { slotBefore: props.editable ? _jsx(MenuBar, {}) : null, extensions: extensions, content: props.content, editable: props.editable, onUpdate: (e) => {
|
|
46
|
+
props.onUpdate && props.onUpdate(e.editor.storage.markdown.getMarkdown());
|
|
47
|
+
} }, (props.editable ? 'editable' : 'readonly') + props.content) }));
|
|
48
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
iconSize?: string;
|
|
3
|
+
className?: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
enablePushToTalk?: boolean;
|
|
7
|
+
onRecordingStatusChange: (running: boolean) => void;
|
|
8
|
+
onVoiceRecorded: (message: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare const VoiceRecorder: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<unknown>>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
11
|
+
import { useRimori } from '../../../providers/PluginProvider';
|
|
12
|
+
import { FaMicrophone, FaSpinner } from 'react-icons/fa6';
|
|
13
|
+
import { AudioController } from '@rimori/client';
|
|
14
|
+
import { useState, useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
|
|
15
|
+
export const VoiceRecorder = forwardRef(({ onVoiceRecorded, iconSize, className, disabled, loading, onRecordingStatusChange, enablePushToTalk = false, }, ref) => {
|
|
16
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
17
|
+
const [internalIsProcessing, setInternalIsProcessing] = useState(false);
|
|
18
|
+
const audioControllerRef = useRef(null);
|
|
19
|
+
const { ai, plugin } = useRimori();
|
|
20
|
+
// Ref for latest onVoiceRecorded callback
|
|
21
|
+
const onVoiceRecordedRef = useRef(onVoiceRecorded);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
onVoiceRecordedRef.current = onVoiceRecorded;
|
|
24
|
+
}, [onVoiceRecorded]);
|
|
25
|
+
const startRecording = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
|
+
try {
|
|
27
|
+
if (!audioControllerRef.current) {
|
|
28
|
+
audioControllerRef.current = new AudioController(plugin.pluginId);
|
|
29
|
+
}
|
|
30
|
+
yield audioControllerRef.current.startRecording();
|
|
31
|
+
setIsRecording(true);
|
|
32
|
+
onRecordingStatusChange(true);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('Failed to start recording:', error);
|
|
36
|
+
// Handle permission denied or other errors
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const stopRecording = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
40
|
+
try {
|
|
41
|
+
if (audioControllerRef.current && isRecording) {
|
|
42
|
+
const audioResult = yield audioControllerRef.current.stopRecording();
|
|
43
|
+
// console.log("audioResult: ", audioResult);
|
|
44
|
+
setInternalIsProcessing(true);
|
|
45
|
+
// Play the recorded audio from the Blob
|
|
46
|
+
// const blobUrl = URL.createObjectURL(audioResult.recording);
|
|
47
|
+
// const audioRef = new Audio(blobUrl);
|
|
48
|
+
// audioRef.onended = () => URL.revokeObjectURL(blobUrl);
|
|
49
|
+
// audioRef.play().catch((e) => console.error('Playback error:', e));
|
|
50
|
+
// console.log("audioBlob: ", audioResult.recording);
|
|
51
|
+
const text = yield ai.getTextFromVoice(audioResult.recording);
|
|
52
|
+
// console.log("stt result", text);
|
|
53
|
+
// throw new Error("test");
|
|
54
|
+
setInternalIsProcessing(false);
|
|
55
|
+
onVoiceRecordedRef.current(text);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error('Failed to stop recording:', error);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
setIsRecording(false);
|
|
63
|
+
onRecordingStatusChange(false);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
useImperativeHandle(ref, () => ({
|
|
67
|
+
startRecording,
|
|
68
|
+
stopRecording,
|
|
69
|
+
}));
|
|
70
|
+
// push to talk feature
|
|
71
|
+
const spacePressedRef = useRef(false);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!enablePushToTalk)
|
|
74
|
+
return;
|
|
75
|
+
const handleKeyDown = (event) => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
if (event.code === 'Space' && !spacePressedRef.current) {
|
|
77
|
+
spacePressedRef.current = true;
|
|
78
|
+
yield startRecording();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const handleKeyUp = (event) => {
|
|
82
|
+
if (event.code === 'Space' && spacePressedRef.current) {
|
|
83
|
+
spacePressedRef.current = false;
|
|
84
|
+
stopRecording();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
88
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
89
|
+
return () => {
|
|
90
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
91
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
92
|
+
};
|
|
93
|
+
}, [enablePushToTalk]);
|
|
94
|
+
return (_jsx("button", { className: 'flex flex-row justify-center items-center rounded-full mx-auto disabled:opacity-50 ' + className, onClick: isRecording ? stopRecording : startRecording, disabled: disabled || loading || internalIsProcessing, children: loading || internalIsProcessing ? (_jsx(FaSpinner, { className: "animate-spin" })) : (_jsx(FaMicrophone, { size: iconSize, className: isRecording ? 'text-red-600' : '' })) }));
|
|
95
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
export function useTheme(theme) {
|
|
3
|
+
const [isDark, setIsDark] = useState(false);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const root = document.documentElement;
|
|
6
|
+
const nextIsDark = isDarkTheme(theme);
|
|
7
|
+
setIsDark(nextIsDark);
|
|
8
|
+
root.classList.add('dark:text-gray-200');
|
|
9
|
+
if (nextIsDark) {
|
|
10
|
+
root.setAttribute('data-theme', 'dark');
|
|
11
|
+
root.classList.add('dark', 'dark:bg-gray-950');
|
|
12
|
+
root.style.background = 'hsl(var(--background))';
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
root.removeAttribute('data-theme');
|
|
16
|
+
root.classList.remove('dark', 'dark:bg-gray-950');
|
|
17
|
+
root.style.background = '';
|
|
18
|
+
}, [theme]);
|
|
19
|
+
return isDark;
|
|
20
|
+
}
|
|
21
|
+
export function isDarkTheme(theme) {
|
|
22
|
+
// If no theme provided, try to get from URL as fallback (for standalone mode)
|
|
23
|
+
if (!theme) {
|
|
24
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
25
|
+
theme = urlParams.get('theme');
|
|
26
|
+
}
|
|
27
|
+
if (!theme || theme === 'system') {
|
|
28
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
29
|
+
}
|
|
30
|
+
return theme === 'dark';
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function isFullscreen() {
|
|
2
|
+
return !!document.fullscreenElement;
|
|
3
|
+
}
|
|
4
|
+
export function triggerFullscreen(onStateChange, selector) {
|
|
5
|
+
document.addEventListener('fullscreenchange', () => {
|
|
6
|
+
onStateChange(isFullscreen());
|
|
7
|
+
});
|
|
8
|
+
try {
|
|
9
|
+
const ref = document.querySelector(selector || '#root');
|
|
10
|
+
if (!isFullscreen()) {
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
void (ref.requestFullscreen() || ref.webkitRequestFullscreen());
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
void (document.exitFullscreen() || document.webkitExitFullscreen());
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error('Failed to enter fullscreen', error.message);
|
|
21
|
+
}
|
|
22
|
+
onStateChange(isFullscreen());
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimori/react-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-next.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"format": "prettier --write ."
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@rimori/client": "2.
|
|
26
|
+
"@rimori/client": "^2.4.0",
|
|
27
27
|
"react": "^18.1.0",
|
|
28
28
|
"react-dom": "^18.1.0"
|
|
29
29
|
},
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@eslint/js": "^9.37.0",
|
|
37
|
-
"@rimori/client": "2.
|
|
37
|
+
"@rimori/client": "^2.4.0",
|
|
38
38
|
"@types/react": "^18.3.21",
|
|
39
39
|
"eslint-config-prettier": "^10.1.8",
|
|
40
40
|
"eslint-plugin-prettier": "^5.5.4",
|