Files
Odoo-Modules/fusion_clock_ai/static/src/js/ai_chat_backend.js
gsinghpal e56974d46f update
2026-03-16 08:14:56 -04:00

159 lines
4.8 KiB
JavaScript

/** @odoo-module **/
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
import { Component, useState, useRef, onMounted, onWillUnmount } from "@odoo/owl";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { useDropdownState } from "@web/core/dropdown/dropdown_hooks";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { rpc } from "@web/core/network/rpc";
const SUGGESTIONS = [
"Who had the most overtime this week?",
"Show me late arrivals today",
"Absence summary this month",
"Which team is understaffed?",
];
const TOOLS = [
{ key: "anomaly", icon: "fa-exclamation-triangle", label: "Anomaly Scan" },
{ key: "understaffing", icon: "fa-users", label: "Staffing Forecast" },
{ key: "shift", icon: "fa-calendar", label: "Shift Optimizer" },
{ key: "compliance", icon: "fa-gavel", label: "Compliance Check" },
{ key: "geofence", icon: "fa-map-marker", label: "Geofence Tuning" },
];
export class FusionClockAIChat extends Component {
static props = {};
static template = "fusion_clock_ai.AISystray";
static components = { Dropdown, DropdownItem };
setup() {
this.notification = useService("notification");
this.chatBodyRef = useRef("chatBody");
this.dropdown = useDropdownState();
this.state = useState({
activeTab: "chat",
messages: [],
inputText: "",
loading: false,
conversationId: null,
toolLoading: null,
toolResult: null,
});
}
get suggestions() {
return SUGGESTIONS;
}
get tools() {
return TOOLS;
}
get hasMessages() {
return this.state.messages.length > 0;
}
get canSend() {
return this.state.inputText.trim().length > 0 && !this.state.loading;
}
switchTab(tab) {
this.state.activeTab = tab;
this.state.toolResult = null;
}
newConversation() {
this.state.messages = [];
this.state.conversationId = null;
this.state.inputText = "";
}
onInput(ev) {
this.state.inputText = ev.target.value;
}
onKeydown(ev) {
if (ev.key === "Enter" && !ev.shiftKey) {
ev.preventDefault();
this.sendMessage();
}
}
onSuggestionClick(text) {
this.state.inputText = text;
this.sendMessage();
}
_scrollToBottom() {
const el = this.chatBodyRef.el;
if (el) {
requestAnimationFrame(() => {
el.scrollTop = el.scrollHeight;
});
}
}
async sendMessage() {
const text = this.state.inputText.trim();
if (!text || this.state.loading) return;
this.state.messages.push({ role: "user", content: text });
this.state.inputText = "";
this.state.loading = true;
this._scrollToBottom();
try {
const result = await rpc("/fusion_clock_ai/manager_chat", {
message: text,
conversation_id: this.state.conversationId || undefined,
});
if (result.error) {
this.state.messages.push({ role: "assistant", content: result.error });
this.notification.add(result.error, { type: "warning" });
} else {
this.state.messages.push({ role: "assistant", content: result.response });
this.state.conversationId = result.conversation_id;
}
} catch (e) {
const errorMsg = "Failed to get AI response. Please try again.";
this.state.messages.push({ role: "assistant", content: errorMsg });
this.notification.add(errorMsg, { type: "danger" });
}
this.state.loading = false;
this._scrollToBottom();
}
async runTool(toolKey) {
if (this.state.toolLoading) return;
this.state.toolLoading = toolKey;
this.state.toolResult = null;
try {
const result = await rpc("/fusion_clock_ai/run_analysis", {
analysis_type: toolKey,
});
if (result.error) {
this.state.toolResult = { error: true, text: result.error };
this.notification.add(result.error, { type: "warning" });
} else {
this.state.toolResult = { error: false, text: result.response };
}
} catch (e) {
this.state.toolResult = { error: true, text: "Analysis failed. Please try again." };
this.notification.add("Analysis failed.", { type: "danger" });
}
this.state.toolLoading = null;
}
}
registry.category("systray").add("fusion_clock_ai.AISystray", {
Component: FusionClockAIChat,
}, { sequence: 100 });