yuhangzang commited on
Commit
7646924
·
verified ·
1 Parent(s): 9c2f297

Add bilingual Valen API playground with text and image examples

Browse files
Files changed (16) hide show
  1. .gitattributes +1 -0
  2. .gitignore +6 -0
  3. NOTICE.md +12 -0
  4. README.md +81 -4
  5. api.mjs +108 -0
  6. app.mjs +141 -0
  7. assets/OFL.txt +192 -0
  8. assets/dmsans.woff2 +0 -0
  9. assets/manifest.json +9 -0
  10. assets/modern-tokens.css +29 -0
  11. assets/valen-mark.png +3 -0
  12. index.html +40 -17
  13. locales.mjs +22 -0
  14. package.json +8 -0
  15. style.css +3 -28
  16. 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: red
6
  sdk: static
7
- pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

  • SHA256: 657c4be26bbe1a08e450afef47d1859acd771cb049a0847ede05b621865d2583
  • Pointer size: 132 Bytes
  • Size of remote file: 1.05 MB
index.html CHANGED
@@ -1,19 +1,42 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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&#10;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
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
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
+ });