Add AI-powered chatbot for media outlets
Integrate an AI chatbot feature allowing users to interact with media outlets, fetch chat history, and generate AI responses using OpenAI. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9a264234-c5d7-4dcc-adf3-a954b149b30d Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/9a264234-c5d7-4dcc-adf3-a954b149b30d/d35d7YU
This commit is contained in:
134
client/src/components/ChatbotModal.tsx
Normal file
134
client/src/components/ChatbotModal.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { MessageCircle, Send } from "lucide-react";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import type { ChatMessage, MediaOutlet } from "@shared/schema";
|
||||
|
||||
interface ChatbotModalProps {
|
||||
outlet: MediaOutlet;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ChatbotModal({ outlet, isOpen, onClose }: ChatbotModalProps) {
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const { data: messages = [], isLoading } = useQuery<ChatMessage[]>({
|
||||
queryKey: [`/api/media-outlets/${outlet.slug}/chat`],
|
||||
enabled: isOpen
|
||||
});
|
||||
|
||||
const sendMessageMutation = useMutation({
|
||||
mutationFn: async (content: string) => {
|
||||
return await apiRequest("POST", `/api/media-outlets/${outlet.slug}/chat`, { content });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [`/api/media-outlets/${outlet.slug}/chat`] });
|
||||
setMessage("");
|
||||
}
|
||||
});
|
||||
|
||||
const handleSend = () => {
|
||||
if (message.trim()) {
|
||||
sendMessageMutation.mutate(message.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
const getChatbotName = () => {
|
||||
if (outlet.category === 'people') {
|
||||
return outlet.name;
|
||||
} else if (outlet.category === 'topics') {
|
||||
return `${outlet.name} 전문가`;
|
||||
} else {
|
||||
return `${outlet.name} 대표`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[600px] h-[600px] flex flex-col" data-testid="chatbot-modal">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2" data-testid="chatbot-title">
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
{getChatbotName()}와 대화하기
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="flex-1 pr-4" data-testid="chat-messages-container">
|
||||
<div className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
대화 내용을 불러오는 중...
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
<MessageCircle className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>대화를 시작해보세요!</p>
|
||||
<p className="text-sm mt-2">
|
||||
{getChatbotName()}에게 궁금한 것을 물어보세요.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
data-testid={`chat-message-${msg.id}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-lg px-4 py-2 ${
|
||||
msg.role === 'user'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm whitespace-pre-wrap break-words">{msg.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{sendMessageMutation.isPending && (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[80%] rounded-lg px-4 py-2 bg-muted">
|
||||
<p className="text-sm text-muted-foreground">입력 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="flex gap-2 pt-4 border-t">
|
||||
<Textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="메시지를 입력하세요..."
|
||||
className="min-h-[60px] resize-none"
|
||||
disabled={sendMessageMutation.isPending}
|
||||
data-testid="chat-input"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={!message.trim() || sendMessageMutation.isPending}
|
||||
size="icon"
|
||||
className="h-[60px] w-[60px]"
|
||||
data-testid="button-send-message"
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Search, Settings, User, LogOut, Grid, List, Info } from "lucide-react";
|
||||
import { Search, Settings, User, LogOut, Grid, List, Info, MessageCircle } from "lucide-react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { queryClient } from "@/lib/queryClient";
|
||||
@ -14,6 +14,7 @@ import ArticleCard from "@/components/ArticleCard";
|
||||
import Footer from "@/components/Footer";
|
||||
import LoginModal from "@/components/LoginModal";
|
||||
import SearchModal from "@/components/SearchModal";
|
||||
import ChatbotModal from "@/components/ChatbotModal";
|
||||
import type { MediaOutlet, Article } from "@shared/schema";
|
||||
|
||||
export default function MediaOutlet() {
|
||||
@ -22,6 +23,7 @@ export default function MediaOutlet() {
|
||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
|
||||
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
||||
const [isChatbotOpen, setIsChatbotOpen] = useState(false);
|
||||
const [enlargedImage, setEnlargedImage] = useState<string | null>(null);
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const { toast } = useToast();
|
||||
@ -271,6 +273,16 @@ export default function MediaOutlet() {
|
||||
Community
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsChatbotOpen(true)}
|
||||
data-testid="button-chatbot"
|
||||
>
|
||||
<MessageCircle className="h-4 w-4 mr-1" />
|
||||
Chat
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@ -415,6 +427,15 @@ export default function MediaOutlet() {
|
||||
onClose={() => setIsSearchModalOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Chatbot Modal */}
|
||||
{outlet && (
|
||||
<ChatbotModal
|
||||
outlet={outlet}
|
||||
isOpen={isChatbotOpen}
|
||||
onClose={() => setIsChatbotOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Image Enlargement Dialog */}
|
||||
<Dialog open={!!enlargedImage} onOpenChange={() => setEnlargedImage(null)}>
|
||||
<DialogContent className="max-w-3xl p-0 bg-transparent border-none">
|
||||
|
||||
Reference in New Issue
Block a user