Преглед изворни кода

feat(ai-config): redesign AI configuration page with voice selection and business hours

- Replace static voice options with dynamic ai_voices table selection
- Add voice sample playback with play/pause controls
- Add gender filter (male/female/neutral/all) for voice selection
- Add business hours configuration (daily hours per weekday)
- Add special hours/holidays management with date picker
- Add input validation to block emails and URLs in greeting message
- Simplify AIConfig to just voice_type and greeting_message
- Add floating save button for better UX
- Update i18n translations (hu, en, de) for all new UI elements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh пре 4 месеци
родитељ
комит
24fff05275

+ 781 - 122
shopcall.ai-main/src/components/AIConfigContent.tsx

@@ -2,67 +2,388 @@
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
+import { Input } from "@/components/ui/input";
 import { Switch } from "@/components/ui/switch";
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { Mic, MessageSquare, Store, Loader2, Bot } from "lucide-react";
-import { useState, useEffect } from "react";
+import { Mic, MessageSquare, Store, Loader2, Bot, Play, Pause, User, Users, Clock, Calendar, Trash2, Plus, X } from "lucide-react";
+import { useState, useEffect, useRef } from "react";
 import { API_URL } from "@/lib/config";
 import { useToast } from "@/hooks/use-toast";
 import { useTranslation } from "react-i18next";
 import { useShop } from "@/components/context/ShopContext";
 import { LoadingScreen } from "@/components/ui/loading-screen";
+import { supabase } from "@/lib/supabase";
+import { cn } from "@/lib/utils";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Calendar as CalendarComponent } from "@/components/ui/calendar";
+import { format } from "date-fns";
+import { hu, de, enUS } from "date-fns/locale";
+
+interface AIVoice {
+  id: string;
+  provider_voice_id: string;
+  sample_url: string;
+  gender: 'male' | 'female' | 'neutral';
+  name: string;
+}
 
 interface AIConfig {
   voice_type: string;
-  speaking_speed: string;
-  accent_language: string;
   greeting_message: string;
-  business_hours_mode: boolean;
-  local_currency_support: boolean;
-  escalation_policy: string;
 }
 
+interface DayHours {
+  is_open: boolean;
+  open: string;
+  close: string;
+}
+
+interface DailyHours {
+  monday: DayHours;
+  tuesday: DayHours;
+  wednesday: DayHours;
+  thursday: DayHours;
+  friday: DayHours;
+  saturday: DayHours;
+  sunday: DayHours;
+}
+
+interface BusinessHours {
+  id?: string;
+  store_id: string;
+  is_enabled: boolean;
+  daily_hours: DailyHours;
+}
+
+interface SpecialHours {
+  id?: string;
+  store_id: string;
+  date: string;
+  opening_time: string | null;
+  closing_time: string | null;
+  is_closed: boolean;
+  note: string | null;
+}
+
+const WEEKDAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const;
+type WeekDay = typeof WEEKDAYS[number];
+
+const TIME_OPTIONS = [
+  '00:00', '00:30', '01:00', '01:30', '02:00', '02:30', '03:00', '03:30',
+  '04:00', '04:30', '05:00', '05:30', '06:00', '06:30', '07:00', '07:30',
+  '08:00', '08:30', '09:00', '09:30', '10:00', '10:30', '11:00', '11:30',
+  '12:00', '12:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30',
+  '16:00', '16:30', '17:00', '17:30', '18:00', '18:30', '19:00', '19:30',
+  '20:00', '20:30', '21:00', '21:30', '22:00', '22:30', '23:00', '23:30'
+];
+
+const DEFAULT_DAILY_HOURS: DailyHours = {
+  monday: { is_open: true, open: '09:00', close: '18:00' },
+  tuesday: { is_open: true, open: '09:00', close: '18:00' },
+  wednesday: { is_open: true, open: '09:00', close: '18:00' },
+  thursday: { is_open: true, open: '09:00', close: '18:00' },
+  friday: { is_open: true, open: '09:00', close: '18:00' },
+  saturday: { is_open: false, open: '10:00', close: '16:00' },
+  sunday: { is_open: false, open: '10:00', close: '14:00' }
+};
+
+// Validation regex to block emails and URLs
+const INVALID_INPUT_PATTERN = /[@]|https?:\/\/|www\.|\.com|\.hu|\.net|\.org|\.eu|\.io|\.co/i;
+
+// Helper to get date-fns locale based on i18n language
+const getDateLocale = (lang: string) => {
+  switch (lang) {
+    case 'hu': return hu;
+    case 'de': return de;
+    default: return enUS;
+  }
+};
+
 export function AIConfigContent() {
-  const { t } = useTranslation();
+  const { t, i18n } = useTranslation();
   const { toast } = useToast();
   const { selectedShop, stores, isLoading } = useShop();
+  const dateLocale = getDateLocale(i18n.language);
   const [saving, setSaving] = useState(false);
+  const [voices, setVoices] = useState<AIVoice[]>([]);
+  const [voicesLoading, setVoicesLoading] = useState(true);
+  const [genderFilter, setGenderFilter] = useState<'all' | 'male' | 'female' | 'neutral'>('all');
+  const [playingVoiceId, setPlayingVoiceId] = useState<string | null>(null);
+  const audioRef = useRef<HTMLAudioElement | null>(null);
+  const [greetingError, setGreetingError] = useState<string | null>(null);
+
   const [aiConfig, setAiConfig] = useState<AIConfig>({
-    voice_type: "sarah",
-    speaking_speed: "normal",
-    accent_language: "us-english",
-    greeting_message: "",
-    business_hours_mode: true,
-    local_currency_support: true,
-    escalation_policy: "medium"
+    voice_type: "",
+    greeting_message: ""
+  });
+
+  // Business hours state
+  const [businessHours, setBusinessHours] = useState<BusinessHours>({
+    store_id: "",
+    is_enabled: false,
+    daily_hours: DEFAULT_DAILY_HOURS
   });
+  const [specialHours, setSpecialHours] = useState<SpecialHours[]>([]);
+  const [businessHoursLoading, setBusinessHoursLoading] = useState(false);
+
+  // New special hours form state
+  const [newSpecialDate, setNewSpecialDate] = useState<Date | undefined>(undefined);
+  const [newSpecialNote, setNewSpecialNote] = useState("");
+  const [newSpecialIsClosed, setNewSpecialIsClosed] = useState(false);
+  const [newSpecialOpeningTime, setNewSpecialOpeningTime] = useState("09:00");
+  const [newSpecialClosingTime, setNewSpecialClosingTime] = useState("18:00");
+  const [calendarOpen, setCalendarOpen] = useState(false);
+
+  // Load voices from database
+  useEffect(() => {
+    loadVoices();
+  }, []);
+
+  const loadVoices = async () => {
+    setVoicesLoading(true);
+    try {
+      const { data, error } = await supabase
+        .from('ai_voices')
+        .select('id, provider_voice_id, sample_url, gender, name')
+        .eq('is_enabled', true)
+        .order('name', { ascending: true });
+
+      if (error) {
+        console.error('Error loading voices:', error);
+        toast({
+          title: t('common.error'),
+          description: t('aiConfig.voiceSettings.loadError', 'Failed to load voices'),
+          variant: "destructive"
+        });
+        return;
+      }
+
+      setVoices(data || []);
+    } catch (error) {
+      console.error('Error loading voices:', error);
+    } finally {
+      setVoicesLoading(false);
+    }
+  };
+
+  // Handle voice sample playback
+  const handlePlayVoice = (voice: AIVoice) => {
+    if (playingVoiceId === voice.id) {
+      if (audioRef.current) {
+        audioRef.current.pause();
+        audioRef.current.currentTime = 0;
+      }
+      setPlayingVoiceId(null);
+    } else {
+      if (audioRef.current) {
+        audioRef.current.pause();
+      }
+      audioRef.current = new Audio(voice.sample_url);
+      audioRef.current.onended = () => setPlayingVoiceId(null);
+      audioRef.current.onerror = () => {
+        setPlayingVoiceId(null);
+        toast({
+          title: t('common.error'),
+          description: t('aiConfig.voiceSettings.playError', 'Failed to play voice sample'),
+          variant: "destructive"
+        });
+      };
+      audioRef.current.play();
+      setPlayingVoiceId(voice.id);
+    }
+  };
+
+  // Stop audio on unmount
+  useEffect(() => {
+    return () => {
+      if (audioRef.current) {
+        audioRef.current.pause();
+      }
+    };
+  }, []);
+
+  // Filter voices by gender
+  const filteredVoices = voices.filter(voice =>
+    genderFilter === 'all' || voice.gender === genderFilter
+  );
 
-  // Load AI config when selected shop changes
+  // Load AI config and business hours when selected shop changes
   useEffect(() => {
     if (selectedShop) {
       loadAIConfig(selectedShop);
+      loadBusinessHours(selectedShop.id);
     }
   }, [selectedShop]);
 
   const loadAIConfig = (store: any) => {
-    // Load AI config from store's alt_data or use defaults
     const config = store.alt_data?.ai_config || {};
-
     setAiConfig({
-      voice_type: config.voice_type || "sarah",
-      speaking_speed: config.speaking_speed || "normal",
-      accent_language: config.accent_language || "us-english",
-      greeting_message: config.greeting_message || `Hello! Thank you for calling ${store.store_name || 'our store'}. I'm your AI assistant, and I'm here to help you with any questions about your order or our products. How can I assist you today?`,
-      business_hours_mode: config.business_hours_mode !== undefined ? config.business_hours_mode : true,
-      local_currency_support: config.local_currency_support !== undefined ? config.local_currency_support : true,
-      escalation_policy: config.escalation_policy || "medium"
+      voice_type: config.voice_type || "",
+      greeting_message: config.greeting_message || `Hello! Thank you for calling ${store.store_name || 'our store'}. How can I help you?`
     });
   };
 
+  const loadBusinessHours = async (storeId: string) => {
+    setBusinessHoursLoading(true);
+    try {
+      // Load business hours
+      const { data: hoursData, error: hoursError } = await supabase
+        .from('store_business_hours')
+        .select('*')
+        .eq('store_id', storeId)
+        .single();
+
+      if (hoursError && hoursError.code !== 'PGRST116') {
+        console.error('Error loading business hours:', hoursError);
+      }
+
+      if (hoursData) {
+        setBusinessHours({
+          ...hoursData,
+          daily_hours: hoursData.daily_hours || DEFAULT_DAILY_HOURS
+        });
+      } else {
+        setBusinessHours({
+          store_id: storeId,
+          is_enabled: false,
+          daily_hours: DEFAULT_DAILY_HOURS
+        });
+      }
+
+      // Load special hours (only future dates)
+      const { data: specialData, error: specialError } = await supabase
+        .from('store_special_hours')
+        .select('*')
+        .eq('store_id', storeId)
+        .gte('date', new Date().toISOString().split('T')[0])
+        .order('date', { ascending: true });
+
+      if (specialError) {
+        console.error('Error loading special hours:', specialError);
+      }
+
+      setSpecialHours(specialData || []);
+    } catch (error) {
+      console.error('Error loading business hours:', error);
+    } finally {
+      setBusinessHoursLoading(false);
+    }
+  };
+
+  // Validate greeting message
+  const validateGreetingMessage = (value: string) => {
+    if (INVALID_INPUT_PATTERN.test(value)) {
+      setGreetingError(t('aiConfig.conversationBehavior.greetingError', 'No emails, URLs, or website addresses allowed'));
+      return false;
+    }
+    setGreetingError(null);
+    return true;
+  };
+
+  const handleGreetingChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const value = e.target.value;
+    if (value.length <= 255) {
+      validateGreetingMessage(value);
+      setAiConfig({ ...aiConfig, greeting_message: value });
+    }
+  };
+
+  // Add special hours
+  const handleAddSpecialHours = async () => {
+    if (!newSpecialDate || !selectedShop) return;
+
+    const dateStr = format(newSpecialDate, 'yyyy-MM-dd');
+
+    // Check if date already exists
+    if (specialHours.some(sh => sh.date === dateStr)) {
+      toast({
+        title: t('common.error'),
+        description: t('aiConfig.businessHours.dateExists', 'This date already has special hours'),
+        variant: "destructive"
+      });
+      return;
+    }
+
+    try {
+      const newEntry: Omit<SpecialHours, 'id'> = {
+        store_id: selectedShop.id,
+        date: dateStr,
+        opening_time: newSpecialIsClosed ? null : newSpecialOpeningTime,
+        closing_time: newSpecialIsClosed ? null : newSpecialClosingTime,
+        is_closed: newSpecialIsClosed,
+        note: newSpecialNote || null
+      };
+
+      const { data, error } = await supabase
+        .from('store_special_hours')
+        .insert(newEntry)
+        .select()
+        .single();
+
+      if (error) throw error;
+
+      setSpecialHours([...specialHours, data].sort((a, b) => a.date.localeCompare(b.date)));
+
+      // Reset form
+      setNewSpecialDate(undefined);
+      setNewSpecialNote("");
+      setNewSpecialIsClosed(false);
+      setNewSpecialOpeningTime("09:00");
+      setNewSpecialClosingTime("18:00");
+
+      toast({
+        title: t('common.success'),
+        description: t('aiConfig.businessHours.specialAdded', 'Special hours added'),
+      });
+    } catch (error) {
+      console.error('Error adding special hours:', error);
+      toast({
+        title: t('common.error'),
+        description: t('common.tryAgain'),
+        variant: "destructive"
+      });
+    }
+  };
+
+  // Delete special hours
+  const handleDeleteSpecialHours = async (id: string) => {
+    try {
+      const { error } = await supabase
+        .from('store_special_hours')
+        .delete()
+        .eq('id', id);
+
+      if (error) throw error;
+
+      setSpecialHours(specialHours.filter(sh => sh.id !== id));
+
+      toast({
+        title: t('common.success'),
+        description: t('aiConfig.businessHours.specialDeleted', 'Special hours deleted'),
+      });
+    } catch (error) {
+      console.error('Error deleting special hours:', error);
+      toast({
+        title: t('common.error'),
+        description: t('common.tryAgain'),
+        variant: "destructive"
+      });
+    }
+  };
+
   const handleSaveConfig = async () => {
     if (!selectedShop) return;
 
+    // Validate greeting message before saving
+    if (!validateGreetingMessage(aiConfig.greeting_message)) {
+      toast({
+        title: t('common.error'),
+        description: t('aiConfig.conversationBehavior.greetingError', 'No emails, URLs, or website addresses allowed'),
+        variant: "destructive"
+      });
+      return;
+    }
+
     setSaving(true);
     try {
       const sessionData = localStorage.getItem('session_data');
@@ -72,7 +393,7 @@ export function AIConfigContent() {
 
       const session = JSON.parse(sessionData);
 
-      // Update store's alt_data with AI config
+      // Save AI config
       const response = await fetch(`${API_URL}/api/stores/${selectedShop.id}/ai-config`, {
         method: 'PUT',
         headers: {
@@ -86,6 +407,22 @@ export function AIConfigContent() {
         throw new Error('Failed to save configuration');
       }
 
+      // Save business hours (upsert)
+      const { error: hoursError } = await supabase
+        .from('store_business_hours')
+        .upsert({
+          ...businessHours,
+          store_id: selectedShop.id,
+          updated_at: new Date().toISOString()
+        }, {
+          onConflict: 'store_id'
+        });
+
+      if (hoursError) {
+        console.error('Error saving business hours:', hoursError);
+        throw hoursError;
+      }
+
       toast({
         title: t('common.success'),
         description: t('aiConfig.saveConfiguration'),
@@ -137,84 +474,172 @@ export function AIConfigContent() {
   }
 
   return (
-    <div className="flex-1 space-y-6 p-8 bg-slate-900">
+    <div className="flex-1 space-y-6 p-8 bg-slate-900 relative">
+      {/* Floating Save Button */}
+      <div className="fixed top-20 right-8 z-50">
+        <Button
+          className="bg-cyan-500 hover:bg-cyan-600 text-white shadow-lg"
+          onClick={handleSaveConfig}
+          disabled={saving}
+        >
+          {saving ? (
+            <>
+              <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+              {t('aiConfig.saving')}
+            </>
+          ) : (
+            t('aiConfig.saveConfiguration')
+          )}
+        </Button>
+      </div>
+
       <div className="flex items-center justify-between">
         <div>
           <h2 className="text-3xl font-bold tracking-tight text-white">{t('aiConfig.title')}</h2>
           <p className="text-slate-400">{t('aiConfig.subtitle')}</p>
         </div>
-        <div className="flex gap-3">
-          <Button
-            className="bg-cyan-500 hover:bg-cyan-600 text-white"
-            onClick={handleSaveConfig}
-            disabled={saving}
-          >
-            {saving ? (
-              <>
-                <Loader2 className="w-4 h-4 mr-2 animate-spin" />
-                {t('aiConfig.saving')}
-              </>
-            ) : (
-              t('aiConfig.saveConfiguration')
-            )}
-          </Button>
-        </div>
       </div>
 
       <div className="grid gap-6">
+        {/* Voice Settings Card */}
         <Card className="bg-slate-800 border-slate-700">
           <CardHeader>
-            <div className="flex items-center gap-3">
-              <Mic className="w-6 h-6 text-cyan-500" />
-              <CardTitle className="text-white">{t('aiConfig.voiceSettings.title')}</CardTitle>
-            </div>
-            <p className="text-slate-400">{t('aiConfig.voiceSettings.subtitle')} {selectedShop.store_name || 'your store'}</p>
-          </CardHeader>
-          <CardContent className="space-y-6">
-            <div className="grid gap-6 md:grid-cols-2">
-              <div className="space-y-2">
-                <Label className="text-slate-300">{t('aiConfig.voiceSettings.voiceType')}</Label>
-                <Select value={aiConfig.voice_type} onValueChange={(value) => setAiConfig({ ...aiConfig, voice_type: value })}>
-                  <SelectTrigger className="bg-slate-700 border-slate-600 text-white">
-                    <SelectValue />
-                  </SelectTrigger>
-                  <SelectContent className="bg-slate-700 border-slate-600">
-                    <SelectItem value="sarah">{t('aiConfig.voiceSettings.voices.sarah')}</SelectItem>
-                    <SelectItem value="james">{t('aiConfig.voiceSettings.voices.james')}</SelectItem>
-                    <SelectItem value="emma">{t('aiConfig.voiceSettings.voices.emma')}</SelectItem>
-                  </SelectContent>
-                </Select>
+            <div className="flex items-center justify-between">
+              <div className="flex items-center gap-3">
+                <Mic className="w-6 h-6 text-cyan-500" />
+                <div>
+                  <CardTitle className="text-white">{t('aiConfig.voiceSettings.title')}</CardTitle>
+                  <p className="text-slate-400 text-sm mt-1">{t('aiConfig.voiceSettings.subtitle')} {selectedShop.store_name || 'your store'}</p>
+                </div>
               </div>
-              <div className="space-y-2">
-                <Label className="text-slate-300">{t('aiConfig.voiceSettings.speakingSpeed')}</Label>
-                <Select value={aiConfig.speaking_speed} onValueChange={(value) => setAiConfig({ ...aiConfig, speaking_speed: value })}>
-                  <SelectTrigger className="bg-slate-700 border-slate-600 text-white">
+              {/* Gender Filter */}
+              <div className="flex items-center gap-2">
+                <Label className="text-slate-400 text-sm">{t('aiConfig.voiceSettings.filterByGender', 'Filter:')}</Label>
+                <Select value={genderFilter} onValueChange={(value: 'all' | 'male' | 'female' | 'neutral') => setGenderFilter(value)}>
+                  <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[140px]">
                     <SelectValue />
                   </SelectTrigger>
                   <SelectContent className="bg-slate-700 border-slate-600">
-                    <SelectItem value="slow">{t('aiConfig.voiceSettings.speeds.slow')}</SelectItem>
-                    <SelectItem value="normal">{t('aiConfig.voiceSettings.speeds.normal')}</SelectItem>
-                    <SelectItem value="fast">{t('aiConfig.voiceSettings.speeds.fast')}</SelectItem>
+                    <SelectItem value="all">
+                      <div className="flex items-center gap-2">
+                        <Users className="w-4 h-4" />
+                        {t('aiConfig.voiceSettings.genders.all', 'All')}
+                      </div>
+                    </SelectItem>
+                    <SelectItem value="female">
+                      <div className="flex items-center gap-2">
+                        <User className="w-4 h-4 text-pink-400" />
+                        {t('aiConfig.voiceSettings.genders.female', 'Female')}
+                      </div>
+                    </SelectItem>
+                    <SelectItem value="male">
+                      <div className="flex items-center gap-2">
+                        <User className="w-4 h-4 text-blue-400" />
+                        {t('aiConfig.voiceSettings.genders.male', 'Male')}
+                      </div>
+                    </SelectItem>
+                    <SelectItem value="neutral">
+                      <div className="flex items-center gap-2">
+                        <User className="w-4 h-4 text-purple-400" />
+                        {t('aiConfig.voiceSettings.genders.neutral', 'Neutral')}
+                      </div>
+                    </SelectItem>
                   </SelectContent>
                 </Select>
               </div>
             </div>
-            <div className="space-y-2">
-              <Label className="text-slate-300">{t('aiConfig.voiceSettings.accentLanguage')}</Label>
-              <Select value={aiConfig.accent_language} onValueChange={(value) => setAiConfig({ ...aiConfig, accent_language: value })}>
-                <SelectTrigger className="bg-slate-700 border-slate-600 text-white">
-                  <SelectValue />
-                </SelectTrigger>
-                <SelectContent className="bg-slate-700 border-slate-600">
-                  <SelectItem value="us-english">{t('aiConfig.voiceSettings.accents.usEnglish')}</SelectItem>
-                  <SelectItem value="uk-english">{t('aiConfig.voiceSettings.accents.ukEnglish')}</SelectItem>
-                  <SelectItem value="australian">{t('aiConfig.voiceSettings.accents.australian')}</SelectItem>
-                </SelectContent>
-              </Select>
+          </CardHeader>
+          <CardContent>
+            {/* Voice Selection Cards - Horizontal Single Line Layout */}
+            <div className="space-y-3">
+              <Label className="text-slate-300">{t('aiConfig.voiceSettings.selectVoice', 'Select Voice')}</Label>
+              {voicesLoading ? (
+                <div className="flex items-center justify-center py-8">
+                  <Loader2 className="w-6 h-6 animate-spin text-cyan-500" />
+                  <span className="ml-2 text-slate-400">{t('aiConfig.voiceSettings.loadingVoices', 'Loading voices...')}</span>
+                </div>
+              ) : filteredVoices.length === 0 ? (
+                <div className="text-center py-8 text-slate-400">
+                  {t('aiConfig.voiceSettings.noVoicesFound', 'No voices found for the selected filter.')}
+                </div>
+              ) : (
+                <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 max-h-[300px] overflow-y-auto pr-2">
+                  {filteredVoices.map((voice) => (
+                    <div
+                      key={voice.id}
+                      className={cn(
+                        "flex items-center gap-2 rounded-lg border-2 p-2 cursor-pointer transition-all",
+                        aiConfig.voice_type === voice.provider_voice_id
+                          ? "border-cyan-500 bg-slate-700/80"
+                          : "border-slate-600 bg-slate-700/40 hover:border-slate-500 hover:bg-slate-700/60"
+                      )}
+                      onClick={() => setAiConfig({ ...aiConfig, voice_type: voice.provider_voice_id })}
+                    >
+                      {/* Avatar with gender color */}
+                      <div className={cn(
+                        "w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0",
+                        voice.gender === 'female' ? "bg-pink-500/20" :
+                        voice.gender === 'male' ? "bg-blue-500/20" :
+                        "bg-purple-500/20"
+                      )}>
+                        <User className={cn(
+                          "w-4 h-4",
+                          voice.gender === 'female' ? "text-pink-400" :
+                          voice.gender === 'male' ? "text-blue-400" :
+                          "text-purple-400"
+                        )} />
+                      </div>
+
+                      {/* Name */}
+                      <span className="text-white font-medium text-sm truncate flex-1">
+                        {voice.name}
+                      </span>
+
+                      {/* Gender badge */}
+                      <span className={cn(
+                        "text-xs px-1.5 py-0.5 rounded flex-shrink-0",
+                        voice.gender === 'female' ? "bg-pink-500/20 text-pink-300" :
+                        voice.gender === 'male' ? "bg-blue-500/20 text-blue-300" :
+                        "bg-purple-500/20 text-purple-300"
+                      )}>
+                        {t(`aiConfig.voiceSettings.genders.${voice.gender}`, voice.gender)}
+                      </span>
+
+                      {/* Play button */}
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        className={cn(
+                          "h-7 w-7 p-0 rounded-full flex-shrink-0",
+                          playingVoiceId === voice.id
+                            ? "bg-cyan-500 text-white hover:bg-cyan-600"
+                            : "bg-slate-600 text-slate-300 hover:bg-slate-500"
+                        )}
+                        onClick={(e) => {
+                          e.stopPropagation();
+                          handlePlayVoice(voice);
+                        }}
+                      >
+                        {playingVoiceId === voice.id ? (
+                          <Pause className="w-3.5 h-3.5" />
+                        ) : (
+                          <Play className="w-3.5 h-3.5 ml-0.5" />
+                        )}
+                      </Button>
+
+                      {/* Selection indicator */}
+                      {aiConfig.voice_type === voice.provider_voice_id && (
+                        <div className="w-2 h-2 rounded-full bg-cyan-500 flex-shrink-0" />
+                      )}
+                    </div>
+                  ))}
+                </div>
+              )}
             </div>
           </CardContent>
         </Card>
 
+        {/* Conversation Behavior Card */}
         <Card className="bg-slate-800 border-slate-700">
           <CardHeader>
             <div className="flex items-center gap-3">
@@ -224,54 +649,288 @@ export function AIConfigContent() {
             <p className="text-slate-400">{t('aiConfig.conversationBehavior.subtitle')} {selectedShop.store_name || 'your store'} customers</p>
           </CardHeader>
           <CardContent className="space-y-6">
+            {/* Greeting Message */}
             <div className="space-y-2">
-              <Label className="text-slate-300">{t('aiConfig.conversationBehavior.greetingMessage')}</Label>
-              <Textarea
-                className="bg-slate-700 border-slate-600 text-white min-h-[100px]"
+              <div className="flex items-center justify-between">
+                <Label className="text-slate-300">{t('aiConfig.conversationBehavior.greetingMessage')}</Label>
+                <span className="text-xs text-slate-400">{aiConfig.greeting_message.length}/255</span>
+              </div>
+              <Input
+                type="text"
+                className={cn(
+                  "bg-slate-700 border-slate-600 text-white",
+                  greetingError && "border-red-500"
+                )}
                 value={aiConfig.greeting_message}
-                onChange={(e) => setAiConfig({ ...aiConfig, greeting_message: e.target.value })}
+                onChange={handleGreetingChange}
+                maxLength={255}
+                placeholder={t('aiConfig.conversationBehavior.greetingPlaceholder', 'Enter a greeting message...')}
               />
+              {greetingError && (
+                <p className="text-xs text-red-400">{greetingError}</p>
+              )}
+              <p className="text-xs text-slate-500">{t('aiConfig.conversationBehavior.greetingHint', 'No emails, URLs, or website addresses allowed')}</p>
             </div>
+          </CardContent>
+        </Card>
 
-            <div className="grid gap-6 md:grid-cols-2">
-              <div className="flex items-center justify-between">
-                <div className="space-y-1">
-                  <Label className="text-slate-300">{t('aiConfig.conversationBehavior.businessHoursMode')}</Label>
-                  <p className="text-sm text-slate-400">{t('aiConfig.conversationBehavior.businessHoursModeDesc')}</p>
-                </div>
-                <Switch
-                  checked={aiConfig.business_hours_mode}
-                  onCheckedChange={(checked) => setAiConfig({ ...aiConfig, business_hours_mode: checked })}
-                  className="data-[state=checked]:bg-cyan-500"
-                />
-              </div>
-              <div className="flex items-center justify-between">
-                <div className="space-y-1">
-                  <Label className="text-slate-300">{t('aiConfig.conversationBehavior.localCurrencySupport')}</Label>
-                  <p className="text-sm text-slate-400">{t('aiConfig.conversationBehavior.localCurrencySupportDesc')}</p>
+        {/* Business Hours Card */}
+        <Card className="bg-slate-800 border-slate-700">
+          <CardHeader>
+            <div className="flex items-center justify-between">
+              <div className="flex items-center gap-3">
+                <Clock className="w-6 h-6 text-cyan-500" />
+                <div>
+                  <CardTitle className="text-white">{t('aiConfig.businessHours.title', 'Business Hours')}</CardTitle>
+                  <p className="text-slate-400 text-sm mt-1">{t('aiConfig.businessHours.subtitle', 'Set your store operating hours')}</p>
                 </div>
-                <Switch
-                  checked={aiConfig.local_currency_support}
-                  onCheckedChange={(checked) => setAiConfig({ ...aiConfig, local_currency_support: checked })}
-                  className="data-[state=checked]:bg-cyan-500"
-                />
               </div>
+              <Switch
+                checked={businessHours.is_enabled}
+                onCheckedChange={(checked) => setBusinessHours({ ...businessHours, is_enabled: checked })}
+                className="data-[state=checked]:bg-cyan-500"
+              />
             </div>
+          </CardHeader>
 
-            <div className="space-y-2">
-              <Label className="text-slate-300">{t('aiConfig.conversationBehavior.escalationPolicy')}</Label>
-              <Select value={aiConfig.escalation_policy} onValueChange={(value) => setAiConfig({ ...aiConfig, escalation_policy: value })}>
-                <SelectTrigger className="bg-slate-700 border-slate-600 text-white">
-                  <SelectValue />
-                </SelectTrigger>
-                <SelectContent className="bg-slate-700 border-slate-600">
-                  <SelectItem value="low">{t('aiConfig.conversationBehavior.escalationPolicies.low')}</SelectItem>
-                  <SelectItem value="medium">{t('aiConfig.conversationBehavior.escalationPolicies.medium')}</SelectItem>
-                  <SelectItem value="high">{t('aiConfig.conversationBehavior.escalationPolicies.high')}</SelectItem>
-                </SelectContent>
-              </Select>
-            </div>
-          </CardContent>
+          {businessHours.is_enabled && (
+            <CardContent className="space-y-6">
+              {businessHoursLoading ? (
+                <div className="flex items-center justify-center py-8">
+                  <Loader2 className="w-6 h-6 animate-spin text-cyan-500" />
+                </div>
+              ) : (
+                <>
+                  {/* Regular Hours - Per day */}
+                  <div className="space-y-3">
+                    <Label className="text-slate-300">{t('aiConfig.businessHours.regularHours', 'Regular Hours')}</Label>
+
+                    <div className="space-y-2">
+                      {WEEKDAYS.map((day) => {
+                        const dayHours = businessHours.daily_hours[day as WeekDay];
+                        return (
+                          <div key={day} className="flex items-center gap-3 bg-slate-700/30 p-2 rounded-lg">
+                            {/* Day name */}
+                            <span className="text-white w-[100px] text-sm">
+                              {t(`aiConfig.businessHours.days.${day}`, day.charAt(0).toUpperCase() + day.slice(1))}
+                            </span>
+
+                            {/* Is Open Switch */}
+                            <Switch
+                              checked={dayHours.is_open}
+                              onCheckedChange={(checked) => {
+                                setBusinessHours({
+                                  ...businessHours,
+                                  daily_hours: {
+                                    ...businessHours.daily_hours,
+                                    [day]: { ...dayHours, is_open: checked }
+                                  }
+                                });
+                              }}
+                              className="data-[state=checked]:bg-cyan-500"
+                            />
+
+                            {dayHours.is_open ? (
+                              <>
+                                {/* Opening Time */}
+                                <Select
+                                  value={dayHours.open}
+                                  onValueChange={(value) => {
+                                    setBusinessHours({
+                                      ...businessHours,
+                                      daily_hours: {
+                                        ...businessHours.daily_hours,
+                                        [day]: { ...dayHours, open: value }
+                                      }
+                                    });
+                                  }}
+                                >
+                                  <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[85px]">
+                                    <SelectValue />
+                                  </SelectTrigger>
+                                  <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
+                                    {TIME_OPTIONS.map((time) => (
+                                      <SelectItem key={time} value={time}>{time}</SelectItem>
+                                    ))}
+                                  </SelectContent>
+                                </Select>
+                                <span className="text-slate-400">-</span>
+                                {/* Closing Time */}
+                                <Select
+                                  value={dayHours.close}
+                                  onValueChange={(value) => {
+                                    setBusinessHours({
+                                      ...businessHours,
+                                      daily_hours: {
+                                        ...businessHours.daily_hours,
+                                        [day]: { ...dayHours, close: value }
+                                      }
+                                    });
+                                  }}
+                                >
+                                  <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[85px]">
+                                    <SelectValue />
+                                  </SelectTrigger>
+                                  <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
+                                    {TIME_OPTIONS.map((time) => (
+                                      <SelectItem key={time} value={time}>{time}</SelectItem>
+                                    ))}
+                                  </SelectContent>
+                                </Select>
+                              </>
+                            ) : (
+                              <span className="text-red-400 text-sm">{t('aiConfig.businessHours.closed', 'Closed')}</span>
+                            )}
+                          </div>
+                        );
+                      })}
+                    </div>
+                  </div>
+
+                  {/* Special Hours / Holidays */}
+                  <div className="space-y-4 pt-4 border-t border-slate-700">
+                    <Label className="text-slate-300">{t('aiConfig.businessHours.specialHours', 'Special Hours / Holidays')}</Label>
+
+                    {/* Add Special Hours Form */}
+                    <div className="flex items-center gap-2 flex-wrap bg-slate-700/50 p-3 rounded-lg">
+                      {/* Date Picker */}
+                      <Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
+                        <PopoverTrigger asChild>
+                          <Button
+                            variant="outline"
+                            className={cn(
+                              "w-[140px] justify-start text-left font-normal bg-slate-700 border-slate-600 text-white hover:bg-slate-600",
+                              !newSpecialDate && "text-slate-400"
+                            )}
+                          >
+                            <Calendar className="mr-2 h-4 w-4" />
+                            {newSpecialDate ? format(newSpecialDate, 'yyyy-MM-dd') : t('aiConfig.businessHours.pickDate', 'Pick date')}
+                          </Button>
+                        </PopoverTrigger>
+                        <PopoverContent className="w-auto p-0 bg-slate-800 border-slate-700" align="start">
+                          <CalendarComponent
+                            mode="single"
+                            selected={newSpecialDate}
+                            onSelect={(date) => {
+                              setNewSpecialDate(date);
+                              setCalendarOpen(false);
+                            }}
+                            disabled={(date) => date < new Date()}
+                            initialFocus
+                            locale={dateLocale}
+                            className="bg-slate-800"
+                          />
+                        </PopoverContent>
+                      </Popover>
+
+                      {/* Note */}
+                      <Input
+                        type="text"
+                        placeholder={t('aiConfig.businessHours.notePlaceholder', 'Note (e.g., Christmas)')}
+                        value={newSpecialNote}
+                        onChange={(e) => setNewSpecialNote(e.target.value)}
+                        className="w-[160px] bg-slate-700 border-slate-600 text-white"
+                      />
+
+                      {/* Closed Checkbox */}
+                      <div className="flex items-center gap-2">
+                        <input
+                          type="checkbox"
+                          id="isClosed"
+                          checked={newSpecialIsClosed}
+                          onChange={(e) => setNewSpecialIsClosed(e.target.checked)}
+                          className="w-4 h-4 rounded border-slate-600 bg-slate-700 text-cyan-500 focus:ring-cyan-500"
+                        />
+                        <Label htmlFor="isClosed" className="text-slate-300 text-sm cursor-pointer">
+                          {t('aiConfig.businessHours.closed', 'Closed')}
+                        </Label>
+                      </div>
+
+                      {/* Time Range (only if not closed) */}
+                      {!newSpecialIsClosed && (
+                        <>
+                          <Select
+                            value={newSpecialOpeningTime}
+                            onValueChange={setNewSpecialOpeningTime}
+                          >
+                            <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[90px]">
+                              <SelectValue />
+                            </SelectTrigger>
+                            <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
+                              {TIME_OPTIONS.map((time) => (
+                                <SelectItem key={time} value={time}>{time}</SelectItem>
+                              ))}
+                            </SelectContent>
+                          </Select>
+                          <span className="text-slate-400">-</span>
+                          <Select
+                            value={newSpecialClosingTime}
+                            onValueChange={setNewSpecialClosingTime}
+                          >
+                            <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[90px]">
+                              <SelectValue />
+                            </SelectTrigger>
+                            <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
+                              {TIME_OPTIONS.map((time) => (
+                                <SelectItem key={time} value={time}>{time}</SelectItem>
+                              ))}
+                            </SelectContent>
+                          </Select>
+                        </>
+                      )}
+
+                      {/* Add Button */}
+                      <Button
+                        size="sm"
+                        className="bg-cyan-500 hover:bg-cyan-600 text-white"
+                        onClick={handleAddSpecialHours}
+                        disabled={!newSpecialDate}
+                      >
+                        <Plus className="w-4 h-4" />
+                      </Button>
+                    </div>
+
+                    {/* Special Hours List */}
+                    {specialHours.length > 0 && (
+                      <div className="space-y-2">
+                        {specialHours.map((sh) => (
+                          <div
+                            key={sh.id}
+                            className="flex items-center justify-between bg-slate-700/30 p-2 rounded-lg"
+                          >
+                            <div className="flex items-center gap-3">
+                              <Calendar className="w-4 h-4 text-slate-400" />
+                              <span className="text-white">{format(new Date(sh.date), 'yyyy-MM-dd')}</span>
+                              {sh.is_closed ? (
+                                <span className="text-red-400 text-sm">{t('aiConfig.businessHours.closed', 'Closed')}</span>
+                              ) : (
+                                <span className="text-slate-300 text-sm">{sh.opening_time} - {sh.closing_time}</span>
+                              )}
+                              {sh.note && (
+                                <span className="text-slate-400 text-sm">({sh.note})</span>
+                              )}
+                            </div>
+                            <Button
+                              variant="ghost"
+                              size="sm"
+                              className="text-red-400 hover:text-red-300 hover:bg-red-500/20 h-8 w-8 p-0"
+                              onClick={() => sh.id && handleDeleteSpecialHours(sh.id)}
+                            >
+                              <Trash2 className="w-4 h-4" />
+                            </Button>
+                          </div>
+                        ))}
+                      </div>
+                    )}
+
+                    {specialHours.length === 0 && (
+                      <p className="text-slate-500 text-sm">{t('aiConfig.businessHours.noSpecialHours', 'No special hours configured')}</p>
+                    )}
+                  </div>
+                </>
+              )}
+            </CardContent>
+          )}
         </Card>
       </div>
     </div>

+ 38 - 0
shopcall.ai-main/src/i18n/locales/de.json

@@ -563,6 +563,18 @@
       "voiceType": "Stimmtyp",
       "speakingSpeed": "Sprechgeschwindigkeit",
       "accentLanguage": "Akzent & Sprache",
+      "selectVoice": "Stimme Auswählen",
+      "filterByGender": "Filter:",
+      "loadingVoices": "Stimmen werden geladen...",
+      "noVoicesFound": "Keine Stimmen für den ausgewählten Filter gefunden.",
+      "loadError": "Stimmen konnten nicht geladen werden",
+      "playError": "Stimmprobe konnte nicht abgespielt werden",
+      "genders": {
+        "all": "Alle",
+        "female": "Weiblich",
+        "male": "Männlich",
+        "neutral": "Neutral"
+      },
       "voices": {
         "sarah": "Sarah (Professionell)",
         "james": "James (Freundlich)",
@@ -583,6 +595,9 @@
       "title": "Gesprächsverhalten",
       "subtitle": "Definieren Sie, wie die KI mit",
       "greetingMessage": "Begrüßungsnachricht",
+      "greetingPlaceholder": "Geben Sie eine Begrüßungsnachricht ein...",
+      "greetingHint": "E-Mail-Adressen, URLs und Websites sind nicht erlaubt",
+      "greetingError": "E-Mail-Adressen, URLs und Websites sind nicht erlaubt",
       "businessHoursMode": "Geschäftszeiten-Modus",
       "businessHoursModeDesc": "Verhalten basierend auf Shop-Öffnungszeiten anpassen",
       "localCurrencySupport": "Lokale Währungsunterstützung",
@@ -609,6 +624,29 @@
       "orderHistory": "Bestellhistorie",
       "customerDatabase": "Kundendatenbank",
       "manageStoreData": "Shop-Daten Verwalten"
+    },
+    "businessHours": {
+      "title": "Geschäftszeiten",
+      "subtitle": "Legen Sie die Öffnungszeiten Ihres Geschäfts fest",
+      "regularHours": "Reguläre Öffnungszeiten",
+      "specialHours": "Sonderöffnungszeiten / Feiertage",
+      "days": {
+        "monday": "Montag",
+        "tuesday": "Dienstag",
+        "wednesday": "Mittwoch",
+        "thursday": "Donnerstag",
+        "friday": "Freitag",
+        "saturday": "Samstag",
+        "sunday": "Sonntag"
+      },
+      "to": "bis",
+      "closed": "Geschlossen",
+      "pickDate": "Datum wählen",
+      "notePlaceholder": "Hinweis (z.B. Weihnachten)",
+      "dateExists": "Für dieses Datum existiert bereits ein Eintrag",
+      "specialAdded": "Sonderöffnungszeit hinzugefügt",
+      "specialDeleted": "Sonderöffnungszeit gelöscht",
+      "noSpecialHours": "Keine Sonderöffnungszeiten konfiguriert"
     }
   },
   "onboarding": {

+ 38 - 0
shopcall.ai-main/src/i18n/locales/en.json

@@ -565,6 +565,18 @@
       "voiceType": "Voice Type",
       "speakingSpeed": "Speaking Speed",
       "accentLanguage": "Accent & Language",
+      "selectVoice": "Select Voice",
+      "filterByGender": "Filter:",
+      "loadingVoices": "Loading voices...",
+      "noVoicesFound": "No voices found for the selected filter.",
+      "loadError": "Failed to load voices",
+      "playError": "Failed to play voice sample",
+      "genders": {
+        "all": "All",
+        "female": "Female",
+        "male": "Male",
+        "neutral": "Neutral"
+      },
       "voices": {
         "sarah": "Sarah (Professional)",
         "james": "James (Friendly)",
@@ -585,6 +597,9 @@
       "title": "Conversation Behavior",
       "subtitle": "Define how the AI interacts with",
       "greetingMessage": "Greeting Message",
+      "greetingPlaceholder": "Enter a greeting message...",
+      "greetingHint": "No emails, URLs, or website addresses allowed",
+      "greetingError": "No emails, URLs, or website addresses allowed",
       "businessHoursMode": "Business Hours Mode",
       "businessHoursModeDesc": "Adjust behavior based on store hours",
       "localCurrencySupport": "Local Currency Support",
@@ -611,6 +626,29 @@
       "orderHistory": "Order history",
       "customerDatabase": "Customer database",
       "manageStoreData": "Manage Store Data"
+    },
+    "businessHours": {
+      "title": "Business Hours",
+      "subtitle": "Set your store operating hours",
+      "regularHours": "Regular Hours",
+      "specialHours": "Special Hours / Holidays",
+      "days": {
+        "monday": "Monday",
+        "tuesday": "Tuesday",
+        "wednesday": "Wednesday",
+        "thursday": "Thursday",
+        "friday": "Friday",
+        "saturday": "Saturday",
+        "sunday": "Sunday"
+      },
+      "to": "to",
+      "closed": "Closed",
+      "pickDate": "Pick date",
+      "notePlaceholder": "Note (e.g., Christmas)",
+      "dateExists": "A special hours entry already exists for this date",
+      "specialAdded": "Special hours added",
+      "specialDeleted": "Special hours deleted",
+      "noSpecialHours": "No special hours configured"
     }
   },
   "onboarding": {

+ 38 - 0
shopcall.ai-main/src/i18n/locales/hu.json

@@ -565,6 +565,18 @@
       "voiceType": "Hangtípus",
       "speakingSpeed": "Beszéd Sebessége",
       "accentLanguage": "Akcentus és Nyelv",
+      "selectVoice": "Hang Kiválasztása",
+      "filterByGender": "Szűrő:",
+      "loadingVoices": "Hangok betöltése...",
+      "noVoicesFound": "Nem található hang a kiválasztott szűrőhöz.",
+      "loadError": "Nem sikerült betölteni a hangokat",
+      "playError": "Nem sikerült lejátszani a hangmintát",
+      "genders": {
+        "all": "Mind",
+        "female": "Női",
+        "male": "Férfi",
+        "neutral": "Semleges"
+      },
       "voices": {
         "sarah": "Sarah (Professzionális)",
         "james": "James (Barátságos)",
@@ -585,6 +597,9 @@
       "title": "Beszélgetési Viselkedés",
       "subtitle": "Határozza meg, hogyan lépjen kapcsolatba az AI ezzel:",
       "greetingMessage": "Üdvözlő Üzenet",
+      "greetingPlaceholder": "Írja be az üdvözlő üzenetet...",
+      "greetingHint": "E-mail címek, URL-ek és weboldalak nem engedélyezettek",
+      "greetingError": "E-mail címek, URL-ek és weboldalak nem engedélyezettek",
       "businessHoursMode": "Nyitvatartási Idő Mód",
       "businessHoursModeDesc": "Viselkedés módosítása az üzlet nyitvatartása alapján",
       "localCurrencySupport": "Helyi Pénznem Támogatás",
@@ -611,6 +626,29 @@
       "orderHistory": "Rendelési előzmények",
       "customerDatabase": "Ügyfél adatbázis",
       "manageStoreData": "Áruház Adatok Kezelése"
+    },
+    "businessHours": {
+      "title": "Nyitvatartási Idő",
+      "subtitle": "Állítsa be az üzlet nyitvatartási idejét",
+      "regularHours": "Rendes Nyitvatartás",
+      "specialHours": "Speciális Nyitvatartás / Ünnepnapok",
+      "days": {
+        "monday": "Hétfő",
+        "tuesday": "Kedd",
+        "wednesday": "Szerda",
+        "thursday": "Csütörtök",
+        "friday": "Péntek",
+        "saturday": "Szombat",
+        "sunday": "Vasárnap"
+      },
+      "to": "-tól/-ig",
+      "closed": "Zárva",
+      "pickDate": "Dátum választása",
+      "notePlaceholder": "Megjegyzés (pl. Karácsony)",
+      "dateExists": "Ehhez a dátumhoz már létezik speciális nyitvatartás",
+      "specialAdded": "Speciális nyitvatartás hozzáadva",
+      "specialDeleted": "Speciális nyitvatartás törölve",
+      "noSpecialHours": "Nincs speciális nyitvatartás beállítva"
     }
   },
   "onboarding": {