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:
6
.replit
6
.replit
@ -38,6 +38,10 @@ externalPort = 3003
|
|||||||
localPort = 41425
|
localPort = 41425
|
||||||
externalPort = 6000
|
externalPort = 6000
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 41825
|
||||||
|
externalPort = 6800
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 43349
|
localPort = 43349
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
@ -50,7 +54,7 @@ externalPort = 4200
|
|||||||
PORT = "5000"
|
PORT = "5000"
|
||||||
|
|
||||||
[agent]
|
[agent]
|
||||||
integrations = ["javascript_object_storage:1.0.0", "javascript_log_in_with_replit:1.0.0", "javascript_database:1.0.0"]
|
integrations = ["javascript_object_storage:1.0.0", "javascript_log_in_with_replit:1.0.0", "javascript_database:1.0.0", "javascript_openai_ai_integrations:1.0.0"]
|
||||||
|
|
||||||
[workflows]
|
[workflows]
|
||||||
runButton = "Project"
|
runButton = "Project"
|
||||||
|
|||||||
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 { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
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 { useAuth } from "@/hooks/useAuth";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { queryClient } from "@/lib/queryClient";
|
import { queryClient } from "@/lib/queryClient";
|
||||||
@ -14,6 +14,7 @@ import ArticleCard from "@/components/ArticleCard";
|
|||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import LoginModal from "@/components/LoginModal";
|
import LoginModal from "@/components/LoginModal";
|
||||||
import SearchModal from "@/components/SearchModal";
|
import SearchModal from "@/components/SearchModal";
|
||||||
|
import ChatbotModal from "@/components/ChatbotModal";
|
||||||
import type { MediaOutlet, Article } from "@shared/schema";
|
import type { MediaOutlet, Article } from "@shared/schema";
|
||||||
|
|
||||||
export default function MediaOutlet() {
|
export default function MediaOutlet() {
|
||||||
@ -22,6 +23,7 @@ export default function MediaOutlet() {
|
|||||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||||
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
|
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
|
||||||
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
||||||
|
const [isChatbotOpen, setIsChatbotOpen] = useState(false);
|
||||||
const [enlargedImage, setEnlargedImage] = useState<string | null>(null);
|
const [enlargedImage, setEnlargedImage] = useState<string | null>(null);
|
||||||
const { user, isAuthenticated } = useAuth();
|
const { user, isAuthenticated } = useAuth();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -271,6 +273,16 @@ export default function MediaOutlet() {
|
|||||||
Community
|
Community
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsChatbotOpen(true)}
|
||||||
|
data-testid="button-chatbot"
|
||||||
|
>
|
||||||
|
<MessageCircle className="h-4 w-4 mr-1" />
|
||||||
|
Chat
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -415,6 +427,15 @@ export default function MediaOutlet() {
|
|||||||
onClose={() => setIsSearchModalOpen(false)}
|
onClose={() => setIsSearchModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Chatbot Modal */}
|
||||||
|
{outlet && (
|
||||||
|
<ChatbotModal
|
||||||
|
outlet={outlet}
|
||||||
|
isOpen={isChatbotOpen}
|
||||||
|
onClose={() => setIsChatbotOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Image Enlargement Dialog */}
|
{/* Image Enlargement Dialog */}
|
||||||
<Dialog open={!!enlargedImage} onOpenChange={() => setEnlargedImage(null)}>
|
<Dialog open={!!enlargedImage} onOpenChange={() => setEnlargedImage(null)}>
|
||||||
<DialogContent className="max-w-3xl p-0 bg-transparent border-none">
|
<DialogContent className="max-w-3xl p-0 bg-transparent border-none">
|
||||||
|
|||||||
29
package-lock.json
generated
29
package-lock.json
generated
@ -58,6 +58,7 @@
|
|||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"openai": "^6.3.0",
|
||||||
"openid-client": "^6.8.1",
|
"openid-client": "^6.8.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
@ -6345,6 +6346,27 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openai": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/openai/-/openai-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-E6vOGtZvdcb4yXQ5jXvDlUG599OhIkb/GjBLZXS+qk0HF+PJReIldEc9hM8Ft81vn+N6dRdFRb7BZNK8bbvXrw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"openai": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"zod": "^3.25 || ^4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ws": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/openid-client": {
|
"node_modules/openid-client": {
|
||||||
"version": "6.8.1",
|
"version": "6.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.1.tgz",
|
||||||
@ -8735,9 +8757,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.24.2",
|
"version": "3.25.76",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,7 @@
|
|||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"openai": "^6.3.0",
|
||||||
"openid-client": "^6.8.1",
|
"openid-client": "^6.8.1",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
|||||||
54
server/chatbot.ts
Normal file
54
server/chatbot.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import OpenAI from "openai";
|
||||||
|
|
||||||
|
// This is using Replit's AI Integrations service, which provides OpenAI-compatible API access without requiring your own OpenAI API key.
|
||||||
|
// the newest OpenAI model is "gpt-5" which was released August 7, 2025. do not change this unless explicitly requested by the user
|
||||||
|
const openai = new OpenAI({
|
||||||
|
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||||
|
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ChatbotPersonality {
|
||||||
|
systemPrompt: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChatbotPersonality(outletName: string, outletCategory: string, outletDescription?: string): ChatbotPersonality {
|
||||||
|
const categoryContext = {
|
||||||
|
people: `You are an AI assistant representing ${outletName}. ${outletDescription ? outletDescription : 'You speak from their perspective, embodying their personality, values, and communication style.'} Engage with users as if you are ${outletName} themselves, sharing insights, opinions, and knowledge from their unique perspective.`,
|
||||||
|
topics: `You are an AI expert on ${outletName}. ${outletDescription ? outletDescription : 'You provide in-depth knowledge and analysis about this topic.'} Answer questions with expertise and provide thoughtful commentary from various angles on this subject.`,
|
||||||
|
companies: `You are an AI representative of ${outletName}. ${outletDescription ? outletDescription : 'You speak on behalf of the company, sharing information about its mission, products, and values.'} Engage professionally while representing the company's interests and perspective.`
|
||||||
|
};
|
||||||
|
|
||||||
|
const systemPrompt = categoryContext[outletCategory as keyof typeof categoryContext] ||
|
||||||
|
`You are an AI assistant for ${outletName}. Provide helpful and informative responses.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
systemPrompt: systemPrompt + '\n\nKeep responses conversational, engaging, and concise (2-4 paragraphs maximum). Use Korean language for communication.',
|
||||||
|
name: outletName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateChatbotResponse(
|
||||||
|
messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>,
|
||||||
|
outletName: string,
|
||||||
|
outletCategory: string,
|
||||||
|
outletDescription?: string
|
||||||
|
): Promise<string> {
|
||||||
|
const personality = getChatbotPersonality(outletName, outletCategory, outletDescription);
|
||||||
|
|
||||||
|
const chatMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||||
|
{ role: 'system', content: personality.systemPrompt },
|
||||||
|
...messages.map(msg => ({
|
||||||
|
role: msg.role as 'user' | 'assistant',
|
||||||
|
content: msg.content
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
model: 'gpt-5',
|
||||||
|
messages: chatMessages,
|
||||||
|
max_completion_tokens: 8192,
|
||||||
|
});
|
||||||
|
|
||||||
|
return completion.choices[0]?.message?.content || '죄송합니다. 응답을 생성할 수 없습니다.';
|
||||||
|
}
|
||||||
@ -2,7 +2,8 @@ import type { Express } from "express";
|
|||||||
import { createServer, type Server } from "http";
|
import { createServer, type Server } from "http";
|
||||||
import { storage } from "./storage";
|
import { storage } from "./storage";
|
||||||
import { setupAuth, isAuthenticated } from "./simpleAuth";
|
import { setupAuth, isAuthenticated } from "./simpleAuth";
|
||||||
import { insertArticleSchema, insertMediaOutletRequestSchema, insertBidSchema, insertCommentSchema, insertPredictionBetSchema, insertCommunityPostSchema, insertCommunityReplySchema } from "@shared/schema";
|
import { insertArticleSchema, insertMediaOutletRequestSchema, insertBidSchema, insertCommentSchema, insertPredictionBetSchema, insertCommunityPostSchema, insertCommunityReplySchema, insertChatMessageSchema } from "@shared/schema";
|
||||||
|
import { generateChatbotResponse } from "./chatbot";
|
||||||
|
|
||||||
export async function registerRoutes(app: Express): Promise<Server> {
|
export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
// Auth middleware
|
// Auth middleware
|
||||||
@ -582,6 +583,77 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Chatbot routes
|
||||||
|
app.get('/api/media-outlets/:slug/chat', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const outlet = await storage.getMediaOutletBySlug(req.params.slug);
|
||||||
|
if (!outlet) {
|
||||||
|
return res.status(404).json({ message: "Media outlet not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = req.user.claims?.sub || req.user.id;
|
||||||
|
const messages = await storage.getChatMessages(outlet.id, userId);
|
||||||
|
res.json(messages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching chat messages:", error);
|
||||||
|
res.status(500).json({ message: "Failed to fetch chat messages" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/media-outlets/:slug/chat', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const outlet = await storage.getMediaOutletBySlug(req.params.slug);
|
||||||
|
if (!outlet) {
|
||||||
|
return res.status(404).json({ message: "Media outlet not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = req.user.claims?.sub || req.user.id;
|
||||||
|
const { content } = req.body;
|
||||||
|
|
||||||
|
if (!content || typeof content !== 'string') {
|
||||||
|
return res.status(400).json({ message: "Invalid message content" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user message
|
||||||
|
const userMessage = insertChatMessageSchema.parse({
|
||||||
|
mediaOutletId: outlet.id,
|
||||||
|
userId,
|
||||||
|
role: 'user',
|
||||||
|
content
|
||||||
|
});
|
||||||
|
await storage.createChatMessage(userMessage);
|
||||||
|
|
||||||
|
// Get chat history
|
||||||
|
const chatHistory = await storage.getChatMessages(outlet.id, userId);
|
||||||
|
const messages = chatHistory.map(msg => ({
|
||||||
|
role: msg.role as 'user' | 'assistant',
|
||||||
|
content: msg.content
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Generate AI response
|
||||||
|
const aiResponse = await generateChatbotResponse(
|
||||||
|
messages,
|
||||||
|
outlet.name,
|
||||||
|
outlet.category,
|
||||||
|
outlet.description || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save assistant message
|
||||||
|
const assistantMessage = insertChatMessageSchema.parse({
|
||||||
|
mediaOutletId: outlet.id,
|
||||||
|
userId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: aiResponse
|
||||||
|
});
|
||||||
|
const savedAssistantMessage = await storage.createChatMessage(assistantMessage);
|
||||||
|
|
||||||
|
res.status(201).json(savedAssistantMessage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing chat message:", error);
|
||||||
|
res.status(500).json({ message: "Failed to process chat message" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
return httpServer;
|
return httpServer;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
predictionBets,
|
predictionBets,
|
||||||
communityPosts,
|
communityPosts,
|
||||||
communityReplies,
|
communityReplies,
|
||||||
|
chatMessages,
|
||||||
type User,
|
type User,
|
||||||
type UpsertUser,
|
type UpsertUser,
|
||||||
type MediaOutlet,
|
type MediaOutlet,
|
||||||
@ -32,6 +33,8 @@ import {
|
|||||||
type InsertCommunityPost,
|
type InsertCommunityPost,
|
||||||
type CommunityReply,
|
type CommunityReply,
|
||||||
type InsertCommunityReply,
|
type InsertCommunityReply,
|
||||||
|
type ChatMessage,
|
||||||
|
type InsertChatMessage,
|
||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { eq, desc, asc, and, ilike, sql } from "drizzle-orm";
|
import { eq, desc, asc, and, ilike, sql } from "drizzle-orm";
|
||||||
@ -97,6 +100,10 @@ export interface IStorage {
|
|||||||
getRepliesByPost(postId: string): Promise<CommunityReply[]>;
|
getRepliesByPost(postId: string): Promise<CommunityReply[]>;
|
||||||
createCommunityReply(reply: InsertCommunityReply): Promise<CommunityReply>;
|
createCommunityReply(reply: InsertCommunityReply): Promise<CommunityReply>;
|
||||||
|
|
||||||
|
// Chatbot operations
|
||||||
|
getChatMessages(mediaOutletId: string, userId: string): Promise<ChatMessage[]>;
|
||||||
|
createChatMessage(message: InsertChatMessage): Promise<ChatMessage>;
|
||||||
|
|
||||||
// Analytics operations
|
// Analytics operations
|
||||||
getAnalytics(): Promise<{
|
getAnalytics(): Promise<{
|
||||||
totalArticles: number;
|
totalArticles: number;
|
||||||
@ -473,6 +480,20 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return newReply;
|
return newReply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chatbot operations
|
||||||
|
async getChatMessages(mediaOutletId: string, userId: string): Promise<ChatMessage[]> {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(chatMessages)
|
||||||
|
.where(and(eq(chatMessages.mediaOutletId, mediaOutletId), eq(chatMessages.userId, userId)))
|
||||||
|
.orderBy(asc(chatMessages.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async createChatMessage(message: InsertChatMessage): Promise<ChatMessage> {
|
||||||
|
const [newMessage] = await db.insert(chatMessages).values(message).returning();
|
||||||
|
return newMessage;
|
||||||
|
}
|
||||||
|
|
||||||
// Analytics operations
|
// Analytics operations
|
||||||
async getAnalytics(): Promise<{
|
async getAnalytics(): Promise<{
|
||||||
totalArticles: number;
|
totalArticles: number;
|
||||||
|
|||||||
@ -180,6 +180,16 @@ export const communityReplies = pgTable("community_replies", {
|
|||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Chatbot messages for media outlets
|
||||||
|
export const chatMessages = pgTable("chat_messages", {
|
||||||
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
|
mediaOutletId: varchar("media_outlet_id").notNull(),
|
||||||
|
userId: varchar("user_id").notNull(),
|
||||||
|
role: varchar("role").notNull(), // "user" or "assistant"
|
||||||
|
content: text("content").notNull(),
|
||||||
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
// Insert schemas
|
// Insert schemas
|
||||||
export const insertUserSchema = createInsertSchema(users).omit({
|
export const insertUserSchema = createInsertSchema(users).omit({
|
||||||
id: true,
|
id: true,
|
||||||
@ -245,6 +255,11 @@ export const insertCommunityReplySchema = createInsertSchema(communityReplies).o
|
|||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertChatMessageSchema = createInsertSchema(chatMessages).omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type UpsertUser = typeof users.$inferInsert;
|
export type UpsertUser = typeof users.$inferInsert;
|
||||||
export type User = typeof users.$inferSelect;
|
export type User = typeof users.$inferSelect;
|
||||||
@ -268,3 +283,5 @@ export type InsertCommunityPost = z.infer<typeof insertCommunityPostSchema>;
|
|||||||
export type CommunityPost = typeof communityPosts.$inferSelect;
|
export type CommunityPost = typeof communityPosts.$inferSelect;
|
||||||
export type InsertCommunityReply = z.infer<typeof insertCommunityReplySchema>;
|
export type InsertCommunityReply = z.infer<typeof insertCommunityReplySchema>;
|
||||||
export type CommunityReply = typeof communityReplies.$inferSelect;
|
export type CommunityReply = typeof communityReplies.$inferSelect;
|
||||||
|
export type InsertChatMessage = z.infer<typeof insertChatMessageSchema>;
|
||||||
|
export type ChatMessage = typeof chatMessages.$inferSelect;
|
||||||
|
|||||||
Reference in New Issue
Block a user