@lynn123411/dsh-model-submenu 0.3.3 → 0.3.10

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.
@@ -1,125 +1,123 @@
1
1
  /*
2
- * The composer model seat and the picker it opens. The menu surface renders
3
- * through the primitives' MenuSurface and the dialog through Modal, so neither
4
- * this file paints a floating fill — it only places content on the surfaces
5
- * those two own.
2
+ * The reasoning-level slider, the picker, and the composer overlay that hosts
3
+ * the picker. The dialog renders through the primitives' Modal, so this file
4
+ * paints no floating fill — it only places content on the surface that
5
+ * component owns.
6
6
  */
7
7
 
8
- .root {
9
- min-width: 0;
10
- position: relative;
11
- display: flex;
8
+ /* The overlay entry is only a mount point for the dialog and its toast; it must
9
+ * not take space inside the composer card. */
10
+ .pickerHost {
11
+ display: contents;
12
12
  }
13
13
 
14
- /* The Models glyph replaces the text when the composer row cannot fit it; the
15
- * row owns both variables, so this plugin follows the same compact contract as
16
- * the built-in occupant. */
17
- .trigger {
18
- min-width: 0;
19
- max-width: min(360px, 45cqw);
20
- height: 28px;
21
- color: var(--dsw-alias-label-secondary);
22
- cursor: pointer;
23
- background: 0 0;
24
- border: none;
25
- border-radius: 24px;
26
- outline: none;
27
- align-items: center;
28
- gap: 4px;
29
- padding: 0 4px 0 8px;
30
- font-size: 13px;
31
- font-weight: 500;
32
- line-height: 20px;
33
- display: flex;
34
- }
14
+ /* ── the reasoning-level slider ──────────────────────────────────────────── */
35
15
 
36
- .trigger:hover:not(:disabled) {
37
- background: var(--dsw-alias-interactive-bg-hover);
16
+ /* The Host's effort cell is a flex row: label, value, chevron. The slider takes
17
+ * the slack between the label and the value, so the cell keeps the Host's own
18
+ * metrics and the menu does not grow. */
19
+ .sliderCell {
20
+ display: flex;
21
+ flex: 1 1 auto;
22
+ align-items: center;
23
+ align-self: stretch;
24
+ min-width: 96px;
38
25
  }
39
26
 
40
- .trigger:focus-visible {
41
- box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);
27
+ .slider {
28
+ display: flex;
29
+ flex: 1;
30
+ align-items: center;
31
+ height: 100%;
32
+ min-width: 0;
33
+ outline: none;
34
+ cursor: pointer;
35
+ touch-action: none;
36
+ user-select: none;
37
+ -webkit-user-select: none;
42
38
  }
43
39
 
44
- .trigger:disabled {
45
- color: var(--dsw-alias-label-dimmed);
40
+ .slider[data-busy] {
46
41
  cursor: default;
47
42
  }
48
43
 
49
- .triggerLabel {
50
- text-overflow: ellipsis;
51
- white-space: nowrap;
52
- min-width: 0;
53
- overflow: hidden;
54
- display: var(--dsh-composer-model-text-display, block);
44
+ .sliderTrack {
45
+ position: relative;
46
+ width: 100%;
47
+ height: 20px;
48
+ border-radius: 999px;
49
+ corner-shape: round;
50
+ background: var(--dsw-alias-border-l3);
55
51
  }
56
52
 
57
- /* The effort caption is the accent the reference UI uses to say "this is the
58
- * thinking level in force"; it carries no extra meaning of its own. */
59
- .triggerEffort {
60
- text-overflow: ellipsis;
61
- white-space: nowrap;
62
- min-width: 0;
63
- color: var(--dsw-alias-state-business-primary);
64
- flex-shrink: 1000;
53
+ /* The fill takes the level's color and is clipped to the filled part, so the
54
+ * color under the knob is the color the level's text takes. */
55
+ .sliderFill {
56
+ position: absolute;
57
+ top: 0;
58
+ bottom: 0;
59
+ left: 0;
60
+ border-radius: 999px;
61
+ corner-shape: round;
65
62
  overflow: hidden;
66
- display: var(--dsh-composer-model-text-display, block);
63
+ pointer-events: none;
64
+ }
65
+
66
+ /* One detent per level: the dot is the only thing that says the levels are
67
+ * discrete, so it stays visible on both sides of the knob. */
68
+ .sliderTick {
69
+ position: absolute;
70
+ top: 50%;
71
+ width: 4px;
72
+ height: 4px;
73
+ margin: -2px 0 0 -2px;
74
+ border-radius: 50%;
75
+ corner-shape: round;
76
+ background: var(--dsw-alias-label-tertiary);
77
+ pointer-events: none;
78
+ }
79
+
80
+ /* On the filled part the detent goes dark: white would read as one more grain
81
+ * of stardust, and the dot has to stay the thing that says "levels". */
82
+ .sliderTick[data-on] {
83
+ background: rgba(0, 0, 0, 0.28);
84
+ }
85
+
86
+ .sliderKnob {
87
+ position: absolute;
88
+ top: 50%;
89
+ width: 20px;
90
+ height: 20px;
91
+ margin: -10px 0 0 -10px;
92
+ border-radius: 50%;
93
+ corner-shape: round;
94
+ background: var(--dsw-alias-label-primary-foreground);
95
+ box-shadow: var(--dsw-elevation-soft);
96
+ pointer-events: none;
67
97
  }
68
98
 
69
- .triggerIcon {
70
- flex: 0 0 auto;
71
- display: var(--dsh-composer-model-icon-display, none);
99
+ /* Provider default: nothing is filled and the knob is a hollow ring, so the
100
+ * state never reads as the weakest level. */
101
+ .slider[data-default] .sliderKnob {
102
+ background: var(--dsw-alias-bg-layer-1);
103
+ border: 1.5px solid var(--dsw-alias-label-tertiary);
104
+ box-shadow: none;
72
105
  }
73
106
 
74
- .chevron {
75
- color: var(--dsw-alias-label-caption);
76
- flex: none;
77
- transition: transform .12s;
107
+ .slider[data-busy] .sliderKnob {
108
+ opacity: 0.6;
78
109
  }
79
110
 
80
- .chevronOpen {
81
- transform: rotate(180deg);
111
+ .slider:focus-visible .sliderTrack {
112
+ outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
113
+ outline-offset: 3px;
82
114
  }
83
115
 
84
- .menu {
85
- z-index: 1100;
86
- --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
87
- /* A fixed width keeps the card independent of the trigger's text length, so
88
- * opening the menu never reflows the composer. */
89
- box-sizing: border-box;
90
- width: min(252px, calc(100vw - 24px));
91
- max-height: min(70vh, calc(100vh - 96px));
92
- box-shadow: var(--dsw-elevation-prominent);
93
- color: var(--dsw-alias-label-primary);
94
- --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
95
- --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
96
- border: 0;
97
- flex-direction: column;
98
- padding: 4px;
99
- display: flex;
100
- position: fixed;
101
- overflow: hidden;
102
- }
116
+ /* ── the picker rows ─────────────────────────────────────────────────────── */
103
117
 
104
- /* The provider name is a heading, not a control: it says which provider's
105
- * models the list below belongs to. */
106
- .providerName {
107
- text-overflow: ellipsis;
108
- white-space: nowrap;
109
- min-height: 24px;
110
- color: var(--dsw-alias-label-tertiary);
111
- align-items: center;
112
- padding: 3px 8px;
113
- font-size: 12px;
114
- font-weight: 500;
115
- line-height: 18px;
116
- overflow: hidden;
117
- display: flex;
118
- }
119
-
120
- .groups {
121
- min-height: 0;
122
- overflow-y: auto;
118
+ .rowBadge {
119
+ flex: none;
120
+ margin-inline-start: 6px;
123
121
  }
124
122
 
125
123
  .row {
@@ -146,7 +144,7 @@
146
144
  cursor: pointer;
147
145
  background: 0 0;
148
146
  border: none;
149
- border-radius: 10px;
147
+ border-radius: var(--dsw-radius-md);
150
148
  outline: none;
151
149
  align-items: center;
152
150
  gap: 4px;
@@ -181,13 +179,6 @@
181
179
  overflow: hidden;
182
180
  }
183
181
 
184
- /* The menu's names carry the weight: at this size a regular face reads thin
185
- * against the card. The picker's rows keep theirs regular — there the metadata
186
- * line already separates the rows. */
187
- .menu .rowName {
188
- font-weight: 600;
189
- }
190
-
191
182
  .rowMeta {
192
183
  color: var(--dsw-alias-label-tertiary);
193
184
  text-overflow: ellipsis;
@@ -204,16 +195,6 @@
204
195
  display: grid;
205
196
  }
206
197
 
207
- /* The menu's favorite mark: it lives inside the row button, so hovering it
208
- * highlights the row and clicking it picks the model — no separate hit area. */
209
- .starStatic {
210
- color: var(--dsw-alias-state-warn-primary);
211
- flex: none;
212
- place-items: center;
213
- width: 14px;
214
- display: grid;
215
- }
216
-
217
198
  /* The picker's star is a real control: outline until hover or focus, solid and
218
199
  * gold once favorited, and visible without hover on touch screens. */
219
200
  .starButton {
@@ -221,7 +202,7 @@
221
202
  cursor: pointer;
222
203
  background: 0 0;
223
204
  border: none;
224
- border-radius: 8px;
205
+ border-radius: var(--dsw-radius-sm);
225
206
  outline: none;
226
207
  flex: none;
227
208
  place-items: center;
@@ -255,114 +236,7 @@
255
236
  flex: none;
256
237
  }
257
238
 
258
- .divider {
259
- height: .5px;
260
- background: var(--dsw-alias-border-l2);
261
- flex: none;
262
- margin: 3px 2px;
263
- }
264
-
265
- .effortRow {
266
- align-items: center;
267
- gap: 6px;
268
- padding: 3px 6px;
269
- display: flex;
270
- }
271
-
272
- .effortHeading {
273
- color: var(--dsw-alias-label-primary);
274
- flex: none;
275
- font-size: 12px;
276
- line-height: 18px;
277
- }
278
-
279
- /* The segments scroll sideways rather than wrap, so the row keeps one height
280
- * however many levels a model advertises. */
281
- .effortTrack {
282
- background: var(--dsw-alias-interactive-bg-hover);
283
- border-radius: 999px;
284
- align-items: center;
285
- gap: 1px;
286
- min-width: 0;
287
- margin-left: auto;
288
- padding: 2px;
289
- display: flex;
290
- overflow-x: auto;
291
- scrollbar-width: none;
292
- }
293
-
294
- .effortTrack::-webkit-scrollbar {
295
- display: none;
296
- }
297
-
298
- .effortSegment {
299
- color: var(--dsw-alias-label-secondary);
300
- cursor: pointer;
301
- background: 0 0;
302
- border: none;
303
- border-radius: 999px;
304
- outline: none;
305
- flex: none;
306
- padding: 2px 7px;
307
- font-size: 11px;
308
- line-height: 16px;
309
- }
310
-
311
- .effortSegment:hover:not(:disabled) {
312
- color: var(--dsw-alias-label-primary);
313
- }
314
-
315
- .effortSegment[aria-checked='true'] {
316
- color: var(--dsw-alias-label-primary);
317
- background: var(--dsw-alias-bg-layer-1);
318
- box-shadow: var(--dsw-elevation-soft);
319
- font-weight: 600;
320
- }
321
-
322
- .effortSegment:disabled {
323
- color: var(--dsw-alias-label-dimmed);
324
- cursor: default;
325
- }
326
-
327
- .effortSegment:focus-visible {
328
- outline: 2px solid var(--dsw-alias-border-l3);
329
- outline-offset: -2px;
330
- }
331
-
332
- .hint {
333
- color: var(--dsw-alias-label-tertiary);
334
- margin: 0;
335
- padding: 6px 8px;
336
- font-size: 11px;
337
- line-height: 16px;
338
- }
339
-
340
- .moreRow {
341
- color: var(--dsw-alias-label-secondary);
342
- cursor: pointer;
343
- background: 0 0;
344
- border: none;
345
- border-radius: 10px;
346
- outline: none;
347
- align-items: center;
348
- gap: 6px;
349
- min-height: 32px;
350
- padding: 5px 8px;
351
- font-size: 12px;
352
- line-height: 18px;
353
- display: flex;
354
- }
355
-
356
- .moreRow:hover,
357
- .moreRow:focus-visible {
358
- color: var(--dsw-alias-label-primary);
359
- background: var(--dsw-alias-interactive-bg-hover);
360
- }
361
-
362
- .moreIcon {
363
- color: var(--dsw-alias-label-tertiary);
364
- flex: none;
365
- }
239
+ /* ── shared status lines ─────────────────────────────────────────────────── */
366
240
 
367
241
  .notice {
368
242
  color: var(--dsw-alias-label-tertiary);
@@ -423,8 +297,7 @@
423
297
 
424
298
  /* The tabs scroll sideways rather than wrap: the dialog keeps one row of
425
299
  * providers however many the catalog has, so the list below starts at the same
426
- * height every time. The bar keeps its hairline and hides its scrollbar the way
427
- * the effort track does. */
300
+ * height every time. The bar keeps its hairline and hides its scrollbar. */
428
301
  .pickerTabs {
429
302
  border-bottom: .5px solid var(--dsw-alias-border-l2);
430
303
  gap: 2px;
@@ -445,7 +318,7 @@
445
318
  cursor: pointer;
446
319
  background: 0 0;
447
320
  border: none;
448
- border-radius: 8px;
321
+ border-radius: var(--dsw-radius-sm);
449
322
  outline: none;
450
323
  flex: none;
451
324
  padding: 6px 10px;
@@ -29,18 +29,22 @@ import {
29
29
  reasoningOf,
30
30
  rowsForTab,
31
31
  searchRows,
32
+ type CatalogModel,
32
33
  type ModelRow as CatalogRow,
33
34
  type PickerTab,
34
35
  } from './rows.ts'
36
+ import { Tag, isMobileDevice } from '@deepseek-ai/dsh-client-ui-primitives'
35
37
  import type { ModelFactsLookup } from './facts-source.ts'
36
38
  import type { Selection } from './selection.ts'
37
39
  import { planModelCommit } from './selection.ts'
38
- import css from './ModelSubmenu.module.css'
40
+ import css from './ModelOptions.module.css'
39
41
 
40
42
  /** Props the seat hands the picker. */
41
43
  export interface ModelPickerProps {
42
44
  /** Whether the dialog is showing. */
43
45
  readonly open: boolean
46
+ /** Resolve a catalog badge's localized text for the active language. */
47
+ readonly resolveText: (text: NonNullable<CatalogModel['badge']>['text']) => string
44
48
  /** The shared directory snapshot. */
45
49
  readonly state: ModelDirectoryState
46
50
  /** The selection in force, null before a first selection resolves. */
@@ -51,6 +55,13 @@ export interface ModelPickerProps {
51
55
  readonly favorites: readonly { readonly provider: string; readonly model: string }[]
52
56
  /** Whether a selection is in flight. */
53
57
  readonly busy: boolean
58
+ /**
59
+ * The reasoning level this browser last used for one model, if any.
60
+ * @param provider - the provider route key.
61
+ * @param model - the model id inside that provider.
62
+ * @returns the remembered level, or undefined when none is remembered.
63
+ */
64
+ readonly rememberedOf?: (provider: string, model: string) => string | undefined
54
65
  /** Submit one selection. */
55
66
  readonly onPick: (selection: Selection) => void
56
67
  /** Toggle one model's favorite. */
@@ -67,7 +78,7 @@ export interface ModelPickerProps {
67
78
  * @returns the dialog; nothing while closed.
68
79
  */
69
80
  export function ModelPicker({
70
- open, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t,
81
+ open, resolveText, state, live, facts, favorites, busy, rememberedOf, onPick, onToggleFavorite, onReload, onClose, t,
71
82
  }: ModelPickerProps & PropsLocale<'modelSubmenu'>) {
72
83
  const id = useId()
73
84
  const searchRef = useRef<HTMLInputElement | null>(null)
@@ -97,7 +108,10 @@ export function ModelPicker({
97
108
  setTab(ALL_TAB)
98
109
  setHighlight(0)
99
110
  if (listRef.current !== null) listRef.current.scrollTop = 0
100
- const frame = requestAnimationFrame(() => { searchRef.current?.focus() })
111
+ // 粗指针设备不自动聚焦文本输入:移动端弹键盘会顶起布局,宿主座位也这样守。
112
+ const frame = requestAnimationFrame(() => {
113
+ if (!isMobileDevice()) searchRef.current?.focus()
114
+ })
101
115
  return () => { cancelAnimationFrame(frame) }
102
116
  }, [open])
103
117
 
@@ -130,7 +144,7 @@ export function ModelPicker({
130
144
  }
131
145
  }
132
146
  const pick = (row: CatalogRow) => {
133
- const selection = planModelCommit(live, row.provider, row.model, reason)
147
+ const selection = planModelCommit(live, row.provider, row.model, reason, rememberedOf?.(row.provider, row.model))
134
148
  if (selection === null) {
135
149
  onClose()
136
150
  return
@@ -246,9 +260,11 @@ export function ModelPicker({
246
260
  <div key={`${row.provider}/${row.model}`} data-row-index={index} id={`${id}-row-${index}`}>
247
261
  <ModelRow
248
262
  name={row.entry.name}
263
+ badge={row.entry.badge === undefined
264
+ ? undefined
265
+ : <Tag tone={row.entry.badge.tone} className={css.rowBadge}>{resolveText(row.entry.badge.text)}</Tag>}
249
266
  selected={live?.provider === row.provider && live.model === row.model}
250
267
  busy={busy}
251
- star="interactive"
252
268
  favorited={isFavorite(favorites, row.provider, row.model)}
253
269
  meta={metaLine({
254
270
  row,
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The composer overlay's picker view: the dialog, its catalog, and its verbs.
3
+ *
4
+ * It lives in `conversation.input.overlay` rather than beside the row, because
5
+ * the Host unmounts the model menu as soon as a row is clicked. Everything the
6
+ * dialog reads arrives through this entry's injected face.
7
+ */
8
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
9
+ import { IconWarningOutlineRegular, Toast } from '@deepseek-ai/dsh-client-ui-primitives';
10
+ import type { ModelSelectInjected } from '@deepseek-ai/dsh-client-ui-model-selection/client';
11
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
12
+ import { ModelPicker } from './ModelPicker.tsx';
13
+ import type { FactsSource } from './facts-source.ts';
14
+ import {
15
+ readFavorites,
16
+ toggleFavorite,
17
+ type FavoriteModel,
18
+ type FavoritesStorage,
19
+ } from './favorites.ts';
20
+ import {
21
+ effortFor,
22
+ readEfforts,
23
+ rememberEffort,
24
+ type RememberedEffort,
25
+ } from './effort-memory.ts';
26
+ import { selectionOf, type Selection } from './selection.ts';
27
+ import type { PickerStore } from './picker-state.ts';
28
+ import css from './ModelOptions.module.css';
29
+
30
+ /** Props of the overlay view: the session, the store, and the seat's injected face. */
31
+ export type ModelPickerHostProps = PropsLocale<'modelSubmenu'>
32
+ & ModelSelectInjected
33
+ & {
34
+ /** Session whose catalog this view renders. */
35
+ readonly sessionId: string;
36
+ /** The request written by the menu row. */
37
+ readonly picker: PickerStore;
38
+ /** The settings-derived metadata the picker's line reads. */
39
+ readonly factsSource: FactsSource;
40
+ }
41
+
42
+ /**
43
+ * Render the picker for this session.
44
+ * @param props - session, store, catalog face, and localized copy.
45
+ * @returns the dialog while the store names this session; otherwise nothing.
46
+ */
47
+ export function ModelPickerHost({
48
+ sessionId, picker, available, directory, load, select, resolveText, factsSource, t,
49
+ }: ModelPickerHostProps) {
50
+ const request = useSyncExternalStore(picker.subscribe, picker.getSnapshot);
51
+ const state = useSyncExternalStore((listener) => directory.subscribe(listener), () => directory.getSnapshot());
52
+ const [favorites, setFavorites] = useState<readonly FavoriteModel[]>(() => readFavorites(storageOf()));
53
+ const [memory, setMemory] = useState<readonly RememberedEffort[]>(() => readEfforts(storageOf()));
54
+ const [toast, setToast] = useState<{ seq: number; text: string } | null>(null);
55
+ const toastSeq = useRef(0);
56
+ const rootRef = useRef<HTMLDivElement | null>(null);
57
+
58
+ const busy = state.status === 'selecting';
59
+ const live = useMemo(() => selectionOf(state), [state]);
60
+ const facts = useSyncExternalStore((listener) => factsSource.subscribe(listener), () => factsSource.getSnapshot());
61
+ const open = request.sessionId === sessionId;
62
+ // The injected face is cached per entry and session, but the read goes through
63
+ // a ref anyway: a catalog load writes the store, so an unstable `load` in the
64
+ // dependency list would reload on every render it caused.
65
+ const loadRef = useRef(load);
66
+ loadRef.current = load;
67
+
68
+ // Opening reads the catalog again: another client may have changed it, and a
69
+ // failed load has to be retryable from the same gesture.
70
+ useEffect(() => {
71
+ if (open) loadRef.current();
72
+ }, [open]);
73
+
74
+ // The Host's own menu changes the reasoning level without passing through this
75
+ // plugin, so the memory is written from the directory snapshot too — a picker
76
+ // click alone would only ever record levels chosen in the dialog. The key guard
77
+ // keeps one write per distinct selection instead of one per snapshot.
78
+ const current = state.current;
79
+ const rememberedRef = useRef<string | null>(null);
80
+ useEffect(() => {
81
+ if (current === null) return;
82
+ const key = `${current.provider}/${current.model}/${current.reasoningEffort ?? ''}`;
83
+ if (rememberedRef.current === key) return;
84
+ rememberedRef.current = key;
85
+ setMemory(rememberEffort(storageOf(), current.provider, current.model, current.reasoningEffort));
86
+ }, [current]);
87
+
88
+ if (!available) return null;
89
+
90
+ const raiseToast = (text: string): void => {
91
+ toastSeq.current += 1;
92
+ setToast({ seq: toastSeq.current, text });
93
+ };
94
+ const submit = (selection: Selection): void => {
95
+ void select(selection).then((result) => {
96
+ if (result === undefined || result.ok) {
97
+ setMemory(rememberEffort(storageOf(), selection.provider, selection.model, selection.reasoningEffort));
98
+ picker.close();
99
+ return;
100
+ }
101
+ raiseToast(result.error.code === 'session/writer-held'
102
+ ? t('error.sessionInUse')
103
+ : t('error.action', { message: `${result.error.code}: ${result.error.message}` }));
104
+ });
105
+ };
106
+
107
+ return <div ref={rootRef} className={css.pickerHost}>
108
+ <ModelPicker
109
+ open={open}
110
+ resolveText={resolveText}
111
+ state={state}
112
+ live={live}
113
+ facts={facts}
114
+ favorites={favorites}
115
+ busy={busy}
116
+ t={t}
117
+ rememberedOf={(provider, model) => effortFor(memory, provider, model)}
118
+ onPick={submit}
119
+ onToggleFavorite={(provider, model) => {
120
+ setFavorites(toggleFavorite(storageOf(), provider, model));
121
+ }}
122
+ onReload={load}
123
+ onClose={() => {
124
+ picker.close();
125
+ }}
126
+ />
127
+ {toast !== null && <Toast
128
+ key={toast.seq}
129
+ text={toast.text}
130
+ icon={<IconWarningOutlineRegular />}
131
+ anchor={rootRef.current?.closest('[data-composer-card]') ?? null}
132
+ onDone={() => {
133
+ setToast(null);
134
+ }}
135
+ />}
136
+ </div>;
137
+ }
138
+
139
+ /** Isolate the browser's storage so a locked-down browser reads as "no favorites". */
140
+ function storageOf(): FavoritesStorage | undefined {
141
+ try {
142
+ return globalThis.localStorage;
143
+ } catch {
144
+ return undefined;
145
+ }
146
+ }