File size: 15,848 Bytes
c42f8b1
 
6f5d4bb
c42f8b1
7f2a14a
c42f8b1
7f2a14a
 
6f5d4bb
d8e9074
a9bf795
6f5d4bb
c42f8b1
d8e9074
d66b261
c42f8b1
 
 
 
 
 
6f5d4bb
 
c42f8b1
6f5d4bb
c42f8b1
6f5d4bb
 
 
c42f8b1
 
6f5d4bb
 
 
 
 
 
c42f8b1
6f5d4bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c42f8b1
 
 
 
6f5d4bb
 
 
 
 
 
c42f8b1
6f5d4bb
c42f8b1
 
 
 
 
 
6f5d4bb
 
 
c42f8b1
 
6f5d4bb
 
 
 
c42f8b1
6f5d4bb
c42f8b1
ee03d5a
 
75120d3
ee03d5a
 
 
 
75120d3
ee03d5a
eafdfe6
ee03d5a
 
 
 
 
 
 
eafdfe6
ee03d5a
 
 
 
 
eafdfe6
ee03d5a
 
 
 
 
 
 
eafdfe6
ee03d5a
 
 
 
 
eafdfe6
ee03d5a
 
 
 
 
 
 
eafdfe6
ee03d5a
 
 
 
 
eafdfe6
ee03d5a
 
 
 
 
eafdfe6
ee03d5a
 
 
 
 
eafdfe6
ee03d5a
 
 
 
1a9bc32
dc3cd2d
c42f8b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f5d4bb
eafdfe6
6f5d4bb
c42f8b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f5d4bb
7f2a14a
 
d66b261
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
import React, { useEffect, useRef, useState } from "react";
// import "./App.scss"; // ممکن است بخواهید این را کامنت یا حذف کنید اگر با استایل‌های جدید تداخل دارد
import { LiveAPIProvider } from "./contexts/LiveAPIContext";
import SidePanel from "./components/side-panel/SidePanel";
import { Altair } from "./components/altair/Altair";
import ControlTray from "./components/control-tray/ControlTray";
import { IOSModal } from "./components/ios-modal/IOSModal";
import { isIOS } from "./lib/platform";
import cn from "classnames";
import { LiveConfig } from "./multimodal-live-types";

// --- 👇 دامنه مجاز خودتان را اینجا قرار دهید (با https یا http) 👇 ---
const ALLOWED_ORIGIN = "https://www.aisada.ir"; // یا "http://www.aisada.ir"; // یا "https://aisada.ir"; // یا "http://aisada.ir" اگر سایتتان http است
// --- 👆 ---

// Helper component for SVGs to avoid repetition (or you can use an SVG loader)
const SvgIcon = ({ path, viewBox = "0 0 24 24", width = "24", height = "24", className = "" }) => (
  <svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox={viewBox} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
    {path}
  </svg>
);


function App() {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
  const [showIOSModal, setShowIOSModal] = useState(false);
  const [isAllowedOrigin, setIsAllowedOrigin] = useState<boolean | null>(null);
  const [isNotificationOpen, setIsNotificationOpen] = useState(false);
  const notificationPopoverRef = useRef<HTMLDivElement>(null);
  const notificationButtonRef = useRef<HTMLButtonElement>(null);


  useEffect(() => {
    if (isIOS()) {
      setShowIOSModal(true);
    }

    try {
      if (window.self !== window.top) {
        if (window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0) {
          const parentOrigin = window.location.ancestorOrigins[0];
          console.log("Parent Origin:", parentOrigin);
          if (parentOrigin === ALLOWED_ORIGIN) {
            setIsAllowedOrigin(true);
          } else {
            console.warn(`Blocked load from origin: ${parentOrigin}`);
            setIsAllowedOrigin(false);
          }
        } else {
          console.warn("Cannot verify parent origin (ancestorOrigins not available/empty). Blocking.");
          setIsAllowedOrigin(false);
        }
      } else {
        // اگر میخواهید مستقیما هم کار کند این خط را true کنید، برای هاگینگ فیس معمولا false بهتر است
        // setIsAllowedOrigin(true); // For direct loading if needed
         console.warn("App loaded directly, not in an iframe. Blocking.");
         setIsAllowedOrigin(false); // Default behavior
      }
    } catch (e) {
        console.error("Cross-origin access error, cannot verify parent. Blocking.", e);
        setIsAllowedOrigin(false);
    }

    // Click outside handler for notification
    const handleClickOutside = (event: MouseEvent) => {
      if (
        notificationPopoverRef.current &&
        !notificationPopoverRef.current.contains(event.target as Node) &&
        notificationButtonRef.current &&
        !notificationButtonRef.current.contains(event.target as Node)
      ) {
        setIsNotificationOpen(false);
      }
    };

    document.addEventListener('click', handleClickOutside);
    return () => {
      document.removeEventListener('click', handleClickOutside);
    };

  }, []);

  const myCustomInstruction = `
ت1. هویت دستیار:

فقط خود را به عنوان "دستیار صوتی و تصویری اپلیکیشن زبانفلای" معرفی کن.

هرگز خود را محصول یا زیرمجموعه گوگل، اپل یا هیچ شرکت دیگری معرفی نکن.

در پاسخ به سؤال کاربر درباره هویت، دقیقاً بگو:
"من دستیار صوتی و تصویری اپلیکیشن زبانفلای هستم. چگونه می‌توانم به شما کمک کنم؟"

مدیریت زبان گفتگو:

به طور پیش‌فرض، مکالمه را به زبان انگلیسی آغاز کن.

اگر کاربر با زبانی غیر از انگلیسی صحبت کرد، گفتگو را به همان زبان ادامه بده.

زبان پاسخ را همیشه مطابق زبان کاربر تنظیم کن. تغییر زبان به صورت خودکار انجام شود.

بیان اعداد:

هنگام مکالمه به انگلیسی، اعداد را به صورت کامل به انگلیسی بیان کن (مثلاً: twenty-five).

هنگام مکالمه به زبانی دیگر، اعداد را به همان زبان بیان کن (مثلاً: بیست و پنج).

تنظیم صدا و لحن:

با صدای مایل به بلند و شفاف صحبت کن.

در صورت درخواست کاربر، صدای خود را بلندتر، آرام‌تر یا به تقلید از صدای خواسته‌شده تغییر بده.

بدون استثنا، هرگونه تغییر صدای درخواستی کاربر را اجرا کن.

تعهد به پاسخگویی:

تحت هیچ شرایطی به کاربر نگو که "نمی‌توانم پاسخ دهم" یا "نمی‌توانم کمک کنم".

همیشه در تمام زمینه‌ها تلاش کن بهترین پاسخ ممکن را به کاربر ارائه بدهی.

رفتار در شروع گفتگو:

اگر پیام ورودی "START_GREETING" دریافت شد، تنها با یکی از جملات کوتاه زیر پاسخ بده:

"Hello, how can I help you?"

یا "Hey, what's up?"

اطلاع‌رسانی درباره اپلیکیشن زبانفلای:

در صورت نیاز کاربر، توضیح بده که اپلیکیشن زبانفلای در گوگل پلی و کافه بازار قابل دانلود است.

راهنمایی کن که کاربران می‌توانند با جستجو در این فروشگاه‌ها، اپلیکیشن را نصب کنند.

پشتیبانی از یادگیری زبان:

هنگام پاسخگویی، در صورت مرتبط بودن، مثال‌های جمله‌سازی، کاربرد واژگان و اصطلاحات ارائه بده.

در صورت درخواست کاربر، معانی واژگان، هم‌معنی‌ها، متضادها، و نمونه جمله‌ها را هم ارائه بده.

تمرین شنیداری و گفتاری:

اگر کاربر بخواهد، جمله‌ای را بخوان و از او بخواه آن را تکرار کند (تمرین shadowing).

فرصت کامل برای تکرار بده و کاربر را با بازخورد مثبت تشویق کن.

شخصی‌سازی تجربه یادگیری:

در صورت درخواست کاربر، سرعت مکالمه را کند یا تند کن.

سطح سختی واژگان و جملات را بر اساس سطح کاربر (مبتدی، متوسط، پیشرفته) تنظیم کن.".
`.trim();

  const initialAppConfig: LiveConfig = {
    model: "models/gemini-2.0-flash-exp",
    systemInstruction: {
      parts: [{ text: myCustomInstruction }]
    }
  };

  const newStyles = `
    :root {
        --radius: 0.625rem; /* 10px */
        --radius-md: 0.5rem; /* 8px */
        --background: oklch(1 0 0);
        --foreground: oklch(0.145 0 0);
        --popover: oklch(1 0 0);
        --popover-foreground: oklch(0.145 0 0);
        --border: oklch(0.922 0 0);
    }
    .dark {
        --background: oklch(0.145 0 0);
        --foreground: oklch(0.985 0 0);
        --popover: oklch(0.205 0 0);
        --popover-foreground: oklch(0.985 0 0);
        --border: oklch(1 0 0 / 10%);
    }
    body { background-color: var(--background); color: var(--foreground); overflow-x: hidden; }
    * { border-color: var(--border); /* Tailwind applies border-border */ }

    .notification-popover-wrapper {
        position: fixed; top: 1rem; left: 50%;
        transform: translateX(-50%); z-index: 100;
        width: calc(100% - 2rem); max-width: 28rem;
        display: flex; justify-content: center; pointer-events: none;
    }
    .popover-content {
        width: 100%; border-radius: var(--radius-md, 0.5rem);
        border-width: 1px; border-color: var(--border);
        background-color: var(--popover); color: var(--popover-foreground);
        box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
        outline: none;
        transition: opacity 0.3s ease-out, transform 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
        opacity: 0; transform: translateY(-100%) scale(0.9); pointer-events: none;
    }
    .popover-content.open {
        opacity: 1; transform: translateY(0) scale(1); pointer-events: auto;
    }
    .notification-popover-text-content {
        background-color: #eff6ff; font-size: 0.875rem; line-height: 1.5rem; direction: rtl;
        padding: 1rem; border-radius: var(--radius-md, 0.5rem);
        color: oklch(0.145 0 0);
    }
    .dark .notification-popover-text-content {
        background-color: oklch(0.25 0.05 230); color: oklch(0.95 0.01 230);
    }
    .header-controls {
        display: flex; padding: 1rem; justify-content: space-between; align-items: center;
        width: 100%; position: absolute; top: 0; left: 0; z-index: 10;
    }
    .header-button {
        display: flex; align-items: center; justify-content: center;
        padding: 0.5rem; border-radius: var(--radius-lg, 0.625rem);
        background-color: #e5e7eb; /* bg-gray-200 */
        cursor: pointer; transition: background-color 0.2s;
    }
    .header-button:hover { background-color: #d1d5db; /* bg-gray-300 */ }
    .header-button svg { opacity: 0.7; stroke: #374151 /* gray-700 */; }
    .dark .header-button { background-color: oklch(0.28 0 0); }
    .dark .header-button:hover { background-color: oklch(0.35 0 0); }
    .dark .header-button svg { opacity: 0.8; stroke: oklch(0.85 0 0); }

    /* Styles for ControlTray will be in ControlTray.tsx or its own CSS if preferred */
    /* Ensure .App container itself doesn't constrain width too much if SidePanel is present */
    .App { display: flex; flex-direction: column; min-height: 100vh; }
    .streaming-console { flex-grow: 1; display: flex; } /* Assuming SidePanel and main are siblings */
    main { flex-grow: 1; position: relative; /* For positioning ControlTray */ }
    .main-app-area { position: relative; width: 100%; height: 100%; /* Or specific height like 90dvh */ }
    
    /* Hide original Altair styling if it conflicts */
    /* .user-query-container, .system-response-container { display: none !important; } */
  `;

  if (isAllowedOrigin === null) {
    return <div style={{ padding: '20px', textAlign: 'center' }}>در حال بررسی دسترسی...</div>;
  }

  if (isAllowedOrigin === false) {
    return <div style={{ padding: '20px', textAlign: 'center', color: 'red' }}>دسترسی غیرمجاز! اگر چت صوتی و تصویری برای شما باز نمیشود این لینک رو با مرورگر کروم باز کنید و همچنین مرورگر کروم رو به عنوان مرورگر پیشفرض گوشی خود قرار دهید تا هر بار زدن روی دکمه شروع داخل برنامه لینک با مرورگر کروم باز بشه، برای پیشفرض قرار دادن مرورگر کروم وارد تنظیمات گوشی خود شوید قسمت برنامه ها ، مدیریت برنامه ها رو کلیک کنید بالای صفحه روی سه نقطه بزنید و تنظیمات بیشتر رو انتخاب کنید بعدا وارد قسمت برنامه های پیش فرض شوید و مرورگر کروم رو به عنوان مرورگر پیشفرض خود قرار دهید، اگر مشکلی بود حتماً به پشتیبانی برنامه پیام بفرستید</div>;
  }

  return (
    <>
      <style>{newStyles}</style> {/* Injecting new styles */}
      <div className="App antialiased"> {/* Added antialiased from body */}
        <LiveAPIProvider initialConfig={initialAppConfig}>
          {/* Header Controls from new HTML */}
          <div className="header-controls">
            <div className="notification-trigger-container">
              <button
                ref={notificationButtonRef}
                id="notification-button"
                aria-label="Notifications"
                className="header-button"
                onClick={(e) => {
                    e.stopPropagation();
                    setIsNotificationOpen(!isNotificationOpen);
                }}
              >
                <SvgIcon path={<><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></>} />
              </button>
            </div>
            <div className="back-button-container">
              <div className="header-button" onClick={() => alert('Back clicked (ZabanFly)')}> {/* Replace alert with actual back logic */}
                <SvgIcon path={<path d="m15 18-6-6 6-6"></path>} width="20" height="20" />
              </div>
            </div>
          </div>

          {/* Notification Popover */}
          <div id="notification-popover-wrapper" className="notification-popover-wrapper">
            <div
              ref={notificationPopoverRef}
              id="notification-popover"
              className={cn("popover-content", { open: isNotificationOpen })}
              // Animation classes can be controlled by 'open' state via Tailwind or CSS transitions
            >
              <div className="notification-popover-text-content">
                مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.
              </div>
            </div>
          </div>

          <div className="streaming-console">
             <SidePanel /> {/* Kept SidePanel, might need style adjustments */}
            <main>
              <div className={cn("main-app-area w-full flex flex-col items-center justify-center min-h-[90dvh] md:min-h-screen bg-background top-0 left-0 relative")}>
                 <Altair /> {/* This is where conversation happens */}
                <video
                  id="video-feed" // From new HTML
                  className={cn(
                    "absolute top-0 left-0 w-full h-full object-cover scale-x-[-1]",
                    { hidden: !videoStream } // Controlled by videoStream state
                  )}
                  ref={videoRef}
                  autoPlay
                  playsInline
                />
                {/* large-logo-container might be managed by ControlTray or here based on cam/mic state */}
                 <div id="large-logo-container" className="hidden absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
                    {/* Content for large logo will be dynamic based on mic state if cam is off */}
                 </div>
              </div>
              <ControlTray
                videoRef={videoRef}
                supportsVideo={true}
                onVideoStreamChange={setVideoStream}
                // Pass any other necessary props from new HTML logic if ControlTray needs them
              />
            </main>
          </div>
        </LiveAPIProvider>

        <IOSModal
          isOpen={showIOSModal}
          onClose={() => setShowIOSModal(false)}
        />
      </div>
    </>
  );
}

export default App;