Skip to content

Commit dbd01ba

Browse files
committed
feat: Implement file mention feature for assistant
1 parent 4bf57e1 commit dbd01ba

13 files changed

Lines changed: 465 additions & 14 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ coverage
55
dist
66
docs/build
77
node_modules
8-
.worktrees
98

109
# Yarn
1110
.pnp.*
@@ -15,3 +14,6 @@ node_modules
1514
!.yarn/releases
1615
!.yarn/sdks
1716
!.yarn/versions
17+
18+
# Worktrees
19+
.worktrees

packages/cozy-search/src/components/Conversations/ConversationBar.jsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ComposerPrimitive } from '@assistant-ui/react'
22
import cx from 'classnames'
3-
import React, { useRef } from 'react'
3+
import React, { useEffect, useRef, useState } from 'react'
44

55
import Button from 'cozy-ui/transpiled/react/Buttons'
66
import Icon from 'cozy-ui/transpiled/react/Icon'
@@ -12,6 +12,8 @@ import useEventListener from 'cozy-ui/transpiled/react/hooks/useEventListener'
1212
import { useBreakpoints } from 'cozy-ui/transpiled/react/providers/Breakpoints'
1313
import { useI18n } from 'twake-i18n'
1414

15+
import { useFileMention } from './FileMentionContext'
16+
import FileMentionMenu from './FileMentionMenu'
1517
import styles from './styles.styl'
1618

1719
const ConversationBar = ({
@@ -21,11 +23,29 @@ const ConversationBar = ({
2123
onKeyDown,
2224
onSend,
2325
onCancel,
26+
onChange,
27+
composerRuntime,
2428
...props
2529
}) => {
2630
const { t } = useI18n()
2731
const { isMobile } = useBreakpoints()
2832
const inputRef = useRef()
33+
const containerRef = useRef()
34+
const { handleInputChange, hasMention, mentionSearchTerm, menuKeyDownRef } =
35+
useFileMention()
36+
const [anchorEl, setAnchorEl] = useState(null)
37+
38+
useEffect(() => {
39+
setAnchorEl(hasMention ? containerRef.current : null)
40+
}, [hasMention])
41+
42+
const handleChange = e => {
43+
const newValue = e.target.value
44+
handleInputChange(newValue)
45+
if (onChange) {
46+
onChange(e)
47+
}
48+
}
2949

3050
// to adjust input height for multiline when typing in it
3151
// eslint-disable-next-line react-hooks/refs
@@ -46,13 +66,17 @@ const ConversationBar = ({
4666
}
4767

4868
const handleKeyDown = e => {
69+
if (hasMention && menuKeyDownRef.current) {
70+
menuKeyDownRef.current(e)
71+
if (e.defaultPrevented) return
72+
}
4973
if (isEmpty) return
5074

5175
onKeyDown(e)
5276
}
5377

5478
return (
55-
<div className="u-w-100 u-maw-7 u-mh-auto">
79+
<div className="u-w-100 u-maw-7 u-mh-auto" ref={containerRef}>
5680
<SearchBar
5781
{...props}
5882
className={cx(styles['conversationBar'], {
@@ -62,6 +86,7 @@ const ConversationBar = ({
6286
size="auto"
6387
placeholder={t('assistant.search.placeholder')}
6488
value={value}
89+
onChange={handleChange}
6590
disabledClear
6691
componentsProps={{
6792
inputBase: {
@@ -102,6 +127,13 @@ const ConversationBar = ({
102127
}
103128
}}
104129
/>
130+
{hasMention && (
131+
<FileMentionMenu
132+
anchorEl={anchorEl}
133+
searchTerm={mentionSearchTerm}
134+
composerRuntime={composerRuntime}
135+
/>
136+
)}
105137
</div>
106138
)
107139
}

packages/cozy-search/src/components/Conversations/ConversationComposer.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { useBreakpoints } from 'cozy-ui/transpiled/react/providers/Breakpoints'
1313
import ConversationBar from './ConversationBar'
1414
import AssistantSelection from '../Assistant/AssistantSelection'
1515
import { useAssistant } from '../AssistantProvider'
16+
import FileChipsList from './FileChipsList'
17+
import { useFileMention } from './FileMentionContext'
1618
import TwakeKnowledgeSelector from '../TwakeKnowledges/TwakeKnowledgeSelector'
1719

1820
const ConversationComposer = () => {
@@ -21,13 +23,15 @@ const ConversationComposer = () => {
2123
const isRunning = useThread(state => state.isRunning)
2224
const isThreadEmpty = useThread(state => state.messages.length === 0)
2325
const { setOpenedKnowledgePanel } = useAssistant()
26+
const { reset: resetFileMention } = useFileMention()
2427

2528
const value = useComposer(state => state.text)
2629
const isEmpty = useComposer(state => state.isEmpty)
2730

2831
const handleSend = useCallback(() => {
2932
composerRuntime.send()
30-
}, [composerRuntime])
33+
resetFileMention()
34+
}, [composerRuntime, resetFileMention])
3135

3236
const handleCancel = useCallback(() => {
3337
composerRuntime.cancel()
@@ -64,8 +68,11 @@ const ConversationComposer = () => {
6468
onKeyDown={handleKeyDown}
6569
onCancel={handleCancel}
6670
onSend={handleSend}
71+
composerRuntime={composerRuntime}
6772
/>
6873

74+
<FileChipsList />
75+
6976
<div className="u-flex u-flex-items-center u-flex-justify-between u-mt-1">
7077
{flag('cozy.assistant.create-assistant.enabled') && (
7178
<AssistantSelection disabled={!isThreadEmpty} />
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react'
2+
3+
import Chip from 'cozy-ui/transpiled/react/Chips'
4+
import Icon from 'cozy-ui/transpiled/react/Icon'
5+
6+
import { useFileMention } from './FileMentionContext'
7+
8+
const FileChipsList = () => {
9+
const { selectedFiles, removeFile } = useFileMention()
10+
11+
if (selectedFiles.length === 0) return null
12+
13+
return (
14+
<div className="u-flex u-flex-wrap u-mt-half">
15+
{selectedFiles.map(file => (
16+
<Chip
17+
key={file.id}
18+
label={file.name}
19+
icon={
20+
file.icon?.component ? (
21+
<Icon icon={file.icon.component} size={16} />
22+
) : undefined
23+
}
24+
onDelete={() => removeFile(file.id)}
25+
className="u-mr-half u-mb-half"
26+
size="small"
27+
/>
28+
))}
29+
</div>
30+
)
31+
}
32+
33+
export default FileChipsList
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React, {
2+
createContext,
3+
useCallback,
4+
useContext,
5+
useMemo,
6+
useRef,
7+
useState
8+
} from 'react'
9+
10+
import { MENTION_MATCH_REGEX, MENTION_REPLACE_REGEX } from './mentionConstants'
11+
12+
const FileMentionContext = createContext(null)
13+
14+
export const FileMentionProvider = ({ children }) => {
15+
const [selectedFiles, setSelectedFiles] = useState([])
16+
const [inputValue, setInputValue] = useState('')
17+
const menuKeyDownRef = useRef(null)
18+
19+
const addFile = useCallback(file => {
20+
setSelectedFiles(prev => {
21+
if (prev.some(f => f.id === file.id)) return prev
22+
return [...prev, file]
23+
})
24+
}, [])
25+
26+
const removeFile = useCallback(fileId => {
27+
setSelectedFiles(prev => prev.filter(f => f.id !== fileId))
28+
}, [])
29+
30+
const reset = useCallback(() => {
31+
setSelectedFiles([])
32+
setInputValue('')
33+
}, [])
34+
35+
const removeMentionText = useCallback(() => {
36+
setInputValue(prev => prev.replace(MENTION_REPLACE_REGEX, '$1'))
37+
}, [])
38+
39+
const handleInputChange = useCallback(newValue => {
40+
setInputValue(newValue)
41+
}, [])
42+
43+
const mentionMatch = inputValue.match(MENTION_MATCH_REGEX)
44+
const hasMention = mentionMatch !== null
45+
const mentionSearchTerm = mentionMatch ? mentionMatch[2] : ''
46+
47+
const getAttachmentsIDs = useCallback(
48+
() => selectedFiles.map(f => f.id),
49+
[selectedFiles]
50+
)
51+
52+
const value = useMemo(
53+
() => ({
54+
selectedFiles,
55+
inputValue,
56+
addFile,
57+
removeFile,
58+
reset,
59+
removeMentionText,
60+
handleInputChange,
61+
hasMention,
62+
mentionSearchTerm,
63+
getAttachmentsIDs,
64+
menuKeyDownRef
65+
}),
66+
[
67+
selectedFiles,
68+
inputValue,
69+
addFile,
70+
removeFile,
71+
reset,
72+
removeMentionText,
73+
handleInputChange,
74+
hasMention,
75+
mentionSearchTerm,
76+
getAttachmentsIDs
77+
]
78+
)
79+
80+
return (
81+
<FileMentionContext.Provider value={value}>
82+
{children}
83+
</FileMentionContext.Provider>
84+
)
85+
}
86+
87+
export const useFileMention = () => {
88+
const context = useContext(FileMentionContext)
89+
if (!context) {
90+
throw new Error('useFileMention must be used within FileMentionProvider')
91+
}
92+
return context
93+
}

0 commit comments

Comments
 (0)