@mosetta/editor 0.0.32 → 0.0.33

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @mosetta/editor
2
2
 
3
+ ## 0.0.33
4
+
5
+ - The assistant's chat lets the person choose the model it works with, among the ones the platform
6
+ offers (a larger one writes blocks that look better, and costs more); the choice is kept in this
7
+ browser.
8
+
3
9
  ## 0.0.32
4
10
 
5
11
  - Built with the framework 0.0.12, whose `@mosetta/react` ships the guide to writing tiles — a new
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mosetta/editor",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "license": "MIT",
5
5
  "description": "mosetta visual editor tile palette: block picker, page tree, inspector, isolated draft preview and the content API behind them.",
6
6
  "repository": {
@@ -14,15 +14,15 @@
14
14
  "dependencies": {
15
15
  "react": "*",
16
16
  "react-dom": "*",
17
+ "@mosetta/content": "0.0.12",
17
18
  "@mosetta/core": "0.0.12",
18
19
  "@mosetta/di": "0.0.12",
20
+ "@mosetta/runtime": "0.0.12",
19
21
  "@mosetta/stores": "0.0.12",
20
- "@mosetta/content": "0.0.12",
21
- "@mosetta/react": "0.0.12",
22
- "@mosetta/settings": "0.0.12",
23
22
  "@mosetta/server-action": "0.0.12",
24
- "@mosetta/runtime": "0.0.12",
25
- "@mosetta/ui": "0.0.12"
23
+ "@mosetta/settings": "0.0.12",
24
+ "@mosetta/ui": "0.0.12",
25
+ "@mosetta/react": "0.0.12"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/react": "*",
@@ -68,6 +68,21 @@
68
68
  font-variant-numeric: tabular-nums;
69
69
  }
70
70
 
71
+ .assistant__model {
72
+ max-width: 11em;
73
+ padding: 2px 4px;
74
+ border: 1px solid var(--wb-border, #e4e4e7);
75
+ border-radius: 6px;
76
+ background: #ffffff;
77
+ color: var(--wb-ink, #18181b);
78
+ font: inherit;
79
+ font-size: 0.85em;
80
+ }
81
+
82
+ .assistant__model:disabled {
83
+ opacity: 0.5;
84
+ }
85
+
71
86
  .assistant__head-button {
72
87
  padding: 3px 8px;
73
88
  border: 1px solid transparent;
@@ -28,6 +28,7 @@ import {
28
28
  EMPTY_CHAT,
29
29
  IChatState,
30
30
  IDraftTouched,
31
+ modelLabel,
31
32
  nodeOfCall,
32
33
  TChatItem,
33
34
  touchedNodes,
@@ -48,6 +49,23 @@ function readChat(): IChatState {
48
49
  return EMPTY_CHAT;
49
50
  }
50
51
 
52
+ /** the model the person chose, kept for this browser — one the platform no longer offers is forgotten */
53
+ const MODEL_KEY = 'mosetta-assistant-model';
54
+
55
+ function readModel(): string | null {
56
+ try {
57
+ return window.localStorage.getItem(MODEL_KEY);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function writeModel(model: string): void {
64
+ try {
65
+ window.localStorage.setItem(MODEL_KEY, model);
66
+ } catch { /* storage denied: the choice lives for the page only */ }
67
+ }
68
+
51
69
  function writeChat(state: IChatState): void {
52
70
  try {
53
71
  window.sessionStorage.setItem(storageKey(), JSON.stringify(state));
@@ -232,6 +250,10 @@ export function AssistantPanel({ placement, transcription, onClose, onWorking }:
232
250
  const draftRef = useRef(draft);
233
251
  draftRef.current = draft;
234
252
  const [working, setWorking] = useState(false);
253
+ // the models the platform offers, its own first; the one chosen goes with every message
254
+ const [models, setModels] = useState<readonly string[]>([]);
255
+ const [chosen, setChosen] = useState<string | null>(readModel);
256
+ const model = chosen && models.includes(chosen) ? chosen : models[0] ?? null;
235
257
  // over Home and the lists the island folds to its input when the person wants the page back
236
258
  const [folded, setFolded] = useState(false);
237
259
  const abort = useRef<AbortController | null>(null);
@@ -239,6 +261,19 @@ export function AssistantPanel({ placement, transcription, onClose, onWorking }:
239
261
  const input = useRef<HTMLTextAreaElement>(null);
240
262
  const dictation = useDictation(setDraft, () => draftRef.current, transcription);
241
263
  useEffect(() => writeChat(chat), [chat]);
264
+ useEffect(() => {
265
+ let live = true;
266
+ fetch(ASSISTANT_PATH, { headers: { accept: 'application/json' } })
267
+ .then((res) => (res.ok ? res.json() as Promise<{ models?: unknown }> : null))
268
+ .then((answer) => {
269
+ const listed = Array.isArray(answer?.models) ? answer.models.filter((m): m is string => typeof m === 'string') : [];
270
+ if (live) setModels(listed);
271
+ })
272
+ .catch(() => undefined);
273
+ return () => {
274
+ live = false;
275
+ };
276
+ }, []);
242
277
  useEffect(() => {
243
278
  list.current?.scrollTo({ top: list.current.scrollHeight });
244
279
  }, [chat.items, folded]);
@@ -353,7 +388,7 @@ export function AssistantPanel({ placement, transcription, onClose, onWorking }:
353
388
  const res = await fetch(ASSISTANT_PATH, {
354
389
  method: 'POST',
355
390
  headers: { 'content-type': 'application/json', accept: 'text/event-stream' },
356
- body: JSON.stringify({ history: chat.history, input: { text: message, context: contextNow() } }),
391
+ body: JSON.stringify({ history: chat.history, input: { text: message, context: contextNow(), ...(model && model !== models[0] ? { model } : {}) } }),
357
392
  signal: controller.signal,
358
393
  });
359
394
  if (!res.ok || !res.body) {
@@ -389,7 +424,7 @@ export function AssistantPanel({ placement, transcription, onClose, onWorking }:
389
424
  di.getByToken(SERVER_DATA_MANAGER).invalidate(['pages']);
390
425
  }
391
426
  }
392
- }, [chat, di, dictation, openDraft, reload, sessionsOn, working]);
427
+ }, [chat, di, dictation, model, models, openDraft, reload, sessionsOn, working]);
393
428
 
394
429
  const startOver = (): void => {
395
430
  if (working) return;
@@ -404,6 +439,23 @@ export function AssistantPanel({ placement, transcription, onClose, onWorking }:
404
439
  data-working={working || undefined}>
405
440
  <div className="assistant__head">
406
441
  <span className="assistant__title">Assistant</span>
442
+ {models.length > 1 && model ? (
443
+ <select className="assistant__model"
444
+ data-test-id="assistant-model"
445
+ aria-label="The model the assistant works with"
446
+ data-tooltip="The model the assistant works with — a larger one does better and costs more"
447
+ value={model}
448
+ disabled={working}
449
+ onChange={(e) => {
450
+ setChosen(e.target.value);
451
+ writeModel(e.target.value);
452
+ }}>
453
+ {models.map((m) => (
454
+ <option key={m}
455
+ value={m}>{modelLabel(m)}</option>
456
+ ))}
457
+ </select>
458
+ ) : null}
407
459
  {chat.cost > 0 ? <span className="assistant__cost"
408
460
  data-tooltip="What this conversation has cost">${chat.cost.toFixed(3)}</span> : null}
409
461
  <button type="button"
@@ -163,3 +163,9 @@ export function nodeOfCall(name: string, args: unknown): string | null {
163
163
  }
164
164
  return null;
165
165
  }
166
+
167
+ /** a model as the person reads it: `anthropic/claude-opus-5.5` → `Claude Opus 5.5` */
168
+ export function modelLabel(id: string): string {
169
+ return id.slice(id.indexOf('/') + 1).split('-').filter(Boolean)
170
+ .map((word) => (word === 'gpt' ? 'GPT' : `${word[0].toUpperCase()}${word.slice(1)}`)).join(' ');
171
+ }