diff --git a/frontend/components/Productivity/ProductivityAssistant.tsx b/frontend/components/Productivity/ProductivityAssistant.tsx index 695a50d..7564b3e 100644 --- a/frontend/components/Productivity/ProductivityAssistant.tsx +++ b/frontend/components/Productivity/ProductivityAssistant.tsx @@ -15,6 +15,7 @@ import TaskModal from '../Task/TaskModal'; import { fetchTaskById, updateTask, deleteTask } from '../../utils/tasksService'; import { fetchProjects, createProject } from '../../utils/projectsService'; import { useToast } from '../Shared/ToastContext'; +import { getVagueTasks } from '../../utils/taskIntelligenceService'; interface ProductivityInsight { type: 'stalled_projects' | 'completed_no_next' | 'tasks_are_projects' | 'vague_tasks' | 'overdue_tasks' | 'stuck_projects'; @@ -53,7 +54,7 @@ const ProductivityAssistant: React.FC = ({ tasks, pr const newInsights: ProductivityInsight[] = []; // Filter to only include non-completed tasks - const activeTasks = tasks.filter(task => task.status !== 'done' && task.status !== 'completed'); + const activeTasks = tasks.filter(task => task.status !== 'done' && task.status !== 'archived'); // 1. Stalled Projects (no tasks/actions) const stalledProjects = projects.filter(project => @@ -74,7 +75,7 @@ const ProductivityAssistant: React.FC = ({ tasks, pr // 2. Projects with completed tasks but no next action const projectsNeedingNextAction = projects.filter(project => { const projectTasks = tasks.filter(task => task.project_id === project.id); - const hasCompletedTasks = projectTasks.some(task => task.status === 'done' || task.status === 'completed'); + const hasCompletedTasks = projectTasks.some(task => task.status === 'done' || task.status === 'archived'); const hasNextAction = activeTasks.some(task => task.project_id === project.id && (task.status === 'not_started' || task.status === 'in_progress') ); @@ -111,86 +112,7 @@ const ProductivityAssistant: React.FC = ({ tasks, pr } // 4. Tasks without clear verbs - const vagueTasks = activeTasks.filter(task => { - const taskName = task.name.toLowerCase().trim(); - - // Skip if it's already a next action (contains →) - if (taskName.includes('→')) return false; - - // More comprehensive action verb patterns - const actionVerbPatterns = [ - // Direct action verbs at start - /^(call|email|text|message|phone|contact)/, - /^(write|draft|compose|type|create|make)/, - /^(read|review|check|examine|study|analyze)/, - /^(buy|purchase|order|get|obtain|acquire)/, - /^(schedule|book|arrange|plan|set up|setup)/, - /^(meet|discuss|talk|speak|chat)/, - /^(send|deliver|ship|mail|forward)/, - /^(update|edit|modify|change|fix|correct)/, - /^(finish|complete|finalize|wrap up)/, - /^(submit|file|upload|post|publish)/, - /^(organize|sort|clean|tidy|arrange)/, - /^(research|find|search|look up|investigate)/, - /^(prepare|gather|collect|assemble)/, - /^(install|download|set up|configure)/, - /^(test|try|experiment|validate)/, - /^(backup|save|export|archive)/, - /^(delete|remove|uninstall|cancel)/, - - // Gerund forms (-ing verbs) which are often good actions - /^(calling|emailing|writing|reading|buying|scheduling|meeting|sending|updating|finishing|submitting|organizing|researching|preparing|installing|testing|backing)/, - - // Question patterns (usually clear next actions) - /^(what|how|when|where|why|which)/, - /\?$/, - - // Imperative patterns with objects - /^(add|remove|insert|attach|include|exclude)/, - /^(start|begin|initiate|launch|kick off)/, - /^(stop|end|terminate|close|shut)/, - - // Common task patterns - /^(follow up|followup)/, - /^(sign up|signup)/, - /^(log in|login)/, - /^(pick up|pickup)/, - /^(drop off|dropoff)/, - /^(set up|setup)/, - /^(clean up|cleanup)/, - /^(wrap up|wrapup)/ - ]; - - // Check if task starts with any action verb pattern - const hasActionVerb = actionVerbPatterns.some(pattern => pattern.test(taskName)); - if (hasActionVerb) return false; - - // Check for common non-actionable patterns (these are vague) - const vaguePatterns = [ - // Single words without context - /^[a-zA-Z]+$/, - // Just names without action - /^[A-Z][a-z]+ [A-Z][a-z]+$/, - // Just project/area names - /^(project|website|app|system|process|issue|problem|bug|feature)$/i, - // Very short tasks without clear action (less than 3 words) - /^(\w+\s+\w+|^\w+)$/ - ]; - - // Only flag as vague if it matches vague patterns AND is not clearly actionable - const isVague = vaguePatterns.some(pattern => pattern.test(taskName)); - - // Additional checks for good tasks that shouldn't be flagged - const hasGoodStructure = ( - taskName.length > 15 || // Longer tasks are usually more specific - taskName.split(' ').length > 3 || // More than 3 words usually means more specific - /\b(for|with|to|from|about|regarding|re:|fwd:)\b/.test(taskName) || // Prepositions indicate context - /\b(tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next week|this week)\b/.test(taskName) || // Time references - /\b(project|meeting|appointment|deadline|due|urgent|important)\b/.test(taskName) // Context indicators - ); - - return isVague && !hasGoodStructure; - }); + const vagueTasks = getVagueTasks(activeTasks); if (vagueTasks.length > 0) { newInsights.push({ @@ -208,8 +130,8 @@ const ProductivityAssistant: React.FC = ({ tasks, pr const thresholdDate = new Date(now.getTime() - (OVERDUE_THRESHOLD_DAYS * 24 * 60 * 60 * 1000)); const staleTasks = activeTasks.filter(task => { - const taskDate = task.updated_at ? new Date(task.updated_at) : - task.created_at ? new Date(task.created_at) : null; + // Only use created_at since updated_at doesn't exist in the interface + const taskDate = task.created_at ? new Date(task.created_at) : null; return taskDate && taskDate < thresholdDate; }); @@ -229,10 +151,19 @@ const ProductivityAssistant: React.FC = ({ tasks, pr const stuckProjects = projects.filter(project => { if (!project.active) return false; - const projectDate = project.updated_at ? new Date(project.updated_at) : - project.created_at ? new Date(project.created_at) : null; + // Projects don't have date fields in the interface, so we'll check if they have recent tasks + const projectTasks = activeTasks.filter(task => task.project_id === project.id); - return projectDate && projectDate < thresholdDate; + if (projectTasks.length === 0) return false; // Empty projects are handled by "stalled projects" + + // Find the most recent task date for this project + const mostRecentTaskDate = projectTasks.reduce((latest, task) => { + const taskDate = task.created_at ? new Date(task.created_at) : null; + if (!taskDate) return latest; + return !latest || taskDate > latest ? taskDate : latest; + }, null as Date | null); + + return mostRecentTaskDate && mostRecentTaskDate < thresholdDate; }); if (stuckProjects.length > 0) { diff --git a/frontend/components/Task/TaskModal.tsx b/frontend/components/Task/TaskModal.tsx index 5c47d93..eca0189 100644 --- a/frontend/components/Task/TaskModal.tsx +++ b/frontend/components/Task/TaskModal.tsx @@ -12,6 +12,7 @@ import { useStore } from "../../store/useStore"; import { fetchTags } from '../../utils/tagsService'; import { fetchTaskById } from '../../utils/tasksService'; import { getTaskIntelligenceEnabled } from '../../utils/profileService'; +import { analyzeTaskName, TaskAnalysis } from '../../utils/taskIntelligenceService'; import { useTranslation } from "react-i18next"; interface TaskModalProps { @@ -49,7 +50,7 @@ const TaskModal: React.FC = ({ const [tagsLoading, setTagsLoading] = useState(false); const [parentTask, setParentTask] = useState(null); const [parentTaskLoading, setParentTaskLoading] = useState(false); - const [showNameLengthHelper, setShowNameLengthHelper] = useState(false); + const [taskAnalysis, setTaskAnalysis] = useState(null); const [taskIntelligenceEnabled, setTaskIntelligenceEnabled] = useState(true); const { showSuccessToast, showErrorToast } = useToast(); const { t } = useTranslation(); @@ -58,10 +59,12 @@ const TaskModal: React.FC = ({ setFormData(task); setTags(task.tags?.map((tag) => tag.name) || []); - // Check if task name is short and show helper when modal opens (only if intelligence is enabled) + // Analyze task name and show helper when modal opens (only if intelligence is enabled) if (isOpen && task.name && taskIntelligenceEnabled) { - const trimmedName = task.name.trim(); - setShowNameLengthHelper(trimmedName.length > 0 && trimmedName.length < 10); + const analysis = analyzeTaskName(task.name); + setTaskAnalysis(analysis); + } else { + setTaskAnalysis(null); } // Safely find the current project, handling the case where projects might be undefined @@ -167,10 +170,10 @@ const TaskModal: React.FC = ({ const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); - // Show helper message for task name if it's too short (only if intelligence is enabled) + // Analyze task name in real-time (only if intelligence is enabled) if (name === 'name' && taskIntelligenceEnabled) { - const trimmedValue = value.trim(); - setShowNameLengthHelper(trimmedValue.length > 0 && trimmedValue.length < 10); + const analysis = analyzeTaskName(value); + setTaskAnalysis(analysis); } }; @@ -312,21 +315,51 @@ const TaskModal: React.FC = ({ className="block w-full text-xl font-semibold dark:bg-gray-800 text-black dark:text-white border-b-2 border-gray-200 dark:border-gray-900 focus:outline-none shadow-sm py-2" placeholder={t('forms.task.namePlaceholder', 'Add Task Name')} /> - {showNameLengthHelper && taskIntelligenceEnabled && ( -
+ {taskAnalysis && taskAnalysis.isVague && taskIntelligenceEnabled && ( +
- +
-

- {t('task.nameHelper.title', 'Make it more descriptive!')} -

-

- {t('task.nameHelper.suggestion', 'Try adding more details like "Call dentist to schedule cleaning appointment" instead of just "Call dentist"')} +

+ + {taskAnalysis.reason === 'short' && t('task.nameHelper.short', 'Make it more descriptive!')} + {taskAnalysis.reason === 'no_verb' && t('task.nameHelper.noVerb', 'Add an action verb!')} + {taskAnalysis.reason === 'vague_pattern' && t('task.nameHelper.vague', 'Be more specific!')} +

+ {taskAnalysis.suggestion && ( +

+ {taskAnalysis.suggestion} +

+ )}
diff --git a/frontend/utils/taskIntelligenceService.ts b/frontend/utils/taskIntelligenceService.ts new file mode 100644 index 0000000..32492fc --- /dev/null +++ b/frontend/utils/taskIntelligenceService.ts @@ -0,0 +1,158 @@ +import { Task } from '../entities/Task'; + +/** + * Analyzes a task name to determine if it's vague or needs improvement + * Returns an object with analysis results and suggestions + */ +export interface TaskAnalysis { + isVague: boolean; + reason: 'short' | 'no_verb' | 'vague_pattern' | 'good'; + suggestion?: string; + severity: 'low' | 'medium' | 'high'; +} + +export const analyzeTaskName = (taskName: string): TaskAnalysis => { + const trimmedName = taskName.toLowerCase().trim(); + + // Very short tasks (less than 10 chars) - original logic + if (trimmedName.length > 0 && trimmedName.length < 10) { + return { + isVague: true, + reason: 'short', + suggestion: 'Try to be more specific about what needs to be done', + severity: 'medium' + }; + } + + // Skip if it's already a next action (contains →) + if (trimmedName.includes('→')) { + return { + isVague: false, + reason: 'good', + severity: 'low' + }; + } + + // More comprehensive action verb patterns + const actionVerbPatterns = [ + // Direct action verbs at start + /^(call|email|text|message|phone|contact)/, + /^(write|draft|compose|type|create|make)/, + /^(read|review|check|examine|study|analyze)/, + /^(buy|purchase|order|get|obtain|acquire)/, + /^(schedule|book|arrange|plan|set up|setup)/, + /^(meet|discuss|talk|speak|chat)/, + /^(send|deliver|ship|mail|forward)/, + /^(update|edit|modify|change|fix|correct)/, + /^(finish|complete|finalize|wrap up)/, + /^(submit|file|upload|post|publish)/, + /^(organize|sort|clean|tidy|arrange)/, + /^(research|find|search|look up|investigate)/, + /^(prepare|gather|collect|assemble)/, + /^(install|download|set up|configure)/, + /^(test|try|experiment|validate)/, + /^(backup|save|export|archive)/, + /^(delete|remove|uninstall|cancel)/, + + // Gerund forms (-ing verbs) which are often good actions + /^(calling|emailing|writing|reading|buying|scheduling|meeting|sending|updating|finishing|submitting|organizing|researching|preparing|installing|testing|backing)/, + + // Question patterns (usually clear next actions) + /^(what|how|when|where|why|which)/, + /\?$/, + + // Imperative patterns with objects + /^(add|remove|insert|attach|include|exclude)/, + /^(start|begin|initiate|launch|kick off)/, + /^(stop|end|terminate|close|shut)/, + + // Common task patterns + /^(follow up|followup)/, + /^(sign up|signup)/, + /^(log in|login)/, + /^(pick up|pickup)/, + /^(drop off|dropoff)/, + /^(set up|setup)/, + /^(clean up|cleanup)/, + /^(wrap up|wrapup)/ + ]; + + // Check if task starts with any action verb pattern + const hasActionVerb = actionVerbPatterns.some(pattern => pattern.test(trimmedName)); + if (hasActionVerb) { + return { + isVague: false, + reason: 'good', + severity: 'low' + }; + } + + // Check for common non-actionable patterns (these are vague) + const vaguePatterns = [ + // Single words without context + /^[a-zA-Z]+$/, + // Just names without action + /^[A-Z][a-z]+ [A-Z][a-z]+$/, + // Just project/area names + /^(project|website|app|system|process|issue|problem|bug|feature)$/i, + // Very short tasks without clear action (less than 3 words) + /^(\w+\s+\w+|^\w+)$/ + ]; + + // Only flag as vague if it matches vague patterns AND is not clearly actionable + const matchesVaguePattern = vaguePatterns.some(pattern => pattern.test(trimmedName)); + + // Additional checks for good tasks that shouldn't be flagged + const hasGoodStructure = ( + trimmedName.length > 15 || // Longer tasks are usually more specific + trimmedName.split(' ').length > 3 || // More than 3 words usually means more specific + /\b(for|with|to|from|about|regarding|re:|fwd:)\b/.test(trimmedName) || // Prepositions indicate context + /\b(tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next week|this week)\b/.test(trimmedName) || // Time references + /\b(project|meeting|appointment|deadline|due|urgent|important)\b/.test(trimmedName) // Context indicators + ); + + if (matchesVaguePattern && !hasGoodStructure) { + return { + isVague: true, + reason: 'vague_pattern', + suggestion: 'Try starting with an action verb like "Call", "Write", "Schedule", or "Research"', + severity: 'high' + }; + } + + // Check for missing action verbs (less strict) + if (!hasActionVerb && trimmedName.split(' ').length <= 2) { + return { + isVague: true, + reason: 'no_verb', + suggestion: 'What specific action do you need to take? Try starting with a verb.', + severity: 'medium' + }; + } + + return { + isVague: false, + reason: 'good', + severity: 'low' + }; +}; + +/** + * Filters tasks to find vague ones using the enhanced logic + */ +export const getVagueTasks = (tasks: Task[]): Task[] => { + return tasks.filter(task => { + if (task.status === 'done' || task.status === 'archived') return false; + + const analysis = analyzeTaskName(task.name); + return analysis.isVague; + }); +}; + +/** + * Gets a user-friendly suggestion for improving a task name + */ +export const getTaskNameSuggestion = (taskName: string): string | null => { + const analysis = analyzeTaskName(taskName); + return analysis.isVague ? (analysis.suggestion || null) : null; +}; \ No newline at end of file diff --git a/public/locales/el/translation.json b/public/locales/el/translation.json index 43f3cf9..1f9d15c 100644 --- a/public/locales/el/translation.json +++ b/public/locales/el/translation.json @@ -350,7 +350,10 @@ "saveAsTask": "Αποθήκευση ως Εργασία", "nameHelper": { "title": "Κάντε το πιο περιγραφικό!", - "suggestion": "Δοκιμάστε να προσθέσετε περισσότερες λεπτομέρειες όπως \"Καλέστε τον οδοντίατρο για ραντεβού καθαρισμού\" αντί για απλά \"Καλέστε οδοντίατρο\"" + "suggestion": "Δοκιμάστε να προσθέσετε περισσότερες λεπτομέρειες όπως \"Καλέστε τον οδοντίατρο για ραντεβού καθαρισμού\" αντί για απλά \"Καλέστε οδοντίατρο\"", + "short": "Κάντε το πιο περιγραφικό!", + "noVerb": "Προσθέστε ένα ρήμα ενέργειας!", + "vague": "Να είστε πιο συγκεκριμένοι!" } }, "dateFormats": { diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index bf5ce82..5b47271 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -258,7 +258,10 @@ "task": { "nameHelper": { "title": "Make it more descriptive!", - "suggestion": "Try adding more details like \"Call dentist to schedule cleaning appointment\" instead of just \"Call dentist\"" + "suggestion": "Try adding more details like \"Call dentist to schedule cleaning appointment\" instead of just \"Call dentist\"", + "short": "Make it more descriptive!", + "noVerb": "Add an action verb!", + "vague": "Be more specific!" } } },