/** @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 });