Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions kits/dashboard/app/src/lib/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,42 @@ export function chartOption(result, viz, theme) {
// forbidding effective rather than advisory.
option.series = option.series.map(({ data, ...rest }) => rest);
}
fitAxes(option, viz.option);
return { option };
}

/** A value axis's tick: compact from a thousand up (18M, 1.2K), as written below that (0.25, 500). */
export function axisTick(v) {
const n = Number(v);
if (!Number.isFinite(n)) return String(v);
return formatValue(n, { format: Math.abs(n) >= 1000 ? 'compact' : 'decimal' });
}

/** Labels that fit the panel. A horizontal bar chart's value axis ended on "18,000,0": ECharts
* centres the last label on the axis end, and half of a long label hangs past the grid's right
* margin, which containLabel does not count. Each value axis with no formatter of its own gets
* compact ticks; a horizontal one also ends its last label at the axis end. The grid holds its
* labels unless the chart set the grid itself. Whatever the chart wrote is kept. */
function fitAxes(option, own) {
for (const dim of ['xAxis', 'yAxis']) {
if (option[dim] == null) continue;
const fit = (axis) => {
if (!axis || typeof axis !== 'object') return axis;
// ECharts' own defaults: an x axis is a category axis, a y axis a value axis.
if ((axis.type ?? (dim === 'yAxis' ? 'value' : 'category')) !== 'value') return axis;
const label = { ...(axis.axisLabel || {}) };
if (!('formatter' in label)) label.formatter = axisTick;
if (dim === 'xAxis' && !('alignMaxLabel' in label)) label.alignMaxLabel = 'right';
return { ...axis, axisLabel: label };
};
option[dim] = Array.isArray(option[dim]) ? option[dim].map(fit) : fit(option[dim]);
}
if (!(own && 'grid' in own) && option.grid && typeof option.grid === 'object' && !Array.isArray(option.grid)
&& !('containLabel' in option.grid)) {
option.grid = { ...option.grid, containLabel: true };
}
}

/** Deep merge for plain objects; arrays and scalars from `over` replace. The theme sets axis and
* grid defaults, and a chart that specifies its own axis means to replace it, not to blend. */
export function mergeOption(base, over) {
Expand Down
51 changes: 50 additions & 1 deletion kits/dashboard/app/src/lib/dashboard.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
chartOption, formatValue, mergeOption, normalizeLayout, parseDashboard, queriesToRun,
axisTick, chartOption, formatValue, mergeOption, normalizeLayout, parseDashboard, queriesToRun,
statValue, tableView, toFile,
} from './dashboard.js';

Expand Down Expand Up @@ -181,6 +181,55 @@ test('an array in the override replaces rather than merges', () => {
assert.deepEqual(mergeOption({ color: ['a', 'b'] }, { color: ['c'] }), { color: ['c'] });
});

// A horizontal bar chart's value axis ended on "18,000,0" (RNA sequences per database, staging,
// 2026-10-19): ECharts centres the last label on the axis end, and half of a long label hangs past
// the grid's right margin, which containLabel does not count.

const HBAR = { kind: 'chart', option: { xAxis: { type: 'value' }, yAxis: { type: 'category' },
series: [{ type: 'bar', encode: { x: 'revenue', y: 'month' } }] } };

test('a value axis with no formatter of its own gets compact ticks, and a horizontal one ends inside the chart', () => {
const { option } = chartOption(RESULT, HBAR, {});
assert.equal(option.xAxis.axisLabel.formatter, axisTick);
assert.equal(option.xAxis.axisLabel.alignMaxLabel, 'right');
assert.equal(option.yAxis.axisLabel, undefined); // a category axis is left alone
assert.equal(axisTick(18000000), formatValue(18000000, { format: 'compact' }));
assert.ok(axisTick(18000000).length < '18,000,000'.length);
assert.equal(axisTick(0.25), '0.25'); // small numbers keep their digits
assert.equal(axisTick(500), '500');
});

test('a vertical value axis gets compact ticks but keeps its labels where they are', () => {
const viz = { kind: 'chart', option: { yAxis: { type: 'value' }, series: [{ type: 'line' }] } };
const { option } = chartOption(RESULT, viz, { yAxis: { axisLabel: { color: '#888' } } });
assert.equal(option.yAxis.axisLabel.formatter, axisTick);
assert.equal(option.yAxis.axisLabel.color, '#888');
assert.equal(option.yAxis.axisLabel.alignMaxLabel, undefined);
});

test('a formatter or label alignment the chart wrote is kept', () => {
const own = (v) => `${v} seqs`;
const viz = { kind: 'chart', option: { xAxis: { type: 'value', axisLabel: { formatter: own, alignMaxLabel: 'center' } },
series: [{ type: 'bar' }] } };
const { option } = chartOption(RESULT, viz, {});
assert.equal(option.xAxis.axisLabel.formatter, own);
assert.equal(option.xAxis.axisLabel.alignMaxLabel, 'center');
});

test('each of several value axes is fitted the same way', () => {
const viz = { kind: 'chart', option: { yAxis: [{ type: 'value' }, { type: 'value', axisLabel: { formatter: '{value} %' } }],
series: [{ type: 'line' }] } };
const { option } = chartOption(RESULT, viz, {});
assert.equal(option.yAxis[0].axisLabel.formatter, axisTick);
assert.equal(option.yAxis[1].axisLabel.formatter, '{value} %');
});

test('the grid holds its labels unless the chart set the grid itself', () => {
assert.equal(chartOption(RESULT, HBAR, { grid: { left: 8 } }).option.grid.containLabel, true);
const viz = { kind: 'chart', option: { grid: { top: 24, containLabel: false }, series: [{ type: 'line' }] } };
assert.deepEqual(chartOption(RESULT, viz, { grid: { left: 8 } }).option.grid, { left: 8, top: 24, containLabel: false });
});

test('a chart panel with no option says so instead of drawing an empty canvas', () => {
const { error } = chartOption(RESULT, { kind: 'chart', option: null }, {});
assert.match(error, /no chart definition/);
Expand Down
14 changes: 13 additions & 1 deletion kits/sheets/app/src/components/HarnessCell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@
// the server exposes no queue β€” past its concurrency limit turns are accepted and block silently,
// indistinguishable over the API from working ones β€” so a progress bar inside one cell would be
// invented. A spinner is the truth.
import { Fragment } from 'react';
import { Play } from 'lucide-react';
import { FileCard } from 'reifyui';
import { containerFileUrl } from 'reifyui/harness';
import { linkParts } from '../lib/answer';

/** The answer's text with its inline citations drawn as links named by their text ("qdrant.tech"),
* never the brackets and the address. A click opens the page in a new tab and does not select or
* open the cell under it. */
function AnswerText({ value }) {
return linkParts(value).map((p, i) => (p.href
? <a key={i} className="hcell-link" href={p.href} target="_blank" rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}>{p.text}</a>
: <Fragment key={i}>{p.text}</Fragment>));
}

/** The files a cell produced, as the same card the chat draws β€” type glyph, name, download,
* preview. They were a row of green text chips: no glyph, no download, and no way to look inside
Expand Down Expand Up @@ -84,7 +96,7 @@ export function HarnessCell({ cell, live, readOnly, onRun, onPreviewFile }) {
}
return (
<span className="hcell">
<span className="hcell-txt" data-shg-clamp-text>{cell.value}</span>
<span className="hcell-txt" data-shg-clamp-text><AnswerText value={cell.value} /></span>
<Artifacts artifacts={cell.artifacts} onPreview={onPreviewFile} />
{tools}
</span>
Expand Down
27 changes: 27 additions & 0 deletions kits/sheets/app/src/lib/answer.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,30 @@ export function cellAnswer(response) {
export function settledAnswer(response) {
return endsOnToolStep(response) ? '' : cellAnswer(response);
}

// An inline Markdown link: [text](address), the address allowed one level of balanced parentheses
// (Wikipedia's "Rust_(programming_language)").
const MD_LINK = /\[([^[\]\n]+)\]\(((?:[^\s()<>]|\([^\s()<>]*\))+)\)/g;

/** A cell's value as the grid draws it: text parts, and a link part for each inline Markdown link
* whose address is http or https, named by the link's own text. The value itself is unchanged
* (it keeps its citations, and the export carries them as written); this is only how it reads.
* Any other address (javascript:, mailto:, a relative path) stays text, exactly as written. */
export function linkParts(value) {
const text = String(value ?? '');
const parts = [];
let at = 0;
for (const m of text.matchAll(MD_LINK)) {
let href = '';
try {
const u = new URL(m[2]);
if (u.protocol === 'http:' || u.protocol === 'https:') href = m[2];
} catch { /* not an address: stays text */ }
if (!href) continue;
if (m.index > at) parts.push({ text: text.slice(at, m.index) });
parts.push({ text: m[1], href });
at = m.index + m[0].length;
}
if (at < text.length) parts.push({ text: text.slice(at) });
return parts;
}
34 changes: 33 additions & 1 deletion kits/sheets/app/src/lib/answer.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
cellAnswer, endsOnToolStep, lastAssistantText, settledAnswer, stripTracking,
cellAnswer, endsOnToolStep, lastAssistantText, linkParts, settledAnswer, stripTracking,
} from './answer.js';

const msg = (text, annotations = []) =>
Expand Down Expand Up @@ -110,3 +110,35 @@ test('the cell value is the answer with its links cleaned', () => {
] };
assert.equal(cellAnswer(res), 'Open source, Apache 2.0 ([github.com](https://github.com/qdrant/qdrant)).');
});

// ── a cell's links ───────────────────────────────────────────────────────────
// The value keeps its inline citations (cellAnswer above); the grid draws each as a link named by
// its text. Shown raw it read "Zero-ops hybrid retrieval for AI apps ([docs.trychroma.com]…" (staging,
// 2026-10-19).

test('an inline Markdown link becomes a link part named by its text; the text around it stays', () => {
assert.deepEqual(
linkParts('Hybrid search for RAG workloads. ([qdrant.tech](https://qdrant.tech/documentation/search/))'),
[{ text: 'Hybrid search for RAG workloads. (' },
{ text: 'qdrant.tech', href: 'https://qdrant.tech/documentation/search/' },
{ text: ')' }]);
});

test('a link whose address is not http or https stays text, exactly as written', () => {
assert.deepEqual(linkParts('[click](javascript:alert(1))'), [{ text: '[click](javascript:alert(1))' }]);
assert.deepEqual(linkParts('Write to [us](mailto:team@example.com).'), [{ text: 'Write to [us](mailto:team@example.com).' }]);
});

test('plain text is one text part, and an empty value is no parts', () => {
assert.deepEqual(linkParts('Best at production-grade semantic search.'), [{ text: 'Best at production-grade semantic search.' }]);
assert.deepEqual(linkParts(''), []);
assert.deepEqual(linkParts(null), []);
});

test('an address with balanced parentheses keeps them, and two links in a row are two parts', () => {
assert.deepEqual(
linkParts('[Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)) and [docs](https://doc.rust-lang.org)'),
[{ text: 'Rust', href: 'https://en.wikipedia.org/wiki/Rust_(programming_language)' },
{ text: ' and ' },
{ text: 'docs', href: 'https://doc.rust-lang.org' }]);
});
2 changes: 2 additions & 0 deletions kits/sheets/app/src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@
display: -webkit-box; -webkit-box-orient: vertical;
-webkit-line-clamp: var(--shg-clamp, 1); line-clamp: var(--shg-clamp, 1);
}
.hcell-link { font-size: 12px; color: var(--brand-ink); text-decoration: none; }
.hcell-link:hover { text-decoration: underline; }
.hcell.failed .hcell-txt { color: #B91C1C; }
.hcell.skipped .hcell-txt { color: var(--mute-2); font-style: italic; }
.hcell-live { color: var(--mute); font-size: 12px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
Expand Down
Loading