ability to parse user and label

This commit is contained in:
Mo Tarbin
2026-07-20 01:54:39 -04:00
parent e0257445ef
commit a133b271d0
5 changed files with 436 additions and 59 deletions

View File

@@ -1,6 +1,7 @@
import { Add } from '@mui/icons-material'
import { Box, Button, Typography } from '@mui/joy'
import { useMediaQuery } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import * as chrono from 'chrono-node'
import moment from 'moment'
import { useCallback, useEffect, useRef, useState } from 'react'
@@ -25,6 +26,7 @@ import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
import { useDocumentScanner } from '../../hooks/useDocumentScanner'
import { localAIService } from '../../service/LocalAIService'
import { voiceInputService } from '../../service/VoiceInputService'
import { CreateLabel } from '../../utils/Fetcher'
import { TASK_COLOR } from '../../utils/Colors'
import AdvancedOptionsSection, {
AdvancedOptionsTrigger,
@@ -69,6 +71,7 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
useCircleMembers()
const { isLoading: isProjectsLoading } = useProjects()
const createChoreMutation = useCreateChore()
const queryClient = useQueryClient()
const { data: userProfile } = useUserProfile()
@@ -92,6 +95,9 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
const richTextEditorRef = useRef(null)
const latestRef = useRef({})
// Picker edits made on a voice task card, applied once after the reparse
// that follows landing the spoken text in the smart input
const pendingVoiceOverridesRef = useRef(null)
const [priority, setPriority] = useState(0)
const [dueDate, setDueDate] = useState(null)
const [description, setDescription] = useState(null)
@@ -492,6 +498,28 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
)
setRenderedParts(parts)
const overrides = pendingVoiceOverridesRef.current
if (overrides) {
pendingVoiceOverridesRef.current = null
if ('priority' in overrides) setPriority(overrides.priority || 0)
if ('frequency' in overrides) setFrequency(overrides.frequency)
if ('labelIds' in overrides) setLabelsV2(overrides.labelIds || [])
if ('assignees' in overrides || 'isAnyone' in overrides) {
setIsAnyoneTask(!!overrides.isAnyone)
setAssignees(overrides.assignees || [])
}
if ('dueDate' in overrides) {
if (overrides.dueDate) {
syncDueDateStates(overrides.dueDate)
} else {
setDueDate(null)
setDueDateOnly(null)
setDueTime(null)
setUseCustomTime(false)
}
}
}
},
[userLabels, renderHighlightedSentence, circleMembers, userProfile],
)
@@ -595,23 +623,73 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
}
}
// Creates labels that were spoken but don't exist yet. Returns a Map of
// lowercase name → label id covering both created and already-existing ones.
const createMissingLabels = async newLabels => {
const resolved = new Map(
(userLabels || []).map(l => [l.name.toLowerCase(), l.id]),
)
let createdAny = false
for (const label of newLabels) {
const key = label.name.toLowerCase()
if (resolved.has(key)) continue
try {
const resp = await CreateLabel({
name: label.name,
color: label.color || '#3b82f6',
})
const data = await resp.json()
const created = data?.res ?? data
if (created?.id) {
resolved.set(key, created.id)
createdAny = true
}
} catch (error) {
console.error('Error creating label:', error)
}
}
if (createdAny) {
queryClient.invalidateQueries({ queryKey: ['labels'] })
}
return resolved
}
// Single voice-captured task: land it in the smart input so the user
// reviews it with the normal pickers before creating.
const handleVoiceSingle = text => {
// reviews it with the normal pickers before creating. Setting taskText
// (rather than calling processText directly) lets the reparse effect run
// exactly once, consuming any picker overrides from the panel.
const handleVoiceSingle = async (text, overrides = {}) => {
setShowVoice(false)
processText(text)
if (Object.keys(overrides).length > 0) {
pendingVoiceOverridesRef.current = overrides
}
setTaskText(text)
const labels = parseLabels(text, userLabels || [])
if (labels.newLabels?.length) {
// Once the labels query refetches, the reparse links them automatically
await createMissingLabels(labels.newLabels)
}
}
// Multiple voice-captured tasks: they were reviewed as cards in the panel,
// so create them all directly.
const handleVoiceCreateMany = async parsedTasks => {
const notificationTemplates = getDefaultNotification()
const allNewLabels = parsedTasks.flatMap(t => t.newLabels || [])
const labelIdsByName =
allNewLabels.length > 0 ? await createMissingLabels(allNewLabels) : null
for (const parsed of parsedTasks) {
const chore = buildChorePayload(parsed, {
userProfile,
projectId,
notificationTemplates,
})
const extraLabelIds = (parsed.newLabels || [])
.map(nl => labelIdsByName?.get(nl.name.toLowerCase()))
.filter(id => id != null && !parsed.labelIds.includes(id))
const chore = buildChorePayload(
{ ...parsed, labelIds: [...parsed.labelIds, ...extraLabelIds] },
{
userProfile,
projectId,
notificationTemplates,
},
)
try {
const result = await createChoreMutation.mutateAsync(chore)
if (result?._pendingCreate) {