@manta-eu/horns-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/app/coverage-run.d.ts +5 -0
- package/dist/app/coverage-run.js +221 -0
- package/dist/app/home-page.d.ts +1 -0
- package/dist/app/home-page.js +11 -0
- package/dist/app/horn-page.d.ts +1 -0
- package/dist/app/horn-page.js +38 -0
- package/dist/app/horn-studio.d.ts +30 -0
- package/dist/app/horn-studio.js +428 -0
- package/dist/app/horn-view-page.d.ts +1 -0
- package/dist/app/horn-view-page.js +96 -0
- package/dist/app/horns-page.d.ts +1 -0
- package/dist/app/horns-page.js +43 -0
- package/dist/app/new-horn-page.d.ts +1 -0
- package/dist/app/new-horn-page.js +34 -0
- package/dist/app/paths.d.ts +13 -0
- package/dist/app/paths.js +32 -0
- package/dist/app/routes.d.ts +12 -0
- package/dist/app/routes.js +32 -0
- package/dist/app/rule-page.d.ts +1 -0
- package/dist/app/rule-page.js +84 -0
- package/dist/app/rules-page.d.ts +1 -0
- package/dist/app/rules-page.js +74 -0
- package/dist/app/saved-horn.d.ts +7 -0
- package/dist/app/saved-horn.js +26 -0
- package/dist/app/session-gate.d.ts +6 -0
- package/dist/app/session-gate.js +19 -0
- package/dist/app/shape-entity-page.d.ts +1 -0
- package/dist/app/shape-entity-page.js +68 -0
- package/dist/app/shape-view-studio.d.ts +14 -0
- package/dist/app/shape-view-studio.js +163 -0
- package/dist/app/shape-zoo-page.d.ts +1 -0
- package/dist/app/shape-zoo-page.js +58 -0
- package/dist/app/shapes-page.d.ts +1 -0
- package/dist/app/shapes-page.js +75 -0
- package/dist/app/showcase-page.d.ts +2 -0
- package/dist/app/showcase-page.js +99 -0
- package/dist/app/variable-bar.d.ts +6 -0
- package/dist/app/variable-bar.js +42 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +22 -0
- package/dist/components/app-shell.d.ts +1 -0
- package/dist/components/app-shell.js +18 -0
- package/dist/components/catalog-card-body.d.ts +8 -0
- package/dist/components/catalog-card-body.js +29 -0
- package/dist/components/catalog-card-view.d.ts +2 -0
- package/dist/components/catalog-card-view.js +13 -0
- package/dist/components/chat-column.d.ts +22 -0
- package/dist/components/chat-column.js +20 -0
- package/dist/components/page-header.d.ts +15 -0
- package/dist/components/page-header.js +11 -0
- package/dist/components/plan-runs.d.ts +28 -0
- package/dist/components/plan-runs.js +90 -0
- package/dist/components/rasterize.d.ts +34 -0
- package/dist/components/rasterize.js +115 -0
- package/dist/components/read-the-code.d.ts +14 -0
- package/dist/components/read-the-code.js +43 -0
- package/dist/components/snapshot-view.d.ts +12 -0
- package/dist/components/snapshot-view.js +25 -0
- package/dist/components/states.d.ts +23 -0
- package/dist/components/states.js +22 -0
- package/dist/components/studio-chat.d.ts +47 -0
- package/dist/components/studio-chat.js +63 -0
- package/dist/components/written-horn-view.d.ts +15 -0
- package/dist/components/written-horn-view.js +25 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +16 -0
- package/dist/lib/horn-review.d.ts +27 -0
- package/dist/lib/horn-review.js +59 -0
- package/dist/lib/manta/__tests__/sdk-realm.d.ts +2 -0
- package/dist/lib/manta/__tests__/sdk-realm.js +17 -0
- package/dist/lib/manta/ldo.d.ts +6 -0
- package/dist/lib/manta/ldo.js +14 -0
- package/dist/lib/manta/providers.d.ts +10 -0
- package/dist/lib/manta/providers.js +48 -0
- package/dist/lib/manta/session.d.ts +47 -0
- package/dist/lib/manta/session.js +204 -0
- package/dist/lib/manta/sparql-select.d.ts +2 -0
- package/dist/lib/manta/sparql-select.js +19 -0
- package/dist/lib/plan-runs.d.ts +14 -0
- package/dist/lib/plan-runs.js +26 -0
- package/dist/lib/problem.d.ts +8 -0
- package/dist/lib/problem.js +16 -0
- package/dist/lib/shapes/catalog.d.ts +14 -0
- package/dist/lib/shapes/catalog.js +25 -0
- package/dist/lib/shapes/ldkit-source.d.ts +14 -0
- package/dist/lib/shapes/ldkit-source.js +26 -0
- package/dist/lib/studio-progress.d.ts +7 -0
- package/dist/lib/studio-progress.js +36 -0
- package/dist/lib/telemetry/sentry.d.ts +1 -0
- package/dist/lib/telemetry/sentry.js +17 -0
- package/dist/lib/theme.d.ts +2 -0
- package/dist/lib/theme.js +7 -0
- package/dist/lib/time-ago.d.ts +5 -0
- package/dist/lib/time-ago.js +35 -0
- package/dist/lib/written-surface.d.ts +9 -0
- package/dist/lib/written-surface.js +10 -0
- package/dist/render/horn-client.d.ts +1 -0
- package/dist/render/horn-client.js +10 -0
- package/dist/styles.css +2 -0
- package/package.json +77 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { planStatusQuery, readPlanStatus } from "@manta-eu/horns/render";
|
|
4
|
+
import { planSummary } from "@manta-eu/horns-kit";
|
|
5
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@manta-eu/react";
|
|
6
|
+
import { browserSparqlSelect } from "../lib/manta/sparql-select";
|
|
7
|
+
import { planPollAttempts, planPollInterval, runSeen, runSettled, startPlanRun } from "../lib/plan-runs";
|
|
8
|
+
/**
|
|
9
|
+
* Previews and runs of the action plans a Horn shows. A preview starts straight away: it changes nothing. A real
|
|
10
|
+
* run waits for a yes in `PlanRunDialog`. Either way the graph is then asked every few seconds how the run went,
|
|
11
|
+
* and a real run that finished redraws the Horn, because the things it changed now look different.
|
|
12
|
+
*/
|
|
13
|
+
export function usePlanRuns(args) {
|
|
14
|
+
const { datastoreId, realm, onChanged } = args;
|
|
15
|
+
const [status, setStatus] = useState({});
|
|
16
|
+
const [pending, setPending] = useState(new Set());
|
|
17
|
+
const [confirming, setConfirming] = useState();
|
|
18
|
+
const [problem, setProblem] = useState();
|
|
19
|
+
const alive = useRef(true);
|
|
20
|
+
const changed = useRef(onChanged);
|
|
21
|
+
changed.current = onChanged;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
alive.current = true;
|
|
24
|
+
return () => {
|
|
25
|
+
alive.current = false;
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
const follow = async (plan, preview, startedBefore) => {
|
|
29
|
+
const select = browserSparqlSelect(datastoreId);
|
|
30
|
+
for (let attempt = 0; attempt < planPollAttempts && alive.current; attempt += 1) {
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, planPollInterval));
|
|
32
|
+
if (!alive.current)
|
|
33
|
+
return;
|
|
34
|
+
const now = await select(planStatusQuery(realm, [plan.iri]))
|
|
35
|
+
.then((results) => readPlanStatus(results).get(plan.iri))
|
|
36
|
+
.catch(() => undefined);
|
|
37
|
+
if (now === undefined || !runSeen(now, startedBefore))
|
|
38
|
+
continue;
|
|
39
|
+
setStatus((before) => ({ ...before, [plan.iri]: now }));
|
|
40
|
+
if (runSettled(now, startedBefore)) {
|
|
41
|
+
if (!preview && now.state === "completed")
|
|
42
|
+
changed.current();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const start = async (plan, preview) => {
|
|
48
|
+
const startedBefore = (status[plan.iri] ?? plan).lastRun?.startedAt;
|
|
49
|
+
setProblem(undefined);
|
|
50
|
+
setPending((before) => new Set([...before, plan.iri]));
|
|
51
|
+
try {
|
|
52
|
+
await startPlanRun({ datastoreId, plan: plan.iri, preview });
|
|
53
|
+
setStatus((before) => ({
|
|
54
|
+
...before,
|
|
55
|
+
[plan.iri]: { ...(before[plan.iri] ?? plan), state: "running", lastRun: { startedAt: "", preview } },
|
|
56
|
+
}));
|
|
57
|
+
void follow(plan, preview, startedBefore);
|
|
58
|
+
}
|
|
59
|
+
catch (caught) {
|
|
60
|
+
setProblem(caught instanceof Error ? caught.message : String(caught));
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
setPending((before) => new Set([...before].filter((one) => one !== plan.iri)));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
actions: {
|
|
68
|
+
onPreview: (plan) => void start(plan, true),
|
|
69
|
+
onRun: (plan) => setConfirming(plan),
|
|
70
|
+
status,
|
|
71
|
+
pending,
|
|
72
|
+
},
|
|
73
|
+
confirming,
|
|
74
|
+
confirm: () => {
|
|
75
|
+
const plan = confirming;
|
|
76
|
+
setConfirming(undefined);
|
|
77
|
+
if (plan !== undefined)
|
|
78
|
+
void start(plan, false);
|
|
79
|
+
},
|
|
80
|
+
cancel: () => setConfirming(undefined),
|
|
81
|
+
problem,
|
|
82
|
+
dismiss: () => setProblem(undefined),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function PlanRunDialog(props) {
|
|
86
|
+
const { runs, labels } = props;
|
|
87
|
+
const plan = runs.confirming;
|
|
88
|
+
const count = plan?.changes.length ?? 0;
|
|
89
|
+
return (_jsx(Dialog, { open: plan !== undefined, onOpenChange: (open) => (open ? undefined : runs.cancel()), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { text: count === 1 ? "Make this change?" : `Make these ${count} changes?` }), _jsxs(DialogDescription, { children: [plan === undefined ? "" : planSummary(plan, labels), " Manta will change them in the platforms they came from, the same way the preview checked. This cannot be undone from here."] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: runs.cancel, children: "Not now" }), _jsx(Button, { variant: "destructive", onClick: runs.confirm, children: "Make the changes" })] })] }) }));
|
|
90
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type HornSnapshot } from "@manta-eu/horns/render";
|
|
2
|
+
/** A view that would not draw, and what it said. */
|
|
3
|
+
export type RenderProblem = {
|
|
4
|
+
viewId: string;
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
export type Rasterized = {
|
|
8
|
+
png: Blob;
|
|
9
|
+
problems: RenderProblem[];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The Horn drawn off-screen at the capture width, handed back as a PNG with whatever would not draw.
|
|
13
|
+
*
|
|
14
|
+
* The problems come free: this is a real draw of every view, so a view that throws throws here. It is the
|
|
15
|
+
* only place the app draws a Horn where somebody is waiting for the answer.
|
|
16
|
+
*/
|
|
17
|
+
export declare function rasterizeHorn(snapshot: HornSnapshot): Promise<Rasterized>;
|
|
18
|
+
export type ScreenshotKept = {
|
|
19
|
+
ok: true;
|
|
20
|
+
capturedAt?: string;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
error: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Take the picture and hand it to the worker, which stores it where Manta keeps files. Never throws: a Horn
|
|
27
|
+
* that saved is saved whether or not anyone managed to photograph it, so a caller after a save can ignore the
|
|
28
|
+
* answer and a caller who asked for it can show what went wrong.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sendHornScreenshot(args: {
|
|
31
|
+
datastoreId: string;
|
|
32
|
+
slug: string;
|
|
33
|
+
snapshot: HornSnapshot;
|
|
34
|
+
}): Promise<ScreenshotKept>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { toBlob } from "html-to-image";
|
|
4
|
+
import { captureDeviceScaleFactor, captureWidth } from "@manta-eu/horns/render";
|
|
5
|
+
import { HornSnapshotView } from "@manta-eu/horns-kit";
|
|
6
|
+
import { makeWrittenHornView } from "./written-horn-view";
|
|
7
|
+
import { isRecord } from "@manta-eu/horns";
|
|
8
|
+
import { problemOf } from "../lib/problem";
|
|
9
|
+
// A picture of a Horn, taken by the browser that already has it.
|
|
10
|
+
//
|
|
11
|
+
// The browser is the only thing that can draw a Horn now: a view is model-written TSX composing
|
|
12
|
+
// `@manta-eu/react`, so it needs a transpiler, the design system and a stylesheet. `html-to-image` clones the
|
|
13
|
+
// drawn node, inlines the computed style of every element and embeds the fonts, which is what carries the
|
|
14
|
+
// design system into the picture — serializing the markup alone would lose all of it.
|
|
15
|
+
//
|
|
16
|
+
// Drawn off-screen at the capture width rather than photographing what is on screen, so the picture is the
|
|
17
|
+
// same whatever size the window is and whatever the page around it is doing.
|
|
18
|
+
// A tall Horn would otherwise produce a picture measured in megabytes; a card shows the top of it anyway.
|
|
19
|
+
const maxCaptureHeight = 2000;
|
|
20
|
+
/** Long enough for a written view to transpile and draw; past it the picture is taken of whatever is there. */
|
|
21
|
+
const settleTimeoutMs = 8000;
|
|
22
|
+
/** No mutation for this long means the views have finished drawing. */
|
|
23
|
+
const quietMs = 150;
|
|
24
|
+
/**
|
|
25
|
+
* Waits for the off-screen tree to stop changing.
|
|
26
|
+
*
|
|
27
|
+
* A written view draws after a dynamic import and two effects, and nothing in the kit says when it is done, so
|
|
28
|
+
* quiet is the signal: no mutation for `quietMs`, and the deadline wins either way. A picture of a view still
|
|
29
|
+
* showing its placeholder is worth more than no picture.
|
|
30
|
+
*/
|
|
31
|
+
function settled(node) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
let quiet;
|
|
34
|
+
const observer = new MutationObserver(() => {
|
|
35
|
+
clearTimeout(quiet);
|
|
36
|
+
quiet = setTimeout(done, quietMs);
|
|
37
|
+
});
|
|
38
|
+
const done = () => {
|
|
39
|
+
clearTimeout(quiet);
|
|
40
|
+
clearTimeout(deadline);
|
|
41
|
+
observer.disconnect();
|
|
42
|
+
resolve();
|
|
43
|
+
};
|
|
44
|
+
const deadline = setTimeout(done, settleTimeoutMs);
|
|
45
|
+
quiet = setTimeout(done, quietMs);
|
|
46
|
+
observer.observe(node, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The Horn drawn off-screen at the capture width, handed back as a PNG with whatever would not draw.
|
|
51
|
+
*
|
|
52
|
+
* The problems come free: this is a real draw of every view, so a view that throws throws here. It is the
|
|
53
|
+
* only place the app draws a Horn where somebody is waiting for the answer.
|
|
54
|
+
*/
|
|
55
|
+
export async function rasterizeHorn(snapshot) {
|
|
56
|
+
const problems = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const writtenView = makeWrittenHornView((problem) => {
|
|
59
|
+
if (seen.has(problem.viewId))
|
|
60
|
+
return;
|
|
61
|
+
seen.add(problem.viewId);
|
|
62
|
+
problems.push(problem);
|
|
63
|
+
});
|
|
64
|
+
const holder = document.createElement("div");
|
|
65
|
+
holder.setAttribute("aria-hidden", "true");
|
|
66
|
+
holder.style.position = "fixed";
|
|
67
|
+
holder.style.top = "0";
|
|
68
|
+
holder.style.left = "-20000px";
|
|
69
|
+
holder.style.width = `${captureWidth}px`;
|
|
70
|
+
holder.style.pointerEvents = "none";
|
|
71
|
+
document.body.append(holder);
|
|
72
|
+
const root = createRoot(holder);
|
|
73
|
+
try {
|
|
74
|
+
root.render(_jsx(HornSnapshotView, { snapshot: snapshot, mode: "capture", writtenView: writtenView }));
|
|
75
|
+
await settled(holder);
|
|
76
|
+
const drawing = holder.firstElementChild;
|
|
77
|
+
if (!(drawing instanceof HTMLElement))
|
|
78
|
+
throw new Error("The Horn drew nothing to take a picture of.");
|
|
79
|
+
const height = Math.min(Math.ceil(drawing.getBoundingClientRect().height), maxCaptureHeight);
|
|
80
|
+
const png = await toBlob(drawing, {
|
|
81
|
+
width: captureWidth,
|
|
82
|
+
height,
|
|
83
|
+
pixelRatio: captureDeviceScaleFactor,
|
|
84
|
+
// The clone is measured from the top: a Horn taller than the cap shows its first screens, not a squeeze.
|
|
85
|
+
style: { margin: "0", height: `${height}px` },
|
|
86
|
+
});
|
|
87
|
+
if (png === null)
|
|
88
|
+
throw new Error("The picture could not be encoded.");
|
|
89
|
+
return { png, problems };
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
root.unmount();
|
|
93
|
+
holder.remove();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Take the picture and hand it to the worker, which stores it where Manta keeps files. Never throws: a Horn
|
|
98
|
+
* that saved is saved whether or not anyone managed to photograph it, so a caller after a save can ignore the
|
|
99
|
+
* answer and a caller who asked for it can show what went wrong.
|
|
100
|
+
*/
|
|
101
|
+
export async function sendHornScreenshot(args) {
|
|
102
|
+
try {
|
|
103
|
+
const { png } = await rasterizeHorn(args.snapshot);
|
|
104
|
+
const response = await fetch(`/api/d/${encodeURIComponent(args.datastoreId)}/horns/${encodeURIComponent(args.slug)}/screenshot`, { method: "POST", headers: { "content-type": "image/png" }, body: png });
|
|
105
|
+
const body = await response.json().catch(() => undefined);
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
return { ok: false, error: problemOf(body, `The picture was not kept (${response.status}).`) };
|
|
108
|
+
}
|
|
109
|
+
const capturedAt = isRecord(body) && typeof body["capturedAt"] === "string" ? body["capturedAt"] : undefined;
|
|
110
|
+
return { ok: true, ...(capturedAt === undefined ? {} : { capturedAt }) };
|
|
111
|
+
}
|
|
112
|
+
catch (caught) {
|
|
113
|
+
return { ok: false, error: caught instanceof Error ? caught.message : String(caught) };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReviewState } from "@manta-eu/horns/render";
|
|
2
|
+
import type { Horn } from "@manta-eu/horns";
|
|
3
|
+
import type { VisualEdit } from "@manta-eu/horns/studio";
|
|
4
|
+
export type ReadTheCodeProps = {
|
|
5
|
+
viewId: string;
|
|
6
|
+
title: string;
|
|
7
|
+
/** The code as it would run. */
|
|
8
|
+
source: string;
|
|
9
|
+
state: ReviewState["state"];
|
|
10
|
+
onKeep: (edit: VisualEdit) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare function ReadTheCode({ viewId, title, source, state, onKeep }: ReadTheCodeProps): import("react").JSX.Element;
|
|
13
|
+
/** The review state of every written view in a draft, by view id, for the Studio to show on each card. */
|
|
14
|
+
export declare function writtenReviewStates(horn: Horn): Promise<Record<string, ReviewState["state"]>>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@manta-eu/react";
|
|
4
|
+
import { reviewOf, reviewStateOf } from "@manta-eu/horns/render";
|
|
5
|
+
const wording = {
|
|
6
|
+
unread: {
|
|
7
|
+
badge: "Not read yet",
|
|
8
|
+
title: "Read the code before you keep it",
|
|
9
|
+
lead: "The Studio wrote this view. It draws with Manta's own components and can reach nothing else, but nobody has read what it does yet. This Horn will not save until someone has.",
|
|
10
|
+
action: "I have read this — keep it",
|
|
11
|
+
},
|
|
12
|
+
stale: {
|
|
13
|
+
badge: "Changed since it was read",
|
|
14
|
+
title: "This changed after it was read",
|
|
15
|
+
lead: "Someone read an earlier version of this view and kept it; it has been rewritten since. What was read is not what would run, so it needs reading again.",
|
|
16
|
+
action: "I have read this — keep it",
|
|
17
|
+
},
|
|
18
|
+
reviewed: {
|
|
19
|
+
badge: "Read and kept",
|
|
20
|
+
title: "This code has been read",
|
|
21
|
+
lead: "Someone read this exact code and kept it. Change a character and it will need reading again.",
|
|
22
|
+
action: "Close",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export function ReadTheCode({ viewId, title, source, state, onKeep }) {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const words = wording[state];
|
|
28
|
+
async function keep() {
|
|
29
|
+
onKeep({ kind: "keep-code", viewId, reviewed: await reviewOf(source, new Date()) });
|
|
30
|
+
setOpen(false);
|
|
31
|
+
}
|
|
32
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: state === "reviewed" ? "ghost" : "secondary", size: "xs", onClick: () => setOpen(true), "aria-label": `${words.title}: ${title}`, children: words.badge }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { text: words.title }) }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { children: title }), _jsx("span", { className: "text-muted-foreground text-sm", children: words.badge })] }), _jsx("p", { className: "text-muted-foreground m-0 text-sm", children: words.lead }), _jsx("pre", { className: "border-muted-border max-h-[50vh] overflow-auto rounded-lg border p-3 text-xs leading-relaxed", children: _jsx("code", { children: source }) }), _jsx("div", { className: "flex justify-end gap-2", children: state === "reviewed" ? (_jsx(Button, { onClick: () => setOpen(false), children: words.action })) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: () => setOpen(false), children: "Not yet" }), _jsx(Button, { onClick: () => void keep(), children: words.action })] })) })] })] }) })] }));
|
|
33
|
+
}
|
|
34
|
+
/** The review state of every written view in a draft, by view id, for the Studio to show on each card. */
|
|
35
|
+
export async function writtenReviewStates(horn) {
|
|
36
|
+
const pairs = await Promise.all(horn.views.map(async (view) => {
|
|
37
|
+
if ("ref" in view || view.render.kind !== "tsx")
|
|
38
|
+
return [];
|
|
39
|
+
const { state } = await reviewStateOf(view.render);
|
|
40
|
+
return [[view.id, state]];
|
|
41
|
+
}));
|
|
42
|
+
return Object.fromEntries(pairs.flat());
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HornSnapshot, SnapshotView } from "@manta-eu/horns/render";
|
|
2
|
+
/**
|
|
3
|
+
* One view of a snapshot, drawn on its own.
|
|
4
|
+
*
|
|
5
|
+
* `HornSnapshotView` draws a whole Horn with its frame; a zoo card, a coverage run and an entity page each
|
|
6
|
+
* want one view and their own chrome around it. Both go through `writtenHornView`, so what a person reviews
|
|
7
|
+
* on one page is what they see on the others.
|
|
8
|
+
*/
|
|
9
|
+
export declare function SnapshotViewBody(props: {
|
|
10
|
+
snapshot: HornSnapshot;
|
|
11
|
+
view: SnapshotView;
|
|
12
|
+
}): string | number | bigint | boolean | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { entityLinks } from "@manta-eu/horns-kit";
|
|
3
|
+
import { ErrorState } from "./states";
|
|
4
|
+
import { writtenHornView } from "./written-horn-view";
|
|
5
|
+
/**
|
|
6
|
+
* One view of a snapshot, drawn on its own.
|
|
7
|
+
*
|
|
8
|
+
* `HornSnapshotView` draws a whole Horn with its frame; a zoo card, a coverage run and an entity page each
|
|
9
|
+
* want one view and their own chrome around it. Both go through `writtenHornView`, so what a person reviews
|
|
10
|
+
* on one page is what they see on the others.
|
|
11
|
+
*/
|
|
12
|
+
export function SnapshotViewBody(props) {
|
|
13
|
+
const { snapshot, view } = props;
|
|
14
|
+
const { content } = view;
|
|
15
|
+
if (content.lane === "error")
|
|
16
|
+
return _jsx(ErrorState, { title: view.title, hint: content.message });
|
|
17
|
+
if (content.lane !== "sandbox")
|
|
18
|
+
return null;
|
|
19
|
+
const context = {
|
|
20
|
+
labels: snapshot.labels,
|
|
21
|
+
links: entityLinks(snapshot),
|
|
22
|
+
datastoreId: snapshot.horn.datastoreId,
|
|
23
|
+
};
|
|
24
|
+
return writtenHornView(content, view, context);
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
type EmptyStateProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
/** What to do about it: an empty state that offers the way out of itself. */
|
|
7
|
+
action?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
export declare function EmptyState(props: EmptyStateProps): import("react").JSX.Element;
|
|
10
|
+
export declare function ErrorState({ title, hint }: {
|
|
11
|
+
title: string;
|
|
12
|
+
hint?: string;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
export declare function Loading({ label, rows }: {
|
|
15
|
+
label?: string;
|
|
16
|
+
rows?: number;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
export declare function DatastoreMissing(): import("react").JSX.Element;
|
|
19
|
+
export declare function BackLink({ to, label }: {
|
|
20
|
+
to: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
import { CircleAlert, Inbox } from "lucide-react";
|
|
5
|
+
import { Skeleton } from "@manta-eu/react";
|
|
6
|
+
export function EmptyState(props) {
|
|
7
|
+
const { title, hint, icon, action } = props;
|
|
8
|
+
return (_jsxs("div", { "data-empty-state": true, className: "flex flex-col items-center gap-2 rounded-lg border border-default-border bg-card px-6 py-12 text-center", children: [_jsx("span", { className: "text-muted-foreground", "aria-hidden": "true", children: icon ?? _jsx(Inbox, { size: 20 }) }), _jsx("p", { className: "font-medium text-default-foreground", children: title }), isPresent(hint) ? _jsx("p", { className: "max-w-md text-sm text-muted-foreground", children: hint }) : null, isPresent(action) ? _jsx("div", { className: "mt-2", children: action }) : null] }));
|
|
9
|
+
}
|
|
10
|
+
export function ErrorState({ title, hint }) {
|
|
11
|
+
return (_jsxs("div", { role: "alert", "data-error-state": true, className: "flex items-start gap-3 rounded-lg border border-error bg-error/10 px-4 py-3 text-sm", children: [_jsx(CircleAlert, { className: "mt-0.5 shrink-0 text-[var(--color-error)]", size: 16, "aria-hidden": "true" }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("p", { className: "font-medium text-default-foreground", children: title }), isPresent(hint) ? _jsx("p", { className: "text-muted-foreground", children: hint }) : null] })] }));
|
|
12
|
+
}
|
|
13
|
+
export function Loading({ label = "Loading…", rows = 3 }) {
|
|
14
|
+
return (_jsx("div", { "data-loading": true, className: "flex flex-col gap-3", "aria-busy": "true", "aria-label": label, children: Array.from({ length: rows }, (_unused, index) => (_jsx(Skeleton, { className: "h-12 w-full rounded-lg" }, index))) }));
|
|
15
|
+
}
|
|
16
|
+
// Every datastore-scoped page refuses the same way, because the session's grants decide what exists.
|
|
17
|
+
export function DatastoreMissing() {
|
|
18
|
+
return (_jsx(EmptyState, { title: "This datastore is not available to your account.", hint: "Pick another datastore from the sidebar, or ask an operator for access." }));
|
|
19
|
+
}
|
|
20
|
+
export function BackLink({ to, label }) {
|
|
21
|
+
return (_jsx(Link, { className: "text-sm text-muted-foreground hover:text-default-foreground", to: to, children: label }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { HornChange } from "@manta-eu/horns/studio";
|
|
2
|
+
import type { StudioStep } from "../lib/studio-progress";
|
|
3
|
+
/**
|
|
4
|
+
* One line of a Studio conversation. An answer that changed the Horn carries the change and the version it
|
|
5
|
+
* made, which is what its card previews; what the turn did on the way there is kept as its thinking.
|
|
6
|
+
*/
|
|
7
|
+
export type StudioEntry = {
|
|
8
|
+
kind: "user";
|
|
9
|
+
content: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "assistant";
|
|
12
|
+
content: string;
|
|
13
|
+
thinking: readonly StudioStep[];
|
|
14
|
+
seconds: number;
|
|
15
|
+
change?: {
|
|
16
|
+
summary: HornChange;
|
|
17
|
+
versionId: number;
|
|
18
|
+
};
|
|
19
|
+
/** What a second model said about the picture of what was drawn. Advice, never a verdict. */
|
|
20
|
+
advice?: readonly string[];
|
|
21
|
+
/** The turn ran out of steps with work left. */
|
|
22
|
+
unfinished?: boolean;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "failure";
|
|
25
|
+
content: string;
|
|
26
|
+
};
|
|
27
|
+
type StudioChatProps = {
|
|
28
|
+
entries: readonly StudioEntry[];
|
|
29
|
+
pending: boolean;
|
|
30
|
+
/** What the running turn has done so far. */
|
|
31
|
+
progress: readonly StudioStep[];
|
|
32
|
+
/** The version the preview shows. */
|
|
33
|
+
previewing: number;
|
|
34
|
+
welcome: string;
|
|
35
|
+
/** Said under the welcome: what to do, and that trying something is safe. */
|
|
36
|
+
reassurance: string;
|
|
37
|
+
starters: readonly string[];
|
|
38
|
+
placeholder: string;
|
|
39
|
+
onSend: (text: string) => void;
|
|
40
|
+
onPreview: (versionId: number) => void;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The Studio's conversation as a rail beside the preview: what was asked, what the turn did while it thought,
|
|
44
|
+
* what it said, and a card for each change that can put that version back in the preview.
|
|
45
|
+
*/
|
|
46
|
+
export declare function StudioChat(props: StudioChatProps): import("react").JSX.Element;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
4
|
+
import { ArrowUp, CircleAlert, Eye, GitCommitHorizontal } from "lucide-react";
|
|
5
|
+
import { Button, ChatConversationStarters, ChatInput, ChatInputSubmit, ChatInputTextarea, ChatMessage, ChatReasoning, ChatWelcome, cn, } from "@manta-eu/react";
|
|
6
|
+
/**
|
|
7
|
+
* One step, with what it was actually on folded under it.
|
|
8
|
+
*
|
|
9
|
+
* "Querying the datastore" fifteen times says nothing. The query itself is too long for a line and too
|
|
10
|
+
* useful to drop, so it sits under the step for whoever wants it.
|
|
11
|
+
*/
|
|
12
|
+
function stepItem(step) {
|
|
13
|
+
return {
|
|
14
|
+
icon: null,
|
|
15
|
+
title: step.title,
|
|
16
|
+
...(step.detail === undefined
|
|
17
|
+
? {}
|
|
18
|
+
: {
|
|
19
|
+
children: (_jsx("pre", { className: "text-muted-foreground mt-1 max-h-48 overflow-auto rounded-md bg-muted/40 p-2 font-mono text-[11px] leading-relaxed whitespace-pre-wrap", children: step.detail })),
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What the reviewer saw, and the one button that acts on it.
|
|
25
|
+
*
|
|
26
|
+
* The reviewer is a second model looking at a picture of what was drawn, so its advice is a reading rather
|
|
27
|
+
* than a verdict — the person decides whether it is worth another turn.
|
|
28
|
+
*/
|
|
29
|
+
function ReviewCard({ advice, onSend }) {
|
|
30
|
+
return (_jsxs("section", { "data-review-card": true, className: "border-default-border flex flex-col gap-2 rounded-lg border p-3", children: [_jsxs("h3", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx(Eye, { size: 14, "aria-hidden": "true", className: "text-muted-foreground" }), "What a second look found"] }), _jsx("ul", { className: "text-muted-foreground flex list-disc flex-col gap-1 pl-4 text-sm", children: advice.map((line) => (_jsx("li", { children: line }, line))) }), _jsx(Button, { size: "sm", variant: "secondary", className: "self-start", onClick: () => onSend(fixRequest(advice)), children: "Fix these" })] }));
|
|
31
|
+
}
|
|
32
|
+
function fixRequest(advice) {
|
|
33
|
+
return `Fix these, and keep everything else as it is:\n${advice.map((line) => `- ${line}`).join("\n")}`;
|
|
34
|
+
}
|
|
35
|
+
function ChangeCard(props) {
|
|
36
|
+
const { change, previewing, onPreview } = props;
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
return (_jsxs("section", { "data-change-card": true, className: cn("flex flex-col gap-3 rounded-xl border bg-base-background p-3", previewing ? "border-primary" : "border-muted-border"), children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(GitCommitHorizontal, { size: 16, "aria-hidden": "true", className: "mt-0.5 shrink-0 text-muted-foreground" }), _jsx("p", { className: "text-sm font-medium text-default-foreground", children: change.title })] }), open ? (_jsx("ul", { className: "flex list-disc flex-col gap-1 pl-9 text-xs text-muted-foreground", children: change.details.map((detail) => (_jsx("li", { children: detail }, detail))) })) : null, _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(Button, { variant: "secondary", size: "sm", "aria-expanded": open, onClick: () => setOpen((was) => !was), children: "Details" }), _jsx(Button, { variant: previewing ? "tertiary" : "secondary", size: "sm", disabled: previewing, onClick: onPreview, children: previewing ? "Previewing" : "Preview" })] })] }));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The Studio's conversation as a rail beside the preview: what was asked, what the turn did while it thought,
|
|
42
|
+
* what it said, and a card for each change that can put that version back in the preview.
|
|
43
|
+
*/
|
|
44
|
+
export function StudioChat(props) {
|
|
45
|
+
const { entries, pending, progress, previewing, welcome, reassurance, starters, placeholder, onSend, onPreview } = props;
|
|
46
|
+
const scroller = useRef(null);
|
|
47
|
+
// The newest line is the one being read, so the rail follows it down.
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const element = scroller.current;
|
|
50
|
+
if (isPresent(element))
|
|
51
|
+
element.scrollTop = element.scrollHeight;
|
|
52
|
+
}, [entries, progress]);
|
|
53
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { ref: scroller, "data-chat-history": true, className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4", children: [!isPresent(entries) && !pending ? (_jsxs(_Fragment, { children: [_jsx(ChatWelcome, { channel: "MANTA", title: welcome, subtitle: reassurance }), _jsx(ChatConversationStarters, { starters: starters.map((title, index) => ({ id: `starter-${index}`, title })), onStarterClick: (starter) => onSend(starter.title) })] })) : null, entries.map((entry, index) => {
|
|
54
|
+
if (entry.kind === "user") {
|
|
55
|
+
return (_jsx(ChatMessage, { role: "user", children: entry.content }, index));
|
|
56
|
+
}
|
|
57
|
+
if (entry.kind === "failure") {
|
|
58
|
+
return (_jsxs("p", { role: "alert", className: "flex items-start gap-2 text-sm text-muted-foreground", children: [_jsx(CircleAlert, { size: 16, "aria-hidden": "true", className: "mt-0.5 shrink-0 text-[var(--color-error)]" }), entry.content] }, index));
|
|
59
|
+
}
|
|
60
|
+
const change = entry.change;
|
|
61
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [isPresent(entry.thinking) ? (_jsx(ChatReasoning, { phase: "done", text: "Finished thinking", seconds: entry.seconds, items: entry.thinking.map(stepItem) })) : null, _jsx(ChatMessage, { role: "assistant", children: entry.content }), isPresent(change) ? (_jsx(ChangeCard, { change: change.summary, previewing: previewing === change.versionId, onPreview: () => onPreview(change.versionId) })) : null, isPresent(entry.advice) ? _jsx(ReviewCard, { advice: entry.advice, onSend: onSend }) : null, entry.unfinished === true ? (_jsx(Button, { size: "sm", variant: "secondary", className: "self-start", onClick: () => onSend("Continue where you left off."), children: "Keep going" })) : null] }, index));
|
|
62
|
+
}), pending ? (_jsx(ChatReasoning, { phase: "pending", text: "Thinking", items: (isPresent(progress) ? progress : [{ title: "Sending" }]).map(stepItem) })) : null] }), _jsx("div", { className: "shrink-0 border-t border-muted-border p-3", children: _jsxs(ChatInput, { onSubmit: (value) => onSend(value), children: [_jsx(ChatInputTextarea, { placeholder: placeholder, "aria-label": placeholder }), _jsx(ChatInputSubmit, { icon: ArrowUp, "aria-label": "Send", isLoading: pending, disabled: pending })] }) })] }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HornSnapshotViewProps } from "@manta-eu/horns-kit";
|
|
2
|
+
/**
|
|
3
|
+
* Draws a written view, telling `onError` when one will not draw.
|
|
4
|
+
*
|
|
5
|
+
* A view that throws is caught by the kit's boundary and shown to the person, and nothing else ever hears
|
|
6
|
+
* about it — so the Studio cannot fix what it just wrote. A caller that wants to know passes `onError`; the
|
|
7
|
+
* off-screen draw the picture is taken from passes one, which is how a broken view reaches the next turn.
|
|
8
|
+
*/
|
|
9
|
+
export declare function makeWrittenHornView(onError?: (problem: {
|
|
10
|
+
viewId: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}) => void): NonNullable<HornSnapshotViewProps["writtenView"]>;
|
|
13
|
+
export declare const writtenHornView: (content: Extract<import("@manta-eu/horns/render").SnapshotView["content"], {
|
|
14
|
+
lane: "sandbox";
|
|
15
|
+
}>, view: import("@manta-eu/horns/render").SnapshotView, context: import("@manta-eu/horns-kit").WrittenViewContext) => import("react").ReactNode;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { lazy, Suspense } from "react";
|
|
3
|
+
import { Loader } from "@manta-eu/react";
|
|
4
|
+
import { loadWrittenSurface, writtenHooks } from "../lib/written-surface";
|
|
5
|
+
// Behind `lazy` because the runtime carries a transpiler; the server's placeholder stays until it resolves.
|
|
6
|
+
// Not passed on the capture path, which runs with JavaScript off.
|
|
7
|
+
const WrittenView = lazy(async () => {
|
|
8
|
+
const written = await import("@manta-eu/horns-kit/written");
|
|
9
|
+
return { default: written.WrittenView };
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Draws a written view, telling `onError` when one will not draw.
|
|
13
|
+
*
|
|
14
|
+
* A view that throws is caught by the kit's boundary and shown to the person, and nothing else ever hears
|
|
15
|
+
* about it — so the Studio cannot fix what it just wrote. A caller that wants to know passes `onError`; the
|
|
16
|
+
* off-screen draw the picture is taken from passes one, which is how a broken view reaches the next turn.
|
|
17
|
+
*/
|
|
18
|
+
export function makeWrittenHornView(onError) {
|
|
19
|
+
return (content, view, context) => (_jsx(Suspense, { fallback: _jsx(Loader, {}), children: _jsx(WrittenView, { source: content.source, rows: content.rows, columns: content.columns.map((column) => column.name), labels: context.labels, links: context.links, loadSurface: loadWrittenSurface, hooks: writtenHooks, fallback: _jsx(Loader, {}), renderError: (message) => {
|
|
20
|
+
onError?.({ viewId: view.id, message });
|
|
21
|
+
// Plain words: the person reading this asked for a view, not a stack trace.
|
|
22
|
+
return (_jsx("p", { role: "alert", className: "text-destructive-foreground m-0 text-sm", children: message }));
|
|
23
|
+
} }) }));
|
|
24
|
+
}
|
|
25
|
+
export const writtenHornView = makeWrittenHornView();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { AppRoutes, hornsRoutes } from "./app/routes.tsx";
|
|
2
|
+
export { AppShell } from "./components/app-shell.tsx";
|
|
3
|
+
export { MantaSparqlProvider, datastoreSparqlPath, queryClientFor } from "./lib/manta/providers.tsx";
|
|
4
|
+
export { MantaSessionProvider, useMantaSession } from "./lib/manta/session.tsx";
|
|
5
|
+
export { browserSparqlSelect } from "./lib/manta/sparql-select.ts";
|
|
6
|
+
export { ldkitShapeCatalog, provideShapeCatalog, shapesRealm, type ShapeCatalogSource } from "./lib/shapes/catalog.ts";
|
|
7
|
+
export { rasterizeHorn, sendHornScreenshot } from "./components/rasterize.tsx";
|
|
8
|
+
export { reviewHorn, type HornReview } from "./lib/horn-review.ts";
|
|
9
|
+
export { writtenHornView, makeWrittenHornView } from "./components/written-horn-view.tsx";
|
|
10
|
+
export { catalogCardView } from "./components/catalog-card-view.tsx";
|
|
11
|
+
export { hornPath, hornsPath } from "./app/paths.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The Horns app, as a library.
|
|
2
|
+
//
|
|
3
|
+
// A host mounts `<HornsRoutes />` inside its own router, or takes `mountHorns` for a page of its own. What
|
|
4
|
+
// the browser needs beyond this is the stylesheet: `@manta-eu/horns-client/styles.css`, built here rather
|
|
5
|
+
// than compiled by the host, so a host with no Tailwind still gets the app looking like Manta.
|
|
6
|
+
export { AppRoutes, hornsRoutes } from "./app/routes.js";
|
|
7
|
+
export { AppShell } from "./components/app-shell.js";
|
|
8
|
+
export { MantaSparqlProvider, datastoreSparqlPath, queryClientFor } from "./lib/manta/providers.js";
|
|
9
|
+
export { MantaSessionProvider, useMantaSession } from "./lib/manta/session.js";
|
|
10
|
+
export { browserSparqlSelect } from "./lib/manta/sparql-select.js";
|
|
11
|
+
export { ldkitShapeCatalog, provideShapeCatalog, shapesRealm } from "./lib/shapes/catalog.js";
|
|
12
|
+
export { rasterizeHorn, sendHornScreenshot } from "./components/rasterize.js";
|
|
13
|
+
export { reviewHorn } from "./lib/horn-review.js";
|
|
14
|
+
export { writtenHornView, makeWrittenHornView } from "./components/written-horn-view.js";
|
|
15
|
+
export { catalogCardView } from "./components/catalog-card-view.js";
|
|
16
|
+
export { hornPath, hornsPath } from "./app/paths.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Horn } from "@manta-eu/horns";
|
|
2
|
+
import { type RenderProblem } from "../components/rasterize";
|
|
3
|
+
/**
|
|
4
|
+
* What the draw found: what a second model said about it, any view that would not draw at all, and why
|
|
5
|
+
* there is no advice when there is none.
|
|
6
|
+
*
|
|
7
|
+
* `unavailable` matters: a reviewer that is simply not configured looks exactly like a reviewer that found
|
|
8
|
+
* nothing wrong, and those are opposite things to know.
|
|
9
|
+
*/
|
|
10
|
+
export type HornReview = {
|
|
11
|
+
advice: string[];
|
|
12
|
+
problems: RenderProblem[];
|
|
13
|
+
unavailable?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Draw the Horn that was just made, look at it, and say what is wrong.
|
|
17
|
+
*
|
|
18
|
+
* The browser is the only thing that can draw a Horn, so both answers come from one off-screen draw: a view
|
|
19
|
+
* that throws throws here, and the picture of the rest goes to a second model. Never throws — a Horn that
|
|
20
|
+
* changed has changed whether or not anyone managed to look at it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function reviewHorn(args: {
|
|
23
|
+
datastoreId: string;
|
|
24
|
+
horn: Horn;
|
|
25
|
+
asked: string;
|
|
26
|
+
variables?: Readonly<Record<string, unknown>> | undefined;
|
|
27
|
+
}): Promise<HornReview>;
|