Company:
+Generated:
+| Domain | +Metric | +Status | +
|---|---|---|
| Bank Reconciliation | +
+ |
+ |
| Accounts Receivable | +$ |
+
+ |
+
| Accounts Payable | +$ |
+ Info | +
| HST Balance | +$ |
+ |
| Audit Score | +
+ |
+ |
| Month-End Status | +
+ |
+
+ Amount: $
| ${inlineFormat(c)} | `)); + output.push("
|---|
| ${inlineFormat(c)} | `)); + output.push("
${inlineFormat(trimmed)}
`); + } + + // Close open elements + if (inTable) output.push(""); + if (inList) output.push(listType === "ul" ? "" : "$1')
+ // Links [text](url)
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
+ // Odoo record links #model,id
+ .replace(/#([\w.]+),(\d+)/g, '$1#$2');
+}
+
+
+export class FusionChatPanel extends Component {
+ static template = "fusion_accounting.ChatPanel";
+ static components = { FusionApprovalCard };
+ static props = ["sessionId?"];
+
+ setup() {
+ this.inputRef = useRef("chatInput");
+ this.messagesRef = useRef("messages");
+ this.state = useState({
+ messages: [],
+ pendingApprovals: [],
+ inputText: "",
+ sending: false,
+ loading: true,
+ internalSessionId: null,
+ sessionName: null,
+ });
+
+ onWillStart(async () => {
+ await this.loadLatestSession();
+ });
+
+ onMounted(() => {
+ this._renderRichMessages();
+ });
+
+ onPatched(() => {
+ this._renderRichMessages();
+ });
+ }
+
+ _renderRichMessages() {
+ const container = this.messagesRef.el;
+ if (!container) return;
+ const richDivs = container.querySelectorAll(".fusion_rich_slot[data-idx]");
+ for (const div of richDivs) {
+ const idx = parseInt(div.dataset.idx);
+ const msg = this.state.messages[idx];
+ if (msg && msg.role === "assistant" && msg.content) {
+ const html = mdToHtml(msg.content);
+ if (div.innerHTML !== html) {
+ div.innerHTML = html;
+ }
+ }
+ }
+ }
+
+ get sessionId() {
+ return this.state.internalSessionId || this.props.sessionId;
+ }
+
+ async loadLatestSession() {
+ this.state.loading = true;
+ try {
+ const data = await rpc("/fusion_accounting/session/latest", {});
+ if (data.session_id) {
+ this.state.internalSessionId = data.session_id;
+ this.state.messages = data.messages || [];
+ this.state.sessionName = data.name;
+ }
+ } catch (e) {
+ console.error("Failed to load session:", e);
+ }
+ this.state.loading = false;
+ this.scrollToBottom();
+ }
+
+ async onNewChat() {
+ if (this.sessionId) {
+ try {
+ await rpc("/fusion_accounting/session/close", { session_id: this.sessionId });
+ } catch (e) { /* not critical */ }
+ }
+ const session = await rpc("/fusion_accounting/session/create", {});
+ this.state.internalSessionId = session.session_id;
+ this.state.sessionName = session.name;
+ this.state.messages = [];
+ this.state.pendingApprovals = [];
+ }
+
+ async sendMessage() {
+ const text = this.state.inputText.trim();
+ if (!text || this.state.sending) return;
+
+ if (!this.sessionId) {
+ const session = await rpc("/fusion_accounting/session/create", {});
+ this.state.internalSessionId = session.session_id;
+ this.state.sessionName = session.name;
+ }
+
+ this.state.messages.push({ role: "user", content: text });
+ this.state.inputText = "";
+ this.state.sending = true;
+ this.scrollToBottom();
+
+ try {
+ const result = await rpc("/fusion_accounting/chat", {
+ session_id: this.sessionId,
+ message: text,
+ });
+ if (result.text) {
+ this.state.messages.push({ role: "assistant", content: result.text });
+ }
+ if (result.pending_approvals) {
+ this.state.pendingApprovals = result.pending_approvals;
+ }
+ } catch (e) {
+ this.state.messages.push({
+ role: "assistant",
+ content: `Error: ${e.message || "Something went wrong."}`,
+ });
+ }
+ this.state.sending = false;
+ this.scrollToBottom();
+ }
+
+ onKeyDown(ev) {
+ if (ev.key === "Enter" && !ev.shiftKey) {
+ ev.preventDefault();
+ this.sendMessage();
+ }
+ }
+
+ scrollToBottom() {
+ const el = this.messagesRef.el;
+ if (el) {
+ setTimeout(() => { el.scrollTop = el.scrollHeight; }, 100);
+ }
+ }
+
+ async onApprove(matchHistoryId) {
+ await rpc("/fusion_accounting/approve", { match_history_id: matchHistoryId });
+ this.state.pendingApprovals = this.state.pendingApprovals.filter(a => a.id !== matchHistoryId);
+ this.state.messages.push({ role: "assistant", content: "Action approved and executed." });
+ }
+
+ async onReject(matchHistoryId) {
+ await rpc("/fusion_accounting/reject", { match_history_id: matchHistoryId, reason: "User rejected" });
+ this.state.pendingApprovals = this.state.pendingApprovals.filter(a => a.id !== matchHistoryId);
+ this.state.messages.push({ role: "assistant", content: "Action rejected." });
+ }
+
+ async onApproveAll() {
+ const ids = this.state.pendingApprovals.map(a => a.id);
+ if (!ids.length) return;
+ await rpc("/fusion_accounting/approve_all", { match_history_ids: ids });
+ const count = this.state.pendingApprovals.length;
+ this.state.pendingApprovals = [];
+ this.state.messages.push({ role: "assistant", content: `${count} actions approved and executed.` });
+ }
+
+ async onRejectAll() {
+ const ids = this.state.pendingApprovals.map(a => a.id);
+ if (!ids.length) return;
+ await rpc("/fusion_accounting/reject_all", { match_history_ids: ids, reason: "Batch rejected" });
+ const count = this.state.pendingApprovals.length;
+ this.state.pendingApprovals = [];
+ this.state.messages.push({ role: "assistant", content: `${count} actions rejected.` });
+ }
+}
diff --git a/fusion_accounting/static/src/components/chat/chat_panel.xml b/fusion_accounting/static/src/components/chat/chat_panel.xml
new file mode 100644
index 00000000..fa3e19ad
--- /dev/null
+++ b/fusion_accounting/static/src/components/chat/chat_panel.xml
@@ -0,0 +1,103 @@
+
+Loading dashboard...
+AI-prioritised items will appear here after the first audit scan.
+No match history yet
+AI tool calls and their outcomes will appear here.
+No rules defined yet
+Create rules to teach the AI your accounting patterns.
++ No AI sessions yet +
+Start a conversation with Fusion AI from the dashboard.
+