@@ -5,25 +5,23 @@ import { useLocation, useNavigate } from 'react-router-dom'
55import { useI18n } from 'twake-i18n'
66
77import { useClient } from 'cozy-client'
8- import { extractText , chatCompletion } from 'cozy-client/dist/models/ai'
8+ import { chatCompletion } from 'cozy-client/dist/models/ai'
99import { fetchBlobFileById } from 'cozy-client/dist/models/file'
1010import flag from 'cozy-flags'
1111import 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'
1912import Paper from 'cozy-ui/transpiled/react/Paper'
2013import Stack from 'cozy-ui/transpiled/react/Stack'
21- import Typography from 'cozy-ui/transpiled/react/Typography'
2214import { 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'
2424import { SUMMARY_SYSTEM_PROMPT , getSummaryUserPrompt } from './prompts'
25- import styles from './styles.styl'
26- import { roughTokensEstimation } from '../../helpers'
2725import { useViewer } from '../../providers/ViewerProvider'
2826
2927const 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
266188AIAssistantPanel . 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
278192export default AIAssistantPanel
0 commit comments