mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
5
.env.example
Normal file
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
HOSTNAME=localhost
|
||||
|
||||
# PORTS
|
||||
KAFKA_PORT=9092
|
||||
REDIS_PORT=6377
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
**/.env
|
||||
**/.venv
|
||||
7
Makefile
7
Makefile
@@ -5,8 +5,13 @@ TEX := main.tex
|
||||
JOBNAME := main
|
||||
PDF := paper/$(BUILDDIR)/$(JOBNAME).pdf
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
all: pdf
|
||||
|
||||
run.webapp:
|
||||
@cd web && npm install && npm run dev
|
||||
|
||||
$(BUILDDIR):
|
||||
mkdir -p paper/$(BUILDDIR)
|
||||
|
||||
@@ -31,4 +36,4 @@ clean:
|
||||
rm -rf paper/$(BUILDDIR)/*
|
||||
|
||||
|
||||
.PHONY: all pdf clean watch
|
||||
.PHONY: all pdf clean watch run.webapp
|
||||
|
||||
23
backend/worker/main.py
Normal file
23
backend/worker/main.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import os
|
||||
import time
|
||||
from celery import Celery
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# Redis connection
|
||||
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379")
|
||||
app = Celery('worker', broker=redis_url, backend=redis_url)
|
||||
|
||||
@app.task
|
||||
def simple_task(message):
|
||||
"""A simple task that processes a message and returns a result"""
|
||||
time.sleep(2) # Simulate some work
|
||||
return f"Processed: {message}"
|
||||
|
||||
@app.task
|
||||
def add_numbers(x, y):
|
||||
"""Simple math task"""
|
||||
return x + y
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.start()
|
||||
@@ -0,0 +1,50 @@
|
||||
services:
|
||||
redis:
|
||||
container_name: "PHANTOM-redis"
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "${REDIS_PORT:-6378}:6379"
|
||||
volumes:
|
||||
- phantom_redis_data:/data
|
||||
restart: unless-stopped
|
||||
zookeeper:
|
||||
container_name: "PHANTOM-zookeeper"
|
||||
image: confluentinc/cp-zookeeper:latest
|
||||
environment:
|
||||
ZOOKEEPER_CLIENT_PORT: 2181
|
||||
ports:
|
||||
- "2181:2181"
|
||||
|
||||
kafka:
|
||||
container_name: "PHANTOM-kafka"
|
||||
image: confluentinc/cp-kafka:7.5.0
|
||||
depends_on:
|
||||
- zookeeper
|
||||
environment:
|
||||
KAFKA_BROKER_ID: 1
|
||||
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
||||
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
|
||||
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,PLAINTEXT_HOST://0.0.0.0:9092
|
||||
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
||||
ports:
|
||||
- "${KAFKA_PORT:-9092}:9092"
|
||||
volumes:
|
||||
- phantom_kafka_data:/var/lib/kafka/data
|
||||
|
||||
redpanda-console:
|
||||
container_name: "PHANTOM-redpanda-console"
|
||||
image: docker.redpanda.com/redpandadata/console:latest
|
||||
depends_on:
|
||||
- kafka
|
||||
environment:
|
||||
KAFKA_BROKERS: kafka:29092
|
||||
ports:
|
||||
- "${REDPANDA_CONSOLE_PORT:-8080}:8080"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
phantom_kafka_data:
|
||||
phantom_redis_data:
|
||||
|
||||
721
experiments/data_export.ipynb
Normal file
721
experiments/data_export.ipynb
Normal file
@@ -0,0 +1,721 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 98,
|
||||
"id": "62eafcd9-5462-4063-8873-0e7fb9add907",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"True"
|
||||
]
|
||||
},
|
||||
"execution_count": 98,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from kafka import KafkaConsumer\n",
|
||||
"import pandas as pd\n",
|
||||
"import json\n",
|
||||
"import numpy as np\n",
|
||||
"import os\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"from IPython.display import display, SVG, Image\n",
|
||||
"load_dotenv()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 86,
|
||||
"id": "4af65cb4-e8cf-4877-b2db-13ac19f3838f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"<class 'pandas.core.frame.DataFrame'>\n",
|
||||
"RangeIndex: 141 entries, 0 to 140\n",
|
||||
"Data columns (total 10 columns):\n",
|
||||
" # Column Non-Null Count Dtype \n",
|
||||
"--- ------ -------------- ----- \n",
|
||||
" 0 sessionId 141 non-null object \n",
|
||||
" 1 eventType 141 non-null object \n",
|
||||
" 2 ts 141 non-null int64 \n",
|
||||
" 3 targetEl 14 non-null object \n",
|
||||
" 4 targetUrl 1 non-null object \n",
|
||||
" 5 metadata_path 141 non-null object \n",
|
||||
" 6 metadata_referrer 6 non-null object \n",
|
||||
" 7 metadata_x 14 non-null float64\n",
|
||||
" 8 metadata_y 14 non-null float64\n",
|
||||
" 9 metadata_scrollY 121 non-null float64\n",
|
||||
"dtypes: float64(3), int64(1), object(6)\n",
|
||||
"memory usage: 11.1+ KB\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"KAFKA_PORT=os.getenv(\"KAFKA_PORT\", 9092)\n",
|
||||
"topic = \"user-interactions\"\n",
|
||||
"consumer = KafkaConsumer(\n",
|
||||
" topic, \n",
|
||||
" enable_auto_commit=True,\n",
|
||||
" value_deserializer=lambda x: json.loads(x.decode('utf-8')),\n",
|
||||
" auto_offset_reset='earliest',\n",
|
||||
" bootstrap_servers=['localhost:9092'])\n",
|
||||
"messages=consumer.poll(timeout_ms=1000,max_records=10000)\n",
|
||||
"df = []\n",
|
||||
"for m in messages.values():\n",
|
||||
" for i in m:\n",
|
||||
" df.append(i.value)\n",
|
||||
"df = pd.DataFrame(df)\n",
|
||||
"# explode metadata col json\n",
|
||||
"df = df.join(pd.json_normalize(df.pop(\"metadata\"), sep=\".\").add_prefix(\"metadata_\"))\n",
|
||||
"df.info()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 87,
|
||||
"id": "f6819a1c-32ab-49c7-845b-5df7bf60f561",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>sessionId</th>\n",
|
||||
" <th>eventType</th>\n",
|
||||
" <th>ts</th>\n",
|
||||
" <th>targetEl</th>\n",
|
||||
" <th>targetUrl</th>\n",
|
||||
" <th>metadata_path</th>\n",
|
||||
" <th>metadata_referrer</th>\n",
|
||||
" <th>metadata_x</th>\n",
|
||||
" <th>metadata_y</th>\n",
|
||||
" <th>metadata_scrollY</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>1761225843899-qaiwwwyj2o</td>\n",
|
||||
" <td>pageview</td>\n",
|
||||
" <td>1761226211163</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td></td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>1761225843899-qaiwwwyj2o</td>\n",
|
||||
" <td>click</td>\n",
|
||||
" <td>1761226218090</td>\n",
|
||||
" <td>MAIN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>815.0</td>\n",
|
||||
" <td>331.0</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>1761225843899-qaiwwwyj2o</td>\n",
|
||||
" <td>click</td>\n",
|
||||
" <td>1761226220890</td>\n",
|
||||
" <td>MAIN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>1129.0</td>\n",
|
||||
" <td>605.0</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>1761225843899-qaiwwwyj2o</td>\n",
|
||||
" <td>click</td>\n",
|
||||
" <td>1761226225801</td>\n",
|
||||
" <td>DIV</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>532.0</td>\n",
|
||||
" <td>545.0</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>4</th>\n",
|
||||
" <td>1761225843899-qaiwwwyj2o</td>\n",
|
||||
" <td>click</td>\n",
|
||||
" <td>1761226229364</td>\n",
|
||||
" <td>DIV</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>481.0</td>\n",
|
||||
" <td>399.0</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>5</th>\n",
|
||||
" <td>1761227236286-e7mphcvw6t</td>\n",
|
||||
" <td>pageview</td>\n",
|
||||
" <td>1761227236426</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td></td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>6</th>\n",
|
||||
" <td>1761227236286-e7mphcvw6t</td>\n",
|
||||
" <td>click</td>\n",
|
||||
" <td>1761227239328</td>\n",
|
||||
" <td>DIV</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>202.0</td>\n",
|
||||
" <td>351.0</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>7</th>\n",
|
||||
" <td>1761227236286-e7mphcvw6t</td>\n",
|
||||
" <td>click</td>\n",
|
||||
" <td>1761227244783</td>\n",
|
||||
" <td>A</td>\n",
|
||||
" <td>https://vercel.com/new?utm_source=create-next-...</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>377.0</td>\n",
|
||||
" <td>723.0</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>8</th>\n",
|
||||
" <td>1761828056433-0gz7aboz86h</td>\n",
|
||||
" <td>pageview</td>\n",
|
||||
" <td>1761828261783</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td></td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>9</th>\n",
|
||||
" <td>1761828056433-0gz7aboz86h</td>\n",
|
||||
" <td>click</td>\n",
|
||||
" <td>1761828266484</td>\n",
|
||||
" <td>H1</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>527.0</td>\n",
|
||||
" <td>169.0</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>10</th>\n",
|
||||
" <td>1761828056433-0gz7aboz86h</td>\n",
|
||||
" <td>scroll</td>\n",
|
||||
" <td>1761828270314</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>51.666668</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>11</th>\n",
|
||||
" <td>1761828056433-0gz7aboz86h</td>\n",
|
||||
" <td>scroll</td>\n",
|
||||
" <td>1761828270328</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>50.000000</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>12</th>\n",
|
||||
" <td>1761828056433-0gz7aboz86h</td>\n",
|
||||
" <td>scroll</td>\n",
|
||||
" <td>1761828270336</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>/</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>49.166668</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" sessionId eventType ts targetEl \\\n",
|
||||
"0 1761225843899-qaiwwwyj2o pageview 1761226211163 NaN \n",
|
||||
"1 1761225843899-qaiwwwyj2o click 1761226218090 MAIN \n",
|
||||
"2 1761225843899-qaiwwwyj2o click 1761226220890 MAIN \n",
|
||||
"3 1761225843899-qaiwwwyj2o click 1761226225801 DIV \n",
|
||||
"4 1761225843899-qaiwwwyj2o click 1761226229364 DIV \n",
|
||||
"5 1761227236286-e7mphcvw6t pageview 1761227236426 NaN \n",
|
||||
"6 1761227236286-e7mphcvw6t click 1761227239328 DIV \n",
|
||||
"7 1761227236286-e7mphcvw6t click 1761227244783 A \n",
|
||||
"8 1761828056433-0gz7aboz86h pageview 1761828261783 NaN \n",
|
||||
"9 1761828056433-0gz7aboz86h click 1761828266484 H1 \n",
|
||||
"10 1761828056433-0gz7aboz86h scroll 1761828270314 NaN \n",
|
||||
"11 1761828056433-0gz7aboz86h scroll 1761828270328 NaN \n",
|
||||
"12 1761828056433-0gz7aboz86h scroll 1761828270336 NaN \n",
|
||||
"\n",
|
||||
" targetUrl metadata_path \\\n",
|
||||
"0 NaN / \n",
|
||||
"1 NaN / \n",
|
||||
"2 NaN / \n",
|
||||
"3 NaN / \n",
|
||||
"4 NaN / \n",
|
||||
"5 NaN / \n",
|
||||
"6 NaN / \n",
|
||||
"7 https://vercel.com/new?utm_source=create-next-... / \n",
|
||||
"8 NaN / \n",
|
||||
"9 NaN / \n",
|
||||
"10 NaN / \n",
|
||||
"11 NaN / \n",
|
||||
"12 NaN / \n",
|
||||
"\n",
|
||||
" metadata_referrer metadata_x metadata_y metadata_scrollY \n",
|
||||
"0 NaN NaN NaN \n",
|
||||
"1 NaN 815.0 331.0 NaN \n",
|
||||
"2 NaN 1129.0 605.0 NaN \n",
|
||||
"3 NaN 532.0 545.0 NaN \n",
|
||||
"4 NaN 481.0 399.0 NaN \n",
|
||||
"5 NaN NaN NaN \n",
|
||||
"6 NaN 202.0 351.0 NaN \n",
|
||||
"7 NaN 377.0 723.0 NaN \n",
|
||||
"8 NaN NaN NaN \n",
|
||||
"9 NaN 527.0 169.0 NaN \n",
|
||||
"10 NaN NaN NaN 51.666668 \n",
|
||||
"11 NaN NaN NaN 50.000000 \n",
|
||||
"12 NaN NaN NaN 49.166668 "
|
||||
]
|
||||
},
|
||||
"execution_count": 87,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"df.groupby('sessionId').head()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 88,
|
||||
"id": "380eca5f-8304-4fb2-be32-e8bcfd312085",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"['1761225843899-qaiwwwyj2o',\n",
|
||||
" '1761828056433-0gz7aboz86h',\n",
|
||||
" '1761227236286-e7mphcvw6t']"
|
||||
]
|
||||
},
|
||||
"execution_count": 88,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"sessions = list(set(df['sessionId'])); sessions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 89,
|
||||
"id": "f4ae6f81-dcb8-44be-aee7-30dbc3a6bae1",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# map sessions to experiments"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 101,
|
||||
"id": "050d90a4-20a9-47f5-b998-c31178a54cb3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def build_transition_prob_matrix(df: pd.DataFrame):\n",
|
||||
" df = df.dropna(subset=['eventType'])\n",
|
||||
" events = df['eventType'].tolist()\n",
|
||||
" labels = pd.Index(events).unique().tolist()\n",
|
||||
" idx = {e:i for i,e in enumerate(labels)}\n",
|
||||
" M = np.zeros((len(labels), len(labels)), dtype=float)\n",
|
||||
" for a, b in zip(events, events[1:]):\n",
|
||||
" M[idx[a], idx[b]] += 1\n",
|
||||
" row_sums = M.sum(axis=1, keepdims=True)\n",
|
||||
" with np.errstate(divide='ignore', invalid='ignore'):\n",
|
||||
" P = np.divide(M, row_sums, where=row_sums>0) # row-normalized\n",
|
||||
" return P, labels"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 107,
|
||||
"id": "e68f9004-82f5-4826-aece-e3dc6e15a18f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# https://medium.com/data-science/time-series-data-markov-transition-matrices-7060771e362b\n",
|
||||
"from graphviz import Digraph\n",
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"def _as_prob_df(matrix, labels=None):\n",
|
||||
" \"\"\"Return a square DataFrame with index=columns=labels.\"\"\"\n",
|
||||
" if isinstance(matrix, pd.DataFrame):\n",
|
||||
" # Ensure square and aligned\n",
|
||||
" assert (matrix.index == matrix.columns).all(), \"Index/columns must match.\"\n",
|
||||
" return matrix\n",
|
||||
" matrix = np.asarray(matrix, dtype=float)\n",
|
||||
" assert matrix.shape[0] == matrix.shape[1], \"Matrix must be square.\"\n",
|
||||
" if labels is None:\n",
|
||||
" raise ValueError(\"labels are required when matrix is not a DataFrame\")\n",
|
||||
" assert len(labels) == matrix.shape[0], \"labels length must match matrix size.\"\n",
|
||||
" return pd.DataFrame(matrix, index=list(labels), columns=list(labels))\n",
|
||||
"\n",
|
||||
"def _df_to_edgelist(P: pd.DataFrame, threshold=0.0, round_digits=2):\n",
|
||||
" \"\"\"Build weighted edges > threshold.\"\"\"\n",
|
||||
" edges = []\n",
|
||||
" for src in P.index:\n",
|
||||
" for dst in P.columns:\n",
|
||||
" w = float(P.loc[src, dst])\n",
|
||||
" if w > threshold:\n",
|
||||
" edges.append((str(src), str(dst), f\"{w:.{round_digits}f}\"))\n",
|
||||
" return edges\n",
|
||||
"\n",
|
||||
"def render_graph(fname, matrix, ls_index=None, threshold=0.0, fmt=\"svg\", view=False):\n",
|
||||
" \"\"\"\n",
|
||||
" fname: output file stem (no extension)\n",
|
||||
" matrix: NumPy array or pandas DataFrame of transition PROBABILITIES\n",
|
||||
" ls_index: ordered labels (required if matrix is not a DataFrame)\n",
|
||||
" threshold: hide edges with weight <= threshold\n",
|
||||
" fmt: 'svg'|'png'|'pdf' etc.\n",
|
||||
" view: open after rendering\n",
|
||||
" \"\"\"\n",
|
||||
" P = _as_prob_df(matrix, labels=ls_index)\n",
|
||||
" edges = _df_to_edgelist(P, threshold=threshold)\n",
|
||||
"\n",
|
||||
" g = Digraph(format=fmt)\n",
|
||||
" g.attr(rankdir=\"LR\", size=\"30\")\n",
|
||||
" g.attr(\"node\", shape=\"circle\")\n",
|
||||
"\n",
|
||||
" # ensure isolated nodes appear\n",
|
||||
" for node in P.index:\n",
|
||||
" g.node(str(node), width=\"1\", height=\"1\")\n",
|
||||
"\n",
|
||||
" for src, dst, label in edges:\n",
|
||||
" g.edge(src, dst, label=label)\n",
|
||||
"\n",
|
||||
" g.render(fname, view=view, cleanup=True)\n",
|
||||
" return g\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 108,
|
||||
"id": "e255a2c1-6454-4e5e-89f6-ef8ac51ab6cc",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/svg+xml": [
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
|
||||
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
|
||||
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
|
||||
"<!-- Generated by graphviz version 13.1.2 (0)\n",
|
||||
" -->\n",
|
||||
"<!-- Pages: 1 -->\n",
|
||||
"<svg width=\"228pt\" height=\"124pt\"\n",
|
||||
" viewBox=\"0.00 0.00 228.00 124.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
|
||||
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 119.83)\">\n",
|
||||
"<polygon fill=\"white\" stroke=\"none\" points=\"-4,4 -4,-119.83 223.66,-119.83 223.66,4 -4,4\"/>\n",
|
||||
"<!-- pageview -->\n",
|
||||
"<g id=\"node1\" class=\"node\">\n",
|
||||
"<title>pageview</title>\n",
|
||||
"<ellipse fill=\"none\" stroke=\"black\" cx=\"44.58\" cy=\"-44.58\" rx=\"44.58\" ry=\"44.58\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"44.58\" y=\"-39.91\" font-family=\"Times,serif\" font-size=\"14.00\">pageview</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- click -->\n",
|
||||
"<g id=\"node2\" class=\"node\">\n",
|
||||
"<title>click</title>\n",
|
||||
"<ellipse fill=\"none\" stroke=\"black\" cx=\"183.66\" cy=\"-44.58\" rx=\"36\" ry=\"36\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"183.66\" y=\"-39.91\" font-family=\"Times,serif\" font-size=\"14.00\">click</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- pageview->click -->\n",
|
||||
"<g id=\"edge1\" class=\"edge\">\n",
|
||||
"<title>pageview->click</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M89.33,-44.58C104.32,-44.58 121.13,-44.58 136.31,-44.58\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"136.04,-48.08 146.04,-44.58 136.04,-41.08 136.04,-48.08\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"118.41\" y=\"-48.53\" font-family=\"Times,serif\" font-size=\"14.00\">1.0</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- click->click -->\n",
|
||||
"<g id=\"edge2\" class=\"edge\">\n",
|
||||
"<title>click->click</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M171.43,-78.86C171.56,-89.86 175.63,-98.58 183.66,-98.58 188.68,-98.58 192.16,-95.17 194.09,-89.93\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"197.49,-90.78 195.65,-80.35 190.58,-89.66 197.49,-90.78\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"183.66\" y=\"-102.53\" font-family=\"Times,serif\" font-size=\"14.00\">1.0</text>\n",
|
||||
"</g>\n",
|
||||
"</g>\n",
|
||||
"</svg>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"<graphviz.graphs.Digraph at 0x7fd404165c70>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[[0. 1.]\n",
|
||||
" [0. 1.]]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/svg+xml": [
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
|
||||
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
|
||||
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
|
||||
"<!-- Generated by graphviz version 13.1.2 (0)\n",
|
||||
" -->\n",
|
||||
"<!-- Pages: 1 -->\n",
|
||||
"<svg width=\"358pt\" height=\"132pt\"\n",
|
||||
" viewBox=\"0.00 0.00 358.00 132.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
|
||||
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 128.41)\">\n",
|
||||
"<polygon fill=\"white\" stroke=\"none\" points=\"-4,4 -4,-128.41 354.16,-128.41 354.16,4 -4,4\"/>\n",
|
||||
"<!-- pageview -->\n",
|
||||
"<g id=\"node1\" class=\"node\">\n",
|
||||
"<title>pageview</title>\n",
|
||||
"<ellipse fill=\"none\" stroke=\"black\" cx=\"44.58\" cy=\"-44.58\" rx=\"44.58\" ry=\"44.58\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"44.58\" y=\"-39.91\" font-family=\"Times,serif\" font-size=\"14.00\">pageview</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- pageview->pageview -->\n",
|
||||
"<g id=\"edge1\" class=\"edge\">\n",
|
||||
"<title>pageview->pageview</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M30.86,-87.29C31.64,-98.6 36.22,-107.16 44.58,-107.16 49.94,-107.16 53.74,-103.65 55.99,-98.15\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"59.33,-99.28 57.99,-88.77 52.48,-97.82 59.33,-99.28\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"44.58\" y=\"-111.11\" font-family=\"Times,serif\" font-size=\"14.00\">0.2</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- click -->\n",
|
||||
"<g id=\"node2\" class=\"node\">\n",
|
||||
"<title>click</title>\n",
|
||||
"<ellipse fill=\"none\" stroke=\"black\" cx=\"183.66\" cy=\"-44.58\" rx=\"36\" ry=\"36\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"183.66\" y=\"-39.91\" font-family=\"Times,serif\" font-size=\"14.00\">click</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- pageview->click -->\n",
|
||||
"<g id=\"edge2\" class=\"edge\">\n",
|
||||
"<title>pageview->click</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M89.33,-44.58C104.32,-44.58 121.13,-44.58 136.31,-44.58\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"136.04,-48.08 146.04,-44.58 136.04,-41.08 136.04,-48.08\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"118.41\" y=\"-48.53\" font-family=\"Times,serif\" font-size=\"14.00\">0.8</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- click->pageview -->\n",
|
||||
"<g id=\"edge3\" class=\"edge\">\n",
|
||||
"<title>click->pageview</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M150.74,-29.52C143.93,-26.96 136.67,-24.68 129.66,-23.33 119.02,-21.28 107.71,-22.06 96.96,-24.24\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"96.33,-20.79 87.47,-26.6 98.02,-27.59 96.33,-20.79\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"118.41\" y=\"-27.28\" font-family=\"Times,serif\" font-size=\"14.00\">0.3</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- click->click -->\n",
|
||||
"<g id=\"edge4\" class=\"edge\">\n",
|
||||
"<title>click->click</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M171.43,-78.86C171.56,-89.86 175.63,-98.58 183.66,-98.58 188.68,-98.58 192.16,-95.17 194.09,-89.93\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"197.49,-90.78 195.65,-80.35 190.58,-89.66 197.49,-90.78\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"183.66\" y=\"-102.53\" font-family=\"Times,serif\" font-size=\"14.00\">0.6</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- scroll -->\n",
|
||||
"<g id=\"node3\" class=\"node\">\n",
|
||||
"<title>scroll</title>\n",
|
||||
"<ellipse fill=\"none\" stroke=\"black\" cx=\"314.16\" cy=\"-44.58\" rx=\"36\" ry=\"36\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"314.16\" y=\"-39.91\" font-family=\"Times,serif\" font-size=\"14.00\">scroll</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- click->scroll -->\n",
|
||||
"<g id=\"edge5\" class=\"edge\">\n",
|
||||
"<title>click->scroll</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M220.12,-44.58C234.44,-44.58 251.18,-44.58 266.47,-44.58\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"266.31,-48.08 276.31,-44.58 266.31,-41.08 266.31,-48.08\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"248.91\" y=\"-48.53\" font-family=\"Times,serif\" font-size=\"14.00\">0.1</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- scroll->scroll -->\n",
|
||||
"<g id=\"edge6\" class=\"edge\">\n",
|
||||
"<title>scroll->scroll</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M301.93,-78.86C302.06,-89.86 306.13,-98.58 314.16,-98.58 319.18,-98.58 322.66,-95.17 324.59,-89.93\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"327.99,-90.78 326.15,-80.35 321.08,-89.66 327.99,-90.78\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"314.16\" y=\"-102.53\" font-family=\"Times,serif\" font-size=\"14.00\">1.0</text>\n",
|
||||
"</g>\n",
|
||||
"</g>\n",
|
||||
"</svg>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"<graphviz.graphs.Digraph at 0x7fd406e21a90>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[[0.25 0.75 0. ]\n",
|
||||
" [0.28571429 0.57142857 0.14285714]\n",
|
||||
" [0. 0.00826446 0.99173554]]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/svg+xml": [
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
|
||||
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
|
||||
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
|
||||
"<!-- Generated by graphviz version 13.1.2 (0)\n",
|
||||
" -->\n",
|
||||
"<!-- Pages: 1 -->\n",
|
||||
"<svg width=\"228pt\" height=\"124pt\"\n",
|
||||
" viewBox=\"0.00 0.00 228.00 124.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
|
||||
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 119.83)\">\n",
|
||||
"<polygon fill=\"white\" stroke=\"none\" points=\"-4,4 -4,-119.83 223.66,-119.83 223.66,4 -4,4\"/>\n",
|
||||
"<!-- pageview -->\n",
|
||||
"<g id=\"node1\" class=\"node\">\n",
|
||||
"<title>pageview</title>\n",
|
||||
"<ellipse fill=\"none\" stroke=\"black\" cx=\"44.58\" cy=\"-44.58\" rx=\"44.58\" ry=\"44.58\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"44.58\" y=\"-39.91\" font-family=\"Times,serif\" font-size=\"14.00\">pageview</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- click -->\n",
|
||||
"<g id=\"node2\" class=\"node\">\n",
|
||||
"<title>click</title>\n",
|
||||
"<ellipse fill=\"none\" stroke=\"black\" cx=\"183.66\" cy=\"-44.58\" rx=\"36\" ry=\"36\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"183.66\" y=\"-39.91\" font-family=\"Times,serif\" font-size=\"14.00\">click</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- pageview->click -->\n",
|
||||
"<g id=\"edge1\" class=\"edge\">\n",
|
||||
"<title>pageview->click</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M89.33,-44.58C104.32,-44.58 121.13,-44.58 136.31,-44.58\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"136.04,-48.08 146.04,-44.58 136.04,-41.08 136.04,-48.08\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"118.41\" y=\"-48.53\" font-family=\"Times,serif\" font-size=\"14.00\">1.0</text>\n",
|
||||
"</g>\n",
|
||||
"<!-- click->click -->\n",
|
||||
"<g id=\"edge2\" class=\"edge\">\n",
|
||||
"<title>click->click</title>\n",
|
||||
"<path fill=\"none\" stroke=\"black\" d=\"M171.43,-78.86C171.56,-89.86 175.63,-98.58 183.66,-98.58 188.68,-98.58 192.16,-95.17 194.09,-89.93\"/>\n",
|
||||
"<polygon fill=\"black\" stroke=\"black\" points=\"197.49,-90.78 195.65,-80.35 190.58,-89.66 197.49,-90.78\"/>\n",
|
||||
"<text xml:space=\"preserve\" text-anchor=\"middle\" x=\"183.66\" y=\"-102.53\" font-family=\"Times,serif\" font-size=\"14.00\">1.0</text>\n",
|
||||
"</g>\n",
|
||||
"</g>\n",
|
||||
"</svg>\n"
|
||||
],
|
||||
"text/plain": [
|
||||
"<graphviz.graphs.Digraph at 0x7fd4041662b0>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[[0. 1.]\n",
|
||||
" [0. 1.]]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def explore_session(session_id: str):\n",
|
||||
" subset = df[df['sessionId'] == session_id] # not .where(...)\n",
|
||||
" P, labels = build_transition_prob_matrix(subset)\n",
|
||||
" g = render_graph(f\"session_{session_id}\", P, ls_index=labels, threshold=0.01, fmt=\"svg\", view=False)\n",
|
||||
" display(g)\n",
|
||||
" return P\n",
|
||||
"for session in sessions:\n",
|
||||
" print(explore_session(session))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "4d278c2d-406e-4dc0-b219-5f7b236e852b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python (PHANTOM)",
|
||||
"language": "python",
|
||||
"name": "phantom"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
1
paper/academic_page/.nojekyll
Normal file
1
paper/academic_page/.nojekyll
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
80
paper/academic_page/README.md
Normal file
80
paper/academic_page/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Academic Project Page Template
|
||||
|
||||
> **Update (September 2025)**: This template has been modernized with better design, SEO, and mobile support. For the original version, see the [original-version branch](https://github.com/eliahuhorwitz/Academic-project-page-template/tree/original-version).
|
||||
|
||||
A clean, responsive template for academic project pages.
|
||||
|
||||
|
||||
Example project pages built using this template are:
|
||||
- https://horwitz.ai/probex
|
||||
- https://vision.huji.ac.il/probegen
|
||||
- https://horwitz.ai/mother
|
||||
- https://horwitz.ai/spectral_detuning
|
||||
- https://vision.huji.ac.il/ladeda
|
||||
- https://vision.huji.ac.il/dsire
|
||||
- https://horwitz.ai/podd
|
||||
- https://dreamix-video-editing.github.io
|
||||
- https://horwitz.ai/conffusion
|
||||
- https://horwitz.ai/3d_ads/
|
||||
- https://vision.huji.ac.il/ssrl_ad
|
||||
- https://vision.huji.ac.il/deepsim
|
||||
|
||||
|
||||
|
||||
## Start using the template
|
||||
To start using the template click on `Use this Template`.
|
||||
|
||||
The template uses html for controlling the content and css for controlling the style.
|
||||
To edit the websites contents edit the `index.html` file. It contains different HTML "building blocks", use whichever ones you need and comment out the rest.
|
||||
|
||||
**IMPORTANT!** Make sure to replace the `favicon.ico` under `static/images/` with one of your own, otherwise your favicon is going to be a dreambooth image of me.
|
||||
|
||||
## What's New
|
||||
|
||||
- Modern, clean design with better mobile support
|
||||
- Improved SEO with proper meta tags and structured data
|
||||
- Performance improvements (lazy loading, optimized assets)
|
||||
- More Works dropdown
|
||||
- Copy button for BibTeX citations
|
||||
- Better accessibility
|
||||
|
||||
## Components
|
||||
|
||||
- Teaser video
|
||||
- Image carousel
|
||||
- YouTube video embedding
|
||||
- Video carousel
|
||||
- PDF poster viewer
|
||||
- BibTeX citation
|
||||
|
||||
## Customization
|
||||
|
||||
The HTML file has TODO comments showing what to replace:
|
||||
|
||||
- Paper title, authors, institution, conference
|
||||
- Links (arXiv, GitHub, etc.)
|
||||
- Abstract and descriptions
|
||||
- Videos, images, and PDFs
|
||||
- Related works in the dropdown
|
||||
- Meta tags for SEO and social sharing
|
||||
|
||||
### Meta Tags
|
||||
The template includes meta tags for better search engine visibility and social media sharing. These appear in the `<head>` section and help with:
|
||||
- Google Scholar indexing
|
||||
- Social media previews (Twitter, Facebook, LinkedIn)
|
||||
- Search engine optimization
|
||||
|
||||
Create a 1200x630px social preview image at `static/images/social_preview.png`.
|
||||
|
||||
## Tips
|
||||
|
||||
- Compress images with [TinyPNG](https://tinypng.com)
|
||||
- Use YouTube for large videos (>10MB)
|
||||
- Replace the favicon in `static/images/`
|
||||
- Works with GitHub Pages
|
||||
|
||||
## Acknowledgments
|
||||
Parts of this project page were adopted from the [Nerfies](https://nerfies.github.io/) page.
|
||||
|
||||
## Website License
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
|
||||
496
paper/academic_page/index.html
Normal file
496
paper/academic_page/index.html
Normal file
@@ -0,0 +1,496 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<meta name="title" content="PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms">
|
||||
<meta name="description" content="Developing pricing heuristics to protect e-commerce platforms from systematic exploitation by LLM agents in dynamic pricing environments through behavioral signature detection.">
|
||||
<meta name="keywords" content="dynamic pricing, LLM agents, e-commerce security, behavioral detection, pricing heuristics, recommendation systems, agent detection, AI security, pricing integrity">
|
||||
<meta name="author" content="Daniel Alves Rösel">
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta name="language" content="English">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:site_name" content="PHANTOM Research">
|
||||
<meta property="og:title" content="PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms">
|
||||
<meta property="og:description" content="Developing pricing heuristics to protect e-commerce platforms from systematic exploitation by LLM agents in dynamic pricing environments through behavioral signature detection.">
|
||||
<meta property="og:url" content="TODO">
|
||||
<meta property="og:image" content="TODO">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="PHANTOM Research Preview">
|
||||
<meta property="article:published_time" content="2025-01-01T00:00:00.000Z">
|
||||
<meta property="article:author" content="Daniel Alves Rösel">
|
||||
<meta property="article:section" content="Research">
|
||||
<meta property="article:tag" content="dynamic pricing">
|
||||
<meta property="article:tag" content="LLM agents">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<!-- TODO: Replace with your lab/institution Twitter handle -->
|
||||
<meta name="twitter:site" content="@YOUR_TWITTER_HANDLE">
|
||||
<!-- TODO: Replace with first author's Twitter handle -->
|
||||
<meta name="twitter:creator" content="@AUTHOR_TWITTER_HANDLE">
|
||||
<!-- TODO: Same as paper title above -->
|
||||
<meta name="twitter:title" content="PAPER_TITLE">
|
||||
<!-- TODO: Same as description above -->
|
||||
<meta name="twitter:description" content="BRIEF_DESCRIPTION_OF_YOUR_RESEARCH_CONTRIBUTION_AND_FINDINGS">
|
||||
<!-- TODO: Same as social preview image above -->
|
||||
<meta name="twitter:image" content="https://YOUR_DOMAIN.com/static/images/social_preview.png">
|
||||
<meta name="twitter:image:alt" content="PAPER_TITLE - Research Preview">
|
||||
|
||||
<!-- Academic/Research Specific -->
|
||||
<meta name="citation_title" content="Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms">
|
||||
<meta name="citation_author" content="Rösel, Daniel">
|
||||
<meta name="citation_publication_date" content="2025">
|
||||
<meta name="citation_conference_title" content="IE University Bachelor's Thesis">
|
||||
<meta name="citation_pdf_url" content="TODO">
|
||||
|
||||
<!-- Additional SEO -->
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<meta name="msapplication-TileColor" content="#2563eb">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
|
||||
<!-- Preconnect for performance -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="preconnect" href="https://ajax.googleapis.com">
|
||||
<link rel="preconnect" href="https://documentcloud.adobe.com">
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||
|
||||
|
||||
<title>PHANTOM: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms - Daniel Rösel | Academic Research</title>
|
||||
|
||||
<!-- Favicon and App Icons -->
|
||||
<link rel="icon" type="image/x-icon" href="static/images/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="static/images/favicon.ico">
|
||||
|
||||
<!-- Critical CSS - Load synchronously -->
|
||||
<link rel="stylesheet" href="static/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="static/css/index.css">
|
||||
|
||||
<!-- Non-critical CSS - Load asynchronously -->
|
||||
<link rel="preload" href="static/css/bulma-carousel.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<link rel="preload" href="static/css/bulma-slider.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<link rel="preload" href="static/css/fontawesome.all.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<link rel="preload" href="https://cdn.jsdelivr.net/gh/jpswalsh/academicons@1/css/academicons.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
|
||||
<!-- Fallback for browsers that don't support preload -->
|
||||
<noscript>
|
||||
<link rel="stylesheet" href="static/css/bulma-carousel.min.css">
|
||||
<link rel="stylesheet" href="static/css/bulma-slider.min.css">
|
||||
<link rel="stylesheet" href="static/css/fontawesome.all.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jpswalsh/academicons@1/css/academicons.min.css">
|
||||
</noscript>
|
||||
|
||||
<!-- Fonts - Optimized loading -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Defer non-critical JavaScript -->
|
||||
<script defer src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script defer src="https://documentcloud.adobe.com/view-sdk/main.js"></script>
|
||||
<script defer src="static/js/fontawesome.all.min.js"></script>
|
||||
<script defer src="static/js/bulma-carousel.min.js"></script>
|
||||
<script defer src="static/js/bulma-slider.min.js"></script>
|
||||
<script defer src="static/js/index.js"></script>
|
||||
|
||||
<!-- Structured Data for Academic Papers -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ScholarlyArticle",
|
||||
"headline": "PAPER_TITLE",
|
||||
"description": "BRIEF_DESCRIPTION_OF_YOUR_RESEARCH_CONTRIBUTION_AND_FINDINGS",
|
||||
"author": [
|
||||
{
|
||||
"@type": "Person",
|
||||
"name": "FIRST_AUTHOR_NAME",
|
||||
"affiliation": {
|
||||
"@type": "Organization",
|
||||
"name": "INSTITUTION_NAME"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Person",
|
||||
"name": "SECOND_AUTHOR_NAME",
|
||||
"affiliation": {
|
||||
"@type": "Organization",
|
||||
"name": "INSTITUTION_NAME"
|
||||
}
|
||||
}
|
||||
],
|
||||
"datePublished": "2024-01-01",
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "CONFERENCE_OR_JOURNAL_NAME"
|
||||
},
|
||||
"url": "https://YOUR_DOMAIN.com/YOUR_PROJECT_PAGE",
|
||||
"image": "https://YOUR_DOMAIN.com/static/images/social_preview.png",
|
||||
"keywords": ["KEYWORD1", "KEYWORD2", "KEYWORD3", "machine learning", "computer vision"],
|
||||
"abstract": "FULL_ABSTRACT_TEXT_HERE",
|
||||
"citation": "BIBTEX_CITATION_HERE",
|
||||
"isAccessibleForFree": true,
|
||||
"license": "https://creativecommons.org/licenses/by/4.0/",
|
||||
"mainEntity": {
|
||||
"@type": "WebPage",
|
||||
"@id": "https://YOUR_DOMAIN.com/YOUR_PROJECT_PAGE"
|
||||
},
|
||||
"about": [
|
||||
{
|
||||
"@type": "Thing",
|
||||
"name": "RESEARCH_AREA_1"
|
||||
},
|
||||
{
|
||||
"@type": "Thing",
|
||||
"name": "RESEARCH_AREA_2"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Website/Organization Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "IE University",
|
||||
"url": "https://www.ie.edu",
|
||||
"logo": "TODO"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<!-- Scroll to Top Button -->
|
||||
<button class="scroll-to-top" onclick="scrollToTop()" title="Scroll to top" aria-label="Scroll to top">
|
||||
<i class="fas fa-chevron-up"></i>
|
||||
</button>
|
||||
|
||||
<!-- More Works Dropdown -->
|
||||
<div class="more-works-container">
|
||||
<button class="more-works-btn" onclick="toggleMoreWorks()" title="View More Works from Our Lab">
|
||||
<i class="fas fa-flask"></i>
|
||||
More Works
|
||||
<i class="fas fa-chevron-down dropdown-arrow"></i>
|
||||
</button>
|
||||
<div class="more-works-dropdown" id="moreWorksDropdown">
|
||||
<div class="dropdown-header">
|
||||
<h4>More Works from Our Lab</h4>
|
||||
<button class="close-btn" onclick="toggleMoreWorks()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="works-list">
|
||||
<!-- TODO: Replace with your lab's related works -->
|
||||
<a href="https://arxiv.org/abs/PAPER_ID_1" class="work-item" target="_blank">
|
||||
<div class="work-info">
|
||||
<!-- TODO: Replace with actual paper title -->
|
||||
<h5>Paper Title 1</h5>
|
||||
<!-- TODO: Replace with brief description -->
|
||||
<p>Brief description of the work and its main contribution.</p>
|
||||
<!-- TODO: Replace with venue and year -->
|
||||
<span class="work-venue">Conference/Journal 2024</span>
|
||||
</div>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
<!-- TODO: Add more related works or remove extra items -->
|
||||
<a href="https://arxiv.org/abs/PAPER_ID_2" class="work-item" target="_blank">
|
||||
<div class="work-info">
|
||||
<h5>Paper Title 2</h5>
|
||||
<p>Brief description of the work and its main contribution.</p>
|
||||
<span class="work-venue">Conference/Journal 2023</span>
|
||||
</div>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
<a href="https://arxiv.org/abs/PAPER_ID_3" class="work-item" target="_blank">
|
||||
<div class="work-info">
|
||||
<h5>Paper Title 3</h5>
|
||||
<p>Brief description of the work and its main contribution.</p>
|
||||
<span class="work-venue">Conference/Journal 2023</span>
|
||||
</div>
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main id="main-content">
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns is-centered">
|
||||
<div class="column has-text-centered">
|
||||
<h1 class="title is-1 publication-title">Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms</h1>
|
||||
<div class="is-size-5 publication-authors">
|
||||
<span class="author-block">
|
||||
<a href="https://alves.world" target="_blank">Daniel Rösel</a></span>
|
||||
</div>
|
||||
|
||||
<div class="is-size-5 publication-authors">
|
||||
<span class="author-block">IE University<br>Bachelor's Thesis 2025</span>
|
||||
<span class="eql-cntrb"><small><br>Advisor: <a href="SECOND AUTHOR PERSONAL LINK" target="_blank">Alberto Martín Izquierdo</a></small></span>
|
||||
</div>
|
||||
|
||||
<div class="column has-text-centered">
|
||||
<div class="publication-links">
|
||||
<!-- TODO: Update with your arXiv paper ID -->
|
||||
<span class="link-block">
|
||||
<a href="https://arxiv.org/pdf/<ARXIV PAPER ID>.pdf" target="_blank"
|
||||
class="external-link button is-normal is-rounded is-dark">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</span>
|
||||
<span>Paper</span>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<!-- TODO: Add your supplementary material PDF or remove this section -->
|
||||
<span class="link-block">
|
||||
<a href="static/pdfs/supplementary_material.pdf" target="_blank"
|
||||
class="external-link button is-normal is-rounded is-dark">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</span>
|
||||
<span>Supplementary</span>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<span class="link-block">
|
||||
<a href="https://github.com/velocitatem/PHANTOM" target="_blank"
|
||||
class="external-link button is-normal is-rounded is-dark">
|
||||
<span class="icon">
|
||||
<i class="fab fa-github"></i>
|
||||
</span>
|
||||
<span>Code</span>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<!-- TODO: Update with your arXiv paper ID -->
|
||||
<span class="link-block">
|
||||
<a href="https://arxiv.org/abs/<ARXIV PAPER ID>" target="_blank"
|
||||
class="external-link button is-normal is-rounded is-dark">
|
||||
<span class="icon">
|
||||
<i class="ai ai-arxiv"></i>
|
||||
</span>
|
||||
<span>arXiv</span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Teaser video-->
|
||||
<section class="hero teaser">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="hero-body">
|
||||
<!-- TODO: Replace with your teaser video -->
|
||||
<video poster="" id="tree" autoplay controls muted loop height="100%" preload="metadata">
|
||||
<!-- TODO: Add your video file path here -->
|
||||
<source src="static/videos/banner_video.mp4" type="video/mp4">
|
||||
</video>
|
||||
<!-- TODO: Replace with your video description -->
|
||||
<h2 class="subtitle has-text-centered">
|
||||
Aliquam vitae elit ullamcorper tellus egestas pellentesque. Ut lacus tellus, maximus vel lectus at, placerat pretium mi. Maecenas dignissim tincidunt vestibulum. Sed consequat hendrerit nisl ut maximus.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- End teaser video -->
|
||||
|
||||
<!-- Paper abstract -->
|
||||
<section class="section hero is-light">
|
||||
<div class="container is-max-desktop">
|
||||
<div class="columns is-centered has-text-centered">
|
||||
<div class="column is-four-fifths">
|
||||
<h2 class="title is-3">Abstract</h2>
|
||||
<div class="content has-text-justified">
|
||||
<p>
|
||||
The primary objective of this thesis is to develop and validate pricing heuristics that protect e-commerce platforms from systematic exploitation by Large Language Model (LLM) agents within dynamic pricing environments. As AI agents increasingly mediate consumer transactions, they enable users to circumvent the Cost of Information (the price premium accumulated through demand signal expression) by conducting reconnaissance in isolated sessions before executing purchases through clean sessions at base prices. This research will make an anticipatory contribution by adapting recommendation system methodologies to distinguish between genuine human browsing behaviour and agent-orchestrated information gathering, thereby enabling pricing systems to maintain margin integrity without degrading the user experience for legitimate customers or getting rid of leads generated by LLMs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- End paper abstract -->
|
||||
|
||||
|
||||
<!-- Image carousel -->
|
||||
<section class="hero is-small">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div id="results-carousel" class="carousel results-carousel">
|
||||
<div class="item">
|
||||
<!-- TODO: Replace with your research result images -->
|
||||
<img src="static/images/carousel1.jpg" alt="First research result visualization" loading="lazy"/>
|
||||
<!-- TODO: Replace with description of this result -->
|
||||
<h2 class="subtitle has-text-centered">
|
||||
First image description.
|
||||
</h2>
|
||||
</div>
|
||||
<div class="item">
|
||||
<!-- Your image here -->
|
||||
<img src="static/images/carousel2.jpg" alt="Second research result visualization" loading="lazy"/>
|
||||
<h2 class="subtitle has-text-centered">
|
||||
Second image description.
|
||||
</h2>
|
||||
</div>
|
||||
<div class="item">
|
||||
<!-- Your image here -->
|
||||
<img src="static/images/carousel3.jpg" alt="Third research result visualization" loading="lazy"/>
|
||||
<h2 class="subtitle has-text-centered">
|
||||
Third image description.
|
||||
</h2>
|
||||
</div>
|
||||
<div class="item">
|
||||
<!-- Your image here -->
|
||||
<img src="static/images/carousel4.jpg" alt="Fourth research result visualization" loading="lazy"/>
|
||||
<h2 class="subtitle has-text-centered">
|
||||
Fourth image description.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- End image carousel -->
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Youtube video -->
|
||||
<section class="hero is-small is-light">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<!-- Paper video. -->
|
||||
<h2 class="title is-3">Video Presentation</h2>
|
||||
<div class="columns is-centered has-text-centered">
|
||||
<div class="column is-four-fifths">
|
||||
|
||||
<div class="publication-video">
|
||||
<!-- TODO: Replace with your YouTube video ID -->
|
||||
<iframe src="https://www.youtube.com/embed/JkaxUblCGz0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- End youtube video -->
|
||||
|
||||
|
||||
<!-- Video carousel -->
|
||||
<section class="hero is-small">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h2 class="title is-3">Another Carousel</h2>
|
||||
<div id="results-carousel" class="carousel results-carousel">
|
||||
<div class="item item-video1">
|
||||
<!-- TODO: Add poster image for better preview -->
|
||||
<video poster="" id="video1" controls muted loop height="100%" preload="metadata">
|
||||
<!-- Your video file here -->
|
||||
<source src="static/videos/carousel1.mp4" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
<div class="item item-video2">
|
||||
<!-- TODO: Add poster image for better preview -->
|
||||
<video poster="" id="video2" controls muted loop height="100%" preload="metadata">
|
||||
<!-- Your video file here -->
|
||||
<source src="static/videos/carousel2.mp4" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
<div class="item item-video3">
|
||||
<!-- TODO: Add poster image for better preview -->
|
||||
<video poster="" id="video3" controls muted loop height="100%" preload="metadata">
|
||||
<!-- Your video file here -->
|
||||
<source src="static/videos/carousel3.mp4" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- End video carousel -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Paper poster -->
|
||||
<section class="hero is-small is-light">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h2 class="title">Poster</h2>
|
||||
|
||||
<!-- TODO: Replace with your poster PDF -->
|
||||
<iframe src="static/pdfs/sample.pdf" width="100%" height="550">
|
||||
</iframe>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!--End paper poster -->
|
||||
|
||||
|
||||
|
||||
<!--BibTex citation -->
|
||||
<section class="section" id="BibTeX">
|
||||
<div class="container is-max-desktop content">
|
||||
<div class="bibtex-header">
|
||||
<h2 class="title">BibTeX</h2>
|
||||
<button class="copy-bibtex-btn" onclick="copyBibTeX()" title="Copy BibTeX to clipboard">
|
||||
<i class="fas fa-copy"></i>
|
||||
<span class="copy-text">Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre id="bibtex-code"><code>@thesis{Rosel2025PHANTOM,
|
||||
title={Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms},
|
||||
author={R{\"o}sel, Daniel},
|
||||
school={IE University},
|
||||
year={2025},
|
||||
address={Madrid, Spain},
|
||||
type={Bachelor's Thesis},
|
||||
note={Advisor: Alberto Mart{\'i}n Izquierdo}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
<!--End BibTex citation -->
|
||||
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-8">
|
||||
<div class="content">
|
||||
|
||||
<p>
|
||||
This page was built using the <a href="https://github.com/eliahuhorwitz/Academic-project-page-template" target="_blank">Academic Project Page Template</a> which was adopted from the <a href="https://nerfies.github.io" target="_blank">Nerfies</a> project page.
|
||||
You are free to borrow the source code of this website, we just ask that you link back to this page in the footer. <br> This website is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/" target="_blank">Creative
|
||||
Commons Attribution-ShareAlike 4.0 International License</a>.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Statcounter tracking code -->
|
||||
|
||||
<!-- You can add a tracker to track page visits by creating an account at statcounter.com -->
|
||||
|
||||
<!-- End of Statcounter Code -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1
paper/academic_page/static/css/bulma-carousel.min.css
vendored
Normal file
1
paper/academic_page/static/css/bulma-carousel.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@-webkit-keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes spinAround{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.slider{position:relative;width:100%}.slider-container{display:flex;flex-wrap:nowrap;flex-direction:row;overflow:hidden;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);min-height:100%}.slider-container.is-vertical{flex-direction:column}.slider-container .slider-item{flex:none}.slider-container .slider-item .image.is-covered img{-o-object-fit:cover;object-fit:cover;-o-object-position:center center;object-position:center center;height:100%;width:100%}.slider-container .slider-item .video-container{height:0;padding-bottom:0;padding-top:56.25%;margin:0;position:relative}.slider-container .slider-item .video-container.is-1by1,.slider-container .slider-item .video-container.is-square{padding-top:100%}.slider-container .slider-item .video-container.is-4by3{padding-top:75%}.slider-container .slider-item .video-container.is-21by9{padding-top:42.857143%}.slider-container .slider-item .video-container embed,.slider-container .slider-item .video-container iframe,.slider-container .slider-item .video-container object{position:absolute;top:0;left:0;width:100%!important;height:100%!important}.slider-navigation-next,.slider-navigation-previous{display:flex;justify-content:center;align-items:center;position:absolute;width:42px;height:42px;background:#fff center center no-repeat;background-size:20px 20px;border:1px solid #fff;border-radius:25091983px;box-shadow:0 2px 5px #3232321a;top:50%;margin-top:-20px;left:0;cursor:pointer;transition:opacity .3s,-webkit-transform .3s;transition:transform .3s,opacity .3s;transition:transform .3s,opacity .3s,-webkit-transform .3s}.slider-navigation-next:hover,.slider-navigation-previous:hover{-webkit-transform:scale(1.2);transform:scale(1.2)}.slider-navigation-next.is-hidden,.slider-navigation-previous.is-hidden{display:none;opacity:0}.slider-navigation-next svg,.slider-navigation-previous svg{width:25%}.slider-navigation-next{left:auto;right:0;background:#fff center center no-repeat;background-size:20px 20px}.slider-pagination{display:none;justify-content:center;align-items:center;position:absolute;bottom:0;left:0;right:0;padding:.5rem 1rem;text-align:center}.slider-pagination .slider-page{background:#fff;width:10px;height:10px;border-radius:25091983px;display:inline-block;margin:0 3px;box-shadow:0 2px 5px #3232321a;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s;cursor:pointer}.slider-pagination .slider-page.is-active,.slider-pagination .slider-page:hover{-webkit-transform:scale(1.4);transform:scale(1.4)}@media screen and (min-width:800px){.slider-pagination{display:flex}}.hero.has-carousel{position:relative}.hero.has-carousel+.hero-body,.hero.has-carousel+.hero-footer,.hero.has-carousel+.hero-head{z-index:10;overflow:hidden}.hero.has-carousel .hero-carousel{position:absolute;top:0;left:0;bottom:0;right:0;height:auto;border:none;margin:auto;padding:0;z-index:0}.hero.has-carousel .hero-carousel .slider{width:100%;max-width:100%;overflow:hidden;height:100%!important;max-height:100%;z-index:0}.hero.has-carousel .hero-carousel .slider .has-background{max-height:100%}.hero.has-carousel .hero-carousel .slider .has-background .is-background{-o-object-fit:cover;object-fit:cover;-o-object-position:center center;object-position:center center;height:100%;width:100%}.hero.has-carousel .hero-body{margin:0 3rem;z-index:10}
|
||||
1
paper/academic_page/static/css/bulma-slider.min.css
vendored
Normal file
1
paper/academic_page/static/css/bulma-slider.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
paper/academic_page/static/css/bulma.css.map.txt
Normal file
1
paper/academic_page/static/css/bulma.css.map.txt
Normal file
File diff suppressed because one or more lines are too long
1
paper/academic_page/static/css/bulma.min.css
vendored
Normal file
1
paper/academic_page/static/css/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
5
paper/academic_page/static/css/fontawesome.all.min.css
vendored
Normal file
5
paper/academic_page/static/css/fontawesome.all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
753
paper/academic_page/static/css/index.css
Normal file
753
paper/academic_page/static/css/index.css
Normal file
@@ -0,0 +1,753 @@
|
||||
/* Modern CSS Variables for consistent theming */
|
||||
:root {
|
||||
--primary-color: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--secondary-color: #64748b;
|
||||
--accent-color: #0ea5e9;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-light: #94a3b8;
|
||||
--background-primary: #ffffff;
|
||||
--background-secondary: #f8fafc;
|
||||
--background-accent: #f1f5f9;
|
||||
--border-color: #e2e8f0;
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--gradient-accent: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
--gradient-subtle: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||
--border-radius: 12px;
|
||||
--border-radius-lg: 16px;
|
||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Smooth scrolling for better UX */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
background-color: var(--background-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
|
||||
/* Modern Button Styles */
|
||||
.button {
|
||||
border-radius: var(--border-radius) !important;
|
||||
font-weight: 600 !important;
|
||||
transition: var(--transition) !important;
|
||||
border: 2px solid transparent !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button.is-dark {
|
||||
background: var(--text-primary) !important;
|
||||
border: none !important;
|
||||
color: white !important;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.button.is-dark:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.button.is-dark:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.footer .icon-link {
|
||||
font-size: 25px;
|
||||
color: var(--text-secondary);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.footer .icon-link:hover {
|
||||
color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.link-block a {
|
||||
margin: 8px 4px;
|
||||
}
|
||||
|
||||
.dnerf {
|
||||
font-variant: small-caps;
|
||||
}
|
||||
|
||||
|
||||
/* Hero Section Modernization */
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero.is-light {
|
||||
background: var(--background-secondary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.hero-body {
|
||||
padding: 4rem 1.5rem;
|
||||
}
|
||||
|
||||
.teaser .hero-body {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.teaser {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* Publication Content Styling */
|
||||
.publication-title {
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
font-weight: 800 !important;
|
||||
color: var(--text-primary) !important;
|
||||
margin-bottom: 2rem !important;
|
||||
line-height: 1.1 !important;
|
||||
}
|
||||
|
||||
.publication-banner {
|
||||
max-height: 70vh;
|
||||
border-radius: var(--border-radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-xl);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.publication-banner video {
|
||||
position: relative;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
.publication-header .hero-body {
|
||||
padding: 6rem 1.5rem 4rem;
|
||||
}
|
||||
|
||||
.publication-authors {
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
font-weight: 500;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.publication-venue {
|
||||
color: var(--text-secondary);
|
||||
width: fit-content;
|
||||
font-weight: 600;
|
||||
background: var(--background-accent);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
margin-top: 1rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.publication-awards {
|
||||
color: #ef4444;
|
||||
width: fit-content;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #fef2f2, #fee2e2);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
border-left: 4px solid #ef4444;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.publication-authors a {
|
||||
color: var(--primary-color) !important;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.publication-authors a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
background: var(--gradient-accent);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.publication-authors a:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.publication-authors a:hover {
|
||||
color: var(--primary-hover) !important;
|
||||
}
|
||||
|
||||
.author-block {
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.publication-banner img {
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.publication-banner img:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
/* Modern Video and Carousel Styling */
|
||||
.publication-video {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 56.25%;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius-lg) !important;
|
||||
box-shadow: var(--shadow-xl);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.publication-video:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.publication-video iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--border-radius-lg);
|
||||
}
|
||||
|
||||
.publication-body img {
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.publication-body img:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.results-carousel {
|
||||
overflow: hidden;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.results-carousel .item {
|
||||
margin: 1rem;
|
||||
overflow: hidden;
|
||||
padding: 1.5rem;
|
||||
font-size: 0;
|
||||
background: var(--background-primary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: var(--transition);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.results-carousel .item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.results-carousel .item img,
|
||||
.results-carousel video {
|
||||
margin: 0;
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.results-carousel .subtitle {
|
||||
font-size: 1rem !important;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Pagination and Misc Improvements */
|
||||
.slider-pagination .slider-page {
|
||||
background: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.slider-pagination .slider-page.is-active {
|
||||
background: var(--primary-hover);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.eql-cntrb {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-light);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Section Titles */
|
||||
.title.is-3 {
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2rem !important;
|
||||
position: relative;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.title.is-3::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background: var(--gradient-accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Content Improvements */
|
||||
.content.has-text-justified {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.content.has-text-justified p {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Footer Improvements */
|
||||
.footer {
|
||||
background: var(--background-secondary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
|
||||
.footer .content {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
color: var(--primary-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* BibTeX Styling */
|
||||
pre {
|
||||
background: var(--background-accent) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
border-radius: var(--border-radius) !important;
|
||||
padding: 1.5rem !important;
|
||||
font-size: 0.9rem !important;
|
||||
overflow-x: auto;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--background-accent) !important;
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace !important;
|
||||
}
|
||||
|
||||
/* BibTeX Section Improvements */
|
||||
.bibtex-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.copy-bibtex-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.copy-bibtex-btn:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.copy-bibtex-btn.copied {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.copy-bibtex-btn.copied .copy-text::after {
|
||||
content: "ied!";
|
||||
}
|
||||
|
||||
/* Scroll to Top Button */
|
||||
.scroll-to-top {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: var(--transition);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.scroll-to-top:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.scroll-to-top.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
||||
/* More Works Dropdown */
|
||||
.more-works-container {
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.more-works-btn {
|
||||
background: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: var(--transition);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.more-works-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: var(--background-secondary);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.more-works-btn .dropdown-arrow {
|
||||
transition: var(--transition);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.more-works-btn.active .dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.more-works-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem);
|
||||
right: 0;
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: var(--transition);
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.more-works-dropdown.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1.5rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.dropdown-header h4 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: var(--background-accent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.works-list {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.work-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: var(--transition);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.work-item:hover {
|
||||
background: var(--background-accent);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.work-info h5 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.work-info p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.work-venue {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.work-item .fas {
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Mobile Responsive Improvements */
|
||||
@media screen and (max-width: 768px) {
|
||||
.hero-body {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.publication-header .hero-body {
|
||||
padding: 3rem 1rem 2rem;
|
||||
}
|
||||
|
||||
.publication-title {
|
||||
font-size: 2.5rem !important;
|
||||
line-height: 1.2 !important;
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.publication-authors {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0.25rem !important;
|
||||
font-size: 0.875rem !important;
|
||||
padding: 0.75rem 1rem !important;
|
||||
}
|
||||
|
||||
.more-works-container {
|
||||
bottom: 2rem;
|
||||
right: 1rem;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.more-works-btn {
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.more-works-dropdown {
|
||||
width: calc(100vw - 2rem);
|
||||
right: -1rem;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.results-carousel .item {
|
||||
margin: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.teaser .hero-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.content.has-text-justified {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.publication-title {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
|
||||
.hero-body {
|
||||
padding: 1.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.more-works-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.more-works-btn {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.more-works-dropdown {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
right: 0;
|
||||
width: calc(100vw - 2rem);
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.link-block {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet Responsive */
|
||||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||||
.hero-body {
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.publication-header .hero-body {
|
||||
padding: 4rem 2rem 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for page load */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.hero, .section {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Improved focus states for accessibility */
|
||||
.button:focus,
|
||||
.related-works-btn:focus,
|
||||
a:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.more-works-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero, .section {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: transparent !important;
|
||||
color: var(--text-primary) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
paper/academic_page/static/images/carousel1.jpg
Normal file
BIN
paper/academic_page/static/images/carousel1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
BIN
paper/academic_page/static/images/carousel2.jpg
Normal file
BIN
paper/academic_page/static/images/carousel2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 363 KiB |
BIN
paper/academic_page/static/images/carousel3.jpg
Normal file
BIN
paper/academic_page/static/images/carousel3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 496 KiB |
BIN
paper/academic_page/static/images/carousel4.jpg
Normal file
BIN
paper/academic_page/static/images/carousel4.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
BIN
paper/academic_page/static/images/favicon.ico
Normal file
BIN
paper/academic_page/static/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
2371
paper/academic_page/static/js/bulma-carousel.js
Normal file
2371
paper/academic_page/static/js/bulma-carousel.js
Normal file
File diff suppressed because it is too large
Load Diff
1
paper/academic_page/static/js/bulma-carousel.min.js
vendored
Normal file
1
paper/academic_page/static/js/bulma-carousel.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
461
paper/academic_page/static/js/bulma-slider.js
Normal file
461
paper/academic_page/static/js/bulma-slider.js
Normal file
@@ -0,0 +1,461 @@
|
||||
(function webpackUniversalModuleDefinition(root, factory) {
|
||||
if(typeof exports === 'object' && typeof module === 'object')
|
||||
module.exports = factory();
|
||||
else if(typeof define === 'function' && define.amd)
|
||||
define([], factory);
|
||||
else if(typeof exports === 'object')
|
||||
exports["bulmaSlider"] = factory();
|
||||
else
|
||||
root["bulmaSlider"] = factory();
|
||||
})(typeof self !== 'undefined' ? self : this, function() {
|
||||
return /******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) {
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ i: moduleId,
|
||||
/******/ l: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.l = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // define getter function for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||
/******/ Object.defineProperty(exports, name, {
|
||||
/******/ configurable: false,
|
||||
/******/ enumerable: true,
|
||||
/******/ get: getter
|
||||
/******/ });
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function getDefault() { return module['default']; } :
|
||||
/******/ function getModuleExports() { return module; };
|
||||
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Object.prototype.hasOwnProperty.call
|
||||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
/******/
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(__webpack_require__.s = 0);
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ([
|
||||
/* 0 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
|
||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isString", function() { return isString; });
|
||||
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__events__ = __webpack_require__(1);
|
||||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
||||
|
||||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
||||
|
||||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
|
||||
|
||||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
|
||||
|
||||
|
||||
|
||||
var isString = function isString(unknown) {
|
||||
return typeof unknown === 'string' || !!unknown && (typeof unknown === 'undefined' ? 'undefined' : _typeof(unknown)) === 'object' && Object.prototype.toString.call(unknown) === '[object String]';
|
||||
};
|
||||
|
||||
var bulmaSlider = function (_EventEmitter) {
|
||||
_inherits(bulmaSlider, _EventEmitter);
|
||||
|
||||
function bulmaSlider(selector) {
|
||||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
|
||||
_classCallCheck(this, bulmaSlider);
|
||||
|
||||
var _this = _possibleConstructorReturn(this, (bulmaSlider.__proto__ || Object.getPrototypeOf(bulmaSlider)).call(this));
|
||||
|
||||
_this.element = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
||||
// An invalid selector or non-DOM node has been provided.
|
||||
if (!_this.element) {
|
||||
throw new Error('An invalid selector or non-DOM node has been provided.');
|
||||
}
|
||||
|
||||
_this._clickEvents = ['click'];
|
||||
/// Set default options and merge with instance defined
|
||||
_this.options = _extends({}, options);
|
||||
|
||||
_this.onSliderInput = _this.onSliderInput.bind(_this);
|
||||
|
||||
_this.init();
|
||||
return _this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate all DOM element containing selector
|
||||
* @method
|
||||
* @return {Array} Array of all slider instances
|
||||
*/
|
||||
|
||||
|
||||
_createClass(bulmaSlider, [{
|
||||
key: 'init',
|
||||
|
||||
|
||||
/**
|
||||
* Initiate plugin
|
||||
* @method init
|
||||
* @return {void}
|
||||
*/
|
||||
value: function init() {
|
||||
this._id = 'bulmaSlider' + new Date().getTime() + Math.floor(Math.random() * Math.floor(9999));
|
||||
this.output = this._findOutputForSlider();
|
||||
|
||||
this._bindEvents();
|
||||
|
||||
if (this.output) {
|
||||
if (this.element.classList.contains('has-output-tooltip')) {
|
||||
// Get new output position
|
||||
var newPosition = this._getSliderOutputPosition();
|
||||
|
||||
// Set output position
|
||||
this.output.style['left'] = newPosition.position;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('bulmaslider:ready', this.element.value);
|
||||
}
|
||||
}, {
|
||||
key: '_findOutputForSlider',
|
||||
value: function _findOutputForSlider() {
|
||||
var _this2 = this;
|
||||
|
||||
var result = null;
|
||||
var outputs = document.getElementsByTagName('output') || [];
|
||||
|
||||
Array.from(outputs).forEach(function (output) {
|
||||
if (output.htmlFor == _this2.element.getAttribute('id')) {
|
||||
result = output;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}, {
|
||||
key: '_getSliderOutputPosition',
|
||||
value: function _getSliderOutputPosition() {
|
||||
// Update output position
|
||||
var newPlace, minValue;
|
||||
|
||||
var style = window.getComputedStyle(this.element, null);
|
||||
// Measure width of range input
|
||||
var sliderWidth = parseInt(style.getPropertyValue('width'), 10);
|
||||
|
||||
// Figure out placement percentage between left and right of input
|
||||
if (!this.element.getAttribute('min')) {
|
||||
minValue = 0;
|
||||
} else {
|
||||
minValue = this.element.getAttribute('min');
|
||||
}
|
||||
var newPoint = (this.element.value - minValue) / (this.element.getAttribute('max') - minValue);
|
||||
|
||||
// Prevent bubble from going beyond left or right (unsupported browsers)
|
||||
if (newPoint < 0) {
|
||||
newPlace = 0;
|
||||
} else if (newPoint > 1) {
|
||||
newPlace = sliderWidth;
|
||||
} else {
|
||||
newPlace = sliderWidth * newPoint;
|
||||
}
|
||||
|
||||
return {
|
||||
'position': newPlace + 'px'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind all events
|
||||
* @method _bindEvents
|
||||
* @return {void}
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: '_bindEvents',
|
||||
value: function _bindEvents() {
|
||||
if (this.output) {
|
||||
// Add event listener to update output when slider value change
|
||||
this.element.addEventListener('input', this.onSliderInput, false);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'onSliderInput',
|
||||
value: function onSliderInput(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.element.classList.contains('has-output-tooltip')) {
|
||||
// Get new output position
|
||||
var newPosition = this._getSliderOutputPosition();
|
||||
|
||||
// Set output position
|
||||
this.output.style['left'] = newPosition.position;
|
||||
}
|
||||
|
||||
// Check for prefix and postfix
|
||||
var prefix = this.output.hasAttribute('data-prefix') ? this.output.getAttribute('data-prefix') : '';
|
||||
var postfix = this.output.hasAttribute('data-postfix') ? this.output.getAttribute('data-postfix') : '';
|
||||
|
||||
// Update output with slider value
|
||||
this.output.value = prefix + this.element.value + postfix;
|
||||
|
||||
this.emit('bulmaslider:ready', this.element.value);
|
||||
}
|
||||
}], [{
|
||||
key: 'attach',
|
||||
value: function attach() {
|
||||
var _this3 = this;
|
||||
|
||||
var selector = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'input[type="range"].slider';
|
||||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
|
||||
var instances = new Array();
|
||||
|
||||
var elements = isString(selector) ? document.querySelectorAll(selector) : Array.isArray(selector) ? selector : [selector];
|
||||
elements.forEach(function (element) {
|
||||
if (typeof element[_this3.constructor.name] === 'undefined') {
|
||||
var instance = new bulmaSlider(element, options);
|
||||
element[_this3.constructor.name] = instance;
|
||||
instances.push(instance);
|
||||
} else {
|
||||
instances.push(element[_this3.constructor.name]);
|
||||
}
|
||||
});
|
||||
|
||||
return instances;
|
||||
}
|
||||
}]);
|
||||
|
||||
return bulmaSlider;
|
||||
}(__WEBPACK_IMPORTED_MODULE_0__events__["a" /* default */]);
|
||||
|
||||
/* harmony default export */ __webpack_exports__["default"] = (bulmaSlider);
|
||||
|
||||
/***/ }),
|
||||
/* 1 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
var EventEmitter = function () {
|
||||
function EventEmitter() {
|
||||
var listeners = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
||||
|
||||
_classCallCheck(this, EventEmitter);
|
||||
|
||||
this._listeners = new Map(listeners);
|
||||
this._middlewares = new Map();
|
||||
}
|
||||
|
||||
_createClass(EventEmitter, [{
|
||||
key: "listenerCount",
|
||||
value: function listenerCount(eventName) {
|
||||
if (!this._listeners.has(eventName)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var eventListeners = this._listeners.get(eventName);
|
||||
return eventListeners.length;
|
||||
}
|
||||
}, {
|
||||
key: "removeListeners",
|
||||
value: function removeListeners() {
|
||||
var _this = this;
|
||||
|
||||
var eventName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
||||
var middleware = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
||||
|
||||
if (eventName !== null) {
|
||||
if (Array.isArray(eventName)) {
|
||||
name.forEach(function (e) {
|
||||
return _this.removeListeners(e, middleware);
|
||||
});
|
||||
} else {
|
||||
this._listeners.delete(eventName);
|
||||
|
||||
if (middleware) {
|
||||
this.removeMiddleware(eventName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._listeners = new Map();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "middleware",
|
||||
value: function middleware(eventName, fn) {
|
||||
var _this2 = this;
|
||||
|
||||
if (Array.isArray(eventName)) {
|
||||
name.forEach(function (e) {
|
||||
return _this2.middleware(e, fn);
|
||||
});
|
||||
} else {
|
||||
if (!Array.isArray(this._middlewares.get(eventName))) {
|
||||
this._middlewares.set(eventName, []);
|
||||
}
|
||||
|
||||
this._middlewares.get(eventName).push(fn);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "removeMiddleware",
|
||||
value: function removeMiddleware() {
|
||||
var _this3 = this;
|
||||
|
||||
var eventName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
||||
|
||||
if (eventName !== null) {
|
||||
if (Array.isArray(eventName)) {
|
||||
name.forEach(function (e) {
|
||||
return _this3.removeMiddleware(e);
|
||||
});
|
||||
} else {
|
||||
this._middlewares.delete(eventName);
|
||||
}
|
||||
} else {
|
||||
this._middlewares = new Map();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "on",
|
||||
value: function on(name, callback) {
|
||||
var _this4 = this;
|
||||
|
||||
var once = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
||||
|
||||
if (Array.isArray(name)) {
|
||||
name.forEach(function (e) {
|
||||
return _this4.on(e, callback);
|
||||
});
|
||||
} else {
|
||||
name = name.toString();
|
||||
var split = name.split(/,|, | /);
|
||||
|
||||
if (split.length > 1) {
|
||||
split.forEach(function (e) {
|
||||
return _this4.on(e, callback);
|
||||
});
|
||||
} else {
|
||||
if (!Array.isArray(this._listeners.get(name))) {
|
||||
this._listeners.set(name, []);
|
||||
}
|
||||
|
||||
this._listeners.get(name).push({ once: once, callback: callback });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: "once",
|
||||
value: function once(name, callback) {
|
||||
this.on(name, callback, true);
|
||||
}
|
||||
}, {
|
||||
key: "emit",
|
||||
value: function emit(name, data) {
|
||||
var _this5 = this;
|
||||
|
||||
var silent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
||||
|
||||
name = name.toString();
|
||||
var listeners = this._listeners.get(name);
|
||||
var middlewares = null;
|
||||
var doneCount = 0;
|
||||
var execute = silent;
|
||||
|
||||
if (Array.isArray(listeners)) {
|
||||
listeners.forEach(function (listener, index) {
|
||||
// Start Middleware checks unless we're doing a silent emit
|
||||
if (!silent) {
|
||||
middlewares = _this5._middlewares.get(name);
|
||||
// Check and execute Middleware
|
||||
if (Array.isArray(middlewares)) {
|
||||
middlewares.forEach(function (middleware) {
|
||||
middleware(data, function () {
|
||||
var newData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
||||
|
||||
if (newData !== null) {
|
||||
data = newData;
|
||||
}
|
||||
doneCount++;
|
||||
}, name);
|
||||
});
|
||||
|
||||
if (doneCount >= middlewares.length) {
|
||||
execute = true;
|
||||
}
|
||||
} else {
|
||||
execute = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If Middleware checks have been passed, execute
|
||||
if (execute) {
|
||||
if (listener.once) {
|
||||
listeners[index] = null;
|
||||
}
|
||||
listener.callback(data);
|
||||
}
|
||||
});
|
||||
|
||||
// Dirty way of removing used Events
|
||||
while (listeners.indexOf(null) !== -1) {
|
||||
listeners.splice(listeners.indexOf(null), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
return EventEmitter;
|
||||
}();
|
||||
|
||||
/* harmony default export */ __webpack_exports__["a"] = (EventEmitter);
|
||||
|
||||
/***/ })
|
||||
/******/ ])["default"];
|
||||
});
|
||||
1
paper/academic_page/static/js/bulma-slider.min.js
vendored
Normal file
1
paper/academic_page/static/js/bulma-slider.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
paper/academic_page/static/js/fontawesome.all.min.js
vendored
Normal file
5
paper/academic_page/static/js/fontawesome.all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
142
paper/academic_page/static/js/index.js
Normal file
142
paper/academic_page/static/js/index.js
Normal file
@@ -0,0 +1,142 @@
|
||||
window.HELP_IMPROVE_VIDEOJS = false;
|
||||
|
||||
// More Works Dropdown Functionality
|
||||
function toggleMoreWorks() {
|
||||
const dropdown = document.getElementById('moreWorksDropdown');
|
||||
const button = document.querySelector('.more-works-btn');
|
||||
|
||||
if (dropdown.classList.contains('show')) {
|
||||
dropdown.classList.remove('show');
|
||||
button.classList.remove('active');
|
||||
} else {
|
||||
dropdown.classList.add('show');
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const container = document.querySelector('.more-works-container');
|
||||
const dropdown = document.getElementById('moreWorksDropdown');
|
||||
const button = document.querySelector('.more-works-btn');
|
||||
|
||||
if (container && !container.contains(event.target)) {
|
||||
dropdown.classList.remove('show');
|
||||
button.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Close dropdown on escape key
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
const dropdown = document.getElementById('moreWorksDropdown');
|
||||
const button = document.querySelector('.more-works-btn');
|
||||
dropdown.classList.remove('show');
|
||||
button.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Copy BibTeX to clipboard
|
||||
function copyBibTeX() {
|
||||
const bibtexElement = document.getElementById('bibtex-code');
|
||||
const button = document.querySelector('.copy-bibtex-btn');
|
||||
const copyText = button.querySelector('.copy-text');
|
||||
|
||||
if (bibtexElement) {
|
||||
navigator.clipboard.writeText(bibtexElement.textContent).then(function() {
|
||||
// Success feedback
|
||||
button.classList.add('copied');
|
||||
copyText.textContent = 'Cop';
|
||||
|
||||
setTimeout(function() {
|
||||
button.classList.remove('copied');
|
||||
copyText.textContent = 'Copy';
|
||||
}, 2000);
|
||||
}).catch(function(err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = bibtexElement.textContent;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
button.classList.add('copied');
|
||||
copyText.textContent = 'Cop';
|
||||
setTimeout(function() {
|
||||
button.classList.remove('copied');
|
||||
copyText.textContent = 'Copy';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to top functionality
|
||||
function scrollToTop() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
// Show/hide scroll to top button
|
||||
window.addEventListener('scroll', function() {
|
||||
const scrollButton = document.querySelector('.scroll-to-top');
|
||||
if (window.pageYOffset > 300) {
|
||||
scrollButton.classList.add('visible');
|
||||
} else {
|
||||
scrollButton.classList.remove('visible');
|
||||
}
|
||||
});
|
||||
|
||||
// Video carousel autoplay when in view
|
||||
function setupVideoCarouselAutoplay() {
|
||||
const carouselVideos = document.querySelectorAll('.results-carousel video');
|
||||
|
||||
if (carouselVideos.length === 0) return;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
const video = entry.target;
|
||||
if (entry.isIntersecting) {
|
||||
// Video is in view, play it
|
||||
video.play().catch(e => {
|
||||
// Autoplay failed, probably due to browser policy
|
||||
console.log('Autoplay prevented:', e);
|
||||
});
|
||||
} else {
|
||||
// Video is out of view, pause it
|
||||
video.pause();
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: 0.5 // Trigger when 50% of the video is visible
|
||||
});
|
||||
|
||||
carouselVideos.forEach(video => {
|
||||
observer.observe(video);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// Check for click events on the navbar burger icon
|
||||
|
||||
var options = {
|
||||
slidesToScroll: 1,
|
||||
slidesToShow: 1,
|
||||
loop: true,
|
||||
infinite: true,
|
||||
autoplay: true,
|
||||
autoplaySpeed: 5000,
|
||||
}
|
||||
|
||||
// Initialize all div with carousel class
|
||||
var carousels = bulmaCarousel.attach('.carousel', options);
|
||||
|
||||
bulmaSlider.attach();
|
||||
|
||||
// Setup video autoplay for carousel
|
||||
setupVideoCarouselAutoplay();
|
||||
|
||||
})
|
||||
BIN
paper/academic_page/static/pdfs/sample.pdf
Normal file
BIN
paper/academic_page/static/pdfs/sample.pdf
Normal file
Binary file not shown.
BIN
paper/academic_page/static/videos/banner_video.mp4
Normal file
BIN
paper/academic_page/static/videos/banner_video.mp4
Normal file
Binary file not shown.
BIN
paper/academic_page/static/videos/carousel1.mp4
Normal file
BIN
paper/academic_page/static/videos/carousel1.mp4
Normal file
Binary file not shown.
BIN
paper/academic_page/static/videos/carousel2.mp4
Normal file
BIN
paper/academic_page/static/videos/carousel2.mp4
Normal file
Binary file not shown.
BIN
paper/academic_page/static/videos/carousel3.mp4
Normal file
BIN
paper/academic_page/static/videos/carousel3.mp4
Normal file
Binary file not shown.
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
kafka-python
|
||||
dotenv
|
||||
pandas
|
||||
jupyter
|
||||
ipykernel
|
||||
matplotlib
|
||||
graphviz
|
||||
10
web/package-lock.json
generated
10
web/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"kafkajs": "^2.2.4",
|
||||
"next": "16.0.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0"
|
||||
@@ -1040,6 +1041,15 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/kafkajs": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz",
|
||||
"integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
|
||||
@@ -8,16 +8,17 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"kafkajs": "^2.2.4",
|
||||
"next": "16.0.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"next": "16.0.0"
|
||||
"react-dom": "19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4"
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
33
web/src/app/api/track/route.ts
Normal file
33
web/src/app/api/track/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { sendInteractionEvent } from '@/lib/kafka';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { sessionId, eventType, targetEl, targetUrl, metadata } = body;
|
||||
|
||||
if (!sessionId || !eventType) {
|
||||
return NextResponse.json(
|
||||
{ error: 'sessionId and eventType required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await sendInteractionEvent({
|
||||
sessionId,
|
||||
eventType,
|
||||
targetEl,
|
||||
targetUrl,
|
||||
metadata,
|
||||
ts: Date.now(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err: any) {
|
||||
console.error('track error:', err);
|
||||
return NextResponse.json(
|
||||
{ error: err.message || 'unknown error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { TrackingProvider } from "@/components/TrackingProvider";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -27,7 +28,7 @@ export default function RootLayout({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<TrackingProvider>{children}</TrackingProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
8
web/src/components/TrackingProvider.tsx
Normal file
8
web/src/components/TrackingProvider.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useInteractionTracking } from '@/hooks/useInteractionTracking';
|
||||
|
||||
export const TrackingProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
useInteractionTracking();
|
||||
return <>{children}</>;
|
||||
};
|
||||
117
web/src/hooks/useInteractionTracking.ts
Normal file
117
web/src/hooks/useInteractionTracking.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import '@/lib/experiments' // ensure experiments lib is loaded
|
||||
|
||||
const genSessionId = () => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
let sid = sessionStorage.getItem('phantom_session_id');
|
||||
if (!sid) {
|
||||
sid = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
sessionStorage.setItem('phantom_session_id', sid);
|
||||
// TODO: when creating new id send to exepriemtn tracking db
|
||||
// match between sesion-id and experiment-id for this session
|
||||
// so that we can identify all interactions aligning with a specific experiment goal.
|
||||
}
|
||||
return sid;
|
||||
};
|
||||
|
||||
const track = async (ev: {
|
||||
sessionId: string;
|
||||
eventType: string;
|
||||
targetEl?: string;
|
||||
targetUrl?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}) => {
|
||||
try {
|
||||
await fetch('/api/track', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ev),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('track failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
export const useInteractionTracking = () => {
|
||||
const sidRef = useRef<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
sidRef.current = genSessionId();
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
const tgt = e.target as HTMLElement;
|
||||
track({
|
||||
sessionId: sidRef.current,
|
||||
eventType: 'click',
|
||||
targetEl: tgt.tagName,
|
||||
targetUrl: tgt instanceof HTMLAnchorElement ? tgt.href : undefined,
|
||||
metadata: {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
path: window.location.pathname,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
track({
|
||||
sessionId: sidRef.current,
|
||||
eventType: 'scroll',
|
||||
metadata: {
|
||||
scrollY: window.scrollY,
|
||||
path: window.location.pathname,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handlePageView = () => {
|
||||
track({
|
||||
sessionId: sidRef.current,
|
||||
eventType: 'pageview',
|
||||
metadata: {
|
||||
path: window.location.pathname,
|
||||
referrer: document.referrer,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
enum DefinedInteractions {
|
||||
ADD_TO_CART = 'add_to_cart',
|
||||
PURCHASE = 'purchase',
|
||||
}
|
||||
|
||||
// called when clicking on "Add to Cart" button or "Purchase" button
|
||||
const handleDefinedInteraction = (
|
||||
interactionType: DefinedInteractions,
|
||||
metadata?: Record<string, any>
|
||||
) => {
|
||||
track({
|
||||
sessionId: sidRef.current,
|
||||
eventType: interactionType,
|
||||
metadata: {
|
||||
path: window.location.pathname,
|
||||
...metadata,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
handlePageView();
|
||||
document.addEventListener('click', handleClick);
|
||||
document.addEventListener('definedInteraction', (e: Event) => {
|
||||
const customEvent = e as CustomEvent;
|
||||
handleDefinedInteraction(customEvent.detail.interactionType, customEvent.detail.metadata);
|
||||
});
|
||||
// TOO NOISY: enable if needed but tbh not worth it
|
||||
//window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick);
|
||||
document.removeEventListener('definedInteraction', (e: Event) => {
|
||||
const customEvent = e as CustomEvent;
|
||||
handleDefinedInteraction(customEvent.detail.interactionType, customEvent.detail.metadata);
|
||||
});
|
||||
//window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
1
web/src/lib/experiments.ts
Normal file
1
web/src/lib/experiments.ts
Normal file
@@ -0,0 +1 @@
|
||||
//
|
||||
42
web/src/lib/kafka.ts
Normal file
42
web/src/lib/kafka.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Kafka, Producer } from 'kafkajs';
|
||||
|
||||
let producer: Producer | null = null;
|
||||
|
||||
const kafka = new Kafka({
|
||||
clientId: 'phantom-web',
|
||||
brokers: [`${process.env.KAFKA_HOST || 'localhost'}:${process.env.KAFKA_PORT || '9092'}`],
|
||||
});
|
||||
|
||||
export const getProducer = async (): Promise<Producer> => {
|
||||
if (!producer) {
|
||||
producer = kafka.producer();
|
||||
await producer.connect();
|
||||
}
|
||||
return producer;
|
||||
};
|
||||
|
||||
export const sendInteractionEvent = async (ev: {
|
||||
sessionId: string;
|
||||
eventType: string;
|
||||
targetEl?: string;
|
||||
targetUrl?: string;
|
||||
metadata?: Record<string, any>;
|
||||
ts: number;
|
||||
}) => {
|
||||
const p = await getProducer();
|
||||
// add to the metadata
|
||||
await p.send({
|
||||
topic: 'user-interactions',
|
||||
messages: [{
|
||||
key: ev.sessionId,
|
||||
value: JSON.stringify(ev),
|
||||
}],
|
||||
});
|
||||
};
|
||||
|
||||
export const disconnect = async () => {
|
||||
if (producer) {
|
||||
await producer.disconnect();
|
||||
producer = null;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user