@dxos/react-ui-chat 0.8.4-main.f5c0578 → 0.8.4-main.fcc0d83b33

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 (51) hide show
  1. package/dist/lib/browser/index.mjs +215 -339
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/translations.mjs +14 -0
  5. package/dist/lib/browser/translations.mjs.map +7 -0
  6. package/dist/lib/node-esm/index.mjs +215 -339
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/lib/node-esm/translations.mjs +16 -0
  10. package/dist/lib/node-esm/translations.mjs.map +7 -0
  11. package/dist/types/src/components/ChatDialog/ChatDialog.d.ts +17 -1
  12. package/dist/types/src/components/ChatDialog/ChatDialog.d.ts.map +1 -1
  13. package/dist/types/src/components/ChatDialog/ChatDialog.stories.d.ts +35 -5
  14. package/dist/types/src/components/ChatDialog/ChatDialog.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/ChatEditor/ChatEditor.d.ts +7 -9
  16. package/dist/types/src/components/ChatEditor/ChatEditor.d.ts.map +1 -1
  17. package/dist/types/src/components/ChatEditor/ChatEditor.stories.d.ts +20 -5
  18. package/dist/types/src/components/ChatEditor/ChatEditor.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/ChatEditor/references.d.ts +4 -0
  20. package/dist/types/src/components/ChatEditor/references.d.ts.map +1 -1
  21. package/dist/types/src/components/ChatStatus/ChatStatus.d.ts +40 -0
  22. package/dist/types/src/components/ChatStatus/ChatStatus.d.ts.map +1 -0
  23. package/dist/types/src/components/ChatStatus/ChatStatus.stories.d.ts +27 -0
  24. package/dist/types/src/components/ChatStatus/ChatStatus.stories.d.ts.map +1 -0
  25. package/dist/types/src/components/ChatStatus/format.d.ts +7 -0
  26. package/dist/types/src/components/ChatStatus/format.d.ts.map +1 -0
  27. package/dist/types/src/components/ChatStatus/format.test.d.ts +2 -0
  28. package/dist/types/src/components/ChatStatus/format.test.d.ts.map +1 -0
  29. package/dist/types/src/components/ChatStatus/index.d.ts +3 -0
  30. package/dist/types/src/components/ChatStatus/index.d.ts.map +1 -0
  31. package/dist/types/src/components/index.d.ts +1 -0
  32. package/dist/types/src/components/index.d.ts.map +1 -1
  33. package/dist/types/src/translations.d.ts +2 -35
  34. package/dist/types/src/translations.d.ts.map +1 -1
  35. package/dist/types/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +51 -42
  37. package/src/components/ChatDialog/ChatDialog.stories.tsx +7 -9
  38. package/src/components/ChatDialog/ChatDialog.tsx +27 -17
  39. package/src/components/ChatEditor/ChatEditor.stories.tsx +23 -23
  40. package/src/components/ChatEditor/ChatEditor.tsx +35 -65
  41. package/src/components/ChatEditor/references.ts +18 -12
  42. package/src/components/ChatStatus/ChatStatus.stories.tsx +130 -0
  43. package/src/components/ChatStatus/ChatStatus.tsx +183 -0
  44. package/src/components/ChatStatus/format.test.ts +48 -0
  45. package/src/components/ChatStatus/format.ts +27 -0
  46. package/src/components/ChatStatus/index.ts +6 -0
  47. package/src/components/index.ts +1 -0
  48. package/src/translations.ts +2 -41
  49. package/dist/types/src/components/ChatEditor/autocomplete.d.ts +0 -26
  50. package/dist/types/src/components/ChatEditor/autocomplete.d.ts.map +0 -1
  51. package/src/components/ChatEditor/autocomplete.ts +0 -220
@@ -0,0 +1,130 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useRef } from 'react';
7
+
8
+ import { Button, Toolbar } from '@dxos/react-ui';
9
+ import { Matrix } from '@dxos/react-ui-sfx';
10
+ import { withTheme, withLayout } from '@dxos/react-ui/testing';
11
+
12
+ import { ChatStatus, type ChatStatusController, useChatStatusContext } from './ChatStatus';
13
+
14
+ const meta = {
15
+ title: 'ui/react-ui-chat/ChatStatus',
16
+ component: ChatStatus.Root,
17
+ decorators: [
18
+ withTheme(),
19
+ withLayout({ layout: 'centered', classNames: 'py-1 px-2 border border-separator rounded-sm' }),
20
+ ],
21
+ parameters: {
22
+ layout: 'centered',
23
+ },
24
+ } satisfies Meta<typeof ChatStatus.Root>;
25
+
26
+ export default meta;
27
+
28
+ type Story = StoryObj<typeof meta>;
29
+
30
+ export const Default: Story = {
31
+ render: () => (
32
+ <ChatStatus.Root>
33
+ <ChatStatus.Icon />
34
+ <ChatStatus.Stopwatch />
35
+ </ChatStatus.Root>
36
+ ),
37
+ };
38
+
39
+ export const WithMeta: Story = {
40
+ render: () => (
41
+ <ChatStatus.Root>
42
+ <ChatStatus.Icon />
43
+ <ChatStatus.Stopwatch />
44
+ <ChatStatus.Separator />
45
+ <ChatStatus.Text>↑ 234</ChatStatus.Text>
46
+ <ChatStatus.Separator />
47
+ <ChatStatus.Text>↓ 1.2k</ChatStatus.Text>
48
+ </ChatStatus.Root>
49
+ ),
50
+ };
51
+
52
+ /**
53
+ * Reads the running flag from Status context and forwards it to the Matrix as
54
+ * the `active` prop, so the Matrix runs its own internal interval loop only
55
+ * while the Status tick is running. Pausing the Status freezes the dots.
56
+ */
57
+ const MatrixIcon = () => {
58
+ const { running } = useChatStatusContext('MatrixIcon');
59
+ return (
60
+ <Matrix
61
+ classNames='mr-2'
62
+ dim={4}
63
+ dotSize={3}
64
+ // TODO(burdon): Change with activity?
65
+ count={10}
66
+ interval={500}
67
+ active={running}
68
+ />
69
+ );
70
+ };
71
+
72
+ export const WithCustomIcon: Story = {
73
+ render: () => (
74
+ <ChatStatus.Root classNames='text-sm'>
75
+ <ChatStatus.Icon>
76
+ <MatrixIcon />
77
+ </ChatStatus.Icon>
78
+ <ChatStatus.Stopwatch />
79
+ <ChatStatus.Separator />
80
+ <ChatStatus.Text>↑ 234</ChatStatus.Text>
81
+ <ChatStatus.Separator />
82
+ <ChatStatus.Text>↓ 1.2k</ChatStatus.Text>
83
+ </ChatStatus.Root>
84
+ ),
85
+ };
86
+
87
+ export const LongRunning: Story = {
88
+ render: () => (
89
+ <ChatStatus.Root>
90
+ <ChatStatus.Icon />
91
+ <ChatStatus.Stopwatch offset={65 * 60} />
92
+ <ChatStatus.Separator />
93
+ <ChatStatus.Text>↑ 4.5k</ChatStatus.Text>
94
+ <ChatStatus.Separator />
95
+ <ChatStatus.Text>↓ 12.3k</ChatStatus.Text>
96
+ </ChatStatus.Root>
97
+ ),
98
+ };
99
+
100
+ export const TextOnly: Story = {
101
+ render: () => (
102
+ <ChatStatus.Root>
103
+ <ChatStatus.Icon />
104
+ <ChatStatus.Text>Connecting…</ChatStatus.Text>
105
+ </ChatStatus.Root>
106
+ ),
107
+ };
108
+
109
+ /**
110
+ * Demonstrates the imperative controller — start/stop the 1Hz tick from the outside.
111
+ */
112
+ export const Controller: Story = {
113
+ render: () => {
114
+ const ref = useRef<ChatStatusController>(null);
115
+ return (
116
+ <div className='flex flex-col gap-4'>
117
+ <Toolbar.Root>
118
+ <Button onClick={() => ref.current?.start()}>Start</Button>
119
+ <Button onClick={() => ref.current?.stop()}>Stop</Button>
120
+ </Toolbar.Root>
121
+ <ChatStatus.Root ref={ref}>
122
+ <ChatStatus.Icon>
123
+ <MatrixIcon />
124
+ </ChatStatus.Icon>
125
+ <ChatStatus.Stopwatch />
126
+ </ChatStatus.Root>
127
+ </div>
128
+ );
129
+ },
130
+ };
@@ -0,0 +1,183 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import React, { type PropsWithChildren, type Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react';
7
+
8
+ import { type ThemedClassName } from '@dxos/react-ui';
9
+ import { mx } from '@dxos/ui-theme';
10
+
11
+ import { formatElapsed } from './format';
12
+
13
+ const TICK_MS = 1_000;
14
+
15
+ //
16
+ // Context
17
+ //
18
+
19
+ type ChatStatusContextValue = {
20
+ /** Whole seconds elapsed since ChatStatus.Root mounted. Only advances while `running` is true. */
21
+ elapsed: number;
22
+ /** Whether the ChatStatus.Root tick is currently active. Toggled via the ChatStatusController. */
23
+ running: boolean;
24
+ };
25
+
26
+ const [ChatStatusProvider, useChatStatusContext] = createContext<ChatStatusContextValue>('ChatStatus');
27
+
28
+ export { useChatStatusContext };
29
+
30
+ //
31
+ // Controller
32
+ //
33
+
34
+ export type ChatStatusController = {
35
+ /** Resume the tick. No-op if already running. */
36
+ start: () => void;
37
+ /** Pause the tick. The reported `elapsed` freezes at its last value. */
38
+ stop: () => void;
39
+ };
40
+
41
+ //
42
+ // Root
43
+ //
44
+
45
+ export type RootProps = ThemedClassName<
46
+ PropsWithChildren<{
47
+ /** Whether the tick starts running on mount. Defaults to `true`. */
48
+ defaultRunning?: boolean;
49
+ }>
50
+ >;
51
+
52
+ const Root = forwardRef<ChatStatusController, RootProps>(
53
+ ({ classNames, children, defaultRunning = true }: RootProps, forwardedRef: Ref<ChatStatusController>) => {
54
+ const [elapsed, setElapsed] = useState(0);
55
+ const [running, setRunning] = useState(defaultRunning);
56
+
57
+ useEffect(() => {
58
+ if (!running) {
59
+ return;
60
+ }
61
+
62
+ // Anchor to a wall-clock timestamp so `elapsed` self-corrects after
63
+ // tab-throttling: browsers clamp setTimeout to >=1s in inactive tabs
64
+ // and >=1min when the page is hidden, so a count-the-ticks approach
65
+ // would under-report after a background period. Computing elapsed from
66
+ // (Date.now() - startedAt) restores the true wall-clock value on the
67
+ // next fired tick. The initial elapsed (preserved across stop/start)
68
+ // is folded into the anchor so paused intervals are excluded.
69
+ const startedAt = Date.now() - elapsed * TICK_MS;
70
+ let timeout: ReturnType<typeof setTimeout>;
71
+ const tick = () => {
72
+ setElapsed(Math.floor((Date.now() - startedAt) / TICK_MS));
73
+ timeout = setTimeout(tick, TICK_MS - (Date.now() % TICK_MS));
74
+ };
75
+ timeout = setTimeout(tick, TICK_MS - (Date.now() % TICK_MS));
76
+ return () => clearTimeout(timeout);
77
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `elapsed` is
78
+ // captured once at resume; depending on it would restart the timer on
79
+ // every tick.
80
+ }, [running]);
81
+
82
+ useImperativeHandle(
83
+ forwardedRef,
84
+ () => ({
85
+ start: () => setRunning(true),
86
+ stop: () => setRunning(false),
87
+ }),
88
+ [],
89
+ );
90
+
91
+ return (
92
+ <ChatStatusProvider elapsed={elapsed} running={running}>
93
+ <span className={mx('inline-flex items-center gap-2 text-description font-mono tabular-nums', classNames)}>
94
+ {children}
95
+ </span>
96
+ </ChatStatusProvider>
97
+ );
98
+ },
99
+ );
100
+
101
+ Root.displayName = 'ChatChatStatus.Root';
102
+
103
+ //
104
+ // Icon
105
+ //
106
+
107
+ export type IconProps = ThemedClassName<PropsWithChildren>;
108
+
109
+ /**
110
+ * Animated leading indicator. Defaults to a halo-pulse dot. Pass children to override.
111
+ */
112
+ const Icon = ({ classNames, children }: IconProps) => {
113
+ if (children !== undefined) {
114
+ return <>{children}</>;
115
+ }
116
+ return (
117
+ <span
118
+ aria-hidden='true'
119
+ className={mx('inline-block size-2 rounded-full bg-current animate-halo-pulse', classNames)}
120
+ />
121
+ );
122
+ };
123
+
124
+ //
125
+ // Stopwatch
126
+ //
127
+
128
+ export type StopwatchProps = ThemedClassName<{
129
+ /** Seconds to add to the context elapsed value before formatting. Defaults to 0. */
130
+ offset?: number;
131
+ }>;
132
+
133
+ /**
134
+ * Elapsed-time display, driven by the ChatStatus.Root context tick.
135
+ * Use `offset` to shift the displayed value; remount `ChatStatus.Root` to reset the underlying counter.
136
+ * Marked `aria-live='off'` so screen readers don't announce every tick.
137
+ */
138
+ const Stopwatch = ({ classNames, offset = 0 }: StopwatchProps) => {
139
+ const { elapsed } = useChatStatusContext('ChatChatStatus.Stopwatch');
140
+ return (
141
+ <span aria-live='off' className={mx(classNames)}>
142
+ {formatElapsed((elapsed + offset) * 1_000)}
143
+ </span>
144
+ );
145
+ };
146
+
147
+ //
148
+ // Separator
149
+ //
150
+
151
+ export type SeparatorProps = ThemedClassName<unknown>;
152
+
153
+ /**
154
+ * Middle-dot separator. Decorative — `aria-hidden`.
155
+ */
156
+ const Separator = ({ classNames }: SeparatorProps) => (
157
+ <span aria-hidden='true' className={mx('opacity-50', classNames)}>
158
+ ·
159
+ </span>
160
+ );
161
+
162
+ //
163
+ // Text
164
+ //
165
+
166
+ export type TextProps = ThemedClassName<PropsWithChildren>;
167
+
168
+ /**
169
+ * Generic text node — useful for token counts and other inline metadata.
170
+ */
171
+ const Text = ({ classNames, children }: TextProps) => <span className={mx(classNames)}>{children}</span>;
172
+
173
+ //
174
+ // ChatStatus
175
+ //
176
+
177
+ export const ChatStatus = {
178
+ Root,
179
+ Icon,
180
+ Stopwatch,
181
+ Separator,
182
+ Text,
183
+ };
@@ -0,0 +1,48 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { formatElapsed } from './format';
8
+
9
+ describe('formatElapsed', () => {
10
+ test('zero returns 0s', ({ expect }) => {
11
+ expect(formatElapsed(0)).toBe('0s');
12
+ });
13
+
14
+ test('negative inputs clamp to 0s', ({ expect }) => {
15
+ expect(formatElapsed(-1_000)).toBe('0s');
16
+ expect(formatElapsed(-Infinity)).toBe('0s');
17
+ });
18
+
19
+ test('seconds tier under one minute', ({ expect }) => {
20
+ expect(formatElapsed(999)).toBe('0s');
21
+ expect(formatElapsed(1_000)).toBe('1s');
22
+ expect(formatElapsed(12_400)).toBe('12s');
23
+ expect(formatElapsed(59_999)).toBe('59s');
24
+ });
25
+
26
+ test('boundary at one minute', ({ expect }) => {
27
+ expect(formatElapsed(60_000)).toBe('1m 0s');
28
+ expect(formatElapsed(60_999)).toBe('1m 0s');
29
+ expect(formatElapsed(61_000)).toBe('1m 1s');
30
+ });
31
+
32
+ test('minutes tier under one hour', ({ expect }) => {
33
+ expect(formatElapsed(92_000)).toBe('1m 32s');
34
+ expect(formatElapsed(3_540_000)).toBe('59m 0s');
35
+ expect(formatElapsed(3_599_999)).toBe('59m 59s');
36
+ });
37
+
38
+ test('boundary at one hour', ({ expect }) => {
39
+ expect(formatElapsed(3_600_000)).toBe('1h 0m');
40
+ expect(formatElapsed(3_660_000)).toBe('1h 1m');
41
+ });
42
+
43
+ test('hours tier drops smaller units', ({ expect }) => {
44
+ expect(formatElapsed(3_720_000)).toBe('1h 2m');
45
+ expect(formatElapsed(7_323_000)).toBe('2h 2m');
46
+ expect(formatElapsed(36_000_000)).toBe('10h 0m');
47
+ });
48
+ });
@@ -0,0 +1,27 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ const SECOND = 1_000;
6
+ const MINUTE = 60 * SECOND;
7
+ const HOUR = 60 * MINUTE;
8
+
9
+ /**
10
+ * Formats elapsed milliseconds as a human-readable duration.
11
+ * Tiers: `Ns` (< 60s), `Nm Xs` (< 60m), `Nh Xm` (≥ 60m).
12
+ * Negative inputs clamp to `0s`.
13
+ */
14
+ export const formatElapsed = (ms: number): string => {
15
+ const safe = Math.max(0, ms);
16
+ if (safe < MINUTE) {
17
+ return `${Math.floor(safe / SECOND)}s`;
18
+ }
19
+ if (safe < HOUR) {
20
+ const m = Math.floor(safe / MINUTE);
21
+ const s = Math.floor((safe % MINUTE) / SECOND);
22
+ return `${m}m ${s}s`;
23
+ }
24
+ const h = Math.floor(safe / HOUR);
25
+ const m = Math.floor((safe % HOUR) / MINUTE);
26
+ return `${h}h ${m}m`;
27
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './ChatStatus';
6
+ export * from './format';
@@ -4,3 +4,4 @@
4
4
 
5
5
  export * from './ChatDialog';
6
6
  export * from './ChatEditor';
7
+ export * from './ChatStatus';
@@ -4,51 +4,12 @@
4
4
 
5
5
  import { type Resource } from '@dxos/react-ui';
6
6
 
7
- export const translationKey = 'react-ui-chat';
7
+ export const translationKey = '@dxos/react-ui-chat';
8
8
 
9
9
  export const translations = [
10
10
  {
11
11
  'en-US': {
12
- [translationKey]: {
13
- 'templates label': 'Templates',
14
- 'open ambient chat label': 'Open Assistant',
15
- 'assistant chat label': 'Assistant',
16
- 'plugin name': 'Assistant',
17
- 'object placeholder': 'New prompt',
18
- 'create object label': 'Create prompt',
19
- 'create trigger label': 'Create trigger',
20
- 'create stack section label': 'Create prompt',
21
- 'command placeholder': 'Enter slash command...',
22
- 'template placeholder': 'Enter template...',
23
- 'value placeholder': 'Enter value...',
24
- 'prompt rules label': 'Prompt Rules',
25
- 'typename placeholder': 'Enter typename of objects which this template is for',
26
- 'description placeholder': 'Enter description of when this template should be used',
27
- 'select preset template placeholder': 'Select preset',
28
- 'service registry label': 'Service Registry',
29
-
30
- 'sequence logs label': 'Logs',
31
-
32
- 'assistant dialog title': 'Assistant',
33
- 'open assistant label': 'Open assistant',
34
-
35
- 'search input placeholder': 'Search...',
36
- 'chat input placeholder': 'Ask a question...',
37
- 'chat stop': 'Cancel request.',
38
-
39
- 'button save': 'Save',
40
- 'button run': 'Run',
41
-
42
- 'prompt placeholder': 'Ask a question...',
43
- 'microphone button': 'Click to speak',
44
- 'cancel processing button': 'Stop processing',
45
-
46
- 'settings default label': 'Default',
47
- 'settings custom prompts label': 'Use custom prompts',
48
- 'settings llm provider label': 'LLM provider',
49
- 'settings edge llm model label': 'Remote language model',
50
- 'settings ollama llm model label': 'Ollama language model',
51
- },
12
+ [translationKey]: {},
52
13
  },
53
14
  },
54
15
  ] as const satisfies Resource[];
@@ -1,26 +0,0 @@
1
- import { type Extension } from '@codemirror/state';
2
- export type AutocompleteOptions = {
3
- fireIfEmpty?: boolean;
4
- /**
5
- * Callback triggered when Enter is pressed.
6
- * @param text The current text in the editor
7
- * @returns true if the editor should reset the document.
8
- */
9
- onSubmit?: (text: string) => boolean | void;
10
- /**
11
- * Function that returns a list of suggestions based on the current text.
12
- * @param text The current text before the cursor
13
- * @returns Array of suggestion strings
14
- */
15
- onSuggest?: (text: string) => string[];
16
- /**
17
- * ESC pressed.
18
- */
19
- onCancel?: () => void;
20
- };
21
- /**
22
- * Creates an autocomplete extension that shows inline suggestions.
23
- * Pressing Tab will complete the suggestion.
24
- */
25
- export declare const autocomplete: ({ fireIfEmpty, onSubmit, onSuggest, onCancel }: AutocompleteOptions) => Extension;
26
- //# sourceMappingURL=autocomplete.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../../../../../src/components/ChatEditor/autocomplete.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAQ,MAAM,mBAAmB,CAAC;AAWzD,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAE5C;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAEvC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAEF;;;GAGG;AAEH,eAAO,MAAM,YAAY,GAAI,gDAAgD,mBAAmB,KAAG,SA+JlG,CAAC"}