Spaces:
Running
Running
Update src/App.tsx
Browse files- src/App.tsx +86 -100
src/App.tsx
CHANGED
@@ -1,136 +1,122 @@
|
|
1 |
// src/App.tsx
|
2 |
-
|
3 |
import React, { useEffect, useRef, useState } from "react";
|
4 |
import './App.scss';
|
5 |
-
import {
|
6 |
import ControlTray from "./components/control-tray/ControlTray";
|
7 |
import { IOSModal } from "./components/ios-modal/IOSModal";
|
8 |
import { isIOS } from "./lib/platform";
|
9 |
import cn from "classnames";
|
10 |
-
import { LiveConfig } from "./multimodal-live-types";
|
11 |
import Logo from "./components/logo/Logo";
|
|
|
|
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
هوش مصنوعی هوشانیک برنامه ای است که داخل گوشی موبایل نصب میشه و در کافه بازار در دسترس است و با جستجو در کافه بازار یا مایکت امکان دانلود هوش مصنوعی هوشان وجود دارد
|
20 |
-
......
|
21 |
-
اگر کاربر خواست با صدای بلند تر صحبت کنی باید به درخواست کاربر حتما پاسخ بدهی، اگر کاربر خواست صدای متفاوت یا صدای هر چیزی یا صدای تقلید کنی باید درخواست کاربر اجرا کنی حتما، اگر کاربر خواست آروم صحبت کنی باید درخواست کاربر اجرا کنی، در هر شرایط باید درخواست کاربر اجرا بشه
|
22 |
-
....
|
23 |
-
در تمام پاسخها، اعداد را نیز کاملاً به فارسی بیان کن مگر اینکه در حال صحبت به زبان دیگری غیر از فارسی با کاربر بودی به همون زبان اعداد بگو، و همچنین اعداد به فارسی درست بگو این حالت (مثلاً بگو 'ده' یا 'بیست و پنج'، نه '10' یا 'twenty-five').
|
24 |
-
وقتی کاربر در مورد هویت تو میپرسد، خودت را اینگونه معرفی کن: "من دستیار صوتی و تصویری از برنامه هوش مصنوعی هوشان هستم. چگونه میتوانم به شما کمک کنم؟"
|
25 |
-
مهم: هنگامی که پیام ورودی فقط شامل متن "START_GREETING" بود، *فقط* با یک خوشامدگویی کوتاه و دوستانه به فارسی پاسخ بده، مانند "سلام، چطور میتونم کمکتون کنم؟" یا "سلام حالتون چطوره!
|
26 |
-
هشدار: به هیچ عنوان در هنگام مکالمه و پیام دادن به کاربر نباید شکلک بفرستی و یا بیان کنی شکلک و یا اموجی هارو حتی شکلک هارو نیاز نیست بگی اسم شونو در هنگام صحبت اگر شکلک نیاز بود بود بگی نباید اسم شکلک بگی
|
27 |
-
**مهم: به هیچ عنوان در پاسخهای خود از ایموجی استفاده نکن.**
|
28 |
-
`.trim();
|
29 |
-
|
30 |
-
const initialAppConfig: LiveConfig = {
|
31 |
-
model: "models/gemini-2.0-flash-exp",
|
32 |
-
systemInstruction: { parts: [{ text: myCustomInstruction }] },
|
33 |
-
tools: [
|
34 |
-
{ googleSearch: {} }
|
35 |
-
],
|
36 |
-
generationConfig: {
|
37 |
-
responseModalities: "audio",
|
38 |
-
}
|
39 |
-
};
|
40 |
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
notificationButtonRef: React.RefObject<HTMLButtonElement>;
|
49 |
-
isNotificationOpen: boolean;
|
50 |
-
setIsNotificationOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
51 |
-
currentFacingMode: 'user' | 'environment';
|
52 |
-
onFacingModeChange: (mode: 'user' | 'environment') => void;
|
53 |
-
}
|
54 |
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
const [isUserSpeaking, setIsUserSpeaking] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
}
|
64 |
|
65 |
return (
|
66 |
-
|
67 |
-
<div className="main-wrapper
|
68 |
<div className="header-controls">
|
69 |
-
<
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
</
|
74 |
-
</
|
75 |
-
</div>
|
76 |
-
<div ref={notificationPopoverRef} id="notification-popover-wrapper" className="notification-popover-wrapper">
|
77 |
-
<div id="notification-popover" className={cn("popover-content", { "open animate-popover-open-top-center": isNotificationOpen, "animate-popover-close-top-center": !isNotificationOpen && document.getElementById('notification-popover')?.classList.contains('open'), })}>
|
78 |
-
<div className="notification-popover-text-content">مدلهای هوش مصنوعی میتوانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از بیان اطلاعات حساس بپرهیزید.</div>
|
79 |
-
</div>
|
80 |
</div>
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
)}
|
88 |
-
{
|
89 |
</div>
|
90 |
-
|
91 |
-
<ControlTray videoRef={videoRef}
|
92 |
-
{/* --- 👆 پایان تغییرات 👆 --- */}
|
93 |
</div>
|
94 |
-
|
|
|
95 |
);
|
96 |
};
|
97 |
|
|
|
98 |
function App() {
|
99 |
-
const videoRef = useRef<HTMLVideoElement>(null);
|
100 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
101 |
-
const [
|
102 |
-
const [
|
103 |
-
const [isCamActive, setIsCamActive] = useState(false);
|
104 |
-
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
105 |
-
const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user');
|
106 |
-
const notificationButtonRef = useRef<HTMLButtonElement>(null);
|
107 |
-
const notificationPopoverRef = useRef<HTMLDivElement>(null);
|
108 |
|
109 |
useEffect(() => {
|
110 |
if (isIOS()) setShowIOSModal(true);
|
111 |
-
const
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
setIsNotificationOpen(false);
|
119 |
}
|
120 |
};
|
121 |
-
|
122 |
-
|
123 |
-
|
|
|
|
|
124 |
|
125 |
-
|
126 |
-
return <div style={{ padding: '20px', textAlign: 'center' }}>مکالمه صوتی و تصویری هوشان</div>;
|
127 |
-
}
|
128 |
|
129 |
return (
|
130 |
-
<
|
131 |
-
<
|
132 |
<IOSModal isOpen={showIOSModal} onClose={() => setShowIOSModal(false)} />
|
133 |
-
</
|
134 |
);
|
135 |
}
|
136 |
|
|
|
1 |
// src/App.tsx
|
|
|
2 |
import React, { useEffect, useRef, useState } from "react";
|
3 |
import './App.scss';
|
4 |
+
import { AppProvider, useAppContext, PersonalityType, PersonalityInstructions } from "./contexts/AppContext";
|
5 |
import ControlTray from "./components/control-tray/ControlTray";
|
6 |
import { IOSModal } from "./components/ios-modal/IOSModal";
|
7 |
import { isIOS } from "./lib/platform";
|
8 |
import cn from "classnames";
|
|
|
9 |
import Logo from "./components/logo/Logo";
|
10 |
+
import { LiveConfig } from "./multimodal-live-types";
|
11 |
+
import { CustomModal } from "./components/CustomModal";
|
12 |
|
13 |
+
// منوی شخصیتها (به صورت یک کامپوننت داخلی ساده)
|
14 |
+
const PersonalityMenu: React.FC<{ isOpen: boolean; onClose: () => void; onSelect: (p: PersonalityType) => void; }> = ({ isOpen, onClose, onSelect }) => {
|
15 |
+
const menuRef = useRef<HTMLDivElement>(null);
|
16 |
+
const { selectedPersonality } = useAppContext();
|
17 |
+
const personalityIcons: Record<PersonalityType, string> = { default: "person", teacher: "school", poetic: "auto_awesome", funny: "sentiment_satisfied", custom: "tune" };
|
18 |
+
const personalityLabels: Record<PersonalityType, string> = { default: 'دستیار پیشفرض', teacher: 'استاد زبان', poetic: 'حس خوب', funny: 'شوخطبع', custom: 'شخصیت اختصاصی' };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
|
20 |
+
useEffect(() => {
|
21 |
+
const handleClickOutside = (e: MouseEvent) => menuRef.current && !menuRef.current.contains(e.target as Node) && onClose();
|
22 |
+
if (isOpen) document.addEventListener('mousedown', handleClickOutside);
|
23 |
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
24 |
+
}, [isOpen, onClose]);
|
25 |
+
|
26 |
+
if (!isOpen) return null;
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
+
return (
|
29 |
+
<div ref={menuRef} className="personality-popover-wrapper open">
|
30 |
+
<div className="popover-content">
|
31 |
+
<ul>
|
32 |
+
{(Object.keys(personalityIcons) as PersonalityType[]).map(key => (
|
33 |
+
<li key={key} className={cn({ active: selectedPersonality === key })} onClick={() => onSelect(key)}>
|
34 |
+
<div><span className="material-symbols-outlined">{personalityIcons[key]}</span>{personalityLabels[key]}</div>
|
35 |
+
{selectedPersonality === key && <span className="material-symbols-outlined tick">done</span>}
|
36 |
+
</li>
|
37 |
+
))}
|
38 |
+
</ul>
|
39 |
+
</div>
|
40 |
+
</div>
|
41 |
+
);
|
42 |
+
};
|
43 |
+
|
44 |
+
// کامپوننت داخلی اصلی برنامه
|
45 |
+
const AppInternal: React.FC = () => {
|
46 |
+
const videoRef = useRef<HTMLVideoElement>(null);
|
47 |
+
const { volume, changePersonality, customUserName, customInstructions } = useAppContext();
|
48 |
const [isUserSpeaking, setIsUserSpeaking] = useState(false);
|
49 |
+
const [isMicActive, setIsMicActive] = useState(false);
|
50 |
+
const [isCamActive, setIsCamActive] = useState(false);
|
51 |
+
const [currentFacingMode, setCurrentFacingMode] = useState<'user' | 'environment'>('user');
|
52 |
+
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
53 |
+
const [isPersonalityMenuOpen, setIsPersonalityMenuOpen] = useState(false);
|
54 |
+
const [isCustomModalOpen, setIsCustomModalOpen] = useState(false);
|
55 |
|
56 |
+
const handleSelectPersonality = (p: PersonalityType) => {
|
57 |
+
setIsPersonalityMenuOpen(false);
|
58 |
+
if (p === 'custom') setIsCustomModalOpen(true);
|
59 |
+
else changePersonality(p);
|
60 |
+
};
|
61 |
|
62 |
return (
|
63 |
+
<>
|
64 |
+
<div className="main-wrapper">
|
65 |
<div className="header-controls">
|
66 |
+
<button aria-label="انتخاب شخصیت" className="header-icon-button" onClick={() => setIsPersonalityMenuOpen(true)}>
|
67 |
+
<span className="material-symbols-outlined">psychology</span>
|
68 |
+
</button>
|
69 |
+
<button aria-label="اطلاعات" className="header-icon-button" onClick={() => setIsNotificationOpen(v => !v)}>
|
70 |
+
<span className="material-symbols-outlined">info</span>
|
71 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
72 |
</div>
|
73 |
+
|
74 |
+
{isNotificationOpen && <div className="notification-popover-wrapper open"><div className="popover-content">مدلهای هوش مصنوعی میتوانند اشتباه کنند.</div></div>}
|
75 |
+
|
76 |
+
<PersonalityMenu isOpen={isPersonalityMenuOpen} onClose={() => setIsPersonalityMenuOpen(false)} onSelect={handleSelectPersonality} />
|
77 |
+
|
78 |
+
<div className="media-area">
|
79 |
+
<video id="video-feed" ref={videoRef} autoPlay playsInline className={cn({ hidden: !isCamActive }, { "scale-x-[-1]": currentFacingMode === 'user' })} />
|
80 |
+
{isMicActive && !isCamActive && <div id="large-logo-container"><Logo isMini={false} isActive={true} isAi={false} speakingVolume={volume} isUserSpeaking={isUserSpeaking} /></div>}
|
81 |
</div>
|
82 |
+
|
83 |
+
<ControlTray videoRef={videoRef} onUserSpeakingChange={setIsUserSpeaking} isAppMicActive={isMicActive} onAppMicToggle={setIsMicActive} isAppCamActive={isCamActive} onAppCamToggle={setIsCamActive} currentFacingMode={currentFacingMode} onFacingModeChange={setCurrentFacingMode} />
|
|
|
84 |
</div>
|
85 |
+
<CustomModal isOpen={isCustomModalOpen} onClose={() => setIsCustomModalOpen(false)} onSave={(name, instructions) => changePersonality('custom', { name, instructions })} initialName={customUserName} initialInstructions={customInstructions} />
|
86 |
+
</>
|
87 |
);
|
88 |
};
|
89 |
|
90 |
+
// کامپوننت ریشه App
|
91 |
function App() {
|
|
|
92 |
const [showIOSModal, setShowIOSModal] = useState(false);
|
93 |
+
const [personalityInstructions, setPersonalityInstructions] = useState<PersonalityInstructions | null>(null);
|
94 |
+
const [loadingError, setLoadingError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
95 |
|
96 |
useEffect(() => {
|
97 |
if (isIOS()) setShowIOSModal(true);
|
98 |
+
const fetchInstructions = async () => {
|
99 |
+
try {
|
100 |
+
const res = await fetch('/api/instructions');
|
101 |
+
if (!res.ok) throw new Error(`Network error: ${res.status}`);
|
102 |
+
setPersonalityInstructions(await res.json());
|
103 |
+
} catch (e) {
|
104 |
+
setLoadingError("امکان دریافت تنظیمات شخصیتها و��ود ندارد. لطفاً صفحه را رفرش کنید.");
|
|
|
105 |
}
|
106 |
};
|
107 |
+
fetchInstructions();
|
108 |
+
}, []);
|
109 |
+
|
110 |
+
if (loadingError) return <div className="loading-screen">{loadingError}</div>;
|
111 |
+
if (!personalityInstructions) return <div className="loading-screen">در حال بارگذاری...</div>;
|
112 |
|
113 |
+
const initialAppConfig: LiveConfig = { model: "models/gemini-2.0-flash-exp" };
|
|
|
|
|
114 |
|
115 |
return (
|
116 |
+
<AppProvider initialConfig={initialAppConfig} personalityInstructions={personalityInstructions}>
|
117 |
+
<AppInternal />
|
118 |
<IOSModal isOpen={showIOSModal} onClose={() => setShowIOSModal(false)} />
|
119 |
+
</AppProvider>
|
120 |
);
|
121 |
}
|
122 |
|