@dxos/react-ui-chat 0.8.4-main.03d5cd7b56

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 (59) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +1 -0
  3. package/dist/lib/browser/index.mjs +359 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/lib/browser/translations.mjs +14 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +361 -0
  9. package/dist/lib/node-esm/index.mjs.map +7 -0
  10. package/dist/lib/node-esm/meta.json +1 -0
  11. package/dist/lib/node-esm/translations.mjs +16 -0
  12. package/dist/lib/node-esm/translations.mjs.map +7 -0
  13. package/dist/types/src/components/ChatDialog/ChatDialog.d.ts +50 -0
  14. package/dist/types/src/components/ChatDialog/ChatDialog.d.ts.map +1 -0
  15. package/dist/types/src/components/ChatDialog/ChatDialog.stories.d.ts +38 -0
  16. package/dist/types/src/components/ChatDialog/ChatDialog.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/ChatDialog/index.d.ts +2 -0
  18. package/dist/types/src/components/ChatDialog/index.d.ts.map +1 -0
  19. package/dist/types/src/components/ChatEditor/ChatEditor.d.ts +20 -0
  20. package/dist/types/src/components/ChatEditor/ChatEditor.d.ts.map +1 -0
  21. package/dist/types/src/components/ChatEditor/ChatEditor.stories.d.ts +25 -0
  22. package/dist/types/src/components/ChatEditor/ChatEditor.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/ChatEditor/index.d.ts +3 -0
  24. package/dist/types/src/components/ChatEditor/index.d.ts.map +1 -0
  25. package/dist/types/src/components/ChatEditor/references.d.ts +34 -0
  26. package/dist/types/src/components/ChatEditor/references.d.ts.map +1 -0
  27. package/dist/types/src/components/ChatStatus/ChatStatus.d.ts +40 -0
  28. package/dist/types/src/components/ChatStatus/ChatStatus.d.ts.map +1 -0
  29. package/dist/types/src/components/ChatStatus/ChatStatus.stories.d.ts +27 -0
  30. package/dist/types/src/components/ChatStatus/ChatStatus.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/ChatStatus/format.d.ts +7 -0
  32. package/dist/types/src/components/ChatStatus/format.d.ts.map +1 -0
  33. package/dist/types/src/components/ChatStatus/format.test.d.ts +2 -0
  34. package/dist/types/src/components/ChatStatus/format.test.d.ts.map +1 -0
  35. package/dist/types/src/components/ChatStatus/index.d.ts +3 -0
  36. package/dist/types/src/components/ChatStatus/index.d.ts.map +1 -0
  37. package/dist/types/src/components/index.d.ts +4 -0
  38. package/dist/types/src/components/index.d.ts.map +1 -0
  39. package/dist/types/src/index.d.ts +2 -0
  40. package/dist/types/src/index.d.ts.map +1 -0
  41. package/dist/types/src/translations.d.ts +7 -0
  42. package/dist/types/src/translations.d.ts.map +1 -0
  43. package/dist/types/tsconfig.tsbuildinfo +1 -0
  44. package/package.json +84 -0
  45. package/src/components/ChatDialog/ChatDialog.stories.tsx +64 -0
  46. package/src/components/ChatDialog/ChatDialog.tsx +203 -0
  47. package/src/components/ChatDialog/index.ts +5 -0
  48. package/src/components/ChatEditor/ChatEditor.stories.tsx +97 -0
  49. package/src/components/ChatEditor/ChatEditor.tsx +59 -0
  50. package/src/components/ChatEditor/index.ts +7 -0
  51. package/src/components/ChatEditor/references.ts +181 -0
  52. package/src/components/ChatStatus/ChatStatus.stories.tsx +130 -0
  53. package/src/components/ChatStatus/ChatStatus.tsx +183 -0
  54. package/src/components/ChatStatus/format.test.ts +48 -0
  55. package/src/components/ChatStatus/format.ts +27 -0
  56. package/src/components/ChatStatus/index.ts +6 -0
  57. package/src/components/index.ts +7 -0
  58. package/src/index.ts +5 -0
  59. package/src/translations.ts +15 -0
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@dxos/react-ui-chat",
3
+ "version": "0.8.4-main.03d5cd7b56",
4
+ "description": "Chat components.",
5
+ "homepage": "https://dxos.org",
6
+ "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
11
+ "license": "MIT",
12
+ "author": "DXOS.org",
13
+ "type": "module",
14
+ "imports": {
15
+ "#translations": "./src/translations.ts"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "source": "./src/index.ts",
20
+ "types": "./dist/types/src/index.d.ts",
21
+ "browser": "./dist/lib/browser/index.mjs",
22
+ "node": "./dist/lib/node-esm/index.mjs"
23
+ },
24
+ "./translations": {
25
+ "source": "./src/translations.ts",
26
+ "types": "./dist/types/src/translations.d.ts",
27
+ "browser": "./dist/lib/browser/translations.mjs",
28
+ "node": "./dist/lib/node-esm/translations.mjs"
29
+ }
30
+ },
31
+ "types": "dist/types/src/index.d.ts",
32
+ "files": [
33
+ "dist",
34
+ "src"
35
+ ],
36
+ "dependencies": {
37
+ "@codemirror/autocomplete": "^6.19.0",
38
+ "@codemirror/language": "^6.11.3",
39
+ "@codemirror/search": "^6.5.11",
40
+ "@codemirror/state": "^6.5.2",
41
+ "@codemirror/view": "^6.38.5",
42
+ "@fluentui/react-tabster": "9.26.11",
43
+ "@radix-ui/react-accordion": "1.2.3",
44
+ "@radix-ui/react-context": "1.1.1",
45
+ "@dxos/async": "0.8.4-main.03d5cd7b56",
46
+ "@dxos/debug": "0.8.4-main.03d5cd7b56",
47
+ "@dxos/echo": "0.8.4-main.03d5cd7b56",
48
+ "@dxos/invariant": "0.8.4-main.03d5cd7b56",
49
+ "@dxos/log": "0.8.4-main.03d5cd7b56",
50
+ "@dxos/react-ui": "0.8.4-main.03d5cd7b56",
51
+ "@dxos/react-ui-dnd": "0.8.4-main.03d5cd7b56",
52
+ "@dxos/react-ui-components": "0.8.4-main.03d5cd7b56",
53
+ "@dxos/react-ui-editor": "0.8.4-main.03d5cd7b56",
54
+ "@dxos/react-ui-sfx": "0.8.4-main.03d5cd7b56",
55
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.03d5cd7b56",
56
+ "@dxos/react-ui-text-tooltip": "0.8.4-main.03d5cd7b56",
57
+ "@dxos/ui-editor": "0.8.4-main.03d5cd7b56",
58
+ "@dxos/ui-types": "0.8.4-main.03d5cd7b56",
59
+ "@dxos/util": "0.8.4-main.03d5cd7b56",
60
+ "@dxos/ui-theme": "0.8.4-main.03d5cd7b56"
61
+ },
62
+ "devDependencies": {
63
+ "@types/react": "~19.2.7",
64
+ "@types/react-dom": "~19.2.3",
65
+ "effect": "3.20.0",
66
+ "react": "~19.2.3",
67
+ "react-dom": "~19.2.3",
68
+ "vite": "^8.0.10",
69
+ "@dxos/echo": "0.8.4-main.03d5cd7b56",
70
+ "@dxos/react-client": "0.8.4-main.03d5cd7b56",
71
+ "@dxos/storybook-utils": "0.8.4-main.03d5cd7b56",
72
+ "@dxos/random": "0.8.4-main.03d5cd7b56"
73
+ },
74
+ "peerDependencies": {
75
+ "effect": "3.20.0",
76
+ "react": "~19.2.3",
77
+ "react-dom": "~19.2.3",
78
+ "@dxos/react-ui": "0.8.4-main.03d5cd7b56",
79
+ "@dxos/ui-theme": "0.8.4-main.03d5cd7b56"
80
+ },
81
+ "publishConfig": {
82
+ "access": "public"
83
+ }
84
+ }
@@ -0,0 +1,64 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useState } from 'react';
7
+
8
+ import { Input, Toolbar } from '@dxos/react-ui';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+
11
+ import { translations } from '#translations';
12
+
13
+ import { ChatDialog } from './ChatDialog';
14
+
15
+ const items = Array.from({ length: 100 }, (_, i) => `Item ${i}`);
16
+
17
+ const meta = {
18
+ title: 'ui/react-ui-chat/ChatDialog',
19
+ component: ChatDialog.Root,
20
+ render: (args) => {
21
+ const [open, setOpen] = useState(true);
22
+ const [expanded, setExpanded] = useState(true);
23
+ return (
24
+ <>
25
+ <Toolbar.Root>
26
+ <Toolbar.Button onClick={() => setOpen((open) => !open)}>Open</Toolbar.Button>
27
+ <Toolbar.Button onClick={() => setExpanded((expanded) => !expanded)}>Expand</Toolbar.Button>
28
+ </Toolbar.Root>
29
+
30
+ <ChatDialog.Root
31
+ {...args}
32
+ open={open}
33
+ onOpenChange={setOpen}
34
+ expanded={expanded}
35
+ onExpandedChange={setExpanded}
36
+ >
37
+ <ChatDialog.Header title='Chat' />
38
+ <ChatDialog.Content>
39
+ {items.map((item) => (
40
+ <div key={item} className='ps-4 pb-1'>
41
+ {item}
42
+ </div>
43
+ ))}
44
+ </ChatDialog.Content>
45
+ <ChatDialog.Footer classNames='px-2 items-center'>
46
+ <Input.Root>
47
+ <Input.TextInput classNames='border-none' placeholder='Test' />
48
+ </Input.Root>
49
+ </ChatDialog.Footer>
50
+ </ChatDialog.Root>
51
+ </>
52
+ );
53
+ },
54
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
55
+ parameters: {
56
+ translations,
57
+ },
58
+ } satisfies Meta<typeof ChatDialog.Root>;
59
+
60
+ export default meta;
61
+
62
+ type Story = StoryObj<typeof meta>;
63
+
64
+ export const Default: Story = {};
@@ -0,0 +1,203 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import React, { type Dispatch, type PropsWithChildren, type SetStateAction, useEffect, useState } from 'react';
7
+
8
+ import { Dialog, Icon, IconButton, type ThemedClassName, useControlledState } from '@dxos/react-ui';
9
+ import { ResizeHandle, type Size, resizeAttributes, sizeStyle } from '@dxos/react-ui-dnd';
10
+ import { mx } from '@dxos/ui-theme';
11
+
12
+ const preventDefault = (event: Event) => event.preventDefault();
13
+
14
+ // TODO(burdon): Factor out.
15
+ const Endcap = ({ children }: PropsWithChildren) => {
16
+ return (
17
+ <div className='grid w-[var(--dx-rail-action)] h-[var(--dx-rail-action)] items-center justify-center'>
18
+ {children}
19
+ </div>
20
+ );
21
+ };
22
+
23
+ //
24
+ // Context
25
+ //
26
+
27
+ type ChatDialogContextValue = {
28
+ open: boolean;
29
+ setOpen: Dispatch<SetStateAction<boolean>>;
30
+ expanded: boolean;
31
+ setExpanded: Dispatch<SetStateAction<boolean>>;
32
+ size: Size;
33
+ setSize: Dispatch<SetStateAction<Size>>;
34
+ };
35
+
36
+ const [ChatDialogContextProvider, useChatDialogContext] = createContext<ChatDialogContextValue>('ChatDialog');
37
+
38
+ //
39
+ // Root
40
+ //
41
+
42
+ type ChatDialogRootProps = PropsWithChildren<{
43
+ open?: boolean;
44
+ expanded?: boolean;
45
+ onOpenChange?: (open: boolean) => void;
46
+ onExpandedChange?: (expanded: boolean) => void;
47
+ onEscape?: () => void;
48
+ }>;
49
+
50
+ const ChatDialogRoot = ({
51
+ children,
52
+ open: openProp = false,
53
+ expanded: expandedProp = false,
54
+ onOpenChange,
55
+ onExpandedChange,
56
+ onEscape,
57
+ }: ChatDialogRootProps) => {
58
+ const [size, setSize] = useState<Size>('min-content');
59
+ const [open, setOpen] = useControlledState<boolean>(openProp, onOpenChange);
60
+ const [expanded, setExpanded] = useControlledState<boolean>(expandedProp, onExpandedChange);
61
+
62
+ // NOTE: We set the min size to 5rem (80px), and the header and prompt bar to 40px (i.e., the rail-size) each.
63
+ // The dialog has no vertical padding and has box-content so that when closed it collapses to the size of the header and prompt bar.
64
+ return (
65
+ <ChatDialogContextProvider
66
+ open={open}
67
+ setOpen={setOpen}
68
+ expanded={expanded}
69
+ setExpanded={setExpanded}
70
+ size={size}
71
+ setSize={setSize}
72
+ >
73
+ <Dialog.Root modal={false} open={open} onOpenChange={setOpen}>
74
+ <div className='dx-dialog__overlay bg-transparent pointer-events-none' data-block-align='end'>
75
+ <Dialog.Content
76
+ size='md'
77
+ inOverlayLayout
78
+ classNames={[
79
+ 'grid grid-rows-[var(--dx-rail-action)_1fr_min-content] p-0 overflow-hidden box-content pointer-events-auto',
80
+ ]}
81
+ onEscapeKeyDown={onEscape}
82
+ onInteractOutside={preventDefault}
83
+ >
84
+ {children}
85
+ </Dialog.Content>
86
+ </div>
87
+ </Dialog.Root>
88
+ </ChatDialogContextProvider>
89
+ );
90
+ };
91
+
92
+ ChatDialogRoot.displayName = 'ChatDialog.Root';
93
+
94
+ //
95
+ // Header
96
+ //
97
+
98
+ const CHAT_DIALOG_HEADER_NAME = 'ChatDialog.Header';
99
+
100
+ type ChatDialogHeaderProps = ThemedClassName<{
101
+ title?: string;
102
+ }>;
103
+
104
+ const ChatDialogHeader = ({ classNames, title }: ChatDialogHeaderProps) => {
105
+ const { expanded, setExpanded } = useChatDialogContext(CHAT_DIALOG_HEADER_NAME);
106
+
107
+ return (
108
+ <div
109
+ className={mx('grid grid-cols-[var(--dx-rail-action)_1fr_min-content] items-center overflow-hidden', classNames)}
110
+ >
111
+ <Endcap>
112
+ <Dialog.Close>
113
+ <Icon icon='ph--x--regular' />
114
+ </Dialog.Close>
115
+ </Endcap>
116
+ <Dialog.Title
117
+ classNames='flex w-full justify-center text-sm text-subdued select-none cursor-pointer'
118
+ onClick={() => setExpanded((expanded) => !expanded)}
119
+ >
120
+ {title}
121
+ </Dialog.Title>
122
+ <Endcap>
123
+ <IconButton
124
+ variant='ghost'
125
+ icon='ph--caret-up--regular'
126
+ iconOnly
127
+ classNames={mx('p-1! [&>svg]:transition [&>svg]:duration-200', expanded && '[&>svg]:rotate-180')}
128
+ label={expanded ? 'Close' : 'Open'}
129
+ onClick={() => setExpanded((expanded) => !expanded)}
130
+ />
131
+ </Endcap>
132
+ </div>
133
+ );
134
+ };
135
+
136
+ ChatDialogHeader.displayName = CHAT_DIALOG_HEADER_NAME;
137
+
138
+ //
139
+ // Content
140
+ //
141
+
142
+ const CHAT_DIALOG_CONTENT_NAME = 'ChatDialog.Content';
143
+
144
+ type ChatDialogContentProps = ThemedClassName<PropsWithChildren>;
145
+
146
+ const ChatDialogContent = ({ children, classNames }: ChatDialogContentProps) => {
147
+ const { expanded, size, setSize } = useChatDialogContext(CHAT_DIALOG_CONTENT_NAME);
148
+ useEffect(() => {
149
+ setSize(expanded ? 'min-content' : 0);
150
+ }, [expanded]);
151
+
152
+ return (
153
+ <div
154
+ className={mx(
155
+ 'border-t border-b border-subdued-separator',
156
+ 'transition ease-in-out duration-0 [&:not([data-dx-resizing="true"])]:duration-200',
157
+ classNames,
158
+ )}
159
+ style={{
160
+ ...sizeStyle(size, 'vertical', true),
161
+ maxBlockSize: 'calc(100dvh - env(safe-area-inset-bottom) - env(safe-area-inset-top) - 14rem)',
162
+ }}
163
+ {...resizeAttributes}
164
+ >
165
+ <ResizeHandle
166
+ side='block-start'
167
+ iconPosition='center'
168
+ defaultSize='min-content'
169
+ fallbackSize={0}
170
+ minSize={0}
171
+ onSizeChange={setSize}
172
+ />
173
+ {children}
174
+ </div>
175
+ );
176
+ };
177
+
178
+ ChatDialogContent.displayName = CHAT_DIALOG_CONTENT_NAME;
179
+
180
+ //
181
+ // Footer
182
+ //
183
+
184
+ const CHAT_DIALOG_FOOTER_NAME = 'ChatDialog.Footer';
185
+
186
+ type ChatDialogFooterProps = ThemedClassName<PropsWithChildren>;
187
+
188
+ const ChatDialogFooter = ({ children, classNames }: ChatDialogFooterProps) => {
189
+ return <div className={mx(classNames)}>{children}</div>;
190
+ };
191
+
192
+ ChatDialogFooter.displayName = CHAT_DIALOG_FOOTER_NAME;
193
+
194
+ //
195
+ // ChatDialog
196
+ //
197
+
198
+ export const ChatDialog = {
199
+ Root: ChatDialogRoot,
200
+ Header: ChatDialogHeader,
201
+ Content: ChatDialogContent,
202
+ Footer: ChatDialogFooter,
203
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './ChatDialog';
@@ -0,0 +1,97 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+
7
+ import { log } from '@dxos/log';
8
+ import { withTheme } from '@dxos/react-ui/testing';
9
+
10
+ import { translations } from '#translations';
11
+
12
+ import { ChatEditor } from './ChatEditor';
13
+ import { type ReferenceData } from './references';
14
+
15
+ const meta = {
16
+ title: 'ui/react-ui-chat/ChatEditor',
17
+ component: ChatEditor,
18
+ decorators: [withTheme()],
19
+ parameters: {
20
+ layout: 'centered',
21
+ translations,
22
+ },
23
+ } satisfies Meta<typeof ChatEditor>;
24
+
25
+ export default meta;
26
+
27
+ const classNames = 'w-[20rem] p-2 border border-separator';
28
+
29
+ type Story = StoryObj<typeof meta>;
30
+
31
+ export const Default: Story = {
32
+ args: {
33
+ classNames,
34
+ placeholder: 'Ask a question...',
35
+ },
36
+ };
37
+
38
+ // TODO(burdon): Restore and reconcile with suggest/typeahead.
39
+ export const WithSuggestions: Story = {
40
+ args: {
41
+ classNames,
42
+ onSubmit: (text) => {
43
+ log('onSubmit', { text });
44
+ },
45
+
46
+ // onSuggest: (text) => {
47
+ // const trimmed = text.trim().toLowerCase();
48
+ // if (trimmed.length < 2) {
49
+ // return [];
50
+ // }
51
+ // const suggestions = [
52
+ // 'Create a kanban board',
53
+ // 'Create a new project',
54
+ // 'Find flights to Tokyo',
55
+ // "Let's play chess",
56
+ // 'Show me Paris on a map',
57
+ // ];
58
+ // return suggestions.filter((s) => s.toLowerCase().startsWith(text));
59
+ // },
60
+ },
61
+ };
62
+
63
+ // TODO(burdon): Replace.
64
+ const references: ReferenceData[] = [
65
+ {
66
+ uri: 'dxn:echo:@:AAAAAAAA',
67
+ label: 'Meeting Notes',
68
+ },
69
+ {
70
+ uri: 'dxn:echo:@:BBBBBBBB',
71
+ label: 'Project Plan',
72
+ },
73
+ {
74
+ uri: 'dxn:echo:@:CCCCCCCC',
75
+ label: 'Meeting Plan',
76
+ },
77
+ ];
78
+
79
+ export const WithReferences: Story = {
80
+ args: {
81
+ classNames,
82
+ references: {
83
+ provider: {
84
+ getReferences: async ({ query }) => {
85
+ const res = references.filter((i) => i.label.toLowerCase().startsWith(query.toLowerCase()));
86
+ log('getReferences', { query, res });
87
+ return res;
88
+ },
89
+ resolveReference: async ({ uri }) => {
90
+ const res = references.find((i) => i.uri === uri);
91
+ log('resolveReference', { uri, res });
92
+ return res ?? null;
93
+ },
94
+ },
95
+ },
96
+ },
97
+ };
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import React, { forwardRef, useMemo } from 'react';
7
+
8
+ import { type ThemedClassName, useThemeContext } from '@dxos/react-ui';
9
+ import { Editor, type EditorController, type UseTextEditorProps } from '@dxos/react-ui-editor';
10
+ import {
11
+ type BasicExtensionsOptions,
12
+ type SubmitOptions,
13
+ createBasicExtensions,
14
+ createThemeExtensions,
15
+ submit,
16
+ xmlFormatting,
17
+ } from '@dxos/ui-editor';
18
+ import { isTruthy } from '@dxos/util';
19
+
20
+ import { type ReferencesOptions } from './references';
21
+
22
+ export interface ChatEditorController extends EditorController {}
23
+
24
+ export type ChatEditorProps = ThemedClassName<
25
+ {
26
+ extensions?: Extension;
27
+ references?: ReferencesOptions;
28
+ } & (SubmitOptions &
29
+ Pick<UseTextEditorProps, 'id' | 'autoFocus'> &
30
+ Pick<BasicExtensionsOptions, 'lineWrapping' | 'placeholder'>)
31
+ >;
32
+
33
+ export const useChatExtensions = ({ extensions, lineWrapping = false, placeholder, onSubmit }: ChatEditorProps) => {
34
+ const { themeMode } = useThemeContext();
35
+ return useMemo<Extension[]>(
36
+ () =>
37
+ [
38
+ createThemeExtensions({ themeMode }),
39
+ createBasicExtensions({ bracketMatching: false, lineWrapping, placeholder }),
40
+ xmlFormatting(),
41
+ submit({ onSubmit }),
42
+ extensions,
43
+ ].filter(isTruthy),
44
+ [themeMode, extensions, onSubmit],
45
+ );
46
+ };
47
+
48
+ export const ChatEditor = forwardRef<ChatEditorController, ChatEditorProps>(
49
+ ({ classNames, autoFocus, extensions: extensionsProp, lineWrapping = false, placeholder, onSubmit }, forwardRef) => {
50
+ const extensions = useChatExtensions({ extensions: extensionsProp, lineWrapping, placeholder, onSubmit });
51
+
52
+ // TODO(burdon): Popover.
53
+ return (
54
+ <Editor.Root ref={forwardRef}>
55
+ <Editor.View classNames={classNames} autoFocus={autoFocus} extensions={extensions} />
56
+ </Editor.Root>
57
+ );
58
+ },
59
+ );
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './references';
6
+
7
+ export * from './ChatEditor';
@@ -0,0 +1,181 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type CompletionResult, autocompletion, completionKeymap } from '@codemirror/autocomplete';
6
+ import { type Extension, RangeSet } from '@codemirror/state';
7
+ import {
8
+ Decoration,
9
+ type DecorationSet,
10
+ EditorView,
11
+ ViewPlugin,
12
+ type ViewUpdate,
13
+ WidgetType,
14
+ keymap,
15
+ } from '@codemirror/view';
16
+
17
+ import { Mutex } from '@dxos/async';
18
+ import { invariant } from '@dxos/invariant';
19
+
20
+ // TODO(dmaretskyi): Consider adding details renderer for when you hover over the reference.
21
+ export type ReferenceData = {
22
+ uri: string; // TODO(burdon): Rename dxn.
23
+ label: string;
24
+ };
25
+
26
+ /**
27
+ * @deprecated
28
+ */
29
+ // TODO(burdon): Remove.
30
+ export interface ReferencesProvider {
31
+ getReferences({ query }: { query: string }): Promise<ReferenceData[]>;
32
+ resolveReference({ uri }: { uri: string }): Promise<ReferenceData | null>;
33
+ }
34
+
35
+ export type ReferencesOptions = {
36
+ provider: ReferencesProvider;
37
+
38
+ /**
39
+ * @default '@'
40
+ */
41
+ triggerCharacter?: string;
42
+
43
+ /**
44
+ * Prevent the autocomplete from closing when the user blurs the editor.
45
+ * @default false
46
+ */
47
+ debug?: boolean;
48
+ };
49
+
50
+ /**
51
+ * Lookup object references.
52
+ * @deprecated
53
+ */
54
+ // TODO(burdon): Remove.
55
+ export const references = ({ provider, triggerCharacter = '@', debug = false }: ReferencesOptions): Extension => {
56
+ invariant(triggerCharacter.length === 1);
57
+
58
+ const decorationField = ViewPlugin.fromClass(
59
+ class ReferenceView {
60
+ // TODO(burdon): Why?
61
+ private _mutex = new Mutex();
62
+
63
+ decorations: DecorationSet = Decoration.set([]);
64
+
65
+ constructor(view: EditorView) {
66
+ queueMicrotask(async () => {
67
+ const guard = await this._mutex.acquire();
68
+ try {
69
+ this.decorations = await this._computeDecorations(view);
70
+ } finally {
71
+ guard.release();
72
+ }
73
+ });
74
+ }
75
+
76
+ update(update: ViewUpdate) {
77
+ if (update.docChanged) {
78
+ queueMicrotask(async () => {
79
+ const guard = await this._mutex.acquire();
80
+ try {
81
+ this.decorations = await this._computeDecorations(update.view);
82
+ } finally {
83
+ guard.release();
84
+ }
85
+ });
86
+ }
87
+ }
88
+
89
+ private async _computeDecorations(view: EditorView): Promise<DecorationSet> {
90
+ const text = view.state.doc.toString();
91
+ const references = text.matchAll(new RegExp(`${triggerCharacter}[a-zA-Z0-9@:]+\\s`, 'g'));
92
+
93
+ const decorations = [];
94
+ for (const match of references) {
95
+ const reference = match[0];
96
+ const uri = reference.slice(1, -1);
97
+ const data = await provider.resolveReference({ uri });
98
+ if (data) {
99
+ decorations.push(
100
+ Decoration.replace({
101
+ widget: new ReferenceWidget(data),
102
+ }).range(match.index!, match.index! + reference.length),
103
+ );
104
+ }
105
+ }
106
+
107
+ return Decoration.set(decorations);
108
+ }
109
+ },
110
+ {
111
+ decorations: (v) => v.decorations,
112
+ provide: () => [
113
+ EditorView.atomicRanges.of(
114
+ (view): DecorationSet => view.plugin(decorationField)?.decorations ?? RangeSet.empty,
115
+ ),
116
+ ],
117
+ },
118
+ );
119
+
120
+ return [
121
+ decorationField,
122
+
123
+ EditorView.theme({
124
+ '.cm-reference-pill': {
125
+ borderRadius: '0.25rem',
126
+ borderWidth: '1px',
127
+ marginRight: '0.25rem',
128
+ marginLeft: '0.25rem',
129
+ },
130
+ }),
131
+
132
+ autocompletion({
133
+ tooltipClass: () => 'shadow rounded-sm',
134
+ activateOnTyping: true,
135
+ aboveCursor: true,
136
+ closeOnBlur: !debug,
137
+ override: [
138
+ async (context): Promise<CompletionResult | null> => {
139
+ const match = context.matchBefore(new RegExp(`${triggerCharacter}[a-zA-Z0-9]+`));
140
+ if (!match || match?.to === match?.from) {
141
+ return null;
142
+ }
143
+
144
+ const query = match.text.slice(1);
145
+ const references = await provider.getReferences({ query });
146
+ return {
147
+ from: match.from,
148
+ filter: false,
149
+ options: references.map((reference) => ({
150
+ label: reference.label,
151
+ apply: `${triggerCharacter}${reference.uri} `,
152
+ })),
153
+ };
154
+ },
155
+ ],
156
+ }),
157
+
158
+ keymap.of(completionKeymap),
159
+ ];
160
+ };
161
+
162
+ class ReferenceWidget extends WidgetType {
163
+ constructor(private data: ReferenceData) {
164
+ super();
165
+ }
166
+
167
+ override ignoreEvent() {
168
+ return true;
169
+ }
170
+
171
+ override eq(other: this) {
172
+ return this.data.uri === other.data.uri;
173
+ }
174
+
175
+ override toDOM() {
176
+ const span = document.createElement('span');
177
+ span.textContent = `@${this.data.label}`;
178
+ span.className = 'cm-reference-pill';
179
+ return span;
180
+ }
181
+ }