Spaces:
Running on Zero
Running on Zero
Redesign: custom The Fin AI page + gr.api /chat backend (ZeroGPU)
Browse files- .gitattributes +1 -0
- README.md +38 -5
- app.py +94 -119
- baklava.png → assets/examples/image_107.png +2 -2
- image_659.png → assets/examples/image_659.png +0 -0
- assets/finai-logo.png +0 -0
- assets/orgs/airc.png +0 -0
- assets/orgs/archimedes.svg +29 -0
- assets/orgs/columbia.png +0 -0
- assets/orgs/cuhksz.png +0 -0
- assets/orgs/harvard.svg +9 -0
- assets/orgs/manchester.png +0 -0
- assets/orgs/nju.svg +389 -0
- assets/orgs/nus.svg +0 -0
- assets/orgs/nvidia.svg +9 -0
- assets/orgs/nyu.svg +43 -0
- assets/orgs/rpi.svg +0 -0
- assets/orgs/stevens.png +0 -0
- assets/orgs/thefinai.png +0 -0
- assets/orgs/udem.svg +526 -0
- assets/orgs/ufl.svg +7 -0
- assets/orgs/whu.svg +62 -0
- assets/orgs/yale.svg +3 -0
- bee.jpg +0 -3
- fa_page.py +123 -0
- image_107.png +0 -0
- llava_llama3/.DS_Store +0 -0
- llava_llama3/model/__pycache__/__init__.cpython-311.pyc +0 -0
- llava_llama3/model/__pycache__/builder.cpython-311.pyc +0 -0
- llava_llama3/model/__pycache__/llava_arch.cpython-311.pyc +0 -0
- llava_llama3/model/language_model/__pycache__/llava_llama.cpython-311.pyc +0 -0
- llava_llama3/model/language_model/__pycache__/llava_mistral.cpython-311.pyc +0 -0
- llava_llama3/model/language_model/__pycache__/llava_mpt.cpython-311.pyc +0 -0
- llava_llama3/model/language_model/__pycache__/llava_phi3.cpython-311.pyc +0 -0
- llava_llama3/model/multimodal_encoder/__pycache__/builder.cpython-311.pyc +0 -0
- llava_llama3/model/multimodal_encoder/__pycache__/clip_encoder.cpython-311.pyc +0 -0
- llava_llama3/model/multimodal_projector/__pycache__/builder.cpython-311.pyc +0 -0
- llava_llama3/serve/__pycache__/__init__.cpython-311.pyc +0 -0
- llava_llama3/serve/__pycache__/cli.cpython-311.pyc +0 -0
- llava_llama3/train/__pycache__/llama_xformers_attn_monkey_patch.cpython-311.pyc +0 -0
- llava_llama3/train/__pycache__/llava_trainer.cpython-311.pyc +0 -0
- llava_llama3/train/__pycache__/train.cpython-311.pyc +0 -0
- page/chat.css +137 -0
- page/chat.js +156 -0
- page/style.css +270 -0
- requirements.txt +5 -1
.gitattributes
CHANGED
|
@@ -35,3 +35,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
baklava.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
bee.jpg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
baklava.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
bee.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/examples/image_107.png filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,14 +1,47 @@
|
|
| 1 |
---
|
| 2 |
title: FinLLaVA
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
| 8 |
python_version: "3.10"
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: FinLLaVA
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.49.1
|
| 8 |
python_version: "3.10"
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
+
short_description: Ask FinLLaVA about financial charts, tables and scans
|
| 13 |
+
models:
|
| 14 |
+
- TheFinAI/FinLLaVA
|
| 15 |
+
tags:
|
| 16 |
+
- finance
|
| 17 |
+
- multimodal
|
| 18 |
+
- vision-language
|
| 19 |
+
- thefinai
|
| 20 |
+
- open-finllms
|
| 21 |
---
|
| 22 |
|
| 23 |
+
# FinLLaVA demo
|
| 24 |
+
|
| 25 |
+
📄 [Open-FinLLMs paper](https://arxiv.org/abs/2408.11878) · 🤗 [Model](https://huggingface.co/TheFinAI/FinLLaVA) · 🤗 [Open-FinLLMs collection](https://huggingface.co/collections/TheFinAI/open-finllms-66b671f2b4958a65e20decbe) · 🌐 [The Fin AI](https://thefin.ai)
|
| 26 |
+
|
| 27 |
+
Ask **FinLLaVA**, the multimodal member of Open-FinLLMs, about financial charts, tables and scanned pages. Each question is answered about the current image only.
|
| 28 |
+
|
| 29 |
+
## How this Space works
|
| 30 |
+
|
| 31 |
+
The page is our own HTML/CSS/JS (`page/`), served by a small Gradio app that also exposes one API endpoint, `/chat` (`gr.api`, streaming, takes an image). The page talks to it with [`@gradio/client`](https://www.gradio.app/guides/getting-started-with-the-js-client). Inference runs on ZeroGPU with the bundled `llava_llama3` code.
|
| 32 |
+
|
| 33 |
+
The Space stays on Gradio 5 because `llava_llama3` needs `transformers==4.44`, which requires `huggingface_hub<1.0`; Gradio 6 needs a newer `huggingface_hub`.
|
| 34 |
+
|
| 35 |
+
Run the page locally without a GPU: `DEMO_UI_ONLY=1 python app.py`.
|
| 36 |
+
|
| 37 |
+
```bibtex
|
| 38 |
+
@misc{huang2025openfinllmsopenmultimodallarge,
|
| 39 |
+
title={Open-FinLLMs: Open Multimodal Large Language Models for Financial Applications},
|
| 40 |
+
author={Jimin Huang and Mengxi Xiao and Dong Li and Zihao Jiang and Yuzhe Yang and Yifei Zhang and Lingfei Qian and Yan Wang and Xueqing Peng and Yang Ren and Ruoyu Xiang and Zhengyu Chen and Xiao Zhang and Yueru He and Weiguang Han and Shunian Chen and Lihang Shen and Daniel Kim and Yangyang Yu and Yupeng Cao and Zhiyang Deng and Haohang Li and Duanyu Feng and Yongfu Dai and VijayaSai Somasundaram and Peng Lu and Guojun Xiong and Zhiwei Liu and Zheheng Luo and Zhiyuan Yao and Ruey-Ling Weng and Meikang Qiu and Kaleb E Smith and Honghai Yu and Yanzhao Lai and Min Peng and Jian-Yun Nie and Jordan W. Suchow and Xiao-Yang Liu and Benyou Wang and Alejandro Lopez-Lira and Qianqian Xie and Sophia Ananiadou and Junichi Tsujii},
|
| 41 |
+
year={2025},
|
| 42 |
+
eprint={2408.11878},
|
| 43 |
+
archivePrefix={arXiv},
|
| 44 |
+
primaryClass={cs.CL},
|
| 45 |
+
url={https://arxiv.org/abs/2408.11878},
|
| 46 |
+
}
|
| 47 |
+
```
|
app.py
CHANGED
|
@@ -1,126 +1,101 @@
|
|
| 1 |
-
|
| 2 |
-
from threading import Thread
|
| 3 |
-
|
| 4 |
-
import gradio as gr
|
| 5 |
-
import torch
|
| 6 |
-
from PIL import Image
|
| 7 |
-
from transformers import AutoProcessor, LlavaForConditionalGeneration, TextIteratorStreamer, TextStreamer
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
import argparse
|
| 11 |
-
|
| 12 |
-
from llava_llama3.model.builder import load_pretrained_model
|
| 13 |
-
from llava_llama3.constants import IMAGE_TOKEN_INDEX, DEFAULT_IMAGE_TOKEN, DEFAULT_IM_START_TOKEN, DEFAULT_IM_END_TOKEN
|
| 14 |
-
from llava_llama3.conversation import conv_templates, SeparatorStyle
|
| 15 |
-
from llava_llama3.utils import disable_torch_init
|
| 16 |
-
from llava_llama3.mm_utils import process_images, tokenizer_image_token, get_model_name_from_path
|
| 17 |
-
from llava_llama3.serve.cli import chat_llava
|
| 18 |
-
|
| 19 |
-
import requests
|
| 20 |
-
from io import BytesIO
|
| 21 |
-
import base64
|
| 22 |
import os
|
| 23 |
-
import
|
| 24 |
-
|
| 25 |
-
from
|
| 26 |
-
import json
|
| 27 |
-
|
| 28 |
-
root_path = os.path.dirname(os.path.abspath(__file__))
|
| 29 |
-
print(root_path)
|
| 30 |
-
os.environ['GRADIO_TEMP_DIR'] = root_path
|
| 31 |
-
|
| 32 |
-
parser = argparse.ArgumentParser()
|
| 33 |
-
parser.add_argument("--model-path", type=str, default="TheFinAI/FinLLaVA")
|
| 34 |
-
parser.add_argument("--device", type=str, default="cuda")
|
| 35 |
-
parser.add_argument("--conv-mode", type=str, default="llama_3")
|
| 36 |
-
parser.add_argument("--temperature", type=float, default=0.01)
|
| 37 |
-
parser.add_argument("--max-new-tokens", type=int, default=512)
|
| 38 |
-
parser.add_argument("--load-8bit", action="store_true")
|
| 39 |
-
parser.add_argument("--load-4bit", action="store_true")
|
| 40 |
-
args = parser.parse_args()
|
| 41 |
-
|
| 42 |
-
# args.load_8bit = True
|
| 43 |
-
|
| 44 |
-
# Load model
|
| 45 |
-
tokenizer, llava_model, image_processor, context_len = load_pretrained_model(
|
| 46 |
-
args.model_path,
|
| 47 |
-
None,
|
| 48 |
-
'llava_llama3',
|
| 49 |
-
args.load_8bit,
|
| 50 |
-
args.load_4bit,
|
| 51 |
-
device=args.device)
|
| 52 |
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
""
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
return
|
| 77 |
-
|
| 78 |
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FinLLaVA demo: custom The Fin AI page (page/) on top of a Gradio API (`/chat`), running on ZeroGPU.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
Stays on Gradio 5: the bundled llava_llama3 code needs transformers 4.44, which requires huggingface_hub<1.0 (Gradio 6 needs >=1.16).
|
| 4 |
+
Local UI test without a GPU: DEMO_UI_ONLY=1 python app.py
|
| 5 |
+
"""
|
| 6 |
import argparse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
import os
|
| 8 |
+
import time
|
| 9 |
+
from collections.abc import Iterator
|
| 10 |
+
from threading import Thread
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
UI_ONLY = os.getenv("DEMO_UI_ONLY") == "1"
|
| 13 |
+
if not UI_ONLY:
|
| 14 |
+
import spaces # must be imported before torch on ZeroGPU
|
| 15 |
|
| 16 |
+
import gradio as gr
|
| 17 |
+
from gradio.data_classes import FileData
|
| 18 |
+
|
| 19 |
+
import fa_page
|
| 20 |
+
|
| 21 |
+
MODEL_ID = "TheFinAI/FinLLaVA"
|
| 22 |
+
ARGS = argparse.Namespace(model_path=MODEL_ID, device="cuda", conv_mode="llama_3", temperature=0.01, max_new_tokens=512,
|
| 23 |
+
load_8bit=False, load_4bit=False)
|
| 24 |
+
|
| 25 |
+
if not UI_ONLY:
|
| 26 |
+
from transformers import TextIteratorStreamer
|
| 27 |
+
|
| 28 |
+
from llava_llama3.model.builder import load_pretrained_model
|
| 29 |
+
from llava_llama3.serve.cli import chat_llava
|
| 30 |
+
|
| 31 |
+
tokenizer, llava_model, image_processor, context_len = load_pretrained_model(
|
| 32 |
+
ARGS.model_path, None, "llava_llama3", ARGS.load_8bit, ARGS.load_4bit, device=ARGS.device)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def chat(message: str, history: list[dict], image: FileData | None = None, temperature: float = 0.01,
|
| 36 |
+
max_new_tokens: int = 512) -> Iterator[str]:
|
| 37 |
+
"""Ask FinLLaVA about a chart or table image. Each call answers about `image` only; `history` is not used as context."""
|
| 38 |
+
path = image.get("path") if isinstance(image, dict) else getattr(image, "path", None)
|
| 39 |
+
if not path:
|
| 40 |
+
raise gr.Error("Please add an image (a chart, table or scanned page).")
|
| 41 |
+
if UI_ONLY:
|
| 42 |
+
reply = (f"**UI test mode.** Received an image (`{os.path.basename(path)}`) and the question:\n\n> {message}\n\n"
|
| 43 |
+
f"Settings: temperature={temperature}, max_new_tokens={max_new_tokens}.")
|
| 44 |
+
out = ""
|
| 45 |
+
for word in reply.split(" "):
|
| 46 |
+
out += word + " "
|
| 47 |
+
time.sleep(0.03)
|
| 48 |
+
yield out
|
| 49 |
return
|
| 50 |
+
args = argparse.Namespace(**{**vars(ARGS), "temperature": float(temperature), "max_new_tokens": int(max_new_tokens)})
|
| 51 |
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 52 |
+
Thread(target=chat_llava, kwargs=dict(args=args, image_file=path, text=message, tokenizer=tokenizer, model=llava_model,
|
| 53 |
+
image_processor=image_processor, context_len=context_len, streamer=streamer)).start()
|
| 54 |
+
out = ""
|
| 55 |
+
for text in streamer:
|
| 56 |
+
out += text
|
| 57 |
+
yield out
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if not UI_ONLY:
|
| 61 |
+
chat = spaces.GPU(chat)
|
| 62 |
+
|
| 63 |
+
page = fa_page.Page(dict(
|
| 64 |
+
page_title="FinLLaVA · financial charts & tables · The Fin AI",
|
| 65 |
+
nav=[("Model", f"https://huggingface.co/{MODEL_ID}"), ("Open-FinLLMs", "https://huggingface.co/collections/TheFinAI/open-finllms-66b671f2b4958a65e20decbe"),
|
| 66 |
+
("Paper ↗", "https://arxiv.org/abs/2408.11878")],
|
| 67 |
+
kicker="Demo · multimodal financial LLM",
|
| 68 |
+
title_html="Fin<em>LLaVA</em>",
|
| 69 |
+
lead_html="Ask questions about financial charts, tables and scanned pages. FinLLaVA is the multimodal member of "
|
| 70 |
+
"Open-FinLLMs: a LLaVA model built on FinLLaMA and CLIP, tuned with 1.43M multimodal pairs.",
|
| 71 |
+
stats=[("8", "billion parameters", 0), ("1.43", "million multimodal tuning pairs", 2), ("52", "billion-token FinLLaMA pretraining", 0)],
|
| 72 |
+
links=[("Model card", f"https://huggingface.co/{MODEL_ID}"), ("FinLLaMA", "https://huggingface.co/TheFinAI/FinLLaMA"),
|
| 73 |
+
("Open-FinLLMs collection", "https://huggingface.co/collections/TheFinAI/open-finllms-66b671f2b4958a65e20decbe")],
|
| 74 |
+
image=True,
|
| 75 |
+
examples=[{"label": "Read a bar chart", "text": "What is the spending on Healthcare in July? A. 450 B. 600 C. 520 D. 510", "image": "examples/image_107.png"},
|
| 76 |
+
{"label": "Reason over a pension table", "text": "If 2012 net periodic opeb cost increased at the same pace as the pension cost, what would the estimated 2013 cost be in millions? A. 14.83333 B. 12.5 C. 15.5 D. 13.5", "image": "examples/image_659.png"}],
|
| 77 |
+
params=[dict(key="temperature", label="Temperature", min=0.01, max=1.0, step=0.01, value=0.01),
|
| 78 |
+
dict(key="max_new_tokens", label="Max new tokens", min=32, max=1024, step=1, value=512)],
|
| 79 |
+
greeting=dict(title="Show FinLLaVA a chart", text="Upload, drop or paste a chart, table or scanned page, then ask a question — or try an example."),
|
| 80 |
+
placeholder="Ask about the image…",
|
| 81 |
+
hint_html="Each question is answered about the current image only; earlier turns are not used as context. Runs on ZeroGPU. Not financial advice.",
|
| 82 |
+
avatar="orgs/thefinai.png",
|
| 83 |
+
orgs_sub_html='Author affiliations on the <a href="https://arxiv.org/abs/2408.11878" target="_blank" rel="noreferrer">Open-FinLLMs paper</a>.',
|
| 84 |
+
orgs=[("The Fin AI", "thefinai.png", "https://thefin.ai", "mark"), ("Wuhan University", "whu.svg", "https://en.whu.edu.cn", "emblem"),
|
| 85 |
+
("Columbia University", "columbia.png", "https://www.columbia.edu", "emblem"), ("The Chinese University of Hong Kong, Shenzhen", "cuhksz.png", "https://www.cuhk.edu.cn/en", ""),
|
| 86 |
+
("Nanjing University", "nju.svg", "https://www.nju.edu.cn/en/", "emblem"), ("Rensselaer Polytechnic Institute", "rpi.svg", "https://www.rpi.edu", ""),
|
| 87 |
+
("The University of Manchester", "manchester.png", "https://www.manchester.ac.uk", ""), ("Stevens Institute of Technology", "stevens.png", "https://www.stevens.edu", ""),
|
| 88 |
+
("National University of Singapore", "nus.svg", "https://nus.edu.sg", "emblem"), ("University of Florida", "ufl.svg", "https://www.ufl.edu", ""),
|
| 89 |
+
("University of Montreal", "udem.svg", "https://www.umontreal.ca", ""), ("Yale University", "yale.svg", "https://www.yale.edu", ""),
|
| 90 |
+
("New York University", "nyu.svg", "https://www.nyu.edu", ""), ("Harvard University", "harvard.svg", "https://www.harvard.edu", ""),
|
| 91 |
+
("NVIDIA", "nvidia.svg", "https://www.nvidia.com", ""), ("Artificial Intelligence Research Centre (AIST)", "airc.png", "https://www.airc.aist.go.jp/en/", "emblem"),
|
| 92 |
+
("Archimedes / Athena Research Center", "archimedes.svg", "https://archimedesai.gr/en/", "")],
|
| 93 |
+
disclaimer="FinLLaVA is a research model. Outputs can be inaccurate and are not financial advice.",
|
| 94 |
+
))
|
| 95 |
+
|
| 96 |
+
with gr.Blocks(**page.blocks_kwargs()) as demo:
|
| 97 |
+
page.render()
|
| 98 |
+
gr.api(chat, api_name="chat")
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
demo.queue(max_size=20).launch(**page.launch_kwargs())
|
baklava.png → assets/examples/image_107.png
RENAMED
|
File without changes
|
image_659.png → assets/examples/image_659.png
RENAMED
|
File without changes
|
assets/finai-logo.png
ADDED
|
assets/orgs/airc.png
ADDED
|
assets/orgs/archimedes.svg
ADDED
|
|
assets/orgs/columbia.png
ADDED
|
assets/orgs/cuhksz.png
ADDED
|
assets/orgs/harvard.svg
ADDED
|
|
assets/orgs/manchester.png
ADDED
|
assets/orgs/nju.svg
ADDED
|
|
assets/orgs/nus.svg
ADDED
|
|
assets/orgs/nvidia.svg
ADDED
|
|
assets/orgs/nyu.svg
ADDED
|
|
assets/orgs/rpi.svg
ADDED
|
|
assets/orgs/stevens.png
ADDED
|
assets/orgs/thefinai.png
ADDED
|
assets/orgs/udem.svg
ADDED
|
|
assets/orgs/ufl.svg
ADDED
|
|
assets/orgs/whu.svg
ADDED
|
|
assets/orgs/yale.svg
ADDED
|
|
bee.jpg
DELETED
Git LFS Details
|
fa_page.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Custom The Fin AI page for Gradio-backed demo Spaces.
|
| 2 |
+
|
| 3 |
+
Gradio only serves the page and the `/chat` API (gr.api); the whole UI is our own HTML/CSS/JS (page/), which talks to
|
| 4 |
+
the API with @gradio/client. Works with Gradio 5 (head/css on Blocks) and Gradio 6 (head/css on launch).
|
| 5 |
+
|
| 6 |
+
page = fa_page.Page(cfg) # cfg: see build_body()
|
| 7 |
+
with gr.Blocks(**page.blocks_kwargs()) as demo:
|
| 8 |
+
page.render()
|
| 9 |
+
gr.api(chat, api_name="chat")
|
| 10 |
+
demo.queue().launch(**page.launch_kwargs())
|
| 11 |
+
"""
|
| 12 |
+
import html
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
|
| 16 |
+
import gradio as gr
|
| 17 |
+
|
| 18 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 19 |
+
PAGE = os.path.join(HERE, "page")
|
| 20 |
+
ASSETS = os.path.join(HERE, "assets")
|
| 21 |
+
gr.set_static_paths(paths=[ASSETS])
|
| 22 |
+
GRADIO6 = int(gr.__version__.split(".")[0]) >= 6
|
| 23 |
+
CLIENT = "2.7.1" if GRADIO6 else "1.19.1" # @gradio/client matching the Gradio major version
|
| 24 |
+
e = lambda s: html.escape(str(s), quote=True)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def asset(path):
|
| 28 |
+
return f"/gradio_api/file={os.path.join(ASSETS, path)}"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _read(name):
|
| 32 |
+
with open(os.path.join(PAGE, name), encoding="utf-8") as f:
|
| 33 |
+
return f.read()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
ICON_SEND = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>'
|
| 37 |
+
ICON_STOP = '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>'
|
| 38 |
+
ICON_IMG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="3"/><circle cx="9" cy="10" r="2"/><path d="m21 16-5-5-9 9"/></svg>'
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Page:
|
| 42 |
+
def __init__(self, cfg):
|
| 43 |
+
self.cfg = cfg
|
| 44 |
+
|
| 45 |
+
def head(self):
|
| 46 |
+
c = self.cfg
|
| 47 |
+
chat = {"client": CLIENT, "endpoint": "/chat", "image": c.get("image", False), "avatar": asset(c["avatar"]),
|
| 48 |
+
"greeting": c["greeting"], "params": c["params"], "readyText": c.get("ready_text", "Ready"),
|
| 49 |
+
"examples": [{**x, "image": asset(x["image"]) if x.get("image") else None} for x in c["examples"]]}
|
| 50 |
+
fonts = ('<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>'
|
| 51 |
+
'<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400'
|
| 52 |
+
'&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">')
|
| 53 |
+
return (fonts + f'<link rel="icon" href="{asset("finai-logo.png")}"><style>{_read("style.css")}\n{_read("chat.css")}</style>'
|
| 54 |
+
f'<script>window.FA_CHAT={json.dumps(chat)};</script><script type="module">{_read("chat.js")}</script>')
|
| 55 |
+
|
| 56 |
+
def body(self):
|
| 57 |
+
c = self.cfg
|
| 58 |
+
nav = "".join(f'<a href="{e(u)}" target="_blank" rel="noreferrer">{e(l)}</a>' for l, u in c["nav"])
|
| 59 |
+
stats = "".join(f'<div><b data-count="{e(v)}"{f" data-dec={d}" if d else ""}>0</b><span>{e(k)}</span></div>' for v, k, d in c["stats"])
|
| 60 |
+
pills = "".join(f'<a class="pill" href="{e(u)}" target="_blank" rel="noreferrer">{e(l)}</a>' for l, u in c["links"])
|
| 61 |
+
orgs = "".join(f'<a class="org" href="{e(u)}" target="_blank" rel="noreferrer" style="--d:{i * 40}ms" title="{e(n)}">'
|
| 62 |
+
f'<img src="{asset("orgs/" + f)}" alt="" class="{e(k)}" loading="lazy"><span>{e(n)}</span></a>' for i, (n, f, u, k) in enumerate(c["orgs"]))
|
| 63 |
+
attach = (f'<label class="icon-btn attach" title="Add an image">{ICON_IMG}<input id="fa-image" type="file" accept="image/*"></label>'
|
| 64 |
+
'<div id="fa-preview" hidden><img alt=""><button type="button" aria-label="Remove image">×</button></div>') if c.get("image") else ""
|
| 65 |
+
return f'''<div id="fa-app">
|
| 66 |
+
<div class="progress" aria-hidden="true"></div>
|
| 67 |
+
<header class="header scrolled"><div class="wrap nav">
|
| 68 |
+
<a class="brand" href="https://thefin.ai" target="_blank" rel="noreferrer"><img src="{asset("finai-logo.png")}" alt="">The Fin AI</a>
|
| 69 |
+
<nav class="links">{nav}<a href="https://thefin.ai" target="_blank" rel="noreferrer">thefin.ai ↗</a></nav>
|
| 70 |
+
</div></header>
|
| 71 |
+
<main class="wrap demo">
|
| 72 |
+
<section class="intro">
|
| 73 |
+
<span class="kicker">{e(c["kicker"])}</span>
|
| 74 |
+
<h1>{c["title_html"]}</h1>
|
| 75 |
+
<p class="lead">{c["lead_html"]}</p>
|
| 76 |
+
<div class="fb-stats">{stats}</div>
|
| 77 |
+
<div class="links-row">{pills}</div>
|
| 78 |
+
<h3 class="side-h">Try an example</h3>
|
| 79 |
+
<div id="fa-examples"></div>
|
| 80 |
+
<details class="params"><summary>Generation settings</summary><div id="fa-params"></div></details>
|
| 81 |
+
</section>
|
| 82 |
+
<section class="panel" aria-label="Chat">
|
| 83 |
+
<div class="panel-top"><span id="fa-status" data-state="connecting"><i></i><span>Connecting…</span></span><button type="button" class="ghost" id="fa-clear">New chat</button></div>
|
| 84 |
+
<div id="fa-log" aria-live="polite"></div>
|
| 85 |
+
<div class="composer">
|
| 86 |
+
{attach}
|
| 87 |
+
<textarea id="fa-input" rows="1" placeholder="{e(c["placeholder"])}"></textarea>
|
| 88 |
+
<button type="button" class="icon-btn" id="fa-send" aria-label="Send">{ICON_SEND}</button>
|
| 89 |
+
<button type="button" class="icon-btn" id="fa-stop" aria-label="Stop" hidden>{ICON_STOP}</button>
|
| 90 |
+
<div id="fa-note" role="status"></div>
|
| 91 |
+
</div>
|
| 92 |
+
<p class="hint">{c["hint_html"]}</p>
|
| 93 |
+
</section>
|
| 94 |
+
</main>
|
| 95 |
+
<section class="wrap credits-sec reveal">
|
| 96 |
+
<span class="kicker">People & partners</span>
|
| 97 |
+
<h2>Built together</h2>
|
| 98 |
+
<p class="sub">{c["orgs_sub_html"]}</p>
|
| 99 |
+
<div class="orgs">{orgs}</div>
|
| 100 |
+
<p class="disclaimer">{c["disclaimer"]}</p>
|
| 101 |
+
</section>
|
| 102 |
+
<footer class="footer wrap">
|
| 103 |
+
<span class="brand"><img src="{asset("finai-logo.png")}" alt="">The Fin AI</span>
|
| 104 |
+
<nav><a href="https://github.com/The-FinAI" target="_blank" rel="noreferrer">GitHub</a><a href="https://huggingface.co/TheFinAI" target="_blank" rel="noreferrer">Hugging Face</a><a href="https://huggingface.co/collections/TheFinAI/the-fin-ai-leaderboards-6ac7154319e5aa2b52037515" target="_blank" rel="noreferrer">Leaderboards</a><a href="https://discord.gg/Wpt347rusk" target="_blank" rel="noreferrer">Discord</a></nav>
|
| 105 |
+
<span class="copy">© 2026 The Fin AI · AI Readiness in Finance</span>
|
| 106 |
+
</footer>
|
| 107 |
+
</div>'''
|
| 108 |
+
|
| 109 |
+
def render(self):
|
| 110 |
+
return gr.HTML(self.body())
|
| 111 |
+
|
| 112 |
+
def blocks_kwargs(self):
|
| 113 |
+
kw = dict(title=self.cfg["page_title"], fill_width=True)
|
| 114 |
+
if not GRADIO6:
|
| 115 |
+
kw["head"] = self.head()
|
| 116 |
+
return kw
|
| 117 |
+
|
| 118 |
+
def launch_kwargs(self):
|
| 119 |
+
kw = {} if not GRADIO6 else dict(footer_links=[])
|
| 120 |
+
if GRADIO6:
|
| 121 |
+
kw["head"] = self.head()
|
| 122 |
+
kw["ssr_mode"] = False
|
| 123 |
+
return kw
|
image_107.png
DELETED
|
Binary file (242 kB)
|
|
|
llava_llama3/.DS_Store
DELETED
|
Binary file (6.15 kB)
|
|
|
llava_llama3/model/__pycache__/__init__.cpython-311.pyc
DELETED
|
Binary file (709 Bytes)
|
|
|
llava_llama3/model/__pycache__/builder.cpython-311.pyc
DELETED
|
Binary file (11 kB)
|
|
|
llava_llama3/model/__pycache__/llava_arch.cpython-311.pyc
DELETED
|
Binary file (23.2 kB)
|
|
|
llava_llama3/model/language_model/__pycache__/llava_llama.cpython-311.pyc
DELETED
|
Binary file (6.37 kB)
|
|
|
llava_llama3/model/language_model/__pycache__/llava_mistral.cpython-311.pyc
DELETED
|
Binary file (6.35 kB)
|
|
|
llava_llama3/model/language_model/__pycache__/llava_mpt.cpython-311.pyc
DELETED
|
Binary file (5.05 kB)
|
|
|
llava_llama3/model/language_model/__pycache__/llava_phi3.cpython-311.pyc
DELETED
|
Binary file (6.28 kB)
|
|
|
llava_llama3/model/multimodal_encoder/__pycache__/builder.cpython-311.pyc
DELETED
|
Binary file (1.27 kB)
|
|
|
llava_llama3/model/multimodal_encoder/__pycache__/clip_encoder.cpython-311.pyc
DELETED
|
Binary file (10 kB)
|
|
|
llava_llama3/model/multimodal_projector/__pycache__/builder.cpython-311.pyc
DELETED
|
Binary file (3.63 kB)
|
|
|
llava_llama3/serve/__pycache__/__init__.cpython-311.pyc
DELETED
|
Binary file (188 Bytes)
|
|
|
llava_llama3/serve/__pycache__/cli.cpython-311.pyc
DELETED
|
Binary file (5.11 kB)
|
|
|
llava_llama3/train/__pycache__/llama_xformers_attn_monkey_patch.cpython-311.pyc
DELETED
|
Binary file (6.09 kB)
|
|
|
llava_llama3/train/__pycache__/llava_trainer.cpython-311.pyc
DELETED
|
Binary file (24.5 kB)
|
|
|
llava_llama3/train/__pycache__/train.cpython-311.pyc
DELETED
|
Binary file (66.3 kB)
|
|
|
page/chat.css
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* The Fin AI demo page (on top of style.css). Gradio's own chrome is hidden; #fa-app is the whole page. */
|
| 2 |
+
.gradio-container,.gradio-container>.main,.gradio-container .contain,.gradio-container>.main>.wrap{max-width:none !important;padding:0 !important;margin:0 !important;background:transparent !important}
|
| 3 |
+
.gradio-container .html-container,.gradio-container .prose{padding:0 !important;max-width:none !important}
|
| 4 |
+
.gradio-container footer,.gradio-container .api-docs-wrap{display:none !important}
|
| 5 |
+
.gradio-container .block{border:0 !important;box-shadow:none !important;background:transparent !important;padding:0 !important}
|
| 6 |
+
#fa-app{font-family:var(--sans);color:var(--ink);font-size:16px;line-height:1.6}
|
| 7 |
+
#fa-app h1,#fa-app h2,#fa-app h3{font-family:var(--serif);font-weight:500;letter-spacing:-.02em;margin:0;color:var(--ink)}
|
| 8 |
+
#fa-app p{margin:0}
|
| 9 |
+
#fa-app a{text-decoration:none}
|
| 10 |
+
|
| 11 |
+
#fa-app .demo{display:grid;grid-template-columns:minmax(300px,.85fr) 1.35fr;gap:40px;align-items:start;padding-top:104px;padding-bottom:20px}
|
| 12 |
+
#fa-app .intro h1{font-size:clamp(2.6rem,5.4vw,4.2rem);line-height:1.02;letter-spacing:-.035em;margin:18px 0 14px}
|
| 13 |
+
#fa-app .intro h1 em{font-style:italic;color:var(--accent-deep)}
|
| 14 |
+
#fa-app .intro .lead{font-size:1.02rem;color:var(--soft);max-width:46ch}
|
| 15 |
+
#fa-app .intro .fb-stats{margin:26px 0 22px;gap:26px}
|
| 16 |
+
#fa-app .intro .fb-stats b{font-size:2.1rem}
|
| 17 |
+
#fa-app .links-row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:28px}
|
| 18 |
+
#fa-app .pill{display:inline-flex;align-items:center;gap:6px;font-size:.84rem;font-weight:500;padding:8px 14px;border-radius:999px;box-shadow:inset 0 0 0 1px var(--line);color:var(--ink);transition:box-shadow .25s,transform .35s var(--spring)}
|
| 19 |
+
#fa-app .pill:hover{box-shadow:inset 0 0 0 1px var(--accent-light);transform:translateY(-2px)}
|
| 20 |
+
#fa-app .side-h{font-family:var(--sans) !important;font-size:.72rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:0 0 10px}
|
| 21 |
+
#fa-app #fa-examples{display:grid;gap:8px;margin-bottom:26px}
|
| 22 |
+
#fa-app .ex{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto auto;column-gap:12px;text-align:left;border:0;cursor:pointer;padding:12px 14px;border-radius:14px;background:var(--card);box-shadow:0 0 0 1px var(--line);font:inherit;color:var(--ink);opacity:0;animation:fa-up .6s var(--ease) forwards;animation-delay:var(--d);transition:box-shadow .25s,transform .35s var(--spring)}
|
| 23 |
+
#fa-app .ex:hover{box-shadow:0 0 0 1px var(--accent-light),0 12px 26px -16px rgba(24,56,152,.45);transform:translateX(4px)}
|
| 24 |
+
#fa-app .ex:active{transform:scale(.98)}
|
| 25 |
+
#fa-app .ex img{grid-row:span 2;width:52px;height:52px;object-fit:cover;border-radius:10px;box-shadow:0 0 0 1px var(--line)}
|
| 26 |
+
#fa-app .ex-label{font-size:.88rem;font-weight:500;grid-column:2}
|
| 27 |
+
#fa-app .ex-text{font-size:.78rem;color:var(--muted);grid-column:2;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
|
| 28 |
+
#fa-app .ex:not(:has(img)) .ex-label,#fa-app .ex:not(:has(img)) .ex-text{grid-column:1/-1}
|
| 29 |
+
#fa-app details.params{border-radius:14px;background:var(--card);box-shadow:0 0 0 1px var(--line);padding:4px 16px}
|
| 30 |
+
#fa-app details.params summary{cursor:pointer;font-size:.86rem;font-weight:500;padding:10px 0;list-style:none;display:flex;justify-content:space-between}
|
| 31 |
+
#fa-app details.params summary::after{content:"+";color:var(--muted);transition:transform .3s var(--spring)}
|
| 32 |
+
#fa-app details.params[open] summary::after{transform:rotate(45deg)}
|
| 33 |
+
#fa-app #fa-params{display:grid;gap:12px;padding:4px 0 14px}
|
| 34 |
+
#fa-app .param span{display:flex;justify-content:space-between;font-size:.8rem;color:var(--soft)}
|
| 35 |
+
#fa-app .param b{font-variant-numeric:tabular-nums;color:var(--ink);font-weight:600}
|
| 36 |
+
#fa-app .param input{width:100%;accent-color:var(--accent)}
|
| 37 |
+
|
| 38 |
+
/* chat panel */
|
| 39 |
+
#fa-app .panel{position:sticky;top:84px;display:flex;flex-direction:column;height:min(78vh,760px);border-radius:24px;background:var(--card);box-shadow:0 0 0 1px var(--line),0 30px 60px -40px rgba(15,21,40,.45);overflow:hidden;animation:fa-up .8s var(--ease) both .1s}
|
| 40 |
+
#fa-app .panel-top{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-bottom:1px solid var(--line)}
|
| 41 |
+
#fa-app #fa-status{display:inline-flex;align-items:center;gap:8px;font-size:.78rem;color:var(--soft)}
|
| 42 |
+
#fa-app #fa-status i{width:8px;height:8px;border-radius:50%;background:var(--muted)}
|
| 43 |
+
#fa-app #fa-status[data-state=ready] i{background:#16a34a;box-shadow:0 0 0 3px color-mix(in srgb,#16a34a 22%,transparent)}
|
| 44 |
+
#fa-app #fa-status[data-state=connecting] i{background:#d99a1e;animation:fa-pulse 1.2s infinite}
|
| 45 |
+
#fa-app #fa-status[data-state=error] i{background:#dc2626}
|
| 46 |
+
#fa-app .ghost{border:0;background:none;font:500 .8rem var(--sans);color:var(--soft);padding:6px 10px;border-radius:999px;cursor:pointer;transition:background .2s,color .2s}
|
| 47 |
+
#fa-app .ghost:hover{background:var(--accent-soft);color:var(--ink)}
|
| 48 |
+
#fa-app #fa-log{flex:1;overflow-y:auto;padding:22px 20px;display:flex;flex-direction:column;gap:16px;scroll-behavior:smooth}
|
| 49 |
+
#fa-app .fa-empty{margin:auto;text-align:center;max-width:36ch;animation:fa-up .7s var(--ease) both}
|
| 50 |
+
#fa-app .fa-empty img{width:44px;height:44px;margin:0 auto 14px;display:block;object-fit:contain}
|
| 51 |
+
#fa-app .fa-empty h3{font-size:1.4rem;margin-bottom:6px}
|
| 52 |
+
#fa-app .fa-empty p{font-size:.9rem;color:var(--soft)}
|
| 53 |
+
#fa-app .msg{display:flex;gap:10px;max-width:88%;animation:fa-msg .45s var(--spring) both}
|
| 54 |
+
#fa-app .msg.user{align-self:flex-end}
|
| 55 |
+
#fa-app .msg.user .body{background:var(--ink);color:var(--paper);border-radius:18px 18px 4px 18px;padding:10px 15px;font-size:.94rem;white-space:normal}
|
| 56 |
+
#fa-app .msg.assistant{align-self:flex-start;flex-wrap:wrap}
|
| 57 |
+
#fa-app .msg .av{width:28px;height:28px;flex:none;border-radius:50%;object-fit:contain;background:#fff;padding:4px;box-shadow:0 0 0 1px var(--line)}
|
| 58 |
+
#fa-app .msg.assistant .body{flex:1;min-width:0;background:var(--paper-2);border-radius:4px 18px 18px 18px;padding:11px 16px;font-size:.94rem;line-height:1.65}
|
| 59 |
+
#fa-app .msg.assistant .body p{margin:0 0 .6em}.msg.assistant .body p:last-child{margin:0}
|
| 60 |
+
#fa-app .msg.assistant .body ul,#fa-app .msg.assistant .body ol{margin:.3em 0 .6em;padding-left:1.3em}
|
| 61 |
+
#fa-app .msg.assistant .body code{font-family:var(--mono);font-size:.84em;background:var(--card);padding:1px 5px;border-radius:5px}
|
| 62 |
+
#fa-app .msg.assistant .body pre{background:var(--card);padding:10px 12px;border-radius:10px;overflow-x:auto}
|
| 63 |
+
#fa-app .msg.error .body{background:color-mix(in srgb,#dc2626 9%,var(--card));color:var(--ink)}
|
| 64 |
+
#fa-app .msg .thumb{display:block;max-width:220px;max-height:160px;border-radius:10px;margin-bottom:8px}
|
| 65 |
+
#fa-app .msg .meta{flex-basis:100%;margin-left:38px;font-size:.72rem;color:var(--muted)}
|
| 66 |
+
#fa-app .msg .meta .copy{border:0;background:none;color:var(--muted);font:inherit;cursor:pointer;padding:0}
|
| 67 |
+
#fa-app .msg .meta .copy:hover{color:var(--accent)}
|
| 68 |
+
#fa-app .typing{display:inline-flex;gap:4px;padding:4px 0}
|
| 69 |
+
#fa-app .typing i{width:7px;height:7px;border-radius:50%;background:var(--muted);animation:fa-dot 1.2s infinite}
|
| 70 |
+
#fa-app .typing i:nth-child(2){animation-delay:.15s}.typing i:nth-child(3){animation-delay:.3s}
|
| 71 |
+
#fa-app .caret{display:inline-block;width:7px;height:1.05em;margin-left:2px;vertical-align:text-bottom;background:var(--accent);animation:fa-blink 1s steps(2) infinite}
|
| 72 |
+
#fa-app .queue{font-size:.84rem;color:var(--soft)}
|
| 73 |
+
#fa-app .muted{color:var(--muted)}
|
| 74 |
+
|
| 75 |
+
#fa-app .composer{position:relative;margin:0 14px 14px;padding:8px 8px 8px 14px;border-radius:18px;background:var(--paper);box-shadow:inset 0 0 0 1px var(--line);display:flex;align-items:flex-end;gap:8px;transition:box-shadow .25s}
|
| 76 |
+
#fa-app .composer:focus-within{box-shadow:inset 0 0 0 1.5px var(--accent-light),0 0 0 4px color-mix(in srgb,var(--accent-light) 18%,transparent)}
|
| 77 |
+
#fa-app .composer.drag{box-shadow:inset 0 0 0 2px dashed var(--accent)}
|
| 78 |
+
#fa-app #fa-input{flex:1;border:0;outline:none;resize:none;background:transparent;font:400 .95rem/1.5 var(--sans);color:var(--ink);padding:8px 0;max-height:200px}
|
| 79 |
+
#fa-app .icon-btn{flex:none;width:38px;height:38px;border-radius:50%;border:0;display:grid;place-items:center;cursor:pointer;transition:transform .3s var(--spring),background .2s,opacity .2s}
|
| 80 |
+
#fa-app .icon-btn svg{width:18px;height:18px}
|
| 81 |
+
#fa-app #fa-send{background:var(--ink);color:var(--paper)}
|
| 82 |
+
#fa-app #fa-send:hover:not(:disabled){transform:scale(1.08);background:var(--accent-deep)}
|
| 83 |
+
#fa-app #fa-send:disabled{opacity:.3;cursor:default}
|
| 84 |
+
#fa-app #fa-stop{background:#dc2626;color:#fff}
|
| 85 |
+
#fa-app .attach{background:transparent;color:var(--soft)}
|
| 86 |
+
#fa-app .attach:hover{background:var(--accent-soft);color:var(--ink)}
|
| 87 |
+
#fa-app .attach input{display:none}
|
| 88 |
+
#fa-app #fa-preview{position:absolute;left:12px;bottom:calc(100% + 8px);display:flex;align-items:center;gap:8px;padding:6px;border-radius:12px;background:var(--card);box-shadow:0 0 0 1px var(--line),0 12px 24px -14px rgba(15,21,40,.4);animation:fa-up .3s var(--ease)}
|
| 89 |
+
#fa-app #fa-preview img{width:64px;height:64px;object-fit:cover;border-radius:8px}
|
| 90 |
+
#fa-app #fa-preview button{border:0;background:var(--paper-2);border-radius:50%;width:24px;height:24px;cursor:pointer;color:var(--soft)}
|
| 91 |
+
#fa-app #fa-note{position:absolute;left:50%;bottom:calc(100% + 10px);transform:translate(-50%,6px);opacity:0;pointer-events:none;background:var(--ink);color:var(--paper);font-size:.8rem;padding:7px 12px;border-radius:999px;white-space:nowrap;transition:opacity .25s,transform .3s var(--spring)}
|
| 92 |
+
#fa-app #fa-note.on{opacity:1;transform:translate(-50%,0)}
|
| 93 |
+
#fa-app .hint{text-align:center;font-size:.72rem;color:var(--muted);padding:0 14px 12px}
|
| 94 |
+
|
| 95 |
+
/* credits */
|
| 96 |
+
#fa-app .credits-sec{padding-top:70px}
|
| 97 |
+
#fa-app .credits-sec h2{font-size:clamp(1.7rem,3.2vw,2.4rem);margin:12px 0 6px}
|
| 98 |
+
#fa-app .credits-sec .sub{color:var(--soft);font-size:.92rem;margin-bottom:20px}
|
| 99 |
+
#fa-app .credits-sec .sub a{color:var(--accent)}
|
| 100 |
+
#fa-app .orgs{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px}
|
| 101 |
+
#fa-app .org{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;height:108px;padding:12px;border-radius:16px;background:var(--card);box-shadow:0 0 0 1px var(--line);text-align:center;opacity:0;transform:translateY(12px);transition:opacity .6s var(--ease) var(--d),transform .7s var(--ease) var(--d),box-shadow .3s}
|
| 102 |
+
#fa-app .credits-sec.in .org{opacity:1;transform:none}
|
| 103 |
+
#fa-app .org img{max-height:36px;max-width:130px;object-fit:contain;filter:grayscale(1);opacity:.75;transition:filter .4s,opacity .4s,transform .45s var(--spring)}
|
| 104 |
+
#fa-app .org img.emblem{max-height:48px}.org img.mark{max-height:32px}
|
| 105 |
+
#fa-app .org span{font-size:.7rem;line-height:1.3;color:var(--soft)}
|
| 106 |
+
#fa-app a.org:hover{box-shadow:0 0 0 1px var(--accent-light),0 14px 30px -18px rgba(24,56,152,.45)}
|
| 107 |
+
#fa-app a.org:hover img{filter:none;opacity:1;transform:scale(1.06)}
|
| 108 |
+
#fa-app .disclaimer{margin-top:22px;font-size:.78rem;color:var(--muted);max-width:80ch}
|
| 109 |
+
|
| 110 |
+
@keyframes fa-up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
|
| 111 |
+
@keyframes fa-msg{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:none}}
|
| 112 |
+
@keyframes fa-dot{0%,60%,100%{transform:none;opacity:.5}30%{transform:translateY(-4px);opacity:1}}
|
| 113 |
+
@keyframes fa-blink{50%{opacity:0}}
|
| 114 |
+
@keyframes fa-pulse{50%{opacity:.4}}
|
| 115 |
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .org img:not(.mark):not(.emblem){filter:grayscale(1) invert(1) brightness(1.4)}}
|
| 116 |
+
@media (max-width:900px){
|
| 117 |
+
#fa-app .demo{grid-template-columns:1fr;gap:26px;padding-top:92px}
|
| 118 |
+
#fa-app .panel{position:relative;top:0;height:72vh}
|
| 119 |
+
#fa-app .links a:not([href^="http"]){display:none}
|
| 120 |
+
}
|
| 121 |
+
@media (max-width:640px){#fa-app .orgs{grid-template-columns:1fr 1fr}#fa-app .msg{max-width:96%}}
|
| 122 |
+
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none !important;transition-duration:.01ms !important}#fa-app .ex,#fa-app .org{opacity:1 !important;transform:none !important}}
|
| 123 |
+
|
| 124 |
+
#fa-app button{font-family:var(--sans)}
|
| 125 |
+
#fa-app .wrap{max-width:var(--wrap) !important;margin:0 auto !important;padding-left:28px !important;padding-right:28px !important}
|
| 126 |
+
@media (max-width:640px){#fa-app .wrap{padding-left:16px !important;padding-right:16px !important}}
|
| 127 |
+
#fa-app [hidden]{display:none !important}
|
| 128 |
+
#fa-app .brand{color:var(--ink) !important}
|
| 129 |
+
#fa-app .links a{color:var(--soft) !important}
|
| 130 |
+
#fa-app .links a:hover{color:var(--ink) !important}
|
| 131 |
+
#fa-app .footer nav a{color:var(--soft) !important}
|
| 132 |
+
#fa-app .pill,#fa-app .org{color:var(--ink) !important}
|
| 133 |
+
@media (max-width:640px){#fa-app .links a:not(:last-child){display:none}}
|
| 134 |
+
#fa-app .sub a{padding:0 !important;margin:0 !important}
|
| 135 |
+
#fa-app .reuse{display:inline-flex;align-items:center;gap:6px;font-size:.72rem;opacity:.75;margin-bottom:6px}
|
| 136 |
+
#fa-app .reuse img{width:22px;height:22px;object-fit:cover;border-radius:5px}
|
| 137 |
+
#fa-app .msg.user .body .reuse{display:flex}
|
page/chat.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// The Fin AI chat front end for Gradio-backed demo Spaces.
|
| 2 |
+
// The Space's Gradio app exposes `/chat` (gr.api, streaming); this page talks to it with @gradio/client.
|
| 3 |
+
// window.FA_CHAT = { client, endpoint, image: bool, examples: [{text, label, image?}], params: [{key,label,min,max,step,value}], greeting, avatar }
|
| 4 |
+
import { marked } from "https://cdn.jsdelivr.net/npm/marked@12.0.2/lib/marked.esm.js";
|
| 5 |
+
import DOMPurify from "https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.es.mjs";
|
| 6 |
+
|
| 7 |
+
const C = window.FA_CHAT;
|
| 8 |
+
const $ = s => document.querySelector(s);
|
| 9 |
+
// Gradio may run head scripts twice and renders our HTML after hydration:
|
| 10 |
+
// run once, and only after #fa-log exists and is the same node on two checks in a row.
|
| 11 |
+
if (window.__faChat) await new Promise(() => {}); // second copy: park forever
|
| 12 |
+
window.__faChat = true;
|
| 13 |
+
await new Promise(r => { let last = null; const ok = () => { const n = document.querySelector("#fa-log"); if (n && n === last) r(); else { last = n; setTimeout(ok, 120); } }; ok(); });
|
| 14 |
+
// @gradio/client must match the Space's Gradio major version (1.x for Gradio 5, 2.x for Gradio 6)
|
| 15 |
+
const { Client, handle_file } = await import(`https://cdn.jsdelivr.net/npm/@gradio/client@${C.client}/dist/index.min.js`);
|
| 16 |
+
const esc = s => String(s).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
| 17 |
+
const md = s => DOMPurify.sanitize(marked.parse(s || "", { breaks: true }));
|
| 18 |
+
|
| 19 |
+
const log = $("#fa-log"), input = $("#fa-input"), send = $("#fa-send"), stop = $("#fa-stop"), clear = $("#fa-clear"), status = $("#fa-status");
|
| 20 |
+
const history = []; // [{role, content}] text-only turns sent to the model
|
| 21 |
+
let stopNow = null, job = null, client = null, image = null, imageURL = null, lastImage = null, lastImageURL = null; // lastImage: reused for follow-ups
|
| 22 |
+
|
| 23 |
+
// ---------- connection ----------
|
| 24 |
+
async function connect() {
|
| 25 |
+
if (client) return client;
|
| 26 |
+
status.dataset.state = "connecting"; status.querySelector("span").textContent = "Connecting…";
|
| 27 |
+
// ask for status events too: @gradio/client 1.x only streams "data" by default, so "complete" would never arrive
|
| 28 |
+
client = await Client.connect(location.origin + location.pathname.replace(/\/$/, ""), { events: ["data", "status"] });
|
| 29 |
+
status.dataset.state = "ready"; status.querySelector("span").textContent = C.readyText || "Ready";
|
| 30 |
+
return client;
|
| 31 |
+
}
|
| 32 |
+
connect().catch(() => { status.dataset.state = "error"; status.querySelector("span").textContent = "Could not reach the model server"; });
|
| 33 |
+
|
| 34 |
+
// ---------- messages ----------
|
| 35 |
+
function bubble(role, html, extra = "") {
|
| 36 |
+
log.querySelector(".fa-empty")?.remove();
|
| 37 |
+
const el = document.createElement("div");
|
| 38 |
+
el.className = `msg ${role}${extra}`;
|
| 39 |
+
el.innerHTML = role === "assistant" ? `<img class="av" src="${C.avatar}" alt=""><div class="body">${html}</div>` : `<div class="body">${html}</div>`;
|
| 40 |
+
log.appendChild(el);
|
| 41 |
+
log.scrollTo({ top: log.scrollHeight, behavior: "smooth" });
|
| 42 |
+
return el.querySelector(".body");
|
| 43 |
+
}
|
| 44 |
+
function empty() {
|
| 45 |
+
log.innerHTML = `<div class="fa-empty"><img src="${C.avatar}" alt=""><h3>${esc(C.greeting.title)}</h3><p>${esc(C.greeting.text)}</p></div>`;
|
| 46 |
+
}
|
| 47 |
+
empty();
|
| 48 |
+
|
| 49 |
+
// ---------- examples ----------
|
| 50 |
+
$("#fa-examples").innerHTML = C.examples.map((x, i) => `<button type="button" class="ex" data-i="${i}" style="--d:${i * 50}ms">` +
|
| 51 |
+
(x.image ? `<img src="${x.image}" alt="">` : "") + `<span class="ex-label">${esc(x.label)}</span>` + (x.label !== x.text ? `<span class="ex-text">${esc(x.text)}</span>` : "") + `</button>`).join("");
|
| 52 |
+
$("#fa-examples").addEventListener("click", async e => {
|
| 53 |
+
const b = e.target.closest(".ex"); if (!b || job) return;
|
| 54 |
+
const x = C.examples[+b.dataset.i];
|
| 55 |
+
if (x.image) { const blob = await (await fetch(x.image)).blob(); setImage(new File([blob], x.image.split("/").pop(), { type: blob.type })); }
|
| 56 |
+
input.value = x.text; autosize(); submit();
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
// ---------- image (multimodal demos) ----------
|
| 60 |
+
const drop = $("#fa-image"), preview = $("#fa-preview");
|
| 61 |
+
function setImage(f) {
|
| 62 |
+
image = f; if (imageURL && imageURL !== lastImageURL) URL.revokeObjectURL(imageURL);
|
| 63 |
+
imageURL = f ? URL.createObjectURL(f) : null;
|
| 64 |
+
if (preview) { preview.hidden = !f; preview.querySelector("img").src = imageURL || ""; }
|
| 65 |
+
}
|
| 66 |
+
if (C.image && drop) {
|
| 67 |
+
drop.addEventListener("change", () => drop.files[0] && setImage(drop.files[0]));
|
| 68 |
+
preview.querySelector("button").addEventListener("click", () => setImage(null));
|
| 69 |
+
const composer = $(".composer");
|
| 70 |
+
["dragenter", "dragover"].forEach(t => composer.addEventListener(t, e => { e.preventDefault(); composer.classList.add("drag"); }));
|
| 71 |
+
["dragleave", "drop"].forEach(t => composer.addEventListener(t, e => { e.preventDefault(); composer.classList.remove("drag"); }));
|
| 72 |
+
composer.addEventListener("drop", e => { const f = [...e.dataTransfer.files].find(f => f.type.startsWith("image/")); if (f) setImage(f); });
|
| 73 |
+
input.addEventListener("paste", e => { const f = [...(e.clipboardData?.files || [])].find(f => f.type.startsWith("image/")); if (f) setImage(f); });
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// ---------- params ----------
|
| 77 |
+
const params = Object.fromEntries(C.params.map(p => [p.key, p.value]));
|
| 78 |
+
$("#fa-params").innerHTML = C.params.map(p => `<label class="param"><span>${esc(p.label)}<b id="pv-${p.key}">${p.value}</b></span>` +
|
| 79 |
+
`<input type="range" min="${p.min}" max="${p.max}" step="${p.step}" value="${p.value}" data-k="${p.key}"></label>`).join("");
|
| 80 |
+
$("#fa-params").addEventListener("input", e => { const k = e.target.dataset.k; if (!k) return; params[k] = +e.target.value; $(`#pv-${k}`).textContent = e.target.value; });
|
| 81 |
+
|
| 82 |
+
// ---------- composer ----------
|
| 83 |
+
function autosize() { input.style.height = "auto"; input.style.height = Math.min(input.scrollHeight, 200) + "px"; send.disabled = !input.value.trim() || !!job; }
|
| 84 |
+
input.addEventListener("input", autosize);
|
| 85 |
+
input.addEventListener("keydown", e => { if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); submit(); } });
|
| 86 |
+
send.addEventListener("click", submit);
|
| 87 |
+
stop.addEventListener("click", () => { job?.cancel(); stopNow?.(); });
|
| 88 |
+
clear.addEventListener("click", () => { if (job) return; history.length = 0; setImage(null); lastImage = null; empty(); });
|
| 89 |
+
autosize();
|
| 90 |
+
|
| 91 |
+
async function submit() {
|
| 92 |
+
const text = input.value.trim();
|
| 93 |
+
if (!text || job) return;
|
| 94 |
+
const img = image || lastImage, imgURL = image ? imageURL : lastImageURL;
|
| 95 |
+
if (C.image && !img) { flash("Add an image first: upload, drop or paste a chart or table."); return; }
|
| 96 |
+
input.value = ""; autosize();
|
| 97 |
+
bubble("user", (image ? `<img class="thumb" src="${imageURL}" alt="">` : img ? `<span class="reuse"><img src="${imgURL}" alt="">same image</span>` : "") + esc(text).replace(/\n/g, "<br>"));
|
| 98 |
+
const out = bubble("assistant", `<span class="typing"><i></i><i></i><i></i></span>`, " pending");
|
| 99 |
+
const msgEl = out.parentElement;
|
| 100 |
+
document.body.classList.add("busy"); stop.hidden = false; send.hidden = true;
|
| 101 |
+
let reply = "", t0 = performance.now();
|
| 102 |
+
try {
|
| 103 |
+
const c = await connect();
|
| 104 |
+
const args = { message: text, history: history.slice(), ...params };
|
| 105 |
+
if (C.image) args.image = handle_file(img);
|
| 106 |
+
job = c.submit(C.endpoint, args);
|
| 107 |
+
// Don't `break` out of the iterator: with @gradio/client 1.x its return() never settles. Settle on our own instead,
|
| 108 |
+
// on "complete", on "error", or when Stop is pressed.
|
| 109 |
+
await new Promise((resolve, reject) => {
|
| 110 |
+
stopNow = resolve;
|
| 111 |
+
(async () => {
|
| 112 |
+
try {
|
| 113 |
+
for await (const ev of job) {
|
| 114 |
+
if (ev.type === "data") {
|
| 115 |
+
reply = ev.data[0] ?? "";
|
| 116 |
+
msgEl.classList.remove("pending");
|
| 117 |
+
out.innerHTML = md(reply) + `<span class="caret"></span>`;
|
| 118 |
+
log.scrollTo({ top: log.scrollHeight });
|
| 119 |
+
} else if (ev.type === "status") {
|
| 120 |
+
if (ev.stage === "pending" && ev.queue && ev.position > 0) out.innerHTML = `<span class="queue">In queue · position ${ev.position + 1}</span>`;
|
| 121 |
+
if (ev.stage === "error") return reject(new Error(ev.message || "The model returned an error."));
|
| 122 |
+
if (ev.stage === "complete") return resolve();
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
resolve();
|
| 126 |
+
} catch (err) { reject(err); }
|
| 127 |
+
})();
|
| 128 |
+
});
|
| 129 |
+
out.innerHTML = md(reply) || `<em class="muted">No answer.</em>`;
|
| 130 |
+
msgEl.insertAdjacentHTML("beforeend", `<div class="meta">${((performance.now() - t0) / 1000).toFixed(1)} s · <button type="button" class="copy">Copy</button></div>`);
|
| 131 |
+
msgEl.querySelector(".copy").addEventListener("click", e => navigator.clipboard.writeText(reply).then(() => (e.target.textContent = "Copied ✓")));
|
| 132 |
+
history.push({ role: "user", content: text }, { role: "assistant", content: reply });
|
| 133 |
+
if (C.image && image) { lastImage = image; lastImageURL = imageURL; imageURL = null; }
|
| 134 |
+
} catch (err) {
|
| 135 |
+
msgEl.classList.remove("pending"); msgEl.classList.add("error");
|
| 136 |
+
out.innerHTML = reply ? md(reply) : `<b>Something went wrong.</b> ${esc(err.message || err)}`;
|
| 137 |
+
} finally {
|
| 138 |
+
job = null; stopNow = null; document.body.classList.remove("busy"); stop.hidden = true; send.hidden = false; autosize();
|
| 139 |
+
if (C.image) { image = null; if (preview) preview.hidden = true; }
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
function flash(t) {
|
| 144 |
+
const n = $("#fa-note"); n.textContent = t; n.classList.add("on");
|
| 145 |
+
clearTimeout(flash._t); flash._t = setTimeout(() => n.classList.remove("on"), 3200);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// ---------- page chrome ----------
|
| 149 |
+
document.querySelectorAll("[data-count]").forEach(el => {
|
| 150 |
+
const to = +el.dataset.count, dec = +(el.dataset.dec || 0), t0 = performance.now();
|
| 151 |
+
if (matchMedia("(prefers-reduced-motion: reduce)").matches || document.hidden) { el.textContent = to.toFixed(dec); return; }
|
| 152 |
+
const step = t => { const p = Math.min(1, (t - t0) / 1000); el.textContent = (to * (1 - Math.pow(1 - p, 3))).toFixed(dec); if (p < 1) requestAnimationFrame(step); };
|
| 153 |
+
requestAnimationFrame(step); setTimeout(() => (el.textContent = to.toFixed(dec)), 1300);
|
| 154 |
+
});
|
| 155 |
+
const io = new IntersectionObserver(es => es.forEach(en => { if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); } }), { threshold: .1 });
|
| 156 |
+
document.querySelectorAll(".reveal").forEach(el => io.observe(el));
|
page/style.css
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* The Fin AI — leaderboard page (tokens from thefin.ai) */
|
| 2 |
+
:root{
|
| 3 |
+
--ink:#0f1528; --soft:#4f5a6e; --muted:#8a93a3; --line:#e4e7ee;
|
| 4 |
+
--paper:#fcfcfd; --paper-2:#f4f6fa; --card:#fff;
|
| 5 |
+
--accent:#0060b0; --accent-deep:#183898; --accent-light:#68a8d8; --accent-soft:#e8eff8; --gold:#c9a24a;
|
| 6 |
+
--serif:'Newsreader',Georgia,serif; --sans:'Inter',system-ui,-apple-system,sans-serif; --mono:'JetBrains Mono',ui-monospace,monospace;
|
| 7 |
+
--wrap:1120px; --ease:cubic-bezier(.22,1,.36,1); --spring:cubic-bezier(.34,1.56,.64,1);
|
| 8 |
+
color-scheme:light;
|
| 9 |
+
}
|
| 10 |
+
@media (prefers-color-scheme:dark){
|
| 11 |
+
:root:not([data-theme="light"]){--ink:#e8ecf4;--soft:#a3adbf;--muted:#6f7a8e;--line:#232b40;--paper:#0b0f1c;--paper-2:#10162a;--card:#121829;--accent:#68a8d8;--accent-deep:#9cc6ea;--accent-soft:#16244a;color-scheme:dark}
|
| 12 |
+
}
|
| 13 |
+
:root[data-theme="dark"]{--ink:#e8ecf4;--soft:#a3adbf;--muted:#6f7a8e;--line:#232b40;--paper:#0b0f1c;--paper-2:#10162a;--card:#121829;--accent:#68a8d8;--accent-deep:#9cc6ea;--accent-soft:#16244a;color-scheme:dark}
|
| 14 |
+
*{box-sizing:border-box}
|
| 15 |
+
html{scroll-behavior:smooth}
|
| 16 |
+
body{margin:0;font-family:var(--sans);color:var(--ink);background:var(--paper);font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;overflow-x:hidden}
|
| 17 |
+
h1,h2,h3{font-family:var(--serif);font-weight:500;line-height:1.1;letter-spacing:-.02em;margin:0}
|
| 18 |
+
p{margin:0;color:var(--soft)}
|
| 19 |
+
a{color:inherit;text-decoration:none}
|
| 20 |
+
code{font-family:var(--mono);font-size:.85em;background:var(--paper-2);padding:1px 5px;border-radius:5px}
|
| 21 |
+
.wrap{width:100%;max-width:var(--wrap);margin:0 auto;padding:0 28px}
|
| 22 |
+
::selection{background:var(--accent-deep);color:#fff}
|
| 23 |
+
|
| 24 |
+
.progress{position:fixed;inset:0 0 auto 0;height:2px;z-index:60;transform-origin:0 50%;transform:scaleX(0);background:linear-gradient(90deg,var(--accent-light),var(--accent),var(--accent-deep))}
|
| 25 |
+
.header{position:fixed;top:0;left:0;right:0;z-index:50;transition:background .4s var(--ease),box-shadow .4s}
|
| 26 |
+
.header.scrolled{background:color-mix(in srgb,var(--paper) 80%,transparent);backdrop-filter:blur(14px) saturate(1.4);-webkit-backdrop-filter:blur(14px) saturate(1.4);box-shadow:0 1px 0 var(--line)}
|
| 27 |
+
.nav{display:flex;align-items:center;justify-content:space-between;height:68px}
|
| 28 |
+
.brand{display:inline-flex;align-items:center;gap:10px;font-family:var(--serif);font-size:1.2rem;font-weight:500;white-space:nowrap}
|
| 29 |
+
.brand img{height:24px;width:auto;transition:transform .6s var(--spring)}
|
| 30 |
+
.brand:hover img{transform:rotate(-12deg) scale(1.08)}
|
| 31 |
+
.links{display:flex;gap:2px}
|
| 32 |
+
.links a{font-size:.88rem;color:var(--soft);padding:7px 13px;border-radius:999px;transition:background .25s,color .25s}
|
| 33 |
+
.links a:hover{background:var(--accent-soft);color:var(--ink)}
|
| 34 |
+
|
| 35 |
+
.kicker{display:inline-flex;align-items:center;gap:10px;font-size:.72rem;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--accent)}
|
| 36 |
+
.kicker::before{content:"";width:22px;height:1px;background:currentColor}
|
| 37 |
+
|
| 38 |
+
.btn{--mx:50%;--my:50%;position:relative;display:inline-flex;align-items:center;gap:10px;font:500 .9rem var(--sans);padding:11px 20px;border-radius:999px;border:0;background:var(--ink);color:var(--paper);overflow:hidden;isolation:isolate;cursor:pointer;transition:transform .35s var(--spring),box-shadow .35s}
|
| 39 |
+
.btn::before{content:"";position:absolute;inset:0;z-index:-1;background:radial-gradient(120px circle at var(--mx) var(--my),rgba(104,168,216,.55),transparent 60%);opacity:0;transition:opacity .3s}
|
| 40 |
+
.btn:hover::before{opacity:1}
|
| 41 |
+
.btn:hover{box-shadow:0 10px 30px -10px rgba(24,56,152,.6)}
|
| 42 |
+
.btn:active{transform:scale(.96)}
|
| 43 |
+
.btn .arrow{transition:transform .35s var(--spring)}
|
| 44 |
+
.btn:hover .arrow{transform:translateX(4px)}
|
| 45 |
+
.btn--ghost{background:transparent;color:var(--ink);box-shadow:inset 0 0 0 1px var(--line)}
|
| 46 |
+
.btn--ghost:hover{box-shadow:inset 0 0 0 1px var(--ink)}
|
| 47 |
+
.btn--sm{padding:8px 15px;font-size:.82rem}
|
| 48 |
+
|
| 49 |
+
/* hero */
|
| 50 |
+
.hero{display:grid;grid-template-columns:1.3fr 1fr;gap:56px;align-items:center;min-height:88svh;padding-top:120px;padding-bottom:60px}
|
| 51 |
+
.hero h1{font-size:clamp(2.8rem,6.4vw,5.2rem);letter-spacing:-.035em;margin:22px 0 18px}
|
| 52 |
+
.hero h1 em{font-style:italic;color:var(--accent-deep)}
|
| 53 |
+
.lead{font-size:1.08rem;max-width:52ch}
|
| 54 |
+
.fb-stats{display:flex;gap:34px;margin:34px 0 34px;flex-wrap:wrap}
|
| 55 |
+
.fb-stats div{display:flex;flex-direction:column}
|
| 56 |
+
.fb-stats b{font-family:var(--serif);font-weight:500;font-size:2.6rem;line-height:1;letter-spacing:-.02em;color:var(--ink);font-variant-numeric:tabular-nums}
|
| 57 |
+
.fb-stats span{font-size:.8rem;color:var(--muted);margin-top:6px;letter-spacing:.02em}
|
| 58 |
+
.ctas{display:flex;gap:12px;flex-wrap:wrap}
|
| 59 |
+
.hero-viz{position:relative;margin:0;height:420px;border-radius:24px;background:linear-gradient(160deg,var(--paper-2),var(--card));box-shadow:0 0 0 1px var(--line);padding:20px 22px 0;display:flex;flex-direction:column}
|
| 60 |
+
.hero-viz figcaption{white-space:nowrap;gap:12px;display:flex;justify-content:space-between;align-items:center;font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
|
| 61 |
+
.hero-viz figcaption a{letter-spacing:.02em;text-transform:none;font-size:.8rem;color:var(--accent);transition:transform .35s var(--spring)}
|
| 62 |
+
.hero-viz figcaption a:hover{transform:translateX(3px)}
|
| 63 |
+
.podium{position:relative;flex:1;display:grid;grid-template-columns:repeat(8,1fr);gap:10px;align-items:end;padding:54px 0 0;border-bottom:1px solid var(--line);margin-bottom:84px}
|
| 64 |
+
.podium .axis-note{position:absolute;right:0;bottom:-74px;font-size:.66rem;color:var(--muted)}
|
| 65 |
+
.podium .axis{position:absolute;left:-14px;bottom:-7px;font-size:.66rem;color:var(--muted)}
|
| 66 |
+
|
| 67 |
+
.col{position:relative;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center}
|
| 68 |
+
.col .bar{position:relative;width:100%;max-width:44px;height:0;border-radius:10px 10px 3px 3px;background:linear-gradient(180deg,var(--c),color-mix(in srgb,var(--c) 70%,transparent));transition:height 1.1s var(--ease) var(--d),transform .35s var(--spring),filter .25s;transform-origin:50% 100%}
|
| 69 |
+
.podium.in .col .bar{height:var(--h)}
|
| 70 |
+
.col.hl .bar{background:linear-gradient(180deg,var(--accent-deep),color-mix(in srgb,var(--accent-deep) 70%,transparent));box-shadow:0 10px 24px -10px var(--accent-deep)}
|
| 71 |
+
.col .bar img{position:absolute;left:50%;top:-40px;width:30px;height:30px;margin-left:-15px;border-radius:50%;object-fit:contain;background:#fff;padding:3px;box-shadow:0 0 0 1px var(--line),0 4px 12px -6px rgba(15,21,40,.35);opacity:0;transform:translateY(8px) scale(.6);transition:opacity .5s var(--ease) calc(var(--d) + .6s),transform .6s var(--spring) calc(var(--d) + .6s)}
|
| 72 |
+
.col .val{position:absolute;left:50%;top:8px;transform:translateX(-50%);font-size:.7rem;font-weight:600;color:#fff;font-variant-numeric:tabular-nums;opacity:0;transition:opacity .4s calc(var(--d) + .8s)}
|
| 73 |
+
.podium.in .col .bar img{opacity:1;transform:none}
|
| 74 |
+
.podium.in .col .val{opacity:1}
|
| 75 |
+
.col .nm{position:absolute;top:100%;right:50%;margin-top:10px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.7rem;color:var(--soft);transform-origin:right center;transform:rotate(-38deg)}
|
| 76 |
+
.col.hl .nm{color:var(--accent-deep);font-weight:600}
|
| 77 |
+
.col:hover .bar{transform:scaleX(1.08);filter:brightness(1.08)}
|
| 78 |
+
.col:hover .bar img{transform:translateY(-4px) scale(1.12);transition-delay:0s}
|
| 79 |
+
|
| 80 |
+
/* sections */
|
| 81 |
+
.fb-sec{padding:84px 28px 24px;opacity:0;transform:translateY(26px);transition:opacity .9s var(--ease),transform 1s var(--ease)}
|
| 82 |
+
.fb-sec.in{opacity:1;transform:none}
|
| 83 |
+
.fb-sec h2{font-size:clamp(1.9rem,3.6vw,2.7rem);margin:14px 0 10px}
|
| 84 |
+
.sec-head{display:flex;justify-content:space-between;align-items:flex-end;gap:20px;flex-wrap:wrap;margin-bottom:22px}
|
| 85 |
+
.sec-head p{max-width:60ch}
|
| 86 |
+
.controls{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:18px}
|
| 87 |
+
|
| 88 |
+
/* takeaways */
|
| 89 |
+
.fb-takeaways{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px;margin-top:26px}
|
| 90 |
+
.fb-take{position:relative;padding:26px 24px 24px;border-radius:20px;background:var(--card);box-shadow:0 0 0 1px var(--line);opacity:0;transform:translateY(18px);transition:opacity .8s var(--ease) var(--d),transform .9s var(--ease) var(--d),box-shadow .3s}
|
| 91 |
+
.fb-takeaways.in .fb-take{opacity:1;transform:none}
|
| 92 |
+
.fb-take:hover{box-shadow:0 0 0 1px var(--accent-light),0 16px 34px -18px rgba(24,56,152,.45)}
|
| 93 |
+
.fb-take-i{font-family:var(--mono);font-size:.72rem;color:var(--muted)}
|
| 94 |
+
.fb-take-n{display:block;font-family:var(--serif);font-weight:500;font-size:2.7rem;line-height:1;letter-spacing:-.02em;color:var(--accent-deep);margin:14px 0 12px}
|
| 95 |
+
.fb-take p{font-size:.93rem;line-height:1.55}
|
| 96 |
+
.fb-take p b{color:var(--ink);font-weight:600}
|
| 97 |
+
|
| 98 |
+
/* segmented control + legend */
|
| 99 |
+
.fb-seg{position:relative;display:inline-flex;padding:4px;border-radius:999px;background:var(--paper-2);box-shadow:inset 0 0 0 1px var(--line);overflow-x:auto;max-width:100%}
|
| 100 |
+
.fb-seg button{position:relative;z-index:1;border:0;background:none;font:500 .84rem var(--sans);color:var(--soft);padding:7px 14px;border-radius:999px;cursor:pointer;white-space:nowrap;transition:color .25s}
|
| 101 |
+
.fb-seg button.on{color:var(--paper)}
|
| 102 |
+
.fb-seg-pill{position:absolute;top:4px;left:0;height:calc(100% - 8px);border-radius:999px;background:var(--ink);transition:transform .45s var(--ease),width .45s var(--ease)}
|
| 103 |
+
.fb-legend{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
| 104 |
+
.fb-legend button{display:inline-flex;align-items:center;gap:8px;border:0;background:var(--card);box-shadow:0 0 0 1px var(--line);color:var(--ink);font:500 .82rem var(--sans);padding:6px 12px;border-radius:999px;cursor:pointer;transition:opacity .25s,box-shadow .25s,transform .3s var(--spring)}
|
| 105 |
+
.fb-legend button:hover{box-shadow:0 0 0 1px var(--accent-light)}
|
| 106 |
+
.fb-legend button:active{transform:scale(.95)}
|
| 107 |
+
.fb-legend button.off{opacity:.4}
|
| 108 |
+
.fb-legend button.off i{background:var(--muted) !important}
|
| 109 |
+
.fb-legend i,.pill i,.fb-tip-type i{display:inline-block;width:9px;height:9px;border-radius:50%}
|
| 110 |
+
.fb-legend-hl{display:inline-flex;align-items:center;gap:8px;font-size:.8rem;color:var(--soft);margin-left:6px}
|
| 111 |
+
.fb-legend-hl i{width:12px;height:12px;border-radius:50%;box-shadow:0 0 0 2px var(--paper),0 0 0 3.5px var(--accent-deep)}
|
| 112 |
+
|
| 113 |
+
/* logos */
|
| 114 |
+
.fb-logo{width:20px;height:20px;flex:none;border-radius:50%;object-fit:contain;background:#fff;padding:2px;box-shadow:0 0 0 1px var(--line);vertical-align:middle}
|
| 115 |
+
.fb-logo-sm{width:16px;height:16px;padding:1.5px;margin-right:7px}
|
| 116 |
+
.fb-logo-x{display:inline-grid;place-items:center;font-size:.62rem;font-weight:600;color:var(--soft);background:var(--paper-2)}
|
| 117 |
+
.fb-tip>b{display:flex !important;align-items:center}
|
| 118 |
+
.mname{display:flex;align-items:center;gap:9px}
|
| 119 |
+
.fb-mname{gap:0}
|
| 120 |
+
|
| 121 |
+
/* ranking */
|
| 122 |
+
.fb-rank{position:relative;transition:height .5s var(--ease)}
|
| 123 |
+
.fb-row{position:absolute;left:0;right:0;height:34px;display:grid;grid-template-columns:34px minmax(120px,240px) 1fr 56px;align-items:center;gap:12px;padding:0 8px;border-radius:10px;cursor:default;transition:transform .7s var(--ease),opacity .4s,background .2s}
|
| 124 |
+
.fb-row:hover{background:var(--accent-soft)}
|
| 125 |
+
.fb-row.gone{opacity:0;pointer-events:none}
|
| 126 |
+
.fb-pos{font-family:var(--mono);font-size:.74rem;color:var(--muted);text-align:right}
|
| 127 |
+
.fb-name{display:flex;align-items:center;gap:9px;min-width:0;font-size:.88rem}
|
| 128 |
+
.fb-name>span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 129 |
+
.fb-row:hover .fb-logo{transform:rotate(-10deg) scale(1.12)}
|
| 130 |
+
.fb-logo{transition:transform .35s var(--spring)}
|
| 131 |
+
.fb-row.hl .fb-name{font-weight:600;color:var(--accent-deep)}
|
| 132 |
+
.fb-row.hl .fb-name>span::after{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent-deep);margin-left:7px;vertical-align:middle}
|
| 133 |
+
.fb-track{height:12px;border-radius:999px;background:var(--paper-2);overflow:hidden}
|
| 134 |
+
.fb-bar{display:block;height:100%;width:0;border-radius:999px;transition:width 1s var(--ease)}
|
| 135 |
+
.fb-row.hl .fb-bar{background:var(--accent-deep) !important}
|
| 136 |
+
.fb-val{font-variant-numeric:tabular-nums;font-size:.86rem;font-weight:500;text-align:right}
|
| 137 |
+
|
| 138 |
+
/* strip plot */
|
| 139 |
+
.fb-strip{width:100%;margin-top:8px}
|
| 140 |
+
.fb-strip svg{display:block;overflow:visible;font-family:var(--sans)}
|
| 141 |
+
.fb-grid{stroke:var(--line);stroke-dasharray:2 4}
|
| 142 |
+
.fb-lane{stroke:var(--line)}
|
| 143 |
+
.fb-axis{font-size:11px;fill:var(--muted)}
|
| 144 |
+
.fb-task{font-size:13px;font-weight:500;fill:var(--ink)}
|
| 145 |
+
.fb-med{stroke:var(--ink);stroke-width:1.5;opacity:.5}
|
| 146 |
+
.fb-medt{font-size:10px;fill:var(--muted)}
|
| 147 |
+
.fb-dot{opacity:0;transform-box:fill-box;transform-origin:center;transform:scale(0);stroke:var(--paper);stroke-width:1.5;cursor:pointer;transition:opacity .5s var(--ease) var(--d),transform .6s var(--spring) var(--d),r .2s}
|
| 148 |
+
.fb-strip.in .fb-dot{opacity:.85;transform:none}
|
| 149 |
+
.fb-dot.hl{stroke:var(--accent-deep);stroke-width:2.5}
|
| 150 |
+
.fb-strip.in svg.focus .fb-dot{opacity:.12;transition-delay:0s}
|
| 151 |
+
.fb-strip.in svg.focus .fb-dot.on{opacity:1;r:7.5}
|
| 152 |
+
.fb-path{fill:none;stroke-width:2;stroke-dasharray:4 4;opacity:.9;pointer-events:none}
|
| 153 |
+
.fb-pathlabel{font-size:12px;font-weight:600;fill:var(--ink);paint-order:stroke;stroke:var(--paper);stroke-width:4px;pointer-events:none}
|
| 154 |
+
.task-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:30px}
|
| 155 |
+
|
| 156 |
+
/* cards */
|
| 157 |
+
.card{display:block;padding:20px 22px;border-radius:18px;background:var(--card);box-shadow:0 0 0 1px var(--line);transition:box-shadow .3s,transform .4s var(--spring)}
|
| 158 |
+
a.card:hover{transform:translateY(-3px);box-shadow:0 0 0 1px var(--accent-light),0 14px 30px -16px rgba(24,56,152,.45)}
|
| 159 |
+
.card h3{font-size:1.2rem;margin-bottom:8px}
|
| 160 |
+
.card p{font-size:.9rem;line-height:1.6}
|
| 161 |
+
.card p a{color:var(--accent);text-decoration:underline;text-underline-offset:3px;text-decoration-color:color-mix(in srgb,var(--accent) 35%,transparent)}
|
| 162 |
+
.card .tag{display:inline-block;margin-top:12px;font-size:.7rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
|
| 163 |
+
|
| 164 |
+
/* matrix */
|
| 165 |
+
.fb-matrix{overflow-x:auto}
|
| 166 |
+
.fb-mx{display:grid;grid-template-columns:minmax(150px,1.6fr) repeat(var(--cols),minmax(76px,1fr));gap:3px;min-width:620px}
|
| 167 |
+
.fb-mh{border:0;background:none;font:600 .74rem var(--sans);letter-spacing:.04em;color:var(--soft);text-align:center;padding:8px 4px;border-radius:8px;cursor:pointer;transition:background .2s,color .2s}
|
| 168 |
+
button.fb-mh:hover,.fb-mh.on{background:var(--accent-soft);color:var(--ink)}
|
| 169 |
+
.fb-mh span{margin-left:4px}
|
| 170 |
+
.fb-mh.fb-mname{text-align:left;cursor:default}
|
| 171 |
+
.fb-mname{font-size:.84rem;display:flex;align-items:center;padding:0 8px;border-radius:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:background .15s}
|
| 172 |
+
.fb-mname.hl{font-weight:600;color:var(--accent-deep)}
|
| 173 |
+
.fb-mc{font-size:.8rem;font-variant-numeric:tabular-nums;text-align:center;padding:7px 2px;border-radius:7px;color:var(--ink);opacity:0;transform:scale(.9);transition:opacity .5s var(--ease) var(--d),transform .5s var(--spring) var(--d),box-shadow .15s}
|
| 174 |
+
.fb-mc.dk{color:#fff;font-weight:500}
|
| 175 |
+
.fb-matrix.in .fb-mc{opacity:1;transform:none}
|
| 176 |
+
.fb-mc.x{box-shadow:inset 0 0 0 1.5px var(--ink)}
|
| 177 |
+
.fb-mname.x{background:var(--accent-soft)}
|
| 178 |
+
.fb-note{font-size:.78rem;color:var(--muted);margin-top:12px}
|
| 179 |
+
|
| 180 |
+
/* table */
|
| 181 |
+
.tools{display:flex;gap:10px;align-items:center}
|
| 182 |
+
.tools input{font:400 .9rem var(--sans);color:var(--ink);background:var(--card);border:0;box-shadow:inset 0 0 0 1px var(--line);border-radius:999px;padding:9px 16px;width:240px;outline:none;transition:box-shadow .25s,width .4s var(--ease)}
|
| 183 |
+
.tools input:focus{box-shadow:inset 0 0 0 1.5px var(--accent-light);width:280px}
|
| 184 |
+
.table-wrap{overflow-x:auto;border-radius:18px;box-shadow:0 0 0 1px var(--line);background:var(--card)}
|
| 185 |
+
table{width:100%;border-collapse:collapse;font-size:.88rem;min-width:720px}
|
| 186 |
+
th{white-space:nowrap;position:sticky;top:0;background:var(--paper-2);text-align:left;font-weight:600;font-size:.76rem;letter-spacing:.03em;color:var(--soft);padding:10px 12px}
|
| 187 |
+
th button{border:0;background:none;font:inherit;color:inherit;cursor:pointer;padding:0}
|
| 188 |
+
th button.on,th button:hover{color:var(--ink)}
|
| 189 |
+
td{padding:9px 12px;border-top:1px solid var(--line);white-space:nowrap}
|
| 190 |
+
td.num{font-variant-numeric:tabular-nums;text-align:right}
|
| 191 |
+
th:nth-child(n+3):not(:last-child){text-align:right}
|
| 192 |
+
tbody tr{transition:background .15s}
|
| 193 |
+
tbody tr:hover{background:var(--accent-soft)}
|
| 194 |
+
tr.hl td:nth-child(2){font-weight:600;color:var(--accent-deep)}
|
| 195 |
+
td.empty{text-align:center;color:var(--muted);padding:28px}
|
| 196 |
+
.pill{display:inline-flex;align-items:center;gap:7px;font-size:.8rem;color:var(--soft)}
|
| 197 |
+
|
| 198 |
+
/* about */
|
| 199 |
+
.about-grid{display:grid;grid-template-columns:1fr 1fr 1.4fr;gap:16px}
|
| 200 |
+
.cite pre{font-family:var(--mono);font-size:.7rem;line-height:1.55;white-space:pre-wrap;word-break:break-word;color:var(--soft);background:var(--paper-2);border-radius:12px;padding:12px 14px;margin:0 0 14px;max-height:170px;overflow:auto}
|
| 201 |
+
|
| 202 |
+
/* tooltip */
|
| 203 |
+
.fb-root{position:relative}
|
| 204 |
+
.fb-tip{position:absolute;top:0;left:0;z-index:30;min-width:190px;padding:12px 14px;border-radius:14px;background:var(--ink);color:var(--paper);font-size:.8rem;pointer-events:none;opacity:0;transition:opacity .15s;box-shadow:0 18px 40px -16px rgba(15,21,40,.6)}
|
| 205 |
+
.fb-tip.on{opacity:1}
|
| 206 |
+
.fb-tip>b{display:block;font-size:.9rem;margin-bottom:2px}
|
| 207 |
+
.fb-tip-type{display:flex;align-items:center;gap:6px;font-size:.72rem;opacity:.7;margin-bottom:8px}
|
| 208 |
+
.fb-tip div{display:flex;justify-content:space-between;gap:18px;padding:2px 0;font-variant-numeric:tabular-nums}
|
| 209 |
+
.fb-tip div span{opacity:.7}
|
| 210 |
+
|
| 211 |
+
.footer{display:flex;justify-content:space-between;align-items:center;gap:18px;flex-wrap:wrap;margin-top:80px;padding-top:28px;padding-bottom:40px;border-top:1px solid var(--line);font-size:.85rem;color:var(--muted)}
|
| 212 |
+
.footer nav{display:flex;gap:18px;flex-wrap:wrap}
|
| 213 |
+
.footer nav a{color:var(--soft);transition:color .25s}
|
| 214 |
+
.footer nav a:hover{color:var(--ink)}
|
| 215 |
+
|
| 216 |
+
@media (max-width:900px){
|
| 217 |
+
.hero{grid-template-columns:1fr;min-height:auto;gap:36px}
|
| 218 |
+
.hero-viz{height:360px}
|
| 219 |
+
.about-grid{grid-template-columns:1fr}
|
| 220 |
+
}
|
| 221 |
+
@media (max-width:640px){
|
| 222 |
+
.wrap{padding-left:16px;padding-right:16px}
|
| 223 |
+
.fb-sec{padding:64px 16px 16px}
|
| 224 |
+
.links a:not([href^="http"]){display:none}
|
| 225 |
+
.fb-row{grid-template-columns:24px minmax(90px,130px) 1fr 44px;gap:8px;padding:0 4px}
|
| 226 |
+
.fb-stats{gap:22px}.fb-stats b{font-size:2.1rem}
|
| 227 |
+
.tools{width:100%}.tools input,.tools input:focus{flex:1;width:auto}
|
| 228 |
+
}
|
| 229 |
+
.no-anim,.no-anim *{transition:none !important;animation:none !important}
|
| 230 |
+
@media (prefers-reduced-motion:reduce){*,*::before,*::after{transition-duration:.01ms !important;animation:none !important}}
|
| 231 |
+
|
| 232 |
+
/* last update */
|
| 233 |
+
.hero-top{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
| 234 |
+
.updated{display:inline-flex;align-items:center;gap:8px;font-size:.76rem;color:var(--soft);padding:5px 12px 5px 10px;border-radius:999px;background:var(--card);box-shadow:0 0 0 1px var(--line);animation:fadeUp .6s var(--ease) both}
|
| 235 |
+
.updated i{width:7px;height:7px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 3px color-mix(in srgb,#16a34a 22%,transparent)}
|
| 236 |
+
.updated.stale{color:#8a5a00;background:color-mix(in srgb,#f5b942 14%,var(--card));box-shadow:0 0 0 1px color-mix(in srgb,#d99a1e 45%,transparent)}
|
| 237 |
+
.updated.stale i{background:#d99a1e;box-shadow:0 0 0 3px color-mix(in srgb,#d99a1e 25%,transparent);animation:pulse 2.4s ease-in-out infinite}
|
| 238 |
+
:root[data-theme="dark"] .updated.stale{color:#f5c76a}
|
| 239 |
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .updated.stale{color:#f5c76a}}
|
| 240 |
+
.data-meta{font-size:.86rem;margin-top:6px}
|
| 241 |
+
.stale-note{color:#b07400}
|
| 242 |
+
@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.35)}}
|
| 243 |
+
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
| 244 |
+
|
| 245 |
+
/* credits */
|
| 246 |
+
.credits{display:grid;grid-template-columns:1fr 1fr;gap:40px;margin-top:28px}
|
| 247 |
+
.credits-h{font-size:1.25rem}
|
| 248 |
+
.credits-sub{font-size:.86rem;margin:4px 0 18px}
|
| 249 |
+
.logo-wall{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
| 250 |
+
.logo-wall a{display:flex;align-items:center;justify-content:center;gap:10px;height:92px;padding:18px 22px;border-radius:18px;background:var(--card);box-shadow:0 0 0 1px var(--line);opacity:0;transform:translateY(14px);transition:opacity .7s var(--ease) var(--d),transform .8s var(--ease) var(--d),box-shadow .3s}
|
| 251 |
+
#credits.in .logo-wall a{opacity:1;transform:none}
|
| 252 |
+
.logo-wall img{max-height:40px;max-width:100%;object-fit:contain;filter:grayscale(1);opacity:.75;transition:filter .4s,opacity .4s,transform .45s var(--spring)}
|
| 253 |
+
.logo-wall img.mark{height:30px}
|
| 254 |
+
.logo-wall span{font-family:var(--serif);font-size:1.2rem;color:var(--ink);opacity:.8;transition:opacity .4s}
|
| 255 |
+
.logo-wall a:hover{box-shadow:0 0 0 1px var(--accent-light),0 14px 30px -18px rgba(24,56,152,.45)}
|
| 256 |
+
.logo-wall a:hover img{filter:none;opacity:1;transform:scale(1.06)}
|
| 257 |
+
.logo-wall a:hover span{opacity:1}
|
| 258 |
+
.ack{display:grid;gap:10px}
|
| 259 |
+
.ack a{display:flex;flex-direction:column;gap:2px;padding:13px 18px;border-radius:14px;background:var(--card);box-shadow:0 0 0 1px var(--line);transition:box-shadow .3s,transform .4s var(--spring)}
|
| 260 |
+
.ack a:hover{transform:translateX(4px);box-shadow:0 0 0 1px var(--accent-light)}
|
| 261 |
+
.ack b{font-weight:600;font-size:.95rem}
|
| 262 |
+
.ack a b::after{content:" ↗";color:var(--muted);font-weight:400}
|
| 263 |
+
.ack span{font-size:.84rem;color:var(--soft)}
|
| 264 |
+
.partners{display:flex;align-items:center;gap:22px;flex-wrap:wrap}
|
| 265 |
+
.partners>span:first-child{font-size:.7rem;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
|
| 266 |
+
.partners img{height:40px;width:auto;filter:grayscale(1);opacity:.6;transition:filter .3s,opacity .3s}
|
| 267 |
+
.partners a:hover img,.partners .p-img:hover img{filter:none;opacity:1}
|
| 268 |
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .logo-wall img:not(.mark),:root:not([data-theme="light"]) .partners img{filter:grayscale(1) invert(1) brightness(1.4)}}
|
| 269 |
+
:root[data-theme="dark"] .logo-wall img:not(.mark),:root[data-theme="dark"] .partners img{filter:grayscale(1) invert(1) brightness(1.4)}
|
| 270 |
+
@media (max-width:900px){.credits{grid-template-columns:1fr}}
|
requirements.txt
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
|
|
|
|
|
| 1 |
torch==2.8.0
|
| 2 |
transformers==4.44.0
|
| 3 |
spaces
|
| 4 |
pillow
|
| 5 |
accelerate
|
| 6 |
-
bitsandbytes
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gradio itself comes from sdk_version in README.md (5.49.1).
|
| 2 |
+
# Stays on Gradio 5: llava_llama3 needs transformers 4.44, which requires huggingface_hub<1.0 (Gradio 6 needs >=1.16).
|
| 3 |
torch==2.8.0
|
| 4 |
transformers==4.44.0
|
| 5 |
spaces
|
| 6 |
pillow
|
| 7 |
accelerate
|
| 8 |
+
bitsandbytes
|
| 9 |
+
huggingface-hub<1.0
|
| 10 |
+
pydantic<2.11
|