File size: 6,421 Bytes
fba4fcd
cc21a82
27a5a57
bdec212
a3ef398
4ad9b66
cc21a82
 
323a2a7
bdec212
 
cc21a82
bdec212
 
 
 
 
 
cc21a82
bdec212
 
 
 
 
 
 
6f82055
bdec212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c990d50
bdec212
 
 
 
 
 
cfee1d1
bdec212
 
 
 
 
cfee1d1
 
bdec212
 
cfee1d1
bdec212
 
 
 
 
 
8f94a86
bdec212
 
 
 
 
 
 
 
cfee1d1
bdec212
 
cfee1d1
bdec212
 
cfee1d1
6f82055
cfee1d1
bdec212
cfee1d1
 
bdec212
 
a3ef398
8f94a86
c990d50
bdec212
 
 
 
 
 
 
8f94a86
 
bdec212
 
 
 
 
8f94a86
bdec212
094fd2d
9910b51
bdec212
 
cfee1d1
bdec212
90131db
cfee1d1
90131db
8f94a86
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// src/App.tsx
import React, { useEffect, useRef, useState } from "react";
import './App.scss';
import { AppProvider, useAppContext, PersonalityType, PersonalityInstructions } from "./contexts/AppContext";
import ControlTray from "./components/control-tray/ControlTray";
import { IOSModal } from "./components/ios-modal/IOSModal";
import { isIOS } from "./lib/platform";
import cn from "classnames";
import Logo from "./components/logo/Logo";
import { LiveConfig } from "./multimodal-live-types";
import { CustomModal } from "./components/CustomModal";

// منوی شخصیت‌ها (به صورت یک کامپوننت داخلی ساده)
const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelect: (p: PersonalityType) => void; }> = ({ isOpen, onClose, onSelect }) => {
  const menuRef = useRef<HTMLDivElement>(null);
  const { selectedPersonality } = useAppContext();
  const personalityIcons: Record<PersonalityType, string> = { default: "person", teacher: "school", poetic: "auto_awesome", funny: "sentiment_satisfied", custom: "tune" };
  const personalityLabels: Record<PersonalityType, string> = { default: 'دستیار پیش‌فرض', teacher: 'استاد زبان', poetic: 'حس خوب', funny: 'شوخ‌طبع', custom: 'شخصیت اختصاصی' };

  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => menuRef.current && !menuRef.current.contains(e.target as Node) && onClose();
    if (isOpen) document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <div ref={menuRef} className="personality-popover-wrapper open">
      <div className="popover-content">
        <ul>
          {(Object.keys(personalityIcons) as PersonalityType[]).map(key => (
            <li key={key} className={cn({ active: selectedPersonality === key })} onClick={() => onSelect(key)}>
              <div><span className="material-symbols-outlined">{personalityIcons[key]}</span>{personalityLabels[key]}</div>
              {selectedPersonality === key && <span className="material-symbols-outlined tick">done</span>}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

// کامپوننت داخلی اصلی برنامه
const AppInternal: React.FC = () => {
  const videoRef = useRef<HTMLVideoElement>(null);
  const { volume, changePersonality, customUserName, customInstructions } = useAppContext();
  const [isUserSpeaking, setIsUserSpeaking] = useState(false);
  const [isMicActive, setIsMicActive] = useState(false);
  const [isCamActive, setIsCamActive] = useState(false);
  const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user');
  const [isNotificationOpen, setIsNotificationOpen] = useState(false);
  const [isPersonalityMenuOpen, setIsPersonalityMenuOpen] = useState(false);
  const [isCustomModalOpen, setIsCustomModalOpen] = useState(false);

  const handleSelectPersonality = (p: PersonalityType) => {
    setIsPersonalityMenuOpen(false);
    if (p === 'custom') setIsCustomModalOpen(true);
    else changePersonality(p);
  };

  return (
    <>
      <div className="main-wrapper">
        <div className="header-controls">
          <button aria-label="انتخاب شخصیت" className="header-icon-button" onClick={() => setIsPersonalityMenuOpen(true)}>
            <span className="material-symbols-outlined">psychology</span>
          </button>
          <button aria-label="اطلاعات" className="header-icon-button" onClick={() => setIsNotificationOpen(v => !v)}>
            <span className="material-symbols-outlined">info</span>
          </button>
        </div>

        {isNotificationOpen && <div className="notification-popover-wrapper open"><div className="popover-content">مدل‌های هوش مصنوعی می‌توانند اشتباه کنند.</div></div>}
        
        <PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelect={handleSelectPersonality} />

        <div className="media-area">
          <video id="video-feed" ref={videoRef} autoPlay playsInline className={cn({ hidden: !isCamActive }, { "scale-x-[-1]": currentFacingMode === 'user' })} />
          {isMicActive && !isCamActive && <div id="large-logo-container"><Logo isMini={false} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isUserSpeaking} /></div>}
        </div>

        <ControlTray videoRef={videoRef} onUserSpeakingChange={setIsUserSpeaking} isAppMicActive={isMicActive} onAppMicToggle={setIsMicActive} isAppCamActive={isCamActive} onAppCamToggle={setIsCamActive} currentFacingMode={currentFacingMode} onFacingModeChange={setCurrentFacingMode} />
      </div>
      <CustomModal isOpen={isCustomModalOpen} onClose={() => setIsCustomModalOpen(false)} onSave={(name, instructions) => changePersonality('custom', { name, instructions })} initialName={customUserName} initialInstructions={customInstructions} />
    </>
  );
};

// کامپوننت ریشه App
function App() {
  const [showIOSModal, setShowIOSModal] = useState(false);
  const [personalityInstructions, setPersonalityInstructions] = useState<PersonalityInstructions | null>(null);
  const [loadingError, setLoadingError] = useState<string | null>(null);

  useEffect(() => {
    if (isIOS()) setShowIOSModal(true);
    const fetchInstructions = async () => {
      try {
        const res = await fetch('/api/instructions');
        if (!res.ok) throw new Error(`Network error: ${res.status}`);
        setPersonalityInstructions(await res.json());
      } catch (e) {
        setLoadingError("امکان دریافت تنظیمات شخصیت‌ها وجود ندارد. لطفاً صفحه را رفرش کنید.");
      }
    };
    fetchInstructions();
  }, []);

  if (loadingError) return <div className="loading-screen">{loadingError}</div>;
  if (!personalityInstructions) return <div className="loading-screen">در حال بارگذاری...</div>;

  const initialAppConfig: LiveConfig = { model: "models/gemini-2.0-flash-exp" };

  return (
    <AppProvider initialConfig={initialAppConfig} personalityInstructions={personalityInstructions}>
      <AppInternal />
      <IOSModal isOpen={showIOSModal} onClose={() => setShowIOSModal(false)} />
    </AppProvider>
  );
}

export default App;