@rimori/react-client 0.3.0 → 0.4.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/.github/workflows/pre-release.yml +149 -0
- package/dist/components/ContextMenu.js +7 -10
- package/dist/components/ai/Assistant.js +6 -0
- package/dist/components/ai/Avatar.js +6 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.d.ts +1 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/MessageSender.js +3 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.d.ts +2 -0
- package/dist/components/ai/EmbeddedAssistent/TTS/Player.js +23 -3
- package/dist/components/audio/Playbutton.js +52 -4
- package/dist/providers/PluginProvider.js +17 -23
- package/package.json +13 -6
- package/src/components/ContextMenu.tsx +13 -17
- package/src/components/ai/Assistant.tsx +7 -0
- package/src/components/ai/Avatar.tsx +7 -0
- package/src/components/ai/EmbeddedAssistent/TTS/MessageSender.ts +4 -0
- package/src/components/ai/EmbeddedAssistent/TTS/Player.ts +24 -3
- package/src/components/audio/Playbutton.tsx +58 -4
- package/src/providers/PluginProvider.tsx +22 -28
|
@@ -0,0 +1,149 @@
|
|
|
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
|
|
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: Output published version
|
|
118
|
+
run: |
|
|
119
|
+
echo "✅ Published @rimori/react-client@${{ steps.version.outputs.new_version }} to npm with @next tag"
|
|
120
|
+
echo "Using @rimori/client@${{ steps.client-version.outputs.version }}"
|
|
121
|
+
|
|
122
|
+
- name: Create git tag
|
|
123
|
+
run: |
|
|
124
|
+
git config --local user.email "action@github.com"
|
|
125
|
+
git config --local user.name "GitHub Action"
|
|
126
|
+
git tag "v${{ steps.version.outputs.new_version }}" -m "Pre-release v${{ steps.version.outputs.new_version }}"
|
|
127
|
+
git push origin "v${{ steps.version.outputs.new_version }}"
|
|
128
|
+
echo "🏷️ Created and pushed tag v${{ steps.version.outputs.new_version }}"
|
|
129
|
+
|
|
130
|
+
- name: Notify Slack
|
|
131
|
+
if: always()
|
|
132
|
+
uses: slackapi/slack-github-action@v1.24.0
|
|
133
|
+
with:
|
|
134
|
+
channel-id: ${{ secrets.SLACK_CHANNEL_ID }}
|
|
135
|
+
payload: |
|
|
136
|
+
{
|
|
137
|
+
"text": "Pre-Release Pipeline Status",
|
|
138
|
+
"blocks": [
|
|
139
|
+
{
|
|
140
|
+
"type": "section",
|
|
141
|
+
"text": {
|
|
142
|
+
"type": "mrkdwn",
|
|
143
|
+
"text": "📦 *@rimori/react-client Pre-Release*\n\n*Branch:* ${{ github.ref_name }}\n*Version:* ${{ steps.version.outputs.new_version }}\n*Using @rimori/client:* ${{ steps.client-version.outputs.version }}\n*Author:* ${{ github.actor }}\n*Pipeline:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>\n\n${{ job.status == 'success' && '✅ Successfully published to npm with @next tag!' || '❌ Pipeline failed. Check the logs for details.' }}"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
env:
|
|
149
|
+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
|
@@ -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) => {
|
|
@@ -26,6 +26,12 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
|
|
|
26
26
|
sender.handleNewText(autoStartConversation.assistantMessage, isLoading);
|
|
27
27
|
}
|
|
28
28
|
}, []);
|
|
29
|
+
// Cleanup: stop audio playback and clean up resources on unmount
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
sender.cleanup();
|
|
33
|
+
};
|
|
34
|
+
}, [sender]);
|
|
29
35
|
useEffect(() => {
|
|
30
36
|
var _a;
|
|
31
37
|
let message = lastAssistantMessage;
|
|
@@ -38,6 +38,12 @@ export function Avatar({ avatarImageUrl, voiceId, agentTools, autoStartConversat
|
|
|
38
38
|
append([{ role: 'user', content: autoStartConversation.userMessage, id: messages.length.toString() }]);
|
|
39
39
|
}
|
|
40
40
|
}, [autoStartConversation, voiceId]);
|
|
41
|
+
// Cleanup: stop audio playback and clean up resources on unmount
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
return () => {
|
|
44
|
+
sender.cleanup();
|
|
45
|
+
};
|
|
46
|
+
}, [sender]);
|
|
41
47
|
useEffect(() => {
|
|
42
48
|
if ((lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.role) === 'assistant') {
|
|
43
49
|
sender.handleNewText(lastMessage.content, isLoading);
|
|
@@ -12,6 +12,7 @@ export declare class ChunkedAudioPlayer {
|
|
|
12
12
|
private currentIndex;
|
|
13
13
|
private startedPlaying;
|
|
14
14
|
private onEndOfSpeech;
|
|
15
|
+
private readonly backgroundNoiseLevel;
|
|
15
16
|
constructor();
|
|
16
17
|
private init;
|
|
17
18
|
setOnLoudnessChange(callback: (value: number) => void): void;
|
|
@@ -19,6 +20,7 @@ export declare class ChunkedAudioPlayer {
|
|
|
19
20
|
addChunk(chunk: ArrayBuffer, position: number): Promise<void>;
|
|
20
21
|
private playChunks;
|
|
21
22
|
stopPlayback(): void;
|
|
23
|
+
cleanup(): void;
|
|
22
24
|
private playChunk;
|
|
23
25
|
playAgain(): Promise<void>;
|
|
24
26
|
private monitorLoudness;
|
|
@@ -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() {
|
|
@@ -88,6 +89,16 @@ export class ChunkedAudioPlayer {
|
|
|
88
89
|
this.shouldMonitorLoudness = false;
|
|
89
90
|
cancelAnimationFrame(this.handle);
|
|
90
91
|
}
|
|
92
|
+
cleanup() {
|
|
93
|
+
// Stop playback first
|
|
94
|
+
this.stopPlayback();
|
|
95
|
+
// Close AudioContext to free resources
|
|
96
|
+
if (this.audioContext && this.audioContext.state !== 'closed') {
|
|
97
|
+
this.audioContext.close().catch((e) => {
|
|
98
|
+
console.warn('Error closing AudioContext:', e);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
91
102
|
playChunk(chunk) {
|
|
92
103
|
// console.log({queue: this.chunkQueue})
|
|
93
104
|
if (!chunk) {
|
|
@@ -162,9 +173,18 @@ export class ChunkedAudioPlayer {
|
|
|
162
173
|
if (loudnessInDb > maxDb) {
|
|
163
174
|
loudnessInDb = maxDb;
|
|
164
175
|
}
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
|
|
176
|
+
let loudnessScale = ((loudnessInDb - minDb) / (maxDb - minDb)) * 100;
|
|
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
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Remap from [backgroundNoiseLevel, 100] to [0, 100]
|
|
185
|
+
loudnessScale = ((loudnessScale - this.backgroundNoiseLevel) / (100 - this.backgroundNoiseLevel)) * 100;
|
|
186
|
+
}
|
|
187
|
+
this.loudnessCallback(Math.round(loudnessScale));
|
|
168
188
|
}
|
|
169
189
|
// Call this method again at regular intervals if you want continuous loudness monitoring
|
|
170
190
|
this.handle = requestAnimationFrame(() => this.monitorLoudness());
|
|
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { useState, useEffect } from 'react';
|
|
11
|
+
import { useState, useEffect, useRef } from 'react';
|
|
12
12
|
import { FaPlayCircle, FaStopCircle } from 'react-icons/fa';
|
|
13
13
|
import { useRimori } from '../../providers/PluginProvider';
|
|
14
14
|
import { EventBus } from '@rimori/client';
|
|
@@ -20,6 +20,8 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
20
20
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
21
21
|
const [isLoading, setIsLoading] = useState(false);
|
|
22
22
|
const { ai } = useRimori();
|
|
23
|
+
const audioRef = useRef(null);
|
|
24
|
+
const eventBusListenerRef = useRef(null);
|
|
23
25
|
useEffect(() => {
|
|
24
26
|
if (audioUrl)
|
|
25
27
|
setAudioUrl(null);
|
|
@@ -37,9 +39,22 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
37
39
|
});
|
|
38
40
|
// Effect to play audio when audioUrl changes and play state is true
|
|
39
41
|
useEffect(() => {
|
|
40
|
-
if (!audioUrl || !isPlaying)
|
|
42
|
+
if (!audioUrl || !isPlaying) {
|
|
43
|
+
// Stop any existing audio when not playing
|
|
44
|
+
if (audioRef.current) {
|
|
45
|
+
audioRef.current.pause();
|
|
46
|
+
audioRef.current.currentTime = 0;
|
|
47
|
+
audioRef.current = null;
|
|
48
|
+
}
|
|
41
49
|
return;
|
|
50
|
+
}
|
|
51
|
+
// Clean up previous audio instance if it exists
|
|
52
|
+
if (audioRef.current) {
|
|
53
|
+
audioRef.current.pause();
|
|
54
|
+
audioRef.current.currentTime = 0;
|
|
55
|
+
}
|
|
42
56
|
const audio = new Audio(audioUrl);
|
|
57
|
+
audioRef.current = audio;
|
|
43
58
|
audio.playbackRate = speed;
|
|
44
59
|
audio
|
|
45
60
|
.play()
|
|
@@ -47,16 +62,41 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
47
62
|
audio.onended = () => {
|
|
48
63
|
setIsPlaying(false);
|
|
49
64
|
isFetchingAudio = false;
|
|
65
|
+
audioRef.current = null;
|
|
50
66
|
};
|
|
51
67
|
})
|
|
52
68
|
.catch((e) => {
|
|
53
69
|
console.warn('Error playing audio:', e);
|
|
54
70
|
setIsPlaying(false);
|
|
71
|
+
audioRef.current = null;
|
|
55
72
|
});
|
|
56
73
|
return () => {
|
|
57
|
-
|
|
74
|
+
if (audioRef.current) {
|
|
75
|
+
audioRef.current.pause();
|
|
76
|
+
audioRef.current.currentTime = 0;
|
|
77
|
+
audioRef.current = null;
|
|
78
|
+
}
|
|
58
79
|
};
|
|
59
80
|
}, [audioUrl, isPlaying, speed]);
|
|
81
|
+
// Cleanup on unmount - stop audio and revoke object URL
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
return () => {
|
|
84
|
+
if (audioRef.current) {
|
|
85
|
+
audioRef.current.pause();
|
|
86
|
+
audioRef.current.currentTime = 0;
|
|
87
|
+
audioRef.current = null;
|
|
88
|
+
}
|
|
89
|
+
setIsPlaying(false);
|
|
90
|
+
};
|
|
91
|
+
}, []);
|
|
92
|
+
// Cleanup audioUrl on unmount
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
return () => {
|
|
95
|
+
if (audioUrl) {
|
|
96
|
+
URL.revokeObjectURL(audioUrl);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}, [audioUrl]);
|
|
60
100
|
const togglePlayback = () => {
|
|
61
101
|
if (!isPlaying && !audioUrl) {
|
|
62
102
|
generateAudio().then(() => setIsPlaying(true));
|
|
@@ -68,7 +108,15 @@ export const AudioPlayer = ({ text, voice, language, hide, playListenerEvent, in
|
|
|
68
108
|
useEffect(() => {
|
|
69
109
|
if (!playListenerEvent)
|
|
70
110
|
return;
|
|
71
|
-
|
|
111
|
+
const handler = () => togglePlayback();
|
|
112
|
+
const listener = EventBus.on(playListenerEvent, handler);
|
|
113
|
+
eventBusListenerRef.current = listener;
|
|
114
|
+
return () => {
|
|
115
|
+
if (eventBusListenerRef.current) {
|
|
116
|
+
eventBusListenerRef.current.off();
|
|
117
|
+
eventBusListenerRef.current = null;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
72
120
|
}, [playListenerEvent]);
|
|
73
121
|
useEffect(() => {
|
|
74
122
|
if (!playOnMount || isFetchingAudio)
|
|
@@ -14,7 +14,7 @@ import ContextMenu from '../components/ContextMenu';
|
|
|
14
14
|
import { useTheme } from '../hooks/ThemeSetter';
|
|
15
15
|
const PluginContext = createContext(null);
|
|
16
16
|
export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
17
|
-
const [
|
|
17
|
+
const [client, setClient] = useState(null);
|
|
18
18
|
const [standaloneClient, setStandaloneClient] = useState(false);
|
|
19
19
|
const [applicationMode, setApplicationMode] = useState(null);
|
|
20
20
|
const [theme, setTheme] = useState(null);
|
|
@@ -30,9 +30,9 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
30
30
|
void client.needsLogin().then((needLogin) => setStandaloneClient(needLogin ? client : true));
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
-
if ((!standaloneDetected && !
|
|
33
|
+
if ((!standaloneDetected && !client) || (standaloneDetected && standaloneClient === true)) {
|
|
34
34
|
void RimoriClient.getInstance(pluginId).then((client) => {
|
|
35
|
-
|
|
35
|
+
setClient(client);
|
|
36
36
|
// Get applicationMode and theme from MessageChannel query params
|
|
37
37
|
if (!standaloneDetected) {
|
|
38
38
|
const mode = client.getQueryParam('applicationMode');
|
|
@@ -43,36 +43,30 @@ export const PluginProvider = ({ children, pluginId, settings }) => {
|
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
-
}, [pluginId, standaloneClient,
|
|
47
|
-
//route change
|
|
46
|
+
}, [pluginId, standaloneClient, client]);
|
|
48
47
|
useEffect(() => {
|
|
49
|
-
if (!
|
|
48
|
+
if (!client)
|
|
50
49
|
return;
|
|
51
|
-
//sidebar pages should not report url changes
|
|
52
50
|
if (isSidebar)
|
|
53
|
-
return;
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}, 1000);
|
|
63
|
-
emitUrlChange(lastHash);
|
|
64
|
-
return () => clearInterval(interval);
|
|
65
|
-
}, [plugin, isSidebar]);
|
|
51
|
+
return; //sidebar pages should not report url changes
|
|
52
|
+
// react router overwrites native pushstate so it gets wrapped to detect url changes
|
|
53
|
+
const originalPushState = history.pushState;
|
|
54
|
+
history.pushState = (...args) => {
|
|
55
|
+
const result = originalPushState.apply(history, args);
|
|
56
|
+
client.event.emit('session.triggerUrlChange', { url: location.hash });
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
}, [client, isSidebar]);
|
|
66
60
|
if (standaloneClient instanceof StandaloneClient) {
|
|
67
61
|
return (_jsx(StandaloneAuth, { onLogin: (email, password) => __awaiter(void 0, void 0, void 0, function* () {
|
|
68
62
|
if (yield standaloneClient.login(email, password))
|
|
69
63
|
setStandaloneClient(true);
|
|
70
64
|
}) }));
|
|
71
65
|
}
|
|
72
|
-
if (!
|
|
66
|
+
if (!client) {
|
|
73
67
|
return '';
|
|
74
68
|
}
|
|
75
|
-
return (_jsxs(PluginContext.Provider, { value:
|
|
69
|
+
return (_jsxs(PluginContext.Provider, { value: client, children: [!(settings === null || settings === void 0 ? void 0 : settings.disableContextMenu) && !isSidebar && !isSettings && _jsx(ContextMenu, { client: client }), children] }));
|
|
76
70
|
};
|
|
77
71
|
export const useRimori = () => {
|
|
78
72
|
const context = useContext(PluginContext);
|
|
@@ -118,7 +112,7 @@ function StandaloneAuth({ onLogin }) {
|
|
|
118
112
|
display: 'flex',
|
|
119
113
|
alignItems: 'center',
|
|
120
114
|
justifyContent: 'center',
|
|
121
|
-
}, children: [_jsx("p", { style: { fontSize: '2rem', fontWeight: 'bold', marginBottom: '1rem', textAlign: 'center' }, children: "Rimori Login" }), _jsx("p", { style: { marginBottom: '1rem', textAlign: 'center' }, children: "Please login with your Rimori developer account for this plugin to be able to access the Rimori platform the same it
|
|
115
|
+
}, children: [_jsx("p", { style: { fontSize: '2rem', fontWeight: 'bold', marginBottom: '1rem', textAlign: 'center' }, children: "Rimori Login Required" }), _jsx("p", { style: { marginBottom: '1rem', textAlign: 'center' }, children: "Please login with your Rimori developer account for this plugin to be able to access the Rimori platform the same way it would when being deployed." }), _jsx("input", { style: {
|
|
122
116
|
marginBottom: '1rem',
|
|
123
117
|
width: '100%',
|
|
124
118
|
padding: '0.5rem',
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimori/react-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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.4.0",
|
|
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": "^2.
|
|
37
|
+
"@rimori/client": "^2.4.0",
|
|
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);
|
|
@@ -161,7 +157,7 @@ const ContextMenu = ({ client }: { client: RimoriClient }) => {
|
|
|
161
157
|
);
|
|
162
158
|
};
|
|
163
159
|
|
|
164
|
-
function MenuEntryItem(props: { iconUrl?: string; text: string; onClick: () => void }) {
|
|
160
|
+
function MenuEntryItem(props: { iconUrl?: string; text: string; onClick: () => void }): JSX.Element {
|
|
165
161
|
return (
|
|
166
162
|
<button
|
|
167
163
|
onClick={props.onClick}
|
|
@@ -38,6 +38,13 @@ export function AssistantChat({ avatarImageUrl, voiceId, onComplete, autoStartCo
|
|
|
38
38
|
}
|
|
39
39
|
}, []);
|
|
40
40
|
|
|
41
|
+
// Cleanup: stop audio playback and clean up resources on unmount
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
return () => {
|
|
44
|
+
sender.cleanup();
|
|
45
|
+
};
|
|
46
|
+
}, [sender]);
|
|
47
|
+
|
|
41
48
|
useEffect(() => {
|
|
42
49
|
let message = lastAssistantMessage;
|
|
43
50
|
if (message !== messages[messages.length - 1]?.content) {
|
|
@@ -61,6 +61,13 @@ export function Avatar({
|
|
|
61
61
|
}
|
|
62
62
|
}, [autoStartConversation, voiceId]);
|
|
63
63
|
|
|
64
|
+
// Cleanup: stop audio playback and clean up resources on unmount
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
return () => {
|
|
67
|
+
sender.cleanup();
|
|
68
|
+
};
|
|
69
|
+
}, [sender]);
|
|
70
|
+
|
|
64
71
|
useEffect(() => {
|
|
65
72
|
if (lastMessage?.role === 'assistant') {
|
|
66
73
|
sender.handleNewText(lastMessage.content, isLoading);
|
|
@@ -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();
|
|
@@ -87,6 +88,17 @@ export class ChunkedAudioPlayer {
|
|
|
87
88
|
cancelAnimationFrame(this.handle);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
public cleanup(): void {
|
|
92
|
+
// Stop playback first
|
|
93
|
+
this.stopPlayback();
|
|
94
|
+
// Close AudioContext to free resources
|
|
95
|
+
if (this.audioContext && this.audioContext.state !== 'closed') {
|
|
96
|
+
this.audioContext.close().catch((e) => {
|
|
97
|
+
console.warn('Error closing AudioContext:', e);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
private playChunk(chunk: ArrayBuffer): Promise<void> {
|
|
91
103
|
// console.log({queue: this.chunkQueue})
|
|
92
104
|
if (!chunk) {
|
|
@@ -171,10 +183,19 @@ export class ChunkedAudioPlayer {
|
|
|
171
183
|
loudnessInDb = maxDb;
|
|
172
184
|
}
|
|
173
185
|
|
|
174
|
-
|
|
175
|
-
|
|
186
|
+
let loudnessScale = ((loudnessInDb - minDb) / (maxDb - minDb)) * 100;
|
|
187
|
+
|
|
188
|
+
// Adjust loudness: shift zero level up by background noise amount
|
|
189
|
+
// Values below background noise level are set to 0
|
|
190
|
+
// Values above are remapped to 0-100 scale
|
|
191
|
+
if (loudnessScale < this.backgroundNoiseLevel) {
|
|
192
|
+
loudnessScale = 0;
|
|
193
|
+
} else {
|
|
194
|
+
// Remap from [backgroundNoiseLevel, 100] to [0, 100]
|
|
195
|
+
loudnessScale = ((loudnessScale - this.backgroundNoiseLevel) / (100 - this.backgroundNoiseLevel)) * 100;
|
|
196
|
+
}
|
|
176
197
|
|
|
177
|
-
this.loudnessCallback(loudnessScale);
|
|
198
|
+
this.loudnessCallback(Math.round(loudnessScale));
|
|
178
199
|
}
|
|
179
200
|
|
|
180
201
|
// Call this method again at regular intervals if you want continuous loudness monitoring
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { FaPlayCircle, FaStopCircle } from 'react-icons/fa';
|
|
3
3
|
import { useRimori } from '../../providers/PluginProvider';
|
|
4
4
|
import { EventBus } from '@rimori/client';
|
|
@@ -34,6 +34,8 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
34
34
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
35
35
|
const [isLoading, setIsLoading] = useState(false);
|
|
36
36
|
const { ai } = useRimori();
|
|
37
|
+
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
38
|
+
const eventBusListenerRef = useRef<{ off: () => void } | null>(null);
|
|
37
39
|
|
|
38
40
|
useEffect(() => {
|
|
39
41
|
if (audioUrl) setAudioUrl(null);
|
|
@@ -53,8 +55,24 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
53
55
|
|
|
54
56
|
// Effect to play audio when audioUrl changes and play state is true
|
|
55
57
|
useEffect(() => {
|
|
56
|
-
if (!audioUrl || !isPlaying)
|
|
58
|
+
if (!audioUrl || !isPlaying) {
|
|
59
|
+
// Stop any existing audio when not playing
|
|
60
|
+
if (audioRef.current) {
|
|
61
|
+
audioRef.current.pause();
|
|
62
|
+
audioRef.current.currentTime = 0;
|
|
63
|
+
audioRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Clean up previous audio instance if it exists
|
|
69
|
+
if (audioRef.current) {
|
|
70
|
+
audioRef.current.pause();
|
|
71
|
+
audioRef.current.currentTime = 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
const audio = new Audio(audioUrl);
|
|
75
|
+
audioRef.current = audio;
|
|
58
76
|
audio.playbackRate = speed;
|
|
59
77
|
audio
|
|
60
78
|
.play()
|
|
@@ -62,18 +80,45 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
62
80
|
audio.onended = () => {
|
|
63
81
|
setIsPlaying(false);
|
|
64
82
|
isFetchingAudio = false;
|
|
83
|
+
audioRef.current = null;
|
|
65
84
|
};
|
|
66
85
|
})
|
|
67
86
|
.catch((e) => {
|
|
68
87
|
console.warn('Error playing audio:', e);
|
|
69
88
|
setIsPlaying(false);
|
|
89
|
+
audioRef.current = null;
|
|
70
90
|
});
|
|
71
91
|
|
|
72
92
|
return () => {
|
|
73
|
-
|
|
93
|
+
if (audioRef.current) {
|
|
94
|
+
audioRef.current.pause();
|
|
95
|
+
audioRef.current.currentTime = 0;
|
|
96
|
+
audioRef.current = null;
|
|
97
|
+
}
|
|
74
98
|
};
|
|
75
99
|
}, [audioUrl, isPlaying, speed]);
|
|
76
100
|
|
|
101
|
+
// Cleanup on unmount - stop audio and revoke object URL
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
return () => {
|
|
104
|
+
if (audioRef.current) {
|
|
105
|
+
audioRef.current.pause();
|
|
106
|
+
audioRef.current.currentTime = 0;
|
|
107
|
+
audioRef.current = null;
|
|
108
|
+
}
|
|
109
|
+
setIsPlaying(false);
|
|
110
|
+
};
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
// Cleanup audioUrl on unmount
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
return () => {
|
|
116
|
+
if (audioUrl) {
|
|
117
|
+
URL.revokeObjectURL(audioUrl);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}, [audioUrl]);
|
|
121
|
+
|
|
77
122
|
const togglePlayback = () => {
|
|
78
123
|
if (!isPlaying && !audioUrl) {
|
|
79
124
|
generateAudio().then(() => setIsPlaying(true));
|
|
@@ -84,7 +129,16 @@ export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
|
84
129
|
|
|
85
130
|
useEffect(() => {
|
|
86
131
|
if (!playListenerEvent) return;
|
|
87
|
-
|
|
132
|
+
const handler = () => togglePlayback();
|
|
133
|
+
const listener = EventBus.on(playListenerEvent, handler);
|
|
134
|
+
eventBusListenerRef.current = listener;
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
if (eventBusListenerRef.current) {
|
|
138
|
+
eventBusListenerRef.current.off();
|
|
139
|
+
eventBusListenerRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
88
142
|
}, [playListenerEvent]);
|
|
89
143
|
|
|
90
144
|
useEffect(() => {
|
|
@@ -14,7 +14,7 @@ interface PluginProviderProps {
|
|
|
14
14
|
const PluginContext = createContext<RimoriClient | null>(null);
|
|
15
15
|
|
|
16
16
|
export const PluginProvider: React.FC<PluginProviderProps> = ({ children, pluginId, settings }) => {
|
|
17
|
-
const [
|
|
17
|
+
const [client, setClient] = useState<RimoriClient | null>(null);
|
|
18
18
|
const [standaloneClient, setStandaloneClient] = useState<StandaloneClient | boolean>(false);
|
|
19
19
|
const [applicationMode, setApplicationMode] = useState<string | null>(null);
|
|
20
20
|
const [theme, setTheme] = useState<string | null>(null);
|
|
@@ -36,9 +36,9 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
if ((!standaloneDetected && !
|
|
39
|
+
if ((!standaloneDetected && !client) || (standaloneDetected && standaloneClient === true)) {
|
|
40
40
|
void RimoriClient.getInstance(pluginId).then((client) => {
|
|
41
|
-
|
|
41
|
+
setClient(client);
|
|
42
42
|
// Get applicationMode and theme from MessageChannel query params
|
|
43
43
|
if (!standaloneDetected) {
|
|
44
44
|
const mode = client.getQueryParam('applicationMode');
|
|
@@ -49,28 +49,20 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
-
}, [pluginId, standaloneClient,
|
|
52
|
+
}, [pluginId, standaloneClient, client]);
|
|
53
53
|
|
|
54
|
-
//route change
|
|
55
54
|
useEffect(() => {
|
|
56
|
-
if (!
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// console.log('url changed:', lastHash);
|
|
68
|
-
emitUrlChange(lastHash);
|
|
69
|
-
}, 1000);
|
|
70
|
-
|
|
71
|
-
emitUrlChange(lastHash);
|
|
72
|
-
return () => clearInterval(interval);
|
|
73
|
-
}, [plugin, isSidebar]);
|
|
55
|
+
if (!client) return;
|
|
56
|
+
if (isSidebar) return; //sidebar pages should not report url changes
|
|
57
|
+
|
|
58
|
+
// react router overwrites native pushstate so it gets wrapped to detect url changes
|
|
59
|
+
const originalPushState = history.pushState;
|
|
60
|
+
history.pushState = (...args) => {
|
|
61
|
+
const result = originalPushState.apply(history, args);
|
|
62
|
+
client.event.emit('session.triggerUrlChange', { url: location.hash });
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
}, [client, isSidebar]);
|
|
74
66
|
|
|
75
67
|
if (standaloneClient instanceof StandaloneClient) {
|
|
76
68
|
return (
|
|
@@ -82,13 +74,13 @@ export const PluginProvider: React.FC<PluginProviderProps> = ({ children, plugin
|
|
|
82
74
|
);
|
|
83
75
|
}
|
|
84
76
|
|
|
85
|
-
if (!
|
|
77
|
+
if (!client) {
|
|
86
78
|
return '';
|
|
87
79
|
}
|
|
88
80
|
|
|
89
81
|
return (
|
|
90
|
-
<PluginContext.Provider value={
|
|
91
|
-
{!settings?.disableContextMenu && !isSidebar && !isSettings && <ContextMenu client={
|
|
82
|
+
<PluginContext.Provider value={client}>
|
|
83
|
+
{!settings?.disableContextMenu && !isSidebar && !isSettings && <ContextMenu client={client} />}
|
|
92
84
|
{children}
|
|
93
85
|
</PluginContext.Provider>
|
|
94
86
|
);
|
|
@@ -148,10 +140,12 @@ function StandaloneAuth({ onLogin }: { onLogin: (user: string, password: string)
|
|
|
148
140
|
justifyContent: 'center',
|
|
149
141
|
}}
|
|
150
142
|
>
|
|
151
|
-
<p style={{ fontSize: '2rem', fontWeight: 'bold', marginBottom: '1rem', textAlign: 'center' }}>
|
|
143
|
+
<p style={{ fontSize: '2rem', fontWeight: 'bold', marginBottom: '1rem', textAlign: 'center' }}>
|
|
144
|
+
Rimori Login Required
|
|
145
|
+
</p>
|
|
152
146
|
<p style={{ marginBottom: '1rem', textAlign: 'center' }}>
|
|
153
147
|
Please login with your Rimori developer account for this plugin to be able to access the Rimori platform the
|
|
154
|
-
same it
|
|
148
|
+
same way it would when being deployed.
|
|
155
149
|
</p>
|
|
156
150
|
{/* email and password input */}
|
|
157
151
|
<input
|