@dxos/react-ui-chat 0.8.4-main.84f28bd
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/LICENSE +8 -0
- package/README.md +1 -0
- package/dist/lib/browser/index.mjs +480 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/node-esm/index.mjs +482 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/types/src/components/ChatDialog/ChatDialog.d.ts +34 -0
- package/dist/types/src/components/ChatDialog/ChatDialog.d.ts.map +1 -0
- package/dist/types/src/components/ChatDialog/ChatDialog.stories.d.ts +8 -0
- package/dist/types/src/components/ChatDialog/ChatDialog.stories.d.ts.map +1 -0
- package/dist/types/src/components/ChatDialog/index.d.ts +2 -0
- package/dist/types/src/components/ChatDialog/index.d.ts.map +1 -0
- package/dist/types/src/components/ChatEditor/ChatEditor.d.ts +21 -0
- package/dist/types/src/components/ChatEditor/ChatEditor.d.ts.map +1 -0
- package/dist/types/src/components/ChatEditor/ChatEditor.stories.d.ts +10 -0
- package/dist/types/src/components/ChatEditor/ChatEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/ChatEditor/autocomplete.d.ts +25 -0
- package/dist/types/src/components/ChatEditor/autocomplete.d.ts.map +1 -0
- package/dist/types/src/components/ChatEditor/index.d.ts +3 -0
- package/dist/types/src/components/ChatEditor/index.d.ts.map +1 -0
- package/dist/types/src/components/ChatEditor/references.d.ts +30 -0
- package/dist/types/src/components/ChatEditor/references.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +40 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +74 -0
- package/src/components/ChatDialog/ChatDialog.stories.tsx +65 -0
- package/src/components/ChatDialog/ChatDialog.tsx +193 -0
- package/src/components/ChatDialog/index.ts +5 -0
- package/src/components/ChatEditor/ChatEditor.stories.tsx +96 -0
- package/src/components/ChatEditor/ChatEditor.tsx +91 -0
- package/src/components/ChatEditor/autocomplete.ts +218 -0
- package/src/components/ChatEditor/index.ts +7 -0
- package/src/components/ChatEditor/references.ts +175 -0
- package/src/components/index.ts +6 -0
- package/src/index.ts +5 -0
- package/src/translations.ts +54 -0
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dxos/react-ui-chat",
|
|
3
|
+
"version": "0.8.4-main.84f28bd",
|
|
4
|
+
"description": "Chat components.",
|
|
5
|
+
"homepage": "https://dxos.org",
|
|
6
|
+
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "DXOS.org",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/types/src/index.d.ts",
|
|
13
|
+
"browser": "./dist/lib/browser/index.mjs",
|
|
14
|
+
"node": "./dist/lib/node-esm/index.mjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"types": "dist/types/src/index.d.ts",
|
|
18
|
+
"typesVersions": {
|
|
19
|
+
"*": {}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@codemirror/autocomplete": "^6.18.1",
|
|
27
|
+
"@codemirror/language": "^6.10.3",
|
|
28
|
+
"@codemirror/search": "^6.5.6",
|
|
29
|
+
"@codemirror/state": "^6.4.1",
|
|
30
|
+
"@codemirror/view": "^6.34.1",
|
|
31
|
+
"@preact-signals/safe-react": "^0.9.0",
|
|
32
|
+
"@preact/signals-core": "^1.9.0",
|
|
33
|
+
"@radix-ui/react-accordion": "1.2.3",
|
|
34
|
+
"@radix-ui/react-context": "1.1.1",
|
|
35
|
+
"@dxos/async": "0.8.4-main.84f28bd",
|
|
36
|
+
"@dxos/debug": "0.8.4-main.84f28bd",
|
|
37
|
+
"@dxos/echo-schema": "0.8.4-main.84f28bd",
|
|
38
|
+
"@dxos/invariant": "0.8.4-main.84f28bd",
|
|
39
|
+
"@dxos/live-object": "0.8.4-main.84f28bd",
|
|
40
|
+
"@dxos/log": "0.8.4-main.84f28bd",
|
|
41
|
+
"@dxos/react-ui": "0.8.4-main.84f28bd",
|
|
42
|
+
"@dxos/react-ui-components": "0.8.4-main.84f28bd",
|
|
43
|
+
"@dxos/react-ui-dnd": "0.8.4-main.84f28bd",
|
|
44
|
+
"@dxos/react-ui-sfx": "0.8.4-main.84f28bd",
|
|
45
|
+
"@dxos/react-ui-editor": "0.8.4-main.84f28bd",
|
|
46
|
+
"@dxos/react-ui-syntax-highlighter": "0.8.4-main.84f28bd",
|
|
47
|
+
"@dxos/react-ui-text-tooltip": "0.8.4-main.84f28bd",
|
|
48
|
+
"@dxos/react-ui-theme": "0.8.4-main.84f28bd",
|
|
49
|
+
"@dxos/react-ui-types": "0.8.4-main.84f28bd",
|
|
50
|
+
"@dxos/util": "0.8.4-main.84f28bd"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/react": "~18.2.0",
|
|
54
|
+
"@types/react-dom": "~18.2.0",
|
|
55
|
+
"effect": "3.14.21",
|
|
56
|
+
"react": "~18.2.0",
|
|
57
|
+
"react-dom": "~18.2.0",
|
|
58
|
+
"vite": "5.4.7",
|
|
59
|
+
"@dxos/echo": "0.8.4-main.84f28bd",
|
|
60
|
+
"@dxos/random": "0.8.4-main.84f28bd",
|
|
61
|
+
"@dxos/react-client": "0.8.4-main.84f28bd",
|
|
62
|
+
"@dxos/storybook-utils": "0.8.4-main.84f28bd"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"effect": "3.13.3",
|
|
66
|
+
"react": "~18.2.0",
|
|
67
|
+
"react-dom": "~18.2.0",
|
|
68
|
+
"@dxos/react-ui": "0.8.4-main.84f28bd",
|
|
69
|
+
"@dxos/react-ui-theme": "0.8.4-main.84f28bd"
|
|
70
|
+
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos-theme';
|
|
6
|
+
|
|
7
|
+
import { type StoryObj, type Meta } from '@storybook/react-vite';
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { Input, Toolbar } from '@dxos/react-ui';
|
|
11
|
+
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
12
|
+
|
|
13
|
+
import { ChatDialog } from './ChatDialog';
|
|
14
|
+
import { translations } from '../../translations';
|
|
15
|
+
|
|
16
|
+
const items = Array.from({ length: 100 }, (_, i) => `Item ${i}`);
|
|
17
|
+
|
|
18
|
+
const meta: Meta<typeof ChatDialog.Root> = {
|
|
19
|
+
title: 'ui/react-ui-chat/ChatDialog',
|
|
20
|
+
component: ChatDialog.Root,
|
|
21
|
+
render: (args) => {
|
|
22
|
+
const [open, setOpen] = useState(true);
|
|
23
|
+
const [expanded, setExpanded] = useState(true);
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<Toolbar.Root>
|
|
27
|
+
<Toolbar.Button onClick={() => setOpen((open) => !open)}>Open</Toolbar.Button>
|
|
28
|
+
<Toolbar.Button onClick={() => setExpanded((expanded) => !expanded)}>Expand</Toolbar.Button>
|
|
29
|
+
</Toolbar.Root>
|
|
30
|
+
|
|
31
|
+
<ChatDialog.Root
|
|
32
|
+
{...args}
|
|
33
|
+
open={open}
|
|
34
|
+
onOpenChange={setOpen}
|
|
35
|
+
expanded={expanded}
|
|
36
|
+
onExpandedChange={setExpanded}
|
|
37
|
+
>
|
|
38
|
+
<ChatDialog.Header title='Chat' />
|
|
39
|
+
<ChatDialog.Content>
|
|
40
|
+
{items.map((item) => (
|
|
41
|
+
<div key={item} className='pis-4 pbe-1'>
|
|
42
|
+
{item}
|
|
43
|
+
</div>
|
|
44
|
+
))}
|
|
45
|
+
</ChatDialog.Content>
|
|
46
|
+
<ChatDialog.Footer classNames='px-2 items-center'>
|
|
47
|
+
<Input.Root>
|
|
48
|
+
<Input.TextInput classNames='border-none' placeholder='Test' />
|
|
49
|
+
</Input.Root>
|
|
50
|
+
</ChatDialog.Footer>
|
|
51
|
+
</ChatDialog.Root>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
decorators: [withTheme, withLayout({ fullscreen: true, classNames: 'flex flex-col' })],
|
|
56
|
+
parameters: {
|
|
57
|
+
translations,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default meta;
|
|
62
|
+
|
|
63
|
+
type Story = StoryObj<typeof ChatDialog.Root>;
|
|
64
|
+
|
|
65
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,193 @@
|
|
|
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, useControlledState, type ThemedClassName } from '@dxos/react-ui';
|
|
9
|
+
import { resizeAttributes, ResizeHandle, type Size, sizeStyle } from '@dxos/react-ui-dnd';
|
|
10
|
+
import { mx } from '@dxos/react-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(--rail-action)] h-[var(--rail-action)] items-center justify-center'>{children}</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
//
|
|
22
|
+
// Context
|
|
23
|
+
//
|
|
24
|
+
|
|
25
|
+
type ChatDialogContextValue = {
|
|
26
|
+
open: boolean;
|
|
27
|
+
setOpen: Dispatch<SetStateAction<boolean>>;
|
|
28
|
+
expanded: boolean;
|
|
29
|
+
setExpanded: Dispatch<SetStateAction<boolean>>;
|
|
30
|
+
size: Size;
|
|
31
|
+
setSize: Dispatch<SetStateAction<Size>>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const [ChatDialogContextProvider, useChatDialogContext] = createContext<ChatDialogContextValue>('ChatDialog');
|
|
35
|
+
|
|
36
|
+
//
|
|
37
|
+
// Root
|
|
38
|
+
//
|
|
39
|
+
|
|
40
|
+
type ChatDialogRootProps = PropsWithChildren<{
|
|
41
|
+
open?: boolean;
|
|
42
|
+
expanded?: boolean;
|
|
43
|
+
onOpenChange?: (open: boolean) => void;
|
|
44
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
45
|
+
onEscape?: () => void;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
const ChatDialogRoot = ({
|
|
49
|
+
children,
|
|
50
|
+
open: _open = false,
|
|
51
|
+
expanded: _expanded = false,
|
|
52
|
+
onOpenChange,
|
|
53
|
+
onExpandedChange,
|
|
54
|
+
onEscape,
|
|
55
|
+
}: ChatDialogRootProps) => {
|
|
56
|
+
const [size, setSize] = useState<Size>('min-content');
|
|
57
|
+
const [open, setOpen] = useControlledState<boolean>(_open, onOpenChange);
|
|
58
|
+
const [expanded, setExpanded] = useControlledState<boolean>(_expanded, onExpandedChange);
|
|
59
|
+
|
|
60
|
+
// NOTE: We set the min size to 5rem (80px), and the header and prompt bar to 40px (i.e., the rail-size) each.
|
|
61
|
+
// 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.
|
|
62
|
+
return (
|
|
63
|
+
<ChatDialogContextProvider
|
|
64
|
+
open={open}
|
|
65
|
+
setOpen={setOpen}
|
|
66
|
+
expanded={expanded}
|
|
67
|
+
setExpanded={setExpanded}
|
|
68
|
+
size={size}
|
|
69
|
+
setSize={setSize}
|
|
70
|
+
>
|
|
71
|
+
<Dialog.Root modal={false} open={open} onOpenChange={setOpen}>
|
|
72
|
+
<div role='none' className='dx-dialog__overlay bg-transparent pointer-events-none' data-block-align='end'>
|
|
73
|
+
<Dialog.Content
|
|
74
|
+
inOverlayLayout
|
|
75
|
+
classNames={[
|
|
76
|
+
'grid grid-rows-[var(--rail-action)_1fr_min-content] p-0 overflow-hidden',
|
|
77
|
+
'box-content md:is-[35rem] md:max-is-none pointer-events-auto',
|
|
78
|
+
]}
|
|
79
|
+
onEscapeKeyDown={onEscape}
|
|
80
|
+
onInteractOutside={preventDefault}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</Dialog.Content>
|
|
84
|
+
</div>
|
|
85
|
+
</Dialog.Root>
|
|
86
|
+
</ChatDialogContextProvider>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
ChatDialogRoot.displayName = 'ChatDialog.Root';
|
|
91
|
+
|
|
92
|
+
//
|
|
93
|
+
// Header
|
|
94
|
+
//
|
|
95
|
+
|
|
96
|
+
type ChatDialogHeaderProps = ThemedClassName<{
|
|
97
|
+
title?: string;
|
|
98
|
+
}>;
|
|
99
|
+
|
|
100
|
+
const ChatDialogHeader = ({ classNames, title }: ChatDialogHeaderProps) => {
|
|
101
|
+
const { expanded, setExpanded } = useChatDialogContext(ChatDialogHeader.displayName);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className={mx('grid grid-cols-[var(--rail-action)_1fr_min-content] items-center overflow-hidden', classNames)}>
|
|
105
|
+
<Endcap>
|
|
106
|
+
<Dialog.Close>
|
|
107
|
+
<Icon icon='ph--x--regular' />
|
|
108
|
+
</Dialog.Close>
|
|
109
|
+
</Endcap>
|
|
110
|
+
<Dialog.Title
|
|
111
|
+
classNames='flex w-full justify-center text-sm text-subdued select-none cursor-pointer'
|
|
112
|
+
onClick={() => setExpanded((expanded) => !expanded)}
|
|
113
|
+
>
|
|
114
|
+
{title}
|
|
115
|
+
</Dialog.Title>
|
|
116
|
+
<Endcap>
|
|
117
|
+
<IconButton
|
|
118
|
+
variant='ghost'
|
|
119
|
+
icon='ph--caret-up--regular'
|
|
120
|
+
iconOnly
|
|
121
|
+
classNames={mx('!p-1 [&>svg]:transition [&>svg]:duration-200', expanded && '[&>svg]:rotate-180')}
|
|
122
|
+
label={expanded ? 'Close' : 'Open'}
|
|
123
|
+
onClick={() => setExpanded((expanded) => !expanded)}
|
|
124
|
+
/>
|
|
125
|
+
</Endcap>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
ChatDialogHeader.displayName = 'ChatDialog.Header';
|
|
131
|
+
|
|
132
|
+
//
|
|
133
|
+
// Content
|
|
134
|
+
//
|
|
135
|
+
|
|
136
|
+
type ChatDialogContentProps = ThemedClassName<PropsWithChildren>;
|
|
137
|
+
|
|
138
|
+
const ChatDialogContent = ({ children, classNames }: ChatDialogContentProps) => {
|
|
139
|
+
const { expanded, size, setSize } = useChatDialogContext(ChatDialogContent.displayName);
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
setSize(expanded ? 'min-content' : 0);
|
|
142
|
+
}, [expanded]);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
className={mx(
|
|
147
|
+
'flex flex-col overflow-y-auto border-t border-b border-subduedSeparator',
|
|
148
|
+
'transition-[block-size] ease-in-out duration-0 [&:not([data-dx-resizing="true"])]:duration-200',
|
|
149
|
+
classNames,
|
|
150
|
+
)}
|
|
151
|
+
style={{
|
|
152
|
+
...sizeStyle(size, 'vertical', true),
|
|
153
|
+
maxBlockSize: 'calc(100dvh - env(safe-area-inset-bottom) - env(safe-area-inset-top) - 14rem)',
|
|
154
|
+
}}
|
|
155
|
+
{...resizeAttributes}
|
|
156
|
+
>
|
|
157
|
+
<ResizeHandle
|
|
158
|
+
side='block-start'
|
|
159
|
+
iconPosition='center'
|
|
160
|
+
defaultSize='min-content'
|
|
161
|
+
fallbackSize={0}
|
|
162
|
+
minSize={0}
|
|
163
|
+
onSizeChange={setSize}
|
|
164
|
+
/>
|
|
165
|
+
{children}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
ChatDialogContent.displayName = 'ChatDialog.Content';
|
|
171
|
+
|
|
172
|
+
//
|
|
173
|
+
// Footer
|
|
174
|
+
//
|
|
175
|
+
|
|
176
|
+
type ChatDialogFooterProps = ThemedClassName<PropsWithChildren>;
|
|
177
|
+
|
|
178
|
+
const ChatDialogFooter = ({ children, classNames }: ChatDialogFooterProps) => {
|
|
179
|
+
return <div className={mx(classNames)}>{children}</div>;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
ChatDialogFooter.displayName = 'ChatDialog.Footer';
|
|
183
|
+
|
|
184
|
+
//
|
|
185
|
+
// ChatDialog
|
|
186
|
+
//
|
|
187
|
+
|
|
188
|
+
export const ChatDialog = {
|
|
189
|
+
Root: ChatDialogRoot,
|
|
190
|
+
Header: ChatDialogHeader,
|
|
191
|
+
Content: ChatDialogContent,
|
|
192
|
+
Footer: ChatDialogFooter,
|
|
193
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos-theme';
|
|
6
|
+
|
|
7
|
+
import { type StoryObj, type Meta } from '@storybook/react-vite';
|
|
8
|
+
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
11
|
+
|
|
12
|
+
import { ChatEditor } from './ChatEditor';
|
|
13
|
+
import { type ReferenceData } from './references';
|
|
14
|
+
import { translations } from '../../translations';
|
|
15
|
+
|
|
16
|
+
const meta: Meta<typeof ChatEditor> = {
|
|
17
|
+
title: 'ui/react-ui-chat/ChatEditor',
|
|
18
|
+
component: ChatEditor,
|
|
19
|
+
decorators: [withTheme],
|
|
20
|
+
parameters: {
|
|
21
|
+
layout: 'centered',
|
|
22
|
+
translations,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
|
|
28
|
+
const classNames = 'w-[20rem] p-2 border border-separator';
|
|
29
|
+
|
|
30
|
+
type Story = StoryObj<typeof ChatEditor>;
|
|
31
|
+
|
|
32
|
+
export const Default: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
classNames,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const WithSuggestions: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
classNames,
|
|
41
|
+
onSubmit: (text) => {
|
|
42
|
+
log('onSubmit', { text });
|
|
43
|
+
},
|
|
44
|
+
onSuggest: (text) => {
|
|
45
|
+
const trimmed = text.trim().toLowerCase();
|
|
46
|
+
if (trimmed.length < 2) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const suggestions = [
|
|
51
|
+
'Create a kanban board',
|
|
52
|
+
'Create a new project',
|
|
53
|
+
'Find flights to Tokyo',
|
|
54
|
+
"Let's play chess",
|
|
55
|
+
'Show me Paris on a map',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return suggestions.filter((s) => s.toLowerCase().startsWith(text));
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const references: ReferenceData[] = [
|
|
64
|
+
{
|
|
65
|
+
uri: 'dxn:echo:@:AAAAAAAA',
|
|
66
|
+
label: 'Meeting Notes',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
uri: 'dxn:echo:@:BBBBBBBB',
|
|
70
|
+
label: 'Project Plan',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
uri: 'dxn:echo:@:CCCCCCCC',
|
|
74
|
+
label: 'Meeting Plan',
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export const WithReferences: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
classNames,
|
|
81
|
+
references: {
|
|
82
|
+
provider: {
|
|
83
|
+
getReferences: async ({ query }) => {
|
|
84
|
+
const res = references.filter((i) => i.label.toLowerCase().startsWith(query.toLowerCase()));
|
|
85
|
+
log('getReferences', { query, res });
|
|
86
|
+
return res;
|
|
87
|
+
},
|
|
88
|
+
resolveReference: async ({ uri }) => {
|
|
89
|
+
const res = references.find((i) => i.uri === uri);
|
|
90
|
+
log('resolveReference', { uri, res });
|
|
91
|
+
return res ?? null;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import React, { forwardRef, useImperativeHandle } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type ThemedClassName, useThemeContext } from '@dxos/react-ui';
|
|
9
|
+
import {
|
|
10
|
+
type BasicExtensionsOptions,
|
|
11
|
+
type UseTextEditorProps,
|
|
12
|
+
createBasicExtensions,
|
|
13
|
+
createThemeExtensions,
|
|
14
|
+
useTextEditor,
|
|
15
|
+
} from '@dxos/react-ui-editor';
|
|
16
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
17
|
+
import { isNonNullable } from '@dxos/util';
|
|
18
|
+
|
|
19
|
+
import { autocomplete, type AutocompleteOptions } from './autocomplete';
|
|
20
|
+
import { references as referencesExtension, type ReferencesOptions } from './references';
|
|
21
|
+
|
|
22
|
+
// TODO(burdon): Handle object references.
|
|
23
|
+
|
|
24
|
+
export interface ChatEditorController {
|
|
25
|
+
focus(): void;
|
|
26
|
+
setText(text: string): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ChatEditorProps = ThemedClassName<
|
|
30
|
+
{
|
|
31
|
+
extensions?: Extension;
|
|
32
|
+
references?: ReferencesOptions;
|
|
33
|
+
} & AutocompleteOptions &
|
|
34
|
+
Pick<UseTextEditorProps, 'autoFocus'> &
|
|
35
|
+
Pick<BasicExtensionsOptions, 'lineWrapping' | 'placeholder'>
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
export const ChatEditor = forwardRef<ChatEditorController, ChatEditorProps>(
|
|
39
|
+
(
|
|
40
|
+
{ classNames, extensions, references, autoFocus, lineWrapping = false, placeholder, onSubmit, onSuggest, onCancel },
|
|
41
|
+
forwardRef,
|
|
42
|
+
) => {
|
|
43
|
+
const { themeMode } = useThemeContext();
|
|
44
|
+
const { parentRef, view } = useTextEditor(
|
|
45
|
+
{
|
|
46
|
+
debug: true,
|
|
47
|
+
autoFocus,
|
|
48
|
+
extensions: [
|
|
49
|
+
createThemeExtensions({ themeMode }),
|
|
50
|
+
autocomplete({ onSubmit, onSuggest, onCancel }),
|
|
51
|
+
references ? referencesExtension({ provider: references.provider }) : [],
|
|
52
|
+
createBasicExtensions({
|
|
53
|
+
bracketMatching: false,
|
|
54
|
+
lineWrapping,
|
|
55
|
+
placeholder,
|
|
56
|
+
}),
|
|
57
|
+
extensions,
|
|
58
|
+
].filter(isNonNullable),
|
|
59
|
+
},
|
|
60
|
+
[themeMode, extensions, onSubmit, onSuggest],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Expose editor view.
|
|
64
|
+
useImperativeHandle(
|
|
65
|
+
forwardRef,
|
|
66
|
+
() => {
|
|
67
|
+
return {
|
|
68
|
+
focus: () => {
|
|
69
|
+
view?.focus();
|
|
70
|
+
},
|
|
71
|
+
setText: (text: string) => {
|
|
72
|
+
view?.dispatch({
|
|
73
|
+
changes: {
|
|
74
|
+
from: 0,
|
|
75
|
+
to: view.state.doc.length,
|
|
76
|
+
insert: text,
|
|
77
|
+
},
|
|
78
|
+
selection: {
|
|
79
|
+
anchor: text.length,
|
|
80
|
+
head: text.length,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
[view, onSubmit],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return <div ref={parentRef} className={mx('is-full', classNames)} />;
|
|
90
|
+
},
|
|
91
|
+
);
|