Skip to content

Commit be35042

Browse files
lethemanhlethemanh
authored andcommitted
feat: Skip extract file content with txt or markdown file ✨
1 parent f66279b commit be35042

File tree

12 files changed

+273
-144
lines changed

12 files changed

+273
-144
lines changed

packages/cozy-viewer/src/Panel/AI/AIAssistantPanel.jsx

Lines changed: 54 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,23 @@ import { useLocation, useNavigate } from 'react-router-dom'
55
import { useI18n } from 'twake-i18n'
66

77
import { useClient } from 'cozy-client'
8-
import { extractText, chatCompletion } from 'cozy-client/dist/models/ai'
8+
import { chatCompletion } from 'cozy-client/dist/models/ai'
99
import { fetchBlobFileById } from 'cozy-client/dist/models/file'
1010
import flag from 'cozy-flags'
1111
import logger from 'cozy-logger'
12-
import Button from 'cozy-ui/transpiled/react/Buttons'
13-
import Icon from 'cozy-ui/transpiled/react/Icon'
14-
import IconButton from 'cozy-ui/transpiled/react/IconButton'
15-
import AssistantIcon from 'cozy-ui/transpiled/react/Icons/Assistant'
16-
import CopyIcon from 'cozy-ui/transpiled/react/Icons/Copy'
17-
import CrossMediumIcon from 'cozy-ui/transpiled/react/Icons/CrossMedium'
18-
import RefreshIcon from 'cozy-ui/transpiled/react/Icons/Refresh'
1912
import Paper from 'cozy-ui/transpiled/react/Paper'
2013
import Stack from 'cozy-ui/transpiled/react/Stack'
21-
import Typography from 'cozy-ui/transpiled/react/Typography'
2214
import { useAlert } from 'cozy-ui/transpiled/react/providers/Alert'
2315

16+
import LoadingState from './LoadingState'
17+
import PanelHeader from './PanelHeader'
18+
import SummaryContent from './SummaryContent'
19+
import {
20+
extractFileContent,
21+
validateContentSize,
22+
getErrorMessage
23+
} from './helpers'
2424
import { SUMMARY_SYSTEM_PROMPT, getSummaryUserPrompt } from './prompts'
25-
import styles from './styles.styl'
26-
import { roughTokensEstimation } from '../../helpers'
2725
import { useViewer } from '../../providers/ViewerProvider'
2826

2927
const AIAssistantPanel = ({ className }) => {
@@ -52,22 +50,10 @@ const AIAssistantPanel = ({ className }) => {
5250
const summarizeFile = async ({ client, file, stream = false, model }) => {
5351
try {
5452
const fileBlob = await fetchBlobFileById(client, file?._id)
53+
const textContent = await extractFileContent(client, fileBlob, file)
5554

56-
const rawTextContent = await extractText(client, fileBlob, {
57-
name: file.name,
58-
mime: file.mime
59-
})
60-
const textContent = rawTextContent ? JSON.stringify(rawTextContent) : ''
61-
62-
const summaryConfig = flag('drive.summary')
63-
if (
64-
summaryConfig?.maxTokens &&
65-
roughTokensEstimation(textContent) > summaryConfig.maxTokens
66-
) {
67-
const error = new Error('DOCUMENT_TOO_LARGE')
68-
error.code = 'DOCUMENT_TOO_LARGE'
69-
throw error
70-
}
55+
const { maxTokens } = flag('drive.summary') ?? {}
56+
validateContentSize(textContent, maxTokens)
7157

7258
const messages = [
7359
{ role: 'system', content: SUMMARY_SYSTEM_PROMPT },
@@ -89,23 +75,22 @@ const AIAssistantPanel = ({ className }) => {
8975
}
9076
}
9177

92-
const persistedSummary = async (
93-
fileMetadata,
94-
targetFileId,
95-
summaryContent
96-
) => {
97-
try {
98-
await client
99-
.collection('io.cozy.files')
100-
.updateMetadataAttribute(targetFileId, {
101-
...fileMetadata,
102-
description: summaryContent
103-
})
104-
fetchedFileIdRef.current = targetFileId
105-
} catch (error) {
106-
logger.error('Error when persisting summary to file metadata:', error)
107-
}
108-
}
78+
const persistedSummary = useCallback(
79+
async (fileMetadata, targetFileId, summaryContent) => {
80+
try {
81+
await client
82+
.collection('io.cozy.files')
83+
.updateMetadataAttribute(targetFileId, {
84+
...fileMetadata,
85+
description: summaryContent
86+
})
87+
fetchedFileIdRef.current = targetFileId
88+
} catch (error) {
89+
logger.error('Error when persisting summary to file metadata:', error)
90+
}
91+
},
92+
[client]
93+
)
10994

11095
useEffect(() => {
11196
activeFileIdRef.current = file?._id || null
@@ -142,11 +127,7 @@ const AIAssistantPanel = ({ className }) => {
142127
await persistedSummary(fileMetadata, targetFileId, summaryContent)
143128
} catch (err) {
144129
if (activeFileIdRef.current === targetFileId) {
145-
const errorMessage =
146-
err.code === 'DOCUMENT_TOO_LARGE'
147-
? t('Viewer.ai.error.documentTooLarge')
148-
: t('Viewer.ai.error.summary')
149-
setError(errorMessage)
130+
setError(getErrorMessage(err, t))
150131
}
151132
} finally {
152133
if (inFlightFileIdRef.current === targetFileId) {
@@ -157,7 +138,7 @@ const AIAssistantPanel = ({ className }) => {
157138
}
158139
}
159140
},
160-
[client, file, t]
141+
[client, file, persistedSummary, t]
161142
)
162143

163144
const handleRefresh = () => {
@@ -177,102 +158,35 @@ const AIAssistantPanel = ({ className }) => {
177158
}, [fetchSummary])
178159

179160
return (
180-
<>
181-
<Stack
182-
spacing="s"
183-
className={cx('u-flex u-flex-column u-h-100', className)}
161+
<Stack
162+
spacing="s"
163+
className={cx('u-flex u-flex-column u-h-100', className)}
164+
>
165+
<Paper
166+
className={cx({
167+
'u-flex-grow-1': !isLoading
168+
})}
169+
elevation={2}
170+
square
184171
>
185-
<Paper
186-
className={cx({
187-
'u-flex-grow-1': !isLoading
188-
})}
189-
elevation={2}
190-
square
191-
>
192-
<div className="u-flex u-flex-items-center u-flex-justify-between u-h-3 u-ph-1 u-flex-shrink-0">
193-
<Typography variant="h4">
194-
<Icon icon={AssistantIcon} /> {t('Viewer.ai.panelTitle')}
195-
</Typography>
196-
<IconButton aria-label="Close AI Assistant" onClick={handleClose}>
197-
<Icon icon={CrossMediumIcon} />
198-
</IconButton>
199-
</div>
200-
{!isLoading && (
201-
<Stack spacing="s" className="u-ph-1">
202-
<div>
203-
<div className="u-flex u-flex-items-center u-flex-justify-between u-mb-1">
204-
<Typography variant="subtitle1">
205-
{t('Viewer.ai.bodyText')}
206-
</Typography>
207-
<div className="u-flex">
208-
<IconButton size="small" onClick={handleRefresh}>
209-
<Icon icon={RefreshIcon} />
210-
</IconButton>
211-
{summary && (
212-
<IconButton size="small" onClick={handleCopy}>
213-
<Icon icon={CopyIcon} />
214-
</IconButton>
215-
)}
216-
</div>
217-
</div>
218-
<Typography className="u-mb-1">
219-
{error ? (
220-
<span style={{ color: 'var(--errorColor)' }}>{error}</span>
221-
) : (
222-
summary
223-
)}
224-
</Typography>
225-
{!isLoading && summary && (
226-
<Typography variant="caption" color="textSecondary">
227-
{t('Viewer.ai.footerText')}
228-
</Typography>
229-
)}
230-
</div>
231-
</Stack>
232-
)}
233-
</Paper>
234-
{isLoading ? (
235-
<>
236-
<div className={styles.loaderContainer}>
237-
<div className={styles.loaderBar} />
238-
</div>
239-
<div className="u-flex u-flex-items-center u-flex-justify-between u-ph-1">
240-
<Typography
241-
variant="body1"
242-
className="u-flex u-flex-items-center"
243-
>
244-
<Icon
245-
icon={AssistantIcon}
246-
color="var(--primaryColor)"
247-
className="u-mr-1"
248-
/>
249-
{t('Viewer.ai.loadingText')}
250-
</Typography>
251-
<Button
252-
size="small"
253-
variant="text"
254-
color="default"
255-
label={t('Viewer.ai.stop')}
256-
onClick={handleClose}
257-
/>
258-
</div>
259-
</>
260-
) : null}
261-
</Stack>
262-
</>
172+
<PanelHeader onClose={handleClose} t={t} />
173+
{!isLoading && (
174+
<SummaryContent
175+
summary={summary}
176+
error={error}
177+
onRefresh={handleRefresh}
178+
onCopy={handleCopy}
179+
t={t}
180+
/>
181+
)}
182+
</Paper>
183+
{isLoading && <LoadingState onStop={handleClose} t={t} />}
184+
</Stack>
263185
)
264186
}
265187

266188
AIAssistantPanel.propTypes = {
267-
isLoading: PropTypes.bool,
268-
summary: PropTypes.string,
269-
onStop: PropTypes.func,
270-
onSend: PropTypes.func
271-
}
272-
273-
AIAssistantPanel.defaultProps = {
274-
isLoading: false,
275-
summary: ''
189+
className: PropTypes.string
276190
}
277191

278192
export default AIAssistantPanel
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import PropTypes from 'prop-types'
2+
import React from 'react'
3+
4+
import Button from 'cozy-ui/transpiled/react/Buttons'
5+
import Icon from 'cozy-ui/transpiled/react/Icon'
6+
import AssistantIcon from 'cozy-ui/transpiled/react/Icons/Assistant'
7+
import Typography from 'cozy-ui/transpiled/react/Typography'
8+
9+
import styles from './styles.styl'
10+
11+
const LoadingState = ({ onStop, t }) => {
12+
return (
13+
<>
14+
<div className={styles.loaderContainer}>
15+
<div className={styles.loaderBar} />
16+
</div>
17+
<div className="u-flex u-flex-items-center u-flex-justify-between u-ph-1">
18+
<Typography variant="body1" className="u-flex u-flex-items-center">
19+
<Icon
20+
icon={AssistantIcon}
21+
color="var(--primaryColor)"
22+
className="u-mr-1"
23+
/>
24+
{t('Viewer.ai.loadingText')}
25+
</Typography>
26+
<Button
27+
size="small"
28+
variant="text"
29+
color="default"
30+
label={t('Viewer.ai.stop')}
31+
onClick={onStop}
32+
/>
33+
</div>
34+
</>
35+
)
36+
}
37+
38+
LoadingState.propTypes = {
39+
onStop: PropTypes.func.isRequired,
40+
t: PropTypes.func.isRequired
41+
}
42+
43+
export default LoadingState
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import PropTypes from 'prop-types'
2+
import React from 'react'
3+
4+
import Icon from 'cozy-ui/transpiled/react/Icon'
5+
import IconButton from 'cozy-ui/transpiled/react/IconButton'
6+
import AssistantIcon from 'cozy-ui/transpiled/react/Icons/Assistant'
7+
import CrossMediumIcon from 'cozy-ui/transpiled/react/Icons/CrossMedium'
8+
import Typography from 'cozy-ui/transpiled/react/Typography'
9+
10+
const PanelHeader = ({ onClose, t }) => {
11+
return (
12+
<div className="u-flex u-flex-items-center u-flex-justify-between u-h-3 u-ph-1 u-flex-shrink-0">
13+
<Typography variant="h4">
14+
<Icon icon={AssistantIcon} /> {t('Viewer.ai.panelTitle')}
15+
</Typography>
16+
<IconButton aria-label="Close AI Assistant" onClick={onClose}>
17+
<Icon icon={CrossMediumIcon} />
18+
</IconButton>
19+
</div>
20+
)
21+
}
22+
23+
PanelHeader.propTypes = {
24+
onClose: PropTypes.func.isRequired,
25+
t: PropTypes.func.isRequired
26+
}
27+
28+
export default PanelHeader
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import PropTypes from 'prop-types'
2+
import React from 'react'
3+
4+
import Icon from 'cozy-ui/transpiled/react/Icon'
5+
import IconButton from 'cozy-ui/transpiled/react/IconButton'
6+
import CopyIcon from 'cozy-ui/transpiled/react/Icons/Copy'
7+
import RefreshIcon from 'cozy-ui/transpiled/react/Icons/Refresh'
8+
import Stack from 'cozy-ui/transpiled/react/Stack'
9+
import Typography from 'cozy-ui/transpiled/react/Typography'
10+
11+
const SummaryContent = ({ summary, error, onRefresh, onCopy, t }) => {
12+
return (
13+
<Stack spacing="s" className="u-ph-1">
14+
<div>
15+
<div className="u-flex u-flex-items-center u-flex-justify-between u-mb-1">
16+
<Typography variant="subtitle1">{t('Viewer.ai.bodyText')}</Typography>
17+
<div className="u-flex">
18+
<IconButton size="small" onClick={onRefresh}>
19+
<Icon icon={RefreshIcon} />
20+
</IconButton>
21+
{summary && (
22+
<IconButton size="small" onClick={onCopy}>
23+
<Icon icon={CopyIcon} />
24+
</IconButton>
25+
)}
26+
</div>
27+
</div>
28+
<Typography className="u-mb-1">
29+
{error ? (
30+
<span style={{ color: 'var(--errorColor)' }}>{error}</span>
31+
) : (
32+
summary
33+
)}
34+
</Typography>
35+
{summary && (
36+
<Typography variant="caption" color="textSecondary">
37+
{t('Viewer.ai.footerText')}
38+
</Typography>
39+
)}
40+
</div>
41+
</Stack>
42+
)
43+
}
44+
45+
SummaryContent.propTypes = {
46+
summary: PropTypes.string,
47+
error: PropTypes.string,
48+
onRefresh: PropTypes.func.isRequired,
49+
onCopy: PropTypes.func.isRequired,
50+
t: PropTypes.func.isRequired
51+
}
52+
53+
export default SummaryContent

0 commit comments

Comments
 (0)