@linto-ai/transcript-ui-core 0.9.11 → 0.9.13
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/package.json +3 -3
- package/src/components/Header.vue +22 -1
- package/src/components/Layout.vue +5 -1
- package/src/components/VerbatimPanel.vue +2 -2
- package/src/core/createCore.ts +11 -3
- package/src/core/index.ts +1 -0
- package/src/core/types.ts +59 -5
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linto-ai/transcript-ui-core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
4
4
|
"description": "State layer and UI shell for @linto-ai/transcript-ui — core, stores, format adapters, and the TranscriptUI component",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vue",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"vue": "^3.5.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@linto-ai/transcript-ui-i18n": "0.9.
|
|
43
|
-
"@linto-ai/transcript-ui-ui": "0.9.
|
|
42
|
+
"@linto-ai/transcript-ui-i18n": "0.9.13",
|
|
43
|
+
"@linto-ai/transcript-ui-ui": "0.9.13",
|
|
44
44
|
"vue-stick-to-bottom": "^1.0.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -11,11 +11,15 @@ const props = defineProps<{
|
|
|
11
11
|
speakerCount: number
|
|
12
12
|
isMobile: boolean
|
|
13
13
|
canAsk?: boolean
|
|
14
|
+
canUndo?: boolean
|
|
15
|
+
canRedo?: boolean
|
|
14
16
|
}>()
|
|
15
17
|
|
|
16
18
|
defineEmits<{
|
|
17
19
|
toggleSidebar: []
|
|
18
20
|
openChat: []
|
|
21
|
+
undo: []
|
|
22
|
+
redo: []
|
|
19
23
|
}>()
|
|
20
24
|
|
|
21
25
|
const { t, locale } = useI18n()
|
|
@@ -45,7 +49,10 @@ const metaParts = computed(() =>
|
|
|
45
49
|
<div class="header-main">
|
|
46
50
|
<h1 class="document-title">{{ formattedTitle }}</h1>
|
|
47
51
|
<div v-if="metaParts.length" class="document-meta">
|
|
48
|
-
<span
|
|
52
|
+
<span
|
|
53
|
+
v-for="(part, i) in metaParts"
|
|
54
|
+
:key="i"
|
|
55
|
+
class="document-meta__part">
|
|
49
56
|
{{ part }}
|
|
50
57
|
</span>
|
|
51
58
|
</div>
|
|
@@ -58,6 +65,20 @@ const metaParts = computed(() =>
|
|
|
58
65
|
@click="$emit('toggleSidebar')">
|
|
59
66
|
<template #icon><EditorIcon name="users" :size="16" /></template>
|
|
60
67
|
</Button>
|
|
68
|
+
<Button
|
|
69
|
+
variant="secondary"
|
|
70
|
+
:aria-label="t('header.undo')"
|
|
71
|
+
:disabled="!props.canUndo"
|
|
72
|
+
@click="$emit('undo')">
|
|
73
|
+
<template #icon><EditorIcon name="undo" :size="16" /></template>
|
|
74
|
+
</Button>
|
|
75
|
+
<Button
|
|
76
|
+
variant="secondary"
|
|
77
|
+
:aria-label="t('header.redo')"
|
|
78
|
+
:disabled="!props.canRedo"
|
|
79
|
+
@click="$emit('redo')">
|
|
80
|
+
<template #icon><EditorIcon name="redo" :size="16" /></template>
|
|
81
|
+
</Button>
|
|
61
82
|
<Button
|
|
62
83
|
variant="primary"
|
|
63
84
|
:aria-label="t('header.ask')"
|
|
@@ -109,8 +109,12 @@ function onTranslationChange(translationId: string) {
|
|
|
109
109
|
:speaker-count="speakers.size"
|
|
110
110
|
:is-mobile="isMobile"
|
|
111
111
|
:can-ask="!!core.chat"
|
|
112
|
+
:can-undo="core.transcriptionEditor?.canUndo.value ?? false"
|
|
113
|
+
:can-redo="core.transcriptionEditor?.canRedo.value ?? false"
|
|
112
114
|
@toggle-sidebar="isSidebarOpen = !isSidebarOpen"
|
|
113
|
-
@open-chat="core.chat?.setDrawerOpen(true)"
|
|
115
|
+
@open-chat="core.chat?.setDrawerOpen(true)"
|
|
116
|
+
@undo="core.transcriptionEditor?.undo()"
|
|
117
|
+
@redo="core.transcriptionEditor?.redo()" />
|
|
114
118
|
<TabBar v-model="activeTab" />
|
|
115
119
|
<SelectionActionBar v-if="showTranscription" />
|
|
116
120
|
<main class="editor-body">
|
|
@@ -56,7 +56,7 @@ function exportAsText(): void {
|
|
|
56
56
|
|
|
57
57
|
function onExport(format?: string): void {
|
|
58
58
|
if (!format) return
|
|
59
|
-
if (format === "txt" && core.verbatimFormatsAreDefault) {
|
|
59
|
+
if (format === "txt" && core.verbatimFormatsAreDefault.value) {
|
|
60
60
|
exportAsText()
|
|
61
61
|
return
|
|
62
62
|
}
|
|
@@ -68,7 +68,7 @@ function onExport(format?: string): void {
|
|
|
68
68
|
<section class="verbatim-panel">
|
|
69
69
|
<DocumentArticle>
|
|
70
70
|
<template #toolbar-right>
|
|
71
|
-
<DownloadMenu :formats="core.verbatimFormats" @select="onExport" />
|
|
71
|
+
<DownloadMenu :formats="core.verbatimFormats.value" @select="onExport" />
|
|
72
72
|
</template>
|
|
73
73
|
|
|
74
74
|
<article class="verbatim-panel__content">
|
package/src/core/createCore.ts
CHANGED
|
@@ -33,9 +33,17 @@ export function createCore(options: CoreOptions = {}): Core {
|
|
|
33
33
|
const capabilities = ref<CoreCapabilities>(
|
|
34
34
|
options.capabilities ?? { text: "edit", speakers: "edit" },
|
|
35
35
|
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// Ref, not a plain array: unlike `document` (setDocument) or `capabilities`
|
|
37
|
+
// (already a Ref), there is no dedicated setter, and no web component prop
|
|
38
|
+
// either (an array can't cross an HTML attribute cleanly) — a host sets
|
|
39
|
+
// this the same way it sets capabilities, core.verbatimFormats.value = […],
|
|
40
|
+
// once it holds `core` (see TranscriptUI's defineExpose).
|
|
41
|
+
const verbatimFormats = ref<DownloadFormat[]>(
|
|
42
|
+
options.verbatimFormats ?? DEFAULT_VERBATIM_FORMATS,
|
|
43
|
+
)
|
|
44
|
+
const verbatimFormatsAreDefault = computed(
|
|
45
|
+
() => verbatimFormats.value === DEFAULT_VERBATIM_FORMATS,
|
|
46
|
+
)
|
|
39
47
|
|
|
40
48
|
// ── Event bus ──────────────────────────────────────────────────────
|
|
41
49
|
|
package/src/core/index.ts
CHANGED
package/src/core/types.ts
CHANGED
|
@@ -263,6 +263,10 @@ export interface TurnSpeakerUpdated {
|
|
|
263
263
|
speaker: { id: string; name: string }
|
|
264
264
|
removedSpeakerId?: string
|
|
265
265
|
version?: number
|
|
266
|
+
/** @see SpeakerRenamed.revisionId */
|
|
267
|
+
revisionId?: string | null
|
|
268
|
+
/** @see SpeakerRenamed.redoRevisionId */
|
|
269
|
+
redoRevisionId?: string | null
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
export interface SpeakerRenamed {
|
|
@@ -270,6 +274,18 @@ export interface SpeakerRenamed {
|
|
|
270
274
|
speakerId: string
|
|
271
275
|
name: string
|
|
272
276
|
version?: number
|
|
277
|
+
/** The undo cursor AFTER this broadcast (see onUndo/onRedo server-side) —
|
|
278
|
+
* a plain rename and an undo/redo replaying one are indistinguishable on
|
|
279
|
+
* the wire, and don't need to be: both just mean "the cursor is here
|
|
280
|
+
* now". Absent from broadcasts predating the undo/redo feature. */
|
|
281
|
+
revisionId?: string | null
|
|
282
|
+
/** The revision a redo would target FROM this new cursor, server-computed
|
|
283
|
+
* — null when there's nothing to redo to. This is server truth, not a
|
|
284
|
+
* client guess: right after a fresh mutation it's always null (nothing
|
|
285
|
+
* could already be chained onto a revision that didn't exist a moment
|
|
286
|
+
* ago), right after an undo it's the revision just undone, right after a
|
|
287
|
+
* redo it's whatever (if anything) comes next in the chain. */
|
|
288
|
+
redoRevisionId?: string | null
|
|
273
289
|
}
|
|
274
290
|
|
|
275
291
|
/** From-speaker replaced by to-speaker on every turn — its removal is
|
|
@@ -279,6 +295,27 @@ export interface SpeakerReplaced {
|
|
|
279
295
|
fromSpeakerId: string
|
|
280
296
|
toSpeakerId: string
|
|
281
297
|
version?: number
|
|
298
|
+
/** @see SpeakerRenamed.revisionId */
|
|
299
|
+
revisionId?: string | null
|
|
300
|
+
/** @see SpeakerRenamed.redoRevisionId */
|
|
301
|
+
redoRevisionId?: string | null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Undo of a replace_speaker: fromSpeaker resurrected and turnIds moved back
|
|
305
|
+
* onto it — the mirror of SpeakerReplaced, not its opposite payload shape
|
|
306
|
+
* (fromSpeaker rides the raw stored speaker record, not just its id, since
|
|
307
|
+
* restoring it is literally re-inserting that record). Redo of the same
|
|
308
|
+
* replace_speaker re-broadcasts SpeakerReplaced, not this. */
|
|
309
|
+
export interface SpeakerRestored {
|
|
310
|
+
translationId: string
|
|
311
|
+
fromSpeaker: { speaker_id: string; speaker_name: string }
|
|
312
|
+
toSpeakerId: string
|
|
313
|
+
turnIds: string[]
|
|
314
|
+
version?: number
|
|
315
|
+
/** @see SpeakerRenamed.revisionId */
|
|
316
|
+
revisionId?: string | null
|
|
317
|
+
/** @see SpeakerRenamed.redoRevisionId */
|
|
318
|
+
redoRevisionId?: string | null
|
|
282
319
|
}
|
|
283
320
|
|
|
284
321
|
export interface TranscriptionEditorPluginApi {
|
|
@@ -310,6 +347,16 @@ export interface TranscriptionEditorPluginApi {
|
|
|
310
347
|
/** Reassign every turn of a speaker to another (speaker merge). */
|
|
311
348
|
replaceSpeaker(fromSpeakerId: string, toSpeakerId: string): void
|
|
312
349
|
|
|
350
|
+
// ── Undo/redo (speaker mutations only — see server EditorHandler) ──
|
|
351
|
+
/** True when the active translation has a speaker mutation to undo. */
|
|
352
|
+
readonly canUndo: ComputedRef<boolean>
|
|
353
|
+
/** True when the active translation has an undo to redo — server-computed
|
|
354
|
+
* (see SpeakerRenamed.redoRevisionId), not a client guess: stays correct
|
|
355
|
+
* across collaborators, no staleness caveat. */
|
|
356
|
+
readonly canRedo: ComputedRef<boolean>
|
|
357
|
+
undo(): void
|
|
358
|
+
redo(): void
|
|
359
|
+
|
|
313
360
|
// ── Server sync (host-pushed from broadcasts) ──
|
|
314
361
|
/** Apply a saved turn broadcast by the server (any track, any author). */
|
|
315
362
|
applyTurnUpdate(update: TurnUpdate): void
|
|
@@ -330,6 +377,9 @@ export interface TranscriptionEditorPluginApi {
|
|
|
330
377
|
applyTurnSpeakerUpdated(update: TurnSpeakerUpdated): void
|
|
331
378
|
applySpeakerRenamed(renamed: SpeakerRenamed): void
|
|
332
379
|
applySpeakerReplaced(replaced: SpeakerReplaced): void
|
|
380
|
+
/** Apply an undo of replace_speaker (server event editor:speaker_restored
|
|
381
|
+
* — redo re-broadcasts applySpeakerReplaced instead, see SpeakerRestored). */
|
|
382
|
+
applySpeakerRestored(restored: SpeakerRestored): void
|
|
333
383
|
|
|
334
384
|
// ── Locks (host-pushed from server broadcasts; UI pulls per turn) ──
|
|
335
385
|
/** Lock held on a turn of the ACTIVE translation, if any. */
|
|
@@ -570,11 +620,15 @@ export interface Core {
|
|
|
570
620
|
readonly date: Ref<string | number | null>
|
|
571
621
|
readonly activeChannelId: Ref<string>
|
|
572
622
|
readonly capabilities: Ref<CoreCapabilities>
|
|
573
|
-
/** Formats shown in the verbatim panel's download menu (see
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
*
|
|
577
|
-
|
|
623
|
+
/** Formats shown in the verbatim panel's download menu (see
|
|
624
|
+
* CoreOptions.verbatimFormats for the initial value). A Ref, not a plain
|
|
625
|
+
* array: there's no dedicated setter and no web component prop (a web
|
|
626
|
+
* component attribute can't carry an array) — set verbatimFormats.value
|
|
627
|
+
* directly once you hold `core`, the same way you set capabilities. */
|
|
628
|
+
readonly verbatimFormats: Ref<DownloadFormat[]>
|
|
629
|
+
/** True when verbatimFormats is still the built-in default — the verbatim
|
|
630
|
+
* panel then fulfills "txt" itself instead of emitting "verbatim:export". */
|
|
631
|
+
readonly verbatimFormatsAreDefault: ComputedRef<boolean>
|
|
578
632
|
|
|
579
633
|
// ── Stores ───────────────────────────────────────────────────────────
|
|
580
634
|
readonly speakers: SpeakersStore
|