@morscherlab/mint-sdk 1.0.39 → 1.0.42
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/dist/{ExperimentPopover-DEzCbTqo.js → ExperimentPopover-8A4Rhffp.js} +1 -1
- package/dist/{ExperimentPopover-mzmSfAUp.js → ExperimentPopover-BbPkIFsI.js} +8 -2
- package/dist/ExperimentPopover-BbPkIFsI.js.map +1 -0
- package/dist/{ExperimentSelectorModal-Bn0Hmg07.js → ExperimentSelectorModal-B2qek_YG.js} +91 -46
- package/dist/ExperimentSelectorModal-B2qek_YG.js.map +1 -0
- package/dist/{ExperimentSelectorModal-BAIlIybO.js → ExperimentSelectorModal-BwPbQN1g.js} +1 -1
- package/dist/__tests__/components/AutoGroupModal.preview.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/classKey.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/groupTree.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/tokenLength.test.d.ts +1 -0
- package/dist/components/AppTopBar.navigation.d.ts +0 -1
- package/dist/components/index.js +3 -3
- package/dist/{components-Cyi0IfRl.js → components-BGVwavdd.js} +5632 -5629
- package/dist/components-BGVwavdd.js.map +1 -0
- package/dist/composables/autoGroup/classKey.d.ts +1 -0
- package/dist/composables/autoGroup/index.d.ts +2 -1
- package/dist/composables/autoGroup/replicatePreGroup.d.ts +10 -12
- package/dist/composables/autoGroup/tokenLength.d.ts +17 -0
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.js +3 -3
- package/dist/composables/useAutoGroup.d.ts +2 -0
- package/dist/composables/usePluginClient.d.ts +82 -5
- package/dist/{composables-CFSn4NN3.js → composables-C_hPF0Gn.js} +256 -9
- package/dist/{composables-CFSn4NN3.js.map → composables-C_hPF0Gn.js.map} +1 -1
- package/dist/index.js +6 -6
- package/dist/install.js +3 -3
- package/dist/styles.css +602 -555
- package/dist/types/auto-group.d.ts +19 -0
- package/dist/{useProtocolTemplates-CXP2ZosM.js → useProtocolTemplates-BbvlHoPD.js} +218 -90
- package/dist/useProtocolTemplates-BbvlHoPD.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.navigation.test.ts +3 -5
- package/src/__tests__/components/AppTopBar.test.ts +2 -5
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +22 -0
- package/src/__tests__/components/AutoGroupModal.preview.test.ts +46 -0
- package/src/__tests__/components/PluginWorkspaceView.test.ts +18 -0
- package/src/__tests__/composables/autoGroup/classKey.test.ts +25 -0
- package/src/__tests__/composables/autoGroup/fingerprint.test.ts +72 -0
- package/src/__tests__/composables/autoGroup/groupTree.test.ts +99 -0
- package/src/__tests__/composables/autoGroup/tokenLength.test.ts +85 -0
- package/src/__tests__/composables/useAutoGroup.test.ts +111 -19
- package/src/__tests__/composables/usePluginClient.test.ts +129 -3
- package/src/components/AppTopBar.navigation.ts +0 -2
- package/src/components/AppTopBar.story.vue +5 -5
- package/src/components/AppTopBar.vue +0 -1
- package/src/components/AutoGroupModal.vue +23 -19
- package/src/components/BaseModal.story.vue +7 -15
- package/src/components/ExperimentDataViewer.vue +1 -0
- package/src/components/ExperimentPopover.vue +6 -4
- package/src/components/ExperimentSelectorModal.vue +30 -3
- package/src/components/IconButton.story.vue +5 -0
- package/src/components/PluginWorkspaceView.vue +5 -1
- package/src/components/SampleSelector.vue +3 -2
- package/src/components/SampleSelectorSampleRow.vue +4 -2
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +0 -1
- package/src/composables/autoGroup/classKey.ts +5 -2
- package/src/composables/autoGroup/columns.ts +2 -2
- package/src/composables/autoGroup/compose.ts +56 -0
- package/src/composables/autoGroup/fingerprint.ts +15 -1
- package/src/composables/autoGroup/index.ts +2 -0
- package/src/composables/autoGroup/replicatePreGroup.ts +34 -0
- package/src/composables/autoGroup/template.ts +2 -2
- package/src/composables/autoGroup/tokenLength.ts +53 -0
- package/src/composables/autoGroup/vocab.json +1 -2
- package/src/composables/index.ts +6 -0
- package/src/composables/useAutoGroup.ts +34 -13
- package/src/composables/usePluginClient.ts +453 -8
- package/src/styles/components/app-page-selector.css +3 -5
- package/src/styles/components/auto-group-modal.css +7 -11
- package/src/styles/components/button.css +14 -4
- package/src/styles/components/modal.css +3 -0
- package/src/styles/components/sample-selector.css +17 -0
- package/src/styles/variables.css +8 -0
- package/src/types/auto-group.ts +19 -0
- package/dist/ExperimentPopover-mzmSfAUp.js.map +0 -1
- package/dist/ExperimentSelectorModal-Bn0Hmg07.js.map +0 -1
- package/dist/components-Cyi0IfRl.js.map +0 -1
- package/dist/useProtocolTemplates-CXP2ZosM.js.map +0 -1
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import { computed, shallowRef, watch, type ComputedRef, type Ref } from 'vue'
|
|
1
|
+
import { computed, onMounted, onUnmounted, ref, shallowRef, watch, type ComputedRef, type Ref } from 'vue'
|
|
2
2
|
import { useApi } from './useApi'
|
|
3
3
|
import { usePlatformContext } from './usePlatformContext'
|
|
4
4
|
import { useRequestSyncState } from './useRequestSyncState'
|
|
5
|
+
import { useAuthStore } from '../stores/auth'
|
|
5
6
|
import {
|
|
6
7
|
currentExperimentFromContext,
|
|
7
8
|
getInjectedPlatformContext,
|
|
8
9
|
resolveCurrentExperimentId,
|
|
9
10
|
} from './platformContextHelpers'
|
|
10
11
|
import {
|
|
12
|
+
buildPluginEndpointUrl,
|
|
11
13
|
hasKeys,
|
|
12
14
|
pluginEndpointRequestParts,
|
|
13
15
|
requestBodyFromPayload,
|
|
14
16
|
} from './pluginEndpointBuilder'
|
|
15
|
-
import type { ExperimentSummary, PageSelectorItem } from '../types'
|
|
17
|
+
import type { ExperimentSummary, PageSelectorItem, SettingsModalSchema, TopBarSettingsConfig } from '../types'
|
|
16
18
|
|
|
17
19
|
export {
|
|
18
20
|
buildPluginEndpointUrl,
|
|
@@ -66,6 +68,10 @@ export interface PluginContract {
|
|
|
66
68
|
analysisResultReaders?: string[]
|
|
67
69
|
capabilities?: Record<string, unknown>
|
|
68
70
|
}
|
|
71
|
+
settings?: {
|
|
72
|
+
modelName?: string | null
|
|
73
|
+
schema?: SettingsModalSchema | null
|
|
74
|
+
}
|
|
69
75
|
endpoints: PluginEndpointContract[]
|
|
70
76
|
hash: string
|
|
71
77
|
}
|
|
@@ -114,6 +120,84 @@ export interface DownloadBlobOptions {
|
|
|
114
120
|
revokeObjectUrlDelayMs?: number
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
export type PluginSettingsValues<TSettings> = Partial<TSettings> & Record<string, unknown>
|
|
124
|
+
|
|
125
|
+
export interface UsePluginSettingsOptions {
|
|
126
|
+
/** Plugin name for platform persistence. Defaults to the integrated platform plugin name. */
|
|
127
|
+
pluginName?: string
|
|
128
|
+
/** Plugin API base URL for standalone/local settings routes. Generated clients pass pluginApiPrefix. */
|
|
129
|
+
apiBaseUrl?: string
|
|
130
|
+
/** Platform API base URL for /plugins/{name}/config. Defaults to the injected platform API URL. */
|
|
131
|
+
platformApiBaseUrl?: string
|
|
132
|
+
/** SettingsModal schema generated from the backend settings_model. */
|
|
133
|
+
schema?: SettingsModalSchema | null
|
|
134
|
+
/** SettingsModal title when using settingsConfig. */
|
|
135
|
+
title?: string
|
|
136
|
+
/** Whether SettingsModal should include SDK appearance controls. */
|
|
137
|
+
showAppearance?: boolean
|
|
138
|
+
/** Load persisted values on component mount. */
|
|
139
|
+
loadOnMount?: boolean
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface UsePluginSettingsReturn<TSettings = Record<string, unknown>> {
|
|
143
|
+
/** Current typed settings values. */
|
|
144
|
+
settings: ComputedRef<PluginSettingsValues<TSettings>>
|
|
145
|
+
/** Editable settings values for SettingsModal. */
|
|
146
|
+
values: Ref<PluginSettingsValues<TSettings>>
|
|
147
|
+
/** Alias for values, kept for callers that think in platform config terms. */
|
|
148
|
+
config: Ref<PluginSettingsValues<TSettings>>
|
|
149
|
+
/** Ready-to-pass AppTopBar/PluginWorkspaceView settingsConfig. */
|
|
150
|
+
settingsConfig: ComputedRef<TopBarSettingsConfig>
|
|
151
|
+
isLoading: Ref<boolean>
|
|
152
|
+
isSaving: Ref<boolean>
|
|
153
|
+
error: Ref<string | null>
|
|
154
|
+
lastLoadedAt: Ref<Date | null>
|
|
155
|
+
lastSavedAt: Ref<Date | null>
|
|
156
|
+
isDirty: ComputedRef<boolean>
|
|
157
|
+
setValues: (values: Record<string, unknown>) => void
|
|
158
|
+
load: () => Promise<void>
|
|
159
|
+
save: (values?: Record<string, unknown>) => Promise<boolean>
|
|
160
|
+
reset: () => void
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface PluginEventStreamMessage<TData = string> {
|
|
164
|
+
event: string
|
|
165
|
+
data: TData
|
|
166
|
+
id?: string
|
|
167
|
+
retry?: number
|
|
168
|
+
raw: string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface PluginEventStreamOptions<TData = string> {
|
|
172
|
+
/** Override the generated or platform-injected API base URL. */
|
|
173
|
+
baseUrl?: string
|
|
174
|
+
/** Start the stream immediately. Defaults to true. */
|
|
175
|
+
immediate?: boolean
|
|
176
|
+
/** Reconnect after errors or server closes. Defaults to true. */
|
|
177
|
+
reconnect?: boolean
|
|
178
|
+
/** Delay before reconnecting after a stream failure or close. Defaults to 2000ms. */
|
|
179
|
+
reconnectDelayMs?: number
|
|
180
|
+
/** Parse `data:` as JSON before calling onMessage. */
|
|
181
|
+
parseJson?: boolean
|
|
182
|
+
/** Fetch credentials mode. Defaults to same-origin. */
|
|
183
|
+
credentials?: RequestCredentials
|
|
184
|
+
/** Extra request headers. Authorization is injected unless already supplied. */
|
|
185
|
+
headers?: Record<string, string>
|
|
186
|
+
onOpen?: () => void
|
|
187
|
+
onMessage?: (message: PluginEventStreamMessage<TData>) => void
|
|
188
|
+
onError?: (error: unknown) => void
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface UsePluginEventStreamReturn<TData = string> {
|
|
192
|
+
isConnected: Ref<boolean>
|
|
193
|
+
isConnecting: Ref<boolean>
|
|
194
|
+
error: Ref<string | null>
|
|
195
|
+
lastMessage: Ref<PluginEventStreamMessage<TData> | null>
|
|
196
|
+
lastEventId: Ref<string | null>
|
|
197
|
+
start: () => void
|
|
198
|
+
stop: () => void
|
|
199
|
+
}
|
|
200
|
+
|
|
117
201
|
export type PluginFormDataValue =
|
|
118
202
|
| string
|
|
119
203
|
| number
|
|
@@ -240,7 +324,6 @@ export function getPluginPageSelectorItems(contract: PluginContract): PageSelect
|
|
|
240
324
|
label: item.label,
|
|
241
325
|
to: normalizePluginNavPath(item.path),
|
|
242
326
|
icon: item.icon || pluginIcon || undefined,
|
|
243
|
-
hint: item.description || contract.plugin.name,
|
|
244
327
|
}))
|
|
245
328
|
}
|
|
246
329
|
|
|
@@ -337,11 +420,373 @@ export function usePluginClient<TClient>(client: TClient): TClient {
|
|
|
337
420
|
return client
|
|
338
421
|
}
|
|
339
422
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
423
|
+
function normalizePluginSettingsOptions(options: UsePluginSettingsOptions | string = {}): UsePluginSettingsOptions {
|
|
424
|
+
return typeof options === 'string' ? { pluginName: options } : options
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
|
|
428
|
+
return { ...value }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function responseSettingsPayload(response: unknown): Record<string, unknown> {
|
|
432
|
+
if (!response || typeof response !== 'object') return {}
|
|
433
|
+
const data = response as {
|
|
434
|
+
config?: unknown
|
|
435
|
+
settings?: unknown
|
|
436
|
+
}
|
|
437
|
+
if (data.config && typeof data.config === 'object') {
|
|
438
|
+
return cloneRecord(data.config as Record<string, unknown>)
|
|
439
|
+
}
|
|
440
|
+
if (data.settings && typeof data.settings === 'object') {
|
|
441
|
+
return cloneRecord(data.settings as Record<string, unknown>)
|
|
442
|
+
}
|
|
443
|
+
return {}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Load, edit, and persist plugin settings from platform config or the plugin-local settings router. */
|
|
447
|
+
export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
448
|
+
options: UsePluginSettingsOptions | string = {},
|
|
449
|
+
): UsePluginSettingsReturn<TSettings> {
|
|
450
|
+
const resolvedOptions = normalizePluginSettingsOptions(options)
|
|
451
|
+
const injectedContext = getInjectedPlatformContext()
|
|
452
|
+
const { plugin, isIntegrated } = usePlatformContext()
|
|
453
|
+
const platformApi = useApi({ baseUrl: resolvedOptions.platformApiBaseUrl ?? injectedContext?.platformApiUrl })
|
|
454
|
+
const pluginApi = useApi({ baseUrl: resolvedOptions.apiBaseUrl ?? injectedContext?.plugin?.api_prefix })
|
|
455
|
+
|
|
456
|
+
const values = shallowRef<PluginSettingsValues<TSettings>>({} as PluginSettingsValues<TSettings>)
|
|
457
|
+
const savedValues = shallowRef<Record<string, unknown>>({})
|
|
458
|
+
const request = useRequestSyncState('Plugin settings request failed.')
|
|
459
|
+
const isLoading = ref(false)
|
|
460
|
+
const isSaving = ref(false)
|
|
461
|
+
|
|
462
|
+
const resolvedPluginName = computed(() =>
|
|
463
|
+
resolvedOptions.pluginName ?? plugin.value?.name ?? injectedContext?.plugin?.name ?? '',
|
|
464
|
+
)
|
|
465
|
+
const usesPlatformConfig = computed(() =>
|
|
466
|
+
Boolean((isIntegrated.value || injectedContext?.isIntegrated) && resolvedPluginName.value),
|
|
467
|
+
)
|
|
468
|
+
const settings = computed(() => values.value)
|
|
469
|
+
const isDirty = computed(() => JSON.stringify(values.value) !== JSON.stringify(savedValues.value))
|
|
470
|
+
const settingsConfig = computed<TopBarSettingsConfig>(() => ({
|
|
471
|
+
title: resolvedOptions.title,
|
|
472
|
+
schema: resolvedOptions.schema ?? undefined,
|
|
473
|
+
showAppearance: resolvedOptions.showAppearance,
|
|
474
|
+
values: values.value,
|
|
475
|
+
}))
|
|
476
|
+
|
|
477
|
+
function setValues(nextValues: Record<string, unknown>): void {
|
|
478
|
+
values.value = cloneRecord(nextValues) as PluginSettingsValues<TSettings>
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function applyLoaded(nextValues: Record<string, unknown>): void {
|
|
482
|
+
const cloned = cloneRecord(nextValues)
|
|
483
|
+
values.value = cloned as PluginSettingsValues<TSettings>
|
|
484
|
+
savedValues.value = cloneRecord(cloned)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function load(): Promise<void> {
|
|
488
|
+
isLoading.value = true
|
|
489
|
+
try {
|
|
490
|
+
const response = usesPlatformConfig.value
|
|
491
|
+
? await request.run(
|
|
492
|
+
() => platformApi.get<{ plugin_name: string; config: Record<string, unknown> }>(
|
|
493
|
+
`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
|
|
494
|
+
),
|
|
495
|
+
{ success: 'load', errorMessage: 'Failed to load plugin settings' },
|
|
496
|
+
)
|
|
497
|
+
: await request.run(
|
|
498
|
+
() => pluginApi.get<{ settings: Record<string, unknown>; mode?: string }>('/settings'),
|
|
499
|
+
{ success: 'load', errorMessage: 'Failed to load plugin settings' },
|
|
500
|
+
)
|
|
501
|
+
applyLoaded(responseSettingsPayload(response))
|
|
502
|
+
} catch {
|
|
503
|
+
// Error state is handled by request.run().
|
|
504
|
+
} finally {
|
|
505
|
+
isLoading.value = false
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function save(nextValues?: Record<string, unknown>): Promise<boolean> {
|
|
510
|
+
if (nextValues) {
|
|
511
|
+
setValues(nextValues)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
isSaving.value = true
|
|
515
|
+
try {
|
|
516
|
+
const payload = cloneRecord(values.value)
|
|
517
|
+
const response = usesPlatformConfig.value
|
|
518
|
+
? await request.run(
|
|
519
|
+
() => platformApi.patch<{ plugin_name: string; config: Record<string, unknown> }>(
|
|
520
|
+
`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
|
|
521
|
+
{ config: payload },
|
|
522
|
+
),
|
|
523
|
+
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
524
|
+
)
|
|
525
|
+
: await request.run(
|
|
526
|
+
() => pluginApi.put<{ settings: Record<string, unknown>; mode?: string }>('/settings', payload),
|
|
527
|
+
{ success: 'save', errorMessage: 'Failed to save plugin settings' },
|
|
528
|
+
)
|
|
529
|
+
applyLoaded(responseSettingsPayload(response))
|
|
530
|
+
return true
|
|
531
|
+
} catch {
|
|
532
|
+
return false
|
|
533
|
+
} finally {
|
|
534
|
+
isSaving.value = false
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function reset(): void {
|
|
539
|
+
values.value = cloneRecord(savedValues.value) as PluginSettingsValues<TSettings>
|
|
540
|
+
request.clearError()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
onMounted(() => {
|
|
544
|
+
if (resolvedOptions.loadOnMount !== false) {
|
|
545
|
+
void load()
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
settings,
|
|
551
|
+
values,
|
|
552
|
+
config: values,
|
|
553
|
+
settingsConfig,
|
|
554
|
+
isLoading,
|
|
555
|
+
isSaving,
|
|
556
|
+
error: request.error,
|
|
557
|
+
lastLoadedAt: request.lastLoadedAt,
|
|
558
|
+
lastSavedAt: request.lastSavedAt,
|
|
559
|
+
isDirty,
|
|
560
|
+
setValues,
|
|
561
|
+
load,
|
|
562
|
+
save,
|
|
563
|
+
reset,
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function streamHeaders(options: PluginEventStreamOptions<unknown>): Headers {
|
|
568
|
+
const headers = new Headers(options.headers)
|
|
569
|
+
const authStore = useAuthStore()
|
|
570
|
+
if (!authStore.isInitialized) {
|
|
571
|
+
authStore.initialize()
|
|
572
|
+
}
|
|
573
|
+
if (authStore.token && !headers.has('Authorization')) {
|
|
574
|
+
headers.set('Authorization', `Bearer ${authStore.token}`)
|
|
575
|
+
}
|
|
576
|
+
headers.set('Accept', 'text/event-stream')
|
|
577
|
+
return headers
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseSseEvent<TData>(
|
|
581
|
+
block: string,
|
|
582
|
+
parseJson: boolean,
|
|
583
|
+
): PluginEventStreamMessage<TData> | null {
|
|
584
|
+
let event = 'message'
|
|
585
|
+
let id: string | undefined
|
|
586
|
+
let retry: number | undefined
|
|
587
|
+
const dataLines: string[] = []
|
|
588
|
+
|
|
589
|
+
for (const line of block.split(/\r?\n/)) {
|
|
590
|
+
if (!line || line.startsWith(':')) continue
|
|
591
|
+
const separator = line.indexOf(':')
|
|
592
|
+
const field = separator === -1 ? line : line.slice(0, separator)
|
|
593
|
+
const value = separator === -1
|
|
594
|
+
? ''
|
|
595
|
+
: line.slice(separator + 1).replace(/^ /, '')
|
|
596
|
+
|
|
597
|
+
if (field === 'event') event = value || 'message'
|
|
598
|
+
if (field === 'data') dataLines.push(value)
|
|
599
|
+
if (field === 'id') id = value
|
|
600
|
+
if (field === 'retry') {
|
|
601
|
+
const parsed = Number(value)
|
|
602
|
+
if (Number.isFinite(parsed)) retry = parsed
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!dataLines.length && !id && retry === undefined) return null
|
|
607
|
+
const data = dataLines.join('\n')
|
|
608
|
+
return {
|
|
609
|
+
event,
|
|
610
|
+
data: (parseJson && data ? JSON.parse(data) : data) as TData,
|
|
611
|
+
id,
|
|
612
|
+
retry,
|
|
613
|
+
raw: block,
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function looksLikeEventStreamOptions(value: unknown): value is PluginEventStreamOptions {
|
|
618
|
+
if (!value || typeof value !== 'object') return false
|
|
619
|
+
const candidate = value as Record<string, unknown>
|
|
620
|
+
return [
|
|
621
|
+
'baseUrl',
|
|
622
|
+
'immediate',
|
|
623
|
+
'reconnect',
|
|
624
|
+
'reconnectDelayMs',
|
|
625
|
+
'parseJson',
|
|
626
|
+
'credentials',
|
|
627
|
+
'headers',
|
|
628
|
+
'onOpen',
|
|
629
|
+
'onMessage',
|
|
630
|
+
'onError',
|
|
631
|
+
].some(key => key in candidate)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Consume a generated plugin SSE endpoint with auth headers, reconnect, and teardown. */
|
|
635
|
+
export function usePluginEventStream<TData = string>(
|
|
636
|
+
contract: PluginContract,
|
|
637
|
+
endpoint: PluginEndpointDefinition,
|
|
638
|
+
options?: PluginEventStreamOptions<TData>,
|
|
639
|
+
): UsePluginEventStreamReturn<TData>
|
|
640
|
+
export function usePluginEventStream<TData = string>(
|
|
641
|
+
contract: PluginContract,
|
|
642
|
+
endpoint: PluginEndpointDefinition,
|
|
643
|
+
payload?: unknown,
|
|
644
|
+
options?: PluginEventStreamOptions<TData>,
|
|
645
|
+
): UsePluginEventStreamReturn<TData>
|
|
646
|
+
export function usePluginEventStream<TData = string>(
|
|
647
|
+
contract: PluginContract,
|
|
648
|
+
endpoint: PluginEndpointDefinition,
|
|
649
|
+
payloadOrOptions?: unknown,
|
|
650
|
+
maybeOptions?: PluginEventStreamOptions<TData>,
|
|
651
|
+
): UsePluginEventStreamReturn<TData> {
|
|
652
|
+
const payload = maybeOptions === undefined && looksLikeEventStreamOptions(payloadOrOptions)
|
|
653
|
+
? undefined
|
|
654
|
+
: payloadOrOptions
|
|
655
|
+
const options = (
|
|
656
|
+
maybeOptions === undefined && looksLikeEventStreamOptions(payloadOrOptions)
|
|
657
|
+
? payloadOrOptions
|
|
658
|
+
: maybeOptions ?? {}
|
|
659
|
+
) as PluginEventStreamOptions<TData>
|
|
660
|
+
const isConnected = ref(false)
|
|
661
|
+
const isConnecting = ref(false)
|
|
662
|
+
const error = ref<string | null>(null)
|
|
663
|
+
const lastMessage = shallowRef<PluginEventStreamMessage<TData> | null>(null)
|
|
664
|
+
const lastEventId = ref<string | null>(null)
|
|
665
|
+
let controller: AbortController | null = null
|
|
666
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
667
|
+
let stopped = true
|
|
668
|
+
|
|
669
|
+
function clearReconnectTimer(): void {
|
|
670
|
+
if (reconnectTimer !== null) {
|
|
671
|
+
clearTimeout(reconnectTimer)
|
|
672
|
+
reconnectTimer = null
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function scheduleReconnect(): void {
|
|
677
|
+
if (stopped || options.reconnect === false) return
|
|
678
|
+
clearReconnectTimer()
|
|
679
|
+
reconnectTimer = setTimeout(() => {
|
|
680
|
+
void connect()
|
|
681
|
+
}, options.reconnectDelayMs ?? 2000)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function connect(): Promise<void> {
|
|
685
|
+
if (endpoint.method !== 'get') {
|
|
686
|
+
throw new Error(`[MINT SDK] Plugin event streams must use GET; got ${endpoint.method}`)
|
|
687
|
+
}
|
|
688
|
+
if (typeof fetch !== 'function') {
|
|
689
|
+
throw new Error('[MINT SDK] fetch is required for plugin event streams.')
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
controller?.abort()
|
|
693
|
+
controller = new AbortController()
|
|
694
|
+
isConnecting.value = true
|
|
695
|
+
error.value = null
|
|
696
|
+
|
|
697
|
+
try {
|
|
698
|
+
const url = buildPluginEndpointUrl(contract, endpoint, payload, {
|
|
699
|
+
baseUrl: options.baseUrl,
|
|
700
|
+
})
|
|
701
|
+
const response = await fetch(url, {
|
|
702
|
+
method: 'GET',
|
|
703
|
+
headers: streamHeaders(options as PluginEventStreamOptions<unknown>),
|
|
704
|
+
credentials: options.credentials ?? 'same-origin',
|
|
705
|
+
signal: controller.signal,
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
if (!response.ok) {
|
|
709
|
+
throw new Error(`Plugin event stream failed with HTTP ${response.status}`)
|
|
710
|
+
}
|
|
711
|
+
if (!response.body) {
|
|
712
|
+
throw new Error('Plugin event stream response did not include a readable body.')
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
options.onOpen?.()
|
|
716
|
+
isConnected.value = true
|
|
717
|
+
const reader = response.body.getReader()
|
|
718
|
+
const decoder = new TextDecoder()
|
|
719
|
+
let buffer = ''
|
|
720
|
+
|
|
721
|
+
for (;;) {
|
|
722
|
+
const { value, done } = await reader.read()
|
|
723
|
+
if (done) break
|
|
724
|
+
buffer += decoder.decode(value, { stream: true })
|
|
725
|
+
|
|
726
|
+
let boundary = buffer.search(/\r?\n\r?\n/)
|
|
727
|
+
while (boundary !== -1) {
|
|
728
|
+
const block = buffer.slice(0, boundary)
|
|
729
|
+
buffer = buffer.slice(buffer[boundary] === '\r' ? boundary + 4 : boundary + 2)
|
|
730
|
+
const message = parseSseEvent<TData>(block, options.parseJson === true)
|
|
731
|
+
if (message) {
|
|
732
|
+
lastMessage.value = message
|
|
733
|
+
if (message.id !== undefined) {
|
|
734
|
+
lastEventId.value = message.id
|
|
735
|
+
}
|
|
736
|
+
if (message.retry !== undefined) {
|
|
737
|
+
options.reconnectDelayMs = message.retry
|
|
738
|
+
}
|
|
739
|
+
options.onMessage?.(message)
|
|
740
|
+
}
|
|
741
|
+
boundary = buffer.search(/\r?\n\r?\n/)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
} catch (err) {
|
|
745
|
+
if (!stopped && !(err instanceof DOMException && err.name === 'AbortError')) {
|
|
746
|
+
const message = err instanceof Error ? err.message : 'Plugin event stream failed'
|
|
747
|
+
error.value = message
|
|
748
|
+
options.onError?.(err)
|
|
749
|
+
}
|
|
750
|
+
} finally {
|
|
751
|
+
isConnecting.value = false
|
|
752
|
+
isConnected.value = false
|
|
753
|
+
scheduleReconnect()
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function start(): void {
|
|
758
|
+
stopped = false
|
|
759
|
+
clearReconnectTimer()
|
|
760
|
+
void connect()
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function stop(): void {
|
|
764
|
+
stopped = true
|
|
765
|
+
clearReconnectTimer()
|
|
766
|
+
controller?.abort()
|
|
767
|
+
controller = null
|
|
768
|
+
isConnecting.value = false
|
|
769
|
+
isConnected.value = false
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (options.immediate !== false && typeof window === 'undefined') {
|
|
773
|
+
onMounted(start)
|
|
774
|
+
}
|
|
775
|
+
onUnmounted(stop)
|
|
776
|
+
|
|
777
|
+
if (options.immediate !== false && typeof window !== 'undefined') {
|
|
778
|
+
start()
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return {
|
|
782
|
+
isConnected,
|
|
783
|
+
isConnecting,
|
|
784
|
+
error,
|
|
785
|
+
lastMessage,
|
|
786
|
+
lastEventId,
|
|
787
|
+
start,
|
|
788
|
+
stop,
|
|
789
|
+
}
|
|
345
790
|
}
|
|
346
791
|
|
|
347
792
|
/** Read and optionally load the current platform experiment for integrated plugin views. */
|
|
@@ -205,10 +205,8 @@
|
|
|
205
205
|
|
|
206
206
|
.mint-page-selector__item-label {
|
|
207
207
|
flex: 1;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
font-size: 0.65625rem;
|
|
212
|
-
color: var(--text-muted);
|
|
208
|
+
min-width: 0;
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
text-overflow: ellipsis;
|
|
213
211
|
white-space: nowrap;
|
|
214
212
|
}
|
|
@@ -866,7 +866,7 @@
|
|
|
866
866
|
font-size: 0.8125rem;
|
|
867
867
|
color: var(--mint-error);
|
|
868
868
|
background: var(--mint-error-bg);
|
|
869
|
-
border: 1px solid
|
|
869
|
+
border: 1px solid var(--mint-error-border);
|
|
870
870
|
border-radius: var(--radius);
|
|
871
871
|
}
|
|
872
872
|
|
|
@@ -889,7 +889,7 @@
|
|
|
889
889
|
|
|
890
890
|
.mint-auto-group__experiment-error {
|
|
891
891
|
padding: 1.5rem;
|
|
892
|
-
border: 1px solid
|
|
892
|
+
border: 1px solid var(--mint-error-border);
|
|
893
893
|
border-radius: var(--radius-md);
|
|
894
894
|
background: var(--mint-error-bg);
|
|
895
895
|
font-size: 0.8125rem;
|
|
@@ -925,7 +925,7 @@
|
|
|
925
925
|
.mint-auto-group__link-btn:focus-visible {
|
|
926
926
|
outline: 2px solid var(--color-primary);
|
|
927
927
|
outline-offset: 1px;
|
|
928
|
-
border-radius:
|
|
928
|
+
border-radius: var(--radius-sm);
|
|
929
929
|
}
|
|
930
930
|
|
|
931
931
|
.mint-auto-group__experiment-loaded {
|
|
@@ -1258,14 +1258,10 @@
|
|
|
1258
1258
|
.mint-auto-group__preview-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
1259
1259
|
.mint-auto-group__preview-panel { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; }
|
|
1260
1260
|
.mint-auto-group__preview-panel h4 { margin: 0 0 10px; font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; }
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
.mint-auto-group__preview-
|
|
1264
|
-
.mint-auto-group__preview-
|
|
1265
|
-
.mint-auto-group__preview-dot { width: 10px; height: 10px; border-radius: 9999px; flex-shrink: 0; }
|
|
1266
|
-
.mint-auto-group__preview-count { margin-left: auto; font-family: 'Fira Code', monospace; font-size: 12px; color: var(--text-secondary); background: var(--bg-primary); border: 1px solid var(--border); padding: 2px 8px; border-radius: 9999px; }
|
|
1267
|
-
.mint-auto-group__preview-samples { display: flex; flex-wrap: wrap; gap: 6px; padding: 0.5rem 0.875rem 0.625rem; }
|
|
1268
|
-
.mint-auto-group__preview-sample { font-family: 'Fira Code', monospace; font-size: 11.5px; color: var(--text-primary); background: var(--bg-primary); border: 1px solid var(--border); padding: 3px 8px; border-radius: 9999px; }
|
|
1261
|
+
/* Nested experimental groups render as a collapsible SampleHierarchyTree; cap
|
|
1262
|
+
its height so a large batch scrolls inside the panel instead of the modal. */
|
|
1263
|
+
.mint-auto-group__preview-tree { max-height: 320px; overflow-y: auto; }
|
|
1264
|
+
.mint-auto-group__preview-empty { margin: 0; font-size: 13px; color: var(--text-muted); }
|
|
1269
1265
|
.mint-auto-group__qc-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1270
1266
|
.mint-auto-group__qc-chip { background: var(--mint-warning-bg); color: #92400E; border: 1px solid var(--mint-warning-border); padding: 4px 10px; border-radius: 9999px; font-size: 12px; }
|
|
1271
1267
|
.mint-auto-group__fingerprint { margin-top: 14px; }
|
|
@@ -78,10 +78,11 @@
|
|
|
78
78
|
|
|
79
79
|
.mint-button--secondary {
|
|
80
80
|
background-color: var(--bg-secondary);
|
|
81
|
-
color: var(--text-
|
|
81
|
+
color: var(--text-secondary-strong);
|
|
82
82
|
border: 1px solid var(--border-color);
|
|
83
|
-
/*
|
|
84
|
-
|
|
83
|
+
/* Secondary is lower-emphasis through surface, border, and shadow, but keeps
|
|
84
|
+
medium label weight so it still reads as an actionable button. */
|
|
85
|
+
font-weight: 500;
|
|
85
86
|
box-shadow: 0 1px 1px rgba(15, 23, 42, 0.03);
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -125,7 +126,9 @@
|
|
|
125
126
|
|
|
126
127
|
.mint-button--ghost {
|
|
127
128
|
background-color: transparent;
|
|
128
|
-
color: var(--text-
|
|
129
|
+
color: var(--text-secondary-strong);
|
|
130
|
+
/* Lowest-emphasis variant: regular (400) weight, below secondary. */
|
|
131
|
+
font-weight: 400;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
.mint-button--ghost:hover:not(.mint-button--disabled) {
|
|
@@ -136,9 +139,16 @@
|
|
|
136
139
|
.mint-button--sm {
|
|
137
140
|
padding: 0.375rem 0.75rem;
|
|
138
141
|
font-size: 0.875rem;
|
|
142
|
+
/* Small buttons live in dense/secondary contexts — regular (400) weight
|
|
143
|
+
keeps the compact label from reading too heavy. Applies to all variants. */
|
|
144
|
+
font-weight: 400;
|
|
139
145
|
min-height: var(--form-height-sm);
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
.mint-button--secondary.mint-button--sm {
|
|
149
|
+
font-weight: 500;
|
|
150
|
+
}
|
|
151
|
+
|
|
142
152
|
.mint-button--md {
|
|
143
153
|
padding: 0.5rem 1rem;
|
|
144
154
|
font-size: 0.875rem;
|
|
@@ -165,6 +165,9 @@
|
|
|
165
165
|
|
|
166
166
|
.mint-modal__close {
|
|
167
167
|
flex-shrink: 0;
|
|
168
|
+
/* Always pin the close affordance to the right edge of the header, even when
|
|
169
|
+
there is no title/subtitle (no flex-1 header-text to push it over). */
|
|
170
|
+
margin-left: auto;
|
|
168
171
|
width: 1.75rem;
|
|
169
172
|
height: 1.75rem;
|
|
170
173
|
padding: 0;
|
|
@@ -239,6 +239,11 @@
|
|
|
239
239
|
background-color: var(--bg-hover);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
.mint-sample-selector__expand-btn:focus-visible {
|
|
243
|
+
outline: none;
|
|
244
|
+
box-shadow: var(--focus-ring);
|
|
245
|
+
}
|
|
246
|
+
|
|
242
247
|
.mint-sample-selector__expand-icon {
|
|
243
248
|
width: 0.875rem;
|
|
244
249
|
height: 0.875rem;
|
|
@@ -363,6 +368,11 @@
|
|
|
363
368
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08), 0 0 0 3px rgba(0, 0, 0, 0.06);
|
|
364
369
|
}
|
|
365
370
|
|
|
371
|
+
.mint-sample-selector__color-dot--clickable:focus-visible {
|
|
372
|
+
outline: none;
|
|
373
|
+
box-shadow: var(--focus-ring);
|
|
374
|
+
}
|
|
375
|
+
|
|
366
376
|
/* Count Badge */
|
|
367
377
|
.mint-sample-selector__count-badge {
|
|
368
378
|
font-size: 0.625rem;
|
|
@@ -396,6 +406,13 @@
|
|
|
396
406
|
color: var(--mint-error);
|
|
397
407
|
}
|
|
398
408
|
|
|
409
|
+
/* Reveal the hover-hidden delete button when reached by keyboard, and ring it. */
|
|
410
|
+
.mint-sample-selector__delete-btn:focus-visible {
|
|
411
|
+
outline: none;
|
|
412
|
+
opacity: 1;
|
|
413
|
+
box-shadow: var(--focus-ring);
|
|
414
|
+
}
|
|
415
|
+
|
|
399
416
|
.mint-sample-selector__delete-btn--hidden {
|
|
400
417
|
opacity: 0;
|
|
401
418
|
}
|
package/src/styles/variables.css
CHANGED
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
--text-primary: #1E293B;
|
|
23
23
|
--text-secondary: #64748B;
|
|
24
24
|
--text-muted: #94A3B8;
|
|
25
|
+
/* De-emphasized label for low-emphasis interactive controls (secondary/ghost
|
|
26
|
+
buttons). A true dark gray — softer than --text-primary's near-black — that
|
|
27
|
+
still clears WCAG AA (4.5:1) on the secondary hover surface (--bg-tertiary),
|
|
28
|
+
where --text-secondary would drop to ~4.35:1. */
|
|
29
|
+
--text-secondary-strong: #475569;
|
|
25
30
|
|
|
26
31
|
/* Brand colors */
|
|
27
32
|
--color-primary: #6366F1;
|
|
@@ -129,6 +134,9 @@ html.dark {
|
|
|
129
134
|
--text-primary: #F8FAFC;
|
|
130
135
|
--text-secondary: #94A3B8;
|
|
131
136
|
--text-muted: #64748B;
|
|
137
|
+
/* Dark-mode analog: soft light-gray instead of near-white, AA-safe (7.2:1)
|
|
138
|
+
on the dark secondary hover surface (--bg-tertiary #334155). */
|
|
139
|
+
--text-secondary-strong: #CBD5E1;
|
|
132
140
|
|
|
133
141
|
/* Scrollbar */
|
|
134
142
|
--scrollbar-track: #1E293B;
|