Integrate advanced voice commands for faster navigation and task management

Adds enhanced voice control with navigation, task management, and OpenAI/Anthropic integration using SpeechRecognition API.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ff0be73b-afdd-4747-978b-bb8301fb0a82
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/f810338f-6638-4b8d-a3ed-38475b263843.jpg
This commit is contained in:
ghaddaditw
2025-06-08 00:11:10 +00:00
parent 56f2f98a5e
commit 7f9d54e150
6 changed files with 1635 additions and 45 deletions
@@ -0,0 +1,44 @@
import React, { createContext, useContext, useEffect } from 'react';
import { useEnhancedVoice } from '@/hooks/useEnhancedVoice';
interface EnhancedVoiceContextType {
isListening: boolean;
isSupported: boolean;
isProcessing: boolean;
currentTranscript: string;
voiceCommands: any[];
settings: any;
startListening: () => void;
stopListening: () => void;
toggleListening: () => void;
speak: (text: string, options?: any) => void;
executeVoiceCommand: (command: string) => Promise<void>;
updateSettings: (settings: any) => void;
}
const EnhancedVoiceContext = createContext<EnhancedVoiceContextType | undefined>(undefined);
export function EnhancedVoiceProvider({ children }: { children: React.ReactNode }) {
const voiceSystem = useEnhancedVoice();
// Initialize voice system on mount
useEffect(() => {
if (voiceSystem.isSupported) {
console.log('Enhanced voice system initialized');
}
}, [voiceSystem.isSupported]);
return (
<EnhancedVoiceContext.Provider value={voiceSystem}>
{children}
</EnhancedVoiceContext.Provider>
);
}
export function useEnhancedVoiceContext() {
const context = useContext(EnhancedVoiceContext);
if (context === undefined) {
throw new Error('useEnhancedVoiceContext must be used within an EnhancedVoiceProvider');
}
return context;
}
+333 -20
View File
@@ -1,15 +1,21 @@
import React, { createContext, useContext, useState, useEffect } from "react";
import { useVoice } from "@/hooks/useVoice";
import { useLocation } from "wouter";
import { useQueryClient } from "@tanstack/react-query";
interface VoiceContextType {
isListening: boolean;
isSupported: boolean;
isProcessing: boolean;
currentTranscript: string;
language: 'en' | 'ar';
startListening: () => void;
stopListening: () => void;
toggleListening: () => void;
setLanguage: (lang: 'en' | 'ar') => void;
executeVoiceCommand: (command: string) => Promise<void>;
speak: (text: string, lang?: 'en' | 'ar') => void;
speakAndExecute: (text: string, command?: () => Promise<void>) => Promise<void>;
}
const VoiceContext = createContext<VoiceContextType | undefined>(undefined);
@@ -17,41 +23,348 @@ const VoiceContext = createContext<VoiceContextType | undefined>(undefined);
export function VoiceProvider({ children }: { children: React.ReactNode }) {
const { isListening, startListening, stopListening, isSupported } = useVoice();
const [language, setLanguage] = useState<'en' | 'ar'>('en');
const [isProcessing, setIsProcessing] = useState(false);
const [currentTranscript, setCurrentTranscript] = useState('');
const [, setLocation] = useLocation();
const queryClient = useQueryClient();
const executeVoiceCommand = async (command: string) => {
setIsProcessing(true);
setCurrentTranscript(command);
try {
// Process voice commands for navigation and actions
const lowerCommand = command.toLowerCase();
const lowerCommand = command.toLowerCase().trim();
if (lowerCommand.includes('navigate') || lowerCommand.includes('go to')) {
// Handle navigation commands
if (lowerCommand.includes('dashboard')) {
window.location.href = '/dashboard';
} else if (lowerCommand.includes('tasks')) {
window.location.href = '/dashboard?tab=tasks';
} else if (lowerCommand.includes('finance')) {
window.location.href = '/dashboard?tab=finance';
// Navigation commands with immediate feedback
if (lowerCommand.includes('go to') || lowerCommand.includes('navigate') || lowerCommand.includes('open')) {
if (lowerCommand.includes('dashboard') || lowerCommand.includes('home')) {
setLocation('/dashboard');
speak('Opening dashboard');
} else if (lowerCommand.includes('task') && !lowerCommand.includes('create')) {
setLocation('/tasks');
speak('Opening tasks');
} else if (lowerCommand.includes('finance') || lowerCommand.includes('money')) {
setLocation('/finances');
speak('Opening finances');
} else if (lowerCommand.includes('voice')) {
setLocation('/voice');
speak('Opening voice commands');
} else if (lowerCommand.includes('ai') || lowerCommand.includes('chat')) {
setLocation('/ai');
speak('Opening AI assistant');
} else if (lowerCommand.includes('analytics')) {
setLocation('/analytics');
speak('Opening analytics');
} else if (lowerCommand.includes('settings')) {
setLocation('/settings');
speak('Opening settings');
}
} else if (lowerCommand.includes('create task') || lowerCommand.includes('أنشئ مهمة')) {
// Handle task creation
// This would integrate with your task creation API
console.log('Voice task creation:', command);
} else if (lowerCommand.includes('book appointment') || lowerCommand.includes('احجز موعد')) {
// Handle appointment booking
console.log('Voice appointment booking:', command);
return;
}
// Task management with API integration
if (lowerCommand.includes('create task') || lowerCommand.includes('new task') || lowerCommand.includes('add task')) {
const taskMatch = command.match(/(?:create task|new task|add task)\s+(.+)/i);
if (taskMatch) {
await createTaskByVoice(taskMatch[1].trim(), command);
} else {
speak('What task would you like to create?');
}
return;
}
if (lowerCommand.includes('complete task') || lowerCommand.includes('finish task')) {
const taskMatch = command.match(/(?:complete task|finish task)\s+(.+)/i);
if (taskMatch) {
await completeTaskByVoice(taskMatch[1].trim());
} else {
speak('Which task would you like to complete?');
}
return;
}
// Financial management with real-time updates
if (lowerCommand.includes('add expense') || lowerCommand.includes('record expense')) {
const amountMatch = command.match(/(\d+(?:\.\d{2})?)/);
if (amountMatch) {
const description = extractDescription(command);
await addExpenseByVoice(parseFloat(amountMatch[1]), description);
} else {
speak('How much was the expense?');
}
return;
}
if (lowerCommand.includes('add income') || lowerCommand.includes('record income')) {
const amountMatch = command.match(/(\d+(?:\.\d{2})?)/);
if (amountMatch) {
const description = extractDescription(command);
await addIncomeByVoice(parseFloat(amountMatch[1]), description);
} else {
speak('How much income would you like to record?');
}
return;
}
// AI interactions
if (lowerCommand.includes('tell me a joke') || lowerCommand.includes('joke')) {
await getJokeByVoice();
return;
}
if (lowerCommand.includes('help') || lowerCommand.includes('commands') || lowerCommand.includes('what can i say')) {
speakAvailableCommands();
return;
}
// General AI chat processing
await processAIChat(command);
} catch (error) {
console.error('Voice command execution error:', error);
speak('Sorry, I had trouble processing that command. Please try again.');
} finally {
setIsProcessing(false);
}
};
// Voice command processing functions
const createTaskByVoice = async (title: string, fullCommand: string) => {
try {
const priority = extractPriority(fullCommand) || 'medium';
const dueDate = extractDueDate(fullCommand);
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title,
description: `Created by voice: "${fullCommand}"`,
priority,
dueDate,
}),
});
if (response.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
speak(`Task "${title}" created successfully with ${priority} priority`);
} else {
speak('Failed to create task. Please try again.');
}
} catch (error) {
speak('There was an error creating the task.');
}
};
const completeTaskByVoice = async (taskTitle: string) => {
try {
const tasksResponse = await fetch('/api/tasks', { credentials: 'include' });
if (tasksResponse.ok) {
const { tasks } = await tasksResponse.json();
const matchingTask = tasks.find((task: any) =>
task.title.toLowerCase().includes(taskTitle.toLowerCase())
);
if (matchingTask) {
const updateResponse = await fetch(`/api/tasks/${matchingTask.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ status: 'completed' }),
});
if (updateResponse.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
speak(`Task "${matchingTask.title}" marked as completed`);
}
} else {
speak(`Could not find a task matching "${taskTitle}"`);
}
}
} catch (error) {
speak('There was an error completing the task.');
}
};
const addExpenseByVoice = async (amount: number, description: string) => {
try {
const response = await fetch('/api/financial/records', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
type: 'expense',
amount,
description: description || 'Voice recorded expense',
category: 'general',
}),
});
if (response.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/financial/records'] });
queryClient.invalidateQueries({ queryKey: ['/api/financial/summary'] });
speak(`Expense of $${amount} recorded successfully`);
}
} catch (error) {
speak('There was an error recording the expense.');
}
};
const addIncomeByVoice = async (amount: number, description: string) => {
try {
const response = await fetch('/api/financial/records', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
type: 'income',
amount,
description: description || 'Voice recorded income',
category: 'general',
}),
});
if (response.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/financial/records'] });
queryClient.invalidateQueries({ queryKey: ['/api/financial/summary'] });
speak(`Income of $${amount} recorded successfully`);
}
} catch (error) {
speak('There was an error recording the income.');
}
};
const getJokeByVoice = async () => {
try {
const response = await fetch('/api/ai/daily-joke', { credentials: 'include' });
if (response.ok) {
const { joke } = await response.json();
speak(joke);
} else {
speak('Sorry, I could not get a joke right now.');
}
} catch (error) {
speak('There was an error getting a joke.');
}
};
const processAIChat = async (message: string) => {
try {
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ message }),
});
if (response.ok) {
const { content } = await response.json();
speak(content);
} else {
speak('I did not understand that command. Say "help" to hear available commands.');
}
} catch (error) {
speak('I did not understand that command. Say "help" to hear available commands.');
}
};
const speakAvailableCommands = () => {
const commands = [
'You can say:',
'Go to dashboard, tasks, finances, voice, AI, analytics, or settings',
'Create task followed by the task name',
'Complete task followed by the task name',
'Add expense or income followed by the amount',
'Tell me a joke',
'Or ask me any question'
].join('. ');
speak(commands);
};
// Utility functions
const extractDescription = (command: string): string => {
const patterns = [
/(?:for|on)\s+(.+)/i,
/(?:expense|income)\s+(?:of\s+)?(?:\$?\d+(?:\.\d{2})?)\s+(?:for|on)\s+(.+)/i,
];
for (const pattern of patterns) {
const match = command.match(pattern);
if (match) return match[1].trim();
}
return '';
};
const extractPriority = (command: string): 'low' | 'medium' | 'high' | null => {
if (command.toLowerCase().includes('high priority') || command.toLowerCase().includes('urgent')) {
return 'high';
}
if (command.toLowerCase().includes('low priority')) {
return 'low';
}
return null;
};
const extractDueDate = (command: string): string | null => {
const today = new Date();
if (command.toLowerCase().includes('today')) {
return today.toISOString().split('T')[0];
}
if (command.toLowerCase().includes('tomorrow')) {
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
return tomorrow.toISOString().split('T')[0];
}
if (command.toLowerCase().includes('next week')) {
const nextWeek = new Date(today);
nextWeek.setDate(nextWeek.getDate() + 7);
return nextWeek.toISOString().split('T')[0];
}
return null;
};
const speak = (text: string, lang: 'en' | 'ar' = language) => {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = lang === 'ar' ? 'ar-OM' : 'en-US'; // Omani Arabic support
utterance.rate = 0.9;
utterance.lang = lang === 'ar' ? 'ar-OM' : 'en-US';
utterance.rate = 1.1; // Faster for better UX
utterance.pitch = 1;
speechSynthesis.speak(utterance);
utterance.volume = 0.8;
// Use enhanced voice if available
const voices = window.speechSynthesis.getVoices();
const preferredVoice = voices.find(voice =>
voice.lang.startsWith(lang === 'ar' ? 'ar' : 'en') &&
(voice.name.includes('Enhanced') || voice.name.includes('Premium'))
) || voices.find(voice => voice.lang.startsWith(lang === 'ar' ? 'ar' : 'en'));
if (preferredVoice) {
utterance.voice = preferredVoice;
}
window.speechSynthesis.speak(utterance);
}
};
const toggleListening = () => {
if (isListening) {
stopListening();
} else {
startListening();
}
};
const speakAndExecute = async (text: string, command?: () => Promise<void>) => {
speak(text);
if (command) {
await command();
}
};
+574
View File
@@ -0,0 +1,574 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useLocation } from 'wouter';
import { useQueryClient } from '@tanstack/react-query';
import { useToast } from '@/hooks/use-toast';
// Extend Window interface for speech recognition
declare global {
interface Window {
SpeechRecognition: any;
webkitSpeechRecognition: any;
}
}
interface VoiceCommand {
command: string;
confidence: number;
timestamp: Date;
processed: boolean;
}
interface VoiceSettings {
language: 'en-US' | 'ar-SA';
continuous: boolean;
interimResults: boolean;
maxAlternatives: number;
confidenceThreshold: number;
}
export function useEnhancedVoice() {
const [isListening, setIsListening] = useState(false);
const [isSupported, setIsSupported] = useState(false);
const [currentTranscript, setCurrentTranscript] = useState('');
const [voiceCommands, setVoiceCommands] = useState<VoiceCommand[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [, setLocation] = useLocation();
const queryClient = useQueryClient();
const { toast } = useToast();
const recognitionRef = useRef<SpeechRecognition | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const commandQueueRef = useRef<string[]>([]);
const [settings, setSettings] = useState<VoiceSettings>({
language: 'en-US',
continuous: true,
interimResults: true,
maxAlternatives: 3,
confidenceThreshold: 0.7,
});
// Initialize speech recognition
useEffect(() => {
if (typeof window !== 'undefined') {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SpeechRecognition) {
setIsSupported(true);
recognitionRef.current = new SpeechRecognition();
setupRecognition();
}
}
}, []);
const setupRecognition = useCallback(() => {
if (!recognitionRef.current) return;
const recognition = recognitionRef.current;
recognition.continuous = settings.continuous;
recognition.interimResults = settings.interimResults;
recognition.lang = settings.language;
recognition.maxAlternatives = settings.maxAlternatives;
recognition.onstart = () => {
setIsListening(true);
console.log('Voice recognition started');
};
recognition.onend = () => {
setIsListening(false);
console.log('Voice recognition ended');
};
recognition.onerror = (event) => {
console.error('Voice recognition error:', event.error);
setIsListening(false);
if (event.error === 'no-speech') {
toast({
title: "No speech detected",
description: "Please try speaking again",
variant: "default",
});
}
};
recognition.onresult = (event) => {
let finalTranscript = '';
let interimTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
const transcript = result[0].transcript;
if (result.isFinal) {
finalTranscript += transcript;
if (result[0].confidence >= settings.confidenceThreshold) {
processVoiceCommand(transcript, result[0].confidence);
}
} else {
interimTranscript += transcript;
}
}
setCurrentTranscript(finalTranscript || interimTranscript);
};
}, [settings, toast]);
const processVoiceCommand = useCallback(async (command: string, confidence: number) => {
const voiceCommand: VoiceCommand = {
command,
confidence,
timestamp: new Date(),
processed: false,
};
setVoiceCommands(prev => [...prev, voiceCommand]);
commandQueueRef.current.push(command);
if (!isProcessing) {
executeCommandQueue();
}
}, [isProcessing]);
const executeCommandQueue = useCallback(async () => {
if (commandQueueRef.current.length === 0) return;
setIsProcessing(true);
while (commandQueueRef.current.length > 0) {
const command = commandQueueRef.current.shift();
if (command) {
await executeVoiceCommand(command);
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay between commands
}
}
setIsProcessing(false);
}, []);
const executeVoiceCommand = useCallback(async (command: string) => {
const lowerCommand = command.toLowerCase().trim();
try {
// Navigation commands
if (lowerCommand.includes('go to') || lowerCommand.includes('navigate') || lowerCommand.includes('open')) {
if (lowerCommand.includes('dashboard') || lowerCommand.includes('home')) {
setLocation('/dashboard');
speak('Navigating to dashboard');
} else if (lowerCommand.includes('task') && !lowerCommand.includes('create')) {
setLocation('/dashboard?tab=tasks');
speak('Opening tasks');
} else if (lowerCommand.includes('finance') || lowerCommand.includes('money')) {
setLocation('/dashboard?tab=finance');
speak('Opening finances');
} else if (lowerCommand.includes('voice') || lowerCommand.includes('command')) {
setLocation('/dashboard?tab=voice');
speak('Opening voice commands');
} else if (lowerCommand.includes('ai') || lowerCommand.includes('chat')) {
setLocation('/dashboard?tab=ai');
speak('Opening AI assistant');
}
return;
}
// Task management commands
if (lowerCommand.includes('create task') || lowerCommand.includes('new task') || lowerCommand.includes('add task')) {
const taskTitle = extractTaskTitle(command);
if (taskTitle) {
await createTaskByVoice(taskTitle, command);
} else {
speak('What task would you like to create?');
}
return;
}
if (lowerCommand.includes('complete task') || lowerCommand.includes('finish task')) {
const taskTitle = extractTaskTitle(command);
if (taskTitle) {
await completeTaskByVoice(taskTitle);
} else {
speak('Which task would you like to complete?');
}
return;
}
// Financial commands
if (lowerCommand.includes('add expense') || lowerCommand.includes('record expense')) {
const amount = extractAmount(command);
const description = extractDescription(command);
if (amount) {
await addExpenseByVoice(amount, description);
} else {
speak('How much was the expense?');
}
return;
}
if (lowerCommand.includes('add income') || lowerCommand.includes('record income')) {
const amount = extractAmount(command);
const description = extractDescription(command);
if (amount) {
await addIncomeByVoice(amount, description);
} else {
speak('How much income would you like to record?');
}
return;
}
// General AI commands
if (lowerCommand.includes('tell me a joke') || lowerCommand.includes('joke')) {
await getJokeByVoice();
return;
}
if (lowerCommand.includes('what can i say') || lowerCommand.includes('help') || lowerCommand.includes('commands')) {
speakAvailableCommands();
return;
}
// If no specific command matched, try general AI processing
await processGeneralCommand(command);
} catch (error) {
console.error('Error executing voice command:', error);
speak('Sorry, I had trouble processing that command.');
}
}, [setLocation, queryClient]);
// Voice command processing functions
const createTaskByVoice = async (title: string, fullCommand: string) => {
try {
const priority = extractPriority(fullCommand) || 'medium';
const dueDate = extractDueDate(fullCommand);
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title,
description: `Created by voice: "${fullCommand}"`,
priority,
dueDate,
}),
});
if (response.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
speak(`Task "${title}" created successfully with ${priority} priority`);
toast({
title: "Task Created",
description: `"${title}" has been added to your tasks`,
});
} else {
speak('Failed to create task. Please try again.');
}
} catch (error) {
speak('There was an error creating the task.');
}
};
const completeTaskByVoice = async (taskTitle: string) => {
try {
// First get all tasks to find the matching one
const tasksResponse = await fetch('/api/tasks', {
credentials: 'include',
});
if (tasksResponse.ok) {
const { tasks } = await tasksResponse.json();
const matchingTask = tasks.find((task: any) =>
task.title.toLowerCase().includes(taskTitle.toLowerCase())
);
if (matchingTask) {
const updateResponse = await fetch(`/api/tasks/${matchingTask.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ status: 'completed' }),
});
if (updateResponse.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
speak(`Task "${matchingTask.title}" marked as completed`);
toast({
title: "Task Completed",
description: `"${matchingTask.title}" has been completed`,
});
}
} else {
speak(`Could not find a task matching "${taskTitle}"`);
}
}
} catch (error) {
speak('There was an error completing the task.');
}
};
const addExpenseByVoice = async (amount: number, description: string) => {
try {
const response = await fetch('/api/financial/records', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
type: 'expense',
amount,
description: description || 'Voice recorded expense',
category: 'general',
}),
});
if (response.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/financial/records'] });
queryClient.invalidateQueries({ queryKey: ['/api/financial/summary'] });
speak(`Expense of $${amount} recorded successfully`);
toast({
title: "Expense Recorded",
description: `$${amount} expense has been added`,
});
}
} catch (error) {
speak('There was an error recording the expense.');
}
};
const addIncomeByVoice = async (amount: number, description: string) => {
try {
const response = await fetch('/api/financial/records', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
type: 'income',
amount,
description: description || 'Voice recorded income',
category: 'general',
}),
});
if (response.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/financial/records'] });
queryClient.invalidateQueries({ queryKey: ['/api/financial/summary'] });
speak(`Income of $${amount} recorded successfully`);
toast({
title: "Income Recorded",
description: `$${amount} income has been added`,
});
}
} catch (error) {
speak('There was an error recording the income.');
}
};
const getJokeByVoice = async () => {
try {
const response = await fetch('/api/ai/daily-joke', {
credentials: 'include',
});
if (response.ok) {
const { joke } = await response.json();
speak(joke);
} else {
speak('Sorry, I could not get a joke right now.');
}
} catch (error) {
speak('There was an error getting a joke.');
}
};
const processGeneralCommand = async (command: string) => {
try {
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ message: command }),
});
if (response.ok) {
const { content } = await response.json();
speak(content);
} else {
speak('I did not understand that command. Say "help" to hear available commands.');
}
} catch (error) {
speak('I did not understand that command. Say "help" to hear available commands.');
}
};
const speakAvailableCommands = () => {
const commands = [
'You can say:',
'Go to dashboard, tasks, finances, voice, or AI',
'Create task followed by the task name',
'Complete task followed by the task name',
'Add expense or income followed by the amount',
'Tell me a joke',
'Or ask me any question'
].join('. ');
speak(commands);
};
// Utility functions for command parsing
const extractTaskTitle = (command: string): string | null => {
const patterns = [
/(?:create task|new task|add task)\s+(.+)/i,
/(?:complete task|finish task)\s+(.+)/i,
];
for (const pattern of patterns) {
const match = command.match(pattern);
if (match) return match[1].trim();
}
return null;
};
const extractAmount = (command: string): number | null => {
const patterns = [
/\$(\d+(?:\.\d{2})?)/,
/(\d+(?:\.\d{2})?)\s*dollars?/i,
/(\d+(?:\.\d{2})?)/,
];
for (const pattern of patterns) {
const match = command.match(pattern);
if (match) {
const amount = parseFloat(match[1]);
if (!isNaN(amount) && amount > 0) return amount;
}
}
return null;
};
const extractDescription = (command: string): string => {
const patterns = [
/(?:for|on)\s+(.+)/i,
/(?:expense|income)\s+(?:of\s+)?(?:\$?\d+(?:\.\d{2})?)\s+(?:for|on)\s+(.+)/i,
];
for (const pattern of patterns) {
const match = command.match(pattern);
if (match) return match[1].trim();
}
return '';
};
const extractPriority = (command: string): 'low' | 'medium' | 'high' | null => {
if (command.toLowerCase().includes('high priority') || command.toLowerCase().includes('urgent')) {
return 'high';
}
if (command.toLowerCase().includes('low priority')) {
return 'low';
}
return null;
};
const extractDueDate = (command: string): string | null => {
const today = new Date();
if (command.toLowerCase().includes('today')) {
return today.toISOString().split('T')[0];
}
if (command.toLowerCase().includes('tomorrow')) {
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
return tomorrow.toISOString().split('T')[0];
}
if (command.toLowerCase().includes('next week')) {
const nextWeek = new Date(today);
nextWeek.setDate(nextWeek.getDate() + 7);
return nextWeek.toISOString().split('T')[0];
}
return null;
};
const speak = useCallback((text: string, options?: { rate?: number; pitch?: number; volume?: number }) => {
if ('speechSynthesis' in window) {
// Cancel any ongoing speech
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = settings.language;
utterance.rate = options?.rate || 1.1; // Slightly faster for better UX
utterance.pitch = options?.pitch || 1;
utterance.volume = options?.volume || 0.8;
// Use a more natural voice if available
const voices = window.speechSynthesis.getVoices();
const preferredVoice = voices.find(voice =>
voice.lang.startsWith(settings.language.split('-')[0]) &&
(voice.name.includes('Enhanced') || voice.name.includes('Premium'))
) || voices.find(voice => voice.lang.startsWith(settings.language.split('-')[0]));
if (preferredVoice) {
utterance.voice = preferredVoice;
}
window.speechSynthesis.speak(utterance);
}
}, [settings.language]);
const startListening = useCallback(() => {
if (recognitionRef.current && !isListening) {
setCurrentTranscript('');
recognitionRef.current.start();
}
}, [isListening]);
const stopListening = useCallback(() => {
if (recognitionRef.current && isListening) {
recognitionRef.current.stop();
}
}, [isListening]);
const toggleListening = useCallback(() => {
if (isListening) {
stopListening();
} else {
startListening();
}
}, [isListening, startListening, stopListening]);
const updateSettings = useCallback((newSettings: Partial<VoiceSettings>) => {
setSettings(prev => ({ ...prev, ...newSettings }));
}, []);
// Auto-restart listening if it stops unexpectedly
useEffect(() => {
if (isListening && recognitionRef.current) {
const recognition = recognitionRef.current;
const handleEnd = () => {
if (isListening) {
// Restart after a short delay
setTimeout(() => {
if (isListening) {
recognition.start();
}
}, 1000);
}
};
recognition.addEventListener('end', handleEnd);
return () => recognition.removeEventListener('end', handleEnd);
}
}, [isListening]);
return {
isListening,
isSupported,
isProcessing,
currentTranscript,
voiceCommands,
settings,
startListening,
stopListening,
toggleListening,
speak,
executeVoiceCommand,
updateSettings,
};
}
+338
View File
@@ -0,0 +1,338 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useLocation } from 'wouter';
import { useQueryClient } from '@tanstack/react-query';
import { useToast } from '@/hooks/use-toast';
interface VoiceSettings {
language: string;
continuous: boolean;
interimResults: boolean;
confidenceThreshold: number;
}
export function useVoiceIntegration() {
const [isListening, setIsListening] = useState(false);
const [isSupported, setIsSupported] = useState(false);
const [currentTranscript, setCurrentTranscript] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [, setLocation] = useLocation();
const queryClient = useQueryClient();
const { toast } = useToast();
const recognitionRef = useRef<any>(null);
const [settings] = useState<VoiceSettings>({
language: 'en-US',
continuous: true,
interimResults: true,
confidenceThreshold: 0.7,
});
// Initialize speech recognition
useEffect(() => {
if (typeof window !== 'undefined') {
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
if (SpeechRecognition) {
setIsSupported(true);
recognitionRef.current = new SpeechRecognition();
setupRecognition();
}
}
}, []);
const setupRecognition = useCallback(() => {
if (!recognitionRef.current) return;
const recognition = recognitionRef.current;
recognition.continuous = settings.continuous;
recognition.interimResults = settings.interimResults;
recognition.lang = settings.language;
recognition.maxAlternatives = 3;
recognition.onstart = () => {
setIsListening(true);
};
recognition.onend = () => {
setIsListening(false);
};
recognition.onerror = (event: any) => {
console.error('Voice recognition error:', event.error);
setIsListening(false);
};
recognition.onresult = (event: any) => {
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
const transcript = result[0].transcript;
if (result.isFinal) {
finalTranscript += transcript;
if (result[0].confidence >= settings.confidenceThreshold) {
processVoiceCommand(transcript);
}
}
}
if (finalTranscript) {
setCurrentTranscript(finalTranscript);
}
};
}, [settings]);
const processVoiceCommand = useCallback(async (command: string) => {
setIsProcessing(true);
await executeVoiceCommand(command);
setIsProcessing(false);
}, []);
const executeVoiceCommand = useCallback(async (command: string) => {
const lowerCommand = command.toLowerCase().trim();
try {
// Navigation commands
if (lowerCommand.includes('dashboard') || lowerCommand.includes('home')) {
setLocation('/dashboard');
speak('Opening dashboard');
return;
}
if (lowerCommand.includes('task') && !lowerCommand.includes('create')) {
setLocation('/dashboard');
// Trigger tasks tab if possible
speak('Opening tasks');
return;
}
if (lowerCommand.includes('finance') || lowerCommand.includes('money')) {
setLocation('/dashboard');
speak('Opening finances');
return;
}
// Task commands
if (lowerCommand.includes('create task') || lowerCommand.includes('new task')) {
const taskMatch = command.match(/(?:create task|new task)\s+(.+)/i);
if (taskMatch) {
await createTask(taskMatch[1].trim());
} else {
speak('What task would you like to create?');
}
return;
}
// Financial commands
if (lowerCommand.includes('add expense')) {
const amountMatch = command.match(/(\d+(?:\.\d{2})?)/);
if (amountMatch) {
await addExpense(parseFloat(amountMatch[1]), command);
} else {
speak('How much was the expense?');
}
return;
}
if (lowerCommand.includes('add income')) {
const amountMatch = command.match(/(\d+(?:\.\d{2})?)/);
if (amountMatch) {
await addIncome(parseFloat(amountMatch[1]), command);
} else {
speak('How much income would you like to record?');
}
return;
}
// AI commands
if (lowerCommand.includes('joke')) {
await getJoke();
return;
}
if (lowerCommand.includes('help') || lowerCommand.includes('commands')) {
speakHelp();
return;
}
// General AI chat
await processAIChat(command);
} catch (error) {
console.error('Error executing voice command:', error);
speak('Sorry, I had trouble processing that command.');
}
}, [setLocation, queryClient]);
const createTask = async (title: string) => {
try {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
title,
description: 'Created by voice command',
priority: 'medium',
}),
});
if (response.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/tasks'] });
speak(`Task "${title}" created successfully`);
toast({
title: "Task Created",
description: `"${title}" has been added`,
});
} else {
speak('Failed to create task');
}
} catch (error) {
speak('Error creating task');
}
};
const addExpense = async (amount: number, description: string) => {
try {
const response = await fetch('/api/financial/records', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
type: 'expense',
amount,
description: description || 'Voice expense',
category: 'general',
}),
});
if (response.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/financial/records'] });
queryClient.invalidateQueries({ queryKey: ['/api/financial/summary'] });
speak(`Expense of $${amount} recorded`);
toast({
title: "Expense Added",
description: `$${amount} expense recorded`,
});
}
} catch (error) {
speak('Error recording expense');
}
};
const addIncome = async (amount: number, description: string) => {
try {
const response = await fetch('/api/financial/records', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
type: 'income',
amount,
description: description || 'Voice income',
category: 'general',
}),
});
if (response.ok) {
queryClient.invalidateQueries({ queryKey: ['/api/financial/records'] });
queryClient.invalidateQueries({ queryKey: ['/api/financial/summary'] });
speak(`Income of $${amount} recorded`);
toast({
title: "Income Added",
description: `$${amount} income recorded`,
});
}
} catch (error) {
speak('Error recording income');
}
};
const getJoke = async () => {
try {
const response = await fetch('/api/ai/daily-joke', {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
speak(data.joke);
}
} catch (error) {
speak('Could not get a joke right now');
}
};
const processAIChat = async (message: string) => {
try {
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ message }),
});
if (response.ok) {
const data = await response.json();
speak(data.content);
} else {
speak('I did not understand that command');
}
} catch (error) {
speak('I did not understand that command');
}
};
const speakHelp = () => {
const helpText = `You can say: Go to dashboard, Create task followed by the task name, Add expense or income with amount, Tell me a joke, or ask me any question`;
speak(helpText);
};
const speak = useCallback((text: string) => {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = settings.language;
utterance.rate = 1.1;
utterance.pitch = 1;
utterance.volume = 0.8;
window.speechSynthesis.speak(utterance);
}
}, [settings.language]);
const startListening = useCallback(() => {
if (recognitionRef.current && !isListening) {
setCurrentTranscript('');
recognitionRef.current.start();
}
}, [isListening]);
const stopListening = useCallback(() => {
if (recognitionRef.current && isListening) {
recognitionRef.current.stop();
}
}, [isListening]);
const toggleListening = useCallback(() => {
if (isListening) {
stopListening();
} else {
startListening();
}
}, [isListening, startListening, stopListening]);
return {
isListening,
isSupported,
isProcessing,
currentTranscript,
startListening,
stopListening,
toggleListening,
speak,
executeVoiceCommand,
};
}