```
This commit is contained in:
@@ -10,6 +10,7 @@ import { createAnalyticsRouter } from "../modules/analytics/analytics.routes.js"
|
|||||||
import { createAuthRouter } from "../modules/auth/auth.routes.js";
|
import { createAuthRouter } from "../modules/auth/auth.routes.js";
|
||||||
import { createConfigRouter } from "../modules/config/config.routes.js";
|
import { createConfigRouter } from "../modules/config/config.routes.js";
|
||||||
import { createHealthRouter } from "../modules/health/health.routes.js";
|
import { createHealthRouter } from "../modules/health/health.routes.js";
|
||||||
|
import { createMascotChatRouter } from "../modules/mascot-chat/mascot-chat.routes.js";
|
||||||
import { createMediaRouter } from "../modules/media/media.routes.js";
|
import { createMediaRouter } from "../modules/media/media.routes.js";
|
||||||
import { createMessagesRouter } from "../modules/messages/messages.routes.js";
|
import { createMessagesRouter } from "../modules/messages/messages.routes.js";
|
||||||
import { createRecordingsRouter } from "../modules/recordings/recordings.routes.js";
|
import { createRecordingsRouter } from "../modules/recordings/recordings.routes.js";
|
||||||
@@ -66,6 +67,7 @@ export function createHttpApp(): Express {
|
|||||||
app.use("/api", createMessagesRouter());
|
app.use("/api", createMessagesRouter());
|
||||||
app.use("/api", createAnalysisRouter());
|
app.use("/api", createAnalysisRouter());
|
||||||
app.use("/api", createAnalyticsRouter());
|
app.use("/api", createAnalyticsRouter());
|
||||||
|
app.use("/api", createMascotChatRouter());
|
||||||
app.use("/api", createMediaRouter());
|
app.use("/api", createMediaRouter());
|
||||||
app.use("/api", createVoiceRouter());
|
app.use("/api", createVoiceRouter());
|
||||||
app.use("/api", createRecordingsRouter());
|
app.use("/api", createRecordingsRouter());
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
|
import { mascotChatService } from "./mascot-chat.service.js";
|
||||||
|
|
||||||
|
const logger = createChildLogger("mascot-chat.controller");
|
||||||
|
|
||||||
|
export async function handleMascotChat(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { message, context } = req.body;
|
||||||
|
|
||||||
|
if (!message || typeof message !== "string") {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "INVALID_INPUT",
|
||||||
|
message: "Message is required and must be a string",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user ID from auth middleware (if available)
|
||||||
|
const userId = (req as any).userId || "anonymous";
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
{ userId, messageLength: message.length, context },
|
||||||
|
"Received mascot chat message"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process message & generate response
|
||||||
|
const response = await mascotChatService.processMessage(message, context, userId);
|
||||||
|
|
||||||
|
// Save conversation to database
|
||||||
|
await mascotChatService.saveConversation({
|
||||||
|
userId,
|
||||||
|
userMessage: message,
|
||||||
|
mascotResponse: response,
|
||||||
|
context,
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info({ userId }, "Mascot chat processed successfully");
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
response,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, "Error processing mascot chat");
|
||||||
|
res.status(500).json({
|
||||||
|
error: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to process mascot chat",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMascotChatHistory(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = (req as any).userId || "anonymous";
|
||||||
|
const limit = Math.min(parseInt(req.query.limit as string) || 50, 100);
|
||||||
|
|
||||||
|
const history = await mascotChatService.getChatHistory(userId, limit);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
history,
|
||||||
|
total: history.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, "Error fetching chat history");
|
||||||
|
res.status(500).json({
|
||||||
|
error: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to fetch chat history",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearMascotChatHistory(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = (req as any).userId || "anonymous";
|
||||||
|
|
||||||
|
await mascotChatService.clearChatHistory(userId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: "Chat history cleared successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, "Error clearing chat history");
|
||||||
|
res.status(500).json({
|
||||||
|
error: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to clear chat history",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import express, { type Router } from "express";
|
||||||
|
import { asyncHandler } from "../../shared/middlewares/index.js";
|
||||||
|
import {
|
||||||
|
clearMascotChatHistory,
|
||||||
|
getMascotChatHistory,
|
||||||
|
handleMascotChat,
|
||||||
|
} from "./mascot-chat.controller.js";
|
||||||
|
|
||||||
|
export function createMascotChatRouter(): Router {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/mascot/chat", asyncHandler(handleMascotChat));
|
||||||
|
router.get("/mascot/chat/history", asyncHandler(getMascotChatHistory));
|
||||||
|
router.delete("/mascot/chat/history", asyncHandler(clearMascotChatHistory));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
|
import { getPool } from "../../shared/database/index.js";
|
||||||
|
|
||||||
|
const logger = createChildLogger("mascot-chat.service");
|
||||||
|
|
||||||
|
export interface MascotChatContext {
|
||||||
|
messageCount?: number;
|
||||||
|
activeParticipants?: number;
|
||||||
|
lastActivity?: string;
|
||||||
|
topicsDiscussed?: string[];
|
||||||
|
guildId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveConversationInput {
|
||||||
|
userId: string;
|
||||||
|
userMessage: string;
|
||||||
|
mascotResponse: string;
|
||||||
|
context?: MascotChatContext;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MascotChatHistoryRow {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
user_message: string;
|
||||||
|
mascot_response: string;
|
||||||
|
context: MascotChatContext | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MascotChatService {
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
async processMessage(
|
||||||
|
message: string,
|
||||||
|
context: MascotChatContext | undefined,
|
||||||
|
userId: string,
|
||||||
|
): Promise<string> {
|
||||||
|
await this.ensureSchema();
|
||||||
|
|
||||||
|
const recentContext = await this.getRecentConversationContext(userId);
|
||||||
|
const serverInsights = await this.getServerInsights(context);
|
||||||
|
return this.generateResponse(message, context, serverInsights, recentContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveConversation(input: SaveConversationInput): Promise<void> {
|
||||||
|
await this.ensureSchema();
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
`
|
||||||
|
INSERT INTO mascot_chat_messages
|
||||||
|
(user_id, user_message, mascot_response, context, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4::jsonb, $5)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
input.userId,
|
||||||
|
input.userMessage,
|
||||||
|
input.mascotResponse,
|
||||||
|
JSON.stringify(input.context ?? {}),
|
||||||
|
input.timestamp.toISOString(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChatHistory(
|
||||||
|
userId: string,
|
||||||
|
limit: number,
|
||||||
|
): Promise<MascotChatHistoryRow[]> {
|
||||||
|
await this.ensureSchema();
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
const { rows } = await pool.query<MascotChatHistoryRow>(
|
||||||
|
`
|
||||||
|
SELECT id, user_id, user_message, mascot_response, context, created_at
|
||||||
|
FROM mascot_chat_messages
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $2
|
||||||
|
`,
|
||||||
|
[userId, limit],
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearChatHistory(userId: string): Promise<void> {
|
||||||
|
await this.ensureSchema();
|
||||||
|
const pool = getPool();
|
||||||
|
await pool.query(`DELETE FROM mascot_chat_messages WHERE user_id = $1`, [
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureSchema(): Promise<void> {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
const pool = getPool();
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS mascot_chat_messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
user_message TEXT NOT NULL,
|
||||||
|
mascot_response TEXT NOT NULL,
|
||||||
|
context JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await pool.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mascot_chat_messages_user_created
|
||||||
|
ON mascot_chat_messages (user_id, created_at DESC)
|
||||||
|
`);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
logger.info("Mascot chat schema ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRecentConversationContext(userId: string): Promise<string[]> {
|
||||||
|
const history = await this.getChatHistory(userId, 3);
|
||||||
|
return history.flatMap((row) => [
|
||||||
|
`User: ${row.user_message}`,
|
||||||
|
`Mascot: ${row.mascot_response}`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getServerInsights(context?: MascotChatContext) {
|
||||||
|
const pool = getPool();
|
||||||
|
const guildId = context?.guildId;
|
||||||
|
const channelId = context?.channelId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: string[] = [];
|
||||||
|
const clauses: string[] = [];
|
||||||
|
if (guildId) {
|
||||||
|
params.push(guildId);
|
||||||
|
clauses.push(`guild_id = $${params.length}`);
|
||||||
|
}
|
||||||
|
if (channelId) {
|
||||||
|
params.push(channelId);
|
||||||
|
clauses.push(`channel_id = $${params.length}`);
|
||||||
|
}
|
||||||
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
||||||
|
|
||||||
|
const { rows } = await pool.query<{
|
||||||
|
total_messages: number;
|
||||||
|
active_users: number;
|
||||||
|
flagged: number;
|
||||||
|
warned: number;
|
||||||
|
}>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
COUNT(*)::int AS total_messages,
|
||||||
|
COUNT(DISTINCT user_id)::int AS active_users,
|
||||||
|
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged,
|
||||||
|
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warned
|
||||||
|
FROM messages
|
||||||
|
${where}
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows[0] ?? {
|
||||||
|
total_messages: 0,
|
||||||
|
active_users: 0,
|
||||||
|
flagged: 0,
|
||||||
|
warned: 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn({ error }, "Failed to load mascot server insights");
|
||||||
|
return {
|
||||||
|
total_messages: context?.messageCount ?? 0,
|
||||||
|
active_users: context?.activeParticipants ?? 0,
|
||||||
|
flagged: 0,
|
||||||
|
warned: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateResponse(
|
||||||
|
input: string,
|
||||||
|
context: MascotChatContext | undefined,
|
||||||
|
insights: {
|
||||||
|
total_messages: number;
|
||||||
|
active_users: number;
|
||||||
|
flagged: number;
|
||||||
|
warned: number;
|
||||||
|
},
|
||||||
|
recentContext: string[],
|
||||||
|
): string {
|
||||||
|
const lower = input.toLowerCase();
|
||||||
|
const messageCount = insights.total_messages || context?.messageCount || 0;
|
||||||
|
const activeUsers = insights.active_users || context?.activeParticipants || 0;
|
||||||
|
|
||||||
|
if (lower.includes("ringkasan") || lower.includes("summary")) {
|
||||||
|
return `Aku rangkum ya ✨ Ada ${messageCount} pesan dari ${activeUsers} user aktif. Moderasi menemukan ${insights.flagged} flagged dan ${insights.warned} warning. Kesimpulannya: obrolan sedang ${messageCount > 50 ? "ramai" : "cukup tenang"}, dan aku sarankan fokus ke pesan yang punya status warn/flagged dulu.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.includes("berapa") || lower.includes("jumlah")) {
|
||||||
|
if (lower.includes("pesan")) {
|
||||||
|
return `Ada ${messageCount} pesan yang tercatat di konteks ini 📊`;
|
||||||
|
}
|
||||||
|
if (lower.includes("orang") || lower.includes("user")) {
|
||||||
|
return `Ada ${activeUsers} user aktif yang ikut dalam obrolan ini 👥`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.includes("flag") || lower.includes("bahaya") || lower.includes("moderasi")) {
|
||||||
|
return `Status moderasi: ${insights.flagged} pesan flagged dan ${insights.warned} pesan warning. Kalau mau aman, mulai review dari daftar flagged karena itu prioritas tertinggi 🚨`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.includes("saran") || lower.includes("apa yang harus")) {
|
||||||
|
return `Saran mascot: 1) review pesan flagged, 2) cek user paling aktif di Analytics, 3) pantau channel dengan traffic tinggi, 4) kalau obrolan mulai panas, lakukan follow-up manual sebelum eskalasi 🔎`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.includes("halo") || lower.includes("hai") || lower.includes("hi")) {
|
||||||
|
return `Halo! Aku siap bantu baca situasi chat. Kamu bisa tanya "ringkasan obrolan", "berapa pesan", atau "ada yang perlu dimoderasi?" 😊`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousHint = recentContext.length
|
||||||
|
? ` Aku juga mengingat konteks chat mascot sebelumnya (${Math.ceil(recentContext.length / 2)} percakapan terakhir).`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `Menurutku, pertanyaan "${input}" berkaitan dengan kondisi obrolan saat ini. Data cepat: ${messageCount} pesan, ${activeUsers} user aktif, ${insights.flagged} flagged.${previousHint} Coba tanya lebih spesifik seperti "ringkasan", "moderasi", atau "saran" ya.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mascotChatService = new MascotChatService();
|
||||||
@@ -17,13 +17,11 @@ import {
|
|||||||
} from "./shared/api/client";
|
} from "./shared/api/client";
|
||||||
import { useAudioPlayback } from "./shared/hooks/useAudioPlayback";
|
import { useAudioPlayback } from "./shared/hooks/useAudioPlayback";
|
||||||
import { useAudioTransmit } from "./shared/hooks/useAudioTransmit";
|
import { useAudioTransmit } from "./shared/hooks/useAudioTransmit";
|
||||||
import { useMascotChat } from "./shared/hooks/useMascotChat";
|
|
||||||
import { useUIState } from "./shared/hooks/useUIState";
|
import { useUIState } from "./shared/hooks/useUIState";
|
||||||
import { Skeleton } from "./shared/ui";
|
import { Skeleton } from "./shared/ui";
|
||||||
import { MobileTabBar } from "./shared/ui/MobileTabBar";
|
import { MobileTabBar } from "./shared/ui/MobileTabBar";
|
||||||
import { useDashboardSocket } from "./shared/ws/socket";
|
import { useDashboardSocket } from "./shared/ws/socket";
|
||||||
import { DashboardLayout } from "./widgets/DashboardLayout";
|
import { DashboardLayout } from "./widgets/DashboardLayout";
|
||||||
import { MascotChatbot } from "./widgets/mascot/MascotChatbot";
|
|
||||||
|
|
||||||
const AnalyticsPanel = lazy(() =>
|
const AnalyticsPanel = lazy(() =>
|
||||||
import("./features/analytics").then((module) => ({
|
import("./features/analytics").then((module) => ({
|
||||||
@@ -62,15 +60,6 @@ export default function App() {
|
|||||||
!!localStorage.getItem("admin-password"),
|
!!localStorage.getItem("admin-password"),
|
||||||
);
|
);
|
||||||
const [monitorGuildId, setMonitorGuildId] = useState("");
|
const [monitorGuildId, setMonitorGuildId] = useState("");
|
||||||
const [isMascotChatOpen, setIsMascotChatOpen] = useState(false);
|
|
||||||
|
|
||||||
// Mascot chat hook with message context
|
|
||||||
const mascotChat = useMascotChat({
|
|
||||||
messageCount: messages.messages.length,
|
|
||||||
activeParticipants: new Set(messages.messages.map((m) => m.user_id)).size,
|
|
||||||
lastActivity: messages.messages.length > 0 ? "Active" : "Idle",
|
|
||||||
topicsDiscussed: ["Analytics", "Conversation", "Insights"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const audio = useAudioPlayback();
|
const audio = useAudioPlayback();
|
||||||
const activeTab = uiState.activeTab || "live";
|
const activeTab = uiState.activeTab || "live";
|
||||||
@@ -174,6 +163,8 @@ export default function App() {
|
|||||||
voiceStatus={voice.voiceStatus}
|
voiceStatus={voice.voiceStatus}
|
||||||
onTabChange={(tab) => patchUIState({ activeTab: tab })}
|
onTabChange={(tab) => patchUIState({ activeTab: tab })}
|
||||||
recentMessages={messages.messages}
|
recentMessages={messages.messages}
|
||||||
|
guildId={monitorGuildId}
|
||||||
|
channelId={uiState.selectedTextChannel || uiState.selectedVoiceChannel || undefined}
|
||||||
>
|
>
|
||||||
{activeTab === "live" ? (
|
{activeTab === "live" ? (
|
||||||
!isAuthenticated ? (
|
!isAuthenticated ? (
|
||||||
@@ -246,13 +237,6 @@ export default function App() {
|
|||||||
onTabChange={(tab) => patchUIState({ activeTab: tab })}
|
onTabChange={(tab) => patchUIState({ activeTab: tab })}
|
||||||
/>
|
/>
|
||||||
<ModerationAlertListener />
|
<ModerationAlertListener />
|
||||||
<MascotChatbot
|
|
||||||
isOpen={isMascotChatOpen}
|
|
||||||
onSetIsOpen={setIsMascotChatOpen}
|
|
||||||
onSendMessage={mascotChat.handleSendMessage}
|
|
||||||
mascotName="Discord Watcher"
|
|
||||||
mascotAvatar="https://raw.githubusercontent.com/IMPHNEN/imphnen-frontend-service/develop/apps/dimentorin/public/image/mascot-1.png"
|
|
||||||
/>
|
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
/**
|
export interface ChatContext {
|
||||||
* useMascotChat — Hook untuk handle mascot chatbot responses
|
|
||||||
* Dapat di-extend dengan Discord Gateway atau API backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface ChatContext {
|
|
||||||
messageCount: number;
|
messageCount: number;
|
||||||
activeParticipants: number;
|
activeParticipants: number;
|
||||||
lastActivity: string;
|
lastActivity: string;
|
||||||
topicsDiscussed: string[];
|
topicsDiscussed: string[];
|
||||||
|
guildId?: string;
|
||||||
|
channelId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMascotChat(context?: ChatContext) {
|
export function useMascotChat(context?: ChatContext) {
|
||||||
@@ -17,14 +14,25 @@ export function useMascotChat(context?: ChatContext) {
|
|||||||
|
|
||||||
const handleSendMessage = useCallback(
|
const handleSendMessage = useCallback(
|
||||||
async (message: string): Promise<string> => {
|
async (message: string): Promise<string> => {
|
||||||
// Simulate API call delay
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
const response = await fetch("/api/mascot/chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ message, context }),
|
||||||
|
});
|
||||||
|
|
||||||
// For now, generate response based on keywords
|
if (!response.ok) {
|
||||||
// Later dapat di-replace dengan actual AI backend atau Discord integration
|
throw new Error(`Mascot backend responded with ${response.status}`);
|
||||||
return generateIntelligentResponse(message, context);
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as { response?: string };
|
||||||
|
return data.response || fallbackResponse(message, context);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Mascot backend unavailable, using fallback", error);
|
||||||
|
return fallbackResponse(message, context);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[context]
|
[context],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -34,120 +42,20 @@ export function useMascotChat(context?: ChatContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function fallbackResponse(input: string, context?: ChatContext): string {
|
||||||
* Intelligent response generator
|
|
||||||
* Can be extended to call backend API, Discord Gateway, atau AI service
|
|
||||||
*/
|
|
||||||
function generateIntelligentResponse(
|
|
||||||
input: string,
|
|
||||||
context?: ChatContext
|
|
||||||
): string {
|
|
||||||
const lower = input.toLowerCase();
|
const lower = input.toLowerCase();
|
||||||
|
|
||||||
// Analytics-related questions
|
if (lower.includes("ringkasan") || lower.includes("summary")) {
|
||||||
if (
|
return `Aku rangkum cepat ya ✨ Ada ${context?.messageCount || 0} pesan dari ${context?.activeParticipants || 0} user aktif. Backend belum bisa dihubungi, jadi ini ringkasan lokal sementara.`;
|
||||||
lower.includes("berapa") ||
|
|
||||||
lower.includes("jumlah") ||
|
|
||||||
lower.includes("total")
|
|
||||||
) {
|
|
||||||
if (lower.includes("pesan")) {
|
|
||||||
return `📊 Ada ${context?.messageCount || 0} pesan dalam conversation. Cukup aktif ya! Mau tahu siapa yang paling banyak chat?`;
|
|
||||||
}
|
|
||||||
if (lower.includes("orang") || lower.includes("partisipan")) {
|
|
||||||
return `👥 Ada ${context?.activeParticipants || 0} orang yang aktif chat. Mereka bekerja sama dengan baik!`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insights-related questions
|
if (lower.includes("berapa") && lower.includes("pesan")) {
|
||||||
if (
|
return `Ada ${context?.messageCount || 0} pesan di konteks dashboard saat ini 📊`;
|
||||||
lower.includes("insight") ||
|
|
||||||
lower.includes("ringkasan") ||
|
|
||||||
lower.includes("summary")
|
|
||||||
) {
|
|
||||||
return `📈 Dari yang aku lihat:
|
|
||||||
• Activity Level: ${context?.lastActivity || "Tinggi"}
|
|
||||||
• Top Topics: ${context?.topicsDiscussed?.join(", ") || "General discussion"}
|
|
||||||
• Engagement: Very Good! 🎯`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recommendations
|
if (lower.includes("berapa") && (lower.includes("orang") || lower.includes("user"))) {
|
||||||
if (lower.includes("saran") || lower.includes("rekomendasi")) {
|
return `Ada ${context?.activeParticipants || 0} user aktif yang terdeteksi 👥`;
|
||||||
return `💡 Rekomendasi aku:
|
|
||||||
1. Tingkatkan engagement dengan more interactive discussions
|
|
||||||
2. Dokumentasikan insights untuk future reference
|
|
||||||
3. Libatkan semua partisipan dalam decision making
|
|
||||||
4. Monitor trends untuk continuous improvement
|
|
||||||
|
|
||||||
Bagus banget perkembangannya! 🚀`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help/Info
|
return `Aku belum bisa menghubungi backend, tapi dari konteks lokal ada ${context?.messageCount || 0} pesan dan ${context?.activeParticipants || 0} user aktif. Coba tanya "ringkasan obrolan" atau "berapa pesan" ya.`;
|
||||||
if (
|
|
||||||
lower.includes("bantuan") ||
|
|
||||||
lower.includes("apa aja") ||
|
|
||||||
lower.includes("bisa")
|
|
||||||
) {
|
|
||||||
return `🤖 Aku bisa membantu dengan:
|
|
||||||
• Analytics & Insights
|
|
||||||
• Conversation Summaries
|
|
||||||
• Participant Analysis
|
|
||||||
• Trend Detection
|
|
||||||
• Recommendations
|
|
||||||
• General Q&A
|
|
||||||
|
|
||||||
Tanya aja yang pengen kamu tahu! 😊`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Greeting
|
|
||||||
if (
|
|
||||||
lower.includes("halo") ||
|
|
||||||
lower.includes("hi") ||
|
|
||||||
lower.includes("hey") ||
|
|
||||||
lower.includes("pagi")
|
|
||||||
) {
|
|
||||||
return `Halo! 👋 Apa kabar? Ada yang bisa aku bantu tentang conversation ini?`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default intelligent response
|
|
||||||
return `Interessant! "${input}" - itu observation yang valid. Dari analytics, ini berhubungan dengan conversation patterns yang kami track. Ada follow-up question? 🎯`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Backend integration hook
|
|
||||||
* Uncomment dan modify untuk integrate dengan actual backend/Discord Gateway
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
export async function callMascotAIBackend(message: string, context?: ChatContext): Promise<string> {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/mascot/chat', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ message, context }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Backend error');
|
|
||||||
const data = await response.json();
|
|
||||||
return data.response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error calling mascot backend:', error);
|
|
||||||
return generateIntelligentResponse(message, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function callDiscordGateway(message: string, guildId: string): Promise<string> {
|
|
||||||
// Call Discord Gateway untuk mendapat context lebih kaya
|
|
||||||
// Implementasi akan bergantung pada Discord API integration
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/discord/guild-context', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ guildId, query: message }),
|
|
||||||
});
|
|
||||||
const context = await response.json();
|
|
||||||
return generateIntelligentResponse(message, context);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error calling Discord Gateway:', error);
|
|
||||||
return generateIntelligentResponse(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useMemo } from "react";
|
|
||||||
import type { DashboardTab } from "../entities/ui/types";
|
import type { DashboardTab } from "../entities/ui/types";
|
||||||
import type { VoiceStatus } from "../shared/api/client";
|
import type { MessageRecord, VoiceStatus } from "../shared/api/client";
|
||||||
import { fadeSlideUp } from "../shared/hooks/useFramerStagger";
|
import { fadeSlideUp } from "../shared/hooks/useFramerStagger";
|
||||||
import { useMascotSummary } from "../shared/hooks/useMascotSummary";
|
import { useMascotSummary } from "../shared/hooks/useMascotSummary";
|
||||||
import type { WsStatus } from "../shared/ws/socket";
|
import type { WsStatus } from "../shared/ws/socket";
|
||||||
@@ -16,7 +15,9 @@ interface DashboardLayoutProps {
|
|||||||
voiceStatus: VoiceStatus;
|
voiceStatus: VoiceStatus;
|
||||||
onTabChange: (tab: DashboardTab) => void;
|
onTabChange: (tab: DashboardTab) => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
recentMessages?: any[];
|
recentMessages?: MessageRecord[];
|
||||||
|
guildId?: string;
|
||||||
|
channelId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardLayout({
|
export function DashboardLayout({
|
||||||
@@ -26,6 +27,8 @@ export function DashboardLayout({
|
|||||||
onTabChange,
|
onTabChange,
|
||||||
children,
|
children,
|
||||||
recentMessages = [],
|
recentMessages = [],
|
||||||
|
guildId,
|
||||||
|
channelId,
|
||||||
}: DashboardLayoutProps) {
|
}: DashboardLayoutProps) {
|
||||||
// Generate mascot summary from recent messages
|
// Generate mascot summary from recent messages
|
||||||
const mascotSummary = useMascotSummary({
|
const mascotSummary = useMascotSummary({
|
||||||
@@ -42,6 +45,9 @@ export function DashboardLayout({
|
|||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
mascotChatMessage={mascotSummary}
|
mascotChatMessage={mascotSummary}
|
||||||
|
recentMessages={recentMessages}
|
||||||
|
guildId={guildId}
|
||||||
|
channelId={channelId}
|
||||||
/>
|
/>
|
||||||
<main className="flex min-w-0 flex-1 flex-col">
|
<main className="flex min-w-0 flex-1 flex-col">
|
||||||
<Header
|
<Header
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { motion } from "framer-motion";
|
|||||||
import { BarChart3, MessageSquare, Radio } from "lucide-react";
|
import { BarChart3, MessageSquare, Radio } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { DashboardTab } from "../entities/ui/types";
|
import type { DashboardTab } from "../entities/ui/types";
|
||||||
|
import type { MessageRecord } from "../shared/api/client";
|
||||||
|
import { useMascotChat } from "../shared/hooks/useMascotChat";
|
||||||
import { cn } from "../shared/lib/utils";
|
import { cn } from "../shared/lib/utils";
|
||||||
|
import { MascotChatbot } from "./mascot/MascotChatbot";
|
||||||
import { MascotImage } from "./mascot/MascotImage";
|
import { MascotImage } from "./mascot/MascotImage";
|
||||||
|
|
||||||
const navItems: Array<{ id: DashboardTab; label: string; icon: typeof Radio }> =
|
const navItems: Array<{ id: DashboardTab; label: string; icon: typeof Radio }> =
|
||||||
@@ -17,6 +20,9 @@ interface SidebarProps {
|
|||||||
onTabChange: (tab: DashboardTab) => void;
|
onTabChange: (tab: DashboardTab) => void;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
mascotChatMessage?: string;
|
mascotChatMessage?: string;
|
||||||
|
recentMessages?: MessageRecord[];
|
||||||
|
guildId?: string;
|
||||||
|
channelId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
@@ -24,8 +30,19 @@ export function Sidebar({
|
|||||||
onTabChange,
|
onTabChange,
|
||||||
collapsed = true,
|
collapsed = true,
|
||||||
mascotChatMessage = "",
|
mascotChatMessage = "",
|
||||||
|
recentMessages = [],
|
||||||
|
guildId,
|
||||||
|
channelId,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const [showChat, setShowChat] = useState(false);
|
const [showChat, setShowChat] = useState(false);
|
||||||
|
const mascotChat = useMascotChat({
|
||||||
|
messageCount: recentMessages.length,
|
||||||
|
activeParticipants: new Set(recentMessages.map((message) => message.user_id)).size,
|
||||||
|
lastActivity: recentMessages.length > 0 ? "Active" : "Idle",
|
||||||
|
topicsDiscussed: ["Messages", "Moderation", "Analytics"],
|
||||||
|
guildId,
|
||||||
|
channelId,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mascotChatMessage) {
|
if (mascotChatMessage) {
|
||||||
@@ -35,7 +52,7 @@ export function Sidebar({
|
|||||||
return (
|
return (
|
||||||
<motion.nav
|
<motion.nav
|
||||||
className={cn(
|
className={cn(
|
||||||
"hidden shrink-0 flex-col border-r border-[#7EC8E3]/20 bg-white/70 backdrop-blur-md transition-all duration-300 md:flex",
|
"relative hidden shrink-0 flex-col overflow-visible border-r border-[#7EC8E3]/20 bg-white/70 backdrop-blur-md transition-all duration-300 md:flex",
|
||||||
collapsed ? "w-16" : "w-64",
|
collapsed ? "w-16" : "w-64",
|
||||||
)}
|
)}
|
||||||
layout
|
layout
|
||||||
@@ -94,13 +111,27 @@ export function Sidebar({
|
|||||||
{/* Spacer pushes mascot to bottom */}
|
{/* Spacer pushes mascot to bottom */}
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
{/* Mascot PNG with chat bubble */}
|
{/* Mascot PNG with anchored chatbot */}
|
||||||
<div className="flex justify-center pb-4">
|
<div className="relative flex justify-center pb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => mascotChat.setIsOpen(!mascotChat.isOpen)}
|
||||||
|
className="rounded-2xl p-1 transition-transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-primary/40"
|
||||||
|
title="Chat dengan mascot"
|
||||||
|
>
|
||||||
<MascotImage
|
<MascotImage
|
||||||
size="sm"
|
size="sm"
|
||||||
showChat={showChat && !collapsed}
|
showChat={showChat && !mascotChat.isOpen}
|
||||||
chatMessage={mascotChatMessage}
|
chatMessage={mascotChatMessage}
|
||||||
/>
|
/>
|
||||||
|
</button>
|
||||||
|
<MascotChatbot
|
||||||
|
isOpen={mascotChat.isOpen}
|
||||||
|
onClose={() => mascotChat.setIsOpen(false)}
|
||||||
|
onSendMessage={mascotChat.handleSendMessage}
|
||||||
|
mascotName="Discord Watcher"
|
||||||
|
className="absolute bottom-16 left-12 z-50"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</motion.nav>
|
</motion.nav>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { Send, X, MessageCircle, Minimize2, Maximize2 } from "lucide-react";
|
import { Maximize2, MessageCircle, Minimize2, Send, X } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { cn } from "../../shared/lib/utils";
|
import { cn } from "../../shared/lib/utils";
|
||||||
|
|
||||||
@@ -12,23 +12,21 @@ export interface ChatMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MascotChatbotProps {
|
interface MascotChatbotProps {
|
||||||
onOpen?: () => void;
|
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
onSetIsOpen?: (isOpen: boolean) => void;
|
|
||||||
onSendMessage?: (message: string) => Promise<string>;
|
onSendMessage?: (message: string) => Promise<string>;
|
||||||
mascotName?: string;
|
mascotName?: string;
|
||||||
mascotAvatar?: string;
|
mascotAvatar?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MascotChatbot({
|
export function MascotChatbot({
|
||||||
onOpen,
|
|
||||||
onClose,
|
onClose,
|
||||||
isOpen = false,
|
isOpen = false,
|
||||||
onSetIsOpen,
|
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
mascotName = "Mascot",
|
mascotName = "Mascot",
|
||||||
mascotAvatar = "https://raw.githubusercontent.com/IMPHNEN/imphnen-frontend-service/develop/apps/dimentorin/public/image/mascot-1.png",
|
mascotAvatar = "https://raw.githubusercontent.com/IMPHNEN/imphnen-frontend-service/develop/apps/dimentorin/public/image/mascot-1.png",
|
||||||
|
className,
|
||||||
}: MascotChatbotProps) {
|
}: MascotChatbotProps) {
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||||
{
|
{
|
||||||
@@ -51,7 +49,7 @@ export function MascotChatbot({
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
const handleSendMessage = async (e: React.FormEvent) => {
|
const handleSendMessage = async (e: { preventDefault: () => void }) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!input.trim() || loading) return;
|
if (!input.trim() || loading) return;
|
||||||
|
|
||||||
@@ -98,22 +96,7 @@ export function MascotChatbot({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isOpen) {
|
if (!isOpen) return null;
|
||||||
return (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
onClick={() => {
|
|
||||||
onSetIsOpen?.(true);
|
|
||||||
onOpen?.();
|
|
||||||
}}
|
|
||||||
className="fixed bottom-6 right-6 bg-gradient-to-br from-primary to-primary/80 text-white rounded-full p-4 shadow-lg hover:shadow-xl transition-all"
|
|
||||||
title="Buka chat mascot"
|
|
||||||
>
|
|
||||||
<MessageCircle className="h-6 w-6" />
|
|
||||||
</motion.button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -122,8 +105,9 @@ export function MascotChatbot({
|
|||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed bottom-6 right-6 w-96 bg-white rounded-2xl shadow-2xl border border-primary/10 overflow-hidden flex flex-col",
|
"w-96 bg-white rounded-2xl shadow-2xl border border-primary/10 overflow-hidden flex flex-col",
|
||||||
isMinimized ? "h-16" : "h-[600px]"
|
isMinimized ? "h-16" : "h-[520px]",
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
Reference in New Issue
Block a user