@rimori/client 2.4.0 → 2.5.0-next.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.
Files changed (70) hide show
  1. package/dist/cli/scripts/init/dev-registration.js +4 -2
  2. package/dist/cli/scripts/init/main.js +1 -0
  3. package/dist/cli/scripts/release/release.js +0 -0
  4. package/dist/controller/SettingsController.d.ts +1 -1
  5. package/dist/controller/SharedContentController.d.ts +1 -1
  6. package/dist/fromRimori/EventBus.js +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/plugin/CommunicationHandler.d.ts +13 -8
  9. package/dist/plugin/CommunicationHandler.js +44 -59
  10. package/dist/plugin/RimoriClient.d.ts +11 -195
  11. package/dist/plugin/RimoriClient.js +17 -298
  12. package/dist/plugin/StandaloneClient.d.ts +1 -1
  13. package/dist/plugin/StandaloneClient.js +3 -2
  14. package/dist/plugin/module/AIModule.d.ts +49 -0
  15. package/dist/plugin/module/AIModule.js +81 -0
  16. package/dist/plugin/module/DbModule.d.ts +30 -0
  17. package/dist/plugin/module/DbModule.js +51 -0
  18. package/dist/plugin/module/EventModule.d.ts +99 -0
  19. package/dist/plugin/module/EventModule.js +162 -0
  20. package/dist/{controller/ExerciseController.d.ts → plugin/module/ExerciseModule.d.ts} +20 -16
  21. package/dist/{controller/ExerciseController.js → plugin/module/ExerciseModule.js} +27 -20
  22. package/dist/plugin/module/PluginModule.d.ts +76 -0
  23. package/dist/plugin/module/PluginModule.js +88 -0
  24. package/package.json +8 -3
  25. package/.github/workflows/pre-release.yml +0 -126
  26. package/.prettierignore +0 -35
  27. package/eslint.config.js +0 -53
  28. package/example/docs/devdocs.md +0 -241
  29. package/example/docs/overview.md +0 -29
  30. package/example/docs/userdocs.md +0 -126
  31. package/example/rimori.config.ts +0 -91
  32. package/example/worker/vite.config.ts +0 -26
  33. package/example/worker/worker.ts +0 -11
  34. package/prettier.config.js +0 -8
  35. package/src/cli/scripts/init/dev-registration.ts +0 -189
  36. package/src/cli/scripts/init/env-setup.ts +0 -44
  37. package/src/cli/scripts/init/file-operations.ts +0 -58
  38. package/src/cli/scripts/init/html-cleaner.ts +0 -45
  39. package/src/cli/scripts/init/main.ts +0 -175
  40. package/src/cli/scripts/init/package-setup.ts +0 -113
  41. package/src/cli/scripts/init/router-transformer.ts +0 -332
  42. package/src/cli/scripts/init/tailwind-config.ts +0 -66
  43. package/src/cli/scripts/init/vite-config.ts +0 -73
  44. package/src/cli/scripts/release/detect-translation-languages.ts +0 -37
  45. package/src/cli/scripts/release/release-config-upload.ts +0 -119
  46. package/src/cli/scripts/release/release-db-update.ts +0 -97
  47. package/src/cli/scripts/release/release-file-upload.ts +0 -138
  48. package/src/cli/scripts/release/release.ts +0 -85
  49. package/src/cli/types/DatabaseTypes.ts +0 -125
  50. package/src/controller/AIController.ts +0 -295
  51. package/src/controller/AccomplishmentController.ts +0 -188
  52. package/src/controller/AudioController.ts +0 -64
  53. package/src/controller/ExerciseController.ts +0 -117
  54. package/src/controller/ObjectController.ts +0 -120
  55. package/src/controller/SettingsController.ts +0 -186
  56. package/src/controller/SharedContentController.ts +0 -365
  57. package/src/controller/TranslationController.ts +0 -136
  58. package/src/controller/VoiceController.ts +0 -33
  59. package/src/fromRimori/EventBus.ts +0 -382
  60. package/src/fromRimori/PluginTypes.ts +0 -214
  61. package/src/fromRimori/readme.md +0 -2
  62. package/src/index.ts +0 -19
  63. package/src/plugin/CommunicationHandler.ts +0 -310
  64. package/src/plugin/Logger.ts +0 -394
  65. package/src/plugin/RimoriClient.ts +0 -530
  66. package/src/plugin/StandaloneClient.ts +0 -125
  67. package/src/utils/difficultyConverter.ts +0 -15
  68. package/src/utils/endpoint.ts +0 -3
  69. package/src/worker/WorkerSetup.ts +0 -35
  70. package/tsconfig.json +0 -17
@@ -1,120 +0,0 @@
1
- type PrimitiveType = 'string' | 'number' | 'boolean';
2
-
3
- // This is the type that can appear in the `type` property
4
- type ObjectToolParameterType =
5
- | PrimitiveType
6
- | { [key: string]: ObjectToolParameter } // for nested objects
7
- | [{ [key: string]: ObjectToolParameter }]; // for arrays of objects (notice the tuple type)
8
-
9
- interface ObjectToolParameter {
10
- type: ObjectToolParameterType;
11
- description?: string;
12
- enum?: string[];
13
- optional?: boolean;
14
- }
15
-
16
- /**
17
- * The tools that the AI can use.
18
- *
19
- * The key is the name of the tool.
20
- * The value is the parameter of the tool.
21
- *
22
- */
23
- export type ObjectTool = {
24
- [key: string]: ObjectToolParameter;
25
- };
26
-
27
- export interface ObjectRequest {
28
- /**
29
- * The tools that the AI can use.
30
- */
31
- tool: ObjectTool;
32
- /**
33
- * High level instructions for the AI to follow. Behaviour, tone, restrictions, etc.
34
- * Example: "Act like a recipe writer."
35
- */
36
- behaviour?: string;
37
- /**
38
- * The specific instruction for the AI to follow.
39
- * Example: "Generate a recipe using chicken, rice and vegetables."
40
- */
41
- instructions: string;
42
- }
43
-
44
- export async function generateObject<T = any>(backendUrl: string, request: ObjectRequest, token: string): Promise<T> {
45
- return await fetch(`${backendUrl}/ai/llm-object`, {
46
- method: 'POST',
47
- body: JSON.stringify({
48
- stream: false,
49
- tool: request.tool,
50
- behaviour: request.behaviour,
51
- instructions: request.instructions,
52
- }),
53
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
54
- }).then((response) => response.json());
55
- }
56
-
57
- // TODO adjust stream to work with object
58
- export type OnLLMResponse = (id: string, response: string, finished: boolean, toolInvocations?: any[]) => void;
59
-
60
- export async function streamObject(
61
- backendUrl: string,
62
- request: ObjectRequest,
63
- onResponse: OnLLMResponse,
64
- token: string,
65
- ) {
66
- const messageId = Math.random().toString(36).substring(3);
67
- const response = await fetch(`${backendUrl}/ai/llm-object`, {
68
- method: 'POST',
69
- body: JSON.stringify({
70
- stream: true,
71
- tools: request.tool,
72
- systemInstructions: request.behaviour,
73
- secondaryInstructions: request.instructions,
74
- }),
75
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
76
- });
77
-
78
- if (!response.body) {
79
- console.error('No response body.');
80
- return;
81
- }
82
-
83
- const reader = response.body.getReader();
84
- const decoder = new TextDecoder('utf-8');
85
-
86
- let content = '';
87
- let done = false;
88
- const toolInvocations: any[] = [];
89
- while (!done) {
90
- const { value } = await reader.read();
91
-
92
- if (value) {
93
- const chunk = decoder.decode(value, { stream: true });
94
- const lines = chunk.split('\n').filter((line) => line.trim() !== '');
95
-
96
- for (const line of lines) {
97
- const data = line.substring(3, line.length - 1);
98
- const command = line.substring(0, 1);
99
- // console.log("data: ", { line, data, command });
100
-
101
- if (command === '0') {
102
- content += data;
103
- // console.log("AI response:", content);
104
-
105
- //content \n\n should be real line break when message is displayed
106
- onResponse(messageId, content.replace(/\\n/g, '\n').replace(/\\+"/g, '"'), false);
107
- } else if (command === 'd') {
108
- // console.log("AI usage:", JSON.parse(line.substring(2)));
109
- done = true;
110
- break;
111
- } else if (command === '9') {
112
- // console.log("tool call:", JSON.parse(line.substring(2)));
113
- // console.log("tools", tools);
114
- toolInvocations.push(JSON.parse(line.substring(2)));
115
- }
116
- }
117
- }
118
- }
119
- onResponse(messageId, content.replace(/\\n/g, '\n').replace(/\\+"/g, '"'), true, toolInvocations);
120
- }
@@ -1,186 +0,0 @@
1
- import { SupabaseClient } from '@supabase/supabase-js';
2
- import { LanguageLevel } from '../utils/difficultyConverter';
3
- import { Guild } from '../plugin/CommunicationHandler';
4
-
5
- export interface Buddy {
6
- id: string;
7
- name: string;
8
- description: string;
9
- avatarUrl: string;
10
- voiceId: string;
11
- aiPersonality: string;
12
- }
13
-
14
- export interface Language {
15
- code: string;
16
- name: string;
17
- native: string;
18
- capitalized: string;
19
- uppercase: string;
20
- }
21
-
22
- export type UserRole = 'user' | 'plugin_moderator' | 'lang_moderator' | 'admin';
23
-
24
- export interface UserInfo {
25
- skill_level_reading: LanguageLevel;
26
- skill_level_writing: LanguageLevel;
27
- skill_level_grammar: LanguageLevel;
28
- skill_level_speaking: LanguageLevel;
29
- skill_level_listening: LanguageLevel;
30
- skill_level_understanding: LanguageLevel;
31
- goal_longterm: string;
32
- goal_weekly: string;
33
- study_buddy: Buddy;
34
- story_genre: string;
35
- study_duration: number;
36
- /**
37
- * The language the user speaks natively.
38
- */
39
- mother_tongue: Language;
40
- /**
41
- * The language the user targets to learn.
42
- */
43
- target_language: Language;
44
- motivation_type: string;
45
- onboarding_completed: boolean;
46
- context_menu_on_select: boolean;
47
- user_name?: string;
48
- /**
49
- * ISO 3166-1 alpha-2 country code of user's target location (exposed to plugins)
50
- */
51
- target_country: string;
52
- /**
53
- * Optional: nearest big city (>100,000) near user's location
54
- */
55
- target_city?: string;
56
- /**
57
- * The user's role: 'user', 'plugin_moderator', 'lang_moderator', or 'admin'
58
- */
59
- user_role: UserRole;
60
- }
61
-
62
- export class SettingsController {
63
- private pluginId: string;
64
- private supabase: SupabaseClient;
65
- private guild: Guild;
66
-
67
- constructor(supabase: SupabaseClient, pluginId: string, guild: Guild) {
68
- this.supabase = supabase;
69
- this.pluginId = pluginId;
70
- this.guild = guild;
71
- }
72
-
73
- /**
74
- * Fetches settings based on guild configuration.
75
- * If guild doesn't allow user settings, fetches guild-level settings.
76
- * Otherwise, fetches user-specific settings.
77
- * @returns The settings object or null if not found.
78
- */
79
- private async fetchSettings(): Promise<any | null> {
80
- const isGuildSetting = !this.guild.allowUserPluginSettings;
81
-
82
- const { data } = await this.supabase
83
- .from('plugin_settings')
84
- .select('*')
85
- .eq('plugin_id', this.pluginId)
86
- .eq('guild_id', this.guild.id)
87
- .eq('is_guild_setting', isGuildSetting)
88
- .maybeSingle();
89
-
90
- return data?.settings ?? null;
91
- }
92
-
93
- /**
94
- * Sets settings for the plugin.
95
- * Automatically saves as guild settings if guild doesn't allow user settings,
96
- * otherwise saves as user-specific settings.
97
- * @param settings - The settings object to save.
98
- * @throws {Error} if RLS blocks the operation.
99
- */
100
- public async setSettings(settings: any): Promise<void> {
101
- const isGuildSetting = !this.guild.allowUserPluginSettings;
102
-
103
- const payload: any = {
104
- plugin_id: this.pluginId,
105
- settings,
106
- guild_id: this.guild.id,
107
- is_guild_setting: isGuildSetting,
108
- };
109
-
110
- if (isGuildSetting) {
111
- payload.user_id = null;
112
- }
113
-
114
- // Try UPDATE first (safe with RLS). If nothing updated, INSERT.
115
- const updateQuery = this.supabase
116
- .from('plugin_settings')
117
- .update({ settings })
118
- .eq('plugin_id', this.pluginId)
119
- .eq('guild_id', this.guild.id)
120
- .eq('is_guild_setting', isGuildSetting);
121
-
122
- const { data: updatedRows, error: updateError } = await (isGuildSetting
123
- ? updateQuery.is('user_id', null).select('id')
124
- : updateQuery.select('id'));
125
-
126
- if (updateError) {
127
- if (updateError.code === '42501' || updateError.message?.includes('policy')) {
128
- throw new Error(`Cannot set ${isGuildSetting ? 'guild' : 'user'} settings: Permission denied.`);
129
- }
130
- // proceed to try insert in case of other issues
131
- }
132
-
133
- if (updatedRows && updatedRows.length > 0) {
134
- return; // updated successfully
135
- }
136
-
137
- // No row updated -> INSERT
138
- const { error: insertError } = await this.supabase.from('plugin_settings').insert(payload);
139
-
140
- if (insertError) {
141
- // In case of race condition (duplicate), try one more UPDATE
142
- if (insertError.code === '23505' /* unique_violation */) {
143
- const retry = this.supabase
144
- .from('plugin_settings')
145
- .update({ settings })
146
- .eq('plugin_id', this.pluginId)
147
- .eq('guild_id', this.guild.id)
148
- .eq('is_guild_setting', isGuildSetting);
149
- const { error: retryError } = await (isGuildSetting ? retry.is('user_id', null) : retry);
150
- if (!retryError) return;
151
- }
152
-
153
- throw insertError;
154
- }
155
- }
156
-
157
- /**
158
- * Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
159
- * @param defaultSettings The default settings to use if no settings are found.
160
- * @returns The settings for the plugin.
161
- */
162
- public async getSettings<T extends object>(defaultSettings: T): Promise<T> {
163
- const storedSettings = (await this.fetchSettings()) as T | null;
164
-
165
- if (!storedSettings) {
166
- await this.setSettings(defaultSettings);
167
- return defaultSettings;
168
- }
169
-
170
- // Handle settings migration
171
- const storedKeys = Object.keys(storedSettings);
172
- const defaultKeys = Object.keys(defaultSettings);
173
-
174
- if (storedKeys.length !== defaultKeys.length) {
175
- const validStoredSettings = Object.fromEntries(
176
- Object.entries(storedSettings).filter(([key]) => defaultKeys.includes(key)),
177
- );
178
- const mergedSettings = { ...defaultSettings, ...validStoredSettings } as T;
179
-
180
- await this.setSettings(mergedSettings);
181
- return mergedSettings;
182
- }
183
-
184
- return storedSettings;
185
- }
186
- }
@@ -1,365 +0,0 @@
1
- import { SupabaseClient } from '@supabase/supabase-js';
2
- import { RimoriClient } from '../plugin/RimoriClient';
3
- import { ObjectRequest } from './ObjectController';
4
-
5
- export interface SharedContentObjectRequest extends ObjectRequest {
6
- fixedProperties?: Record<string, string | number | boolean>;
7
- }
8
-
9
- export type SharedContentFilter = Record<string, string | number | boolean>;
10
-
11
- export class SharedContentController {
12
- private supabase: SupabaseClient;
13
- private rimoriClient: RimoriClient;
14
-
15
- constructor(supabase: SupabaseClient, rimoriClient: RimoriClient) {
16
- this.supabase = supabase;
17
- this.rimoriClient = rimoriClient;
18
- }
19
-
20
- /**
21
- * Fetch new shared content for a given content type.
22
- * @param contentType - The type of content to fetch.
23
- * @param generatorInstructions - The instructions for the generator. The object needs to have a tool property with a topic and keywords property to let a new unique topic be generated.
24
- * @param filter - An optional filter to apply to the query.
25
- * @param options - Optional options.
26
- * @param options.privateTopic - If the topic should be private and only be visible to the user.
27
- * @param options.skipDbSave - If true, do not persist a newly generated content to the DB (default false).
28
- * @param options.alwaysGenerateNew - If true, always generate a new content even if there is already a content with the same filter.
29
- * @param options.excludeIds - Optional list of shared_content ids to exclude from selection.
30
- * @returns The new shared content.
31
- */
32
- public async getNewSharedContent<T>(
33
- contentType: string,
34
- generatorInstructions: SharedContentObjectRequest,
35
- //this filter is there if the content should be filtered additionally by a column and value
36
- filter?: SharedContentFilter,
37
- options?: { privateTopic?: boolean; skipDbSave?: boolean; alwaysGenerateNew?: boolean; excludeIds?: string[] },
38
- ): Promise<SharedContent<T>> {
39
- // The db cache of the shared content is temporary disabled until the new shared content implementation is completed
40
- // if (false) {
41
- // let query = this.supabase
42
- // .from('shared_content')
43
- // .select('*, scc:shared_content_completed(id, state)')
44
- // .eq('content_type', contentType)
45
- // .not('scc.state', 'in', '("completed","ongoing","hidden")')
46
- // .is('deleted_at', null);
47
-
48
- // if (options?.excludeIds?.length ?? 0 > 0) {
49
- // const excludeIds = options.excludeIds.filter((id) => !id.startsWith('internal-temp-id-'));
50
- // // Supabase expects raw PostgREST syntax like '("id1","id2")'.
51
- // const excludeList = `(${excludeIds.map((id) => `"${id}"`).join(',')})`;
52
- // query = query.not('id', 'in', excludeList);
53
- // }
54
-
55
- // if (filter) {
56
- // query.contains('data', filter);
57
- // }
58
-
59
- // const { data: newAssignments, error } = await query.limit(30);
60
-
61
- // if (error) {
62
- // console.error('error fetching new assignments:', error);
63
- // throw new Error('error fetching new assignments');
64
- // }
65
-
66
- // // console.log('newAssignments:', newAssignments);
67
-
68
- // if (!options?.alwaysGenerateNew && newAssignments.length > 0) {
69
- // const index = Math.floor(Math.random() * newAssignments.length);
70
- // return newAssignments[index];
71
- // }
72
- // }
73
- const instructions = await this.generateNewAssignment(contentType, generatorInstructions, filter);
74
-
75
- console.log('instructions:', instructions);
76
-
77
- //create the shared content object
78
- const data: SharedContent<T> = {
79
- id: 'internal-temp-id-' + Math.random().toString(36).substring(2, 15),
80
- contentType,
81
- title: instructions.title,
82
- keywords: instructions.keywords.map(({ text }: { text: string }) => text),
83
- data: { ...instructions, title: undefined, keywords: undefined, ...generatorInstructions.fixedProperties },
84
- privateTopic: options?.privateTopic,
85
- };
86
-
87
- if (options?.skipDbSave) {
88
- return data;
89
- }
90
-
91
- return await this.createSharedContent(data);
92
- }
93
-
94
- private async generateNewAssignment(
95
- contentType: string,
96
- generatorInstructions: SharedContentObjectRequest,
97
- filter?: SharedContentFilter,
98
- ): Promise<any> {
99
- const fullInstructions = await this.getGeneratorInstructions(contentType, generatorInstructions, filter);
100
-
101
- console.log('fullInstructions:', fullInstructions);
102
-
103
- return await this.rimoriClient.ai.getObject(fullInstructions);
104
- }
105
-
106
- private async getGeneratorInstructions(
107
- contentType: string,
108
- generatorInstructions: ObjectRequest,
109
- filter?: SharedContentFilter,
110
- ): Promise<ObjectRequest> {
111
- const completedTopics = await this.getCompletedTopics(contentType, filter);
112
-
113
- generatorInstructions.instructions += `
114
- The following topics are already taken: ${completedTopics.join(', ')}`;
115
-
116
- generatorInstructions.tool.title = {
117
- type: 'string',
118
- description: 'What the topic is about. Short. ',
119
- };
120
- generatorInstructions.tool.keywords = {
121
- type: [{ text: { type: 'string' } }],
122
- description: 'Keywords around the topic of the assignment.',
123
- };
124
- return generatorInstructions;
125
- }
126
-
127
- private async getCompletedTopics(contentType: string, filter?: SharedContentFilter): Promise<string[]> {
128
- const query = this.supabase
129
- .from('shared_content')
130
- .select('title, keywords, scc:shared_content_completed(id)')
131
- .eq('content_type', contentType)
132
- .not('scc.id', 'is', null)
133
- .is('deleted_at', null);
134
-
135
- if (filter) {
136
- query.contains('data', filter);
137
- }
138
-
139
- const { data: oldAssignments, error } = await query;
140
-
141
- if (error) {
142
- console.error('error fetching old assignments:', error);
143
- return [];
144
- }
145
- return oldAssignments.map(({ title, keywords }) => `${title}(${keywords.join(',')})`);
146
- }
147
-
148
- public async getSharedContent<T>(contentType: string, id: string): Promise<SharedContent<T>> {
149
- const { data, error } = await this.supabase
150
- .from('shared_content')
151
- .select()
152
- .eq('content_type', contentType)
153
- .eq('id', id)
154
- .is('deleted_at', null)
155
- .single();
156
- if (error) {
157
- console.error('error fetching shared content:', error);
158
- throw new Error('error fetching shared content');
159
- }
160
- return data;
161
- }
162
-
163
- public async completeSharedContent(contentType: string, assignmentId: string) {
164
- // Idempotent completion: upsert on (id, user_id) so repeated calls don't fail
165
- const { error } = await this.supabase
166
- .from('shared_content_completed')
167
- .upsert({ content_type: contentType, id: assignmentId } as any, { onConflict: 'id' });
168
-
169
- if (error) {
170
- console.error('error completing shared content:', error);
171
- throw new Error('error completing shared content');
172
- }
173
- }
174
-
175
- /**
176
- * Update state details for a shared content entry in shared_content_completed.
177
- * Assumes table has columns: state ('completed'|'ongoing'|'hidden'), reaction ('liked'|'disliked'|null), bookmarked boolean.
178
- * Upserts per (id, content_type, user).
179
- * @param param
180
- * @param param.contentType - The content type.
181
- * @param param.id - The shared content id.
182
- * @param param.state - The state to set.
183
- * @param param.reaction - Optional reaction.
184
- * @param param.bookmarked - Optional bookmark flag.
185
- */
186
- public async updateSharedContentState({
187
- contentType,
188
- id,
189
- state,
190
- reaction,
191
- bookmarked,
192
- }: {
193
- contentType: string;
194
- id: string;
195
- state?: 'completed' | 'ongoing' | 'hidden';
196
- reaction?: 'liked' | 'disliked' | null;
197
- bookmarked?: boolean;
198
- }): Promise<void> {
199
- const payload: Record<string, unknown> = { content_type: contentType, id };
200
- if (state !== undefined) payload.state = state;
201
- if (reaction !== undefined) payload.reaction = reaction;
202
- if (bookmarked !== undefined) payload.bookmarked = bookmarked;
203
-
204
- // Prefer upsert, fall back to insert/update if upsert not allowed
205
- const { error } = await this.supabase.from('shared_content_completed').upsert(payload as any, { onConflict: 'id' });
206
-
207
- if (error) {
208
- console.error('error updating shared content state:', error);
209
- throw new Error('error updating shared content state');
210
- }
211
- }
212
-
213
- /**
214
- * Fetch shared content from the database based on optional filters.
215
- * @param contentType - The type of content to fetch.
216
- * @param filter - Optional filter to apply to the query.
217
- * @param limit - Optional limit for the number of results.
218
- * @returns Array of shared content matching the criteria.
219
- */
220
- public async getSharedContentList<T>(
221
- contentType: string,
222
- filter?: SharedContentFilter,
223
- limit?: number,
224
- ): Promise<SharedContent<T>[]> {
225
- const query = this.supabase
226
- .from('shared_content')
227
- .select('*')
228
- .eq('content_type', contentType)
229
- .is('deleted_at', null)
230
- .limit(limit ?? 30);
231
-
232
- if (filter) {
233
- query.contains('data', filter);
234
- }
235
-
236
- const { data, error } = await query;
237
-
238
- if (error) {
239
- console.error('error fetching shared content:', error);
240
- throw new Error('error fetching shared content');
241
- }
242
-
243
- return data;
244
- }
245
-
246
- /**
247
- * Insert new shared content into the database.
248
- * @param param
249
- * @param param.contentType - The type of content to insert.
250
- * @param param.title - The title of the content.
251
- * @param param.keywords - Keywords associated with the content.
252
- * @param param.data - The content data to store.
253
- * @param param.privateTopic - Optional flag to indicate if the topic should be private.
254
- * @returns The inserted shared content.
255
- * @throws {Error} if insertion fails.
256
- */
257
- public async createSharedContent<T>({
258
- contentType,
259
- title,
260
- keywords,
261
- data,
262
- privateTopic,
263
- }: Omit<SharedContent<T>, 'id'>): Promise<SharedContent<T>> {
264
- const { data: newContent, error } = await this.supabase
265
- .from('shared_content')
266
- .insert({
267
- private: privateTopic,
268
- content_type: contentType,
269
- title,
270
- keywords,
271
- data,
272
- })
273
- .select();
274
-
275
- if (error) {
276
- console.error('error inserting shared content:', error);
277
- throw new Error('error inserting shared content');
278
- }
279
-
280
- return newContent[0];
281
- }
282
-
283
- /**
284
- * Update existing shared content in the database.
285
- * @param id - The ID of the content to update.
286
- * @param updates - The updates to apply to the shared content.
287
- * @returns The updated shared content.
288
- * @throws {Error} if update fails.
289
- */
290
- public async updateSharedContent<T>(id: string, updates: Partial<SharedContent<T>>): Promise<SharedContent<T>> {
291
- const updateData: any = {};
292
-
293
- if (updates.contentType) updateData.content_type = updates.contentType;
294
- if (updates.title) updateData.title = updates.title;
295
- if (updates.keywords) updateData.keywords = updates.keywords;
296
- if (updates.data) updateData.data = updates.data;
297
- if (updates.privateTopic !== undefined) updateData.private = updates.privateTopic;
298
-
299
- const { data: updatedContent, error } = await this.supabase
300
- .from('shared_content')
301
- .update(updateData)
302
- .eq('id', id)
303
- .select();
304
-
305
- if (error) {
306
- console.error('error updating shared content:', error);
307
- throw new Error('error updating shared content');
308
- }
309
-
310
- if (!updatedContent || updatedContent.length === 0) {
311
- throw new Error('shared content not found');
312
- }
313
-
314
- return updatedContent[0];
315
- }
316
-
317
- /**
318
- * Soft delete shared content by setting the deleted_at timestamp.
319
- * @param id - The ID of the content to delete.
320
- * @returns The deleted shared content record.
321
- * @throws {Error} if deletion fails or content not found.
322
- */
323
- public async removeSharedContent(id: string): Promise<SharedContent<any>> {
324
- const { data: deletedContent, error } = await this.supabase
325
- .from('shared_content')
326
- .update({ deleted_at: new Date().toISOString() })
327
- .eq('id', id)
328
- .select();
329
-
330
- if (error) {
331
- console.error('error deleting shared content:', error);
332
- throw new Error('error deleting shared content');
333
- }
334
-
335
- if (!deletedContent || deletedContent.length === 0) {
336
- throw new Error('shared content not found or already deleted');
337
- }
338
-
339
- return deletedContent[0];
340
- }
341
- }
342
-
343
- /**
344
- * Interface representing shared content in the system.
345
- * @template T The type of data stored in the content
346
- */
347
- export interface SharedContent<T> {
348
- /** The id of the content */
349
- id: string;
350
-
351
- /** The type/category of the content (e.g. 'grammar_exercises', 'flashcards', etc.) */
352
- contentType: string;
353
-
354
- /** The human readable title of the content */
355
- title: string;
356
-
357
- /** Array of keywords/tags associated with the content for search and categorization */
358
- keywords: string[];
359
-
360
- /** The actual content data of type T */
361
- data: T;
362
-
363
- /** Whether this content should only be visible to the creator. Defaults to false if not specified */
364
- privateTopic?: boolean;
365
- }