Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cozy-search/src/components/AssistantProvider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface AssistantContextValue {
setSelectedTwakeKnowledge: React.Dispatch<
React.SetStateAction<TwakeKnowledgeState>
>
websearchEnabled: boolean
setWebsearchEnabled: React.Dispatch<React.SetStateAction<boolean>>
}

export function useAssistant(): AssistantContextValue
8 changes: 6 additions & 2 deletions packages/cozy-search/src/components/AssistantProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const AssistantProvider = ({ children }) => {
chat: []
})
const [openedKnowledgePanel, setOpenedKnowledgePanel] = useState(null)
const [websearchEnabled, setWebsearchEnabled] = useState(false)

const value = useMemo(
() => ({
Expand All @@ -50,7 +51,9 @@ const AssistantProvider = ({ children }) => {
setSelectedAssistantId,
setIsOpenSearchConversation,
setOpenedKnowledgePanel,
setSelectedTwakeKnowledge
setSelectedTwakeKnowledge,
websearchEnabled,
setWebsearchEnabled
}),
[
isOpenCreateAssistant,
Expand All @@ -60,7 +63,8 @@ const AssistantProvider = ({ children }) => {
selectedAssistantId,
isOpenSearchConversation,
openedKnowledgePanel,
selectedTwakeKnowledge
selectedTwakeKnowledge,
websearchEnabled
]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useEventListener from 'cozy-ui/transpiled/react/hooks/useEventListener'
import { useBreakpoints } from 'cozy-ui/transpiled/react/providers/Breakpoints'
import { useI18n } from 'twake-i18n'

import WebsearchButton from './WebsearchButton'
import styles from './styles.styl'

const ConversationBar = ({
Expand All @@ -21,6 +22,8 @@ const ConversationBar = ({
onKeyDown,
onSend,
onCancel,
websearchEnabled,
onToggleWebsearch,
...props
}) => {
const { t } = useI18n()
Expand Down Expand Up @@ -74,29 +77,39 @@ const ConversationBar = ({
},
autoFocus: !isMobile,
inputComponent: ComposerPrimitive.Input,
endAdornment: isRunning ? (
<IconButton className="u-p-0 u-mr-half">
<Button
size="small"
component="div"
className="u-miw-auto u-w-2 u-h-2 u-bdrs-circle"
classes={{ label: 'u-flex u-w-auto' }}
label={<Icon icon={StopIcon} size={12} />}
onClick={onCancel}
endAdornment: (
<>
<WebsearchButton
websearchEnabled={websearchEnabled}
onToggleWebsearch={onToggleWebsearch}
/>
</IconButton>
) : (
<IconButton className="u-p-0 u-mr-half">
<Button
size="small"
component="div"
className="u-miw-auto u-w-2 u-h-2 u-bdrs-circle"
classes={{ label: 'u-flex u-w-auto' }}
label={<Icon icon={PaperplaneIcon} size={12} rotate={-45} />}
disabled={isEmpty}
onClick={handleSend}
/>
</IconButton>
{isRunning ? (
<IconButton className="u-p-0 u-mr-half">
<Button
size="small"
component="div"
className="u-miw-auto u-w-2 u-h-2 u-bdrs-circle"
classes={{ label: 'u-flex u-w-auto' }}
label={<Icon icon={StopIcon} size={12} />}
onClick={onCancel}
/>
</IconButton>
) : (
<IconButton className="u-p-0 u-mr-half">
<Button
size="small"
component="div"
className="u-miw-auto u-w-2 u-h-2 u-bdrs-circle"
classes={{ label: 'u-flex u-w-auto' }}
label={
<Icon icon={PaperplaneIcon} size={12} rotate={-45} />
}
disabled={isEmpty}
onClick={handleSend}
/>
</IconButton>
)}
</>
),
onKeyDown: handleKeyDown
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const ConversationComposer = () => {
const composerRuntime = useComposerRuntime()
const isRunning = useThread(state => state.isRunning)
const isThreadEmpty = useThread(state => state.messages.length === 0)
const { setOpenedKnowledgePanel } = useAssistant()
const { setOpenedKnowledgePanel, websearchEnabled, setWebsearchEnabled } =
useAssistant()

const value = useComposer(state => state.text)
const isEmpty = useComposer(state => state.isEmpty)
Expand Down Expand Up @@ -49,6 +50,11 @@ const ConversationComposer = () => {
[isMobile, handleSend]
)

const handleToggleWebsearch = useCallback(() => {
if (isRunning) return
setWebsearchEnabled(prev => !prev)
}, [isRunning, setWebsearchEnabled])

return (
<ComposerPrimitive.Root
className={cx('u-w-100 u-maw-7 u-mh-auto', {
Expand All @@ -64,6 +70,8 @@ const ConversationComposer = () => {
onKeyDown={handleKeyDown}
onCancel={handleCancel}
onSend={handleSend}
websearchEnabled={websearchEnabled}
onToggleWebsearch={handleToggleWebsearch}
/>

<div className="u-flex u-flex-items-center u-flex-justify-between u-mt-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ import { useI18n } from 'twake-i18n'

import EmailSourceItem from './EmailSourceItem'
import FileSourcesItem from './FileSourcesItem'
import WebSourceItem from './WebSourceItem'
import { EMAIL_DOCTYPE, buildFilesByIds } from '../../queries'

const Sources = ({ messageId, files, emails }) => {
const WEB_SOURCE_TYPE = 'web'

const Sources = ({ messageId, files, emails, urls }) => {
const [showSources, setShowSources] = useState(false)
const { t } = useI18n()
const ref = useRef()
Expand Down Expand Up @@ -47,7 +50,10 @@ const Sources = ({ messageId, files, emails }) => {
<Chip
className="u-mb-1"
icon={<Icon icon={MultiFilesIcon} className="u-ml-half" />}
label={t('assistant.sources', files.length + emails.length)}
label={t(
'assistant.sources',
files.length + emails.length + urls.length
)}
deleteIcon={
<Icon
className="u-h-1"
Expand All @@ -72,6 +78,12 @@ const Sources = ({ messageId, files, emails }) => {
{emails.map(email => (
<EmailSourceItem key={`${messageId}-${email.id}`} email={email} />
))}
{urls?.map((url, index) => (
<WebSourceItem
key={`${messageId}-${url.url || index}`}
source={url}
/>
))}
</div>
</Grow>
</Box>
Expand All @@ -81,11 +93,16 @@ const Sources = ({ messageId, files, emails }) => {
const SourcesWithFilesQuery = ({ messageId, sources }) => {
const fileIds = []
const emails = []
const urls = []
let files
sources.map(source => {
source.doctype === EMAIL_DOCTYPE
? emails.push(source)
: fileIds.push(source.id)
if (source.sourceType === WEB_SOURCE_TYPE) {
urls.push(source)
} else if (source.doctype === EMAIL_DOCTYPE) {
emails.push(source)
} else {
fileIds.push(source.id)
}
})
const enabled = fileIds && fileIds.length > 0
const filesByIds = buildFilesByIds(fileIds, enabled)
Expand All @@ -97,10 +114,15 @@ const SourcesWithFilesQuery = ({ messageId, sources }) => {
const isLoading = isQueryLoading(queryResult)
files = fetchedFiles || []

if ((isLoading && enabled) || (files.length === 0 && emails.length === 0))
if (
(isLoading && enabled) ||
(files.length === 0 && emails.length === 0 && urls.length === 0)
)
return null

return <Sources messageId={messageId} files={files} emails={emails} />
return (
<Sources messageId={messageId} files={files} emails={emails} urls={urls} />
)
}

export default SourcesWithFilesQuery
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react'

import Icon from 'cozy-ui/transpiled/react/Icon'
import GlobeIcon from 'cozy-ui/transpiled/react/Icons/Globe'
import ListItem from 'cozy-ui/transpiled/react/ListItem'
import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
import ListItemText from 'cozy-ui/transpiled/react/ListItemText'

import styles from './styles.styl'

const isHttpUrl = url =>
typeof url === 'string' && /^https?:\/\//i.test(url.trim())

const WebSourceItem = ({ source }) => {
const { url, title } = source

if (!isHttpUrl(url)) return null

const displayTitle = title || url
Comment thread
paultranvan marked this conversation as resolved.

return (
<ListItem
className={styles['sourcesItem']}
component="a"
href={url}
target="_blank"
rel="noopener noreferrer"
button
>
<ListItemIcon>
<Icon icon={GlobeIcon} size={32} color="var(--primaryColor)" />
</ListItemIcon>
<ListItemText primary={displayTitle} secondary={url} />
</ListItem>
)
}

export default WebSourceItem
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'

import flag from 'cozy-flags'
import Button from 'cozy-ui/transpiled/react/Buttons'
import Icon from 'cozy-ui/transpiled/react/Icon'
import IconButton from 'cozy-ui/transpiled/react/IconButton'
import GlobeIcon from 'cozy-ui/transpiled/react/Icons/Globe'
import { useI18n } from 'twake-i18n'

const WebsearchButton = ({ websearchEnabled, onToggleWebsearch }) => {
const { t } = useI18n()

if (!flag('cozy.assistant.websearch.enabled')) {
return null
}

return (
<IconButton
className="u-p-0 u-mr-half"
aria-pressed={websearchEnabled}
aria-label={t('assistant.websearch.label')}
title={t('assistant.websearch.label')}
onClick={onToggleWebsearch}
>
<Button
size="small"
component="div"
className="u-miw-auto u-w-2 u-h-2 u-bdrs-circle"
classes={{ label: 'u-flex u-w-auto' }}
disabled={!websearchEnabled}
label={<Icon icon={GlobeIcon} size={12} />}
/>
</IconButton>
)
}

export default WebsearchButton
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@ interface ConversationMessage {
id: string
role: 'user' | 'assistant'
content: string
sources?: Array<{ id: string; doctype?: string }>
sources?: Array<{
id?: string
doctype?: string
sourceType?: string
url?: string
title?: string
snippet?: string
}>
}

interface Conversation {
Expand Down Expand Up @@ -143,7 +150,7 @@ const CozyAssistantRuntimeProviderInner = ({
const messagesIdRef = useRef<string[]>([])
const cancelledMessageIdsRef = useRef<Set<string>>(new Set())
const currentStreamingMessageIdRef = useRef<string | null>(null)
const { selectedAssistantId } = useAssistant()
const { selectedAssistantId, websearchEnabled } = useAssistant()

useEffect(() => {
messagesIdRef.current = initialMessages
Expand Down Expand Up @@ -225,7 +232,14 @@ const CozyAssistantRuntimeProviderInner = ({
| {
_id: string
object: 'sources'
content: Array<{ id: string; doctype?: string }>
content: Array<{
id?: string
doctype?: string
sourceType?: string
url?: string
title?: string
snippet?: string
}>
}
| { _id: string; object: 'error'; message: string }
) => {
Expand Down Expand Up @@ -293,13 +307,14 @@ const CozyAssistantRuntimeProviderInner = ({
typeof createCozyRealtimeChatAdapter
>[0]['client'],
conversationId,
// eslint-disable-next-line react-hooks/refs
streamBridge: streamBridgeRef.current,
assistantId: selectedAssistantId
assistantId: selectedAssistantId,
websearchEnabled
},
t
t,
// eslint-disable-next-line react-hooks/refs -- streamBridgeRef is stable and only read inside adapter.run(), not during render
streamBridgeRef
),
[client, conversationId, selectedAssistantId, t]
[client, conversationId, selectedAssistantId, websearchEnabled, t]
)

const runtime = useLocalRuntime(adapter, {
Expand Down
Loading
Loading