jiminHuang commited on
Commit
dadccad
·
verified ·
1 Parent(s): 292c29d

Redesign: custom The Fin AI page + gr.api /chat backend (ZeroGPU)

Browse files
Files changed (46) hide show
  1. .gitattributes +1 -0
  2. README.md +38 -5
  3. app.py +94 -119
  4. baklava.png → assets/examples/image_107.png +2 -2
  5. image_659.png → assets/examples/image_659.png +0 -0
  6. assets/finai-logo.png +0 -0
  7. assets/orgs/airc.png +0 -0
  8. assets/orgs/archimedes.svg +29 -0
  9. assets/orgs/columbia.png +0 -0
  10. assets/orgs/cuhksz.png +0 -0
  11. assets/orgs/harvard.svg +9 -0
  12. assets/orgs/manchester.png +0 -0
  13. assets/orgs/nju.svg +389 -0
  14. assets/orgs/nus.svg +0 -0
  15. assets/orgs/nvidia.svg +9 -0
  16. assets/orgs/nyu.svg +43 -0
  17. assets/orgs/rpi.svg +0 -0
  18. assets/orgs/stevens.png +0 -0
  19. assets/orgs/thefinai.png +0 -0
  20. assets/orgs/udem.svg +526 -0
  21. assets/orgs/ufl.svg +7 -0
  22. assets/orgs/whu.svg +62 -0
  23. assets/orgs/yale.svg +3 -0
  24. bee.jpg +0 -3
  25. fa_page.py +123 -0
  26. image_107.png +0 -0
  27. llava_llama3/.DS_Store +0 -0
  28. llava_llama3/model/__pycache__/__init__.cpython-311.pyc +0 -0
  29. llava_llama3/model/__pycache__/builder.cpython-311.pyc +0 -0
  30. llava_llama3/model/__pycache__/llava_arch.cpython-311.pyc +0 -0
  31. llava_llama3/model/language_model/__pycache__/llava_llama.cpython-311.pyc +0 -0
  32. llava_llama3/model/language_model/__pycache__/llava_mistral.cpython-311.pyc +0 -0
  33. llava_llama3/model/language_model/__pycache__/llava_mpt.cpython-311.pyc +0 -0
  34. llava_llama3/model/language_model/__pycache__/llava_phi3.cpython-311.pyc +0 -0
  35. llava_llama3/model/multimodal_encoder/__pycache__/builder.cpython-311.pyc +0 -0
  36. llava_llama3/model/multimodal_encoder/__pycache__/clip_encoder.cpython-311.pyc +0 -0
  37. llava_llama3/model/multimodal_projector/__pycache__/builder.cpython-311.pyc +0 -0
  38. llava_llama3/serve/__pycache__/__init__.cpython-311.pyc +0 -0
  39. llava_llama3/serve/__pycache__/cli.cpython-311.pyc +0 -0
  40. llava_llama3/train/__pycache__/llama_xformers_attn_monkey_patch.cpython-311.pyc +0 -0
  41. llava_llama3/train/__pycache__/llava_trainer.cpython-311.pyc +0 -0
  42. llava_llama3/train/__pycache__/train.cpython-311.pyc +0 -0
  43. page/chat.css +137 -0
  44. page/chat.js +156 -0
  45. page/style.css +270 -0
  46. 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: yellow
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 5.15.0
8
  python_version: "3.10"
9
  app_file: app.py
10
  pinned: false
11
  license: apache-2.0
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- import time
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
- import spaces
 
 
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 glob
24
- import pandas as pd
25
- from tqdm import tqdm
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
- PLACEHOLDER = """
55
- <div style="padding: 30px; text-align: center; display: flex; flex-direction: column; align-items: center;">
56
- <h1 style="font-size: 28px; margin-bottom: 2px; opacity: 0.55;">FinLLaVA</h1>
57
- <p style="font-size: 18px; margin-bottom: 2px; opacity: 0.65;">FinLLaVA is a LLaVA model fine-tuned from FinLLaMA and CLIP by the Fin AI</p>
58
- </div>
59
- """
60
-
61
- @spaces.GPU
62
- def bot_streaming(message, history):
63
- image_file = None
64
- if message["files"]:
65
- if type(message["files"][-1]) == dict:
66
- image_file = message["files"][-1]["path"]
67
- else:
68
- image_file = message["files"][-1]
69
- else:
70
- for hist in history:
71
- if type(hist[0]) == tuple:
72
- image_file = hist[0][0]
73
-
74
- if image_file is None:
75
- gr.Error("You need to upload an image for LLaVA to work.")
 
 
 
 
 
 
 
 
 
 
 
76
  return
77
-
78
  streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
79
-
80
- def generate():
81
- print('Running chat')
82
- output = chat_llava(
83
- args=args,
84
- image_file=image_file,
85
- text=message['text'],
86
- tokenizer=tokenizer,
87
- model=llava_model,
88
- image_processor=image_processor,
89
- context_len=context_len,
90
- streamer=streamer)
91
- return output
92
-
93
- thread = Thread(target=generate)
94
- thread.start()
95
- # thread.join()
96
-
97
- buffer = ""
98
- time.sleep(1.0)
99
- # output = generate()
100
- for new_text in streamer:
101
- buffer += new_text
102
- generated_text_without_prompt = buffer
103
- time.sleep(0.05)
104
- print (generated_text_without_prompt)
105
- yield generated_text_without_prompt
106
-
107
- chatbot = gr.Chatbot(placeholder=PLACEHOLDER,scale=1)
108
- chat_input = gr.MultimodalTextbox(interactive=True, file_types=["image"], placeholder="Enter message or upload file...", show_label=False)
109
- with gr.Blocks(fill_height=True) as demo:
110
- gr.ChatInterface(
111
- fn=bot_streaming,
112
- title="FinLLaVA-8B Demo",
113
- examples=[
114
- {"text": "What is the spending on Healthcare in July? A. 450 B. 600 C. 520 D. 510", "files": ["image_107.png"]},
115
- {"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", "files": ["image_659.png"]},
116
- ],
117
- run_examples_on_click=False,
118
- description="This is a demo of FinLLaVA-8B. For more details, see our paper: https://huggingface.co/papers/2408.11878",
119
- stop_btn="Stop Generation",
120
- multimodal=True,
121
- textbox=chat_input,
122
- chatbot=chatbot,
123
- )
124
-
125
- demo.queue(api_open=True)
126
- demo.launch(show_api=True, share=False)
 
 
 
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

  • SHA256: 8b21ba78250f852ca5990063866b1ace6432521d0251bde7f8de783b22c99a6d
  • Pointer size: 132 Bytes
  • Size of remote file: 5.37 MB
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 &amp; 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 => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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