Ability to have open OCR from the field

This commit is contained in:
Mo Tarbin
2026-07-04 15:22:24 -04:00
parent 3c0f4402a8
commit 4460d80b72
5 changed files with 921 additions and 263 deletions

View File

@@ -1,4 +1,4 @@
import { Add, CameraAlt } from '@mui/icons-material'
import { Add } from '@mui/icons-material'
import { Box, Button, Typography } from '@mui/joy'
import { useMediaQuery } from '@mui/material'
import * as chrono from 'chrono-node'
@@ -28,7 +28,8 @@ import DueDatePickerField from './DueDatePickerField'
import LabelsPickerField from './LabelsPickerField'
import LearnMoreButton from './LearnMore'
import NotificationPickerField from './NotificationPickerField'
import PhotoTaskModal from './PhotoTaskModal'
import ScanPanel from './ScanToTask/ScanPanel'
import { useDocumentScanner } from '../../hooks/useDocumentScanner'
import PriorityPickerField from './PriorityPickerField'
import RepeatPickerField from './RepeatPickerField'
import RichTextEditor from './RichTextEditor'
@@ -104,7 +105,9 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
const [projectId, setProjectId] = useState(getInitialProject())
const [attachments, setAttachments] = useState([])
const [photoModalOpen, setPhotoModalOpen] = useState(false)
const [showScan, setShowScan] = useState(false)
const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null)
const { isNativeScanner } = useDocumentScanner()
// Priority colors
const priorityColors = {
@@ -564,6 +567,7 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
const handleCloseModal = forceRefetch => {
onClose(forceRefetch)
setShowScan(false)
setTaskText('')
setTaskTitle('')
setDueDate(null)
@@ -730,263 +734,261 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
</Box>
}
>
<Box>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}}
>
<Typography level='body-sm'>Task in a sentence:</Typography>
<LearnMoreButton
content={
<>
<Typography level='body-sm' sx={{ mb: 1 }}>
This feature lets you create a task simply by typing a
sentence. It attempt parses the sentence to identify the
task&apos;s due date, priority, and frequency.
</Typography>
{!showScan && (
<>
<Box>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}}
>
<Typography level='body-sm'>Task in a sentence:</Typography>
<LearnMoreButton
content={
<>
<Typography level='body-sm' sx={{ mb: 1 }}>
This feature lets you create a task simply by typing a
sentence. It attempt parses the sentence to identify the
task&apos;s due date, priority, and frequency.
</Typography>
<Typography level='body-sm' sx={{ fontWeight: 'bold', mt: 2 }}>
Examples:
</Typography>
<Typography level='body-sm' sx={{ fontWeight: 'bold', mt: 2 }}>
Examples:
</Typography>
<Typography
level='body-sm'
component='ul'
sx={{ pl: 2, mt: 1, listStyle: 'disc' }}
>
<li>
<strong>Priority:</strong>For highest priority any of the
following keyword <em>P1</em>, <em>Urgent</em>,{' '}
<em>Important</em>, or <em>ASAP</em>. For lower priorities,
use <em>P2</em>, <em>P3</em>, or <em>P4</em>.
</li>
<li>
<strong>Due date:</strong> Specify dates with phrases like{' '}
<em>tomorrow</em>, <em>next week</em>, <em>Monday</em>, or{' '}
<em>August 1st at 12pm</em>.
</li>
<li>
<strong>Frequency:</strong> Set recurring tasks with terms
like <em>daily</em>, <em>weekly</em>, <em>monthly</em>,{' '}
<em>yearly</em>, or patterns such as{' '}
<em>every Tuesday and Thursday</em>.
</li>
</Typography>
</>
}
/>
</Box>
<Typography
level='body-sm'
component='ul'
sx={{ pl: 2, mt: 1, listStyle: 'disc' }}
>
<li>
<strong>Priority:</strong>For highest priority any of the
following keyword <em>P1</em>, <em>Urgent</em>,{' '}
<em>Important</em>, or <em>ASAP</em>. For lower
priorities, use <em>P2</em>, <em>P3</em>, or <em>P4</em>.
</li>
<li>
<strong>Due date:</strong> Specify dates with phrases
like <em>tomorrow</em>, <em>next week</em>,{' '}
<em>Monday</em>, or <em>August 1st at 12pm</em>.
</li>
<li>
<strong>Frequency:</strong> Set recurring tasks with
terms like <em>daily</em>, <em>weekly</em>,{' '}
<em>monthly</em>, <em>yearly</em>, or patterns such as{' '}
<em>every Tuesday and Thursday</em>.
</li>
</Typography>
</>
}
/>
</Box>
<SmartTaskTitleInput
autoFocus
value={taskText}
placeholder='Type your task...'
onChange={text => {
setTaskText(text)
if (!text) setTaskTitle('')
}}
customRenderer={renderedParts}
onEnterPressed={handleEnterPressed}
suggestions={{
'#': {
value: 'id',
display: 'name',
options: userLabels ? userLabels : [],
},
'!': {
value: 'id',
display: 'name',
options: [
{ id: '1', name: 'P1' },
{ id: '2', name: 'P2' },
{ id: '3', name: 'P3' },
{ id: '4', name: 'P4' },
],
},
'@': {
value: 'userId',
display: 'displayName',
options: [
{ userId: 'anyone', displayName: 'Anyone' },
...(circleMembers?.res || []),
],
},
'*': {
value: 'id',
display: 'name',
options: [
{ id: '1', name: '1 point' },
{ id: '5', name: '5 points' },
{ id: '10', name: '10 points' },
{ id: '25', name: '25 points' },
{ id: '50', name: '50 points' },
{ id: '100', name: '100 points' },
],
},
}}
/>
</Box>
<Box
sx={{
paddingTop: 2,
paddingBottom: 1,
display: 'flex',
flexDirection: 'row',
gap: 1.5,
// scrollable horizontally but hide the scrollbar:
overflowX: 'auto',
'&::-webkit-scrollbar': {
display: 'none',
},
// if not mobile then go to next line if not enough space( show chip on next line):
flexWrap: isMobile ? 'nowrap' : 'wrap',
}}
>
<DueDatePickerField
emptyDisplay={pickerEmptyDisplay}
dueDateOnly={dueDateOnly}
dueTime={dueTime}
useCustomTime={useCustomTime}
onDueDateChange={handleDueDateChange}
onDueTimeChange={handleDueTimeChange}
onUseCustomTimeChange={handleUseCustomTimeChange}
onClear={() => {
setDueDate(null)
setDueDateOnly(null)
setDueTime(null)
setUseCustomTime(false)
}}
/>
<RepeatPickerField
emptyDisplay={pickerEmptyDisplay}
value={frequency}
onChange={setFrequency}
onClear={() => setFrequency(null)}
/>
<PriorityPickerField
value={priority}
onChange={setPriority}
onClear={() => setPriority(0)}
emptyDisplay={pickerEmptyDisplay}
priorityColors={priorityColors}
priorityLabels={priorityLabels}
/>
<AssigneePickerField
emptyDisplay={pickerEmptyDisplay}
value={assignees?.[0]?.userId || null}
onChange={userId => {
if (!userId) {
setAssignees([])
} else {
setAssignees([{ userId }])
}
}}
onClear={() => setAssignees([])}
currentUserId={userProfile?.id}
members={circleMembers?.res || []}
/>
<LabelsPickerField
emptyDisplay={pickerEmptyDisplay}
values={labelsV2 || []}
onChange={setLabelsV2}
onClear={() => setLabelsV2([])}
labels={userLabels || []}
/>
<AttachmentPickerField
attachments={attachments}
onChange={setAttachments}
onClear={() => setAttachments([])}
emptyDisplay={pickerEmptyDisplay}
entityType='chore_attachment'
/>
<NotificationPickerField
value={notificationMetadata}
onChange={setNotificationMetadata}
onClear={() => setNotificationMetadata({ templates: [] })}
emptyDisplay={pickerEmptyDisplay}
/>
</Box>
<Box mt={2} sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
{!hasDescription && (
<Button
startDecorator={<Add />}
variant='outlined'
color='neutral'
size='md'
onClick={() => {
setHasDescription(true)
// Focus will be handled by the useEffect hook
}}
endDecorator={
showKeyboardShortcuts && <KeyboardShortcutHint shortcut='E' />
}
>
Description
</Button>
)}
<Button
startDecorator={<CameraAlt />}
variant='plain'
size='sm'
onClick={() => setPhotoModalOpen(true)}
>
Scan Photo
</Button>
{!hasSubTasks && (
<Button
startDecorator={<Add />}
variant='outlined'
color='neutral'
size='md'
onClick={() => {
setHasSubTasks(true)
}}
endDecorator={
showKeyboardShortcuts && <KeyboardShortcutHint shortcut='J' />
}
>
Subtasks
</Button>
)}
</Box>
{hasDescription && (
<Box>
<Typography level='body-sm'>Description:</Typography>
<div>
<RichTextEditor
ref={richTextEditorRef}
onChange={setDescription}
value={description || ''}
entityType={'chore_description'}
<SmartTaskTitleInput
autoFocus
value={taskText}
isNativeScanner={isNativeScanner}
onScanClick={() => setShowScan(true)}
onPhotoSelected={dataUrl => {
setPendingPhotoUrl(dataUrl)
setShowScan(true)
}}
placeholder='Type your task...'
onChange={text => {
setTaskText(text)
if (!text) setTaskTitle('')
}}
customRenderer={renderedParts}
onEnterPressed={handleEnterPressed}
suggestions={{
'#': {
value: 'id',
display: 'name',
options: userLabels ? userLabels : [],
},
'!': {
value: 'id',
display: 'name',
options: [
{ id: '1', name: 'P1' },
{ id: '2', name: 'P2' },
{ id: '3', name: 'P3' },
{ id: '4', name: 'P4' },
],
},
'@': {
value: 'userId',
display: 'displayName',
options: [
{ userId: 'anyone', displayName: 'Anyone' },
...(circleMembers?.res || []),
],
},
'*': {
value: 'id',
display: 'name',
options: [
{ id: '1', name: '1 point' },
{ id: '5', name: '5 points' },
{ id: '10', name: '10 points' },
{ id: '25', name: '25 points' },
{ id: '50', name: '50 points' },
{ id: '100', name: '100 points' },
],
},
}}
/>
</div>
</Box>
</Box>
<Box
sx={{
paddingTop: 2,
paddingBottom: 1,
display: 'flex',
flexDirection: 'row',
gap: 1.5,
overflowX: 'auto',
'&::-webkit-scrollbar': { display: 'none' },
flexWrap: isMobile ? 'nowrap' : 'wrap',
}}
>
<DueDatePickerField
emptyDisplay={pickerEmptyDisplay}
dueDateOnly={dueDateOnly}
dueTime={dueTime}
useCustomTime={useCustomTime}
onDueDateChange={handleDueDateChange}
onDueTimeChange={handleDueTimeChange}
onUseCustomTimeChange={handleUseCustomTimeChange}
onClear={() => {
setDueDate(null)
setDueDateOnly(null)
setDueTime(null)
setUseCustomTime(false)
}}
/>
<RepeatPickerField
emptyDisplay={pickerEmptyDisplay}
value={frequency}
onChange={setFrequency}
onClear={() => setFrequency(null)}
/>
<PriorityPickerField
value={priority}
onChange={setPriority}
onClear={() => setPriority(0)}
emptyDisplay={pickerEmptyDisplay}
priorityColors={priorityColors}
priorityLabels={priorityLabels}
/>
<AssigneePickerField
emptyDisplay={pickerEmptyDisplay}
value={assignees?.[0]?.userId || null}
onChange={userId => {
if (!userId) {
setAssignees([])
} else {
setAssignees([{ userId }])
}
}}
onClear={() => setAssignees([])}
currentUserId={userProfile?.id}
members={circleMembers?.res || []}
/>
<LabelsPickerField
emptyDisplay={pickerEmptyDisplay}
values={labelsV2 || []}
onChange={setLabelsV2}
onClear={() => setLabelsV2([])}
labels={userLabels || []}
/>
<AttachmentPickerField
attachments={attachments}
onChange={setAttachments}
onClear={() => setAttachments([])}
emptyDisplay={pickerEmptyDisplay}
entityType='chore_attachment'
/>
<NotificationPickerField
value={notificationMetadata}
onChange={setNotificationMetadata}
onClear={() => setNotificationMetadata({ templates: [] })}
emptyDisplay={pickerEmptyDisplay}
/>
</Box>
<Box mt={2} sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
{!hasDescription && (
<Button
startDecorator={<Add />}
variant='outlined'
color='neutral'
size='md'
onClick={() => setHasDescription(true)}
endDecorator={
showKeyboardShortcuts && <KeyboardShortcutHint shortcut='E' />
}
>
Description
</Button>
)}
{!hasSubTasks && (
<Button
startDecorator={<Add />}
variant='outlined'
color='neutral'
size='md'
onClick={() => setHasSubTasks(true)}
endDecorator={
showKeyboardShortcuts && <KeyboardShortcutHint shortcut='J' />
}
>
Subtasks
</Button>
)}
</Box>
{hasDescription && (
<Box>
<Typography level='body-sm'>Description:</Typography>
<div>
<RichTextEditor
ref={richTextEditorRef}
onChange={setDescription}
value={description || ''}
entityType={'chore_description'}
/>
</div>
</Box>
)}
{hasSubTasks && (
<Box>
<Typography level='body-sm'>Subtasks:</Typography>
<SubTasks
editMode={true}
tasks={subTasks ? subTasks : []}
setTasks={setSubTasks}
shouldFocus={true}
/>
</Box>
)}
</>
)}
{hasSubTasks && (
<Box>
<Typography level='body-sm'>Subtasks:</Typography>
<SubTasks
editMode={true}
tasks={subTasks ? subTasks : []}
setTasks={setSubTasks}
shouldFocus={true}
/>
</Box>
{showScan && (
<ScanPanel
open
onTaskExtracted={handleTaskExtracted}
initialImageUrl={pendingPhotoUrl}
onClose={() => {
setShowScan(false)
setPendingPhotoUrl(null)
}}
/>
)}
</ResponsiveModal>
<PhotoTaskModal
open={photoModalOpen}
onClose={() => setPhotoModalOpen(false)}
onTaskExtracted={handleTaskExtracted}
/>
</>
)
}