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:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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) => {
|
||||||
try {
|
setIsProcessing(true);
|
||||||
// Process voice commands for navigation and actions
|
setCurrentTranscript(command);
|
||||||
const lowerCommand = command.toLowerCase();
|
|
||||||
|
|
||||||
if (lowerCommand.includes('navigate') || lowerCommand.includes('go to')) {
|
try {
|
||||||
// Handle navigation commands
|
const lowerCommand = command.toLowerCase().trim();
|
||||||
if (lowerCommand.includes('dashboard')) {
|
|
||||||
window.location.href = '/dashboard';
|
// Navigation commands with immediate feedback
|
||||||
} else if (lowerCommand.includes('tasks')) {
|
if (lowerCommand.includes('go to') || lowerCommand.includes('navigate') || lowerCommand.includes('open')) {
|
||||||
window.location.href = '/dashboard?tab=tasks';
|
if (lowerCommand.includes('dashboard') || lowerCommand.includes('home')) {
|
||||||
} else if (lowerCommand.includes('finance')) {
|
setLocation('/dashboard');
|
||||||
window.location.href = '/dashboard?tab=finance';
|
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('أنشئ مهمة')) {
|
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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Generated
+344
-25
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user