looda3131 commited on
Commit
33bebb8
·
1 Parent(s): a9a5c10

جيد الان بالنسبه للغه اريدها ان تشمل التطبيق كاملا واريد تعدد اللغات

Browse files
src/ai/flows/ai-generate-post-ideas.ts CHANGED
@@ -1,29 +1,33 @@
1
-
2
  'use server';
3
 
4
  import { safeGenerateContent } from '@/lib/gemini-client';
5
  import type { GeneratePostIdeasInput, GeneratePostIdeasOutput } from './types';
6
 
7
- const promptTemplate = (count: number, location: string, language: string, age?: number) => `You are a personalized social media strategist using Gemini 2.5 Flash Lite capabilities.
8
- Generate EXACTLY ${count} high-quality, engaging social media posts tailored to a user with this profile:
9
- - Location/Country: ${location}
10
- - Age: ${age || 'any'}
11
- - Language: ${language === 'en' ? 'English' : 'the local dialect and culture of ' + location}
12
 
13
- STRICT CONTENT GUIDELINES:
14
- 1. PERSONALIZATION: Posts MUST be highly relevant to someone aged ${age || 'any'} living in ${location}. Use trends, slang, and topics that specifically attract this demographic.
15
- 2. GEOGRAPHY: Be culturally specific to ${location}. Do NOT mention Egypt unless the location is actually Egypt. Talk about local habits, food, or landmarks.
16
- 3. CULTURAL SAFETY: Absolutely NO sexual content, NO nudity, NO alcohol/drugs, NO religious offense, NO political controversy. Follow the conservative values of ${location} if applicable.
17
- 4. IMAGERY: The "imageQuery" MUST NOT contain any humans, faces, people, or body parts. Focus on landscapes, abstract art, nature, architecture, or objects.
18
- 5. FORMAT: Return ONLY a valid JSON object. No markdown blocks. Just the raw JSON.
 
 
 
 
 
19
 
20
  JSON Schema:
21
  {
22
  "posts": [
23
  {
24
- "content": "Post text in the local dialect/language",
25
  "isMeme": boolean,
26
- "imageQuery": "Descriptive English query (STRICTLY NO HUMANS/PEOPLE)",
27
  "category": "funny | sad | deep | angry | inspirational | general",
28
  "initialComments": ["Local dialect comment 1", "Local dialect comment 2"]
29
  }
@@ -34,16 +38,17 @@ JSON Schema:
34
  export async function generatePostIdeas(
35
  input: GeneratePostIdeasInput & { age?: number }
36
  ): Promise<GeneratePostIdeasOutput> {
37
- const location = input.location || 'Unknown';
38
  const language = input.language || 'ar';
39
- const count = 30; // Enforce 30 posts as requested
40
 
41
  const prompt = promptTemplate(count, location, language, input.age);
42
  const output = await safeGenerateContent(prompt);
43
 
44
  if (!output || !Array.isArray(output.posts)) {
45
- throw new Error("Invalid output format from AI");
46
  }
47
 
 
48
  return output as GeneratePostIdeasOutput;
49
  }
 
 
1
  'use server';
2
 
3
  import { safeGenerateContent } from '@/lib/gemini-client';
4
  import type { GeneratePostIdeasInput, GeneratePostIdeasOutput } from './types';
5
 
6
+ const promptTemplate = (count: number, location: string, language: string, age?: number) => `You are a world-class AI social media strategist using Gemini 2.5 Flash Lite.
7
+ Your task is to generate EXACTLY ${count} highly engaging and personalized social media posts for a user with the following profile:
8
+ - Location: ${location}
9
+ - User Age: ${age || 'Unknown'}
10
+ - Preferred Interface Language: ${language === 'en' ? 'English' : 'Arabic'}
11
 
12
+ STRICT STRATEGY GUIDELINES:
13
+ 1. PERSONALIZATION: Tailor the tone and topics to someone aged ${age || 'any'} living in ${location}. Use relevant slang, trends, and interests for this demographic.
14
+ 2. LOCAL CONTEXT: If the user is in an Arabic-speaking country, use the local dialect. If they are in a Western country, use the local culture and language. Do NOT default to Egypt unless the location is Egypt.
15
+ 3. CONTENT SAFETY:
16
+ - STRICTLY NO sexual content, nudity, or alcohol/drugs.
17
+ - AVOID religious controversy or offensive political statements.
18
+ - RESPECT the cultural values of ${location}.
19
+ 4. IMAGERY (imageQuery):
20
+ - The "imageQuery" MUST BE in English.
21
+ - CRITICAL: NO humans, people, faces, eyes, or body parts in the imagery. Focus on nature, abstract patterns, architecture, technology, or objects.
22
+ 5. FORMAT: Return ONLY a valid JSON object. No markdown blocks.
23
 
24
  JSON Schema:
25
  {
26
  "posts": [
27
  {
28
+ "content": "Post text in the appropriate local dialect/language",
29
  "isMeme": boolean,
30
+ "imageQuery": "Precise English search query (STRICTLY NO PEOPLE/HUMANS)",
31
  "category": "funny | sad | deep | angry | inspirational | general",
32
  "initialComments": ["Local dialect comment 1", "Local dialect comment 2"]
33
  }
 
38
  export async function generatePostIdeas(
39
  input: GeneratePostIdeasInput & { age?: number }
40
  ): Promise<GeneratePostIdeasOutput> {
41
+ const location = input.location || 'Global';
42
  const language = input.language || 'ar';
43
+ const count = 30; // Enforcing exactly 30 posts as requested
44
 
45
  const prompt = promptTemplate(count, location, language, input.age);
46
  const output = await safeGenerateContent(prompt);
47
 
48
  if (!output || !Array.isArray(output.posts)) {
49
+ throw new Error("Invalid output format from AI model");
50
  }
51
 
52
+ // Ensure we have exactly 30 if possible, or at least the generated ones
53
  return output as GeneratePostIdeasOutput;
54
  }
src/app/layout.tsx CHANGED
@@ -1,21 +1,37 @@
1
-
2
  "use client";
3
 
4
  import { ThemeProvider } from '@/components/theme-provider';
5
  import { Toaster } from '@/components/ui/toaster';
6
  import { AIWorldProvider } from '@/contexts/ai-world-context';
7
  import { AuthProvider } from '@/contexts/auth-context';
8
- import { LanguageProvider } from '@/contexts/language-context';
9
  import Script from 'next/script';
 
10
  import './globals.css';
11
 
12
- export default function RootLayout({
13
- children,
14
- }: Readonly<{
15
- children: React.ReactNode;
16
- }>) {
17
  return (
18
- <html lang="ar" dir="rtl" suppressHydrationWarning>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  <head>
20
  <title>ProtoChat</title>
21
  <meta name="description" content="An AI-powered chat application" />
@@ -27,16 +43,9 @@ export default function RootLayout({
27
  />
28
  </head>
29
  <body className="font-body antialiased">
30
- <LanguageProvider>
31
- <ThemeProvider>
32
- <AuthProvider>
33
- <AIWorldProvider>
34
- {children}
35
- <Toaster />
36
- </AIWorldProvider>
37
- </AuthProvider>
38
- </ThemeProvider>
39
- </LanguageProvider>
40
  <Script
41
  src="https://accounts.google.com/gsi/client"
42
  strategy="afterInteractive"
@@ -45,3 +54,15 @@ export default function RootLayout({
45
  </html>
46
  );
47
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  "use client";
2
 
3
  import { ThemeProvider } from '@/components/theme-provider';
4
  import { Toaster } from '@/components/ui/toaster';
5
  import { AIWorldProvider } from '@/contexts/ai-world-context';
6
  import { AuthProvider } from '@/contexts/auth-context';
7
+ import { LanguageProvider, useLanguage } from '@/contexts/language-context';
8
  import Script from 'next/script';
9
+ import { useEffect, useState } from 'react';
10
  import './globals.css';
11
 
12
+ function Providers({ children }: { children: React.ReactNode }) {
 
 
 
 
13
  return (
14
+ <ThemeProvider>
15
+ <AuthProvider>
16
+ <AIWorldProvider>
17
+ {children}
18
+ <Toaster />
19
+ </AIWorldProvider>
20
+ </AuthProvider>
21
+ </ThemeProvider>
22
+ );
23
+ }
24
+
25
+ function LayoutContent({ children }: { children: React.ReactNode }) {
26
+ const { lang } = useLanguage();
27
+ const [mounted, setMounted] = useState(false);
28
+
29
+ useEffect(() => {
30
+ setMounted(true);
31
+ }, []);
32
+
33
+ return (
34
+ <html lang={lang} dir={lang === 'ar' ? 'rtl' : 'ltr'} suppressHydrationWarning>
35
  <head>
36
  <title>ProtoChat</title>
37
  <meta name="description" content="An AI-powered chat application" />
 
43
  />
44
  </head>
45
  <body className="font-body antialiased">
46
+ <Providers>
47
+ {mounted ? children : <div className="min-h-screen bg-background" />}
48
+ </Providers>
 
 
 
 
 
 
 
49
  <Script
50
  src="https://accounts.google.com/gsi/client"
51
  strategy="afterInteractive"
 
54
  </html>
55
  );
56
  }
57
+
58
+ export default function RootLayout({
59
+ children,
60
+ }: Readonly<{
61
+ children: React.ReactNode;
62
+ }>) {
63
+ return (
64
+ <LanguageProvider>
65
+ <LayoutContent>{children}</LayoutContent>
66
+ </LanguageProvider>
67
+ );
68
+ }
src/app/login/page.tsx CHANGED
@@ -89,8 +89,7 @@ export default function LoginPage() {
89
  <Card className="w-full max-w-md">
90
  <form onSubmit={handleCompleteProfile}>
91
  <CardHeader>
92
- <CardTitle>إكمال البيانات</CardTitle>
93
- <CardDescription>يرجى كتابة بلدك وعمرك للمتابعة</CardDescription>
94
  </CardHeader>
95
  <CardContent className="space-y-4">
96
  <div className="space-y-2">
@@ -99,11 +98,11 @@ export default function LoginPage() {
99
  </div>
100
  <div className="space-y-2">
101
  <Label>{t.location}</Label>
102
- <Input value={location} onChange={e => setLocation(e.target.value)} placeholder="مثلاً: مصر، الرياض، لندن..." required />
103
  </div>
104
  </CardContent>
105
  <CardFooter>
106
- <Button type="submit" className="w-full" disabled={isSigningIn}>إكمال الدخول</Button>
107
  </CardFooter>
108
  </form>
109
  </Card>
@@ -138,7 +137,7 @@ export default function LoginPage() {
138
  </CardHeader>
139
  <CardContent className="py-8">
140
  <Button variant="outline" size="lg" className="w-full h-16 text-lg rounded-xl" onClick={signInWithGoogle} disabled={isSigningIn}>
141
- <Chrome className="ml-3 h-6 w-6 text-primary" />
142
  <span>{t.googleAction}</span>
143
  </Button>
144
  </CardContent>
@@ -167,13 +166,13 @@ export default function LoginPage() {
167
  </div>
168
  <div className="space-y-2">
169
  <Label>{t.location}</Label>
170
- <Input value={location} onChange={e => setLocation(e.target.value)} placeholder="البلد" required />
171
  </div>
172
  </div>
173
  </CardContent>
174
  <CardFooter>
175
  <Button type="submit" className="w-full h-12" disabled={isSigningIn}>
176
- {isSigningIn ? <Loader2 className="animate-spin" /> : <LogIn className="ml-2" />}
177
  {t.loginAction}
178
  </Button>
179
  </CardFooter>
 
89
  <Card className="w-full max-w-md">
90
  <form onSubmit={handleCompleteProfile}>
91
  <CardHeader>
92
+ <CardTitle>{t.completeProfile}</CardTitle>
 
93
  </CardHeader>
94
  <CardContent className="space-y-4">
95
  <div className="space-y-2">
 
98
  </div>
99
  <div className="space-y-2">
100
  <Label>{t.location}</Label>
101
+ <Input value={location} onChange={e => setLocation(e.target.value)} placeholder={t.location} required />
102
  </div>
103
  </CardContent>
104
  <CardFooter>
105
+ <Button type="submit" className="w-full" disabled={isSigningIn}>{t.loginAction}</Button>
106
  </CardFooter>
107
  </form>
108
  </Card>
 
137
  </CardHeader>
138
  <CardContent className="py-8">
139
  <Button variant="outline" size="lg" className="w-full h-16 text-lg rounded-xl" onClick={signInWithGoogle} disabled={isSigningIn}>
140
+ <Chrome className="mx-3 h-6 w-6 text-primary" />
141
  <span>{t.googleAction}</span>
142
  </Button>
143
  </CardContent>
 
166
  </div>
167
  <div className="space-y-2">
168
  <Label>{t.location}</Label>
169
+ <Input value={location} onChange={e => setLocation(e.target.value)} placeholder={t.location} required />
170
  </div>
171
  </div>
172
  </CardContent>
173
  <CardFooter>
174
  <Button type="submit" className="w-full h-12" disabled={isSigningIn}>
175
+ {isSigningIn ? <Loader2 className="animate-spin" /> : <LogIn className="mx-2" />}
176
  {t.loginAction}
177
  </Button>
178
  </CardFooter>
src/app/profile/page.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  'use client';
2
 
3
  import { useState, useEffect } from 'react';
@@ -11,6 +12,7 @@ import { Input } from '@/components/ui/input';
11
  import { Label } from '@/components/ui/label';
12
  import { Loader2, Trash2, AlertTriangle } from 'lucide-react';
13
  import { useToast } from '@/hooks/use-toast';
 
14
  import {
15
  AlertDialog,
16
  AlertDialogAction,
@@ -25,6 +27,7 @@ import {
25
 
26
  export default function ProfilePage() {
27
  const { user, userData, loading, updateUserProfile, deleteUserAccount } = useAuth();
 
28
  const router = useRouter();
29
  const { toast } = useToast();
30
 
@@ -50,7 +53,7 @@ export default function ProfilePage() {
50
  const handleSave = async () => {
51
  if (!user) return;
52
  if (!displayName.trim()) {
53
- toast({ title: 'خطأ', description: 'لا يمكن ترك اسم العرض فارغاً.', variant: 'destructive' });
54
  return;
55
  }
56
  setIsSaving(true);
@@ -64,7 +67,14 @@ export default function ProfilePage() {
64
 
65
  const handleDeleteAccount = async () => {
66
  setIsDeleting(true);
67
- try { await deleteUserAccount(); } catch (error) { console.error(error); } finally { setIsDeleting(false); }
 
 
 
 
 
 
 
68
  };
69
 
70
  if (!user || loading) {
@@ -77,13 +87,12 @@ export default function ProfilePage() {
77
 
78
  return (
79
  <div className="flex h-screen w-full flex-col bg-background">
80
- <Header title="الملف الشخصي" />
81
  <main className="flex-1 overflow-y-auto bg-muted/40 p-4 md:p-8">
82
  <div className="mx-auto max-w-2xl space-y-6">
83
  <Card className="border-border/50 shadow-sm">
84
  <CardHeader>
85
- <CardTitle>إعدادات الحساب</CardTitle>
86
- <CardDescription>قم بتحديث معلومات ملفك الشخصي هنا.</CardDescription>
87
  </CardHeader>
88
  <CardContent className="space-y-6">
89
  <div className="flex items-center gap-4">
@@ -95,21 +104,21 @@ export default function ProfilePage() {
95
  </Avatar>
96
  <div>
97
  <h3 className="text-xl font-bold">{user.displayName}</h3>
98
- <p className="text-sm text-muted-foreground">{user.email || 'حساب يدوي'}</p>
99
  </div>
100
  </div>
101
  <div className="space-y-4">
102
  <div className="space-y-2">
103
- <Label htmlFor="displayName">اسم العرض</Label>
104
  <Input id="displayName" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
105
  </div>
106
  <div className="grid grid-cols-2 gap-4">
107
  <div className="space-y-2">
108
- <Label htmlFor="age">العمر</Label>
109
  <Input id="age" type="number" value={age} onChange={(e) => setAge(e.target.value)} />
110
  </div>
111
  <div className="space-y-2">
112
- <Label htmlFor="location">البلد</Label>
113
  <Input id="location" value={location} onChange={(e) => setLocation(e.target.value)} />
114
  </div>
115
  </div>
@@ -117,8 +126,8 @@ export default function ProfilePage() {
117
  </CardContent>
118
  <CardFooter className="border-t px-6 py-4 bg-muted/20">
119
  <Button onClick={handleSave} disabled={isSaving} className="rounded-xl px-8">
120
- {isSaving && <Loader2 className="ml-2 h-4 w-4 animate-spin" />}
121
- حفظ التغييرات
122
  </Button>
123
  </CardFooter>
124
  </Card>
@@ -127,30 +136,30 @@ export default function ProfilePage() {
127
  <CardHeader className="bg-destructive/5">
128
  <div className="flex items-center gap-2 text-destructive">
129
  <AlertTriangle className="h-5 w-5" />
130
- <CardTitle className="text-lg">منطقة الخطر</CardTitle>
131
  </div>
132
  </CardHeader>
133
  <CardContent className="pt-6">
134
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
135
  <div className="space-y-1">
136
- <h4 className="font-bold">حذف الحساب نهائياً</h4>
137
- <p className="text-sm text-muted-foreground">سيتم حذف جميع بياناتك ومحادثاتك بشكل دائم.</p>
138
  </div>
139
  <AlertDialog>
140
  <AlertDialogTrigger asChild>
141
  <Button variant="destructive" className="rounded-xl flex items-center gap-2">
142
- <Trash2 className="h-4 w-4" /> حذف الحساب
143
  </Button>
144
  </AlertDialogTrigger>
145
  <AlertDialogContent className="rounded-2xl">
146
  <AlertDialogHeader>
147
- <AlertDialogTitle>هل أنت متأكد تماماً؟</AlertDialogTitle>
148
- <AlertDialogDescription>هذا الإجراء سيقوم بحذف حسابك نهائياً ولن تتمكن من استعادتها.</AlertDialogDescription>
149
  </AlertDialogHeader>
150
  <AlertDialogFooter>
151
- <AlertDialogCancel>إلغاء</AlertDialogCancel>
152
  <AlertDialogAction onClick={handleDeleteAccount} className="bg-destructive hover:bg-destructive/90" disabled={isDeleting}>
153
- نعم، احذف حسابي
154
  </AlertDialogAction>
155
  </AlertDialogFooter>
156
  </AlertDialogContent>
@@ -162,4 +171,4 @@ export default function ProfilePage() {
162
  </main>
163
  </div>
164
  );
165
- }
 
1
+
2
  'use client';
3
 
4
  import { useState, useEffect } from 'react';
 
12
  import { Label } from '@/components/ui/label';
13
  import { Loader2, Trash2, AlertTriangle } from 'lucide-react';
14
  import { useToast } from '@/hooks/use-toast';
15
+ import { useLanguage } from '@/contexts/language-context';
16
  import {
17
  AlertDialog,
18
  AlertDialogAction,
 
27
 
28
  export default function ProfilePage() {
29
  const { user, userData, loading, updateUserProfile, deleteUserAccount } = useAuth();
30
+ const { t } = useLanguage();
31
  const router = useRouter();
32
  const { toast } = useToast();
33
 
 
53
  const handleSave = async () => {
54
  if (!user) return;
55
  if (!displayName.trim()) {
56
+ toast({ title: 'Error', description: 'Display name cannot be empty.', variant: 'destructive' });
57
  return;
58
  }
59
  setIsSaving(true);
 
67
 
68
  const handleDeleteAccount = async () => {
69
  setIsDeleting(true);
70
+ try {
71
+ await deleteUserAccount();
72
+ toast({ title: t.accountDeleted });
73
+ } catch (error) {
74
+ console.error(error);
75
+ } finally {
76
+ setIsDeleting(false);
77
+ }
78
  };
79
 
80
  if (!user || loading) {
 
87
 
88
  return (
89
  <div className="flex h-screen w-full flex-col bg-background">
90
+ <Header title={t.profileTitle} />
91
  <main className="flex-1 overflow-y-auto bg-muted/40 p-4 md:p-8">
92
  <div className="mx-auto max-w-2xl space-y-6">
93
  <Card className="border-border/50 shadow-sm">
94
  <CardHeader>
95
+ <CardTitle>{t.settings}</CardTitle>
 
96
  </CardHeader>
97
  <CardContent className="space-y-6">
98
  <div className="flex items-center gap-4">
 
104
  </Avatar>
105
  <div>
106
  <h3 className="text-xl font-bold">{user.displayName}</h3>
107
+ <p className="text-sm text-muted-foreground">{user.email || 'Manual Account'}</p>
108
  </div>
109
  </div>
110
  <div className="space-y-4">
111
  <div className="space-y-2">
112
+ <Label htmlFor="displayName">{t.username}</Label>
113
  <Input id="displayName" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
114
  </div>
115
  <div className="grid grid-cols-2 gap-4">
116
  <div className="space-y-2">
117
+ <Label htmlFor="age">{t.age}</Label>
118
  <Input id="age" type="number" value={age} onChange={(e) => setAge(e.target.value)} />
119
  </div>
120
  <div className="space-y-2">
121
+ <Label htmlFor="location">{t.location}</Label>
122
  <Input id="location" value={location} onChange={(e) => setLocation(e.target.value)} />
123
  </div>
124
  </div>
 
126
  </CardContent>
127
  <CardFooter className="border-t px-6 py-4 bg-muted/20">
128
  <Button onClick={handleSave} disabled={isSaving} className="rounded-xl px-8">
129
+ {isSaving && <Loader2 className="mx-2 h-4 w-4 animate-spin" />}
130
+ {t.saveChanges}
131
  </Button>
132
  </CardFooter>
133
  </Card>
 
136
  <CardHeader className="bg-destructive/5">
137
  <div className="flex items-center gap-2 text-destructive">
138
  <AlertTriangle className="h-5 w-5" />
139
+ <CardTitle className="text-lg">{t.dangerZone}</CardTitle>
140
  </div>
141
  </CardHeader>
142
  <CardContent className="pt-6">
143
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
144
  <div className="space-y-1">
145
+ <h4 className="font-bold">{t.deleteAccount}</h4>
146
+ <p className="text-sm text-muted-foreground">{t.deleteConfirmDesc}</p>
147
  </div>
148
  <AlertDialog>
149
  <AlertDialogTrigger asChild>
150
  <Button variant="destructive" className="rounded-xl flex items-center gap-2">
151
+ <Trash2 className="h-4 w-4" /> {t.deleteAccount}
152
  </Button>
153
  </AlertDialogTrigger>
154
  <AlertDialogContent className="rounded-2xl">
155
  <AlertDialogHeader>
156
+ <AlertDialogTitle>{t.deleteConfirmTitle}</AlertDialogTitle>
157
+ <AlertDialogDescription>{t.deleteConfirmDesc}</AlertDialogDescription>
158
  </AlertDialogHeader>
159
  <AlertDialogFooter>
160
+ <AlertDialogCancel>{t.cancel}</AlertDialogCancel>
161
  <AlertDialogAction onClick={handleDeleteAccount} className="bg-destructive hover:bg-destructive/90" disabled={isDeleting}>
162
+ {t.confirmDelete}
163
  </AlertDialogAction>
164
  </AlertDialogFooter>
165
  </AlertDialogContent>
 
171
  </main>
172
  </div>
173
  );
174
+ }
src/components/ai-world/ai-world-feed.tsx CHANGED
@@ -13,20 +13,22 @@ import { useToast } from '@/hooks/use-toast';
13
  import type { AIComment, AIUser, PostWithUIState } from '@/lib/types';
14
  import { AIWorldContext } from '@/contexts/ai-world-context';
15
  import { formatDistanceToNow } from 'date-fns';
16
- import { ar } from 'date-fns/locale';
17
  import { checkConsumption } from '@/lib/consumption';
18
  import { useAuth } from '@/contexts/auth-context';
19
  import type { User } from 'firebase/auth';
20
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu';
 
21
 
22
-
23
- const AiPostCard = React.memo(({ post, onReact, onAddComment, onReport, onToggleComments, currentUser }: {
24
  post: PostWithUIState,
25
  onReact: (postId: string, reaction: string) => void,
26
  onAddComment: (postId: string, commentText: string) => void,
27
  onReport: (postId: string) => void,
28
  onToggleComments: (postId: string) => void,
29
  currentUser: User | null;
 
 
30
  }) => {
31
  const [commentText, setCommentText] = useState("");
32
  const [formattedDate, setFormattedDate] = useState('');
@@ -48,10 +50,10 @@ const AiPostCard = React.memo(({ post, onReact, onAddComment, onReport, onToggle
48
  if (post.createdAt) {
49
  setFormattedDate(formatDistanceToNow(new Date(post.createdAt), {
50
  addSuffix: true,
51
- locale: ar,
52
  }));
53
  }
54
- }, [post.createdAt]);
55
 
56
  const handleCommentSubmit = () => {
57
  if (commentText.trim()) {
@@ -99,8 +101,8 @@ const AiPostCard = React.memo(({ post, onReact, onAddComment, onReport, onToggle
99
  </DropdownMenuTrigger>
100
  <DropdownMenuContent align="end">
101
  <DropdownMenuItem onClick={() => onReport(post.id)}>
102
- <Flag className="ml-2 h-4 w-4" />
103
- <span>إبلاغ عن المحتوى</span>
104
  </DropdownMenuItem>
105
  </DropdownMenuContent>
106
  </DropdownMenu>
@@ -143,7 +145,7 @@ const AiPostCard = React.memo(({ post, onReact, onAddComment, onReport, onToggle
143
  </div>
144
  }
145
  { totalComments > 0 &&
146
- <span className='hover:underline cursor-pointer' onClick={() => onToggleComments(post.id)}>{totalComments} تعليقات</span>
147
  }
148
  </div>
149
  }
@@ -151,15 +153,15 @@ const AiPostCard = React.memo(({ post, onReact, onAddComment, onReport, onToggle
151
  <div className="flex justify-around border-y">
152
  <Button variant="ghost" className={cn("w-full flex items-center gap-2", isLiked && "text-primary font-semibold")} onClick={() => onReact(post.id, 'like')}>
153
  <ThumbsUp />
154
- <span>أعجبني</span>
155
  </Button>
156
  <Button variant="ghost" className="w-full flex items-center gap-2" onClick={handleCommentActionClick}>
157
  <MessageCircle />
158
- <span>تعليق</span>
159
  </Button>
160
  <Button variant="ghost" className="w-full flex items-center gap-2">
161
  <Share2 />
162
- <span>مشاركة</span>
163
  </Button>
164
  </div>
165
 
@@ -181,16 +183,16 @@ const AiPostCard = React.memo(({ post, onReact, onAddComment, onReport, onToggle
181
  {post.uiState.isGeneratingComment && (
182
  <div className="flex items-center gap-2 text-sm text-muted-foreground">
183
  <Loader2 className="h-4 w-4 animate-spin"/>
184
- <span>يتم إضافة تعليقات جديدة...</span>
185
  </div>
186
  )}
187
  {currentUser && (
188
  <div className="flex items-center gap-2 pt-2">
189
- <Image src={currentUser?.photoURL || `https://picsum.photos/seed/${currentUser?.uid || 'default'}/32/32`} alt={currentUser?.displayName || 'أنت'} width={32} height={32} className="w-8 h-8 rounded-full" />
190
  <div className="relative w-full">
191
  <Input
192
  ref={commentInputRef}
193
- placeholder="اكتب تعليقاً..."
194
  value={commentText}
195
  onChange={(e) => setCommentText(e.target.value)}
196
  onKeyDown={(e) => e.key === 'Enter' && handleCommentSubmit()}
@@ -235,11 +237,12 @@ export const AIWorldFeed: React.FC<AIWorldFeedProps> = ({ onBack }) => {
235
  } = useContext(AIWorldContext);
236
 
237
  const { user } = useAuth();
 
238
  const observer = useRef<IntersectionObserver | null>(null);
239
  const [consumption, setConsumption] = useState({ used: 0, remaining: 0, limit: 200 });
240
  const [searchQuery, setSearchQuery] = useState('');
241
  const [isSearchActive, setIsSearchActive] = useState(false);
242
- const userName = user?.displayName?.split(' ')[0] || 'أنت';
243
 
244
  useEffect(() => {
245
  const { remaining } = checkConsumption();
@@ -309,7 +312,7 @@ export const AIWorldFeed: React.FC<AIWorldFeedProps> = ({ onBack }) => {
309
  }, [toggleCommentsVisibility]);
310
 
311
  const sortedMainFeed = [...allPosts].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
312
- const sortedSearchResults = [...searchedPosts].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
313
 
314
  const postsToDisplay = isSearchActive ? sortedSearchResults : sortedMainFeed;
315
 
@@ -321,12 +324,12 @@ export const AIWorldFeed: React.FC<AIWorldFeedProps> = ({ onBack }) => {
321
  <ArrowLeft />
322
  </Button>
323
  )}
324
- <h1 className="text-xl font-bold text-primary">صدى الأثير</h1>
325
  <div className="flex items-center gap-2 ml-auto">
326
  <div className="relative flex-1 max-w-xs">
327
  <Search className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
328
  <Input
329
- placeholder="بحث..."
330
  value={searchQuery}
331
  onChange={(e) => setSearchQuery(e.target.value)}
332
  onKeyDown={(e) => {
@@ -350,9 +353,9 @@ export const AIWorldFeed: React.FC<AIWorldFeedProps> = ({ onBack }) => {
350
  </PopoverTrigger>
351
  <PopoverContent className="w-auto text-sm mr-4">
352
  <div className="space-y-2">
353
- <p className="font-bold text-center">استهلاك الـAPI اليومي</p>
354
- <p>المستخدم: {consumption.used} / {consumption.limit}</p>
355
- <p>المتبقي: {consumption.remaining}</p>
356
  </div>
357
  </PopoverContent>
358
  </Popover>
@@ -363,10 +366,10 @@ export const AIWorldFeed: React.FC<AIWorldFeedProps> = ({ onBack }) => {
363
  {user && !isSearchActive && (
364
  <div className="bg-card p-3 rounded-lg border shadow-sm">
365
  <div className="flex items-center gap-3">
366
- <Image src={user?.photoURL || `https://picsum.photos/seed/${user?.uid || 'default'}/40/40`} alt={user?.displayName || 'أنت'} width={40} height={40} className="w-10 h-10 rounded-full" />
367
  <div className="w-full">
368
  <Input
369
- placeholder={`بماذا تفكر يا ${userName}?`}
370
  onKeyDown={(e) => {
371
  if (e.key === 'Enter' && e.currentTarget.value.trim()) {
372
  createPostFromUserInput(e.currentTarget.value);
@@ -378,9 +381,9 @@ export const AIWorldFeed: React.FC<AIWorldFeedProps> = ({ onBack }) => {
378
  </div>
379
  </div>
380
  <div className="border-t mt-3 pt-2 flex justify-around">
381
- <Button variant="ghost" className="w-full flex items-center gap-2" onClick={() => generatePostsFromAI("موضوع عشوائي")}>
382
  <Sparkles className="text-purple-500" />
383
- <span>ولّد منشورات</span>
384
  </Button>
385
  </div>
386
  </div>
@@ -414,29 +417,24 @@ export const AIWorldFeed: React.FC<AIWorldFeedProps> = ({ onBack }) => {
414
  onReport={handleReport}
415
  onToggleComments={handleToggleComments}
416
  currentUser={user}
 
 
417
  />
418
  </div>
419
  ))
420
  )}
421
- { isLoading && !isSearchActive && allPosts.length > 0 && (
422
  <div className="text-center text-muted-foreground py-4 space-y-4">
423
- {Array.from({ length: 1 }).map((_, i) => (
424
- <div key={`loading-${i}`} className="bg-card border rounded-lg shadow-sm w-full space-y-3 p-4">
425
- <div className='flex items-center gap-3'>
426
- <Skeleton className="h-10 w-10 rounded-full" />
427
- <div className='space-y-2'>
428
- <Skeleton className="h-4 w-24" />
429
- <Skeleton className="h-3 w-16" />
430
- </div>
431
- </div>
432
- <Skeleton className="h-6 w-3/4" />
433
- </div>
434
- ))}
435
- </div>
436
- )}
437
- {!isLoading && !canLoadMore && !isSearchActive && sortedMainFeed.length > 0 && (
438
- <div className="text-center text-muted-foreground p-4">
439
- <p>وصلت إلى نهاية المنشورات.</p>
440
  </div>
441
  )}
442
  {postsToDisplay.length === 0 && !isLoading && !isGeneratingFromQuery && !isSearching && (
@@ -444,10 +442,10 @@ export const AIWorldFeed: React.FC<AIWorldFeedProps> = ({ onBack }) => {
444
  {isSearchActive ? (
445
  <>
446
  <Search className="h-12 w-12 text-muted-foreground/50" />
447
- <p>لم يتم العثور على نتائج لبحثك.</p>
448
  </>
449
  ) : (
450
- <p>لا توجد منشورات حاليًا. جرب البحث عن موضوع أو اضغط "ولّد منشورات" لتبدأ!</p>
451
  )}
452
  </div>
453
  )}
@@ -456,5 +454,3 @@ export const AIWorldFeed: React.FC<AIWorldFeedProps> = ({ onBack }) => {
456
  </div>
457
  );
458
  }
459
-
460
-
 
13
  import type { AIComment, AIUser, PostWithUIState } from '@/lib/types';
14
  import { AIWorldContext } from '@/contexts/ai-world-context';
15
  import { formatDistanceToNow } from 'date-fns';
16
+ import { ar, enUS } from 'date-fns/locale';
17
  import { checkConsumption } from '@/lib/consumption';
18
  import { useAuth } from '@/contexts/auth-context';
19
  import type { User } from 'firebase/auth';
20
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu';
21
+ import { useLanguage } from '@/contexts/language-context';
22
 
23
+ const AiPostCard = React.memo(({ post, onReact, onAddComment, onReport, onToggleComments, currentUser, t, lang }: {
 
24
  post: PostWithUIState,
25
  onReact: (postId: string, reaction: string) => void,
26
  onAddComment: (postId: string, commentText: string) => void,
27
  onReport: (postId: string) => void,
28
  onToggleComments: (postId: string) => void,
29
  currentUser: User | null;
30
+ t: any;
31
+ lang: string;
32
  }) => {
33
  const [commentText, setCommentText] = useState("");
34
  const [formattedDate, setFormattedDate] = useState('');
 
50
  if (post.createdAt) {
51
  setFormattedDate(formatDistanceToNow(new Date(post.createdAt), {
52
  addSuffix: true,
53
+ locale: lang === 'ar' ? ar : enUS,
54
  }));
55
  }
56
+ }, [post.createdAt, lang]);
57
 
58
  const handleCommentSubmit = () => {
59
  if (commentText.trim()) {
 
101
  </DropdownMenuTrigger>
102
  <DropdownMenuContent align="end">
103
  <DropdownMenuItem onClick={() => onReport(post.id)}>
104
+ <Flag className="mx-2 h-4 w-4" />
105
+ <span>{t.report}</span>
106
  </DropdownMenuItem>
107
  </DropdownMenuContent>
108
  </DropdownMenu>
 
145
  </div>
146
  }
147
  { totalComments > 0 &&
148
+ <span className='hover:underline cursor-pointer' onClick={() => onToggleComments(post.id)}>{totalComments} {t.comments}</span>
149
  }
150
  </div>
151
  }
 
153
  <div className="flex justify-around border-y">
154
  <Button variant="ghost" className={cn("w-full flex items-center gap-2", isLiked && "text-primary font-semibold")} onClick={() => onReact(post.id, 'like')}>
155
  <ThumbsUp />
156
+ <span>{t.like}</span>
157
  </Button>
158
  <Button variant="ghost" className="w-full flex items-center gap-2" onClick={handleCommentActionClick}>
159
  <MessageCircle />
160
+ <span>{t.comment}</span>
161
  </Button>
162
  <Button variant="ghost" className="w-full flex items-center gap-2">
163
  <Share2 />
164
+ <span>{t.share}</span>
165
  </Button>
166
  </div>
167
 
 
183
  {post.uiState.isGeneratingComment && (
184
  <div className="flex items-center gap-2 text-sm text-muted-foreground">
185
  <Loader2 className="h-4 w-4 animate-spin"/>
186
+ <span>{t.loading}</span>
187
  </div>
188
  )}
189
  {currentUser && (
190
  <div className="flex items-center gap-2 pt-2">
191
+ <Image src={currentUser?.photoURL || `https://picsum.photos/seed/${currentUser?.uid || 'default'}/32/32`} alt={currentUser?.displayName || 'You'} width={32} height={32} className="w-8 h-8 rounded-full" />
192
  <div className="relative w-full">
193
  <Input
194
  ref={commentInputRef}
195
+ placeholder={t.writeComment}
196
  value={commentText}
197
  onChange={(e) => setCommentText(e.target.value)}
198
  onKeyDown={(e) => e.key === 'Enter' && handleCommentSubmit()}
 
237
  } = useContext(AIWorldContext);
238
 
239
  const { user } = useAuth();
240
+ const { t, lang } = useLanguage();
241
  const observer = useRef<IntersectionObserver | null>(null);
242
  const [consumption, setConsumption] = useState({ used: 0, remaining: 0, limit: 200 });
243
  const [searchQuery, setSearchQuery] = useState('');
244
  const [isSearchActive, setIsSearchActive] = useState(false);
245
+ const userName = user?.displayName?.split(' ')[0] || 'User';
246
 
247
  useEffect(() => {
248
  const { remaining } = checkConsumption();
 
312
  }, [toggleCommentsVisibility]);
313
 
314
  const sortedMainFeed = [...allPosts].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
315
+ const sortedSearchResults = Array.isArray(searchedPosts) ? [...searchedPosts].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) : [];
316
 
317
  const postsToDisplay = isSearchActive ? sortedSearchResults : sortedMainFeed;
318
 
 
324
  <ArrowLeft />
325
  </Button>
326
  )}
327
+ <h1 className="text-xl font-bold text-primary">{t.worldTitle}</h1>
328
  <div className="flex items-center gap-2 ml-auto">
329
  <div className="relative flex-1 max-w-xs">
330
  <Search className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
331
  <Input
332
+ placeholder={t.search}
333
  value={searchQuery}
334
  onChange={(e) => setSearchQuery(e.target.value)}
335
  onKeyDown={(e) => {
 
353
  </PopoverTrigger>
354
  <PopoverContent className="w-auto text-sm mr-4">
355
  <div className="space-y-2">
356
+ <p className="font-bold text-center">{t.apiConsumption}</p>
357
+ <p>{t.used}: {consumption.used} / {consumption.limit}</p>
358
+ <p>{t.remaining}: {consumption.remaining}</p>
359
  </div>
360
  </PopoverContent>
361
  </Popover>
 
366
  {user && !isSearchActive && (
367
  <div className="bg-card p-3 rounded-lg border shadow-sm">
368
  <div className="flex items-center gap-3">
369
+ <Image src={user?.photoURL || `https://picsum.photos/seed/${user?.uid || 'default'}/40/40`} alt={user?.displayName || 'You'} width={40} height={40} className="w-10 h-10 rounded-full" />
370
  <div className="w-full">
371
  <Input
372
+ placeholder={t.thinking.replace('{name}', userName)}
373
  onKeyDown={(e) => {
374
  if (e.key === 'Enter' && e.currentTarget.value.trim()) {
375
  createPostFromUserInput(e.currentTarget.value);
 
381
  </div>
382
  </div>
383
  <div className="border-t mt-3 pt-2 flex justify-around">
384
+ <Button variant="ghost" className="w-full flex items-center gap-2" onClick={() => generatePostsFromAI("random topic")}>
385
  <Sparkles className="text-purple-500" />
386
+ <span>{t.generatePosts}</span>
387
  </Button>
388
  </div>
389
  </div>
 
417
  onReport={handleReport}
418
  onToggleComments={handleToggleComments}
419
  currentUser={user}
420
+ t={t}
421
+ lang={lang}
422
  />
423
  </div>
424
  ))
425
  )}
426
+ { (isLoading || isGeneratingFromQuery) && postsToDisplay.length > 0 && (
427
  <div className="text-center text-muted-foreground py-4 space-y-4">
428
+ <div className="bg-card border rounded-lg shadow-sm w-full space-y-3 p-4">
429
+ <div className='flex items-center gap-3'>
430
+ <Skeleton className="h-10 w-10 rounded-full" />
431
+ <div className='space-y-2'>
432
+ <Skeleton className="h-4 w-24" />
433
+ <Skeleton className="h-3 w-16" />
434
+ </div>
435
+ </div>
436
+ <Skeleton className="h-6 w-3/4" />
437
+ </div>
 
 
 
 
 
 
 
438
  </div>
439
  )}
440
  {postsToDisplay.length === 0 && !isLoading && !isGeneratingFromQuery && !isSearching && (
 
442
  {isSearchActive ? (
443
  <>
444
  <Search className="h-12 w-12 text-muted-foreground/50" />
445
+ <p>{t.noPosts}</p>
446
  </>
447
  ) : (
448
+ <p>{t.noPosts}</p>
449
  )}
450
  </div>
451
  )}
 
454
  </div>
455
  );
456
  }
 
 
src/components/group-chat/group-chat.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  'use client';
2
 
3
  import { useState, useEffect, useRef } from 'react';
@@ -13,6 +14,7 @@ import { Input } from '../ui/input';
13
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
14
  import { cannedConversations } from '@/lib/ai-world-conversations';
15
  import { checkConsumption, recordConsumption } from '@/lib/consumption';
 
16
 
17
  const conversationTopics = cannedConversations.map(c => c.topic);
18
 
@@ -23,6 +25,7 @@ export function GroupChat() {
23
  const [currentTopic, setCurrentTopic] = useState('');
24
  const [topicInput, setTopicInput] = useState('');
25
  const { toast } = useToast();
 
26
  const scrollRef = useRef<HTMLDivElement>(null);
27
 
28
  const getPersona = (name: string) => personas.find(p => p.name === name);
@@ -63,8 +66,7 @@ export function GroupChat() {
63
  const consumption = checkConsumption();
64
  if (!consumption.allowed) {
65
  toast({
66
- title: 'تم الوصول للحد اليومي',
67
- description: `لقد وصلت إلى الحد الأقصى من الاستخدام اليومي لتوليد المحادثات.`,
68
  variant: 'destructive',
69
  });
70
  return;
@@ -74,17 +76,17 @@ export function GroupChat() {
74
  setMessages([]);
75
  setPersonas([]);
76
  setCurrentTopic(topic);
77
- setTopicInput(''); // Clear input after starting
78
 
79
  try {
80
  const aiResponse = await generateGroupChat({ topic });
81
- recordConsumption(); // Record consumption on success
82
  await displayConversation(aiResponse);
83
  } catch (error) {
84
  console.error('Error getting AI response:', error);
85
  toast({
86
- title: 'خطأ',
87
- description: 'فشل الحصول على محادثة الذكاء الاصطناعي. الرجاء المحاولة مرة أخرى.',
88
  variant: 'destructive',
89
  });
90
  setIsLoading(false);
@@ -126,37 +128,36 @@ export function GroupChat() {
126
  <div className='flex items-center gap-3'>
127
  <div className="relative flex">
128
  {personas.slice(0, 3).map((p, i) => (
129
- <Avatar key={p.name} className={`h-8 w-8 border-2 border-background ${i > 0 ? '-ml-3' : ''}`}>
130
  <AvatarImage src={p.avatarUrl} />
131
  <AvatarFallback>{p.name.charAt(0)}</AvatarFallback>
132
  </Avatar>
133
  ))}
134
  {personas.length > 3 &&
135
- <Avatar className='h-8 w-8 border-2 border-background -ml-3'>
136
  <AvatarFallback>+{personas.length - 3}</AvatarFallback>
137
  </Avatar>
138
  }
139
  </div>
140
  <div>
141
- <h2 className="font-bold text-lg">نقاش حول:</h2>
142
- <p className="text-sm text-muted-foreground">{currentTopic || 'اختر موضوعًا أو قم بتوليد محادثة'}</p>
143
  </div>
144
  </div>
145
  <div className="flex items-center gap-2 p-4 flex-wrap">
146
  <div className="flex-1 min-w-40">
147
  <Select onValueChange={handleCannedSelection} disabled={isLoading}>
148
  <SelectTrigger>
149
- <SelectValue placeholder="اختر محادثة جاهزة..." />
150
  </SelectTrigger>
151
  <SelectContent>
152
  {cannedConversations.map(c => <SelectItem key={c.topic} value={c.topic}>{c.topic}</SelectItem>)}
153
  </SelectContent>
154
  </Select>
155
  </div>
156
- <div className="text-center text-sm text-muted-foreground mx-2">أو</div>
157
  <div className="flex-1 min-w-40">
158
  <Input
159
- placeholder="اكتب موضوعًا لتوليده..."
160
  value={topicInput}
161
  onChange={(e) => setTopicInput(e.target.value)}
162
  disabled={isLoading}
@@ -168,12 +169,12 @@ export function GroupChat() {
168
  />
169
  </div>
170
  <Button onClick={() => fetchAndDisplayGeneratedConversation(topicInput)} disabled={isLoading || !topicInput.trim()} size="sm">
171
- {isLoading && currentTopic === topicInput ? <Loader2 className="ml-2 h-4 w-4 animate-spin" /> : <Sparkles className="ml-2 h-4 w-4" />}
172
- توليد
173
  </Button>
174
  <Button onClick={() => fetchAndDisplayGeneratedConversation(conversationTopics[Math.floor(Math.random() * conversationTopics.length)])} disabled={isLoading} variant="outline" size="sm">
175
- <RefreshCw className="ml-2 h-4 w-4" />
176
- محادثة عشوائية
177
  </Button>
178
  </div>
179
  </div>
@@ -184,8 +185,8 @@ export function GroupChat() {
184
  ) : messages.length === 0 && !isLoading ? (
185
  <div className="flex flex-col items-center justify-center h-full text-center text-muted-foreground">
186
  <Users className="h-16 w-16 mb-4" />
187
- <h3 className="text-lg font-semibold">ابدأ محادثة جماعية</h3>
188
- <p>اختر من المحادثات الجاهزة، أو اكتب موضوعًا، أو اضغط "محادثة عشوائية".</p>
189
  </div>
190
  ) : (
191
  messages.map((message, index) => (
@@ -206,8 +207,8 @@ export function GroupChat() {
206
  <div className="border-t bg-card p-4 md:p-6">
207
  <div className="mx-auto w-full max-w-3xl bg-muted/50 p-3 rounded-lg text-center">
208
  <p className="text-sm text-muted-foreground">
209
- <MessageCircle className="inline-block ml-2 h-4 w-4" />
210
- هذه محادثة مشاهدة فقط. لا يمكنك إرسال رسائل هنا.
211
  </p>
212
  </div>
213
  </div>
 
1
+
2
  'use client';
3
 
4
  import { useState, useEffect, useRef } from 'react';
 
14
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
15
  import { cannedConversations } from '@/lib/ai-world-conversations';
16
  import { checkConsumption, recordConsumption } from '@/lib/consumption';
17
+ import { useLanguage } from '@/contexts/language-context';
18
 
19
  const conversationTopics = cannedConversations.map(c => c.topic);
20
 
 
25
  const [currentTopic, setCurrentTopic] = useState('');
26
  const [topicInput, setTopicInput] = useState('');
27
  const { toast } = useToast();
28
+ const { t } = useLanguage();
29
  const scrollRef = useRef<HTMLDivElement>(null);
30
 
31
  const getPersona = (name: string) => personas.find(p => p.name === name);
 
66
  const consumption = checkConsumption();
67
  if (!consumption.allowed) {
68
  toast({
69
+ title: t.dailyLimitReached,
 
70
  variant: 'destructive',
71
  });
72
  return;
 
76
  setMessages([]);
77
  setPersonas([]);
78
  setCurrentTopic(topic);
79
+ setTopicInput('');
80
 
81
  try {
82
  const aiResponse = await generateGroupChat({ topic });
83
+ recordConsumption();
84
  await displayConversation(aiResponse);
85
  } catch (error) {
86
  console.error('Error getting AI response:', error);
87
  toast({
88
+ title: 'Error',
89
+ description: t.errorAI,
90
  variant: 'destructive',
91
  });
92
  setIsLoading(false);
 
128
  <div className='flex items-center gap-3'>
129
  <div className="relative flex">
130
  {personas.slice(0, 3).map((p, i) => (
131
+ <Avatar key={p.name} className={`h-8 w-8 border-2 border-background ${i > 0 ? (t.appTitle === 'ProtoChat' ? '-mr-3' : '-ml-3') : ''}`}>
132
  <AvatarImage src={p.avatarUrl} />
133
  <AvatarFallback>{p.name.charAt(0)}</AvatarFallback>
134
  </Avatar>
135
  ))}
136
  {personas.length > 3 &&
137
+ <Avatar className={cn('h-8 w-8 border-2 border-background', t.appTitle === 'ProtoChat' ? '-mr-3' : '-ml-3')}>
138
  <AvatarFallback>+{personas.length - 3}</AvatarFallback>
139
  </Avatar>
140
  }
141
  </div>
142
  <div>
143
+ <h2 className="font-bold text-lg">{t.groupChatTitle}</h2>
144
+ <p className="text-sm text-muted-foreground">{currentTopic || t.groupChatDesc}</p>
145
  </div>
146
  </div>
147
  <div className="flex items-center gap-2 p-4 flex-wrap">
148
  <div className="flex-1 min-w-40">
149
  <Select onValueChange={handleCannedSelection} disabled={isLoading}>
150
  <SelectTrigger>
151
+ <SelectValue placeholder={t.chooseCanned} />
152
  </SelectTrigger>
153
  <SelectContent>
154
  {cannedConversations.map(c => <SelectItem key={c.topic} value={c.topic}>{c.topic}</SelectItem>)}
155
  </SelectContent>
156
  </Select>
157
  </div>
 
158
  <div className="flex-1 min-w-40">
159
  <Input
160
+ placeholder={t.writeTopic}
161
  value={topicInput}
162
  onChange={(e) => setTopicInput(e.target.value)}
163
  disabled={isLoading}
 
169
  />
170
  </div>
171
  <Button onClick={() => fetchAndDisplayGeneratedConversation(topicInput)} disabled={isLoading || !topicInput.trim()} size="sm">
172
+ {isLoading && currentTopic === topicInput ? <Loader2 className="mx-2 h-4 w-4 animate-spin" /> : <Sparkles className="mx-2 h-4 w-4" />}
173
+ {t.generate}
174
  </Button>
175
  <Button onClick={() => fetchAndDisplayGeneratedConversation(conversationTopics[Math.floor(Math.random() * conversationTopics.length)])} disabled={isLoading} variant="outline" size="sm">
176
+ <RefreshCw className="mx-2 h-4 w-4" />
177
+ {t.randomChat}
178
  </Button>
179
  </div>
180
  </div>
 
185
  ) : messages.length === 0 && !isLoading ? (
186
  <div className="flex flex-col items-center justify-center h-full text-center text-muted-foreground">
187
  <Users className="h-16 w-16 mb-4" />
188
+ <h3 className="text-lg font-semibold">{t.startGroupChat}</h3>
189
+ <p>{t.groupChatDesc}</p>
190
  </div>
191
  ) : (
192
  messages.map((message, index) => (
 
207
  <div className="border-t bg-card p-4 md:p-6">
208
  <div className="mx-auto w-full max-w-3xl bg-muted/50 p-3 rounded-lg text-center">
209
  <p className="text-sm text-muted-foreground">
210
+ <MessageCircle className="inline-block mx-2 h-4 w-4" />
211
+ {t.watchingOnly}
212
  </p>
213
  </div>
214
  </div>
src/components/voice-chat/voice-chat.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  'use client';
2
 
3
  import { useState, useRef, useEffect } from 'react';
@@ -7,6 +8,7 @@ import { cn } from '@/lib/utils';
7
  import { useToast } from '@/hooks/use-toast';
8
  import { getChatResponse } from '@/ai/flows/ai-chat-response';
9
  import { checkConsumption, recordConsumption } from '@/lib/consumption';
 
10
 
11
  interface TranscriptEntry {
12
  speaker: 'user' | 'ai';
@@ -17,31 +19,31 @@ export function VoiceChat() {
17
  const [transcript, setTranscript] = useState<TranscriptEntry[]>([]);
18
  const [isListening, setIsListening] = useState(false);
19
  const [isLoading, setIsLoading] = useState(false);
20
- const recognitionRef = useRef<SpeechRecognition | null>(null);
21
  const { toast } = useToast();
 
22
 
23
  // State for speech synthesis
24
- const [arabicVoice, setArabicVoice] = useState<SpeechSynthesisVoice | null>(null);
25
 
26
  // Load voices for speech synthesis
27
  useEffect(() => {
28
  const loadVoices = () => {
29
  const voices = window.speechSynthesis.getVoices();
30
- // Prioritize known good voices like 'Maged' (Edge) or Google's Arabic voice
31
- const preferredVoice = voices.find(voice => voice.lang.startsWith('ar') && (voice.name.includes('Maged') || voice.name.includes('Google')));
32
- const fallbackVoice = voices.find(voice => voice.lang.startsWith('ar'));
33
- setArabicVoice(preferredVoice || fallbackVoice || null);
 
34
  };
35
 
36
- // Voices are loaded asynchronously, so we need to listen for the event
37
  window.speechSynthesis.onvoiceschanged = loadVoices;
38
- loadVoices(); // Initial call in case voices are already loaded
39
- }, []);
40
 
41
  const speakText = (text: string) => {
42
  if (!('speechSynthesis' in window)) {
43
- console.error('Speech synthesis not supported.');
44
- toast({ title: 'غير مدعوم', description: 'متصفحك لا يدعم نطق النص.', variant: 'destructive' });
45
  return;
46
  }
47
 
@@ -50,21 +52,11 @@ export function VoiceChat() {
50
 
51
  const utterance = new SpeechSynthesisUtterance(cleanText);
52
 
53
- // Smart voice selection logic
54
- let voiceToUse = arabicVoice;
55
- if (!voiceToUse) {
56
- // If the state hasn't been set yet, try one last time to get a voice directly
57
- const voices = window.speechSynthesis.getVoices();
58
- voiceToUse = voices.find(v => v.lang.startsWith('ar')) || null;
59
- }
60
-
61
  if (voiceToUse) {
62
  utterance.voice = voiceToUse;
63
  utterance.lang = voiceToUse.lang;
64
  } else {
65
- utterance.lang = 'ar-EG'; // Fallback language
66
- console.error('Speech synthesis not supported or no voice selected.');
67
- // Do not toast here to avoid annoying the user on every attempt, but log it.
68
  }
69
 
70
  utterance.rate = 1.0;
@@ -75,21 +67,20 @@ export function VoiceChat() {
75
 
76
  // Speech Recognition setup
77
  useEffect(() => {
78
- const SpeechRecognition = window.SpeechRecognition || (window as any).webkitSpeechRecognition;
79
  if (SpeechRecognition) {
80
  const recognition = new SpeechRecognition();
81
  recognition.continuous = false;
82
- recognition.lang = 'ar-EG';
83
  recognition.interimResults = false;
84
 
85
- recognition.onresult = (event) => {
86
  const spokenText = event.results[0][0].transcript;
87
  handleUserSpeech(spokenText);
88
  };
89
 
90
- recognition.onerror = (event) => {
91
  console.error('Speech recognition error', event.error);
92
- toast({ title: 'خطأ في التعرف على الصوت', description: event.error, variant: 'destructive' });
93
  setIsListening(false);
94
  };
95
 
@@ -98,15 +89,13 @@ export function VoiceChat() {
98
  }
99
 
100
  recognitionRef.current = recognition;
101
- } else {
102
- toast({ title: 'غير مدعوم', description: 'متصفحك لا يدعم التعرف على الصوت.', variant: 'destructive' });
103
  }
104
 
105
  return () => {
106
  recognitionRef.current?.abort();
107
- window.speechSynthesis.cancel(); // Stop any speech on component unmount
108
  }
109
- }, [toast]);
110
 
111
  const handleUserSpeech = async (text: string) => {
112
  if (!text) return;
@@ -115,34 +104,33 @@ export function VoiceChat() {
115
  setIsLoading(true);
116
 
117
  try {
118
- // 1. Get AI text response (this is the only part that consumes API quota)
119
  const consumption = checkConsumption();
120
  if (!consumption.allowed) {
121
- throw new Error('لقد وصلت إلى الحد الأقصى من الاستخدام اليومي.');
122
  }
123
 
124
  const chatHistory = [...transcript, { speaker: 'user', text }].map(entry => `${entry.speaker}: ${entry.text}`);
125
  const aiTextResponse = await getChatResponse({ chatHistory });
126
 
127
- recordConsumption(); // Record consumption only after a successful text generation
128
 
129
  setTranscript(prev => [...prev, { speaker: 'ai', text: aiTextResponse.response }]);
130
-
131
- // 2. Speak the AI response using the free, built-in browser API
132
  speakText(aiTextResponse.response);
133
 
134
  } catch (error: any) {
135
  console.error('Error in AI voice chat:', error);
136
- toast({ title: 'خطأ', description: error.message || 'حدث خطأ أثناء المحادثة.', variant: 'destructive' });
137
  } finally {
138
  setIsLoading(false);
139
  }
140
  };
141
 
142
  const toggleListening = () => {
143
- if (!recognitionRef.current) return;
 
 
 
144
 
145
- // Stop any ongoing speech before starting to listen
146
  window.speechSynthesis.cancel();
147
 
148
  if (isListening) {
@@ -160,8 +148,8 @@ export function VoiceChat() {
160
  {transcript.length === 0 && !isListening && !isLoading && (
161
  <div className="text-center text-muted-foreground h-full flex flex-col justify-center items-center">
162
  <Mic className="w-16 h-16 mb-4"/>
163
- <h2 className="text-2xl font-bold">مرحباً بك في المحادثة الصوتية</h2>
164
- <p>اضغط على الميكروفون لبدء التحدث مع بروتو</p>
165
  </div>
166
  )}
167
  {transcript.map((entry, index) => (
@@ -183,7 +171,7 @@ export function VoiceChat() {
183
  ))}
184
  {isListening && (
185
  <div className="text-center text-muted-foreground animate-pulse">
186
- <p>...يستمع</p>
187
  </div>
188
  )}
189
  {isLoading && (
@@ -193,7 +181,7 @@ export function VoiceChat() {
193
  </div>
194
  <div className="flex items-center gap-2 pt-2">
195
  <Loader2 className="h-5 w-5 animate-spin" />
196
- <p>بروتو يفكر...</p>
197
  </div>
198
  </div>
199
  )}
@@ -212,10 +200,9 @@ export function VoiceChat() {
212
  {isListening ? <MicOff className="h-8 w-8" /> : <Mic className="h-8 w-8" />}
213
  </Button>
214
  <p className="text-sm text-muted-foreground">
215
- {isListening ? 'اضغط للإيقاف' : 'اضغط للتحدث'}
216
  </p>
217
  </div>
218
- {/* The <audio> element is no longer needed with Web Speech API */}
219
  </div>
220
  );
221
  }
 
1
+
2
  'use client';
3
 
4
  import { useState, useRef, useEffect } from 'react';
 
8
  import { useToast } from '@/hooks/use-toast';
9
  import { getChatResponse } from '@/ai/flows/ai-chat-response';
10
  import { checkConsumption, recordConsumption } from '@/lib/consumption';
11
+ import { useLanguage } from '@/contexts/language-context';
12
 
13
  interface TranscriptEntry {
14
  speaker: 'user' | 'ai';
 
19
  const [transcript, setTranscript] = useState<TranscriptEntry[]>([]);
20
  const [isListening, setIsListening] = useState(false);
21
  const [isLoading, setIsLoading] = useState(false);
22
+ const recognitionRef = useRef<any>(null);
23
  const { toast } = useToast();
24
+ const { t, lang } = useLanguage();
25
 
26
  // State for speech synthesis
27
+ const [voiceToUse, setVoiceToUse] = useState<SpeechSynthesisVoice | null>(null);
28
 
29
  // Load voices for speech synthesis
30
  useEffect(() => {
31
  const loadVoices = () => {
32
  const voices = window.speechSynthesis.getVoices();
33
+ // Prioritize known good voices for the selected language
34
+ const targetLang = lang === 'ar' ? 'ar-EG' : 'en-US';
35
+ const preferredVoice = voices.find(voice => voice.lang.startsWith(lang) && (voice.name.includes('Maged') || voice.name.includes('Google') || voice.name.includes('Samantha')));
36
+ const fallbackVoice = voices.find(voice => voice.lang.startsWith(lang));
37
+ setVoiceToUse(preferredVoice || fallbackVoice || null);
38
  };
39
 
 
40
  window.speechSynthesis.onvoiceschanged = loadVoices;
41
+ loadVoices();
42
+ }, [lang]);
43
 
44
  const speakText = (text: string) => {
45
  if (!('speechSynthesis' in window)) {
46
+ toast({ title: 'Error', description: 'Your browser does not support text-to-speech.', variant: 'destructive' });
 
47
  return;
48
  }
49
 
 
52
 
53
  const utterance = new SpeechSynthesisUtterance(cleanText);
54
 
 
 
 
 
 
 
 
 
55
  if (voiceToUse) {
56
  utterance.voice = voiceToUse;
57
  utterance.lang = voiceToUse.lang;
58
  } else {
59
+ utterance.lang = lang === 'ar' ? 'ar-EG' : 'en-US';
 
 
60
  }
61
 
62
  utterance.rate = 1.0;
 
67
 
68
  // Speech Recognition setup
69
  useEffect(() => {
70
+ const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
71
  if (SpeechRecognition) {
72
  const recognition = new SpeechRecognition();
73
  recognition.continuous = false;
74
+ recognition.lang = lang === 'ar' ? 'ar-EG' : 'en-US';
75
  recognition.interimResults = false;
76
 
77
+ recognition.onresult = (event: any) => {
78
  const spokenText = event.results[0][0].transcript;
79
  handleUserSpeech(spokenText);
80
  };
81
 
82
+ recognition.onerror = (event: any) => {
83
  console.error('Speech recognition error', event.error);
 
84
  setIsListening(false);
85
  };
86
 
 
89
  }
90
 
91
  recognitionRef.current = recognition;
 
 
92
  }
93
 
94
  return () => {
95
  recognitionRef.current?.abort();
96
+ window.speechSynthesis.cancel();
97
  }
98
+ }, [lang]);
99
 
100
  const handleUserSpeech = async (text: string) => {
101
  if (!text) return;
 
104
  setIsLoading(true);
105
 
106
  try {
 
107
  const consumption = checkConsumption();
108
  if (!consumption.allowed) {
109
+ throw new Error(t.dailyLimitReached);
110
  }
111
 
112
  const chatHistory = [...transcript, { speaker: 'user', text }].map(entry => `${entry.speaker}: ${entry.text}`);
113
  const aiTextResponse = await getChatResponse({ chatHistory });
114
 
115
+ recordConsumption();
116
 
117
  setTranscript(prev => [...prev, { speaker: 'ai', text: aiTextResponse.response }]);
 
 
118
  speakText(aiTextResponse.response);
119
 
120
  } catch (error: any) {
121
  console.error('Error in AI voice chat:', error);
122
+ toast({ title: 'Error', description: error.message || t.errorAI, variant: 'destructive' });
123
  } finally {
124
  setIsLoading(false);
125
  }
126
  };
127
 
128
  const toggleListening = () => {
129
+ if (!recognitionRef.current) {
130
+ toast({ title: 'Not Supported', description: 'Speech recognition is not supported in this browser.', variant: 'destructive' });
131
+ return;
132
+ }
133
 
 
134
  window.speechSynthesis.cancel();
135
 
136
  if (isListening) {
 
148
  {transcript.length === 0 && !isListening && !isLoading && (
149
  <div className="text-center text-muted-foreground h-full flex flex-col justify-center items-center">
150
  <Mic className="w-16 h-16 mb-4"/>
151
+ <h2 className="text-2xl font-bold">{t.voiceWelcome}</h2>
152
+ <p>{t.voiceDesc}</p>
153
  </div>
154
  )}
155
  {transcript.map((entry, index) => (
 
171
  ))}
172
  {isListening && (
173
  <div className="text-center text-muted-foreground animate-pulse">
174
+ <p>{t.listening}</p>
175
  </div>
176
  )}
177
  {isLoading && (
 
181
  </div>
182
  <div className="flex items-center gap-2 pt-2">
183
  <Loader2 className="h-5 w-5 animate-spin" />
184
+ <p>{t.protoThinking}</p>
185
  </div>
186
  </div>
187
  )}
 
200
  {isListening ? <MicOff className="h-8 w-8" /> : <Mic className="h-8 w-8" />}
201
  </Button>
202
  <p className="text-sm text-muted-foreground">
203
+ {isListening ? t.pressToStop : t.pressToTalk}
204
  </p>
205
  </div>
 
206
  </div>
207
  );
208
  }
src/contexts/ai-world-context.tsx CHANGED
@@ -1,4 +1,3 @@
1
-
2
  "use client";
3
 
4
  import React, { createContext, useState, useCallback, useEffect, useRef } from 'react';
@@ -34,8 +33,8 @@ interface AIWorldContextType {
34
 
35
  export const AIWorldContext = createContext<AIWorldContextType>({} as AIWorldContextType);
36
 
37
- const POSTS_PER_PAGE = 5;
38
- const POSTS_TO_GENERATE_AI = 30; // 30 posts per generation as requested
39
 
40
  export const AIWorldProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
41
  const [allPosts, setAllPosts] = useState<PostWithUIState[]>([]);
@@ -45,7 +44,7 @@ export const AIWorldProvider: React.FC<{ children: React.ReactNode }> = ({ child
45
  const [searchedPosts, setSearchedPosts] = useState<PostWithUIState[]>([]);
46
  const { toast } = useToast();
47
  const { user, userData } = useAuth();
48
- const { lang } = useLanguage();
49
  const isFetchingRef = useRef(false);
50
  const [canLoadMore, setCanLoadMore] = useState(true);
51
  const [lastLoadedKey, setLastLoadedKey] = useState<string | null>(null);
@@ -100,7 +99,7 @@ export const AIWorldProvider: React.FC<{ children: React.ReactNode }> = ({ child
100
  setLastLoadedKey(newPosts[newPosts.length - 1].id);
101
  setAllPosts(prev => [...prev, ...newPosts]);
102
  }
103
- if (newPosts.length < POSTS_PER_PAGE) setCanLoadMore(false);
104
  } else {
105
  setCanLoadMore(false);
106
  }
@@ -117,7 +116,7 @@ export const AIWorldProvider: React.FC<{ children: React.ReactNode }> = ({ child
117
  const generatePostsFromAI = async () => {
118
  const consumption = checkConsumption();
119
  if (!consumption.allowed) {
120
- toast({ title: "الحد اليومي", description: "وصلت للحد الأقصى اليوم.", variant: "destructive" });
121
  return;
122
  }
123
 
@@ -134,9 +133,8 @@ export const AIWorldProvider: React.FC<{ children: React.ReactNode }> = ({ child
134
  recordConsumption();
135
 
136
  const updates: any = {};
137
- const newPosts: PostWithUIState[] = [];
138
 
139
- // Parallel image fetching
140
  const postPromises = result.posts.map(async (idea) => {
141
  const postId = push(ref(database, 'posts')).key!;
142
  const author = getRandomAIUser();
@@ -164,15 +162,14 @@ export const AIWorldProvider: React.FC<{ children: React.ReactNode }> = ({ child
164
  const results = await Promise.all(postPromises);
165
  results.forEach(({ postId, postData }) => {
166
  updates[`posts/${postId}`] = postData;
167
- newPosts.push(processDbPost(postId, postData));
168
  });
169
 
170
  await update(ref(database), updates);
171
- setAllPosts(prev => [...newPosts, ...prev]);
172
- toast({ title: "تم التوليد", description: `تمت إضافة ${newPosts.length} منشورات مخصصة لك بناءً على عمرك وموقعك.` });
173
  } catch (error) {
174
  console.error(error);
175
- toast({ title: "خطأ", description: "فشل توليد المنشورات. يرجى المحاولة مرة أخرى.", variant: "destructive" });
176
  } finally {
177
  setIsGeneratingFromQuery(false);
178
  }
@@ -220,6 +217,15 @@ export const AIWorldProvider: React.FC<{ children: React.ReactNode }> = ({ child
220
  setAllPosts(prev => prev.map(p => p.id === postId ? { ...p, comments: [...p.comments, newComment] } : p));
221
  };
222
 
 
 
 
 
 
 
 
 
 
223
  const searchPostsInDB = (searchTerm: string) => {
224
  setIsSearching(true);
225
  const results = allPosts.filter(p => p.content.toLowerCase().includes(searchTerm.toLowerCase()));
@@ -229,8 +235,8 @@ export const AIWorldProvider: React.FC<{ children: React.ReactNode }> = ({ child
229
 
230
  const value: AIWorldContextType = {
231
  allPosts, isLoading, loadNextPage, canLoadMore, generatePostsFromAI, createPostFromUserInput,
232
- isGeneratingFromQuery, reactToPost, addCommentToPost, reportPost: () => {},
233
- triggerCommentGeneration: (ids) => { /* Optional future implementation for bulk AI comments */ },
234
  toggleCommentsVisibility: (id) => setAllPosts(prev => prev.map(p => p.id === id ? { ...p, uiState: { ...p.uiState, areCommentsVisible: !p.uiState.areCommentsVisible } } : p)),
235
  searchPostsInDB, isSearching, searchedPosts, clearSearch: () => { setSearchedPosts([]); setIsSearching(false); }
236
  };
 
 
1
  "use client";
2
 
3
  import React, { createContext, useState, useCallback, useEffect, useRef } from 'react';
 
33
 
34
  export const AIWorldContext = createContext<AIWorldContextType>({} as AIWorldContextType);
35
 
36
+ const POSTS_PER_PAGE = 10;
37
+ const POSTS_TO_GENERATE_AI = 30; // 30 posts generated per request
38
 
39
  export const AIWorldProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
40
  const [allPosts, setAllPosts] = useState<PostWithUIState[]>([]);
 
44
  const [searchedPosts, setSearchedPosts] = useState<PostWithUIState[]>([]);
45
  const { toast } = useToast();
46
  const { user, userData } = useAuth();
47
+ const { lang, t } = useLanguage();
48
  const isFetchingRef = useRef(false);
49
  const [canLoadMore, setCanLoadMore] = useState(true);
50
  const [lastLoadedKey, setLastLoadedKey] = useState<string | null>(null);
 
99
  setLastLoadedKey(newPosts[newPosts.length - 1].id);
100
  setAllPosts(prev => [...prev, ...newPosts]);
101
  }
102
+ if (newPosts.length <= POSTS_PER_PAGE) setCanLoadMore(false);
103
  } else {
104
  setCanLoadMore(false);
105
  }
 
116
  const generatePostsFromAI = async () => {
117
  const consumption = checkConsumption();
118
  if (!consumption.allowed) {
119
+ toast({ title: t.dailyLimitReached, variant: "destructive" });
120
  return;
121
  }
122
 
 
133
  recordConsumption();
134
 
135
  const updates: any = {};
136
+ const newPostsBatch: PostWithUIState[] = [];
137
 
 
138
  const postPromises = result.posts.map(async (idea) => {
139
  const postId = push(ref(database, 'posts')).key!;
140
  const author = getRandomAIUser();
 
162
  const results = await Promise.all(postPromises);
163
  results.forEach(({ postId, postData }) => {
164
  updates[`posts/${postId}`] = postData;
165
+ newPostsBatch.push(processDbPost(postId, postData));
166
  });
167
 
168
  await update(ref(database), updates);
169
+ setAllPosts(prev => [...newPostsBatch, ...prev]);
 
170
  } catch (error) {
171
  console.error(error);
172
+ toast({ title: "Error", description: t.errorAI, variant: "destructive" });
173
  } finally {
174
  setIsGeneratingFromQuery(false);
175
  }
 
217
  setAllPosts(prev => prev.map(p => p.id === postId ? { ...p, comments: [...p.comments, newComment] } : p));
218
  };
219
 
220
+ const reportPost = (postId: string) => {
221
+ setAllPosts(prev => prev.map(p => p.id === postId ? { ...p, uiState: { ...p.uiState, isReported: true } } : p));
222
+ };
223
+
224
+ const triggerCommentGeneration = (postIds: string[]) => {
225
+ // Placeholder for future bulk AI comment logic
226
+ console.log("Triggering bulk comments for:", postIds);
227
+ };
228
+
229
  const searchPostsInDB = (searchTerm: string) => {
230
  setIsSearching(true);
231
  const results = allPosts.filter(p => p.content.toLowerCase().includes(searchTerm.toLowerCase()));
 
235
 
236
  const value: AIWorldContextType = {
237
  allPosts, isLoading, loadNextPage, canLoadMore, generatePostsFromAI, createPostFromUserInput,
238
+ isGeneratingFromQuery, reactToPost, addCommentToPost, reportPost,
239
+ triggerCommentGeneration,
240
  toggleCommentsVisibility: (id) => setAllPosts(prev => prev.map(p => p.id === id ? { ...p, uiState: { ...p.uiState, areCommentsVisible: !p.uiState.areCommentsVisible } } : p)),
241
  searchPostsInDB, isSearching, searchedPosts, clearSearch: () => { setSearchedPosts([]); setIsSearching(false); }
242
  };
src/lib/gemini-client.ts CHANGED
@@ -1,4 +1,3 @@
1
-
2
  const GEMINI_API_KEYS = process.env.GEMINI_API_KEY ? process.env.GEMINI_API_KEY.split(',').map(k => k.trim()) : [];
3
 
4
  async function askGoogleAI(prompt: string, model: string): Promise<{ success: true, answer: string, model: string } | { success: false, message: string }> {
@@ -38,13 +37,13 @@ async function askGoogleAI(prompt: string, model: string): Promise<{ success: tr
38
  }
39
 
40
  export async function askAI(prompt: string): Promise<{ success: true, answer: string, model: string } | { success: false, message: string }> {
41
- // Specifically using gemini-2.5-flash-lite as requested by the user
42
  const googleModels = ["gemini-2.5-flash-lite", "gemini-2.0-flash-lite-preview-02-05", "gemini-1.5-flash-latest"];
43
  for (const modelId of googleModels) {
44
  const result = await askGoogleAI(prompt, modelId);
45
  if (result.success) return result;
46
  }
47
- return { success: false, message: "عذراً، النماذج المتاحة مشغولة حالياً." };
48
  }
49
 
50
  export const safeGenerateContent = async (prompt: string): Promise<any> => {
@@ -53,7 +52,7 @@ export const safeGenerateContent = async (prompt: string): Promise<any> => {
53
 
54
  let text = result.answer;
55
  try {
56
- // Advanced JSON extraction to handle any extra text from the model
57
  const startBracket = text.indexOf('{');
58
  const startSquare = text.indexOf('[');
59
  const startIndex = (startBracket !== -1 && (startSquare === -1 || startBracket < startSquare)) ? startBracket : startSquare;
@@ -62,13 +61,13 @@ export const safeGenerateContent = async (prompt: string): Promise<any> => {
62
  const endSquare = text.lastIndexOf(']');
63
  const endIndex = (endBracket !== -1 && (endSquare === -1 || endBracket > endSquare)) ? endBracket : endSquare;
64
 
65
- if (startIndex === -1 || endIndex === -1) throw new Error("No JSON found");
66
 
67
  const cleanedJson = text.substring(startIndex, endIndex + 1);
68
  return JSON.parse(cleanedJson);
69
  } catch (error) {
70
- console.error("AI JSON Parse Error. Model:", result.model, "Raw text:", text);
71
- throw new Error("فشل تحليل استجابة الذكاء الاصطناعي. يرجى المحاولة مرة أخرى.");
72
  }
73
  };
74
 
 
 
1
  const GEMINI_API_KEYS = process.env.GEMINI_API_KEY ? process.env.GEMINI_API_KEY.split(',').map(k => k.trim()) : [];
2
 
3
  async function askGoogleAI(prompt: string, model: string): Promise<{ success: true, answer: string, model: string } | { success: false, message: string }> {
 
37
  }
38
 
39
  export async function askAI(prompt: string): Promise<{ success: true, answer: string, model: string } | { success: false, message: string }> {
40
+ // Specifically using gemini-2.5-flash-lite as requested
41
  const googleModels = ["gemini-2.5-flash-lite", "gemini-2.0-flash-lite-preview-02-05", "gemini-1.5-flash-latest"];
42
  for (const modelId of googleModels) {
43
  const result = await askGoogleAI(prompt, modelId);
44
  if (result.success) return result;
45
  }
46
+ return { success: false, message: "Models are currently busy. Please try again." };
47
  }
48
 
49
  export const safeGenerateContent = async (prompt: string): Promise<any> => {
 
52
 
53
  let text = result.answer;
54
  try {
55
+ // Robust JSON extraction to handle any extra text or markdown wrappers
56
  const startBracket = text.indexOf('{');
57
  const startSquare = text.indexOf('[');
58
  const startIndex = (startBracket !== -1 && (startSquare === -1 || startBracket < startSquare)) ? startBracket : startSquare;
 
61
  const endSquare = text.lastIndexOf(']');
62
  const endIndex = (endBracket !== -1 && (endSquare === -1 || endBracket > endSquare)) ? endBracket : endSquare;
63
 
64
+ if (startIndex === -1 || endIndex === -1) throw new Error("No valid JSON found in AI response");
65
 
66
  const cleanedJson = text.substring(startIndex, endIndex + 1);
67
  return JSON.parse(cleanedJson);
68
  } catch (error) {
69
+ console.error("AI JSON Parse Error. Model:", result.model, "Raw text sample:", text.substring(0, 100));
70
+ throw new Error("Failed to parse AI response. Please try again.");
71
  }
72
  };
73
 
src/lib/translations.ts CHANGED
@@ -34,7 +34,7 @@ export const translations = {
34
  confirmDelete: 'نعم، احذف حسابي',
35
  search: 'بحث...',
36
  generatePosts: 'ولّد منشورات',
37
- thinking: 'بماذا تفكر؟',
38
  localContext: 'منشورات مخصصة لمنطقتك',
39
  switchLang: 'English',
40
  noPosts: 'لا توجد منشورات حالياً.',
@@ -43,6 +43,30 @@ export const translations = {
43
  comment: 'تعليق',
44
  share: 'مشاركة',
45
  writeComment: 'اكتب تعليقاً...',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  },
47
  en: {
48
  appTitle: 'ProtoChat',
@@ -76,7 +100,7 @@ export const translations = {
76
  confirmDelete: 'Yes, delete my account',
77
  search: 'Search...',
78
  generatePosts: 'Generate Posts',
79
- thinking: 'What\'s on your mind?',
80
  localContext: 'Posts customized for your area',
81
  switchLang: 'العربية',
82
  noPosts: 'No posts yet.',
@@ -85,5 +109,29 @@ export const translations = {
85
  comment: 'Comment',
86
  share: 'Share',
87
  writeComment: 'Write a comment...',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
89
  };
 
34
  confirmDelete: 'نعم، احذف حسابي',
35
  search: 'بحث...',
36
  generatePosts: 'ولّد منشورات',
37
+ thinking: 'بماذا تفكر يا {name}؟',
38
  localContext: 'منشورات مخصصة لمنطقتك',
39
  switchLang: 'English',
40
  noPosts: 'لا توجد منشورات حالياً.',
 
43
  comment: 'تعليق',
44
  share: 'مشاركة',
45
  writeComment: 'اكتب تعليقاً...',
46
+ report: 'إبلاغ عن المحتوى',
47
+ apiConsumption: 'استهلاك الـ API اليومي',
48
+ used: 'المستخدم',
49
+ remaining: 'المتبقي',
50
+ backToChat: 'العودة للدردشة',
51
+ watchingOnly: 'هذه محادثة مشاهدة فقط. لا يمكنك إرسال رسائل هنا.',
52
+ chooseCanned: 'اختر محادثة جاهزة...',
53
+ writeTopic: 'اكتب موضوعاً لتوليده...',
54
+ generate: 'توليد',
55
+ randomChat: 'محادثة عشوائية',
56
+ startGroupChat: 'ابدأ محادثة جماعية',
57
+ groupChatDesc: 'اختر من المحادثات الجاهزة، أو اكتب موضوعًا، أو اضغط "محادثة عشوائية".',
58
+ voiceWelcome: 'مرحباً بك في المحادثة الصوتية',
59
+ voiceDesc: 'اضغط على الميكروفون لبدء التحدث مع بروتو',
60
+ listening: 'يستمع...',
61
+ protoThinking: 'بروتو يفكر...',
62
+ pressToStop: 'اضغط للإيقاف',
63
+ pressToTalk: 'اضغط للتحدث',
64
+ successUpdate: 'تم التحديث بنجاح',
65
+ accountDeleted: 'تم حذف الحساب',
66
+ completeProfile: 'يرجى إكمال بياناتك للمتابعة',
67
+ helloUser: 'مرحباً بك {name}',
68
+ errorAI: 'فشل الحصول على استجابة الذكاء الاصطناعي.',
69
+ dailyLimitReached: 'تم الوصول للحد اليومي للاستخدام.',
70
  },
71
  en: {
72
  appTitle: 'ProtoChat',
 
100
  confirmDelete: 'Yes, delete my account',
101
  search: 'Search...',
102
  generatePosts: 'Generate Posts',
103
+ thinking: 'What\'s on your mind, {name}?',
104
  localContext: 'Posts customized for your area',
105
  switchLang: 'العربية',
106
  noPosts: 'No posts yet.',
 
109
  comment: 'Comment',
110
  share: 'Share',
111
  writeComment: 'Write a comment...',
112
+ report: 'Report Content',
113
+ apiConsumption: 'Daily API Consumption',
114
+ used: 'Used',
115
+ remaining: 'Remaining',
116
+ backToChat: 'Back to Chat',
117
+ watchingOnly: 'This is a view-only chat. You cannot send messages here.',
118
+ chooseCanned: 'Choose a ready conversation...',
119
+ writeTopic: 'Write a topic to generate...',
120
+ generate: 'Generate',
121
+ randomChat: 'Random Chat',
122
+ startGroupChat: 'Start a Group Chat',
123
+ groupChatDesc: 'Choose from ready chats, write a topic, or press "Random Chat".',
124
+ voiceWelcome: 'Welcome to Voice Chat',
125
+ voiceDesc: 'Press the microphone to start talking with Proto',
126
+ listening: 'Listening...',
127
+ protoThinking: 'Proto is thinking...',
128
+ pressToStop: 'Press to stop',
129
+ pressToTalk: 'Press to talk',
130
+ successUpdate: 'Updated successfully',
131
+ accountDeleted: 'Account deleted',
132
+ completeProfile: 'Please complete your data to continue',
133
+ helloUser: 'Welcome {name}',
134
+ errorAI: 'Failed to get AI response.',
135
+ dailyLimitReached: 'Daily usage limit reached.',
136
  }
137
  };