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 React, { createContext, useContext, useState, useEffect } from "react";
import { useVoice } from "@/hooks/useVoice"; import { useVoice } from "@/hooks/useVoice";
import { useLocation } from "wouter";
import { useQueryClient } from "@tanstack/react-query";
interface VoiceContextType { interface VoiceContextType {
isListening: boolean; isListening: boolean;
isSupported: boolean; isSupported: boolean;
isProcessing: boolean;
currentTranscript: string;
language: 'en' | 'ar'; language: 'en' | 'ar';
startListening: () => void; startListening: () => void;
stopListening: () => void; stopListening: () => void;
toggleListening: () => void;
setLanguage: (lang: 'en' | 'ar') => void; setLanguage: (lang: 'en' | 'ar') => void;
executeVoiceCommand: (command: string) => Promise<void>; executeVoiceCommand: (command: string) => Promise<void>;
speak: (text: string, lang?: 'en' | 'ar') => void; speak: (text: string, lang?: 'en' | 'ar') => void;
speakAndExecute: (text: string, command?: () => Promise<void>) => Promise<void>;
} }
const VoiceContext = createContext<VoiceContextType | undefined>(undefined); const VoiceContext = createContext<VoiceContextType | undefined>(undefined);
@@ -17,41 +23,348 @@ const VoiceContext = createContext<VoiceContextType | undefined>(undefined);
export function VoiceProvider({ children }: { children: React.ReactNode }) { export function VoiceProvider({ children }: { children: React.ReactNode }) {
const { isListening, startListening, stopListening, isSupported } = useVoice(); const { isListening, startListening, stopListening, isSupported } = useVoice();
const [language, setLanguage] = useState<'en' | 'ar'>('en'); 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) => { const executeVoiceCommand = async (command: string) => {
setIsProcessing(true);
setCurrentTranscript(command);
try { try {
// Process voice commands for navigation and actions const lowerCommand = command.toLowerCase().trim();
const lowerCommand = command.toLowerCase();
if (lowerCommand.includes('navigate') || lowerCommand.includes('go to')) { // Navigation commands with immediate feedback
// Handle navigation commands if (lowerCommand.includes('go to') || lowerCommand.includes('navigate') || lowerCommand.includes('open')) {
if (lowerCommand.includes('dashboard')) { if (lowerCommand.includes('dashboard') || lowerCommand.includes('home')) {
window.location.href = '/dashboard'; setLocation('/dashboard');
} else if (lowerCommand.includes('tasks')) { speak('Opening dashboard');
window.location.href = '/dashboard?tab=tasks'; } else if (lowerCommand.includes('task') && !lowerCommand.includes('create')) {
} else if (lowerCommand.includes('finance')) { setLocation('/tasks');
window.location.href = '/dashboard?tab=finance'; 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('أنشئ مهمة')) { return;
// 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);
} }
// 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) { } catch (error) {
console.error('Voice command execution error:', 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) => { const speak = (text: string, lang: 'en' | 'ar' = language) => {
if ('speechSynthesis' in window) { if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text); const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = lang === 'ar' ? 'ar-OM' : 'en-US'; // Omani Arabic support utterance.lang = lang === 'ar' ? 'ar-OM' : 'en-US';
utterance.rate = 0.9; utterance.rate = 1.1; // Faster for better UX
utterance.pitch = 1; 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,
};
}
+344 -25
View File
@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^3.10.0",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"@neondatabase/serverless": "^0.10.4", "@neondatabase/serverless": "^0.10.4",
@@ -58,6 +59,7 @@
"multer": "^2.0.0", "multer": "^2.0.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"openai": "^5.1.1",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"react": "^18.3.1", "react": "^18.3.1",
@@ -130,6 +132,36 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@anthropic-ai/sdk": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.37.0.tgz",
"integrity": "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==",
"license": "MIT",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
}
},
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
"version": "18.19.111",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz",
"integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.26.2", "version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@@ -3483,6 +3515,16 @@
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
} }
}, },
"node_modules/@types/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@types/passport": { "node_modules/@types/passport": {
"version": "1.0.17", "version": "1.0.17",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
@@ -3622,6 +3664,18 @@
"vite": "^4.2.0 || ^5.0.0" "vite": "^4.2.0 || ^5.0.0"
} }
}, },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -3635,6 +3689,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
@@ -3708,6 +3774,12 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.20", "version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -3925,6 +3997,19 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/camelcase-css": { "node_modules/camelcase-css": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -4043,6 +4128,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -4330,6 +4427,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -4965,6 +5071,20 @@
"zod": ">=3.0.0" "zod": ">=3.0.0"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -5038,13 +5158,10 @@
} }
}, },
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT", "license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@@ -5058,6 +5175,33 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.0", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
@@ -5137,6 +5281,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -5359,6 +5512,41 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/form-data": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
"license": "MIT"
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"license": "MIT",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -5452,16 +5640,21 @@
} }
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"has-proto": "^1.0.1", "get-proto": "^1.0.1",
"has-symbols": "^1.0.3", "gopd": "^1.2.0",
"hasown": "^2.0.0" "has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5478,6 +5671,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-tsconfig": { "node_modules/get-tsconfig": {
"version": "4.8.1", "version": "4.8.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
@@ -5534,12 +5740,12 @@
} }
}, },
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT", "license": "MIT",
"dependencies": { "engines": {
"get-intrinsic": "^1.1.3" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -5563,10 +5769,10 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-proto": { "node_modules/has-symbols": {
"version": "1.0.3", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5575,11 +5781,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-symbols": { "node_modules/has-tostringtag": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT", "license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -5615,6 +5824,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -6105,6 +6323,15 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -6354,6 +6581,46 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
} }
}, },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-gyp-build": { "node_modules/node-gyp-build": {
"version": "4.8.3", "version": "4.8.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
@@ -6449,6 +6716,27 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/openai": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.1.1.tgz",
"integrity": "sha512-lgIdLqvpLpz8xPUKcEIV6ml+by74mbSBz8zv/AHHebtLn/WdpH4kdXT3/Q5uUKDHg3vHV/z9+G9wZINRX6rkDg==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/package-json-from-dist": { "node_modules/package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -7914,6 +8202,12 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-interface-checker": { "node_modules/ts-interface-checker": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -9091,6 +9385,31 @@
"@esbuild/win32-x64": "0.21.5" "@esbuild/win32-x64": "0.21.5"
} }
}, },
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+2
View File
@@ -11,6 +11,7 @@
"db:push": "drizzle-kit push" "db:push": "drizzle-kit push"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^3.10.0",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"@neondatabase/serverless": "^0.10.4", "@neondatabase/serverless": "^0.10.4",
@@ -60,6 +61,7 @@
"multer": "^2.0.0", "multer": "^2.0.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"openai": "^5.1.1",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"react": "^18.3.1", "react": "^18.3.1",