@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,59 @@
|
|
|
1
|
+
import { hornSnapshotSchema } from "@manta-eu/horns/render";
|
|
2
|
+
import { rasterizeHorn } from "../components/rasterize";
|
|
3
|
+
import { isRecord } from "@manta-eu/horns";
|
|
4
|
+
/**
|
|
5
|
+
* Draw the Horn that was just made, look at it, and say what is wrong.
|
|
6
|
+
*
|
|
7
|
+
* The browser is the only thing that can draw a Horn, so both answers come from one off-screen draw: a view
|
|
8
|
+
* that throws throws here, and the picture of the rest goes to a second model. Never throws — a Horn that
|
|
9
|
+
* changed has changed whether or not anyone managed to look at it.
|
|
10
|
+
*/
|
|
11
|
+
export async function reviewHorn(args) {
|
|
12
|
+
let problems = [];
|
|
13
|
+
try {
|
|
14
|
+
const snapshot = await snapshotOf(args);
|
|
15
|
+
const drawn = await rasterizeHorn(snapshot);
|
|
16
|
+
problems = drawn.problems;
|
|
17
|
+
const image = await dataUrl(drawn.png);
|
|
18
|
+
const response = await fetch(`/api/d/${encodeURIComponent(args.datastoreId)}/horns/review`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify({ image, asked: args.asked }),
|
|
22
|
+
});
|
|
23
|
+
const body = await response.json().catch(() => undefined);
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const reason = isRecord(body) && typeof body["error"] === "string" ? body["error"] : undefined;
|
|
26
|
+
return { advice: [], problems, unavailable: reason ?? `The reviewer answered ${response.status}.` };
|
|
27
|
+
}
|
|
28
|
+
const said = isRecord(body) ? body["advice"] : undefined;
|
|
29
|
+
const advice = Array.isArray(said) ? said.filter((one) => typeof one === "string") : [];
|
|
30
|
+
return { advice, problems };
|
|
31
|
+
}
|
|
32
|
+
catch (caught) {
|
|
33
|
+
return {
|
|
34
|
+
advice: [],
|
|
35
|
+
problems,
|
|
36
|
+
unavailable: caught instanceof Error ? caught.message : "The Horn could not be drawn to look at.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// The draft as it stands, not the preview's: the preview is still refetching when a turn ends.
|
|
41
|
+
async function snapshotOf(args) {
|
|
42
|
+
const response = await fetch(`/api/d/${encodeURIComponent(args.datastoreId)}/horns/snapshot`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "content-type": "application/json" },
|
|
45
|
+
body: JSON.stringify({ horn: args.horn, ...(args.variables ? { variables: args.variables } : {}) }),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
throw new Error(`The draft could not be drawn (${response.status}).`);
|
|
49
|
+
const body = await response.json();
|
|
50
|
+
return hornSnapshotSchema.parse(isRecord(body) ? body["snapshot"] : undefined);
|
|
51
|
+
}
|
|
52
|
+
function dataUrl(png) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const reader = new FileReader();
|
|
55
|
+
reader.onerror = () => reject(new Error("The picture could not be read back."));
|
|
56
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : "");
|
|
57
|
+
reader.readAsDataURL(png);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { realmEnv, realmNamespace, realmNamespaces } from "@manta-eu/horns";
|
|
2
|
+
import { expect, vi } from "vitest";
|
|
3
|
+
// The SDK reads its realm once per module load, so each realm gets its own test file (and with it
|
|
4
|
+
// a fresh module graph) rather than sharing one.
|
|
5
|
+
export async function expectSdkRealm(realm) {
|
|
6
|
+
for (const [key, value] of Object.entries(realmEnv(realm)))
|
|
7
|
+
vi.stubEnv(key, value);
|
|
8
|
+
const { activeRealm, namespaces } = await import("@manta-eu/rdf-lib/namespaces");
|
|
9
|
+
const clientNamespaces = Object.fromEntries(Object.entries(namespaces).filter((entry) => typeof entry[1] === "string"));
|
|
10
|
+
expect(activeRealm()).toBe(realm);
|
|
11
|
+
expect(clientNamespaces[""]).toBe(realmNamespace(realm));
|
|
12
|
+
// Catalog CURIEs are expanded by @manta-eu/horns; they must land on the IRIs the SDK queries.
|
|
13
|
+
const shared = Object.entries(realmNamespaces(realm)).filter(([prefix]) => prefix in clientNamespaces);
|
|
14
|
+
expect(shared.length).toBeGreaterThan(10);
|
|
15
|
+
for (const [prefix, iri] of shared)
|
|
16
|
+
expect({ prefix, iri }).toEqual({ prefix, iri: clientNamespaces[prefix] });
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { activeRealm } from "@manta-eu/ldo/realm";
|
|
2
|
+
export { activeRealm };
|
|
3
|
+
export { namespaces, namespacesForRealm } from "@manta-eu/rdf-lib/namespaces";
|
|
4
|
+
export { createSparqlEngine, createSparqlOptions } from "@manta-eu/react/engine";
|
|
5
|
+
export { SparqlContext, optionsToContext, useSparqlEngine, useSparqlOptions, } from "@manta-eu/react/hooks/use-sparql-options";
|
|
6
|
+
export { bindingValue, collectBindings } from "@manta-eu/react/ldo-ui";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
2
|
+
import { activeRealm } from "@manta-eu/ldo/realm";
|
|
3
|
+
export { activeRealm };
|
|
4
|
+
export { namespaces, namespacesForRealm } from "@manta-eu/rdf-lib/namespaces";
|
|
5
|
+
export { createSparqlEngine, createSparqlOptions } from "@manta-eu/react/engine";
|
|
6
|
+
export { SparqlContext, optionsToContext, useSparqlEngine, useSparqlOptions, } from "@manta-eu/react/hooks/use-sparql-options";
|
|
7
|
+
export { bindingValue, collectBindings } from "@manta-eu/react/ldo-ui";
|
|
8
|
+
// The SDK composes every vocabulary IRI from the realm it resolved at module load. If the realm
|
|
9
|
+
// shim in index.html did not run first, reads would silently return nothing, so fail loudly.
|
|
10
|
+
const buildRealm = import.meta.env.VITE_KEYCLOAK_REALM;
|
|
11
|
+
if (isPresent(buildRealm) && activeRealm() !== buildRealm) {
|
|
12
|
+
throw new Error(`The Manta SDK resolved the "${activeRealm()}" realm, but this build is for "${buildRealm}". ` +
|
|
13
|
+
"The realm shim must run before any module script.");
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
3
|
+
export declare function datastoreSparqlPath(datastoreId: string): string;
|
|
4
|
+
export declare function queryClientFor(datastoreId: string): QueryClient;
|
|
5
|
+
type MantaSparqlProviderProps = {
|
|
6
|
+
datastoreId: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
export declare function MantaSparqlProvider({ datastoreId, children }: MantaSparqlProviderProps): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import { isRecord } from "@manta-eu/horns";
|
|
5
|
+
import { SparqlContext, createSparqlEngine, createSparqlOptions } from "./ldo";
|
|
6
|
+
export function datastoreSparqlPath(datastoreId) {
|
|
7
|
+
return `/d/${encodeURIComponent(datastoreId)}/sparql`;
|
|
8
|
+
}
|
|
9
|
+
function fetchWithSparqlErrorBody(baseFetch) {
|
|
10
|
+
return async (input, init) => {
|
|
11
|
+
const response = await baseFetch(input, init);
|
|
12
|
+
if (response.ok)
|
|
13
|
+
return response;
|
|
14
|
+
const text = await response.text();
|
|
15
|
+
let detail = text;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(text);
|
|
18
|
+
if (isRecord(parsed) && typeof parsed.error === "string" && parsed.error.trim())
|
|
19
|
+
detail = parsed.error;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Non-JSON upstream body — keep the raw text.
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`${response.status} ${response.statusText} — ${detail}`);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// One cache per datastore: a cache shared across datastores would answer one tenant's rows under
|
|
28
|
+
// another tenant's name the moment someone opens a different datastore.
|
|
29
|
+
const clientsByDatastore = new Map();
|
|
30
|
+
export function queryClientFor(datastoreId) {
|
|
31
|
+
const existing = clientsByDatastore.get(datastoreId);
|
|
32
|
+
if (existing)
|
|
33
|
+
return existing;
|
|
34
|
+
const created = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1 } } });
|
|
35
|
+
clientsByDatastore.set(datastoreId, created);
|
|
36
|
+
return created;
|
|
37
|
+
}
|
|
38
|
+
export function MantaSparqlProvider({ datastoreId, children }) {
|
|
39
|
+
const client = queryClientFor(datastoreId);
|
|
40
|
+
const value = useMemo(() => {
|
|
41
|
+
const options = createSparqlOptions({ sparqlEndpoint: datastoreSparqlPath(datastoreId) });
|
|
42
|
+
return {
|
|
43
|
+
options: { ...options, fetch: fetchWithSparqlErrorBody(options.fetch ?? fetch) },
|
|
44
|
+
engine: createSparqlEngine(),
|
|
45
|
+
};
|
|
46
|
+
}, [datastoreId]);
|
|
47
|
+
return (_jsx(QueryClientProvider, { client: client, children: _jsx(SparqlContext.Provider, { value: value, children: children }) }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type MantaDatastore = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
depth?: number;
|
|
6
|
+
selectable?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type MantaSessionUser = {
|
|
9
|
+
id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
isOperator: boolean;
|
|
13
|
+
datastore?: MantaDatastore;
|
|
14
|
+
};
|
|
15
|
+
export type MantaSessionStatus = "loading" | "signed-out" | "ready" | "error";
|
|
16
|
+
export type MantaSessionValue = {
|
|
17
|
+
status: MantaSessionStatus;
|
|
18
|
+
isLive: boolean;
|
|
19
|
+
user?: MantaSessionUser;
|
|
20
|
+
datastore?: MantaDatastore;
|
|
21
|
+
datastores: MantaDatastore[];
|
|
22
|
+
isOperator: boolean;
|
|
23
|
+
canSwitchDatastore: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
authError?: string;
|
|
26
|
+
justSignedIn: boolean;
|
|
27
|
+
revision: number;
|
|
28
|
+
reload: () => void;
|
|
29
|
+
};
|
|
30
|
+
export declare function signInHref(redirect?: string): string;
|
|
31
|
+
export declare function readJsonError(response: Response, fallback: string): Promise<string>;
|
|
32
|
+
export declare class RequestFailed extends Error {
|
|
33
|
+
readonly status: number;
|
|
34
|
+
constructor(status: number, message: string);
|
|
35
|
+
}
|
|
36
|
+
export declare function statusOf(caught: unknown): number | undefined;
|
|
37
|
+
export declare function requestJson<T>(path: string, init: RequestInit, fallback: string, parse: (body: unknown) => T): Promise<T | undefined>;
|
|
38
|
+
export type SessionGate = "loading" | "sign-in" | "looped" | "blocked" | "ready";
|
|
39
|
+
export declare function sessionGate(value: Pick<MantaSessionValue, "status" | "authError" | "justSignedIn">): SessionGate;
|
|
40
|
+
export declare function realmDatastore(value: Pick<MantaSessionValue, "datastores">): MantaDatastore | undefined;
|
|
41
|
+
export declare function activeDatastore(value: Pick<MantaSessionValue, "isOperator" | "user" | "datastores">): MantaDatastore | undefined;
|
|
42
|
+
type MantaSessionProviderProps = {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
};
|
|
45
|
+
export declare function MantaSessionProvider({ children }: MantaSessionProviderProps): import("react").JSX.Element;
|
|
46
|
+
export declare function useMantaSession(): MantaSessionValue;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, use, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
4
|
+
import { isRecord, isString } from "@manta-eu/horns";
|
|
5
|
+
import { signedInParam } from "@manta-eu/horns-server/shared";
|
|
6
|
+
export function signInHref(redirect) {
|
|
7
|
+
const params = new URLSearchParams();
|
|
8
|
+
if (isPresent(redirect) && redirect !== "/")
|
|
9
|
+
params.set("redirect", redirect);
|
|
10
|
+
const query = params.toString();
|
|
11
|
+
return `/login${query ? `?${query}` : ""}`;
|
|
12
|
+
}
|
|
13
|
+
export async function readJsonError(response, fallback) {
|
|
14
|
+
const body = await response.json().catch(() => undefined);
|
|
15
|
+
if (typeof body === "object" && isPresent(body) && "error" in body && typeof body.error === "string") {
|
|
16
|
+
return body.error;
|
|
17
|
+
}
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
export class RequestFailed extends Error {
|
|
21
|
+
status;
|
|
22
|
+
constructor(status, message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "RequestFailed";
|
|
25
|
+
this.status = status;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function statusOf(caught) {
|
|
29
|
+
return caught instanceof RequestFailed ? caught.status : undefined;
|
|
30
|
+
}
|
|
31
|
+
// 204 responses are success, not a parse failure. The body is untrusted until `parse` has checked it.
|
|
32
|
+
export async function requestJson(path, init, fallback, parse) {
|
|
33
|
+
const response = await fetch(path, { credentials: "same-origin", ...init });
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
throw new RequestFailed(response.status, await readJsonError(response, fallback));
|
|
36
|
+
const text = await response.text();
|
|
37
|
+
if (!isPresent(text.trim()))
|
|
38
|
+
return undefined;
|
|
39
|
+
const body = JSON.parse(text);
|
|
40
|
+
return parse(body);
|
|
41
|
+
}
|
|
42
|
+
const readJson = (path, fallback, parse) => requestJson(path, { headers: { accept: "application/json" } }, fallback, parse);
|
|
43
|
+
function parseDatastore(value) {
|
|
44
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.name !== "string")
|
|
45
|
+
return undefined;
|
|
46
|
+
return {
|
|
47
|
+
id: value.id,
|
|
48
|
+
name: value.name,
|
|
49
|
+
...(typeof value.depth === "number" ? { depth: value.depth } : {}),
|
|
50
|
+
...(typeof value.selectable === "boolean" ? { selectable: value.selectable } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function parseLoginStatus(body) {
|
|
54
|
+
if (!isRecord(body) || typeof body.valid !== "boolean")
|
|
55
|
+
throw new Error("Sign-in status is malformed.");
|
|
56
|
+
return {
|
|
57
|
+
valid: body.valid,
|
|
58
|
+
canSwitchDatastore: body.canSwitchDatastore === true,
|
|
59
|
+
allowedModels: Array.isArray(body.allowedModels) ? body.allowedModels.filter(isString) : [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function parseUserContext(body) {
|
|
63
|
+
if (!isRecord(body) || !isRecord(body.user))
|
|
64
|
+
throw new Error("User context is malformed.");
|
|
65
|
+
const { user } = body;
|
|
66
|
+
if (typeof user.id !== "string" || typeof user.email !== "string")
|
|
67
|
+
throw new Error("User context is malformed.");
|
|
68
|
+
const datastore = parseDatastore(user.datastore);
|
|
69
|
+
return {
|
|
70
|
+
user: {
|
|
71
|
+
id: user.id,
|
|
72
|
+
email: user.email,
|
|
73
|
+
isOperator: user.isOperator === true,
|
|
74
|
+
...(typeof user.name === "string" ? { name: user.name } : {}),
|
|
75
|
+
...(datastore ? { datastore } : {}),
|
|
76
|
+
},
|
|
77
|
+
datastores: Array.isArray(body.datastores) ? body.datastores.flatMap((entry) => parseDatastore(entry) ?? []) : [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function sessionGate(value) {
|
|
81
|
+
if (isPresent(value.authError))
|
|
82
|
+
return "blocked";
|
|
83
|
+
if (value.status === "loading")
|
|
84
|
+
return "loading";
|
|
85
|
+
if (value.status === "error")
|
|
86
|
+
return "blocked";
|
|
87
|
+
if (value.status === "signed-out")
|
|
88
|
+
return value.justSignedIn ? "looped" : "sign-in";
|
|
89
|
+
return "ready";
|
|
90
|
+
}
|
|
91
|
+
// The realm's own top-level datastore is the one with no depth that cannot be picked.
|
|
92
|
+
export function realmDatastore(value) {
|
|
93
|
+
return value.datastores.find((datastore) => !isPresent(datastore.depth) && datastore.selectable !== true);
|
|
94
|
+
}
|
|
95
|
+
export function activeDatastore(value) {
|
|
96
|
+
if (isPresent(value.user?.datastore))
|
|
97
|
+
return value.user.datastore;
|
|
98
|
+
if (!value.isOperator)
|
|
99
|
+
return undefined;
|
|
100
|
+
return realmDatastore(value);
|
|
101
|
+
}
|
|
102
|
+
const MantaSessionContext = createContext(undefined);
|
|
103
|
+
function readAuthError() {
|
|
104
|
+
const raw = new URLSearchParams(window.location.search).get("auth_error");
|
|
105
|
+
return isPresent(raw) ? raw : undefined;
|
|
106
|
+
}
|
|
107
|
+
function readJustSignedIn() {
|
|
108
|
+
return new URLSearchParams(window.location.search).get(signedInParam) === "1";
|
|
109
|
+
}
|
|
110
|
+
export function MantaSessionProvider({ children }) {
|
|
111
|
+
const [status, setStatus] = useState("loading");
|
|
112
|
+
const [context, setContext] = useState();
|
|
113
|
+
const [canSwitchDatastore, setCanSwitchDatastore] = useState(false);
|
|
114
|
+
const [error, setError] = useState();
|
|
115
|
+
const [reloads, setReloads] = useState(0);
|
|
116
|
+
const [revision, setRevision] = useState(0);
|
|
117
|
+
const [authError, setAuthError] = useState(readAuthError);
|
|
118
|
+
const [justSignedIn] = useState(readJustSignedIn);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!isPresent(authError) && !justSignedIn)
|
|
121
|
+
return;
|
|
122
|
+
const url = new URL(window.location.href);
|
|
123
|
+
url.searchParams.delete("auth_error");
|
|
124
|
+
url.searchParams.delete(signedInParam);
|
|
125
|
+
window.history.replaceState(window.history.state, "", url.pathname + url.search);
|
|
126
|
+
}, [authError, justSignedIn]);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
let live = true;
|
|
129
|
+
void (async () => {
|
|
130
|
+
if (reloads === 0)
|
|
131
|
+
setStatus("loading");
|
|
132
|
+
setError(undefined);
|
|
133
|
+
try {
|
|
134
|
+
const login = await readJson("/api/login", "Sign-in status is unavailable.", parseLoginStatus);
|
|
135
|
+
if (!live)
|
|
136
|
+
return;
|
|
137
|
+
if (!isPresent(login))
|
|
138
|
+
throw new Error("Sign-in status is unavailable.");
|
|
139
|
+
setCanSwitchDatastore(login.canSwitchDatastore);
|
|
140
|
+
if (!login.valid) {
|
|
141
|
+
setContext(undefined);
|
|
142
|
+
setStatus("signed-out");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const loaded = await readJson("/api/data/context", "User context is unavailable.", parseUserContext);
|
|
146
|
+
if (!live)
|
|
147
|
+
return;
|
|
148
|
+
if (!isPresent(loaded))
|
|
149
|
+
throw new Error("User context is unavailable.");
|
|
150
|
+
setContext(loaded);
|
|
151
|
+
setStatus("ready");
|
|
152
|
+
setRevision((count) => count + 1);
|
|
153
|
+
}
|
|
154
|
+
catch (caught) {
|
|
155
|
+
if (!live)
|
|
156
|
+
return;
|
|
157
|
+
// The upstream session can expire between the /api/login check and this fetch.
|
|
158
|
+
if (statusOf(caught) === 401) {
|
|
159
|
+
setContext(undefined);
|
|
160
|
+
setStatus("signed-out");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
setError(caught instanceof Error ? caught.message : "Manta Horns is unavailable.");
|
|
164
|
+
setStatus("error");
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
167
|
+
return () => {
|
|
168
|
+
live = false;
|
|
169
|
+
};
|
|
170
|
+
}, [reloads]);
|
|
171
|
+
const reload = useCallback(() => {
|
|
172
|
+
setAuthError(undefined);
|
|
173
|
+
setReloads((count) => count + 1);
|
|
174
|
+
}, []);
|
|
175
|
+
const value = useMemo(() => {
|
|
176
|
+
const isOperator = context?.user.isOperator ?? false;
|
|
177
|
+
const datastore = activeDatastore({
|
|
178
|
+
isOperator,
|
|
179
|
+
...(isPresent(context) ? { user: context.user } : {}),
|
|
180
|
+
datastores: context?.datastores ?? [],
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
status,
|
|
184
|
+
isLive: status === "ready",
|
|
185
|
+
...(isPresent(context) ? { user: context.user } : {}),
|
|
186
|
+
...(isPresent(datastore) ? { datastore } : {}),
|
|
187
|
+
datastores: context?.datastores ?? [],
|
|
188
|
+
isOperator,
|
|
189
|
+
canSwitchDatastore,
|
|
190
|
+
...(isPresent(error) ? { error } : {}),
|
|
191
|
+
...(isPresent(authError) ? { authError } : {}),
|
|
192
|
+
justSignedIn,
|
|
193
|
+
revision,
|
|
194
|
+
reload,
|
|
195
|
+
};
|
|
196
|
+
}, [status, context, canSwitchDatastore, error, authError, justSignedIn, revision, reload]);
|
|
197
|
+
return _jsx(MantaSessionContext.Provider, { value: value, children: children });
|
|
198
|
+
}
|
|
199
|
+
export function useMantaSession() {
|
|
200
|
+
const value = use(MantaSessionContext);
|
|
201
|
+
if (!isPresent(value))
|
|
202
|
+
throw new Error("useMantaSession must be used inside MantaSessionProvider");
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { sparqlResultsSchema } from "@manta-eu/horns";
|
|
2
|
+
import { datastoreSparqlPath } from "./providers";
|
|
3
|
+
import { readJsonError } from "./session";
|
|
4
|
+
// SELECTs from the browser through the worker's read-only, grant-checked `/d/:ds/sparql` proxy.
|
|
5
|
+
export function browserSparqlSelect(datastoreId) {
|
|
6
|
+
return async (query, signal) => {
|
|
7
|
+
const response = await fetch(datastoreSparqlPath(datastoreId), {
|
|
8
|
+
method: "POST",
|
|
9
|
+
credentials: "same-origin",
|
|
10
|
+
headers: { "content-type": "application/sparql-query", accept: "application/sparql-results+json" },
|
|
11
|
+
body: query,
|
|
12
|
+
...(signal ? { signal } : {}),
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok)
|
|
15
|
+
throw new Error(await readJsonError(response, `The datastore answered ${response.status}.`));
|
|
16
|
+
const body = await response.json();
|
|
17
|
+
return sparqlResultsSchema.parse(body);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PlanStatus } from "@manta-eu/horns/render";
|
|
2
|
+
export declare const planPollInterval = 2000;
|
|
3
|
+
export declare const planPollAttempts = 120;
|
|
4
|
+
/**
|
|
5
|
+
* Whether the graph has caught up with a run just started: a run newer than the one before it, and finished. Right
|
|
6
|
+
* after starting, the graph still says how the previous run ended, which is not an answer.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runSettled(status: PlanStatus, startedBefore: string | undefined): boolean;
|
|
9
|
+
export declare function runSeen(status: PlanStatus, startedBefore: string | undefined): boolean;
|
|
10
|
+
export declare function startPlanRun(args: {
|
|
11
|
+
datastoreId: string;
|
|
12
|
+
plan: string;
|
|
13
|
+
preview: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { problemFrom } from "./problem";
|
|
2
|
+
export const planPollInterval = 2000;
|
|
3
|
+
export const planPollAttempts = 120;
|
|
4
|
+
/**
|
|
5
|
+
* Whether the graph has caught up with a run just started: a run newer than the one before it, and finished. Right
|
|
6
|
+
* after starting, the graph still says how the previous run ended, which is not an answer.
|
|
7
|
+
*/
|
|
8
|
+
export function runSettled(status, startedBefore) {
|
|
9
|
+
const started = status.lastRun?.startedAt;
|
|
10
|
+
return started !== undefined && started !== startedBefore && status.state !== "running";
|
|
11
|
+
}
|
|
12
|
+
export function runSeen(status, startedBefore) {
|
|
13
|
+
const started = status.lastRun?.startedAt;
|
|
14
|
+
return started !== undefined && started !== startedBefore;
|
|
15
|
+
}
|
|
16
|
+
export async function startPlanRun(args) {
|
|
17
|
+
const response = await fetch(`/api/d/${encodeURIComponent(args.datastoreId)}/action-plans/run`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
credentials: "same-origin",
|
|
20
|
+
headers: { "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify({ plan: args.plan, preview: args.preview }),
|
|
22
|
+
});
|
|
23
|
+
if (response.ok)
|
|
24
|
+
return;
|
|
25
|
+
throw new Error(await problemFrom(response, `The plan could not be started (${response.status}).`));
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sentence to show when a request did not work out. Every route answers `{ error }` when it has something
|
|
3
|
+
* to say; anything else — a proxy's HTML, an empty body, a worker that crashed — has only its status, so the
|
|
4
|
+
* caller supplies the words for that case.
|
|
5
|
+
*/
|
|
6
|
+
export declare function problemOf(body: unknown, fallback: string): string;
|
|
7
|
+
/** The same, for a response nobody has read yet: a body that is not JSON is no worse than one without `error`. */
|
|
8
|
+
export declare function problemFrom(response: Response, fallback: string): Promise<string>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
2
|
+
import { isRecord } from "@manta-eu/horns";
|
|
3
|
+
/**
|
|
4
|
+
* The sentence to show when a request did not work out. Every route answers `{ error }` when it has something
|
|
5
|
+
* to say; anything else — a proxy's HTML, an empty body, a worker that crashed — has only its status, so the
|
|
6
|
+
* caller supplies the words for that case.
|
|
7
|
+
*/
|
|
8
|
+
export function problemOf(body, fallback) {
|
|
9
|
+
const said = isRecord(body) ? body["error"] : undefined;
|
|
10
|
+
return typeof said === "string" && isPresent(said) ? said : fallback;
|
|
11
|
+
}
|
|
12
|
+
/** The same, for a response nobody has read yet: a body that is not JSON is no worse than one without `error`. */
|
|
13
|
+
export async function problemFrom(response, fallback) {
|
|
14
|
+
const body = await response.json().catch(() => undefined);
|
|
15
|
+
return problemOf(body, fallback);
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Realm } from "@manta-eu/horns";
|
|
2
|
+
import type { ShapeCatalog } from "@manta-eu/horns/shapes";
|
|
3
|
+
export declare function shapesRealm(): Realm;
|
|
4
|
+
export type ShapeCatalogSource = (realm: Realm) => ShapeCatalog;
|
|
5
|
+
/**
|
|
6
|
+
* Hand Horns the realm's SHACL-derived shapes. A host builds them from its own `@manta-eu/ldo` schemas with
|
|
7
|
+
* `shapeCatalogFromLdkit`, at build time, and passes the result here before mounting.
|
|
8
|
+
*
|
|
9
|
+
* Horns ships no catalog of its own: the shapes describe a realm's data model, so they belong to whoever
|
|
10
|
+
* deploys rather than to the library.
|
|
11
|
+
*/
|
|
12
|
+
export declare function provideShapeCatalog(next: ShapeCatalogSource): void;
|
|
13
|
+
/** The realm's shapes, as the host supplied them. */
|
|
14
|
+
export declare function ldkitShapeCatalog(): ShapeCatalog;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { activeRealm } from "@manta-eu/ldo/realm";
|
|
2
|
+
import { parseRealm } from "@manta-eu/horns";
|
|
3
|
+
export function shapesRealm() {
|
|
4
|
+
return parseRealm(activeRealm());
|
|
5
|
+
}
|
|
6
|
+
let source;
|
|
7
|
+
/**
|
|
8
|
+
* Hand Horns the realm's SHACL-derived shapes. A host builds them from its own `@manta-eu/ldo` schemas with
|
|
9
|
+
* `shapeCatalogFromLdkit`, at build time, and passes the result here before mounting.
|
|
10
|
+
*
|
|
11
|
+
* Horns ships no catalog of its own: the shapes describe a realm's data model, so they belong to whoever
|
|
12
|
+
* deploys rather than to the library.
|
|
13
|
+
*/
|
|
14
|
+
export function provideShapeCatalog(next) {
|
|
15
|
+
source = next;
|
|
16
|
+
}
|
|
17
|
+
/** The realm's shapes, as the host supplied them. */
|
|
18
|
+
export function ldkitShapeCatalog() {
|
|
19
|
+
if (!source) {
|
|
20
|
+
throw new Error("No shape catalog: call provideShapeCatalog() before mounting. Build one for your realm with " +
|
|
21
|
+
"shapeCatalogFromLdkit() from @manta-eu/horns/shapes. Answering with no shapes would look like an " +
|
|
22
|
+
"empty datastore rather than a missing build step.");
|
|
23
|
+
}
|
|
24
|
+
return source(shapesRealm());
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Realm } from "@manta-eu/horns";
|
|
2
|
+
export type LdkitSource = {
|
|
3
|
+
/** An LDkit schema module: every `…Schema` export, as `shapeCatalogFromLdkit` reads them. */
|
|
4
|
+
module: object;
|
|
5
|
+
/** Whether this realm's own shapes were read, or the canonical set stood in for them. */
|
|
6
|
+
origin: "generated" | "canonical";
|
|
7
|
+
};
|
|
8
|
+
/** Where `just generate` writes. A parameter only so a test can point at a fixture tree. */
|
|
9
|
+
export declare const generatedRoot: URL;
|
|
10
|
+
/** Whether `just generate` has been run for this realm in this checkout. */
|
|
11
|
+
export declare function hasGeneratedSchemas(realm: Realm, root?: URL): boolean;
|
|
12
|
+
export declare const generatedNotice = "built from this realm's own datastore shapes, which are a subset of the canonical set";
|
|
13
|
+
/** Node only: it reads the filesystem. The worker reads the snapshot `shapes:snapshot` wrote. */
|
|
14
|
+
export declare function ldkitSchemasFor(realm: Realm, root?: URL, preferGenerated?: boolean): Promise<LdkitSource>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
/** Where `just generate` writes. A parameter only so a test can point at a fixture tree. */
|
|
11
|
+
export const generatedRoot = new URL("../../generated/ldkit/", import.meta.url);
|
|
12
|
+
function generatedDirFor(realm, root) {
|
|
13
|
+
return new URL(`${realm}/`, root);
|
|
14
|
+
}
|
|
15
|
+
/** Whether `just generate` has been run for this realm in this checkout. */
|
|
16
|
+
export function hasGeneratedSchemas(realm, root = generatedRoot) {
|
|
17
|
+
return existsSync(new URL("index.ts", generatedDirFor(realm, root)));
|
|
18
|
+
}
|
|
19
|
+
export const generatedNotice = "built from this realm's own datastore shapes, which are a subset of the canonical set";
|
|
20
|
+
/** Node only: it reads the filesystem. The worker reads the snapshot `shapes:snapshot` wrote. */
|
|
21
|
+
export async function ldkitSchemasFor(realm, root = generatedRoot, preferGenerated = false) {
|
|
22
|
+
if (preferGenerated && hasGeneratedSchemas(realm, root)) {
|
|
23
|
+
return { module: await import(__rewriteRelativeImportExtension(new URL("index.ts", generatedDirFor(realm, root)).href)), origin: "generated" };
|
|
24
|
+
}
|
|
25
|
+
return { module: await import("@manta-eu/ldo/schemas"), origin: "canonical" };
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** One thing a turn did, as a watcher reads it: what it was, and what it was on. */
|
|
2
|
+
export type StudioStep = {
|
|
3
|
+
title: string;
|
|
4
|
+
detail?: string;
|
|
5
|
+
};
|
|
6
|
+
/** One line for one streamed event, or nothing for an event not worth a line of its own. */
|
|
7
|
+
export declare function studioProgressLine(line: Record<string, unknown>): StudioStep | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { match } from "ts-pattern";
|
|
2
|
+
import { isRecord } from "@manta-eu/horns";
|
|
3
|
+
// What a Horn turn is doing, in words, from the events the route streams. A tool name is the model's
|
|
4
|
+
// vocabulary; the person watching wants to know what is being looked at.
|
|
5
|
+
const toolLines = {
|
|
6
|
+
list_classes: "Looking at what this datastore holds",
|
|
7
|
+
describe_class: "Reading how a kind of thing is described",
|
|
8
|
+
components_for_class: "Looking for components that already fit",
|
|
9
|
+
run_sparql: "Querying the datastore",
|
|
10
|
+
investigate_data: "Investigating the data",
|
|
11
|
+
propose_horn_patch: "Changing the Horn",
|
|
12
|
+
propose_library_item: "Offering a part for the library",
|
|
13
|
+
report_harness_issue: "Noting a problem with the tools",
|
|
14
|
+
};
|
|
15
|
+
// A turn can start a loop of its own. Its steps are the same steps, said by somebody else.
|
|
16
|
+
const agentNames = { investigate: "Investigating" };
|
|
17
|
+
/** One line for one streamed event, or nothing for an event not worth a line of its own. */
|
|
18
|
+
export function studioProgressLine(line) {
|
|
19
|
+
const event = line["event"];
|
|
20
|
+
if (!isRecord(event))
|
|
21
|
+
return undefined;
|
|
22
|
+
const name = typeof event["name"] === "string" ? event["name"] : "";
|
|
23
|
+
const detail = typeof event["detail"] === "string" ? event["detail"] : undefined;
|
|
24
|
+
const agent = typeof event["agent"] === "string" ? (agentNames[event["agent"]] ?? event["agent"]) : undefined;
|
|
25
|
+
const step = (title) => title === undefined
|
|
26
|
+
? undefined
|
|
27
|
+
: { title: agent === undefined ? title : `${agent} · ${title}`, ...(detail === undefined ? {} : { detail }) };
|
|
28
|
+
return match(event["type"])
|
|
29
|
+
.with("reading", () => step("Reading the Horn and the data behind it"))
|
|
30
|
+
.with("round", () => (event["step"] === 1 && agent === undefined ? step("Thinking") : undefined))
|
|
31
|
+
.with("tool-call", () => event["repeated"] === true || name === "propose_horn_patch" ? undefined : step(toolLines[name] ?? `Using ${name}`))
|
|
32
|
+
.with("proposal-accepted", () => (name === "propose_horn_patch" ? step("Changed the Horn") : undefined))
|
|
33
|
+
.with("proposal-rejected", () => step("That change did not hold up against the data, trying again"))
|
|
34
|
+
.with("rehydrated", () => step("Starting over from the Horn as it stands"))
|
|
35
|
+
.otherwise(() => undefined);
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initSentry(): void;
|