import pandas as pd import numpy as np import gradio as gr # ═══════════════════════════════════════════════════════════ # MONKEY-PATCH — fix gradio_client bug where get_type() crashes # when schema is a bool (e.g. {"additionalProperties": true}). # Upstream bug: TypeError: argument of type 'bool' is not iterable # This must run BEFORE any Gradio route is registered. # ═══════════════════════════════════════════════════════════ try: import gradio_client.utils as _gcu _original_get_type = _gcu.get_type def _safe_get_type(schema): if not isinstance(schema, dict): return "Any" return _original_get_type(schema) _gcu.get_type = _safe_get_type _original_json_schema_to_python_type = _gcu._json_schema_to_python_type def _safe_json_schema_to_python_type(schema, defs=None): if not isinstance(schema, dict): return "Any" try: return _original_json_schema_to_python_type(schema, defs) except (TypeError, KeyError, AttributeError): return "Any" _gcu._json_schema_to_python_type = _safe_json_schema_to_python_type print("[patch] gradio_client schema handler patched successfully") except Exception as _e: print(f"[patch] could not patch gradio_client: {_e}") import plotly.graph_objects as go import datetime import os import json import random import urllib.request import urllib.error import tempfile import re import base64 import mimetypes # ══════════════════════════════════════════════ # 1. GLOBAL STATE & CHART OF ACCOUNTS # ══════════════════════════════════════════════ general_ledger_df = pd.DataFrame(columns=['Date','Account','Type','Sub','Debit','Credit','Description']) journal_entries_df = pd.DataFrame(columns=['JE #','Date','Description','Entries','Status']) COA_MAP = { "Cash":{"type":"Asset","sub":"Current Asset","normal":"Debit","code":"1010"}, "Accounts Receivable":{"type":"Asset","sub":"Current Asset","normal":"Debit","code":"1200"}, "Inventory":{"type":"Asset","sub":"Current Asset","normal":"Debit","code":"1300"}, "Prepaid Expenses":{"type":"Asset","sub":"Current Asset","normal":"Debit","code":"1400"}, "Supplies":{"type":"Asset","sub":"Current Asset","normal":"Debit","code":"1500"}, "Equipment":{"type":"Asset","sub":"Fixed Asset","normal":"Debit","code":"1600"}, "Accumulated Depreciation":{"type":"Asset","sub":"Contra Asset","normal":"Credit","code":"1650"}, "Land":{"type":"Asset","sub":"Fixed Asset","normal":"Debit","code":"1700"}, "Buildings":{"type":"Asset","sub":"Fixed Asset","normal":"Debit","code":"1800"}, "Accounts Payable":{"type":"Liability","sub":"Current Liability","normal":"Credit","code":"2010"}, "Salaries Payable":{"type":"Liability","sub":"Current Liability","normal":"Credit","code":"2020"}, "Interest Payable":{"type":"Liability","sub":"Current Liability","normal":"Credit","code":"2030"}, "Unearned Revenue":{"type":"Liability","sub":"Current Liability","normal":"Credit","code":"2040"}, "Notes Payable":{"type":"Liability","sub":"Long-term Liability","normal":"Credit","code":"2500"}, "Bonds Payable":{"type":"Liability","sub":"Long-term Liability","normal":"Credit","code":"2600"}, "Common Stock":{"type":"Equity","sub":"Equity","normal":"Credit","code":"3010"}, "Retained Earnings":{"type":"Equity","sub":"Equity","normal":"Credit","code":"3020"}, "Dividends":{"type":"Equity","sub":"Contra Equity","normal":"Debit","code":"3030"}, "Revenue":{"type":"Revenue","sub":"Operating Revenue","normal":"Credit","code":"4010"}, "Service Revenue":{"type":"Revenue","sub":"Operating Revenue","normal":"Credit","code":"4020"}, "Sales Revenue":{"type":"Revenue","sub":"Operating Revenue","normal":"Credit","code":"4030"}, "Interest Revenue":{"type":"Revenue","sub":"Other Revenue","normal":"Credit","code":"4500"}, "Cost of Goods Sold":{"type":"Expense","sub":"Operating Expense","normal":"Debit","code":"5010"}, "Salaries Expense":{"type":"Expense","sub":"Operating Expense","normal":"Debit","code":"5100"}, "Rent Expense":{"type":"Expense","sub":"Operating Expense","normal":"Debit","code":"5200"}, "Utilities Expense":{"type":"Expense","sub":"Operating Expense","normal":"Debit","code":"5300"}, "Insurance Expense":{"type":"Expense","sub":"Operating Expense","normal":"Debit","code":"5400"}, "Depreciation Expense":{"type":"Expense","sub":"Operating Expense","normal":"Debit","code":"5500"}, "Supplies Expense":{"type":"Expense","sub":"Operating Expense","normal":"Debit","code":"5600"}, "Interest Expense":{"type":"Expense","sub":"Operating Expense","normal":"Debit","code":"5700"}, "Operating Expenses":{"type":"Expense","sub":"Operating Expense","normal":"Debit","code":"5900"}, } INSTRUCTOR_SETTINGS = {"show_transaction_manager": False} je_counter = [0] # ══════════════════════════════════════════════ # 2. HELPERS # ══════════════════════════════════════════════ def get_coa_display(): rows = [] for acc, info in sorted(COA_MAP.items(), key=lambda x: x[1]['code']): rows.append({"Code":info["code"],"Account Name":acc,"Type":info["type"],"Sub-Type":info["sub"],"Normal Bal.":info["normal"]}) return pd.DataFrame(rows) def add_new_account(name, atype, asub, anormal): global COA_MAP name = name.strip() if not name: raise gr.Error("Account name cannot be empty.") if name in COA_MAP: raise gr.Error(f"'{name}' already exists.") pfx = {"Asset":"1","Liability":"2","Equity":"3","Revenue":"4","Expense":"5"}.get(atype,"9") codes = [int(v["code"]) for v in COA_MAP.values() if v["code"].startswith(pfx)] nc = str(max(codes)+10) if codes else f"{pfx}010" COA_MAP[name] = {"type":atype,"sub":asub,"normal":anormal,"code":nc} return get_coa_display(), f"✅ '{name}' added (Code: {nc})" def search_accounts(query): if not query or not query.strip(): return get_coa_display() q = query.strip().lower() rows = [{"Code":i["code"],"Account Name":a,"Type":i["type"],"Sub-Type":i["sub"],"Normal Bal.":i["normal"]} for a,i in sorted(COA_MAP.items(), key=lambda x:x[1]['code']) if q in a.lower() or q in i['type'].lower() or q in i['sub'].lower()] return pd.DataFrame(rows) if rows else pd.DataFrame({"Result":["No matches found."]}) def search_accounts_short(query): if not query or not query.strip(): return get_coa_display()[['Account Name','Normal Bal.']] q = query.strip().lower() rows = [{"Account Name":a,"Normal Bal.":i["normal"]} for a,i in sorted(COA_MAP.items(), key=lambda x:x[1]['code']) if q in a.lower() or q in i['type'].lower() or q in i['sub'].lower()] return pd.DataFrame(rows) if rows else pd.DataFrame({"Result":["No matches."]}) # ══════════════════════════════════════════════ # 3. AI CHATBOT (ENHANCED WITH FILE SUPPORT) # ══════════════════════════════════════════════ ACCOUNTING_KB = { "debit": "📘 **DEBIT** — Left side of a T-account.\n\n**Increases:** Assets, Expenses, Dividends (DEAD: Debits increase Expenses, Assets, Dividends)\n**Decreases:** Liabilities, Equity, Revenue\n\n**Example:** Receive cash → DEBIT Cash (asset increases).", "credit": "📗 **CREDIT** — Right side of a T-account.\n\n**Increases:** Liabilities, Equity, Revenue\n**Decreases:** Assets, Expenses, Dividends\n\n**Example:** Earn revenue → CREDIT Revenue (increases it).", "journal entry": "📝 **JOURNAL ENTRY** — Records a transaction.\n\n**Rules:**\n1. Every entry needs DATE + DESCRIPTION\n2. Debits listed first, credits indented\n3. Total Debits MUST = Total Credits\n4. Minimum 2 accounts affected\n\n**Types:** General, Adjusting, Closing, Reversing entries", "trial balance": "⚖️ **TRIAL BALANCE** — Lists ALL accounts with debit/credit balances.\n\n**Purpose:** Verify debits = credits before financial statements.\n\n⚠️ A balanced TB does NOT guarantee no errors — won't catch omission or principle errors.", "balance sheet": "📊 **BALANCE SHEET** — Financial position at a specific date.\n\n**Assets = Liabilities + Equity**\n\nCurrent Assets → Fixed Assets\nCurrent Liabilities → Long-term Liabilities\nCommon Stock → Retained Earnings", "income statement": "📈 **INCOME STATEMENT (P&L)** — Profitability over a period.\n\n**Revenue − Expenses = Net Income**\n\nRevenue → COGS = Gross Profit → Operating Expenses = Operating Income → Net Income", "accounting equation": "⚖️ **THE ACCOUNTING EQUATION**\n\n**Assets = Liabilities + Stockholders' Equity**\n\nEvery transaction keeps this in balance. Foundation since Luca Pacioli, 1494!", "depreciation": "📉 **DEPRECIATION** — Allocating asset cost over useful life.\n\n**Methods:**\n• **Straight-Line:** (Cost − Salvage) / Life\n• **Double-Declining:** 2/Life × Book Value\n• **Units of Production:** Based on usage\n\nLand is NEVER depreciated.", "amortization": "📉 **AMORTIZATION** — For intangible assets (patents, goodwill) or loan repayment.\n\nEach loan payment = Interest + Principal\nEarly payments → more interest\nLater payments → more principal", "gaap": "📜 **GAAP** — Generally Accepted Accounting Principles.\n\nRevenue Recognition, Matching, Cost, Full Disclosure, Going Concern, Materiality, Conservatism, Consistency.\n\nSet by FASB. Rules-based (vs IFRS principles-based).", "normal balance": "📘 **NORMAL BALANCE** — Side that increases an account:\n\n• Assets → Debit\n• Expenses → Debit\n• Dividends → Debit\n• Liabilities → Credit\n• Equity → Credit\n• Revenue → Credit\n\nRemember **DEALER**!", "t-account": "📐 **T-ACCOUNT** — Visual account representation.\n\nLeft = Debits | Right = Credits\n\nIncreases on NORMAL balance side. Decreases on OPPOSITE side.", "retained earnings": "💰 **RETAINED EARNINGS** — Accumulated undistributed profits.\n\nBeginning RE + Net Income − Dividends = Ending RE\n\nPart of Equity. NOT the same as Cash!", "accounts payable": "📋 **ACCOUNTS PAYABLE** — Money owed to suppliers.\n\nCurrent Liability, Credit normal.\nPurchase on credit: DR Supplies / CR AP\nPay bill: DR AP / CR Cash", "accounts receivable": "📋 **ACCOUNTS RECEIVABLE** — Money owed by customers.\n\nCurrent Asset, Debit normal.\nSale on credit: DR AR / CR Revenue\nCollect: DR Cash / CR AR", "cash flow": "💵 **CASH FLOW STATEMENT**\n\n1. **Operating:** Day-to-day business\n2. **Investing:** Long-term assets\n3. **Financing:** Debt and equity\n\nEnding Cash must match Balance Sheet!", "double entry": "📖 **DOUBLE-ENTRY BOOKKEEPING** — Every transaction affects 2+ accounts.\n\nTotal Debits = Total Credits (always).\nFormalized by Luca Pacioli in 1494.", "adjusting entries": "🔧 **ADJUSTING ENTRIES** — Period-end updates.\n\n• Accrued Revenue/Expenses\n• Deferred Revenue (Unearned)\n• Prepaid Expenses\n• Depreciation\n\nAdjusting entries NEVER involve Cash!", "cogs": "📦 **COST OF GOODS SOLD**\n\nBeginning Inventory + Purchases − Ending Inventory = COGS\n\nMethods: FIFO, LIFO, Weighted Average\nRevenue − COGS = Gross Profit", "closing entries": "🔒 **CLOSING ENTRIES** — Transfer temporary accounts to RE.\n\n1. Close Revenue → Income Summary\n2. Close Expenses → Income Summary\n3. Close Income Summary → RE\n4. Close Dividends → RE", "equity": "🏛️ **EQUITY** — Owner's residual interest.\n\nComponents: Common Stock, APIC, Retained Earnings, Treasury Stock\nEquity = Assets − Liabilities", "asset": "🏢 **ASSETS** — Resources providing future benefit.\n\nCurrent: Cash, AR, Inventory, Prepaid\nFixed: Equipment, Buildings, Land\nIntangible: Patents, Goodwill\n\nDebit normal. Listed by liquidity.", "liability": "📋 **LIABILITIES** — Obligations owed.\n\nCurrent: AP, Salaries Payable, Unearned Revenue\nLong-term: Notes Payable, Bonds Payable\n\nCredit normal balance.", "revenue": "💰 **REVENUE** — Income from primary business.\n\nCredit normal. Recognized when EARNED (accrual), not when cash received.", "expense": "💸 **EXPENSES** — Costs to generate revenue.\n\nDebit normal. Recorded when INCURRED (matching principle). Reduces Net Income.", "ratio": "📊 **FINANCIAL RATIOS**\n\nLiquidity: Current Ratio, Quick Ratio\nProfitability: Gross Margin, Net Margin, ROE, ROA\nLeverage: Debt-to-Equity\nEfficiency: Inventory Turnover, AR Turnover", "bond": "🏦 **BONDS** — Long-term debt.\n\nPar: Coupon = Market rate\nPremium: Coupon > Market rate\nDiscount: Coupon < Market rate", "inventory": "📦 **INVENTORY** — Goods for sale.\n\nTypes: Raw Materials, WIP, Finished Goods\nMethods: FIFO, LIFO, Weighted Average\nLower of Cost or Market (LCM) applies.", "ifrs": "🌍 **IFRS** — International Financial Reporting Standards.\n\n140+ countries. Principles-based vs GAAP rules-based.\nLIFO not allowed. Asset revaluation allowed.", "tax": "🏛️ **TAX ACCOUNTING**\n\nTaxable Income ≠ Book Income\nPermanent vs Temporary differences\nUS Corporate rate: 21% (TCJA 2017)", "audit": "🔍 **AUDITING** — Independent examination.\n\nTypes: External, Internal, Government\nOpinions: Unqualified, Qualified, Adverse, Disclaimer\nSOX 2002 requires public company audits.", "accrual": "📅 **ACCRUAL vs CASH BASIS**\n\nAccrual: Revenue when earned, expenses when incurred (GAAP)\nCash: Revenue when received, expenses when paid", "working capital": "💼 **WORKING CAPITAL** = Current Assets − Current Liabilities\n\nPositive = can cover obligations ✅\nCurrent Ratio = CA / CL (healthy: >1.5)", "ledger": "📓 **GENERAL LEDGER** — Master record of all accounts.\n\nTransaction → Journal → Ledger → Trial Balance → Financial Statements", "bank reconciliation": "🏦 **BANK RECONCILIATION**\n\nBank Balance + Deposits in Transit − Outstanding Checks = Adjusted Bank\nBook Balance + Interest − Fees − NSF = Adjusted Book\nBoth must equal!", } # ────────────────────────────────────────────── # REAL LLM INTEGRATION (free APIs) # ────────────────────────────────────────────── DEFAULT_API_KEY = "" GROQ_API_KEY = os.environ.get("GROQ_API_KEY", DEFAULT_API_KEY).strip() OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "").strip() HF_API_KEY = os.environ.get("HF_API_KEY", "").strip() SYSTEM_PROMPT = ( "You are an expert Accounting AI Assistant built into an accounting intelligence tool " "for SUNY Polytechnic. You are helpful, accurate, and thorough. " "You can answer ANY question the user asks — accounting, finance, math, general knowledge, " "explanations, coding, writing, etc. — just like ChatGPT, Claude, or Gemini. " "When the question is about accounting, give clear examples, journal entries, and use GAAP. " "Format answers with markdown (headings, bullets, bold) for readability.\n\n" "IMPORTANT — FILE GENERATION:\n" "When a user asks you to create, generate, or produce a file (Excel spreadsheet, Word document, " "CSV, PDF table, code file, etc.), you MUST include a special code block in your response that " "contains the file generation code. Use the following format:\n\n" "```generate_file\n" "FILENAME: desired_filename.xlsx\n" "TYPE: excel|csv|word|text|python|html\n" "---\n" "...actual data content here, described as structured text...\n" "```\n\n" "For Excel/CSV, format data as pipe-separated rows:\n" "```generate_file\n" "FILENAME: report.xlsx\n" "TYPE: excel\n" "---\n" "Column1|Column2|Column3\n" "value1|value2|value3\n" "```\n\n" "For Word/text, just write the content as plain text.\n" "For code files (python, html, etc.), write the actual code.\n\n" "Always include this code block when the user asks for a downloadable file. " "Also provide a text explanation alongside it.\n\n" "When a user attaches a file, they will provide the file content or a summary of it. " "Analyze and respond to their questions about the attached file content." ) def _http_post_json(url, headers, payload, timeout=60): data = json.dumps(payload).encode("utf-8") req = urllib.request.Request(url, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=timeout) as resp: return json.loads(resp.read().decode("utf-8")) def call_groq(messages): if not GROQ_API_KEY: return None try: out = _http_post_json( "https://api.groq.com/openai/v1/chat/completions", {"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"}, {"model": "llama-3.3-70b-versatile", "messages": messages, "temperature": 0.7, "max_tokens": 2500}, ) return out["choices"][0]["message"]["content"] except Exception as e: print(f"[Groq error] {e}") return None def call_openrouter(messages): if not OPENROUTER_API_KEY: return None free_models = [ "openrouter/free", "meta-llama/llama-3.3-70b-instruct:free", "google/gemma-3-27b-it:free", "nvidia/nemotron-nano-9b-v2:free", "deepseek/deepseek-r1:free", "qwen/qwen3-coder:free", ] for model in free_models: try: out = _http_post_json( "https://openrouter.ai/api/v1/chat/completions", { "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json", "HTTP-Referer": "https://huggingface.co", "X-Title": "Accounting Intelligence", }, {"model": model, "messages": messages, "max_tokens": 2500}, ) content = out["choices"][0]["message"]["content"] print(f"[OpenRouter] SUCCESS with {model}") return content except urllib.error.HTTPError as e: body = "" try: body = e.read().decode("utf-8", errors="ignore")[:200] except: pass print(f"[OpenRouter] {model} → HTTP {e.code}: {body}") continue except Exception as e: print(f"[OpenRouter] {model} → {type(e).__name__}: {e}") continue print("[OpenRouter] All free models failed.") return None def call_hf(messages): if not HF_API_KEY: return None try: prompt = SYSTEM_PROMPT + "\n\n" for m in messages: if m["role"] != "system": prompt += f"{m['role'].upper()}: {m['content']}\n" prompt += "ASSISTANT:" out = _http_post_json( "https://api-inference.huggingface.co/models/meta-llama/Llama-3.2-3B-Instruct", {"Authorization": f"Bearer {HF_API_KEY}", "Content-Type": "application/json"}, {"inputs": prompt, "parameters": {"max_new_tokens": 1200, "temperature": 0.7, "return_full_text": False}}, ) if isinstance(out, list) and out: return out[0].get("generated_text", "").strip() except Exception as e: print(f"[HF error] {e}") return None def call_llm(user_message, history): messages = [{"role": "system", "content": SYSTEM_PROMPT}] if history: for h in history[-10:]: if isinstance(h, dict) and h.get("role") in ("user", "assistant"): messages.append({"role": h["role"], "content": str(h.get("content", ""))}) messages.append({"role": "user", "content": user_message}) for fn in (call_groq, call_openrouter, call_hf): reply = fn(messages) if reply: return reply return None def local_kb_fallback(msg_lower): best_score, response = 0, None for kw, ans in ACCOUNTING_KB.items(): score = 0 if kw in msg_lower: score += 5 for w in kw.split(): if w in msg_lower: score += 2 if score > best_score: best_score, response = score, ans if response and best_score >= 2: return response return ( "⚠️ **No AI API key configured** (or all providers failed).\n\n" "To unlock full AI answers for **any** question, set one of these free API keys " "as an environment variable before launching the app:\n\n" "• `GROQ_API_KEY` — get free at https://console.groq.com/keys *(recommended)*\n" "• `OPENROUTER_API_KEY` — get free at https://openrouter.ai/keys\n" "• `HF_API_KEY` — get free at https://huggingface.co/settings/tokens\n\n" "**Example (Windows):** `set GROQ_API_KEY=gsk_...` then run `python app.py`\n" "**Example (Mac/Linux):** `export GROQ_API_KEY=gsk_...`\n\n" "Meanwhile, try asking about: debits, credits, journal entries, trial balance, " "balance sheet, income statement, depreciation, GAAP, etc." ) # ────────────────────────────────────────────── # FILE READING HELPERS # ────────────────────────────────────────────── def read_uploaded_file(filepath): """Read an uploaded file and return its text content for the LLM.""" if not filepath or not os.path.exists(filepath): return None, None fname = os.path.basename(filepath) ext = os.path.splitext(fname)[1].lower() try: if ext == '.csv': df = pd.read_csv(filepath) preview = df.head(50).to_string(index=False) summary = f"📄 **File:** {fname}\n**Rows:** {len(df)} | **Columns:** {list(df.columns)}\n\n```\n{preview}\n```" return summary, fname elif ext in ('.xlsx', '.xls'): df = pd.read_excel(filepath, engine='openpyxl' if ext == '.xlsx' else None) preview = df.head(50).to_string(index=False) summary = f"📄 **File:** {fname}\n**Rows:** {len(df)} | **Columns:** {list(df.columns)}\n\n```\n{preview}\n```" return summary, fname elif ext == '.json': with open(filepath, 'r', encoding='utf-8', errors='replace') as f: data = json.load(f) content = json.dumps(data, indent=2)[:5000] summary = f"📄 **File:** {fname}\n\n```json\n{content}\n```" return summary, fname elif ext in ('.txt', '.md', '.py', '.html', '.css', '.js', '.log', '.cfg', '.ini', '.yml', '.yaml', '.xml', '.sql'): with open(filepath, 'r', encoding='utf-8', errors='replace') as f: content = f.read()[:8000] summary = f"📄 **File:** {fname}\n\n```\n{content}\n```" return summary, fname elif ext == '.pdf': try: import subprocess result = subprocess.run( ['python3', '-c', f""" import fitz doc = fitz.open("{filepath}") text = "" for page in doc: text += page.get_text() print(text[:8000]) """], capture_output=True, text=True, timeout=30 ) if result.returncode == 0 and result.stdout.strip(): summary = f"📄 **File:** {fname}\n\n```\n{result.stdout.strip()}\n```" return summary, fname except: pass # Fallback: try with pdfplumber try: import pdfplumber text = "" with pdfplumber.open(filepath) as pdf: for page in pdf.pages[:20]: text += (page.extract_text() or "") + "\n" if text.strip(): summary = f"📄 **File:** {fname}\n\n```\n{text[:8000]}\n```" return summary, fname except: pass return f"📄 **File:** {fname} (PDF — could not extract text. Install `pymupdf` or `pdfplumber` for PDF support.)", fname elif ext in ('.docx',): try: from docx import Document doc = Document(filepath) text = "\n".join([p.text for p in doc.paragraphs])[:8000] summary = f"📄 **File:** {fname}\n\n```\n{text}\n```" return summary, fname except: return f"📄 **File:** {fname} (Word doc — install `python-docx` for Word support.)", fname elif ext in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'): summary = f"🖼️ **Image:** {fname} (Image uploaded — I can see it's an image file. Ask me anything about what you need!)" return summary, fname else: # Try reading as text try: with open(filepath, 'r', encoding='utf-8', errors='replace') as f: content = f.read()[:5000] summary = f"📄 **File:** {fname}\n\n```\n{content}\n```" return summary, fname except: return f"📄 **File:** {fname} (Could not read this file type.)", fname except Exception as e: return f"📄 **File:** {fname}\n⚠️ Error reading: {str(e)}", fname # ────────────────────────────────────────────── # FILE GENERATION FROM LLM RESPONSE # ────────────────────────────────────────────── def parse_and_generate_files(response_text): """ Parse the LLM response for ```generate_file blocks and create actual files. Returns: list of file paths created. """ generated_files = [] pattern = r'```generate_file\s*\n(.*?)```' matches = re.findall(pattern, response_text, re.DOTALL) for match in matches: lines = match.strip().split('\n') filename = "output.txt" filetype = "text" data_lines = [] in_data = False for line in lines: if line.startswith("FILENAME:"): filename = line.split(":", 1)[1].strip() elif line.startswith("TYPE:"): filetype = line.split(":", 1)[1].strip().lower() elif line.strip() == "---": in_data = True elif in_data: data_lines.append(line) content = "\n".join(data_lines) if not content.strip(): continue try: outdir = tempfile.mkdtemp() filepath = os.path.join(outdir, filename) if filetype in ('excel', 'xlsx'): # Parse pipe-separated data rows = [r.split('|') for r in content.strip().split('\n') if r.strip()] if len(rows) >= 2: headers = [h.strip() for h in rows[0]] data = [[c.strip() for c in r] for r in rows[1:]] df = pd.DataFrame(data, columns=headers) # Try to convert numeric columns for col in df.columns: try: df[col] = pd.to_numeric(df[col]) except: pass if not filepath.endswith('.xlsx'): filepath = filepath.rsplit('.', 1)[0] + '.xlsx' df.to_excel(filepath, index=False, engine='openpyxl') generated_files.append(filepath) else: # Single row or raw content — write as text with open(filepath, 'w') as f: f.write(content) generated_files.append(filepath) elif filetype == 'csv': rows = [r.split('|') for r in content.strip().split('\n') if r.strip()] if len(rows) >= 2: headers = [h.strip() for h in rows[0]] data = [[c.strip() for c in r] for r in rows[1:]] df = pd.DataFrame(data, columns=headers) for col in df.columns: try: df[col] = pd.to_numeric(df[col]) except: pass if not filepath.endswith('.csv'): filepath = filepath.rsplit('.', 1)[0] + '.csv' df.to_csv(filepath, index=False) generated_files.append(filepath) else: with open(filepath, 'w') as f: f.write(content) generated_files.append(filepath) elif filetype in ('word', 'docx'): try: from docx import Document doc = Document() for para in content.split('\n'): if para.strip(): doc.add_paragraph(para.strip()) if not filepath.endswith('.docx'): filepath = filepath.rsplit('.', 1)[0] + '.docx' doc.save(filepath) generated_files.append(filepath) except ImportError: # Fallback to plain text if not filepath.endswith('.txt'): filepath = filepath.rsplit('.', 1)[0] + '.txt' with open(filepath, 'w', encoding='utf-8') as f: f.write(content) generated_files.append(filepath) else: # text, python, html, etc. with open(filepath, 'w', encoding='utf-8') as f: f.write(content) generated_files.append(filepath) except Exception as e: print(f"[File generation error] {e}") continue return generated_files def clean_response_for_display(response_text): """Remove the generate_file blocks from the displayed response and add download notice.""" cleaned = re.sub( r'```generate_file\s*\n.*?```', '', response_text, flags=re.DOTALL ) # Clean up excessive whitespace cleaned = re.sub(r'\n{3,}', '\n\n', cleaned) return cleaned.strip() # ────────────────────────────────────────────── # ENHANCED CHATBOT WITH FILE ATTACHMENT & OUTPUT # ────────────────────────────────────────────── def accounting_chatbot_with_files(user_message, history, uploaded_files): """Enhanced chatbot that handles file uploads and generates downloadable files.""" if (not user_message or not user_message.strip()) and not uploaded_files: return history, "", None, [] user_message = (user_message or "").strip() history = history or [] file_context = "" attached_names = [] # Process uploaded files if uploaded_files: for fpath in uploaded_files: if fpath: file_content, fname = read_uploaded_file(fpath) if file_content: file_context += f"\n\n--- ATTACHED FILE ---\n{file_content}\n--- END FILE ---\n" attached_names.append(fname) # Build the user message with file context full_message = user_message if file_context: full_message = f"{user_message}\n\n[The user has attached the following file(s): {', '.join(attached_names)}]\n{file_context}" if not full_message.strip(): full_message = "I've uploaded a file. Please analyze it and summarize its contents." # Display message (without the file dump) display_msg = user_message if attached_names: file_badges = " ".join([f"📎 `{n}`" for n in attached_names]) display_msg = f"{file_badges}\n\n{user_message}" if user_message else f"{file_badges}\n\nPlease analyze this file." # Try real LLM response = call_llm(full_message, history) if not response: response = local_kb_fallback(user_message.lower()) # Parse response for file generation generated_files = parse_and_generate_files(response) display_response = clean_response_for_display(response) # Add download notice if files were generated if generated_files: fnames = [os.path.basename(f) for f in generated_files] display_response += f"\n\n📥 **Files ready for download:** {', '.join(fnames)}\n*(Check the download area below the chat)*" history.append({"role": "user", "content": display_msg}) history.append({"role": "assistant", "content": display_response}) return history, "", None, generated_files if generated_files else [] def export_chat_history(history): """Export the entire chat conversation as a text/markdown file.""" if not history: raise gr.Error("No chat history to export.") outdir = tempfile.mkdtemp() filepath = os.path.join(outdir, f"chat_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.md") with open(filepath, 'w', encoding='utf-8') as f: f.write("# Accounting Intelligence — Chat Export\n") f.write(f"**Exported:** {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"**Source:** SUNY Polytechnic — Accounting Intelligence Tool\n\n") f.write("---\n\n") for msg in history: role = msg.get("role", "unknown") content = msg.get("content", "") if role == "user": f.write(f"## 👤 You\n{content}\n\n") elif role == "assistant": f.write(f"## 🤖 AI Assistant\n{content}\n\n") f.write("---\n\n") return filepath # ══════════════════════════════════════════════ # 4. CORE ACCOUNTING LOGIC # ══════════════════════════════════════════════ def update_equation_status(): if general_ledger_df.empty: a, l, te, bal_ok = 0.0, 0.0, 0.0, True else: df = general_ledger_df.copy() a = df[df['Type']=='Asset'].pipe(lambda x: x['Debit'].sum()-x['Credit'].sum()) l = df[df['Type']=='Liability'].pipe(lambda x: x['Credit'].sum()-x['Debit'].sum()) eq = df[df['Type']=='Equity'].pipe(lambda x: x['Credit'].sum()-x['Debit'].sum()) r = df[df['Type']=='Revenue']['Credit'].sum() e = df[df['Type']=='Expense']['Debit'].sum() te = eq+(r-e) bal_ok = round(a,2) == round(l+te,2) bal_cls = "eq-pill bal" if bal_ok else "eq-pill unbal" bal_txt = "✓ In balance" if bal_ok else "✗ Off balance" bal_icon = ('' '' '') if bal_ok else '' bal_label = 'In balance' if bal_ok else 'Off balance' return ( f'
' f'
Assets
${a:,.2f}
' f'
=
' f'
Liabilities
${l:,.2f}
' f'
+
' f'
Equity
${te:,.2f}
' f'
{bal_icon} {bal_label}
' f'
' ) def generate_financial_reports(): if general_ledger_df.empty: e = pd.DataFrame({"Status":["No transactions recorded yet."]}) return e,e,e,e,e,go.Figure() df = general_ledger_df.copy(); df['Net']=df['Debit']-df['Credit'] s = df.groupby(['Account','Type','Sub'])['Net'].sum().reset_index() rev=s[s['Type']=='Revenue']['Net'].sum()*-1; exp=s[s['Type']=='Expense']['Net'].sum(); ni=rev-exp is_r=[] for _,r in s[s['Type']=='Revenue'].iterrows(): is_r.append([f" {r['Account']}",f"${abs(r['Net']):,.2f}"]) is_r+=[["Total Revenue",f"${rev:,.2f}"],["",""]] for _,r in s[s['Type']=='Expense'].iterrows(): is_r.append([f" {r['Account']}",f"${abs(r['Net']):,.2f}"]) is_r+=[["Total Expenses",f"${exp:,.2f}"],["─"*20,"─"*14],["NET INCOME",f"${ni:,.2f}"]] is_df=pd.DataFrame(is_r,columns=["Line Item","Amount"]) cs=s[s['Account']=='Common Stock']['Net'].sum()*-1 re=s[s['Account']=='Retained Earnings']['Net'].sum()*-1+ni dv=s[s['Account']=='Dividends']['Net'].sum() if 'Dividends' in s['Account'].values else 0 se_df=pd.DataFrame([["Common Stock",f"${cs:,.2f}"],["+Net Income",f"${ni:,.2f}"],["-Dividends",f"${dv:,.2f}"], ["Retained Earnings",f"${re:,.2f}"],["─"*20,"─"*14],["TOTAL EQUITY",f"${cs+re:,.2f}"]],columns=["Component","Value"]) at=s[s['Type']=='Asset']['Net'].sum(); lt=s[s['Type']=='Liability']['Net'].sum()*-1 bs_r=[["━ ASSETS ━",""]] for _,r in s[s['Type']=='Asset'].iterrows(): bs_r.append([f" {r['Account']}",f"${r['Net']:,.2f}"]) bs_r+=[["Total Assets",f"${at:,.2f}"],["",""],["━ LIABILITIES ━",""]] for _,r in s[s['Type']=='Liability'].iterrows(): bs_r.append([f" {r['Account']}",f"${abs(r['Net']):,.2f}"]) bs_r+=[["Total Liabilities",f"${lt:,.2f}"],["",""],["━ EQUITY ━",""],["Total Equity",f"${cs+re:,.2f}"],["─"*20,"─"*14],["L + E",f"${lt+cs+re:,.2f}"]] bs_df=pd.DataFrame(bs_r,columns=["Category","Amount"]) cb=s[s['Account']=='Cash']['Net'].sum() if 'Cash' in s['Account'].values else 0 cf_df=pd.DataFrame([["Net Income",f"${ni:,.2f}"],["Cash on Hand",f"${cb:,.2f}"]],columns=["Activity","Amount"]) tb_r,td,tc=[],0,0 for _,r in s.iterrows(): n=r['Net'];dv2=abs(n) if n>=0 else 0;cv=abs(n) if n<0 else 0;td+=dv2;tc+=cv tb_r.append([r['Account'],f"${dv2:,.2f}" if dv2>0 else "—",f"${cv:,.2f}" if cv>0 else "—"]) tb_r+=[["─"*20,"─"*14,"─"*14],["TOTALS",f"${td:,.2f}",f"${tc:,.2f}"], [""," ✅" if round(td,2)==round(tc,2) else " ❌",""]] tb_df=pd.DataFrame(tb_r,columns=["Account","Debit","Credit"]) cm={'Asset':'#1A3C6E','Liability':'#BF0A30','Equity':'#7C3AED','Revenue':'#059669','Expense':'#EA580C'} fig=go.Figure() for _,r in s[s['Net'].abs()>0].iterrows(): fig.add_trace(go.Bar(x=[r['Account']],y=[abs(r['Net'])],marker=dict(color=cm.get(r['Type'],'#64748B'),cornerradius=6), text=f"${abs(r['Net']):,.0f}",textposition='outside',textfont=dict(size=11,family="DM Sans",color="#1e293b"), hovertemplate=f"{r['Account']}
{r['Type']}
${abs(r['Net']):,.2f}")) fig.update_layout(title=dict(text="Account Balances",font=dict(size=17,family="DM Sans",color="#0F172A"),x=0.02), template="plotly_white",showlegend=False,font=dict(family="DM Sans",color="#334155"), plot_bgcolor='rgba(0,0,0,0)',paper_bgcolor='rgba(0,0,0,0)',margin=dict(l=40,r=30,t=55,b=50), xaxis=dict(showgrid=False,tickangle=-35),yaxis=dict(showgrid=True,gridcolor='rgba(0,0,0,0.06)',tickprefix='$'),bargap=0.3) return is_df,se_df,bs_df,cf_df,tb_df,fig def add_journal_entry_from_table(date_str, description, table_data): global general_ledger_df, journal_entries_df if not description or not description.strip(): raise gr.Error("⛔ Description is REQUIRED.") vr=table_data.dropna(subset=['Account']); vr=vr[vr['Account'].astype(str).str.strip()!=""] if vr.empty: raise gr.Error("⛔ No valid rows.") unk=list(set([str(r['Account']).strip() for _,r in vr.iterrows() if str(r['Account']).strip() not in COA_MAP and str(r['Account']).strip()!=""])) if unk: raise gr.Error(f"⚠️ Unknown: {', '.join(unk)}. Add in Chart of Accounts first.") dr=pd.to_numeric(vr['Debit'],errors='coerce').fillna(0).sum() cr=pd.to_numeric(vr['Credit'],errors='coerce').fillna(0).sum() if round(dr,2)!=round(cr,2) or dr==0: raise gr.Error(f"⛔ Unbalanced! DR:${dr:,.2f} CR:${cr:,.2f}") je_counter[0]+=1;jn=je_counter[0];gl=[] for _,row in vr.iterrows(): a=str(row['Account']).strip();inf=COA_MAP.get(a,{"type":"Other","sub":"Other","normal":"Debit","code":"9999"}) gl.append({'Date':pd.to_datetime(date_str),'Account':a,'Type':inf['type'],'Sub':inf['sub'], 'Debit':float(row['Debit'] or 0),'Credit':float(row['Credit'] or 0),'Description':description,'JE_Num':jn}) ng=pd.DataFrame(gl) general_ledger_df=pd.concat([general_ledger_df,ng],ignore_index=True) nj=pd.DataFrame([{'JE #':f"JE-{jn:04d}",'Date':date_str,'Description':description, 'Entries':f"DR ${dr:,.2f} / CR ${cr:,.2f}",'Status':'✅ Posted'}]) journal_entries_df=pd.concat([journal_entries_df,nj],ignore_index=True) fig=go.Figure() for _,e2 in ng.iterrows(): ac=e2['Account'];nm=COA_MAP.get(ac,{}).get('normal','Debit') if e2['Debit']>0: c='#1A3C6E' if nm=='Debit' else '#BF0A30' fig.add_trace(go.Bar(x=[ac],y=[e2['Debit']],marker=dict(color=c,cornerradius=5),text=f"DR ${e2['Debit']:,.0f}",textposition='outside',textfont=dict(size=11,color="#1e293b"))) if e2['Credit']>0: c='#1A3C6E' if nm=='Credit' else '#BF0A30' fig.add_trace(go.Bar(x=[ac],y=[-e2['Credit']],marker=dict(color=c,cornerradius=5),text=f"CR ${e2['Credit']:,.0f}",textposition='outside',textfont=dict(size=11,color="#1e293b"))) fig.update_layout(height=290,barmode='relative',title=dict(text=f"JE-{jn:04d} · 🔵 Increase 🔴 Decrease",font=dict(size=13,family="DM Sans",color="#0F172A")), template="plotly_white",showlegend=False,font=dict(family="DM Sans",color="#334155"), plot_bgcolor='rgba(0,0,0,0)',paper_bgcolor='rgba(0,0,0,0)',margin=dict(l=20,r=20,t=50,b=20), xaxis=dict(showgrid=False),yaxis=dict(showgrid=True,gridcolor='rgba(0,0,0,0.05)',zeroline=True,zerolinecolor='#94A3B8')) return general_ledger_df,journal_entries_df,fig,update_equation_status() def delete_journal_entry(jstr): global general_ledger_df,journal_entries_df if not jstr or not jstr.strip(): raise gr.Error("Enter JE number (e.g. JE-0001)") js=jstr.strip().upper() if not js.startswith("JE-"): js=f"JE-{js}" try: jn=int(js.replace("JE-","")) except: raise gr.Error(f"Invalid: {jstr}") if 'JE_Num' not in general_ledger_df.columns or jn not in general_ledger_df['JE_Num'].values: raise gr.Error(f"{js} not found.") general_ledger_df=general_ledger_df[general_ledger_df['JE_Num']!=jn].reset_index(drop=True) journal_entries_df.loc[journal_entries_df['JE #']==f"JE-{jn:04d}",'Status']='🗑️ Deleted' return general_ledger_df,journal_entries_df,update_equation_status(),f"✅ {js} deleted." def loan_ui(principal,rate,months,start_date): p,ar,m=float(principal),float(rate),int(months);mr=ar/12 pmt=p*(mr*(1+mr)**m)/((1+mr)**m-1) if mr>0 else p/m sch,rem=[],p;cd=pd.to_datetime(start_date) for i in range(1,m+1): intr=rem*mr;pr=pmt-intr;rem-=pr sch.append([i,cd.strftime('%Y-%m-%d'),round(pmt,2),round(intr,2),round(pr,2),round(max(0,rem),2)]) cd+=pd.DateOffset(months=1) df=pd.DataFrame(sch,columns=['Period','Date','Payment','Interest','Principal','Balance']) total_int=df['Interest'].sum() # Cumulative paid amounts over time cum_principal = df['Principal'].cumsum().tolist() cum_interest = df['Interest'].cumsum().tolist() periods = df['Period'].tolist() fig=go.Figure() # Interest area (red, underneath) fig.add_trace(go.Scatter( x=periods, y=cum_interest, fill='tozeroy', name='Interest', line=dict(color='#BF0A30', width=3), fillcolor='rgba(191,10,48,0.22)', mode='lines', hovertemplate='Month %{x}
Interest: $%{y:,.2f}' )) # Principal area (navy, larger values on top) fig.add_trace(go.Scatter( x=periods, y=cum_principal, fill='tonexty', name='Principal', line=dict(color='#1A3C6E', width=3), fillcolor='rgba(26,60,110,0.3)', mode='lines', hovertemplate='Month %{x}
Principal: $%{y:,.2f}' )) header = ( f"MONTHLY PAYMENT" f"

" f"${pmt:,.2f}" f"
" f"Total interest: ${total_int:,.0f}" ) fig.update_layout( title=dict(text=header, x=0.01, xanchor='left', y=0.99, yanchor='top', font=dict(family="DM Sans"), pad=dict(t=10, b=0)), template="plotly_white", height=520, font=dict(family="DM Sans", color="#334155", size=12), plot_bgcolor='rgba(244,246,250,0.6)', paper_bgcolor='rgba(0,0,0,0)', legend=dict(orientation='h', y=-0.15, x=0.5, xanchor='center', font=dict(size=12)), margin=dict(l=70, r=30, t=175, b=70), xaxis=dict(title='Month', showgrid=False, showline=True, linecolor='#DFE3EA', zeroline=False), yaxis=dict(title='', showgrid=True, gridcolor='rgba(0,0,0,0.05)', tickprefix='$', tickformat=',.0f', zeroline=False, rangemode='tozero'), hovermode='x unified', ) return df, fig def asset_ui(cost,life,start_date): c,l=float(cost),int(life);y=max(1,int(l/12));ad=c/y;d,v=[],c for i in range(1,y+1):v-=ad;d.append([i,round(ad,2),round(max(0,v),2)]) df=pd.DataFrame(d,columns=['Year','Depreciation','Book Value']) total_dep=df['Depreciation'].sum() years = df['Year'].tolist() dep_vals = df['Depreciation'].tolist() bv_vals = df['Book Value'].tolist() fig=go.Figure() fig.add_trace(go.Bar( x=years, y=dep_vals, name='Annual depreciation', marker=dict(color='#BF0A30', cornerradius=8), opacity=0.88, width=0.55, hovertemplate='Year %{x}
Depreciation: $%{y:,.2f}' )) fig.add_trace(go.Scatter( x=years, y=bv_vals, name='Book value', line=dict(color='#1A3C6E', width=3.5), mode='lines+markers', marker=dict(size=10, color='#1A3C6E', line=dict(color='white', width=2)), hovertemplate='Year %{x}
Book value: $%{y:,.2f}' )) header = ( f"ANNUAL DEPRECIATION" f"

" f"${ad:,.2f}" f"
" f"Over {y} year{'s' if y!=1 else ''} · Total: ${total_dep:,.0f}" ) fig.update_layout( title=dict(text=header, x=0.01, xanchor='left', y=0.99, yanchor='top', font=dict(family="DM Sans"), pad=dict(t=10, b=0)), template="plotly_white", height=520, font=dict(family="DM Sans", color="#334155", size=12), plot_bgcolor='rgba(244,246,250,0.6)', paper_bgcolor='rgba(0,0,0,0)', legend=dict(orientation='h', y=-0.15, x=0.5, xanchor='center', font=dict(size=12)), margin=dict(l=70, r=30, t=175, b=70), xaxis=dict(title='Year', showgrid=False, dtick=1, showline=True, linecolor='#DFE3EA', zeroline=False), yaxis=dict(title='', showgrid=True, gridcolor='rgba(0,0,0,0.05)', tickprefix='$', tickformat=',.0f', zeroline=False, rangemode='tozero'), hovermode='x unified', ) return df, fig def get_account_balances(): if general_ledger_df.empty: return pd.DataFrame(columns=['Account','Type','Normal','Balance','Status']),go.Figure() b=general_ledger_df.groupby('Account').apply(lambda x:x['Debit'].sum()-x['Credit'].sum()).reset_index();b.columns=['Account','Balance'] b['Type']=b['Account'].map(lambda a:COA_MAP.get(a,{}).get('type','Other')) b['Normal']=b['Account'].map(lambda a:COA_MAP.get(a,{}).get('normal','Debit')) b['Status']=b.apply(lambda r:'🟢 Up' if (r['Normal']=='Debit' and r['Balance']>=0) or (r['Normal']=='Credit' and r['Balance']<=0) else '🔴 Down',axis=1) db=b.copy();db['Balance']=b['Balance'].apply(lambda x:f"${abs(x):,.2f}") fig=go.Figure(go.Treemap(labels=b['Account'],parents=[""]*len(b),values=b['Balance'].abs(), marker=dict(colors=['#1A3C6E' if '🟢' in d else '#BF0A30' for d in b['Status']],line=dict(width=2,color='white'),cornerradius=5), textinfo='label+value',texttemplate='%{label}
$%{value:,.0f}',textfont=dict(size=12,family="DM Sans"))) fig.update_layout(title=dict(text="Balance Treemap",font=dict(size=17,family="DM Sans",color="#0F172A")), font=dict(family="DM Sans"),margin=dict(l=10,r=10,t=50,b=10),height=380,paper_bgcolor='rgba(0,0,0,0)') return db[['Account','Type','Normal','Balance','Status']],fig def export_to_excel(): if general_ledger_df.empty: raise gr.Error("No data.") p="Accounting_Report.xlsx" with pd.ExcelWriter(p,engine='openpyxl') as w: general_ledger_df.to_excel(w,index=False,sheet_name='Ledger') journal_entries_df.to_excel(w,index=False,sheet_name='Journal') get_coa_display().to_excel(w,index=False,sheet_name='COA') return p def rec_sale(d,ds,a): if not ds.strip(): raise gr.Error("⛔ Description required.") return add_journal_entry_from_table(d,ds,pd.DataFrame([{"Account":"Cash","Debit":a,"Credit":0},{"Account":"Revenue","Debit":0,"Credit":a}])) def rec_stock(d,ds,a): if not ds.strip(): raise gr.Error("⛔ Description required.") return add_journal_entry_from_table(d,ds,pd.DataFrame([{"Account":"Cash","Debit":a,"Credit":0},{"Account":"Common Stock","Debit":0,"Credit":a}])) def rec_borrow(d,ds,a): if not ds.strip(): raise gr.Error("⛔ Description required.") return add_journal_entry_from_table(d,ds,pd.DataFrame([{"Account":"Cash","Debit":a,"Credit":0},{"Account":"Notes Payable","Debit":0,"Credit":a}])) def rec_asset(d,ds,a): if not ds.strip(): raise gr.Error("⛔ Description required.") return add_journal_entry_from_table(d,ds,pd.DataFrame([{"Account":"Equipment","Debit":a,"Credit":0},{"Account":"Cash","Debit":0,"Credit":a}])) def toggle_tm(pw): if pw.strip()=="instructor2026": INSTRUCTOR_SETTINGS["show_transaction_manager"]=not INSTRUCTOR_SETTINGS["show_transaction_manager"] return f"✅ Transaction Manager: **{'VISIBLE' if INSTRUCTOR_SETTINGS['show_transaction_manager'] else 'HIDDEN'}**" return "❌ Incorrect password." # ══════════════════════════════════════════════ # 5. CSS — Editorial Luxury Theme (Unified Hero) # ══════════════════════════════════════════════ css = r""" @import url('https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;500&display=swap'); * { box-sizing: border-box; } :root { --ink:#0A1F3D; --navy:#1A3C6E; --navy-mid:#2B5797; --accent:#BF0A30; --accent-soft:#E84260; --gold:#C9A961; --parchment:#FAFBFD; --mist:#F4F6FA; --g200:#DFE3EA; --g500:#6B7A8D; --g700:#3A4555; --g900:#1A2332; } html, body { margin: 0 !important; padding: 0 !important; background: var(--mist) !important; } /* ════ OUTER CONTAINER — one white rounded card ════ */ /* Page body background matches hero so no visible white gap at edges */ html, body, gradio-app, .dark { background: #F4F6FA !important; } .gradio-container { font-family: 'DM Sans', -apple-system, sans-serif !important; color: var(--g900) !important; font-size: 14px !important; line-height: 1.55 !important; background: #fff !important; max-width: 1200px !important; margin: 24px auto !important; padding: 0 !important; border: none !important; border-radius: 14px !important; overflow: hidden !important; box-shadow: 0 12px 40px rgba(10,31,61,0.08) !important; } /* Gradio 5 wraps content in .main / .app / #component-0 — strip ALL their padding */ .gradio-container .main, .gradio-container .app, .gradio-container > .main, .gradio-container > .app, .gradio-container > div, .gradio-container > div > div, .gradio-container #component-0, .gradio-container > .contain, .gradio-container main, .gradio-container .fillable, .gradio-container .prose { padding: 0 !important; margin: 0 !important; gap: 0 !important; max-width: 100% !important; width: 100% !important; background: transparent !important; border: none !important; } /* Kill any leftover container padding from Gradio 5's layout grid */ .gradio-container .gradio-row, .gradio-container .gradio-column, .gradio-container > div > div > div { padding: 0 !important; margin: 0 !important; } /* Make SURE the hero block (first child after container strip) goes edge-to-edge. Target the gr.HTML wrapper and every possible Gradio 5 intermediate layer. */ .gradio-container .ai-hero-v2 { margin: 0 !important; border-radius: 0 !important; width: 100% !important; max-width: none !important; box-sizing: border-box !important; display: block !important; } /* Kill padding on the gr.HTML wrapper and all its layers that contain the hero */ .gradio-container .gradio-html, .gradio-container .gradio-html > div, .gradio-container .prose, .gradio-container div:has(> .ai-hero-v2), .gradio-container div:has(.ai-hero-v2) { padding: 0 !important; margin: 0 !important; background: transparent !important; width: 100% !important; max-width: none !important; border: none !important; box-shadow: none !important; } /* Belt-and-suspenders: anything that is a direct ancestor of the hero, strip its padding */ .gradio-container *:has(> .ai-hero-v2) { padding: 0 !important; margin: 0 !important; } /* Strip Gradio's default wrapper chrome */ .gradio-container .gr-panel, .gradio-container .gr-box, .gradio-container .gr-form, .gradio-container .block, .gradio-container .form, .gradio-container .panel, .gradio-container .contain, .gradio-container .wrap, .gradio-container div[id^="component-"], .gradio-container .tabitem, .gradio-container .tabs, .gradio-container .report-box, .gradio-container .gr-dataframe, .gradio-container .gradio-html, .gradio-container .gradio-markdown, .gradio-container .gradio-dataframe, .gradio-container .gradio-dropdown, .gradio-container .gradio-textbox, .gradio-container .gradio-number, .gradio-container .gradio-chatbot, .gradio-container .gradio-plot, .gradio-container .gradio-file, .gradio-container footer { border: none !important; box-shadow: none !important; outline: none !important; } /* ════ HIDE GRADIO'S AUTO-ADDED FOOTER AND TRAILING WHITESPACE ════ */ /* Gradio 5 appends its own