86 lines
5.6 KiB
JavaScript
86 lines
5.6 KiB
JavaScript
// The AI workbench's rules (web/modules/ai_task_view.js): folding reads, words, counts, addresses.
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
activityIds, audioResult, duration, flowStages, groupSteps, isVideo, lookTools, outputUrl, outputPicture, pictureKey, picturePreview, uniquePictures, stepDetail, taskCounts, taskResults, taskState, toolKey,
|
|
} from '../web/modules/ai_task_view.js';
|
|
|
|
const look = (tool, ok = true, state = 'done') => ({ kind: 'look', tool, ok, state, summary: tool });
|
|
const act = (tool, kind = 'canvas') => ({ kind, tool, ok: true, state: 'done', summary: `${tool}: K` });
|
|
|
|
// Reads that follow each other fold into one row; a running or failed read and the work stay rows.
|
|
const items = groupSteps([look('search_models'), look('describe_canvas'), act('place_combo'), look('list_combos', false), look('search_images')]);
|
|
assert.deepEqual(items.map(item => item.type), ['looks', 'step', 'step', 'looks']);
|
|
assert.equal(items[0].steps.length, 2);
|
|
const inFlight = groupSteps([look('search_models'), look('describe_canvas', true, 'running')]);
|
|
assert.deepEqual(inFlight.map(item => item.type), ['looks', 'step'], 'an in-flight read stays visible until it finishes');
|
|
assert.deepEqual(groupSteps(undefined), []);
|
|
assert.deepEqual(lookTools([look('a'), look('b'), look('a')]), [{ tool: 'a', count: 2 }, { tool: 'b', count: 1 }]);
|
|
|
|
// Words.
|
|
assert.equal(toolKey('place_combo'), 'aiTool_place_combo');
|
|
assert.equal(toolKey('x<y>'), 'aiTool_xy');
|
|
assert.equal(stepDetail(act('place_combo')), 'K');
|
|
assert.equal(stepDetail({ tool: 'describe_canvas', summary: 'describe_canvas' }), '');
|
|
assert.equal(stepDetail({ tool: 'report', summary: 'something else' }), 'something else');
|
|
|
|
// A task's state and counts.
|
|
assert.equal(taskState({ live: true, errors: 1 }), 'live');
|
|
assert.equal(taskState({ live: true, asks: 1 }), 'asks', 'a question waiting for the user comes first');
|
|
assert.equal(taskState({ waiting: true }), 'waiting', 'a task the user started, before an AI takes it');
|
|
assert.deepEqual(taskCounts({ asks: 2 }), [{ key: 'aiCountAsks', values: { count: 2 } }]);
|
|
assert.equal(taskState({ errors: 2 }), 'error');
|
|
assert.equal(taskState({}), 'idle', 'silence without an explicit end is not completion');
|
|
assert.equal(taskState({ ended: 100 }), 'done');
|
|
assert.deepEqual(taskCounts({ kinds: { canvas: 2, look: 5, speech: 1 }, images: 3, errors: 1 }), [
|
|
{ key: 'aiCountCanvas', values: { count: 2 } }, { key: 'aiCountImages', values: { count: 3 } },
|
|
{ key: 'aiCountSpeech', values: { count: 1 } }, { key: 'aiCountErrors', values: { count: 1 } },
|
|
]);
|
|
assert.deepEqual(taskCounts({}), []);
|
|
|
|
// What a task made, and where it is served.
|
|
const made = taskResults({ steps: [
|
|
{ results: [{ type: 'image', filename: 'a.png', subfolder: 'x' }, { type: 'canvas', activity: 'e1' }] },
|
|
{ results: [{ type: 'model', model_id: 'm' }, { type: 'weird' }] },
|
|
] });
|
|
assert.equal(made.image.length, 1);
|
|
assert.equal(made.canvas[0].activity, 'e1');
|
|
assert.equal(made.model.length, 1);
|
|
assert.equal(outputUrl({ filename: 'a b.png', subfolder: 'x/y' }), '/view?filename=a+b.png&subfolder=x%2Fy&type=output');
|
|
assert.deepEqual(audioResult({ file: 'audio/Aris/a.flac' }), { filename: 'a.flac', subfolder: 'audio/Aris', folder: 'output' });
|
|
assert.deepEqual(audioResult({ file: 'a.flac' }), { filename: 'a.flac', subfolder: '', folder: 'output' });
|
|
assert.equal(isVideo({ filename: 'clip.MP4' }), true);
|
|
assert.equal(isVideo({ filename: 'a.png' }), false);
|
|
|
|
// Output-only reuse: never compare an input/temp file with a same-named output by accident.
|
|
assert.equal(outputPicture({ filename: 'a.png', folder: 'temp' }), null);
|
|
assert.equal(outputPicture({ filename: 'a.mp4' }), null);
|
|
assert.equal(outputPicture({ filename: 'a.flac' }), null);
|
|
assert.deepEqual(outputPicture({ filename: 'a.png', subfolder: 'x' }), { type: 'output', filename: 'a.png', subfolder: 'x' });
|
|
const repeated = uniquePictures([{ filename: 'a.png' }, { filename: 'a.png', folder: 'output' }, { filename: 'a.png', subfolder: 'b' }]);
|
|
assert.equal(repeated.length, 2);
|
|
assert.notEqual(pictureKey(repeated[0]), pictureKey(repeated[1]));
|
|
assert.match(picturePreview({ filename: 'a b.png', subfolder: 'x/y' }), /^\/anomalous\/output_thumbnail\?filename=a\+b.png&subfolder=x%2Fy$/);
|
|
assert.equal(picturePreview({ filename: 'a.mp4' }), outputUrl({ filename: 'a.mp4' }));
|
|
|
|
// How long a step took.
|
|
assert.equal(duration(400), null);
|
|
assert.deepEqual(duration(48_200), { key: 'aiSeconds', values: { count: 48 } });
|
|
assert.deepEqual(duration(150_000), { key: 'aiMinutes', values: { count: 3 } });
|
|
|
|
// The flow: stages in the order they came, each with its last step's state.
|
|
const staged = (id, stage, extra = {}) => ({ id, stage, ok: true, state: 'done', results: [], ...extra });
|
|
const flow = flowStages({ steps: [
|
|
staged('a', 'model'), staged('b', 'prompt'), staged('c', 'model', { state: 'running' }),
|
|
{ id: 'd', kind: 'note', ok: true, results: [{ type: 'image', filename: 'x.png' }] }, { id: 'e', kind: 'user', ok: true },
|
|
staged('f', 'run', { state: 'asking' }), staged('g', 'canvas', { ok: false, state: 'stopped' }),
|
|
] });
|
|
assert.deepEqual(flow.map(item => [item.stage, item.state, item.first, item.count]), [
|
|
['model', 'running', 'a', 2], ['prompt', 'done', 'b', 1], ['picture', 'done', 'd', 1], ['run', 'asking', 'f', 1], ['canvas', 'error', 'g', 1],
|
|
]);
|
|
assert.deepEqual(flowStages(null), []);
|
|
|
|
// The activity entries a task shows: the AI's canvas changes and the user's own.
|
|
assert.deepEqual(activityIds({ steps: [{ results: [{ type: 'canvas', activity: 'a1' }, { type: 'model' }] }, { tool: 'user_activity', activity: 'u1', results: [] }] }), ['a1', 'u1']);
|
|
|
|
console.log('ai_task_view: ok');
|