Add AI-powered description generation for video content

Integrates OpenAI API to generate video descriptions using AI, with new API endpoints for single and bulk generation, and a UI button in the admin panel to trigger the process. Includes OpenAI dependency and a new `aiService.ts` file.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 170e18f0-0f13-4eca-8643-546bba1dd8cc
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/170e18f0-0f13-4eca-8643-546bba1dd8cc/td5Y4HG
This commit is contained in:
sebastjanartic
2025-09-02 12:59:45 +00:00
parent a9d7322fa8
commit 475534134c
6 changed files with 268 additions and 2 deletions
+60 -2
View File
@@ -13,7 +13,7 @@ import { useToast } from "@/hooks/use-toast";
import { LoadingSpinner } from "@/components/loading-spinner";
import { apiRequest } from "@/lib/queryClient";
import type { Video } from "@shared/schema";
import { Shield, Edit, Upload, Search, Filter, Save, X } from "lucide-react";
import { Shield, Edit, Upload, Search, Filter, Save, X, Sparkles, Loader2 } from "lucide-react";
export default function AdminPage() {
const { user, isLoading: authLoading, isAuthenticated, isAdmin } = useAuth();
@@ -216,6 +216,7 @@ function EditVideoDialog({
genre: video.genre,
customThumbnailUrl: video.customThumbnailUrl || "",
});
const [isGeneratingAI, setIsGeneratingAI] = useState(false);
const updateMutation = useMutation({
mutationFn: (data: any) => apiRequest("PATCH", `/api/admin/videos/${video.id}`, data),
@@ -240,6 +241,42 @@ function EditVideoDialog({
updateMutation.mutate(formData);
};
const generateAIDescription = async () => {
if (!formData.title.trim()) {
toast({
title: "Error",
description: "Please enter a title first",
variant: "destructive",
});
return;
}
setIsGeneratingAI(true);
try {
const response = await apiRequest("POST", `/api/admin/videos/${video.id}/generate-description`, {
maxCharacters: 500,
includeArtistInfo: true,
includeLabelInfo: true
});
if (response.description) {
setFormData(prev => ({ ...prev, description: response.description }));
toast({
title: "Success",
description: `AI description generated (${response.characterCount}/${response.maxCharacters} characters)`,
});
}
} catch (error: any) {
toast({
title: "Error",
description: error.message || "Failed to generate AI description",
variant: "destructive",
});
} finally {
setIsGeneratingAI(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl bg-[#2D1B69] border-white/20 text-white">
@@ -263,13 +300,34 @@ function EditVideoDialog({
</div>
<div className="md:col-span-2">
<Label className="text-white/90">Description</Label>
<div className="flex items-center justify-between mb-2">
<Label className="text-white/90">Description</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={generateAIDescription}
disabled={isGeneratingAI || !formData.title.trim()}
className="border-white/20 text-white hover:bg-white/10 disabled:opacity-50"
>
{isGeneratingAI ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Sparkles className="w-4 h-4 mr-2" />
)}
{isGeneratingAI ? "Generating..." : "Generate AI Description"}
</Button>
</div>
<Textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="bg-white/10 border-white/20 text-white min-h-[100px]"
rows={3}
placeholder="Enter description or use AI to generate one from the title..."
/>
<div className="text-xs text-white/60 mt-1">
{formData.description.length}/500 characters
</div>
</div>
<div>