@rimori/client 1.0.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/CRUDModal.d.ts +16 -0
- package/dist/CRUDModal.js +31 -0
- package/dist/MarkdownEditor.d.ts +8 -0
- package/dist/MarkdownEditor.js +46 -0
- package/dist/audio/Playbutton.d.ts +14 -0
- package/dist/audio/Playbutton.js +73 -0
- package/dist/components/CRUDModal.d.ts +17 -0
- package/dist/components/CRUDModal.js +25 -0
- package/dist/components/MarkdownEditor.d.ts +8 -0
- package/dist/components/MarkdownEditor.js +46 -0
- package/dist/components/Spinner.d.ts +8 -0
- package/dist/components/Spinner.js +5 -0
- package/dist/components/audio/Playbutton.d.ts +15 -0
- package/dist/components/audio/Playbutton.js +78 -0
- package/dist/components/hooks/UseChatHook.d.ts +15 -0
- package/dist/components/hooks/UseChatHook.js +21 -0
- package/dist/controller/AIController.d.ts +22 -0
- package/dist/controller/AIController.js +68 -0
- package/dist/controller/ObjectController.d.ts +34 -0
- package/dist/controller/ObjectController.js +77 -0
- package/dist/controller/SettingsController.d.ts +24 -0
- package/dist/controller/SettingsController.js +72 -0
- package/dist/controller/SharedContentController.d.ts +22 -0
- package/dist/controller/SharedContentController.js +56 -0
- package/dist/controller/VoiceController.d.ts +10 -0
- package/dist/controller/VoiceController.js +28 -0
- package/dist/hooks/UseChatHook.d.ts +9 -0
- package/dist/hooks/UseChatHook.js +21 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/plugin/AIController copy.d.ts +22 -0
- package/dist/plugin/AIController copy.js +68 -0
- package/dist/plugin/AIController.d.ts +22 -0
- package/dist/plugin/AIController.js +68 -0
- package/dist/plugin/ObjectController.d.ts +34 -0
- package/dist/plugin/ObjectController.js +77 -0
- package/dist/plugin/PluginController.d.ts +29 -0
- package/dist/plugin/PluginController.js +138 -0
- package/dist/plugin/RimoriClient.d.ts +91 -0
- package/dist/plugin/RimoriClient.js +163 -0
- package/dist/plugin/SettingController.d.ts +13 -0
- package/dist/plugin/SettingController.js +55 -0
- package/dist/plugin/ThemeSetter.d.ts +1 -0
- package/dist/plugin/ThemeSetter.js +13 -0
- package/dist/plugin/VoiceController.d.ts +2 -0
- package/dist/plugin/VoiceController.js +27 -0
- package/dist/providers/EventEmitter.d.ts +11 -0
- package/dist/providers/EventEmitter.js +41 -0
- package/dist/providers/EventEmitterContext.d.ts +6 -0
- package/dist/providers/EventEmitterContext.js +19 -0
- package/dist/providers/PluginProvider.d.ts +8 -0
- package/dist/providers/PluginProvider.js +52 -0
- package/dist/style.css +110 -0
- package/dist/style.css.map +1 -0
- package/dist/utils/DifficultyConverter.d.ts +3 -0
- package/dist/utils/DifficultyConverter.js +7 -0
- package/dist/utils/PluginUtils.d.ts +2 -0
- package/dist/utils/PluginUtils.js +23 -0
- package/dist/utils/constants.d.ts +4 -0
- package/dist/utils/constants.js +12 -0
- package/dist/utils/difficultyConverter.d.ts +3 -0
- package/dist/utils/difficultyConverter.js +7 -0
- package/dist/utils/plugin/Client.d.ts +72 -0
- package/dist/utils/plugin/Client.js +118 -0
- package/dist/utils/plugin/PluginController.d.ts +36 -0
- package/dist/utils/plugin/PluginController.js +119 -0
- package/dist/utils/plugin/PluginUtils.d.ts +2 -0
- package/dist/utils/plugin/PluginUtils.js +23 -0
- package/dist/utils/plugin/RimoriClient.d.ts +72 -0
- package/dist/utils/plugin/RimoriClient.js +118 -0
- package/dist/utils/plugin/ThemeSetter.d.ts +1 -0
- package/dist/utils/plugin/ThemeSetter.js +13 -0
- package/dist/utils/plugin/WhereClauseBuilder.d.ts +24 -0
- package/dist/utils/plugin/WhereClauseBuilder.js +79 -0
- package/dist/utils/plugin/providers/EventEmitter.d.ts +11 -0
- package/dist/utils/plugin/providers/EventEmitter.js +41 -0
- package/dist/utils/plugin/providers/EventEmitterContext.d.ts +6 -0
- package/dist/utils/plugin/providers/EventEmitterContext.js +19 -0
- package/dist/utils/plugin/providers/PluginProvider.d.ts +8 -0
- package/dist/utils/plugin/providers/PluginProvider.js +49 -0
- package/package.json +30 -0
- package/src/components/CRUDModal.tsx +61 -0
- package/src/components/MarkdownEditor.tsx +111 -0
- package/src/components/Spinner.tsx +24 -0
- package/src/components/audio/Playbutton.tsx +119 -0
- package/src/controller/AIController.ts +87 -0
- package/src/controller/ObjectController.ts +109 -0
- package/src/controller/SettingsController.ts +87 -0
- package/src/controller/SharedContentController.ts +71 -0
- package/src/controller/VoiceController.ts +26 -0
- package/src/hooks/UseChatHook.ts +25 -0
- package/src/index.ts +14 -0
- package/src/plugin/PluginController.ts +158 -0
- package/src/plugin/RimoriClient.ts +207 -0
- package/src/plugin/ThemeSetter.ts +17 -0
- package/src/providers/EventEmitter.ts +48 -0
- package/src/providers/EventEmitterContext.tsx +27 -0
- package/src/providers/PluginProvider.tsx +68 -0
- package/src/style.scss +136 -0
- package/src/utils/PluginUtils.ts +26 -0
- package/src/utils/constants.ts +18 -0
- package/src/utils/difficultyConverter.ts +11 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface SpinnerProps {
|
|
4
|
+
text?: string;
|
|
5
|
+
size?: string
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Spinner: React.FC<SpinnerProps> = ({ text, className, size = "30px" }) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className={"flex items-center space-x-2 " + className}>
|
|
12
|
+
<svg
|
|
13
|
+
style={{ width: size, height: size }}
|
|
14
|
+
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
16
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
17
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
18
|
+
</svg>
|
|
19
|
+
{text && <span className="">{text}</span>}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default Spinner;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { FaPlayCircle, FaStopCircle } from "react-icons/fa";
|
|
3
|
+
import { usePlugin } from "../../providers/PluginProvider";
|
|
4
|
+
import Spinner from '../Spinner';
|
|
5
|
+
import { EmitterSingleton } from '../../providers/EventEmitter';
|
|
6
|
+
|
|
7
|
+
type AudioPlayerProps = {
|
|
8
|
+
text: string;
|
|
9
|
+
voice?: string;
|
|
10
|
+
language?: string;
|
|
11
|
+
hide?: boolean;
|
|
12
|
+
playOnMount?: boolean;
|
|
13
|
+
initialSpeed?: number;
|
|
14
|
+
enableSpeedAdjustment?: boolean;
|
|
15
|
+
playListenerEvent?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const AudioPlayOptions = [0.8, 0.9, 1.0, 1.1, 1.2, 1.5];
|
|
19
|
+
export type AudioPlayOptionType = 0.8 | 0.9 | 1.0 | 1.1 | 1.2 | 1.5;
|
|
20
|
+
|
|
21
|
+
let isFetchingAudio = false;
|
|
22
|
+
|
|
23
|
+
export const AudioPlayer: React.FC<AudioPlayerProps> = ({
|
|
24
|
+
text,
|
|
25
|
+
voice,
|
|
26
|
+
language,
|
|
27
|
+
hide,
|
|
28
|
+
playListenerEvent,
|
|
29
|
+
initialSpeed = 1.0,
|
|
30
|
+
playOnMount = false,
|
|
31
|
+
enableSpeedAdjustment = false,
|
|
32
|
+
}) => {
|
|
33
|
+
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
|
34
|
+
const [speed, setSpeed] = useState(initialSpeed);
|
|
35
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
36
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
37
|
+
const { getVoiceResponse, } = usePlugin();
|
|
38
|
+
const emitter = EmitterSingleton;
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!playListenerEvent) return;
|
|
42
|
+
emitter.on(playListenerEvent, () => togglePlayback());
|
|
43
|
+
}, [playListenerEvent]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
audioUrl && setAudioUrl(null);
|
|
47
|
+
return () => {
|
|
48
|
+
audioUrl && URL.revokeObjectURL(audioUrl);
|
|
49
|
+
}
|
|
50
|
+
}, [text]);
|
|
51
|
+
|
|
52
|
+
// Function to generate audio from text using API
|
|
53
|
+
const generateAudio = async () => {
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
|
|
56
|
+
const blob = await getVoiceResponse(text, voice || (language ? "aws_default" : "openai_alloy"), 1, language);
|
|
57
|
+
setAudioUrl(URL.createObjectURL(blob));
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Effect to play audio when audioUrl changes and play state is true
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!audioUrl || !isPlaying) return;
|
|
64
|
+
const audio = new Audio(audioUrl);
|
|
65
|
+
audio.playbackRate = speed;
|
|
66
|
+
audio.play().then(() => {
|
|
67
|
+
audio.onended = () => {
|
|
68
|
+
setIsPlaying(false);
|
|
69
|
+
isFetchingAudio = false;
|
|
70
|
+
};
|
|
71
|
+
}).catch(e => {
|
|
72
|
+
console.warn("Error playing audio:", e);
|
|
73
|
+
setIsPlaying(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
audio.pause();
|
|
78
|
+
};
|
|
79
|
+
}, [audioUrl, isPlaying, speed]);
|
|
80
|
+
|
|
81
|
+
const togglePlayback = () => {
|
|
82
|
+
if (!isPlaying && !audioUrl) {
|
|
83
|
+
generateAudio().then(() => setIsPlaying(true));
|
|
84
|
+
} else {
|
|
85
|
+
setIsPlaying((prev) => !prev);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!playOnMount || isFetchingAudio) return;
|
|
91
|
+
isFetchingAudio = true;
|
|
92
|
+
// console.log("playOnMount", playOnMount);
|
|
93
|
+
togglePlayback();
|
|
94
|
+
}, [playOnMount]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="group relative">
|
|
98
|
+
<div className='flex flex-row items-end'>
|
|
99
|
+
{!hide && <button className="text-gray-500" onClick={togglePlayback} disabled={isLoading}>
|
|
100
|
+
{isLoading ? <Spinner /> : isPlaying ? <FaStopCircle size={"25px"} /> : <FaPlayCircle size={"25px"} />}
|
|
101
|
+
</button>}
|
|
102
|
+
{enableSpeedAdjustment && (
|
|
103
|
+
<div className="ml-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-row text-sm text-gray-500">
|
|
104
|
+
<span className='pr-1'>Speed: </span>
|
|
105
|
+
<select
|
|
106
|
+
value={speed}
|
|
107
|
+
className='appearance-none cursor-pointer pr-0 p-0 rounded shadow leading-tight focus:outline-none focus:bg-gray-800 focus:ring bg-transparent border-0'
|
|
108
|
+
onChange={(e) => setSpeed(parseFloat(e.target.value))}
|
|
109
|
+
disabled={isLoading}>
|
|
110
|
+
{AudioPlayOptions.map((s) => (
|
|
111
|
+
<option key={s} value={s}>{s}</option>
|
|
112
|
+
))}
|
|
113
|
+
</select>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { env } from "../utils/constants";
|
|
2
|
+
|
|
3
|
+
export interface ToolInvocation {
|
|
4
|
+
toolName: string;
|
|
5
|
+
args: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Tool {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
parameters: {
|
|
12
|
+
name: string;
|
|
13
|
+
type: "string" | "number" | "boolean";
|
|
14
|
+
description: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Message {
|
|
19
|
+
id: string;
|
|
20
|
+
role: string;
|
|
21
|
+
content: string;
|
|
22
|
+
toolInvocations?: ToolInvocation[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function generateText(messages: Message[], tools: Tool[], token: string) {
|
|
26
|
+
const response = await fetch(`${env.SUPABASE_URL}/functions/v1/llm`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
body: JSON.stringify({ messages, tools }),
|
|
29
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return await response.json();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type OnLLMResponse = (id: string, response: string, finished: boolean, toolInvocations?: ToolInvocation[]) => void;
|
|
36
|
+
|
|
37
|
+
export async function streamChatGPT(messages: Message[], tools: Tool[], onResponse: OnLLMResponse, token: string) {
|
|
38
|
+
const messageId = Math.random().toString(36).substring(3);
|
|
39
|
+
const response = await fetch(`${env.SUPABASE_URL}/functions/v1/llm`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
body: JSON.stringify({ messages, tools, stream: true }),
|
|
42
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.body) {
|
|
46
|
+
console.error('No response body.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const reader = response.body.getReader();
|
|
51
|
+
const decoder = new TextDecoder('utf-8');
|
|
52
|
+
|
|
53
|
+
let content = "";
|
|
54
|
+
let done = false;
|
|
55
|
+
let toolInvocations: ToolInvocation[] = [];
|
|
56
|
+
while (!done) {
|
|
57
|
+
const { value } = await reader.read();
|
|
58
|
+
|
|
59
|
+
if (value) {
|
|
60
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
61
|
+
const lines = chunk.split('\n').filter(line => line.trim() !== '');
|
|
62
|
+
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const data = line.substring(3, line.length - 1);
|
|
65
|
+
const command = line.substring(0, 1);
|
|
66
|
+
// console.log("data: ", { line, data, command });
|
|
67
|
+
|
|
68
|
+
if (command === '0') {
|
|
69
|
+
content += data;
|
|
70
|
+
// console.log("AI response:", content);
|
|
71
|
+
|
|
72
|
+
//content \n\n should be real line break when message is displayed
|
|
73
|
+
onResponse(messageId, content.replace(/\\n/g, '\n'), false);
|
|
74
|
+
} else if (command === 'd') {
|
|
75
|
+
// console.log("AI usage:", JSON.parse(line.substring(2)));
|
|
76
|
+
done = true;
|
|
77
|
+
break;
|
|
78
|
+
} else if (command === '9') {
|
|
79
|
+
// console.log("tool call:", JSON.parse(line.substring(2)));
|
|
80
|
+
// console.log("tools", tools);
|
|
81
|
+
toolInvocations.push(JSON.parse(line.substring(2)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
onResponse(messageId, content.replace(/\\n/g, '\n'), true, toolInvocations);
|
|
87
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { env } from "../utils/constants";
|
|
2
|
+
|
|
3
|
+
type PrimitiveType = 'string' | 'number' | 'boolean';
|
|
4
|
+
|
|
5
|
+
// This is the type that can appear in the `type` property
|
|
6
|
+
type ObjectToolParameterType =
|
|
7
|
+
| PrimitiveType
|
|
8
|
+
| { [key: string]: ObjectToolParameter } // for nested objects
|
|
9
|
+
| [{ [key: string]: ObjectToolParameter }]; // for arrays of objects (notice the tuple type)
|
|
10
|
+
|
|
11
|
+
interface ObjectToolParameter {
|
|
12
|
+
type: ObjectToolParameterType;
|
|
13
|
+
description?: string;
|
|
14
|
+
enum?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ObjectTool = {
|
|
18
|
+
[key: string]: ObjectToolParameter;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface ObjectRequest {
|
|
22
|
+
/**
|
|
23
|
+
* The tools that the AI can use.
|
|
24
|
+
*/
|
|
25
|
+
tool: ObjectTool;
|
|
26
|
+
/**
|
|
27
|
+
* High level instructions for the AI to follow. Behaviour, tone, restrictions, etc.
|
|
28
|
+
* Example: "Act like a recipe writer."
|
|
29
|
+
*/
|
|
30
|
+
behaviour?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The specific instruction for the AI to follow.
|
|
33
|
+
* Example: "Generate a recipe using chicken, rice and vegetables."
|
|
34
|
+
*/
|
|
35
|
+
instructions: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function generateObject(request: ObjectRequest, token: string) {
|
|
39
|
+
return await fetch(`${env.SUPABASE_URL}/functions/v1/llm-object`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
stream: false,
|
|
43
|
+
tool: request.tool,
|
|
44
|
+
behaviour: request.behaviour,
|
|
45
|
+
instructions: request.instructions,
|
|
46
|
+
}),
|
|
47
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
48
|
+
}).then(response => response.json());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// TODO adjust stream to work with object
|
|
52
|
+
export type OnLLMResponse = (id: string, response: string, finished: boolean, toolInvocations?: any[]) => void;
|
|
53
|
+
|
|
54
|
+
export async function streamObject(request: ObjectRequest, onResponse: OnLLMResponse, token: string) {
|
|
55
|
+
const messageId = Math.random().toString(36).substring(3);
|
|
56
|
+
const response = await fetch(`${env.SUPABASE_URL}/functions/v1/llm-object`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
stream: true,
|
|
60
|
+
tools: request.tool,
|
|
61
|
+
systemInstructions: request.behaviour,
|
|
62
|
+
secondaryInstructions: request.instructions,
|
|
63
|
+
}),
|
|
64
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.body) {
|
|
68
|
+
console.error('No response body.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const reader = response.body.getReader();
|
|
73
|
+
const decoder = new TextDecoder('utf-8');
|
|
74
|
+
|
|
75
|
+
let content = "";
|
|
76
|
+
let done = false;
|
|
77
|
+
let toolInvocations: any[] = [];
|
|
78
|
+
while (!done) {
|
|
79
|
+
const { value } = await reader.read();
|
|
80
|
+
|
|
81
|
+
if (value) {
|
|
82
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
83
|
+
const lines = chunk.split('\n').filter(line => line.trim() !== '');
|
|
84
|
+
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const data = line.substring(3, line.length - 1);
|
|
87
|
+
const command = line.substring(0, 1);
|
|
88
|
+
// console.log("data: ", { line, data, command });
|
|
89
|
+
|
|
90
|
+
if (command === '0') {
|
|
91
|
+
content += data;
|
|
92
|
+
// console.log("AI response:", content);
|
|
93
|
+
|
|
94
|
+
//content \n\n should be real line break when message is displayed
|
|
95
|
+
onResponse(messageId, content.replace(/\\n/g, '\n'), false);
|
|
96
|
+
} else if (command === 'd') {
|
|
97
|
+
// console.log("AI usage:", JSON.parse(line.substring(2)));
|
|
98
|
+
done = true;
|
|
99
|
+
break;
|
|
100
|
+
} else if (command === '9') {
|
|
101
|
+
// console.log("tool call:", JSON.parse(line.substring(2)));
|
|
102
|
+
// console.log("tools", tools);
|
|
103
|
+
toolInvocations.push(JSON.parse(line.substring(2)));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
onResponse(messageId, content.replace(/\\n/g, '\n'), true, toolInvocations);
|
|
109
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import { LanguageLevel } from "../utils/difficultyConverter";
|
|
3
|
+
|
|
4
|
+
type SettingsType = "user" | "system" | "plugin";
|
|
5
|
+
|
|
6
|
+
export interface UserSettings {
|
|
7
|
+
motherTongue: string;
|
|
8
|
+
languageLevel: LanguageLevel;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SystemSettings {
|
|
12
|
+
// TODO: add system settings
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SettingsController {
|
|
16
|
+
private pluginId: string;
|
|
17
|
+
private supabase: SupabaseClient;
|
|
18
|
+
|
|
19
|
+
constructor(supabase: SupabaseClient, pluginId: string) {
|
|
20
|
+
this.supabase = supabase;
|
|
21
|
+
this.pluginId = pluginId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private getSettingsType(genericSettings?: "user" | "system"): SettingsType {
|
|
25
|
+
return genericSettings || "plugin";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private async fetchSettings(type: SettingsType): Promise<any | null> {
|
|
29
|
+
const pluginId = type === "plugin" ? this.pluginId : type;
|
|
30
|
+
const { data } = await this.supabase.from("plugin_settings").select("*").eq("plugin_id", pluginId)
|
|
31
|
+
|
|
32
|
+
if (!data || data.length === 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return data[0].settings;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async saveSettings(settings: any, type: SettingsType): Promise<void> {
|
|
40
|
+
if (type !== "plugin") {
|
|
41
|
+
throw new Error(`Cannot modify ${type} settings`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await this.supabase.from("plugin_settings").upsert({ plugin_id: this.pluginId, settings });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
|
|
49
|
+
* @param defaultSettings The default settings to use if no settings are found.
|
|
50
|
+
* @param genericSettings The type of settings to get.
|
|
51
|
+
* @returns The settings for the plugin.
|
|
52
|
+
*/
|
|
53
|
+
public async getSettings<T extends object>(defaultSettings: T, genericSettings?: "user" | "system"): Promise<T> {
|
|
54
|
+
const type = this.getSettingsType(genericSettings);
|
|
55
|
+
const storedSettings = await this.fetchSettings(type) as T | null;
|
|
56
|
+
|
|
57
|
+
if (!storedSettings) {
|
|
58
|
+
if (type === "plugin") {
|
|
59
|
+
await this.saveSettings(defaultSettings, type);
|
|
60
|
+
}
|
|
61
|
+
return defaultSettings;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle settings migration
|
|
65
|
+
const storedKeys = Object.keys(storedSettings);
|
|
66
|
+
const defaultKeys = Object.keys(defaultSettings);
|
|
67
|
+
|
|
68
|
+
if (storedKeys.length !== defaultKeys.length) {
|
|
69
|
+
const validStoredSettings = Object.fromEntries(
|
|
70
|
+
Object.entries(storedSettings).filter(([key]) => defaultKeys.includes(key))
|
|
71
|
+
);
|
|
72
|
+
const mergedSettings = { ...defaultSettings, ...validStoredSettings } as T;
|
|
73
|
+
|
|
74
|
+
if (type === "plugin") {
|
|
75
|
+
await this.saveSettings(mergedSettings, type);
|
|
76
|
+
}
|
|
77
|
+
return mergedSettings;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return storedSettings;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public async setSettings(settings: any, genericSettings?: "user" | "system"): Promise<void> {
|
|
84
|
+
const type = this.getSettingsType(genericSettings);
|
|
85
|
+
await this.saveSettings(settings, type);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { ObjectRequest } from "./ObjectController";
|
|
3
|
+
import { RimoriClient } from "../plugin/RimoriClient";
|
|
4
|
+
|
|
5
|
+
export interface BasicAssignment {
|
|
6
|
+
id: string;
|
|
7
|
+
createdAt: Date;
|
|
8
|
+
topic: string;
|
|
9
|
+
createdBy: string;
|
|
10
|
+
verified: boolean;
|
|
11
|
+
keywords: any;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SharedContentController {
|
|
15
|
+
private rimoriClient: RimoriClient;
|
|
16
|
+
|
|
17
|
+
constructor(rimoriClient: RimoriClient) {
|
|
18
|
+
this.rimoriClient = rimoriClient;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async fetchNewSharedContent<T, R = T & BasicAssignment>(
|
|
22
|
+
type: string,
|
|
23
|
+
generatorInstructions: (reservedTopics: string[]) => Promise<ObjectRequest> | ObjectRequest,
|
|
24
|
+
filter?: { column: string, value: string | number | boolean },
|
|
25
|
+
): Promise<R[]> {
|
|
26
|
+
const queryParameter = { filter_column: filter?.column || null, filter_value: filter?.value || null, unread: true }
|
|
27
|
+
const { data: newAssignments } = await this.rimoriClient.rpc(type + "_entries", queryParameter)
|
|
28
|
+
console.log('newAssignments:', newAssignments);
|
|
29
|
+
|
|
30
|
+
if ((newAssignments as any[]).length > 0) {
|
|
31
|
+
return newAssignments as R[];
|
|
32
|
+
}
|
|
33
|
+
// generate new assignments
|
|
34
|
+
const { data: oldAssignments } = await this.rimoriClient.rpc(type + "_entries", { ...queryParameter, unread: false })
|
|
35
|
+
console.log('oldAssignments:', oldAssignments);
|
|
36
|
+
const reservedTopics = this.getReservedTopics(oldAssignments as BasicAssignment[]);
|
|
37
|
+
|
|
38
|
+
const request = await generatorInstructions(reservedTopics);
|
|
39
|
+
if (!request.tool.keywords || !request.tool.topic) {
|
|
40
|
+
throw new Error("topic or keywords not found in the request schema");
|
|
41
|
+
}
|
|
42
|
+
const instructions = await this.rimoriClient.generateObject(request);
|
|
43
|
+
console.log('instructions:', instructions);
|
|
44
|
+
|
|
45
|
+
const preparedData = {
|
|
46
|
+
id: uuidv4(),
|
|
47
|
+
...instructions,
|
|
48
|
+
keywords: this.purifyStringArray(instructions.keywords),
|
|
49
|
+
};
|
|
50
|
+
return await this.rimoriClient.from(type).insert(preparedData).then(() => [preparedData] as R[]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private getReservedTopics(oldAssignments: BasicAssignment[]) {
|
|
54
|
+
return oldAssignments.map(({ topic, keywords }) => {
|
|
55
|
+
const keywordTexts = this.purifyStringArray(keywords).join(',');
|
|
56
|
+
return `${topic}(${keywordTexts})`;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private purifyStringArray(array: { text: string }[]): string[] {
|
|
61
|
+
return array.map(({ text }) => text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public async getSharedContent<T extends BasicAssignment>(type: string, id: string): Promise<T> {
|
|
65
|
+
return await this.rimoriClient.from(type).select().eq('id', id).single() as unknown as T;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public async completeSharedContent(type: string, assignmentId: string) {
|
|
69
|
+
await this.rimoriClient.from(type + "_result").insert({ assignment_id: assignmentId });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
export async function getSTTResponse(supabase: SupabaseClient, audio: Blob) {
|
|
4
|
+
const formData = new FormData();
|
|
5
|
+
formData.append('file', audio);
|
|
6
|
+
|
|
7
|
+
return await supabase.functions.invoke('speech', { method: 'POST', body: formData }).then((r: any) => r.text);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getTTSResponse(supabaseUrl: string, request: TTSRequest, token: string) {
|
|
11
|
+
return await fetch(`${supabaseUrl}/functions/v1/speech`, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'Authorization': `Bearer ${token}`
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify(request),
|
|
18
|
+
}).then(r => r.blob());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface TTSRequest {
|
|
22
|
+
input: string;
|
|
23
|
+
voice: string;
|
|
24
|
+
speed: number;
|
|
25
|
+
language?: string;
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { usePlugin } from "../providers/PluginProvider";
|
|
3
|
+
import { ToolInvocation, Tool, Message } from "../controller/AIController";
|
|
4
|
+
|
|
5
|
+
export function useChat(tools?: Tool[]) {
|
|
6
|
+
const [messages, setMessages] = React.useState<Message[]>([]);
|
|
7
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
8
|
+
const { getAIResponseStream } = usePlugin();
|
|
9
|
+
|
|
10
|
+
const append = (appendMessages: Message[]) => {
|
|
11
|
+
getAIResponseStream([...messages, ...appendMessages], (id, message, finished: boolean, toolInvocations?: ToolInvocation[]) => {
|
|
12
|
+
const lastMessage = messages[messages.length - 1];
|
|
13
|
+
setIsLoading(!finished);
|
|
14
|
+
|
|
15
|
+
if (lastMessage?.id === id) {
|
|
16
|
+
lastMessage.content = message;
|
|
17
|
+
setMessages([...messages, lastMessage]);
|
|
18
|
+
} else {
|
|
19
|
+
setMessages([...messages, ...appendMessages, { id, role: 'assistant', content: message, toolInvocations }]);
|
|
20
|
+
}
|
|
21
|
+
}, tools);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return { messages, append, isLoading, setMessages, lastMessage: messages[messages.length - 1] as Message | undefined };
|
|
25
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./components/MarkdownEditor";
|
|
2
|
+
export * from "./components/CRUDModal";
|
|
3
|
+
export * from "./plugin/ThemeSetter";
|
|
4
|
+
export * from "./components/audio/Playbutton";
|
|
5
|
+
export * from "./providers/EventEmitterContext";
|
|
6
|
+
export * from "./providers/PluginProvider";
|
|
7
|
+
export * from "./providers/EventEmitter";
|
|
8
|
+
export * from "./components/Spinner";
|
|
9
|
+
export * from "./hooks/UseChatHook";
|
|
10
|
+
export * from "./utils/PluginUtils";
|
|
11
|
+
export * from "./plugin/RimoriClient";
|
|
12
|
+
export * from "./controller/AIController";
|
|
13
|
+
export * from "./controller/SharedContentController";
|
|
14
|
+
export * from "./controller/SettingsController";
|