@rimori/client 2.5.50 → 2.5.51

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.
@@ -129,12 +129,17 @@ export declare class AIModule {
129
129
  * @param params.cache Whether to cache the result by prompt hash (default: true).
130
130
  * @param params.aspectRatio Shape + size preset for the image (default: 'square_medium').
131
131
  * `small` ≈ 200px on the longest edge, `medium` ≈ 512px, `big` keeps native HD resolution.
132
+ * @param params.bypassCache Force a fresh generation even if a cached image exists for this
133
+ * prompt, and overwrite that cache entry with the new result — use for a "regenerate, this
134
+ * looks bad" action so later callers get the improved image too. Unlike `cache: false`
135
+ * (which never reads OR writes the shared cache), this still writes it.
132
136
  * @returns `{ url, cached }` where `url` is a stored CDN URL.
133
137
  */
134
138
  getImage(params: {
135
139
  prompt: string;
136
140
  cache?: boolean;
137
141
  aspectRatio?: ImageAspectRatio;
142
+ bypassCache?: boolean;
138
143
  }): Promise<{
139
144
  url: string;
140
145
  cached: boolean;
@@ -127,16 +127,21 @@ export class AIModule {
127
127
  * @param params.cache Whether to cache the result by prompt hash (default: true).
128
128
  * @param params.aspectRatio Shape + size preset for the image (default: 'square_medium').
129
129
  * `small` ≈ 200px on the longest edge, `medium` ≈ 512px, `big` keeps native HD resolution.
130
+ * @param params.bypassCache Force a fresh generation even if a cached image exists for this
131
+ * prompt, and overwrite that cache entry with the new result — use for a "regenerate, this
132
+ * looks bad" action so later callers get the improved image too. Unlike `cache: false`
133
+ * (which never reads OR writes the shared cache), this still writes it.
130
134
  * @returns `{ url, cached }` where `url` is a stored CDN URL.
131
135
  */
132
136
  async getImage(params) {
133
- const { prompt, cache = true, aspectRatio = 'square_medium' } = params;
137
+ const { prompt, cache = true, aspectRatio = 'square_medium', bypassCache = false } = params;
134
138
  const response = await this.controller.fetchBackend('/ai/image', {
135
139
  method: 'POST',
136
140
  body: JSON.stringify({
137
141
  prompt,
138
142
  cache,
139
143
  aspectRatio,
144
+ bypassCache,
140
145
  session_token_id: this.sessionTokenId ?? undefined,
141
146
  }),
142
147
  });
@@ -2,6 +2,7 @@ import { ObjectTool } from '../../fromRimori/PluginTypes';
2
2
  import { SupabaseClient } from '../CommunicationHandler';
3
3
  import { RimoriClient } from '../RimoriClient';
4
4
  import { LanguageLevel } from '../../utils/difficultyConverter';
5
+ import { OnStreamedObjectResult } from './AIModule';
5
6
  export type SharedContent<T> = BasicSharedContent & T;
6
7
  export type ContentStatus = 'featured' | 'community' | 'unverified';
7
8
  export type SharedContentSkillType = 'grammar' | 'reading' | 'writing' | 'speaking' | 'listening' | 'understanding';
@@ -43,6 +44,11 @@ export declare class SharedContentController {
43
44
  * @param params.skipDbSave - If true, don't save generated content to database
44
45
  * @param params.isPrivate - If true, content is guild-specific
45
46
  * @param params.ignoreSkillLevel - If true, don't filter by skill level or add skill level guidance to AI instructions
47
+ * @param params.onResult - When provided, the request streams: this fires with each partial
48
+ * object as the LLM output arrives (e.g. a long markdown field can render progressively),
49
+ * then once more with the final object and `isLoading: false`. Only fires for an actual
50
+ * generation — a cache hit against existing content resolves the returned promise directly
51
+ * without ever calling it.
46
52
  * @returns Existing or newly generated shared content item
47
53
  */
48
54
  getNew<T>(params: {
@@ -58,7 +64,14 @@ export declare class SharedContentController {
58
64
  skipDbSave?: boolean;
59
65
  isPrivate?: boolean;
60
66
  ignoreSkillLevel?: boolean;
67
+ onResult?: OnStreamedObjectResult<Partial<SharedContent<T>>>;
61
68
  }): Promise<SharedContent<T>>;
69
+ /**
70
+ * SSE variant of `getNew` — same request/response shape, but reads the response as a stream of
71
+ * `data: {...}\n\n` frames (mirrors the framing `AIModule.getStreamedObject` parses off
72
+ * `/ai/llm`) and calls `onResult` with each partial object, then the final one.
73
+ */
74
+ private getNewStreamed;
62
75
  /**
63
76
  * Search for shared content by topic using RAG (semantic similarity).
64
77
  * Returns the first matching content that hasn't been completed by the user.
@@ -20,31 +20,89 @@ export class SharedContentController {
20
20
  * @param params.skipDbSave - If true, don't save generated content to database
21
21
  * @param params.isPrivate - If true, content is guild-specific
22
22
  * @param params.ignoreSkillLevel - If true, don't filter by skill level or add skill level guidance to AI instructions
23
+ * @param params.onResult - When provided, the request streams: this fires with each partial
24
+ * object as the LLM output arrives (e.g. a long markdown field can render progressively),
25
+ * then once more with the final object and `isLoading: false`. Only fires for an actual
26
+ * generation — a cache hit against existing content resolves the returned promise directly
27
+ * without ever calling it.
23
28
  * @returns Existing or newly generated shared content item
24
29
  */
25
30
  async getNew(params) {
31
+ const requestBody = {
32
+ tableName: params.table,
33
+ skillType: params.skillType,
34
+ placeholders: params.placeholders,
35
+ filter: params.filter,
36
+ customFields: params.customFields,
37
+ tool: params.tool,
38
+ options: {
39
+ skipDbSave: params.skipDbSave,
40
+ isPrivate: params.isPrivate,
41
+ ignoreSkillLevel: params.ignoreSkillLevel,
42
+ },
43
+ };
44
+ if (params.onResult) {
45
+ return this.getNewStreamed(requestBody, params.onResult);
46
+ }
26
47
  // Generate new content via backend endpoint
27
48
  const response = await this.rimoriClient.runtime.fetchBackend('/shared-content/generate', {
28
49
  method: 'POST',
29
- body: JSON.stringify({
30
- tableName: params.table,
31
- skillType: params.skillType,
32
- placeholders: params.placeholders,
33
- filter: params.filter,
34
- customFields: params.customFields,
35
- tool: params.tool,
36
- options: {
37
- skipDbSave: params.skipDbSave,
38
- isPrivate: params.isPrivate,
39
- ignoreSkillLevel: params.ignoreSkillLevel,
40
- },
41
- }),
50
+ body: JSON.stringify(requestBody),
42
51
  });
43
52
  if (!response.ok) {
44
53
  throw new Error(`Failed to generate shared content: ${response.statusText}`);
45
54
  }
46
55
  return await response.json();
47
56
  }
57
+ /**
58
+ * SSE variant of `getNew` — same request/response shape, but reads the response as a stream of
59
+ * `data: {...}\n\n` frames (mirrors the framing `AIModule.getStreamedObject` parses off
60
+ * `/ai/llm`) and calls `onResult` with each partial object, then the final one.
61
+ */
62
+ async getNewStreamed(requestBody, onResult) {
63
+ const response = await this.rimoriClient.runtime.fetchBackend('/shared-content/generate', {
64
+ method: 'POST',
65
+ body: JSON.stringify({ ...requestBody, stream: true }),
66
+ });
67
+ if (!response.ok) {
68
+ throw new Error(`Failed to generate shared content: ${response.statusText}`);
69
+ }
70
+ if (!response.body) {
71
+ throw new Error('No response body.');
72
+ }
73
+ const reader = response.body.getReader();
74
+ const decoder = new TextDecoder('utf-8');
75
+ let currentObject = {};
76
+ // See AIModule.streamObject — a chunk boundary can split a line mid-way, so an incomplete
77
+ // trailing line is buffered and prepended to the next chunk rather than parsed as-is.
78
+ let lineBuffer = '';
79
+ while (true) {
80
+ const { value, done } = await reader.read();
81
+ if (done)
82
+ break;
83
+ if (!value)
84
+ continue;
85
+ const combined = lineBuffer + decoder.decode(value, { stream: true });
86
+ const parts = combined.split('\n');
87
+ lineBuffer = parts.pop() ?? '';
88
+ for (const line of parts.filter((l) => l.trim())) {
89
+ if (line.startsWith('error:')) {
90
+ throw new Error(JSON.parse(line.slice(6).trim()).message ?? 'Error generating shared content');
91
+ }
92
+ if (!line.startsWith('data:'))
93
+ continue;
94
+ const dataStr = line.slice(5).trim();
95
+ if (dataStr === '[DONE]') {
96
+ onResult(currentObject, false);
97
+ return currentObject;
98
+ }
99
+ currentObject = JSON.parse(dataStr);
100
+ onResult(currentObject, true);
101
+ }
102
+ }
103
+ onResult(currentObject, false);
104
+ return currentObject;
105
+ }
48
106
  /**
49
107
  * Search for shared content by topic using RAG (semantic similarity).
50
108
  * Returns the first matching content that hasn't been completed by the user.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "2.5.50",
3
+ "version": "2.5.51",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {