Spaces:
Running
Running
Add bilingual Valen API playground with text and image examples
Browse files- .gitattributes +1 -0
- .gitignore +6 -0
- NOTICE.md +12 -0
- README.md +81 -4
- api.mjs +108 -0
- app.mjs +141 -0
- assets/OFL.txt +192 -0
- assets/dmsans.woff2 +0 -0
- assets/manifest.json +9 -0
- assets/modern-tokens.css +29 -0
- assets/valen-mark.png +3 -0
- index.html +40 -17
- locales.mjs +22 -0
- package.json +8 -0
- style.css +3 -28
- tests/api.test.mjs +59 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/valen-mark.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
.venv/
|
| 4 |
+
.env
|
| 5 |
+
.gradio/
|
| 6 |
+
flagged/
|
NOTICE.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Asset provenance
|
| 2 |
+
|
| 3 |
+
This Space uses a subset of the Valen shared design system, version 0.5.1.
|
| 4 |
+
|
| 5 |
+
- `assets/modern-tokens.css`: unchanged from `design-system/styles/modern-tokens.css`.
|
| 6 |
+
- `assets/valen-mark.png`: unchanged `design-system/assets/brand/valen-mark-white.png`.
|
| 7 |
+
- `assets/dmsans.woff2`: DM Sans font from the shared design system.
|
| 8 |
+
- `assets/OFL.txt`: font license notices, preserved with the font.
|
| 9 |
+
|
| 10 |
+
Valen branding identifies Valen Team and its projects. No trademark rights are
|
| 11 |
+
granted. No additional open-source license is asserted for the application or
|
| 12 |
+
design source. Snapshot hashes are recorded in `assets/manifest.json`.
|
README.md
CHANGED
|
@@ -1,10 +1,87 @@
|
|
| 1 |
---
|
| 2 |
title: Valen API
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Valen API
|
| 3 |
+
emoji: 🌊
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
+
fullWidth: true
|
| 9 |
+
short_description: Valen decisions with text, images and API examples
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Valen API
|
| 13 |
+
|
| 14 |
+
A bilingual, static playground for the [Valen Decisions API](https://valen-team.github.io/docs/valen/api/).
|
| 15 |
+
Use text or images with native **predicate**, **choice**, and **score** questions.
|
| 16 |
+
Results show probability bars, optional confidence, the returned model ID, and raw JSON.
|
| 17 |
+
The Python example follows the current inputs, so you can reproduce a request in your own application.
|
| 18 |
+
|
| 19 |
+
## Architecture
|
| 20 |
+
|
| 21 |
+
```text
|
| 22 |
+
Browser on valen-team-valen-api.static.hf.space
|
| 23 |
+
→ https://api.valen-team.workers.dev/v1/decisions
|
| 24 |
+
→ hosted Valen model
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
The browser sends requests directly to the existing public API. There is no Python
|
| 28 |
+
server, model download, GPU, API key, or build step. The Space does not proxy traffic
|
| 29 |
+
and does not solve network restrictions on `workers.dev`. It shares the website's
|
| 30 |
+
upstream service and limits. HTTP 429 means wait at least a minute; the app does not
|
| 31 |
+
automatically retry or call the model when an example loads.
|
| 32 |
+
|
| 33 |
+
The Worker must allow the exact browser origin:
|
| 34 |
+
|
| 35 |
+
```text
|
| 36 |
+
ALLOWED_ORIGINS=https://valen-team.github.io,https://valen-team-valen-api.static.hf.space
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
Keep the website origin when updating the setting. Do not allow `*` or all of
|
| 40 |
+
`hf.space`. Browsers set Origin automatically. Python/cURL examples include the
|
| 41 |
+
documented `Origin: https://valen-team.github.io` header; this is not a credential.
|
| 42 |
+
|
| 43 |
+
## Run and test
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
python3 -m http.server 7860 --bind 127.0.0.1
|
| 47 |
+
node --test tests/*.test.mjs
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Local preview supports the interface and code generation. Live requests from
|
| 51 |
+
localhost are intentionally not in the production CORS allowlist; test the full
|
| 52 |
+
browser flow on the deployed Space. `?lang=zh` opens the Chinese interface.
|
| 53 |
+
|
| 54 |
+
## API contract
|
| 55 |
+
|
| 56 |
+
- Models: `valen-2b`, `valen-4b`, `valen-0-8b`, `valen-preview-0923`.
|
| 57 |
+
- One question named `q0`, with `temperature: 1.0`.
|
| 58 |
+
- `predicate`: `probability` is the probability the condition is true.
|
| 59 |
+
- `choice`: 2–8 `{value, description}` candidates; response has `choice` and `probabilities`.
|
| 60 |
+
- `score`: 2–8 `{label, description}` levels ordered low to high. The score is the
|
| 61 |
+
probability-weighted **zero-based** level index. Five levels span 0–4.
|
| 62 |
+
- `confidence` is optional and separate from the candidate probability.
|
| 63 |
+
- The response `model_id` is shown independently from the requested model.
|
| 64 |
+
|
| 65 |
+
Questions are limited to 1000 characters, context to 4000, each option to 300.
|
| 66 |
+
JPG/PNG/WebP uploads are limited to 3 MB. Images are resized locally to fit
|
| 67 |
+
1536 × 1536 and encoded as JPEG before submission. The request preview omits the
|
| 68 |
+
base64 bytes; the generated Python example reads a local `image.jpg` instead.
|
| 69 |
+
|
| 70 |
+
## Data
|
| 71 |
+
|
| 72 |
+
Inputs are sent to the API only when **Ask Valen** is pressed. This static app
|
| 73 |
+
does not upload images to a Hugging Face backend, store conversations, write
|
| 74 |
+
local storage, or include analytics. Inputs and results remain in page memory.
|
| 75 |
+
The API and hosting providers may have their own logging and retention policies.
|
| 76 |
+
Requests can be cancelled and time out after two minutes.
|
| 77 |
+
|
| 78 |
+
## Files
|
| 79 |
+
|
| 80 |
+
- `index.html`, `style.css`, `app.mjs`, `locales.mjs`: responsive bilingual UI.
|
| 81 |
+
- `api.mjs`: request validation, response parsing and Python examples.
|
| 82 |
+
- `tests/`: offline contract, error and example-generation tests.
|
| 83 |
+
- `assets/`: pinned Valen design tokens, brand mark and font; see `NOTICE.md`.
|
| 84 |
+
|
| 85 |
+
Upload these files to the static Space `Valen-Team/Valen-API`. Do not upload local
|
| 86 |
+
virtual environments, caches, credentials or `.env` files. Static configuration
|
| 87 |
+
follows the [Hugging Face reference](https://huggingface.co/docs/hub/spaces-config-reference).
|
api.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const ENDPOINT = 'https://api.valen-team.workers.dev/v1/decisions';
|
| 2 |
+
export const MODELS = ['valen-2b', 'valen-4b', 'valen-0-8b', 'valen-preview-0923'];
|
| 3 |
+
|
| 4 |
+
export function buildPayload({model, mode, question, context = '', options = '', image}) {
|
| 5 |
+
if (!MODELS.includes(model) || !['predicate', 'choice', 'score'].includes(mode)) throw new Error('model');
|
| 6 |
+
question = question.trim(); context = context.trim();
|
| 7 |
+
if (!question || question.length > 1000) throw new Error('question');
|
| 8 |
+
if (context.length > 4000) throw new Error('context');
|
| 9 |
+
const q = {name: 'q0', type: mode, instructions: question};
|
| 10 |
+
if (mode !== 'predicate') {
|
| 11 |
+
const lines = options.split('\n').map(s => s.trim()).filter(Boolean);
|
| 12 |
+
if (lines.length < 2 || lines.length > 8 || lines.some(s => s.length > 300)) throw new Error('options');
|
| 13 |
+
if (mode === 'choice') q.choices = lines.map((description, i) => ({value: `option_${i+1}`, description}));
|
| 14 |
+
else q.levels = lines.map((description, i) => ({label: String(i), description}));
|
| 15 |
+
}
|
| 16 |
+
const evidence = context || (image ? 'Answer using only the visible image.' : question);
|
| 17 |
+
if (image && !/^data:image\/jpeg;base64,[A-Za-z0-9+/]+={0,2}$/.test(image)) throw new Error('image');
|
| 18 |
+
const input = image ? [{role: 'user', content: [{type: 'input_text', text: evidence}, {type: 'input_image', image_url: image}]}] : evidence;
|
| 19 |
+
const payload = {model, input, questions: [q], temperature: 1.0};
|
| 20 |
+
if (new TextEncoder().encode(JSON.stringify(payload)).length > 4500000) throw new Error('size');
|
| 21 |
+
return payload;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function previewPayload(payload) {
|
| 25 |
+
const value = structuredClone(payload);
|
| 26 |
+
if (Array.isArray(value.input)) value.input[0].content[1].image_url = '<JPEG data URL omitted from preview>';
|
| 27 |
+
return value;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export function pythonExample(payload) {
|
| 31 |
+
const value = structuredClone(payload);
|
| 32 |
+
const hasImage = Array.isArray(value.input);
|
| 33 |
+
if (hasImage) value.input[0].content[1].image_url = 'IMAGE_DATA_URL';
|
| 34 |
+
const setup = hasImage ? `import base64
|
| 35 |
+
import io
|
| 36 |
+
from PIL import Image, ImageOps
|
| 37 |
+
|
| 38 |
+
# Replace image.jpg with your selected image.
|
| 39 |
+
with Image.open("image.jpg") as image:
|
| 40 |
+
image = ImageOps.exif_transpose(image).convert("RGB")
|
| 41 |
+
image.thumbnail((1536, 1536))
|
| 42 |
+
buffer = io.BytesIO()
|
| 43 |
+
image.save(buffer, format="JPEG", quality=85)
|
| 44 |
+
image_url = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode("ascii")
|
| 45 |
+
|
| 46 |
+
` : '';
|
| 47 |
+
// json.loads safely preserves quotes, Unicode and literal user text in Python.
|
| 48 |
+
const literal = JSON.stringify(JSON.stringify(value)).replaceAll('\u2028', '\\u2028').replaceAll('\u2029', '\\u2029');
|
| 49 |
+
return `# pip install requests${hasImage ? ' Pillow' : ''}
|
| 50 |
+
import json
|
| 51 |
+
import requests
|
| 52 |
+
${setup}
|
| 53 |
+
payload = json.loads(${literal})
|
| 54 |
+
${hasImage ? 'payload["input"][0]["content"][1]["image_url"] = image_url\n' : ''}
|
| 55 |
+
response = requests.post(
|
| 56 |
+
"${ENDPOINT}",
|
| 57 |
+
headers={"Origin": "https://valen-team.github.io"},
|
| 58 |
+
json=payload,
|
| 59 |
+
timeout=120,
|
| 60 |
+
)
|
| 61 |
+
response.raise_for_status()
|
| 62 |
+
result = response.json()
|
| 63 |
+
print(result["answers"][0])
|
| 64 |
+
print("Model ID:", result.get("model_id"))
|
| 65 |
+
`;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
export function parseAnswer(data, payload) {
|
| 69 |
+
const fail = () => {throw new Error('response');};
|
| 70 |
+
const q = payload.questions[0];
|
| 71 |
+
if (!Array.isArray(data?.answers) || data.answers.length !== 1) fail();
|
| 72 |
+
const a = data.answers[0];
|
| 73 |
+
const probability = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;
|
| 74 |
+
if (!a || a.name !== 'q0' || a.type !== q.type || (a.confidence !== undefined && !probability(a.confidence))) fail();
|
| 75 |
+
if (q.type === 'predicate') {
|
| 76 |
+
if (!probability(a.probability)) fail();
|
| 77 |
+
return {title: a.probability >= .5 ? 'yes' : 'no', confidence: a.confidence,
|
| 78 |
+
rows: [{label: 'yes', probability: a.probability}, {label: 'no', probability: 1 - a.probability}]};
|
| 79 |
+
}
|
| 80 |
+
const labels = q.type === 'choice' ? new Map(q.choices.map(c => [c.value, c.description])) : new Map(q.levels.map((l, i) => [i, l.description]));
|
| 81 |
+
if (!Array.isArray(a.probabilities) || a.probabilities.length !== labels.size) fail();
|
| 82 |
+
const seen = new Set();
|
| 83 |
+
const rows = a.probabilities.map(p => {
|
| 84 |
+
if (!p) fail();
|
| 85 |
+
const key = q.type === 'score' && typeof p.value === 'string' && /^\d+$/.test(p.value) ? Number(p.value) : p.value;
|
| 86 |
+
if (!labels.has(key) || seen.has(key) || !probability(p.probability)) fail();
|
| 87 |
+
seen.add(key);
|
| 88 |
+
return {label: labels.get(key), probability: p.probability};
|
| 89 |
+
});
|
| 90 |
+
if (Math.abs(rows.reduce((sum, row) => sum + row.probability, 0) - 1) > .02) fail();
|
| 91 |
+
let title;
|
| 92 |
+
if (q.type === 'choice') {if (!labels.has(a.choice)) fail(); title = labels.get(a.choice);}
|
| 93 |
+
else {
|
| 94 |
+
if (typeof a.score !== 'number' || !Number.isFinite(a.score) || a.score < 0 || a.score > labels.size-1) fail();
|
| 95 |
+
title = `${a.score.toFixed(2)} / ${labels.size-1}`;
|
| 96 |
+
}
|
| 97 |
+
return {title, rows, confidence: a.confidence};
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
export async function requestDecision(payload, signal, fetcher = fetch) {
|
| 101 |
+
// The browser supplies the Space's real Origin. Never try to spoof it in JS.
|
| 102 |
+
const response = await fetcher(ENDPOINT, {method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 103 |
+
body: JSON.stringify(payload), signal, credentials: 'omit', redirect: 'error'});
|
| 104 |
+
if (!response.ok) throw new Error(`http_${response.status}`);
|
| 105 |
+
let data;
|
| 106 |
+
try {data = await response.json();} catch {throw new Error('response');}
|
| 107 |
+
return {data, answer: parseAnswer(data, payload)};
|
| 108 |
+
}
|
app.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {buildPayload, previewPayload, pythonExample, requestDecision} from './api.mjs';
|
| 2 |
+
import {words, examples} from './locales.mjs';
|
| 3 |
+
const $ = id => document.getElementById(id);
|
| 4 |
+
let language = new URL(location.href).searchParams.get('lang') === 'zh' ? 'zh' : 'en';
|
| 5 |
+
let imageData, imageTask = 0, controller, resultData, requestPayload, lastError, state = 'ready', statusKey = '';
|
| 6 |
+
const t = key => words[language][key] || key;
|
| 7 |
+
|
| 8 |
+
function renderLanguage() {
|
| 9 |
+
document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
|
| 10 |
+
document.querySelectorAll('[data-i18n]').forEach(el => {el.textContent = t(el.dataset.i18n);});
|
| 11 |
+
document.querySelectorAll('[data-placeholder]').forEach(el => {el.placeholder = t(el.dataset.placeholder);});
|
| 12 |
+
$('language').textContent = language === 'en' ? '中文' : 'EN';
|
| 13 |
+
$('language').ariaLabel = language === 'en' ? '切换到中文' : 'Switch to English';
|
| 14 |
+
$('theme').ariaLabel = language === 'en' ? 'Switch color theme' : '切换主题';
|
| 15 |
+
configureOptions(); renderState();
|
| 16 |
+
if (resultData) renderAnswer(resultData.answer, resultData.data, requestPayload);
|
| 17 |
+
}
|
| 18 |
+
function configureOptions() {
|
| 19 |
+
$('options-field').hidden = $('mode').value === 'predicate';
|
| 20 |
+
$('options-label').textContent = t($('mode').value === 'score' ? 'levelsLabel' : 'choicesLabel');
|
| 21 |
+
}
|
| 22 |
+
function renderState() {
|
| 23 |
+
$('state-badge').dataset.state = state;
|
| 24 |
+
$('state-badge').textContent = t(state);
|
| 25 |
+
$('status').className = state === 'error' ? 'error' : '';
|
| 26 |
+
$('status').textContent = lastError ? (words[language].errors[lastError] || `HTTP ${lastError.replace('http_', '')}`) : statusKey ? t(statusKey) : '';
|
| 27 |
+
}
|
| 28 |
+
function clearResult() {
|
| 29 |
+
resultData = null; lastError = null; state = 'ready'; statusKey = '';
|
| 30 |
+
$('answer').hidden = true; $('empty').hidden = false;
|
| 31 |
+
$('response-json').textContent = ''; $('probabilities').replaceChildren();renderState();
|
| 32 |
+
}
|
| 33 |
+
function showError(error) {
|
| 34 |
+
state = 'error'; lastError = error instanceof TypeError ? 'network' : error.message;
|
| 35 |
+
if (!words[language].errors[lastError] && !/^http_\d+$/.test(lastError)) lastError = 'response';
|
| 36 |
+
renderState();
|
| 37 |
+
}
|
| 38 |
+
function currentPayload() {
|
| 39 |
+
return buildPayload({model:$('model').value,mode:$('mode').value,question:$('question').value,
|
| 40 |
+
context:$('context').value,options:$('options').value,image:imageData});
|
| 41 |
+
}
|
| 42 |
+
function updatePreview(report = false) {
|
| 43 |
+
try {
|
| 44 |
+
const payload = currentPayload();
|
| 45 |
+
$('python-code').textContent = pythonExample(payload);
|
| 46 |
+
$('request-json').textContent = JSON.stringify(previewPayload(payload),null,2);
|
| 47 |
+
$('copy-code').disabled = false;return payload;
|
| 48 |
+
} catch (error) {
|
| 49 |
+
$('python-code').textContent = t('noCode');$('request-json').textContent = '';$('copy-code').disabled = true;
|
| 50 |
+
if (report) showError(error);
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
function renderAnswer(answer, data, payload) {
|
| 54 |
+
$('empty').hidden = true;$('answer').hidden = false;
|
| 55 |
+
const isPredicate = payload.questions[0].type === 'predicate';
|
| 56 |
+
$('decision-title').textContent = isPredicate ? t(answer.title) : answer.title;
|
| 57 |
+
$('probabilities').replaceChildren();
|
| 58 |
+
for (const row of answer.rows) {
|
| 59 |
+
const label = isPredicate ? t(row.label) : row.label;
|
| 60 |
+
const block = document.createElement('div');block.className = 'prob-row';
|
| 61 |
+
const top = document.createElement('div');top.className = 'prob-label';
|
| 62 |
+
const name = document.createElement('span');name.textContent = label;
|
| 63 |
+
const number = document.createElement('strong');number.textContent = `${(row.probability*100).toFixed(1)}%`;top.append(name,number);
|
| 64 |
+
const progress = document.createElement('progress');progress.max=1;progress.value=row.probability;progress.ariaLabel=label;
|
| 65 |
+
block.append(top,progress);$('probabilities').append(block);
|
| 66 |
+
}
|
| 67 |
+
$('score-note').hidden = payload.questions[0].type !== 'score';
|
| 68 |
+
$('confidence').textContent = answer.confidence === undefined ? '' : `${t('confidence')}: ${(answer.confidence*100).toFixed(1)}%`;
|
| 69 |
+
$('model-id').textContent = data.model_id ? `${t('modelId')}: ${data.model_id}` : '';
|
| 70 |
+
$('response-json').textContent = JSON.stringify(data,null,2);
|
| 71 |
+
}
|
| 72 |
+
function lock(value) {
|
| 73 |
+
$('fields').disabled=value;
|
| 74 |
+
document.querySelectorAll('[data-example]').forEach(button=>{button.disabled=value;});
|
| 75 |
+
$('preview-code').disabled=value;
|
| 76 |
+
}
|
| 77 |
+
function removeImage() {
|
| 78 |
+
imageTask++;imageData=undefined;$('image').value='';
|
| 79 |
+
$('thumbnail').removeAttribute('src');$('image-preview').hidden=true;clearResult();updatePreview();
|
| 80 |
+
}
|
| 81 |
+
$('image').addEventListener('change', async () => {
|
| 82 |
+
const file = $('image').files[0];if (!file) {removeImage();return;}
|
| 83 |
+
const task=++imageTask;imageData=undefined;clearResult();$('image-preview').hidden=true;
|
| 84 |
+
lock(true);state='busy';statusKey='reading';renderState();let url;
|
| 85 |
+
try {
|
| 86 |
+
if (!['image/jpeg','image/png','image/webp'].includes(file.type)) throw new Error('image');
|
| 87 |
+
if (file.size > 3000000) throw new Error('size');
|
| 88 |
+
url=URL.createObjectURL(file);const image=new Image();image.src=url;await image.decode();
|
| 89 |
+
if (!image.naturalWidth || !image.naturalHeight) throw new Error('image');
|
| 90 |
+
const factor=Math.min(1,1536/image.naturalWidth,1536/image.naturalHeight);
|
| 91 |
+
const canvas=document.createElement('canvas');canvas.width=Math.max(1,Math.round(image.naturalWidth*factor));canvas.height=Math.max(1,Math.round(image.naturalHeight*factor));
|
| 92 |
+
const ctx=canvas.getContext('2d');ctx.fillStyle='#fff';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.drawImage(image,0,0,canvas.width,canvas.height);
|
| 93 |
+
if(task!==imageTask)return;
|
| 94 |
+
imageData=canvas.toDataURL('image/jpeg',.85);
|
| 95 |
+
$('thumbnail').src=imageData;$('image-name').textContent=file.name;$('image-preview').hidden=false;
|
| 96 |
+
state='ready';statusKey='';renderState();updatePreview();
|
| 97 |
+
} catch(error) {
|
| 98 |
+
if(task!==imageTask)return;
|
| 99 |
+
$('image').value='';showError(new Error(error.message==='size'?'size':'image'));
|
| 100 |
+
} finally {if(url)URL.revokeObjectURL(url);if(task===imageTask)lock(false);}
|
| 101 |
+
});
|
| 102 |
+
$('decision-form').addEventListener('submit', async event => {
|
| 103 |
+
event.preventDefault();if(controller)return;
|
| 104 |
+
clearResult();const payload=updatePreview(true);if(!payload)return;
|
| 105 |
+
requestPayload=payload;controller=new AbortController();const active=controller;
|
| 106 |
+
const timeout=setTimeout(()=>active.abort('timeout'),120000);
|
| 107 |
+
lock(true);$('cancel').hidden=false;state='busy';statusKey='waiting';renderState();
|
| 108 |
+
try {
|
| 109 |
+
const result=await requestDecision(payload,active.signal);if(active.signal.aborted)return;
|
| 110 |
+
resultData=result;state='success';statusKey='';renderState();renderAnswer(result.answer,result.data,payload);
|
| 111 |
+
} catch(error) {
|
| 112 |
+
if(active.signal.aborted) {
|
| 113 |
+
if(active.signal.reason==='timeout')showError(new Error('timeout'));
|
| 114 |
+
else{state='ready';statusKey='cancelled';renderState();}
|
| 115 |
+
} else showError(error);
|
| 116 |
+
} finally {clearTimeout(timeout);if(controller===active){controller=null;lock(false);$('cancel').hidden=true;}}
|
| 117 |
+
});
|
| 118 |
+
$('cancel').addEventListener('click',()=>controller?.abort('cancelled'));
|
| 119 |
+
$('remove-image').addEventListener('click',removeImage);
|
| 120 |
+
$('language').addEventListener('click',()=>{language=language==='en'?'zh':'en';renderLanguage();updatePreview();});
|
| 121 |
+
$('theme').addEventListener('click',()=>{document.body.dataset.theme=document.body.dataset.theme==='dark'?'light':'dark';});
|
| 122 |
+
$('mode').addEventListener('change',()=>{
|
| 123 |
+
if($('mode').value==='score'&&!$('options').value.trim())$('options').value=examples[language][2][3];configureOptions();
|
| 124 |
+
});
|
| 125 |
+
for (const name of ['question','context','options','model','mode']) {
|
| 126 |
+
$(name).addEventListener('input',()=>{clearResult();updatePreview();});
|
| 127 |
+
$(name).addEventListener('change',()=>{configureOptions();updatePreview();});
|
| 128 |
+
}
|
| 129 |
+
document.querySelectorAll('[data-example]').forEach(button=>button.addEventListener('click',()=>{
|
| 130 |
+
removeImage();const example=examples[language][Number(button.dataset.example)];
|
| 131 |
+
['mode','question','context','options'].forEach((id,index)=>{$(id).value=example[index];});
|
| 132 |
+
configureOptions();updatePreview();$('question').focus({preventScroll:true});
|
| 133 |
+
$('decision-form').scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth',block:'center'});
|
| 134 |
+
}));
|
| 135 |
+
$('preview-code').addEventListener('click',()=>{updatePreview(true);});
|
| 136 |
+
$('copy-code').addEventListener('click',async()=>{
|
| 137 |
+
try{await navigator.clipboard.writeText($('python-code').textContent);$('copy-status').textContent=t('copied');}
|
| 138 |
+
catch{$('copy-status').textContent=t('copyFailed');}
|
| 139 |
+
});
|
| 140 |
+
document.body.dataset.theme=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';
|
| 141 |
+
renderLanguage();updatePreview();
|
assets/OFL.txt
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DM Sans
|
| 2 |
+
================================
|
| 3 |
+
Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts)
|
| 4 |
+
|
| 5 |
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
| 6 |
+
This license is copied below, and is also available with a FAQ at:
|
| 7 |
+
https://scripts.sil.org/OFL
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
-----------------------------------------------------------
|
| 11 |
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
| 12 |
+
-----------------------------------------------------------
|
| 13 |
+
|
| 14 |
+
PREAMBLE
|
| 15 |
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
| 16 |
+
development of collaborative font projects, to support the font creation
|
| 17 |
+
efforts of academic and linguistic communities, and to provide a free and
|
| 18 |
+
open framework in which fonts may be shared and improved in partnership
|
| 19 |
+
with others.
|
| 20 |
+
|
| 21 |
+
The OFL allows the licensed fonts to be used, studied, modified and
|
| 22 |
+
redistributed freely as long as they are not sold by themselves. The
|
| 23 |
+
fonts, including any derivative works, can be bundled, embedded,
|
| 24 |
+
redistributed and/or sold with any software provided that any reserved
|
| 25 |
+
names are not used by derivative works. The fonts and derivatives,
|
| 26 |
+
however, cannot be released under any other type of license. The
|
| 27 |
+
requirement for fonts to remain under this license does not apply
|
| 28 |
+
to any document created using the fonts or their derivatives.
|
| 29 |
+
|
| 30 |
+
DEFINITIONS
|
| 31 |
+
"Font Software" refers to the set of files released by the Copyright
|
| 32 |
+
Holder(s) under this license and clearly marked as such. This may
|
| 33 |
+
include source files, build scripts and documentation.
|
| 34 |
+
|
| 35 |
+
"Reserved Font Name" refers to any names specified as such after the
|
| 36 |
+
copyright statement(s).
|
| 37 |
+
|
| 38 |
+
"Original Version" refers to the collection of Font Software components as
|
| 39 |
+
distributed by the Copyright Holder(s).
|
| 40 |
+
|
| 41 |
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
| 42 |
+
or substituting -- in part or in whole -- any of the components of the
|
| 43 |
+
Original Version, by changing formats or by porting the Font Software to a
|
| 44 |
+
new environment.
|
| 45 |
+
|
| 46 |
+
"Author" refers to any designer, engineer, programmer, technical
|
| 47 |
+
writer or other person who contributed to the Font Software.
|
| 48 |
+
|
| 49 |
+
PERMISSION & CONDITIONS
|
| 50 |
+
Permission is hereby granted, free of charge, to any person obtaining
|
| 51 |
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
| 52 |
+
redistribute, and sell modified and unmodified copies of the Font
|
| 53 |
+
Software, subject to the following conditions:
|
| 54 |
+
|
| 55 |
+
1) Neither the Font Software nor any of its individual components,
|
| 56 |
+
in Original or Modified Versions, may be sold by itself.
|
| 57 |
+
|
| 58 |
+
2) Original or Modified Versions of the Font Software may be bundled,
|
| 59 |
+
redistributed and/or sold with any software, provided that each copy
|
| 60 |
+
contains the above copyright notice and this license. These can be
|
| 61 |
+
included either as stand-alone text files, human-readable headers or
|
| 62 |
+
in the appropriate machine-readable metadata fields within text or
|
| 63 |
+
binary files as long as those fields can be easily viewed by the user.
|
| 64 |
+
|
| 65 |
+
3) No Modified Version of the Font Software may use the Reserved Font
|
| 66 |
+
Name(s) unless explicit written permission is granted by the corresponding
|
| 67 |
+
Copyright Holder. This restriction only applies to the primary font name as
|
| 68 |
+
presented to the users.
|
| 69 |
+
|
| 70 |
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
| 71 |
+
Software shall not be used to promote, endorse or advertise any
|
| 72 |
+
Modified Version, except to acknowledge the contribution(s) of the
|
| 73 |
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
| 74 |
+
permission.
|
| 75 |
+
|
| 76 |
+
5) The Font Software, modified or unmodified, in part or in whole,
|
| 77 |
+
must be distributed entirely under this license, and must not be
|
| 78 |
+
distributed under any other license. The requirement for fonts to
|
| 79 |
+
remain under this license does not apply to any document created
|
| 80 |
+
using the Font Software.
|
| 81 |
+
|
| 82 |
+
TERMINATION
|
| 83 |
+
This license becomes null and void if any of the above conditions are
|
| 84 |
+
not met.
|
| 85 |
+
|
| 86 |
+
DISCLAIMER
|
| 87 |
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
| 88 |
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
| 89 |
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
| 90 |
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
| 91 |
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
| 92 |
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
| 93 |
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 94 |
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
| 95 |
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
Space Grotesk
|
| 99 |
+
================================
|
| 100 |
+
Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
|
| 101 |
+
|
| 102 |
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
| 103 |
+
This license is copied below, and is also available with a FAQ at:
|
| 104 |
+
http://scripts.sil.org/OFL
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
-----------------------------------------------------------
|
| 108 |
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
| 109 |
+
-----------------------------------------------------------
|
| 110 |
+
|
| 111 |
+
PREAMBLE
|
| 112 |
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
| 113 |
+
development of collaborative font projects, to support the font creation
|
| 114 |
+
efforts of academic and linguistic communities, and to provide a free and
|
| 115 |
+
open framework in which fonts may be shared and improved in partnership
|
| 116 |
+
with others.
|
| 117 |
+
|
| 118 |
+
The OFL allows the licensed fonts to be used, studied, modified and
|
| 119 |
+
redistributed freely as long as they are not sold by themselves. The
|
| 120 |
+
fonts, including any derivative works, can be bundled, embedded,
|
| 121 |
+
redistributed and/or sold with any software provided that any reserved
|
| 122 |
+
names are not used by derivative works. The fonts and derivatives,
|
| 123 |
+
however, cannot be released under any other type of license. The
|
| 124 |
+
requirement for fonts to remain under this license does not apply
|
| 125 |
+
to any document created using the fonts or their derivatives.
|
| 126 |
+
|
| 127 |
+
DEFINITIONS
|
| 128 |
+
"Font Software" refers to the set of files released by the Copyright
|
| 129 |
+
Holder(s) under this license and clearly marked as such. This may
|
| 130 |
+
include source files, build scripts and documentation.
|
| 131 |
+
|
| 132 |
+
"Reserved Font Name" refers to any names specified as such after the
|
| 133 |
+
copyright statement(s).
|
| 134 |
+
|
| 135 |
+
"Original Version" refers to the collection of Font Software components as
|
| 136 |
+
distributed by the Copyright Holder(s).
|
| 137 |
+
|
| 138 |
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
| 139 |
+
or substituting -- in part or in whole -- any of the components of the
|
| 140 |
+
Original Version, by changing formats or by porting the Font Software to a
|
| 141 |
+
new environment.
|
| 142 |
+
|
| 143 |
+
"Author" refers to any designer, engineer, programmer, technical
|
| 144 |
+
writer or other person who contributed to the Font Software.
|
| 145 |
+
|
| 146 |
+
PERMISSION & CONDITIONS
|
| 147 |
+
Permission is hereby granted, free of charge, to any person obtaining
|
| 148 |
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
| 149 |
+
redistribute, and sell modified and unmodified copies of the Font
|
| 150 |
+
Software, subject to the following conditions:
|
| 151 |
+
|
| 152 |
+
1) Neither the Font Software nor any of its individual components,
|
| 153 |
+
in Original or Modified Versions, may be sold by itself.
|
| 154 |
+
|
| 155 |
+
2) Original or Modified Versions of the Font Software may be bundled,
|
| 156 |
+
redistributed and/or sold with any software, provided that each copy
|
| 157 |
+
contains the above copyright notice and this license. These can be
|
| 158 |
+
included either as stand-alone text files, human-readable headers or
|
| 159 |
+
in the appropriate machine-readable metadata fields within text or
|
| 160 |
+
binary files as long as those fields can be easily viewed by the user.
|
| 161 |
+
|
| 162 |
+
3) No Modified Version of the Font Software may use the Reserved Font
|
| 163 |
+
Name(s) unless explicit written permission is granted by the corresponding
|
| 164 |
+
Copyright Holder. This restriction only applies to the primary font name as
|
| 165 |
+
presented to the users.
|
| 166 |
+
|
| 167 |
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
| 168 |
+
Software shall not be used to promote, endorse or advertise any
|
| 169 |
+
Modified Version, except to acknowledge the contribution(s) of the
|
| 170 |
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
| 171 |
+
permission.
|
| 172 |
+
|
| 173 |
+
5) The Font Software, modified or unmodified, in part or in whole,
|
| 174 |
+
must be distributed entirely under this license, and must not be
|
| 175 |
+
distributed under any other license. The requirement for fonts to
|
| 176 |
+
remain under this license does not apply to any document created
|
| 177 |
+
using the Font Software.
|
| 178 |
+
|
| 179 |
+
TERMINATION
|
| 180 |
+
This license becomes null and void if any of the above conditions are
|
| 181 |
+
not met.
|
| 182 |
+
|
| 183 |
+
DISCLAIMER
|
| 184 |
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
| 185 |
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
| 186 |
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
| 187 |
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
| 188 |
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
| 189 |
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
| 190 |
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
| 191 |
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
| 192 |
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
assets/dmsans.woff2
ADDED
|
Binary file (36.9 kB). View file
|
|
|
assets/manifest.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"design_system_version": "0.5.1",
|
| 3 |
+
"sha256": {
|
| 4 |
+
"OFL.txt": "56c64fa0f5ec4b0b627546ef7f80be19ce6779143e079d912fb7fc87df694d6a",
|
| 5 |
+
"dmsans.woff2": "9fea608a947e67020c33cad9a6fe3d60c54119dfb8cff87768a8117a15ed7543",
|
| 6 |
+
"modern-tokens.css": "c1577156c6b106a78b29667e8af83aad28b3df1d4c310b84280738dc9da79eec",
|
| 7 |
+
"valen-mark.png": "657c4be26bbe1a08e450afef47d1859acd771cb049a0847ede05b621865d2583"
|
| 8 |
+
}
|
| 9 |
+
}
|
assets/modern-tokens.css
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Generated from tokens/modern.json. Optional modern profile. */
|
| 2 |
+
:root {
|
| 3 |
+
--primary: #3964FE;
|
| 4 |
+
--primary-soft: #E8EEFF;
|
| 5 |
+
--text: #1E232C;
|
| 6 |
+
--heading: #152443;
|
| 7 |
+
--text-muted: rgba(0,0,0,.55);
|
| 8 |
+
--text-faint: rgba(0,0,0,.40);
|
| 9 |
+
--bg: #F9F8F8;
|
| 10 |
+
--bg-doc: #FFFFFF;
|
| 11 |
+
--surface: #F5F6F7;
|
| 12 |
+
--border: rgba(9,45,78,.12);
|
| 13 |
+
--border-strong: rgba(9,45,78,.18);
|
| 14 |
+
--code-bg: #1B1B1D;
|
| 15 |
+
--green-bg: #E6FAED;
|
| 16 |
+
--green: #15803D;
|
| 17 |
+
--blue-bg: #E8F0FF;
|
| 18 |
+
--blue: #2563EB;
|
| 19 |
+
--red-bg: #FDECEC;
|
| 20 |
+
--red: #DC2626;
|
| 21 |
+
--r-pill: 100px;
|
| 22 |
+
--r-card: 20px;
|
| 23 |
+
--r-sm: 8px;
|
| 24 |
+
--r-xs: 6px;
|
| 25 |
+
--font-display: 'Montserrat', sans-serif;
|
| 26 |
+
--font-body: 'DM Sans', 'Inter', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
| 27 |
+
--font-doc: 'Inter', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
| 28 |
+
--font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
| 29 |
+
}
|
assets/valen-mark.png
ADDED
|
Git LFS Details
|
index.html
CHANGED
|
@@ -1,19 +1,42 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
+
<meta name="description" content="Try Valen's free decision API with text and images. Yes/no, choices, and ratings with ready-to-run Python examples.">
|
| 7 |
+
<meta name="color-scheme" content="light dark">
|
| 8 |
+
<title>Valen API</title>
|
| 9 |
+
<link rel="icon" href="./assets/valen-mark.png">
|
| 10 |
+
<link rel="stylesheet" href="./style.css">
|
| 11 |
+
<script type="module" src="./app.mjs"></script>
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
<header class="nav"><a class="brand" href="https://valen-team.github.io/" target="_blank" rel="noreferrer"><img src="./assets/valen-mark.png" alt=""><strong>valen</strong><span>API</span></a><nav aria-label="Navigation"><a href="https://valen-team.github.io/docs/valen/api/" target="_blank" rel="noreferrer" data-i18n="docs">Documentation ↗</a><button id="language" type="button" aria-label="Switch language">中文</button><button id="theme" type="button" aria-label="Switch color theme">◐</button></nav></header>
|
| 15 |
+
<main>
|
| 16 |
+
<section class="intro"><span class="eyebrow" data-i18n="eyebrow">THE VALEN API PLAYGROUND</span><h1 data-i18n="title">A question. A clear decision.</h1><p data-i18n="subtitle">Bring text or an image. Get probabilities for what comes next.</p><div class="intro-meta"><span class="dot"></span><span data-i18n="free">Free API · No key needed</span><span class="divider">/</span><span data-i18n="formats">Yes / No · Choices · Rating</span></div></section>
|
| 17 |
+
<div class="workspace">
|
| 18 |
+
<section class="panel input-panel"><div class="panel-heading"><span class="step">01</span><h2 data-i18n="input">Your input</h2></div>
|
| 19 |
+
<form id="decision-form"><fieldset id="fields">
|
| 20 |
+
<div class="two-columns"><label><span data-i18n="model">Model</span><select id="model"><option value="valen-2b">Valen 2B</option><option value="valen-4b">Valen 4B</option><option value="valen-0-8b">Valen 0.8B</option><option value="valen-preview-0923">Valen Preview</option></select></label><label><span data-i18n="task">Output</span><select id="mode"><option value="predicate" data-i18n="predicate">Yes / No</option><option value="choice" data-i18n="choice">Choices</option><option value="score" data-i18n="score">Rating</option></select></label></div>
|
| 21 |
+
<label><span data-i18n="question">Question</span><textarea id="question" maxlength="1000" rows="2" required data-placeholder="questionPlaceholder" placeholder="What would you like Valen to decide?"></textarea></label>
|
| 22 |
+
<label><span data-i18n="context">Context <small>optional</small></span><textarea id="context" maxlength="4000" rows="3" data-placeholder="contextPlaceholder" placeholder="Paste the text to assess, or add some context…"></textarea></label>
|
| 23 |
+
<label id="options-field" hidden><span id="options-label">Choices · one per line</span><textarea id="options" rows="4" placeholder="Option A Option B"></textarea><small data-i18n="optionsHint">2–8 entries, up to 300 characters each.</small></label>
|
| 24 |
+
<div class="attachment"><label class="upload-label" for="image"><span class="upload-icon">+</span><span><strong data-i18n="attach">Add an image</strong><small data-i18n="imageHint">Optional · JPG, PNG, WebP · up to 3 MB</small></span></label><input id="image" type="file" accept="image/jpeg,image/png,image/webp"><div id="image-preview" hidden><img id="thumbnail" alt="Selected image"><span id="image-name"></span><button id="remove-image" type="button" data-i18n="remove">Remove</button></div></div>
|
| 25 |
+
<button id="submit" class="primary" type="submit"><span data-i18n="submit">Ask Valen</span><span aria-hidden="true">↗</span></button>
|
| 26 |
+
</fieldset></form>
|
| 27 |
+
<button id="cancel" class="cancel" type="button" hidden data-i18n="cancel">Cancel request</button>
|
| 28 |
+
<p class="privacy" data-i18n="privacy">On submit, your input is sent directly to the Valen API.</p>
|
| 29 |
+
</section>
|
| 30 |
+
<section class="panel output-panel"><div class="panel-heading"><span class="step">02</span><h2 data-i18n="result">Decision</h2><span id="state-badge" class="badge" data-state="ready">Ready</span></div>
|
| 31 |
+
<div id="status" role="status" aria-live="polite"></div>
|
| 32 |
+
<div id="empty"><div class="empty-mark">V</div><h3 data-i18n="emptyTitle">Small input. Useful signal.</h3><p data-i18n="emptyText">Your result and probability distribution will appear here.</p></div>
|
| 33 |
+
<div id="answer" hidden><h3 id="decision-title"></h3><div id="probabilities"></div><p id="score-note" class="note" hidden data-i18n="scoreNote">Rating is the probability-weighted level index, starting at 0. Five levels span 0–4.</p><p id="confidence" class="note"></p><p id="model-id" class="model-id"></p><details><summary data-i18n="raw">Response JSON</summary><pre id="response-json"></pre></details></div>
|
| 34 |
+
<div class="api-note"><span class="mini-label">POST</span><code>/v1/decisions</code><p data-i18n="quota">Free capacity is limited. On HTTP 429, wait at least one minute before retrying.</p></div>
|
| 35 |
+
</section>
|
| 36 |
+
</div>
|
| 37 |
+
<section class="examples"><div class="section-heading"><h2 data-i18n="examplesTitle">Start with a question</h2><span data-i18n="examplesNote">Loads an example. You choose when to send.</span></div><div class="example-grid"><button type="button" data-example="0"><span data-i18n="example0Tag">YES / NO</span><strong data-i18n="example0Title">A happy customer?</strong><p data-i18n="example0Text">Find the sentiment behind a message.</p><i aria-hidden="true">↗</i></button><button type="button" data-example="1"><span data-i18n="example1Tag">CHOICES</span><strong data-i18n="example1Title">What do they need?</strong><p data-i18n="example1Text">Choose the right next step for support.</p><i aria-hidden="true">↗</i></button><button type="button" data-example="2"><span data-i18n="example2Tag">RATING</span><strong data-i18n="example2Title">Ready to buy?</strong><p data-i18n="example2Text">Rate purchase intent on five levels.</p><i aria-hidden="true">↗</i></button></div></section>
|
| 38 |
+
<section class="code-section"><details id="code-details"><summary><span data-i18n="codeTitle">Make it your own · API example</span><span class="code-tag">PYTHON</span></summary><p data-i18n="codeNote">Code follows your current input. Generating a preview does not call the API.</p><div class="code-actions"><button id="preview-code" type="button" data-i18n="preview">Update preview</button><button id="copy-code" type="button" data-i18n="copy">Copy Python</button><span id="copy-status" role="status"></span></div><pre><code id="python-code"></code></pre><details><summary data-i18n="requestPreview">Request JSON · image data omitted</summary><pre id="request-json"></pre></details></details></section>
|
| 39 |
+
<footer><span>© 2026 Valen Team / 万澜</span><p data-i18n="network">Your browser connects directly to api.valen-team.workers.dev. This Space uses the same API as the website.</p><a href="https://huggingface.co/spaces/Valen-Team/Valen-API/tree/main" target="_blank" rel="noreferrer" data-i18n="source">View source ↗</a></footer>
|
| 40 |
+
</main>
|
| 41 |
+
</body>
|
| 42 |
</html>
|
locales.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const words = {
|
| 2 |
+
en: {
|
| 3 |
+
docs:'Documentation ↗', eyebrow:'THE VALEN API PLAYGROUND', title:'A question. A clear decision.', subtitle:'Bring text or an image. Get probabilities for what comes next.', free:'Free API · No key needed', formats:'Yes / No · Choices · Rating', input:'Your input', model:'Model', task:'Output', predicate:'Yes / No', choice:'Choices', score:'Rating', question:'Question', context:'Context · optional', questionPlaceholder:'What would you like Valen to decide?', contextPlaceholder:'Paste the text to assess, or add some context…', choicesLabel:'Choices · one per line', levelsLabel:'Rating levels · low to high, one per line', optionsHint:'2–8 entries, up to 300 characters each.', attach:'Add an image', imageHint:'Optional · JPG, PNG, WebP · up to 3 MB', remove:'Remove', submit:'Ask Valen', cancel:'Cancel request', privacy:'On submit, your input is sent directly to the Valen API.', result:'Decision', emptyTitle:'Small input. Useful signal.', emptyText:'Your result and probability distribution will appear here.', scoreNote:'Rating is the probability-weighted level index, starting at 0. Five levels span 0–4.', raw:'Response JSON', quota:'Free capacity is limited. On HTTP 429, wait at least one minute before retrying.', examplesTitle:'Start with a question', examplesNote:'Loads an example. You choose when to send.', example0Tag:'YES / NO', example0Title:'A happy customer?', example0Text:'Find the sentiment behind a message.', example1Tag:'CHOICES', example1Title:'What do they need?', example1Text:'Choose the right next step for support.', example2Tag:'RATING', example2Title:'Ready to buy?', example2Text:'Rate purchase intent on five levels.', codeTitle:'Make it your own · API example', codeNote:'Code follows your current input. Generating a preview does not call the API.', preview:'Update preview', copy:'Copy Python', copied:'Copied', copyFailed:'Select the code and copy it manually.', requestPreview:'Request JSON · image data omitted', network:'Your browser connects directly to api.valen-team.workers.dev. This Space uses the same API as the website.', source:'View source ↗', ready:'Ready', busy:'Deciding…', success:'Complete', error:'Try again', reading:'Reading image…', cancelled:'Request cancelled.', waiting:'Valen is deciding…', yes:'Yes', no:'No', confidence:'Confidence', modelId:'Returned model ID', noCode:'Enter a valid question and options to generate your API example.',
|
| 4 |
+
errors:{model:'Choose a supported model and output type.',question:'Enter a question (up to 1000 characters).',context:'Context can be up to 4000 characters.',options:'Enter 2–8 options, one per line, up to 300 characters each.',image:'Choose a readable JPG, PNG or WebP image.',size:'The image is too large. Choose a file under 3 MB.',response:'The API returned an unexpected response. Please retry later.',network:'Could not connect to the API. Check your network or proxy and try again.',timeout:'The request timed out after two minutes. Please retry later.',http_400:'HTTP 400 · The API rejected this request. Check the question and options.',http_401:'HTTP 401 · API authentication failed.',http_403:'HTTP 403 · This website is not allowed to call the API.',http_413:'HTTP 413 · The request is too large.',http_429:'HTTP 429 · Free API capacity is limited. Wait at least one minute before retrying.',http_502:'HTTP 502 · The model service could not complete the request.',http_503:'HTTP 503 · The API is not configured or is unavailable.'}
|
| 5 |
+
},
|
| 6 |
+
zh: {
|
| 7 |
+
docs:'开发者文档 ↗',eyebrow:'VALEN API 体验',title:'一个问题,一次清晰判断。',subtitle:'输入文字或图片,让下一步决策有据可依。',free:'免费 API · 无需 Key',formats:'是 / 否 · 选择 · 评分',input:'你的输入',model:'模型',task:'输出类型',predicate:'是 / 否',choice:'选择答案',score:'评分',question:'问题',context:'补充背景 · 可选',questionPlaceholder:'你想让 Valen 判断什么?',contextPlaceholder:'粘贴需要判断的文字,或补充相关信息…',choicesLabel:'候选项 · 每行一个',levelsLabel:'评分档位 · 从低到高,每行一个',optionsHint:'2–8 项,每项最多 300 字符。',attach:'添加图片',imageHint:'可选 · JPG、PNG、WebP · 最大 3 MB',remove:'移除',submit:'提交判断',cancel:'取消请求',privacy:'提交后,你的输入会直接发送至 Valen API。',result:'判断结果',emptyTitle:'简单输入,清晰信号。',emptyText:'判断结果与各个选项的概率将在这里显示。',scoreNote:'评分按从 0 开始的档位索引进行概率加权。五个档位对应 0–4 分。',raw:'原始响应 JSON',quota:'免费 API 容量有限。遇到 HTTP 429,请至少等待一分钟后重试。',examplesTitle:'从一个问题开始',examplesNote:'点击填入示例,提交时才会请求模型。',example0Tag:'是非判断',example0Title:'客户满意吗?',example0Text:'从一段反馈中判断客户的态度。',example1Tag:'选择答案',example1Title:'客户需要什么?',example1Text:'为���服选择合适的下一步处理方式。',example2Tag:'评分',example2Title:'准备好购买了吗?',example2Text:'通过五个档位判断客户的购买意愿。',codeTitle:'接入你的应用 · API 示例',codeNote:'代码根据当前输入生成。生成预览不会调用 API。',preview:'更新预览',copy:'复制 Python',copied:'已复制',copyFailed:'请选中代码后手动复制。',requestPreview:'请求 JSON · 已省略图片数据',network:'浏览器直接连接 api.valen-team.workers.dev。此 Space 与官网共用同一个 API。',source:'查看源码 ↗',ready:'等待提问',busy:'正在判断…',success:'已完成',error:'请求未完成',reading:'正在读取图片…',cancelled:'请求已取消。',waiting:'Valen 正在判断…',yes:'是',no:'否',confidence:'置信度',modelId:'返回的模型标识',noCode:'填写有效的问题与选项后,即可生成对应的 API 示例。',
|
| 8 |
+
errors:{model:'请选择支持的模型与输出类型。',question:'请填写问题,最多 1000 字符。',context:'补充背景最多 4000 字符。',options:'请输入 2–8 项,每行一个,每项最多 300 字符。',image:'请选择可读取的 JPG、PNG 或 WebP 图片。',size:'图片过大,请选择小于 3 MB 的文件。',response:'API 返回的格式异常,请稍后重试。',network:'无法连接 API,请检查当前网络或代理后重试。',timeout:'请求等待超过两分钟,请稍后重试。',http_400:'HTTP 400 · API 拒绝了请求,请检查问题与选项。',http_401:'HTTP 401 · API 鉴权失败。',http_403:'HTTP 403 · 当前网站尚未获准调用 API。',http_413:'HTTP 413 · 请求体积超出限制。',http_429:'HTTP 429 · 免费 API 额度受限,请至少等待一分钟后重试。',http_502:'HTTP 502 · 模型服务暂时无法完成请求。',http_503:'HTTP 503 · API 尚未配置或暂不可用。'}
|
| 9 |
+
}
|
| 10 |
+
};
|
| 11 |
+
export const examples = {
|
| 12 |
+
en: [
|
| 13 |
+
['predicate','Does this customer sound satisfied?','The delivery arrived early and everything works perfectly. Thank you!',''],
|
| 14 |
+
['choice','What does this customer want?','I ordered a blue jacket but received a red one. Can you send the right color?','Request a refund\nExchange an item\nTrack delivery'],
|
| 15 |
+
['score','Rate the purchase intent.','This looks like a great fit. Can you send a quote for 20 seats? We plan to start next week.','No interest\nSlight interest\nConsidering\nStrong interest\nReady to buy']
|
| 16 |
+
],
|
| 17 |
+
zh: [
|
| 18 |
+
['predicate','这位客户对服务满意吗?','快递比预计时间还早到,所有东西都很好用。谢谢!',''],
|
| 19 |
+
['choice','这位客户想做什么?','我订的是蓝色外套,收到的却是红色。可以帮我换成正确的颜色吗?','申请退款\n更换商品\n查询物流'],
|
| 20 |
+
['score','请评估这位客户的购买意愿。','这个方案很适合我们。可以发一份 20 个席位的报价吗?我们计划下周就开始使用。','没有兴趣\n略有兴趣\n正在考虑\n意愿较强\n准备购买']
|
| 21 |
+
]
|
| 22 |
+
};
|
package.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "valen-api-space",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {"test": "node --test tests/*.test.mjs"},
|
| 7 |
+
"engines": {"node": ">=22"}
|
| 8 |
+
}
|
style.css
CHANGED
|
@@ -1,28 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
h1 {
|
| 7 |
-
font-size: 16px;
|
| 8 |
-
margin-top: 0;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
p {
|
| 12 |
-
color: rgb(107, 114, 128);
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
-
}
|
|
|
|
| 1 |
+
@import url('./assets/modern-tokens.css');
|
| 2 |
+
@font-face{font-family:'DM Sans';src:url('./assets/dmsans.woff2') format('woff2');font-display:swap}
|
| 3 |
+
*{box-sizing:border-box}body{margin:0;background:var(--bg-doc);color:var(--text);font:15px/1.5 var(--font-body)}body[data-theme=dark]{--bg-doc:#101319;--bg:#101319;--surface:#1b202a;--text:#e4e9f3;--heading:#f0f4ff;--text-muted:#a1adbf;--text-faint:#8794a7;--border:#30394a;--primary:#8aa5ff;--primary-soft:#202e53;--green-bg:#173828;--green:#83d5a0;--red:#ff9b9b;--red-bg:#3b2027}button,input,select,textarea{font:inherit}button,a,input,select,textarea,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer}button:disabled{cursor:wait;opacity:.55}a{color:inherit;text-decoration:none}a:hover{color:var(--primary)}button,select,textarea{outline-offset:4px}button:focus-visible,a:focus-visible,summary:focus-visible{outline:2px solid var(--primary);outline-offset:4px}[hidden]{display:none!important}.nav{height:72px;display:flex;justify-content:space-between;align-items:center;max-width:1200px;margin:auto;padding:0 28px;border-bottom:1px solid var(--border)}.brand{display:flex;align-items:center;gap:9px}.brand img{width:36px;height:36px;object-fit:cover;border-radius:8px}.brand strong{font-size:27px;letter-spacing:-1.3px;color:var(--heading)}.brand span{font:11px var(--font-mono);color:var(--text-muted);border-left:1px solid var(--border);padding-left:14px;margin-left:6px}.nav nav{display:flex;align-items:center;gap:22px;font-size:13px}.nav button{background:none;color:var(--text);border:0;padding:6px}.nav #theme{font-size:22px}main{max-width:1200px;padding:0 28px;margin:auto}.intro{padding:55px 0 34px}.eyebrow{font-size:11px;letter-spacing:2px;color:var(--primary);font-weight:700}h1{font-size:clamp(30px,4.5vw,46px);line-height:1.15;color:var(--heading);letter-spacing:-1.5px;margin:15px 0}h2,h3,p{margin-top:0}.intro p{color:var(--text-muted);font-size:17px;margin-bottom:20px}.intro-meta{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-muted)}.dot{height:6px;width:6px;background:var(--green);border-radius:50%}.divider{padding:0 8px;color:var(--text-faint)}.workspace{display:grid;grid-template-columns:1fr 1fr;gap:24px}.panel{border:1px solid var(--border);border-radius:16px;padding:25px;min-width:0}.panel-heading{display:flex;align-items:center;gap:10px;margin-bottom:22px}.panel-heading h2{font-size:16px;margin:0;letter-spacing:-.2px}.step{font:11px var(--font-mono);color:var(--text-faint)}fieldset{border:0;padding:0;margin:0;min-width:0}label{display:block;margin-bottom:17px}label>span{display:block;font-size:12px;font-weight:600;margin-bottom:7px}label small{color:var(--text-muted);font-size:11px;font-weight:400;margin-left:5px}.two-columns{display:grid;grid-template-columns:1fr 1fr;gap:15px}select,textarea{width:100%;border:1px solid var(--border);background:var(--bg-doc);border-radius:8px;color:var(--text);padding:10px 12px;font-size:13px}textarea{resize:vertical;min-height:75px;line-height:1.6;display:block}textarea::placeholder{color:var(--text-faint)}select:focus,textarea:focus{outline:2px solid var(--primary-soft);border-color:var(--primary)}.attachment{border:1px dashed var(--border-strong,var(--border));border-radius:10px;margin-bottom:18px;padding:12px}.upload-label{display:flex;gap:10px;align-items:center;margin:0;cursor:pointer}.upload-label span{margin:0}.upload-label strong{font-size:12px}.upload-label small{display:block;margin:2px 0 0}.upload-icon{font-size:26px;color:var(--primary)}#image{width:100%;font-size:11px;margin-top:10px;color:var(--text-muted)}#image::file-selector-button{border:1px solid var(--border);padding:4px 8px;border-radius:5px;background:var(--surface);color:var(--text);margin-right:8px}#image-preview{display:flex;align-items:center;gap:12px;padding-top:10px}#thumbnail{width:48px;height:48px;object-fit:cover;border-radius:5px}#image-name{min-width:0;overflow-wrap:anywhere;font-size:12px}#remove-image{margin-left:auto;border:0;background:none;color:var(--text-muted);font-size:12px}.primary{width:100%;display:flex;justify-content:center;gap:10px;background:var(--primary);color:white;border:0;border-radius:8px;padding:12px;font-weight:600;font-size:14px}.primary:hover{filter:brightness(.95)}body[data-theme=dark] .primary{color:#101319}.privacy{font-size:11px;text-align:center;color:var(--text-muted);margin:12px 0 0}.cancel{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:6px 12px;border-radius:6px;display:block;margin:10px auto 0;font-size:12px}.output-panel{display:flex;flex-direction:column}.badge{margin-left:auto;color:var(--text-muted);background:var(--surface);padding:4px 9px;border-radius:100px;font-size:10px}.badge[data-state=success]{color:var(--green);background:var(--green-bg)}.badge[data-state=error]{color:var(--red);background:var(--red-bg)}.badge[data-state=busy]{color:var(--primary);background:var(--primary-soft)}#status{font-size:13px;color:var(--text-muted);overflow-wrap:anywhere}#status.error{color:var(--red);padding:12px;background:var(--red-bg);border-radius:8px;margin-bottom:18px}#empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;min-height:290px;padding:25px 10px}.empty-mark{color:var(--primary);font-size:26px;background:var(--primary-soft);width:58px;height:58px;display:grid;place-items:center;border-radius:16px;margin-bottom:22px;font-weight:600}#empty h3{font-size:19px;letter-spacing:-.4px;margin-bottom:8px}#empty p{max-width:270px;color:var(--text-muted);font-size:13px}#answer{padding:8px 0 24px}#decision-title{font-size:26px;line-height:1.3;overflow-wrap:anywhere;color:var(--heading);margin:12px 0 26px}.prob-row{margin:0 0 16px}.prob-label{display:flex;justify-content:space-between;gap:15px;margin-bottom:6px;font-size:13px}.prob-label span{overflow-wrap:anywhere}.prob-label strong{font:12px var(--font-mono);white-space:nowrap}progress{display:block;width:100%;height:6px;appearance:none;border:0;border-radius:6px;overflow:hidden;background:var(--surface)}progress::-webkit-progress-bar{background:var(--surface)}progress::-webkit-progress-value{background:var(--primary);border-radius:6px}progress::-moz-progress-bar{background:var(--primary);border-radius:6px}.note{font-size:12px;color:var(--text-muted);margin:20px 0 10px}.model-id{font:10px/1.5 var(--font-mono);color:var(--text-faint);overflow-wrap:anywhere}.api-note{border-top:1px solid var(--border);padding-top:18px;margin-top:auto}.mini-label{color:var(--green);background:var(--green-bg);font:10px var(--font-mono);padding:3px 5px;border-radius:4px;margin-right:7px}.api-note code{font:11px var(--font-mono)}.api-note p{font-size:11px;color:var(--text-muted);margin:10px 0 0}.examples{margin-top:35px}.section-heading{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-bottom:15px}.section-heading h2{font-size:16px;margin:0}.section-heading>span{font-size:11px;color:var(--text-muted)}.example-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.example-grid button{position:relative;background:var(--bg-doc);border:1px solid var(--border);border-radius:12px;padding:19px;text-align:left;color:var(--text)}.example-grid button:hover{border-color:var(--primary);background:var(--primary-soft)}.example-grid button>span{display:block;font-size:9px;letter-spacing:1.1px;color:var(--text-muted);margin-bottom:13px}.example-grid strong{font-size:15px;font-weight:600}.example-grid p{font-size:12px;color:var(--text-muted);margin:6px 0 0}.example-grid i{position:absolute;right:17px;top:15px;color:var(--text-muted);font-style:normal}.code-section{margin:32px 0;border:1px solid var(--border);padding:18px 22px;border-radius:12px}summary{cursor:pointer;font-size:12px;font-weight:600;line-height:1.6}summary::marker{color:var(--text-muted)}.code-section>details>summary{font-size:14px}.code-tag{float:right;font:9px var(--font-mono);color:var(--text-muted);padding-top:4px}.code-section p{font-size:12px;color:var(--text-muted);margin:16px 0}.code-actions{display:flex;gap:8px;align-items:center}.code-actions button{background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:11px}#copy-status{font-size:11px;color:var(--text-muted)}pre{background:var(--surface);border-radius:8px;padding:16px;font:11px/1.65 var(--font-mono);overflow:auto;max-height:420px;white-space:pre-wrap;overflow-wrap:anywhere}.code-section>details>pre{background:var(--code-bg);color:#e2e6ef;margin-top:15px}footer{border-top:1px solid var(--border);padding:20px 0 30px;display:flex;align-items:flex-start;gap:30px;font-size:10px;color:var(--text-muted)}footer p{margin:0;max-width:550px}footer>a{margin-left:auto;white-space:nowrap}footer>span{white-space:nowrap}@media(max-width:760px){.nav{padding:0 18px;height:62px}.nav nav{gap:12px}.nav nav>a{font-size:11px}main{padding:0 18px}.intro{padding:35px 0 26px}.intro p{font-size:14px}.intro-meta{font-size:10px;flex-wrap:wrap}.workspace{grid-template-columns:1fr;gap:16px}.panel{padding:19px}.example-grid{grid-template-columns:1fr}.example-grid button{padding:15px}.example-grid button>span{margin-bottom:6px}.section-heading{align-items:flex-start;flex-direction:column;gap:5px}#empty{min-height:200px}footer{flex-direction:column;gap:12px}footer>a{margin-left:0}.code-section{padding:15px}.code-tag{display:none}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/api.test.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import test from 'node:test';
|
| 2 |
+
import assert from 'node:assert/strict';
|
| 3 |
+
import {spawnSync} from 'node:child_process';
|
| 4 |
+
import {readFileSync} from 'node:fs';
|
| 5 |
+
import {buildPayload, previewPayload, pythonExample, parseAnswer, requestDecision, ENDPOINT} from '../api.mjs';
|
| 6 |
+
import {words, examples} from '../locales.mjs';
|
| 7 |
+
|
| 8 |
+
const make = (extra={}) => buildPayload({model:'valen-2b',mode:'predicate',question:'Is it positive?',context:'Great service!',options:'Low\nHigh',...extra});
|
| 9 |
+
|
| 10 |
+
test('native predicate, choice and zero-based score request contracts',()=>{
|
| 11 |
+
const p=make();assert.equal(p.questions[0].type,'predicate');assert.equal(p.temperature,1);
|
| 12 |
+
assert.equal(p.questions[0].choices,undefined);
|
| 13 |
+
assert.deepEqual(make({mode:'choice'}).questions[0].choices[0],{value:'option_1',description:'Low'});
|
| 14 |
+
assert.deepEqual(make({mode:'score'}).questions[0].levels[1],{label:'1',description:'High'});
|
| 15 |
+
assert.equal(make({context:''}).input,'Is it positive?');
|
| 16 |
+
});
|
| 17 |
+
test('rejects invalid input before network calls',()=>{
|
| 18 |
+
for(const value of [{model:'invalid'},{question:' '},{context:'x'.repeat(4001)},{mode:'choice',options:'one'},{mode:'score',options:Array.from({length:9},(_,i)=>String(i)).join('\n')},{image:'https://example.com/image.jpg'}])assert.throws(()=>make(value));
|
| 19 |
+
});
|
| 20 |
+
test('images use inline data; preview omits bytes without changing request',()=>{
|
| 21 |
+
const p=make({image:'data:image/jpeg;base64,Ymx1ZQ=='});
|
| 22 |
+
assert.equal(p.input[0].content[1].type,'input_image');
|
| 23 |
+
assert(!JSON.stringify(previewPayload(p)).includes('Ymx1ZQ=='));
|
| 24 |
+
assert(p.input[0].content[1].image_url.includes('Ymx1ZQ=='));
|
| 25 |
+
assert(pythonExample(p).includes('Image.open("image.jpg")'));
|
| 26 |
+
});
|
| 27 |
+
test('generated Python preserves literal user text and is syntactically valid',()=>{
|
| 28 |
+
const p=make({context:'Quotes " and \'\'\', backslash \\, newlines\n中文 \u2028 and </script>'});
|
| 29 |
+
const script=`import ast,json,sys\ndata=json.load(sys.stdin)\ntree=ast.parse(data['code'])\nnode=next(n for n in tree.body if isinstance(n,ast.Assign) and isinstance(n.targets[0],ast.Name) and n.targets[0].id=='payload')\nassert json.loads(ast.literal_eval(node.value.args[0]))==data['payload']\n`;
|
| 30 |
+
const result=spawnSync('python3',['-c',script],{input:JSON.stringify({code:pythonExample(p),payload:p}),encoding:'utf8'});
|
| 31 |
+
assert.equal(result.status,0,result.stderr);
|
| 32 |
+
});
|
| 33 |
+
test('native results preserve optional confidence separately',()=>{
|
| 34 |
+
const a=parseAnswer({answers:[{name:'q0',type:'predicate',probability:.8}]},make());
|
| 35 |
+
assert.equal(a.title,'yes');assert(Math.abs(a.rows[1].probability-.2)<1e-10);assert.equal(a.confidence,undefined);
|
| 36 |
+
const c=parseAnswer({answers:[{name:'q0',type:'choice',choice:'option_2',confidence:.4,probabilities:[{value:'option_1',probability:.1},{value:'option_2',probability:.9}]}]},make({mode:'choice'}));
|
| 37 |
+
assert.equal(c.title,'High');assert.equal(c.confidence,.4);
|
| 38 |
+
const s=parseAnswer({answers:[{name:'q0',type:'score',score:.9,probabilities:[{value:0,probability:.1},{value:1,probability:.9}]}]},make({mode:'score'}));
|
| 39 |
+
assert.equal(s.title,'0.90 / 1');
|
| 40 |
+
});
|
| 41 |
+
test('malformed results cannot render as successful answers',()=>{
|
| 42 |
+
for(const body of [null,{answers:[]},{answers:[null]},{answers:[{name:'q0',type:'predicate',probability:NaN}]},{answers:[{name:'wrong',type:'predicate',probability:.8}]}])assert.throws(()=>parseAnswer(body,make()),/response/);
|
| 43 |
+
assert.throws(()=>parseAnswer({answers:[{name:'q0',type:'choice',choice:'option_1',probabilities:[{value:'option_1',probability:.5},{value:'option_1',probability:.5}]}]},make({mode:'choice'})),/response/);
|
| 44 |
+
});
|
| 45 |
+
test('browser request uses real origin, no credentials or automatic retries',async()=>{
|
| 46 |
+
let calls=0;
|
| 47 |
+
await assert.rejects(requestDecision(make(),undefined,async(url,options)=>{
|
| 48 |
+
calls++;assert.equal(url,ENDPOINT);assert.equal(options.credentials,'omit');assert.equal(options.headers.Origin,undefined);assert.equal(options.redirect,'error');
|
| 49 |
+
return {ok:false,status:429};
|
| 50 |
+
}),/http_429/);assert.equal(calls,1);
|
| 51 |
+
await assert.rejects(requestDecision(make(),undefined,async()=>({ok:true,json:async()=>{throw new Error('invalid json');}})),/response/);
|
| 52 |
+
});
|
| 53 |
+
test('both locales cover UI labels and all examples create valid payloads',()=>{
|
| 54 |
+
const html=readFileSync(new URL('../index.html',import.meta.url),'utf8');
|
| 55 |
+
for(const locale of ['en','zh']){
|
| 56 |
+
for(const [,key] of html.matchAll(/data-(?:i18n|placeholder)="([^"]+)"/g))assert.equal(typeof words[locale][key],'string',`${locale}:${key}`);
|
| 57 |
+
for(const [mode,question,context,options] of examples[locale])make({mode,question,context,options});
|
| 58 |
+
}
|
| 59 |
+
});
|