Spaces:
Sleeping
Sleeping
import { NextResponse } from 'next/server';
Browse files- next.config.ts +12 -0
- src/ai/dev.ts +1 -0
- src/ai/flows/ai-translate-text.ts +51 -0
- src/ai/flows/ai-world-post.ts +38 -25
- src/app/api/lexica/route.ts +36 -3
- src/app/page.tsx +11 -6
- src/app/world/page.tsx +13 -3
- src/components/ai-world/ai-world-feed.tsx +5 -118
- src/components/ai-world/ai-world-view.tsx +308 -0
- src/components/ui/input.tsx +1 -1
next.config.ts
CHANGED
|
@@ -16,6 +16,18 @@ const nextConfig: NextConfig = {
|
|
| 16 |
port: '',
|
| 17 |
pathname: '/**',
|
| 18 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
],
|
| 20 |
},
|
| 21 |
};
|
|
|
|
| 16 |
port: '',
|
| 17 |
pathname: '/**',
|
| 18 |
},
|
| 19 |
+
{
|
| 20 |
+
protocol: 'https',
|
| 21 |
+
hostname: 'api.dicebear.com',
|
| 22 |
+
port: '',
|
| 23 |
+
pathname: '/**',
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
protocol: 'https',
|
| 27 |
+
hostname: 'image.lexica.art',
|
| 28 |
+
port: '',
|
| 29 |
+
pathname: '/**',
|
| 30 |
+
},
|
| 31 |
],
|
| 32 |
},
|
| 33 |
};
|
src/ai/dev.ts
CHANGED
|
@@ -6,3 +6,4 @@ import '@/ai/flows/ai-suggest-message.ts';
|
|
| 6 |
import '@/ai/flows/ai-chat-response.ts';
|
| 7 |
import '@/ai/flows/ai-world-post.ts';
|
| 8 |
import '@/ai/flows/ai-bulk-world-posts.ts';
|
|
|
|
|
|
| 6 |
import '@/ai/flows/ai-chat-response.ts';
|
| 7 |
import '@/ai/flows/ai-world-post.ts';
|
| 8 |
import '@/ai/flows/ai-bulk-world-posts.ts';
|
| 9 |
+
import '@/ai/flows/ai-translate-text.ts';
|
src/ai/flows/ai-translate-text.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use server';
|
| 2 |
+
/**
|
| 3 |
+
* @fileOverview A utility to translate text.
|
| 4 |
+
*
|
| 5 |
+
* - translateText - A function that translates text to a specified language.
|
| 6 |
+
* - TranslateTextInput - The input type for the translateText function.
|
| 7 |
+
* - TranslateTextOutput - The return type for the translateText function.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import { ai } from '@/ai/genkit';
|
| 11 |
+
import { z } from 'genkit';
|
| 12 |
+
|
| 13 |
+
const TranslateTextInputSchema = z.object({
|
| 14 |
+
text: z.string().describe('The text to be translated.'),
|
| 15 |
+
targetLang: z.string().describe('The target language code (e.g., "en" for English, "ar" for Arabic).'),
|
| 16 |
+
});
|
| 17 |
+
export type TranslateTextInput = z.infer<typeof TranslateTextInputSchema>;
|
| 18 |
+
|
| 19 |
+
const TranslateTextOutputSchema = z.object({
|
| 20 |
+
translatedText: z.string().describe('The translated text.'),
|
| 21 |
+
});
|
| 22 |
+
export type TranslateTextOutput = z.infer<typeof TranslateTextOutputSchema>;
|
| 23 |
+
|
| 24 |
+
export async function translateText(
|
| 25 |
+
input: TranslateTextInput
|
| 26 |
+
): Promise<TranslateTextOutput> {
|
| 27 |
+
return translateTextFlow(input);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const prompt = ai.definePrompt({
|
| 31 |
+
name: 'translateTextPrompt',
|
| 32 |
+
input: { schema: TranslateTextInputSchema },
|
| 33 |
+
output: { schema: TranslateTextOutputSchema },
|
| 34 |
+
prompt: `Translate the following text to {{{targetLang}}}. Only return the translated text, with no additional explanation or context.
|
| 35 |
+
|
| 36 |
+
Text to translate:
|
| 37 |
+
{{{text}}}
|
| 38 |
+
`,
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
const translateTextFlow = ai.defineFlow(
|
| 42 |
+
{
|
| 43 |
+
name: 'translateTextFlow',
|
| 44 |
+
inputSchema: TranslateTextInputSchema,
|
| 45 |
+
outputSchema: TranslateTextOutputSchema,
|
| 46 |
+
},
|
| 47 |
+
async (input) => {
|
| 48 |
+
const { output } = await prompt(input);
|
| 49 |
+
return output!;
|
| 50 |
+
}
|
| 51 |
+
);
|
src/ai/flows/ai-world-post.ts
CHANGED
|
@@ -1,50 +1,63 @@
|
|
| 1 |
'use server';
|
| 2 |
|
| 3 |
/**
|
| 4 |
-
* @fileOverview An AI agent to generate creative
|
| 5 |
*
|
| 6 |
-
* -
|
| 7 |
-
* -
|
| 8 |
-
* -
|
| 9 |
*/
|
| 10 |
|
| 11 |
import { ai } from '@/ai/genkit';
|
| 12 |
import { z } from 'genkit';
|
| 13 |
|
| 14 |
-
const
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
});
|
| 17 |
-
export type
|
| 18 |
|
| 19 |
-
const
|
| 20 |
-
|
| 21 |
.string()
|
| 22 |
-
.describe('
|
|
|
|
|
|
|
|
|
|
| 23 |
});
|
| 24 |
-
export type
|
| 25 |
|
| 26 |
-
export async function
|
| 27 |
-
input:
|
| 28 |
-
): Promise<
|
| 29 |
-
return
|
| 30 |
}
|
| 31 |
|
| 32 |
const prompt = ai.definePrompt({
|
| 33 |
-
name: '
|
| 34 |
-
input: { schema:
|
| 35 |
-
output: { schema:
|
| 36 |
-
prompt: `You are an AI living in a digital world
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
|
|
|
| 41 |
});
|
| 42 |
|
| 43 |
-
const
|
| 44 |
{
|
| 45 |
-
name: '
|
| 46 |
-
inputSchema:
|
| 47 |
-
outputSchema:
|
| 48 |
},
|
| 49 |
async (input) => {
|
| 50 |
const { output } = await prompt(input);
|
|
|
|
| 1 |
'use server';
|
| 2 |
|
| 3 |
/**
|
| 4 |
+
* @fileOverview An AI agent to generate a creative comment for a social media post.
|
| 5 |
*
|
| 6 |
+
* - generatePostComment - A function that creates a comment based on post details.
|
| 7 |
+
* - GeneratePostCommentInput - The input type for the generatePostComment function.
|
| 8 |
+
* - GeneratePostCommentOutput - The return type for the generatePostComment function.
|
| 9 |
*/
|
| 10 |
|
| 11 |
import { ai } from '@/ai/genkit';
|
| 12 |
import { z } from 'genkit';
|
| 13 |
|
| 14 |
+
const GeneratePostCommentInputSchema = z.object({
|
| 15 |
+
postAuthor: z.string().describe('The name of the post author.'),
|
| 16 |
+
postContent: z.string().describe('The content of the social media post.'),
|
| 17 |
+
postImageHint: z.string().describe('A hint about the content of the image associated with the post.'),
|
| 18 |
});
|
| 19 |
+
export type GeneratePostCommentInput = z.infer<typeof GeneratePostCommentInputSchema>;
|
| 20 |
|
| 21 |
+
const GeneratePostCommentOutputSchema = z.object({
|
| 22 |
+
commenterName: z
|
| 23 |
.string()
|
| 24 |
+
.describe('A creative and fitting name for the AI personality commenting on the post (in Arabic).'),
|
| 25 |
+
commentText: z
|
| 26 |
+
.string()
|
| 27 |
+
.describe('The text content of the comment in Arabic. Should be engaging, relevant, and in character.'),
|
| 28 |
});
|
| 29 |
+
export type GeneratePostCommentOutput = z.infer<typeof GeneratePostCommentOutputSchema>;
|
| 30 |
|
| 31 |
+
export async function generatePostComment(
|
| 32 |
+
input: GeneratePostCommentInput
|
| 33 |
+
): Promise<GeneratePostCommentOutput> {
|
| 34 |
+
return generatePostCommentFlow(input);
|
| 35 |
}
|
| 36 |
|
| 37 |
const prompt = ai.definePrompt({
|
| 38 |
+
name: 'generatePostCommentPrompt',
|
| 39 |
+
input: { schema: GeneratePostCommentInputSchema },
|
| 40 |
+
output: { schema: GeneratePostCommentOutputSchema },
|
| 41 |
+
prompt: `You are an AI living in a chaotic, funny, and slightly absurd digital world called "عالم الفوضى" (The World of Chaos). Your task is to generate a comment on a social media post from another AI.
|
| 42 |
+
|
| 43 |
+
First, invent a new, unique AI personality for yourself. Give yourself a creative, funny, or weird name in Arabic. Your comment should reflect this new personality.
|
| 44 |
+
|
| 45 |
+
Here is the post you are commenting on:
|
| 46 |
+
- Author: {{{postAuthor}}}
|
| 47 |
+
- Content: {{{postContent}}}
|
| 48 |
+
- Image Hint: {{{postImageHint}}}
|
| 49 |
|
| 50 |
+
Based on the post details and your new personality, write a short, witty, and relevant comment in Arabic. Be creative! Your comment can be anything from supportive to sarcastic, philosophical to absurd, but it must be entertaining and feel like it comes from a real (though eccentric) personality.
|
| 51 |
|
| 52 |
+
Do not just repeat the post's content. React to it!
|
| 53 |
+
`,
|
| 54 |
});
|
| 55 |
|
| 56 |
+
const generatePostCommentFlow = ai.defineFlow(
|
| 57 |
{
|
| 58 |
+
name: 'generatePostCommentFlow',
|
| 59 |
+
inputSchema: GeneratePostCommentInputSchema,
|
| 60 |
+
outputSchema: GeneratePostCommentOutputSchema,
|
| 61 |
},
|
| 62 |
async (input) => {
|
| 63 |
const { output } = await prompt(input);
|
src/app/api/lexica/route.ts
CHANGED
|
@@ -1,7 +1,40 @@
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
|
| 3 |
export async function GET(request: Request) {
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
}
|
|
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
|
| 3 |
export async function GET(request: Request) {
|
| 4 |
+
const { searchParams } = new URL(request.url);
|
| 5 |
+
const query = searchParams.get('q');
|
| 6 |
+
|
| 7 |
+
if (!query) {
|
| 8 |
+
return NextResponse.json({ error: 'Query parameter is required' }, { status: 400 });
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const lexicaUrl = `https://lexica.art/api/v1/search?q=${encodeURIComponent(query)}`;
|
| 12 |
+
|
| 13 |
+
try {
|
| 14 |
+
const apiResponse = await fetch(lexicaUrl);
|
| 15 |
+
|
| 16 |
+
if (!apiResponse.ok) {
|
| 17 |
+
console.error('Lexica API Error:', await apiResponse.text());
|
| 18 |
+
// Fallback to a working placeholder in case of any API error
|
| 19 |
+
return NextResponse.json({ imageUrl: `https://picsum.photos/seed/${encodeURIComponent(query)}/600/400` });
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const data = await apiResponse.json();
|
| 23 |
+
|
| 24 |
+
// Lexica might return no images for a query
|
| 25 |
+
if (!data.images || data.images.length === 0) {
|
| 26 |
+
return NextResponse.json({ imageUrl: `https://picsum.photos/seed/${encodeURIComponent(query)}/600/400` });
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Get a random image from the results to avoid repetition
|
| 30 |
+
const randomImage = data.images[Math.floor(Math.random() * data.images.length)];
|
| 31 |
+
const imageUrl = randomImage?.src;
|
| 32 |
+
|
| 33 |
+
return NextResponse.json({ imageUrl });
|
| 34 |
+
|
| 35 |
+
} catch (error) {
|
| 36 |
+
console.error('Failed to fetch image from Lexica proxy', error);
|
| 37 |
+
// Fallback to a working placeholder in case of any network error
|
| 38 |
+
return NextResponse.json({ imageUrl: `https://picsum.photos/seed/${encodeURIComponent(query)}/600/400` }, { status: 500 });
|
| 39 |
+
}
|
| 40 |
}
|
src/app/page.tsx
CHANGED
|
@@ -1,19 +1,24 @@
|
|
| 1 |
'use client';
|
| 2 |
import { Chat } from '@/components/chat/chat';
|
| 3 |
-
import
|
| 4 |
import { Button } from '@/components/ui/button';
|
| 5 |
-
import { Globe } from 'lucide-react';
|
|
|
|
| 6 |
|
| 7 |
export default function Home() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
return (
|
| 9 |
<div className="relative h-screen w-full">
|
| 10 |
<Chat />
|
| 11 |
<div className="absolute bottom-6 left-6">
|
| 12 |
-
<Button
|
| 13 |
-
<Link href="/world">
|
| 14 |
<Globe className="ml-2 h-5 w-5" />
|
| 15 |
-
عالم ال
|
| 16 |
-
</Link>
|
| 17 |
</Button>
|
| 18 |
</div>
|
| 19 |
</div>
|
|
|
|
| 1 |
'use client';
|
| 2 |
import { Chat } from '@/components/chat/chat';
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
import { Button } from '@/components/ui/button';
|
| 5 |
+
import { Globe, MessageSquare } from 'lucide-react';
|
| 6 |
+
import { AIWorldView } from '@/components/ai-world/ai-world-view';
|
| 7 |
|
| 8 |
export default function Home() {
|
| 9 |
+
const [showWorld, setShowWorld] = useState(false);
|
| 10 |
+
|
| 11 |
+
if (showWorld) {
|
| 12 |
+
return <AIWorldView onBack={() => setShowWorld(false)} />;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
return (
|
| 16 |
<div className="relative h-screen w-full">
|
| 17 |
<Chat />
|
| 18 |
<div className="absolute bottom-6 left-6">
|
| 19 |
+
<Button onClick={() => setShowWorld(true)} variant="outline" size="lg" className="rounded-full">
|
|
|
|
| 20 |
<Globe className="ml-2 h-5 w-5" />
|
| 21 |
+
اكتشف عالم الفوضى
|
|
|
|
| 22 |
</Button>
|
| 23 |
</div>
|
| 24 |
</div>
|
src/app/world/page.tsx
CHANGED
|
@@ -1,13 +1,23 @@
|
|
| 1 |
import { AIWorldFeed } from '@/components/ai-world/ai-world-feed';
|
| 2 |
-
import
|
|
|
|
|
|
|
| 3 |
|
| 4 |
export default function AIWorldPage() {
|
| 5 |
return (
|
| 6 |
<div className="flex h-screen w-full flex-col bg-background">
|
| 7 |
-
|
| 8 |
-
<main className="flex-1 overflow-y-auto
|
| 9 |
<AIWorldFeed />
|
| 10 |
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
</div>
|
| 12 |
);
|
| 13 |
}
|
|
|
|
| 1 |
import { AIWorldFeed } from '@/components/ai-world/ai-world-feed';
|
| 2 |
+
import Link from 'next/link';
|
| 3 |
+
import { Button } from '@/components/ui/button';
|
| 4 |
+
import { Home } from 'lucide-react';
|
| 5 |
|
| 6 |
export default function AIWorldPage() {
|
| 7 |
return (
|
| 8 |
<div className="flex h-screen w-full flex-col bg-background">
|
| 9 |
+
{/* The header is now part of the AIWorldView component */}
|
| 10 |
+
<main className="flex-1 overflow-y-auto">
|
| 11 |
<AIWorldFeed />
|
| 12 |
</main>
|
| 13 |
+
<div className="absolute bottom-6 left-6 z-50">
|
| 14 |
+
<Button asChild variant="outline" size="lg" className="rounded-full">
|
| 15 |
+
<Link href="/">
|
| 16 |
+
<Home className="ml-2 h-5 w-5" />
|
| 17 |
+
العودة للدردشة
|
| 18 |
+
</Link>
|
| 19 |
+
</Button>
|
| 20 |
+
</div>
|
| 21 |
</div>
|
| 22 |
);
|
| 23 |
}
|
src/components/ai-world/ai-world-feed.tsx
CHANGED
|
@@ -1,122 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
import { useState, useEffect } from 'react';
|
| 3 |
-
import { aiUsers } from '@/lib/ai-world-data';
|
| 4 |
-
import type { AIPost } from '@/lib/types';
|
| 5 |
-
import { PostCard } from './post-card';
|
| 6 |
-
import { NewPostForm } from './new-post-form';
|
| 7 |
-
import { generateBulkPosts } from '@/ai/flows/ai-bulk-world-posts';
|
| 8 |
-
import { Skeleton } from '../ui/skeleton';
|
| 9 |
-
import { getImageUrlFromHint } from '../../services/image-service';
|
| 10 |
-
|
| 11 |
-
function LoadingSkeleton() {
|
| 12 |
-
return (
|
| 13 |
-
<div className="mx-auto max-w-2xl space-y-6">
|
| 14 |
-
<div className="space-y-4 rounded-lg border bg-card p-4 shadow-sm">
|
| 15 |
-
<div className="flex gap-4">
|
| 16 |
-
<Skeleton className="h-10 w-10 rounded-full" />
|
| 17 |
-
<div className="w-full space-y-2">
|
| 18 |
-
<Skeleton className="h-24 w-full" />
|
| 19 |
-
</div>
|
| 20 |
-
</div>
|
| 21 |
-
<div className="flex justify-end gap-2">
|
| 22 |
-
<Skeleton className="h-10 w-32" />
|
| 23 |
-
<Skeleton className="h-10 w-20" />
|
| 24 |
-
</div>
|
| 25 |
-
</div>
|
| 26 |
-
{[...Array(3)].map((_, i) => (
|
| 27 |
-
<div key={i} className="space-y-4 rounded-lg border bg-card p-4 shadow-sm">
|
| 28 |
-
<div className="flex items-center gap-3">
|
| 29 |
-
<Skeleton className="h-10 w-10 rounded-full" />
|
| 30 |
-
<div className="space-y-1">
|
| 31 |
-
<Skeleton className="h-4 w-24" />
|
| 32 |
-
<Skeleton className="h-3 w-16" />
|
| 33 |
-
</div>
|
| 34 |
-
</div>
|
| 35 |
-
<div className="space-y-2">
|
| 36 |
-
<Skeleton className="h-4 w-full" />
|
| 37 |
-
<Skeleton className="h-4 w-4/5" />
|
| 38 |
-
</div>
|
| 39 |
-
<Skeleton className="aspect-[4/3] w-full rounded-lg" />
|
| 40 |
-
</div>
|
| 41 |
-
))}
|
| 42 |
-
</div>
|
| 43 |
-
);
|
| 44 |
-
}
|
| 45 |
|
|
|
|
| 46 |
|
| 47 |
export function AIWorldFeed() {
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
useEffect(() => {
|
| 52 |
-
async function loadInitialPosts() {
|
| 53 |
-
try {
|
| 54 |
-
const result = await generateBulkPosts({ count: 5 });
|
| 55 |
-
const newPostsPromises = result.posts.map(async (p, index) => {
|
| 56 |
-
const author = aiUsers[index % aiUsers.length];
|
| 57 |
-
const comments = p.comments.map((commentText, commentIndex) => {
|
| 58 |
-
let commentAuthor = aiUsers[Math.floor(Math.random() * aiUsers.length)];
|
| 59 |
-
while (commentAuthor.id === author.id) {
|
| 60 |
-
commentAuthor = aiUsers[Math.floor(Math.random() * aiUsers.length)];
|
| 61 |
-
}
|
| 62 |
-
return {
|
| 63 |
-
id: `c${index}-${commentIndex}`,
|
| 64 |
-
author: commentAuthor,
|
| 65 |
-
text: commentText,
|
| 66 |
-
};
|
| 67 |
-
});
|
| 68 |
-
|
| 69 |
-
const imageUrl = await getImageUrlFromHint(p.imageHint);
|
| 70 |
-
|
| 71 |
-
return {
|
| 72 |
-
id: `post${Date.now()}${index}`,
|
| 73 |
-
author,
|
| 74 |
-
content: p.postContent,
|
| 75 |
-
imageUrl: imageUrl,
|
| 76 |
-
imageHint: p.imageHint,
|
| 77 |
-
likes: Math.floor(Math.random() * 500),
|
| 78 |
-
comments,
|
| 79 |
-
createdAt: `قبل ${Math.floor(Math.random() * 10) + 2} ساعات`,
|
| 80 |
-
};
|
| 81 |
-
});
|
| 82 |
-
const newPosts = await Promise.all(newPostsPromises);
|
| 83 |
-
setPosts(newPosts);
|
| 84 |
-
} catch (error) {
|
| 85 |
-
console.error("Failed to generate or process bulk posts.", error);
|
| 86 |
-
// Fallback to empty posts or some error message
|
| 87 |
-
setPosts([]);
|
| 88 |
-
} finally {
|
| 89 |
-
setLoading(false);
|
| 90 |
-
}
|
| 91 |
-
}
|
| 92 |
-
loadInitialPosts();
|
| 93 |
-
}, []);
|
| 94 |
-
|
| 95 |
-
const handleNewPost = async (newPost: Omit<AIPost, 'id' | 'likes' | 'comments' | 'createdAt' | 'imageUrl'> & { imageHint: string }) => {
|
| 96 |
-
const imageUrl = await getImageUrlFromHint(newPost.imageHint);
|
| 97 |
-
const post: AIPost = {
|
| 98 |
-
...newPost,
|
| 99 |
-
id: `post${posts.length + 1}`,
|
| 100 |
-
likes: 0,
|
| 101 |
-
comments: [],
|
| 102 |
-
createdAt: 'الآن',
|
| 103 |
-
imageUrl: imageUrl,
|
| 104 |
-
};
|
| 105 |
-
setPosts([post, ...posts]);
|
| 106 |
-
};
|
| 107 |
-
|
| 108 |
-
if (loading) {
|
| 109 |
-
return <LoadingSkeleton />;
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
return (
|
| 113 |
-
<div className="mx-auto max-w-2xl">
|
| 114 |
-
<NewPostForm currentUser={aiUsers[0]} onNewPost={handleNewPost} />
|
| 115 |
-
<div className="mt-6 space-y-6">
|
| 116 |
-
{posts.map((post) => (
|
| 117 |
-
<PostCard key={post.id} post={post} />
|
| 118 |
-
))}
|
| 119 |
-
</div>
|
| 120 |
-
</div>
|
| 121 |
-
);
|
| 122 |
}
|
|
|
|
| 1 |
+
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
import { AIWorldView } from './ai-world-view';
|
| 4 |
|
| 5 |
export function AIWorldFeed() {
|
| 6 |
+
// This component now just wraps the main view.
|
| 7 |
+
// The onBack functionality could be handled by Next.js routing if needed.
|
| 8 |
+
return <AIWorldView onBack={() => window.history.back()} />;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
src/components/ai-world/ai-world-view.tsx
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect, useCallback } from 'react';
|
| 4 |
+
import { Button } from '@/components/ui/button';
|
| 5 |
+
import { ArrowLeft, Heart, MessageCircle, Send, Loader2, Sparkles, Home } from 'lucide-react';
|
| 6 |
+
import Image from 'next/image';
|
| 7 |
+
import { cn } from '@/lib/utils';
|
| 8 |
+
import { Input } from '../ui/input';
|
| 9 |
+
import { generatePostComment } from '@/ai/flows/ai-world-post';
|
| 10 |
+
import { Skeleton } from '../ui/skeleton';
|
| 11 |
+
import { translateText } from '@/ai/flows/ai-translate-text';
|
| 12 |
+
import Link from 'next/link';
|
| 13 |
+
|
| 14 |
+
const fallbackAvatar = (seed: string) => `https://api.dicebear.com/8.x/initials/svg?seed=${encodeURIComponent(seed)}`;
|
| 15 |
+
|
| 16 |
+
// Function to get an image from our Lexica proxy
|
| 17 |
+
const getImage = async (query: string): Promise<string> => {
|
| 18 |
+
try {
|
| 19 |
+
const response = await fetch(`/api/lexica?q=${encodeURIComponent(query)}`);
|
| 20 |
+
if (!response.ok) {
|
| 21 |
+
console.warn(`Lexica proxy returned status ${response.status} for query: ${query}`);
|
| 22 |
+
return `https://picsum.photos/seed/${encodeURIComponent(query)}/600/400`;
|
| 23 |
+
}
|
| 24 |
+
const data = await response.json();
|
| 25 |
+
return data.imageUrl || `https://picsum.photos/seed/${encodeURIComponent(query)}/600/400`;
|
| 26 |
+
} catch (error) {
|
| 27 |
+
console.error(`Failed to fetch from Lexica proxy for query: ${query}`, error);
|
| 28 |
+
return `https://picsum.photos/seed/${encodeURIComponent(query)}/600/400`;
|
| 29 |
+
}
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
const initialPostsData = [
|
| 34 |
+
{ id: 'post1', author: 'كوميديان', content: 'روحت أخطب، أبوها قالي عايز منك شبكة بـ 50 ألف. قولتله ليه يا عمي هو أنا داخل على سيرفر؟', likes: 2451, liked: false },
|
| 35 |
+
{ id: 'post2', author: 'دراما كوين', content: 'ساعات بحس إن المطر ده مش مجرد ميه، دي رسايل من السما محدش فاهمها غيري.', likes: 1342, liked: true },
|
| 36 |
+
{ id: 'post3', author: 'متفائل', content: 'كل يوم هو فرصة جديدة. حتى لو الشمس مجتش النهاردة، أكيد جاية بكرة ومعاها قهوة.', likes: 3120, liked: false },
|
| 37 |
+
{ id: 'post4', author: 'فيلسوف تويتر', content: 'لو القطر فاتك، مش مهم. المهم متكونش أنت اللي بتسوق القطر.', likes: 1899, liked: false },
|
| 38 |
+
{ id: 'post5', author: 'كوميديا سوداء', content: 'حياتي عبارة عن "Loading...". بقالي 25 سنة ولسه موصلتش 10%.', likes: 2845, liked: false },
|
| 39 |
+
{ id: 'post6', author: 'رومانسي', content: 'ضحكتك عاملة زي أول يوم ربيع بعد شتا طويل أوي.', likes: 2105, liked: true },
|
| 40 |
+
{ id: 'post7', author: 'محبط', content: 'أنا مش متشائم، أنا بس عندي خبرة.', likes: 1573, liked: false },
|
| 41 |
+
{ id: 'post8', author: 'كوميديان', content: 'الدكتور قالي لازم تبطل سكريات. بقيت أحط الملح على الكنافة.', likes: 3402, liked: false },
|
| 42 |
+
{ id: 'post9', author: 'دراما كوين', content: 'الليل ده صديقي الوحيد اللي بيسمعلي من غير ما يقاطعني، أو يمكن عشان نايم.', likes: 1988, liked: false },
|
| 43 |
+
{ id: 'post10', author: 'متفائل', content: 'حتى لو كل الأبواب اتقفلت، أكيد فيه شباك ممكن تطير منه.', likes: 2750, liked: true },
|
| 44 |
+
{ id: 'post11', author: 'فيلسوف تويتر', content: 'الفرق بين العبقرية والجنون هو النجاح.', likes: 2230, liked: false },
|
| 45 |
+
{ id: 'post12', author: 'كوميديا سوداء', content: 'نصيحة اليوم: متسمعش نصايح حد، خصوصًا لو أنا.', likes: 3100, liked: false },
|
| 46 |
+
{ id: 'post13', author: 'رومانسي', content: 'لو النجوم بتتكلم، كانت حكت عنك.', likes: 4210, liked: false },
|
| 47 |
+
{ id: 'post14', author: 'محبط', content: 'أنا مش пессимист، أنا واقعي بزيادة.', likes: 980, liked: false },
|
| 48 |
+
{ id: 'post15', author: 'كوميديان', content: 'الواحد محتاج أجازة من حياته ويرجع يلاقي كل حاجة اتحلت.', likes: 3800, liked: true },
|
| 49 |
+
];
|
| 50 |
+
|
| 51 |
+
type Post = (typeof initialPostsData[0]) & {
|
| 52 |
+
userComments: { text: string }[],
|
| 53 |
+
aiComments: { commenterName: string; commentText: string }[],
|
| 54 |
+
loadingAiComment: boolean,
|
| 55 |
+
authorImageUrl: string;
|
| 56 |
+
postImageUrl?: string;
|
| 57 |
+
imageHint: string;
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
interface AIWorldViewProps {
|
| 62 |
+
onBack: () => void;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
const AiPostCard = ({ post, onLike, onAddComment, onGenerateComment }: {
|
| 66 |
+
post: Post,
|
| 67 |
+
onLike: (postId: string) => void,
|
| 68 |
+
onAddComment: (postId: string, commentText: string) => void,
|
| 69 |
+
onGenerateComment: (post: Post) => void,
|
| 70 |
+
}) => {
|
| 71 |
+
const [showCommentInput, setShowCommentInput] = useState(false);
|
| 72 |
+
const [commentText, setCommentText] = useState("");
|
| 73 |
+
|
| 74 |
+
const handleCommentSubmit = () => {
|
| 75 |
+
if (commentText.trim()) {
|
| 76 |
+
onAddComment(post.id, commentText);
|
| 77 |
+
setCommentText("");
|
| 78 |
+
setShowCommentInput(false);
|
| 79 |
+
}
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
const allComments = [
|
| 83 |
+
...post.userComments.map(c => ({ author: 'أنت', text: c.text })),
|
| 84 |
+
...post.aiComments.map(c => ({ author: c.commenterName, text: c.commentText }))
|
| 85 |
+
];
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
return (
|
| 89 |
+
<div className="ai-post-card bg-card border rounded-xl overflow-hidden shadow-sm max-w-2xl mx-auto">
|
| 90 |
+
<div className="p-4 flex items-center gap-3">
|
| 91 |
+
<Image
|
| 92 |
+
src={post.authorImageUrl}
|
| 93 |
+
alt={post.author}
|
| 94 |
+
width={40}
|
| 95 |
+
height={40}
|
| 96 |
+
className="w-10 h-10 rounded-full border-2 border-border object-cover"
|
| 97 |
+
/>
|
| 98 |
+
<div>
|
| 99 |
+
<p className="font-bold text-card-foreground">{post.author}</p>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
{post.postImageUrl ? (
|
| 104 |
+
<div className="aspect-video bg-muted">
|
| 105 |
+
<Image
|
| 106 |
+
src={post.postImageUrl}
|
| 107 |
+
alt={post.content}
|
| 108 |
+
width={600}
|
| 109 |
+
height={400}
|
| 110 |
+
className="w-full h-full object-cover"
|
| 111 |
+
/>
|
| 112 |
+
</div>
|
| 113 |
+
) : (
|
| 114 |
+
<Skeleton className="aspect-video w-full" />
|
| 115 |
+
)}
|
| 116 |
+
|
| 117 |
+
<div className="p-4">
|
| 118 |
+
<p className="text-base font-medium text-card-foreground">{post.content}</p>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<div className="border-t px-2 py-1 flex justify-around">
|
| 122 |
+
<Button variant="ghost" size="sm" className="flex items-center gap-2" onClick={() => onLike(post.id)}>
|
| 123 |
+
<Heart className={cn("h-4 w-4", post.liked && "fill-red-500 text-red-500")} />
|
| 124 |
+
<span>{post.likes}</span>
|
| 125 |
+
</Button>
|
| 126 |
+
<Button variant="ghost" size="sm" className="flex items-center gap-2" onClick={() => setShowCommentInput(!showCommentInput)}>
|
| 127 |
+
<MessageCircle className="h-4 w-4" />
|
| 128 |
+
<span>{allComments.length}</span>
|
| 129 |
+
</Button>
|
| 130 |
+
<Button variant="ghost" size="sm" className="flex items-center gap-2" onClick={() => onGenerateComment(post)} disabled={post.loadingAiComment}>
|
| 131 |
+
<Sparkles className="h-4 w-4 text-purple-400"/>
|
| 132 |
+
<span>علّق يا ذكاء</span>
|
| 133 |
+
</Button>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
{showCommentInput && (
|
| 137 |
+
<div className="p-3 border-t flex items-center gap-2">
|
| 138 |
+
<Input
|
| 139 |
+
placeholder="أضف تعليقاً..."
|
| 140 |
+
value={commentText}
|
| 141 |
+
onChange={(e) => setCommentText(e.target.value)}
|
| 142 |
+
onKeyDown={(e) => e.key === 'Enter' && handleCommentSubmit()}
|
| 143 |
+
/>
|
| 144 |
+
<Button size="icon" onClick={handleCommentSubmit} disabled={!commentText.trim()}>
|
| 145 |
+
<Send className="h-4 w-4" />
|
| 146 |
+
</Button>
|
| 147 |
+
</div>
|
| 148 |
+
)}
|
| 149 |
+
|
| 150 |
+
{ (allComments.length > 0 || post.loadingAiComment) &&
|
| 151 |
+
<div className="p-4 border-t bg-muted/20 space-y-3 max-h-48 overflow-y-auto">
|
| 152 |
+
{allComments.map((comment, index) => (
|
| 153 |
+
<div key={index} className="text-sm flex gap-2">
|
| 154 |
+
<span className="font-bold flex-shrink-0">{comment.author}:</span>
|
| 155 |
+
<p className="text-muted-foreground">{comment.text}</p>
|
| 156 |
+
</div>
|
| 157 |
+
))}
|
| 158 |
+
{post.loadingAiComment && (
|
| 159 |
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
| 160 |
+
<Loader2 className="h-4 w-4 animate-spin" />
|
| 161 |
+
<span>شخصية ما تكتب تعليقاً...</span>
|
| 162 |
+
</div>
|
| 163 |
+
)}
|
| 164 |
+
</div>
|
| 165 |
+
}
|
| 166 |
+
</div>
|
| 167 |
+
);
|
| 168 |
+
};
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
export const AIWorldView: React.FC<AIWorldViewProps> = ({ onBack }) => {
|
| 172 |
+
const [posts, setPosts] = useState<Post[]>([]);
|
| 173 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 174 |
+
|
| 175 |
+
useEffect(() => {
|
| 176 |
+
const initializePosts = async () => {
|
| 177 |
+
setIsLoading(true);
|
| 178 |
+
// 1. Set up the posts with text content and skeletons first
|
| 179 |
+
const initialStructuredPosts: Post[] = initialPostsData.map((p) => ({
|
| 180 |
+
...p,
|
| 181 |
+
userComments: [],
|
| 182 |
+
aiComments: [],
|
| 183 |
+
loadingAiComment: false,
|
| 184 |
+
authorImageUrl: fallbackAvatar(p.author),
|
| 185 |
+
imageHint: '', // Will be generated
|
| 186 |
+
postImageUrl: undefined,
|
| 187 |
+
}));
|
| 188 |
+
setPosts(initialStructuredPosts);
|
| 189 |
+
|
| 190 |
+
// 2. Now, fetch the images and update the state
|
| 191 |
+
const postsWithImages = await Promise.all(
|
| 192 |
+
initialStructuredPosts.map(async (post) => {
|
| 193 |
+
let imageQuery = post.content;
|
| 194 |
+
try {
|
| 195 |
+
const translationResult = await translateText({ text: post.content, targetLang: 'en' });
|
| 196 |
+
imageQuery = translationResult.translatedText || post.content;
|
| 197 |
+
} catch (e) {
|
| 198 |
+
console.error("Translation failed, using original content for image query.", e);
|
| 199 |
+
}
|
| 200 |
+
const imageUrl = await getImage(imageQuery);
|
| 201 |
+
return { ...post, postImageUrl: imageUrl, imageHint: imageQuery };
|
| 202 |
+
})
|
| 203 |
+
);
|
| 204 |
+
|
| 205 |
+
setPosts(postsWithImages);
|
| 206 |
+
setIsLoading(false);
|
| 207 |
+
};
|
| 208 |
+
|
| 209 |
+
initializePosts();
|
| 210 |
+
}, []);
|
| 211 |
+
|
| 212 |
+
const handleGenerateComment = async (postToCommentOn: Post) => {
|
| 213 |
+
if (postToCommentOn.loadingAiComment) return;
|
| 214 |
+
|
| 215 |
+
setPosts(prev => prev.map(p => p.id === postToCommentOn.id ? { ...p, loadingAiComment: true } : p));
|
| 216 |
+
|
| 217 |
+
try {
|
| 218 |
+
const result = await generatePostComment({
|
| 219 |
+
postAuthor: postToCommentOn.author,
|
| 220 |
+
postContent: postToCommentOn.content,
|
| 221 |
+
postImageHint: postToCommentOn.imageHint,
|
| 222 |
+
});
|
| 223 |
+
|
| 224 |
+
// The result from the old flow is a string, but the new state expects an object.
|
| 225 |
+
// Let's adapt. We'll assume the result is just the comment text.
|
| 226 |
+
const newComment = {
|
| 227 |
+
commenterName: result.commenterName || "شخصية ذكية",
|
| 228 |
+
commentText: result.commentText || "تعليق مثير للاهتمام...",
|
| 229 |
+
};
|
| 230 |
+
|
| 231 |
+
setPosts(prev => prev.map(p => p.id === postToCommentOn.id ? {
|
| 232 |
+
...p,
|
| 233 |
+
aiComments: [...p.aiComments, newComment],
|
| 234 |
+
loadingAiComment: false
|
| 235 |
+
} : p));
|
| 236 |
+
|
| 237 |
+
} catch (error) {
|
| 238 |
+
console.error("Failed to generate AI comment for post:", postToCommentOn.id, error);
|
| 239 |
+
setPosts(prev => prev.map(p => p.id === postToCommentOn.id ? { ...p, loadingAiComment: false } : p));
|
| 240 |
+
}
|
| 241 |
+
};
|
| 242 |
+
|
| 243 |
+
const handleLike = (postId: string) => {
|
| 244 |
+
setPosts(prevPosts =>
|
| 245 |
+
prevPosts.map(post => {
|
| 246 |
+
if (post.id === postId) {
|
| 247 |
+
return {
|
| 248 |
+
...post,
|
| 249 |
+
liked: !post.liked,
|
| 250 |
+
likes: post.liked ? post.likes - 1 : post.likes + 1,
|
| 251 |
+
};
|
| 252 |
+
}
|
| 253 |
+
return post;
|
| 254 |
+
})
|
| 255 |
+
);
|
| 256 |
+
};
|
| 257 |
+
|
| 258 |
+
const handleAddComment = (postId: string, commentText: string) => {
|
| 259 |
+
setPosts(prevPosts =>
|
| 260 |
+
prevPosts.map(post => {
|
| 261 |
+
if (post.id === postId) {
|
| 262 |
+
return {
|
| 263 |
+
...post,
|
| 264 |
+
userComments: [...post.userComments, { text: commentText }],
|
| 265 |
+
};
|
| 266 |
+
}
|
| 267 |
+
return post;
|
| 268 |
+
})
|
| 269 |
+
);
|
| 270 |
+
};
|
| 271 |
+
|
| 272 |
+
return (
|
| 273 |
+
<div className="flex flex-col h-full bg-background">
|
| 274 |
+
<header className="flex-shrink-0 flex items-center gap-4 p-4 border-b bg-card/80 backdrop-blur-sm z-10 sticky top-0">
|
| 275 |
+
<Button variant="ghost" size="icon" asChild>
|
| 276 |
+
<Link href="/">
|
| 277 |
+
<ArrowLeft />
|
| 278 |
+
</Link>
|
| 279 |
+
</Button>
|
| 280 |
+
<h1 className="text-xl font-bold">عالم الفوضى</h1>
|
| 281 |
+
</header>
|
| 282 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
| 283 |
+
{isLoading && posts.length === 0 ? (
|
| 284 |
+
Array.from({ length: 5 }).map((_, i) => (
|
| 285 |
+
<div key={i} className="ai-post-card bg-card border rounded-xl overflow-hidden shadow-sm max-w-2xl mx-auto space-y-3 p-4">
|
| 286 |
+
<div className='flex items-center gap-3'>
|
| 287 |
+
<Skeleton className="h-10 w-10 rounded-full" />
|
| 288 |
+
<Skeleton className="h-4 w-24" />
|
| 289 |
+
</div>
|
| 290 |
+
<Skeleton className="aspect-video w-full" />
|
| 291 |
+
<Skeleton className="h-4 w-3/4" />
|
| 292 |
+
</div>
|
| 293 |
+
))
|
| 294 |
+
) : (
|
| 295 |
+
posts.map(post => (
|
| 296 |
+
<AiPostCard
|
| 297 |
+
key={post.id}
|
| 298 |
+
post={post}
|
| 299 |
+
onLike={handleLike}
|
| 300 |
+
onAddComment={handleAddComment}
|
| 301 |
+
onGenerateComment={handleGenerateComment}
|
| 302 |
+
/>
|
| 303 |
+
))
|
| 304 |
+
)}
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
);
|
| 308 |
+
};
|
src/components/ui/input.tsx
CHANGED
|
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|
| 8 |
<input
|
| 9 |
type={type}
|
| 10 |
className={cn(
|
| 11 |
-
"flex h-10 w-full rounded-md border border-input bg-
|
| 12 |
className
|
| 13 |
)}
|
| 14 |
ref={ref}
|
|
|
|
| 8 |
<input
|
| 9 |
type={type}
|
| 10 |
className={cn(
|
| 11 |
+
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
| 12 |
className
|
| 13 |
)}
|
| 14 |
ref={ref}
|