@rimori/react-client 0.2.0 → 0.3.0-next.1
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/.github/workflows/pre-release.yml +129 -0
- package/dist/components/ContextMenu.js +9 -12
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.d.ts +1 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +13 -3
- package/package.json +13 -6
- package/src/components/ContextMenu.tsx +17 -19
- package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +13 -3
- package/dist/components/MarkdownEditor.d.ts +0 -8
- package/dist/components/MarkdownEditor.js +0 -48
- package/dist/components/Spinner.d.ts +0 -1
- package/dist/components/Spinner.js +0 -1
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.d.ts +0 -11
- package/dist/components/ai/EmbeddedAssistent/VoiceRecoder.js +0 -95
- package/dist/plugin/ThemeSetter.d.ts +0 -2
- package/dist/plugin/ThemeSetter.js +0 -31
- package/dist/utils/FullscreenUtils.d.ts +0 -2
- package/dist/utils/FullscreenUtils.js +0 -23
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
name: Pre-Release Rimori React Client
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [dev]
|
|
6
|
+
paths:
|
|
7
|
+
- '**'
|
|
8
|
+
- '!.github/workflows/**'
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
pre-release:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
id-token: write
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
|
|
24
|
+
- name: Setup Node.js
|
|
25
|
+
uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: '20'
|
|
28
|
+
registry-url: 'https://registry.npmjs.org'
|
|
29
|
+
cache: 'yarn'
|
|
30
|
+
cache-dependency-path: yarn.lock
|
|
31
|
+
|
|
32
|
+
- name: Update npm
|
|
33
|
+
run: npm install -g npm@latest
|
|
34
|
+
|
|
35
|
+
- name: Get latest @rimori/client@next version
|
|
36
|
+
id: client-version
|
|
37
|
+
run: |
|
|
38
|
+
VERSION=$(npm view @rimori/client@next version 2>/dev/null || echo "")
|
|
39
|
+
if [ -z "$VERSION" ]; then
|
|
40
|
+
echo "⚠️ Warning: No @rimori/client@next version found. Using current dependency version."
|
|
41
|
+
VERSION=$(node -p "require('./package.json').peerDependencies['@rimori/client'] || require('./package.json').devDependencies['@rimori/client']")
|
|
42
|
+
# Remove ^ prefix if present
|
|
43
|
+
VERSION="${VERSION#^}"
|
|
44
|
+
fi
|
|
45
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
46
|
+
echo "Using @rimori/client version: $VERSION"
|
|
47
|
+
|
|
48
|
+
- name: Update @rimori/client dependency
|
|
49
|
+
run: |
|
|
50
|
+
# Update both peerDependencies and devDependencies
|
|
51
|
+
yarn add "@rimori/client@${{ steps.client-version.outputs.version }}" --dev --exact
|
|
52
|
+
# Also update peerDependencies using node
|
|
53
|
+
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json')); if (pkg.peerDependencies && pkg.peerDependencies['@rimori/client']) { pkg.peerDependencies['@rimori/client'] = '${{ steps.client-version.outputs.version }}'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); }"
|
|
54
|
+
echo "Updated @rimori/client to ${{ steps.client-version.outputs.version }}"
|
|
55
|
+
|
|
56
|
+
- name: Install dependencies
|
|
57
|
+
run: yarn install --frozen-lockfile
|
|
58
|
+
|
|
59
|
+
- name: Build react-client (TypeScript verification)
|
|
60
|
+
run: yarn build
|
|
61
|
+
|
|
62
|
+
- name: Calculate next pre-release version
|
|
63
|
+
id: version
|
|
64
|
+
run: |
|
|
65
|
+
# Read current version from package.json (may be base or pre-release)
|
|
66
|
+
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
|
67
|
+
|
|
68
|
+
# Extract base version (strip any pre-release suffix)
|
|
69
|
+
# Examples: "0.3.0" -> "0.3.0", "0.3.0-next.5" -> "0.3.0"
|
|
70
|
+
if [[ "$CURRENT_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+) ]]; then
|
|
71
|
+
BASE_VERSION="${BASH_REMATCH[1]}"
|
|
72
|
+
else
|
|
73
|
+
BASE_VERSION="$CURRENT_VERSION"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Try to get latest next version from npm
|
|
77
|
+
PACKAGE_NAME="@rimori/react-client"
|
|
78
|
+
LATEST_NEXT=$(npm view ${PACKAGE_NAME}@next version 2>/dev/null || echo "none")
|
|
79
|
+
|
|
80
|
+
if [ "$LATEST_NEXT" != "none" ]; then
|
|
81
|
+
# Extract base version and pre-release number from latest next version
|
|
82
|
+
# Example: "0.3.0-next.5" -> extract "0.3.0" and "5"
|
|
83
|
+
if [[ "$LATEST_NEXT" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-next\.([0-9]+)$ ]]; then
|
|
84
|
+
LATEST_BASE="${BASH_REMATCH[1]}"
|
|
85
|
+
PRERELEASE_NUM="${BASH_REMATCH[2]}"
|
|
86
|
+
|
|
87
|
+
# If base version changed, reset to 1, otherwise increment
|
|
88
|
+
if [ "$LATEST_BASE" != "$BASE_VERSION" ]; then
|
|
89
|
+
NEW_NUM=1
|
|
90
|
+
else
|
|
91
|
+
NEW_NUM=$((PRERELEASE_NUM + 1))
|
|
92
|
+
fi
|
|
93
|
+
else
|
|
94
|
+
# Fallback: if format doesn't match, start at 1
|
|
95
|
+
NEW_NUM=1
|
|
96
|
+
fi
|
|
97
|
+
else
|
|
98
|
+
# First pre-release
|
|
99
|
+
NEW_NUM=1
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
NEW_VERSION="${BASE_VERSION}-next.${NEW_NUM}"
|
|
103
|
+
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
|
104
|
+
echo "Base version: $BASE_VERSION"
|
|
105
|
+
echo "Calculated next version: $NEW_VERSION"
|
|
106
|
+
|
|
107
|
+
- name: Update package.json version
|
|
108
|
+
run: |
|
|
109
|
+
# Use node to update version directly (yarn version creates git tags)
|
|
110
|
+
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json')); pkg.version = '${{ steps.version.outputs.new_version }}'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');"
|
|
111
|
+
|
|
112
|
+
- name: Publish to npm
|
|
113
|
+
run: npm publish --tag next --access public
|
|
114
|
+
# Uses OIDC token automatically (no NODE_AUTH_TOKEN needed)
|
|
115
|
+
# Requires npm 11.5.1+ and id-token: write permission (already set)
|
|
116
|
+
|
|
117
|
+
- name: Commit version bump
|
|
118
|
+
run: |
|
|
119
|
+
git config --local user.email "action@github.com"
|
|
120
|
+
git config --local user.name "GitHub Action"
|
|
121
|
+
git add package.json yarn.lock
|
|
122
|
+
git commit -m "chore: bump @rimori/react-client to ${{ steps.version.outputs.new_version }} [skip ci]"
|
|
123
|
+
git push
|
|
124
|
+
|
|
125
|
+
- name: Output published version
|
|
126
|
+
run: |
|
|
127
|
+
echo "✅ Published @rimori/react-client@${{ steps.version.outputs.new_version }} to npm with @next tag"
|
|
128
|
+
echo "Using @rimori/client@${{ steps.client-version.outputs.version }}"
|
|
129
|
+
|
|
@@ -2,12 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState, useEffect, useRef } from 'react';
|
|
3
3
|
const ContextMenu = ({ client }) => {
|
|
4
4
|
const [isOpen, setIsOpen] = useState(false);
|
|
5
|
-
const [actions, setActions] = useState([]);
|
|
6
5
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
7
|
-
const [openOnTextSelect, setOpenOnTextSelect] = useState(false);
|
|
8
6
|
const [menuWidth, setMenuWidth] = useState(0);
|
|
9
7
|
const menuRef = useRef(null);
|
|
10
8
|
const isMobile = window.innerWidth < 768;
|
|
9
|
+
const openOnTextSelect = client.plugin.getUserInfo().context_menu_on_select;
|
|
10
|
+
const actions = client.plugin
|
|
11
|
+
.getPluginInfo()
|
|
12
|
+
.installedPlugins.flatMap((p) => p.context_menu_actions)
|
|
13
|
+
.filter(Boolean);
|
|
11
14
|
/**
|
|
12
15
|
* Calculates position for mobile context menu based on selected text bounds.
|
|
13
16
|
* Centers the menu horizontally over the selected text and positions it 30px below the text's end.
|
|
@@ -29,14 +32,8 @@ const ContextMenu = ({ client }) => {
|
|
|
29
32
|
return { x: centerX, y: textEndY, text: selectedText };
|
|
30
33
|
};
|
|
31
34
|
useEffect(() => {
|
|
32
|
-
const actions = client.plugin
|
|
33
|
-
.getPluginInfo()
|
|
34
|
-
.installedPlugins.flatMap((p) => p.context_menu_actions)
|
|
35
|
-
.filter(Boolean);
|
|
36
|
-
setActions(actions);
|
|
37
|
-
setOpenOnTextSelect(client.plugin.getUserInfo().context_menu_on_select);
|
|
38
35
|
client.event.on('global.contextMenu.createActions', ({ data }) => {
|
|
39
|
-
|
|
36
|
+
actions.push(...data.actions);
|
|
40
37
|
});
|
|
41
38
|
}, []);
|
|
42
39
|
// Update menu width when menu is rendered
|
|
@@ -44,7 +41,7 @@ const ContextMenu = ({ client }) => {
|
|
|
44
41
|
if (isOpen && menuRef.current) {
|
|
45
42
|
setMenuWidth(menuRef.current.offsetWidth);
|
|
46
43
|
}
|
|
47
|
-
}, [isOpen
|
|
44
|
+
}, [isOpen]);
|
|
48
45
|
useEffect(() => {
|
|
49
46
|
// Track mouse position globally
|
|
50
47
|
const handleMouseMove = (e) => {
|
|
@@ -121,7 +118,7 @@ const ContextMenu = ({ client }) => {
|
|
|
121
118
|
if (!isOpen) {
|
|
122
119
|
return null;
|
|
123
120
|
}
|
|
124
|
-
return (_jsx("div", { ref: menuRef, className: "fixed bg-gray-400 dark:bg-gray-700 shadow-lg border border-gray-400 rounded-md overflow-hidden dark:text-white z-50", style: { top: position.y, left: position.x }, children: actions.map((action, index) => (_jsx(MenuEntryItem, {
|
|
121
|
+
return (_jsx("div", { ref: menuRef, className: "fixed bg-gray-400 dark:bg-gray-700 shadow-lg border border-gray-400 rounded-md overflow-hidden dark:text-white z-50", style: { top: position.y, left: position.x }, children: actions.map((action, index) => (_jsx(MenuEntryItem, { iconUrl: action.iconUrl, text: action.text, onClick: () => {
|
|
125
122
|
var _a;
|
|
126
123
|
setIsOpen(false);
|
|
127
124
|
(_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.removeAllRanges();
|
|
@@ -129,6 +126,6 @@ const ContextMenu = ({ client }) => {
|
|
|
129
126
|
} }, index))) }));
|
|
130
127
|
};
|
|
131
128
|
function MenuEntryItem(props) {
|
|
132
|
-
return (_jsxs("button", { onClick: props.onClick, className: "px-4 py-2 text-left hover:bg-gray-500 dark:hover:bg-gray-600 w-full flex flex-row", children: [_jsx("span", { className: "flex-grow", children: props.
|
|
129
|
+
return (_jsxs("button", { onClick: props.onClick, className: "px-4 py-2 text-left hover:bg-gray-500 dark:hover:bg-gray-600 w-full flex flex-row", children: [_jsx("span", { className: "flex-grow", children: props.iconUrl && _jsx("img", { src: props.iconUrl, alt: props.text, className: "w-4 h-4 mr-2" }) }), _jsx("span", { className: "flex-grow", children: props.text })] }));
|
|
133
130
|
}
|
|
134
131
|
export default ContextMenu;
|
|
@@ -19,6 +19,7 @@ export class ChunkedAudioPlayer {
|
|
|
19
19
|
this.currentIndex = 0;
|
|
20
20
|
this.startedPlaying = false;
|
|
21
21
|
this.onEndOfSpeech = () => { };
|
|
22
|
+
this.backgroundNoiseLevel = 30; // Background noise level that should be treated as baseline (0)
|
|
22
23
|
this.init();
|
|
23
24
|
}
|
|
24
25
|
init() {
|
|
@@ -162,9 +163,18 @@ export class ChunkedAudioPlayer {
|
|
|
162
163
|
if (loudnessInDb > maxDb) {
|
|
163
164
|
loudnessInDb = maxDb;
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
|
|
166
|
+
let loudnessScale = ((loudnessInDb - minDb) / (maxDb - minDb)) * 100;
|
|
167
|
+
// Adjust loudness: shift zero level up by background noise amount
|
|
168
|
+
// Values below background noise level are set to 0
|
|
169
|
+
// Values above are remapped to 0-100 scale
|
|
170
|
+
if (loudnessScale < this.backgroundNoiseLevel) {
|
|
171
|
+
loudnessScale = 0;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// Remap from [backgroundNoiseLevel, 100] to [0, 100]
|
|
175
|
+
loudnessScale = ((loudnessScale - this.backgroundNoiseLevel) / (100 - this.backgroundNoiseLevel)) * 100;
|
|
176
|
+
}
|
|
177
|
+
this.loudnessCallback(Math.round(loudnessScale));
|
|
168
178
|
}
|
|
169
179
|
// Call this method again at regular intervals if you want continuous loudness monitoring
|
|
170
180
|
this.handle = requestAnimationFrame(() => this.monitorLoudness());
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimori/react-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-next.1",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/rimori-org/react-client.git"
|
|
8
|
+
},
|
|
4
9
|
"main": "dist/index.js",
|
|
5
10
|
"types": "dist/index.d.ts",
|
|
6
11
|
"type": "module",
|
|
@@ -18,17 +23,19 @@
|
|
|
18
23
|
"format": "prettier --write ."
|
|
19
24
|
},
|
|
20
25
|
"peerDependencies": {
|
|
21
|
-
"
|
|
22
|
-
"react
|
|
23
|
-
"
|
|
26
|
+
"@rimori/client": "2.2.0-next.1",
|
|
27
|
+
"react": "^18.1.0",
|
|
28
|
+
"react-dom": "^18.1.0"
|
|
24
29
|
},
|
|
25
30
|
"dependencies": {
|
|
26
31
|
"html2canvas": "1.4.1",
|
|
27
|
-
"react-icons": "5.4.0"
|
|
32
|
+
"react-icons": "5.4.0",
|
|
33
|
+
"react-markdown": "^10.1.0"
|
|
28
34
|
},
|
|
29
35
|
"devDependencies": {
|
|
30
36
|
"@eslint/js": "^9.37.0",
|
|
31
|
-
"@rimori/client": "
|
|
37
|
+
"@rimori/client": "2.2.0-next.1",
|
|
38
|
+
"@types/react": "^18.3.21",
|
|
32
39
|
"eslint-config-prettier": "^10.1.8",
|
|
33
40
|
"eslint-plugin-prettier": "^5.5.4",
|
|
34
41
|
"eslint-plugin-react-hooks": "^7.0.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { RimoriClient, MenuEntry } from '@rimori/client';
|
|
3
3
|
|
|
4
4
|
export interface Position {
|
|
@@ -9,12 +9,15 @@ export interface Position {
|
|
|
9
9
|
|
|
10
10
|
const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
11
11
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
12
|
-
const [actions, setActions] = useState<MenuEntry[]>([]);
|
|
13
12
|
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
|
|
14
|
-
const [openOnTextSelect, setOpenOnTextSelect] = useState(false);
|
|
15
13
|
const [menuWidth, setMenuWidth] = useState<number>(0);
|
|
16
14
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
17
15
|
const isMobile = window.innerWidth < 768;
|
|
16
|
+
const openOnTextSelect = client.plugin.getUserInfo().context_menu_on_select;
|
|
17
|
+
const actions = client.plugin
|
|
18
|
+
.getPluginInfo()
|
|
19
|
+
.installedPlugins.flatMap((p) => p.context_menu_actions)
|
|
20
|
+
.filter(Boolean);
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Calculates position for mobile context menu based on selected text bounds.
|
|
@@ -23,7 +26,7 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
23
26
|
* @param menuWidth - The width of the menu to center properly
|
|
24
27
|
* @returns Position object with x and y coordinates
|
|
25
28
|
*/
|
|
26
|
-
const calculateMobilePosition = (selectedText: string, menuWidth
|
|
29
|
+
const calculateMobilePosition = (selectedText: string, menuWidth = 0): Position => {
|
|
27
30
|
const selection = window.getSelection();
|
|
28
31
|
if (!selection || !selectedText) {
|
|
29
32
|
return { x: 0, y: 0, text: selectedText };
|
|
@@ -42,15 +45,8 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
useEffect(() => {
|
|
45
|
-
const actions = client.plugin
|
|
46
|
-
.getPluginInfo()
|
|
47
|
-
.installedPlugins.flatMap((p) => p.context_menu_actions)
|
|
48
|
-
.filter(Boolean);
|
|
49
|
-
setActions(actions);
|
|
50
|
-
setOpenOnTextSelect(client.plugin.getUserInfo().context_menu_on_select);
|
|
51
|
-
|
|
52
48
|
client.event.on<{ actions: MenuEntry[] }>('global.contextMenu.createActions', ({ data }) => {
|
|
53
|
-
|
|
49
|
+
actions.push(...data.actions);
|
|
54
50
|
});
|
|
55
51
|
}, []);
|
|
56
52
|
|
|
@@ -59,11 +55,11 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
59
55
|
if (isOpen && menuRef.current) {
|
|
60
56
|
setMenuWidth(menuRef.current.offsetWidth);
|
|
61
57
|
}
|
|
62
|
-
}, [isOpen
|
|
58
|
+
}, [isOpen]);
|
|
63
59
|
|
|
64
60
|
useEffect(() => {
|
|
65
61
|
// Track mouse position globally
|
|
66
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
62
|
+
const handleMouseMove = (e: MouseEvent): void => {
|
|
67
63
|
const selectedText = window.getSelection()?.toString().trim();
|
|
68
64
|
if (isOpen && selectedText === position.text) return;
|
|
69
65
|
|
|
@@ -74,7 +70,7 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
74
70
|
}
|
|
75
71
|
};
|
|
76
72
|
|
|
77
|
-
const handleMouseUp = (e: MouseEvent) => {
|
|
73
|
+
const handleMouseUp = (e: MouseEvent): void => {
|
|
78
74
|
const selectedText = window.getSelection()?.toString().trim();
|
|
79
75
|
// Check if click is inside the context menu
|
|
80
76
|
if (menuRef.current && menuRef.current.contains(e.target as Node)) {
|
|
@@ -112,7 +108,7 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
112
108
|
};
|
|
113
109
|
|
|
114
110
|
// Add selectionchange listener to close menu if selection is cleared and update position for mobile
|
|
115
|
-
const handleSelectionChange = () => {
|
|
111
|
+
const handleSelectionChange = (): void => {
|
|
116
112
|
const selectedText = window.getSelection()?.toString().trim();
|
|
117
113
|
if (!selectedText && isOpen) {
|
|
118
114
|
setIsOpen(false);
|
|
@@ -148,7 +144,7 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
148
144
|
{actions.map((action, index) => (
|
|
149
145
|
<MenuEntryItem
|
|
150
146
|
key={index}
|
|
151
|
-
|
|
147
|
+
iconUrl={action.iconUrl}
|
|
152
148
|
text={action.text}
|
|
153
149
|
onClick={() => {
|
|
154
150
|
setIsOpen(false);
|
|
@@ -161,13 +157,15 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
161
157
|
);
|
|
162
158
|
};
|
|
163
159
|
|
|
164
|
-
function MenuEntryItem(props: {
|
|
160
|
+
function MenuEntryItem(props: { iconUrl?: string; text: string; onClick: () => void }): JSX.Element {
|
|
165
161
|
return (
|
|
166
162
|
<button
|
|
167
163
|
onClick={props.onClick}
|
|
168
164
|
className="px-4 py-2 text-left hover:bg-gray-500 dark:hover:bg-gray-600 w-full flex flex-row"
|
|
169
165
|
>
|
|
170
|
-
<span className="flex-grow">
|
|
166
|
+
<span className="flex-grow">
|
|
167
|
+
{props.iconUrl && <img src={props.iconUrl} alt={props.text} className="w-4 h-4 mr-2" />}
|
|
168
|
+
</span>
|
|
171
169
|
<span className="flex-grow">{props.text}</span>
|
|
172
170
|
{/* <span className="text-sm">Ctrl+Shift+xxxx</span> */}
|
|
173
171
|
</button>
|
|
@@ -12,6 +12,7 @@ export class ChunkedAudioPlayer {
|
|
|
12
12
|
private currentIndex = 0;
|
|
13
13
|
private startedPlaying = false;
|
|
14
14
|
private onEndOfSpeech: () => void = () => {};
|
|
15
|
+
private readonly backgroundNoiseLevel = 30; // Background noise level that should be treated as baseline (0)
|
|
15
16
|
|
|
16
17
|
constructor() {
|
|
17
18
|
this.init();
|
|
@@ -171,10 +172,19 @@ export class ChunkedAudioPlayer {
|
|
|
171
172
|
loudnessInDb = maxDb;
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
|
|
175
|
-
// console.log("root:corrent loudness", loudnessScale);
|
|
175
|
+
let loudnessScale = ((loudnessInDb - minDb) / (maxDb - minDb)) * 100;
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
// Adjust loudness: shift zero level up by background noise amount
|
|
178
|
+
// Values below background noise level are set to 0
|
|
179
|
+
// Values above are remapped to 0-100 scale
|
|
180
|
+
if (loudnessScale < this.backgroundNoiseLevel) {
|
|
181
|
+
loudnessScale = 0;
|
|
182
|
+
} else {
|
|
183
|
+
// Remap from [backgroundNoiseLevel, 100] to [0, 100]
|
|
184
|
+
loudnessScale = ((loudnessScale - this.backgroundNoiseLevel) / (100 - this.backgroundNoiseLevel)) * 100;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.loudnessCallback(Math.round(loudnessScale));
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
// Call this method again at regular intervals if you want continuous loudness monitoring
|
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,11 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,95 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
}
|