@dxos/plugin-transcription 0.8.1 → 0.8.2-main.12df754

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.
Files changed (191) hide show
  1. package/dist/lib/browser/{TranscriptContainer-JQ3HFHLQ.mjs → TranscriptContainer-4D4BHE7G.mjs} +3 -2
  2. package/dist/lib/browser/{app-graph-builder-VDQOVX7P.mjs → app-graph-builder-I4SSIQMW.mjs} +18 -41
  3. package/dist/lib/browser/app-graph-builder-I4SSIQMW.mjs.map +7 -0
  4. package/dist/lib/browser/{chunk-YEPT334A.mjs → chunk-5BE6RTNC.mjs} +1 -1
  5. package/dist/lib/browser/chunk-5BE6RTNC.mjs.map +7 -0
  6. package/dist/lib/browser/{chunk-OGNXNGJB.mjs → chunk-5Q3R53U3.mjs} +8 -8
  7. package/dist/lib/browser/chunk-5Q3R53U3.mjs.map +7 -0
  8. package/dist/lib/browser/{chunk-YCZISX63.mjs → chunk-DR3NBV6Q.mjs} +6 -6
  9. package/dist/lib/browser/{chunk-YCZISX63.mjs.map → chunk-DR3NBV6Q.mjs.map} +3 -3
  10. package/dist/lib/browser/{chunk-K7FOYAXO.mjs → chunk-GI5LZ4SV.mjs} +2 -2
  11. package/dist/lib/browser/{chunk-GU2BNYKZ.mjs → chunk-KLD7O5GJ.mjs} +2 -12
  12. package/dist/lib/browser/chunk-KLD7O5GJ.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-RHHJZQPM.mjs +702 -0
  14. package/dist/lib/browser/chunk-RHHJZQPM.mjs.map +7 -0
  15. package/dist/lib/browser/index.mjs +22 -197
  16. package/dist/lib/browser/index.mjs.map +4 -4
  17. package/dist/lib/browser/{intent-resolver-4R66ZCKT.mjs → intent-resolver-ZQQEH27F.mjs} +7 -7
  18. package/dist/lib/browser/intent-resolver-ZQQEH27F.mjs.map +7 -0
  19. package/dist/lib/browser/{meeting-transcription-state-DLFHQK72.mjs → meeting-transcription-state-5D52VTHU.mjs} +4 -4
  20. package/dist/lib/browser/meeting-transcription-state-5D52VTHU.mjs.map +7 -0
  21. package/dist/lib/browser/meta.json +1 -1
  22. package/dist/lib/browser/{react-surface-FQUHU5G7.mjs → react-surface-HETJ5XY6.mjs} +5 -4
  23. package/dist/lib/browser/{transcriber-LGMIWDYI.mjs → transcriber-SMRLU2KY.mjs} +8 -7
  24. package/dist/lib/browser/{transcriber-LGMIWDYI.mjs.map → transcriber-SMRLU2KY.mjs.map} +3 -3
  25. package/dist/lib/browser/types/index.mjs +1 -1
  26. package/dist/lib/node/{TranscriptContainer-QEYTTT2V.cjs → TranscriptContainer-ZWP6UKY6.cjs} +8 -7
  27. package/dist/lib/node/TranscriptContainer-ZWP6UKY6.cjs.map +7 -0
  28. package/dist/lib/node/{app-graph-builder-IFSIVNUR.cjs → app-graph-builder-WWC4WNTG.cjs} +31 -54
  29. package/dist/lib/node/app-graph-builder-WWC4WNTG.cjs.map +7 -0
  30. package/dist/lib/node/{chunk-P4VVD6ZO.cjs → chunk-3QEJ5EBO.cjs} +10 -10
  31. package/dist/lib/node/chunk-3QEJ5EBO.cjs.map +7 -0
  32. package/dist/lib/node/{chunk-CZMLKKWT.cjs → chunk-HYGDHS6X.cjs} +4 -4
  33. package/dist/lib/node/chunk-HYGDHS6X.cjs.map +7 -0
  34. package/dist/lib/node/{chunk-3KHUSQNW.cjs → chunk-J72PUQ3C.cjs} +6 -15
  35. package/dist/lib/node/chunk-J72PUQ3C.cjs.map +7 -0
  36. package/dist/lib/node/chunk-KO6MKCED.cjs +718 -0
  37. package/dist/lib/node/chunk-KO6MKCED.cjs.map +7 -0
  38. package/dist/lib/node/{chunk-FCCJALOT.cjs → chunk-LA73IHCB.cjs} +10 -10
  39. package/dist/lib/node/{chunk-FCCJALOT.cjs.map → chunk-LA73IHCB.cjs.map} +3 -3
  40. package/dist/lib/node/{chunk-LP4FG423.cjs → chunk-WXG4Z2EZ.cjs} +5 -5
  41. package/dist/lib/node/index.cjs +49 -233
  42. package/dist/lib/node/index.cjs.map +4 -4
  43. package/dist/lib/node/{intent-resolver-WMMCOUEH.cjs → intent-resolver-VY3XWYAA.cjs} +11 -10
  44. package/dist/lib/node/intent-resolver-VY3XWYAA.cjs.map +7 -0
  45. package/dist/lib/node/{meeting-transcription-state-URIYILEQ.cjs → meeting-transcription-state-OYWCY5XE.cjs} +7 -7
  46. package/dist/lib/node/meeting-transcription-state-OYWCY5XE.cjs.map +7 -0
  47. package/dist/lib/node/meta.json +1 -1
  48. package/dist/lib/node/{react-surface-WI2MGWCC.cjs → react-surface-M2JQIIDY.cjs} +10 -9
  49. package/dist/lib/node/{react-surface-WI2MGWCC.cjs.map → react-surface-M2JQIIDY.cjs.map} +1 -1
  50. package/dist/lib/node/{transcriber-QG5WPKX4.cjs → transcriber-E6AAB4SU.cjs} +14 -13
  51. package/dist/lib/node/{transcriber-QG5WPKX4.cjs.map → transcriber-E6AAB4SU.cjs.map} +3 -3
  52. package/dist/lib/node/types/index.cjs +7 -7
  53. package/dist/lib/node/types/index.cjs.map +1 -1
  54. package/dist/lib/node-esm/{TranscriptContainer-ZXUCOWGY.mjs → TranscriptContainer-3HJ752CP.mjs} +3 -2
  55. package/dist/lib/node-esm/{app-graph-builder-UL6XLIZY.mjs → app-graph-builder-PPJQAFSI.mjs} +18 -41
  56. package/dist/lib/node-esm/app-graph-builder-PPJQAFSI.mjs.map +7 -0
  57. package/dist/lib/node-esm/{chunk-GIO6J6FE.mjs → chunk-54HDN33F.mjs} +8 -8
  58. package/dist/lib/node-esm/chunk-54HDN33F.mjs.map +7 -0
  59. package/dist/lib/node-esm/{chunk-YQYBLFNI.mjs → chunk-K3R3YK3S.mjs} +6 -6
  60. package/dist/lib/node-esm/{chunk-YQYBLFNI.mjs.map → chunk-K3R3YK3S.mjs.map} +3 -3
  61. package/dist/lib/node-esm/{chunk-HKATMD34.mjs → chunk-QMHMEKB3.mjs} +2 -12
  62. package/dist/lib/node-esm/chunk-QMHMEKB3.mjs.map +7 -0
  63. package/dist/lib/node-esm/chunk-SDMGJZLN.mjs +703 -0
  64. package/dist/lib/node-esm/chunk-SDMGJZLN.mjs.map +7 -0
  65. package/dist/lib/node-esm/{chunk-J4D2RBBH.mjs → chunk-XY22OP6H.mjs} +2 -2
  66. package/dist/lib/node-esm/{chunk-PTZHDZ36.mjs → chunk-YXZOLOFT.mjs} +1 -1
  67. package/dist/lib/node-esm/chunk-YXZOLOFT.mjs.map +7 -0
  68. package/dist/lib/node-esm/index.mjs +22 -197
  69. package/dist/lib/node-esm/index.mjs.map +4 -4
  70. package/dist/lib/node-esm/{intent-resolver-QN2LO5TX.mjs → intent-resolver-FCTNJPWY.mjs} +7 -7
  71. package/dist/lib/node-esm/intent-resolver-FCTNJPWY.mjs.map +7 -0
  72. package/dist/lib/node-esm/{meeting-transcription-state-ZYVRAW6Y.mjs → meeting-transcription-state-XMZJ45P4.mjs} +4 -4
  73. package/dist/lib/node-esm/meeting-transcription-state-XMZJ45P4.mjs.map +7 -0
  74. package/dist/lib/node-esm/meta.json +1 -1
  75. package/dist/lib/node-esm/{react-surface-PCH6NTVB.mjs → react-surface-PXFSIM6V.mjs} +5 -4
  76. package/dist/lib/{browser/react-surface-FQUHU5G7.mjs.map → node-esm/react-surface-PXFSIM6V.mjs.map} +1 -1
  77. package/dist/lib/node-esm/{transcriber-TMPYZ5L4.mjs → transcriber-REAGRCPY.mjs} +8 -7
  78. package/dist/lib/node-esm/{transcriber-TMPYZ5L4.mjs.map → transcriber-REAGRCPY.mjs.map} +3 -3
  79. package/dist/lib/node-esm/types/index.mjs +1 -1
  80. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  81. package/dist/types/src/capabilities/capabilities.d.ts +4 -6
  82. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  83. package/dist/types/src/capabilities/index.d.ts +1 -4
  84. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  85. package/dist/types/src/capabilities/meeting-transcription-state.d.ts +2 -4
  86. package/dist/types/src/capabilities/meeting-transcription-state.d.ts.map +1 -1
  87. package/dist/types/src/capabilities/transcriber.d.ts.map +1 -1
  88. package/dist/types/src/components/Transcript/Transcript.d.ts +12 -4
  89. package/dist/types/src/components/Transcript/Transcript.d.ts.map +1 -1
  90. package/dist/types/src/components/Transcript/Transcript.stories.d.ts +15 -4
  91. package/dist/types/src/components/Transcript/Transcript.stories.d.ts.map +1 -1
  92. package/dist/types/src/components/Transcript/deprecated/Transcript.d.ts +13 -0
  93. package/dist/types/src/components/Transcript/deprecated/Transcript.d.ts.map +1 -0
  94. package/dist/types/src/components/Transcript/testings.d.ts +51 -0
  95. package/dist/types/src/components/Transcript/testings.d.ts.map +1 -0
  96. package/dist/types/src/components/Transcript/transcript-extension.d.ts +19 -0
  97. package/dist/types/src/components/Transcript/transcript-extension.d.ts.map +1 -0
  98. package/dist/types/src/components/TranscriptContainer.d.ts.map +1 -1
  99. package/dist/types/src/components/transcription.stories.d.ts.map +1 -1
  100. package/dist/types/src/entity-extraction/entity-extraction.d.ts +16 -0
  101. package/dist/types/src/entity-extraction/entity-extraction.d.ts.map +1 -0
  102. package/dist/types/src/entity-extraction/entity-extraction.test.d.ts +2 -0
  103. package/dist/types/src/entity-extraction/entity-extraction.test.d.ts.map +1 -0
  104. package/dist/types/src/entity-extraction/index.d.ts +2 -0
  105. package/dist/types/src/entity-extraction/index.d.ts.map +1 -0
  106. package/dist/types/src/hooks/index.d.ts +1 -0
  107. package/dist/types/src/hooks/index.d.ts.map +1 -1
  108. package/dist/types/src/hooks/useQueueModelAdapter.d.ts +7 -0
  109. package/dist/types/src/hooks/useQueueModelAdapter.d.ts.map +1 -0
  110. package/dist/types/src/hooks/useTranscriber.d.ts +1 -1
  111. package/dist/types/src/hooks/useTranscriber.d.ts.map +1 -1
  112. package/dist/types/src/model/index.d.ts +2 -0
  113. package/dist/types/src/model/index.d.ts.map +1 -0
  114. package/dist/types/src/model/model.d.ts +68 -0
  115. package/dist/types/src/model/model.d.ts.map +1 -0
  116. package/dist/types/src/model/model.test.d.ts +2 -0
  117. package/dist/types/src/model/model.test.d.ts.map +1 -0
  118. package/dist/types/src/testing/test-data.d.ts +73 -0
  119. package/dist/types/src/testing/test-data.d.ts.map +1 -0
  120. package/dist/types/src/transcriber/transcriber.browser.test.d.ts +2 -0
  121. package/dist/types/src/transcriber/transcriber.browser.test.d.ts.map +1 -0
  122. package/dist/types/src/types/schema.d.ts +9 -9
  123. package/dist/types/src/util/index.d.ts +0 -1
  124. package/dist/types/src/util/index.d.ts.map +1 -1
  125. package/package.json +47 -39
  126. package/src/capabilities/app-graph-builder.ts +15 -26
  127. package/src/capabilities/capabilities.ts +3 -2
  128. package/src/capabilities/intent-resolver.ts +4 -4
  129. package/src/capabilities/meeting-transcription-state.ts +2 -2
  130. package/src/capabilities/transcriber.ts +2 -1
  131. package/src/components/Transcript/Transcript.stories.tsx +165 -43
  132. package/src/components/Transcript/Transcript.tsx +78 -210
  133. package/src/components/Transcript/deprecated/Transcript.tsx +231 -0
  134. package/src/components/Transcript/testings.ts +182 -0
  135. package/src/components/Transcript/transcript-extension.ts +233 -0
  136. package/src/components/TranscriptContainer.tsx +6 -3
  137. package/src/components/transcription.stories.tsx +97 -48
  138. package/src/entity-extraction/entity-extraction.test.ts +32 -0
  139. package/src/entity-extraction/entity-extraction.ts +94 -0
  140. package/src/entity-extraction/index.ts +5 -0
  141. package/src/entity-extraction/system-prompt.tpl +9 -0
  142. package/src/entity-extraction/types.d.ts +8 -0
  143. package/src/hooks/index.ts +1 -0
  144. package/src/hooks/useQueueModelAdapter.ts +30 -0
  145. package/src/hooks/useTranscriber.ts +3 -2
  146. package/src/model/index.ts +5 -0
  147. package/src/model/model.test.ts +123 -0
  148. package/src/model/model.ts +229 -0
  149. package/src/testing/test-data.ts +314 -0
  150. package/src/transcriber/transcriber.ts +1 -1
  151. package/src/transcriber/transcription-manager.ts +2 -2
  152. package/src/types/schema.ts +7 -7
  153. package/src/types/types.ts +2 -2
  154. package/src/util/index.ts +0 -1
  155. package/dist/lib/browser/app-graph-builder-VDQOVX7P.mjs.map +0 -7
  156. package/dist/lib/browser/chunk-G4B6CPC2.mjs +0 -253
  157. package/dist/lib/browser/chunk-G4B6CPC2.mjs.map +0 -7
  158. package/dist/lib/browser/chunk-GU2BNYKZ.mjs.map +0 -7
  159. package/dist/lib/browser/chunk-OGNXNGJB.mjs.map +0 -7
  160. package/dist/lib/browser/chunk-YEPT334A.mjs.map +0 -7
  161. package/dist/lib/browser/intent-resolver-4R66ZCKT.mjs.map +0 -7
  162. package/dist/lib/browser/meeting-transcription-state-DLFHQK72.mjs.map +0 -7
  163. package/dist/lib/node/TranscriptContainer-QEYTTT2V.cjs.map +0 -7
  164. package/dist/lib/node/app-graph-builder-IFSIVNUR.cjs.map +0 -7
  165. package/dist/lib/node/chunk-3KHUSQNW.cjs.map +0 -7
  166. package/dist/lib/node/chunk-CZMLKKWT.cjs.map +0 -7
  167. package/dist/lib/node/chunk-P4VVD6ZO.cjs.map +0 -7
  168. package/dist/lib/node/chunk-UOFHT4GZ.cjs +0 -280
  169. package/dist/lib/node/chunk-UOFHT4GZ.cjs.map +0 -7
  170. package/dist/lib/node/intent-resolver-WMMCOUEH.cjs.map +0 -7
  171. package/dist/lib/node/meeting-transcription-state-URIYILEQ.cjs.map +0 -7
  172. package/dist/lib/node-esm/app-graph-builder-UL6XLIZY.mjs.map +0 -7
  173. package/dist/lib/node-esm/chunk-6X7X2UZF.mjs +0 -254
  174. package/dist/lib/node-esm/chunk-6X7X2UZF.mjs.map +0 -7
  175. package/dist/lib/node-esm/chunk-GIO6J6FE.mjs.map +0 -7
  176. package/dist/lib/node-esm/chunk-HKATMD34.mjs.map +0 -7
  177. package/dist/lib/node-esm/chunk-PTZHDZ36.mjs.map +0 -7
  178. package/dist/lib/node-esm/intent-resolver-QN2LO5TX.mjs.map +0 -7
  179. package/dist/lib/node-esm/meeting-transcription-state-ZYVRAW6Y.mjs.map +0 -7
  180. package/dist/types/src/transcriber/transcriber.test.d.ts +0 -2
  181. package/dist/types/src/transcriber/transcriber.test.d.ts.map +0 -1
  182. package/dist/types/src/util/random-queue-dxn.d.ts +0 -3
  183. package/dist/types/src/util/random-queue-dxn.d.ts.map +0 -1
  184. package/src/util/random-queue-dxn.ts +0 -9
  185. /package/dist/lib/browser/{TranscriptContainer-JQ3HFHLQ.mjs.map → TranscriptContainer-4D4BHE7G.mjs.map} +0 -0
  186. /package/dist/lib/browser/{chunk-K7FOYAXO.mjs.map → chunk-GI5LZ4SV.mjs.map} +0 -0
  187. /package/dist/lib/{node-esm/react-surface-PCH6NTVB.mjs.map → browser/react-surface-HETJ5XY6.mjs.map} +0 -0
  188. /package/dist/lib/node/{chunk-LP4FG423.cjs.map → chunk-WXG4Z2EZ.cjs.map} +0 -0
  189. /package/dist/lib/node-esm/{TranscriptContainer-ZXUCOWGY.mjs.map → TranscriptContainer-3HJ752CP.mjs.map} +0 -0
  190. /package/dist/lib/node-esm/{chunk-J4D2RBBH.mjs.map → chunk-XY22OP6H.mjs.map} +0 -0
  191. /package/src/transcriber/{transcriber.test.ts → transcriber.browser.test.ts} +0 -0
@@ -3,18 +3,18 @@
3
3
  //
4
4
 
5
5
  import { Capabilities, contributes, createResolver, type PluginsContext } from '@dxos/app-framework';
6
- import { create, refFromDXN } from '@dxos/live-object';
7
- import { type SpaceId } from '@dxos/react-client/echo';
6
+ import { live, refFromDXN } from '@dxos/live-object';
7
+ import { type SpaceId, randomQueueDxn } from '@dxos/react-client/echo';
8
8
 
9
9
  import { TranscriptionAction, TranscriptType } from '../types';
10
- import { getTimeStr, randomQueueDxn } from '../util';
10
+ import { getTimeStr } from '../util';
11
11
 
12
12
  export default (context: PluginsContext) =>
13
13
  contributes(Capabilities.IntentResolver, [
14
14
  createResolver({
15
15
  intent: TranscriptionAction.Create,
16
16
  resolve: ({ name, spaceId }) => {
17
- const transcript = create(TranscriptType, {
17
+ const transcript = live(TranscriptType, {
18
18
  queue: refFromDXN(randomQueueDxn(spaceId as SpaceId)),
19
19
  name: name ?? `Transcript ${getTimeStr(Date.now())}`,
20
20
  });
@@ -3,12 +3,12 @@
3
3
  //
4
4
 
5
5
  import { contributes } from '@dxos/app-framework';
6
- import { create } from '@dxos/live-object';
6
+ import { live } from '@dxos/live-object';
7
7
 
8
8
  import { TranscriptionCapabilities } from './capabilities';
9
9
 
10
10
  export default () => {
11
- const state = create<TranscriptionCapabilities.MeetingTranscriptionState>({
11
+ const state = live<TranscriptionCapabilities.MeetingTranscriptionState>({
12
12
  enabled: false,
13
13
  });
14
14
 
@@ -29,12 +29,13 @@ const TRANSCRIBE_AFTER_CHUNKS_AMOUNT = 50;
29
29
  * Records audio while user is speaking and transcribes it after user is done speaking.
30
30
  */
31
31
  export default () => {
32
- const getTranscriber: TranscriptionCapabilities.GetTranscriber = ({ audioStreamTrack, onSegments }) => {
32
+ const getTranscriber: TranscriptionCapabilities.GetTranscriber = ({ audioStreamTrack, onSegments, config }) => {
33
33
  // Initialize audio transcription.
34
34
  return new Transcriber({
35
35
  config: {
36
36
  transcribeAfterChunksAmount: TRANSCRIBE_AFTER_CHUNKS_AMOUNT,
37
37
  prefixBufferChunksAmount: PREFIXED_CHUNKS_AMOUNT,
38
+ ...config,
38
39
  },
39
40
  recorder: new MediaStreamRecorder({
40
41
  mediaStreamTrack: audioStreamTrack,
@@ -4,60 +4,164 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { type StoryObj, type Meta } from '@storybook/react';
8
- import React, { useEffect, useState } from 'react';
7
+ import { type Meta, type StoryObj } from '@storybook/react';
8
+ import React, { useEffect, useMemo, useState, type FC } from 'react';
9
9
 
10
+ import { IntentPlugin, SettingsPlugin } from '@dxos/app-framework';
11
+ import { withPluginManager } from '@dxos/app-framework/testing';
12
+ import { ClientPlugin } from '@dxos/plugin-client';
13
+ import { PreviewPlugin } from '@dxos/plugin-preview';
14
+ import { SpacePlugin } from '@dxos/plugin-space';
15
+ import { StorybookLayoutPlugin } from '@dxos/plugin-storybook-layout';
16
+ import { ThemePlugin } from '@dxos/plugin-theme';
10
17
  import { faker } from '@dxos/random';
11
- import { hues } from '@dxos/react-ui-theme';
12
- import { withTheme, withLayout } from '@dxos/storybook-utils';
18
+ import { useSpace } from '@dxos/react-client/echo';
19
+ import { IconButton, Toolbar } from '@dxos/react-ui';
20
+ import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
21
+ import { defaultTx } from '@dxos/react-ui-theme';
22
+ import { withLayout } from '@dxos/storybook-utils';
13
23
 
14
- import { Transcript } from './Transcript';
24
+ import { renderMarkdown, Transcript, type TranscriptProps } from './Transcript';
25
+ import {
26
+ BlockBuilder,
27
+ TestItem,
28
+ useTestTranscriptionQueue,
29
+ useTestTranscriptionQueueWithEntityExtraction,
30
+ } from './testings';
31
+ import { useQueueModelAdapter } from '../../hooks';
32
+ import { BlockModel } from '../../model';
15
33
  import translations from '../../translations';
34
+ import { type TranscriptBlock } from '../../types';
16
35
 
17
- let start = new Date(Date.now() - 24 * 60 * 60 * 10_000);
18
- const next = () => {
19
- start = new Date(start.getTime() + Math.random() * 10_000);
20
- return start;
36
+ faker.seed(1);
37
+
38
+ /**
39
+ * Story wrapper with test controls.
40
+ */
41
+ const TranscriptContainer: FC<
42
+ TranscriptProps & {
43
+ running: boolean;
44
+ onRunningChange: (running: boolean) => void;
45
+ onReset?: () => void;
46
+ }
47
+ > = ({ space, model, running, onRunningChange, onReset }) => {
48
+ return (
49
+ <div className='grid grid-rows-[1fr_40px] grow divide-y divide-separator'>
50
+ <Transcript space={space} model={model} />
51
+ <div className='grid grid-cols-[1fr_16rem] overflow-hidden'>
52
+ <div className='flex items-center'>
53
+ <SyntaxHighlighter language='json' className='text-sm'>
54
+ {JSON.stringify(model.toJSON())}
55
+ </SyntaxHighlighter>
56
+ </div>
57
+ <Toolbar.Root classNames='justify-end'>
58
+ <IconButton
59
+ icon={running ? 'ph--pause--regular' : 'ph--play--regular'}
60
+ label={running ? 'Pause' : 'Start'}
61
+ onClick={() => onRunningChange(!running)}
62
+ />
63
+ {onReset && <IconButton icon='ph--x--regular' label='Reset' onClick={onReset} />}
64
+ </Toolbar.Root>
65
+ </div>
66
+ </div>
67
+ );
21
68
  };
22
69
 
23
- const users = Array.from({ length: 5 }, () => ({
24
- authorName: faker.person.fullName(),
25
- authorHue: faker.helpers.arrayElement(hues),
26
- }));
27
-
28
- const createBlock = () => {
29
- const author = faker.helpers.arrayElement(users);
30
- return {
31
- id: faker.string.uuid(),
32
- ...author,
33
- segments: Array.from({ length: 1 + Math.floor(Math.random() * 2) }, () => ({
34
- started: next(),
35
- text: faker.lorem.paragraph(),
36
- })),
70
+ type StoryProps = { blocks?: TranscriptBlock[] } & Pick<TranscriptProps, 'ignoreAttention' | 'attendableId'>;
71
+
72
+ /**
73
+ * Basic story mutates array of blocks.
74
+ */
75
+ const BasicStory = ({ blocks: initialBlocks = [], ...props }: StoryProps) => {
76
+ const [reset, setReset] = useState({});
77
+ const builder = useMemo(() => new BlockBuilder(), []);
78
+ const model = useMemo(() => new BlockModel<TranscriptBlock>(renderMarkdown, initialBlocks), [initialBlocks, reset]);
79
+ const [running, setRunning] = useState(true);
80
+ const [currentBlock, setCurrentBlock] = useState<TranscriptBlock | null>(null);
81
+ useEffect(() => {
82
+ if (!running) {
83
+ return;
84
+ }
85
+
86
+ if (!currentBlock) {
87
+ const block = builder.createBlock();
88
+ model.appendBlock(block);
89
+ setCurrentBlock(block);
90
+ return;
91
+ }
92
+
93
+ const i = setInterval(() => {
94
+ if (currentBlock?.segments.length && currentBlock.segments.length >= 3) {
95
+ setCurrentBlock(null);
96
+ clearInterval(i);
97
+ return;
98
+ }
99
+
100
+ currentBlock.segments.push(builder.createSegment());
101
+ model.updateBlock(currentBlock);
102
+ }, 3_000);
103
+
104
+ return () => clearInterval(i);
105
+ }, [model, currentBlock, running]);
106
+
107
+ const handleReset = () => {
108
+ setCurrentBlock(null);
109
+ setRunning(false);
110
+ setReset({});
37
111
  };
112
+
113
+ return (
114
+ <TranscriptContainer
115
+ model={model}
116
+ running={running}
117
+ onRunningChange={setRunning}
118
+ onReset={handleReset}
119
+ {...props}
120
+ />
121
+ );
122
+ };
123
+
124
+ /**
125
+ * Queue story mutates queue with model adapter.
126
+ */
127
+ const QueueStory = ({ blocks: initialBlocks = [], ...props }: StoryProps) => {
128
+ const [running, setRunning] = useState(true);
129
+ const space = useSpace();
130
+ const queue = useTestTranscriptionQueue(space, running, 2_000);
131
+ const model = useQueueModelAdapter(renderMarkdown, queue, initialBlocks);
132
+
133
+ return <TranscriptContainer space={space} model={model} running={running} onRunningChange={setRunning} {...props} />;
38
134
  };
39
135
 
40
- const meta: Meta<typeof Transcript> = {
136
+ const EntityExtractionQueueStory = () => {
137
+ const [running, setRunning] = useState(true);
138
+ const space = useSpace();
139
+ const queue = useTestTranscriptionQueueWithEntityExtraction(space, running, 2_000);
140
+ const model = useQueueModelAdapter(renderMarkdown, queue, []);
141
+
142
+ return <TranscriptContainer space={space} model={model} running={running} onRunningChange={setRunning} />;
143
+ };
144
+
145
+ const meta: Meta<typeof QueueStory> = {
41
146
  title: 'plugins/plugin-transcription/Transcript',
42
- component: Transcript,
43
- render: ({ blocks: initialBlocks = [], ...args }) => {
44
- const [blocks, setBlocks] = useState(initialBlocks);
45
- useEffect(() => {
46
- const i = setInterval(() => {
47
- setBlocks((blocks) => [...blocks, createBlock()]);
48
- }, 1_000);
49
-
50
- return () => clearInterval(i);
51
- }, []);
52
-
53
- return <Transcript {...args} blocks={blocks} />;
54
- },
55
147
  decorators: [
56
- withTheme,
57
- withLayout({
58
- tooltips: true,
59
- fullscreen: true,
148
+ withPluginManager({
149
+ plugins: [
150
+ ThemePlugin({ tx: defaultTx }),
151
+ StorybookLayoutPlugin(),
152
+ ClientPlugin({
153
+ types: [TestItem],
154
+ onClientInitialized: async (_, client) => {
155
+ await client.halo.createIdentity();
156
+ },
157
+ }),
158
+ PreviewPlugin(),
159
+ SpacePlugin(),
160
+ SettingsPlugin(),
161
+ IntentPlugin(),
162
+ ],
60
163
  }),
164
+ withLayout({ tooltips: true, fullscreen: true }),
61
165
  ],
62
166
  parameters: {
63
167
  translations,
@@ -66,19 +170,37 @@ const meta: Meta<typeof Transcript> = {
66
170
 
67
171
  export default meta;
68
172
 
69
- type Story = StoryObj<typeof Transcript>;
173
+ type Story = StoryObj<StoryProps>;
70
174
 
71
175
  export const Default: Story = {
176
+ render: BasicStory,
72
177
  args: {
73
178
  ignoreAttention: true,
74
179
  attendableId: 'story',
75
- blocks: Array.from({ length: 10 }, createBlock),
180
+ blocks: Array.from({ length: 10 }, () => BlockBuilder.singleton.createBlock()),
76
181
  },
77
182
  };
78
183
 
79
184
  export const Empty: Story = {
185
+ render: BasicStory,
186
+ args: {
187
+ ignoreAttention: true,
188
+ attendableId: 'story',
189
+ },
190
+ };
191
+
192
+ export const WithQueue: Story = {
193
+ render: QueueStory,
80
194
  args: {
81
195
  ignoreAttention: true,
82
196
  attendableId: 'story',
83
197
  },
84
198
  };
199
+
200
+ export const WithEntityExtractionQueue: Story = {
201
+ render: EntityExtractionQueueStory,
202
+ args: {
203
+ // ignoreAttention: true,
204
+ // attendableId: 'story',
205
+ },
206
+ };
@@ -1,225 +1,93 @@
1
1
  //
2
- // Copyright 2023 DXOS.org
2
+ // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { intervalToDuration } from 'date-fns/intervalToDuration';
6
- import { yieldOrContinue } from 'main-thread-scheduling';
7
- import React, { type FC, useCallback, useEffect, useMemo, useRef, useState, type WheelEvent } from 'react';
8
- import { type OnResizeCallback, useResizeDetector } from 'react-resize-detector';
5
+ import React from 'react';
9
6
 
10
- import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
11
- import { useAttention } from '@dxos/react-ui-attention';
7
+ import { DXN } from '@dxos/keys';
8
+ import { log } from '@dxos/log';
9
+ import { resolveRef, useClient } from '@dxos/react-client';
10
+ import { type Space } from '@dxos/react-client/echo';
11
+ import { IconButton, type ThemedClassName, useThemeContext } from '@dxos/react-ui';
12
12
  import {
13
- type DxGridCellValue,
14
- type DxGridElement,
15
- type DxGridAxisMeta,
16
- type DxGridPlaneCells,
17
- Grid,
18
- toPlaneCellIndex,
19
- } from '@dxos/react-ui-grid';
13
+ createBasicExtensions,
14
+ createMarkdownExtensions,
15
+ createRenderer,
16
+ createThemeExtensions,
17
+ decorateMarkdown,
18
+ editorWidth,
19
+ preview,
20
+ useTextEditor,
21
+ } from '@dxos/react-ui-editor';
20
22
  import { mx } from '@dxos/react-ui-theme';
21
-
22
- import { TRANSCRIPTION_PLUGIN } from '../../meta';
23
- import { type TranscriptBlock } from '../../types';
24
-
25
- // TODO(burdon): Actions (e.g., mark, summarize, translate, label, delete).
26
-
27
- const lineHeight = 20;
28
- const cellSpacing = 8 + 2;
29
- const timestampColumnWidth = 68;
30
-
31
- const authorClasses = 'font-medium text-base leading-[20px]';
32
- const timestampClasses = 'text-xs leading-[20px] text-description pie-0 tabular-nums';
33
- const segmentTextClasses = 'text-sm whitespace-normal hyphens-auto';
34
- const measureClasses = mx(
35
- // NOTE(thure): The `inline-start` value must equal `timestampColumnWidth` plus grid’s gap (1px)
36
- 'absolute inline-start-[69px] inline-end-0 invisible z-[-1] border',
37
- 'pli-[--dx-grid-cell-padding-inline] plb-[--dx-grid-cell-padding-block] leading-[20px]',
38
- segmentTextClasses,
39
- );
40
-
41
- const rowDefault = {
42
- grid: { size: lineHeight + cellSpacing },
43
- };
44
-
45
- type QueueRows = [number, number][];
46
-
47
- const mapTranscriptQueue = (blocks?: TranscriptBlock[]): QueueRows => {
48
- if (!blocks || !Array.isArray(blocks) || blocks.length === 0) {
49
- return [];
50
- } else {
51
- return blocks.flatMap((block, blockIndex) => {
52
- return [
53
- [blockIndex, -1],
54
- ...block.segments.map((segment, segmentIndex) => {
55
- return [blockIndex, segmentIndex] as [number, number];
56
- }),
57
- ];
58
- });
59
- }
60
- };
61
-
62
- const pad = (n = 0) => String(n).padStart(2, '0');
63
-
64
- const formatTimestamp = (start: Date, end: Date) => {
65
- const { hours, minutes, seconds } = intervalToDuration({ start, end });
66
- return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
67
- };
68
-
69
- const measureRows = async (
70
- host: HTMLDivElement,
71
- blocks: TranscriptBlock[],
72
- queueMap: QueueRows,
73
- signal: AbortSignal,
74
- ) => {
75
- const result: DxGridAxisMeta = { grid: {} };
76
- for (let row = 0; row < queueMap.length; row++) {
77
- await yieldOrContinue('smooth', signal);
78
- const [blockIndex, segmentIndex] = queueMap[row];
79
- if (segmentIndex < 0) {
80
- result.grid[row] = { size: lineHeight + cellSpacing };
81
- } else {
82
- host.textContent = blocks[blockIndex]!.segments[segmentIndex]!.text;
83
- result.grid[row] = { size: host.offsetHeight };
84
- }
85
- }
86
- return result;
23
+ import { isNotFalsy } from '@dxos/util';
24
+
25
+ import { transcript } from './transcript-extension';
26
+ import { type BlockModel } from '../../model';
27
+ import { type TranscriptType, type TranscriptBlock } from '../../types';
28
+
29
+ export const renderMarkdown = (block: TranscriptBlock, debug = false): string[] => {
30
+ // TODO(burdon): Use link/reference markup for users (with popover).
31
+ // TODO(burdon): Color and avatar.
32
+ return [
33
+ `###### ${block.authorName}` + (debug ? ` (${block.id})` : ''),
34
+ block.segments.map((segment) => segment.text.trim()).join(' '),
35
+ '',
36
+ ];
87
37
  };
88
38
 
89
39
  export type TranscriptProps = ThemedClassName<{
90
- blocks?: TranscriptBlock[];
40
+ transcript?: TranscriptType;
41
+ space?: Space;
42
+ model: BlockModel<TranscriptBlock>;
43
+ // TODO(wittjosiah): Move to container.
91
44
  attendableId?: string;
92
45
  ignoreAttention?: boolean;
93
46
  }>;
94
47
 
95
- export const Transcript: FC<TranscriptProps> = ({ classNames, blocks, attendableId, ignoreAttention }) => {
96
- const { t } = useTranslation(TRANSCRIPTION_PLUGIN);
97
- const { hasAttention } = useAttention(attendableId);
98
- const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
99
- const [rows, setRows] = useState<DxGridAxisMeta | undefined>(undefined);
100
- const [columns, setColumns] = useState<DxGridAxisMeta | undefined>(undefined);
101
- const [autoScroll, setAutoScroll] = useState(true);
102
-
103
- const queueMap = useMemo(() => mapTranscriptQueue(blocks), [blocks]);
104
-
105
- const handleWheel = useCallback(
106
- (event: WheelEvent) => {
107
- setAutoScroll(false);
108
- if (!ignoreAttention && !hasAttention) {
109
- event.stopPropagation();
110
- }
111
- },
112
- [hasAttention, ignoreAttention],
113
- );
114
-
115
- const abortControllerRef = useRef<AbortController>();
116
-
117
- const handleResize = useCallback(
118
- async ({ entry }: { entry: { target: HTMLDivElement } | null }) => {
119
- if (entry?.target && Array.isArray(blocks) && blocks[0] && queueMap) {
120
- abortControllerRef.current?.abort();
121
- abortControllerRef.current = new AbortController();
122
- setColumns({ grid: { 0: { size: timestampColumnWidth }, 1: { size: entry.target.offsetWidth } } });
123
- return measureRows(entry.target, blocks, queueMap, abortControllerRef.current.signal)
124
- .then(setRows)
125
- .catch(() => {
126
- // Aborted mid-measurement by new size.
127
- });
128
- }
129
- },
130
- [blocks, queueMap],
131
- );
132
-
133
- const { width, ref: measureRef } = useResizeDetector({
134
- onResize: handleResize as OnResizeCallback,
135
- refreshOptions: { leading: true },
136
- });
137
-
138
- useEffect(() => {
139
- if (queueMap.length !== Object.keys(rows?.grid ?? {}).length) {
140
- void handleResize({ entry: { target: measureRef.current } }).then(() => {
141
- if (autoScroll) {
142
- // TODO(thure): Implement a deterministic way to do this when `rows` has fully settled and grid has a new `maxPosBlock`.
143
- setTimeout(() => dxGrid?.scrollToEndRow(), 50);
144
- }
145
- });
146
- }
147
- }, [blocks, queueMap, width, autoScroll]);
148
-
149
- useEffect(() => {
150
- if (dxGrid && Array.isArray(blocks) && blocks[0]) {
151
- const transcriptStart = blocks[0]!.segments[0]!.started;
152
- dxGrid.getCells = (range, plane) => {
153
- switch (plane) {
154
- case 'grid': {
155
- const cells: DxGridPlaneCells = {};
156
- for (let row = range.start.row; row <= range.end.row && row < queueMap.length; row++) {
157
- const [blockIndex, segmentIndex] = queueMap[row];
158
-
159
- cells[toPlaneCellIndex({ col: 0, row })] = {
160
- readonly: true,
161
- value:
162
- segmentIndex < 0
163
- ? formatTimestamp(transcriptStart, blocks[blockIndex]!.segments[Math.max(0, segmentIndex)]!.started)
164
- : '',
165
- className: timestampClasses,
166
- } satisfies DxGridCellValue;
167
-
168
- cells[toPlaneCellIndex({ col: 1, row })] = (
169
- segmentIndex < 0
170
- ? {
171
- readonly: true,
172
- accessoryHtml: `<span data-hue="${blocks[blockIndex]!.authorHue}" class="dx-text-hue">${blocks[blockIndex]!.authorName}</span>`,
173
- className: authorClasses,
174
- }
175
- : {
176
- readonly: true,
177
- value: blocks[blockIndex]!.segments[segmentIndex]!.text,
178
- className: segmentTextClasses,
179
- }
180
- ) satisfies DxGridCellValue;
181
- }
182
- return cells;
183
- }
184
- default:
185
- return {};
186
- }
187
- };
188
- }
189
- }, [dxGrid, blocks]);
190
-
191
- const handleScrollToEnd = useCallback(() => {
192
- setAutoScroll(true);
193
- dxGrid?.scrollToEndRow();
194
- }, [dxGrid, autoScroll]);
48
+ /**
49
+ * Transcript component implemented using the editor.
50
+ */
51
+ export const Transcript = ({
52
+ classNames,
53
+ transcript: object,
54
+ space,
55
+ model,
56
+ attendableId,
57
+ ignoreAttention,
58
+ }: TranscriptProps) => {
59
+ const client = useClient();
60
+ const { themeMode } = useThemeContext();
61
+ const { parentRef } = useTextEditor(() => {
62
+ return {
63
+ extensions: [
64
+ createBasicExtensions({ readOnly: true, lineWrapping: true }),
65
+ createMarkdownExtensions({ themeMode }),
66
+ createThemeExtensions({ themeMode }),
67
+ decorateMarkdown(),
68
+ space &&
69
+ preview({
70
+ onLookup: async ({ label, ref }) => {
71
+ log.info('onLookup', { label, ref });
72
+ const dxn = DXN.parse(ref);
73
+ if (!dxn) {
74
+ return null;
75
+ }
76
+
77
+ const object = await resolveRef(client, dxn, space);
78
+ return { label, object };
79
+ },
80
+ }),
81
+ transcript({
82
+ model,
83
+ started: object?.started ? new Date(object.started) : undefined,
84
+ renderButton: createRenderer(({ onClick }) => (
85
+ <IconButton icon='ph--arrow-line-down--regular' iconOnly label='Scroll to bottom' onClick={onClick} />
86
+ )),
87
+ }),
88
+ ].filter(isNotFalsy),
89
+ };
90
+ }, [space, model]);
195
91
 
196
- return (
197
- <div role='none' className={mx('relative min-bs-0', classNames)}>
198
- <Grid.Root id={`${attendableId}--transcript`}>
199
- <Grid.Content
200
- limitColumns={2}
201
- limitRows={queueMap.length}
202
- columns={columns}
203
- rows={rows}
204
- rowDefault={rowDefault}
205
- onWheel={handleWheel}
206
- className='[--dx-grid-base:var(--dx-baseSurface)] [--dx-grid-lines:var(--dx-baseSurface)] [&_.dx-grid]:min-bs-0 [&_.dx-grid]:min-is-0 [&_.dx-grid]:select-auto'
207
- ref={setDxGrid}
208
- />
209
- </Grid.Root>
210
- <div role='none' {...{ inert: '' }} aria-hidden className={measureClasses} ref={measureRef} />
211
- <IconButton
212
- icon='ph--arrow-line-down--regular'
213
- iconOnly
214
- label={t('scroll to end label')}
215
- tooltipSide='left'
216
- data-state={autoScroll ? 'invisible' : 'visible'}
217
- classNames={[
218
- 'absolute inline-end-2 block-end-2 opacity-0 pointer-events-none',
219
- 'data-[state="visible"]:pointer-events-auto data-[state="visible"]:opacity-100 transition-opacity',
220
- ]}
221
- onClick={handleScrollToEnd}
222
- />
223
- </div>
224
- );
92
+ return <div ref={parentRef} className={mx('flex grow overflow-hidden', editorWidth, classNames)} />;
225
93
  };