| import pandas as pd |
| import numpy as np |
| import gradio as gr |
|
|
| |
| |
| |
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| 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] |
|
|
| |
| |
| |
| 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."]}) |
|
|
| |
| |
| |
| 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!", |
| } |
|
|
| |
| |
| |
| 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://huggingface.co/proxy/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." |
| ) |
|
|
|
|
| |
| |
| |
| 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 |
| |
| 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: |
| 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 |
|
|
|
|
| |
| |
| |
| 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'): |
| |
| 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('.xlsx'): |
| filepath = filepath.rsplit('.', 1)[0] + '.xlsx' |
| df.to_excel(filepath, index=False, engine='openpyxl') |
| generated_files.append(filepath) |
| else: |
| |
| 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: |
| |
| 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: |
| |
| 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 |
| ) |
| |
| cleaned = re.sub(r'\n{3,}', '\n\n', cleaned) |
| return cleaned.strip() |
|
|
|
|
| |
| |
| |
| 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 = [] |
|
|
| |
| 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) |
|
|
| |
| 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_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." |
|
|
| |
| response = call_llm(full_message, history) |
| if not response: |
| response = local_kb_fallback(user_message.lower()) |
|
|
| |
| generated_files = parse_and_generate_files(response) |
| display_response = clean_response_for_display(response) |
|
|
| |
| 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 |
|
|
|
|
| |
| |
| |
| 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 = ('<svg width="10" height="10" viewBox="0 0 10 10" style="margin-right:2px;">' |
| '<path d="M2 5 L4 7 L8 3" stroke="#5DCAA5" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>' |
| '</svg>') if bal_ok else '<span style="margin-right:2px;">✗</span>' |
| bal_label = 'In balance' if bal_ok else 'Off balance' |
| return ( |
| f'<div class="eq-cards">' |
| f' <div class="eq-card"><div class="eq-lbl">Assets</div><div class="eq-val">${a:,.2f}</div></div>' |
| f' <div class="eq-op">=</div>' |
| f' <div class="eq-card"><div class="eq-lbl">Liabilities</div><div class="eq-val">${l:,.2f}</div></div>' |
| f' <div class="eq-op">+</div>' |
| f' <div class="eq-card"><div class="eq-lbl">Equity</div><div class="eq-val">${te:,.2f}</div></div>' |
| f' <div class="{bal_cls}">{bal_icon} {bal_label}</div>' |
| f'</div>' |
| ) |
|
|
| 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"<b>{r['Account']}</b><br>{r['Type']}<br>${abs(r['Net']):,.2f}<extra></extra>")) |
| 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() |
|
|
| |
| cum_principal = df['Principal'].cumsum().tolist() |
| cum_interest = df['Interest'].cumsum().tolist() |
| periods = df['Period'].tolist() |
|
|
| fig=go.Figure() |
| |
| 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}<br>Interest: $%{y:,.2f}<extra></extra>' |
| )) |
| |
| 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}<br>Principal: $%{y:,.2f}<extra></extra>' |
| )) |
|
|
| header = ( |
| f"<span style='color:#8990A1;font-size:10px;letter-spacing:2px;font-weight:600;'>MONTHLY PAYMENT</span>" |
| f"<br> <br>" |
| f"<span style='color:#0A1F3D;font-size:28px;font-family:Libre Baskerville,Georgia,serif;font-weight:400;'>${pmt:,.2f}</span>" |
| f"<br>" |
| f"<span style='color:#6B7A8D;font-size:11px;font-weight:400;'>Total interest: ${total_int:,.0f}</span>" |
| ) |
|
|
| 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}<br>Depreciation: $%{y:,.2f}<extra></extra>' |
| )) |
| 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}<br>Book value: $%{y:,.2f}<extra></extra>' |
| )) |
|
|
| header = ( |
| f"<span style='color:#8990A1;font-size:10px;letter-spacing:2px;font-weight:600;'>ANNUAL DEPRECIATION</span>" |
| f"<br> <br>" |
| f"<span style='color:#0A1F3D;font-size:28px;font-family:Libre Baskerville,Georgia,serif;font-weight:400;'>${ad:,.2f}</span>" |
| f"<br>" |
| f"<span style='color:#6B7A8D;font-size:11px;font-weight:400;'>Over {y} year{'s' if y!=1 else ''} · Total: ${total_dep:,.0f}</span>" |
| ) |
|
|
| 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='<b>%{label}</b><br>$%{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." |
|
|
| |
| |
| |
| 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 <footer> + attribution + version info below |
| the main content, which creates large empty whitespace below our custom |
| footer. Kill all of it completely. */ |
| .gradio-container > footer, |
| .gradio-container > div > footer, |
| gradio-app > footer, |
| gradio-app footer.svelte-1sngr9j, |
| .gradio-container footer.svelte-1sngr9j, |
| footer[class*="svelte"], |
| body > footer, |
| .built-with-gradio, |
| .api-button, |
| .show-api, |
| a[href*="gradio.app"], |
| a[href*="huggingface.co"][class*="footer"] { |
| display: none !important; |
| visibility: hidden !important; |
| height: 0 !important; |
| max-height: 0 !important; |
| min-height: 0 !important; |
| overflow: hidden !important; |
| padding: 0 !important; |
| margin: 0 !important; |
| border: none !important; |
| } |
| |
| /* Ensure the very last block (our custom footer) has no trailing spacer */ |
| .gradio-container > div:last-child, |
| .gradio-container > *:last-child { |
| margin-bottom: 0 !important; |
| padding-bottom: 0 !important; |
| } |
| |
| /* Kill any "gap" on the root flex/grid container that adds bottom spacing */ |
| .gradio-container .main, |
| .gradio-container .app, |
| .gradio-container > div { |
| gap: 0 !important; |
| row-gap: 0 !important; |
| } |
| |
| /* ════ HERO SHELL — nuclear option: kill every possible gap ════ */ |
| .hero-shell, |
| .gradio-container .hero-shell { |
| border-radius: 0 !important; |
| overflow: hidden !important; |
| background: #0A1F3D !important; |
| padding: 0 !important; |
| margin: 0 !important; |
| gap: 0 !important; |
| row-gap: 0 !important; |
| column-gap: 0 !important; |
| border: none !important; |
| box-shadow: none !important; |
| display: flex !important; |
| flex-direction: column !important; |
| } |
| .hero-shell *, |
| .hero-shell > *, |
| .hero-shell > div, |
| .hero-shell > div > div, |
| .hero-shell .form, |
| .hero-shell .block, |
| .hero-shell .html-container, |
| .hero-shell [data-testid="html"], |
| .hero-shell .gradio-html, |
| .hero-shell .prose, |
| .hero-shell [class*="styler"], |
| .hero-shell [class*="wrap"], |
| .hero-shell [class*="block"] { |
| margin: 0 !important; |
| padding-top: 0 !important; |
| padding-bottom: 0 !important; |
| border-radius: 0 !important; |
| min-height: 0 !important; |
| gap: 0 !important; |
| row-gap: 0 !important; |
| box-shadow: none !important; |
| border-top: none !important; |
| border-bottom: none !important; |
| } |
| .hero-shell .ai-hero { |
| padding: 34px 32px 18px !important; |
| background: #0A1F3D !important; |
| } |
| .hero-shell .eq-bar { |
| padding: 0 32px 26px 32px !important; |
| background: #0A1F3D !important; |
| } |
| |
| /* When JS moves .eq-bar inside .ai-hero, style it as an integral part of the hero */ |
| .ai-hero .eq-bar { |
| margin: 18px 0 0 0 !important; |
| padding: 0 !important; |
| background: transparent !important; |
| position: relative !important; |
| z-index: 3 !important; |
| } |
| .ai-hero .eq-bar > *, |
| .ai-hero .eq-bar > div { |
| background: transparent !important; |
| padding: 0 !important; |
| margin: 0 !important; |
| border: none !important; |
| } |
| |
| /* ════ HERO ════ */ |
| .ai-hero { |
| position: relative; |
| background: #0A1F3D !important; |
| color: #fff !important; |
| padding: 34px 32px 18px !important; |
| overflow: hidden !important; |
| border-radius: 0 !important; |
| } |
| .ai-hero::before { |
| content: ''; position: absolute; inset: 0; |
| background: |
| radial-gradient(ellipse 600px 320px at 85% 15%, rgba(191,10,48,0.2), transparent 60%), |
| radial-gradient(ellipse 500px 400px at 10% 100%, rgba(43,87,151,0.32), transparent 60%); |
| pointer-events: none; |
| } |
| .ai-hero::after { |
| content: ''; position: absolute; inset: 0; |
| background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.04) 1px, transparent 0); |
| background-size: 24px 24px; |
| pointer-events: none; |
| } |
| .ai-hero-grid { |
| position: relative; z-index: 2; |
| display: grid; grid-template-columns: 120px 1fr auto; |
| gap: 22px; align-items: center; |
| } |
| |
| /* LOGO */ |
| .ai-logo { width: 112px; height: 112px; display: block; } |
| .ai-ring-outer { animation: ai-spin 55s linear infinite; transform-origin: 60px 60px; transform-box: fill-box; } |
| .ai-ring-inner { animation: ai-spin 22s linear infinite reverse; transform-origin: 60px 60px; transform-box: fill-box; } |
| @keyframes ai-spin { to { transform: rotate(360deg); } } |
| .ai-beam { animation: ai-sway 5s ease-in-out infinite; transform-origin: 60px 60px; transform-box: fill-box; } |
| @keyframes ai-sway { 0%,100% { transform: rotate(-1.5deg); } 50% { transform: rotate(1.5deg); } } |
| .ai-fulcrum { animation: ai-glow 2.5s ease-in-out infinite; transform-origin: 60px 42px; transform-box: fill-box; } |
| @keyframes ai-glow { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.3); } } |
| .ai-plumb { animation: ai-drop 4s ease-in-out infinite; } |
| @keyframes ai-drop { 0%,100% { transform: translateY(0); opacity: 0.6; } 50% { transform: translateY(2px); opacity: 1; } } |
| .ai-digit { opacity: 0; animation: ai-rise 3.5s ease-out infinite; } |
| .ai-d1 { animation-delay: 0s; } |
| .ai-d2 { animation-delay: 0.88s; } |
| .ai-d3 { animation-delay: 1.75s; } |
| .ai-d4 { animation-delay: 2.63s; } |
| @keyframes ai-rise { 0% { opacity: 0; transform: translateY(4px); } 25% { opacity: 0.9; } 75% { opacity: 0.9; } 100% { opacity: 0; transform: translateY(-8px); } } |
| |
| /* BRAND */ |
| .ai-brand .kicker { font-size: 10.5px; letter-spacing: 3.5px; text-transform: uppercase; color: rgba(255,255,255,0.48); margin-bottom: 6px; font-weight: 500; } |
| .ai-brand h1 { font-family: 'Libre Baskerville', Georgia, serif !important; font-size: 30px !important; letter-spacing: -0.4px !important; line-height: 1.05 !important; margin: 0 !important; font-weight: 400 !important; color: #fff !important; } |
| .ai-brand h1 em { font-style: italic; color: rgba(255,255,255,0.65); } |
| .ai-brand .tag { font-size: 12.5px; color: rgba(255,255,255,0.58); margin-top: 7px; line-height: 1.5; } |
| |
| /* STATUS PILL */ |
| .ai-status { |
| display: inline-flex; align-items: center; gap: 8px; |
| background: rgba(255,255,255,0.08); border: 0.5px solid rgba(255,255,255,0.22); |
| padding: 7px 13px; border-radius: 999px; |
| font-size: 11.5px; letter-spacing: 0.5px; color: #ffffff !important; |
| font-weight: 500; |
| white-space: nowrap; |
| } |
| .ai-pulse-dot { width: 6px; height: 6px; border-radius: 50%; background: #5DCAA5; animation: ai-pulse 2s ease-in-out infinite; } |
| @keyframes ai-pulse { 0% { box-shadow: 0 0 0 0 rgba(93,202,165,0.5); } 70% { box-shadow: 0 0 0 9px rgba(93,202,165,0); } 100% { box-shadow: 0 0 0 0 rgba(93,202,165,0); } } |
| |
| /* ════ EQUATION BAR — force dark hero background even if Gradio ejects it ════ */ |
| .eq-bar, |
| .eq-bar > div, |
| .gradio-container .eq-bar, |
| .gradio-container .eq-bar > div { |
| background: #0A1F3D !important; |
| padding: 0 32px 26px 32px !important; |
| margin: 0 !important; |
| border: none !important; |
| border-radius: 0 !important; |
| position: relative !important; |
| z-index: 2 !important; |
| } |
| .eq-bar::before { |
| content: ''; |
| position: absolute; |
| inset: -40px 0 0 0; |
| background: |
| radial-gradient(ellipse 600px 320px at 85% -20%, rgba(191,10,48,0.2), transparent 60%), |
| radial-gradient(ellipse 500px 400px at 10% 120%, rgba(43,87,151,0.32), transparent 60%); |
| pointer-events: none; |
| z-index: 0; |
| } |
| .eq-bar::after { |
| content: ''; |
| position: absolute; |
| inset: -40px 0 0 0; |
| background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.04) 1px, transparent 0); |
| background-size: 24px 24px; |
| pointer-events: none; |
| z-index: 0; |
| } |
| .eq-bar > *, |
| .eq-bar .prose, |
| .eq-bar .md, |
| .eq-bar [data-testid="markdown"] { |
| background: transparent !important; |
| border: none !important; |
| padding: 0 !important; |
| position: relative !important; |
| z-index: 2 !important; |
| } |
| .eq-bar p { margin: 0 !important; padding: 0 !important; } |
| |
| .eq-cards { |
| display: grid !important; |
| grid-template-columns: 1fr auto 1fr auto 1fr auto !important; |
| gap: 14px !important; |
| align-items: center !important; |
| padding: 14px 20px !important; |
| border: 1px solid rgba(255,255,255,0.18) !important; |
| border-radius: 12px !important; |
| background: rgba(255,255,255,0.04) !important; |
| margin: 0 !important; |
| position: relative !important; |
| z-index: 2 !important; |
| } |
| .eq-card { |
| display: flex !important; flex-direction: column !important; gap: 3px !important; |
| padding: 2px 0 !important; |
| } |
| .eq-lbl { |
| font-family: 'DM Sans', sans-serif !important; |
| font-size: 10px !important; font-weight: 500 !important; |
| letter-spacing: 2.2px !important; text-transform: uppercase !important; |
| color: rgba(255,255,255,0.45) !important; line-height: 1 !important; |
| } |
| .eq-val { |
| font-family: 'Libre Baskerville', Georgia, serif !important; |
| font-size: 19px !important; font-weight: 400 !important; |
| letter-spacing: -0.2px !important; color: #fff !important; |
| line-height: 1.2 !important; |
| } |
| .eq-op { |
| font-family: 'Libre Baskerville', serif !important; |
| font-size: 22px !important; color: rgba(255,255,255,0.4) !important; |
| text-align: center !important; padding: 0 !important; |
| } |
| .eq-pill { |
| display: inline-flex !important; align-items: center !important; justify-content: center !important; |
| gap: 7px !important; |
| padding: 6px 13px !important; |
| border-radius: 999px !important; |
| font-family: 'DM Sans', sans-serif !important; |
| font-size: 11px !important; font-weight: 500 !important; |
| white-space: nowrap !important; justify-self: end !important; |
| } |
| .eq-pill.bal { background: rgba(93,202,165,0.12) !important; border: 0.5px solid rgba(93,202,165,0.3) !important; color: #9FE1CB !important; } |
| .eq-pill.unbal { background: rgba(191,10,48,0.1) !important; border: 0.5px solid rgba(232,66,96,0.35) !important; color: #F5A8B4 !important; } |
| |
| /* Trim hero bottom padding — equation bar provides its own space */ |
| |
| @media (max-width: 680px) { |
| .eq-cards { grid-template-columns: 1fr !important; gap: 10px !important; text-align: center !important; } |
| .eq-op { display: none !important; } |
| .eq-pill { justify-self: center !important; } |
| } |
| |
| /* ════ TABS — animated red underline ════ */ |
| .tabs { background: #fff !important; border-radius: 0 !important; margin: 0 !important; } |
| /* Breathing room between hero and tabs — essential to match reference design */ |
| .gradio-container .tabs, |
| .gradio-container .gradio-tabs, |
| .gradio-container .tabs.svelte-710i53 { |
| margin-top: 0 !important; |
| padding-top: 0 !important; |
| background: #fff !important; |
| } |
| .gradio-container .tabs::before { |
| content: ''; |
| display: block; |
| height: 8px; |
| width: 100%; |
| background: #fff; |
| } |
| |
| .tabs > .tab-nav { |
| display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important; |
| border-bottom: 0.5px solid var(--g200) !important; |
| background: #fff !important; |
| border-radius: 0 !important; |
| padding: 20px 28px 0 !important; |
| gap: 8px !important; |
| scrollbar-width: none !important; |
| margin: 0 !important; |
| } |
| .tabs > .tab-nav::-webkit-scrollbar { display: none !important; } |
| button.tab-nav { |
| font-family: 'DM Sans', sans-serif !important; |
| font-weight: 500 !important; |
| font-size: 14px !important; |
| color: var(--g500) !important; |
| background: transparent !important; |
| border: none !important; |
| border-bottom: 2px solid transparent !important; |
| padding: 14px 16px !important; |
| white-space: nowrap !important; |
| flex-shrink: 0 !important; |
| transition: color 0.25s, border-color 0.25s !important; |
| letter-spacing: 0.1px !important; |
| display: inline-flex !important; |
| align-items: center !important; |
| gap: 8px !important; |
| box-shadow: none !important; |
| margin-bottom: -0.5px !important; |
| } |
| button.tab-nav:hover { color: var(--ink) !important; } |
| button.tab-nav.selected, |
| button[role="tab"][aria-selected="true"] { |
| color: var(--ink) !important; |
| border-bottom: 2.5px solid var(--accent) !important; |
| background: transparent !important; |
| font-weight: 600 !important; |
| } |
| |
| /* Tab icons are injected by JS (see HERO_JS_SCRIPT). The inline <span> |
| wrappers carry the styling, so we only need a selected-state rule here. */ |
| button.tab-nav.selected .tab-icon, |
| button[role="tab"][aria-selected="true"] .tab-icon { |
| opacity: 1 !important; |
| } |
| button.tab-nav:hover .tab-icon { |
| opacity: 0.95 !important; |
| } |
| |
| .tabitem { background: #fff !important; border-radius: 0 !important; padding: 32px 28px !important; margin-top: -1px !important; } |
| |
| /* ════ PANEL HEAD (kick + h2 + sub) ════ */ |
| .panel-head { margin-bottom: 22px; } |
| .panel-head .kick { font-size: 10.5px; letter-spacing: 2.2px; text-transform: uppercase; color: #8990A1; margin-bottom: 8px; font-weight: 500; } |
| .panel-head .h2 { font-family: 'Libre Baskerville', serif; font-size: 22px; letter-spacing: -0.3px; line-height: 1.2; margin: 0 0 8px; font-weight: 400; color: var(--ink); } |
| .panel-head .h2 em { font-style: italic; color: var(--g500); } |
| .panel-head .sub { font-size: 13.5px; color: var(--g500); line-height: 1.65; max-width: 68ch; margin: 0; } |
| |
| /* ════ INPUTS ════ */ |
| .gr-input, .gr-text-input, textarea, |
| input[type="text"], input[type="number"], input[type="password"], input[type="date"] { |
| background: #fff !important; |
| border: 0.5px solid var(--g200) !important; |
| border-radius: 8px !important; |
| color: var(--ink) !important; |
| font-family: 'DM Sans', sans-serif !important; |
| padding: 9px 12px !important; |
| font-size: 13.5px !important; |
| line-height: 1.5 !important; |
| transition: border-color 0.2s, box-shadow 0.2s !important; |
| } |
| input:focus, textarea:focus { |
| border-color: var(--navy) !important; |
| box-shadow: 0 0 0 3px rgba(26,60,110,0.08) !important; |
| outline: none !important; |
| } |
| |
| /* ════ BUTTONS ════ */ |
| .gr-button-primary, button.primary, button.lg.primary { |
| background: var(--ink) !important; |
| color: #fff !important; |
| font-family: 'DM Sans', sans-serif !important; |
| font-weight: 500 !important; |
| font-size: 13px !important; |
| border: 0.5px solid var(--ink) !important; |
| border-radius: 8px !important; |
| padding: 10px 18px !important; |
| transition: background 0.2s !important; |
| box-shadow: none !important; |
| } |
| .gr-button-primary:hover, button.primary:hover, button.lg.primary:hover { |
| background: #000 !important; |
| border-color: #000 !important; |
| } |
| .gr-button-secondary, button.secondary, button.lg.secondary { |
| background: #fff !important; |
| color: var(--ink) !important; |
| border: 0.5px solid var(--g200) !important; |
| border-radius: 8px !important; |
| font-weight: 500 !important; |
| font-size: 13px !important; |
| padding: 9px 16px !important; |
| transition: border-color 0.2s !important; |
| box-shadow: none !important; |
| } |
| .gr-button-secondary:hover, button.secondary:hover { border-color: var(--ink) !important; } |
| |
| /* ════ LABELS ════ */ |
| label, .gr-label, .block-label, span[data-testid="block-info"] { |
| font-family: 'DM Sans', sans-serif !important; |
| font-weight: 500 !important; |
| color: #8990A1 !important; |
| font-size: 10.5px !important; |
| text-transform: uppercase !important; |
| letter-spacing: 1.6px !important; |
| } |
| |
| /* ════ MARKDOWN ════ */ |
| .gr-markdown { color: var(--g700) !important; font-size: 13.5px !important; line-height: 1.65 !important; } |
| .gr-markdown p { color: var(--g700) !important; font-size: 13.5px !important; } |
| .gr-markdown h3 { |
| font-family: 'Libre Baskerville', Georgia, serif !important; |
| color: var(--ink) !important; |
| font-size: 17px !important; |
| font-weight: 400 !important; |
| letter-spacing: -0.2px !important; |
| } |
| .gr-markdown h4 { |
| font-family: 'DM Sans', sans-serif !important; |
| color: var(--navy) !important; |
| font-weight: 500 !important; |
| font-size: 13px !important; |
| text-transform: uppercase !important; |
| letter-spacing: 1.5px !important; |
| } |
| .gr-markdown strong { color: var(--ink) !important; font-weight: 500 !important; } |
| |
| /* ════ TABLES ════ */ |
| table thead th { |
| background: var(--ink) !important; |
| color: #fff !important; |
| font-family: 'DM Sans', sans-serif !important; |
| font-weight: 500 !important; |
| font-size: 10.5px !important; |
| text-transform: uppercase !important; |
| letter-spacing: 1.4px !important; |
| padding: 11px 14px !important; |
| border: none !important; |
| text-align: left !important; |
| } |
| table tbody td { |
| background: #fff !important; |
| color: var(--ink) !important; |
| padding: 10px 14px !important; |
| border: none !important; |
| border-bottom: 0.5px solid var(--g200) !important; |
| font-size: 13px !important; |
| font-family: 'DM Sans', sans-serif !important; |
| line-height: 1.5 !important; |
| } |
| table tbody tr:nth-child(even) td { background: var(--parchment) !important; } |
| table tbody tr:hover td { background: var(--mist) !important; } |
| table { border: none !important; border-collapse: collapse !important; } |
| |
| /* ════ INFO CARDS (for inside-tab callouts) ════ */ |
| .ic { |
| background: var(--mist) !important; |
| border: none !important; |
| border-radius: 10px !important; |
| padding: 14px 18px !important; |
| margin-bottom: 14px !important; |
| box-shadow: none !important; |
| border-left: 3px solid var(--ink) !important; |
| } |
| .ic h3 { |
| font-family: 'Libre Baskerville', Georgia, serif !important; |
| color: var(--ink) !important; |
| margin: 0 0 4px !important; |
| font-size: 15px !important; |
| font-weight: 400 !important; |
| letter-spacing: -0.2px !important; |
| } |
| .ic p { color: var(--g500) !important; margin: 0 !important; font-size: 12.5px !important; line-height: 1.55 !important; } |
| .ic.red { border-left-color: var(--accent) !important; } |
| .ic.grn { border-left-color: #1D9E75 !important; } |
| .ic.vio { border-left-color: #7F77DD !important; } |
| .ic.amb { border-left-color: #EF9F27 !important; } |
| |
| .report-box { padding: 14px !important; border-radius: 10px !important; background: var(--mist) !important; } |
| |
| .gr-dropdown, select { |
| background: #fff !important; |
| color: var(--ink) !important; |
| border: 0.5px solid var(--g200) !important; |
| border-radius: 8px !important; |
| font-size: 13.5px !important; |
| font-family: 'DM Sans', sans-serif !important; |
| } |
| |
| /* ════ CHATBOT ════ */ |
| .gr-chatbot { |
| font-size: 13.5px !important; |
| line-height: 1.6 !important; |
| background: var(--parchment) !important; |
| border-radius: 12px !important; |
| border: 0.5px solid var(--g200) !important; |
| } |
| .message.user, .user .message-row, [data-testid="user"] { |
| background: var(--ink) !important; |
| color: #fff !important; |
| } |
| .message.bot, .bot .message-row, [data-testid="bot"] { |
| background: #fff !important; |
| color: var(--ink) !important; |
| border: 0.5px solid var(--g200) !important; |
| } |
| |
| ::-webkit-scrollbar { width: 6px; height: 6px; } |
| ::-webkit-scrollbar-thumb { background: var(--g200); border-radius: 3px; } |
| ::-webkit-scrollbar-thumb:hover { background: var(--navy); } |
| |
| /* Ensure Plot components render with their configured height */ |
| .gr-plot, .gradio-plot, |
| .gr-plot > div, .gradio-plot > div { |
| min-height: 440px !important; |
| background: transparent !important; |
| } |
| .gr-plot .plotly-graph-div, .gradio-plot .plotly-graph-div, |
| .js-plotly-plot, .plot-container { |
| min-height: 420px !important; |
| width: 100% !important; |
| } |
| |
| .gr-dataframe { overflow-x: auto !important; max-width: 100% !important; } |
| /* Default dataframe: scrollable, taller by default so 15+ rows visible */ |
| .gr-dataframe .table-wrap, |
| .gr-dataframe > div:first-child, |
| .gradio-dataframe .table-wrap, |
| div[data-testid="dataframe"] .table-wrap, |
| div[data-testid="dataframe"] > div { |
| max-height: 640px !important; |
| overflow-y: auto !important; |
| } |
| |
| /* Tall dataframe — used for loan amortization schedule (60 rows). |
| Remove internal scroll entirely so the page scrolls naturally and |
| ALL 60 rows are visible inline. */ |
| .tall-df, |
| .tall-df > div, |
| .tall-df > div > div, |
| .tall-df .gr-dataframe, |
| .tall-df .gradio-dataframe, |
| .tall-df [data-testid="dataframe"] { |
| max-height: none !important; |
| height: auto !important; |
| overflow: visible !important; |
| } |
| .tall-df .table-wrap, |
| .tall-df .gr-dataframe .table-wrap, |
| .tall-df .gr-dataframe > div:first-child, |
| .tall-df div[data-testid="dataframe"] .table-wrap, |
| .tall-df div[data-testid="dataframe"] > div, |
| .tall-df div[data-testid="dataframe"] > div:first-child, |
| .tall-df > div > div:first-child { |
| max-height: none !important; |
| height: auto !important; |
| overflow-y: visible !important; |
| overflow: visible !important; |
| } |
| .tall-df table { |
| width: 100% !important; |
| height: auto !important; |
| } |
| .tall-df tbody tr { height: auto !important; } |
| |
| /* ════ FOOTER ════ */ |
| .footer-wrap, |
| .gradio-container .footer-wrap { |
| background: transparent !important; |
| padding: 0 !important; |
| margin: 0 !important; |
| border: none !important; |
| border-radius: 0 !important; |
| display: block !important; |
| width: 100% !important; |
| max-width: 100% !important; |
| overflow: visible !important; |
| min-height: 0 !important; |
| flex: 0 0 auto !important; |
| } |
| .footer-wrap > *, |
| .footer-wrap > div { |
| background: transparent !important; |
| padding: 0 !important; |
| margin: 0 !important; |
| border: none !important; |
| border-radius: 0 !important; |
| width: 100% !important; |
| max-width: 100% !important; |
| min-height: 0 !important; |
| } |
| .ft, div.ft { |
| background: var(--ink) !important; |
| color: rgba(255,255,255,0.8) !important; |
| padding: 28px 32px !important; |
| border-radius: 0 !important; |
| position: relative !important; |
| overflow: hidden !important; |
| width: 100% !important; |
| box-sizing: border-box !important; |
| display: block !important; |
| } |
| .ft::before { |
| content: ''; position: absolute; inset: 0; |
| background: radial-gradient(ellipse 500px 250px at 15% 30%, rgba(43,87,151,0.3), transparent 60%); |
| pointer-events: none; |
| } |
| .ft > * { position: relative; z-index: 1; } |
| .ft-grid { |
| display: grid !important; |
| grid-template-columns: auto 1fr auto !important; |
| gap: 26px !important; |
| align-items: center !important; |
| } |
| @media (max-width: 860px) { |
| .ft-grid { grid-template-columns: 1fr !important; text-align: center !important; gap: 14px !important; } |
| } |
| .ft-auth { font-size: 12.5px; line-height: 1.7; color: rgba(255,255,255,0.75); } |
| .ft-auth .nm { color: #fff; font-weight: 500; } |
| .ft-meta { font-size: 11px; color: rgba(255,255,255,0.55); letter-spacing: 0.6px; text-align: right; line-height: 1.8; } |
| @media (max-width: 860px) { .ft-meta { text-align: center; } } |
| |
| /* Footer logo animations — mirror the hero logo animations */ |
| .ft-logo-svg { flex-shrink: 0; } |
| .ft-logo-svg .ft-ring-outer { |
| animation: ft-spin 55s linear infinite; |
| transform-origin: 60px 60px; |
| transform-box: fill-box; |
| } |
| .ft-logo-svg .ft-ring-inner { |
| animation: ft-spin 22s linear infinite reverse; |
| transform-origin: 60px 60px; |
| transform-box: fill-box; |
| } |
| .ft-logo-svg .ft-beam { |
| animation: ft-sway 5s ease-in-out infinite; |
| transform-origin: 60px 60px; |
| transform-box: fill-box; |
| } |
| .ft-logo-svg .ft-fulcrum { |
| animation: ft-glow 2.5s ease-in-out infinite; |
| transform-origin: 60px 42px; |
| transform-box: fill-box; |
| } |
| @keyframes ft-spin { to { transform: rotate(360deg); } } |
| @keyframes ft-sway { |
| 0%, 100% { transform: rotate(-1.5deg); } |
| 50% { transform: rotate(1.5deg); } |
| } |
| @keyframes ft-glow { |
| 0%, 100% { opacity: 1; transform: scale(1); } |
| 50% { opacity: 0.7; transform: scale(1.3); } |
| } |
| @media (prefers-reduced-motion: reduce) { |
| .ft-logo-svg .ft-ring-outer, |
| .ft-logo-svg .ft-ring-inner, |
| .ft-logo-svg .ft-beam, |
| .ft-logo-svg .ft-fulcrum { |
| animation: none !important; |
| } |
| } |
| |
| /* ════ RESPONSIVE ════ */ |
| @media (max-width: 900px) { |
| .ai-hero-grid { grid-template-columns: 1fr !important; justify-items: center !important; text-align: center !important; } |
| .ai-brand h1 { font-size: 24px !important; } |
| .gradio-container { margin: 12px auto !important; } |
| } |
| @media (max-width: 680px) { |
| .ai-hero { padding: 24px 20px 14px !important; } |
| .eq-bar { padding: 0 20px 22px 20px !important; } |
| .ai-logo { width: 88px !important; height: 88px !important; } |
| .tabitem { padding: 18px 16px !important; } |
| button.tab-nav { font-size: 12px !important; padding: 13px 10px !important; } |
| .ft { padding: 22px 18px !important; } |
| } |
| |
| /* ════════════════════════════════════════════════════════ |
| HERO V2 — premium animated version |
| Aurora mesh gradient · Floating particles · Glass equation card |
| ════════════════════════════════════════════════════════ */ |
| |
| /* Hide the Gradio-rendered eq-bar — hero has its own glass card now */ |
| .eq-bar { display: none !important; } |
| |
| /* Main hero container */ |
| .ai-hero-v2 { |
| position: relative; |
| background: |
| radial-gradient(ellipse 700px 400px at 95% 0%, rgba(120, 50, 80, 0.22), transparent 65%), |
| radial-gradient(ellipse 600px 500px at 0% 100%, rgba(43, 87, 151, 0.28), transparent 65%), |
| linear-gradient(135deg, #0A1F3D 0%, #0D2345 50%, #0A1F3D 100%); |
| color: #fff !important; |
| padding: 24px 32px 20px !important; |
| overflow: hidden !important; |
| isolation: isolate; |
| } |
| |
| @media (prefers-reduced-motion: reduce) { |
| .aurora-blob, |
| .hero-particles span, |
| .ai-ring-outer, .ai-ring-mid, .ai-ring-inner, .ai-satellite, |
| .ai-beam, .ai-fulcrum, .ai-fulcrum-glow, .ai-plumb, .ai-digit, |
| .kicker-dot, .ai-pulse-dot { |
| animation: none !important; |
| } |
| } |
| |
| /* Aurora blobs — large soft color washes that drift */ |
| .hero-aurora { |
| position: absolute; inset: 0; pointer-events: none; |
| overflow: hidden; z-index: 0; |
| } |
| .aurora-blob { |
| position: absolute; |
| width: 460px; height: 460px; |
| border-radius: 50%; |
| filter: blur(100px); |
| opacity: 0.18; |
| mix-blend-mode: screen; |
| will-change: transform; |
| transform: translateZ(0); |
| } |
| .aurora-blob.a1 { top: -160px; left: 65%; background: #5A1B36; animation: aur-float-1 28s ease-in-out infinite alternate; opacity: 0.22; } |
| .aurora-blob.a2 { bottom: -180px; left: 5%; background: #1F4780; animation: aur-float-2 32s ease-in-out infinite alternate; opacity: 0.35; } |
| .aurora-blob.a3 { top: 30%; left: 45%; background: #2B5797; animation: aur-float-3 36s ease-in-out infinite alternate; opacity: 0.15; } |
| @keyframes aur-float-1 { from { transform: translate(0, 0) scale(1); } to { transform: translate(-80px, 60px) scale(1.15); } } |
| @keyframes aur-float-2 { from { transform: translate(0, 0) scale(1); } to { transform: translate(100px, -40px) scale(1.1); } } |
| @keyframes aur-float-3 { from { transform: translate(0, 0) scale(1); } to { transform: translate(-60px, -90px) scale(1.08); } } |
| |
| /* Dot grid subtle overlay */ |
| .ai-hero-v2::before { |
| content: ''; position: absolute; inset: 0; |
| background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.05) 1px, transparent 0); |
| background-size: 22px 22px; |
| pointer-events: none; z-index: 1; |
| } |
| |
| /* Corner accent ticks — hidden to match reference design */ |
| .hero-ticks { display: none !important; } |
| |
| /* Floating particles — tiny glow dots that drift upward */ |
| .hero-particles { position: absolute; inset: 0; pointer-events: none; z-index: 1; } |
| .hero-particles span { |
| position: absolute; |
| left: var(--x); top: var(--y); |
| width: 3px; height: 3px; |
| border-radius: 50%; |
| background: rgba(255,255,255,0.6); |
| box-shadow: 0 0 8px rgba(255,255,255,0.5); |
| animation: particle-drift var(--dur) ease-in-out var(--d) infinite; |
| } |
| @keyframes particle-drift { |
| 0% { transform: translate(0, 0); opacity: 0; } |
| 15% { opacity: 0.9; } |
| 50% { transform: translate(-14px, -30px); opacity: 0.7; } |
| 85% { opacity: 0.5; } |
| 100% { transform: translate(-28px, -60px); opacity: 0; } |
| } |
| |
| /* Hero grid layout */ |
| .ai-hero-grid { |
| position: relative; z-index: 3; |
| display: grid; grid-template-columns: 118px 1fr auto; |
| gap: 22px; align-items: center; |
| animation: hero-fade-in 0.9s cubic-bezier(0.22, 1, 0.36, 1); |
| } |
| @keyframes hero-fade-in { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| /* LOGO */ |
| .logo-wrap { position: relative; transition: transform 0.4s ease; } |
| .logo-wrap:hover { transform: scale(1.04) rotate(-2deg); } |
| .ai-logo { width: 110px; height: 110px; display: block; filter: drop-shadow(0 4px 20px rgba(191,10,48,0.22)); } |
| .ai-ring-outer { animation: ai-spin 60s linear infinite; transform-origin: 60px 60px; transform-box: fill-box; } |
| .ai-ring-mid { animation: ai-spin 40s linear infinite reverse; transform-origin: 60px 60px; transform-box: fill-box; } |
| .ai-ring-inner { animation: ai-spin 22s linear infinite reverse; transform-origin: 60px 60px; transform-box: fill-box; } |
| .ai-satellite { animation: ai-spin 8s linear infinite; transform-origin: 60px 60px; transform-box: fill-box; } |
| @keyframes ai-spin { to { transform: rotate(360deg); } } |
| .ai-beam { animation: ai-sway 5s ease-in-out infinite; transform-origin: 60px 60px; transform-box: fill-box; } |
| @keyframes ai-sway { 0%,100% { transform: rotate(-1.8deg); } 50% { transform: rotate(1.8deg); } } |
| .ai-fulcrum { animation: ai-glow 2.2s ease-in-out infinite; transform-origin: 60px 42px; transform-box: fill-box; } |
| .ai-fulcrum-glow { animation: ai-glow-ring 2.2s ease-in-out infinite; transform-origin: 60px 42px; transform-box: fill-box; } |
| @keyframes ai-glow { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.75; transform: scale(1.35); } } |
| @keyframes ai-glow-ring { 0%,100% { opacity: 0.2; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.6); } } |
| .ai-plumb { animation: ai-drop 4s ease-in-out infinite; } |
| @keyframes ai-drop { 0%,100% { transform: translateY(0); opacity: 0.6; } 50% { transform: translateY(2px); opacity: 1; } } |
| .ai-digit { opacity: 0; animation: ai-rise 3.5s ease-out infinite; } |
| .ai-d1 { animation-delay: 0s; } |
| .ai-d2 { animation-delay: 0.88s; } |
| .ai-d3 { animation-delay: 1.75s; } |
| .ai-d4 { animation-delay: 2.63s; } |
| @keyframes ai-rise { 0% { opacity: 0; transform: translateY(4px); } 25% { opacity: 0.9; } 75% { opacity: 0.9; } 100% { opacity: 0; transform: translateY(-8px); } } |
| |
| /* BRAND */ |
| .ai-brand .kicker { |
| display: inline-flex; align-items: center; gap: 8px; |
| font-size: 10.5px; letter-spacing: 3.5px; text-transform: uppercase; |
| color: rgba(255,255,255,0.55); margin-bottom: 8px; font-weight: 500; |
| } |
| .kicker-dot { display: none !important; } |
| .ai-brand h1 { |
| font-family: 'Libre Baskerville', Georgia, serif !important; |
| font-size: 32px !important; letter-spacing: -0.5px !important; line-height: 1.05 !important; |
| margin: 0 !important; font-weight: 400 !important; color: #fff !important; |
| text-shadow: 0 2px 14px rgba(0,0,0,0.25); |
| } |
| .ai-brand h1 em { |
| font-style: italic; |
| background: linear-gradient(90deg, #fff 0%, rgba(255,255,255,0.55) 100%); |
| -webkit-background-clip: text; background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| /* Logo-echo letters: the A and g of "Accounting" pick up the logo's accent styling */ |
| .ai-brand h1 .hero-a { |
| display: inline-block; |
| position: relative; |
| font-size: 1.08em; |
| font-weight: 500; |
| letter-spacing: 0; |
| color: #ffffff !important; |
| -webkit-text-fill-color: #ffffff !important; |
| background: none !important; |
| text-shadow: 0 2px 14px rgba(0,0,0,0.35); |
| } |
| .ai-brand h1 .hero-a::after { |
| content: ''; |
| position: absolute; |
| bottom: 4px; left: 50%; |
| width: 3px; height: 3px; |
| border-radius: 50%; |
| background: #BF0A30; |
| transform: translateX(-50%); |
| box-shadow: 0 0 6px rgba(191,10,48,0.7); |
| animation: hero-a-glow 2.5s ease-in-out infinite; |
| } |
| @keyframes hero-a-glow { |
| 0%, 100% { opacity: 0.8; transform: translateX(-50%) scale(1); } |
| 50% { opacity: 1; transform: translateX(-50%) scale(1.4); } |
| } |
| .ai-brand h1 .hero-g { |
| display: inline-block; |
| font-style: italic; |
| color: #ffffff !important; |
| -webkit-text-fill-color: #ffffff !important; |
| background: none !important; |
| letter-spacing: -0.2px; |
| text-shadow: 0 2px 14px rgba(0,0,0,0.25); |
| } |
| @media (prefers-reduced-motion: reduce) { |
| .ai-brand h1 .hero-a::after { animation: none !important; } |
| } |
| .ai-brand .tag { |
| font-size: 13px; color: rgba(255,255,255,0.65); |
| margin-top: 8px; line-height: 1.55; |
| } |
| .tag-accent { |
| color: rgba(255,255,255,0.85); |
| font-weight: 500; |
| } |
| |
| /* STATUS PILL */ |
| .ai-status { |
| display: inline-flex; align-items: center; gap: 8px; |
| background: rgba(255,255,255,0.08); |
| backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); |
| border: 0.5px solid rgba(255,255,255,0.22); |
| padding: 8px 14px; border-radius: 999px; |
| font-size: 11.5px; letter-spacing: 0.5px; color: #ffffff !important; |
| font-weight: 500; |
| white-space: nowrap; |
| transition: transform 0.25s, background 0.25s; |
| } |
| .ai-status:hover { transform: translateY(-1px); background: rgba(255,255,255,0.14); } |
| .ai-pulse-dot { |
| width: 7px; height: 7px; border-radius: 50%; |
| background: #5DCAA5; |
| animation: ai-pulse 2s ease-in-out infinite; |
| } |
| @keyframes ai-pulse { |
| 0% { box-shadow: 0 0 0 0 rgba(93,202,165,0.55); } |
| 70% { box-shadow: 0 0 0 10px rgba(93,202,165,0); } |
| 100% { box-shadow: 0 0 0 0 rgba(93,202,165,0); } |
| } |
| |
| /* ════ GLASS EQUATION CARD ════ */ |
| .hero-eq { |
| position: relative; z-index: 3; |
| margin-top: 22px; |
| display: grid; |
| grid-template-columns: 1fr auto 1fr auto 1fr auto; |
| gap: 14px; align-items: center; |
| padding: 16px 22px; |
| border: 1px solid rgba(255,255,255,0.18); |
| border-radius: 14px; |
| background: |
| linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.03) 100%), |
| rgba(10,31,61,0.5); |
| backdrop-filter: blur(10px); |
| -webkit-backdrop-filter: blur(10px); |
| box-shadow: |
| inset 0 1px 0 rgba(255,255,255,0.08), |
| 0 8px 32px rgba(0,0,0,0.25); |
| animation: heq-slide-up 1s cubic-bezier(0.22, 1, 0.36, 1) 0.15s both; |
| transition: transform 0.3s, box-shadow 0.3s, border-color 0.3s; |
| will-change: transform; |
| } |
| .hero-eq:hover { |
| transform: translateY(-2px); |
| border-color: rgba(255,255,255,0.25); |
| box-shadow: |
| inset 0 1px 0 rgba(255,255,255,0.12), |
| 0 14px 44px rgba(0,0,0,0.3); |
| } |
| @keyframes heq-slide-up { |
| from { opacity: 0; transform: translateY(14px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| /* Shimmer line at the top of the glass card */ |
| .hero-eq::before { |
| content: ''; position: absolute; top: 0; left: 10%; right: 10%; height: 1px; |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); |
| opacity: 0.7; |
| } |
| |
| .heq-cell { display: flex; flex-direction: column; gap: 4px; padding: 2px 0; } |
| .heq-lbl { |
| font-family: 'DM Sans', sans-serif; |
| font-size: 10px; font-weight: 500; |
| letter-spacing: 2.4px; text-transform: uppercase; |
| color: rgba(255,255,255,0.55); line-height: 1; |
| } |
| .heq-val { |
| font-family: 'Libre Baskerville', Georgia, serif; |
| font-size: 22px; font-weight: 400; |
| letter-spacing: -0.3px; color: #fff; |
| line-height: 1.15; |
| transition: color 0.4s; |
| font-variant-numeric: tabular-nums; |
| } |
| .heq-val.flash { color: #5DCAA5; } |
| .heq-op { |
| font-family: 'Libre Baskerville', serif; |
| font-size: 24px; color: rgba(255,255,255,0.4); |
| text-align: center; padding: 0 4px; |
| font-weight: 300; |
| } |
| .heq-status { |
| display: inline-flex; align-items: center; justify-content: center; |
| gap: 7px; |
| padding: 7px 15px; |
| border-radius: 999px; |
| font-family: 'DM Sans', sans-serif; |
| font-size: 11.5px; font-weight: 500; |
| white-space: nowrap; justify-self: end; |
| transition: all 0.3s; |
| } |
| .heq-status.heq-bal { |
| background: rgba(93,202,165,0.18); |
| border: 0.5px solid rgba(93,202,165,0.45); |
| color: #ffffff !important; |
| } |
| .heq-status.heq-unbal { |
| background: rgba(191,10,48,0.2); |
| border: 0.5px solid rgba(232,66,96,0.5); |
| color: #ffffff !important; |
| animation: unbal-shake 0.5s ease-in-out; |
| } |
| @keyframes unbal-shake { |
| 0%,100% { transform: translateX(0); } |
| 25% { transform: translateX(-3px); } |
| 75% { transform: translateX(3px); } |
| } |
| |
| @media (max-width: 960px) { |
| .ai-hero-grid { gap: 16px; } |
| .ai-brand h1 { font-size: 28px !important; } |
| .hero-eq { gap: 10px; padding: 14px 18px; } |
| .heq-val { font-size: 20px; } |
| } |
| @media (max-width: 760px) { |
| .ai-hero-v2 { padding: 24px 20px 22px !important; } |
| .ai-hero-grid { |
| grid-template-columns: 80px 1fr !important; |
| gap: 14px; |
| } |
| .ai-logo { width: 80px !important; height: 80px !important; } |
| .ai-status { |
| grid-column: 1 / -1; |
| justify-self: start; |
| margin-top: 4px; |
| } |
| .ai-brand h1 { font-size: 22px !important; line-height: 1.1 !important; } |
| .ai-brand .tag { font-size: 12px; } |
| .hero-eq { |
| grid-template-columns: 1fr 1fr; |
| gap: 10px 14px; |
| padding: 14px 16px; |
| } |
| .heq-op { display: none; } |
| .heq-status { grid-column: 1 / -1; justify-self: center; } |
| .heq-val { font-size: 18px; } |
| .heq-lbl { font-size: 9.5px; letter-spacing: 2px; } |
| .aurora-blob { width: 320px; height: 320px; filter: blur(60px); } |
| .hero-ticks { width: 16px; height: 16px; } |
| } |
| @media (max-width: 440px) { |
| .ai-hero-grid { grid-template-columns: 1fr !important; justify-items: center !important; text-align: center !important; } |
| .ai-brand { text-align: center; } |
| .ai-logo { width: 72px !important; height: 72px !important; } |
| .ai-brand h1 { font-size: 20px !important; } |
| .hero-eq { grid-template-columns: 1fr; gap: 8px; text-align: center; } |
| } |
| |
| |
| |
| """ |
|
|
| |
| |
| |
| |
| |
| HERO_HTML = """ |
| <div class="ai-hero-v2"> |
| <div class="hero-aurora"> |
| <div class="aurora-blob a1"></div> |
| <div class="aurora-blob a2"></div> |
| <div class="aurora-blob a3"></div> |
| </div> |
| <div class="hero-particles"> |
| <span style="--x:8%; --y:20%; --d:0s; --dur:14s;"></span> |
| <span style="--x:22%; --y:65%; --d:2s; --dur:17s;"></span> |
| <span style="--x:38%; --y:12%; --d:4s; --dur:13s;"></span> |
| <span style="--x:55%; --y:78%; --d:1.5s; --dur:18s;"></span> |
| <span style="--x:68%; --y:35%; --d:3.5s; --dur:15s;"></span> |
| <span style="--x:82%; --y:58%; --d:2.8s; --dur:16s;"></span> |
| <span style="--x:92%; --y:22%; --d:0.8s; --dur:14.5s;"></span> |
| </div> |
| <div class="hero-ticks tl"></div> |
| <div class="hero-ticks tr"></div> |
| <div class="hero-ticks bl"></div> |
| <div class="hero-ticks br"></div> |
| |
| <div class="ai-hero-grid"> |
| <div class="logo-wrap"> |
| <svg class="ai-logo" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" aria-label="Accounting Intelligence"> |
| <defs> |
| <radialGradient id="ai-glow" cx="50%" cy="50%" r="50%"> |
| <stop offset="0%" stop-color="#BF0A30" stop-opacity="0.32"/> |
| <stop offset="60%" stop-color="#BF0A30" stop-opacity="0"/> |
| </radialGradient> |
| <radialGradient id="ai-aura" cx="50%" cy="50%" r="50%"> |
| <stop offset="0%" stop-color="#5DCAA5" stop-opacity="0.22"/> |
| <stop offset="100%" stop-color="#5DCAA5" stop-opacity="0"/> |
| </radialGradient> |
| <linearGradient id="ai-beam-grad" x1="0" y1="0" x2="1" y2="0"> |
| <stop offset="0" stop-color="#fff" stop-opacity="0"/> |
| <stop offset="0.5" stop-color="#fff" stop-opacity="1"/> |
| <stop offset="1" stop-color="#fff" stop-opacity="0"/> |
| </linearGradient> |
| <path id="ai-arc" d="M 18 82 Q 60 100 102 82" fill="none"/> |
| </defs> |
| <circle cx="60" cy="60" r="38" fill="url(#ai-aura)"/> |
| <circle cx="60" cy="60" r="30" fill="url(#ai-glow)"/> |
| <g class="ai-ring-outer"> |
| <circle cx="60" cy="60" r="54" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="0.6" stroke-dasharray="2 6"/> |
| <circle cx="60" cy="6" r="1.3" fill="#fff"/> |
| <circle cx="114" cy="60" r="1" fill="rgba(255,255,255,0.6)"/> |
| <circle cx="60" cy="114" r="1.5" fill="#BF0A30"/> |
| <circle cx="6" cy="60" r="1" fill="rgba(255,255,255,0.6)"/> |
| </g> |
| <g class="ai-ring-mid"> |
| <circle cx="60" cy="60" r="49" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="0.4" stroke-dasharray="1 3"/> |
| </g> |
| <g class="ai-ring-inner"> |
| <circle cx="60" cy="60" r="45" fill="none" stroke="rgba(255,255,255,0.14)" stroke-width="0.5"/> |
| <circle cx="60" cy="15" r="0.9" fill="rgba(255,255,255,0.8)"/> |
| <circle cx="105" cy="60" r="0.9" fill="rgba(255,255,255,0.8)"/> |
| <circle cx="60" cy="105" r="0.9" fill="rgba(255,255,255,0.8)"/> |
| <circle cx="15" cy="60" r="0.9" fill="rgba(255,255,255,0.8)"/> |
| </g> |
| <g class="ai-satellite"> |
| <circle cx="100" cy="60" r="1.5" fill="#5DCAA5"/> |
| <circle cx="100" cy="60" r="3" fill="#5DCAA5" opacity="0.3"/> |
| </g> |
| <g class="ai-plumb"> |
| <line x1="60" y1="20" x2="60" y2="26" stroke="rgba(255,255,255,0.4)" stroke-width="0.6"/> |
| <circle cx="60" cy="26" r="1" fill="#fff"/> |
| </g> |
| <g class="ai-beam"> |
| <line x1="22" y1="60" x2="98" y2="60" stroke="url(#ai-beam-grad)" stroke-width="0.9"/> |
| <line x1="60" y1="60" x2="60" y2="42" stroke="#fff" stroke-width="0.8"/> |
| <rect x="17" y="54" width="16" height="14" fill="none" stroke="#fff" stroke-width="1.4" rx="1.5"/> |
| <text class="ai-digit ai-d1" x="25" y="63" text-anchor="middle" fill="#5DCAA5" font-family="IBM Plex Mono,monospace" font-size="5">1000</text> |
| <text class="ai-digit ai-d2" x="25" y="63" text-anchor="middle" fill="#5DCAA5" font-family="IBM Plex Mono,monospace" font-size="5">2500</text> |
| <text class="ai-digit ai-d3" x="25" y="63" text-anchor="middle" fill="#5DCAA5" font-family="IBM Plex Mono,monospace" font-size="5">450</text> |
| <text class="ai-digit ai-d4" x="25" y="63" text-anchor="middle" fill="#5DCAA5" font-family="IBM Plex Mono,monospace" font-size="5">3200</text> |
| <text x="25" y="74" text-anchor="middle" fill="rgba(255,255,255,0.55)" font-family="Georgia,serif" font-size="4.5" font-style="italic">assets</text> |
| <rect x="87" y="54" width="16" height="14" fill="none" stroke="#fff" stroke-width="1.4" rx="1.5"/> |
| <text class="ai-digit ai-d2" x="95" y="63" text-anchor="middle" fill="#EF9F27" font-family="IBM Plex Mono,monospace" font-size="5">800</text> |
| <text class="ai-digit ai-d3" x="95" y="63" text-anchor="middle" fill="#EF9F27" font-family="IBM Plex Mono,monospace" font-size="5">1700</text> |
| <text class="ai-digit ai-d4" x="95" y="63" text-anchor="middle" fill="#EF9F27" font-family="IBM Plex Mono,monospace" font-size="5">450</text> |
| <text class="ai-digit ai-d1" x="95" y="63" text-anchor="middle" fill="#EF9F27" font-family="IBM Plex Mono,monospace" font-size="5">2500</text> |
| <text x="95" y="74" text-anchor="middle" fill="rgba(255,255,255,0.55)" font-family="Georgia,serif" font-size="4.5" font-style="italic">L + E</text> |
| </g> |
| <circle class="ai-fulcrum-glow" cx="60" cy="42" r="6" fill="#BF0A30" opacity="0.25"/> |
| <circle class="ai-fulcrum" cx="60" cy="42" r="2.5" fill="#BF0A30"/> |
| <text fill="rgba(255,255,255,0.8)" font-family="Georgia,serif" font-size="6.5" letter-spacing="2" font-style="italic"> |
| <textPath href="#ai-arc" startOffset="50%" text-anchor="middle">A C C O U N T I N G</textPath> |
| </text> |
| </svg> |
| </div> |
| |
| <div class="ai-brand"> |
| <div class="kicker"><span class="kicker-dot"></span>SUNY Polytechnic Institute</div> |
| <h1><span class="hero-a">A</span>ccountin<span class="hero-g">g</span> <em>Intelligence</em></h1> |
| <div class="tag">A living classroom for double-entry bookkeeping — <span class="tag-accent">powered by Claude</span></div> |
| </div> |
| |
| <div class="ai-status"> |
| <span class="ai-pulse-dot"></span> |
| <span>Ledger live</span> |
| </div> |
| </div> |
| |
| <div class="hero-eq"> |
| <div class="heq-cell"> |
| <div class="heq-lbl">Assets</div> |
| <div class="heq-val" id="hero-assets">$0.00</div> |
| </div> |
| <div class="heq-op">=</div> |
| <div class="heq-cell"> |
| <div class="heq-lbl">Liabilities</div> |
| <div class="heq-val" id="hero-liab">$0.00</div> |
| </div> |
| <div class="heq-op">+</div> |
| <div class="heq-cell"> |
| <div class="heq-lbl">Equity</div> |
| <div class="heq-val" id="hero-equity">$0.00</div> |
| </div> |
| <div class="heq-status heq-bal" id="hero-status"> |
| <svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 5 L4 7 L8 3" stroke="#5DCAA5" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg> |
| <span>In balance</span> |
| </div> |
| </div> |
| </div> |
| |
| """ |
|
|
| |
| |
| |
| HERO_JS_SCRIPT = """ |
| <script> |
| (function initHeroSync() { |
| function parseMoney(s) { |
| if (!s) return 0; |
| var neg = /^-|−/.test(s.trim()); |
| var n = parseFloat(s.replace(/[^0-9.-]/g, '')) || 0; |
| return neg && n > 0 ? -n : n; |
| } |
| function fmtMoney(n) { |
| return '$' + n.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}); |
| } |
| function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); } |
| |
| var activeAnims = {}; |
| function animateCounter(id, targetVal, duration) { |
| var el = document.getElementById(id); |
| if (!el) return; |
| var startVal = parseMoney(el.textContent); |
| if (Math.abs(startVal - targetVal) < 0.005) return; |
| if (activeAnims[id]) cancelAnimationFrame(activeAnims[id]); |
| el.classList.add('flash'); |
| setTimeout(function(){ el.classList.remove('flash'); }, 600); |
| var start = performance.now(); |
| function tick(now) { |
| var t = Math.min(1, (now - start) / duration); |
| var eased = easeOutCubic(t); |
| var current = startVal + (targetVal - startVal) * eased; |
| el.textContent = fmtMoney(current); |
| if (t < 1) { |
| activeAnims[id] = requestAnimationFrame(tick); |
| } else { |
| el.textContent = fmtMoney(targetVal); |
| delete activeAnims[id]; |
| } |
| } |
| activeAnims[id] = requestAnimationFrame(tick); |
| } |
| |
| function syncFromEqBar() { |
| try { |
| var eqBar = document.querySelector('.eq-bar'); |
| if (!eqBar) return; |
| var cards = eqBar.querySelectorAll('.eq-card'); |
| if (cards.length >= 3) { |
| var a = parseMoney(cards[0].querySelector('.eq-val').textContent); |
| var l = parseMoney(cards[1].querySelector('.eq-val').textContent); |
| var e = parseMoney(cards[2].querySelector('.eq-val').textContent); |
| animateCounter('hero-assets', a, 900); |
| animateCounter('hero-liab', l, 900); |
| animateCounter('hero-equity', e, 900); |
| } |
| var pill = eqBar.querySelector('.eq-pill'); |
| var heroStatus = document.getElementById('hero-status'); |
| if (pill && heroStatus) { |
| var isBal = pill.classList.contains('bal'); |
| heroStatus.className = 'heq-status ' + (isBal ? 'heq-bal' : 'heq-unbal'); |
| heroStatus.innerHTML = isBal |
| ? '<svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 5 L4 7 L8 3" stroke="#5DCAA5" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg><span>In balance</span>' |
| : '<span>✗</span><span>Off balance</span>'; |
| } |
| } catch (e) {} |
| } |
| |
| [0, 120, 400, 1000, 2500].forEach(function(delay){ setTimeout(syncFromEqBar, delay); }); |
| |
| try { |
| var debounceTimer = null; |
| function scheduleSync() { |
| if (debounceTimer) clearTimeout(debounceTimer); |
| debounceTimer = setTimeout(syncFromEqBar, 150); |
| } |
| function attachObserver() { |
| var eqBar = document.querySelector('.eq-bar'); |
| if (!eqBar) { setTimeout(attachObserver, 500); return; } |
| var obs = new MutationObserver(scheduleSync); |
| obs.observe(eqBar, { childList: true, subtree: true }); |
| } |
| attachObserver(); |
| } catch (e) {} |
| |
| // ════ INJECT TAB ICONS ════ |
| // Gradio's <button class="tab-nav"> doesn't support HTML in labels, so we |
| // insert SVG icons as the first child of each tab button via JS. |
| var TAB_ICONS = { |
| 'Chart of accounts': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><path d="M5 4h10a3 3 0 0 1 3 3v13H8a3 3 0 0 1-3-3V4z"/><path d="M5 17a3 3 0 0 1 3-3h10"/><line x1="9" y1="8" x2="14" y2="8"/><line x1="9" y1="11" x2="14" y2="11"/></svg>', |
| 'Journal entries': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><path d="M6 3h12v18l-6-4-6 4V3z"/></svg>', |
| 'Transaction manager': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><polyline points="7 4 3 8 7 12"/><line x1="3" y1="8" x2="17" y2="8"/><polyline points="17 12 21 16 17 20"/><line x1="21" y1="16" x2="7" y2="16"/></svg>', |
| 'Reports': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><line x1="5" y1="21" x2="5" y2="13"/><line x1="12" y1="21" x2="12" y2="7"/><line x1="19" y1="21" x2="19" y2="15"/></svg>', |
| 'Loan': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><polyline points="3 17 9 11 13 15 21 7"/><polyline points="14 7 21 7 21 14"/></svg>', |
| 'Depreciation': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><polyline points="3 7 9 13 13 9 21 17"/><polyline points="14 17 21 17 21 10"/></svg>', |
| 'General ledger': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3.5" y1="6" x2="3.51" y2="6"/><line x1="3.5" y1="12" x2="3.51" y2="12"/><line x1="3.5" y1="18" x2="3.51" y2="18"/></svg>', |
| 'AI tutor': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>' |
| }; |
| |
| function injectTabIcons() { |
| try { |
| var tabs = document.querySelectorAll('button.tab-nav, button[role="tab"]'); |
| tabs.forEach(function(btn) { |
| if (btn.dataset.iconDone === '1') return; |
| var label = (btn.textContent || '').trim(); |
| var svg = TAB_ICONS[label]; |
| if (!svg) return; |
| var wrap = document.createElement('span'); |
| wrap.className = 'tab-icon'; |
| wrap.style.cssText = 'display:inline-flex;align-items:center;margin-right:7px;vertical-align:middle;opacity:0.78;color:#1a2332;'; |
| wrap.innerHTML = svg; |
| btn.insertBefore(wrap, btn.firstChild); |
| btn.dataset.iconDone = '1'; |
| }); |
| } catch (e) {} |
| } |
| |
| [0, 150, 500, 1200, 2500, 4000].forEach(function(d){ setTimeout(injectTabIcons, d); }); |
| try { |
| var tabObs = new MutationObserver(function(){ setTimeout(injectTabIcons, 50); }); |
| tabObs.observe(document.body, { childList: true, subtree: true }); |
| } catch (e) {} |
| })(); |
| </script> |
| """ |
|
|
| with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Accounting Intelligence — SUNY Poly", analytics_enabled=False) as demo: |
|
|
| gr.HTML(HERO_HTML + HERO_JS_SCRIPT) |
|
|
| equation_display = gr.HTML(update_equation_status(), elem_classes=["eq-bar"]) |
|
|
| with gr.Tabs(): |
|
|
| with gr.Tab("Chart of accounts", elem_classes=["tab-coa"]): |
| gr.HTML("""<div class="panel-head"> |
| <div class="kick">Foundation · Chart of accounts</div> |
| <h2 class="h2">Every account, <em>grouped the way accountants think.</em></h2> |
| <p class="sub">Debit-normal on the left of every T. Credit-normal on the right. Search, filter, add new accounts — codes auto-assign.</p> |
| </div>""") |
| with gr.Row(): |
| with gr.Column(scale=3): |
| coa_search=gr.Textbox(label="🔍 Search",placeholder="salary, cash, expense...",interactive=True) |
| coa_table=gr.DataFrame(value=get_coa_display(),label="Accounts",interactive=False,wrap=True) |
| with gr.Column(scale=2): |
| gr.HTML('<div class="ic grn"><h3>➕ Add Account</h3><p>New account? Define type, sub-type, and normal balance.</p></div>') |
| na_name=gr.Textbox(label="Name",placeholder="e.g., Advertising Expense") |
| na_type=gr.Dropdown(label="Type",choices=["Asset","Liability","Equity","Revenue","Expense"],value="Expense") |
| na_sub=gr.Textbox(label="Sub-Type",placeholder="Operating Expense") |
| na_norm=gr.Dropdown(label="Normal Balance",choices=["Debit","Credit"],value="Debit") |
| na_btn=gr.Button("➕ Add",variant="primary"); na_msg=gr.Markdown("") |
| coa_search.change(search_accounts,[coa_search],[coa_table]) |
| na_btn.click(add_new_account,[na_name,na_type,na_sub,na_norm],[coa_table,na_msg]) |
|
|
| with gr.Tab("Journal entries", elem_classes=["tab-je"]): |
| with gr.Row(): |
| with gr.Column(scale=1,min_width=200): |
| gr.HTML('<div class="ic"><h3>Account lookup</h3><p>Search any account by name, type, or normal balance.</p></div>') |
| je_cs=gr.Textbox(label="🔍 Find Account",placeholder="salary, cash...",interactive=True) |
| je_ref=gr.DataFrame(value=get_coa_display()[['Account Name','Normal Bal.']],label="Accounts",interactive=False,wrap=True) |
| je_cs.change(search_accounts_short,[je_cs],[je_ref]) |
| with gr.Column(scale=2): |
| gr.HTML("""<div class="panel-head" style="margin-bottom:14px;"> |
| <div class="kick">Practice · Journal entries</div> |
| <h2 class="h2">Build the entry, <em>watch it balance in real time.</em></h2> |
| <p class="sub">Add lines. Type amounts. Total debits must equal total credits before you can post.</p> |
| </div>""") |
| with gr.Row(): |
| je_date=gr.Textbox(label="📅 Date",value=str(datetime.date.today())) |
| je_desc=gr.Textbox(label="📝 Description (Required)",placeholder="Paid rent for office") |
| je_tbl=gr.Dataframe(headers=["Account","Debit","Credit"],datatype=["str","number","number"], |
| row_count=(4,"dynamic"),label="Lines",interactive=True, |
| type="pandas",value=[["",0,0],["",0,0],["",0,0],["",0,0]],wrap=True) |
| with gr.Row(): |
| je_btn=gr.Button("📥 Post",variant="primary"); exp_btn=gr.Button("📂 Excel",variant="secondary") |
| exp_out=gr.File(label="Download"); je_p=gr.Plot(label="Impact") |
| with gr.Column(scale=1,min_width=200): |
| gr.HTML('<div class="ic"><h3>Entry log</h3><p>Every posted entry, timestamped. Reverse any entry by its JE number.</p></div>') |
| je_hist=gr.DataFrame(label="Log",interactive=False,wrap=True) |
| gr.HTML('<div class="ic red"><h3>Reverse entry</h3><p>Enter the JE number to reverse a posted entry.</p></div>') |
| del_in=gr.Textbox(label="JE #",placeholder="JE-0001"); del_btn=gr.Button("🗑️ Delete",variant="secondary"); del_msg=gr.Markdown("") |
|
|
| with gr.Tab("Transaction manager", elem_classes=["tab-tm"]): |
| gr.HTML("""<div class="panel-head"> |
| <div class="kick">Quick entry · Transactions</div> |
| <h2 class="h2">Common transactions in <em>one click.</em></h2> |
| <p class="sub">Four shortcuts for the most frequent classroom entries: sales, equity raises, borrowing, asset purchases.</p> |
| </div>""") |
| with gr.Row(): |
| i_pw=gr.Textbox(label="🔑 Password",type="password",placeholder="Password..."); i_btn=gr.Button("🔓 Toggle",variant="secondary") |
| i_msg=gr.Markdown(""); i_btn.click(toggle_tm,[i_pw],[i_msg]) |
| gr.HTML('<div class="ic amb"><h3>Quick actions</h3><p>Pick a preset, set the date and amount, write a one-line description, then fire.</p></div>') |
| with gr.Row(): |
| qd=gr.Textbox(label="Date",value=str(datetime.date.today())); qds=gr.Textbox(label="Description",placeholder="March sales"); qa=gr.Number(label="Amount",value=0) |
| with gr.Row(): |
| b1=gr.Button("💰 Sale",variant="primary"); b2=gr.Button("📈 Equity",variant="primary"); b3=gr.Button("🏦 Borrow",variant="primary"); b4=gr.Button("🏗️ Asset",variant="primary") |
| with gr.Row(): |
| tgl=gr.DataFrame(label="Ledger",wrap=True); tgp=gr.Plot(label="Impact") |
|
|
| with gr.Tab("Reports", elem_classes=["tab-rp"]): |
| gr.HTML("""<div class="panel-head"> |
| <div class="kick">Close · Financial reports</div> |
| <h2 class="h2">Five reports, <em>generated the way a CFO reads them.</em></h2> |
| <p class="sub">Income statement, balance sheet, equity, cash flow, trial balance — all update the instant an entry is posted.</p> |
| </div>""") |
| rp_btn=gr.Button("🔄 Generate",variant="primary") |
| with gr.Row(): |
| with gr.Column(elem_classes=["report-box"]): gr.Markdown("#### Income Statement"); is_o=gr.DataFrame(interactive=False,wrap=True) |
| with gr.Column(elem_classes=["report-box"]): gr.Markdown("#### Equity Statement"); se_o=gr.DataFrame(interactive=False,wrap=True) |
| with gr.Row(): |
| with gr.Column(elem_classes=["report-box"]): gr.Markdown("#### Balance Sheet"); bs_o=gr.DataFrame(interactive=False,wrap=True) |
| with gr.Column(elem_classes=["report-box"]): gr.Markdown("#### Cash Flow"); cf_o=gr.DataFrame(interactive=False,wrap=True) |
| gr.Markdown("#### ⚖️ Trial Balance"); tb_o=gr.DataFrame(interactive=False,wrap=True); rp_p=gr.Plot() |
|
|
| with gr.Tab("Loan", elem_classes=["tab-loan"]): |
| gr.HTML("""<div class="panel-head"> |
| <div class="kick">Schedule · Loan amortization</div> |
| <h2 class="h2">See where every dollar of your payment <em>actually goes.</em></h2> |
| <p class="sub">Principal and interest split out month-by-month. Watch the interest shrink as the balance falls.</p> |
| </div>""") |
| |
| _default_loan_df, _default_loan_fig = loan_ui(110000, 0.05, 60, "2025-10-01") |
| with gr.Row(): |
| with gr.Column(scale=1, min_width=260): |
| lp=gr.Number(label="Principal",value=110000) |
| lr=gr.Number(label="Annual rate",value=0.05) |
| lm=gr.Number(label="Term (months)",value=60) |
| ls=gr.Textbox(label="Start date",value="2025-10-01") |
| lb=gr.Button("Calculate schedule",variant="primary") |
| with gr.Column(scale=2): |
| lpl=gr.Plot(value=_default_loan_fig, label="",show_label=False) |
| ldf=gr.DataFrame(value=_default_loan_df, label="Amortization schedule",wrap=True,elem_classes=["tall-df"],max_height=3200) |
| lb.click(loan_ui,[lp,lr,lm,ls],[ldf,lpl]) |
|
|
| with gr.Tab("Depreciation", elem_classes=["tab-dep"]): |
| gr.HTML("""<div class="panel-head"> |
| <div class="kick">Accounting · Depreciation</div> |
| <h2 class="h2">Watch an asset's value <em>fall, year by year.</em></h2> |
| <p class="sub">Straight-line depreciation with running book value. The slope the auditors look for.</p> |
| </div>""") |
| |
| _default_dep_df, _default_dep_fig = asset_ui(110000, 60, "2026-03-04") |
| with gr.Row(): |
| with gr.Column(scale=1, min_width=260): |
| dc=gr.Number(label="Asset cost",value=110000) |
| dl=gr.Number(label="Useful life (months)",value=60) |
| dd=gr.Textbox(label="Start date",value="2026-03-04") |
| db=gr.Button("Calculate depreciation",variant="primary") |
| with gr.Column(scale=2): |
| dpl=gr.Plot(value=_default_dep_fig, label="",show_label=False) |
| ddf=gr.DataFrame(value=_default_dep_df, label="Depreciation schedule",interactive=False,wrap=True,elem_classes=["tall-df"],max_height=800) |
| db.click(asset_ui,[dc,dl,dd],[ddf,dpl]) |
|
|
| with gr.Tab("General ledger", elem_classes=["tab-gl"]): |
| gr.HTML("""<div class="panel-head"> |
| <div class="kick">Record · General ledger</div> |
| <h2 class="h2">Every transaction, <em>every account, one master view.</em></h2> |
| <p class="sub">The source of truth. Refresh to recompute balances and the status of each account.</p> |
| </div>""") |
| gld=gr.DataFrame(label="Ledger",wrap=True); glb=gr.Button("🔄 Refresh",variant="primary") |
| glbt=gr.DataFrame(label="Balances",interactive=False,wrap=True); glp=gr.Plot() |
| glb.click(get_account_balances,[],[glbt,glp]) |
|
|
| |
| |
| |
| with gr.Tab("AI tutor", elem_classes=["tab-ai"]): |
| gr.HTML("""<div class="panel-head"> |
| <div class="kick">AI tutor · Claude</div> |
| <h2 class="h2">Ask anything. <em>The tutor that never tires.</em></h2> |
| <p class="sub">Debits, credits, GAAP, IFRS — explained the way a patient professor would, with worked examples every time. Attach files for analysis, get downloadable outputs.</p> |
| </div>""") |
|
|
| with gr.Row(): |
| with gr.Column(scale=4): |
| chatbot=gr.Chatbot( |
| label="Chat", |
| type="messages", |
| height=520, |
| show_copy_button=True, |
| value=[{"role": "assistant", "content": "👋 **Hi! I'm your AI Assistant — powered by a real LLM.**\n\nI can do everything you'd expect from a modern AI:\n\n**📎 Attach Files** — Upload Excel, CSV, PDF, Word, text, or code files and I'll analyze them.\n\n**📥 Download Outputs** — Ask me to create spreadsheets, documents, reports, or code files and download them directly.\n\n**🧠 Ask Anything** — Accounting, finance, math, coding, writing, general knowledge — I handle it all.\n\n**Try these:**\n- *\"Create an amortization schedule in Excel for a $50,000 loan at 5% over 36 months\"*\n- *Upload a CSV and ask:* *\"Analyze this data and summarize the key trends\"*\n- *\"Generate a trial balance template I can download\"*\n- *\"Explain the difference between FIFO and LIFO with examples\"*\n\nType your question below or attach a file to get started! 🚀"}], |
| ) |
|
|
| |
| with gr.Row(): |
| ai_file_upload = gr.File( |
| label="📎 Attach Files", |
| file_count="multiple", |
| file_types=[".csv", ".xlsx", ".xls", ".pdf", ".docx", ".doc", ".txt", |
| ".json", ".py", ".html", ".css", ".js", ".md", ".xml", |
| ".sql", ".log", ".yaml", ".yml", ".tsv", |
| ".png", ".jpg", ".jpeg", ".gif", ".webp"], |
| scale=1, |
| height=60, |
| elem_classes=["ai-upload-zone"], |
| ) |
|
|
| with gr.Row(): |
| cin=gr.Textbox( |
| label="Your Message", |
| placeholder="Ask anything, or attach a file above and ask about it...", |
| scale=5, |
| show_label=False, |
| lines=2, |
| ) |
| cbtn=gr.Button("Send ➤", variant="primary", scale=1, min_width=100) |
|
|
| with gr.Column(scale=1, min_width=250): |
| gr.HTML("""<div class="ic grn" style="margin-top:0;"> |
| <h3>📥 Generated Files</h3> |
| <p>Files created by the AI will appear here for download. Ask the AI to create Excel sheets, Word docs, CSV files, code, and more!</p> |
| </div>""") |
|
|
| ai_file_output = gr.File( |
| label="📥 Download Generated Files", |
| file_count="multiple", |
| interactive=False, |
| elem_classes=["ai-download-area"], |
| ) |
|
|
| export_chat_btn = gr.Button("💾 Export Chat History", variant="secondary") |
| chat_export_file = gr.File(label="📄 Chat Export", interactive=False) |
|
|
| gr.HTML("""<div class="ic" style="margin-top:12px;"> |
| <h3>💡 Tips</h3> |
| <p style="font-size:13px;line-height:1.6;"> |
| <b>📎 Attach:</b> Upload CSV, Excel, PDF, Word, JSON, code files<br> |
| <b>📥 Create:</b> Ask for "Excel file", "CSV report", "Word document"<br> |
| <b>🔍 Analyze:</b> Upload data and ask for insights, summaries, charts<br> |
| <b>📊 Templates:</b> Ask for accounting templates, schedules, forms |
| </p> |
| </div>""") |
|
|
| gr.HTML("""<div class="ic amb" style="margin-top:8px;"> |
| <h3>📄 Supported File Types</h3> |
| <p style="font-size:12px;line-height:1.8;"> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.xlsx</span> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.csv</span> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.pdf</span> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.docx</span> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.txt</span> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.json</span> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.py</span> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.html</span> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.sql</span> |
| <span style="background:#EBF0F7;padding:2px 8px;border-radius:4px;margin:2px;display:inline-block;">.md</span> |
| </p> |
| </div>""") |
|
|
| |
| cbtn.click( |
| accounting_chatbot_with_files, |
| [cin, chatbot, ai_file_upload], |
| [chatbot, cin, ai_file_upload, ai_file_output] |
| ) |
| cin.submit( |
| accounting_chatbot_with_files, |
| [cin, chatbot, ai_file_upload], |
| [chatbot, cin, ai_file_upload, ai_file_output] |
| ) |
| export_chat_btn.click( |
| export_chat_history, |
| [chatbot], |
| [chat_export_file] |
| ) |
|
|
| gr.HTML("""<div class="ft"> |
| <div class="ft-grid"> |
| <div style="display:flex; align-items:center; gap:14px;"> |
| <svg class="ft-logo-svg" width="56" height="56" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" aria-label="Accounting Intelligence"> |
| <defs> |
| <radialGradient id="ft-bg" cx="50%" cy="50%" r="60%"> |
| <stop offset="0%" stop-color="#1A3C6E"/> |
| <stop offset="60%" stop-color="#0F2847"/> |
| <stop offset="100%" stop-color="#0A1F3D"/> |
| </radialGradient> |
| <radialGradient id="ft-glow" cx="50%" cy="50%" r="50%"> |
| <stop offset="0%" stop-color="#BF0A30" stop-opacity="0.32"/> |
| <stop offset="60%" stop-color="#BF0A30" stop-opacity="0"/> |
| </radialGradient> |
| </defs> |
| <circle cx="60" cy="60" r="60" fill="url(#ft-bg)"/> |
| <circle cx="60" cy="60" r="34" fill="url(#ft-glow)"/> |
| <g class="ft-ring-outer"> |
| <circle cx="60" cy="60" r="54" fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="0.6" stroke-dasharray="2 6"/> |
| <circle cx="60" cy="6" r="1.2" fill="#ffffff"/> |
| <circle cx="114" cy="60" r="1" fill="rgba(255,255,255,0.6)"/> |
| <circle cx="60" cy="114" r="1.4" fill="#BF0A30"/> |
| <circle cx="6" cy="60" r="1" fill="rgba(255,255,255,0.6)"/> |
| </g> |
| <g class="ft-ring-inner"> |
| <circle cx="60" cy="60" r="45" fill="none" stroke="rgba(255,255,255,0.14)" stroke-width="0.5"/> |
| </g> |
| <g class="ft-beam"> |
| <line x1="22" y1="60" x2="98" y2="60" stroke="#ffffff" stroke-width="0.8" opacity="0.75"/> |
| <line x1="60" y1="60" x2="60" y2="42" stroke="#ffffff" stroke-width="0.8"/> |
| <rect x="17" y="54" width="16" height="14" fill="none" stroke="#ffffff" stroke-width="1.4" rx="1.5"/> |
| <rect x="87" y="54" width="16" height="14" fill="none" stroke="#ffffff" stroke-width="1.4" rx="1.5"/> |
| <line x1="25" y1="54" x2="25" y2="60" stroke="rgba(255,255,255,0.5)" stroke-width="0.5"/> |
| <line x1="95" y1="54" x2="95" y2="60" stroke="rgba(255,255,255,0.5)" stroke-width="0.5"/> |
| </g> |
| <circle class="ft-fulcrum" cx="60" cy="42" r="2.5" fill="#BF0A30"/> |
| <path id="ft-arc" d="M 18 82 Q 60 100 102 82" fill="none"/> |
| <text fill="rgba(255,255,255,0.7)" font-family="Georgia, serif" font-size="6.5" letter-spacing="2" font-style="italic"> |
| <textPath href="#ft-arc" startOffset="50%" text-anchor="middle">A C C O U N T I N G</textPath> |
| </text> |
| </svg> |
| <div> |
| <div style="font-family:'Libre Baskerville', serif; font-size:16px; color:#fff; line-height:1.25;">Accounting <em style="font-style:italic; color:rgba(255,255,255,0.85);">Intelligence</em></div> |
| <div style="font-size:10.5px; letter-spacing:2px; text-transform:uppercase; color:rgba(255,255,255,0.45); margin-top:4px;">SUNY Polytechnic Institute</div> |
| </div> |
| </div> |
| <div class="ft-auth"> |
| Built by <span class="nm">Angelica Castro Cardenas</span>, <span class="nm">Katie Matt</span>, and <span class="nm">Rishi Manchi</span><br> |
| <span style="color:rgba(255,255,255,0.5);">For students learning GAAP, double-entry, and the art of clean books.</span> |
| </div> |
| <div class="ft-meta">Python · Gradio · Claude AI<br>Open source · Educational</div> |
| </div> |
| </div>""", elem_classes=["footer-wrap"]) |
|
|
| |
| rp_btn.click(generate_financial_reports,None,[is_o,se_o,bs_o,cf_o,tb_o,rp_p]) |
| je_btn.click(add_journal_entry_from_table,[je_date,je_desc,je_tbl],[gld,je_hist,je_p,equation_display]) |
| del_btn.click(delete_journal_entry,[del_in],[gld,je_hist,equation_display,del_msg]) |
| exp_btn.click(export_to_excel,None,exp_out) |
| for btn,func in [(b1,rec_sale),(b2,rec_stock),(b3,rec_borrow),(b4,rec_asset)]: |
| btn.click(func,[qd,qds,qa],[tgl,je_hist,tgp,equation_display]).then(lambda:(str(datetime.date.today()),"",0),None,[qd,qds,qa]) |
|
|
| demo.launch(ssr_mode=False, show_api=False) |