feat(chatbot): per-user history via X-User-Id + agentic tools calling

Backend:
- New chatbot.tools.ts: 4 tools (get_server_stats, get_top_channels,
  get_recent_activity, get_top_flagged) with real DB executors
- chatbot.service: agentic loop — stream:true, parse SSE, execute
  tool_calls, feed results back, up to 4 rounds
- controller: resolve userId from X-User-Id header (no-login device
  uuid) with auth middleware precedence; history/clear scoped per user

Frontend:
- use-chatbot-user: mint UUID in localStorage, send as X-User-Id
- chatbotApi.send/getHistory/clearHistory accept userId header
- client.ts: apiRequest supports custom headers per call
- provider: history load + send + clear keyed to device user id
This commit is contained in:
asepharyana
2026-08-03 06:24:19 +07:00
parent 7513681b4b
commit d1c1f3e4a7
7 changed files with 506 additions and 56 deletions
@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
const STORAGE_KEY = "gmw-chatbot-user-id";
/**
* Per-device anonymous identity. The app has no login, so we mint a random
* UUID on first visit, persist it to localStorage, and send it as the
* X-User-Id header. Each visitor gets their own chat history — the backend
* keys `chatbot_messages` by this id.
*/
export function useChatbotUserId(): string {
const [userId, setUserId] = useState<string>("");
useEffect(() => {
try {
let id = window.localStorage.getItem(STORAGE_KEY);
if (!id || id.length < 16) {
id =
typeof crypto !== "undefined" && crypto.randomUUID
? crypto.randomUUID()
: `u_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
window.localStorage.setItem(STORAGE_KEY, id);
}
setUserId(id);
} catch {
// localStorage unavailable (private mode) — use in-memory fallback
setUserId(
typeof crypto !== "undefined" && crypto.randomUUID
? crypto.randomUUID()
: `u_${Date.now().toString(36)}`,
);
}
}, []);
return userId;
}
export { STORAGE_KEY };