Skip to content

Commit cb9b3cd

Browse files
lethemanhlethemanh
authored andcommitted
feat: Implement summarization by AI ✨
1 parent 364fcdd commit cb9b3cd

File tree

11 files changed

+413
-13
lines changed

11 files changed

+413
-13
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import cx from 'classnames'
2+
import PropTypes from 'prop-types'
3+
import React, { useCallback, useState, useEffect } from 'react'
4+
5+
import Button from 'cozy-ui/transpiled/react/Buttons'
6+
import Icon from 'cozy-ui/transpiled/react/Icon'
7+
import IconButton from 'cozy-ui/transpiled/react/IconButton'
8+
import AssistantIcon from 'cozy-ui/transpiled/react/Icons/Assistant'
9+
import CopyIcon from 'cozy-ui/transpiled/react/Icons/Copy'
10+
import CrossMediumIcon from 'cozy-ui/transpiled/react/Icons/CrossMedium'
11+
import RefreshIcon from 'cozy-ui/transpiled/react/Icons/Refresh'
12+
import Paper from 'cozy-ui/transpiled/react/Paper'
13+
import Stack from 'cozy-ui/transpiled/react/Stack'
14+
import Typography from 'cozy-ui/transpiled/react/Typography'
15+
import Alerter from 'cozy-ui/transpiled/react/deprecated/Alerter'
16+
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
17+
18+
import { summarizeFile } from '../../helpers/aiApi'
19+
import { useViewer } from '../../providers/ViewerProvider'
20+
21+
const loaderStyles = {
22+
container: {
23+
position: 'relative',
24+
overflow: 'hidden',
25+
height: '3px',
26+
width: '100%',
27+
backgroundColor: 'var(--dividerColor)',
28+
marginTop: '0px'
29+
},
30+
bar: {
31+
position: 'absolute',
32+
top: 0,
33+
left: 0,
34+
height: '100%',
35+
width: '100%',
36+
background:
37+
'linear-gradient(90deg, #00B8D4 0%, #667EEA 25%, #F093FB 50%, #F5576C 75%, #FDB813 100%)',
38+
animation: 'aiLoaderSlide 2s linear infinite'
39+
}
40+
}
41+
42+
const AIAssistantPanel = () => {
43+
const { t } = useI18n()
44+
const { file, setIsOpenAiAssistant } = useViewer()
45+
46+
const [isLoading, setIsLoading] = useState(true)
47+
const [summary, setSummary] = useState('')
48+
const [error, setError] = useState(null)
49+
50+
const handleClose = () => {
51+
setIsOpenAiAssistant(false)
52+
}
53+
54+
const fetchSummary = useCallback(async () => {
55+
setIsLoading(true)
56+
setError(null)
57+
try {
58+
const response = await summarizeFile({ file, stream: false })
59+
if (response && response.content) {
60+
setSummary(response.content)
61+
} else if (response && response.choices && response.choices[0]) {
62+
setSummary(response.choices[0].message.content)
63+
}
64+
} catch (err) {
65+
setError(err.message || 'Failed to generate summary')
66+
Alerter.error(t('Viewer.ai.error.summary'))
67+
} finally {
68+
setIsLoading(false)
69+
}
70+
}, [file, t])
71+
72+
const handleRefresh = () => {
73+
fetchSummary()
74+
}
75+
76+
const handleCopy = () => {
77+
if (summary) {
78+
navigator.clipboard.writeText(summary)
79+
Alerter.success(t('Viewer.ai.copied'))
80+
}
81+
}
82+
83+
useEffect(() => {
84+
fetchSummary()
85+
}, [fetchSummary])
86+
87+
return (
88+
<>
89+
<style>
90+
{`
91+
@keyframes aiLoaderSlide {
92+
0% {
93+
transform: translateX(-100%);
94+
}
95+
100% {
96+
transform: translateX(100%);
97+
}
98+
}
99+
`}
100+
</style>
101+
<Stack spacing="s" className={cx('u-flex u-flex-column u-h-100')}>
102+
<Paper
103+
className={cx({
104+
'u-flex-grow-1': !isLoading
105+
})}
106+
elevation={2}
107+
square
108+
>
109+
<div className="u-flex u-flex-items-center u-flex-justify-between u-h-3 u-ph-1 u-flex-shrink-0">
110+
<Typography variant="h4">
111+
<Icon icon={AssistantIcon} /> Ai assistant
112+
</Typography>
113+
<IconButton aria-label="Close AI Assistant" onClick={handleClose}>
114+
<Icon icon={CrossMediumIcon} />
115+
</IconButton>
116+
</div>
117+
{!isLoading && (
118+
<Stack spacing="s" className="u-ph-1">
119+
<div>
120+
<div className="u-flex u-flex-items-center u-flex-justify-between u-mb-1">
121+
<Typography variant="subtitle1">Summary</Typography>
122+
<div className="u-flex">
123+
<IconButton size="small" onClick={handleRefresh}>
124+
<Icon icon={RefreshIcon} />
125+
</IconButton>
126+
<IconButton size="small" onClick={handleCopy}>
127+
<Icon icon={CopyIcon} />
128+
</IconButton>
129+
</div>
130+
</div>
131+
<Typography className="u-mb-1">
132+
{error ? (
133+
<span style={{ color: 'var(--errorColor)' }}>{error}</span>
134+
) : (
135+
summary
136+
)}
137+
</Typography>
138+
<Typography variant="caption" color="textSecondary">
139+
This content is generated by AI and may contain errors.
140+
</Typography>
141+
</div>
142+
</Stack>
143+
)}
144+
</Paper>
145+
{isLoading ? (
146+
<>
147+
<div style={loaderStyles.container}>
148+
<div style={loaderStyles.bar} />
149+
</div>
150+
<div className="u-flex u-flex-items-center u-flex-justify-between u-ph-1">
151+
<Typography
152+
variant="body1"
153+
className="u-flex u-flex-items-center"
154+
>
155+
<Icon
156+
icon={AssistantIcon}
157+
color="var(--primaryColor)"
158+
className="u-mr-1"
159+
/>
160+
Summarizing content
161+
</Typography>
162+
<Button
163+
size="small"
164+
variant="text"
165+
color="default"
166+
label="Stop"
167+
onClick={handleClose}
168+
/>
169+
</div>
170+
</>
171+
) : null}
172+
</Stack>
173+
</>
174+
)
175+
}
176+
177+
AIAssistantPanel.propTypes = {
178+
isLoading: PropTypes.bool,
179+
summary: PropTypes.string,
180+
onStop: PropTypes.func,
181+
onSend: PropTypes.func
182+
}
183+
184+
AIAssistantPanel.defaultProps = {
185+
isLoading: false,
186+
summary: ''
187+
}
188+
189+
export default AIAssistantPanel

packages/cozy-viewer/src/ViewerInformationsWrapper.jsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { useCozyTheme } from 'cozy-ui/transpiled/react/providers/CozyTheme'
66
import { useTheme } from 'cozy-ui/transpiled/react/styles'
77

88
import FooterContent from './Footer/FooterContent'
9+
import AIAssistantPanel from './Panel/AI/AIAssistantPanel'
910
import PanelContent from './Panel/PanelContent'
1011
import Footer from './components/Footer'
1112
import InformationPanel from './components/InformationPanel'
13+
import { useViewer } from './providers/ViewerProvider'
1214

1315
const ViewerInformationsWrapper = ({
1416
disableFooter,
@@ -18,6 +20,7 @@ const ViewerInformationsWrapper = ({
1820
}) => {
1921
const theme = useTheme()
2022
const { isLight } = useCozyTheme()
23+
const { isOpenAiAssistant } = useViewer()
2124
const sidebar = document.querySelector('[class*="sidebar"]')
2225

2326
useSetFlagshipUI(
@@ -32,15 +35,23 @@ const ViewerInformationsWrapper = ({
3235

3336
return (
3437
<>
35-
{!disableFooter && (
36-
<Footer>
37-
<FooterContent toolbarRef={toolbarRef}>{children}</FooterContent>
38-
</Footer>
39-
)}
40-
{validForPanel && (
38+
{isOpenAiAssistant ? (
4139
<InformationPanel>
42-
<PanelContent />
40+
<AIAssistantPanel />
4341
</InformationPanel>
42+
) : (
43+
<>
44+
{!disableFooter && (
45+
<Footer>
46+
<FooterContent toolbarRef={toolbarRef}>{children}</FooterContent>
47+
</Footer>
48+
)}
49+
{validForPanel && (
50+
<InformationPanel>
51+
<PanelContent />
52+
</InformationPanel>
53+
)}
54+
</>
4455
)}
4556
</>
4657
)

packages/cozy-viewer/src/ViewersByFile/PdfJsViewer.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ToolbarButton from '../components/PdfToolbarButton'
1111
import ViewerSpinner from '../components/ViewerSpinner'
1212
import withFileUrl from '../hoc/withFileUrl'
1313
import { withViewerLocales } from '../hoc/withViewerLocales'
14+
import { ViewerContext } from '../providers/ViewerProvider'
1415

1516
export const MIN_SCALE = 0.25
1617
export const MAX_SCALE = 3
@@ -29,6 +30,8 @@ const makeInputPageStyle = nbPages => {
2930
}
3031

3132
export class PdfJsViewer extends Component {
33+
static contextType = ViewerContext
34+
3235
state = {
3336
totalPages: 1,
3437
scale: 1,
@@ -105,6 +108,11 @@ export class PdfJsViewer extends Component {
105108
parseInt(this.props.file.size, 10) <= MAX_SIZE_FILE,
106109
loaded: true
107110
})
111+
112+
// Update page count in ViewerContext for AI summary compatibility check
113+
if (this.context && this.context.setPdfPageCount) {
114+
this.context.setPdfPageCount(numPages)
115+
}
108116
}
109117

110118
onLoadError = error => {

packages/cozy-viewer/src/components/Toolbar.jsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
33
import React from 'react'
44

55
import { useClient } from 'cozy-client'
6+
import flag from 'cozy-flags'
67
import { useWebviewIntent } from 'cozy-intent'
78
import {
89
OpenSharingLinkButton,
@@ -16,6 +17,7 @@ import Icon from 'cozy-ui/transpiled/react/Icon'
1617
import IconButton from 'cozy-ui/transpiled/react/IconButton'
1718
import DownloadIcon from 'cozy-ui/transpiled/react/Icons/Download'
1819
import PreviousIcon from 'cozy-ui/transpiled/react/Icons/Previous'
20+
import TextIcon from 'cozy-ui/transpiled/react/Icons/Text'
1921
import MidEllipsis from 'cozy-ui/transpiled/react/MidEllipsis'
2022
import Typography from 'cozy-ui/transpiled/react/Typography'
2123
import withBreakpoints from 'cozy-ui/transpiled/react/helpers/withBreakpoints'
@@ -25,6 +27,7 @@ import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
2527
import { ToolbarFilePath } from './ToolbarFilePath'
2628
import styles from './styles.styl'
2729
import { extractChildrenCompByName } from '../Footer/helpers'
30+
import { isFileSummaryCompatible } from '../helpers'
2831
import { useShareModal } from '../providers/ShareModalProvider'
2932
import { useViewer } from '../providers/ViewerProvider'
3033

@@ -36,12 +39,13 @@ const Toolbar = ({
3639
toolbarRef,
3740
breakpoints: { isDesktop },
3841
children,
39-
showFilePath
42+
showFilePath,
43+
onPaywallRedirect
4044
}) => {
4145
const client = useClient()
4246
const { t } = useI18n()
4347
const webviewIntent = useWebviewIntent()
44-
const { file } = useViewer()
48+
const { file, setIsOpenAiAssistant, pdfPageCount } = useViewer()
4549
const { isSharingShortcutCreated, addSharingLink, loading } =
4650
useSharingInfos()
4751
const { isOwner } = useSharingContext()
@@ -56,6 +60,19 @@ const Toolbar = ({
5660
name: 'ToolbarButtons'
5761
})
5862

63+
const isAiAvailable = flag('ai.available')
64+
const isAiEnabled = flag('ai.enabled')
65+
const isSummaryCompatible = isFileSummaryCompatible(file, { pdfPageCount })
66+
const showSummariseButton = isAiAvailable && isSummaryCompatible
67+
68+
const handleSummariseClick = () => {
69+
if (!isAiEnabled && onPaywallRedirect) {
70+
onPaywallRedirect()
71+
} else {
72+
setIsOpenAiAssistant(true)
73+
}
74+
}
75+
5976
return (
6077
<div
6178
ref={toolbarRef}
@@ -103,6 +120,21 @@ const Toolbar = ({
103120
isShortLabel
104121
/>
105122
)}
123+
{showSummariseButton && (
124+
<Button
125+
variant="text"
126+
startIcon={
127+
<Icon
128+
icon={TextIcon}
129+
className={cx(styles['viewer-ai-summarise-btn'])}
130+
/>
131+
}
132+
aria-label={t('Viewer.summriseWithAi')}
133+
label={t('Viewer.summriseWithAi')}
134+
onClick={handleSummariseClick}
135+
className={cx(styles['viewer-ai-summarise-btn'])}
136+
/>
137+
)}
106138
<Button
107139
className="u-white"
108140
variant="text"
@@ -129,7 +161,8 @@ Toolbar.propTypes = {
129161
onMouseEnter: PropTypes.func.isRequired,
130162
onMouseLeave: PropTypes.func.isRequired,
131163
onClose: PropTypes.func,
132-
showFilePath: PropTypes.bool
164+
showFilePath: PropTypes.bool,
165+
onPaywallRedirect: PropTypes.func
133166
}
134167

135168
export default withBreakpoints()(Toolbar)

packages/cozy-viewer/src/components/ViewerControls.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,13 @@ class ViewerControls extends Component {
120120
classes,
121121
isDesktop
122122
} = this.props
123-
const { showToolbar, showClose, toolbarRef, showFilePath } = toolbarProps
123+
const {
124+
showToolbar,
125+
showClose,
126+
toolbarRef,
127+
showFilePath,
128+
onPaywallRedirect
129+
} = toolbarProps
124130
const { hidden } = this.state
125131

126132
return (
@@ -140,6 +146,7 @@ class ViewerControls extends Component {
140146
onMouseEnter={this.showControls}
141147
onMouseLeave={this.hideControls}
142148
onClose={showClose ? onClose : undefined}
149+
onPaywallRedirect={onPaywallRedirect}
143150
>
144151
{children}
145152
</Toolbar>

packages/cozy-viewer/src/components/styles.styl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,8 @@
9191
height $footerHeight
9292
padding-bottom var(--flagship-bottom-height, env(safe-area-inset-bottom))
9393
background var(--paperBackgroundColor)
94+
95+
.viewer-ai-summarise-btn
96+
background linear-gradient(90deg, #00B8D4 0%, #667EEA 50%, #F093FB 100%)
97+
-webkit-text-fill-color transparent
98+
background-clip text

0 commit comments

Comments
 (0)