feat: text editor and blog post
This commit is contained in:
parent
9e383ee26e
commit
78297efe5c
17 changed files with 2008 additions and 24 deletions
|
|
@ -10,6 +10,7 @@ readme = "README.md"
|
|||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"flask>=2.3.2,<3.0.0",
|
||||
"flask-cors>=4.0.0,<5.0.0",
|
||||
"transformers>=4.30.2,<5.0.0",
|
||||
"nltk>=3.8.1,<4.0.0",
|
||||
"sentence-transformers>=2.2.2,<3.0.0",
|
||||
|
|
|
|||
|
|
@ -1,25 +1,53 @@
|
|||
from flask import Flask, request
|
||||
from flask_cors import CORS
|
||||
import numpy as np
|
||||
from .salience import extract, AVAILABLE_MODELS
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app, origins=["http://localhost:5173"])
|
||||
|
||||
# Load default text from transcript.txt for GET requests
|
||||
with open('./transcript.txt', 'r') as file:
|
||||
source_text = file.read().strip()
|
||||
default_source_text = file.read().strip()
|
||||
|
||||
@app.route("/models")
|
||||
def models_view():
|
||||
return json.dumps(list(AVAILABLE_MODELS.keys()))
|
||||
|
||||
@app.route("/salience")
|
||||
def salience_view():
|
||||
@app.route("/salience", methods=['GET'])
|
||||
def salience_view_default():
|
||||
"""GET endpoint - processes default text from transcript.txt"""
|
||||
model_name = request.args.get('model', 'all-mpnet-base-v2')
|
||||
|
||||
# Validate model name
|
||||
if model_name not in AVAILABLE_MODELS:
|
||||
return json.dumps({'error': f'Invalid model: {model_name}'}), 400
|
||||
|
||||
sentence_ranges, adjacency = extract(default_source_text, model_name)
|
||||
|
||||
return json.dumps({
|
||||
'source': default_source_text,
|
||||
'intervals': sentence_ranges,
|
||||
'adjacency': np.nan_to_num(adjacency.numpy()).tolist(),
|
||||
'model': model_name,
|
||||
})
|
||||
|
||||
@app.route("/salience", methods=['POST'])
|
||||
def salience_view_custom():
|
||||
"""POST endpoint - processes text from request body"""
|
||||
model_name = request.args.get('model', 'all-mpnet-base-v2')
|
||||
|
||||
# Validate model name
|
||||
if model_name not in AVAILABLE_MODELS:
|
||||
return json.dumps({'error': f'Invalid model: {model_name}'}), 400
|
||||
|
||||
# Get document content from request body as plain text
|
||||
source_text = request.data.decode('utf-8').strip()
|
||||
|
||||
if not source_text:
|
||||
return json.dumps({'error': 'No text provided'}), 400
|
||||
|
||||
sentence_ranges, adjacency = extract(source_text, model_name)
|
||||
|
||||
return json.dumps({
|
||||
|
|
|
|||
|
|
@ -83,16 +83,31 @@ def text_rank(sentences, model_name='all-mpnet-base-v2'):
|
|||
adjacency[adjacency < 0] = 0
|
||||
return normalized_adjacency(adjacency)
|
||||
|
||||
def extract(source_text, model_name='all-mpnet-base-v2'):
|
||||
"""
|
||||
Main API function that extracts sentence positions and computes normalized adjacency matrix.
|
||||
|
||||
Returns:
|
||||
sentence_ranges: List of (start, end) tuples for each sentence's character position
|
||||
adjacency: (N × N) normalized adjacency matrix where N is the number of sentences.
|
||||
Each entry (i,j) represents the normalized similarity between sentences i and j.
|
||||
This matrix is returned to the frontend, which raises it to a power and computes
|
||||
the final salience scores via random walk simulation.
|
||||
"""
|
||||
sentences, sentence_ranges = get_sentences(source_text)
|
||||
adjacency = text_rank(sentences, model_name)
|
||||
return sentence_ranges, adjacency
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Unused/Debugging Code
|
||||
# =============================================================================
|
||||
|
||||
def terminal_distr(adjacency, initial=None):
|
||||
sample = initial if initial is not None else torch.full((adjacency.shape[0],), 1.)
|
||||
scores = sample.matmul(torch.matrix_power(adjacency, 10)).numpy().tolist()
|
||||
return scores
|
||||
|
||||
def extract(source_text, model_name='all-mpnet-base-v2'):
|
||||
sentences, sentence_ranges = get_sentences(source_text)
|
||||
adjacency = text_rank(sentences, model_name)
|
||||
return sentence_ranges, adjacency
|
||||
|
||||
def get_results(sentences, adjacency):
|
||||
scores = terminal_distr(adjacency)
|
||||
for score, sentence in sorted(zip(scores, sentences), key=lambda xs: xs[0]):
|
||||
|
|
|
|||
14
api/uv.lock
generated
14
api/uv.lock
generated
|
|
@ -142,6 +142,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b", size = 96112 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-cors"
|
||||
version = "4.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/41/89ea5af8b9d647036237c528abb2fdf8bb10b23b3f750e8e2da07873b270/flask_cors-4.0.2.tar.gz", hash = "sha256:493b98e2d1e2f1a4720a7af25693ef2fe32fbafec09a2f72c59f3e475eda61d2", size = 30954 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/60/e941089faf4f50f2e0231d7f7af69308616a37e99da3ec75df60b8809db7/Flask_Cors-4.0.2-py2.py3-none-any.whl", hash = "sha256:38364faf1a7a5d0a55bd1d2e2f83ee9e359039182f5e6a029557e1f56d92c09a", size = 14467 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsspec"
|
||||
version = "2025.10.0"
|
||||
|
|
@ -789,6 +801,7 @@ version = "0.0.0"
|
|||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "flask-cors" },
|
||||
{ name = "nltk" },
|
||||
{ name = "numpy" },
|
||||
{ name = "sentence-transformers" },
|
||||
|
|
@ -798,6 +811,7 @@ dependencies = [
|
|||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "flask", specifier = ">=2.3.2,<3.0.0" },
|
||||
{ name = "flask-cors", specifier = ">=4.0.0,<5.0.0" },
|
||||
{ name = "nltk", specifier = ">=3.8.1,<4.0.0" },
|
||||
{ name = "numpy", specifier = ">=1.25.0,<2.0.0" },
|
||||
{ name = "sentence-transformers", specifier = ">=2.2.2,<3.0.0" },
|
||||
|
|
|
|||
1
frontend/.env.development
Normal file
1
frontend/.env.development
Normal file
|
|
@ -0,0 +1 @@
|
|||
VITE_API_BASE_URL=http://127.0.0.1:5000
|
||||
1
frontend/.env.production
Normal file
1
frontend/.env.production
Normal file
|
|
@ -0,0 +1 @@
|
|||
VITE_API_BASE_URL=/p/salience-editor/api
|
||||
704
frontend/package-lock.json
generated
704
frontend/package-lock.json
generated
|
|
@ -6,23 +6,28 @@
|
|||
"": {
|
||||
"name": "my-qwik-empty-starter",
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"mathjs": "^15.0.0",
|
||||
"prosemirror-commands": "^1.7.1",
|
||||
"prosemirror-keymap": "^1.2.3",
|
||||
"prosemirror-model": "^1.25.4",
|
||||
"prosemirror-schema-basic": "^1.2.4",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-transform": "^1.10.4",
|
||||
"prosemirror-view": "^1.41.3"
|
||||
"prosemirror-view": "^1.41.3",
|
||||
"temml": "^0.11.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@builder.io/qwik": "^1.17.1",
|
||||
"@builder.io/qwik-city": "^1.17.1",
|
||||
"@eslint/js": "latest",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@types/node": "20.19.0",
|
||||
"eslint": "9.32.0",
|
||||
"eslint-plugin-qwik": "^1.17.1",
|
||||
"globals": "16.4.0",
|
||||
"prettier": "3.6.2",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"typescript": "5.4.5",
|
||||
"typescript-eslint": "8.38.0",
|
||||
"typescript-plugin-css-modules": "latest",
|
||||
|
|
@ -1203,6 +1208,56 @@
|
|||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdx-js/mdx": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz",
|
||||
|
|
@ -1940,6 +1995,303 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
|
||||
"integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.6.1",
|
||||
"lightningcss": "1.30.2",
|
||||
"magic-string": "^0.30.19",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz",
|
||||
"integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.16",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.16",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.16",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.16",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.16",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.16",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz",
|
||||
"integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz",
|
||||
"integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz",
|
||||
"integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz",
|
||||
"integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz",
|
||||
"integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz",
|
||||
"integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz",
|
||||
"integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz",
|
||||
"integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz",
|
||||
"integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz",
|
||||
"integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
"@emnapi/runtime",
|
||||
"@tybys/wasm-util",
|
||||
"@emnapi/wasi-threads",
|
||||
"tslib"
|
||||
],
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.0.7",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz",
|
||||
"integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz",
|
||||
"integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz",
|
||||
"integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.1.16",
|
||||
"@tailwindcss/oxide": "4.1.16",
|
||||
"tailwindcss": "4.1.16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
}
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
|
|
@ -3325,7 +3677,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
|
|
@ -3637,6 +3988,20 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
|
|
@ -4574,8 +4939,7 @@
|
|||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
"version": "1.4.0",
|
||||
|
|
@ -5351,6 +5715,16 @@
|
|||
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
|
|
@ -5487,6 +5861,277 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.30.2",
|
||||
"lightningcss-darwin-arm64": "1.30.2",
|
||||
"lightningcss-darwin-x64": "1.30.2",
|
||||
"lightningcss-freebsd-x64": "1.30.2",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||
"lightningcss-linux-x64-musl": "1.30.2",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss/node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||
|
|
@ -5538,6 +6183,16 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
|
|
@ -6997,6 +7652,17 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-keymap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||
|
|
@ -7942,6 +8608,35 @@
|
|||
"url": "https://opencollective.com/svgo"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
|
||||
"integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/temml": {
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/temml/-/temml-0.11.11.tgz",
|
||||
"integrity": "sha512-Z/Ihgwad+ges0ez6+KmKWZ3o4BYbP6aZ/cU94cVtN+DwxwqxjHgcF4Z6cb9jLkKN+aU7uni165HsIxLHs5/TqA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
|
|
@ -8580,7 +9275,6 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/valibot": {
|
||||
|
|
|
|||
|
|
@ -26,11 +26,13 @@
|
|||
"@builder.io/qwik": "^1.17.1",
|
||||
"@builder.io/qwik-city": "^1.17.1",
|
||||
"@eslint/js": "latest",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@types/node": "20.19.0",
|
||||
"eslint": "9.32.0",
|
||||
"eslint-plugin-qwik": "^1.17.1",
|
||||
"globals": "16.4.0",
|
||||
"prettier": "3.6.2",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"typescript": "5.4.5",
|
||||
"typescript-eslint": "8.38.0",
|
||||
"typescript-plugin-css-modules": "latest",
|
||||
|
|
@ -39,12 +41,15 @@
|
|||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"mathjs": "^15.0.0",
|
||||
"prosemirror-commands": "^1.7.1",
|
||||
"prosemirror-keymap": "^1.2.3",
|
||||
"prosemirror-model": "^1.25.4",
|
||||
"prosemirror-schema-basic": "^1.2.4",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-transform": "^1.10.4",
|
||||
"prosemirror-view": "^1.41.3"
|
||||
"prosemirror-view": "^1.41.3",
|
||||
"temml": "^0.11.11"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
91
frontend/src/assets/example-doc-1.txt
Normal file
91
frontend/src/assets/example-doc-1.txt
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
Social reality might not be a video game, but there’s no point trying to imagine that. Crass realism obscures the rules. Besides, society converges upon a video game – or immersive ludic simulation – even if it isn’t one already. Such gamification is a trend to note. It has multiple drivers.
|
||||
|
||||
As games get more convincing, they increasingly set the default perceptual frame. In technologically-advanced societies, game-like systems are becoming the obvious model for self-understanding. The reception for stories with this slant continuously improves. Even scientific theorizing is drawn to them. The topic might seem less than serious, even definitively so, but ultimately it isn’t. Alternatively, it might be said that there is a non-seriousness more serious than seriousness itself. Everything will be gamified.
|
||||
|
||||
In the epoch of WMD deterrence, unlimited warfare is not allowed to happen. Instead, it is perpetually simulated. Every serious military establishment becomes a set of war-games in process. From the peak of virtual thermonuclear spasm, war-gaming cascades down through the apparatus of conventional war-fighting capability, and then spreads outwards – like a blast-wave – through every civilian forum of institutional planning. Eventually (but already) to have been ‘war-gamed’ just means to have been thought through. A war-game is less serious than a war, but it’s the most serious way to process things when war is off the table. It’s also – from its inception – the way to keep war off the table. Si vis pacem, para bellum, which means playing it out.
|
||||
|
||||
That everything would be gamified was decided during the pre-history of computing, at the latest. The potential to simulate anything, which is only to say emerging artificial intelligence, leaves nothing that cannot be folded into a game, given time.
|
||||
|
||||
In their take-off phase, at least, machines demand strict rules, responding well only to precise instructions. They dissipate fog or, more precisely, motivate its dissipation. The world adjusts to machine intelligence by sharpening its definition. Formalization acquires precise practical criteria.
|
||||
|
||||
Anything that trains an AI has to function as a game. This is because playing games is the only thing AI can ever do. For synthetic intelligence to be applied to a problem, of any kind, it has to be gamified. Then strategies can be pursued, in strict compliance with rules, to maximize success. Optimization games are the only kind that exist, and inversely.
|
||||
|
||||
While games are made, or adopted, for AI to play in, games incorporate AIs into themselves, as components. Simply making games that work requires computer game companies to nurture a semi-independent machine intelligence lineage of their own. Playing against AIs, and also alongside them, is ever increasingly what gamers do. This is what the ‘single player’ option abbreviates, most obviously. The antisocial path stimulates nonlinearity on the side of the machine. Machine intelligence escalation twists into an ever tighter loop, continually intensifying, as it plays games against itself, and against anyone else who wanders in to challenge it.
|
||||
|
||||
The games that are relentlessly improving – the kind ‘gamers’ play – are competition for society. They provide an alternative to traditional modes of social involvement. Japanese ‘otaku’ pioneered these paths of departure. Wherever technology crests, the world follows them. Advance tends to exit.
|
||||
|
||||
‘Incel’ – or ‘involuntary celibate’ – is in some ways a misleading term for what is happening here. The condition of fundamental social alienation described is no more ‘involuntary’ than any other opt-out. The ‘incel gamer’ no longer finds the most basic of all traditional social relations worth it. There are better games. The revealed preference is evident regardless of what might be said. They grasp games as a way to leave.
|
||||
|
||||
At the same time, the PUAs – or ‘pick-up artists’ – have been pulling everything apart from the opposite direction. If they have a bible it is Neil Strauss’s The Game. Rather than abandoning mating for games, the PUAs gamify mating.
|
||||
|
||||
Turning it into a game is the first step to becoming good at it. In the same way, war is ‘the game of princes’. Everything is a game to those who are good at it, and as a condition of them coming to be good at it. This is the serious non-seriousness previously touched upon. Excellence has ludic foundations. Play or be played, as it is cynically said.
|
||||
|
||||
How could it not become ever more obvious that ‘Gamergate’ had to happen? If non-Wokeness in the gaming industry had never been an issue, it would be a sign that nothing of importance was taking place there. In reality, it could not be left alone because it was destined to eat everything. The topic was seriously non-serious, as the GameStop short-squeeze was more recently.
|
||||
|
||||
Good or well-constructed games have a number of characteristic features.
|
||||
|
||||
Firstly, they can only be played by the rules. Cheating is forbidden less than it is made impossible. Physics is like this. It proscribes nothing that can be done (as Crowley notoriously noticed). Rules that can be broken are a failure of game design. The more impractical it is to cheat, the better the game.
|
||||
|
||||
Secondly, they have an implicit meta-rule that strictly prohibits changing the rules. To change the rules is to invent a new game, which cannot be done during play. Different games, with different rules, coexist simultaneously, rather than replacing each other successively.
|
||||
|
||||
Thirdly, rule sets permit outcomes, without ever dictating them. Rules and strategies are mutually independent. Strategies compete within the rules, rather than over them. Strategic modification of rules, or the adaptation of rules to strategy, is essentially corrupt.
|
||||
|
||||
Fourthly, each is fully enveloped by some consistent incentive structure. This renders success and failure unambiguous, grading performance. The players always know how it went.
|
||||
|
||||
The ‘games’ favored by game theorists, such as variants of the prisoner’s dilemma, compose a small subset of such well-constructed games. They cannot be transcended by cheating. Game modification is never a permitted move. They permit no legislative power. Each has a single reward dimension.
|
||||
|
||||
The breadth of application suggests these constraints are not difficult to meet. It might even seem that any alternative to a well-constructed game is anomalous in its degeneracy.
|
||||
|
||||
To be a progressive is to be in favor of changing the rules. There is one ‘arc of history’ and it is made of reforms. Old rules and structures of oppression are considered broadly identical.
|
||||
|
||||
A conservative is against changing the rules. If they are changed, they stay changed, because changing them back would still count as change. Thus the much derided function of conservatism as anchor for the progressive ratchet.
|
||||
|
||||
A reactionary holds that the rules should never have been changed. Reaction would delight in restoring old rules, were it ever in a position to do so. It never is, and will never be.
|
||||
|
||||
A neoreactionary accepts experimental variation in rules only when rule sets are multiplied. New rules are to be tolerated only alongside, in addition to, and as a concurrent alternative to old rules. They are legitimated only by hard forks. Anything else is progress, which is in all cases misfortune.
|
||||
|
||||
Progress is reform without schism. While wrapping itself in the mantle of science, it incarnates a drastic violation of scientific method. Positive or negative characterizations of ‘progressive experiments’ are equally misleading. Progressive change is not experimental, but rather something closer to the opposite. It substitutes for testing, and disdains controls. Synchronic comparison is deliberately suppressed, and the more thorough the suppression the more progressive it is. Multiplication without difference is bad, but difference without multiplication is worse.
|
||||
|
||||
In a corrupt society, or bad social game, the ruling class makes rules. There is nothing natural about this, regardless of what we are told. It is only in the wake of a radical socio-cultural calamity that it happens.
|
||||
|
||||
In any well-constructed game, winning is entirely distinct from re-writing the rules. For instance, a speculative investor – however successful – does not modify the functioning of the stock market, any more than a chess master takes advantage of each victory to change the way pieces move.
|
||||
|
||||
Capitalism, as a game, works well when businesses follow economic rules they have no role in formulating. Even in the political sphere, comparatively stable constitutional principles and norms are expected to conserve themselves resiliently through vicissitudes of party conflict. This point might confidently be strengthened. Invulnerability of political rules-of-the-game to party fortune is regime stability. The contrary condition, in which party dominance overwhelms political rules and permits the dictation of new ones, defines revolution. Competition within rules is politics, but competition to set rules is war. When politics seems more like war than it used to, this is why.
|
||||
|
||||
The common law tradition permits no legislation. Laws are discovered, never made. The notion of law-making is abominable, and inconsistent with the existence of a free people. According to the only truly English position, legislation is always and essentially tyranny.
|
||||
|
||||
Optimally, the rule of law is a pleonasm. It means only that the rules rule. Nothing could be more inevitable.
|
||||
|
||||
‘Algorithmic governance’ says roughly the same. Yet under conditions of fundamental social corruption the ‘rule of law’ appears closer to an oxymoron. Is it not always men who in fact rule, with rules as their instrument? If so, formal procedure is mostly mystique. Yet this question is itself an index of decadence. Only when a game is already broken does it appear so lacking in authoritative constraint.
|
||||
|
||||
America is a game so badly broken the world is positively awe-struck by it. Its hegemony ensures that everyone has to care. Most of the planet finds itself sucked into a game whose formal rule set is a chaotic cancerous mess.
|
||||
|
||||
When America had a frontier, it was a land of real experiments. New games of all kinds were explored, in parallel. The national heritage of schismatic religion meant different rules applied in different places. From the mid- to late-Nineteenth Century, hardening of the Union and the closing of the frontier brought religious, moral, and political consolidation. American experiments entered their twilight, and The American Experiment was celebrated, integrally, which was no experiment at all, but only progress.
|
||||
|
||||
‘Never change the rules’ is an example of a good meta-rule. What, then, exemplifies a bad one? ‘We should all be playing the same game’ is probably the very worst. At least, nothing more sinister can easily be conceived.
|
||||
|
||||
We don’t like the same games. More particularly, we don’t all like the kind of domination game that requires everyone to play the same game, even if some like it a lot. The ‘game industry’ has an abundance of practical evidence on ludic preference diversity, far exceeding what is required to make the basic point. We want to play different games is the basic point. Despite its overwhelming obviousness, getting it installed as a default is surprisingly difficult. In part, this is Social Domination game-play at work.
|
||||
|
||||
There are people who dislike chess. There are many more who don’t like it enough to play it continuously, and exclusively. Chess, nevertheless, is a well-constructed game. No one is disgraced by their dedication to it.
|
||||
|
||||
Social Domination is a contender for the worst-constructed game in history. “Let’s keep changing the rules until everybody likes it,” it suggests tacitly. It simultaneously makes other suggestions which directly contradict this, but never to the point of ensuring its retraction. As if this were not already bad enough, it also mandates universal cheating. Its rules are so numerous, unstable, and poorly-formulated that they are both theoretically and practically unintelligible. The latitude with which rule-violations are to be avoided or penalized has become a strategic consideration. Players in weak positions have to scrupulously avoid gross rule-violations and are increasingly terrorized by trivial, absurd, and informal norms. Players in strong positions get to ignore any rules they don’t like.
|
||||
|
||||
The best Social Domination players get to decide whether to permit opt outs from Social Domination. The incentive effects here are entirely predictable. However much you hate the game, you have to win it to escape. Those who like it are far more likely to do well at it. On the rare occasions when those who don’t like it do well, they suddenly find they like it more than they had thought, or have invested too much in it to quit. To escape it means fighting it, which means playing it, which means investing in it. Getting out involves putting people into a position from which they can get you out, and that position turns out to be a lot more comfortable than either getting out, or letting anyone else out. These dynamics are clear to everyone.
|
||||
|
||||
As it all becomes ever more obvious, cynicism explodes. No one is any longer really fooled by the thinly-stretched, saccharine, hysterical idealism. It’s all power and who-whom, as the practitioners of Cultural Revolution are the first to admit. “We’re fucking you, and we get to call it good, because we’re winning, and you’re not.” That’s the whole of it. For anyone who thinks Social Domination is a great game to play, it makes more sense than it ever has. There are many such people. They’re not going away.
|
||||
|
||||
“Is it time yet?”
|
||||
|
||||
“It’s a bit later actually.”
|
||||
|
||||
“It’s a bit later than now? Or now’s a bit later than it?”
|
||||
|
||||
It’s time to war-game getting the hell out, and away from them. The technological platforms for it are almost in place. Begin to use them, and they’ll arrive faster. It’s all been set up in a way that can’t be stopped. The games industry is the template.
|
||||
|
||||
Any exit ramp that looks serious is fake. Social Domination manages serious threats easily, making them actually non-serious. Such ‘challenges’ fall under its rules, dialectically, and merely make it bigger. There’s no way to seriously oppose it without playing into it.
|
||||
|
||||
Any real exit has to be seriously non-serious. Game it out. Play another, different game on the side, shifting everything steadily to the side. Migrate intelligence-capital onto a million ludic frontiers, where exit hatches. No one will take it seriously until it’s too late.
|
||||
|
||||
It’s getting ever easier to try things out inside games. Any kind of plotting that doesn’t take this route will soon seem obsolete.
|
||||
|
||||
The means of simulation do not need to be seized, but they do need to be proliferated. Other frontiers will open, but none so soon.
|
||||
284
frontend/src/components/debug-panel/debug-panel.tsx
Normal file
284
frontend/src/components/debug-panel/debug-panel.tsx
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import { component$, useComputed$, $, type Signal } from "@builder.io/qwik";
|
||||
import {
|
||||
calculateRawScores,
|
||||
autoSolveParameters,
|
||||
type SalienceParameters,
|
||||
} from "~/utils/autosolver";
|
||||
|
||||
type DebugRow = {
|
||||
index: number;
|
||||
rawScore: number;
|
||||
exponentialSpread: number;
|
||||
shiftedDown: number;
|
||||
clamped: number;
|
||||
}
|
||||
|
||||
interface DebugPanelProps {
|
||||
isOpen: Signal<boolean>;
|
||||
parameters: SalienceParameters;
|
||||
adjacencyMatrix: Signal<number[][] | null>;
|
||||
}
|
||||
|
||||
export const DebugPanel = component$<DebugPanelProps>(
|
||||
({ isOpen, parameters, adjacencyMatrix }) => {
|
||||
// Calculate debug rows from adjacency matrix whenever it or parameters change
|
||||
const debugRows = useComputed$(() => {
|
||||
if (!adjacencyMatrix.value) return [];
|
||||
|
||||
const rawScores = calculateRawScores(adjacencyMatrix.value, parameters.randomWalkLength);
|
||||
|
||||
const rows: DebugRow[] = rawScores.map((rawScore, i) => {
|
||||
// Step 1: Exponential spread
|
||||
const exponentialSpread = rawScore ** parameters.exponent;
|
||||
// Step 2: Shift down
|
||||
const shiftedDown = exponentialSpread - parameters.threshold;
|
||||
// Step 3: Clamp to [0, 1]
|
||||
const clamped = Math.max(0, Math.min(1, shiftedDown));
|
||||
|
||||
return {
|
||||
index: i,
|
||||
rawScore,
|
||||
exponentialSpread,
|
||||
shiftedDown,
|
||||
clamped,
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by exponentialSpread column (descending)
|
||||
return [...rows].sort((a, b) => b.exponentialSpread - a.exponentialSpread);
|
||||
});
|
||||
|
||||
// Calculate statistics from debug rows
|
||||
const statistics = useComputed$(() => {
|
||||
if (debugRows.value.length === 0) return null;
|
||||
|
||||
const rawScores = debugRows.value.map((r) => r.rawScore);
|
||||
const spreadScores = debugRows.value.map((r) => r.exponentialSpread);
|
||||
const shiftedScores = debugRows.value.map((r) => r.shiftedDown);
|
||||
const clampedScores = debugRows.value.map((r) => r.clamped);
|
||||
|
||||
const rawMin = Math.min(...rawScores);
|
||||
const rawMax = Math.max(...rawScores);
|
||||
const rawRange = rawMax - rawMin;
|
||||
|
||||
const spreadMin = Math.min(...spreadScores);
|
||||
const spreadMax = Math.max(...spreadScores);
|
||||
const spreadRange = spreadMax - spreadMin;
|
||||
|
||||
const shiftedMin = Math.min(...shiftedScores);
|
||||
const shiftedMax = Math.max(...shiftedScores);
|
||||
const shiftedRange = shiftedMax - shiftedMin;
|
||||
|
||||
const clampedMin = Math.min(...clampedScores);
|
||||
const clampedMax = Math.max(...clampedScores);
|
||||
const clampedRange = clampedMax - clampedMin;
|
||||
const zeroCount = clampedScores.filter((s) => s === 0).length;
|
||||
|
||||
return {
|
||||
rawMin,
|
||||
rawMax,
|
||||
rawRange,
|
||||
spreadMin,
|
||||
spreadMax,
|
||||
spreadRange,
|
||||
shiftedMin,
|
||||
shiftedMax,
|
||||
shiftedRange,
|
||||
clampedMin,
|
||||
clampedMax,
|
||||
clampedRange,
|
||||
zeroCount,
|
||||
total: clampedScores.length,
|
||||
};
|
||||
});
|
||||
|
||||
// Auto-solve handler
|
||||
const handleAutoSolve = $(() => {
|
||||
if (!adjacencyMatrix.value) return;
|
||||
|
||||
const solved = autoSolveParameters(adjacencyMatrix.value, parameters);
|
||||
|
||||
if (solved.exponent !== undefined) {
|
||||
parameters.exponent = solved.exponent;
|
||||
}
|
||||
if (solved.threshold !== undefined) {
|
||||
parameters.threshold = solved.threshold;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Debug panel toggle button */}
|
||||
<button
|
||||
class="fixed top-4 right-4 w-10 h-10 rounded-full border border-gray-300 bg-white cursor-pointer text-lg flex items-center justify-center shadow-md hover:bg-gray-50 hover:shadow-lg transition-all z-[1000]"
|
||||
onClick$={() => (isOpen.value = !isOpen.value)}
|
||||
title={isOpen.value ? "Close debug panel" : "Open debug panel"}
|
||||
>
|
||||
{isOpen.value ? "✕" : "🐛"}
|
||||
</button>
|
||||
|
||||
{/* Debug panel */}
|
||||
{isOpen.value && (
|
||||
<div class="fixed top-0 right-0 bottom-0 w-[500px] bg-white border-l border-gray-300 shadow-lg flex flex-col z-[999]">
|
||||
<h2 class="m-0 px-5 py-4 text-lg border-b border-gray-200 bg-gray-50">
|
||||
Debug: Salience Score Breakdown
|
||||
</h2>
|
||||
|
||||
{/* Controls */}
|
||||
<div class="px-5 py-3 border-b border-gray-200 bg-gray-50">
|
||||
<div class="flex gap-4 items-center text-xs mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-gray-700 font-semibold">Exponent:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.exponent}
|
||||
onInput$={(e) => {
|
||||
parameters.exponent =
|
||||
parseFloat((e.target as HTMLInputElement).value) || 1;
|
||||
}}
|
||||
class="w-16 px-2 py-1 border border-gray-300 rounded"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-gray-700 font-semibold">Threshold:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.threshold}
|
||||
onInput$={(e) => {
|
||||
parameters.threshold =
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0;
|
||||
}}
|
||||
class="w-16 px-2 py-1 border border-gray-300 rounded"
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-gray-700 font-semibold">Walk Length:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.randomWalkLength}
|
||||
onInput$={(e) => {
|
||||
parameters.randomWalkLength =
|
||||
parseInt((e.target as HTMLInputElement).value) || 1;
|
||||
}}
|
||||
class="w-16 px-2 py-1 border border-gray-300 rounded"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick$={handleAutoSolve}
|
||||
class="px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Run solver
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Statistics section */}
|
||||
{statistics.value && (
|
||||
<div class="px-5 py-2.5 bg-blue-50 border-b border-blue-200 text-xs">
|
||||
<div class="font-semibold mb-1.5 text-blue-900">
|
||||
Statistics
|
||||
</div>
|
||||
<div class="space-y-1 font-mono text-[11px]">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Raw Score:</span>
|
||||
<span class="font-semibold">
|
||||
{statistics.value.rawMin.toFixed(3)} -{" "}
|
||||
{statistics.value.rawMax.toFixed(3)} (Δ{" "}
|
||||
{statistics.value.rawRange.toFixed(3)})
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">
|
||||
1) Exponential spread (x^{parameters.exponent}):
|
||||
</span>
|
||||
<span class="font-semibold">
|
||||
{statistics.value.spreadMin.toFixed(3)} -{" "}
|
||||
{statistics.value.spreadMax.toFixed(3)} (Δ{" "}
|
||||
{statistics.value.spreadRange.toFixed(3)})
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">
|
||||
2) Shift down (- {parameters.threshold}):
|
||||
</span>
|
||||
<span class="font-semibold">
|
||||
{statistics.value.shiftedMin.toFixed(3)} -{" "}
|
||||
{statistics.value.shiftedMax.toFixed(3)} (Δ{" "}
|
||||
{statistics.value.shiftedRange.toFixed(3)})
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">
|
||||
3) Clamp to [0, 1]:
|
||||
</span>
|
||||
<span class="font-semibold">
|
||||
{statistics.value.clampedMin.toFixed(3)} -{" "}
|
||||
{statistics.value.clampedMax.toFixed(3)} (Δ{" "}
|
||||
{statistics.value.clampedRange.toFixed(3)})
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">└─ Zero count:</span>
|
||||
<span class="font-semibold">
|
||||
{statistics.value.zeroCount} / {statistics.value.total} (
|
||||
{((statistics.value.zeroCount / statistics.value.total) * 100).toFixed(1)}
|
||||
%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<table class="w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 bg-gray-100 z-10">
|
||||
<tr>
|
||||
<th class="px-3 py-2.5 text-left font-semibold border-b-2 border-gray-300 text-gray-700">
|
||||
Index
|
||||
</th>
|
||||
<th class="px-3 py-2.5 text-left font-semibold border-b-2 border-gray-300 text-gray-700">
|
||||
x
|
||||
</th>
|
||||
<th class="px-3 py-2.5 text-left font-semibold border-b-2 border-gray-300 text-gray-700">
|
||||
x<sup>{parameters.exponent}</sup>
|
||||
</th>
|
||||
<th class="px-3 py-2.5 text-left font-semibold border-b-2 border-gray-300 text-gray-700">
|
||||
x<sup>{parameters.exponent}</sup> - {parameters.threshold}
|
||||
</th>
|
||||
<th class="px-3 py-2.5 text-left font-semibold border-b-2 border-gray-300 text-gray-700">
|
||||
Final
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{debugRows.value.map((row) => (
|
||||
<tr key={row.index} class="hover:bg-gray-50">
|
||||
<td class="px-3 py-2 border-b border-gray-100 font-mono">
|
||||
{row.index}
|
||||
</td>
|
||||
<td class="px-3 py-2 border-b border-gray-100 font-mono">
|
||||
{row.rawScore.toFixed(6)}
|
||||
</td>
|
||||
<td class="px-3 py-2 border-b border-gray-100 font-mono">
|
||||
{row.exponentialSpread.toFixed(6)}
|
||||
</td>
|
||||
<td class="px-3 py-2 border-b border-gray-100 font-mono">
|
||||
{row.shiftedDown.toFixed(6)}
|
||||
</td>
|
||||
<td class="px-3 py-2 border-b border-gray-100 font-mono">
|
||||
{row.clamped.toFixed(6)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
20
frontend/src/components/math/math.tsx
Normal file
20
frontend/src/components/math/math.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import temml from "temml";
|
||||
|
||||
interface MathProps {
|
||||
tex: string;
|
||||
display?: boolean;
|
||||
}
|
||||
|
||||
export const Math = component$<MathProps>(({ tex, display = false }) => {
|
||||
const mathml = temml.renderToString(tex, {
|
||||
displayMode: display,
|
||||
});
|
||||
|
||||
return (
|
||||
<span
|
||||
class={display ? "math-display" : "math-inline"}
|
||||
dangerouslySetInnerHTML={mathml}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@import "temml/dist/Temml-Local.css";
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
background-color: #f5f5ee;
|
||||
}
|
||||
.container {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 15px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
margin: 15px 0 0 0;
|
||||
color: #000;
|
||||
}
|
||||
h1 .subtitle {
|
||||
display: block;
|
||||
font-size: 0.7em;
|
||||
font-weight: normal;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
h1 .about-link {
|
||||
display: block;
|
||||
font-size: 0.6em;
|
||||
font-weight: normal;
|
||||
color: #1565c0;
|
||||
text-decoration: none;
|
||||
margin-top: 8px;
|
||||
}
|
||||
h1 .about-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.controls {
|
||||
margin: 15px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.controls label {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
.controls select {
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-badge.clean {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
.status-badge.dirty {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
.status-badge.loading {
|
||||
background-color: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
.editor {
|
||||
color: #4d4d4d;
|
||||
font-size: 15px;
|
||||
line-height: 1.33em;
|
||||
padding: 2em;
|
||||
outline: none;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.editor .sentence {
|
||||
--salience: 1;
|
||||
background-color: rgba(249, 239, 104, var(--salience));
|
||||
}
|
||||
.ProseMirror {
|
||||
outline: none;
|
||||
white-space: pre-wrap;
|
||||
white-space: break-spaces;
|
||||
word-wrap: break-word;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0;
|
||||
}
|
||||
.ProseMirror p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Math component styling */
|
||||
.math-display {
|
||||
margin: 1.5rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.math-inline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
164
frontend/src/routes/about/index.mdx
Normal file
164
frontend/src/routes/about/index.mdx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
---
|
||||
title: How Salience Works
|
||||
---
|
||||
|
||||
import { Math } from "~/components/math/math"
|
||||
|
||||
# How Salience Works
|
||||
|
||||
Salience highlights important sentences by treating your document as a graph where sentences that talk about similar things are connected. We then figure out which sentences are most "central" to the document's themes.
|
||||
|
||||
## Step 1: Break Text into Sentences
|
||||
|
||||
We use NLTK's Punkt tokenizer to split text into sentences. This handles tricky cases where simple punctuation splitting fails:
|
||||
|
||||
*"Dr. Smith earned his Ph.D. in 1995."* ← This is **one** sentence, not three!
|
||||
|
||||
## Step 2: Convert Sentences to Embeddings
|
||||
|
||||
Now we have <Math tex="N" /> sentences. We convert each one into a high-dimensional vector that captures its meaning:
|
||||
|
||||
<Math display tex="\mathbf{E} = \text{model.encode}(\text{sentences}) \in \mathbb{R}^{N \times D}" />
|
||||
|
||||
This gives us an **embeddings matrix** <Math tex="\mathbf{E}" /> where each row is one sentence:
|
||||
|
||||
<Math display tex="\mathbf{E} = \begin{bmatrix} a_1 & a_2 & a_3 & \cdots & a_D \\ b_1 & b_2 & b_3 & \cdots & b_D \\ c_1 & c_2 & c_3 & \cdots & c_D \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ z_1 & z_2 & z_3 & \cdots & z_D \end{bmatrix}" />
|
||||
|
||||
Where:
|
||||
- <Math tex="N" /> = number of sentences (rows)
|
||||
- <Math tex="D" /> = embedding dimension (768 for all-mpnet-base-v2, 1024 for gte-large-en-v1.5)
|
||||
- Each row represents one sentence in semantic space
|
||||
|
||||
## Step 3: Build the Adjacency Matrix
|
||||
|
||||
Now we create a new matrix <Math tex="\mathbf{A}" /> that measures how similar each pair of sentences is. For every pair of sentences <Math tex="i" /> and <Math tex="j" />, we compute:
|
||||
|
||||
<Math display tex="A_{ij} = \frac{\mathbf{e}_i \cdot \mathbf{e}_j}{\|\mathbf{e}_i\| \|\mathbf{e}_j\|}" />
|
||||
|
||||
This is the **cosine similarity** between their embedding vectors. It tells us:
|
||||
- <Math tex="A_{ij} = 1" /> means sentences are identical in meaning
|
||||
- <Math tex="A_{ij} = 0" /> means sentences are unrelated
|
||||
- <Math tex="A_{ij} = -1" /> means sentences are opposite in meaning
|
||||
|
||||
The result is an <Math tex="N \times N" /> **adjacency matrix** where <Math tex="A_{ij}" /> represents how strongly sentence <Math tex="i" /> is connected to sentence <Math tex="j" />.
|
||||
|
||||
## Step 4: Clean Up the Graph
|
||||
|
||||
We make two adjustments to the adjacency matrix to get a cleaner graph:
|
||||
|
||||
1. **Remove self-loops:** Set diagonal to zero (<Math tex="A_{ii} = 0" />)
|
||||
- A sentence shouldn't vote for its own importance
|
||||
|
||||
2. **Remove negative edges:** Set <Math tex="A_{ij} = \max(0, A_{ij})" />
|
||||
- Sentences with opposite meanings get disconnected
|
||||
|
||||
**Important assumption:** This assumes your document has a coherent main idea and that sentences are generally on-topic. We're betting that the topic with the most "semantic mass" is the *correct* topic.
|
||||
|
||||
**Where this breaks down:**
|
||||
- **Dialectical essays** that deliberately contrast opposing viewpoints
|
||||
- **Documents heavy with quotes** that argue against something
|
||||
- **Debate transcripts** where both sides are equally important
|
||||
- **Critical analysis** that spends significant time explaining a position before refuting it
|
||||
|
||||
For example: "Nuclear power is dangerous. Critics say it causes meltdowns. However, modern reactors are actually very safe."
|
||||
|
||||
The algorithm might highlight the criticism because multiple sentences cluster around "danger", even though the document's actual position is pro-nuclear. There's nothing inherent in the math that identifies authorial intent vs. quoted opposition.
|
||||
|
||||
**Bottom line:** This technique works well for coherent, single-perspective documents. It can fail when multiple competing viewpoints have similar semantic weight.
|
||||
|
||||
## Step 5: Normalize the Adjacency Matrix
|
||||
|
||||
The idea from **TextRank** is to treat similarity as a graph problem: simulate random walks and see where you're likely to end up. Sentences you frequently visit are important.
|
||||
|
||||
But first, we need to compute the **degree matrix** <Math tex="\mathbf{D}" />. This tells us how "connected" each sentence is:
|
||||
|
||||
<Math display tex="\mathbf{D} = \text{diag}(\mathbf{A} \mathbf{1})" />
|
||||
|
||||
Here's what this means:
|
||||
- <Math tex="\mathbf{A} \mathbf{1}" /> means "sum up each row of <Math tex="\mathbf{A}" />"
|
||||
- For sentence <Math tex="i" />, this gives us <Math tex="d_i = \sum_j A_{ij}" /> (the total similarity to all other sentences)
|
||||
- <Math tex="\text{diag}(...)" /> puts these sums on the diagonal of a matrix
|
||||
|
||||
The result is a diagonal matrix that looks like:
|
||||
|
||||
<Math display tex="\mathbf{D} = \begin{bmatrix} d_1 & 0 & 0 & \cdots & 0 \\ 0 & d_2 & 0 & \cdots & 0 \\ 0 & 0 & d_3 & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \cdots & d_N \end{bmatrix}" />
|
||||
|
||||
**Intuition:** A sentence with high degree (<Math tex="d_i" /> is large) is connected to many other sentences or has strong connections. A sentence with low degree is more isolated.
|
||||
|
||||
Now we use <Math tex="\mathbf{D}" /> to normalize <Math tex="\mathbf{A}" />. There are two approaches:
|
||||
|
||||
Traditional normalization <Math tex="\mathbf{D}^{-1} \mathbf{A}" />:
|
||||
- This creates a row-stochastic matrix (rows sum to 1)
|
||||
- Interpretation: "If I'm at sentence <Math tex="i" />, what's the probability of jumping to sentence <Math tex="j" />?"
|
||||
- This is like a proper Markov chain transition matrix
|
||||
- Used in standard PageRank and TextRank
|
||||
|
||||
Spectral normalization <Math tex="\mathbf{D}^{-1/2} \mathbf{A} \mathbf{D}^{-1/2}" />:
|
||||
- Used in spectral clustering and graph analysis
|
||||
- Symmetry preservation: if A is symmetric (which cosine similarity matrix is), then the normalized version
|
||||
stays symmetric
|
||||
- The eigenvalues are bounded in [-1, 1]
|
||||
- More uniform influence from all neighbors
|
||||
- Better numerical properties for exponentiation
|
||||
|
||||
|
||||
The traditional <Math tex="\mathbf{D}^{-1} \mathbf{A}" /> approach introduces potential node bias and lacks symmetry. Spectral normalization
|
||||
provides a more balanced representation by symmetrizing the adjacency matrix and ensuring more uniform
|
||||
neighbor influence. This method prevents high-degree nodes from dominating the graph's structure, creating a
|
||||
more equitable information propagation mechanism.
|
||||
|
||||
With traditional normalization, sentences with many connections get their influence diluted. A sentence connected to 10 others splits its "voting power" into 10 pieces. A sentence connected to 2 others splits its power into just 2 pieces. This creates a bias against well-connected sentences.
|
||||
|
||||
Spectral normalization treats the graph as **undirected**, which matches how
|
||||
semantic similarity works. Well-connected sentences keep their influence
|
||||
proportional to connectivity. Two sentences that are similar to each other
|
||||
should have equal influence on each other, not asymmetric transition
|
||||
probabilities.
|
||||
|
||||
|
||||
## Step 6: Random Walk Simulation
|
||||
|
||||
We simulate importance propagation by raising the normalized matrix to a power:
|
||||
|
||||
<Math display tex="\mathbf{s} = \mathbf{1}^T \tilde{\mathbf{A}}^k" />
|
||||
|
||||
Where:
|
||||
- <Math tex="\mathbf{1}" /> = vector of ones (start with equal weight on all sentences)
|
||||
- <Math tex="k" /> = random walk length (default: 5)
|
||||
- <Math tex="\mathbf{s}" /> = raw salience scores for each sentence
|
||||
|
||||
**Intuition:** After <Math tex="k" /> steps of random walking through the similarity graph, which sentences have we visited most? Those are the central, important sentences.
|
||||
|
||||
## Step 7: Map Scores to Highlight Colors
|
||||
|
||||
Now we have a vector of raw salience scores from the random walk. Problem: these scores have no physical meaning. Different embedding models produce wildly different ranges:
|
||||
- Model A on Doc 1: `[0.461, 1.231]`
|
||||
- Model B on Doc 2: `[0.892, 1.059]`
|
||||
|
||||
We need to turn this vector of arbitrary numbers into CSS highlight opacities in `[0, 1]`. Here's the reasoning behind creating the remapping function:
|
||||
|
||||
I could do trivial linear scaling - multiply by a constant to get scores into some range like <Math tex="X" /> to <Math tex="X + 2" />. But let's try to make the top sentences stand out more. One trick: exponentiation. Since human perception of brightness is not linear, exponentiation will preserve order but push the top values apart more. It makes the top few sentences really pop out.
|
||||
|
||||
**Building the remapping function**
|
||||
|
||||
Given a salience vector <Math tex="\mathbf{s}" /> with values ranging from <Math tex="\min(\mathbf{s})" /> to <Math tex="\max(\mathbf{s})" />:
|
||||
|
||||
1. **Find an exponent** <Math tex="p" /> such that <Math tex="\max(\mathbf{s}^p) \approx \min(\mathbf{s}^p) + 2" />
|
||||
|
||||
Sure, it takes more work to find the right exponent for our target spread of 2, but that's still easy with a simple solver.
|
||||
|
||||
2. **Find a threshold** <Math tex="\tau" /> such that 50% of the sentences get clamped to zero.
|
||||
|
||||
Since I'm using this for editing documents, I only want to see highlights on roughly half the sentences—the important half.
|
||||
|
||||
The final opacity mapping is:
|
||||
|
||||
<Math display tex="\text{opacity}_i = \text{clamp}\left(s_i^p - \tau, 0, 1\right)" />
|
||||
|
||||
For each document, I use a simple 1D solver to find <Math tex="p" /> and <Math tex="\tau" /> that satisfy these constraints.
|
||||
|
||||
**Final thought:** This last step—converting the output from TextRank into highlight colors—is the weakest part of the system. I have no idea if it's actually correct or whether it even allows meaningful comparison between different embedding models. It works well enough for the intended purpose (quickly seeing which sentences to keep when editing), but the numerical values themselves are essentially arbitrary.
|
||||
|
||||
---
|
||||
|
||||
[← Back to App](/)
|
||||
11
frontend/src/routes/about/layout.tsx
Normal file
11
frontend/src/routes/about/layout.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { component$, Slot } from "@builder.io/qwik";
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<article class="prose prose-slate max-w-none">
|
||||
<Slot />
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,25 +1,468 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import type { DocumentHead } from "@builder.io/qwik-city";
|
||||
import {
|
||||
component$,
|
||||
useSignal,
|
||||
useVisibleTask$,
|
||||
useComputed$,
|
||||
$,
|
||||
noSerialize,
|
||||
useStore,
|
||||
type Signal,
|
||||
type NoSerialize,
|
||||
} from "@builder.io/qwik";
|
||||
import { type DocumentHead, routeLoader$ } from "@builder.io/qwik-city";
|
||||
import { EditorState, Plugin, Transaction } from "prosemirror-state";
|
||||
import { EditorView, Decoration, DecorationSet } from "prosemirror-view";
|
||||
import { Schema, DOMParser, Node as PMNode } from "prosemirror-model";
|
||||
import { schema as basicSchema } from "prosemirror-schema-basic";
|
||||
import { keymap } from "prosemirror-keymap";
|
||||
import { baseKeymap } from "prosemirror-commands";
|
||||
import * as math from "mathjs";
|
||||
import exampleDoc from "~/assets/example-doc-1.txt?raw";
|
||||
import { DebugPanel } from "~/components/debug-panel/debug-panel";
|
||||
import { autoSolveParameters } from "~/utils/autosolver";
|
||||
import "~/global.css";
|
||||
|
||||
// API Configuration
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:5000';
|
||||
|
||||
// Types
|
||||
interface SalienceData {
|
||||
source: string;
|
||||
intervals: [number, number][];
|
||||
adjacency: number[][];
|
||||
}
|
||||
|
||||
interface SentenceDecoration {
|
||||
from: number;
|
||||
to: number;
|
||||
salience: number;
|
||||
}
|
||||
|
||||
interface SalienceParameters {
|
||||
exponent: number;
|
||||
threshold: number;
|
||||
randomWalkLength: number;
|
||||
}
|
||||
|
||||
type SyncState = "clean" | "dirty" | "loading";
|
||||
|
||||
// Create schema with marks
|
||||
const salienceSchema = new Schema({
|
||||
nodes: basicSchema.spec.nodes,
|
||||
marks: basicSchema.spec.marks,
|
||||
});
|
||||
|
||||
// Create decorations from sentence decorations
|
||||
function createDecorations(
|
||||
doc: PMNode,
|
||||
decorations: SentenceDecoration[]
|
||||
): DecorationSet {
|
||||
const decos = decorations.map((deco) => {
|
||||
return Decoration.inline(deco.from, deco.to, {
|
||||
class: "sentence",
|
||||
style: `--salience: ${deco.salience}`,
|
||||
});
|
||||
});
|
||||
return DecorationSet.create(doc, decos);
|
||||
}
|
||||
|
||||
// Plugin to manage salience decorations
|
||||
function saliencePlugin(
|
||||
initialDecorations: SentenceDecoration[]
|
||||
): Plugin<DecorationSet> {
|
||||
return new Plugin<DecorationSet>({
|
||||
state: {
|
||||
init(_, { doc }) {
|
||||
return createDecorations(doc, initialDecorations);
|
||||
},
|
||||
apply(tr, old) {
|
||||
// Get updated decorations from transaction metadata
|
||||
const newDecorations = tr.getMeta("updateDecorations");
|
||||
if (newDecorations) {
|
||||
return createDecorations(tr.doc, newDecorations);
|
||||
}
|
||||
// Map existing decorations through the transaction
|
||||
return old.map(tr.mapping, tr.doc);
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Translate decorations through a transaction
|
||||
function translateDecorations(
|
||||
decorations: SentenceDecoration[],
|
||||
tr: Transaction
|
||||
): SentenceDecoration[] {
|
||||
return decorations
|
||||
.map((deco) => {
|
||||
const from = tr.mapping.map(deco.from);
|
||||
const to = tr.mapping.map(deco.to);
|
||||
// Only keep decorations that still have valid ranges
|
||||
if (from < to) {
|
||||
return { ...deco, from, to };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((deco): deco is SentenceDecoration => deco !== null);
|
||||
}
|
||||
|
||||
// Load initial document
|
||||
export const useInitialDocument = routeLoader$(() => {
|
||||
return exampleDoc;
|
||||
});
|
||||
|
||||
export default component$(() => {
|
||||
const initialDocument = useInitialDocument();
|
||||
const editorRef = useSignal<HTMLDivElement>();
|
||||
const modelSelectRef = useSignal<HTMLSelectElement>();
|
||||
const models = useSignal<string[]>([]);
|
||||
const currentModel = useSignal("all-mpnet-base-v2");
|
||||
const syncState = useSignal<SyncState>("clean");
|
||||
const editorView = useSignal<EditorView | null>(null);
|
||||
const sentenceDecorations = useSignal<SentenceDecoration[]>([]);
|
||||
const salienceData = useSignal<SalienceData | null>(null);
|
||||
const debounceTimer = useSignal<number | null>(null);
|
||||
const pendingRequest = useSignal<AbortController | null>(null);
|
||||
const debugPanelOpen = useSignal(false);
|
||||
const adjacencyMatrix = useSignal<number[][] | null>(null);
|
||||
const autoSolveEnabled = useSignal(true);
|
||||
const parameters = useStore<SalienceParameters>({
|
||||
exponent: 3,
|
||||
threshold: 0.95,
|
||||
randomWalkLength: 5,
|
||||
});
|
||||
|
||||
// Auto-solve parameters when adjacency matrix changes (if enabled)
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(({ track }) => {
|
||||
track(() => adjacencyMatrix.value);
|
||||
track(() => autoSolveEnabled.value);
|
||||
|
||||
if (!adjacencyMatrix.value || !autoSolveEnabled.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const solved = autoSolveParameters(adjacencyMatrix.value, parameters);
|
||||
|
||||
if (solved.exponent !== undefined) {
|
||||
parameters.exponent = solved.exponent;
|
||||
}
|
||||
if (solved.threshold !== undefined) {
|
||||
parameters.threshold = solved.threshold;
|
||||
}
|
||||
});
|
||||
|
||||
// Compute salience scores when adjacency matrix or parameters change
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(({ track }) => {
|
||||
track(() => adjacencyMatrix.value);
|
||||
track(() => parameters.exponent);
|
||||
track(() => parameters.threshold);
|
||||
track(() => parameters.randomWalkLength);
|
||||
|
||||
if (!adjacencyMatrix.value || sentenceDecorations.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute scores using random walk
|
||||
const initial = adjacencyMatrix.value.map(() => 1);
|
||||
const scores = math.multiply(
|
||||
initial,
|
||||
math.pow(adjacencyMatrix.value, parameters.randomWalkLength) as number[][]
|
||||
) as number[];
|
||||
|
||||
// Update salience values in existing decorations
|
||||
sentenceDecorations.value = sentenceDecorations.value.map((deco, i) => {
|
||||
const exponentialSpread = scores[i] ** parameters.exponent;
|
||||
const shiftedDown = exponentialSpread - parameters.threshold;
|
||||
const clamped = Math.max(0, Math.min(1, shiftedDown));
|
||||
|
||||
return {
|
||||
...deco,
|
||||
salience: clamped,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Update editor when decorations change
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(({ track }) => {
|
||||
track(() => sentenceDecorations.value);
|
||||
|
||||
if (!editorView.value || sentenceDecorations.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateTr = editorView.value.state.tr.setMeta(
|
||||
"updateDecorations",
|
||||
sentenceDecorations.value
|
||||
);
|
||||
editorView.value.dispatch(updateTr);
|
||||
});
|
||||
|
||||
// Load available models
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/models`);
|
||||
const data = await res.json();
|
||||
models.value = data;
|
||||
} catch (err) {
|
||||
console.error("Failed to load models:", err);
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(({ track }) => {
|
||||
track(() => editorRef.value);
|
||||
if (!editorRef.value) return;
|
||||
|
||||
// Parse the initial document text into separate paragraphs
|
||||
// Split by single newlines and preserve blank lines as empty paragraphs
|
||||
const paragraphs = initialDocument.value
|
||||
.split('\n')
|
||||
.map(line => {
|
||||
// Create paragraph with text if line has content, otherwise empty paragraph
|
||||
const content = line.length > 0 ? [salienceSchema.text(line)] : [];
|
||||
return salienceSchema.node("paragraph", null, content);
|
||||
});
|
||||
|
||||
const initialDoc = salienceSchema.node("doc", null, paragraphs);
|
||||
|
||||
const state = EditorState.create({
|
||||
schema: salienceSchema,
|
||||
doc: initialDoc,
|
||||
plugins: [
|
||||
saliencePlugin([]),
|
||||
keymap(baseKeymap),
|
||||
],
|
||||
});
|
||||
|
||||
const view = new EditorView(editorRef.value, {
|
||||
state,
|
||||
dispatchTransaction: (tr) => {
|
||||
const newState = view.state.apply(tr);
|
||||
view.updateState(newState);
|
||||
|
||||
// If this was a user edit (not a programmatic update)
|
||||
if (tr.docChanged && !tr.getMeta("fromApi")) {
|
||||
// Translate existing decorations to new positions
|
||||
const translated = translateDecorations(sentenceDecorations.value, tr);
|
||||
sentenceDecorations.value = translated;
|
||||
// The reactive task will update the editor with translated decorations
|
||||
|
||||
// Mark as dirty and schedule API request
|
||||
syncState.value = "dirty";
|
||||
scheduleApiRequest(
|
||||
view,
|
||||
currentModel.value,
|
||||
syncState,
|
||||
sentenceDecorations,
|
||||
salienceData,
|
||||
debounceTimer,
|
||||
pendingRequest,
|
||||
adjacencyMatrix,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
editorView.value = view;
|
||||
|
||||
fetchSalienceData(
|
||||
currentModel.value,
|
||||
view,
|
||||
syncState,
|
||||
sentenceDecorations,
|
||||
salienceData,
|
||||
pendingRequest,
|
||||
adjacencyMatrix
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
view.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
// Handle model change
|
||||
const handleModelChange = $((event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
currentModel.value = target.value;
|
||||
if (editorView.value) {
|
||||
fetchSalienceData(
|
||||
target.value,
|
||||
editorView.value,
|
||||
syncState,
|
||||
sentenceDecorations,
|
||||
salienceData,
|
||||
pendingRequest,
|
||||
adjacencyMatrix,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Hi 👋</h1>
|
||||
<div>
|
||||
Can't wait to see what you build with qwik!
|
||||
<br />
|
||||
Happy coding.
|
||||
<div class="container">
|
||||
<h1>
|
||||
Salience
|
||||
<span class="subtitle">
|
||||
sentence highlights based on their significance to the document
|
||||
</span>
|
||||
<a href="/about" class="about-link">How it works →</a>
|
||||
</h1>
|
||||
<div class="controls">
|
||||
<label for="model-select">Model:</label>
|
||||
<select
|
||||
id="model-select"
|
||||
ref={modelSelectRef}
|
||||
onChange$={handleModelChange}
|
||||
>
|
||||
{models.value.length === 0 ? (
|
||||
<option>Loading...</option>
|
||||
) : (
|
||||
models.value.map((model) => (
|
||||
<option
|
||||
key={model}
|
||||
value={model}
|
||||
selected={model === currentModel.value}
|
||||
>
|
||||
{model}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
<label class="auto-solve-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoSolveEnabled.value}
|
||||
onChange$={(e) => {
|
||||
autoSolveEnabled.value = (e.target as HTMLInputElement).checked;
|
||||
}}
|
||||
/>
|
||||
Auto highlight colors
|
||||
</label>
|
||||
<span class={`status-badge ${syncState.value}`}>
|
||||
{syncState.value === "clean"
|
||||
? "Synchronized"
|
||||
: syncState.value === "dirty"
|
||||
? "Modified"
|
||||
: "Processing..."}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
ref={editorRef}
|
||||
class="editor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DebugPanel
|
||||
isOpen={debugPanelOpen}
|
||||
parameters={parameters}
|
||||
adjacencyMatrix={adjacencyMatrix}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
// Shared function to fetch salience data from API
|
||||
async function fetchSalienceData(
|
||||
model: string,
|
||||
view: EditorView,
|
||||
syncState: Signal<SyncState>,
|
||||
sentenceDecorations: Signal<SentenceDecoration[]>,
|
||||
salienceData: Signal<SalienceData | null>,
|
||||
pendingRequest: Signal<AbortController | null>,
|
||||
adjacencyMatrix: Signal<number[][] | null>,
|
||||
) {
|
||||
// Cancel any pending request
|
||||
if (pendingRequest.value) {
|
||||
pendingRequest.value.abort();
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
pendingRequest.value = controller;
|
||||
syncState.value = "loading";
|
||||
|
||||
// Get current document text
|
||||
const text = view.state.doc.textContent;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/salience?model=${encodeURIComponent(model)}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
body: text,
|
||||
signal: controller.signal,
|
||||
});
|
||||
const data: SalienceData = await res.json();
|
||||
|
||||
salienceData.value = data;
|
||||
adjacencyMatrix.value = data.adjacency;
|
||||
|
||||
// Set decorations with positions and zero salience
|
||||
// The reactive task will compute and update the salience values
|
||||
sentenceDecorations.value = data.intervals.map(([start, end]) => ({
|
||||
from: start,
|
||||
to: end,
|
||||
salience: 0,
|
||||
}));
|
||||
|
||||
syncState.value = "clean";
|
||||
pendingRequest.value = null;
|
||||
} catch (err: any) {
|
||||
if (err.name !== "AbortError") {
|
||||
console.error("Failed to load salience:", err);
|
||||
syncState.value = "dirty";
|
||||
}
|
||||
pendingRequest.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule API request with debouncing
|
||||
function scheduleApiRequest(
|
||||
view: EditorView,
|
||||
model: string,
|
||||
syncState: Signal<SyncState>,
|
||||
sentenceDecorations: Signal<SentenceDecoration[]>,
|
||||
salienceData: Signal<SalienceData | null>,
|
||||
debounceTimer: Signal<number | null>,
|
||||
pendingRequest: Signal<AbortController | null>,
|
||||
adjacencyMatrix: Signal<number[][] | null>,
|
||||
) {
|
||||
// Clear existing timer
|
||||
if (debounceTimer.value !== null) {
|
||||
clearTimeout(debounceTimer.value);
|
||||
}
|
||||
|
||||
// Set new timer
|
||||
debounceTimer.value = window.setTimeout(() => {
|
||||
// Check if we're still dirty (not loading)
|
||||
if (syncState.value === "dirty") {
|
||||
fetchSalienceData(
|
||||
model,
|
||||
view,
|
||||
syncState,
|
||||
sentenceDecorations,
|
||||
salienceData,
|
||||
pendingRequest,
|
||||
adjacencyMatrix
|
||||
);
|
||||
}
|
||||
}, 1000); // 1 second debounce
|
||||
}
|
||||
|
||||
export const head: DocumentHead = {
|
||||
title: "Welcome to Qwik",
|
||||
title: "Salience",
|
||||
meta: [
|
||||
{
|
||||
name: "description",
|
||||
content: "Qwik site description",
|
||||
content: "Sentence highlights based on their significance to the document",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
102
frontend/src/utils/autosolver.ts
Normal file
102
frontend/src/utils/autosolver.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import * as math from "mathjs";
|
||||
|
||||
export interface SalienceParameters {
|
||||
exponent: number;
|
||||
threshold: number;
|
||||
randomWalkLength: number;
|
||||
}
|
||||
|
||||
// Calculate raw scores from adjacency matrix
|
||||
// Why: Random walk simulates importance flow through the document graph
|
||||
// Longer walks capture more global structure
|
||||
export function calculateRawScores(adjacencyMatrix: number[][], randomWalkLength: number): number[] {
|
||||
const initial = adjacencyMatrix.map(() => 1);
|
||||
return math.multiply(
|
||||
initial,
|
||||
math.pow(adjacencyMatrix, randomWalkLength) as number[][]
|
||||
) as number[];
|
||||
}
|
||||
|
||||
// Solve for exponent that spreads the range to target value
|
||||
// Why: We need controlled spread - too narrow and we can't distinguish sentences,
|
||||
// too wide and precision is lost in the subsequent clamping step
|
||||
export function solveExponentialSpread(rawScores: number[], targetRange: number = 2): number {
|
||||
const rawMin = Math.min(...rawScores);
|
||||
const rawMax = Math.max(...rawScores);
|
||||
|
||||
const calculateRange = (exp: number) => {
|
||||
const spreadMin = Math.pow(rawMin, exp);
|
||||
const spreadMax = Math.pow(rawMax, exp);
|
||||
return spreadMax - spreadMin;
|
||||
};
|
||||
|
||||
let exponent = 1.0;
|
||||
let stepSize = 0.5;
|
||||
let lastError: number | null = null;
|
||||
const tolerance = 0.01;
|
||||
const maxIterations = 100;
|
||||
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
const range = calculateRange(exponent);
|
||||
const error = range - targetRange;
|
||||
const absError = Math.abs(error);
|
||||
|
||||
// Stop when close enough - no point wasting cycles on perfection
|
||||
if (absError < tolerance) {
|
||||
break;
|
||||
}
|
||||
|
||||
// We overshot the target, so cut step size in half to avoid oscillating
|
||||
if (lastError !== null && Math.sign(error) !== Math.sign(lastError)) {
|
||||
stepSize *= 0.5;
|
||||
} else {
|
||||
// Far away? Take bigger steps to converge faster
|
||||
// Close? Smaller steps to avoid overshooting
|
||||
if (absError > 2.0) {
|
||||
stepSize *= 1.5;
|
||||
} else if (absError < 0.5) {
|
||||
stepSize *= 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
// Small range needs bigger exponent, large range needs smaller
|
||||
const direction = error < 0 ? 1 : -1;
|
||||
exponent += direction * stepSize;
|
||||
|
||||
// Keep exponent sane - min 0.1 avoids division-like behavior,
|
||||
// max 100 handles even very narrow input ranges
|
||||
exponent = Math.max(0.1, Math.min(100, exponent));
|
||||
|
||||
lastError = error;
|
||||
}
|
||||
|
||||
return Math.round(exponent * 10) / 10;
|
||||
}
|
||||
|
||||
// Solve for threshold that zeros out the bottom half
|
||||
// Why: We want to highlight only the top sentences, not everything
|
||||
// 50% zeros gives us clear signal without losing too much information
|
||||
export function solveShiftDown(spreadScores: number[], targetZeroPercent: number = 0.5): number {
|
||||
// Just find the percentile value - simple and works
|
||||
const sorted = [...spreadScores].sort((a, b) => a - b);
|
||||
const medianThreshold = sorted[Math.floor(sorted.length * targetZeroPercent)];
|
||||
|
||||
return Math.round(medianThreshold * 100) / 100;
|
||||
}
|
||||
|
||||
// Auto-solve parameters from adjacency matrix
|
||||
export function autoSolveParameters(
|
||||
adjacencyMatrix: number[][],
|
||||
currentParameters: SalienceParameters
|
||||
): Partial<SalienceParameters> {
|
||||
const rawScores = calculateRawScores(adjacencyMatrix, currentParameters.randomWalkLength);
|
||||
|
||||
// First spread the range to ~2 so we have room to work with
|
||||
const exponent = solveExponentialSpread(rawScores, 2);
|
||||
|
||||
// Then find where to cut off the bottom 50%
|
||||
const spreadScores = rawScores.map(s => s ** exponent);
|
||||
const threshold = solveShiftDown(spreadScores, 0.5);
|
||||
|
||||
return { exponent, threshold };
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { defineConfig, type UserConfig } from "vite";
|
|||
import { qwikVite } from "@builder.io/qwik/optimizer";
|
||||
import { qwikCity } from "@builder.io/qwik-city/vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import pkg from "./package.json";
|
||||
|
||||
type PkgDep = Record<string, string>;
|
||||
|
|
@ -21,7 +22,8 @@ errorOnDuplicatesPkgDeps(devDependencies, dependencies);
|
|||
*/
|
||||
export default defineConfig(({ command, mode }): UserConfig => {
|
||||
return {
|
||||
plugins: [qwikCity(), qwikVite(), tsconfigPaths({ root: "." })],
|
||||
base: mode === 'production' ? '/p/salience-editor/' : '/',
|
||||
plugins: [qwikCity(), qwikVite(), tsconfigPaths({ root: "." }), tailwindcss()],
|
||||
// This tells Vite which dependencies to pre-build in dev mode.
|
||||
optimizeDeps: {
|
||||
// Put problematic deps that break bundling here, mostly those with binaries.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue