@rimori/client 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -1,64 +1,978 @@
1
1
  # Rimori Client Package
2
2
 
3
- This is the React connection package required by plugins to be able to
4
- communicate with the Rimori platform.
3
+ The **@rimori/client** package is a comprehensive React library that enables plugins to seamlessly integrate with the Rimori learning platform. It provides database access, AI/LLM integration, inter-plugin communication, community features, and pre-built UI components.
5
4
 
6
- ## Usage
5
+ ## Table of Contents
7
6
 
8
- In order to use the package first install the package with
7
+ - [Installation](#installation)
8
+ - [Quick Start](#quick-start)
9
+ - [Core API - usePlugin Hook](#core-api---useplugin-hook)
10
+ - [Database Integration](#database-integration)
11
+ - [LLM Integration](#llm-integration)
12
+ - [Event System](#event-system)
13
+ - [Community Features](#community-features)
14
+ - [Components](#components)
15
+ - [Hooks](#hooks)
16
+ - [Utilities](#utilities)
17
+ - [TypeScript Support](#typescript-support)
18
+ - [Examples](#examples)
19
+
20
+ ## Installation
9
21
 
10
22
  ```bash
11
- npm i @rimori/client
23
+ npm install @rimori/client
24
+ # or
25
+ yarn add @rimori/client
12
26
  ```
13
27
 
14
- Then wrap your app the following way to get started:
28
+ ## Quick Start
29
+
30
+ ### Basic Setup
15
31
 
16
32
  ```typescript
17
33
  import { lazy } from "react";
18
- import { PluginProvider } from "@rimori/client";
34
+ import { PluginProvider, usePlugin } from "@rimori/client";
19
35
  import { HashRouter, Route, Routes } from "react-router-dom";
20
36
 
21
- // adding the theme setter
22
-
23
- const queryClient = new QueryClient();
24
-
25
- // load all pages lazy for fast loading speed
37
+ // Load pages lazily for optimal performance
26
38
  const SettingsPage = lazy(() => import("./pages/settings/SettingsPage"));
27
- const DiscussionsPage = lazy(() => import("./pages/discussions/page"));
39
+ const MainPage = lazy(() => import("./pages/MainPage"));
28
40
 
29
41
  const App = () => (
30
- // this provides connectivity to Rimori
31
- <PluginProvider pluginId="my-rimori-plugin-id">
32
- //allows using the routes set the plugin settings
33
- <HashRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
34
- <Routes>
35
- // the plugins pages
36
- <Route path="/discussions" element={<DiscussionsPage />} />
37
- // the settings page
38
- <Route path="/settings" element={<SettingsPage />} />
39
- </Routes>
40
- </HashRouter>
41
- </PluginProvider>
42
+ <PluginProvider pluginId="rimori-plugin-id">
43
+ <HashRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
44
+ <Routes>
45
+ <Route path="/" element={<MainPage />} />
46
+ <Route path="/settings" element={<SettingsPage />} />
47
+ </Routes>
48
+ </HashRouter>
49
+ </PluginProvider>
42
50
  );
43
51
 
44
52
  export default App;
45
53
  ```
46
54
 
47
- Inside the pages simply use the `usePlugin` hook.
48
-
49
- ```typescript
50
- const { getSettings, ... } = usePlugin();
51
- ```
55
+ ### TailwindCSS Configuration
52
56
 
53
- If you use the components then you need to add the library to tailwind.config.js
57
+ Add the library to your `tailwind.config.js`:
54
58
 
55
59
  ```javascript
56
60
  export default {
57
- darkMode: ["class"], // to detect the dark mode set by the plugin
61
+ darkMode: ["class"], // Required for theme detection
58
62
  content: [
59
- ....
63
+ "./src/**/*.{js,jsx,ts,tsx}",
60
64
  "node_modules/@rimori/client/dist/components/**/*.{js,jsx}",
61
65
  ],
62
- ....
66
+ // ... rest of config
67
+ }
68
+ ```
69
+
70
+ ## Core API - usePlugin Hook
71
+
72
+ The `usePlugin()` hook is the main interface for accessing Rimori platform features:
73
+
74
+ ```typescript
75
+ import { usePlugin } from "@rimori/client";
76
+
77
+ const MyComponent = () => {
78
+ const client = usePlugin();
79
+
80
+ // Access all client features
81
+ const { db, llm, event, community, plugin } = client;
82
+
83
+ return <div>My Plugin Content</div>;
84
+ };
85
+ ```
86
+
87
+ ### Plugin Interface
88
+
89
+ ```typescript
90
+ const { plugin } = usePlugin();
91
+
92
+ // Plugin information and settings
93
+ plugin.pluginId: string // Current plugin ID
94
+ plugin.getSettings<T>(defaultSettings: T): Promise<T> // Get plugin settings
95
+ plugin.setSettings(settings: any): Promise<void> // Update plugin settings
96
+ plugin.getInstalled(): Promise<Plugin[]> // Get all installed plugins
97
+ plugin.getUserInfo(): Promise<UserInfo> // Get current user information
98
+ ```
99
+
100
+ **Example: Managing Flashcard Plugin Settings**
101
+
102
+ ```typescript
103
+ interface FlashcardSettings {
104
+ dailyGoal: number;
105
+ reviewInterval: 'easy' | 'medium' | 'hard';
106
+ showAnswerDelay: number;
107
+ enableAudioPronunciation: boolean;
108
+ difficultyAlgorithm: 'spaced-repetition' | 'random' | 'progressive';
109
+ }
110
+
111
+ const FlashcardSettingsComponent = () => {
112
+ const { plugin } = usePlugin();
113
+ const [settings, setSettings] = useState<FlashcardSettings>();
114
+
115
+ useEffect(() => {
116
+ const loadSettings = async () => {
117
+ const defaultSettings: FlashcardSettings = {
118
+ dailyGoal: 20,
119
+ reviewInterval: 'medium',
120
+ showAnswerDelay: 3,
121
+ enableAudioPronunciation: true,
122
+ difficultyAlgorithm: 'spaced-repetition'
123
+ };
124
+
125
+ const currentSettings = await plugin.getSettings(defaultSettings);
126
+ setSettings(currentSettings);
127
+ };
128
+
129
+ loadSettings();
130
+ }, []);
131
+
132
+ const updateSettings = async (newSettings: Partial<FlashcardSettings>) => {
133
+ const updated = { ...settings, ...newSettings };
134
+ await plugin.setSettings(updated);
135
+ setSettings(updated);
136
+ };
137
+
138
+ return (
139
+ <div className="flashcard-settings">
140
+ <label>
141
+ Daily Goal: {settings?.dailyGoal} cards
142
+ <input
143
+ type="range"
144
+ min="5"
145
+ max="100"
146
+ value={settings?.dailyGoal}
147
+ onChange={(e) => updateSettings({ dailyGoal: parseInt(e.target.value) })}
148
+ />
149
+ </label>
150
+
151
+ <label>
152
+ Review Interval:
153
+ <select
154
+ value={settings?.reviewInterval}
155
+ onChange={(e) => updateSettings({ reviewInterval: e.target.value as any })}
156
+ >
157
+ <option value="easy">Easy (longer intervals)</option>
158
+ <option value="medium">Medium</option>
159
+ <option value="hard">Hard (shorter intervals)</option>
160
+ </select>
161
+ </label>
162
+
163
+ <label>
164
+ <input
165
+ type="checkbox"
166
+ checked={settings?.enableAudioPronunciation}
167
+ onChange={(e) => updateSettings({ enableAudioPronunciation: e.target.checked })}
168
+ />
169
+ Enable Audio Pronunciation
170
+ </label>
171
+ </div>
172
+ );
173
+ };
174
+ ```
175
+
176
+ ## Database Integration
177
+
178
+ Access your plugin's dedicated database tables with full TypeScript support:
179
+
180
+ ```typescript
181
+ const { db } = usePlugin();
182
+
183
+ // Database interface
184
+ db.from(tableName) // Query builder for tables/views - supports ALL Supabase operations
185
+ db.storage // File storage access
186
+ db.tablePrefix: string // Your plugin's table prefix
187
+ db.getTableName(table: string): string // Get full table name with prefix
188
+ ```
189
+
190
+ The `db.from()` method provides access to the complete Supabase PostgREST API, supporting all database operations including:
191
+ - **CRUD Operations**: `insert()`, `select()`, `update()`, `delete()`, `upsert()`
192
+ - **Filtering**: `eq()`, `neq()`, `gt()`, `gte()`, `lt()`, `lte()`, `like()`, `ilike()`, `is()`, `in()`, `contains()`, `containedBy()`, `rangeLt()`, `rangeGt()`, `rangeGte()`, `rangeLte()`, `rangeAdjacent()`, `overlaps()`, `textSearch()`, `match()`, `not()`, `or()`, `filter()`
193
+ - **Modifiers**: `order()`, `limit()`, `range()`, `single()`, `maybe_single()`, `csv()`, `geojson()`, `explain()`
194
+ - **Aggregations**: `count()`, `sum()`, `avg()`, `min()`, `max()`
195
+ - **Advanced Features**: Row Level Security (RLS), real-time subscriptions, stored procedures, and custom functions
196
+
197
+ All operations automatically use your plugin's table prefix for security and isolation.
198
+
199
+ **Example: CRUD Operations**
200
+
201
+ ```typescript
202
+ interface StudySession {
203
+ id?: string;
204
+ user_id: string;
205
+ topic: string;
206
+ duration: number;
207
+ completed_at: string;
63
208
  }
64
- ```
209
+
210
+ const StudySessionManager = () => {
211
+ const { db } = usePlugin();
212
+
213
+ // Create a new study session
214
+ const createSession = async (session: Omit<StudySession, 'id'>) => {
215
+ const { data, error } = await db
216
+ .from('study_sessions') // Automatically prefixed
217
+ .insert(session)
218
+ .select()
219
+ .single();
220
+
221
+ if (error) throw error;
222
+ return data;
223
+ };
224
+
225
+ // Get user's study sessions
226
+ const getUserSessions = async (userId: string) => {
227
+ const { data, error } = await db
228
+ .from('study_sessions')
229
+ .select('*')
230
+ .eq('user_id', userId)
231
+ .order('completed_at', { ascending: false });
232
+
233
+ if (error) throw error;
234
+ return data;
235
+ };
236
+
237
+ // Update session
238
+ const updateSession = async (id: string, updates: Partial<StudySession>) => {
239
+ const { data, error } = await db
240
+ .from('study_sessions')
241
+ .update(updates)
242
+ .eq('id', id)
243
+ .select()
244
+ .single();
245
+
246
+ if (error) throw error;
247
+ return data;
248
+ };
249
+
250
+ return (
251
+ <div>
252
+ {/* Your component UI */}
253
+ </div>
254
+ );
255
+ };
256
+ ```
257
+
258
+ **File Storage Example**
259
+
260
+ ```typescript
261
+ const FileManager = () => {
262
+ const { db } = usePlugin();
263
+
264
+ const uploadFile = async (file: File) => {
265
+ const fileName = `uploads/${Date.now()}-${file.name}`;
266
+
267
+ const { data, error } = await db.storage
268
+ .from('plugin-files')
269
+ .upload(fileName, file);
270
+
271
+ if (error) throw error;
272
+ return data;
273
+ };
274
+
275
+ const downloadFile = async (filePath: string) => {
276
+ const { data, error } = await db.storage
277
+ .from('plugin-files')
278
+ .download(filePath);
279
+
280
+ if (error) throw error;
281
+ return data;
282
+ };
283
+
284
+ return <div>File Manager UI</div>;
285
+ };
286
+ ```
287
+
288
+ ## LLM Integration
289
+
290
+ Powerful AI/Language Model capabilities built-in:
291
+
292
+ ```typescript
293
+ const { llm } = usePlugin();
294
+
295
+ // Text generation
296
+ llm.getText(messages: Message[], tools?: Tool[]): Promise<string>
297
+
298
+ // Streaming text generation
299
+ llm.getSteamedText(messages: Message[], onMessage: OnLLMResponse, tools?: Tool[]): void
300
+
301
+ // Structured object generation
302
+ llm.getObject(request: ObjectRequest): Promise<any>
303
+
304
+ // Text-to-speech
305
+ llm.getVoice(text: string, voice?: string, speed?: number, language?: string): Promise<Blob>
306
+
307
+ // Speech-to-text
308
+ llm.getTextFromVoice(file: Blob): Promise<string>
309
+ ```
310
+
311
+ **Example: AI Chat Assistant**
312
+
313
+ ```typescript
314
+ import { useChat } from "@rimori/client";
315
+
316
+ const ChatAssistant = () => {
317
+ const { messages, append, isLoading } = useChat();
318
+ const [input, setInput] = useState('');
319
+
320
+ const sendMessage = () => {
321
+ if (!input.trim()) return;
322
+
323
+ append([{
324
+ role: 'user',
325
+ content: input
326
+ }]);
327
+
328
+ setInput('');
329
+ };
330
+
331
+ return (
332
+ <div className="chat-container">
333
+ <div className="messages">
334
+ {messages.map((message, index) => (
335
+ <div key={index} className={`message ${message.role}`}>
336
+ {message.content}
337
+ </div>
338
+ ))}
339
+ {isLoading && <div className="message assistant">Thinking...</div>}
340
+ </div>
341
+
342
+ <div className="input-area">
343
+ <input
344
+ value={input}
345
+ onChange={(e) => setInput(e.target.value)}
346
+ onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
347
+ placeholder="Ask anything..."
348
+ />
349
+ <button onClick={sendMessage} disabled={isLoading}>
350
+ Send
351
+ </button>
352
+ </div>
353
+ </div>
354
+ );
355
+ };
356
+ ```
357
+
358
+ **Example: Structured Data Generation**
359
+
360
+ ```typescript
361
+ const QuizGenerator = () => {
362
+ const { llm } = usePlugin();
363
+
364
+ const generateQuiz = async (topic: string) => {
365
+ const quiz = await llm.getObject({
366
+ schema: {
367
+ type: "object",
368
+ properties: {
369
+ title: { type: "string" },
370
+ questions: {
371
+ type: "array",
372
+ items: {
373
+ type: "object",
374
+ properties: {
375
+ question: { type: "string" },
376
+ options: { type: "array", items: { type: "string" } },
377
+ correctAnswer: { type: "number" },
378
+ explanation: { type: "string" }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ },
384
+ prompt: `Create a quiz about ${topic} with 5 multiple choice questions.`
385
+ });
386
+
387
+ return quiz;
388
+ };
389
+
390
+ return <div>Quiz Generator UI</div>;
391
+ };
392
+ ```
393
+
394
+ **Example: Voice Integration**
395
+
396
+ ```typescript
397
+ const VoiceAssistant = () => {
398
+ const { llm } = usePlugin();
399
+
400
+ const speakText = async (text: string) => {
401
+ const audioBlob = await llm.getVoice(text, "alloy", 1, "en");
402
+ const audioUrl = URL.createObjectURL(audioBlob);
403
+ const audio = new Audio(audioUrl);
404
+ audio.play();
405
+ };
406
+
407
+ const transcribeAudio = async (audioFile: File) => {
408
+ const transcript = await llm.getTextFromVoice(audioFile);
409
+ return transcript;
410
+ };
411
+
412
+ return <div>Voice Assistant UI</div>;
413
+ };
414
+ ```
415
+
416
+ ## Event System
417
+
418
+ Robust inter-plugin communication and platform integration:
419
+
420
+ ```typescript
421
+ const { event } = usePlugin();
422
+
423
+ // Event methods
424
+ event.emit(topic: string, data?: any, eventId?: number): void
425
+ event.request<T>(topic: string, data?: any): Promise<EventBusMessage<T>>
426
+ event.on<T>(topic: string | string[], callback: EventHandler<T>): string[]
427
+ event.once<T>(topic: string, callback: EventHandler<T>): void
428
+ event.respond<T>(topic: string, data: EventPayload | Function): void
429
+
430
+ // Accomplishments
431
+ event.emitAccomplishment(payload: AccomplishmentPayload): void
432
+ event.onAccomplishment(topic: string, callback: Function): void
433
+
434
+ // Sidebar actions
435
+ event.emitSidebarAction(pluginId: string, actionKey: string, text?: string): void
436
+ ```
437
+
438
+ **Example: Plugin Communication**
439
+
440
+ ```typescript
441
+ const PluginCommunicator = () => {
442
+ const { event } = usePlugin();
443
+
444
+ useEffect(() => {
445
+ // Listen for messages from other plugins
446
+ const unsubscribe = event.on('flashcards.newCard', (message) => {
447
+ console.log('New flashcard created:', message.data);
448
+ });
449
+
450
+ // Listen for global events
451
+ event.on('global.userProgress', (message) => {
452
+ console.log('User progress updated:', message.data);
453
+ });
454
+
455
+ return () => {
456
+ // Cleanup subscriptions
457
+ unsubscribe.forEach(id => event.off(id));
458
+ };
459
+ }, []);
460
+
461
+ const shareData = () => {
462
+ // Emit data to other plugins
463
+ event.emit('studyplan.dataUpdate', {
464
+ type: 'session_completed',
465
+ sessionId: '123',
466
+ score: 85
467
+ });
468
+ };
469
+
470
+ const requestData = async () => {
471
+ // Request data from another plugin
472
+ const response = await event.request('flashcards.getStats', {
473
+ timeframe: 'week'
474
+ });
475
+
476
+ console.log('Flashcard stats:', response.data);
477
+ };
478
+
479
+ return (
480
+ <div>
481
+ <button onClick={shareData}>Share Progress</button>
482
+ <button onClick={requestData}>Get Flashcard Stats</button>
483
+ </div>
484
+ );
485
+ };
486
+ ```
487
+
488
+ **Example: Accomplishment System**
489
+
490
+ ```typescript
491
+ const AccomplishmentTracker = () => {
492
+ const { event } = usePlugin();
493
+
494
+ const trackAccomplishment = () => {
495
+ event.emitAccomplishment({
496
+ type: 'study_milestone',
497
+ title: 'Study Streak',
498
+ description: 'Completed 7 days of studying',
499
+ points: 100,
500
+ metadata: {
501
+ streakDays: 7,
502
+ subject: 'Spanish'
503
+ }
504
+ });
505
+ };
506
+
507
+ useEffect(() => {
508
+ // Listen for accomplishments from this plugin
509
+ event.onAccomplishment('study_milestone', (accomplishment) => {
510
+ console.log('New accomplishment:', accomplishment);
511
+ // Show notification, update UI, etc.
512
+ });
513
+ }, []);
514
+
515
+ return <div>Accomplishment Tracker UI</div>;
516
+ };
517
+ ```
518
+
519
+ **Example: Sidebar Integration**
520
+
521
+ ```typescript
522
+ const SidebarIntegration = () => {
523
+ const { event } = usePlugin();
524
+
525
+ const openTranslator = (text: string) => {
526
+ // Trigger translator plugin in sidebar
527
+ event.emitSidebarAction('translator', 'translate', text);
528
+ };
529
+
530
+ const openFlashcards = () => {
531
+ // Open flashcards plugin
532
+ event.emitSidebarAction('flashcards', 'review');
533
+ };
534
+
535
+ return (
536
+ <div>
537
+ <button onClick={() => openTranslator('Hello world')}>
538
+ Translate "Hello world"
539
+ </button>
540
+ <button onClick={openFlashcards}>
541
+ Review Flashcards
542
+ </button>
543
+ </div>
544
+ );
545
+ };
546
+ ```
547
+
548
+ ## Community Features
549
+
550
+ Share and discover content created by other users:
551
+
552
+ ```typescript
553
+ const { community } = usePlugin();
554
+
555
+ // Shared content methods
556
+ community.sharedContent.get<T>(contentType: string, id: string): Promise<BasicAssignment<T>>
557
+ community.sharedContent.getList<T>(contentType: string, filter?: SharedContentFilter, limit?: number): Promise<BasicAssignment<T>[]>
558
+ community.sharedContent.getNew<T>(contentType: string, instructions: SharedContentObjectRequest, filter?: SharedContentFilter, privateTopic?: boolean): Promise<BasicAssignment<T>>
559
+ community.sharedContent.create<T>(content: SharedContent<T>): Promise<BasicAssignment<T>>
560
+ community.sharedContent.update<T>(id: string, content: Partial<SharedContent<T>>): Promise<BasicAssignment<T>>
561
+ community.sharedContent.complete(contentType: string, assignmentId: string): Promise<void>
562
+ ```
563
+
564
+ **Example: Exercise Sharing Platform**
565
+
566
+ ```typescript
567
+ interface Exercise {
568
+ title: string;
569
+ description: string;
570
+ difficulty: 'beginner' | 'intermediate' | 'advanced';
571
+ questions: Array<{
572
+ question: string;
573
+ answer: string;
574
+ hints?: string[];
575
+ }>;
576
+ }
577
+
578
+ const ExerciseManager = () => {
579
+ const { community } = usePlugin();
580
+ const [exercises, setExercises] = useState<BasicAssignment<Exercise>[]>([]);
581
+
582
+ // Load community exercises
583
+ const loadExercises = async () => {
584
+ const exerciseList = await community.sharedContent.getList<Exercise>(
585
+ 'grammar_exercises',
586
+ { column: 'difficulty', value: 'beginner' },
587
+ 10
588
+ );
589
+ setExercises(exerciseList);
590
+ };
591
+
592
+ // Create new exercise
593
+ const createExercise = async (exercise: Exercise) => {
594
+ const newExercise = await community.sharedContent.create({
595
+ content_type: 'grammar_exercises',
596
+ content: exercise,
597
+ metadata: {
598
+ difficulty: exercise.difficulty,
599
+ questionCount: exercise.questions.length
600
+ }
601
+ });
602
+
603
+ return newExercise;
604
+ };
605
+
606
+ // Generate AI exercise
607
+ const generateExercise = async (topic: string) => {
608
+ const aiExercise = await community.sharedContent.getNew<Exercise>(
609
+ 'grammar_exercises',
610
+ {
611
+ prompt: `Create a grammar exercise about ${topic}`,
612
+ schema: {
613
+ type: "object",
614
+ properties: {
615
+ title: { type: "string" },
616
+ description: { type: "string" },
617
+ difficulty: { type: "string", enum: ["beginner", "intermediate", "advanced"] },
618
+ questions: {
619
+ type: "array",
620
+ items: {
621
+ type: "object",
622
+ properties: {
623
+ question: { type: "string" },
624
+ answer: { type: "string" },
625
+ hints: { type: "array", items: { type: "string" } }
626
+ }
627
+ }
628
+ }
629
+ }
630
+ }
631
+ },
632
+ { column: 'difficulty', value: 'beginner' }
633
+ );
634
+
635
+ return aiExercise;
636
+ };
637
+
638
+ // Complete exercise
639
+ const completeExercise = async (exerciseId: string) => {
640
+ await community.sharedContent.complete('grammar_exercises', exerciseId);
641
+ // Exercise is now marked as completed for the user
642
+ };
643
+
644
+ return (
645
+ <div>
646
+ <button onClick={loadExercises}>Load Exercises</button>
647
+ <button onClick={() => generateExercise('present tense')}>
648
+ Generate Present Tense Exercise
649
+ </button>
650
+ {/* Exercise list UI */}
651
+ </div>
652
+ );
653
+ };
654
+ ```
655
+
656
+ ## Components
657
+
658
+ Pre-built React components for common functionality:
659
+
660
+ ### MarkdownEditor
661
+ Rich text editor with markdown support:
662
+
663
+ ```typescript
664
+ import { MarkdownEditor } from "@rimori/client";
665
+
666
+ const EditorExample = () => {
667
+ const [content, setContent] = useState('');
668
+
669
+ return (
670
+ <MarkdownEditor
671
+ value={content}
672
+ onChange={setContent}
673
+ placeholder="Start writing..."
674
+ />
675
+ );
676
+ };
677
+ ```
678
+
679
+ ### CRUDModal
680
+ Modal component for create/update operations:
681
+
682
+ ```typescript
683
+ import { CRUDModal } from "@rimori/client";
684
+
685
+ const DataManager = () => {
686
+ const [isOpen, setIsOpen] = useState(false);
687
+ const [editItem, setEditItem] = useState(null);
688
+
689
+ return (
690
+ <CRUDModal
691
+ isOpen={isOpen}
692
+ onClose={() => setIsOpen(false)}
693
+ title={editItem ? "Edit Item" : "Create Item"}
694
+ onSave={(data) => {
695
+ // Handle save logic
696
+ console.log('Saving:', data);
697
+ setIsOpen(false);
698
+ }}
699
+ initialData={editItem}
700
+ >
701
+ {/* Your form content */}
702
+ <input placeholder="Item name" />
703
+ <textarea placeholder="Description" />
704
+ </CRUDModal>
705
+ );
706
+ };
707
+ ```
708
+
709
+ ### Spinner
710
+ Loading indicator component:
711
+
712
+ ```typescript
713
+ import { Spinner } from "@rimori/client";
714
+
715
+ const LoadingExample = () => {
716
+ const [isLoading, setIsLoading] = useState(true);
717
+
718
+ if (isLoading) {
719
+ return <Spinner size="large" />;
720
+ }
721
+
722
+ return <div>Content loaded!</div>;
723
+ };
724
+ ```
725
+
726
+ ### PlayButton
727
+ Audio playback component:
728
+
729
+ ```typescript
730
+ import { PlayButton } from "@rimori/client";
731
+
732
+ const AudioPlayer = () => {
733
+ return (
734
+ <PlayButton
735
+ audioUrl="https://example.com/audio.mp3"
736
+ onPlay={() => console.log('Audio playing')}
737
+ onPause={() => console.log('Audio paused')}
738
+ />
739
+ );
740
+ };
741
+ ```
742
+
743
+ ### AI Components
744
+
745
+ ```typescript
746
+ import { Avatar, Assistant } from "@rimori/client";
747
+
748
+ const AIInterface = () => {
749
+ return (
750
+ <div>
751
+ <Avatar
752
+ name="AI Assistant"
753
+ status="online"
754
+ size="large"
755
+ />
756
+
757
+ <Assistant
758
+ onMessage={(message) => console.log('AI message:', message)}
759
+ placeholder="Ask the AI assistant..."
760
+ />
761
+ </div>
762
+ );
763
+ };
764
+ ```
765
+
766
+ ## Hooks
767
+
768
+ ### useChat
769
+ Manage AI chat conversations:
770
+
771
+ ```typescript
772
+ import { useChat } from "@rimori/client";
773
+
774
+ const ChatExample = () => {
775
+ const { messages, append, isLoading, setMessages } = useChat([
776
+ // Optional tools for the AI
777
+ {
778
+ name: "get_weather",
779
+ description: "Get current weather",
780
+ parameters: {
781
+ type: "object",
782
+ properties: {
783
+ location: { type: "string" }
784
+ }
785
+ }
786
+ }
787
+ ]);
788
+
789
+ const sendMessage = (content: string) => {
790
+ append([{ role: 'user', content }]);
791
+ };
792
+
793
+ return (
794
+ <div>
795
+ {messages.map((msg, index) => (
796
+ <div key={index}>{msg.content}</div>
797
+ ))}
798
+ {isLoading && <div>AI is typing...</div>}
799
+ </div>
800
+ );
801
+ };
802
+ ```
803
+
804
+ ## Utilities
805
+
806
+ ### difficultyConverter
807
+ Convert between different difficulty representations:
808
+
809
+ ```typescript
810
+ import { difficultyConverter } from "@rimori/client";
811
+
812
+ const difficulty = difficultyConverter.toNumber('intermediate'); // Returns: 2
813
+ const difficultyText = difficultyConverter.toString(3); // Returns: 'advanced'
814
+ ```
815
+
816
+ ### PluginUtils
817
+ Various utility functions:
818
+
819
+ ```typescript
820
+ import { PluginUtils } from "@rimori/client";
821
+
822
+ // Utility functions for common plugin operations
823
+ const utils = PluginUtils.getInstance();
824
+ // Access various helper methods
825
+ ```
826
+
827
+ ### Language Utilities
828
+ Language detection and processing:
829
+
830
+ ```typescript
831
+ import { Language } from "@rimori/client";
832
+
833
+ // Language-related utility functions
834
+ const languageCode = Language.detectLanguage(text);
835
+ const isSupported = Language.isSupported('es');
836
+ ```
837
+
838
+ ## TypeScript Support
839
+
840
+ The package is fully typed with comprehensive TypeScript definitions:
841
+
842
+ ```typescript
843
+ import type {
844
+ MainPanelAction,
845
+ Message,
846
+ Tool,
847
+ EventPayload,
848
+ AccomplishmentPayload,
849
+ SharedContent,
850
+ BasicAssignment,
851
+ UserInfo
852
+ } from "@rimori/client";
853
+
854
+ // All interfaces and types are exported for use in your plugin
855
+ interface MyPluginData extends SharedContent<any> {
856
+ // Your custom properties
857
+ }
858
+ ```
859
+
860
+ ## Examples
861
+
862
+ ### Complete Plugin Example
863
+
864
+ ```typescript
865
+ import React, { useState, useEffect } from 'react';
866
+ import {
867
+ PluginProvider,
868
+ usePlugin,
869
+ MarkdownEditor,
870
+ Spinner,
871
+ useChat
872
+ } from '@rimori/client';
873
+ import { HashRouter, Route, Routes } from 'react-router-dom';
874
+
875
+ const StudyNotesPlugin = () => {
876
+ const { db, llm, plugin, community } = usePlugin();
877
+ const [notes, setNotes] = useState([]);
878
+ const [isLoading, setIsLoading] = useState(true);
879
+ const { messages, append } = useChat();
880
+
881
+ useEffect(() => {
882
+ loadNotes();
883
+ }, []);
884
+
885
+ const loadNotes = async () => {
886
+ try {
887
+ const { data } = await db.from('notes').select('*').order('created_at', { ascending: false });
888
+ setNotes(data || []);
889
+ } catch (error) {
890
+ console.error('Error loading notes:', error);
891
+ } finally {
892
+ setIsLoading(false);
893
+ }
894
+ };
895
+
896
+ const saveNote = async (content: string) => {
897
+ const { data } = await db.from('notes').insert({
898
+ content,
899
+ created_at: new Date().toISOString()
900
+ }).select().single();
901
+
902
+ setNotes([data, ...notes]);
903
+
904
+ // Share with community
905
+ await community.sharedContent.create({
906
+ content_type: 'study_notes',
907
+ content: { text: content },
908
+ metadata: { wordCount: content.length }
909
+ });
910
+ };
911
+
912
+ const generateSummary = async (noteContent: string) => {
913
+ const summary = await llm.getText([
914
+ { role: 'user', content: `Summarize this study note: ${noteContent}` }
915
+ ]);
916
+
917
+ return summary;
918
+ };
919
+
920
+ if (isLoading) return <Spinner size="large" />;
921
+
922
+ return (
923
+ <div className="study-notes-plugin">
924
+ <h1>Study Notes</h1>
925
+
926
+ <div className="notes-grid">
927
+ {notes.map(note => (
928
+ <div key={note.id} className="note-card">
929
+ <MarkdownEditor
930
+ value={note.content}
931
+ onChange={(content) => {/* Update logic */}}
932
+ />
933
+ <button onClick={() => generateSummary(note.content)}>
934
+ Generate Summary
935
+ </button>
936
+ </div>
937
+ ))}
938
+ </div>
939
+
940
+ <div className="ai-chat">
941
+ <h2>Study Assistant</h2>
942
+ {messages.map((msg, index) => (
943
+ <div key={index} className={`message ${msg.role}`}>
944
+ {msg.content}
945
+ </div>
946
+ ))}
947
+ <button onClick={() => append([{ role: 'user', content: 'Help me study' }])}>
948
+ Get Study Help
949
+ </button>
950
+ </div>
951
+ </div>
952
+ );
953
+ };
954
+
955
+ const App = () => (
956
+ <PluginProvider pluginId="study-notes-plugin">
957
+ <HashRouter>
958
+ <Routes>
959
+ <Route path="/" element={<StudyNotesPlugin />} />
960
+ </Routes>
961
+ </HashRouter>
962
+ </PluginProvider>
963
+ );
964
+
965
+ export default App;
966
+ ```
967
+
968
+ ## Best Practices
969
+
970
+ 1. **Performance**: Use lazy loading for pages and implement proper loading states
971
+ 2. **State Management**: Leverage React hooks and context when needed
972
+ 3. **Type Safety**: Use TypeScript interfaces for all data structures
973
+ 4. **Event Cleanup**: Always unsubscribe from events in useEffect cleanup
974
+ 5. **Responsive Design**: Use TailwindCSS classes for responsive layouts
975
+
976
+ ## License
977
+
978
+ MIT License - see LICENSE file for details.