Extrapolate.jsx
8830 Chars | 323 Lines | 8.84 kb | Text File
Parent Directory
import React, { useState, useEffect, useRef, useContext, useReducer } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { usePack } from '@/stores/pack'
import { Experience } from '../Experience'
import Table from 'react-bootstrap/Table'
import Button from 'react-bootstrap/Button'
import Form from 'react-bootstrap/Form'
import FloatingLabel from 'react-bootstrap/FloatingLabel'
import Accordion from 'react-bootstrap/Accordion'
import Alert from 'react-bootstrap/Alert'
import Row from 'react-bootstrap/Row'
import Col from 'react-bootstrap/Col'
import { useParams } from 'react-router-dom'
import { useUser } from '@/stores/user'
import { ThemeContext } from '@/lib/context'
import { createCategory, generateCards } from './NLP'
import LLMPipeline from '../LLMPipeline'
import { capitalizeFirstLetter } from '@/lib/utilities'
import { v4 as uuidv4 } from 'uuid'
import Tesseract from 'tesseract.js'
function InputWrapper({ value, save }) {
const ref = useRef(null)
useEffect(() => {
resize()
}, [])
// Automatically resizes textareas
function resize() {
ref.current.style.height = 'inherit'
ref.current.style.height = `${ref.current.scrollHeight + 2}px`
}
return (
<Form.Control
type="text"
value={value}
as="textarea"
ref={ref}
onChange={(e) => {
save(e)
resize()
}}
/>
)
}
function reducer(state, action) {
let cards
if (Array.isArray(action)) {
cards = action
} else if (typeof action === 'object') {
cards = [...state.cards]
let found = false
cards.forEach((card, i) => {
if (card.term == action.term) {
cards[i].definition = `${cards[i].definition}. ${action.definition}`
found = true
}
})
if (found) return { cards: cards }
cards = [...state.cards, action]
}
return { cards: cards }
}
class OCR {
static instance = null
static async getInstance() {
if (this.instance === null) this.instance = await Tesseract.createWorker()
return this.instance
}
}
function ExtrapolateComponent() {
const [newPack] = useUser(useShallow((state) => [state.newPack]))
const [pack, addCategory] = usePack(useShallow((state) => [state.pack, state.addCategory]))
const theme = useContext(ThemeContext)
const { packId } = useParams()
const [category, setCategory] = useState('Default')
const [text, setText] = useState('')
const [imagePreview, setImagePreview] = useState('')
const [cards, dispatch] = useReducer(reducer, { cards: [] })
const [removeSubject, setRemoveSubject] = useState(true)
async function generateTerm() {
// Create a new category if one doesn't exist
const uuid = createCategory(category, addCategory)
// Load the LLM
const condenser = await LLMPipeline.getInstance()
// Make a new flashcard for each newline
let split = text.split('\n')
split.forEach(async (paragraph) => {
let term = capitalizeFirstLetter(await condenser(paragraph))
let definition = capitalizeFirstLetter(paragraph)
// Remove the subject from the definition
term.split(' ').forEach((w) => (definition = definition.replaceAll(w, '')))
// Push changes
dispatch({
term: term,
definition: definition,
category: uuid,
uuid: uuidv4(),
})
})
}
async function saveCards() {
dispatch([])
setText('')
let p = { ...pack }
// For whatever reason p.content.push(..cards)
// gives us an "Object is not extensible" error
p.content = p.content.concat(cards.cards)
await newPack(packId, p)
}
async function handleBlob(blob) {
setImagePreview(URL.createObjectURL(blob))
const worker = await OCR.getInstance()
await worker.loadLanguage('eng')
await worker.initialize('eng')
const {
data: { text },
} = await worker.recognize(blob)
setText(text)
await worker.terminate()
}
useEffect(() => {
document.addEventListener('paste', async (e) => {
const clipboardItems =
typeof navigator?.clipboard?.read === 'function'
? await navigator.clipboard.read()
: e.clipboardData.files
for (const clipboardItem of clipboardItems) {
let blob
if (clipboardItem.type?.startsWith('image/')) {
// For files from `e.clipboardData.files`.
blob = clipboardItem
handleBlob(blob)
} else {
// For files from `navigator.clipboard.read()`.
const imageTypes = clipboardItem.types?.filter((type) =>
type.startsWith('image/')
)
for (const imageType of imageTypes) {
blob = await clipboardItem.getType(imageType)
handleBlob(blob)
}
}
}
})
return () => {
document.removeEventListener('paste', () => {})
}
}, [setImagePreview])
return (
<>
<div className="text-center mt-5">
<h1>Extrapolate</h1>
<p className="mb-3">Enter text to automatically generate cards through AI</p>
<FloatingLabel label="Category">
<Form.Control
type="text"
value={category}
onChange={(e) => {
setCategory(e.target.value)
}}
className="mb-3"
/>
</FloatingLabel>
<Row className="mb-3">
<Col sm={9}>
<FloatingLabel label="Excerpt" className="h-100">
<InputWrapper value={text} save={(e) => setText(e.target.value)} />
</FloatingLabel>
</Col>
<Col sm={3} className="d-grid">
<Form.Group controlId="formFile" className="mb-3">
<Form.Label>Drag, paste, or upload an image file</Form.Label>
<Form.Control
type="file"
accept=".bmp,.jpg,.png,.pbm,.webp"
onChange={(e) => handleBlob(e.target.files[0])}
/>
</Form.Group>
</Col>
</Row>
<img src={imagePreview} className="img-fluid mb-3" />
<Alert variant="info" className="text-start">
<h5>★ Lightweight NLP</h5>
<p>
Uses a complex algorithm to generate cards based on sentence structure, but
fails to use context for finding the sentence's subject.
</p>
<h5>Optimized LLM</h5>
<p className="mb-0">
Infers the subject of a sentence through the FLAN-T5 LLM, making it useful
for extracting information from complicated paragraphs.
</p>
</Alert>
<Accordion defaultActiveKey="0" flush className="mt-3 mb-5">
<Accordion.Item eventKey="0">
<Accordion.Header>Lightweight NLP</Accordion.Header>
<Accordion.Body>
<Button
variant={theme.dark ? 'light' : 'dark'}
size="lg"
onClick={() => generateCards(text, category, addCategory, dispatch)}
className="mt-3 mb-3"
>
🏭 Generate
</Button>
</Accordion.Body>
</Accordion.Item>
<Accordion.Item eventKey="1">
<Accordion.Header>Optimized LLM</Accordion.Header>
<Accordion.Body>
<div className="d-flex justify-content-center">
<Form.Check
type="switch"
label="Remove Subject from Definition"
checked={removeSubject}
onChange={(e) => setRemoveSubject(e.target.checked)}
/>
</div>
<Button
variant={theme.dark ? 'light' : 'dark'}
size="lg"
onClick={generateTerm}
className="mt-3 mb-3"
>
⚒️ Generate
</Button>
</Accordion.Body>
</Accordion.Item>
</Accordion>
</div>
{cards.cards.length > 0 && (
<>
<Table striped bordered>
<thead>
<tr>
<th>#</th>
<th>Term</th>
<th>Definition</th>
</tr>
</thead>
<tbody>
{cards.cards.map((card, index) => {
return (
<tr key={index}>
<td>{index + 1}</td>
<td>
<InputWrapper
value={card.term}
save={(e) => {
// We can use the spread syntax because this is synchronous
let t = [...cards.cards]
t[index].term = e.target.value
dispatch(t)
}}
/>
</td>
<td>
<InputWrapper
value={card.definition}
save={(e) => {
let t = [...cards.cards]
t[index].definition = e.target.value
dispatch(t)
}}
/>
</td>
</tr>
)
})}
</tbody>
</Table>
<div className="text-center mb-5">
<Button variant={theme.color} size="lg" onClick={saveCards}>
➕ Add to pack
</Button>
</div>
</>
)}
</>
)
}
const Extrapolate = function () {
return (
<Experience name="extrapolate" showFilter={false}>
<ExtrapolateComponent />
</Experience>
)
}
export default Extrapolate